support export table
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { save } from "@tauri-apps/plugin-dialog";
|
import { X, Calendar, ChevronLeft, ChevronRight, Copy } from "lucide-vue-next";
|
||||||
import { X, Calendar, ChevronLeft, ChevronRight } from "lucide-vue-next";
|
import { currentDate, showToast, getTagName, logicalMinutesToTime, toISODate, mainTags } from "../../store";
|
||||||
import { currentDate, showToast, getTagName, logicalMinutesToTime, toISODate } from "../../store";
|
|
||||||
import { DBEvent } from "../../types";
|
import { DBEvent } from "../../types";
|
||||||
|
|
||||||
defineEmits(['close']);
|
defineEmits(['close']);
|
||||||
@@ -18,6 +17,21 @@ const exportEndMonth = ref(new Date(exportEndDate.value));
|
|||||||
const isExportStartCalendarOpen = ref(false);
|
const isExportStartCalendarOpen = ref(false);
|
||||||
const isExportEndCalendarOpen = ref(false);
|
const isExportEndCalendarOpen = ref(false);
|
||||||
|
|
||||||
|
// 选择的主标签,默认全选
|
||||||
|
const selectedTags = ref<number[]>(mainTags.value.map(t => t.id));
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const dateList = ref<string[]>([]);
|
||||||
|
const previewData = ref<Record<string, Record<number, string>>>({});
|
||||||
|
|
||||||
|
const toggleTag = (id: number) => {
|
||||||
|
if (selectedTags.value.includes(id)) {
|
||||||
|
selectedTags.value = selectedTags.value.filter(tId => tId !== id);
|
||||||
|
} else {
|
||||||
|
selectedTags.value.push(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (isExportStartCalendarOpen.value && startCalendarRef.value && !startCalendarRef.value.contains(target)) {
|
if (isExportStartCalendarOpen.value && startCalendarRef.value && !startCalendarRef.value.contains(target)) {
|
||||||
@@ -60,43 +74,110 @@ const exportEndCalendarDays = computed(() => {
|
|||||||
return days;
|
return days;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleExport = async () => {
|
const parseDateStr = (str: string) => {
|
||||||
try {
|
const [y, m, d] = str.split('-').map(Number);
|
||||||
const events: DBEvent[] = await invoke("get_events_range", { startDate: exportStartDate.value, endDate: exportEndDate.value });
|
return new Date(y, m - 1, d);
|
||||||
if (events.length === 0) {
|
};
|
||||||
showToast("所选范围内没有找到记录", "error");
|
|
||||||
|
const handlePreview = async () => {
|
||||||
|
if (selectedTags.value.length === 0) {
|
||||||
|
showToast("请至少选择一个主标签", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const savePath = await save({
|
|
||||||
filters: [{ name: "JSON 文件", extensions: ["json"] }],
|
const curDate = parseDateStr(exportStartDate.value);
|
||||||
defaultPath: `ChronoSnap_Export_${exportStartDate.value}_${exportEndDate.value}.json`
|
const endDate = parseDateStr(exportEndDate.value);
|
||||||
});
|
|
||||||
if (savePath) {
|
if (curDate > endDate) {
|
||||||
const exportData = events.map(e => ({
|
showToast("开始日期不能晚于结束日期", "error");
|
||||||
date: e.date,
|
return;
|
||||||
start_time: logicalMinutesToTime(e.start_minute),
|
|
||||||
end_time: logicalMinutesToTime(e.end_minute),
|
|
||||||
main_tag: getTagName(e.main_tag_id),
|
|
||||||
sub_tag: getTagName(e.sub_tag_id),
|
|
||||||
content: e.content
|
|
||||||
}));
|
|
||||||
await invoke("write_file", { path: savePath, content: JSON.stringify(exportData, null, 2) });
|
|
||||||
showToast("导出成功");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const events: DBEvent[] = await invoke("get_events_range", { startDate: exportStartDate.value, endDate: exportEndDate.value });
|
||||||
|
|
||||||
|
// 生成日期范围列表
|
||||||
|
const dates: string[] = [];
|
||||||
|
let tempDate = new Date(curDate);
|
||||||
|
while (tempDate <= endDate) {
|
||||||
|
dates.push(toISODate(tempDate));
|
||||||
|
tempDate.setDate(tempDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
dateList.value = dates;
|
||||||
|
|
||||||
|
// 过滤选中的主标签并构建矩阵
|
||||||
|
const matrix: Record<string, Record<number, string>> = {};
|
||||||
|
dates.forEach(d => matrix[d] = {});
|
||||||
|
|
||||||
|
const filteredEvents = events.filter(e => selectedTags.value.includes(e.main_tag_id));
|
||||||
|
|
||||||
|
filteredEvents.forEach(e => {
|
||||||
|
if (!matrix[e.date]) matrix[e.date] = {}; // 容错
|
||||||
|
|
||||||
|
const cellArr = matrix[e.date][e.main_tag_id] ? matrix[e.date][e.main_tag_id].split('\n') : [];
|
||||||
|
const startTime = logicalMinutesToTime(e.start_minute);
|
||||||
|
const endTime = logicalMinutesToTime(e.end_minute);
|
||||||
|
const subTag = getTagName(e.sub_tag_id);
|
||||||
|
// 替换换行符为空格
|
||||||
|
const content = e.content ? e.content.replace(/\n/g, ' ') : '';
|
||||||
|
|
||||||
|
let line = `${startTime}-${endTime}`;
|
||||||
|
if (subTag) line += ` ${subTag}`;
|
||||||
|
if (content) line += ` ${content}`;
|
||||||
|
|
||||||
|
cellArr.push(line);
|
||||||
|
matrix[e.date][e.main_tag_id] = cellArr.join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
previewData.value = matrix;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast("导出失败: " + e, "error");
|
showToast("获取数据失败: " + e, "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
// 生成适合 Excel 的 TSV (Tab-Separated Values) 格式
|
||||||
|
const header = ["日期", ...selectedTags.value.map(id => getTagName(id))];
|
||||||
|
let tsv = header.join("\t") + "\n";
|
||||||
|
|
||||||
|
for (const date of dateList.value) {
|
||||||
|
let row = [date];
|
||||||
|
for (const tagId of selectedTags.value) {
|
||||||
|
let cellStr = previewData.value[date][tagId] || "";
|
||||||
|
// Excel/Sheets 处理带换行符的单元格,需要用双引号包围,且内容中的双引号要转义为两个双引号
|
||||||
|
if (cellStr.includes('\n') || cellStr.includes('\t') || cellStr.includes('"')) {
|
||||||
|
cellStr = `"${cellStr.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
row.push(cellStr);
|
||||||
|
}
|
||||||
|
tsv += row.join("\t") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(tsv);
|
||||||
|
showToast("表格已复制,可直接粘贴到 Excel");
|
||||||
|
} catch(err) {
|
||||||
|
showToast("复制失败", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="$emit('close')">
|
<div class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="$emit('close')">
|
||||||
<div class="bg-bg-card rounded-[40px] shadow-2xl w-full max-w-2xl overflow-visible flex flex-col animate-in fade-in zoom-in duration-200">
|
<div class="bg-bg-card rounded-[40px] shadow-2xl w-full flex flex-col animate-in fade-in zoom-in duration-300 transition-all"
|
||||||
<div class="p-8 border-b flex justify-between items-center"><h2 class="text-2xl font-bold">导出记录</h2><button @click="$emit('close')"><X :size="24" /></button></div>
|
:class="dateList.length > 0 ? 'max-w-[90vw] md:max-w-6xl h-[90vh] overflow-hidden' : 'max-w-4xl min-h-[500px]'">
|
||||||
<div class="p-10 space-y-8">
|
<div class="p-8 border-b flex justify-between items-center flex-shrink-0">
|
||||||
<div class="space-y-4">
|
<h2 class="text-2xl font-bold">导出记录</h2>
|
||||||
|
<button @click="$emit('close')" class="hover:bg-bg-input p-2 rounded-full transition-colors"><X :size="24" /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-8 flex-1 flex flex-col gap-8 no-scrollbar" :class="dateList.length > 0 ? 'overflow-y-auto' : 'overflow-visible'">
|
||||||
|
<!-- 配置区域 -->
|
||||||
|
<div class="space-y-6 flex-shrink-0">
|
||||||
|
<!-- 日期选择 -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-[10px] font-bold text-text-sec">日期范围</label>
|
<label class="text-[11px] font-bold text-text-sec">日期范围</label>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
<div class="relative flex-1" ref="startCalendarRef">
|
<div class="relative flex-1" ref="startCalendarRef">
|
||||||
<button @click="isExportStartCalendarOpen = !isExportStartCalendarOpen; isExportEndCalendarOpen = false" class="w-full bg-bg-input rounded-xl pl-10 pr-4 py-3 text-sm outline-none border border-transparent focus:border-[#007AFF] transition-all text-left flex items-center">
|
<button @click="isExportStartCalendarOpen = !isExportStartCalendarOpen; isExportEndCalendarOpen = false" class="w-full bg-bg-input rounded-xl pl-10 pr-4 py-3 text-sm outline-none border border-transparent focus:border-[#007AFF] transition-all text-left flex items-center">
|
||||||
@@ -139,8 +220,59 @@ const handleExport = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 主标签选择 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[11px] font-bold text-text-sec">选择要导出的主标签</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button v-for="tag in mainTags" :key="tag.id"
|
||||||
|
@click="toggleTag(tag.id)"
|
||||||
|
class="px-3 py-1.5 rounded-xl text-[12px] font-bold border-2 transition-all active:scale-95"
|
||||||
|
:style="selectedTags.includes(tag.id) ? { backgroundColor: tag.color, borderColor: tag.color, color: 'white' } : { borderColor: tag.color, color: tag.color }"
|
||||||
|
>
|
||||||
|
{{ tag.name }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button @click="handleExport" :disabled="!exportStartDate || !exportEndDate" class="w-full bg-[#007AFF] text-white py-4 rounded-2xl font-bold shadow-lg shadow-[#007AFF]/20 active:scale-95 transition-all disabled:opacity-50 disabled:active:scale-100">导出为 JSON</button>
|
</div>
|
||||||
|
|
||||||
|
<button @click="handlePreview" :disabled="!exportStartDate || !exportEndDate" class="w-full bg-bg-main border border-border-main text-text-main hover:bg-bg-input py-3.5 rounded-2xl font-bold shadow-sm transition-all disabled:opacity-50">
|
||||||
|
生成预览表格
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格预览区域 -->
|
||||||
|
<div v-if="dateList.length > 0" class="flex-1 flex flex-col border-t border-border-main pt-6">
|
||||||
|
<div class="flex justify-between items-center mb-4 flex-shrink-0">
|
||||||
|
<h3 class="text-sm font-bold text-text-sec flex items-center gap-2">数据预览</h3>
|
||||||
|
<button @click="copyToClipboard" class="flex items-center gap-1.5 px-4 py-2 bg-[#007AFF] hover:bg-[#007AFF]/90 text-white rounded-xl text-xs font-bold transition-all shadow-lg shadow-[#007AFF]/20 active:scale-95">
|
||||||
|
<Copy :size="14" /> 一键复制完整表格
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-auto border border-border-main rounded-2xl bg-bg-input/30 relative select-text">
|
||||||
|
<table class="w-full text-left border-collapse text-xs">
|
||||||
|
<thead class="bg-bg-card sticky top-0 z-10 shadow-sm">
|
||||||
|
<tr>
|
||||||
|
<th class="p-4 border-b border-border-main font-bold whitespace-nowrap text-text-sec w-32">日期</th>
|
||||||
|
<th v-for="tagId in selectedTags" :key="tagId" class="p-4 border-b border-l border-border-main/50 font-bold whitespace-nowrap" :style="{ color: mainTags.find(t => t.id === tagId)?.color || 'inherit' }">
|
||||||
|
{{ getTagName(tagId) }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-bg-card">
|
||||||
|
<tr v-for="date in dateList" :key="date" class="border-b border-border-main/30 hover:bg-bg-input/50 transition-colors">
|
||||||
|
<td class="p-4 font-bold whitespace-nowrap text-text-main align-top">
|
||||||
|
{{ date }}
|
||||||
|
</td>
|
||||||
|
<td v-for="tagId in selectedTags" :key="tagId" class="p-4 border-l border-border-main/30 align-top whitespace-pre-wrap leading-relaxed text-text-sec min-w-[150px]">
|
||||||
|
{{ previewData[date]?.[tagId] || '' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user