Files
zyt/admin/src/views/workbench/index.vue
T
2026-08-11 17:39:41 +08:00

1246 lines
42 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="workbench-page">
<header class="wb-hero">
<div class="wb-hero-text">
<h1 class="wb-hero-title">数据工作台</h1>
<p class="wb-hero-desc">医助诊单、订单营收与运营趋势一屏掌握</p>
</div>
<el-button type="primary" :icon="Refresh" round class="wb-refresh-btn" @click="refreshAll">
刷新全部
</el-button>
</header>
<!-- 订单统计 -->
<section class="wb-section wb-section--order">
<div class="wb-section-head">
<div class="wb-section-title">
<span class="wb-section-icon wb-section-icon--violet">
<el-icon><Histogram /></el-icon>
</span>
<div>
<div class="wb-section-name">订单统计</div>
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '—' }}</div>
</div>
</div>
<div class="wb-toolbar">
<el-select v-model="orderStatsType" class="wb-select wb-select--wide" @change="loadOrderStats">
<el-option label="全部(已支付)" :value="-1" />
<el-option label="挂号费" :value="1" />
<el-option label="问诊费" :value="2" />
<el-option label="药品费用" :value="3" />
<el-option label="首付费用" :value="4" />
<el-option label="尾款费用" :value="5" />
<el-option label="其他费用" :value="6" />
<el-option label="全部费用" :value="7" />
<el-option label="驼奶费用" :value="8" />
<el-option label="退款统计" :value="0" />
</el-select>
<el-select v-model="orderStatsDays" class="wb-select" @change="loadOrderStats">
<el-option label="今天" :value="0" />
<el-option label="最近7天" :value="7" />
<el-option label="最近30天" :value="30" />
</el-select>
<el-button :icon="Refresh" circle @click="loadOrderStats" />
</div>
</div>
<div class="wb-section-body">
<el-row :gutter="16" class="wb-kpi-row">
<el-col :xs="24" :sm="8">
<div class="wb-kpi wb-kpi--violet">
<div class="wb-kpi-label">今日单数</div>
<div class="wb-kpi-value">{{ orderStatsData.today_total ?? 0 }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="8">
<div class="wb-kpi wb-kpi--amber">
<div class="wb-kpi-label">今日金额</div>
<div class="wb-kpi-value wb-kpi-value--sm">¥{{ orderStatsData.today_amount ?? '0.00' }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="8">
<div class="wb-rank-strip wb-rank-strip--compact">
<span class="wb-rank-strip-label">今日 TOP</span>
<div v-if="orderStatsData.today_ranking?.length" class="wb-rank-chips">
<span
v-for="(a, idx) in orderStatsData.today_ranking.slice(0, 4)"
:key="a.id"
class="wb-rank-chip wb-rank-chip--dense"
>
<em>{{ Number(idx) + 1 }}</em>
{{ a.name }}
<strong>{{ a.count }}</strong>
<small>¥{{ a.amount }}</small>
</span>
</div>
<span v-else class="wb-rank-empty">暂无</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="wb-chart-row">
<el-col :xs="24" :lg="10">
<div class="wb-chart-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">部门单数占比</span>
</div>
<v-charts
v-if="orderDeptCountPieHasData"
class="wb-chart"
style="height: 300px"
:option="orderDeptCountPieOption"
:autoresize="true"
/>
<el-empty v-else class="wb-chart-empty" description="暂无数据" />
</div>
</el-col>
<el-col :xs="24" :lg="14">
<div class="wb-chart-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">开单人 · 单数排行</span>
<span class="wb-chart-hint">柱状 + 趋势</span>
</div>
<v-charts
v-if="orderStatsData.chart?.xAxis?.length"
:key="'order-bar-' + orderStatsType"
class="wb-chart"
style="height: 300px"
:option="orderChartOption"
:autoresize="true"
/>
<el-empty v-else class="wb-chart-empty" description="暂无图表数据" />
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="wb-chart-row">
<el-col :xs="24" :lg="10">
<div class="wb-chart-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">部门金额占比</span>
</div>
<v-charts
v-if="orderDeptAmountPieHasData"
class="wb-chart"
style="height: 300px"
:option="orderDeptAmountPieOption"
:autoresize="true"
/>
<el-empty v-else class="wb-chart-empty" description="暂无数据" />
</div>
</el-col>
<el-col :xs="24" :lg="14">
<div class="wb-chart-card wb-ranking-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">周期内排行</span>
<span class="wb-chart-hint">单数 / 金额</span>
</div>
<div v-if="orderStatsData.ranking?.length" class="wb-ranking-list">
<div
v-for="(a, idx) in orderStatsData.ranking"
:key="a.id"
class="wb-ranking-item"
>
<span class="wb-ranking-rank">{{ Number(idx) + 1 }}</span>
<span class="wb-ranking-name">{{ a.name }}</span>
<span class="wb-ranking-meta">
<b>{{ a.count }}</b> 单
<span class="wb-ranking-money">¥{{ a.amount }}</span>
</span>
</div>
</div>
<el-empty v-else description="暂无排行" />
</div>
</el-col>
</el-row>
<div v-if="orderStatsData.departments?.length" class="wb-dept-block">
<div class="wb-dept-title">按部门统计</div>
<el-row :gutter="16">
<el-col
v-for="dept in orderStatsData.departments"
:key="dept.dept_id"
:xs="24"
:sm="12"
:lg="8"
>
<div class="wb-dept-card wb-dept-card--order">
<div class="wb-dept-card-head">
{{ dept.dept_name }}
<span class="wb-dept-total">{{ dept.total }} 单 · ¥{{ dept.amount }}</span>
</div>
<div class="wb-dept-list">
<div
v-for="m in dept.members"
:key="`${dept.dept_id}-${m.id}`"
class="wb-dept-row"
>
<span>{{ m.name }}</span>
<span class="wb-dept-num">{{ m.count }} <small>¥{{ m.amount }}</small></span>
</div>
<div v-if="!dept.members?.length" class="wb-dept-empty">暂无</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</section>
<!-- 医助理诊单 -->
<section class="wb-section wb-section--diagnosis">
<div class="wb-section-head">
<div class="wb-section-title">
<span class="wb-section-icon wb-section-icon--blue">
<el-icon><DataAnalysis /></el-icon>
</span>
<div>
<div class="wb-section-name">医助理诊单统计</div>
<div class="wb-section-sub">
{{ assistantStats.date_range?.[0] }} 至 {{ assistantStats.date_range?.[1] }}
</div>
</div>
</div>
<div class="wb-toolbar">
<el-select v-model="assistantStats.days" class="wb-select" @change="loadAssistantStats">
<el-option label="今天" :value="0" />
<el-option label="最近7天" :value="7" />
<el-option label="最近14天" :value="14" />
<el-option label="最近30天" :value="30" />
</el-select>
<el-button :icon="Refresh" circle @click="loadAssistantStats" />
</div>
</div>
<div class="wb-section-body">
<el-row :gutter="16" class="wb-kpi-row">
<el-col :xs="24" :sm="8" :lg="6">
<div class="wb-kpi wb-kpi--blue">
<div class="wb-kpi-label">今日创建诊单</div>
<div class="wb-kpi-value">{{ assistantStats.today_total ?? 0 }}</div>
<div class="wb-kpi-hint">当日新增总量</div>
</div>
</el-col>
<el-col :xs="24" :sm="16" :lg="18">
<div class="wb-rank-strip">
<span class="wb-rank-strip-label">今日排行</span>
<div v-if="assistantStats.today_ranking?.length" class="wb-rank-chips">
<span
v-for="(a, idx) in assistantStats.today_ranking.slice(0, 6)"
:key="a.id"
class="wb-rank-chip"
>
<em>{{ idx + 1 }}</em>
{{ a.name }}
<strong>{{ a.count }}</strong>
</span>
</div>
<span v-else class="wb-rank-empty">暂无排行数据</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="wb-chart-row">
<el-col :xs="24" :lg="15">
<div class="wb-chart-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">诊单量分布</span>
<span class="wb-chart-hint">柱状 + 趋势线</span>
</div>
<v-charts
v-if="assistantStats.chart.xAxis?.length"
ref="assistantChart"
class="wb-chart"
style="height: 340px"
:option="assistantChartOption"
:autoresize="true"
/>
<el-empty v-else class="wb-chart-empty" description="暂无图表数据" />
</div>
</el-col>
<el-col :xs="24" :lg="9">
<div class="wb-chart-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">部门占比</span>
<span class="wb-chart-hint">按诊单数</span>
</div>
<v-charts
v-if="assistantDeptPieHasData"
class="wb-chart"
style="height: 340px"
:option="assistantDeptPieOption"
:autoresize="true"
/>
<el-empty v-else class="wb-chart-empty" description="暂无部门数据" />
</div>
</el-col>
</el-row>
<div v-if="assistantStats.departments?.length" class="wb-dept-block">
<div class="wb-dept-title">按部门明细</div>
<el-row :gutter="16">
<el-col
v-for="dept in assistantStats.departments"
:key="dept.dept_id"
:xs="24"
:sm="12"
:lg="8"
>
<div class="wb-dept-card">
<div class="wb-dept-card-head">
{{ dept.dept_name }}
<span class="wb-dept-total">共 {{ dept.total }}</span>
</div>
<div class="wb-dept-list">
<div
v-for="a in dept.assistants"
:key="`${dept.dept_id}-${a.id}`"
class="wb-dept-row"
>
<span>{{ a.name }}</span>
<span class="wb-dept-num">{{ a.count }}</span>
</div>
<div v-if="!dept.assistants?.length" class="wb-dept-empty">暂无医助</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</section>
<!-- 访问 & 销售趋势 -->
<section class="wb-section wb-section--trend">
<div class="wb-section-head">
<div class="wb-section-title">
<span class="wb-section-icon wb-section-icon--teal">
<el-icon><TrendCharts /></el-icon>
</span>
<div>
<div class="wb-section-name">运营趋势</div>
<div class="wb-section-sub">访问量与销售额走势</div>
</div>
</div>
</div>
<div class="wb-section-body wb-trend-body">
<el-row :gutter="20">
<el-col :xs="24" :lg="14">
<div class="wb-chart-card wb-trend-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">访问量趋势</span>
</div>
<v-charts
ref="visitorChart"
class="wb-chart"
style="height: 320px"
:option="workbenchData.visitorOption"
:autoresize="true"
/>
</div>
</el-col>
<el-col :xs="24" :lg="10">
<div class="wb-chart-card wb-trend-card">
<div class="wb-chart-card-head">
<span class="wb-chart-title">销售额趋势</span>
<span class="wb-chart-hint">单位(万)</span>
</div>
<v-charts
ref="saleChart"
class="wb-chart"
style="height: 320px"
:option="workbenchData.saleOption"
:autoresize="true"
/>
</div>
</el-col>
</el-row>
</div>
</section>
</div>
</template>
<script lang="ts" setup name="workbench">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import vCharts from 'vue-echarts'
import { DataAnalysis, Histogram, Refresh, TrendCharts } from '@element-plus/icons-vue'
import { getWorkbench } from '@/api/app'
import { assistantDiagnosisStats } from '@/api/tcm'
import { orderStats } from '@/api/order'
import useSettingStore from '@/stores/modules/setting'
import { useComponentRef } from '@/utils/getExposeType'
import { calcColor } from '@/utils/util'
const settingStore = useSettingStore()
const saleChart = useComponentRef(vCharts)
const visitorChart = useComponentRef(vCharts)
const assistantChart = useComponentRef(vCharts)
const pieColors = ['#5b7cfa', '#36cfc9', '#ffc53d', '#ff7a45', '#b37feb', '#597ef7', '#73d13d', '#ff85c0']
function parseAmount(v: unknown): number {
if (v == null) return 0
const n = parseFloat(String(v).replace(/,/g, ''))
return Number.isFinite(n) ? n : 0
}
const assistantStats = reactive<{
days: number
date_range?: [string, string]
today_total?: number
today_ranking?: { id: number; name: string; count: number }[]
departments: any[]
ranking: { id: number; name: string; count: number }[]
chart: { xAxis: string[]; series: any[] }
}>({
days: 0,
departments: [],
today_ranking: [],
ranking: [],
chart: { xAxis: [], series: [] }
})
const assistantDeptPieHasData = computed(() =>
(assistantStats.departments || []).some((d: any) => Number(d.total) > 0)
)
const assistantDeptPieOption = computed(() => {
const data = (assistantStats.departments || [])
.filter((d: any) => Number(d.total) > 0)
.map((d: any) => ({ name: d.dept_name || '—', value: Number(d.total) }))
return {
color: pieColors,
tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 单 ({d}%)' },
legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 11 } },
series: [
{
name: '诊单',
type: 'pie',
radius: ['46%', '72%'],
center: ['50%', '46%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { formatter: '{b}\n{c}' },
data
}
]
}
})
const assistantChartOption = computed(() => {
const theme = settingStore.theme
const barSeries = (assistantStats.chart.series || []).map((s: any) => ({
...s,
name: s.name || '诊单数',
type: 'bar',
barWidth: '46%',
itemStyle: {
borderRadius: [8, 8, 0, 0],
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: calcColor(theme, 0.75) },
{ offset: 1, color: theme }
]
}
}
}))
const lineData = (barSeries[0] as any)?.data || []
return {
color: [theme, '#f0b429'],
legend: { data: ['诊单数', '趋势'], top: 4 },
xAxis: {
type: 'category',
data: assistantStats.chart.xAxis || [],
axisLabel: { rotate: 28, fontSize: 11 },
axisLine: { lineStyle: { color: '#e4e7ec' } }
},
yAxis: {
type: 'value',
name: '诊单数',
splitLine: { lineStyle: { type: 'dashed', color: '#eef0f4' } }
},
tooltip: { trigger: 'axis' },
grid: { left: '2%', right: '2%', bottom: '12%', top: 48, containLabel: true },
series: [
...barSeries,
{
name: '趋势',
type: 'line',
smooth: 0.35,
symbol: 'circle',
symbolSize: 7,
lineStyle: { width: 3, color: '#f0b429' },
itemStyle: { color: '#f0b429' },
data: lineData
}
]
}
})
const loadAssistantStats = () => {
assistantDiagnosisStats({ days: assistantStats.days })
.then((res: any) => {
assistantStats.date_range = res.date_range
assistantStats.today_total = res.today_total ?? 0
assistantStats.today_ranking = res.today_ranking || []
assistantStats.departments = res.departments || []
assistantStats.ranking = res.ranking || []
assistantStats.chart = res.chart || { xAxis: [], series: [] }
})
.catch(() => {})
}
const orderStatsType = ref(-1)
const orderStatsDays = ref(0)
const orderStatsData = reactive<any>({
order_type_name: '',
date_range: [] as string[],
today_total: 0,
today_amount: '0.00',
today_ranking: [],
departments: [],
ranking: [],
chart: { xAxis: [], series: [] }
})
const orderDeptCountPieHasData = computed(() =>
(orderStatsData.departments || []).some((d: any) => Number(d.total) > 0)
)
const orderDeptAmountPieHasData = computed(() =>
(orderStatsData.departments || []).some((d: any) => parseAmount(d.amount) > 0)
)
const orderDeptCountPieOption = computed(() => {
const data = (orderStatsData.departments || [])
.filter((d: any) => Number(d.total) > 0)
.map((d: any) => ({ name: d.dept_name || '—', value: Number(d.total) }))
return {
color: pieColors,
tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 单 ({d}%)' },
legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 11 } },
series: [
{
name: '单数',
type: 'pie',
radius: ['44%', '70%'],
center: ['50%', '46%'],
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { formatter: '{b}\n{c}' },
data
}
]
}
})
const orderDeptAmountPieOption = computed(() => {
const data = (orderStatsData.departments || [])
.map((d: any) => ({ name: d.dept_name || '—', value: parseAmount(d.amount) }))
.filter((d) => d.value > 0)
return {
color: pieColors,
tooltip: {
trigger: 'item',
formatter: (p: any) => {
const v = p.value
return `${p.name}<br/>¥${Number(v).toFixed(2)} (${p.percent}%)`
}
},
legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 11 } },
series: [
{
name: '金额',
type: 'pie',
radius: ['44%', '70%'],
center: ['50%', '46%'],
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { formatter: (p: any) => `${p.name}\n¥${Number(p.value).toFixed(0)}` },
data
}
]
}
})
const orderStatsDateRangeText = computed(() => {
const arr = orderStatsData.date_range
if (arr?.length >= 2) return `${arr[0]} 至 ${arr[1]}`
const days = orderStatsDays.value
const end = new Date()
const start = new Date()
if (days === 0) {
start.setHours(0, 0, 0, 0)
return `${formatDate(start)} 至 ${formatDate(end)}`
}
start.setDate(start.getDate() - (days || 7))
return `${formatDate(start)} 至 ${formatDate(end)}`
})
function formatDate(d: Date) {
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
}
const orderChartOption = computed(() => {
const theme = settingStore.theme
const barSeries = (orderStatsData.chart?.series || []).map((s: any) => ({
...s,
name: s.name || '单数',
type: 'bar',
barWidth: '46%',
itemStyle: {
borderRadius: [8, 8, 0, 0],
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: calcColor(theme, 0.72) },
{ offset: 1, color: theme }
]
}
}
}))
const lineData = (barSeries[0] as any)?.data || []
return {
color: [theme, '#fa8c16'],
legend: { data: ['单数', '趋势'], top: 4 },
xAxis: {
type: 'category',
data: orderStatsData.chart?.xAxis || [],
axisLabel: { rotate: 28, fontSize: 11 },
axisLine: { lineStyle: { color: '#e4e7ec' } }
},
yAxis: {
type: 'value',
name: '单数',
splitLine: { lineStyle: { type: 'dashed', color: '#eef0f4' } }
},
tooltip: { trigger: 'axis' },
grid: { left: '2%', right: '2%', bottom: '12%', top: 48, containLabel: true },
series: [
...barSeries,
{
name: '趋势',
type: 'line',
smooth: 0.35,
symbol: 'circle',
symbolSize: 7,
lineStyle: { width: 3, color: '#fa8c16' },
itemStyle: { color: '#fa8c16' },
data: lineData
}
]
}
})
const loadOrderStats = () => {
orderStats({ order_type: orderStatsType.value, days: orderStatsDays.value })
.then((res: any) => {
Object.assign(orderStatsData, {
order_type_name: res.order_type_name ?? '',
date_range: res.date_range || [],
today_total: res.today_total ?? 0,
today_amount: res.today_amount ?? '0.00',
today_ranking: res.today_ranking || [],
departments: res.departments || [],
ranking: res.ranking || [],
chart: res.chart || { xAxis: [], series: [] }
})
})
.catch(() => {})
}
const refreshAll = () => {
getData()
loadAssistantStats()
loadOrderStats()
}
watch(
() => settingStore.theme,
() => {
updateColor()
}
)
const workbenchData: any = reactive({
version: {
version: '',
website: '',
based: '',
channel: { gitee: '', website: '' }
},
support: [],
today: {},
menu: [],
visitor: [],
article: [],
visitorOption: {
xAxis: { type: 'category', data: [] },
yAxis: { type: 'value' },
legend: { data: ['访问量'], top: 8 },
tooltip: { trigger: 'axis' },
grid: { left: '2%', right: '3%', bottom: '3%', top: 40, containLabel: true },
series: [
{
name: '访问量',
data: [],
type: 'line',
smooth: true,
color: settingStore.theme,
lineStyle: { color: settingStore.theme, width: 2 },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: settingStore.theme },
{ offset: 1, color: settingStore.theme }
]
},
opacity: 0.12
}
}
]
},
saleOption: {
xAxis: { type: 'category', data: [] },
yAxis: { type: 'value', name: '万' },
tooltip: { trigger: 'axis' },
grid: { left: '2%', right: '3%', bottom: '3%', top: 36, containLabel: true },
series: [
{
name: '销售量',
data: [],
type: 'bar',
showBackground: true,
backgroundStyle: { color: 'rgba(120, 130, 150, 0.08)', borderRadius: [10, 10, 0, 0] },
barWidth: '42%',
itemStyle: {
borderRadius: [10, 10, 0, 0],
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: calcColor(settingStore.theme, 0.65) },
{ offset: 1, color: settingStore.theme }
]
}
}
}
]
}
})
const getData = () => {
getWorkbench()
.then((res: any) => {
workbenchData.version = res.version
workbenchData.today = res.today
workbenchData.menu = res.menu
workbenchData.visitor = res.visitor
workbenchData.support = res.support
workbenchData.visitorOption.xAxis.data = []
workbenchData.visitorOption.series[0].data = []
workbenchData.saleOption.xAxis.data = []
workbenchData.saleOption.series[0].data = []
res.visitor?.date?.reverse().forEach((item: any) => {
workbenchData.visitorOption.xAxis.data.push(item)
})
res.visitor?.list?.[0]?.data?.forEach((item: any) => {
workbenchData.visitorOption.series[0].data.push(item)
})
res.sale?.date?.reverse().forEach((item: any) => {
workbenchData.saleOption.xAxis.data.push(item)
})
res.sale?.list?.[0]?.data?.forEach((item: any) => {
let v: any = item
if (item <= 50) {
v = {
value: item,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: calcColor('#ff8729', 0.7) },
{ offset: 1, color: '#ff8729' }
]
}
}
}
}
workbenchData.saleOption.series[0].data.push(v)
})
})
.catch((err: any) => {
console.log('err', err)
})
}
const updateColor = () => {
const t = settingStore.theme
workbenchData.visitorOption.series[0].color = t
workbenchData.visitorOption.series[0].lineStyle.color = t
workbenchData.visitorOption.series[0].areaStyle.color.colorStops = [
{ offset: 0, color: t },
{ offset: 1, color: t }
]
workbenchData.saleOption.series[0].itemStyle.color.colorStops = [
{ offset: 0, color: calcColor(t, 0.65) },
{ offset: 1, color: t }
]
saleChart.value?.clear()
visitorChart.value?.clear()
saleChart.value?.setOption(workbenchData.saleOption)
visitorChart.value?.setOption(workbenchData.visitorOption)
}
onMounted(() => {
getData()
loadAssistantStats()
loadOrderStats()
})
</script>
<style lang="scss" scoped>
.workbench-page {
min-height: 100%;
padding: 20px 20px 40px;
background: linear-gradient(160deg, #eef2ff 0%, #f8fafc 38%, #f1f5f9 100%);
}
.wb-hero {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
padding: 22px 26px;
border-radius: 20px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.65) 100%);
border: 1px solid rgba(255, 255, 255, 0.95);
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
}
.wb-hero-title {
margin: 0 0 6px;
font-size: 26px;
font-weight: 700;
letter-spacing: 0.02em;
color: #0f172a;
}
.wb-hero-desc {
margin: 0;
font-size: 14px;
color: #64748b;
}
.wb-refresh-btn {
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
}
.wb-section {
margin-bottom: 20px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.05);
overflow: hidden;
backdrop-filter: blur(10px);
}
.wb-section--diagnosis {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #3b82f6, #60a5fa) 1;
}
.wb-section--order {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #8b5cf6, #a78bfa) 1;
}
.wb-section--trend {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #14b8a6, #2dd4bf) 1;
}
.wb-section-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 18px 22px;
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9) 0%, rgba(255, 255, 255, 0) 100%);
}
.wb-section-title {
display: flex;
align-items: center;
gap: 14px;
}
.wb-section-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 14px;
font-size: 22px;
color: #fff;
}
.wb-section-icon--blue {
background: linear-gradient(135deg, #3b82f6, #2563eb);
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.35);
}
.wb-section-icon--violet {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.3);
}
.wb-section-icon--teal {
background: linear-gradient(135deg, #14b8a6, #0d9488);
box-shadow: 0 8px 20px rgba(13, 148, 136, 0.3);
}
.wb-section-name {
font-size: 17px;
font-weight: 600;
color: #0f172a;
}
.wb-section-sub {
margin-top: 2px;
font-size: 12px;
color: #94a3b8;
}
.wb-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.wb-select {
width: 120px;
}
.wb-select--wide {
width: 168px;
}
.wb-section-body {
padding: 20px 22px 24px;
}
.wb-kpi-row {
margin-bottom: 8px;
}
.wb-kpi {
padding: 18px 20px;
border-radius: 16px;
height: 100%;
min-height: 112px;
box-sizing: border-box;
}
.wb-kpi--blue {
background: linear-gradient(145deg, #eff6ff 0%, #dbeafe 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.wb-kpi--violet {
background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%);
border: 1px solid rgba(139, 92, 246, 0.22);
}
.wb-kpi--amber {
background: linear-gradient(145deg, #fffbeb 0%, #fef3c7 100%);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.wb-kpi-label {
font-size: 13px;
color: #64748b;
}
.wb-kpi-value {
margin-top: 8px;
font-size: 32px;
font-weight: 700;
line-height: 1.1;
color: #1e293b;
font-variant-numeric: tabular-nums;
}
.wb-kpi-value--sm {
font-size: 26px;
}
.wb-kpi-hint {
margin-top: 6px;
font-size: 12px;
color: #94a3b8;
}
.wb-rank-strip {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 112px;
padding: 14px 18px;
border-radius: 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.wb-rank-strip--compact {
min-height: 112px;
}
.wb-rank-strip-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 8px;
}
.wb-rank-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.wb-rank-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
font-size: 13px;
background: #fff;
border: 1px solid #e2e8f0;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
em {
font-style: normal;
font-weight: 700;
color: #d97706;
}
strong {
color: var(--el-color-primary);
font-weight: 600;
}
}
.wb-rank-chip--dense {
padding: 5px 10px;
font-size: 12px;
small {
color: #94a3b8;
margin-left: 4px;
}
}
.wb-rank-empty {
font-size: 13px;
color: #cbd5e1;
}
.wb-chart-row {
margin-top: 16px;
}
.wb-chart-card {
height: 100%;
padding: 14px 16px 8px;
border-radius: 16px;
background: #fafbfc;
border: 1px solid #eef0f4;
}
.wb-chart-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 4px;
}
.wb-chart-title {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
.wb-chart-hint {
font-size: 12px;
color: #94a3b8;
}
.wb-chart-empty {
min-height: 280px;
padding: 40px 0;
}
.wb-dept-block {
margin-top: 22px;
padding-top: 18px;
border-top: 1px dashed #e2e8f0;
}
.wb-dept-title {
font-size: 15px;
font-weight: 600;
color: #334155;
margin-bottom: 14px;
}
.wb-dept-card {
margin-bottom: 16px;
padding: 14px 16px;
border-radius: 14px;
background: #fff;
border: 1px solid #e8ecf1;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.03);
}
.wb-dept-card-head {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
color: #1e293b;
margin-bottom: 10px;
font-size: 14px;
}
.wb-dept-total {
font-size: 12px;
font-weight: 500;
color: #64748b;
}
.wb-dept-list {
font-size: 13px;
}
.wb-dept-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f1f5f9;
color: #475569;
&:last-child {
border-bottom: none;
}
}
.wb-dept-num {
font-weight: 600;
color: #0f172a;
small {
font-weight: 500;
color: #94a3b8;
margin-left: 6px;
}
}
.wb-dept-empty {
color: #cbd5e1;
font-size: 13px;
padding: 8px 0;
}
.wb-ranking-card {
min-height: 300px;
}
.wb-ranking-list {
max-height: 280px;
overflow-y: auto;
padding-right: 4px;
}
.wb-ranking-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 12px;
background: #fff;
border: 1px solid #eef0f4;
}
.wb-ranking-rank {
flex-shrink: 0;
width: 28px;
height: 28px;
line-height: 28px;
text-align: center;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
color: #475569;
}
.wb-ranking-name {
flex: 1;
min-width: 0;
font-weight: 500;
color: #334155;
}
.wb-ranking-meta {
font-size: 13px;
color: #64748b;
b {
color: var(--el-color-primary);
}
}
.wb-ranking-money {
margin-left: 8px;
color: #0f172a;
font-weight: 600;
}
.wb-trend-body {
padding-top: 8px;
}
.wb-trend-card {
background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100%);
}
</style>