SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)

恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
<script setup>
import ElementPlusShowcase from './components/ElementPlusShowcase.vue'
</script>
<template>
<ElementPlusShowcase />
</template>
@@ -0,0 +1,114 @@
<script setup>
import { Download, Refresh } from '@element-plus/icons-vue'
import { computed, ref } from 'vue'
import { navigateTo, useHashRoute } from '../composables/useHashRoute'
import { docPages } from '../data/showcaseData'
import ButtonShowcase from './showcase/ButtonShowcase.vue'
import ComponentCatalogShowcase from './showcase/ComponentCatalogShowcase.vue'
import DocumentDrawer from './showcase/DocumentDrawer.vue'
import FormShowcase from './showcase/FormShowcase.vue'
import FoundationShowcase from './showcase/FoundationShowcase.vue'
import MetricsShowcase from './showcase/MetricsShowcase.vue'
import NavigationShowcase from './showcase/NavigationShowcase.vue'
import StatusShowcase from './showcase/StatusShowcase.vue'
import TableShowcase from './showcase/TableShowcase.vue'
import TokenRail from './showcase/TokenRail.vue'
const { route } = useHashRoute()
const drawerVisible = ref(false)
const showAdvanced = ref(true)
const checkValues = ref(['overdue', 'inventory'])
const density = ref('standard')
const sliderValue = ref(68)
const dateRange = ref('')
const selectedWarehouse = ref('central')
const currentPage = ref(1)
const orderStatus = ref('approved')
const note = ref('客户要求分批发货,财务需要在出库前完成信用检查。')
const componentStats = computed(() => [
{ label: '组件族', value: '8' },
{ label: '基础 Token', value: '34' },
{ label: '表格密度', value: '紧凑' },
])
const activeComponentSlug = computed(() => (route.value.segments[0] === 'component' ? route.value.segments[1] : ''))
const activePage = computed(() => {
if (activeComponentSlug.value) return 'catalog'
const pageFromRoute = route.value.segments[0] === 'page' ? route.value.segments[1] : ''
return docPages.some((page) => page.id === pageFromRoute) ? pageFromRoute : 'navigation'
})
const activePageMeta = computed(() => docPages.find((page) => page.id === activePage.value) ?? docPages[0])
function handleMenuSelect(pageId) {
navigateTo(`page/${pageId}`)
}
</script>
<template>
<el-config-provider>
<div class="lab-shell">
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">E</div>
<div>
<div class="brand-title">Element ERP Lab</div>
<div class="brand-subtitle">OFBiz 前端组件定制</div>
</div>
</div>
<el-menu :default-active="activePage" class="lab-menu" @select="handleMenuSelect">
<el-menu-item v-for="page in docPages" :key="page.id" :index="page.id">
<el-icon><component :is="page.icon" /></el-icon>
<span>{{ page.label }}</span>
</el-menu-item>
</el-menu>
<div class="sidebar-footer">
<div v-for="stat in componentStats" :key="stat.label" class="sidebar-stat">
<span>{{ stat.label }}</span>
<strong>{{ stat.value }}</strong>
</div>
</div>
</aside>
<main class="workspace">
<header class="topbar">
<div>
<p class="eyebrow">Design System Draft</p>
<h1>{{ activePageMeta.title }}</h1>
<p class="page-summary">{{ activePageMeta.description }}</p>
</div>
<div class="topbar-actions">
<el-button :icon="Refresh">同步 Token</el-button>
<el-button :icon="Download" type="primary">导出规范</el-button>
</div>
</header>
<FoundationShowcase v-if="activePage === 'foundations'" />
<ComponentCatalogShowcase
v-else-if="activePage === 'catalog'"
:component-slug="activeComponentSlug"
/>
<MetricsShowcase v-else-if="activePage === 'metrics'" />
<ButtonShowcase v-else-if="activePage === 'buttons'" v-model:density="density" />
<FormShowcase
v-else-if="activePage === 'forms'"
v-model:check-values="checkValues"
v-model:date-range="dateRange"
v-model:note="note"
v-model:order-status="orderStatus"
v-model:selected-warehouse="selectedWarehouse"
v-model:show-advanced="showAdvanced"
v-model:slider-value="sliderValue"
/>
<TableShowcase v-else-if="activePage === 'table'" v-model:current-page="currentPage" />
<StatusShowcase v-else-if="activePage === 'status'" />
<NavigationShowcase v-else-if="activePage === 'navigation'" @open-drawer="drawerVisible = true" />
</main>
<TokenRail />
<DocumentDrawer v-model="drawerVisible" />
</div>
</el-config-provider>
</template>
@@ -0,0 +1,46 @@
<script setup>
import { ref } from 'vue'
defineProps({
code: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
eyebrow: {
type: String,
default: 'Component',
},
title: {
type: String,
required: true,
},
})
const initialView = new URLSearchParams(window.location.search).get('view')
const activeTab = ref(initialView === 'code' ? 'code' : 'demo')
</script>
<template>
<section class="demo-block">
<div class="section-heading compact">
<div>
<p class="eyebrow">{{ eyebrow }}</p>
<h2>{{ title }}</h2>
<p v-if="description" class="demo-description">{{ description }}</p>
</div>
</div>
<el-tabs v-model="activeTab" class="demo-tabs">
<el-tab-pane label="展示" name="demo">
<slot />
</el-tab-pane>
<el-tab-pane label="代码" name="code">
<pre class="code-block"><code>{{ code }}</code></pre>
</el-tab-pane>
</el-tabs>
</section>
</template>
@@ -0,0 +1,82 @@
<script setup>
import {
ArrowDown,
Check,
Close,
Download,
Filter,
MoreFilled,
Plus,
Printer,
Search,
Warning,
} from '@element-plus/icons-vue'
import DemoBlock from '../docs/DemoBlock.vue'
defineProps({
density: {
type: String,
required: true,
},
})
defineEmits(['update:density'])
const buttonCode = `<el-button :icon="Search">查询</el-button>
<el-button :icon="Filter">高级筛选</el-button>
<el-button :icon="Plus" type="primary">新建订单</el-button>
<el-button :icon="Check" type="success">审批通过</el-button>
<el-button :icon="Warning" type="warning">标记异常</el-button>
<el-button :icon="Close" type="danger">作废</el-button>
<el-button-group>
<el-button :icon="Printer">打印</el-button>
<el-button :icon="Download">导出</el-button>
<el-button :icon="MoreFilled" />
</el-button-group>`
</script>
<template>
<DemoBlock
:code="buttonCode"
description="按钮层级控制主动作、辅助动作、危险动作和批量动作。"
eyebrow="Buttons"
title="按钮与动作层级"
>
<div class="section-heading">
<el-segmented
:model-value="density"
:options="['compact', 'standard', 'spacious']"
@update:model-value="$emit('update:density', $event)"
/>
</div>
<div class="component-row">
<el-button :icon="Search">查询</el-button>
<el-button :icon="Filter">高级筛选</el-button>
<el-button :icon="Plus" type="primary">新建订单</el-button>
<el-button :icon="Check" type="success">审批通过</el-button>
<el-button :icon="Warning" type="warning">标记异常</el-button>
<el-button :icon="Close" type="danger">作废</el-button>
</div>
<div class="component-row">
<el-button-group>
<el-button :icon="Printer">打印</el-button>
<el-button :icon="Download">导出</el-button>
<el-button :icon="MoreFilled" />
</el-button-group>
<el-dropdown>
<el-button>
更多操作
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>复制为新订单</el-dropdown-item>
<el-dropdown-item>生成采购需求</el-dropdown-item>
<el-dropdown-item divided>查看审计记录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</DemoBlock>
</template>
@@ -0,0 +1,995 @@
<script setup>
import { computed, h, ref, watch } from 'vue'
import {
Files,
Goods,
Money,
Plus,
Search,
Tickets,
} from '@element-plus/icons-vue'
import { navigateTo, useHashRoute } from '../../composables/useHashRoute'
import {
elementPlusDirectiveRecords,
elementPlusInventoryMeta,
elementPlusPublicExportRecords,
} from '../../data/elementPlusInventory'
import { erpComponentDocs, erpComponentUsageMap, erpLayoutPlaybook } from '../../data/erpComponentDocs'
import { getRiskClass, orderRows } from '../../data/showcaseData'
const props = defineProps({
componentSlug: {
type: String,
default: '',
},
})
const { route } = useHashRoute()
const activeGroup = ref(route.value.params.get('group') || '全部')
const searchText = ref('')
const cardViews = ref({})
const detailView = ref(route.value.params.get('view') || 'demo')
const selectedRows = ref([])
const sampleDateRange = ref('')
const sampleChoice = ref(['overdue'])
const sampleRadio = ref('approved')
const sampleInput = ref('')
const sampleNumber = ref(128)
const sampleSelect = ref('east')
const sampleTags = ref(['重点客户'])
const sampleTime = ref('')
const sampleColor = ref('#2764e7')
const sampleRate = ref(3)
const sampleSlider = ref(68)
const sampleSwitch = ref(true)
const transferValue = ref([1])
const activeTabs = ref('summary')
const activeCollapse = ref(['base'])
const activeMenu = ref('orders')
const activeCheckTag = ref(true)
const warehouseOptions = [
{ label: '华东中心仓', value: 'east' },
{ label: '华南备货仓', value: 'south' },
{ label: '财务共享中心', value: 'finance' },
]
const treeData = [
{
label: '集团总部',
children: [
{ label: '销售中心' },
{ label: '采购中心' },
{ label: '华东中心仓' },
],
},
]
const cascaderOptions = [
{
value: 'east',
label: '华东区域',
children: [{ value: 'shanghai', label: '上海仓' }],
},
]
const transferData = [
{ key: 1, label: '销售订单' },
{ key: 2, label: '采购需求' },
{ key: 3, label: '库存预警' },
{ key: 4, label: '财务应收' },
]
const v2Columns = [
{ key: 'orderId', dataKey: 'orderId', title: '单据号', width: 140 },
{ key: 'customer', dataKey: 'customer', title: '客户', width: 220 },
{ key: 'amount', dataKey: 'amount', title: '金额', width: 140, align: 'right' },
]
const groupedCounts = computed(() => {
const groups = ['全部', ...new Set(erpComponentDocs.map((component) => component.group))]
return groups.map((group) => ({
group,
count: group === '全部'
? erpComponentDocs.length
: erpComponentDocs.filter((component) => component.group === group).length,
}))
})
const selectedComponent = computed(() =>
erpComponentDocs.find((component) => component.slug === props.componentSlug)
)
const filteredComponents = computed(() => {
const scoped = activeGroup.value === '全部'
? erpComponentDocs
: erpComponentDocs.filter((component) => component.group === activeGroup.value)
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return scoped
return scoped.filter((component) =>
[
component.name,
component.slug,
component.tag,
component.exportName,
component.group,
component.wrapperName,
component.localClass,
].some((value) => value.toLowerCase().includes(keyword))
)
})
const catalogSummary = computed(() => [
{
label: 'ERP 文档层',
value: erpComponentDocs.length,
note: '每个组件都有本地用法、代码和样式入口',
},
{
label: '高优先级',
value: erpComponentDocs.filter((component) => component.priority === '高').length,
note: '先沉淀为 OFBiz wrapper 或固定 pattern',
},
{
label: '使用场景',
value: erpComponentUsageMap.length,
note: '从业务场景反查组件,不从组件名硬猜',
},
{
label: '页面模板',
value: erpLayoutPlaybook.length,
note: '列表、详情、录入、流程四类骨架',
},
])
const groupedFilteredComponents = computed(() => {
const groupOrder = [...new Set(erpComponentDocs.map((component) => component.group))]
return groupOrder
.map((group) => ({
group,
components: filteredComponents.value.filter((component) => component.group === group),
}))
.filter((section) => section.components.length > 0)
})
const relatedComponents = computed(() => {
if (!selectedComponent.value) return []
return erpComponentDocs
.filter((component) => component.group === selectedComponent.value.group && component.slug !== selectedComponent.value.slug)
.slice(0, 10)
})
const priorityComponents = computed(() =>
erpComponentDocs.filter((component) => component.priority === '高').slice(0, 18)
)
const layoutPrinciples = [
{
title: '先结构后控件',
body: '页面先确定导航、标题、筛选、内容、分页和反馈位置,再选择具体组件。',
},
{
title: '密度按任务分层',
body: '查询和表格保持紧凑,详情和确认弹层保留阅读间距,指标只用大/小/最小三档。',
},
{
title: '主动作只留一个',
body: '同一工作区只保留一个 primary,次要动作降级为普通按钮、link 或 dropdown。',
},
{
title: '状态必须可读',
body: '颜色只做辅助,Tag、Alert、Timeline、Progress 都要有明确文字说明。',
},
{
title: '代码随组件走',
body: '每个组件页保留原始代码、ERP 代码和本地样式,复用 CSS 不散落到业务页。',
},
{
title: '页面模板先行',
body: '改 OFBiz 页面时先选列表、详情、录入或流程模板,再补具体控件。',
},
]
watch(
() => route.value.params.get('group'),
(group) => {
activeGroup.value = group || '全部'
}
)
watch(
() => route.value.params.get('view'),
(view) => {
detailView.value = view || 'demo'
}
)
function viewFor(slug) {
return cardViews.value[slug] ?? 'demo'
}
function setCardView(slug, view) {
cardViews.value = {
...cardViews.value,
[slug]: view,
}
}
function selectGroup(group) {
activeGroup.value = group
navigateTo('page/catalog', group === '全部' ? {} : { group })
}
function openComponent(slug, view = 'demo') {
navigateTo(`component/${slug}`, view === 'demo' ? {} : { view })
}
function updateDetailView(view) {
detailView.value = view
openComponent(selectedComponent.value.slug, view)
}
function renderMiniPreview(component) {
const kind = component.previewKind
if (kind === 'table' || kind === 'table-v2') {
return h('div', { class: 'mini-table-preview' }, [
h('span', 'SO-10642'),
h('span', '已审批'),
h('strong', '¥128k'),
])
}
if (kind === 'menu') {
return h('div', { class: 'mini-menu-preview' }, [
h('span', '销售管理'),
h('span', '销售订单'),
])
}
if (kind === 'form' || kind === 'field' || kind === 'select' || kind === 'date' || kind === 'lookup') {
return h('div', { class: 'mini-field-preview' }, [
h('span', component.name),
h('small', component.localClass),
])
}
if (kind === 'button' || kind === 'dropdown') {
return h('div', { class: 'mini-action-preview' }, [
h('span', '查询'),
h('strong', '新建'),
])
}
if (kind === 'breadcrumb') {
return h('div', { class: 'mini-breadcrumb-preview' }, ['销售 / 订单 / 详情'])
}
if (kind === 'status' || kind === 'feedback' || kind === 'service' || kind === 'overlay') {
return h('div', { class: 'mini-status-preview' }, [
h('span', component.name),
h('small', 'warning'),
])
}
if (['empty', 'progress', 'skeleton', 'calendar', 'timeline', 'steps'].includes(kind)) {
return h('div', { class: 'mini-status-preview' }, [
h('span', component.name),
h('small', 'state'),
])
}
return h('div', { class: 'mini-generic-preview' }, [
h('span', component.name),
h('small', component.family),
])
}
const MiniPreview = {
props: {
component: {
type: Object,
required: true,
},
},
setup(miniProps) {
return () => renderMiniPreview(miniProps.component)
},
}
</script>
<template>
<div v-if="selectedComponent" class="component-detail-page">
<section class="demo-block component-detail-hero">
<div class="component-detail-head">
<div>
<el-button link type="primary" @click="navigateTo('page/catalog')">
返回全组件目录
</el-button>
<p class="eyebrow">{{ selectedComponent.group }} / {{ selectedComponent.family }}</p>
<h2>{{ selectedComponent.name }}</h2>
<p class="demo-description">
{{ selectedComponent.tag }} / {{ selectedComponent.exportName }} / wrapper: {{ selectedComponent.wrapperName }}
</p>
</div>
<div class="component-doc-card__tags">
<el-tag effect="plain">{{ selectedComponent.group }}</el-tag>
<el-tag :type="selectedComponent.priority === '高' ? 'primary' : 'info'" effect="light">
{{ selectedComponent.priority }}
</el-tag>
<el-tag v-if="selectedComponent.parentSlug" type="warning" effect="plain">
跟随 {{ selectedComponent.parentSlug }}
</el-tag>
</div>
</div>
</section>
<section class="demo-block">
<el-tabs :model-value="detailView" class="component-detail-tabs" @update:model-value="updateDetailView">
<el-tab-pane label="展示" name="demo">
<div class="component-demo-layout">
<div class="component-live-preview">
<template v-if="selectedComponent.previewKind === 'table'">
<el-table :data="orderRows" class="erp-table" stripe>
<el-table-column fixed prop="orderId" label="单据号" width="126" />
<el-table-column prop="customer" label="客户 / 对象" min-width="180" />
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.statusType" effect="light">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="140" align="right" />
<el-table-column prop="risk" label="风险" width="90">
<template #default="{ row }">
<span :class="['risk-dot', getRiskClass(row.risk)]">{{ row.risk }}</span>
</template>
</el-table-column>
</el-table>
</template>
<template v-else-if="selectedComponent.previewKind === 'table-v2'">
<el-table-v2
class="erp-virtual-table"
:columns="v2Columns"
:data="orderRows"
:height="260"
:width="760"
/>
</template>
<template v-else-if="selectedComponent.previewKind === 'pagination'">
<el-pagination
class="erp-dense-pagination"
layout="prev, pager, next, sizes"
:page-size="20"
:page-sizes="[20, 50, 100]"
:total="128"
/>
</template>
<template v-else-if="selectedComponent.previewKind === 'menu'">
<div class="detail-menu-preview">
<el-menu class="erp-side-menu" :default-active="activeMenu" @select="activeMenu = $event">
<el-sub-menu index="sales">
<template #title>
<el-icon><Tickets /></el-icon>
<span>销售管理</span>
</template>
<el-menu-item index="orders">销售订单</el-menu-item>
<el-menu-item index="returns">退货处理</el-menu-item>
</el-sub-menu>
<el-menu-item index="inventory">
<el-icon><Goods /></el-icon>
<span>库存管理</span>
</el-menu-item>
<el-menu-item index="finance">
<el-icon><Money /></el-icon>
<span>财务应收</span>
</el-menu-item>
</el-menu>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'breadcrumb'">
<div class="breadcrumb-demo">
<el-breadcrumb class="erp-breadcrumb" separator="/">
<el-breadcrumb-item>销售管理</el-breadcrumb-item>
<el-breadcrumb-item>销售订单</el-breadcrumb-item>
<el-breadcrumb-item>SO-10642</el-breadcrumb-item>
</el-breadcrumb>
<el-page-header class="erp-page-header" title="返回订单列表" content="SO-10642 销售订单" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'form'">
<el-form class="erp-form" label-position="top">
<div class="form-grid">
<el-form-item label="客户 / 供应商">
<el-input v-model="sampleInput" placeholder="输入名称、编码或联系人" :prefix-icon="Search" />
</el-form-item>
<el-form-item label="仓库">
<el-select v-model="sampleSelect">
<el-option label="华东中心仓" value="east" />
<el-option label="华南备货仓" value="south" />
</el-select>
</el-form-item>
<el-form-item label="业务日期">
<el-date-picker v-model="sampleDateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" />
</el-form-item>
</div>
</el-form>
</template>
<template v-else-if="selectedComponent.previewKind === 'field'">
<div class="detail-field-grid">
<el-input v-model="sampleInput" placeholder="搜索订单、客户、负责人" :prefix-icon="Search" />
<el-input-number v-model="sampleNumber" :min="0" />
<el-input-tag v-model="sampleTags" placeholder="输入标签" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'select'">
<div class="detail-field-grid">
<el-select v-model="sampleSelect" placeholder="选择仓库">
<el-option label="华东中心仓" value="east" />
<el-option label="华南备货仓" value="south" />
</el-select>
<el-select-v2 v-model="sampleSelect" :options="warehouseOptions" placeholder="虚拟选择器" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'date'">
<div class="detail-field-grid">
<el-date-picker v-model="sampleDateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" />
<el-time-picker v-model="sampleTime" placeholder="选择时间" />
<el-time-select v-model="sampleTime" start="08:30" step="00:30" end="18:30" placeholder="选择时段" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'lookup'">
<div class="detail-field-grid">
<el-autocomplete v-model="sampleInput" placeholder="搜索客户编码、名称或联系人" />
<el-cascader v-model="selectedRows" :options="cascaderOptions" placeholder="选择组织 / 仓库" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'choice'">
<div class="detail-choice-stack">
<el-checkbox-group v-model="sampleChoice">
<el-checkbox-button label="overdue">逾期</el-checkbox-button>
<el-checkbox-button label="inventory">库存不足</el-checkbox-button>
<el-checkbox-button label="credit">信用风险</el-checkbox-button>
</el-checkbox-group>
<el-radio-group v-model="sampleRadio">
<el-radio-button label="draft">草稿</el-radio-button>
<el-radio-button label="approved">已审批</el-radio-button>
<el-radio-button label="shipping">待发货</el-radio-button>
</el-radio-group>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'button'">
<div class="component-row">
<el-button :icon="Search">查询</el-button>
<el-button :icon="Plus" type="primary">新建订单</el-button>
<el-button type="warning">标记异常</el-button>
<el-button type="danger">作废</el-button>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'dropdown'">
<el-dropdown class="erp-action-dropdown">
<el-button>更多操作</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>复制为新订单</el-dropdown-item>
<el-dropdown-item>生成采购需求</el-dropdown-item>
<el-dropdown-item divided>查看审计记录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else-if="selectedComponent.previewKind === 'status'">
<div class="tag-grid">
<el-tag type="success" effect="light">已审批</el-tag>
<el-tag type="warning" effect="light">信用检查</el-tag>
<el-tag type="danger" effect="light">逾期</el-tag>
<el-check-tag v-model:checked="activeCheckTag">仅看异常</el-check-tag>
<el-badge :value="8"><el-button>待办</el-button></el-badge>
</div>
<el-alert title="信用检查未完成" type="warning" show-icon :closable="false" />
</template>
<template v-else-if="selectedComponent.previewKind === 'descriptions'">
<el-descriptions class="erp-detail-descriptions" :column="3" border>
<el-descriptions-item label="客户">上海辰光零售</el-descriptions-item>
<el-descriptions-item label="账期">Net 30</el-descriptions-item>
<el-descriptions-item label="币种">CNY</el-descriptions-item>
<el-descriptions-item label="仓库">华东中心仓</el-descriptions-item>
<el-descriptions-item label="审批状态">已审批</el-descriptions-item>
</el-descriptions>
</template>
<template v-else-if="selectedComponent.previewKind === 'overlay'">
<div class="overlay-static-preview">
<header>{{ selectedComponent.name }}</header>
<p>确认通过该订单系统会写入审批审计记录</p>
<footer>
<el-button>取消</el-button>
<el-button type="primary">确认</el-button>
</footer>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'service'">
<div class="service-preview">
<el-alert title="服务类组件通过函数触发,不直接在模板中常驻" type="info" show-icon :closable="false" />
<el-button type="primary">触发 {{ selectedComponent.name }}</el-button>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'tabs'">
<el-tabs v-model="activeTabs" class="erp-tabs">
<el-tab-pane label="摘要" name="summary">订单摘要金额和履约状态</el-tab-pane>
<el-tab-pane label="明细" name="lines">商品行税额折扣和发运计划</el-tab-pane>
<el-tab-pane label="审计" name="audit">审批修改通知记录</el-tab-pane>
</el-tabs>
</template>
<template v-else-if="selectedComponent.previewKind === 'tree'">
<div class="detail-field-grid">
<el-tree class="erp-tree" :data="treeData" default-expand-all />
<el-tree-select v-model="sampleSelect" class="erp-tree-select" :data="treeData" placeholder="选择组织" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'card'">
<section class="erp-entity-card">
<header>订单摘要</header>
<strong>SO-10642</strong>
<p>上海辰光零售 / ¥128,420.00 / 已审批</p>
</section>
</template>
<template v-else-if="selectedComponent.previewKind === 'layout'">
<div class="layout-static-preview">
<aside>模块导航</aside>
<main>业务内容区</main>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'data'">
<div class="data-static-preview">
<el-statistic title="待处理订单" :value="128" />
<el-progress :percentage="72" />
<el-empty description="暂无异常数据" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'empty'">
<el-empty class="erp-empty-state" description="暂无符合条件的单据">
<el-button type="primary">新建订单</el-button>
</el-empty>
</template>
<template v-else-if="selectedComponent.previewKind === 'progress'">
<div class="detail-status-stack">
<el-progress class="erp-progress" :percentage="72" />
<el-progress class="erp-progress" :percentage="36" status="warning" />
<el-progress class="erp-progress" :percentage="88" status="success" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'result'">
<el-result class="erp-result-state" icon="success" title="保存成功" sub-title="销售订单 SO-10642 已写入审批记录">
<template #extra>
<el-button type="primary">查看订单</el-button>
<el-button>返回列表</el-button>
</template>
</el-result>
</template>
<template v-else-if="selectedComponent.previewKind === 'skeleton'">
<div class="erp-skeleton">
<el-skeleton :rows="4" animated />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'calendar'">
<div class="calendar-preview">
<el-calendar class="erp-calendar" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'timeline'">
<el-timeline class="erp-timeline">
<el-timeline-item timestamp="09:20" type="success">销售订单完成审批</el-timeline-item>
<el-timeline-item timestamp="10:05" type="primary">库存锁定 18 </el-timeline-item>
<el-timeline-item timestamp="10:44" type="warning">触发信用额度复核</el-timeline-item>
</el-timeline>
</template>
<template v-else-if="selectedComponent.previewKind === 'steps'">
<el-steps class="erp-steps" :active="2" finish-status="success" align-center>
<el-step title="创建订单" />
<el-step title="信用检查" />
<el-step title="库存锁定" />
<el-step title="发运" />
</el-steps>
</template>
<template v-else-if="selectedComponent.previewKind === 'collapse'">
<el-collapse v-model="activeCollapse" class="erp-collapse">
<el-collapse-item title="基础信息" name="base">客户币种账期业务员</el-collapse-item>
<el-collapse-item title="履约信息" name="fulfillment">仓库计划日期发运批次</el-collapse-item>
</el-collapse>
</template>
<template v-else-if="selectedComponent.previewKind === 'media'">
<div class="media-preview">
<el-image
class="erp-media-preview"
fit="cover"
src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=360&q=80"
/>
<span>附件商品图或仓库现场图需要固定比例展示</span>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'carousel'">
<el-carousel class="erp-carousel" height="180px">
<el-carousel-item>
<div class="carousel-panel">待处理订单 128</div>
</el-carousel-item>
<el-carousel-item>
<div class="carousel-panel">库存预警 36</div>
</el-carousel-item>
</el-carousel>
</template>
<template v-else-if="selectedComponent.previewKind === 'typography'">
<div class="typography-preview">
<el-text class="erp-inline-text" type="primary">业务提示客户信用额度接近上限</el-text>
<el-link class="erp-inline-link" type="primary">查看信用明细</el-link>
<el-divider>审计记录</el-divider>
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'space'">
<el-space class="erp-space" wrap>
<el-button>查询</el-button>
<el-button type="primary">新建</el-button>
<el-tag type="warning" effect="light">信用检查</el-tag>
</el-space>
</template>
<template v-else-if="selectedComponent.previewKind === 'statistic'">
<div class="statistic-preview">
<el-statistic class="erp-statistic" title="待处理订单" :value="128" />
<el-statistic class="erp-statistic" title="今日发运" :value="214" />
<el-statistic class="erp-statistic" title="库存预警" :value="36" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'switch'">
<el-switch v-model="sampleSwitch" class="erp-switch" active-text="启用自动锁库" inactive-text="关闭" />
</template>
<template v-else-if="selectedComponent.previewKind === 'slider'">
<div class="detail-status-stack">
<span>优先级阈值 {{ sampleSlider }}</span>
<el-slider v-model="sampleSlider" class="erp-slider" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'rate'">
<div class="detail-status-stack">
<span>供应商评分</span>
<el-rate v-model="sampleRate" class="erp-rate" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'color'">
<div class="detail-status-stack">
<span>状态色映射</span>
<el-color-picker v-model="sampleColor" class="erp-color-picker" />
</div>
</template>
<template v-else-if="selectedComponent.previewKind === 'transfer'">
<el-transfer
v-model="transferValue"
class="erp-transfer"
:data="transferData"
:titles="['可选模块', '已授权']"
/>
</template>
<template v-else-if="selectedComponent.previewKind === 'upload'">
<el-upload class="erp-upload" action="#" drag>
<el-icon><Files /></el-icon>
<div>拖拽附件到此处或点击上传</div>
</el-upload>
</template>
<template v-else>
<div class="generic-component-preview">
<MiniPreview :component="selectedComponent" />
<p>{{ selectedComponent.usage }}</p>
</div>
</template>
</div>
<aside class="component-usage-panel">
<h3>本地化判断</h3>
<p>{{ selectedComponent.usage }}</p>
<div class="usage-rule-block">
<strong>什么时候用</strong>
<ul>
<li v-for="item in selectedComponent.whenToUse" :key="item">{{ item }}</li>
</ul>
</div>
<div class="usage-rule-block">
<strong>什么时候不用</strong>
<ul>
<li v-for="item in selectedComponent.avoidWhen" :key="item">{{ item }}</li>
</ul>
</div>
<div class="usage-rule-block">
<strong>排版规则</strong>
<ul>
<li v-for="item in selectedComponent.layoutRules" :key="item">{{ item }}</li>
</ul>
</div>
<dl>
<div>
<dt>本地 class</dt>
<dd>{{ selectedComponent.localClass }}</dd>
</div>
<div>
<dt>样式重点</dt>
<dd>{{ selectedComponent.styleFocus }}</dd>
</div>
<div>
<dt>落地文件</dt>
<dd>{{ selectedComponent.cssFiles.join(' / ') }}</dd>
</div>
</dl>
</aside>
</div>
</el-tab-pane>
<el-tab-pane label="原始代码" name="raw">
<pre class="code-block"><code>{{ selectedComponent.originalCode }}</code></pre>
</el-tab-pane>
<el-tab-pane label="ERP代码" name="code">
<pre class="code-block"><code>{{ selectedComponent.erpCode }}</code></pre>
</el-tab-pane>
<el-tab-pane label="本地样式" name="style">
<pre class="code-block"><code>{{ selectedComponent.styleCode }}</code></pre>
</el-tab-pane>
</el-tabs>
</section>
<section class="demo-block">
<div class="section-heading compact">
<div>
<p class="eyebrow">Related</p>
<h2>同组组件</h2>
<p class="demo-description">这些会复用同一组 ERP token密度和状态规则</p>
</div>
</div>
<div class="related-components">
<el-button
v-for="component in relatedComponents"
:key="component.slug"
@click="openComponent(component.slug)"
>
{{ component.name }}
</el-button>
</div>
</section>
</div>
<div v-else class="catalog-page">
<section class="demo-block">
<div class="section-heading">
<div>
<p class="eyebrow">Full Element Plus</p>
<h2>本地 Element Plus 全量组件</h2>
<p class="demo-description">
从本地 node_modules 生成完整清单再叠加 ERP 本地化文档层每张卡片可独立切展示/代码每个组件可进入独立页面
</p>
</div>
<div class="catalog-stats">
<el-tag effect="plain">版本{{ elementPlusInventoryMeta.packageVersion }}</el-tag>
<el-tag effect="plain">组件目录{{ elementPlusInventoryMeta.componentDirectories }}</el-tag>
<el-tag effect="plain">El* 导出{{ elementPlusInventoryMeta.publicElExports }}</el-tag>
<el-tag effect="plain">指令目录{{ elementPlusInventoryMeta.directiveDirectories }}</el-tag>
</div>
</div>
<div class="catalog-summary-grid">
<article v-for="item in catalogSummary" :key="item.label">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<p>{{ item.note }}</p>
</article>
</div>
<div class="catalog-filter">
<el-segmented
:model-value="activeGroup"
:options="groupedCounts.map((item) => item.group)"
@update:model-value="selectGroup"
/>
<el-input v-model="searchText" class="catalog-search" clearable placeholder="搜索 Table / Menu / Breadcrumb" />
<div class="catalog-counts">
<span v-for="item in groupedCounts" :key="item.group">{{ item.group }} {{ item.count }}</span>
</div>
</div>
</section>
<section class="demo-block layout-playbook">
<div class="section-heading compact">
<div>
<p class="eyebrow">Page Playbook</p>
<h2>OFBiz 页面组合模板</h2>
<p class="demo-description">先选页面骨架再选组件这样重构一个页面时结构密度动作位置会先被约束住</p>
</div>
</div>
<div class="playbook-grid">
<article v-for="play in erpLayoutPlaybook" :key="play.title" class="playbook-card">
<header>
<div>
<h3>{{ play.title }}</h3>
<p>{{ play.description }}</p>
</div>
<el-tag effect="plain">{{ play.density }}</el-tag>
</header>
<ol>
<li v-for="rule in play.rules" :key="rule">{{ rule }}</li>
</ol>
<div class="usage-map-links">
<button
v-for="component in play.components"
:key="component.slug"
type="button"
@click="openComponent(component.slug)"
>
{{ component.name }}
</button>
</div>
</article>
</div>
</section>
<section class="usage-map-grid">
<article v-for="section in erpComponentUsageMap" :key="section.title" class="usage-map-card">
<div>
<p class="eyebrow">Usage Map</p>
<h3>{{ section.title }}</h3>
<p>{{ section.description }}</p>
</div>
<div class="usage-map-links">
<button
v-for="component in section.components"
:key="component.slug"
type="button"
@click="openComponent(component.slug)"
>
{{ component.name }}
</button>
</div>
</article>
</section>
<section class="demo-block layout-principles">
<div class="section-heading compact">
<div>
<p class="eyebrow">Layout Rules</p>
<h2>ERP 页面排版规则</h2>
<p class="demo-description">先用这些规则约束页面再挑组件这样每个 OFBiz 页面会长得像同一个系统</p>
</div>
</div>
<div class="principle-grid">
<article v-for="principle in layoutPrinciples" :key="principle.title">
<strong>{{ principle.title }}</strong>
<p>{{ principle.body }}</p>
</article>
</div>
</section>
<section class="demo-block priority-map">
<div class="section-heading compact">
<div>
<p class="eyebrow">ERP Priority</p>
<h2>优先本地化组件</h2>
<p class="demo-description">先做 OFBiz 最高频控件表格菜单面包屑表单按钮弹层和状态反馈</p>
</div>
</div>
<div class="priority-link-grid">
<button
v-for="component in priorityComponents"
:key="component.slug"
type="button"
@click="openComponent(component.slug)"
>
<span>{{ component.name }}</span>
<small>{{ component.localClass }}</small>
</button>
</div>
</section>
<section class="component-library-sections">
<section v-for="section in groupedFilteredComponents" :key="section.group" class="component-group-section">
<header class="component-group-heading">
<div>
<p class="eyebrow">Component Group</p>
<h2>{{ section.group }}</h2>
</div>
<span>{{ section.components.length }} 个组件</span>
</header>
<div class="full-component-grid">
<article v-for="component in section.components" :key="component.slug" class="component-doc-card">
<div class="component-doc-card__head">
<div>
<button class="component-title-button" type="button" @click="openComponent(component.slug)">
{{ component.name }}
</button>
<p>{{ component.tag }} / {{ component.exportName }}</p>
</div>
<div class="component-doc-card__tags">
<el-tag size="small" effect="plain">{{ component.group }}</el-tag>
<el-tag size="small" :type="component.priority === '高' ? 'primary' : 'info'" effect="light">
{{ component.priority }}
</el-tag>
</div>
</div>
<p class="component-doc-card__rule">{{ component.whenToUse[0] }}</p>
<el-tabs :model-value="viewFor(component.slug)" class="component-doc-tabs" @update:model-value="setCardView(component.slug, $event)">
<el-tab-pane label="展示" name="demo">
<div class="component-preview">
<MiniPreview :component="component" />
<el-button link type="primary" @click="openComponent(component.slug)">详情</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="代码" name="code">
<pre class="component-code"><code>{{ component.erpCode }}</code></pre>
<el-button link type="primary" @click="openComponent(component.slug, 'code')">打开单组件代码页</el-button>
</el-tab-pane>
</el-tabs>
</article>
</div>
</section>
</section>
<section class="demo-block">
<div class="section-heading compact">
<div>
<p class="eyebrow">Exports & Directives</p>
<h2>公开导出与指令</h2>
<p class="demo-description">服务类导出Popper 子件指令也保留在清单里避免遗漏行为层能力</p>
</div>
</div>
<div class="component-chip-list">
<span v-for="record in elementPlusPublicExportRecords" :key="record.name" class="component-chip">
{{ record.name }}
<small>{{ record.kind === 'component' ? '组件' : '服务' }}</small>
</span>
</div>
<div class="directive-list">
<el-tag v-for="directive in elementPlusDirectiveRecords" :key="directive.slug" effect="plain">
{{ directive.code }}
</el-tag>
</div>
</section>
</div>
</template>
@@ -0,0 +1,27 @@
<script setup>
import { Calendar, CreditCard } from '@element-plus/icons-vue'
defineProps({
modelValue: {
type: Boolean,
required: true,
},
})
defineEmits(['update:modelValue'])
</script>
<template>
<el-drawer :model-value="modelValue" title="SO-10642 单据预览" size="460px" @update:model-value="$emit('update:modelValue', $event)">
<el-descriptions :column="1" border>
<el-descriptions-item label="客户">上海辰光零售</el-descriptions-item>
<el-descriptions-item label="金额">¥128,420.00</el-descriptions-item>
<el-descriptions-item label="计划日期">2026-06-08</el-descriptions-item>
<el-descriptions-item label="负责人">Lina Chen</el-descriptions-item>
</el-descriptions>
<div class="drawer-actions">
<el-button :icon="Calendar">排期</el-button>
<el-button :icon="CreditCard" type="primary">发起收款</el-button>
</div>
</el-drawer>
</template>
@@ -0,0 +1,129 @@
<script setup>
import { User } from '@element-plus/icons-vue'
import DemoBlock from '../docs/DemoBlock.vue'
defineProps({
checkValues: {
type: Array,
required: true,
},
dateRange: {
type: [String, Array, Date],
default: '',
},
note: {
type: String,
required: true,
},
orderStatus: {
type: String,
required: true,
},
selectedWarehouse: {
type: String,
required: true,
},
showAdvanced: {
type: Boolean,
required: true,
},
sliderValue: {
type: Number,
required: true,
},
})
defineEmits([
'update:checkValues',
'update:dateRange',
'update:note',
'update:orderStatus',
'update:selectedWarehouse',
'update:showAdvanced',
'update:sliderValue',
])
const formCode = `<el-form class="erp-form" label-position="top">
<div class="form-grid">
<el-form-item label="客户 / 供应商">
<el-input placeholder="输入名称、编码或联系人" :prefix-icon="User" />
</el-form-item>
<el-form-item label="仓库">
<el-select v-model="selectedWarehouse">
<el-option label="华东中心仓" value="central" />
</el-select>
</el-form-item>
<el-form-item label="业务日期">
<el-date-picker v-model="dateRange" type="daterange" />
</el-form-item>
</div>
</el-form>`
</script>
<template>
<DemoBlock
:code="formCode"
description="查询表单默认紧凑、分组清晰,复杂条件放到第二行或高级区。"
eyebrow="Forms"
title="表单控件与查询条件"
>
<div class="section-heading">
<el-switch
:model-value="showAdvanced"
active-text="显示高级项"
@update:model-value="$emit('update:showAdvanced', $event)"
/>
</div>
<el-form class="erp-form" label-position="top">
<div class="form-grid">
<el-form-item label="客户 / 供应商">
<el-input placeholder="输入名称、编码或联系人" :prefix-icon="User" />
</el-form-item>
<el-form-item label="仓库">
<el-select
:model-value="selectedWarehouse"
@update:model-value="$emit('update:selectedWarehouse', $event)"
>
<el-option label="华东中心仓" value="central" />
<el-option label="华南备货仓" value="south" />
<el-option label="财务共享中心" value="finance" />
</el-select>
</el-form-item>
<el-form-item label="业务日期">
<el-date-picker
:model-value="dateRange"
end-placeholder="结束日期"
range-separator=""
start-placeholder="开始日期"
type="daterange"
@update:model-value="$emit('update:dateRange', $event)"
/>
</el-form-item>
<el-form-item label="订单状态">
<el-select :model-value="orderStatus" @update:model-value="$emit('update:orderStatus', $event)">
<el-option label="草稿" value="draft" />
<el-option label="已审批" value="approved" />
<el-option label="待发货" value="shipping" />
</el-select>
</el-form-item>
</div>
<div class="form-grid secondary">
<el-form-item label="关注事项">
<el-checkbox-group :model-value="checkValues" @update:model-value="$emit('update:checkValues', $event)">
<el-checkbox-button label="overdue">逾期</el-checkbox-button>
<el-checkbox-button label="inventory">库存不足</el-checkbox-button>
<el-checkbox-button label="credit">信用风险</el-checkbox-button>
</el-checkbox-group>
</el-form-item>
<el-form-item label="优先级阈值">
<el-slider :model-value="sliderValue" @update:model-value="$emit('update:sliderValue', $event)" />
</el-form-item>
<el-form-item label="处理备注">
<el-input :model-value="note" type="textarea" @update:model-value="$emit('update:note', $event)" />
</el-form-item>
</div>
</el-form>
</DemoBlock>
</template>
@@ -0,0 +1,53 @@
<script setup>
import DemoBlock from '../docs/DemoBlock.vue'
const tokenCode = `import './styles/erp-ui.css'
:root {
--erp-color-primary: #2764e7;
--erp-color-success: #16845b;
--erp-radius-sm: 6px;
--erp-font-size-sm: 13px;
}`
</script>
<template>
<DemoBlock
:code="tokenCode"
description="这套基线刻意避开营销感和装饰感,优先保证高密度扫描、稳定状态表达、清晰动作层级和长时间操作的视觉耐受度。"
eyebrow="Foundations"
title="基础 Token"
>
<div class="hero-panel">
<div class="panel-copy">
<h2>先锁定产品气质再逐个组件收敛</h2>
</div>
<div class="token-grid">
<div class="token-card">
<span>Primary</span>
<strong>#2764e7</strong>
</div>
<div class="token-card muted">
<span>Surface</span>
<strong>#f7f8fb</strong>
</div>
<div class="token-card success">
<span>Success</span>
<strong>#16845b</strong>
</div>
<div class="token-card warning">
<span>Warning</span>
<strong>#b86b00</strong>
</div>
<div class="token-card danger">
<span>Danger</span>
<strong>#c73535</strong>
</div>
<div class="token-card neutral">
<span>Neutral</span>
<strong>#1d2433</strong>
</div>
</div>
</div>
</DemoBlock>
</template>
@@ -0,0 +1,45 @@
<script setup>
import { metricCardVariants, metricCards } from '../../data/showcaseData'
import DemoBlock from '../docs/DemoBlock.vue'
const metricCode = `<div class="erp-metric-card erp-metric-card--large is-primary">
<div class="erp-metric-card__head">
<span>待处理订单</span>
<el-icon><Tickets /></el-icon>
</div>
<strong>128</strong>
<small>+12.4%</small>
</div>
<div class="erp-metric-card erp-metric-card--small is-warning">...</div>
<div class="erp-metric-card erp-metric-card--mini is-danger">...</div>`
</script>
<template>
<DemoBlock
:code="metricCode"
description="同一个 KPI 组件按业务密度拆成大卡片、小卡片、最小卡片。"
eyebrow="Metrics"
title="KPI 指标卡"
>
<div class="metric-showcase">
<div v-for="variant in metricCardVariants" :key="variant.id" class="metric-variant">
<div class="metric-variant-title">{{ variant.label }}</div>
<div :class="['metrics-grid', `metrics-grid--${variant.id}`]">
<div
v-for="metric in metricCards"
:key="`${variant.id}-${metric.label}`"
:class="['erp-metric-card', variant.className, `is-${metric.tone}`]"
>
<div class="erp-metric-card__head">
<span>{{ metric.label }}</span>
<el-icon><component :is="metric.icon" /></el-icon>
</div>
<strong>{{ metric.value }}</strong>
<small>{{ metric.delta }}</small>
</div>
</div>
</div>
</div>
</DemoBlock>
</template>
@@ -0,0 +1,171 @@
<script setup>
import {
ArrowDown,
Bell,
Files,
Goods,
Grid,
HomeFilled,
Money,
Search,
Setting,
Tickets,
User,
} from '@element-plus/icons-vue'
import DemoBlock from '../docs/DemoBlock.vue'
defineEmits(['open-drawer'])
const sideMenuCode = `<el-menu default-active="orders" class="erp-side-menu">
<el-sub-menu index="sales">
<template #title>
<el-icon><Tickets /></el-icon>
<span>销售管理</span>
</template>
<el-menu-item index="orders">销售订单</el-menu-item>
<el-menu-item index="returns">退货处理</el-menu-item>
</el-sub-menu>
<el-menu-item index="inventory">
<el-icon><Goods /></el-icon>
<span>库存管理</span>
</el-menu-item>
</el-menu>`
const topMenuCode = `<header class="erp-top-menu">
<el-menu mode="horizontal" default-active="sales">
<el-menu-item index="sales">销售</el-menu-item>
<el-menu-item index="purchase">采购</el-menu-item>
<el-menu-item index="finance">财务</el-menu-item>
</el-menu>
<div class="erp-top-menu__actions">
<el-input :prefix-icon="Search" placeholder="搜索菜单、单据、客户" />
<el-button :icon="Bell" />
</div>
</header>`
const breadcrumbCode = `<el-breadcrumb separator="/">
<el-breadcrumb-item>销售管理</el-breadcrumb-item>
<el-breadcrumb-item>销售订单</el-breadcrumb-item>
<el-breadcrumb-item>SO-10642</el-breadcrumb-item>
</el-breadcrumb>
<el-page-header title="返回订单列表" content="SO-10642 销售订单" />`
</script>
<template>
<div class="docs-stack">
<DemoBlock
:code="sideMenuCode"
description="左侧菜单承载 OFBiz 模块树,默认使用可折叠分组,避免所有入口平铺。"
eyebrow="Navigation"
title="左侧菜单"
>
<div class="nav-layout-demo">
<aside class="nav-sidebar-demo">
<div class="nav-brand-demo">
<el-icon><Grid /></el-icon>
<strong>OFBiz</strong>
</div>
<el-menu default-active="orders" class="erp-side-menu">
<el-sub-menu index="sales">
<template #title>
<el-icon><Tickets /></el-icon>
<span>销售管理</span>
</template>
<el-menu-item index="orders">销售订单</el-menu-item>
<el-menu-item index="returns">退货处理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="purchase">
<template #title>
<el-icon><Files /></el-icon>
<span>采购管理</span>
</template>
<el-menu-item index="requests">采购需求</el-menu-item>
<el-menu-item index="vendors">供应商</el-menu-item>
</el-sub-menu>
<el-menu-item index="inventory">
<el-icon><Goods /></el-icon>
<span>库存管理</span>
</el-menu-item>
<el-menu-item index="finance">
<el-icon><Money /></el-icon>
<span>财务应收</span>
</el-menu-item>
</el-menu>
</aside>
<main class="nav-content-demo">
<el-breadcrumb separator="/">
<el-breadcrumb-item>销售管理</el-breadcrumb-item>
<el-breadcrumb-item>销售订单</el-breadcrumb-item>
</el-breadcrumb>
<h3>销售订单工作台</h3>
<p>左侧用于模块级导航页面内部的筛选表格抽屉承载具体业务操作</p>
</main>
</div>
</DemoBlock>
<DemoBlock
:code="topMenuCode"
description="顶部菜单适合跨模块一层导航,同时保留搜索、通知和用户入口。"
eyebrow="Navigation"
title="顶部菜单"
>
<header class="erp-top-menu">
<el-menu mode="horizontal" default-active="sales" class="erp-top-menu__nav">
<el-menu-item index="home">
<el-icon><HomeFilled /></el-icon>
首页
</el-menu-item>
<el-menu-item index="sales">销售</el-menu-item>
<el-menu-item index="purchase">采购</el-menu-item>
<el-menu-item index="inventory">库存</el-menu-item>
<el-menu-item index="finance">财务</el-menu-item>
</el-menu>
<div class="erp-top-menu__actions">
<el-input class="top-search-demo" :prefix-icon="Search" placeholder="搜索菜单、单据、客户" />
<el-button :icon="Bell" />
<el-dropdown>
<el-button>
Lina Chen
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="User">个人设置</el-dropdown-item>
<el-dropdown-item :icon="Setting">系统偏好</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
</DemoBlock>
<DemoBlock
:code="breadcrumbCode"
description="面包屑和页面头用于标识当前位置,抽屉适合承载单据详情与审计信息。"
eyebrow="Navigation"
title="面包屑与页面头"
>
<div class="breadcrumb-demo">
<el-breadcrumb separator="/">
<el-breadcrumb-item>销售管理</el-breadcrumb-item>
<el-breadcrumb-item>销售订单</el-breadcrumb-item>
<el-breadcrumb-item>SO-10642</el-breadcrumb-item>
</el-breadcrumb>
<el-page-header title="返回订单列表" content="SO-10642 销售订单">
<template #extra>
<el-button :icon="Files" @click="$emit('open-drawer')">打开单据抽屉</el-button>
</template>
</el-page-header>
<el-descriptions :column="3" border>
<el-descriptions-item label="客户">上海辰光零售</el-descriptions-item>
<el-descriptions-item label="账期">Net 30</el-descriptions-item>
<el-descriptions-item label="币种">CNY</el-descriptions-item>
<el-descriptions-item label="业务员">Lina Chen</el-descriptions-item>
<el-descriptions-item label="仓库">华东中心仓</el-descriptions-item>
<el-descriptions-item label="审批状态">已审批</el-descriptions-item>
</el-descriptions>
</div>
</DemoBlock>
</div>
</template>
@@ -0,0 +1,67 @@
<script setup>
import { Bell } from '@element-plus/icons-vue'
import { timelineItems } from '../../data/showcaseData'
import DemoBlock from '../docs/DemoBlock.vue'
const statusCode = `<el-tag type="success">已审批</el-tag>
<el-tag type="warning">待复核</el-tag>
<el-tag type="danger">逾期</el-tag>
<el-alert
title="信用检查未完成"
type="warning"
show-icon
/>
<el-timeline>
<el-timeline-item timestamp="09:20" type="success">
销售订单完成审批
</el-timeline-item>
</el-timeline>`
</script>
<template>
<DemoBlock
:code="statusCode"
description="状态、反馈和审计线要低饱和但高可读,用于提醒风险而不是装饰页面。"
eyebrow="Status"
title="状态、反馈、审计线"
>
<div class="split-panel">
<div>
<div class="section-heading compact">
</div>
<div class="tag-grid">
<el-tag>草稿</el-tag>
<el-tag type="success">已审批</el-tag>
<el-tag type="warning">待复核</el-tag>
<el-tag type="danger">逾期</el-tag>
<el-tag type="info">已归档</el-tag>
</div>
<el-alert
title="信用检查未完成"
description="建议在出库前确认客户账期和应收余额,避免后续冲销。"
type="warning"
show-icon
:closable="false"
/>
<div class="feedback-row">
<el-progress :percentage="72" />
<el-badge :value="8">
<el-button :icon="Bell">待办</el-button>
</el-badge>
</div>
</div>
<el-timeline>
<el-timeline-item
v-for="item in timelineItems"
:key="item.timestamp"
:timestamp="item.timestamp"
:type="item.type"
>
{{ item.content }}
</el-timeline-item>
</el-timeline>
</div>
</DemoBlock>
</template>
@@ -0,0 +1,86 @@
<script setup>
import { Location, Search, Sort } from '@element-plus/icons-vue'
import { getRiskClass, orderRows } from '../../data/showcaseData'
import DemoBlock from '../docs/DemoBlock.vue'
defineProps({
currentPage: {
type: Number,
required: true,
},
})
defineEmits(['update:currentPage'])
const tableCode = `<el-table :data="orderRows" class="erp-table" stripe>
<el-table-column fixed prop="orderId" label="单据号" width="126" />
<el-table-column prop="customer" label="客户 / 对象" min-width="180" />
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.statusType" effect="light">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="140" align="right" />
<el-table-column fixed="right" label="操作" width="152" />
</el-table>`
</script>
<template>
<DemoBlock
:code="tableCode"
description="ERP 表格优先保证扫描效率、状态清晰、数字右对齐和固定操作列。"
eyebrow="Data Table"
title="订单工作台表格"
>
<div class="section-heading">
<div class="table-tools">
<el-input class="table-search" placeholder="搜索订单、客户、负责人" :prefix-icon="Search" />
<el-button :icon="Sort">列设置</el-button>
</div>
</div>
<el-table :data="orderRows" class="erp-table" stripe>
<el-table-column fixed prop="orderId" label="单据号" width="126" />
<el-table-column prop="customer" label="客户 / 对象" min-width="180" />
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.statusType" effect="light">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="140" align="right" />
<el-table-column prop="warehouse" label="业务地点" min-width="150">
<template #default="{ row }">
<span class="icon-cell">
<el-icon><Location /></el-icon>
{{ row.warehouse }}
</span>
</template>
</el-table-column>
<el-table-column prop="owner" label="负责人" width="130" />
<el-table-column prop="due" label="计划日期" width="124" />
<el-table-column prop="risk" label="风险" width="90">
<template #default="{ row }">
<span :class="['risk-dot', getRiskClass(row.risk)]">{{ row.risk }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="152">
<template #default>
<el-button link type="primary">查看</el-button>
<el-button link>流转</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<span> 4 条样例数据后续接 OFBiz 真实实体</span>
<el-pagination
:current-page="currentPage"
layout="prev, pager, next, sizes"
:page-size="20"
:page-sizes="[20, 50, 100]"
:total="128"
@update:current-page="$emit('update:currentPage', $event)"
/>
</div>
</DemoBlock>
</template>
@@ -0,0 +1,21 @@
<template>
<aside class="token-rail">
<div class="rail-card">
<p class="eyebrow">Token Notes</p>
<ul>
<li>字号12/13/14/18/24避免随视口缩放</li>
<li>圆角主控件 6px卡片 8px</li>
<li>表格行高 40px数字右对齐</li>
<li>状态色低饱和高可读</li>
<li>按钮主动作每屏只保留一个</li>
</ul>
</div>
<div class="rail-card">
<p class="eyebrow">Next Components</p>
<el-tag effect="plain">Tree</el-tag>
<el-tag effect="plain">Upload</el-tag>
<el-tag effect="plain">Virtual Table</el-tag>
<el-tag effect="plain">Dialog</el-tag>
</div>
</aside>
</template>
@@ -0,0 +1,52 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
function parseHash() {
const rawHash = window.location.hash.replace(/^#\/?/, '')
const [path = '', queryString = ''] = rawHash.split('?')
const params = new URLSearchParams(queryString)
const segments = path.split('/').filter(Boolean)
return {
path,
params,
segments,
}
}
const routeState = ref(parseHash())
function syncRoute() {
routeState.value = parseHash()
}
function buildHash(path, params = {}) {
const search = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
search.set(key, value)
}
})
const query = search.toString()
return `#/${path.replace(/^\/+/, '')}${query ? `?${query}` : ''}`
}
export function navigateTo(path, params = {}) {
window.location.hash = buildHash(path, params)
}
export function useHashRoute() {
onMounted(() => {
window.addEventListener('hashchange', syncRoute)
syncRoute()
})
onBeforeUnmount(() => {
window.removeEventListener('hashchange', syncRoute)
})
return {
route: computed(() => routeState.value),
navigateTo,
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,759 @@
import { elementPlusComponentRecords } from './elementPlusInventory.js'
const groupContracts = {
基础: {
family: '基础控件',
localClass: 'erp-basic-control',
previewKind: 'basic',
usage: '用于低风险通用控件,先继承 ERP token,再按业务动作层级收敛到 wrapper。',
styleFocus: '字号、按钮高度、焦点态、禁用态和图标间距统一。',
},
布局: {
family: '布局容器',
localClass: 'erp-layout-region',
previewKind: 'layout',
usage: '用于页面骨架和内容区组织,避免卡片套卡片,保持业务页面可扫描。',
styleFocus: '边框、留白、分区标题和内容密度统一。',
},
表单: {
family: '表单控件',
localClass: 'erp-field-control',
previewKind: 'form',
usage: '用于查询条件和业务录入,默认 label-position="top",复杂字段进入高级区。',
styleFocus: '输入高度、label 字重、校验色、组合控件间距统一。',
},
数据展示: {
family: '数据展示',
localClass: 'erp-data-view',
previewKind: 'data',
usage: '用于列表、详情、树和状态摘要,优先保证扫描效率和数字对齐。',
styleFocus: '表头、行高、状态色、空态和分页密度统一。',
},
导航: {
family: '导航控件',
localClass: 'erp-navigation-control',
previewKind: 'navigation',
usage: '用于模块树、顶部导航、面包屑和页面路径,保持当前位置明确。',
styleFocus: '选中态、悬停态、层级缩进和横向菜单 1px 下划线统一。',
},
反馈与覆盖层: {
family: '反馈与覆盖层',
localClass: 'erp-feedback-control',
previewKind: 'feedback',
usage: '用于保存结果、审批确认、异常提示和侧边详情,控制打扰程度。',
styleFocus: '弹层宽度、按钮顺序、遮罩、提示色和危险动作层级统一。',
},
内部基础: {
family: '内部基础',
localClass: 'erp-internal-foundation',
previewKind: 'internal',
usage: '多数不在业务页面直接调用,保留在清单中用于识别组合组件依赖。',
styleFocus: '不单独定制视觉,跟随父组件 wrapper 和全局 Element Plus token。',
},
}
const groupGuidance = {
基础: {
useWhen: ['需要触发动作、表达轻量文本、组织间距或控制全局配置时。'],
avoidWhen: ['不要用基础控件承担业务结构;重复业务模式应沉淀为 ERP wrapper。'],
layoutRules: ['基础控件跟随所在区域密度,按钮组靠近被操作对象,图标按钮必须有明确语义。'],
},
布局: {
useWhen: ['需要组织页面骨架、分区、栅格、折叠内容或详情摘要时。'],
avoidWhen: ['不要把页面切成多层浮动卡片;不要为了装饰而增加容器。'],
layoutRules: ['页面优先使用左导航 + 顶部上下文 + 主工作区;分区间距使用 20/24px。'],
},
表单: {
useWhen: ['需要查询条件、实体录入、筛选、选择器、上传或复杂字段编辑时。'],
avoidWhen: ['不要把低频高级条件塞进第一行;不要混用不同 label 对齐方式。'],
layoutRules: ['查询表单默认 4 列,录入表单默认 2 列;label 在上,主按钮靠右。'],
},
数据展示: {
useWhen: ['需要扫描业务对象、查看状态、对比金额、浏览树结构或展示空/加载状态时。'],
avoidWhen: ['不要用大卡片替代表格;有批量操作和排序需求时优先表格。'],
layoutRules: ['列表区顺序是筛选栏、工具栏、表格、分页;数字右对齐,状态靠近主对象。'],
},
导航: {
useWhen: ['需要模块切换、页面路径、局部 tab、流程步骤或审计时间线时。'],
avoidWhen: ['不要在同一屏放多套主导航;顶部菜单选中态只用 1px 下划线。'],
layoutRules: ['模块级导航在左侧,页面内导航用 tabs/steps,当前位置用 breadcrumb/page-header。'],
},
反馈与覆盖层: {
useWhen: ['需要确认危险动作、提示保存结果、展示抽屉详情、阻塞式编辑或轻提示时。'],
avoidWhen: ['不要让 toast 承载必须阅读的信息;不要把复杂流程塞进小 popover。'],
layoutRules: ['非阻塞反馈用 message/notification,确认用 popconfirm/dialog,详情用 drawer。'],
},
内部基础: {
useWhen: ['作为组合组件依赖存在,或为了理解 Element Plus 能力边界时。'],
avoidWhen: ['不要在 OFBiz 业务页直接调用内部基础件。'],
layoutRules: ['跟随父组件语境,不单独规定排版。'],
},
}
const previewGuidance = {
table: {
useWhen: ['对象数量多、字段多、需要排序筛选、批量操作或固定操作列时。'],
avoidWhen: ['不要用表格展示少量摘要;少于 3 项的摘要优先 descriptions 或 metric card。'],
layoutRules: ['上方放查询和批量动作;表头紧凑,金额右对齐,分页贴近表格底部。'],
},
'table-v2': {
useWhen: ['行数非常多、滚动性能成为瓶颈、列结构稳定时。'],
avoidWhen: ['需要复杂插槽、树形展开或高度动态内容时先用普通 Table。'],
layoutRules: ['固定宽高,先定义列宽;不要让虚拟表格撑开页面布局。'],
},
pagination: {
useWhen: ['列表数据超过一页,或用户需要切换 page size 时。'],
avoidWhen: ['小型静态列表不需要分页;无限滚动不适合审计型 ERP 数据。'],
layoutRules: ['分页放在表格右下角,显示条数选择和总数。'],
},
form: {
useWhen: ['需要成组收集业务字段、校验、提交或查询。'],
avoidWhen: ['只读详情不要伪装成表单,使用 descriptions。'],
layoutRules: ['查询 4 列、编辑 2 列;高级条件第二行,按钮右对齐。'],
},
field: {
useWhen: ['短文本、编码、金额、数量、标签等单字段输入。'],
avoidWhen: ['实体查找不要用普通 input,应使用 autocomplete/tree-select/cascader。'],
layoutRules: ['输入框宽度跟随列,不单独拉满整行,单位和帮助信息贴近字段。'],
},
select: {
useWhen: ['选项有限、语义明确、需要从合法值中选择时。'],
avoidWhen: ['超过几十项且需要搜索实体时用 Autocomplete 或 Select V2。'],
layoutRules: ['单选宽度随字段列,多选要预留换行高度,选项文案保持业务名词。'],
},
lookup: {
useWhen: ['需要从客户、供应商、组织、仓库等实体中查找并选择时。'],
avoidWhen: ['简单枚举值不要使用实体查找控件。'],
layoutRules: ['搜索提示写明可搜编码/名称/联系人;结果要显示可区分的次要信息。'],
},
choice: {
useWhen: ['状态过滤、互斥状态、少量布尔/枚举条件。'],
avoidWhen: ['选项超过 6 个时不要横排铺满,改用 Select。'],
layoutRules: ['横向组选项保持同一语义层级,按钮式选择只用于高频筛选。'],
},
button: {
useWhen: ['需要明确动作:查询、新建、审批、导出、作废。'],
avoidWhen: ['不要让多个 primary 同屏竞争;弱动作优先 link/dropdown。'],
layoutRules: ['一个工作区一个主按钮;危险动作靠后并用二次确认。'],
},
dropdown: {
useWhen: ['低频动作、多动作集合或表格行更多操作。'],
avoidWhen: ['核心主流程不要藏进 dropdown。'],
layoutRules: ['主动作外露,更多动作收进 dropdown;危险项分隔。'],
},
menu: {
useWhen: ['模块级导航、业务域切换、多层入口树。'],
avoidWhen: ['不要把页面内部筛选或 tab 放进主菜单。'],
layoutRules: ['左侧菜单分组折叠,选中态安静,图标只用于一级模块。'],
},
breadcrumb: {
useWhen: ['需要说明当前位置、可返回路径或详情页上下文。'],
avoidWhen: ['单层页面或无层级路径时不要强塞 breadcrumb。'],
layoutRules: ['放在页面标题上方或详情头部,最后一级不做跳转。'],
},
tabs: {
useWhen: ['同一业务对象下切换摘要、明细、审计、附件等局部视图。'],
avoidWhen: ['跨模块导航不要用 Tabs,使用 Menu。'],
layoutRules: ['标签数量控制在 2-6 个,内容区高度稳定,默认打开最高频页。'],
},
status: {
useWhen: ['需要展示审批、逾期、风险、库存等状态。'],
avoidWhen: ['不要用颜色替代文字;状态必须有可读标签。'],
layoutRules: ['状态靠近对象名或状态列,颜色低饱和,危险态只用于真实风险。'],
},
overlay: {
useWhen: ['确认、编辑、详情、阻塞式流程或轻量上下文说明。'],
avoidWhen: ['多步骤复杂流程不要塞入 Popover 或小 Dialog。'],
layoutRules: ['Dialog 做确认/短表单,Drawer 做详情和审计,按钮顺序稳定。'],
},
service: {
useWhen: ['保存结果、异步完成、全局通知、加载锁定。'],
avoidWhen: ['必须持久存在的信息不要只用 Message。'],
layoutRules: ['反馈文案短、明确、可恢复;错误信息给下一步动作。'],
},
descriptions: {
useWhen: ['只读详情、审批上下文、实体摘要。'],
avoidWhen: ['需要编辑时不要用 descriptions 模拟输入。'],
layoutRules: ['2-3 列,label 固定宽度,重要字段靠前。'],
},
tree: {
useWhen: ['组织、目录、权限、仓库层级。'],
avoidWhen: ['平铺枚举不要用树。'],
layoutRules: ['默认展开到可决策层级,节点文字短,右侧展示详情或表格。'],
},
empty: {
useWhen: ['搜索无结果、列表初始无数据、筛选条件过窄。'],
avoidWhen: ['加载中不要显示 Empty,应使用 Skeleton/Loading。'],
layoutRules: ['空态给原因和下一步动作,不要占据整屏视觉重心。'],
},
progress: {
useWhen: ['导入、批处理、履约进度、库存同步进度。'],
avoidWhen: ['状态不可量化时不要使用进度条。'],
layoutRules: ['进度条旁边要有任务名或百分比解释。'],
},
result: {
useWhen: ['流程完成、提交成功、不可恢复错误结果页。'],
avoidWhen: ['普通保存成功用 Message,不要跳大结果页。'],
layoutRules: ['结果页只用于流程终点,提供返回或继续动作。'],
},
skeleton: {
useWhen: ['表格、详情或卡片数据还在加载。'],
avoidWhen: ['加载时间极短时不要闪烁骨架屏。'],
layoutRules: ['骨架形状贴近最终内容,避免布局跳动。'],
},
timeline: {
useWhen: ['审计记录、审批流、订单流转、系统事件。'],
avoidWhen: ['当前进度步骤用 Steps,不用 Timeline。'],
layoutRules: ['时间在左,事件短句,异常事件用 warning/danger。'],
},
steps: {
useWhen: ['流程当前位置明确,如创建、审批、锁库、发运。'],
avoidWhen: ['历史记录不要用 Steps,用 Timeline。'],
layoutRules: ['横向步骤不超过 5 个,当前态清楚,完成态低调。'],
},
transfer: {
useWhen: ['权限分配、模块授权、双列表选择。'],
avoidWhen: ['普通多选不要用 Transfer。'],
layoutRules: ['左右标题明确,列表高度固定,搜索与批量选择可用。'],
},
}
const parentUsage = {
'anchor-link': 'anchor',
'avatar-group': 'avatar',
'breadcrumb-item': 'breadcrumb',
'button-group': 'button',
'carousel-item': 'carousel',
'cascader-panel': 'cascader',
'checkbox-button': 'checkbox-group',
'checkbox-group': 'checkbox',
'col': 'row',
'collapse-item': 'collapse',
'color-picker-panel': 'color-picker',
'date-picker-panel': 'date-picker',
'descriptions-item': 'descriptions',
'dropdown-item': 'dropdown',
'dropdown-menu': 'dropdown',
'form-item': 'form',
'image-viewer': 'image',
'menu-item': 'menu',
'menu-item-group': 'menu',
option: 'select',
'option-group': 'select',
'radio-button': 'radio-group',
'radio-group': 'radio',
'skeleton-item': 'skeleton',
'splitter-panel': 'splitter',
step: 'steps',
'sub-menu': 'menu',
'tab-pane': 'tabs',
'table-column': 'table',
'timeline-item': 'timeline',
'tour-step': 'tour',
}
const componentDocOverrides = {
alert: {
previewKind: 'status',
localClass: 'erp-status-alert',
erpCode: '<el-alert class="erp-status-alert" title="信用检查未完成" type="warning" show-icon :closable="false" />',
},
autocomplete: {
previewKind: 'lookup',
localClass: 'erp-entity-lookup',
erpCode: '<el-autocomplete class="erp-entity-lookup" v-model="keyword" :fetch-suggestions="queryCustomer" placeholder="搜索客户编码、名称或联系人" />',
},
badge: {
previewKind: 'status',
localClass: 'erp-status-badge',
erpCode: '<el-badge class="erp-status-badge" :value="8"><el-button>待办事项</el-button></el-badge>',
},
breadcrumb: {
previewKind: 'breadcrumb',
localClass: 'erp-breadcrumb',
erpCode: '<el-breadcrumb class="erp-breadcrumb" separator="/"><el-breadcrumb-item>销售管理</el-breadcrumb-item><el-breadcrumb-item>销售订单</el-breadcrumb-item></el-breadcrumb>',
},
'breadcrumb-item': {
previewKind: 'breadcrumb',
localClass: 'erp-breadcrumb',
},
button: {
previewKind: 'button',
localClass: 'erp-action-button',
erpCode: '<el-button class="erp-action-button" type="primary">新建订单</el-button>',
},
'button-group': {
previewKind: 'button',
localClass: 'erp-action-group',
},
calendar: {
previewKind: 'calendar',
localClass: 'erp-calendar',
},
card: {
previewKind: 'card',
localClass: 'erp-entity-card',
erpCode: '<section class="erp-entity-card"><header>订单摘要</header><strong>SO-10642</strong></section>',
},
carousel: {
previewKind: 'carousel',
localClass: 'erp-carousel',
},
'carousel-item': {
previewKind: 'carousel',
localClass: 'erp-carousel',
},
cascader: {
previewKind: 'lookup',
localClass: 'erp-field-control',
erpCode: '<el-cascader class="erp-field-control" v-model="region" :options="regionOptions" placeholder="选择组织 / 仓库" />',
},
checkbox: {
previewKind: 'choice',
localClass: 'erp-choice-group',
},
'checkbox-button': {
previewKind: 'choice',
localClass: 'erp-choice-group',
},
'checkbox-group': {
previewKind: 'choice',
localClass: 'erp-choice-group',
},
'color-picker': {
previewKind: 'color',
localClass: 'erp-color-picker',
},
'color-picker-panel': {
previewKind: 'color',
localClass: 'erp-color-picker',
},
collapse: {
previewKind: 'collapse',
localClass: 'erp-collapse',
},
'collapse-item': {
previewKind: 'collapse',
localClass: 'erp-collapse',
},
'date-picker': {
previewKind: 'date',
localClass: 'erp-date-range',
erpCode: '<el-date-picker class="erp-date-range" v-model="dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" />',
},
'date-picker-panel': {
previewKind: 'date',
localClass: 'erp-date-range',
},
descriptions: {
previewKind: 'descriptions',
localClass: 'erp-detail-descriptions',
erpCode: '<el-descriptions class="erp-detail-descriptions" :column="3" border><el-descriptions-item label="客户">上海辰光零售</el-descriptions-item></el-descriptions>',
},
'descriptions-item': {
previewKind: 'descriptions',
localClass: 'erp-detail-descriptions',
},
dialog: {
previewKind: 'overlay',
localClass: 'erp-dialog',
erpCode: '<el-dialog v-model="visible" class="erp-dialog" title="审批确认">确认通过该订单?</el-dialog>',
},
drawer: {
previewKind: 'overlay',
localClass: 'erp-drawer',
erpCode: '<el-drawer v-model="visible" class="erp-drawer" title="单据详情">SO-10642</el-drawer>',
},
dropdown: {
previewKind: 'dropdown',
localClass: 'erp-action-dropdown',
erpCode: '<el-dropdown class="erp-action-dropdown"><el-button>更多操作</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item>导出</el-dropdown-item></el-dropdown-menu></template></el-dropdown>',
},
'dropdown-item': {
previewKind: 'dropdown',
localClass: 'erp-action-dropdown',
},
'dropdown-menu': {
previewKind: 'dropdown',
localClass: 'erp-action-dropdown',
},
empty: {
previewKind: 'empty',
localClass: 'erp-empty-state',
},
image: {
previewKind: 'media',
localClass: 'erp-media-preview',
},
'image-viewer': {
previewKind: 'media',
localClass: 'erp-media-preview',
},
form: {
previewKind: 'form',
localClass: 'erp-form',
erpCode: '<el-form class="erp-form" label-position="top"><el-form-item label="客户"><el-input placeholder="输入名称、编码或联系人" /></el-form-item></el-form>',
},
'form-item': {
previewKind: 'form',
localClass: 'erp-form',
},
input: {
previewKind: 'field',
localClass: 'erp-field-control',
erpCode: '<el-input class="erp-field-control" v-model="keyword" placeholder="搜索订单、客户、负责人" />',
},
'input-number': {
previewKind: 'field',
localClass: 'erp-field-control',
},
'input-tag': {
previewKind: 'field',
localClass: 'erp-field-control',
},
link: {
previewKind: 'typography',
localClass: 'erp-inline-link',
},
loading: {
previewKind: 'service',
localClass: 'erp-loading-action',
},
menu: {
previewKind: 'menu',
localClass: 'erp-side-menu',
erpCode: '<el-menu class="erp-side-menu" default-active="orders"><el-sub-menu index="sales"><template #title>销售管理</template><el-menu-item index="orders">销售订单</el-menu-item></el-sub-menu></el-menu>',
},
'menu-item': {
previewKind: 'menu',
localClass: 'erp-side-menu',
},
'menu-item-group': {
previewKind: 'menu',
localClass: 'erp-side-menu',
},
message: {
previewKind: 'service',
localClass: 'erp-message-action',
erpCode: "ElMessage.success('订单已保存')",
},
'message-box': {
previewKind: 'service',
localClass: 'erp-message-box-action',
erpCode: "ElMessageBox.alert('确认通过该订单?', '审批确认')",
},
notification: {
previewKind: 'service',
localClass: 'erp-notification-action',
erpCode: "ElNotification({ title: '库存预警', message: '36 个 SKU 低于安全库存', type: 'warning' })",
},
option: {
previewKind: 'select',
localClass: 'erp-field-control',
},
'option-group': {
previewKind: 'select',
localClass: 'erp-field-control',
},
'page-header': {
previewKind: 'breadcrumb',
localClass: 'erp-page-header',
},
pagination: {
previewKind: 'pagination',
localClass: 'erp-dense-pagination',
erpCode: '<el-pagination class="erp-dense-pagination" layout="prev, pager, next, sizes" :page-size="20" :page-sizes="[20, 50, 100]" :total="128" />',
},
popconfirm: {
previewKind: 'overlay',
localClass: 'erp-popconfirm',
},
progress: {
previewKind: 'progress',
localClass: 'erp-progress',
},
radio: {
previewKind: 'choice',
localClass: 'erp-choice-group',
},
'radio-button': {
previewKind: 'choice',
localClass: 'erp-choice-group',
},
'radio-group': {
previewKind: 'choice',
localClass: 'erp-choice-group',
},
rate: {
previewKind: 'rate',
localClass: 'erp-rate',
},
result: {
previewKind: 'result',
localClass: 'erp-result-state',
},
select: {
previewKind: 'select',
localClass: 'erp-field-control',
erpCode: '<el-select class="erp-field-control" v-model="warehouse"><el-option label="华东中心仓" value="east" /></el-select>',
},
'select-v2': {
previewKind: 'select',
localClass: 'erp-field-control',
erpCode: '<el-select-v2 class="erp-field-control" v-model="warehouse" :options="warehouseOptions" />',
},
segmented: {
previewKind: 'tabs',
localClass: 'erp-segmented',
},
skeleton: {
previewKind: 'skeleton',
localClass: 'erp-skeleton',
},
'skeleton-item': {
previewKind: 'skeleton',
localClass: 'erp-skeleton',
},
slider: {
previewKind: 'slider',
localClass: 'erp-slider',
},
space: {
previewKind: 'space',
localClass: 'erp-space',
},
statistic: {
previewKind: 'statistic',
localClass: 'erp-statistic',
},
step: {
previewKind: 'steps',
localClass: 'erp-steps',
},
steps: {
previewKind: 'steps',
localClass: 'erp-steps',
},
'sub-menu': {
previewKind: 'menu',
localClass: 'erp-side-menu',
},
switch: {
previewKind: 'switch',
localClass: 'erp-switch',
},
'tab-pane': {
previewKind: 'tabs',
localClass: 'erp-tabs',
},
table: {
previewKind: 'table',
localClass: 'erp-table',
erpCode: '<el-table :data="rows" class="erp-table" stripe><el-table-column prop="orderId" label="单据号" width="126" /></el-table>',
},
'table-column': {
previewKind: 'table',
localClass: 'erp-table',
},
'table-v2': {
previewKind: 'table-v2',
localClass: 'erp-virtual-table',
erpCode: '<el-table-v2 class="erp-virtual-table" :columns="columns" :data="rows" :width="760" :height="260" />',
},
tabs: {
previewKind: 'tabs',
localClass: 'erp-tabs',
},
tag: {
previewKind: 'status',
localClass: 'erp-status-tag',
erpCode: '<el-tag class="erp-status-tag" type="success" effect="light">已审批</el-tag>',
},
text: {
previewKind: 'typography',
localClass: 'erp-inline-text',
},
timeline: {
previewKind: 'timeline',
localClass: 'erp-timeline',
},
'timeline-item': {
previewKind: 'timeline',
localClass: 'erp-timeline',
},
tooltip: {
previewKind: 'overlay',
localClass: 'erp-tooltip',
},
transfer: {
previewKind: 'transfer',
localClass: 'erp-transfer',
},
tree: {
previewKind: 'tree',
localClass: 'erp-tree',
},
'tree-select': {
previewKind: 'tree',
localClass: 'erp-tree-select',
},
'tree-v2': {
previewKind: 'tree',
localClass: 'erp-tree',
},
upload: {
previewKind: 'upload',
localClass: 'erp-upload',
},
}
function pascalName(name) {
return name.replace(/\s+/g, '')
}
function fallbackErpCode(record, localClass) {
const parent = parentUsage[record.slug]
if (parent) {
return `<!-- ${record.name} 跟随 ${parent} 的 ERP wrapper 使用。 -->\n<${record.tag} class="${localClass}">${record.name}</${record.tag}>`
}
if (!record.hasPublicExport || record.group === '内部基础') {
return `<!-- ${record.name} 是内部/组合基础件,不建议在 OFBiz 业务页直接调用。 -->\n<${record.tag}>${record.name}</${record.tag}>`
}
return `<${record.tag} class="${localClass}">${record.name}</${record.tag}>`
}
function styleCodeFor(record, localClass, contract) {
return `/* src/styles/erp-patterns.css */\n.${localClass} {\n color: var(--erp-color-text);\n font-size: var(--erp-font-size-sm);\n border-color: var(--erp-color-border-soft);\n}\n\n/* ${record.name}: ${contract.styleFocus} */`
}
function wrapperNameFor(record) {
const parent = parentUsage[record.slug]
if (parent) return `跟随 ${parent} wrapper`
if (!record.hasPublicExport || record.group === '内部基础') return '不直接封装'
return `Erp${pascalName(record.name)}`
}
function guidanceFor(record, previewKind) {
const group = groupGuidance[record.group] ?? groupGuidance['内部基础']
const preview = previewGuidance[previewKind] ?? {}
return {
useWhen: preview.useWhen ?? group.useWhen,
avoidWhen: preview.avoidWhen ?? group.avoidWhen,
layoutRules: preview.layoutRules ?? group.layoutRules,
}
}
export const erpComponentDocs = elementPlusComponentRecords.map((record) => {
const contract = groupContracts[record.group] ?? groupContracts['内部基础']
const override = componentDocOverrides[record.slug] ?? {}
const localClass = override.localClass ?? contract.localClass
const parent = parentUsage[record.slug] ?? ''
const previewKind = override.previewKind ?? contract.previewKind
const guidance = guidanceFor(record, previewKind)
return {
...record,
family: override.family ?? contract.family,
localClass,
parentSlug: parent,
previewKind,
originalCode: override.originalCode ?? record.code,
erpCode: override.erpCode ?? fallbackErpCode(record, localClass),
styleCode: override.styleCode ?? styleCodeFor(record, localClass, contract),
usage: override.usage ?? contract.usage,
styleFocus: override.styleFocus ?? contract.styleFocus,
whenToUse: override.whenToUse ?? guidance.useWhen,
avoidWhen: override.avoidWhen ?? guidance.avoidWhen,
layoutRules: override.layoutRules ?? guidance.layoutRules,
wrapperName: override.wrapperName ?? wrapperNameFor(record),
cssFiles: ['src/styles/erp-ui.css', 'src/styles/tokens.css', 'src/styles/element-overrides.css', 'src/styles/erp-patterns.css'],
detailRoute: `#/component/${record.slug}`,
}
})
export const erpComponentGroups = Object.keys(groupContracts).map((group) => ({
group,
count: erpComponentDocs.filter((component) => component.group === group).length,
contract: groupContracts[group],
}))
export function getErpComponentDoc(slug) {
return erpComponentDocs.find((component) => component.slug === slug)
}
export const erpComponentUsageMap = [
{
title: '页面骨架',
description: '搭建 OFBiz 模块、列表页、详情页的稳定结构。',
components: ['container', 'aside', 'main', 'card', 'collapse', 'splitter'],
},
{
title: '导航定位',
description: '让用户知道自己在哪、可以去哪、流程到哪一步。',
components: ['menu', 'breadcrumb', 'page-header', 'tabs', 'steps', 'timeline'],
},
{
title: '查询录入',
description: '承载筛选、编辑、实体选择和合法值输入。',
components: ['form', 'input', 'select', 'date-picker', 'tree-select', 'autocomplete', 'upload'],
},
{
title: '数据扫描',
description: '高密度展示订单、库存、财务和组织结构。',
components: ['table', 'table-v2', 'pagination', 'descriptions', 'tree', 'statistic', 'tag'],
},
{
title: '动作反馈',
description: '处理审批、保存、作废、异常提示和完成状态。',
components: ['button', 'dropdown', 'popconfirm', 'dialog', 'drawer', 'message', 'alert', 'result'],
},
{
title: '状态与加载',
description: '表达空态、加载、进度、风险、附件和审计上下文。',
components: ['empty', 'skeleton', 'progress', 'badge', 'calendar', 'image', 'transfer'],
},
].map((section) => ({
...section,
components: section.components
.map((slug) => erpComponentDocs.find((component) => component.slug === slug))
.filter(Boolean),
}))
export const erpLayoutPlaybook = [
{
title: '列表页',
description: '用于销售订单、采购需求、库存、应收等高频工作台。',
density: '高密度',
rules: ['面包屑和标题在顶部,查询条件一行优先。', '工具栏紧贴表格,批量动作在选择后出现。', '表格底部右侧放 Pagination。'],
components: ['breadcrumb', 'page-header', 'form', 'input', 'select', 'date-picker', 'button', 'table', 'pagination', 'tag', 'dropdown'],
},
{
title: '详情页',
description: '用于订单、客户、供应商、财务单据等只读和局部编辑场景。',
density: '中密度',
rules: ['上方显示对象身份、状态和主动作。', '摘要使用 Descriptions,明细用 Tabs + Table。', '审计和附件放 Drawer 或独立 Tab。'],
components: ['breadcrumb', 'page-header', 'tag', 'descriptions', 'tabs', 'table', 'timeline', 'drawer', 'upload'],
},
{
title: '录入页',
description: '用于创建订单、维护基础资料、批量导入前的结构化编辑。',
density: '中低密度',
rules: ['表单默认两列,强校验字段靠前。', '复杂实体选择用 Tree Select、Cascader 或 Autocomplete。', '提交区固定一个主按钮,危险动作二次确认。'],
components: ['form', 'form-item', 'input', 'input-number', 'select', 'tree-select', 'cascader', 'date-picker', 'upload', 'button', 'popconfirm'],
},
{
title: '流程页',
description: '用于审批、发运、锁库、导入、结算等有明确进度的操作。',
density: '引导密度',
rules: ['当前步骤用 Steps,不用 Timeline。', '历史记录和系统事件用 Timeline。', '保存结果用 Message,流程终点才用 Result。'],
components: ['steps', 'alert', 'progress', 'skeleton', 'message', 'result', 'timeline', 'dialog'],
},
].map((blueprint) => ({
...blueprint,
components: blueprint.components
.map((slug) => erpComponentDocs.find((component) => component.slug === slug))
.filter(Boolean),
}))
+316
View File
@@ -0,0 +1,316 @@
import {
CircleCheck,
EditPen,
Files,
Goods,
Grid,
HomeFilled,
Money,
Tickets,
Tools,
Van,
} from '@element-plus/icons-vue'
export const sections = [
{ id: 'foundations', label: '基础规范', icon: Grid },
{ id: 'catalog', label: '全组件目录', icon: Files },
{ id: 'metrics', label: '指标卡片', icon: Tickets },
{ id: 'buttons', label: '按钮与动作', icon: Tools },
{ id: 'forms', label: '表单控件', icon: EditPen },
{ id: 'table', label: '数据表格', icon: Tickets },
{ id: 'status', label: '状态与反馈', icon: CircleCheck },
{ id: 'navigation', label: '导航与容器', icon: HomeFilled },
]
export const docPages = sections.map((section) => ({
...section,
title: {
foundations: '基础规范与 Token',
catalog: 'Element Plus 全组件目录',
metrics: 'KPI 指标卡组件',
buttons: '按钮与动作层级',
forms: '表单控件与查询条件',
table: '数据表格与分页',
status: '状态、反馈与审计线',
navigation: '菜单、顶部导航与面包屑',
}[section.id],
description: {
foundations: '统一颜色、字号、间距、圆角和状态色,所有 OFBiz 新前端页面从这里继承。',
catalog: '对照本地 Element Plus 包导出的完整组件、子组件、服务和指令。',
metrics: '业务指标卡按大、小、最小三种密度复用,不再使用左侧粗色条。',
buttons: '定义主动作、辅助动作、危险动作和批量动作的视觉层级。',
forms: '覆盖查询条件、录入控件、选择器、日期范围和高级条件。',
table: '定义 ERP 高密度表格、状态列、金额列、风险列和分页组合。',
status: '覆盖标签、警告、进度、徽标、时间线等反馈模式。',
navigation: '补齐左侧菜单、顶部菜单、面包屑、页面头和详情抽屉入口。',
}[section.id],
}))
export const metricCards = [
{ label: '待处理订单', value: '128', delta: '+12.4%', tone: 'primary', icon: Tickets },
{ label: '逾期应收', value: '¥842k', delta: '-3.1%', tone: 'danger', icon: Money },
{ label: '库存预警', value: '36', delta: '+8', tone: 'warning', icon: Goods },
{ label: '今日发运', value: '214', delta: '+21.8%', tone: 'success', icon: Van },
]
export const metricCardVariants = [
{ id: 'large', label: '大卡片 / 安静底色', className: 'erp-metric-card--large' },
{ id: 'small', label: '小卡片 / 细边框', className: 'erp-metric-card--small' },
{ id: 'mini', label: '最小卡片 / 紧凑列表', className: 'erp-metric-card--mini' },
]
export const orderRows = [
{
orderId: 'SO-10642',
customer: '上海辰光零售',
status: '已审批',
statusType: 'success',
amount: '¥128,420.00',
warehouse: '华东中心仓',
owner: 'Lina Chen',
due: '2026-06-08',
risk: '低',
},
{
orderId: 'SO-10643',
customer: '杭州沐野商贸',
status: '信用检查',
statusType: 'warning',
amount: '¥42,018.50',
warehouse: '华东中心仓',
owner: 'Ming Li',
due: '2026-06-09',
risk: '中',
},
{
orderId: 'PO-77218',
customer: '深圳北仓供应',
status: '等待收货',
statusType: 'primary',
amount: '¥315,900.00',
warehouse: '华南备货仓',
owner: 'Doris Xu',
due: '2026-06-10',
risk: '低',
},
{
orderId: 'INV-44019',
customer: '南京泽同制造',
status: '逾期',
statusType: 'danger',
amount: '¥73,600.00',
warehouse: '财务共享中心',
owner: 'Owen Zhang',
due: '2026-06-03',
risk: '高',
},
]
export const timelineItems = [
{ timestamp: '09:20', content: '销售订单完成审批', type: 'success' },
{ timestamp: '10:05', content: '库存锁定 18 件', type: 'primary' },
{ timestamp: '10:44', content: '触发信用额度复核', type: 'warning' },
]
export const componentInventoryMeta = {
packageName: 'element-plus',
packageVersion: '2.11.8',
componentDirectories: 122,
publicElExports: 123,
directiveDirectories: 4,
}
export const componentCatalog = [
{
group: '基础与全局',
description: '基础动作、排版、全局配置、滚动、固定区域和水印。',
items: [
['Button', '高'],
['Button Group', '高'],
['Icon', '高'],
['Link', '中'],
['Text', '中'],
['Config Provider', '高'],
['Space', '高'],
['Divider', '中'],
['Scrollbar', '中'],
['Affix', '中'],
['Backtop', '低'],
['Watermark', '中'],
],
},
{
group: '布局与容器',
description: '页面骨架、栅格、分割面板、卡片和折叠容器。',
items: [
['Container', '高'],
['Header', '中'],
['Aside', '中'],
['Main', '中'],
['Footer', '低'],
['Row', '高'],
['Col', '高'],
['Splitter', '高'],
['Splitter Panel', '高'],
['Card', '中'],
['Collapse', '中'],
['Collapse Item', '中'],
],
},
{
group: '表单输入',
description: '查询条件、录入、选择、日期、上传和复杂实体选择。',
items: [
['Form', '高'],
['Form Item', '高'],
['Input', '高'],
['Input Number', '高'],
['Input Tag', '中'],
['Input OTP', '低'],
['Autocomplete', '高'],
['Mention', '中'],
['Select', '高'],
['Select V2', '高'],
['Option', '中'],
['Option Group', '中'],
['Cascader', '高'],
['Cascader Panel', '中'],
['Tree Select', '高'],
['Checkbox', '高'],
['Checkbox Button', '中'],
['Checkbox Group', '高'],
['Radio', '中'],
['Radio Button', '中'],
['Radio Group', '中'],
['Switch', '中'],
['Slider', '低'],
['Rate', '低'],
['Color Picker', '低'],
['Color Picker Panel', '低'],
['Date Picker', '高'],
['Date Picker Panel', '中'],
['Time Picker', '中'],
['Time Select', '中'],
['Transfer', '中'],
['Upload', '高'],
],
},
{
group: '数据展示',
description: 'ERP 的列表、详情、树、指标、图片和空状态。',
items: [
['Table', '高'],
['Table Column', '高'],
['Table V2', '高'],
['Pagination', '高'],
['Tag', '高'],
['Check Tag', '中'],
['Badge', '高'],
['Descriptions', '高'],
['Descriptions Item', '高'],
['Tree', '高'],
['Tree V2', '高'],
['Statistic', '中'],
['Countdown', '低'],
['Avatar', '中'],
['Avatar Group', '低'],
['Calendar', '中'],
['Image', '中'],
['Image Viewer', '中'],
['Carousel', '低'],
['Carousel Item', '低'],
['Empty', '高'],
['Skeleton', '中'],
['Skeleton Item', '中'],
['Progress', '中'],
['Result', '中'],
],
},
{
group: '导航',
description: '模块切换、业务路径、标签页和页面标题。',
items: [
['Menu', '高'],
['Menu Item', '高'],
['Menu Item Group', '中'],
['Sub Menu', '高'],
['Tabs', '高'],
['Tab Pane', '高'],
['Breadcrumb', '高'],
['Breadcrumb Item', '高'],
['Dropdown', '高'],
['Dropdown Menu', '高'],
['Dropdown Item', '高'],
['Page Header', '中'],
['Steps', '高'],
['Step', '中'],
['Timeline', '高'],
['Timeline Item', '中'],
['Anchor', '中'],
['Anchor Link', '中'],
['Segmented', '高'],
],
},
{
group: '反馈与覆盖层',
description: '保存结果、校验、确认、通知和异常展示。',
items: [
['Alert', '高'],
['Message', '高'],
['Message Box', '高'],
['Notification', '中'],
['Loading', '高'],
['Popconfirm', '高'],
['Tooltip', '高'],
['Popover', '中'],
['Dialog', '高'],
['Drawer', '高'],
['Popper', '低'],
['Popper Arrow', '低'],
['Popper Content', '低'],
['Popper Trigger', '低'],
['Overlay', '低'],
['Tour', '低'],
['Tour Step', '低'],
],
},
{
group: '虚拟化与内部基础',
description: '多数不直接用于业务页,但会影响性能组件、组合组件和交互行为。',
items: [
['Auto Resizer', '高'],
['Infinite Scroll', '中'],
['Collapse Transition', '低'],
['Focus Trap', '低'],
['Virtual List', '中'],
['Roving Focus Group', '低'],
['Collection', '低'],
['Slot', '低'],
['Base', '低'],
],
},
{
group: '指令与服务导出',
description: '来自本地 directives 目录和公开 El* 导出的行为层能力。',
items: [
['Click Outside', '中'],
['Mousewheel', '低'],
['Repeat Click', '低'],
['Trap Focus', '低'],
['Loading Directive', '高'],
['Loading Service', '高'],
['Popover Directive', '中'],
['Message Service', '高'],
['MessageBox Service', '高'],
['Notification Service', '中'],
],
},
]
export function getRiskClass(risk) {
return {
'risk-low': risk === '低',
'risk-mid': risk === '中',
'risk-high': risk === '高',
}
}
+7
View File
@@ -0,0 +1,7 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import './styles/erp-ui.css'
import './styles/app.css'
import App from './App.vue'
createApp(App).use(ElementPlus).mount('#app')
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
* {
box-sizing: border-box;
}
html {
min-width: 1180px;
background: var(--erp-color-surface-muted);
}
body {
margin: 0;
color: var(--erp-color-text);
font-family: var(--erp-font-family);
font-size: var(--erp-font-size-md);
letter-spacing: 0;
}
button,
input,
textarea,
select {
font-family: inherit;
}
@@ -0,0 +1,114 @@
:root {
--el-color-primary: var(--erp-color-primary);
--el-color-primary-light-3: #6f96ef;
--el-color-primary-light-5: #9bb7f4;
--el-color-primary-light-7: #c9d8fa;
--el-color-primary-light-8: #dce6fc;
--el-color-primary-light-9: var(--erp-color-primary-soft);
--el-color-primary-dark-2: var(--erp-color-primary-strong);
--el-color-success: var(--erp-color-success);
--el-color-success-light-9: var(--erp-color-success-soft);
--el-color-warning: var(--erp-color-warning);
--el-color-warning-light-9: var(--erp-color-warning-soft);
--el-color-danger: var(--erp-color-danger);
--el-color-danger-light-9: var(--erp-color-danger-soft);
--el-color-error: var(--erp-color-danger);
--el-color-error-light-9: var(--erp-color-danger-soft);
--el-color-info: var(--erp-color-info);
--el-border-radius-base: var(--erp-radius-sm);
--el-font-family: var(--erp-font-family);
--el-text-color-primary: var(--erp-color-text);
--el-text-color-regular: var(--erp-color-text-muted);
--el-text-color-secondary: var(--erp-color-text-subtle);
--el-border-color: var(--erp-color-border);
--el-border-color-light: var(--erp-color-border-soft);
--el-fill-color-light: var(--erp-color-surface-muted);
--el-bg-color-page: var(--erp-color-surface-muted);
}
.el-button {
font-weight: 600;
}
.el-button--primary {
--el-button-bg-color: var(--erp-color-primary);
--el-button-border-color: var(--erp-color-primary);
--el-button-hover-bg-color: var(--erp-color-primary-strong);
--el-button-hover-border-color: var(--erp-color-primary-strong);
}
.el-button--success {
--el-button-bg-color: var(--erp-color-success);
--el-button-border-color: var(--erp-color-success);
}
.el-button--warning {
--el-button-bg-color: var(--erp-color-warning);
--el-button-border-color: var(--erp-color-warning);
}
.el-button--danger {
--el-button-bg-color: var(--erp-color-danger);
--el-button-border-color: var(--erp-color-danger);
}
.el-input__wrapper,
.el-select__wrapper,
.el-textarea__inner {
border-radius: var(--erp-radius-sm);
box-shadow: 0 0 0 1px var(--erp-color-border) inset;
}
.el-input__wrapper:hover,
.el-select__wrapper:hover,
.el-textarea__inner:hover {
box-shadow: 0 0 0 1px #bfc8d8 inset;
}
.el-tag {
border-radius: var(--erp-radius-xs);
font-weight: 650;
}
.el-tag--success {
--el-tag-bg-color: var(--erp-color-success-soft);
--el-tag-border-color: #b8decf;
--el-tag-text-color: var(--erp-color-success);
}
.el-tag--warning {
--el-tag-bg-color: var(--erp-color-warning-soft);
--el-tag-border-color: #f2d39b;
--el-tag-text-color: var(--erp-color-warning);
}
.el-tag--danger {
--el-tag-bg-color: var(--erp-color-danger-soft);
--el-tag-border-color: #f1bbbb;
--el-tag-text-color: var(--erp-color-danger);
}
.el-menu {
border-right: 0;
}
.el-menu-item {
height: 38px;
border-radius: var(--erp-radius-sm);
margin: 2px 8px;
color: #465066;
}
.el-menu-item.is-active {
color: var(--erp-color-primary);
background: var(--erp-color-primary-soft);
}
.el-tabs__item {
font-weight: 650;
}
.el-descriptions__label {
color: #566176;
font-weight: 650;
}
@@ -0,0 +1,481 @@
.erp-form {
display: grid;
gap: var(--erp-space-4);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--erp-space-3);
}
.metrics-grid--mini {
gap: var(--erp-space-2);
}
.erp-metric-card {
--erp-metric-color: var(--erp-color-primary);
--erp-metric-soft: var(--erp-color-primary-soft);
position: relative;
min-height: 104px;
padding: var(--erp-space-4);
background: var(--erp-color-surface);
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-metric-card.is-success {
--erp-metric-color: var(--erp-color-success);
--erp-metric-soft: var(--erp-color-success-soft);
}
.erp-metric-card.is-warning {
--erp-metric-color: var(--erp-color-warning);
--erp-metric-soft: var(--erp-color-warning-soft);
}
.erp-metric-card.is-danger {
--erp-metric-color: var(--erp-color-danger);
--erp-metric-soft: var(--erp-color-danger-soft);
}
.erp-metric-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--erp-space-3);
margin-bottom: var(--erp-space-3);
}
.erp-metric-card__head span {
color: var(--erp-color-text-muted);
font-size: var(--erp-font-size-xs);
font-weight: 700;
}
.erp-metric-card__head .el-icon {
color: var(--erp-metric-color);
}
.erp-metric-card strong {
display: block;
margin-bottom: var(--erp-space-1);
color: var(--erp-color-text);
font-size: 25px;
line-height: 1;
}
.erp-metric-card small {
color: var(--erp-color-text-muted);
font-size: var(--erp-font-size-xs);
font-weight: 700;
}
.erp-metric-card--small,
.erp-metric-card--outline {
border-color: color-mix(in srgb, var(--erp-metric-color) 44%, var(--erp-color-border-soft));
box-shadow: 0 1px 0 rgba(29, 36, 51, 0.03);
}
.erp-metric-card--large,
.erp-metric-card--quiet {
background: linear-gradient(180deg, var(--erp-metric-soft), #ffffff 66%);
border-color: color-mix(in srgb, var(--erp-metric-color) 18%, var(--erp-color-border-soft));
}
.erp-metric-card--mini,
.erp-metric-card--compact {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"head value"
"delta value";
min-height: 74px;
align-items: center;
padding: var(--erp-space-3);
border-color: var(--erp-color-border-soft);
}
.erp-metric-card--mini .erp-metric-card__head,
.erp-metric-card--compact .erp-metric-card__head {
grid-area: head;
justify-content: flex-start;
margin-bottom: 2px;
}
.erp-metric-card--mini strong,
.erp-metric-card--compact strong {
grid-area: value;
margin-bottom: 0;
font-size: 22px;
text-align: right;
}
.erp-metric-card--mini small,
.erp-metric-card--compact small {
grid-area: delta;
color: var(--erp-metric-color);
}
.erp-table {
--erp-table-header-bg-color: #f2f5fa;
--erp-table-header-text-color: #344054;
--erp-table-row-hover-bg-color: #f5f8ff;
--erp-table-border-color: var(--erp-color-border-soft);
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
overflow: hidden;
}
.erp-table .el-table__cell {
padding: 7px 0;
}
.erp-table th.el-table__cell {
font-weight: 700;
}
.erp-virtual-table {
overflow: hidden;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-dense-pagination {
--el-pagination-font-size: var(--erp-font-size-sm);
justify-content: flex-end;
}
.erp-field-control,
.erp-date-range,
.erp-entity-lookup,
.erp-tree-select {
width: 100%;
}
.erp-choice-group {
display: inline-flex;
flex-wrap: wrap;
gap: var(--erp-space-2);
}
.erp-side-menu {
border-right: 0;
}
.erp-side-menu .el-menu-item,
.erp-side-menu .el-sub-menu__title {
height: 38px;
color: #465066;
font-size: var(--erp-font-size-sm);
}
.erp-side-menu .el-menu-item.is-active {
color: var(--erp-color-primary);
background: var(--erp-color-primary-soft);
}
.erp-side-menu .el-sub-menu .el-menu-item {
min-width: 0;
padding-left: 44px;
}
.erp-breadcrumb {
font-size: var(--erp-font-size-sm);
}
.erp-breadcrumb .el-breadcrumb__inner {
color: var(--erp-color-text-muted);
font-weight: 650;
}
.erp-breadcrumb .el-breadcrumb__item:last-child .el-breadcrumb__inner {
color: var(--erp-color-text);
}
.erp-page-header {
padding-bottom: var(--erp-space-3);
border-bottom: 1px solid var(--erp-color-border-soft);
}
.erp-tabs .el-tabs__header {
margin-bottom: var(--erp-space-3);
}
.erp-tabs .el-tabs__item {
height: 36px;
}
.erp-action-button,
.erp-action-dropdown .el-button {
min-width: 86px;
}
.erp-status-tag,
.erp-status-badge {
font-weight: 700;
}
.erp-status-alert {
border-radius: var(--erp-radius-sm);
}
.erp-detail-descriptions .el-descriptions__cell {
padding: 9px 12px;
}
.erp-detail-descriptions .el-descriptions__label {
width: 96px;
color: #566176;
font-weight: 700;
background: #f5f7fb;
}
.erp-entity-card {
min-height: 112px;
padding: var(--erp-space-4);
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
box-shadow: var(--erp-shadow-sm);
}
.erp-entity-card header {
margin-bottom: var(--erp-space-2);
color: var(--erp-color-text-muted);
font-size: var(--erp-font-size-xs);
font-weight: 800;
}
.erp-entity-card strong {
display: block;
color: var(--erp-color-text);
font-size: var(--erp-font-size-lg);
}
.erp-entity-card p {
margin: var(--erp-space-2) 0 0;
color: var(--erp-color-text-muted);
}
.erp-tree,
.erp-tree-select {
--el-tree-node-hover-bg-color: var(--erp-color-surface-muted);
}
.erp-upload .el-upload-dragger {
padding: var(--erp-space-5);
border-color: var(--erp-color-border);
border-radius: var(--erp-radius-md);
}
.erp-empty-state {
padding: var(--erp-space-5);
background: #ffffff;
border: 1px dashed var(--erp-color-border);
border-radius: var(--erp-radius-md);
}
.erp-progress {
--el-color-primary: var(--erp-color-primary);
}
.erp-skeleton {
padding: var(--erp-space-4);
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-calendar {
overflow: hidden;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-calendar .el-calendar__body {
padding: var(--erp-space-3);
}
.erp-calendar .el-calendar-table .el-calendar-day {
min-height: 44px;
height: 44px;
padding: 6px;
}
.erp-timeline {
padding: var(--erp-space-2) 0 0;
}
.erp-steps {
padding: var(--erp-space-4);
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-collapse {
overflow: hidden;
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-collapse .el-collapse-item__header,
.erp-collapse .el-collapse-item__content {
padding-right: var(--erp-space-4);
padding-left: var(--erp-space-4);
}
.erp-media-preview {
width: 220px;
height: 132px;
overflow: hidden;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-carousel {
overflow: hidden;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-inline-text,
.erp-inline-link {
font-size: var(--erp-font-size-sm);
font-weight: 650;
}
.erp-space {
padding: var(--erp-space-3);
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-statistic {
min-width: 150px;
padding: var(--erp-space-4);
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-md);
}
.erp-switch,
.erp-slider,
.erp-rate,
.erp-color-picker {
max-width: 360px;
}
.erp-transfer {
max-width: 720px;
}
.erp-dialog,
.erp-drawer,
.erp-popconfirm,
.erp-tooltip {
font-size: var(--erp-font-size-sm);
}
.form-grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr 1.2fr 0.85fr;
gap: var(--erp-space-4);
align-items: end;
}
.form-grid.secondary {
grid-template-columns: 1fr 1fr 1.2fr;
}
.table-search {
width: 270px;
}
.icon-cell {
display: inline-flex;
align-items: center;
gap: 6px;
color: #465066;
}
.risk-dot {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 700;
}
.risk-dot::before {
width: 7px;
height: 7px;
content: "";
border-radius: 50%;
}
.risk-low::before {
background: var(--erp-color-success);
}
.risk-mid::before {
background: var(--erp-color-warning);
}
.risk-high::before {
background: var(--erp-color-danger);
}
.component-chip-list {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.component-chip {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 26px;
padding: 4px 8px;
color: #344054;
font-size: var(--erp-font-size-xs);
font-weight: 700;
background: #ffffff;
border: 1px solid var(--erp-color-border-soft);
border-radius: var(--erp-radius-xs);
}
.component-chip small {
display: inline-grid;
min-width: 16px;
height: 16px;
place-items: center;
padding: 0 4px;
color: var(--erp-color-text-muted);
font-size: 11px;
background: var(--erp-color-surface-muted);
border-radius: 999px;
}
.component-chip.priority-高 {
color: var(--erp-color-primary-strong);
border-color: #bdd0fb;
background: #f4f7ff;
}
.component-chip.priority-高 small {
color: #ffffff;
background: var(--erp-color-primary);
}
.component-chip.priority-中 {
color: #525f76;
}
.component-chip.priority-低 {
color: #7d8798;
background: #fafbfc;
}
+5
View File
@@ -0,0 +1,5 @@
@import 'element-plus/dist/index.css';
@import './tokens.css';
@import './base.css';
@import './element-overrides.css';
@import './erp-patterns.css';
+41
View File
@@ -0,0 +1,41 @@
:root {
color-scheme: light;
--erp-color-primary: #2764e7;
--erp-color-primary-strong: #1947aa;
--erp-color-primary-soft: #e8efff;
--erp-color-success: #16845b;
--erp-color-success-soft: #e6f5ee;
--erp-color-warning: #b86b00;
--erp-color-warning-soft: #fff4df;
--erp-color-danger: #c73535;
--erp-color-danger-soft: #fdecec;
--erp-color-info: #4f5d75;
--erp-color-text: #1d2433;
--erp-color-text-muted: #667085;
--erp-color-text-subtle: #8a94a6;
--erp-color-border: #d9dee8;
--erp-color-border-soft: #e8ebf1;
--erp-color-surface: #ffffff;
--erp-color-surface-muted: #f7f8fb;
--erp-color-surface-raised: #fbfcfe;
--erp-shadow-sm: 0 1px 2px rgba(29, 36, 51, 0.06);
--erp-shadow-md: 0 12px 24px rgba(29, 36, 51, 0.08);
--erp-radius-xs: 4px;
--erp-radius-sm: 6px;
--erp-radius-md: 8px;
--erp-space-1: 4px;
--erp-space-2: 8px;
--erp-space-3: 12px;
--erp-space-4: 16px;
--erp-space-5: 20px;
--erp-space-6: 24px;
--erp-space-8: 32px;
--erp-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--erp-font-size-xs: 12px;
--erp-font-size-sm: 13px;
--erp-font-size-md: 14px;
--erp-font-size-lg: 18px;
--erp-font-size-xl: 24px;
--erp-line-height-tight: 1.25;
--erp-line-height-base: 1.5;
}