|
10 | 10 | <div>
|
11 | 11 | <!-- Task List -->
|
12 | 12 | <el-table :data="tasks" style="width: 100%" @row-click="handleRowClick">
|
13 |
| - <el-table-column label="任务名称" min-width="150"> |
| 13 | + <el-table-column label="项目名称" min-width="150"> |
14 | 14 | <template v-slot="scope">
|
15 |
| - <el-input |
16 |
| - v-if="scope.row.isEditing" |
17 |
| - v-model="scope.row.editingName" |
18 |
| - @blur="finishEdit(scope.row)" |
19 |
| - @keyup.enter="finishEdit(scope.row)" |
20 |
| - ref="nameInput" |
21 |
| - /> |
22 |
| - <span v-else @dblclick="startEdit(scope.row)" class="cursor-pointer hover:text-blue-500"> |
23 |
| - {{ scope.row.name }} |
24 |
| - </span> |
| 15 | + <el-tooltip :content="taskNameTooltip(scope.row)" placement="top"> |
| 16 | + <span> |
| 17 | + <el-input |
| 18 | + v-if="scope.row.isEditing" |
| 19 | + v-model="scope.row.editingName" |
| 20 | + @blur="finishEdit(scope.row)" |
| 21 | + @keyup.enter="finishEdit(scope.row)" |
| 22 | + ref="nameInput" |
| 23 | + /> |
| 24 | + <span v-else @dblclick="startEdit(scope.row)" class="cursor-pointer hover:text-blue-500"> |
| 25 | + {{ scope.row.name }} |
| 26 | + </span> |
| 27 | + <el-icon-question class="ml-1 cursor-pointer" @click="showTooltip(scope.row)" /> |
| 28 | + </span> |
| 29 | + </el-tooltip> |
25 | 30 | </template>
|
26 | 31 | </el-table-column>
|
27 | 32 | <el-table-column label="时长统计(事件计数)" min-width="200">
|
|
41 | 46 | </div>
|
42 | 47 | </template>
|
43 | 48 | </el-table-column>
|
44 |
| - <el-table-column label="任务说明" min-width="200"> |
| 49 | + <el-table-column label="任务说明" min-width="300"> |
45 | 50 | <template v-slot="scope">
|
46 | 51 | <el-input
|
47 | 52 | v-model="taskDescriptions[scope.row.id]"
|
|
52 | 57 | ></el-input>
|
53 | 58 | </template>
|
54 | 59 | </el-table-column>
|
55 |
| - <el-table-column label="计时状态" min-width="150"> |
| 60 | + <el-table-column label="计时状态" min-width="100"> |
56 | 61 | <template v-slot="scope">
|
57 | 62 | <span v-if="isTaskRunning(scope.row.id)" class="text-blue-500 font-bold">
|
58 | 63 | 已计时: {{ getRunningTime(scope.row.id) }}
|
59 | 64 | </span>
|
60 | 65 | <span v-else>未开始计时</span>
|
61 | 66 | </template>
|
62 | 67 | </el-table-column>
|
63 |
| - <el-table-column label="操作" min-width="500"> |
| 68 | + <el-table-column label="部分操作" min-width="100"> |
64 | 69 | <template v-slot="scope">
|
65 |
| - <div class="flex space-x-2"> |
66 |
| - <el-button style="margin-left: 0px;" @click.stop="startTimer(scope.row.id)" type="primary" :disabled="isTaskRunning(scope.row.id)">开始计时</el-button> |
67 |
| - <el-button style="margin-left: 0px;" @click.stop="stopTimer(scope.row.id)" type="danger" :disabled="!isTaskRunning(scope.row.id)">结束计时</el-button> |
68 |
| - <el-dropdown trigger="click" @command="command => handleExport(command, scope.row)"> |
69 |
| - <el-button type="success">导出<el-icon class="el-icon--right"><arrow-down /></el-icon></el-button> |
70 |
| - <template #dropdown> |
71 |
| - <el-dropdown-menu> |
72 |
| - <el-dropdown-item command="json">JSON</el-dropdown-item> |
73 |
| - <el-dropdown-item command="csv">CSV</el-dropdown-item> |
74 |
| - </el-dropdown-menu> |
75 |
| - </template> |
76 |
| - </el-dropdown> |
77 |
| - <el-button @click="deleteTask(scope.row)" type="danger">删除</el-button> |
| 70 | + <div class="flex flex-wrap"> |
| 71 | + <el-button style="margin-left: 10px;" class="mb-2" @click.stop="startTimer(scope.row.id)" type="primary" :disabled="isTaskRunning(scope.row.id)">开始计时</el-button> |
| 72 | + <el-button style="margin-left: 10px;" class="mb-2" @click.stop="stopTimer(scope.row.id)" type="danger" :disabled="!isTaskRunning(scope.row.id)">结束计时</el-button> |
78 | 73 | </div>
|
79 | 74 | </template>
|
80 | 75 | </el-table-column>
|
81 | 76 | </el-table>
|
82 | 77 | <!-- Add Task -->
|
83 |
| - <el-input v-model="newTaskName" placeholder="任务名称" style="margin-top: 20px;"></el-input> |
84 |
| - <el-button @click="addTask" type="success" style="margin-top: 10px;">创建任务</el-button> |
85 |
| - </div> |
86 |
| - <!-- Global Actions --> |
87 |
| - <div class="flex gap-2 mt-4"> |
88 |
| - <el-upload class="upload-demo" action="" :auto-upload="false" :show-file-list="false" |
89 |
| - accept=".json,.csv" :on-change="handleFileChange"> |
90 |
| - <el-button type="primary">导入任务</el-button> |
91 |
| - </el-upload> |
92 |
| - <el-dropdown trigger="click" @command="handleGlobalExport"> |
93 |
| - <el-button type="success">导出所有数据<el-icon |
94 |
| - class="el-icon--right"><arrow-down /></el-icon></el-button> |
95 |
| - <template #dropdown> |
96 |
| - <el-dropdown-menu> |
97 |
| - <el-dropdown-item command="json">JSON</el-dropdown-item> |
98 |
| - </el-dropdown-menu> |
99 |
| - </template> |
100 |
| - </el-dropdown> |
101 |
| - <el-button type="danger" @click="clearAllTasks">清除所有数据</el-button> |
102 |
| - <el-button type="success" @click="addNewEvent">新增记录</el-button> |
| 78 | + <div class="flex mt-5 w-2/4"> |
| 79 | + <el-input v-model="newTaskName" placeholder="项目名称"></el-input> |
| 80 | + <el-button @click="addTask" type="success">添加项目</el-button> |
| 81 | + </div> |
103 | 82 | </div>
|
| 83 | + |
104 | 84 | <!-- Time Records Display -->
|
105 |
| - <div v-if="selectedTask" class="mt-5 p-5 border border-gray-200 rounded-lg"> |
106 |
| - <h3 class="text-lg font-medium mb-4">{{ selectedTask.name }} - 时间记录</h3> |
107 |
| - <div class="flex items-center gap-4 px-5 mb-4"> |
108 |
| - <span>时间单位宽度: {{ (baseUnitWidth * 3600).toFixed(1) }}px/小时</span> |
109 |
| - <el-slider v-model="baseUnitWidth" :min="0.0004" :max="1" :step="0.0001" |
110 |
| - @change="handleZoomChange" class="flex-1" /> |
111 |
| - </div> |
112 |
| - <!-- 增加一行小字提示:小提示:滚轮缩放时间轴,拖拽滚动时间轴 --> |
113 |
| - <div class="text-sm text-gray-500 mb-4"> |
114 |
| - <span>小提示:鼠标滚轮缩放时间轴,拖拽滚动时间轴</span> |
| 85 | + <div class="mt-5 p-5 border border-gray-200 rounded-lg"> |
| 86 | + <div v-if="!selectedTask"> |
| 87 | + <h3 class="text-lg font-medium mb-4">时间线</h3> |
| 88 | + <div class="text-sm text-gray-500 mt-4"> |
| 89 | + <span>请先选择一个项目以查看时间线(任务列表)<br>*你知道吗?双击项目名称可以重命名</span> |
| 90 | + </div> |
115 | 91 | </div>
|
116 |
| - |
117 |
| - <div class="relative border border-gray-200 rounded-lg overflow-hidden"> |
118 |
| - <div @wheel.prevent="handleWheel" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag" |
119 |
| - @mouseleave="stopDrag" ref="scrollContainer" |
120 |
| - style="scrollbar-width: none; min-height: 200px;" |
121 |
| - class="relative w-full overflow-x-auto cursor-grab active:cursor-grabbing select-none"> |
122 |
| - <div class="timeline-content" :style="{ minWidth: `${24 * 3600 * baseUnitWidth}rem` }"> |
123 |
| - <div class="sticky top-0 z-10 flex h-8 items-center bg-gray-50 border-b border-gray-200 pl-24"> |
124 |
| - <div v-for="mark in timeScaleMarks" :key="mark.time" |
125 |
| - :style="{ width: `${mark.width}rem` }" |
126 |
| - class="flex-shrink-0 text-xs text-gray-600 text-center border-r border-gray-200"> |
127 |
| - {{ mark.label }} |
128 |
| - </div> |
129 |
| - </div> |
130 |
| - |
131 |
| - <div class="time-blocks-container"> |
132 |
| - <div v-for="(blocks, date) in formattedTimeBlocks" :key="date" |
133 |
| - class="flex h-10 my-2.5 items-center border-b border-gray-100"> |
134 |
| - <div |
135 |
| - class="sticky left-0 z-20 w-24 px-2.5 py-5 text-sm text-gray-700"> |
136 |
| - {{ date }} |
| 92 | + <div v-if="selectedTask"> |
| 93 | + <h3 class="text-lg font-medium mb-4">时间线 ( {{ selectedTask.name||"" }} )</h3> |
| 94 | + <!-- <div class="flex items-center gap-4 px-5 mb-4"> |
| 95 | + <span>时间单位宽度: {{ (baseUnitWidth * 3600).toFixed(1) }}px/小时</span> |
| 96 | + <el-slider v-model="baseUnitWidth" :min="0.0004" :max="1" :step="0.0001" |
| 97 | + @change="handleZoomChange" class="flex-1" /> |
| 98 | + </div> --> |
| 99 | + <!-- 增加一行小字提示:小提示:滚轮缩放时间轴,拖拽滚动时间轴 --> |
| 100 | + <div class="mb-4"> |
| 101 | + |
| 102 | + <el-button type="success" @click="addNewEvent">新增记录</el-button> |
| 103 | + <el-button @click.stop="handleExport(scope.row)" type="success">导出</el-button> |
| 104 | + <el-button @click="deleteTask(scope.row)" type="danger">删除</el-button> |
| 105 | + </div> |
| 106 | + |
| 107 | + <div class="relative border border-gray-200 rounded-lg overflow-hidden"> |
| 108 | + <div @wheel.prevent="handleWheel" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag" |
| 109 | + @mouseleave="stopDrag" ref="scrollContainer" |
| 110 | + style="scrollbar-width: none; min-height: 200px;" |
| 111 | + class="relative w-full overflow-x-auto cursor-grab active:cursor-grabbing select-none"> |
| 112 | + <div class="timeline-content" :style="{ minWidth: `${24 * 3600 * baseUnitWidth}rem` }"> |
| 113 | + <div class="sticky top-0 z-10 flex h-8 items-center bg-gray-50 border-b border-gray-200 pl-24"> |
| 114 | + <div v-for="mark in timeScaleMarks" :key="mark.time" |
| 115 | + :style="{ width: `${mark.width}rem` }" |
| 116 | + class="flex-shrink-0 text-xs text-gray-600 text-center border-r border-gray-200"> |
| 117 | + {{ mark.label }} |
137 | 118 | </div>
|
138 |
| - <div class="relative flex-grow h-full border-l border-gray-200"> |
139 |
| - <!-- 原有的时间块显示 --> |
140 |
| - <div v-for="block in blocks" :key="block.id" |
141 |
| - class="absolute h-8 top-1 rounded text-xs text-white px-1 flex items-center justify-center overflow-hidden whitespace-nowrap opacity-100 hover:opacity-90 transition-opacity cursor-pointer" |
142 |
| - :style="{ |
143 |
| - left: `${calculateLeftPosition(block.start)}rem`, |
144 |
| - width: `${calculateDuration(block.start, block.end)}rem`, |
145 |
| - backgroundColor: block.color |
146 |
| - }" :title="`${block.description} |
147 |
| -开始:${formatDetailTime(block.start)} |
148 |
| -结束:${formatDetailTime(block.end)} |
149 |
| -持续:${formatDuration(block.start, block.end)} |
150 |
| -相同颜色累计时间:${formatDurationSimple(calculateColorDurations(blocks)[block.color])}`" |
151 |
| - @click="editEvent(block, date)"> |
152 |
| - {{ block.displayText }} |
| 119 | + </div> |
| 120 | + |
| 121 | + <div class="time-blocks-container"> |
| 122 | + <div v-for="(blocks, date) in formattedTimeBlocks" :key="date" |
| 123 | + class="flex h-10 my-2.5 items-center border-b border-gray-100"> |
| 124 | + <div |
| 125 | + class="sticky left-0 z-20 w-24 px-2.5 py-5 text-sm text-gray-700"> |
| 126 | + {{ date }} |
| 127 | + </div> |
| 128 | + <div class="relative flex-grow h-full border-l border-gray-200"> |
| 129 | + <!-- 原有的时间块显示 --> |
| 130 | + <div v-for="block in blocks" :key="block.id" |
| 131 | + class="absolute h-8 top-1 rounded text-xs text-white px-1 flex items-center justify-center overflow-hidden whitespace-nowrap opacity-100 hover:opacity-90 transition-opacity cursor-pointer" |
| 132 | + :style="{ |
| 133 | + left: `${calculateLeftPosition(block.start)}rem`, |
| 134 | + width: `${calculateDuration(block.start, block.end)}rem`, |
| 135 | + backgroundColor: block.color |
| 136 | + }" :title="`${block.description} |
| 137 | + 开始:${formatDetailTime(block.start)} |
| 138 | + 结束:${formatDetailTime(block.end)} |
| 139 | + 持续:${formatDuration(block.start, block.end)} |
| 140 | + 相同颜色累计时间:${formatDurationSimple(calculateColorDurations(blocks)[block.color])}`" |
| 141 | + @click="editEvent(block, date)"> |
| 142 | + {{ block.displayText }} |
| 143 | + </div> |
153 | 144 | </div>
|
154 | 145 | </div>
|
155 | 146 | </div>
|
156 | 147 | </div>
|
157 | 148 | </div>
|
158 | 149 | </div>
|
| 150 | + <div class="text-sm text-gray-500 mt-4"> |
| 151 | + <span>小提示:鼠标滚轮缩放时间轴,拖拽滚动时间轴</span> |
| 152 | + </div> |
159 | 153 | </div>
|
160 | 154 | </div>
|
161 |
| - <el-dialog v-model="editDialogVisible" title="编辑时间事件" width="500px"> |
162 |
| - <el-form :model="editingEvent" label-width="100px"> |
| 155 | + <el-dialog v-model="editDialogVisible" title="编辑时间片" style="width: 70%;"> |
| 156 | + <el-form :model="editingEvent"> |
163 | 157 | <el-form-item label="开始时间">
|
164 | 158 | <el-time-picker v-model="editingEvent.start" format="HH:mm:ss" />
|
165 | 159 | </el-form-item>
|
166 | 160 | <el-form-item label="结束时间">
|
167 | 161 | <el-time-picker v-model="editingEvent.end" format="HH:mm:ss" />
|
168 | 162 | </el-form-item>
|
169 | 163 | <el-form-item label="描述">
|
170 |
| - <el-input v-model="editingEvent.description" type="textarea" /> |
| 164 | + <el-input v-model="editingEvent.description" type="textarea" :rows="7"/> |
171 | 165 | </el-form-item>
|
172 | 166 | <el-form-item label="颜色">
|
173 | 167 | <el-color-picker v-model="editingEvent.color" />
|
174 | 168 | </el-form-item>
|
175 | 169 | </el-form>
|
176 | 170 | <template #footer>
|
177 | 171 | <span class="dialog-footer">
|
| 172 | + <el-button type="danger" @click="deleteEvent">删除</el-button> |
178 | 173 | <el-button @click="editDialogVisible = false">取消</el-button>
|
179 | 174 | <el-button type="primary" @click="saveEventEdit">保存</el-button>
|
180 |
| - <el-button type="danger" @click="deleteEvent">删除事件</el-button> |
181 | 175 | </span>
|
182 | 176 | </template>
|
183 | 177 | </el-dialog>
|
|
191 | 185 | inactive-text="手动导出存档"
|
192 | 186 | @change="saveSettings"
|
193 | 187 | />
|
| 188 | + <!-- Global Actions --> |
| 189 | + <div class="flex gap-2 mt-4"> |
| 190 | + <el-upload class="upload-demo" action="" :auto-upload="false" :show-file-list="false" |
| 191 | + accept=".json" :on-change="handleFileChange"> |
| 192 | + <el-button type="primary">导入任务</el-button> |
| 193 | + </el-upload> |
| 194 | + <el-button type="success" @click="handleGlobalExport('json')">导出所有数据</el-button> |
| 195 | + <el-button type="danger" @click="clearAllTasks">清除所有数据</el-button> |
| 196 | + </div> |
194 | 197 | </div>
|
195 | 198 | <!-- Footer -->
|
196 | 199 | <div class="mt-8 text-center text-gray-500 text-sm">
|
|
214 | 217 | </template>
|
215 | 218 |
|
216 | 219 | <script>
|
| 220 | +import './styles/index.css' |
217 | 221 | import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue'
|
218 | 222 | import { ElTable, ElTableColumn, ElButton, ElInput, ElContainer, ElMain, ElMessage, ElMessageBox } from 'element-plus'
|
219 | 223 | import { ArrowDown } from '@element-plus/icons-vue'
|
220 | 224 | import 'element-plus/dist/index.css'
|
221 |
| -import './styles/index.css' |
222 | 225 |
|
223 | 226 | export default {
|
224 | 227 | components: {
|
@@ -276,6 +279,7 @@ export default {
|
276 | 279 | editingEventOriginal: null,
|
277 | 280 | requireCtrlForZoom: false, // 新增:控制是否需要Ctrl键进行缩放
|
278 | 281 | autoExport: true, // Add this line
|
| 282 | + taskNameTooltipText: '双击可编辑项目名称', |
279 | 283 | }
|
280 | 284 | },
|
281 | 285 | mounted() {
|
@@ -417,7 +421,6 @@ export default {
|
417 | 421 | return;
|
418 | 422 | }
|
419 | 423 | ElMessage.success('计时已结束');
|
420 |
| - this.taskDescriptions[taskId] = ''; // 清空任务说明 |
421 | 424 | this.saveToStorage(); // 保存到本地存储以保持颜色信息
|
422 | 425 | // Only auto-export if enabled
|
423 | 426 | if (this.autoExport) {
|
@@ -654,21 +657,13 @@ export default {
|
654 | 657 | }
|
655 | 658 | },
|
656 | 659 |
|
657 |
| - handleExport(format, task) { |
658 |
| - if (format === 'json') { |
659 |
| - const data = { |
660 |
| - ...task, |
661 |
| - developer: 'createskyblue' |
662 |
| - }; |
663 |
| - const dataStr = JSON.stringify(data, null, 2); |
664 |
| - this.downloadFile(dataStr, `task_${task.id}.json`, 'application/json'); |
665 |
| - } else if (format === 'csv') { |
666 |
| - const headers = ['开始时间,结束时间,描述\n']; |
667 |
| - const rows = task.timers.map(timer => |
668 |
| - `${timer.start},${timer.end || ''},${timer.description}\n` |
669 |
| - ); |
670 |
| - this.downloadFile(headers.concat(rows).join(''), `task_${task.id}.csv`, 'text/csv'); |
671 |
| - } |
| 660 | + handleExport(task) { |
| 661 | + const data = { |
| 662 | + ...task, |
| 663 | + developer: 'createskyblue' |
| 664 | + }; |
| 665 | + const dataStr = JSON.stringify(data, null, 2); |
| 666 | + this.downloadFile(dataStr, `task_${task.id}.json`, 'application/json'); |
672 | 667 | },
|
673 | 668 | handleGlobalExport(format) {
|
674 | 669 | if (format === 'json') {
|
@@ -755,13 +750,13 @@ export default {
|
755 | 750 | },
|
756 | 751 | deleteTask(task) {
|
757 | 752 | ElMessageBox.prompt(
|
758 |
| - '请输入完整的任务名称以确认删除', |
| 753 | + '请输入完整的项目名称以确认删除', |
759 | 754 | '警告',
|
760 | 755 | {
|
761 | 756 | confirmButtonText: '确认',
|
762 | 757 | cancelButtonText: '取消',
|
763 | 758 | inputPattern: new RegExp(`^${task.name}$`),
|
764 |
| - inputErrorMessage: '任务名称不正确' |
| 759 | + inputErrorMessage: '项目名称不正确' |
765 | 760 | }
|
766 | 761 | ).then(({ value }) => {
|
767 | 762 | this.tasks = this.tasks.filter(t => t.id !== task.id);
|
@@ -858,16 +853,23 @@ export default {
|
858 | 853 |
|
859 | 854 | this.editDialogVisible = true;
|
860 | 855 | },
|
861 |
| -
|
862 | 856 | deleteEvent() {
|
863 |
| - if (!this.editingEventOriginal || !this.selectedTask) return; |
| 857 | + if (!this.editingEventOriginal || !this.selectedTask) return; |
864 | 858 |
|
| 859 | + this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', { |
| 860 | + confirmButtonText: '确定', |
| 861 | + cancelButtonText: '取消', |
| 862 | + type: 'warning' |
| 863 | + }).then(() => { |
865 | 864 | // 从任务的时间记录中删除
|
866 | 865 | this.selectedTask.timers = this.selectedTask.timers.filter(timer => timer !== this.editingEventOriginal);
|
867 | 866 | this.formattedTimeBlocks = this.formatTimeBlocks(this.selectedTask);
|
868 | 867 | this.saveToStorage();
|
869 | 868 | ElMessage.success('记录已删除');
|
870 | 869 | this.editDialogVisible = false;
|
| 870 | + }).catch(() => { |
| 871 | + ElMessage.info('已取消删除'); |
| 872 | + }); |
871 | 873 | },
|
872 | 874 | handleRowClick(row, column) {
|
873 | 875 | // 如果点击的是操作列或正在编辑的输入框,不进行跳转
|
@@ -985,6 +987,12 @@ export default {
|
985 | 987 |
|
986 | 988 | return `${hours}小时${minutes}分钟 (${recordCount})`;
|
987 | 989 | },
|
| 990 | + taskNameTooltip(row) { |
| 991 | + return this.taskNameTooltipText; |
| 992 | + }, |
| 993 | + showTooltip(row) { |
| 994 | + // 可以在这里添加显示提示的逻辑,如果需要 |
| 995 | + }, |
988 | 996 | // Add new method for saving settings
|
989 | 997 | saveSettings() {
|
990 | 998 | localStorage.setItem('taskTimeTracker_settings', JSON.stringify({
|
|
0 commit comments