Browse Source
- 新增 src/views/workbench/index.vue:6 项 KPI + 近7日趋势柱状图 + 待办与预警表 + 最新上报前5条 - 新增 src/api/analysis/home.js:overview / todo-warning / trend-latest 三个接口封装 - 平台同步失败无接口展示占位、风控处理禁用;视觉沿用 prototype-theme - 同步更新 PAGES_TODO / CURRENT_STATUS / DEVELOPMENT_LOG;提交前 eslint 与 build:prod 通过master
5 changed files with 605 additions and 1 deletions
@ -0,0 +1,16 @@ |
|||||||
|
import request from '@/utils/request' |
||||||
|
|
||||||
|
// 首页-核心指标概览:待审核数 / 本月已闭环数 / 今日新增上报数 / 今日兑换单数 / 今日发放积分数
|
||||||
|
export function getHomeOverview() { |
||||||
|
return request({ url: '/business/points/analysis/home/overview', method: 'get' }) |
||||||
|
} |
||||||
|
|
||||||
|
// 首页-待办与预警:待审核超 24h / 风控预警(集中兑换) / 申诉待处理
|
||||||
|
export function getHomeTodoWarning() { |
||||||
|
return request({ url: '/business/points/analysis/home/todo-warning', method: 'get' }) |
||||||
|
} |
||||||
|
|
||||||
|
// 首页-近 7 日趋势与最新上报:trend7d[] / latestReports[]
|
||||||
|
export function getHomeTrendLatest() { |
||||||
|
return request({ url: '/business/points/analysis/home/trend-latest', method: 'get' }) |
||||||
|
} |
||||||
@ -0,0 +1,363 @@ |
|||||||
|
<template> |
||||||
|
<div v-loading="loading" class="app-container workbench"> |
||||||
|
<!-- KPI --> |
||||||
|
<div class="kpi-grid"> |
||||||
|
<div |
||||||
|
v-for="card in kpiCards" |
||||||
|
:key="card.key" |
||||||
|
class="kpi-card" |
||||||
|
:class="{ 'is-clickable': card.clickable }" |
||||||
|
@click="handleKpi(card)" |
||||||
|
> |
||||||
|
<div class="kpi-label">{{ card.label }}</div> |
||||||
|
<div class="kpi-value" :style="{ color: card.color }">{{ card.value }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 趋势 + 待办与预警 --> |
||||||
|
<div class="two-col"> |
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-title">近 7 日上报趋势</div> |
||||||
|
<div v-if="trendList.length" ref="trendChart" class="trend-chart" /> |
||||||
|
<div v-else class="panel-empty">暂无趋势数据</div> |
||||||
|
<div class="panel-note">{{ trendRangeText }}</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-title">待办与预警</div> |
||||||
|
<el-table :data="todoRows" size="mini" :show-header="false" class="todo-table"> |
||||||
|
<el-table-column prop="label" min-width="150" /> |
||||||
|
<el-table-column width="90" align="center"> |
||||||
|
<template slot-scope="scope"> |
||||||
|
<span class="count-tag" :class="scope.row.tagClass">{{ scope.row.countText }}</span> |
||||||
|
</template> |
||||||
|
</el-table-column> |
||||||
|
<el-table-column width="96" align="right"> |
||||||
|
<template slot-scope="scope"> |
||||||
|
<el-tooltip |
||||||
|
:disabled="!scope.row.disabled" |
||||||
|
:content="scope.row.tip" |
||||||
|
placement="top" |
||||||
|
> |
||||||
|
<span> |
||||||
|
<el-button |
||||||
|
size="mini" |
||||||
|
:type="scope.row.buttonType" |
||||||
|
:disabled="scope.row.disabled" |
||||||
|
@click="handleTodoAction(scope.row)" |
||||||
|
>{{ scope.row.actionText }}</el-button> |
||||||
|
</span> |
||||||
|
</el-tooltip> |
||||||
|
</template> |
||||||
|
</el-table-column> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 最新上报 --> |
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-title">最新上报(前 5 条)</div> |
||||||
|
<el-table :data="latestReports" size="mini" class="latest-table"> |
||||||
|
<el-table-column label="单号" prop="reportNo" min-width="120" show-overflow-tooltip /> |
||||||
|
<el-table-column label="描述" prop="hazardName" min-width="200" show-overflow-tooltip /> |
||||||
|
<el-table-column label="分级" width="90" align="center"> |
||||||
|
<template slot-scope="scope"> |
||||||
|
<el-tag :type="levelTagType(scope.row)" size="small">{{ levelLabel(scope.row) }}</el-tag> |
||||||
|
</template> |
||||||
|
</el-table-column> |
||||||
|
<el-table-column label="单位/区域" min-width="150" show-overflow-tooltip> |
||||||
|
<template slot-scope="scope">{{ scope.row.factoryDeptName || '—' }}</template> |
||||||
|
</el-table-column> |
||||||
|
<el-table-column label="上报人" prop="reporterName" width="110" show-overflow-tooltip> |
||||||
|
<template slot-scope="scope">{{ scope.row.reporterName || '—' }}</template> |
||||||
|
</el-table-column> |
||||||
|
<el-table-column label="时间" width="150"> |
||||||
|
<template slot-scope="scope">{{ formatTime(scope.row.createTime) }}</template> |
||||||
|
</el-table-column> |
||||||
|
<el-table-column label="操作" width="90" align="center"> |
||||||
|
<template slot-scope="scope"> |
||||||
|
<el-button type="text" size="mini" @click="goAudit(scope.row)">审核</el-button> |
||||||
|
</template> |
||||||
|
</el-table-column> |
||||||
|
</el-table> |
||||||
|
<div v-if="!latestReports.length && !loading" class="panel-empty">暂无上报数据</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import * as echarts from 'echarts' |
||||||
|
import resize from '@/views/dashboard/mixins/resize' |
||||||
|
import { getHomeOverview, getHomeTodoWarning, getHomeTrendLatest } from '@/api/analysis/home' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'Workbench', |
||||||
|
mixins: [resize], |
||||||
|
data() { |
||||||
|
return { |
||||||
|
loading: false, |
||||||
|
chart: null, |
||||||
|
overview: {}, |
||||||
|
warning: {}, |
||||||
|
trendList: [], |
||||||
|
latestReports: [] |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
kpiCards() { |
||||||
|
return [ |
||||||
|
{ key: 'pendingAudit', label: '待审核', value: this.num(this.overview.pendingAuditCount), color: '#d64550', clickable: true, route: '/hazard/ledger' }, |
||||||
|
{ key: 'monthClosed', label: '本月已闭环', value: this.num(this.overview.monthClosedCount), color: '#2d8a4e', clickable: true, route: '/hazard/ledger' }, |
||||||
|
{ key: 'syncFail', label: '平台同步失败', value: '—', color: '#d64550', clickable: false, tip: '后端暂未提供平台同步统计接口' }, |
||||||
|
{ key: 'todayReport', label: '今日新增上报', value: this.num(this.overview.todayReportCount), color: '#1a1a2e', clickable: true, route: '/hazard/ledger' }, |
||||||
|
{ key: 'todayExchange', label: '今日兑换单', value: this.num(this.overview.todayExchangeCount), color: '#1a1a2e', clickable: false, tip: '商城运营页面开发中' }, |
||||||
|
{ key: 'todayPoints', label: '今日发放积分', value: this.plus(this.overview.todayPointsIssued), color: '#2d8a4e', clickable: true, route: '/points/management' } |
||||||
|
] |
||||||
|
}, |
||||||
|
todoRows() { |
||||||
|
return [ |
||||||
|
{ key: 'auditOver24h', label: '待审核超 24h', countText: this.countText(this.warning.pendingAuditOver24hCount), tagClass: 'tag-orange', actionText: '去审核', buttonType: 'default', disabled: false, route: '/hazard/ledger' }, |
||||||
|
{ key: 'syncFail', label: '平台同步失败', countText: '—', tagClass: 'tag-red', actionText: '重试', buttonType: 'default', disabled: true, tip: '后端暂未提供重试接口' }, |
||||||
|
{ key: 'riskWarning', label: '风控预警(集中兑换)', countText: this.countText(this.warning.riskWarningCount), tagClass: 'tag-orange', actionText: '处理', buttonType: 'default', disabled: true, tip: '风控页面开发中' }, |
||||||
|
{ key: 'appealPending', label: '申诉待处理', countText: this.countText(this.warning.pendingAppealCount), tagClass: 'tag-blue', actionText: '查看', buttonType: 'default', disabled: false, route: '/points/management' } |
||||||
|
] |
||||||
|
}, |
||||||
|
trendRangeText() { |
||||||
|
if (!this.trendList.length) return '单位:条' |
||||||
|
const first = this.trendList[0].date || '' |
||||||
|
const last = this.trendList[this.trendList.length - 1].date || '' |
||||||
|
const range = first && last ? `${this.shortDate(first)} ~ ${this.shortDate(last)} · ` : '' |
||||||
|
return `${range}单位:条` |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.loadData() |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
if (this.chart) { |
||||||
|
this.chart.dispose() |
||||||
|
this.chart = null |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
loadData() { |
||||||
|
this.loading = true |
||||||
|
Promise.all([getHomeOverview(), getHomeTodoWarning(), getHomeTrendLatest()]) |
||||||
|
.then(([overviewRes, warningRes, trendRes]) => { |
||||||
|
this.overview = (overviewRes && overviewRes.data) || {} |
||||||
|
this.warning = (warningRes && warningRes.data) || {} |
||||||
|
const trend = (trendRes && trendRes.data) || {} |
||||||
|
this.trendList = Array.isArray(trend.trend7d) ? trend.trend7d : [] |
||||||
|
this.latestReports = Array.isArray(trend.latestReports) ? trend.latestReports : [] |
||||||
|
this.$nextTick(this.renderChart) |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
this.overview = {} |
||||||
|
this.warning = {} |
||||||
|
this.trendList = [] |
||||||
|
this.latestReports = [] |
||||||
|
this.$modal.msgError('工作台数据加载失败,请检查后端服务和登录状态') |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
this.loading = false |
||||||
|
}) |
||||||
|
}, |
||||||
|
renderChart() { |
||||||
|
const el = this.$refs.trendChart |
||||||
|
if (!el) return |
||||||
|
if (!this.chart) { |
||||||
|
this.chart = echarts.init(el) |
||||||
|
} |
||||||
|
this.chart.setOption({ |
||||||
|
grid: { top: 24, left: 8, right: 12, bottom: 4, containLabel: true }, |
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }}, |
||||||
|
xAxis: { |
||||||
|
type: 'category', |
||||||
|
data: this.trendList.map(item => this.shortDate(item.date)), |
||||||
|
axisTick: { alignWithLabel: true }, |
||||||
|
axisLine: { lineStyle: { color: '#e5e7eb' }}, |
||||||
|
axisLabel: { color: '#5a5a72', fontSize: 11 } |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
type: 'value', |
||||||
|
minInterval: 1, |
||||||
|
axisTick: { show: false }, |
||||||
|
axisLine: { show: false }, |
||||||
|
splitLine: { lineStyle: { color: '#f0f0f2' }}, |
||||||
|
axisLabel: { color: '#9898ae', fontSize: 11 } |
||||||
|
}, |
||||||
|
series: [{ |
||||||
|
type: 'bar', |
||||||
|
data: this.trendList.map(item => Number(item.count || 0)), |
||||||
|
barWidth: '46%', |
||||||
|
itemStyle: { color: '#1890ff', borderRadius: [4, 4, 0, 0] }, |
||||||
|
label: { show: true, position: 'top', color: '#5a5a72', fontSize: 11 } |
||||||
|
}] |
||||||
|
}, true) |
||||||
|
this.chart.resize() |
||||||
|
}, |
||||||
|
handleKpi(card) { |
||||||
|
if (card.route) { |
||||||
|
this.$router.push(card.route) |
||||||
|
} else if (card.tip) { |
||||||
|
this.$modal.msgWarning(card.tip) |
||||||
|
} |
||||||
|
}, |
||||||
|
handleTodoAction(row) { |
||||||
|
if (row.disabled) return |
||||||
|
if (row.route) this.$router.push(row.route) |
||||||
|
}, |
||||||
|
goAudit(row) { |
||||||
|
if (row.id === undefined || row.id === null) { |
||||||
|
this.$modal.msgWarning('该记录缺少主键,无法进入审核') |
||||||
|
return |
||||||
|
} |
||||||
|
this.$router.push({ path: '/hazard/audit/detail', query: { id: String(row.id) }}) |
||||||
|
}, |
||||||
|
num(value) { |
||||||
|
return value === undefined || value === null || value === '' ? '—' : String(value) |
||||||
|
}, |
||||||
|
plus(value) { |
||||||
|
if (value === undefined || value === null || value === '') return '—' |
||||||
|
const n = Number(value) |
||||||
|
return Number.isNaN(n) ? String(value) : `${n >= 0 ? '+' : ''}${n.toLocaleString('en-US')}` |
||||||
|
}, |
||||||
|
countText(value) { |
||||||
|
if (value === undefined || value === null || value === '') return '—' |
||||||
|
return `${value} 条` |
||||||
|
}, |
||||||
|
shortDate(value) { |
||||||
|
const text = String(value || '') |
||||||
|
return text.length >= 5 ? text.slice(-5) : text |
||||||
|
}, |
||||||
|
formatTime(value) { |
||||||
|
if (!value) return '—' |
||||||
|
const text = String(value).replace('T', ' ') |
||||||
|
return text.length >= 16 ? text.slice(5, 16) : text |
||||||
|
}, |
||||||
|
levelLabel(row) { |
||||||
|
return row.hazardLevelName || ({ general: '一般', major: '较大', critical: '重大' }[String(row.hazardLevel || '').toLowerCase()] || '—') |
||||||
|
}, |
||||||
|
levelTagType(row) { |
||||||
|
const level = String(row.hazardLevel || '').toLowerCase() |
||||||
|
if (level === 'critical') return 'danger' |
||||||
|
if (level === 'major') return 'warning' |
||||||
|
return 'info' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.workbench { |
||||||
|
min-height: calc(100vh - 52px); |
||||||
|
} |
||||||
|
|
||||||
|
.kpi-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(6, 1fr); |
||||||
|
gap: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
.kpi-card { |
||||||
|
padding: 16px 18px; |
||||||
|
background: var(--prototype-panel, #fff); |
||||||
|
border: 1px solid var(--prototype-divider, #f0f0f2); |
||||||
|
border-radius: var(--prototype-radius, 8px); |
||||||
|
} |
||||||
|
|
||||||
|
.kpi-card.is-clickable { |
||||||
|
cursor: pointer; |
||||||
|
transition: border-color .2s, box-shadow .2s; |
||||||
|
} |
||||||
|
|
||||||
|
.kpi-card.is-clickable:hover { |
||||||
|
border-color: var(--prototype-primary, #1890ff); |
||||||
|
box-shadow: 0 4px 14px rgba(24, 144, 255, .12); |
||||||
|
} |
||||||
|
|
||||||
|
.kpi-label { |
||||||
|
color: var(--prototype-text-secondary, #5a5a72); |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
.kpi-value { |
||||||
|
margin-top: 10px; |
||||||
|
font-size: 26px; |
||||||
|
font-weight: 800; |
||||||
|
line-height: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.two-col { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 2fr 1fr; |
||||||
|
gap: 14px; |
||||||
|
margin-top: 14px; |
||||||
|
} |
||||||
|
|
||||||
|
.panel { |
||||||
|
margin-top: 14px; |
||||||
|
padding: 16px 18px; |
||||||
|
background: var(--prototype-panel, #fff); |
||||||
|
border: 1px solid var(--prototype-divider, #f0f0f2); |
||||||
|
border-radius: var(--prototype-radius, 8px); |
||||||
|
} |
||||||
|
|
||||||
|
.two-col .panel { |
||||||
|
margin-top: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-title { |
||||||
|
margin-bottom: 12px; |
||||||
|
color: var(--prototype-text, #1a1a2e); |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-note { |
||||||
|
margin-top: 8px; |
||||||
|
color: var(--prototype-text-muted, #9898ae); |
||||||
|
font-size: 11px; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-empty { |
||||||
|
padding: 28px 0; |
||||||
|
color: var(--prototype-text-muted, #9898ae); |
||||||
|
font-size: 12px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.trend-chart { |
||||||
|
width: 100%; |
||||||
|
height: 236px; |
||||||
|
} |
||||||
|
|
||||||
|
.count-tag { |
||||||
|
display: inline-block; |
||||||
|
padding: 1px 8px; |
||||||
|
border-radius: 8px; |
||||||
|
font-size: 11px; |
||||||
|
line-height: 18px; |
||||||
|
} |
||||||
|
|
||||||
|
.tag-orange { color: #c97e24; background: #fff4ec; } |
||||||
|
.tag-red { color: #d64550; background: #fef0f0; } |
||||||
|
.tag-blue { color: #1890ff; background: #eaf4ff; } |
||||||
|
|
||||||
|
.todo-table, |
||||||
|
.latest-table { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 1400px) { |
||||||
|
.kpi-grid { grid-template-columns: repeat(3, 1fr); } |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 1100px) { |
||||||
|
.two-col { grid-template-columns: 1fr; } |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 900px) { |
||||||
|
.kpi-grid { grid-template-columns: repeat(2, 1fr); } |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue