feat(F006-W3xB): Dashboard 加图表 + 较昨日真值
- DashboardView.vue 加 2 个 echarts 图表: * 询价趋势线 (近 7 天折线图, 按 publishDate 毫秒时间戳按天分桶) * 询价状态分布 (环图, 4 状态, 已成交大块) - stat 卡 今日询价 + 较昨日 用 inquiry createDate 按天聚合算 - formatTime 工具: 毫秒时间戳 -> "MM-DD HH:mm" 替代原来直接显示 1782662400000 - dateKey 工具: 毫秒时间戳 -> YYYY-MM-DD - ECharts 用 var(--chart-1/2/3/4) + 主题 watch 切主题自动重渲染 - 失败策略: 任何 API 失败回退 FALLBACK_TREND/FALLBACK_STATUS - 验证 (Playwright): * 今日询价 0 0% 较昨日 (无今日数据) * 已成交 192 (真数据) * 趋势 5/24-6/28 真数据折线 * 4 主题切色正常
This commit is contained in:
parent
4141a27a0c
commit
e791520f5d
@ -2,23 +2,45 @@
|
|||||||
/**
|
/**
|
||||||
* F006 数据看板
|
* F006 数据看板
|
||||||
* W1 D3: mock 数据
|
* W1 D3: mock 数据
|
||||||
* W1 D4: 接真实 API(/sys/openapi/inquiry/publish 等)
|
* W2 D3: 接真 API,失败回退 mock(渐进式)
|
||||||
|
* W3.5: 4 stat 全接 inquiry 真 API
|
||||||
|
* - 总数 = inquiry.data.length(后端分页参数未生效,实际一次返全部)
|
||||||
|
* - 状态 = 按 apprStatus 推断:0=待报价,1=报价中,2=已成交
|
||||||
|
* - 待处理 = 全部 - 已成交
|
||||||
|
*
|
||||||
|
* 失败策略:任意一个 API 失败不影响其他,失败回退 mock
|
||||||
*/
|
*/
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||||
|
import { httpGet } from '@/utils/request'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
|
|
||||||
const stats = ref([
|
interface Stat {
|
||||||
{ label: '今日询价', value: '128', delta: '+12.5%', trend: 'up', icon: 'ChatDotRound' },
|
label: string
|
||||||
{ label: '订单总额', value: '¥328K', delta: '+8.3%', trend: 'up', icon: 'Money' },
|
value: string
|
||||||
{ label: '活跃供应商', value: '42', delta: '+3', trend: 'up', icon: 'OfficeBuilding' },
|
delta: string
|
||||||
{ label: '待处理事项', value: '7', delta: '-2', trend: 'down', icon: 'Bell' }
|
trend: 'up' | 'down'
|
||||||
])
|
icon: string
|
||||||
|
}
|
||||||
|
interface Inquiry {
|
||||||
|
id: string
|
||||||
|
customer: string
|
||||||
|
product: string
|
||||||
|
qty: string
|
||||||
|
status: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
const recentInquiries = ref([
|
const stats = ref<Stat[]>([
|
||||||
{ id: 'RFQ-20668', customer: '中铁建设集团', product: 'HRB400 螺纹钢', qty: '200 吨', status: 'pending', time: '10:24' },
|
{ label: '今日询价', value: '--', delta: '', trend: 'up', icon: 'ChatDotRound' },
|
||||||
{ id: 'RFQ-20667', customer: '中建八局', product: '商混 C30', qty: '500 m³', status: 'quoting', time: '09:15' },
|
{ label: '订单总额', value: '--', delta: '', trend: 'up', icon: 'Money' },
|
||||||
{ id: 'RFQ-20666', customer: '上海建工', product: '盘螺 8mm', qty: '80 吨', status: 'done', time: '08:42' },
|
{ label: '活跃供应商', value: '--', delta: '', trend: 'up', icon: 'OfficeBuilding' },
|
||||||
{ id: 'RFQ-20665', customer: '北京城建', product: '水泥 P.O 42.5', qty: '100 吨', status: 'cancelled', time: '昨天 16:30' }
|
{ label: '待处理事项', value: '--', delta: '', trend: 'down', icon: 'Bell' }
|
||||||
])
|
])
|
||||||
|
const recentInquiries = ref<Inquiry[]>([])
|
||||||
|
const apiStatus = ref<'loading' | 'live' | 'fallback'>('loading')
|
||||||
|
// W3.x B.3 图表数据
|
||||||
|
const trendData = ref<{ dates: string[]; counts: number[] }>({ dates: [], counts: [] })
|
||||||
|
const statusSlices = ref<{ name: string; value: number; color: string }[]>([])
|
||||||
|
|
||||||
const STATUS_MAP: Record<string, { label: string; type: string }> = {
|
const STATUS_MAP: Record<string, { label: string; type: string }> = {
|
||||||
pending: { label: '待报价', type: 'primary' },
|
pending: { label: '待报价', type: 'primary' },
|
||||||
@ -27,18 +49,314 @@ const STATUS_MAP: Record<string, { label: string; type: string }> = {
|
|||||||
cancelled: { label: '已取消', type: 'danger' }
|
cancelled: { label: '已取消', type: 'danger' }
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const recentColumns = [
|
||||||
// W1 D4: 这里替换为真实 API 调用
|
{ prop: 'id', label: '单号', width: 140 },
|
||||||
// const res = await api.get('/sys/openapi/inquiry/publish', { params: { customerCode: 'admin' } })
|
{ prop: 'customer', label: '客户' },
|
||||||
// stats.value = transformStats(res)
|
{ prop: 'product', label: '商品' },
|
||||||
|
{ prop: 'qty', label: '数量', width: 100 },
|
||||||
|
{ prop: 'status', label: '状态', width: 110, type: 'status' as const, statusMap: STATUS_MAP },
|
||||||
|
{ prop: 'time', label: '时间', width: 140 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const FALLBACK_INQUIRIES: Inquiry[] = [
|
||||||
|
{ id: 'RFQ-20668', customer: '中铁建设集团', product: 'HRB400 螺纹钢', qty: '200 吨', status: 'pending', time: '10:24' },
|
||||||
|
{ id: 'RFQ-20667', customer: '中建八局', product: '商混 C30', qty: '500 m³', status: 'quoting', time: '09:15' },
|
||||||
|
{ id: 'RFQ-20666', customer: '上海建工', product: '盘螺 8mm', qty: '80 吨', status: 'done', time: '08:42' },
|
||||||
|
{ id: 'RFQ-20665', customer: '北京城建', product: '水泥 P.O 42.5', qty: '100 吨', status: 'cancelled', time: '昨天 16:30' }
|
||||||
|
]
|
||||||
|
const FALLBACK_STATS: Stat[] = [
|
||||||
|
{ label: '今日询价', value: '128', delta: '+12.5%', trend: 'up', icon: 'ChatDotRound' },
|
||||||
|
{ label: '订单总额', value: '¥328K', delta: '+8.3%', trend: 'up', icon: 'Money' },
|
||||||
|
{ label: '活跃供应商', value: '42', delta: '+3', trend: 'up', icon: 'OfficeBuilding' },
|
||||||
|
{ label: '待处理事项', value: '7', delta: '-2', trend: 'down', icon: 'Bell' }
|
||||||
|
]
|
||||||
|
const FALLBACK_TREND = {
|
||||||
|
dates: ['7/1', '7/2', '7/3', '7/4', '7/5', '7/6', '7/7'],
|
||||||
|
counts: [12, 18, 15, 22, 28, 24, 32]
|
||||||
|
}
|
||||||
|
const FALLBACK_STATUS = [
|
||||||
|
{ name: '待报价', value: 28, color: '#3B82F6' },
|
||||||
|
{ name: '报价中', value: 14, color: '#F59E0B' },
|
||||||
|
{ name: '已成交', value: 86, color: '#10B981' },
|
||||||
|
{ name: '已取消', value: 6, color: '#EF4444' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F002 验过:GET /sys/openapi/inquiry/list?status=&page=&limit=
|
||||||
|
* 这里用于拉近期询价;若 status 不支持或返结构变,回退 mock
|
||||||
|
*
|
||||||
|
* 后端无 total 字段、status 字段,前端按 apprStatus 映射状态
|
||||||
|
*/
|
||||||
|
async function loadRecentInquiries(): Promise<Inquiry[]> {
|
||||||
|
try {
|
||||||
|
const data = await httpGet<any[]>('/sys/openapi/inquiry/list', { page: 1, limit: 5 })
|
||||||
|
if (!Array.isArray(data)) throw new Error('inquiry list not array')
|
||||||
|
return data.slice(0, 5).map((it: any) => {
|
||||||
|
const s = it.apprStatus === 0 ? 'pending'
|
||||||
|
: it.apprStatus === 1 ? 'quoting'
|
||||||
|
: it.apprStatus === 2 ? 'done'
|
||||||
|
: 'pending'
|
||||||
|
return {
|
||||||
|
id: it.orderId || it.id || it.rfqId || it.inquiryId || 'RFQ-?',
|
||||||
|
customer: it.inquiryEnterpriseName || it.customerName || it.customer || '--',
|
||||||
|
product: it.productName || it.product || '--',
|
||||||
|
qty: it.purchaseQuantity ? `${it.purchaseQuantity} ${it.unit || ''}`.trim() : '--',
|
||||||
|
status: s,
|
||||||
|
time: formatTime(it.publishDate || it.createDate || it.createTime),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return FALLBACK_INQUIRIES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 看板顶部 4 个 stat - 接 inquiry 真 API
|
||||||
|
* 后端 /sys/openapi/inquiry/list 实际返所有数据(无 total 字段),
|
||||||
|
* 但有 apprStatus 字段可以推断状态:
|
||||||
|
* - 0 = 待报价
|
||||||
|
* - 1 = 报价中
|
||||||
|
* - 2 = 已成交(已完成)
|
||||||
|
* Dashboard 4 stat 字段映射:
|
||||||
|
* - 询价总数 = data.length
|
||||||
|
* - 待报价 = apprStatus == 0 计数
|
||||||
|
* - 报价中 = apprStatus == 1 计数
|
||||||
|
* - 已成交 = apprStatus == 2 计数
|
||||||
|
*
|
||||||
|
* 失败回退 mock(FALLBACK_STATS)
|
||||||
|
*/
|
||||||
|
async function loadStats(): Promise<Stat[]> {
|
||||||
|
try {
|
||||||
|
const data = await httpGet<any[]>('/sys/openapi/inquiry/list', { page: 1, limit: 500 })
|
||||||
|
if (!Array.isArray(data)) throw new Error('inquiry list not array')
|
||||||
|
|
||||||
|
const total = data.length
|
||||||
|
const pending = data.filter((it: any) => it.apprStatus === 0).length
|
||||||
|
const quoting = data.filter((it: any) => it.apprStatus === 1).length
|
||||||
|
const done = data.filter((it: any) => it.apprStatus === 2).length
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: '询价总数', value: String(total), delta: '', trend: 'up', icon: 'ChatDotRound' },
|
||||||
|
{ label: '待报价', value: String(pending), delta: '', trend: 'up', icon: 'Bell' },
|
||||||
|
{ label: '报价中', value: String(quoting), delta: '', trend: 'up', icon: 'Loading' },
|
||||||
|
{ label: '已成交', value: String(done), delta: '', trend: 'down', icon: 'CircleCheck' }
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
return FALLBACK_STATS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* W3.x B.2: 较昨日差值
|
||||||
|
*/
|
||||||
|
function calcDelta(todayCount: number, yesterdayCount: number): { delta: string; trend: 'up' | 'down' } {
|
||||||
|
if (yesterdayCount === 0) {
|
||||||
|
return { delta: todayCount > 0 ? '+100%' : '0%', trend: 'up' }
|
||||||
|
}
|
||||||
|
const pct = ((todayCount - yesterdayCount) / yesterdayCount) * 100
|
||||||
|
const sign = pct >= 0 ? '+' : ''
|
||||||
|
return { delta: `${sign}${pct.toFixed(1)}%`, trend: pct >= 0 ? 'up' : 'down' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* W3.x B.3: 状态分布环图
|
||||||
|
*/
|
||||||
|
interface StatusSlice { name: string; value: number; color: string }
|
||||||
|
function buildStatusSlices(items: any[]): StatusSlice[] {
|
||||||
|
const counts: Record<number, number> = { 0: 0, 1: 0, 2: 0, 3: 0 }
|
||||||
|
items.forEach((it: any) => {
|
||||||
|
const k = typeof it.apprStatus === 'number' ? it.apprStatus : 0
|
||||||
|
counts[k] = (counts[k] || 0) + 1
|
||||||
|
})
|
||||||
|
return [
|
||||||
|
{ name: '待报价', value: counts[0], color: 'var(--chart-1)' },
|
||||||
|
{ name: '报价中', value: counts[1], color: 'var(--chart-2)' },
|
||||||
|
{ name: '已成交', value: counts[2], color: 'var(--chart-3)' },
|
||||||
|
{ name: '已取消', value: counts[3], color: 'var(--chart-4)' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* W3.x B.3: 询价趋势线
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 工具: 后端返的 publishDate 是毫秒时间戳字符串 (e.g. "1782662400000")
|
||||||
|
* 转成 YYYY-MM-DD 字符串供分桶
|
||||||
|
*/
|
||||||
|
function dateKey(ts: string | number | undefined | null): string {
|
||||||
|
if (!ts) return ''
|
||||||
|
const n = typeof ts === 'string' ? Number(ts) : ts
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return ''
|
||||||
|
return new Date(n).toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具: 毫秒时间戳 → "MM-DD HH:mm" 格式
|
||||||
|
*/
|
||||||
|
function formatTime(ts: string | number | undefined | null): string {
|
||||||
|
if (!ts) return '--'
|
||||||
|
const n = typeof ts === 'string' ? Number(ts) : ts
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return '--'
|
||||||
|
const d = new Date(n)
|
||||||
|
const pad = (x: number) => String(x).padStart(2, '0')
|
||||||
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTrend(items: any[]): { dates: string[]; counts: number[] } {
|
||||||
|
const buckets: Record<string, number> = {}
|
||||||
|
items.forEach((it: any) => {
|
||||||
|
const d = dateKey(it.publishDate || it.createDate || it.createTime)
|
||||||
|
if (!d) return
|
||||||
|
buckets[d] = (buckets[d] || 0) + 1
|
||||||
|
})
|
||||||
|
const sorted = Object.keys(buckets).sort()
|
||||||
|
const last7 = sorted.slice(-7)
|
||||||
|
return { dates: last7.map(d => d.slice(5)), counts: last7.map(d => buckets[d]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const [s, r] = await Promise.all([loadStats(), loadRecentInquiries()])
|
||||||
|
stats.value = s
|
||||||
|
recentInquiries.value = r
|
||||||
|
// 拉一次全量算图表 + 较昨日
|
||||||
|
let allInquiries: any[] = []
|
||||||
|
try {
|
||||||
|
allInquiries = await httpGet<any[]>('/sys/openapi/inquiry/list', { page: 1, limit: 500 })
|
||||||
|
if (!Array.isArray(allInquiries)) allInquiries = []
|
||||||
|
} catch { allInquiries = [] }
|
||||||
|
|
||||||
|
const isLive = allInquiries.length > 0
|
||||||
|
if (isLive) {
|
||||||
|
// W3.x B.2 较昨日
|
||||||
|
const buckets: Record<string, number> = {}
|
||||||
|
allInquiries.forEach((it: any) => {
|
||||||
|
const d = dateKey(it.publishDate || it.createDate)
|
||||||
|
if (d) buckets[d] = (buckets[d] || 0) + 1
|
||||||
|
})
|
||||||
|
const todayKey = new Date().toISOString().slice(0, 10)
|
||||||
|
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10)
|
||||||
|
const today = buckets[todayKey] || 0
|
||||||
|
const yest = buckets[yesterday] || 0
|
||||||
|
const d = calcDelta(today, yest)
|
||||||
|
stats.value = [
|
||||||
|
{ label: '今日询价', value: String(today), delta: d.delta, trend: d.trend, icon: 'ChatDotRound' },
|
||||||
|
...stats.value.slice(1)
|
||||||
|
]
|
||||||
|
// W3.x B.3 图表
|
||||||
|
trendData.value = buildTrend(allInquiries)
|
||||||
|
statusSlices.value = buildStatusSlices(allInquiries)
|
||||||
|
} else {
|
||||||
|
trendData.value = FALLBACK_TREND
|
||||||
|
statusSlices.value = FALLBACK_STATUS
|
||||||
|
}
|
||||||
|
apiStatus.value = isLive ? 'live' : 'fallback'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* W3.x B.3 ECharts 渲染
|
||||||
|
* - 趋势线: 折线图, x 轴 7 天日期, y 轴询价数, 主题 brand 色
|
||||||
|
* - 环图: 4 状态 + 主题色, 中心显示总数
|
||||||
|
* - watch 数据 + 主题变化重渲染(切主题时配色变)
|
||||||
|
*/
|
||||||
|
import * as echarts from 'echarts/core'
|
||||||
|
import { LineChart, PieChart } from 'echarts/charts'
|
||||||
|
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
echarts.use([LineChart, PieChart, TooltipComponent, GridComponent, LegendComponent, CanvasRenderer])
|
||||||
|
|
||||||
|
const trendChartEl = ref<HTMLDivElement | null>(null)
|
||||||
|
const statusChartEl = ref<HTMLDivElement | null>(null)
|
||||||
|
let trendChart: echarts.ECharts | null = null
|
||||||
|
let statusChart: echarts.ECharts | null = null
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
function getBrandColor(): string { return getComputedStyle(document.body).getPropertyValue('--brand').trim() || '#2563EB' }
|
||||||
|
function getInkColor(): string { return getComputedStyle(document.body).getPropertyValue('--ink-3').trim() || '#64748B' }
|
||||||
|
function getBorderColor(): string { return getComputedStyle(document.body).getPropertyValue('--border').trim() || '#E2E8F0' }
|
||||||
|
function getSurfaceColor(): string { return getComputedStyle(document.body).getPropertyValue('--surface').trim() || '#FFFFFF' }
|
||||||
|
|
||||||
|
function renderTrend() {
|
||||||
|
if (!trendChartEl.value) return
|
||||||
|
if (!trendChart) trendChart = echarts.init(trendChartEl.value)
|
||||||
|
trendChart.setOption({
|
||||||
|
grid: { left: 40, right: 20, top: 30, bottom: 30 },
|
||||||
|
tooltip: { trigger: 'axis', backgroundColor: getSurfaceColor(), borderColor: getBorderColor(), textStyle: { color: getInkColor() } },
|
||||||
|
xAxis: { type: 'category', data: trendData.value.dates, axisLine: { lineStyle: { color: getBorderColor() } }, axisLabel: { color: getInkColor() } },
|
||||||
|
yAxis: { type: 'value', splitLine: { lineStyle: { color: getBorderColor() } }, axisLabel: { color: getInkColor() } },
|
||||||
|
series: [{
|
||||||
|
type: 'line', data: trendData.value.counts, smooth: true,
|
||||||
|
itemStyle: { color: getBrandColor() },
|
||||||
|
lineStyle: { color: getBrandColor(), width: 2 },
|
||||||
|
areaStyle: { color: getBrandColor(), opacity: 0.15 }
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatus() {
|
||||||
|
if (!statusChartEl.value) return
|
||||||
|
if (!statusChart) statusChart = echarts.init(statusChartEl.value)
|
||||||
|
const total = statusSlices.value.reduce((a, b) => a + b.value, 0)
|
||||||
|
// 解 CSS 变量到真值(echarts 内部 canvas 渲染不解析 var())
|
||||||
|
const color1 = getComputedStyle(document.body).getPropertyValue('--chart-1').trim() || '#3B82F6'
|
||||||
|
const color2 = getComputedStyle(document.body).getPropertyValue('--chart-2').trim() || '#7C3AED'
|
||||||
|
const color3 = getComputedStyle(document.body).getPropertyValue('--chart-3').trim() || '#10B981'
|
||||||
|
const color4 = getComputedStyle(document.body).getPropertyValue('--chart-4').trim() || '#F59E0B'
|
||||||
|
const realColors = [color1, color2, color3, color4]
|
||||||
|
const inkColor = getInkColor()
|
||||||
|
statusChart.setOption({
|
||||||
|
tooltip: { trigger: 'item', backgroundColor: getSurfaceColor(), borderColor: getBorderColor(), textStyle: { color: inkColor } },
|
||||||
|
legend: { bottom: 0, textStyle: { color: inkColor } },
|
||||||
|
series: [{
|
||||||
|
type: 'pie', radius: ['62%', '82%'], center: ['50%', '46%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
label: { show: false }, labelLine: { show: false },
|
||||||
|
data: statusSlices.value.map((s, i) => ({ name: s.name, value: s.value, itemStyle: { color: realColors[i] || s.color } })),
|
||||||
|
emphasis: { label: { show: true, fontSize: 16, fontWeight: 600, color: inkColor } }
|
||||||
|
}],
|
||||||
|
graphic: total > 0 ? [
|
||||||
|
{ type: 'text', left: 'center', top: '38%', style: { text: '总计', fontSize: 11, fill: inkColor, opacity: 0.6 } },
|
||||||
|
{ type: 'text', left: 'center', top: '46%', style: { text: String(total), fontSize: 26, fontWeight: 700, fill: inkColor, textAlign: 'center' } },
|
||||||
|
{ type: 'text', left: 'center', top: '56%', style: { text: '单', fontSize: 10, fill: inkColor, opacity: 0.5 } }
|
||||||
|
] : []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在主 onMounted 跑完后挂图表(已经跑了)
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
renderTrend()
|
||||||
|
renderStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([() => trendData.value, () => statusSlices.value, () => themeStore.current], async () => {
|
||||||
|
await nextTick()
|
||||||
|
renderTrend()
|
||||||
|
renderStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.resize() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-dashboard">
|
<div class="rr-dashboard">
|
||||||
<div class="rr-page-header">
|
<div class="rr-page-header">
|
||||||
|
<div>
|
||||||
<h2 class="rr-page-title">数据看板</h2>
|
<h2 class="rr-page-title">数据看板</h2>
|
||||||
<p class="rr-page-sub">2026 年 6 月 17 日 · 实时数据</p>
|
<p class="rr-page-sub">
|
||||||
|
2026 年 6 月 17 日 · 实时数据
|
||||||
|
<el-tag
|
||||||
|
v-if="apiStatus !== 'loading'"
|
||||||
|
size="small"
|
||||||
|
:type="apiStatus === 'live' ? 'success' : 'info'"
|
||||||
|
effect="light"
|
||||||
|
class="rr-api-badge"
|
||||||
|
>
|
||||||
|
{{ apiStatus === 'live' ? '实时' : '示例数据' }}
|
||||||
|
</el-tag>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 4 个统计卡 -->
|
<!-- 4 个统计卡 -->
|
||||||
@ -49,33 +367,43 @@ onMounted(() => {
|
|||||||
<span class="rr-stat-label">{{ s.label }}</span>
|
<span class="rr-stat-label">{{ s.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rr-stat-value">{{ s.value }}</div>
|
<div class="rr-stat-value">{{ s.value }}</div>
|
||||||
<div class="rr-stat-delta" :class="s.trend">
|
<div v-if="s.delta" class="rr-stat-delta" :class="s.trend">
|
||||||
<el-icon><component :is="s.trend === 'up' ? 'CaretTop' : 'CaretBottom'" /></el-icon>
|
<el-icon><component :is="s.trend === 'up' ? 'CaretTop' : 'CaretBottom'" /></el-icon>
|
||||||
{{ s.delta }} 较昨日
|
{{ s.delta }} 较昨日
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- W3.x B.3 图表区: 询价趋势 + 状态分布 -->
|
||||||
|
<div class="rr-chart-grid">
|
||||||
|
<div class="rr-card rr-chart-card">
|
||||||
|
<div class="rr-card-header">
|
||||||
|
<h3 class="rr-card-title">询价趋势(近 7 天)</h3>
|
||||||
|
</div>
|
||||||
|
<div ref="trendChartEl" class="rr-chart" v-if="trendData.dates.length"></div>
|
||||||
|
<div class="rr-chart-empty" v-else>暂无数据</div>
|
||||||
|
</div>
|
||||||
|
<div class="rr-card rr-chart-card">
|
||||||
|
<div class="rr-card-header">
|
||||||
|
<h3 class="rr-card-title">询价状态分布</h3>
|
||||||
|
</div>
|
||||||
|
<div ref="statusChartEl" class="rr-chart" v-if="statusSlices.length"></div>
|
||||||
|
<div class="rr-chart-empty" v-else>暂无数据</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 近期询价 -->
|
<!-- 近期询价 -->
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
<div class="rr-card-header">
|
<div class="rr-card-header">
|
||||||
<h3 class="rr-card-title">近期询价</h3>
|
<h3 class="rr-card-title">近期询价</h3>
|
||||||
<el-button text type="primary">查看全部 →</el-button>
|
<el-button text type="primary" @click="$router.push('/inquiry')">查看全部 →</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="recentInquiries" stripe>
|
<DataTable
|
||||||
<el-table-column prop="id" label="单号" width="140" />
|
:columns="recentColumns"
|
||||||
<el-table-column prop="customer" label="客户" />
|
:data="recentInquiries"
|
||||||
<el-table-column prop="product" label="商品" />
|
:show-pagination="false"
|
||||||
<el-table-column prop="qty" label="数量" width="100" />
|
empty-text="暂无询价"
|
||||||
<el-table-column label="状态" width="110">
|
/>
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="STATUS_MAP[row.status].type as any" size="small" effect="light">
|
|
||||||
{{ STATUS_MAP[row.status].label }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="time" label="时间" width="100" />
|
|
||||||
</el-table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -88,6 +416,7 @@ onMounted(() => {
|
|||||||
color: var(--ink); margin-bottom: 4px;
|
color: var(--ink); margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||||
|
.rr-api-badge { margin-left: 8px; }
|
||||||
|
|
||||||
.rr-stats-grid {
|
.rr-stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -141,4 +470,18 @@ onMounted(() => {
|
|||||||
.rr-card-title {
|
.rr-card-title {
|
||||||
font-size: 15px; font-weight: 600; color: var(--ink);
|
font-size: 15px; font-weight: 600; color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* W3.x B.3 图表区 */
|
||||||
|
.rr-chart-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.rr-chart-card { padding-bottom: 8px; }
|
||||||
|
.rr-chart { height: 240px; padding: 8px 4px; }
|
||||||
|
.rr-chart-empty {
|
||||||
|
height: 240px; display: flex; align-items: center; justify-content: center;
|
||||||
|
color: var(--ink-4); font-size: 13px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user