front/docs/前端页面设计规范.md
zzh f1d219ae48 feat: ship billing UI for additional services, conditions, and fee insights
Add product/channel billing condition editors, place-order additional services, billing simulator, forecast-to-inbound fee delta and margin warnings, insurance-aware claim tips, OMS preset dict seeds, and related AI skills/system pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 18:09:22 +08:00

23 KiB
Raw Blame History

中后台前端页面设计规范

基于 itfx-front 项目现有技术栈与实现,整理的一套通用、可直接落地的前端设计规范。
后续开发须遵循本规范,确保视觉与交互一致性。


一、技术栈约定

类别 技术选型 版本/说明
框架 Vue 3 使用 Composition API、<script setup>
UI 库 Arco Design Vue 主 UI 框架,优先使用
辅助 UI Element Plus 仅在特殊场景使用
表格 vxe-table / Handsontable 见「表格」章节
构建 Vite 6.x
状态 Pinia
国际化 vue-i18n
样式 Less + UnoCSS + 全局 CSS 变量
路由 vue-router 4.x

二、布局规范

2.1 整体结构

a-layout (垂直)
├── TopNav(顶部导航,fixed)
├── a-layout-content
│   ├── paddingTop: 顶部导航高度 + Tab 栏高度(可选)
│   └── PageLayout(router-view + keep-alive)
└── Footer(可选)

约定:

  • 主内容背景色:#f0f3f4
  • 页面容器使用 [class$='-container'],统一 padding: 16px
  • 内容区域使用 overflow: hidden、height: 100% 保证高度正确

2.2 页面内容结构

单页内容推荐结构:

<div class="xxx-container" style="background-color: #fff;">
  <!-- 1. 页面头部操作区(可选) -->
  <div class="page-header">
    <a-space>...</a-space>
    <a-space>...</a-space>
  </div>

  <!-- 2. 表格工具栏 -->
  <div class="table-toolbar">
    <div class="toolbar-left">主操作、批量操作</div>
    <div class="toolbar-right">分页控制等</div>
  </div>

  <!-- 3. 表格/表单内容 -->
  ...
</div>

2.3 工具栏布局

  • 使用 table-toolbar 作为表格上方工具栏
  • 左侧 toolbar-left:新增、删除、导出等主操作
  • 右侧 toolbar-right:分页、筛选等
  • 分页位置(必选):所有列表的分页组件统一放在紧邻列表上方工具栏的右侧(即相对列表区域的右上角),见「七、7.3 分页」;不得放在表格下方或列表底部
  • 使用 a-space :size="8" 或 display: flex; gap: 8px 控制按钮间距

2.4 响应式断点(Less)

@screen-xs: 480px;   // 手机
@screen-sm: 576px;   // 平板
@screen-md: 768px;   // 小屏
@screen-lg: 992px;   // 桌面
@screen-xl: 1200px;  // 大屏
@screen-xxl: 1600px; // 超大屏

2.5 页面壳、白卡片与列表区尺寸(列表 / Handsontable 类页面)

以下约定与 default-layout + page-layout、*-container / *-page 等实现一致,避免「灰底大块留白、列表高度塌缩、内外边距叠成 32px/40px」等问题。

2.5.1 页面层(灰底壳)

项目 约定
背景色 #f0f3f4(与布局主内容区一致)
页面根容器 padding 16px 16px 24px:上/左/右 16px,底 24px —— 与视口底边留白,列表区随 flex 撑满后,整块内容与屏幕底边保持 24px 间距
参与主内容区高度 flex: 1; min-height: 0; display: flex; flex-direction: column;,与 PageLayout 子节点 flex 链衔接
纵向滚动 页面根容器宜 overflow-y: hidden(或仅最外层允许滚动),表格/Handsontable 在内部 .list-wrap / .wtHolder 滚动,避免整页与表格双滚动条

2.5.2 白卡片(白色内容块)

