1246 lines
42 KiB
Vue
1246 lines
42 KiB
Vue
<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>
|