Files
cma-management/frontend/src/views/MyDashboard.vue
T
Hermes CI Fix 3dddd36866 init: 管理会计OS初始代码
包含前后端完整代码:
- 前端:Vue3+Vite+ElementPlus
- 后端:FastAPI+SQLAlchemy
- 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动
- 当前版本:v1.0.0
2026-05-28 17:32:22 +08:00

206 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="my-dashboard-page">
<!-- 顶部问候 -->
<div class="dash-header">
<div class="header-left">
<h3>你好{{ userName }}</h3>
<span class="header-role">{{ roleLabel }}</span>
</div>
<div class="header-right">
<span class="date-text">📅 {{ todayStr }}</span>
</div>
</div>
<div v-if="loading" class="loading-wrap">
<el-skeleton :rows="5" animated />
</div>
<template v-if="!loading">
<!-- 待办提醒 -->
<div v-if="reminders.length > 0" class="reminder-section">
<h4 class="section-title">📌 待办提醒</h4>
<div v-for="r in reminders" :key="r.type + r.related_id" class="reminder-item" :class="'sev-' + r.severity">
<span class="reminder-icon">{{ r.severity === 'danger' ? '🔴' : '🟡' }}</span>
<span class="reminder-msg">{{ r.message }}</span>
<el-button size="small" text @click="goToDetail(r)">查看详情 →</el-button>
</div>
</div>
<!-- 我的KPI -->
<div class="kpi-section">
<h4 class="section-title">📊 我的KPI</h4>
<div v-if="kpis.length === 0" class="empty-state">暂无关联到你的KPI请管理员在KPI字典中设置负责人</div>
<div v-else class="kpi-list">
<div v-for="k in kpis" :key="k.id" class="kpi-card" :class="'kpi-' + k.level" @click="$router.push('/kpis/' + k.id)">
<div class="kpi-card-left">
<span class="kpi-dot" :class="'dot-' + k.level"></span>
</div>
<div class="kpi-card-body">
<div class="kpi-card-top">
<span class="kpi-name">{{ k.kpi_name }}</span>
<span class="kpi-code">{{ k.kpi_code }}</span>
</div>
<div class="kpi-card-bar">
<el-progress
:percentage="k.target_value ? Math.min(100, Math.round((k.actual_value || 0) / k.target_value * 100)) : 0"
:status="k.level === 'green' ? 'success' : k.level === 'red' ? 'exception' : undefined"
:stroke-width="16"
:text-inside="true"
:format="() => fmtValue(k.actual_value, k.kpi_code) + ' / ' + (k.target_value ?? '-')"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 我的改善行动 -->
<div class="plans-section">
<h4 class="section-title">📋 我的改善行动</h4>
<div v-if="actionPlans.length === 0" class="empty-state">暂无分配给你的改善行动计划</div>
<div v-else class="plans-list">
<div v-for="p in actionPlans" :key="p.id" class="plan-card" :class="{ 'plan-overdue': p.overdue }">
<div class="plan-card-left">
<span class="plan-status-icon">{{ planIcon(p) }}</span>
</div>
<div class="plan-card-body">
<div class="plan-title">{{ p.title }}</div>
<div class="plan-meta">
<span class="plan-kpi" v-if="p.kpi_name">{{ p.kpi_name }}</span>
<el-tag size="small" :type="planTagType(p)" class="plan-status-tag">{{ planLabel(p) }}</el-tag>
<span class="plan-due" v-if="p.due_date" :class="{ 'overdue-text': p.overdue }">
{{ p.overdue ? '逾期' : '截止' }} {{ p.due_date.slice(0,10) }}
</span>
</div>
<el-progress v-if="p.status !== 'completed'" :percentage="p.progress || 0" :stroke-width="8" />
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue"
import { useRouter } from "vue-router"
import api from "../api/index"
const router = useRouter()
// ── 用户信息 ──
const userStr = localStorage.getItem('cma_user') || '{}'
const user = JSON.parse(userStr)
const userName = ref(user.name || user.username || '用户')
const roleLabel = ref({
ceo: '总经理', finance: '财务部', business: '业务部', it: 'IT部'
}[user.role] || user.role)
// ── 状态 ──
const loading = ref(true)
const kpis = ref<any[]>([])
const actionPlans = ref<any[]>([])
const reminders = ref<any[]>([])
const todayStr = new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' })
// ── 工具函数 ──
function fmtValue(val: any, code: string) {
if (val == null) return '-'
if (typeof val === 'number') {
if (Math.abs(val) >= 10000) return (val / 10000).toFixed(1) + '万'
return val.toLocaleString()
}
return val
}
function planIcon(p: any) {
const overdue = p.overdue
if (p.status === 'completed') return '✅'
if (overdue) return '🔴'
if (p.status === 'in_progress') return '🔄'
return '⏳'
}
function planLabel(p: any) {
if (p.overdue) return '逾期'
const labels: Record<string, string> = { pending: '待处理', in_progress: '进行中', completed: '已完成', cancelled: '已取消' }
return labels[p.status] || p.status
}
function planTagType(p: any) {
if (p.overdue) return 'danger'
if (p.status === 'completed') return 'success'
if (p.status === 'in_progress') return 'warning'
return 'info'
}
function goToDetail(r: any) {
if (r.related_type === 'kpi') router.push('/kpis/' + r.related_id)
else if (r.related_type === 'action_plan') router.push('/action-plans')
}
// ── 加载数据 ──
onMounted(async () => {
try {
const r: any = await api.get('/dashboard/my-dashboard')
kpis.value = r.kpis || []
actionPlans.value = r.action_plans || []
reminders.value = r.reminders || []
} catch (e: any) {
console.error('Failed to load my dashboard', e)
} finally {
loading.value = false
}
})
</script>
<style scoped>
.my-dashboard-page { padding: 20px; height: calc(100vh - 60px); overflow-y: auto; }
.dash-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.header-left { display: flex; align-items: center; gap: 12px; }
.header-left h3 { margin: 0; font-size: 20px; }
.header-role { color: #999; font-size: 14px; }
.header-right .date-text { color: #666; font-size: 14px; }
.loading-wrap { padding: 40px; }
.section-title { font-size: 16px; margin: 0 0 12px 0; padding-bottom: 8px; border-bottom: 1px solid #eee; }
.empty-state { text-align: center; color: #999; padding: 30px; }
.reminder-section { margin-bottom: 24px; }
.reminder-item { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 8px; margin-bottom: 6px; font-size: 14px; }
.reminder-item.sev-danger { background: #fef0f0; border: 1px solid #fde2e2; }
.reminder-item.sev-warning { background: #fdf6ec; border: 1px solid #f5dab1; }
.reminder-icon { flex-shrink: 0; }
.reminder-msg { flex: 1; }
.kpi-section { margin-bottom: 24px; }
.kpi-list { display: flex; flex-direction: column; gap: 8px; }
.kpi-card { display: flex; align-items: center; gap: 12px; padding: 14px; border-radius: 10px; cursor: pointer; transition: box-shadow 0.2s; }
.kpi-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
.kpi-green { background: #f0f9eb; border: 1px solid #e1f3d8; }
.kpi-yellow { background: #fdf6ec; border: 1px solid #f5dab1; }
.kpi-red { background: #fef0f0; border: 1px solid #fde2e2; }
.kpi-gray { background: #fafafa; border: 1px solid #eee; }
.kpi-card-left { flex-shrink: 0; }
.kpi-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot-green { background: #67c23a; }
.dot-yellow { background: #e6a23c; }
.dot-red { background: #f56c6c; }
.dot-gray { background: #dcdfe6; }
.kpi-card-body { flex: 1; min-width: 0; }
.kpi-card-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.kpi-name { font-weight: 600; font-size: 14px; }
.kpi-code { font-size: 11px; color: #999; }
.kpi-card-bar { margin-top: 4px; }
.plans-section { margin-bottom: 24px; }
.plans-list { display: flex; flex-direction: column; gap: 8px; }
.plan-card { display: flex; gap: 12px; padding: 14px; border-radius: 10px; background: #fff; border: 1px solid #eee; }
.plan-card.plan-overdue { border-left: 4px solid #f56c6c; background: #fef0f0; }
.plan-card-left { flex-shrink: 0; font-size: 18px; padding-top: 2px; }
.plan-card-body { flex: 1; min-width: 0; }
.plan-title { font-weight: 500; font-size: 14px; margin-bottom: 6px; }
.plan-meta { display: flex; align-items: center; gap: 8px; font-size: 12px; margin-bottom: 6px; }
.plan-kpi { color: #409eff; }
.plan-status-tag { font-size: 11px; }
.plan-due { color: #999; }
.plan-due.overdue-text { color: #f56c6c; font-weight: 500; }
</style>