包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
206 lines
8.5 KiB
Vue
206 lines
8.5 KiB
Vue
<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>
|