TaskListPanel.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <template>
  2. <div class="task-list-panel">
  3. <!-- 操作区域(顶部) -->
  4. <div class="options-area">
  5. <div v-for="item in optionItems" :key="item.type" :class="['options-area-item', `options-${item.type}`]" @click="handleOptionClick(item.type)">
  6. <div class="options-img">
  7. <div class="options-icon"></div>
  8. </div>
  9. <a-button size="small" type="primary">{{ item.label }}</a-button>
  10. </div>
  11. </div>
  12. <!-- 搜索模式 -->
  13. <div v-if="activeOption === 'search'" class="search-header">
  14. <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
  15. </div>
  16. <div class="task-list">
  17. <div
  18. v-for="(task, index) in filteredTaskList"
  19. :key="index"
  20. :class="['task-item', { active: currentTaskId === task.id }]"
  21. @click="emit('switch-task', task.id)"
  22. >
  23. <span class="task-name">{{ task.name }}</span>
  24. <div class="task-right">
  25. <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
  26. <div class="task-delete" @click.stop="emit('delete-task', task, index)">
  27. <DeleteOutlined />
  28. </div>
  29. </div>
  30. </div>
  31. <div v-if="activeOption === 'search' && searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script setup lang="ts">
  36. import { ref, computed } from 'vue';
  37. import { DeleteOutlined } from '@ant-design/icons-vue';
  38. import type { Task } from './types';
  39. const props = defineProps<{
  40. taskList: Task[];
  41. currentTaskId: string;
  42. }>();
  43. const emit = defineEmits<{
  44. (e: 'switch-task', taskId: string): void;
  45. (e: 'create-task'): void;
  46. (e: 'delete-task', task: Task, index: number): void;
  47. }>();
  48. type OptionType = 'add' | 'search' | 'skill';
  49. const activeOption = ref<OptionType>('add');
  50. const searchKeyword = ref('');
  51. const optionItems = [
  52. { type: 'add' as OptionType, label: '新建任务' },
  53. { type: 'search' as OptionType, label: '搜索' },
  54. { type: 'skill' as OptionType, label: '技能' },
  55. ];
  56. const filteredTaskList = computed(() => {
  57. if (activeOption.value !== 'search' || !searchKeyword.value.trim()) {
  58. return props.taskList;
  59. }
  60. const keyword = searchKeyword.value.trim().toLowerCase();
  61. return props.taskList.filter((task) => task.name.toLowerCase().includes(keyword));
  62. });
  63. const formatRelativeTime = (dateStr?: string) => {
  64. if (!dateStr) return '刚刚';
  65. const now = Date.now();
  66. const time = new Date(dateStr).getTime();
  67. const diff = now - time;
  68. if (diff < 60 * 1000) return '刚刚';
  69. if (diff < 60 * 60 * 1000) return `${Math.floor(diff / (60 * 1000))}分钟前`;
  70. if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
  71. return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
  72. };
  73. const handleOptionClick = (type: OptionType) => {
  74. if (type === 'add') {
  75. emit('create-task');
  76. } else if (type === 'skill') {
  77. // TODO: 技能点击事件
  78. } else {
  79. activeOption.value = type;
  80. searchKeyword.value = '';
  81. }
  82. };
  83. </script>
  84. <style scoped lang="less">
  85. .task-list-panel {
  86. width: 260px;
  87. height: 100%;
  88. display: flex;
  89. flex-direction: column;
  90. background-image: var(--img-chat-task-panel-bg);
  91. background-repeat: no-repeat;
  92. background-size: 100% 100%;
  93. border-radius: 8px;
  94. flex-shrink: 0;
  95. padding: 10px;
  96. .search-header {
  97. padding-bottom: 10px;
  98. .search-input {
  99. height: 32px;
  100. background: rgba(10, 132, 255, 0.1);
  101. border: 1px solid rgba(63, 80, 106, 0.5);
  102. color: #e0e6ed;
  103. :deep(.zxm-input) {
  104. background: transparent;
  105. color: #e0e6ed;
  106. }
  107. }
  108. }
  109. .task-list {
  110. flex: 1;
  111. overflow-y: auto;
  112. .task-item {
  113. height: 36px;
  114. display: flex;
  115. align-items: center;
  116. gap: 10px;
  117. padding: 4px;
  118. cursor: pointer;
  119. transition: all 0.3s;
  120. margin-bottom: 4px;
  121. color: #e0e6ed;
  122. &:hover {
  123. background: rgba(10, 132, 255, 0.1);
  124. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  125. }
  126. &.active {
  127. background: transparent;
  128. box-shadow: inset 0 0 8px rgba(10, 132, 255, 0.4);
  129. border: 1px solid rgba(10, 132, 255, 0.3);
  130. color: #e0e6ed;
  131. }
  132. .task-icon {
  133. width: 16px;
  134. height: 16px;
  135. flex-shrink: 0;
  136. background-image: var(--img-chat-task-add-icon);
  137. background-repeat: no-repeat;
  138. background-size: 100% 100%;
  139. }
  140. .task-name {
  141. font-size: 14px;
  142. font-weight: 400;
  143. line-height: 1.4;
  144. overflow: hidden;
  145. text-overflow: ellipsis;
  146. white-space: nowrap;
  147. flex: 1;
  148. }
  149. .task-time {
  150. font-size: 12px;
  151. color: #8b949e;
  152. flex-shrink: 0;
  153. white-space: nowrap;
  154. }
  155. .task-delete {
  156. flex-shrink: 0;
  157. width: 28px;
  158. height: 28px;
  159. display: none;
  160. align-items: center;
  161. justify-content: center;
  162. border-radius: 4px;
  163. color: #8b949e;
  164. font-size: 14px;
  165. transition: all 0.2s;
  166. &:hover {
  167. color: #ff4d4f;
  168. background: rgba(220, 38, 38, 0.1);
  169. }
  170. }
  171. &:hover .task-time {
  172. display: none;
  173. }
  174. &:hover .task-delete {
  175. display: flex;
  176. }
  177. }
  178. .empty-hint {
  179. text-align: center;
  180. color: #8b949e;
  181. font-size: 13px;
  182. padding: 20px 0;
  183. }
  184. }
  185. .options-area {
  186. display: flex;
  187. flex-direction: column;
  188. gap: 4px;
  189. padding-bottom: 10px;
  190. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  191. .options-area-item {
  192. display: flex;
  193. flex-direction: row;
  194. align-items: center;
  195. gap: 10px;
  196. cursor: pointer;
  197. border-radius: 6px;
  198. padding: 6px 8px;
  199. transition: all 0.3s;
  200. &:hover {
  201. background: rgba(10, 132, 255, 0.1);
  202. }
  203. .options-img {
  204. width: 32px;
  205. height: 32px;
  206. display: flex;
  207. align-items: center;
  208. justify-content: center;
  209. flex-shrink: 0;
  210. background-image: var(--img-chat-options-icon-border);
  211. background-repeat: no-repeat;
  212. background-size: 100% 100%;
  213. background-position: center;
  214. }
  215. .options-icon {
  216. width: 14px;
  217. height: 14px;
  218. background-repeat: no-repeat;
  219. background-size: 100% 100%;
  220. background-position: center;
  221. }
  222. &.options-add .options-icon {
  223. background-image: var(--img-chat-options-add-icon);
  224. }
  225. &.options-search .options-icon {
  226. background-image: var(--img-chat-options-search-icon);
  227. }
  228. &.options-skill .options-icon {
  229. background-image: var(--img-chat-options-skill-icon);
  230. }
  231. .zxm-btn {
  232. font-size: 14px;
  233. padding: 0;
  234. height: auto;
  235. background: transparent !important;
  236. border: none;
  237. color: #e0e6ed;
  238. box-shadow: none;
  239. }
  240. }
  241. }
  242. }
  243. </style>