feat(F006): 后台UI统一规范 - DataTable默认borderless/全局表格表头底色圆角统一/页面内边距统一/状态色token化(暗色适配)/标题字号20px统一/统计页echarts异步组件拆分(509KB->6.9KB)

This commit is contained in:
zou 2026-08-18 14:47:39 +08:00
parent 508c84e571
commit 99bc522b24
19 changed files with 314 additions and 103 deletions

View File

@ -63,7 +63,8 @@ const props = withDefaults(defineProps<Props>(), {
pageSizes: () => [10, 20, 50, 100], pageSizes: () => [10, 20, 50, 100],
rowKey: 'id', rowKey: 'id',
stripe: false, stripe: false,
borderless: false, // 2026-08-18 UI 统一: 默认无边框 (带 border 的老页面由全局样式消除, 见 styles/global-ui.scss)
borderless: true,
emptyText: '暂无数据', emptyText: '暂无数据',
errorText: '', errorText: '',
error: null error: null

View File

@ -10,6 +10,7 @@ import 'element-plus/dist/index.css'
import './theme/tokens.scss' import './theme/tokens.scss'
import './theme/themes.scss' import './theme/themes.scss'
import './styles/global-popper.scss' import './styles/global-popper.scss'
import './styles/global-ui.scss' // 2026-08-18 UI 统一规范 (表格/卡片/内边距/暗色状态色)
import { loadTheme } from './theme/presets' import { loadTheme } from './theme/presets'
// 启动时恢复用户主题 // 启动时恢复用户主题

View File

@ -414,7 +414,7 @@ window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.res
.rr-dashboard { max-width: 1400px; } .rr-dashboard { max-width: 1400px; }
.rr-page-header { margin-bottom: 20px; } .rr-page-header { margin-bottom: 20px; }
.rr-page-title { .rr-page-title {
font-size: 22px; font-weight: 600; letter-spacing: -0.02em; font-size: 20px; font-weight: 700;
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; }

View File

@ -241,7 +241,7 @@ onMounted(load)
</div> </div>
</div> </div>
<el-dialog v-model="auditVisible" title="品牌审核" width="440px" :close-on-click-modal="false"> <el-dialog v-model="auditVisible" title="品牌审核" width="480px" :close-on-click-modal="false">
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="审核结果"> <el-form-item label="审核结果">
<el-radio-group v-model="auditForm.status"> <el-radio-group v-model="auditForm.status">
@ -279,7 +279,7 @@ onMounted(load)
<style scoped lang="scss"> <style scoped lang="scss">
.rr-page { max-width: 1500px; } .rr-page { max-width: 1500px; }
.rr-card { background: var(--el-bg-color); border-radius: 8px; border: 1px solid var(--el-border-color-light); overflow: hidden; } .rr-card { background: var(--el-bg-color); border-radius: 12px; border: 1px solid var(--el-border-color-light); overflow: hidden; }
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--el-border-color-light); } .rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--el-border-color-light); }
.brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--el-border-color-light); cursor: pointer; } .brand-auth { width: 50px; height: 50px; border-radius: 4px; border: 1px solid var(--el-border-color-light); cursor: pointer; }
.rr-muted { color: var(--el-text-color-placeholder); } .rr-muted { color: var(--el-text-color-placeholder); }

View File