项目 约定
视觉 背景 #fff,border-radius: 8px,box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08)(可按页面微调透明度)
撑满剩余高度 flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden
内边距(与列表区配合) 二选一,避免与 .list-wrap 重复叠加:
• 推荐:白卡片 padding: 0,由 页头/搜索行、.table-toolbar、.list-wrap 分区控制留白(见下);
• 简化页:白卡片统一 padding: 16px,则内部列表区不再重复加左右底 padding

2.5.3 表格工具栏(.table-toolbar)

项目 约定
与列表横向对齐 padding: 16px 16px 0(上 16、左右 16,底由与列表的间距承担)
与分页/标题间距 margin-bottom: 16px(分页组件(PaginationPro)与列表区 .list-wrap 的固定间距)
收缩 flex-shrink: 0

分页组件放置约束:PaginationPro 放在 .toolbar-right 内,并与 toolbar-left 保持在同一行(同一个 flex 行中)展示;工具栏外层使用 padding: 16px 16px 0 以保证分页组件距上边/右边均为 16px,且不需要在 PaginationPro 外再额外套一层 padding。

有独立搜索行(如轨迹配置页)时:搜索框可用 margin: 16px 16px 16px,与工具栏、列表区 左右 16px 对齐。

2.5.4 列表区(.list-wrap)

用于包裹 HandsontablePro 或需 固定测量高度 的表格容器(配合 useTableContainerHeight 的 tableContainerRef 挂在本层)。

项目 约定
与内容区四边距离 默认:padding: 16px(上、右、下、左均为 16px,相对白卡片内容边缘或分区后的可用区域,勿与白卡片 padding 重复叠加)。
与 .list-toolbar 联用:当 .list-toolbar 使用 margin-bottom: 16px 作为“分页组件到列表”的固定间距时,为避免重复留白,建议 .list-wrap 使用 padding: 0 16px 16px(上 0、左右/下 16)。
布局 flex: 1; min-height: 0; width: 100%; box-sizing: border-box; overflow: hidden; display: flex; flex-direction: column
触摸/滚动冒泡 可选:overscroll-behavior: contain; touch-action: pan-x pan-y
子级 Handsontable 建议 :deep(.handsontable-pro-container) { flex: 1; min-height: 0; display: flex; flex-direction: column; },保证纵向撑满

分页组件同一行约束(列表工具栏场景):当页面存在 .list-toolbar / table-toolbar 时,PaginationPro 必须放在右侧同一行的工具栏区域(如 .toolbar-right),不要将分页组件单独再做成一行(避免“工具栏一行 + 分页下一行”的不一致布局)。

2.5.5 与布局组件的关系

  • PageLayout 下直接子节点(路由页根)需能被主内容区赋予高度:项目中通过 flex: 1; min-height: 0 等与 a-layout-content 链式配合。
  • default-layout 中对 [class$='-container'] 的样式仅作用于布局模板内节点;业务页根类名(如 xxx-page、track-order-container)需在页面自身样式中按上表补齐 flex 与 padding。

三、色彩规范

3.1 主题色(CSS 变量)

通过 :root 统一管理,运行时由 themeColor 工具更新:

:root {
  --app-primary-color: #007979;
  --app-primary-color-hover: #006868;
  --app-primary-color-light: #e6f4f4;
  --app-navbar-bg: #007979;
  --app-sidebar-bg: #007979;
  --app-button-gap: 8px; /* 相邻按钮间距 */
}

约定:

  • 主色:var(--app-primary-color) 或 #007979
  • Hover 色:var(--app-primary-color-hover)
  • 浅底(选中背景):var(--app-primary-color-light)
  • 页面背景:var(--color-bg-1)(Arco)
  • 内容区背景:#fff 或 #f0f3f4

3.2 组件配色

  • 主按钮、Switch、Tabs 指示线、菜单选中:使用主题色
  • 日期选择器、Input focus:border-color: var(--app-primary-color)
  • 链接激活态:color: var(--app-primary-color)

四、字体规范

4.1 字体栈

