refactor(executor/overview): 整改对齐 manager/Workbench 风格

UI 风格:
- 5 个 KPI 卡 (从 3 个补到 5 个: + 已执行会议 + 未执行会议)
- 卡片改深色底 + 白字 (var(--brand-primary) 不用硬编码)
- 消息通知加"更多 →"链接 + 更多链接样式
- breadcrumb '首页' → '首页 / 工作台'
- notices 改用 listMyMessages API (从写死数据改为真接口, 跟 manager 一致)
- section-title 改 div (不写 h2, 跟 SKILL 一致; manager 自己也违规但本次顺手按 SKILL 写对)

业务逻辑:
- 删 page-title 隐藏 (display:none)
- loadStats 与 manager 完全一致 (简化掉无效的 executorList: true 参数)
This commit is contained in:
郭庆泰
2026-08-18 01:27:17 +08:00
parent c74c238ef0
commit ce9de7c880
+82 -41
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="page-card executor-overview"> <div class="page-card executor-overview">
<div class="breadcrumb">首页</div> <div class="breadcrumb">首页 / 工作台</div>
<h1 class="page-title" style="display:none"></h1>
<!-- 5 KPI ( manager/Workbench 风格一致) -->
<div class="stats-grid"> <div class="stats-grid">
<router-link class="stat-card" to="/executor/projects"> <router-link class="stat-card" to="/executor/projects">
<div class="stat-label">项目总数量</div> <div class="stat-label">项目总数量</div>
@@ -14,14 +14,23 @@
</router-link> </router-link>
<router-link class="stat-card" to="/executor/projects"> <router-link class="stat-card" to="/executor/projects">
<div class="stat-label">已结题数量</div> <div class="stat-label">已结题数量</div>
<div class="stat-value">{{ stats.finishedProjects }}</div> <div class="stat-value">{{ stats.completedProjects }}</div>
</router-link>
<router-link class="stat-card" to="/executor/meetings">
<div class="stat-label">已执行会议</div>
<div class="stat-value">{{ stats.executedMeetings }}</div>
</router-link>
<router-link class="stat-card" to="/executor/meetings">
<div class="stat-label">未执行会议</div>
<div class="stat-value">{{ stats.pendingMeetings }}</div>
</router-link> </router-link>
</div> </div>
<!-- 消息通知 -->
<section class="section"> <section class="section">
<h2 class="section-title">消息通知</h2> <div class="section-title">消息通知<a class="more" href="javascript:void(0)">更多 </a></div>
<ul class="notice-list"> <ul class="notice-list">
<li class="notice-item" v-for="n in notices" :key="n.title"> <li class="notice-item" v-for="n in notices" :key="n.id" :class="{ read: n.read }">
<div class="dot"></div> <div class="dot"></div>
<div class="body"> <div class="body">
<div class="title">{{ n.title }}</div> <div class="title">{{ n.title }}</div>
@@ -37,59 +46,85 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { bizList } from '@/api/public' import { bizList, listMyMessages } from '@/api/public'
const stats = ref({ totalProjects: 0, settledProjects: 0, finishedProjects: 0 }) const stats = ref({
const notices = ref([ totalProjects: 0, settledProjects: 0, completedProjects: 0,
{ title: '结算审核', text: 'ZH-2026-650 结算已通过', time: '2小时前' }, executedMeetings: 0, pendingMeetings: 0
{ title: '会议邀请', text: '您有一场新会议待确认', time: '昨天' }, })
{ title: '项目阶段', text: '智慧医院建设项目进入实施阶段', time: '2026-08-01' }, const notices = ref([])
{ title: '系统升级公告', text: '系统将于本周日 02:00-04:00 升级维护', time: '2026-07-28' }
])
async function loadStats() { async function loadStats() {
try { try {
// 项目总数量 (跟 manager 一样调普通 list, 后续若需要按 executor 隔离再换 executorList 接口)
const ps = await bizList('project', { pageNum: 1, pageSize: 1 }) const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
stats.value.totalProjects = ps.total || 0 stats.value.totalProjects = ps.total || ps.data?.total || 0
const settled = await bizList('project', { isSettled: 'Y', pageNum: 1, pageSize: 1 }) // 已结题项目
stats.value.settledProjects = settled.total || 0 const cp = await bizList('project', { pageNum: 1, pageSize: 1, isFinished: '1' })
const finished = await bizList('project', { isFinished: '1', pageNum: 1, pageSize: 1 }) stats.value.completedProjects = cp.total || cp.data?.total || 0
stats.value.finishedProjects = finished.total || 0 // 会议总数量
} catch (e) {} const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
const meetingTotal = ms.total || ms.data?.total || 0
// 已执行会议
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, status: '已执行' })
stats.value.executedMeetings = em.total || em.data?.total || 0
stats.value.pendingMeetings = Math.max(0, meetingTotal - stats.value.executedMeetings)
// 已结算项目
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
stats.value.settledProjects = sp.total || sp.data?.total || 0
} catch (e) {
console.warn('loadStats failed', e)
}
} }
onMounted(loadStats) async function loadNotices() {
try {
const r = await listMyMessages({ limit: 50 })
const list = r.rows || []
notices.value = list.map(n => ({
id: n.id,
title: n.title,
text: n.text,
time: n.time,
read: n.read
}))
} catch (e) {
console.warn('loadNotices failed', e)
}
}
onMounted(() => { loadStats(); loadNotices() })
</script> </script>
<style scoped> <style scoped>
/* 整页面板 (与 People.vue 风格一致) */ /* 整页面板 (与 People.vue 风格一致) */
.executor-overview { padding: 16px; } .executor-overview { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; } .breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-title { display: none; }
/* 统计卡片 */ /* 5 个 KPI 卡 (深色底白字, var(--brand-primary) 不用硬编码) */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; } .stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 12px; }
.stat-card { .stat-card {
background: #fff; background: var(--brand-primary);
border: 1px solid #f0f0f0; border: 1px solid var(--brand-primary);
border-radius: 8px; border-radius: 8px;
padding: 32px 24px; padding: 24px;
cursor: pointer;
text-decoration: none; text-decoration: none;
color: inherit; color: #fff;
display: block; display: flex;
text-align: center; flex-direction: column;
transition: border-color 0.2s, box-shadow 0.2s; align-items: center;
justify-content: center;
transition: opacity .15s, transform .15s;
min-height: 120px;
} }
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); } .stat-card:hover { opacity: .9; transform: translateY(-2px); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; } .stat-label { font-size: 14px; color: rgba(255, 255, 255, 0.85); margin-bottom: 16px; }
.stat-value { .stat-value {
font-size: 40px; font-size: 32px;
font-weight: 700; font-weight: 600;
line-height: 1; line-height: 1.2;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%); color: #fff;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
/* 消息通知 */ /* 消息通知 */
@@ -103,17 +138,23 @@ onMounted(loadStats)
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #1a1a1a; color: #1a1a1a;
margin: 0 0 16px; margin-bottom: 16px;
padding-left: 10px; padding-left: 10px;
border-left: 3px solid var(--brand-primary); border-left: 3px solid var(--brand-primary);
line-height: 1; line-height: 1;
display: flex;
justify-content: space-between;
} }
.notice-list { list-style: none; padding: 0; margin: 0; } .more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
.more:hover { text-decoration: underline; }
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; } .notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.notice-item:last-child { border-bottom: none; } .notice-item:last-child { border-bottom: none; }
.notice-item .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand-primary); flex-shrink: 0; } .notice-item .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand-primary); flex-shrink: 0; }
.notice-item.read .dot { background: transparent; border: 1px solid #d9d9d9; }
.notice-item .body { flex: 1; min-width: 0; } .notice-item .body { flex: 1; min-width: 0; }
.notice-item .title { font-size: 13px; color: #1a1a1a; } .notice-item .title { font-size: 13px; color: #1a1a1a; }
.notice-item.read .title { color: #8c8c8c; }
.notice-item .text { font-size: 12px; color: #595959; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; } .notice-item .text { font-size: 12px; color: #595959; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
.notice-item .time { font-size: 12px; color: #bfbfbf; flex-shrink: 0; } .notice-item .time { font-size: 12px; color: #bfbfbf; flex-shrink: 0; }
.empty { padding: 24px; text-align: center; color: #909399; } .empty { padding: 24px; text-align: center; color: #909399; }