@ -15,16 +15,13 @@
* *
* 失败回退: FALLBACK_* * 失败回退: FALLBACK_*
*/ */
import { ref, onMounted, watch, nextTick } from 'vue' import { ref, onMounted, defineAsyncComponent } from 'vue'
import { httpGet } from '@/utils/request' import { httpGet } from '@/utils/request'
import DataTable from '@/components/DataTable/DataTable.vue' import DataTable from '@/components/DataTable/DataTable.vue'
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]) // 图表组件: echarts 在组件内动态 import, 独立 chunk 懒加载 (2026-08-18)
const TrendChart = defineAsyncComponent(() => import('./charts/EchartsTrendChart.vue'))
const StatusPie = defineAsyncComponent(() => import('./charts/EchartsStatusPie.vue'))
interface OrderItem { interface OrderItem {
orderId: string orderId: string
@ -51,12 +48,6 @@ const recentOrders = ref<any[]>([])
const trendData = ref<{ dates: string[]; counts: number[]; amounts: number[] }>({ dates: [], counts: [], amounts: [] }) const trendData = ref<{ dates: string[]; counts: number[]; amounts: number[] }>({ dates: [], counts: [], amounts: [] })
const statusSlices = ref<{ name: string; value: number; color: string }[]>([]) const statusSlices = ref<{ name: string; value: number; color: string }[]>([])
const apiStatus = ref<'loading' | 'live' | 'fallback'>('loading') const apiStatus = ref<'loading' | 'live' | 'fallback'>('loading')
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()
// 订单状态映射 (老 renren: 1=待付款 2=已付款 3=已发货 4=已完成 5=已取消) // 订单状态映射 (老 renren: 1=待付款 2=已付款 3=已发货 4=已完成 5=已取消)
const ORDER_STATUS: Record<number, { label: string; type: string }> = { const ORDER_STATUS: Record<number, { label: string; type: string }> = {
1: { label: '待付款', type: 'warning' }, 1: { label: '待付款', type: 'warning' },
@ -213,73 +204,8 @@ onMounted(async () => {
} }
apiStatus.value = isLive ? 'live' : 'fallback' apiStatus.value = isLive ? 'live' : 'fallback'
await nextTick()
renderTrend()
renderStatus()
}) })
// ECharts 渲染
function getBrand(): string { return getComputedStyle(document.body).getPropertyValue('--brand').trim() || '#2563EB' }
function getInk(): string { return getComputedStyle(document.body).getPropertyValue('--ink-3').trim() || '#64748B' }
function getBorder(): string { return getComputedStyle(document.body).getPropertyValue('--border').trim() || '#E2E8F0' }
function getSurface(): string { return getComputedStyle(document.body).getPropertyValue('--surface').trim() || '#FFFFFF' }
function getChartColors(): string[] {
return ['--chart-1', '--chart-2', '--chart-3', '--chart-4', '--red'].map(v =>
getComputedStyle(document.body).getPropertyValue(v).trim()
)
}
function renderTrend() {
if (!trendChartEl.value) return
if (!trendChart) trendChart = echarts.init(trendChartEl.value)
const ink = getInk(), border = getBorder(), surface = getSurface(), brand = getBrand()
const chart2 = getComputedStyle(document.body).getPropertyValue('--chart-2').trim() || '#7C3AED'
trendChart.setOption({
grid: { left: 50, right: 50, top: 40, bottom: 30 },
tooltip: { trigger: 'axis', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
legend: { top: 0, textStyle: { color: ink } },
xAxis: { type: 'category', data: trendData.value.dates, axisLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
yAxis: [
{ type: 'value', name: '订单数', position: 'left', splitLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
{ type: 'value', name: '交易额', position: 'right', splitLine: { show: false }, axisLabel: { color: ink, formatter: (v: number) => v >= 1000 ? `${(v/1000).toFixed(0)}K` : String(v) } }
],
series: [
{ name: '订单数', type: 'line', data: trendData.value.counts, smooth: true, itemStyle: { color: brand }, lineStyle: { color: brand, width: 2 }, areaStyle: { color: brand, opacity: 0.15 } },
{ name: '交易额', type: 'line', yAxisIndex: 1, data: trendData.value.amounts, smooth: true, itemStyle: { color: chart2 }, lineStyle: { color: chart2, width: 2 } }
]
})
}
function renderStatus() {
if (!statusChartEl.value) return
if (!statusChart) statusChart = echarts.init(statusChartEl.value)
const ink = getInk(), border = getBorder(), surface = getSurface()
const colors = getChartColors()
const total = statusSlices.value.reduce((a, b) => a + b.value, 0)
statusChart.setOption({
tooltip: { trigger: 'item', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
legend: { bottom: 0, textStyle: { color: ink } },
series: [{
type: 'pie', radius: ['58%', '80%'], center: ['50%', '45%'],
avoidLabelOverlap: true,
label: { show: false }, labelLine: { show: false },
data: statusSlices.value.map((s, i) => ({ name: s.name, value: s.value, itemStyle: { color: colors[i] } })),
emphasis: { label: { show: true, fontSize: 14, fontWeight: 600, color: ink } }
}],
graphic: total > 0 ? [
{ type: 'text', left: 'center', top: '38%', style: { text: '总计', fontSize: 10, fill: ink, opacity: 0.5 } },
{ type: 'text', left: 'center', top: '45%', style: { text: String(total), fontSize: 24, fontWeight: 700, fill: ink } }
] : []
})
}
watch([() => trendData.value, () => statusSlices.value, () => themeStore.current], async () => {
await nextTick()
renderTrend()
renderStatus()
})
window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.resize() })
</script> </script>
<template> <template>
@ -315,13 +241,13 @@ window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.res
<div class="rr-card-header"> <div class="rr-card-header">
<h3 class="rr-card-title">订单趋势(近 7 天)</h3> <h3 class="rr-card-title">订单趋势(近 7 天)</h3>
</div> </div>
<div ref="trendChartEl" class="rr-chart"></div> <TrendChart :dates="trendData.dates" :counts="trendData.counts" :amounts="trendData.amounts" />
</div> </div>
<div class="rr-card rr-chart-card"> <div class="rr-card rr-chart-card">
<div class="rr-card-header"> <div class="rr-card-header">
<h3 class="rr-card-title">订单状态分布</h3> <h3 class="rr-card-title">订单状态分布</h3>
</div> </div>
<div ref="statusChartEl" class="rr-chart"></div> <StatusPie :slices="statusSlices" />
</div> </div>
</div> </div>
@ -338,7 +264,7 @@ window.addEventListener('resize', () => { trendChart?.resize(); statusChart?.res
<style scoped lang="scss"> <style scoped lang="scss">
.rr-dashboard { max-width: 1400px; } .rr-dashboard { max-width: 1400px; }
.rr-page-header { margin-bottom: 20px; } .rr-page-header { margin-bottom: 20px; }
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; } .rr-page-title { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; 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-api-badge { margin-left: 8px; }
.rr-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; } .rr-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
/**
* 统计页 - 订单状态分布环形图 (2026-08-18 从 StatisticsView 抽出)
* echarts 动态 import → 独立 chunk 懒加载
*/
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
defineOptions({ name: 'EchartsStatusPie' })
const props = defineProps<{ slices: { name: string; value: number; color?: string }[] }>()
const el = ref<HTMLDivElement | null>(null)
let chart: any = null
let echartsMod: any = null
const themeStore = useThemeStore()
const total = computed(() => props.slices.reduce((a, b) => a + b.value, 0))
async function ensureEcharts() {
if (echartsMod) return echartsMod
const core = await import('echarts/core')
const { PieChart } = await import('echarts/charts')
const { TooltipComponent, LegendComponent } = await import('echarts/components')
const { CanvasRenderer } = await import('echarts/renderers')
core.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
echartsMod = core
return core
}
function cssVar(name: string, fb: string): string {
return getComputedStyle(document.body).getPropertyValue(name).trim() || fb
}
function getChartColors(): string[] {
return ['--chart-1', '--chart-2', '--chart-3', '--chart-4', '--red'].map(v => cssVar(v, '')) || ['#2563EB', '#7C3AED', '#10B981', '#D97706', '#DC2626']
}
function render() {
if (!el.value || !chart) return
const ink = cssVar('--ink-3', '#64748B')
const border = cssVar('--border', '#E2E8F0')
const surface = cssVar('--surface', '#FFFFFF')
const colors = getChartColors()
chart.setOption({
tooltip: { trigger: 'item', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
legend: { bottom: 0, textStyle: { color: ink } },
series: [{
type: 'pie', radius: ['58%', '80%'], center: ['50%', '45%'],
avoidLabelOverlap: true,
label: { show: false }, labelLine: { show: false },
data: props.slices.map((s, i) => ({ name: s.name, value: s.value, itemStyle: { color: colors[i % colors.length] } })),
emphasis: { label: { show: true, fontSize: 14, fontWeight: 600, color: ink } }
}],
graphic: total.value > 0 ? [
{ type: 'text', left: 'center', top: '38%', style: { text: '总计', fontSize: 10, fill: ink, opacity: 0.5 } },
{ type: 'text', left: 'center', top: '45%', style: { text: String(total.value), fontSize: 24, fontWeight: 700, fill: ink } }
] : []
}, true)
}
function onResize() { chart?.resize() }
onMounted(async () => {
try {
const mod = await ensureEcharts()
if (el.value) {
chart = mod.init(el.value)
render()
}
} catch { /* 懒加载失败静默 */ }
window.addEventListener('resize', onResize)
})
watch(
() => [props.slices, themeStore.current],
() => render(),
{ deep: true }
)
onUnmounted(() => {
window.removeEventListener('resize', onResize)
chart?.dispose()
chart = null
})
</script>
<template>
<div ref="el" class="rr-chart"></div>
</template>
<style scoped>
.rr-chart { width: 100%; height: 300px; }
</style>

View File

@ -0,0 +1,89 @@
<script setup lang="ts">
/**
* 统计页 - 订单趋势折线图 (2026-08-18 从 StatisticsView 抽出)
* echarts 动态 import → 独立 chunk 懒加载 (StatisticsView 主 chunk 减重)
* 主题/数据变化自动重绘
*/
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useThemeStore } from '@/stores/theme'
defineOptions({ name: 'EchartsTrendChart' })
const props = defineProps<{ dates: string[]; counts: number[]; amounts: number[] }>()
const el = ref<HTMLDivElement | null>(null)
let chart: any = null
let echartsMod: any = null
const themeStore = useThemeStore()
/** 动态加载 echarts (独立 chunk, 不随主 bundle) */
async function ensureEcharts() {
if (echartsMod) return echartsMod
const core = await import('echarts/core')
const { LineChart } = await import('echarts/charts')
const { TooltipComponent, GridComponent, LegendComponent } = await import('echarts/components')
const { CanvasRenderer } = await import('echarts/renderers')
core.use([LineChart, TooltipComponent, GridComponent, LegendComponent, CanvasRenderer])
echartsMod = core
return core
}
function cssVar(name: string, fb: string): string {
return getComputedStyle(document.body).getPropertyValue(name).trim() || fb
}
function render() {
if (!el.value || !chart) return
const ink = cssVar('--ink-3', '#64748B')
const border = cssVar('--border', '#E2E8F0')
const surface = cssVar('--surface', '#FFFFFF')
const brand = cssVar('--brand', '#2563EB')
const chart2 = cssVar('--chart-2', '#7C3AED')
chart.setOption({
grid: { left: 50, right: 50, top: 40, bottom: 30 },
tooltip: { trigger: 'axis', backgroundColor: surface, borderColor: border, textStyle: { color: ink } },
legend: { top: 0, textStyle: { color: ink } },
xAxis: { type: 'category', data: props.dates, axisLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
yAxis: [
{ type: 'value', name: '订单数', position: 'left', splitLine: { lineStyle: { color: border } }, axisLabel: { color: ink } },
{ type: 'value', name: '交易额', position: 'right', splitLine: { show: false }, axisLabel: { color: ink, formatter: (v: number) => v >= 1000 ? `${(v / 1000).toFixed(0)}K` : String(v) } }
],
series: [
{ name: '订单数', type: 'line', data: props.counts, smooth: true, itemStyle: { color: brand }, lineStyle: { color: brand, width: 2 }, areaStyle: { color: brand, opacity: 0.15 } },
{ name: '交易额', type: 'line', yAxisIndex: 1, data: props.amounts, smooth: true, itemStyle: { color: chart2 }, lineStyle: { color: chart2, width: 2 } }
]
}, true)
}
function onResize() { chart?.resize() }
onMounted(async () => {
try {
const mod = await ensureEcharts()
if (el.value) {
chart = mod.init(el.value)
render()
}
} catch { /* echarts 懒加载失败时静默 (图表留空) */ }
window.addEventListener('resize', onResize)
})
watch(
() => [props.dates, props.counts, props.amounts, themeStore.current],
() => render()
)
onUnmounted(() => {
window.removeEventListener('resize', onResize)
chart?.dispose()
chart = null
})
</script>
<template>
<div ref="el" class="rr-chart"></div>
</template>
<style scoped>
.rr-chart { width: 100%; height: 300px; }
</style>

View File

@ -226,7 +226,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; } .rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }

View File

@ -229,7 +229,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -213,7 +213,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -137,7 +137,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; } .rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }

View File

@ -275,7 +275,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; } .rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }

View File

@ -260,7 +260,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -221,7 +221,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -254,7 +254,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -189,7 +189,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -193,7 +193,7 @@ onMounted(load)
.rr-muted { color: var(--ink-3); font-size: 12px; } .rr-muted { color: var(--ink-3); font-size: 12px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } .rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; } .rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); } .rr-page-title { margin: 0; font-size: 20px; font-weight: 700; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); } .rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; } .rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; } .rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }

View File

@ -86,19 +86,20 @@ const filters = reactive({
}) })
// status 映射 (老 vue: 1=待审核,2=已认证,3=已驳回) // status 映射 (老 vue: 1=待审核,2=已认证,3=已驳回)
// 状态/等级色引用全局 token (styles/global-ui.scss), 主题切换自动适配暗色
const STATUS_META: Record<string, { label: string; color: string; bg: string }> = { const STATUS_META: Record<string, { label: string; color: string; bg: string }> = {
'1': { label: '待审核', color: '#b45309', bg: '#fef3c7' }, '1': { label: '待审核', color: 'var(--st-warn-fg)', bg: 'var(--st-warn-bg)' },
'2': { label: '已认证', color: '#15803d', bg: '#dcfce7' }, '2': { label: '已认证', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
'3': { label: '已驳回', color: '#b91c1c', bg: '#fee2e2' } '3': { label: '已驳回', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
} }
// supplierLevel 映射 (老 vue: 1=优质,2=钻石,3=金牌,4=银牌,5=铜牌) // supplierLevel 映射 (老 vue: 1=优质,2=钻石,3=金牌,4=银牌,5=铜牌)
const LEVEL_META: Record<number, { label: string; color: string; bg: string }> = { const LEVEL_META: Record<number, { label: string; color: string; bg: string }> = {
1: { label: '优质供应商', color: '#92400e', bg: '#fef3c7' }, 1: { label: '优质供应商', color: 'var(--st-lv1-fg)', bg: 'var(--st-lv1-bg)' },
2: { label: '钻石供应商', color: '#6d28d9', bg: '#ede9fe' }, 2: { label: '钻石供应商', color: 'var(--st-lv2-fg)', bg: 'var(--st-lv2-bg)' },
3: { label: '金牌供应商', color: '#c2410c', bg: '#ffedd5' }, 3: { label: '金牌供应商', color: 'var(--st-lv3-fg)', bg: 'var(--st-lv3-bg)' },
4: { label: '银牌供应商', color: '#475569', bg: '#f1f5f9' }, 4: { label: '银牌供应商', color: 'var(--st-lv4-fg)', bg: 'var(--st-lv4-bg)' },
5: { label: '铜牌供应商', color: '#a16207', bg: '#fefce8' } 5: { label: '铜牌供应商', color: 'var(--st-lv5-fg)', bg: 'var(--st-lv5-bg)' }
} }
// 头像品牌渐变色组 (企业无 logo 时交替使用) // 头像品牌渐变色组 (企业无 logo 时交替使用)

99
src/styles/global-ui.scss Normal file
View File

@ -0,0 +1,99 @@
/* ============================================================
* 后台全局 UI 统一规范 (2026-08-18 建立)
* 由 UI 审查驱动, 解决跨页视觉一致性缺口:
* 1. 页面骨架统一 (rr-page 默认内边距)
* 2. 表格视觉统一 (表头底色/圆角/消除老页面 border 竖线)
* 3. 卡片圆角统一
* 4. 状态标签/el-tag 暗色主题适配
* 设计原则: 只做"兜底默认", 页面 scoped 样式优先覆盖
* ============================================================ */
/* ---------- 1. 页面骨架: 默认 16px 内边距 (页面未定义时生效) ---------- */
.rr-page { padding: 16px; }
/* ---------- 2. 表格视觉统一 ---------- */
/* 表头底色/文字统一 (el-table 全局) */
.el-table {
--el-table-header-bg-color: var(--surface-2, #f1f5f9);
--el-table-header-text-color: var(--ink-3, #64748b);
}
.el-table th.el-table__cell {
background: var(--surface-2, #f1f5f9) !important; /* !important 覆盖 EP .el-table--border 表头白底 */
color: var(--ink-3, #64748b);
font-weight: 600;
}
/* 单元格内边距统一 (与 DataTable 规范一致) */
.el-table td.el-table__cell { padding: 10px 12px; }
/* 消除老页面裸 el-table border 属性产生的边框/竖线 → 统一 borderless 视觉 */
.el-table--border,
.el-table--border .el-table__cell,
.el-table--border th.el-table__cell,
.el-table--border td.el-table__cell {
border: none !important;
}
.el-table--border .el-table__inner-wrapper::after,
.el-table--border .el-table__inner-wrapper::before,
.el-table::before,
.el-table::after {
display: none !important;
}
/* ---------- 3. 卡片圆角统一 ---------- */
.rr-list-card,
.rr-card,
.info-card {
border-radius: 12px;
}
/* ---------- 4. 状态色暗色适配 ---------- */
/* 状态语义色 token (亮色默认) — 各页面状态标签通过 var() 引用, 主题切换自动适配 */
:root,
[data-theme="tech-blue"],
[data-theme="red"],
[data-theme="purple"] {
--st-warn-fg: #b45309; --st-warn-bg: #fef3c7;
--st-ok-fg: #15803d; --st-ok-bg: #dcfce7;
--st-err-fg: #b91c1c; --st-err-bg: #fee2e2;
--st-lv1-fg: #92400e; --st-lv1-bg: #fef3c7;
--st-lv2-fg: #6d28d9; --st-lv2-bg: #ede9fe;
--st-lv3-fg: #c2410c; --st-lv3-bg: #ffedd5;
--st-lv4-fg: #475569; --st-lv4-bg: #f1f5f9;
--st-lv5-fg: #a16207; --st-lv5-bg: #fefce8;
}
[data-theme="dark"] {
--st-warn-fg: #fcd34d; --st-warn-bg: rgba(245, 158, 11, 0.16);
--st-ok-fg: #6ee7b7; --st-ok-bg: rgba(16, 185, 129, 0.16);
--st-err-fg: #fca5a5; --st-err-bg: rgba(239, 68, 68, 0.16);
--st-lv1-fg: #fcd34d; --st-lv1-bg: rgba(245, 158, 11, 0.14);
--st-lv2-fg: #c4b5fd; --st-lv2-bg: rgba(109, 40, 217, 0.2);
--st-lv3-fg: #fdba74; --st-lv3-bg: rgba(194, 65, 12, 0.2);
--st-lv4-fg: #cbd5e1; --st-lv4-bg: rgba(100, 116, 139, 0.2);
--st-lv5-fg: #fde047; --st-lv5-bg: rgba(161, 98, 7, 0.2);
/* el-tag 四态: 暗色下深底浅字 */
.el-tag--success {
--el-tag-bg-color: rgba(16, 185, 129, 0.16);
--el-tag-text-color: #6ee7b7;
--el-tag-border-color: rgba(16, 185, 129, 0.4);
}
.el-tag--warning {
--el-tag-bg-color: rgba(245, 158, 11, 0.16);
--el-tag-text-color: #fcd34d;
--el-tag-border-color: rgba(245, 158, 11, 0.4);
}
.el-tag--danger {
--el-tag-bg-color: rgba(239, 68, 68, 0.16);
--el-tag-text-color: #fca5a5;
--el-tag-border-color: rgba(239, 68, 68, 0.4);
}
.el-tag--info {
--el-tag-bg-color: rgba(148, 163, 184, 0.16);
--el-tag-text-color: #cbd5e1;
--el-tag-border-color: rgba(148, 163, 184, 0.4);
}
/* 老式 rr-tag-* 状态类 (浅底深字 -> 深底浅字) */
.rr-tag-warn { background: rgba(245, 158, 11, 0.16); color: #fcd34d; }
.rr-tag-ok { background: rgba(16, 185, 129, 0.16); color: #6ee7b7; }
.rr-tag-err { background: rgba(239, 68, 68, 0.16); color: #fca5a5; }
}