font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
  "Microsoft YaHei", Inter, Roboto, "Helvetica Neue", Arial, sans-serif;

代码字体:

font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
  "Courier New", monospace;

4.2 字号与行高

  • 全局基准字号:14px
  • 表单、按钮、输入框:14px
  • 日期选择器、部分紧凑区域:12px(如 .notice 内)
  • 菜单文字:12px,font-weight: bold

五、组件规范

5.1 按钮

  • 高度:32px,border-radius: 4px
  • 图标按钮:32px × 32px 正方形
  • 相邻按钮间距:统一 8px
    • 使用 a-space :size="8" 或 gap: 8px / gap: var(--app-button-gap)
    • 适用于工具栏、表格操作列、弹窗底部按钮组
  • 主操作使用 type="primary"

5.2 输入类(Input / Select / Picker)

  • 统一高度:32px(Input)、28px(Picker)
  • border-radius: 4px
  • Focus / Hover 使用主题色 border 与 shadow

5.3 卡片 / 弹窗 / 下拉

  • border-radius: 8px(Card、Modal、Popover、Dropdown、Drawer、Table、Tag、Alert、Message、Notification)

5.4 Pro 系列组件

组件 用途
FormPro 配置驱动表单,itemList + comp + compProps
TablePro VxeGrid 封装,tableConfig + api + formConfig
CustomList 自定义列 + TablePro,支持列管理
HandsontablePro 类 Excel 表格
ModalPro 弹窗,配合 useModal
DrawerPro 抽屉
SelectPro 增强选择器
JsonEditorPro / JsonDiffPro JSON 编辑与对比
CodeMirror 代码编辑

约定:

  • 增强型封装统一使用 Pro 后缀
  • 优先使用 Pro 组件,减少重复造轮子

5.5 消息与确认

  • 使用 $baseMessage、$baseConfirm(如有)
  • 或 Arco 的 Message、Modal.confirm、a-popconfirm

六、表单规范

6.1 布局

  • 表单类型:horizontal、vertical、inline
  • 标签列:label-col-props="{ span: 6 }"(或 5)
  • 内容列:wrapper-col-props="{ span: 16 }"(或 19)
  • FormPro:labelColProps: { span: 5 }、wrapperColProps: { span: 19 }、rowGutter: 20、defaultSpan: 6

6.2 FormPro 配置示例

itemList: [
  {
    comp: 'AInput',
    field: 'name',
    label: '名称',
    required: true,
    span: 6,
    rules: [{ required: true, message: '请输入名称' }],
    compProps: { placeholder: '请输入' },
  },
  {
    comp: 'ASelect',
    field: 'status',
    label: '状态',
    compProps: { options: [...], placeholder: '请选择' },
  },
]

6.3 校验

  • 使用 Arco Form 的 rules
  • 提交前调用 formRef.validate() 统一校验
  • 必填项使用 required: true 并配置 message

七、表格规范

7.1 组件选择

场景 组件 说明
常规 CRUD 列表 TablePro 带搜索表单、分页、API 集成
自定义列 + 列管理 CustomList 包裹 TablePro
类 Excel 编辑 HandsontablePro 单元格编辑、复制粘贴
简单列表(角色、菜单等) a-table Arco 原生

7.2 TablePro 配置

  • tableConfig:columns、toolbarConfig 等
  • api:分页接口,参数含 pageIndex、pageSize、表单值
  • formConfig:搜索区域
  • columnConfig.useKey: true、resizable: true
  • showOverflow: 'tooltip'

7.3 分页

  • 位置(必选):所有分页组件(PaginationPro、Arco a-pagination、项目内其它分页封装等)须置于列表的右上角——即放在紧邻列表上方的工具栏右侧(table-toolbar / .list-toolbar 的 .toolbar-right,与 toolbar-left 同一行),与列表左右对齐、视觉上位于列表区域上方靠右。禁止将分页放在表格下方、列表底部、或单独居中另起一行(与「二、2.5.3」「2.5.4」工具栏约定一致)。
  • 可选 pageSize:10 / 30 / 50 / 100(按需裁剪)
  • 默认 pageSize:30
  • 接口参数:pageIndex、pageSize;返回:totalCount、items 或 data.list

