Instruction file imported from KspTooi/BioCode (
.cursor/rules/web-ui/module/WebUiServiceMR.mdc). Copyright stays with the author.
description: 前端 Service 层(XxxService.ts)ModuleRule — 响应式状态管理与业务逻辑封装 globs: **/service/*Service.ts alwaysApply: false kind: MR
前端 Service 层编码规范(MR)
术语以 Glossary 为准,骨架以 RuleSkeleton 为准。 Api 层约束见 WebUiApiMr。
1. 职责
Service 文件是本模块的业务逻辑层,封装响应式状态与 UI 操作流程:
- 管理响应式状态:查询条件、列表数据、分页、模态框可见性、表单数据、loading
- 编排调用流程:调用 Api 方法 → 处理 Result → 更新状态 → 触发 UI 反馈(ElMessage)
- 暴露 Composition 函数:每个
useXxx()返回一组状态+方法,供 SFC 解构使用
不做什么:不含 <template> / DOM 操作(SFC 职责)、不直接调用 Http.postEntity(走 Api 层)、不声明 interface | enum | type(类型集中在 Api 文件)。
2. 文件骨架
1. Import 区 — import type(Api 类型 + ModalMode)+ import(Api 默认导出、vue、element-plus、公共工具)
2. ModalMode 类型 — 标准值为 `"add" | "edit"`,业务需要时可扩展(如 `"view" | "approve"` 等),始终在 Service 文件顶层定义,所有模态框都必须通过 `modalMode` 控制行为,禁止在 SFC 外另行维护可见性开关
3. 默认导出对象 — export default { useXxxList(), useXxxModal(), ... },每个函数带 JSDoc
一个 Service 文件对应一组 SFC(通常一个列表页 + 一个模态框组件)。
3. 硬性规则(Must)
- 文件命名
<SFC名>Service.ts,与对应 SFC 完全一致 + Service 后缀,禁止去掉 Manager/Modal 等 - 存放位置
views/<域>/service/;组件级 Service 放views/<域>/components/service/ - 默认导出
export default {}对象字面量,禁止export default class或export function - Composition 函数以
use开头,camelCase;类型import type,值import,路径@/ useXxxList暴露固定命名:listForm / listData / listTotal / listLoading / loadList / resetList / removeListuseXxxModal暴露固定命名:modalVisible / modalLoading / modalMode / modalForm / modalRules / openModal / resetModal / submitModal;ModalMode标准值为"add" \| "edit",业务需要时可在文件顶层追加(如"view" \| "approve"),但所有弹窗行为必须通过modalMode区分,禁止在模块外另设独立可见性变量替代modalMode;openModal必须async,第二参数 row 类型为GetXxxListVo \| null(禁止row?可选参数),edit 模式通过if (!row)守卫后await Api.getXxxDetailsmodalForm类型使用GetXxxDetailsVo,禁止Partial<XxxDto>/Pick / Omit/AddXxxDto & EditXxxDto拼合resetList必须逐字段赋值,禁止listForm.value = { ... }整体替换onMounted中调用loadList()加载首屏数据- 删除前必须
ElMessageBox.confirm二次确认;用户取消 catch 后直接return submitModal流程固定:validate → 构造 Dto → 调 Api → ElMessage.success → reloadCallback → modalVisible = false- 禁用
else/switch,短路优先;ID 字段转string(String(row.id)) - 仅本函数内使用一次的小工具(≤ 5 行、无副作用),必须就地内联,禁止提为模块级常量或独立函数
- 暴露的
computed必须满足 ≥ 2 个消费点、含异步归一化、或 ≥ 3 步运算之一;单点派生留给模板表达式 - 禁止暴露
getXxxType/formatXxx/isXxx等纯字面量映射函数给 SFC;内联到模板,或在数据源元素带字段,或下沉到通用 util - 单一用户动作只暴露一个
onXxx,禁止拆出仅被onXxx调用一次的中间桥接函数 - 每个
useXxx与其暴露的onXxx/loadXxx/submitXxx等方法必须有 JSDoc:首行一句话写意图("做什么 / 何时触发 / 副作用"),useXxx接收 props/ref 入参时必须列@param;模块级辅助函数(如approveActionByKind)若保留也必须有 JSDoc;纯赋值箭头与 watch 回调可省 - JSDoc 只写意图与约束,禁止复述函数名或参数名;禁止"
// 调用 Api"等流水账
4. 模板(Templates)
useXxxList 完整模板
useXxxList() {
const listForm = ref<GetXxxListDto>({
pageNum: 1,
pageSize: 10,
});
const listData = ref<GetXxxListVo[]>([]);
const listTotal = ref(0);
const listLoading = ref(false);
const loadList = async (): Promise<void> => {
listLoading.value = true;
const result = await XxxApi.getXxxList(listForm.value);
listLoading.value = false;
if (Result.isSuccess(result)) {
listData.value = result.data;
listTotal.value = result.total;
return;
}
ElMessage.error(result.message || "加载列表失败");
};
const resetList = (): void => {
listForm.value.pageNum = 1;
listForm.value.pageSize = 10;
loadList();
};
const removeList = async (row: GetXxxListVo): Promise<void> => {
try {
await ElMessageBox.confirm("确定删除该条记录吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
} catch {
return;
}
try {
await XxxApi.removeXxx({ id: String(row.id) });
ElMessage.success("删除成功");
await loadList();
} catch (error: any) {
ElMessage.error(error.message);
}
};
onMounted(() => {
loadList();
});
return {
listForm,
listData,
listTotal,
listLoading,
loadList,
resetList,
removeList,
};
},
ModalMode 类型定义
标准模式(文件顶层定义,按业务需要追加):
// 仅 add/edit 时:
type ModalMode = "add" | "edit";
// 含查看模式时:
type ModalMode = "add" | "edit" | "view";
// 含审批模式时:
type ModalMode = "add" | "edit" | "view" | "approve";
规则:
ModalMode必须在 Service 文件顶层声明,不得在函数内部定义;扩展模式只需追加联合类型,不引入任何额外的可见性变量。
useXxxModal 完整模板
useXxxModal(modalFormRef: Ref<FormInstance | undefined>, reloadCallback: () => void) {
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalMode = ref<ModalMode>("add");
const modalForm = reactive<GetXxxDetailsVo>({
id: "",
title: "",
});
const modalRules = reactive<FormRules>({
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
});
/**
* 打开模态框,add 模式直接打开,edit 模式先加载详情
*/
const openModal = async (mode: ModalMode, row: GetXxxListVo | null): Promise<void> => {
modalMode.value = mode;
if (mode === "add") {
modalForm.id = "";
modalForm.title = "";
modalVisible.value = true;
return;
}
if (!row) {
ElMessage.error("未选择要编辑的数据");
return;
}
try {
const details = await XxxApi.getXxxDetails({ id: String(row.id) });
modalForm.id = details.id;
modalForm.title = details.title;
modalVisible.value = true;
} catch (error: any) {
ElMessage.error(error.message);
}
};
const resetModal = (): void => {
modalFormRef.value?.resetFields();
modalMode.value = "add";
modalForm.id = "";
modalForm.title = "";
};
const submitModal = async (): Promise<void> => {
try {
await modalFormRef?.value?.validate();
} catch {
return;
}
modalLoading.value = true;
if (modalMode.value === "add") {
const addDto: AddXxxDto = {
title: modalForm.title,
};
try {
await XxxApi.addXxx(addDto);
ElMessage.success("新增成功");
modalVisible.value = false;
reloadCallback();
} catch (error: any) {
ElMessage.error(error.message);
}
modalLoading.value = false;
return;
}
const editDto: EditXxxDto = {
id: modalForm.id,
title: modalForm.title,
};
try {
await XxxApi.editXxx(editDto);
ElMessage.success("编辑成功");
modalVisible.value = false;
reloadCallback();
} catch (error: any) {
ElMessage.error(error.message);
}
modalLoading.value = false;
};
return {
modalVisible,
modalLoading,
modalMode,
modalForm,
modalRules,
openModal,
resetModal,
submitModal,
};
},
带二次校验的 submitModal 模板(如 unique-validation)
const submitModal = async (): Promise<void> => {
try {
await modalFormRef?.value?.validate();
} catch {
return;
}
const result = await XxxApi.validateUniqueCode(form.code);
if (result.code !== 0) {
ElMessage.error(result.message);
return;
}
modalLoading.value = true;
// ... 构造 Dto → 调 Api → ElMessage.success → close + reload
};
双重 try/catch(删除确认 + API 调用)
const removeList = async (row: GetXxxListVo): Promise<void> => {
try {
await ElMessageBox.confirm("确定删除该条记录吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
} catch {
return;
}
try {
await XxxApi.removeXxx({ id: String(row.id) });
ElMessage.success("删除成功");
await loadList();
} catch (error: any) {
ElMessage.error(error.message);
}
};
前置数据加载的 openModal(如需要先加载下拉选项)
const openModal = async (mode: ModalMode, row: GetXxxListVo | null): Promise<void> => {
modalMode.value = mode;
resetModal();
await loadCapabilityOptions(); // 加载模态框内的下拉选项数据
if (mode === "add") {
modalForm.id = "";
modalForm.name = "";
modalVisible.value = true;
return;
}
if (!row) {
ElMessage.error("未选择要编辑的数据");
return;
}
try {
const details = await XxxApi.getXxxDetails({ id: String(row.id) });
modalForm.id = details.id;
modalForm.name = details.name;
modalVisible.value = true;
} catch (error: any) {
ElMessage.error(error.message);
}
};
5. 反模式(Anti-patterns)
// ❌ 自创命名,破坏 SFC 解构统一性
const queryForm = ref(...); // → 必须用 listForm
const tableData = ref(...); // → 必须用 listData
const showModal = () => {}; // → 必须用 openModal
const saveForm = () => {}; // → 必须用 submitModal
// ❌ 整体替换 listForm,破坏响应式
resetList: () => {
listForm.value = { pageNum: 1, pageSize: 10 };
};
// ❌ Partial 拼合表单类型
const modalForm = reactive<Partial<AddXxxDto & EditXxxDto>>({ ... });
// ❌ 直接把 modalForm 透传给 Api(字段集合与 Dto 不一致)
await XxxApi.addXxx(modalForm);
// ❌ 删除无二次确认,直接调 Api
await XxxApi.removeXxx({ id: row.id });
ElMessage.success("删除成功");
// ❌ 为"查看"另起独立可见性变量,绕开 modalMode
const viewVisible = ref(false);
const openView = (row) => {
viewVisible.value = true;
};
// → 必须复用 modalMode="view",通过 openModal("view", row) 打开同一弹窗
// ❌ ModalMode 定义在函数内部
useXxxModal() {
type ModalMode = "add" | "edit"; // ❌ 必须在文件顶层定义
}
// ❌ 单点派生被包成 computed 暴露
const showComment = computed(() => props.details?.allowComment === 1);
// → 模板里直接 v-show="details?.allowComment === 1"
// ❌ 纯字面量映射被抽成 Service 方法
const getApproveBtnType = (k: number) => (k === 1 ? "success" : "danger");
// → 模板 :type="act.kind === 1 ? 'success' : 'danger'",或 actions[i] 自带 type 字段
// ❌ 单一动作被切成三段中间函数
const actionByKind = (k) => (k === 0 ? 1 : k === 1 ? 0 : null);
const onApproveByKind = (k) => onApprove(actionByKind(k)!);
const onApprove = async (a) => { ... };
// → 合并为单一 onApprove(kind),内部局部映射,不暴露中间层
// ❌ 用 watch(modalVisible/modalMode) 设默认值
watch(modalMode, (v) => { if (v === "add") modalForm.status = 1; });
watch(modalVisible, () => { modalForm.kind = 0; });
// → 直接在 openModal 内逐字段赋值,所有默认值集中一处,禁止 watch
6. 速查(Cheat Sheet)
useXxxList 暴露清单
| 名称 | 类别 | 说明 |
|---|---|---|
listForm |
状态 | 查询条件(ref<GetXxxListDto>) |
listData |
状态 | 列表数据(ref<GetXxxListVo[]>) |
listTotal |
状态 | 总记录数 |
listLoading |
状态 | 加载状态 |
loadList |
方法 | 加载/刷新列表 |
resetList |
方法 | 重置查询条件并刷新 |
removeList |
方法 | 删除单条(含二次确认) |
useXxxModal 暴露清单
| 名称 | 类别 | 说明 |
|---|---|---|
modalVisible |
状态 | 模态框可见性 |
modalLoading |
状态 | 提交/加载状态 |
modalMode |
状态 | 模式(标准 "add" | "edit",按业务可追加 "view" 等) |
modalForm |
状态 | 表单数据(reactive<GetXxxDetailsVo>) |
modalRules |
状态 | 表单校验规则 |
openModal |
方法 | 打开模态框 (mode: ModalMode, row: GetXxxListVo | null) |
resetModal |
方法 | 重置表单状态 |
submitModal |
方法 | 校验 + 提交 |
错误处理场景
| 场景 | 模式 |
|---|---|
| ElMessageBox 取消 | 独立 try/catch,catch 中 return |
| API 调用失败 | 独立 try/catch,ElMessage.error(error.message) |
| 表单校验失败 | try { await validate() } catch { return } |
| loading 收尾 | 有 finally 用 finally {};无则前后手动设置 |
派生值暴露决策
| 派生值类型 | 消费点数 | 处理方式 |
|---|---|---|
一次表达式可算(=== / ?? / 三元 / 可选链) |
1 处 | 模板内联,不进 Service |
| 同样一次表达式可算 | ≥ 2 处 | 进 Service computed 暴露 |
| 含异步数据归一化 / ≥ 3 步运算 | 任意 | 进 Service computed 暴露 |
| 纯字面量映射(入参 → "success" / "danger" 等) | 任意 | 模板三元 或 数据源带字段,不写成 Service 方法 |
| 单次用户动作的中间桥接 | 仅 1 处调用 | 内联进 onXxx,不单独定义 |