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 @@
@@ -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 @@
@@ -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