feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<div style="padding:0 16px;">
|
||||
<!-- 顶部标题+操作 -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h3 style="margin:0;">行动方案库</h3>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<el-input v-model="searchKeyword" placeholder="搜索标题/KPI名称/编码" clearable style="width:240px;" @clear="loadData" @keyup.enter="loadData" />
|
||||
<el-button type="primary" @click="openCreate">+ 新建行动方案</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态统计卡片 -->
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;">
|
||||
<div v-for="card in statCards" :key="card.key"
|
||||
:class="['stat-card', { active: filterStatus === card.key }]"
|
||||
@click="filterStatus = card.key; page = 1; loadData()"
|
||||
style="cursor:pointer;min-width:100px;padding:14px 20px;border-radius:8px;border:2px solid transparent;transition:all .2s;background:#f5f7fa;text-align:center;">
|
||||
<div style="font-size:12px;color:#909399;">{{ card.label }}</div>
|
||||
<div style="font-size:24px;font-weight:600;margin-top:4px;" :style="{ color: card.color }">{{ stats[card.key] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center;">
|
||||
<el-select v-model="filterDimension" placeholder="维度" clearable style="width:100px;" @change="page=1;loadData()">
|
||||
<el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filterKpi" placeholder="关联KPI" clearable filterable style="width:180px;" @change="page=1;loadData()">
|
||||
<el-option v-for="k in kpiOptions" :key="k.id" :label="`${k.kpi_code} ${k.kpi_name}`" :value="k.id" />
|
||||
</el-select>
|
||||
<el-select v-model="filterAssignee" placeholder="负责人" clearable filterable style="width:120px;" @change="page=1;loadData()">
|
||||
<el-option v-for="u in userOptions" :key="u" :label="u" :value="u" />
|
||||
</el-select>
|
||||
<el-select v-model="filterPriority" placeholder="优先级" clearable style="width:100px;" @change="page=1;loadData()">
|
||||
<el-option label="高" value="high" />
|
||||
<el-option label="中" value="medium" />
|
||||
<el-option label="低" value="low" />
|
||||
</el-select>
|
||||
<el-button v-if="hasFilter" size="small" @click="clearFilter">清除筛选</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table :data="list" v-loading="loading" border stripe size="small" style="width:100%;" @selection-change="(sel: any[]) => selectedRows = sel">
|
||||
<el-table-column type="selection" width="40" />
|
||||
<el-table-column label="行动方案" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<span style="font-weight:500;cursor:pointer;color:#409eff;" @click="viewDetail(row)">{{ row.title }}</span>
|
||||
<div v-if="row.target_value" style="font-size:11px;color:#67c23a;margin-top:2px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="font-size:10px;border:1px solid #67c23a;border-radius:2px;padding:0 3px;line-height:14px;">S</span>
|
||||
{{ row.target_value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联KPI" width="180">
|
||||
<template #default="{ row }">
|
||||
<div style="font-size:12px;line-height:1.4;">
|
||||
<span style="color:#666;">{{ row.kpi_code }}</span><br>
|
||||
<span>{{ row.kpi_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="维度" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="dimTagType(row.kpi_dimension)">{{ dimLabel(row.kpi_dimension) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" width="90" prop="assignee" />
|
||||
<el-table-column label="优先级" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.priority === 'high' ? 'danger' : row.priority === 'low' ? 'info' : 'warning'">
|
||||
{{ row.priority === 'high' ? '高' : row.priority === 'low' ? '低' : '中' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<el-progress :percentage="row.progress || 0" :stroke-width="8" style="flex:1;" />
|
||||
<span style="font-size:11px;color:#999;min-width:28px;">{{ row.progress || 0 }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="截止日" width="100" prop="due_date">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!row.due_date" style="color:#ccc;">—</span>
|
||||
<span v-else :style="{ color: isOverdue(row) ? '#f56c6c' : '#666', fontWeight: isOverdue(row) ? 600 : 400 }">
|
||||
{{ row.due_date?.slice(0, 10) }}
|
||||
<el-tooltip v-if="isOverdue(row)" content="已逾期" placement="top"><span style="margin-left:2px;">⚠️</span></el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="editPlan(row)">编辑</el-button>
|
||||
<el-button size="small" link type="danger" @click="deletePlan(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 批量操作栏 -->
|
||||
<div v-if="selectedRows.length > 0" style="margin-top:8px;display:flex;gap:8px;align-items:center;padding:8px 12px;background:#f0f9ff;border-radius:6px;">
|
||||
<span style="font-size:13px;color:#409eff;">已选 {{ selectedRows.length }} 项</span>
|
||||
<el-button size="small" @click="batchSetStatus('in_progress')">置为进行中</el-button>
|
||||
<el-button size="small" type="success" @click="batchSetStatus('completed')">置为已完成</el-button>
|
||||
<el-button size="small" @click="clearSelected">取消选择</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="display:flex;justify-content:center;margin-top:16px;">
|
||||
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev, pager, next, total" @current-change="loadData" background small />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<MyDialog v-model="showForm" :title="formTitle" :width="550">
|
||||
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<el-tag size="small" type="success">S 具体的</el-tag>
|
||||
<el-tag size="small" type="warning">M 可衡量</el-tag>
|
||||
<el-tag size="small">A 可实现</el-tag>
|
||||
<el-tag size="small" type="primary">R 相关(已关联KPI)</el-tag>
|
||||
<el-tag size="small" type="danger">T 有时限(截止日)</el-tag>
|
||||
</div>
|
||||
<el-form :model="form" label-width="80px" size="small">
|
||||
<el-form-item label="行动标题" required>
|
||||
<el-input v-model="form.title" placeholder="请输入行动方案标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标值" help="SMART:具体、可衡量的成功标准">
|
||||
<div style="display:flex;gap:4px;flex-direction:column;width:100%;">
|
||||
<el-input v-model="form.target_value" placeholder="例:满意度从85%提升至92%、不良率降低50%以下" />
|
||||
<span style="font-size:11px;color:#999;line-height:1.4;">设置可量化的成功标准,完成后对照核验</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联KPI" required>
|
||||
<el-select v-model="form.kpi_id" placeholder="选择关联KPI" filterable style="width:100%;" :teleported="false">
|
||||
<el-option v-for="k in kpiOptions" :key="k.id" :label="`${k.kpi_code} ${k.kpi_name}`" :value="k.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="详细描述(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人">
|
||||
<el-select v-model="form.assignee" placeholder="选择负责人" filterable allow-create style="width:100%;" :teleported="false">
|
||||
<el-option v-for="u in userOptions" :key="u" :label="u" :value="u" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级">
|
||||
<el-radio-group v-model="form.priority">
|
||||
<el-radio value="high">高</el-radio>
|
||||
<el-radio value="medium">中</el-radio>
|
||||
<el-radio value="low">低</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="截止日期">
|
||||
<el-date-picker v-model="form.due_date" type="date" placeholder="选择截止日期" style="width:100%;" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editingId" label="状态">
|
||||
<el-select v-model="form.status" style="width:100%;" :teleported="false">
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="进行中" value="in_progress" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editingId" label="进度 %">
|
||||
<el-slider v-model="form.progress" :min="0" :max="100" :step="5" show-input style="width:100%;" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editingId && form.status === 'completed'" label="改善结果">
|
||||
<el-input v-model="form.result" type="textarea" :rows="3" placeholder="描述改善结果,对照目标值核验" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showForm = false">取消</el-button>
|
||||
<el-button type="primary" @click="savePlan" :loading="saving">{{ editingId ? '保存修改' : '创建' }}</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<MyDialog v-model="showDetail" :title="detailTitle" :width="600">
|
||||
<div v-if="detailPlan">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:12px;color:#999;">关联KPI</div>
|
||||
<div style="margin-top:2px;">{{ detailPlan.kpi_code }} {{ detailPlan.kpi_name }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:#999;">维度</div>
|
||||
<div style="margin-top:2px;">
|
||||
<el-tag size="small" :type="dimTagType(detailPlan.kpi_dimension)">{{ dimLabel(detailPlan.kpi_dimension) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:#999;">负责人</div>
|
||||
<div style="margin-top:2px;">{{ detailPlan.assignee || '未指定' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:#999;">优先级</div>
|
||||
<div style="margin-top:2px;">
|
||||
<el-tag size="small" :type="detailPlan.priority === 'high' ? 'danger' : detailPlan.priority === 'low' ? 'info' : 'warning'">
|
||||
{{ detailPlan.priority === 'high' ? '高' : detailPlan.priority === 'low' ? '低' : '中' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:#999;">截止日期</div>
|
||||
<div style="margin-top:2px;" :class="{ 'overdue-text': isOverdue(detailPlan) }">
|
||||
{{ detailPlan.due_date?.slice(0, 10) || '未设置' }}
|
||||
<span v-if="isOverdue(detailPlan)" style="color:#f56c6c;font-size:12px;margin-left:4px;">已逾期</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:#999;">状态</div>
|
||||
<div style="margin-top:2px;">
|
||||
<el-tag size="small" :type="statusTagType(detailPlan.status)">{{ statusLabel(detailPlan.status) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:12px;">
|
||||
<div style="font-size:12px;color:#999;">进度</div>
|
||||
<el-progress :percentage="detailPlan.progress || 0" :stroke-width="12" style="margin-top:4px;" />
|
||||
</div>
|
||||
<!-- 目标值 -->
|
||||
<div v-if="detailPlan.target_value" style="margin-top:12px;padding:10px 14px;background:#f0f9eb;border-radius:6px;border-left:3px solid #67c23a;">
|
||||
<div style="font-size:12px;color:#67c23a;font-weight:500;display:flex;align-items:center;gap:4px;">
|
||||
<span style="font-size:10px;border:1px solid #67c23a;border-radius:2px;padding:0 3px;line-height:14px;">SMART</span>
|
||||
目标值 / 成功标准
|
||||
</div>
|
||||
<div style="margin-top:4px;line-height:1.6;font-size:14px;">{{ detailPlan.target_value }}</div>
|
||||
</div>
|
||||
<div v-if="detailPlan.description" style="margin-top:12px;">
|
||||
<div style="font-size:12px;color:#999;">详细描述</div>
|
||||
<div style="margin-top:4px;line-height:1.6;background:#f9f9f9;padding:8px 12px;border-radius:4px;">{{ detailPlan.description }}</div>
|
||||
</div>
|
||||
<div v-if="detailPlan.result" style="margin-top:12px;">
|
||||
<div style="font-size:12px;color:#999;">改善结果</div>
|
||||
<div style="margin-top:4px;line-height:1.6;background:#f0f9eb;padding:8px 12px;border-radius:4px;">{{ detailPlan.result }}</div>
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:12px;color:#ccc;display:flex;gap:16px;">
|
||||
<span>创建人: {{ detailPlan.created_by }}</span>
|
||||
<span>创建时间: {{ detailPlan.created_at?.slice(0, 16) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showDetail = false">关闭</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { actionPlanApi, kpiApi, userApi } from '../api/index'
|
||||
import MyDialog from '../components/MyDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// ── 数据 ──
|
||||
const list = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const stats = ref<Record<string, number>>({})
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
const kpiOptions = ref<any[]>([])
|
||||
const userOptions = ref<string[]>([])
|
||||
|
||||
// ── 筛选 ──
|
||||
const filterStatus = ref('')
|
||||
const filterDimension = ref('')
|
||||
const filterKpi = ref<number | null>(null)
|
||||
const filterAssignee = ref('')
|
||||
const filterPriority = ref('')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const dimensions = [
|
||||
{ value: 'finance', label: '财务' },
|
||||
{ value: 'customer', label: '客户' },
|
||||
{ value: 'process', label: '内部流程' },
|
||||
{ value: 'learning', label: '学习成长' },
|
||||
]
|
||||
|
||||
const hasFilter = computed(() =>
|
||||
filterStatus.value || filterDimension.value || filterKpi.value ||
|
||||
filterAssignee.value || filterPriority.value || searchKeyword.value
|
||||
)
|
||||
|
||||
function clearFilter() {
|
||||
filterStatus.value = ''
|
||||
filterDimension.value = ''
|
||||
filterKpi.value = null
|
||||
filterAssignee.value = ''
|
||||
filterPriority.value = ''
|
||||
searchKeyword.value = ''
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function clearSelected() {
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
// ── 统计卡片 ──
|
||||
const statCards = [
|
||||
{ key: '', label: '全部', color: '#409eff' },
|
||||
{ key: 'pending', label: '待处理', color: '#909399' },
|
||||
{ key: 'in_progress', label: '进行中', color: '#e6a23c' },
|
||||
{ key: 'completed', label: '已完成', color: '#67c23a' },
|
||||
{ key: 'overdue', label: '已逾期', color: '#f56c6c' },
|
||||
]
|
||||
|
||||
// ── 加载数据 ──
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = { page: page.value, page_size: pageSize.value }
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterDimension.value) params.dimension = filterDimension.value
|
||||
if (filterKpi.value) params.kpi_id = filterKpi.value
|
||||
if (filterAssignee.value) params.assignee = filterAssignee.value
|
||||
if (filterPriority.value) params.priority = filterPriority.value
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value
|
||||
|
||||
const r: any = await actionPlanApi.list(params)
|
||||
const d = r.data || []
|
||||
list.value = Array.isArray(d) ? d : []
|
||||
total.value = r.total || list.value.length
|
||||
} catch { ElMessage.error('加载行动方案失败') }
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const r: any = await actionPlanApi.stats()
|
||||
stats.value = r
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadKpis() {
|
||||
try {
|
||||
const r: any = await kpiApi.list({ page_size: 500 })
|
||||
const d = r.data || r || []
|
||||
kpiOptions.value = Array.isArray(d) ? d : []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const r: any = await userApi.list()
|
||||
const d = r.data || []
|
||||
if (Array.isArray(d)) {
|
||||
userOptions.value = d.map((u: any) => u.name || u.username).filter(Boolean)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ── 新建/编辑 ──
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const editingId = ref<number | null>(null)
|
||||
const saving = ref(false)
|
||||
const form = ref({
|
||||
title: '',
|
||||
target_value: '',
|
||||
kpi_id: null as number | null,
|
||||
description: '',
|
||||
assignee: '',
|
||||
priority: 'medium',
|
||||
due_date: null as string | null,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
result: '',
|
||||
})
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
formTitle.value = '新建行动方案'
|
||||
form.value = { title: '', target_value: '', kpi_id: null, description: '', assignee: '', priority: 'medium', due_date: null, status: 'pending', progress: 0, result: '' }
|
||||
// 如果路由携带 kpi_id 参数,自动填入
|
||||
if (route.query.kpi_id) {
|
||||
form.value.kpi_id = Number(route.query.kpi_id)
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function editPlan(row: any) {
|
||||
editingId.value = row.id
|
||||
formTitle.value = '编辑行动方案'
|
||||
form.value = {
|
||||
title: row.title,
|
||||
target_value: row.target_value || '',
|
||||
kpi_id: row.kpi_id,
|
||||
description: row.description || '',
|
||||
assignee: row.assignee || '',
|
||||
priority: row.priority || 'medium',
|
||||
due_date: row.due_date?.slice(0, 10) || null,
|
||||
status: row.status || 'pending',
|
||||
progress: row.progress || 0,
|
||||
result: row.result || '',
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
async function savePlan() {
|
||||
if (!form.value.title || !form.value.kpi_id) {
|
||||
ElMessage.warning('请填写标题并选择关联KPI')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const data = { ...form.value }
|
||||
if (editingId.value) {
|
||||
await actionPlanApi.update(editingId.value, data)
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await actionPlanApi.create(data)
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
loadStats()
|
||||
} catch { ElMessage.error('保存失败') }
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
// ── 删除 ──
|
||||
async function deletePlan(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除此行动方案?')
|
||||
await actionPlanApi.delete(id)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
loadStats()
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
|
||||
}
|
||||
|
||||
// ── 批量操作 ──
|
||||
async function batchSetStatus(status: string) {
|
||||
const ids = selectedRows.value.map(r => r.id)
|
||||
if (ids.length === 0) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定将选中的 ${ids.length} 项置为「${status === 'completed' ? '已完成' : '进行中'}」?`)
|
||||
for (const id of ids) {
|
||||
await actionPlanApi.update(id, { status })
|
||||
}
|
||||
ElMessage.success(`已更新 ${ids.length} 项`)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
loadStats()
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error('批量操作失败') }
|
||||
}
|
||||
|
||||
// ── 详情 ──
|
||||
const showDetail = ref(false)
|
||||
const detailTitle = ref('')
|
||||
const detailPlan = ref<any>(null)
|
||||
|
||||
function viewDetail(row: any) {
|
||||
detailPlan.value = row
|
||||
detailTitle.value = row.title
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
// ── 辅助函数 ──
|
||||
function isOverdue(row: any) {
|
||||
if (!row.due_date) return false
|
||||
if (row.status === 'completed' || row.status === 'cancelled') return false
|
||||
return new Date(row.due_date.slice(0, 10)) < new Date(new Date().toLocaleDateString())
|
||||
}
|
||||
|
||||
function dimTagType(dim: string) {
|
||||
const map: Record<string, string> = { finance: 'primary', customer: 'success', process: 'warning', learning: 'danger' }
|
||||
return map[dim] || 'info'
|
||||
}
|
||||
|
||||
function dimLabel(dim: string) {
|
||||
const map: Record<string, string> = { finance: '财务', customer: '客户', process: '内部流程', learning: '学习成长' }
|
||||
return map[dim] || dim || '-'
|
||||
}
|
||||
|
||||
function statusTagType(status: string) {
|
||||
const map: Record<string, string> = { pending: 'info', in_progress: 'warning', completed: 'success', cancelled: 'info' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const map: Record<string, string> = { pending: '待处理', in_progress: '进行中', completed: '已完成', cancelled: '已取消' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
// ── 初始化 ──
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadData()
|
||||
loadKpis()
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card:hover {
|
||||
border-color: #dcdfe6;
|
||||
background: #fff;
|
||||
}
|
||||
.stat-card.active {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
.overdue-text {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user