TaskListPanel.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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 class="search-header">
  14. <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
  15. </div>
  16. <!-- 置顶任务区域 -->
  17. <div v-if="pinnedTaskList.length > 0" class="pinned-task-list">
  18. <div
  19. v-for="(task, index) in pinnedTaskList"
  20. :key="'pinned-' + task.id"
  21. :class="['task-item', { active: currentTaskId === task.id }]"
  22. @click="emit('switch-task', task.id)"
  23. >
  24. <div class="task-pin-icon pinned" @click.stop="togglePin(task)" title="取消置顶"></div>
  25. <span class="task-name">{{ task.name }}</span>
  26. <div class="task-right">
  27. <span v-if="task.isStreaming" class="task-loading"></span>
  28. <span v-else class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
  29. <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
  30. </div>
  31. </div>
  32. </div>
  33. <div class="task-list">
  34. <div
  35. v-for="(task, index) in filteredTaskList"
  36. :key="index"
  37. :class="['task-item', { active: currentTaskId === task.id }]"
  38. @click="emit('switch-task', task.id)"
  39. >
  40. <div class="task-pin-icon" @click.stop="togglePin(task)" title="置顶"></div>
  41. <span class="task-name">{{ task.name }}</span>
  42. <div class="task-right">
  43. <span v-if="task.isStreaming" class="task-loading"></span>
  44. <span v-else class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
  45. <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
  46. </div>
  47. </div>
  48. <EmptyState v-if="searchKeyword && filteredTaskList.length === 0" description="未找到匹配的任务" />
  49. <EmptyState v-else-if="filteredTaskList.length === 0" description="还没有任务" />
  50. </div>
  51. </div>
  52. </template>
  53. <script setup lang="ts">
  54. import { ref, computed } from 'vue';
  55. import EmptyState from '/@/views/ventAI/manageAssistent/components/common/EmptyState.vue';
  56. import type { Task } from '/@/views/ventAI/manageAssistent/types/index';
  57. const props = defineProps<{
  58. taskList: Task[];
  59. currentTaskId: string;
  60. }>();
  61. const emit = defineEmits<{
  62. (e: 'switch-task', taskId: string): void;
  63. (e: 'create-task'): void;
  64. (e: 'delete-task', task: Task, index: number): void;
  65. (e: 'pin-task', task: Task): void;
  66. (e: 'toggle-skill'): void;
  67. (e: 'toggle-schedules'): void;
  68. (e: 'toggle-sub-agents'): void;
  69. }>();
  70. type OptionType = 'add' | 'search' | 'skill' | 'schedules' | 'sub-agents';
  71. const searchKeyword = ref('');
  72. const pinnedTaskList = computed(() => {
  73. let list = props.taskList.filter((task) => task.sortOrder === 1);
  74. if (searchKeyword.value.trim()) {
  75. const keyword = searchKeyword.value.trim().toLowerCase();
  76. list = list.filter((task) => task.name.toLowerCase().includes(keyword));
  77. }
  78. return list;
  79. });
  80. const filteredTaskList = computed(() => {
  81. let list = props.taskList.filter((task) => task.sortOrder !== 1);
  82. if (searchKeyword.value.trim()) {
  83. const keyword = searchKeyword.value.trim().toLowerCase();
  84. list = list.filter((task) => task.name.toLowerCase().includes(keyword));
  85. }
  86. return list;
  87. });
  88. const togglePin = (task: Task) => {
  89. emit('pin-task', task);
  90. };
  91. // 顶部操作区的入口列表:四个入口对所有角色都显示。
  92. //
  93. // 这里曾按 isAdmin 过滤掉「技能」「子智能体」两个入口(第四轮审计 M3),现改为入口一律可见:
  94. // 非管理员进去后面板只读(隐藏创建/编辑/删除、禁用启停开关),而不是连看都看不到。
  95. // 因此本组件不再需要 isAdmin —— 角色差异全部收敛在面板内部。
  96. const optionItems = [
  97. { type: 'add' as OptionType, label: '新建任务' },
  98. // { type: 'search' as OptionType, label: '搜索' },
  99. { type: 'skill' as OptionType, label: '技能' },
  100. { type: 'sub-agents' as OptionType, label: '子智能体' },
  101. { type: 'schedules' as OptionType, label: '定时任务' },
  102. ];
  103. const formatRelativeTime = (dateStr?: string) => {
  104. if (!dateStr) return '刚刚';
  105. const now = Date.now();
  106. const time = new Date(dateStr).getTime();
  107. const diff = now - time;
  108. if (diff < 60 * 1000) return '刚刚';
  109. if (diff < 60 * 60 * 1000) return `${Math.floor(diff / (60 * 1000))}分钟前`;
  110. if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
  111. return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
  112. };
  113. const handleOptionClick = (type: OptionType) => {
  114. if (type === 'add') {
  115. emit('create-task');
  116. } else if (type === 'skill') {
  117. emit('toggle-skill');
  118. } else if (type === 'sub-agents') {
  119. emit('toggle-sub-agents');
  120. } else if (type === 'schedules') {
  121. emit('toggle-schedules');
  122. }
  123. };
  124. </script>
  125. <style scoped lang="less">
  126. .task-list-panel {
  127. width: 100%;
  128. height: 100%;
  129. display: flex;
  130. flex-direction: column;
  131. background-image: var(--img-chat-task-panel-bg);
  132. background-repeat: no-repeat;
  133. background-size: 100% 100%;
  134. border-radius: 8px;
  135. flex-shrink: 0;
  136. padding: 10px;
  137. .search-header {
  138. padding-bottom: 10px;
  139. .search-input {
  140. height: 32px;
  141. background: rgba(10, 132, 255, 0.1);
  142. border: 1px solid rgba(63, 80, 106, 0.5);
  143. color: #e0e6ed;
  144. :deep(.zxm-input) {
  145. background: transparent;
  146. color: #e0e6ed;
  147. }
  148. }
  149. }
  150. .pinned-task-list {
  151. max-height: 180px;
  152. overflow-y: auto;
  153. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  154. margin-bottom: 8px;
  155. padding-bottom: 8px;
  156. }
  157. .task-list {
  158. flex: 1;
  159. overflow-y: auto;
  160. }
  161. .pinned-task-list,
  162. .task-list {
  163. .task-item {
  164. height: 36px;
  165. display: flex;
  166. align-items: center;
  167. gap: 4px;
  168. padding: 4px;
  169. cursor: pointer;
  170. transition: all 0.3s;
  171. margin-bottom: 4px;
  172. color: #e0e6ed;
  173. &:hover {
  174. background: rgba(10, 132, 255, 0.1);
  175. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  176. }
  177. &.active {
  178. background: transparent;
  179. box-shadow: inset 0 0 8px rgba(10, 132, 255, 0.4);
  180. border: 1px solid rgba(10, 132, 255, 0.3);
  181. color: #e0e6ed;
  182. }
  183. .task-pin-icon {
  184. width: 28px;
  185. height: 28px;
  186. flex-shrink: 0;
  187. background-image: var(--img-chat-pin-icon);
  188. background-repeat: no-repeat;
  189. background-size: 16px 16px;
  190. background-position: center;
  191. opacity: 0;
  192. transition: opacity 0.2s;
  193. border-radius: 4px;
  194. &.pinned {
  195. opacity: 1;
  196. }
  197. &:hover {
  198. background-color: rgba(10, 132, 255, 0.2);
  199. }
  200. }
  201. &:hover .task-pin-icon {
  202. opacity: 1;
  203. }
  204. .task-icon {
  205. width: 16px;
  206. height: 16px;
  207. flex-shrink: 0;
  208. background-image: var(--img-chat-task-add-icon);
  209. background-repeat: no-repeat;
  210. background-size: 100% 100%;
  211. }
  212. .task-name {
  213. font-size: 14px;
  214. font-weight: 400;
  215. line-height: 1.4;
  216. overflow: hidden;
  217. text-overflow: ellipsis;
  218. white-space: nowrap;
  219. flex: 1;
  220. }
  221. .task-right {
  222. display: flex;
  223. align-items: center;
  224. gap: 6px;
  225. flex-shrink: 0;
  226. }
  227. .task-time {
  228. font-size: 12px;
  229. color: #8b949e;
  230. flex-shrink: 0;
  231. white-space: nowrap;
  232. }
  233. .task-loading {
  234. width: 14px;
  235. height: 14px;
  236. flex-shrink: 0;
  237. border: 2px solid rgba(10, 132, 255, 0.2);
  238. border-top-color: rgba(10, 132, 255, 0.8);
  239. border-radius: 50%;
  240. animation: task-spin 0.8s linear infinite;
  241. }
  242. .task-delete {
  243. flex-shrink: 0;
  244. width: 28px;
  245. height: 28px;
  246. display: none;
  247. background-image: var(--img-chat-delete-icon);
  248. background-repeat: no-repeat;
  249. background-size: 16px 16px;
  250. background-position: center;
  251. transition: all 0.2s;
  252. border-radius: 4px;
  253. &:hover {
  254. background-color: rgba(10, 132, 255, 0.2);
  255. }
  256. }
  257. &:hover .task-time,
  258. &:hover .task-loading {
  259. display: none;
  260. }
  261. &:hover .task-delete {
  262. display: flex;
  263. }
  264. }
  265. }
  266. @keyframes task-spin {
  267. to {
  268. transform: rotate(360deg);
  269. }
  270. }
  271. .options-area {
  272. display: flex;
  273. flex-direction: column;
  274. gap: 4px;
  275. padding-bottom: 10px;
  276. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  277. .options-area-item {
  278. display: flex;
  279. flex-direction: row;
  280. align-items: center;
  281. gap: 10px;
  282. cursor: pointer;
  283. border-radius: 6px;
  284. padding: 6px 8px;
  285. transition: all 0.3s;
  286. &:hover {
  287. background: rgba(10, 132, 255, 0.1);
  288. }
  289. .options-img {
  290. width: 32px;
  291. height: 32px;
  292. display: flex;
  293. align-items: center;
  294. justify-content: center;
  295. flex-shrink: 0;
  296. background-image: var(--img-chat-options-icon-border);
  297. background-repeat: no-repeat;
  298. background-size: 100% 100%;
  299. background-position: center;
  300. }
  301. .options-icon {
  302. width: 14px;
  303. height: 14px;
  304. background-repeat: no-repeat;
  305. background-size: 100% 100%;
  306. background-position: center;
  307. }
  308. &.options-add .options-icon {
  309. background-image: var(--img-chat-options-add-icon);
  310. }
  311. &.options-search .options-icon {
  312. background-image: var(--img-chat-options-search-icon);
  313. }
  314. &.options-skill .options-icon {
  315. background-image: var(--img-chat-options-skill-icon);
  316. }
  317. &.options-schedules .options-icon {
  318. background-image: var(--img-chat-options-schedule-icon);
  319. }
  320. &.options-sub-agents .options-icon {
  321. background-image: var(--img-chat-options-sub-agent-icon);
  322. }
  323. .zxm-btn {
  324. font-size: 14px;
  325. padding: 0;
  326. height: auto;
  327. background: transparent !important;
  328. border: none;
  329. color: #e0e6ed;
  330. box-shadow: none;
  331. }
  332. }
  333. }
  334. }
  335. </style>