7.4 操作列

  • 编辑、删除、分配等使用 a-button type="text" size="small"
  • 删除使用 a-popconfirm
  • 操作按钮需权限控制(见「权限」)

7.5 HandsontablePro 列定义(必读)

HandsontablePro 的 columns 与原生 Handsontable 不同,必须使用项目约定的 ColumnDef 结构:

字段 类型 说明
key string 对应行数据字段名(如 jobId、statusLabel)
label string 表头文案(支持 t('...'))
width number 列宽(像素)
headerMode 'inline' | 'icon' | 'none' 混合表头:内嵌搜索 / 仅图标 / 纯展示;未设时由 searchable + filterType 推导
searchable / searchField / multiValue — 表头搜索;multiValue 显示「多」批量入口

正确示例:

const hotColumns = computed(() => [
  { key: 'jobId', label: t('order.batchJobs.table.jobId'), width: 90 },
  { key: 'statusLabel', label: t('order.batchJobs.table.status'), width: 100 },
])

错误示例(会导致表头、单元格均为空):

// ❌ 勿用 Handsontable 原生 data/title
{ data: 'jobId', title: '任务 ID', width: 90 }

操作列渲染器签名固定为 (rowData, rowIndex) => string,勿写成 (row, col, rowData)。

固定列序(组件内置,勿再传 action-column-position / action-column-width):

顺序 列 说明
1 操作列(可选) 有 show-action-column 时永远第一列;宽度按按钮内容自适应;开启 stretch-columns 时不被拉伸
2 序号列 永远紧随操作列(无操作时为第一列);固定宽度 50px;开启 stretch 时不被拉伸
3 复选框列(可选) show-checkbox-column;固定 60px,stretch 时不被拉伸
4+ 数据列 columns 定义的业务列(stretch 时仅这些列被拉宽)

customCellRenderer(row, col, value, rowData) 的 col 为表格绝对列索引(含操作/序号/复选框)。有操作列时数据列从 col - 2 起算(另有复选框再减 1)。

行数据字段名须与 key 一致;展示用衍生字段(如 actionLabel)在 mapRow 中提前组装。


八、交互规范

8.1 弹窗 / 抽屉

  • 使用 ModalPro + useModal() 管理显隐与数据
  • 标准流程:openModal(true, data) → 编辑 → setLoading(true) → 提交 → 成功 → closeModal()、刷新列表
  • 弹窗宽度:常规 600px,复杂表单 800px 或更高

8.2 加载与空态

  • 表格:loading 状态
  • 弹窗提交:confirm-loading
  • 空数据:使用 Arco Empty 或统一空态组件

8.3 按钮顺序

  • 主操作(确定、保存)在右侧
  • 取消 / 重置在左侧
  • destructive 操作(删除)单独放置,并加确认

九、权限规范

9.1 权限格式

Module.Resource.Action,如:

  • Rbac.Roles.Create
  • Rbac.Users.Update
  • Api.Interfaces.Query
  • Track.Orders.Query

9.2 使用方式

模板:

<a-button
  v-if="hasPermission('Rbac.Roles.Create')"
  type="primary"
  @click="handleAdd"
>
  新增
</a-button>

路由:

{
  path: '/system/roles',
  meta: { permission: 'Rbac.Roles.Query' },
}

工具函数:

import { hasPermission, hasAnyPermission, hasAllPermissions, hasRole } from '@/utils/rbac-permission';

9.3 菜单

  • 菜单基于 RBAC menus 与 userMenuIdSet 过滤
  • 无权限的菜单项不展示

十、命名规范

10.1 文件与目录

