Vue 模板规范
BBC 8.0 页面主要编写 Vue 3 单文件组件,不再以手写完整 HTML 页面为主要开发方式。模板应保持语义清晰、可访问,并让 Prettier 负责缩进和属性换行。
单文件组件结构
新组件优先使用 <script setup lang="ts">:
<script setup lang="ts">
import { computed } from "vue";
import { Button as TButton } from "tdesign-vue-next";
import { useTranslate } from "@shop-tnt/components/useTranslate";
const props = defineProps<{ disabled?: boolean }>();
const emit = defineEmits<{ submit: [] }>();
const t = useTranslate();
const canSubmit = computed(() => !props.disabled);
</script>
<template>
<section class="example-form">
<h1>{{ t("示例表单") }}</h1>
<TButton type="button" :disabled="!canSubmit" @click="emit('submit')">
{{ t("提交") }}
</TButton>
</section>
</template>
<style lang="scss" scoped>
.example-form {
color: var(--shop-color-text-primary);
}
</style>
组件结构可以按文件实际需要省略 style,但不要把业务请求和状态初始化写进模板表达式。
语义与层级
- 页面主体使用
main、section、header、nav等合适元素。 - 标题按信息层级使用
h1至h6,不要用div加粗模拟标题。 - 可点击操作使用
button或路由链接,不用无键盘行为的div @click。 - 普通按钮明确
type="button",避免在表单中意外提交。 - 列表使用
ul/ol与li;表格数据使用表格组件或语义化 table。 - 仅为布局需要时再增加容器,避免无意义的多层 wrapper。
属性与指令
- 静态属性使用双引号。
- 动态值使用
:,事件使用@,不拼接大段 HTML 字符串。 - prop 在脚本中使用 camelCase,在模板中使用 kebab-case。
- 一个元素属性较多时交给 Prettier 换行,不手工对齐空格。
v-if用于真正创建或销毁内容;频繁切换且需要保留状态时再考虑v-show。
<GoodsPicker
v-model:visible="pickerVisible"
owner-type="admin"
type="sku"
:default-data="selectedSkuIds"
:limit="10"
@confirm="handleGoodsConfirm"
/>
列表渲染
v-for 必须使用稳定业务主键:
<li v-for="goods in goodsList" :key="goods.goods_id">
{{ goods.goods_name }}
</li>
不要使用数组下标作为可增删、排序或异步刷新的列表 key。后端 Long ID 应按字符串保留,不能转成可能失真的 Number 后再作为 key。
不要在同一元素同时使用 v-if 与 v-for。先用 computed 过滤数据,或把条件放到内部元素。
表单
TFormItem的name与表单数据字段一致。- label、placeholder、校验提示和按钮文案都通过翻译入口获取。
- 文本输入选择合适的
autocomplete、inputmode和最大长度。 - 金额用
MoneyInput,带单位数值用UnitNumberInput,不要退回普通文本框。 - 提交按钮绑定 loading/disabled,防止重复提交。
- 验证结果用
isFormValidateSuccess()统一判断。
<TFormItem :label="t('金额')" name="amount">
<MoneyInput v-model="form.amount" :max="99999999" />
</TFormItem>
TDesign Dialog 默认保留子树。含动态规则、change 规则或异步回显的表单,重新打开时必须重建表单会话;不能依赖关闭弹窗自动销毁验证状态,也不能用多层 nextTick 或 setTimeout 掩盖竞态。
文案与国际化
用户可见的标题、按钮、表单、提示、状态和空数据文案使用 useTranslate():
<span>{{ t('已选择 {v0} 项', { v0: selectedRows.length }) }}</span>
需要变量的句子使用完整带参数文案,不通过字符串拼接拆散语序。后端业务编码、固定协议值和仅开发者可见的日志不强制翻译。
富文本
禁止在业务页面直接使用 v-html 展示未知内容。需要显示后端富文本时使用公共 SafeHtml:
<SafeHtml :html="article.content" />
即使使用公共组件,也要确认内容来源和清洗边界。不要把 token、个人信息或未验证的 URL 拼入富文本。
可访问性
- 图片必须提供有意义的
alt;纯装饰图片使用空 alt。 - 图标按钮必须有可读的
title或aria-label。 - 异步状态需要在必要位置使用
aria-live,但不要让高频变化制造语音噪音。 - 键盘用户应能完成打开、选择、提交、关闭等核心流程。
- 表单错误要与具体字段关联,不能只在页面顶部显示一条模糊提示。
- 新窗口链接应明确目的;使用
target="_blank"时补充安全的rel。
条件与权限
模板中的 v-if 只能控制显示,不是安全边界。权限、功能开关和业务状态还必须在路由或请求层验证,避免用户通过直接 URL 或手工请求绕过。
不推荐写法
<!-- 不要用 div 模拟按钮。 -->
<div @click="save">保存</div>
<!-- 不要使用不稳定下标。 -->
<div v-for="(item, index) in items" :key="index">{{ item.name }}</div>
<!-- 不要直接渲染未知 HTML。 -->
<div v-html="content"></div>
检查
pnpm lint
pnpm typecheck
只改单个应用时执行对应 pnpm build:dev:<端>;修改公共模板组件时执行 pnpm build:dev。浏览器中还要检查键盘操作、焦点、表单错误、空数据、长文本和明暗主题。