| | |
| | | <script lang="ts" setup> |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { usePortalMenus } from '#/components/portal/use-portal-menus'; |
| | | import { getTaskTodoPage } from '#/api/bpm/task'; |
| | | |
| | | defineOptions({ name: 'DashboardTodoPanel' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | interface TodoItem { |
| | | id: string; |
| | | name: string; |
| | | createTime?: number; |
| | | } |
| | | |
| | | const loading = ref(true); |
| | | const items = ref<TodoItem[]>([]); |
| | | |
| | | /** createTime 为毫秒时间戳,格式化成易读文本 */ |
| | | function formatTime(ts?: number) { |
| | | if (!ts) return '-'; |
| | | const d = new Date(ts); |
| | | const p = (n: number) => String(n).padStart(2, '0'); |
| | | return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; |
| | | } |
| | | |
| | | const { features, goMenu } = usePortalMenus(); |
| | | const loading = ref(true), failed = ref(false); |
| | | const tasks = ref<{ id: string; name: string }[]>([]); |
| | | async function load() { |
| | | loading.value = true; |
| | | try { |
| | | const result = await getTaskTodoPage({ pageNo: 1, pageSize: 6 }); |
| | | items.value = (result.list ?? []).map((t: any) => ({ |
| | | id: String(t.id), |
| | | name: t.name ?? '未命名任务', |
| | | createTime: t.createTime, |
| | | })); |
| | | } catch { |
| | | items.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | loading.value = true; failed.value = false; |
| | | try { const result = await getTaskTodoPage({ pageNo: 1, pageSize: 6 }); tasks.value = (result.list ?? []).map((task) => ({ id: String(task.id), name: task.name || '未命名任务' })); } |
| | | catch { failed.value = true; tasks.value = []; } |
| | | finally { loading.value = false; } |
| | | } |
| | | } |
| | | |
| | | onMounted(load); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="elite-section h-full"> |
| | | <div class="elite-head"> |
| | | <h3 class="elite-title"> |
| | | <span class="elite-bar"></span> |
| | | 我的待办 |
| | | </h3> |
| | | <span class="elite-more" @click="emit('navigate', 'BpmTaskTodo')"> |
| | | 全部 |
| | | <IconifyIcon class="size-3.5" icon="lucide:chevron-right" /> |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 骨架 --> |
| | | <div v-if="loading" class="elite-empty">加载中…</div> |
| | | |
| | | <!-- 空状态 --> |
| | | <div v-else-if="items.length === 0" class="elite-empty"> |
| | | <IconifyIcon class="mb-2 size-8 opacity-40" icon="lucide:inbox" /> |
| | | <div>暂无待办任务</div> |
| | | </div> |
| | | |
| | | <!-- 列表 --> |
| | | <ul v-else class="elite-list"> |
| | | <li |
| | | v-for="item in items" |
| | | :key="item.id" |
| | | class="elite-row" |
| | | @click="emit('navigate', 'BpmTaskTodo')" |
| | | > |
| | | <span class="elite-dot"></span> |
| | | <span class="elite-row-name">{{ item.name }}</span> |
| | | <span class="elite-row-time">{{ formatTime(item.createTime) }}</span> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </template> |
| | | <template><article class="portal-todo-panel"><div class="todo-heading"><h2><IconifyIcon icon="lucide:list-todo" />我的待办</h2><button type="button" @click="goMenu(features.todo)">更多 <IconifyIcon icon="lucide:chevron-right" /></button></div><div v-if="loading" class="todo-status" role="status">正在加载待办…</div><div v-else-if="failed" class="todo-status" role="status"><span>待办加载失败</span><button type="button" @click="load">重新加载</button></div><div v-else-if="!tasks.length" class="todo-status"><IconifyIcon icon="lucide:inbox" /><span>暂无待办任务</span></div><ul v-else><li v-for="task in tasks" :key="task.id"><button type="button" @click="goMenu(features.todo)"><span>{{ task.name }}</span><IconifyIcon icon="lucide:chevron-right" /></button></li></ul></article></template> |
| | | |
| | | <style scoped> |
| | | .elite-section { |
| | | border-radius: 14px; |
| | | padding: 20px; |
| | | background: #fdfcfa; |
| | | border: 1px solid rgba(212, 175, 55, 0.22); |
| | | box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05); |
| | | } |
| | | |
| | | .dark .elite-section { |
| | | background: #101a2b; |
| | | border-color: rgba(212, 175, 55, 0.2); |
| | | box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .elite-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .elite-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | letter-spacing: 1px; |
| | | color: #0f2440; |
| | | } |
| | | |
| | | .dark .elite-title { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | .elite-bar { |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 15px; |
| | | border-radius: 2px; |
| | | background: linear-gradient(180deg, #e3c567, #b8901f); |
| | | } |
| | | |
| | | .elite-more { |
| | | display: inline-flex; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | gap: 2px; |
| | | font-size: 12px; |
| | | color: #b8901f; |
| | | transition: color 0.2s ease; |
| | | } |
| | | |
| | | .elite-more:hover { |
| | | color: #8a6a12; |
| | | } |
| | | |
| | | .elite-empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 34px 0; |
| | | font-size: 13px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-list { |
| | | margin: 0; |
| | | padding: 0; |
| | | list-style: none; |
| | | } |
| | | |
| | | .elite-row { |
| | | display: flex; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 11px 10px; |
| | | border-radius: 8px; |
| | | border-bottom: 1px solid rgba(15, 36, 64, 0.05); |
| | | transition: background-color 0.2s ease; |
| | | } |
| | | |
| | | .elite-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .elite-row:hover { |
| | | background: rgba(212, 175, 55, 0.07); |
| | | } |
| | | |
| | | .elite-dot { |
| | | width: 6px; |
| | | height: 6px; |
| | | flex-shrink: 0; |
| | | border-radius: 999px; |
| | | background: #d4af37; |
| | | } |
| | | |
| | | .elite-row-name { |
| | | overflow: hidden; |
| | | flex: 1; |
| | | font-size: 13.5px; |
| | | color: #0f2440; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dark .elite-row-name { |
| | | color: #e6e9ef; |
| | | } |
| | | |
| | | .elite-row-time { |
| | | flex-shrink: 0; |
| | | font-size: 12px; |
| | | color: #8a94a6; |
| | | } |
| | | .portal-todo-panel { padding: 18px; border: 1px solid #e1ebf5; border-radius: 6px; background: #fff; }.todo-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px; }.todo-heading h2 { display: flex; align-items: center; gap: 9px; margin: 0; color: #153a62; font-size: 17px; font-weight: 700; }.todo-heading h2 :deep(svg) { width: 19px; color: #b7242d; }.todo-heading button { display: flex; align-items: center; color: #5c7187; font-size: 11px; }.todo-status { display: flex; min-height: 230px; flex-direction: column; align-items: center; justify-content: center; gap: 12px; color: #5c7187; font-size: 13px; }.todo-status > :deep(svg) { width: 32px; height: 32px; color: #b6c9dc; }.todo-status button { min-height: 30px; padding: 4px 12px; border: 1px solid #d8e5f3; border-radius: 4px; color: #2b6094; background: #f6faff; }ul { margin: 0; padding: 0; list-style: none; }li + li { border-top: 1px solid #edf3f8; }li button { display: flex; width: 100%; min-height: 39px; align-items: center; justify-content: space-between; gap: 12px; color: #315273; font-size: 12px; text-align: left; }li span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }li :deep(svg) { width: 14px; flex: none; }button { cursor: pointer; }button:focus-visible { outline: 2px solid #2c70b2; outline-offset: 3px; } |
| | | </style> |