类型 规范 示例
页面组件 index.vue 置于功能目录下 views/api/upstreamInterface/index.vue
子组件 PascalCase 或 kebab-case ServiceProviderInterfaceModal.vue、AccountList.vue
布局组件 kebab-case top-nav、tab-bar、page-layout
共享组件 PascalCase FormPro、TablePro、CustomList
API 模块 camelCase 或 PascalCase interface-serviceProvider、TrackOrder
Store 模块 kebab-case rbac-user、tab-bar、app
国际化 zh-CN.ts、en-US.ts locale/zh-CN.ts

10.2 代码命名

类型 规范
变量 / 方法 camelCase
常量 UPPER_SNAKE_CASE 或 camelCase
类型 / 接口 PascalCase
组件名 PascalCase

10.3 路由

  • path:camelCase,如 /api/upstreamInterface、/track/kpiEarlyWarningConfig
  • name:PascalCase,如 InterfaceDetailTest

10.4 国际化 Key

  • 格式:模块.资源.键 或 common.xxx
  • 示例:system.role.actions.add、common.search、common.reset

十一、国际化规范

11.1 结构

  • 根:src/locale/zh-CN.ts、en-US.ts
  • 按页面拆分:views/xxx/locale/zh-CN.ts,在根 locale 中合并

11.2 使用

<template>{{ t('common.search') }}</template>
<script setup>
const { t } = useI18n();
</script>

11.3 必备 Key

  • common.search、common.reset、common.add、common.delete、common.editSuccess、common.addSuccess、common.saveSuccess
  • common.selectPlaceholder、common.actions、common.viewDetail

十二、API 与请求规范

12.1 分页接口

请求:

{
  pageIndex: number;
  pageSize: number;
  // 其它搜索参数
}

响应:

{
  totalCount: number;
  items: T[];  // 或 data.list
}

12.2 错误处理

  • 使用统一的 axios 拦截器(如 rbac-error-handler)
  • 401 自动跳转登录;其它错误统一提示

十三、样式约定

13.1 作用域

  • 组件内使用 scoped
  • 覆盖第三方样式时使用 :deep() 或全局类

13.2 容器类

  • 页面根容器:xxx-container / xxx-page(如 role-page、track-order-container),列表类页面推荐 padding: 16px 16px 24px,详见 §2.5 页面壳、白卡片与列表区尺寸
  • 白卡片:*-page-card、*-section 等,与 .list-wrap 的 padding 勿重复叠加
  • 列表区:.list-wrap,四边 16px(相对分区后的内容边缘)
  • 表格工具栏:table-toolbar、toolbar-left、toolbar-right,工具栏 padding: 16px 16px 0 与列表对齐
  • Handsontable/列表分页工具栏:.list-toolbar(当页面没有标准 table-toolbar 时),与 table-toolbar 保持一致,使用 padding: 16px 16px 0 且 margin-bottom: 16px;PaginationPro 必须与 toolbar-left 同一行展示(放在 .toolbar-right 内,禁止分页另起行),同时 .list-wrap 在联用场景建议 padding-top: 0(例如 padding: 0 16px 16px),实现分页距上/右 16px、距列表 16px

13.3 按钮间距

  • 相邻 arco-btn 之间:margin-left: 8px(全局已统一)
  • 使用 a-space :size="8" 包裹一组按钮

十四、检查清单(新页面开发)

  • 使用 xxx-container / xxx-page 或等效布局结构;列表页见 §2.5:灰底 padding: 16px 16px 24px、flex: 1; min-height: 0
  • 白卡片与 .list-wrap 内边距勿重复叠加(推荐白卡片 padding: 0 + 分区 padding)
  • .list-wrap:padding: 16px、flex: 1; min-height: 0;需测高时 ref 挂在本层
  • 工具栏使用 table-toolbar、toolbar-left、toolbar-right,工具栏 padding: 16px 16px 0
  • 按钮、输入框符合全局样式(高度、圆角)
  • 使用主题色变量,不硬编码 #007979
  • 弹窗 / 抽屉使用 ModalPro / DrawerPro + useModal
  • 表单使用 FormPro 或统一 label/span
  • 表格优先 TablePro / CustomList / HandsontablePro
  • HandsontablePro 列定义使用 key + label,勿用 data + title(§7.5)
  • 分页组件置于列表上方工具栏右侧(列表右上角),§7.3;分页 10/30/50/100,默认 30
  • 操作按钮按权限使用 hasPermission
  • 路由 meta 配置 permission
  • 新增前端页面路径若落在 /order/、/channel/、/system/ 等与 Admin API 同源前缀下,须在 nginx-default.conf / nginx-docker.conf 增加 ^~ 的 try_files /index.html =200 规则(§十五),避免刷新/直链 404
  • 文案通过 i18n,不写死中文
  • 命名符合本规范(文件、路由、变量、i18n key)

