CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
785 lines
22 KiB
Vue
785 lines
22 KiB
Vue
<template>
|
||
<div class="dashboard-page">
|
||
<section class="dashboard-welcome">
|
||
<div>
|
||
<h1>你好,{{ operatorName }}</h1>
|
||
<p>用专业的服务,帮助更多人遇见对的人。今天也是传递幸福的一天!</p>
|
||
</div>
|
||
<div class="dashboard-date">
|
||
<span>{{ today }}</span>
|
||
<small>丹阳 · 公益服务</small>
|
||
</div>
|
||
</section>
|
||
|
||
<div v-if="loading" class="dashboard-loading" aria-label="工作台加载中">
|
||
<el-skeleton v-for="index in 6" :key="index" animated>
|
||
<template #template><el-skeleton-item variant="rect" class="metric-skeleton" /></template>
|
||
</el-skeleton>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<div v-if="fatalError" class="dashboard-error" role="alert">
|
||
<el-icon><Warning /></el-icon>
|
||
<span><strong>工作台数据加载失败</strong><small>{{ fatalError }}</small></span>
|
||
<el-button type="primary" plain @click="retryLoad">重新加载</el-button>
|
||
</div>
|
||
<div v-else-if="isForbidden" class="dashboard-forbidden">
|
||
<el-icon><Lock /></el-icon>
|
||
<span><strong>当前账号暂无统计权限</strong><small>仅展示您职责范围内可访问的业务入口。</small></span>
|
||
</div>
|
||
<div v-else-if="dashboard.notice" class="dashboard-notice">
|
||
<el-icon><InfoFilled /></el-icon><span>{{ dashboard.notice }}</span>
|
||
</div>
|
||
|
||
<section class="dashboard-metrics" aria-label="工作台核心指标">
|
||
<button
|
||
v-for="metric in dashboard.metrics"
|
||
:key="metric.key"
|
||
type="button"
|
||
class="metric-card"
|
||
:class="[`tone-${metric.tone}`, { unavailable: !metric.available }]"
|
||
@click="goTo(metric.route)"
|
||
>
|
||
<span class="metric-icon"><el-icon><component :is="metricIcon(metric.key)" /></el-icon></span>
|
||
<span class="metric-copy">
|
||
<small>{{ metric.label }}</small>
|
||
<strong>{{ metric.available ? formatNumber(metric.value) : '—' }}</strong>
|
||
<em>{{ metric.trendText }}</em>
|
||
</span>
|
||
<el-icon class="metric-arrow"><ArrowRight /></el-icon>
|
||
</button>
|
||
</section>
|
||
|
||
<section class="dashboard-grid dashboard-grid-top">
|
||
<article class="dashboard-panel todo-panel">
|
||
<header class="panel-header">
|
||
<div><h2>今日待办事项 <span>{{ todoCount }}</span></h2></div>
|
||
<button type="button" @click="goTo('/review')">查看更多 <el-icon><ArrowRight /></el-icon></button>
|
||
</header>
|
||
<div v-if="dashboard.todos.length" class="todo-list">
|
||
<button
|
||
v-for="item in dashboard.todos"
|
||
:key="item.key"
|
||
type="button"
|
||
class="todo-row"
|
||
@click="goTo(item.route)"
|
||
>
|
||
<span class="todo-icon" :class="`tone-${item.tone}`"><el-icon><component :is="todoIcon(item.key)" /></el-icon></span>
|
||
<span class="todo-copy"><strong>{{ item.title }}</strong><small>{{ item.description }}</small></span>
|
||
<b>{{ item.count }}</b>
|
||
<em>{{ item.actionText }}</em>
|
||
</button>
|
||
</div>
|
||
<div v-else class="dashboard-empty"><el-icon><CircleCheck /></el-icon><span>当前没有授权范围内的待办事项</span></div>
|
||
</article>
|
||
|
||
<article class="dashboard-panel activity-panel">
|
||
<header class="panel-header">
|
||
<div><h2>近期公益活动</h2></div>
|
||
<button type="button" @click="goTo('/activities/interests')">查看更多 <el-icon><ArrowRight /></el-icon></button>
|
||
</header>
|
||
<div v-if="dashboard.activities.length" class="activity-list">
|
||
<button
|
||
v-for="activity in dashboard.activities"
|
||
:key="activity.id"
|
||
type="button"
|
||
class="activity-row"
|
||
@click="goTo('/activities/interests')"
|
||
>
|
||
<img
|
||
:src="activity.coverUrl || activityFallback"
|
||
:alt="activity.title"
|
||
@error="useActivityFallback"
|
||
/>
|
||
<span class="activity-copy">
|
||
<strong>{{ activity.title }}</strong>
|
||
<small>{{ formatDateTime(activity.startAt) }} · {{ activity.locationName }}</small>
|
||
</span>
|
||
<span class="activity-status">{{ statusLabel(activity.statusText) }}</span>
|
||
<em>{{ activityRegistration(activity) }}</em>
|
||
</button>
|
||
</div>
|
||
<div v-else class="dashboard-empty">
|
||
<el-icon><Calendar /></el-icon>
|
||
<span>{{ dashboard.activitiesAvailable ? '近期暂无公益活动' : '当前账号无活动列表权限' }}</span>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<article v-if="dashboard.followups.length" class="dashboard-panel">
|
||
<header class="panel-header"><div><h2>最新红娘跟进记录</h2></div></header>
|
||
<div class="todo-list">
|
||
<button v-for="item in dashboard.followups" :key="item.serviceId" type="button" class="todo-row"
|
||
@click="goTo(`/matchmaker?serviceId=${encodeURIComponent(item.serviceId)}`)">
|
||
<span class="todo-copy"><strong>{{ item.userDisplayNumber }}</strong><small>{{ item.content }}</small></span>
|
||
<em>{{ formatDateTime(item.createdAt) }} · 查看服务</em>
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<section class="dashboard-grid dashboard-grid-bottom">
|
||
<article class="dashboard-panel service-overview-panel">
|
||
<header class="panel-header"><div><h2>服务数据概览</h2></div><span>当前累计</span></header>
|
||
<div v-if="dashboard.serviceOverview.length" class="overview-grid">
|
||
<div v-for="item in dashboard.serviceOverview" :key="item.key" :class="`tone-${item.tone}`">
|
||
<small>{{ item.label }}</small>
|
||
<strong>{{ formatNumber(item.value) }}</strong>
|
||
<em>{{ item.compareText }}</em>
|
||
</div>
|
||
</div>
|
||
<div v-else class="dashboard-empty"><el-icon><DataAnalysis /></el-icon><span>暂无可查看的服务统计</span></div>
|
||
</article>
|
||
|
||
<article class="dashboard-panel effectiveness-panel">
|
||
<header class="panel-header"><div><h2>项目服务成效</h2></div><span>实时数据</span></header>
|
||
<div v-if="dashboard.effectiveness.length" class="effectiveness-list">
|
||
<div v-for="item in dashboard.effectiveness" :key="item.key" class="effectiveness-row">
|
||
<span>{{ item.label }}</span>
|
||
<div><i :style="{ width: `${item.max > 0 ? Math.min(100, item.value / item.max * 100) : 0}%` }"></i></div>
|
||
<strong>{{ formatNumber(item.value) }} {{ item.unit }}</strong>
|
||
</div>
|
||
</div>
|
||
<div v-else class="dashboard-empty"><el-icon><TrendCharts /></el-icon><span>暂无可查看的成效数据</span></div>
|
||
</article>
|
||
</section>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onActivated, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
ArrowRight,
|
||
Calendar,
|
||
ChatDotRound,
|
||
CircleCheck,
|
||
DataAnalysis,
|
||
DocumentChecked,
|
||
House,
|
||
InfoFilled,
|
||
Link,
|
||
Lock,
|
||
StarFilled,
|
||
TrendCharts,
|
||
UserFilled,
|
||
Warning,
|
||
} from '@element-plus/icons-vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { createEmptyDashboard, loadDashboard } from '@/services/dashboard'
|
||
import { BRAND_ACTIVITY_FALLBACK } from '@/brand'
|
||
import type { DashboardActivity, DashboardModel } from '@/types/dashboard'
|
||
|
||
const router = useRouter()
|
||
const authStore = useAuthStore()
|
||
const activityFallback = BRAND_ACTIVITY_FALLBACK
|
||
const dashboard = ref<DashboardModel>(createEmptyDashboard())
|
||
const loading = ref(true)
|
||
const fatalError = ref('')
|
||
|
||
const permissions = computed(() => authStore.permissions || [])
|
||
const operatorName = computed(() => authStore.operator?.name || '管理员')
|
||
const isPreviewData = computed(() => authStore.token === 'ui-preview-only')
|
||
const hasAnyData = computed(() => dashboard.value.metrics.some((item) => item.available) || dashboard.value.activitiesAvailable)
|
||
const isForbidden = computed(() => !hasAnyData.value && !fatalError.value && !isPreviewData.value)
|
||
const todoCount = computed(() => dashboard.value.todos.reduce((total, item) => total + item.count, 0))
|
||
const today = new Intl.DateTimeFormat('zh-CN', {
|
||
timeZone: 'Asia/Shanghai',
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
weekday: 'long',
|
||
}).format(new Date())
|
||
|
||
const metricIcons: Record<string, unknown> = {
|
||
pending_reviews: DocumentChecked,
|
||
matchmaker_todos: StarFilled,
|
||
counseling_todos: ChatDotRound,
|
||
station_appointments: House,
|
||
effective_matches: Link,
|
||
activity_registrations: UserFilled,
|
||
}
|
||
|
||
const todoIcons: Record<string, unknown> = {
|
||
review: DocumentChecked,
|
||
counseling: ChatDotRound,
|
||
appointment: Calendar,
|
||
risk: Warning,
|
||
}
|
||
|
||
function metricIcon(key: string) {
|
||
return metricIcons[key] || DataAnalysis
|
||
}
|
||
|
||
function todoIcon(key: string) {
|
||
return todoIcons[key] || CircleCheck
|
||
}
|
||
|
||
function formatNumber(value: number) {
|
||
return new Intl.NumberFormat('zh-CN').format(value)
|
||
}
|
||
|
||
function formatDateTime(value: string) {
|
||
if (!value) return '时间待确认'
|
||
return new Intl.DateTimeFormat('zh-CN', {
|
||
timeZone: 'Asia/Shanghai',
|
||
month: 'numeric',
|
||
day: 'numeric',
|
||
weekday: 'short',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
}).format(new Date(value))
|
||
}
|
||
|
||
function statusLabel(status: string) {
|
||
const labels: Record<string, string> = {
|
||
draft: '草稿',
|
||
registration_open: '报名中',
|
||
published: '报名中',
|
||
ongoing: '进行中',
|
||
completed: '已结束',
|
||
cancelled: '已取消',
|
||
}
|
||
return labels[status] || status
|
||
}
|
||
|
||
function activityRegistration(activity: DashboardActivity) {
|
||
if (activity.registered == null) return activity.capacity == null ? '报名数据未提供' : `名额 ${activity.capacity}`
|
||
return activity.capacity == null ? `已报名 ${activity.registered}` : `已报名 ${activity.registered}/${activity.capacity}`
|
||
}
|
||
|
||
function useActivityFallback(event: Event) {
|
||
const image = event.currentTarget as HTMLImageElement
|
||
if (!image.src.endsWith(activityFallback)) image.src = activityFallback
|
||
}
|
||
|
||
function canAccess(path: string) {
|
||
const matched = router.resolve(path).matched
|
||
const required = matched.flatMap((route) => (route.meta.permissions as string[] | undefined) || [])
|
||
return required.every((permission) => permissions.value.includes(permission))
|
||
}
|
||
|
||
function goTo(path: string) {
|
||
if (!path) return
|
||
if (!canAccess(path)) {
|
||
ElMessage.info('当前账号无权访问该模块')
|
||
return
|
||
}
|
||
router.push(path)
|
||
}
|
||
|
||
async function retryLoad() {
|
||
loading.value = true
|
||
fatalError.value = ''
|
||
try {
|
||
if (authStore.token === 'ui-preview-only') {
|
||
dashboard.value = createEmptyDashboard('界面预览模式不展示虚构业务数字')
|
||
return
|
||
}
|
||
dashboard.value = await loadDashboard(permissions.value)
|
||
if (!hasAnyData.value && dashboard.value.notice.includes('加载失败')) {
|
||
fatalError.value = dashboard.value.notice
|
||
}
|
||
} catch (error) {
|
||
fatalError.value = error instanceof Error ? error.message : '服务暂时不可用,请稍后重试'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onActivated(retryLoad)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dashboard-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
min-height: 100%;
|
||
max-width: var(--admin-content-max-width);
|
||
margin: 0 auto;
|
||
padding: var(--space-5) 26px 28px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.dashboard-welcome {
|
||
display: flex;
|
||
min-height: 66px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: var(--space-5);
|
||
}
|
||
|
||
.dashboard-welcome h1 {
|
||
font-size: var(--font-size-3xl);
|
||
line-height: var(--font-line-snug);
|
||
}
|
||
|
||
.dashboard-welcome p {
|
||
margin-top: 6px;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.dashboard-date {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-xs);
|
||
}
|
||
|
||
.dashboard-date small {
|
||
padding: 6px 10px;
|
||
color: var(--admin-coral-strong);
|
||
background: var(--admin-coral-soft);
|
||
border-radius: var(--border-radius-pill);
|
||
font-size: var(--font-size-micro);
|
||
}
|
||
|
||
.dashboard-loading,
|
||
.dashboard-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.dashboard-loading {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.metric-skeleton {
|
||
height: 105px;
|
||
border-radius: var(--border-radius-md);
|
||
}
|
||
|
||
.dashboard-error,
|
||
.dashboard-forbidden,
|
||
.dashboard-notice {
|
||
display: flex;
|
||
min-height: 48px;
|
||
margin: var(--space-2) 0 var(--space-3);
|
||
padding: 10px 14px;
|
||
align-items: center;
|
||
gap: 10px;
|
||
color: var(--color-danger-strong);
|
||
background: var(--color-danger-soft);
|
||
border: 1px solid var(--color-border-danger);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.dashboard-error > span,
|
||
.dashboard-forbidden > span {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.dashboard-error small,
|
||
.dashboard-forbidden small {
|
||
margin-top: 3px;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-micro);
|
||
}
|
||
|
||
.dashboard-metrics {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.metric-card {
|
||
position: relative;
|
||
display: grid;
|
||
min-width: 0;
|
||
min-height: 108px;
|
||
padding: 14px var(--space-3);
|
||
grid-template-columns: 38px 1fr;
|
||
align-items: center;
|
||
gap: 9px;
|
||
overflow: hidden;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
background: var(--color-bg-primary);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--border-radius-md);
|
||
transition: transform var(--motion-normal) var(--ease-standard),
|
||
box-shadow var(--motion-normal) var(--ease-standard);
|
||
}
|
||
|
||
.metric-card:hover {
|
||
box-shadow: var(--shadow-raise);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.metric-card:active {
|
||
transform: translateY(0) scale(var(--press-scale));
|
||
}
|
||
|
||
.metric-card.unavailable {
|
||
cursor: default;
|
||
opacity: 0.72;
|
||
}
|
||
|
||
/* 图标字形属「有意义的非文本内容」,按 WCAG 需 ≥3:1。
|
||
原先用鲜亮色调压同色系浅底(amber 仅 2.2:1),统一改用 -ink 档(≥4.6:1)。 */
|
||
.metric-icon,
|
||
.todo-icon {
|
||
display: grid;
|
||
color: var(--tone-ink);
|
||
background: var(--tone-soft);
|
||
border-radius: 50%;
|
||
place-items: center;
|
||
}
|
||
|
||
.metric-icon {
|
||
width: 38px;
|
||
height: 38px;
|
||
font-size: var(--font-size-xl);
|
||
}
|
||
|
||
.metric-copy {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.metric-copy small {
|
||
overflow: hidden;
|
||
color: var(--color-text-secondary);
|
||
font-size: var(--font-size-xs);
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.metric-copy strong {
|
||
margin-top: 3px;
|
||
color: var(--color-text-primary);
|
||
font-size: var(--font-size-3xl);
|
||
line-height: 1;
|
||
}
|
||
|
||
/* 原为 9px(低于可读下限),提到 11px */
|
||
.metric-copy em {
|
||
margin-top: 6px;
|
||
overflow: hidden;
|
||
color: var(--tone-ink);
|
||
font-size: var(--font-size-micro);
|
||
font-style: normal;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.metric-arrow {
|
||
position: absolute;
|
||
right: var(--space-2);
|
||
bottom: var(--space-2);
|
||
color: var(--tone-ink);
|
||
font-size: var(--font-size-micro);
|
||
}
|
||
|
||
/* 色调组改为引用 theme/index.css 的 --tone-*-ink / --tone-*-soft。
|
||
--tone-ink 是本组件内的局部变量(由下面各 tone-* 类赋值),供图标与文字统一取用。 */
|
||
.tone-rose { --tone-ink: var(--tone-rose-ink); --tone-soft: var(--tone-rose-soft); }
|
||
.tone-amber { --tone-ink: var(--tone-amber-ink); --tone-soft: var(--tone-amber-soft); }
|
||
.tone-blue { --tone-ink: var(--tone-blue-ink); --tone-soft: var(--tone-blue-soft); }
|
||
.tone-green { --tone-ink: var(--tone-green-ink); --tone-soft: var(--tone-green-soft); }
|
||
.tone-purple { --tone-ink: var(--tone-purple-ink); --tone-soft: var(--tone-purple-soft); }
|
||
.tone-red { --tone-ink: var(--tone-red-ink); --tone-soft: var(--tone-red-soft); }
|
||
|
||
.dashboard-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 14px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.dashboard-grid-top {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.dashboard-grid-bottom {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.dashboard-grid-bottom .dashboard-panel {
|
||
height: 100%;
|
||
}
|
||
|
||
.dashboard-panel {
|
||
min-width: 0;
|
||
padding: 14px var(--space-4);
|
||
background: var(--color-bg-primary);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--admin-card-radius);
|
||
box-shadow: var(--shadow-panel);
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
min-height: 30px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--color-border-hairline);
|
||
}
|
||
|
||
.panel-header h2 {
|
||
padding-bottom: 10px;
|
||
font-size: var(--font-size-lg);
|
||
}
|
||
|
||
.panel-header h2 span {
|
||
display: inline-grid;
|
||
min-width: var(--space-5);
|
||
height: var(--space-5);
|
||
margin-left: 6px;
|
||
padding: 0 5px;
|
||
color: var(--color-text-inverse);
|
||
font-size: var(--font-size-micro);
|
||
line-height: var(--space-5);
|
||
background: var(--admin-coral-solid);
|
||
border-radius: var(--border-radius-pill);
|
||
place-items: center;
|
||
}
|
||
|
||
.panel-header button {
|
||
display: flex;
|
||
min-height: var(--tap-control);
|
||
align-items: center;
|
||
gap: var(--space-1);
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-xs);
|
||
cursor: pointer;
|
||
background: transparent;
|
||
border: 0;
|
||
border-radius: var(--border-radius-xs);
|
||
transition: color var(--motion-fast) var(--ease-standard);
|
||
}
|
||
|
||
.panel-header button:hover {
|
||
color: var(--admin-coral-strong);
|
||
}
|
||
|
||
.panel-header > span {
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-micro);
|
||
}
|
||
|
||
.todo-list,
|
||
.activity-list {
|
||
min-height: 190px;
|
||
}
|
||
|
||
.todo-row,
|
||
.activity-row {
|
||
display: flex;
|
||
width: 100%;
|
||
min-height: 47px;
|
||
padding: 6px 5px;
|
||
align-items: center;
|
||
gap: 9px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
background: transparent;
|
||
border: 0;
|
||
border-bottom: 1px solid var(--color-border-hairline);
|
||
transition: background var(--motion-fast) var(--ease-standard);
|
||
}
|
||
|
||
.todo-row:hover,
|
||
.activity-row:hover {
|
||
background: var(--color-bg-secondary);
|
||
}
|
||
|
||
.todo-icon {
|
||
width: 29px;
|
||
height: 29px;
|
||
flex: none;
|
||
}
|
||
|
||
.todo-copy,
|
||
.activity-copy {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.todo-copy strong,
|
||
.activity-copy strong {
|
||
overflow: hidden;
|
||
color: var(--color-text-primary);
|
||
font-size: var(--font-size-xs);
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.todo-copy small,
|
||
.activity-copy small {
|
||
margin-top: 3px;
|
||
overflow: hidden;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-micro);
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.todo-row > b {
|
||
min-width: 29px;
|
||
color: var(--admin-coral-strong);
|
||
font-size: var(--font-size-sm);
|
||
text-align: center;
|
||
}
|
||
|
||
.todo-row > em {
|
||
color: var(--admin-coral-strong);
|
||
font-size: var(--font-size-micro);
|
||
font-style: normal;
|
||
}
|
||
|
||
.activity-row img {
|
||
width: 72px;
|
||
height: 38px;
|
||
flex: none;
|
||
object-fit: cover;
|
||
border-radius: var(--border-radius-xs);
|
||
}
|
||
|
||
.activity-status {
|
||
padding: 3px 6px;
|
||
color: var(--color-success-strong);
|
||
font-size: var(--font-size-micro);
|
||
background: var(--color-success-soft);
|
||
border-radius: var(--border-radius-pill);
|
||
}
|
||
|
||
.activity-row > em {
|
||
min-width: 76px;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-micro);
|
||
font-style: normal;
|
||
text-align: right;
|
||
}
|
||
|
||
.dashboard-empty {
|
||
display: flex;
|
||
min-height: 160px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-xs);
|
||
}
|
||
|
||
.dashboard-empty .el-icon {
|
||
color: var(--color-icon-muted);
|
||
font-size: var(--font-size-5xl);
|
||
}
|
||
|
||
.overview-grid {
|
||
display: grid;
|
||
min-height: 118px;
|
||
padding-top: var(--space-3);
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.overview-grid > div {
|
||
display: flex;
|
||
padding: var(--space-3) 10px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
background: var(--tone-soft);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.overview-grid small {
|
||
color: var(--tone-ink);
|
||
font-size: var(--font-size-micro);
|
||
}
|
||
|
||
.overview-grid strong {
|
||
margin-top: 7px;
|
||
font-size: var(--font-size-3xl);
|
||
}
|
||
|
||
.overview-grid em {
|
||
margin-top: 7px;
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-micro);
|
||
font-style: normal;
|
||
}
|
||
|
||
.effectiveness-list {
|
||
min-height: 118px;
|
||
padding-top: 9px;
|
||
}
|
||
|
||
.effectiveness-row {
|
||
display: grid;
|
||
min-height: 23px;
|
||
grid-template-columns: 92px 1fr 76px;
|
||
align-items: center;
|
||
gap: 9px;
|
||
font-size: var(--font-size-micro);
|
||
}
|
||
|
||
.effectiveness-row > span {
|
||
color: var(--color-text-tertiary);
|
||
}
|
||
|
||
/* 轨道与填充的对比原为 2.53:1(条几乎看不出进度),改后 4.01:1 */
|
||
.effectiveness-row > div {
|
||
height: var(--space-2);
|
||
overflow: hidden;
|
||
background: var(--color-primary-light);
|
||
border-radius: var(--border-radius-pill);
|
||
}
|
||
|
||
.effectiveness-row i {
|
||
display: block;
|
||
height: 100%;
|
||
background: var(--gradient-brand-bar);
|
||
border-radius: inherit;
|
||
}
|
||
|
||
.effectiveness-row strong {
|
||
color: var(--color-text-tertiary);
|
||
font-size: var(--font-size-micro);
|
||
font-weight: var(--font-weight-medium);
|
||
text-align: right;
|
||
}
|
||
|
||
@media (max-width: 1450px), (max-height: 820px) {
|
||
.dashboard-page { padding: 12px 18px 20px; }
|
||
.dashboard-welcome { min-height: 54px; }
|
||
.dashboard-welcome h1 { font-size: var(--font-size-2xl); }
|
||
.dashboard-welcome p { margin-top: 3px; font-size: var(--font-size-xs); }
|
||
.dashboard-metrics { margin-top: 6px; gap: 7px; }
|
||
.metric-card { min-height: 90px; padding: 10px var(--space-2); grid-template-columns: var(--space-8) 1fr; gap: 6px; }
|
||
.metric-icon { width: var(--space-8); height: var(--space-8); font-size: var(--font-size-lg); }
|
||
.metric-copy strong { font-size: var(--font-size-2xl); }
|
||
.dashboard-grid { margin-top: 9px; gap: 9px; }
|
||
.dashboard-panel { padding: 10px var(--space-3); }
|
||
.todo-list, .activity-list { min-height: 152px; }
|
||
.todo-row, .activity-row { min-height: 38px; padding: var(--space-1); }
|
||
.activity-row img { width: 58px; height: 31px; }
|
||
.dashboard-grid-bottom .dashboard-panel { min-height: 150px; }
|
||
.overview-grid, .effectiveness-list { min-height: 98px; }
|
||
.overview-grid > div { padding: var(--space-2) 6px; }
|
||
.overview-grid strong { font-size: var(--font-size-2xl); }
|
||
}
|
||
|
||
@media (max-width: 1180px) {
|
||
.dashboard-loading,
|
||
.dashboard-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||
}
|
||
|
||
@media (max-width: 880px) {
|
||
.dashboard-grid { grid-template-columns: 1fr; }
|
||
.dashboard-date { display: none; }
|
||
}
|
||
</style>
|