doctor/Home.vue: 完善移动端适配 - 原只有 .cols-row → 1fr 一行规则 - 加 welcome-bar 紧凑 padding (20/24 → 14/16) + flex-wrap - 加 welcome-text h2 字号 20 → 16, p 13 → 12 - 加 section padding 16/20 → 12/14, section-title 15 → 14 - 加 breadcrumb 字号 13 → 12, .doctor-home padding 16/20 → 12 sponsor/Home.vue: 补 stats-grid-5 媒体规则 - 原只有 stats-grid-3/2 媒体规则, 漏 stats-grid-5 - 加 stats-grid-5 → 2 列 + ≤480px → 1 列 - 加整页 padding 16 → 12, breadcrumb 13 → 12 - 加 stat-card padding 24 → 16/12 + min-height 120 → 90 - 加 stat-label/Value/Desc 字号缩小 - 加 .section padding 20/24 → 14/16 executor/Overview.vue: 补 stats-grid 5 列媒体规则 - 原 5 个 KPI 卡 5 列布局在 mobile 完全挤爆, 但媒体查询里没改 grid-template-columns - 加 .stats-grid → 2 列 + ≤480px → 1 列 - 加 stat-card padding/min-height 调整 - 加 stat-label/Value 字号缩小 - 加 .section padding 20/24 → 14/16 admin/Workbench.vue: 已三档断点 (1200/768/480), 无需调整 manager/Workbench.vue: 已加 5 → 2 列布局, 无需调整
164 lines
6.6 KiB
Vue
164 lines
6.6 KiB
Vue
<template>
|
|
<!--
|
|
原型: 工作台.html
|
|
SIDEBAR: ['项目管理', '项目策划方案管理', '会议管理', '专家审核']
|
|
筛选区 (top 165-205, 按 left): 项目策划方案管理 / 项目总数量 / 已结算的会议 / 已结题的项目
|
|
列头 (top 285-335, 按 left): 工作台 / 会议管理
|
|
-->
|
|
<div class="page-card sponsor-home">
|
|
<div class="breadcrumb">首页</div>
|
|
|
|
<!-- KPI: 5 个数字面板, 1 行展示 (跳转 + 自动应用筛选, 字段名跟 Projects.vue / Meetings.vue q 一致) -->
|
|
<div class="stats-grid stats-grid-5">
|
|
<router-link class="stat-card" to="/sponsor/projects">
|
|
<div class="stat-label">项目总数量</div>
|
|
<div class="stat-value">{{ stats.totalProjects }}</div>
|
|
<div class="stat-desc">查看全部项目</div>
|
|
</router-link>
|
|
<router-link class="stat-card" to="/sponsor/projects?isSettled=Y">
|
|
<div class="stat-label">已结算的项目</div>
|
|
<div class="stat-value">{{ stats.settledProjects }}</div>
|
|
<div class="stat-desc">查看已结算项目</div>
|
|
</router-link>
|
|
<router-link class="stat-card" to="/sponsor/projects?isFinished=1">
|
|
<div class="stat-label">已结题的项目</div>
|
|
<div class="stat-value">{{ stats.completedProjects }}</div>
|
|
<div class="stat-desc">查看已结题项目</div>
|
|
</router-link>
|
|
<router-link class="stat-card" to="/sponsor/meetings?currentStage=RUNNING">
|
|
<div class="stat-label">已执行会议</div>
|
|
<div class="stat-value">{{ stats.executedMeetings }}</div>
|
|
<div class="stat-desc">查看已执行会议</div>
|
|
</router-link>
|
|
<router-link class="stat-card" to="/sponsor/meetings?currentStage=NOT_STARTED">
|
|
<div class="stat-label">未执行会议</div>
|
|
<div class="stat-value">{{ stats.pendingMeetings }}</div>
|
|
<div class="stat-desc">查看未执行会议</div>
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
|
<section class="section">
|
|
<h2 class="section-title">消息通知<router-link class="more" to="/sponsor/messages">更多 →</router-link></h2>
|
|
<NoticeList :limit="50" />
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { bizList } from '@/api/public'
|
|
import NoticeList from '@/components/NoticeList.vue'
|
|
|
|
// 字段名跟 Projects.vue / Meetings.vue q 一致, 之前 3/5 卡 stats 不更新或用错字段名 (status 后端不认, settledMeetings 误指项目级 isSettled)
|
|
const stats = ref({
|
|
totalProjects: 0,
|
|
settledProjects: 0, // 项目级 isSettled=Y
|
|
completedProjects: 0, // 项目级 isFinished=1
|
|
executedMeetings: 0, // 会议 currentStage=RUNNING (字段名跟 Meetings.vue q 一致, 之前误用 status= 后端不认, 数字一直是错的)
|
|
pendingMeetings: 0 // 总会议 - 已执行
|
|
})
|
|
|
|
async function loadStats() {
|
|
try {
|
|
// 项目总数量 (跟 manager/executor 一样调普通 list, 后续若需要按 sponsor 隔离再换 sponsorList 接口)
|
|
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
|
stats.value.totalProjects = ps.total || ps.data?.total || 0
|
|
// 已结算项目
|
|
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
|
|
stats.value.settledProjects = sp.total || sp.data?.total || 0
|
|
// 已结题项目
|
|
const cp = await bizList('project', { pageNum: 1, pageSize: 1, isFinished: '1' })
|
|
stats.value.completedProjects = cp.total || cp.data?.total || 0
|
|
// 会议总数量
|
|
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
|
|
const meetingTotal = ms.total || ms.data?.total || 0
|
|
// 已执行会议 = currentStage='RUNNING' (阶段已过开始时间, 执行方未提交)
|
|
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStage: 'RUNNING' })
|
|
stats.value.executedMeetings = em.total || em.data?.total || 0
|
|
stats.value.pendingMeetings = Math.max(0, meetingTotal - stats.value.executedMeetings)
|
|
} catch (e) {
|
|
console.warn('loadStats failed', e)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadStats()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 整页面板 (与 People.vue 风格一致) */
|
|
.sponsor-home { padding: 16px; }
|
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
|
|
|
.stats-grid { display: grid; gap: 16px; margin-bottom: 16px; }
|
|
.stats-grid-5 { grid-template-columns: repeat(5, 1fr); }
|
|
.stats-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
|
.stats-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
|
|
|
.stat-card {
|
|
background: var(--brand-primary);
|
|
border: 1px solid var(--brand-primary);
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
color: #fff;
|
|
display: block;
|
|
transition: opacity .15s, transform .15s, box-shadow .2s;
|
|
box-shadow: 0 2px 8px rgba(30, 58, 138, 0.15);
|
|
}
|
|
.stat-card:hover { opacity: .92; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(30, 58, 138, 0.25); }
|
|
.stat-label { font-size: 14px; color: rgba(255, 255, 255, 0.85); margin-bottom: 16px; }
|
|
.stat-value {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
margin-bottom: 8px;
|
|
color: #fff;
|
|
}
|
|
.stat-desc { font-size: 12px; color: rgba(255, 255, 255, 0.7); }
|
|
|
|
.section {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 20px 24px;
|
|
border: 1px solid #f0f0f0;
|
|
}
|
|
.section-title {
|
|
font-size: 15px; font-weight: 600; color: #1a1a1a;
|
|
margin-bottom: 16px; padding-left: 10px;
|
|
border-left: 3px solid var(--brand-primary);
|
|
line-height: 1;
|
|
display: flex; justify-content: space-between;
|
|
}
|
|
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; cursor: pointer; }
|
|
.more:hover { text-decoration: underline; }
|
|
|
|
@media (max-width: 768px) {
|
|
/* 5/3/2 列面板: 全部 ≤768px 改为 2 列布局 */
|
|
.stats-grid-5 { grid-template-columns: repeat(2, 1fr) !important; }
|
|
.stats-grid-3 { grid-template-columns: repeat(2, 1fr); }
|
|
.stats-grid-2 { grid-template-columns: 1fr; }
|
|
|
|
/* 整页 padding 收窄 */
|
|
.sponsor-home { padding: 12px !important; }
|
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
|
|
|
/* 卡片 padding 收窄 + 字号缩小 */
|
|
.stat-card { padding: 16px 12px !important; min-height: 90px; }
|
|
.stat-label { font-size: 12px !important; margin-bottom: 8px !important; }
|
|
.stat-value { font-size: 24px !important; margin-bottom: 4px !important; }
|
|
.stat-desc { font-size: 11px !important; }
|
|
|
|
/* 消息通知 */
|
|
.section { padding: 14px 16px !important; }
|
|
.section-title { font-size: 14px !important; margin-bottom: 12px !important; }
|
|
}
|
|
|
|
/* ≤480px 极窄屏: 5 卡 → 1 列 */
|
|
@media (max-width: 480px) {
|
|
.stats-grid-5 { grid-template-columns: 1fr !important; }
|
|
}
|
|
</style> |