TypeScript 与 Vue 规范
BBC 8.0 使用 TypeScript、Vue 3 Composition API、ES modules 和 ESLint flat config。旧版 .eslintrc.js、Babel parser、Vue 2 Options API 和 webpack import resolver 不再是当前事实来源。
工具配置
项目根目录的真实配置:
eslint.config.js:ESLint 9 flat config、TypeScript ESLint、Vue 推荐规则。.prettierrc:两个空格、单引号、无分号、无尾逗号、120 列。tsconfig.json:workspace 路径别名和 Vue/Vite 类型。tsconfig.packages-api.strict.json:API 包的额外严格类型检查。
格式由 Prettier 决定,不在代码评审中手工争论空格或换行。不要复制一份局部 ESLint 配置绕过根规则。
模块与依赖方向
import { computed, ref } from "vue";
import type { GoodsDO } from "@shop-tnt/api";
import { TableLayout } from "@shop-tnt/components";
import { AdminGoodsApi } from "@/api";
- 运行时导入和 type-only 导入分开。
- 当前 app 内使用
@/,共享包使用@shop-tnt/*公开入口。 - app 不能导入另一个 app 的内部文件。
packages/*不能导入apps/*。- 不通过超长相对路径绕过 package 边界。
- 新增公共导出时同步维护对应
index.ts和必要的package.json.exports。
Vue Composition API
新组件优先使用 <script setup lang="ts">:
<script setup lang="ts">
import { computed, ref } from "vue";
const props = withDefaults(
defineProps<{
modelValue?: string;
disabled?: boolean;
}>(),
{
modelValue: "",
disabled: false,
},
);
const emit = defineEmits<{
"update:modelValue": [value: string];
}>();
const normalizedValue = computed(() => props.modelValue.trim());
const loading = ref(false);
</script>
- props 只读,通过 emits 更新父级状态。
- 派生值使用 computed,不用 watch 维护可直接计算的副本。
- 页面私有状态留在页面或 composable,不把所有状态提升到 Pinia。
- 只有跨页面、长生命周期业务状态才放入端内 store。
- 共享组件不直接读取具体 app store,使用 props、emits、provide/inject 或适配器。
类型
- 优先使用明确的业务类型,避免无边界
any。 - 不确定的外部输入先用
unknown,验证后再缩窄。 - 后端动态结构确实无法确定时使用项目已有
TNTAnyRecord、TNTBackendDynamic,并在业务边界逐步收窄。 - 后端 Long 或雪花 ID 使用 string,不能经过
Number()。 - API 分页使用
TNTPageQuery、TNTPageResponse<T>等公共类型。 - 不用非空断言掩盖加载时序或缺失字段。
interface GoodsListItem {
goods_id: string;
goods_name: string;
goods_price: number;
}
function isGoodsListItem(value: unknown): value is GoodsListItem {
if (!value || typeof value !== "object") return false;
const item = value as Record<string, unknown>;
return (
typeof item.goods_id === "string" && typeof item.goods_name === "string"
);
}
根 tsconfig 当前没有全局 strict;这不是继续扩散弱类型的理由。新增公共 API 和高影响逻辑应尽量提供完整输入、输出和回调类型。
API 模型与请求
packages/api/models 只保存后端生成类型,不保存运行时函数:
import { apiRequest } from "@shop-tnt/api";
import type {
GoodsDO,
TNTApiPromise,
TNTPageQuery,
TNTPageResponse,
} from "@shop-tnt/api";
export function getGoodsList(
params: TNTPageQuery,
): TNTApiPromise<TNTPageResponse<GoodsDO>> {
return apiRequest({
url: "admin/goods",
method: "get",
loading: false,
params,
});
}
单端业务接口放对应 apps/*/src/api。真正跨端的运行时接口使用 packages/api 的明确模块和导出入口,不能借生成类型目录隐式发布。
异步流程与错误
不允许空 catch 或只为关闭 Loading 吞掉错误:
loading.value = true;
try {
detail.value = await AdminGoodsApi.getGoodsDetail(goodsId);
} catch (error) {
feedback.error(t("商品详情加载失败"));
console.error("[goods-detail] load failed", { goodsId, error });
} finally {
loading.value = false;
}
- 用户入口、网络、支付和上传边界提供明确提示、重试或降级。
- 日志保留业务动作、非敏感标识和错误上下文,不记录 token、密码或隐私数据。
- 页面不接管错误时让统一请求层提示;传
message: false后必须自己处理反馈。 - 请求取消与真实错误分开判断,主动取消不显示“网络失败”。
- 提交和上传默认不随路由取消,页面查询默认在路由切换时取消。
并发与生命周期
异步回显、弹窗和搜索需要防止旧响应覆盖新状态:
- 搜索请求使用稳定
cancelKey,让新请求替换旧请求。 - 弹窗异步回显使用递增 request generation,关闭和卸载时让旧 generation 失效。
- 旧请求的
catch/finally不得修改新会话的 loading、timer 或表单数据。 - 注册 window、document、ResizeObserver、定时器或订阅后,在卸载时清理。
- 不用多层
nextTick或setTimeout猜测异步顺序。
表单校验生命周期
TDesign Dialog 默认保留子树。带 change、动态 rules 或异步回显的表单,优先给实际 TForm 使用单调递增的 session key:
- 完整写入模型和影响规则的 mode/type。
- 递增表单 key。
- 再显示弹窗。
简单同步表单且规则全部是 blur/submit 时,才可以在显示后 nextTick(clearValidate)。当前 TDesign Form API 没有 resetFields(),不要调用不存在的方法。
数值与数据转换
- 展示和提交前明确区分
0、空字符串、null 和 undefined。 - 金额不要用浮点运算拼凑结算逻辑;服务端金额契约优先。
- 接口返回的计数、金额等字段需要 Number 时,在 API 边界统一归一化,而不是在模板零散转换。
- Long ID、订单号、流水号保持字符串。
- 不使用
value || fallback覆盖合法的 0 或 false,优先value ?? fallback。
用户文案与国际化
import { useTranslate } from "@shop-tnt/components/useTranslate";
const t = useTranslate();
feedback.success(t("保存成功!"));
按钮、表单、校验、弹窗、状态和路由标题等用户文案使用翻译入口。变量文案使用参数,不拆成字符串拼接。修改语言资源后执行 pnpm locale:check。
注释
注释解释业务意图、边界条件、兼容原因和非显然数据流,不复述语法:
// 后端 Long ID 按字符串保留,避免回传菜单排序时发生精度损失。
const menuId = String(row.id);
以下场景应补充注释:
- 特殊权限或功能开关。
- 并发、取消和旧响应隔离。
- 后端兼容字段与精度处理。
- 暂时保留的历史别名或协议。
简单赋值、显然 import 和常规模板绑定不需要逐行注释。CSS/SCSS 和 Vue style 块不要新增注释。
检查命令
pnpm lint
pnpm typecheck
pnpm locale:check
只改单个 app 时执行对应 pnpm build:dev:<端>;修改 packages/*、根配置或多个 app 时执行 pnpm build:dev。禁止用 lint:fix 的大范围重写掩盖与当前任务无关的旧代码问题。