十五、部署与 nginx(SPA 路由)

生产环境 nginx 将 /order/、/channel/、/system/ 等前缀同时用于 Admin API 反代。浏览器刷新或直接打开前端 URL(如 /order/batchJobs)时,若未单独配置 SPA 回退,请求会被转发到后端 API 并返回 404。

约定:

  1. 每新增一个前端页面路由,若其 path 与 API 前缀冲突,必须在 nginx-default.conf 与 nginx-docker.conf 中、API 正则代理块之前增加更高优先级的 ^~ 规则,例如:
# 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)
location ^~ /order/batchJobs {
    try_files /index.html =200;
}
  1. 修改后执行 nginx -t && nginx -s reload(或重建前端 Docker 镜像使 nginx-docker.conf 生效)。

  2. 本地 Vite 开发不受此限制;仅在部署环境需遵守本节前缀规则。

当前订单模块已配置的前端路径示例:/order/priceCheck、/order/placeOrder、/order/myOrders、/order/batchJobs。


附录:常用代码片段

页面头部 + 权限按钮

<div class="page-header">
  <a-space>
    <a-button
      v-if="hasPermission('Module.Resource.Create')"
      type="primary"
      @click="handleAdd"
    >
      <template #icon><icon-plus /></template>
      {{ t('common.add') }}
    </a-button>
  </a-space>
  <a-space>
    <a-button @click="loadData">
      <template #icon><icon-refresh /></template>
    </a-button>
  </a-space>
</div>

表格操作列(编辑 / 删除)

<template #action="{ record }">
  <div class="action-buttons">
    <a-button
      v-if="hasPermission('Module.Resource.Update')"
      type="text"
      size="small"
      @click="handleEdit(record)"
    >
      <IconEdit />
    </a-button>
    <a-popconfirm
      v-if="hasPermission('Module.Resource.Delete')"
      :content="t('common.deleteConfirm')"
      @ok="handleDelete(record.id)"
    >
      <a-button type="text" size="small">
        <IconDelete />
      </a-button>
    </a-popconfirm>
  </div>
</template>

HandsontablePro 列表页(列 + 高度)

<script setup lang="ts">
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'

const { tableContainerRef, tableHeight: handsontableHeight } = useTableContainerHeight()

const hotColumns = computed(() => [
  { key: 'jobId', label: t('module.table.jobId'), width: 90 },
  { key: 'statusLabel', label: t('module.table.status'), width: 100 },
])
</script>

<template>
  <div class="module-page">
    <div class="module-page-card">
      <div class="table-toolbar">...</div>
      <a-spin class="list-spin" :loading="listLoading">
        <div ref="tableContainerRef" class="list-wrap">
          <HandsontablePro
            :columns="hotColumns"
            :data="tableRows"
            :height="handsontableHeight"
            :stretch-columns="true"
          />
        </div>
      </a-spin>
    </div>
  </div>
</template>

布局样式见 §2.5(flex: 1; min-height: 0,勿给 .list-wrap 写死 min-height: 480px 导致列表区塌缩或撑破)。

FormPro 查询表单

<FormPro
  ref="formProRef"
  :item-list="formItemList"
  :button-config="{ showBtn: true }"
  @submit="handleSearch"
  @reset="handleReset"
/>

文档版本:1.2 | 更新日期:2026-03-23