harness-demo 前端 Vue + TypeScript 代码解析
面向读者:熟悉 Java/Spring 后端,但对 Vue、TypeScript、Vite 前端体系不成系统的研发人员。
分析对象:harness-demo/frontend
1. 先建立整体心智模型
这个前端工程是一个轻量 Vue 3 单页应用,技术栈是:
| 技术 | 类比后端 | 本项目作用 |
|---|---|---|
| Vue 3 | Spring MVC + 模板渲染思想,但运行在浏览器 | 组织页面、组件、事件、状态 |
| TypeScript | Java 的静态类型能力 | 给前端对象、接口响应、组件参数加类型 |
| Vite | Maven/Gradle + Dev Server | 本地启动、热更新、打包 |
| Vue Router | Spring MVC 路由映射 | /orders、/orders/new、/orders/:id 页面跳转 |
| Composable | 前端 UseCase/Service Hook | 复用表单逻辑、异步 loading/error 逻辑 |
.vue 单文件组件 | Controller + View + 少量页面状态 | 一个文件内包含脚本、模板、样式引用 |
当前项目没有引入 Pinia、Axios、Element Plus、Ant Design Vue 等常见前端库,整体比较“手写轻量”。这有利于学习 Vue 基础,但生产级项目通常会进一步补状态管理、请求拦截、组件库、权限路由和统一错误处理。
2. 工程目录导览
关键目录如下:
frontend/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── index.html
├── src/
│ ├── main.ts
│ ├── App.vue
│ ├── router/index.ts
│ ├── layouts/BusinessLayout.vue
│ ├── api/order/
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── mockData.ts
│ ├── pages/order/
│ │ ├── OrderListPage.vue
│ │ ├── OrderCreatePage.vue
│ │ ├── OrderEditPage.vue
│ │ └── OrderDetailPage.vue
│ ├── components/order/
│ │ ├── OrderTable.vue
│ │ ├── OrderFilterForm.vue
│ │ ├── OrderBasicForm.vue
│ │ ├── OrderLineEditor.vue
│ │ ├── ContractPickerModal.vue
│ │ ├── CompleteOrderConfirm.vue
│ │ ├── ImportOrderModal.vue
│ │ └── RelatedDocumentsTabs.vue
│ ├── composables/
│ │ ├── useAsyncState.ts
│ │ └── useOrderForm.ts
│ ├── utils/format.ts
│ └── styles/main.css
└── scripts/
├── verify-contract.mjs
├── verify-visual.mjs
├── order-e2e.mjs
└── static-scan.mjs
可以按后端分层类比理解:
| 前端目录 | 后端类比 | 职责 |
|---|---|---|
main.ts | Spring Boot 启动类 | 创建 Vue 应用,挂载路由 |
router/index.ts | Controller 路由注册 | 定义 URL 到页面组件的映射 |
layouts/ | 页面框架 / Layout | 顶部导航、侧边栏、主内容区 |
pages/ | Controller + 页面编排 | 某个路由页面的业务流程 |
components/ | 可复用 View 组件 | 表格、表单、弹窗、确认框 |
api/order/ | Feign Client / DTO | 请求后端接口或 Mock 数据 |
composables/ | Application Service / UseCase | 复用业务状态和操作流程 |
utils/ | 工具类 | 金额、状态文案格式化 |
scripts/ | 测试/校验脚本 | 契约、静态扫描、轻量 E2E |
3. package.json:前端工程入口
frontend/package.json 是前端项目的 Maven POM。
核心依赖:
"dependencies": {
"@vitejs/plugin-vue": "^5.2.4",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vue": "^3.5.14",
"vue-router": "^4.5.1",
"vue-tsc": "^2.2.10"
}
核心脚本:
"dev": "vite --host 0.0.0.0",
"build": "vue-tsc --noEmit && vite build",
"verify": "npm run build && npm run scan:static && npm run verify:contract && npm run verify:visual",
"test:e2e": "node scripts/order-e2e.mjs"
重点理解:
npm run dev:本地开发服务器,类似启动一个前端版 Spring Boot dev server。npm run build:先用vue-tsc做 TypeScript 类型检查,再用 Vite 打包。npm run verify:项目自定义校验集合。npm run test:e2e:轻量端到端脚本,不是标准 Playwright/Cypress 浏览器自动化。
4. main.ts 与 App.vue:应用启动链路
src/main.ts:
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './styles/main.css';
createApp(App).use(router).mount('#app');
后端类比:
SpringApplication.run(Application.class, args);
这几行做了三件事:
- 创建 Vue 应用实例。
- 注册路由插件。
- 挂载到
index.html中的#app节点。
App.vue 通常是根组件。本项目里它主要承载 RouterView,也就是根据当前 URL 渲染对应页面。
5. Vue 单文件组件 SFC 怎么看
.vue 文件一般由三块组成:
<script setup lang="ts">
// TypeScript 逻辑:变量、方法、请求、事件
</script>
<template>
<!-- HTML-like 模板:页面结构、组件、绑定 -->
</template>
<style scoped>
/* 可选样式 */
</style>
本项目主要使用全局 CSS,所以多数组件没有单独 <style>。
<script setup lang="ts"> 是 Vue 3 的组合式 API 写法。它的特点是:
- 顶层变量和函数可以直接在
<template>中使用。 ref、reactive、computed是最常见的响应式 API。defineProps定义组件入参,类似 Java 方法参数/DTO。defineEmits定义组件向父组件发出的事件。
常见语法对照:
| Vue 写法 | 含义 |
|---|---|
ref(0) | 创建一个响应式单值,需要用 .value 访问 |
reactive({}) | 创建响应式对象 |
computed(() => ...) | 派生值,类似 getter |
onMounted(fn) | 组件挂载后执行,类似页面初始化 |
v-if | 条件渲染 |
v-for | 循环渲染 |
:prop="value" | 动态绑定属性 |
@click="fn" | 绑定事件 |
v-model | 双向绑定 |
6. 路由:router/index.ts
src/router/index.ts 定义了订单模块页面:
const routes: RouteRecordRaw[] = [
{ path: '/', redirect: '/orders' },
{ path: '/orders', name: 'order-list', component: () => import('@/pages/order/OrderListPage.vue') },
{ path: '/orders/new', name: 'order-create', component: () => import('@/pages/order/OrderCreatePage.vue') },
{ path: '/orders/:id/edit', name: 'order-edit', component: () => import('@/pages/order/OrderEditPage.vue'), props: true },
{ path: '/orders/:id', name: 'order-detail', component: () => import('@/pages/order/OrderDetailPage.vue'), props: true },
];
这里有几个点:
component: () => import(...)是懒加载,访问页面时才加载对应组件。props: true表示路径参数:id会作为组件 props 传入。meta存放导航高亮需要的元信息,比如module、menu、title。
后端类比:
@GetMapping("/orders/{id}")
public OrderDetail detail(@PathVariable String id) {}
但前端路由不是返回 JSON,而是决定渲染哪个页面组件。
7. 布局:BusinessLayout.vue
BusinessLayout.vue 是业务管理页面的统一外壳:
- 顶部导航:业务管理、融资服务、基础信息管理。
- 左侧二级导航:合同管理、订单管理。
- 主内容区:
<slot />。
核心逻辑:
const route = useRoute();
const isBusinessActive = computed(() => route.meta.module === 'business');
const isOrderActive = computed(() => route.meta.menu === 'order-management');
它读取当前路由的 meta,判断哪个菜单高亮。
<slot /> 可以理解为 Java 模板里的占位区域:不同页面把自己的内容塞进 Layout。
8. API 层:src/api/order
8.1 types.ts:前端 DTO 契约
types.ts 定义前端和后端交互的数据结构,类似 Java 的 Request/Response DTO。
例如:
export interface ApiResponse<T> {
status: string;
msg: string;
total: number | null;
data: T;
timestamp: number;
}
这对应后端统一响应外壳。
再比如:
export interface OrderDetailDTO {
id: string;
platformOrderNo: string;
actualOrderNo: string;
orderType: OrderType;
status: OrderStatus;
amount: number;
orderDate: string;
buyer: EnterpriseDTO;
seller: EnterpriseDTO;
lines: OrderLineDTO[];
contracts: ContractSummaryDTO[];
permissions: Required<OrderPermissions>;
}
TypeScript 的 interface 很像 Java 的 DTO class,但它只在编译期做类型约束,运行时不会保留。
8.2 index.ts:API Client + Mock 内存服务
api/order/index.ts 是订单模块 API 入口。它同时做了两件事:
- Mock 模式:用内存数组模拟后端。
- Real API 模式:用浏览器原生
fetch请求后端。
开关逻辑:
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '');
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true' || import.meta.env.SSR;
如果 VITE_USE_MOCK=true,则走本地内存数据。否则走真实后端 API。
真实请求封装:
async function request<T>(path: string, init: RequestInit = {}): Promise<ApiResponse<T>> {
const response = await fetch(`${API_BASE_URL}${path}`, { ...init, headers });
const payload = await response.json().catch(() => null) as ApiResponse<T> | null;
if (!payload) {
throw new OrderApiError(`HTTP_${response.status}`, '接口响应格式不正确');
}
if (!response.ok || payload.status !== 'M0200') {
throw new OrderApiError(payload.status, payload.msg || '操作失败');
}
return payload;
}
这类似一个简化版 Feign Client + 统一异常处理。
当前 API 层的优点:
- DTO 类型集中在
types.ts,可读性比较好。 - 请求函数按业务语义命名,如
listOrders、createOrder、completeOrder。 - Mock 数据和真实 API 入口共用同一套函数,页面不用关心数据来源。
当前 API 层的问题和风险:
- 没有 Axios/request 拦截器,鉴权、错误码、超时、重试能力偏弱。
- Mock 内存数组在前端模块变量中维护,刷新页面会丢失。
USE_MOCK默认依赖环境变量,生产部署必须确认关闭 Mock。- 金额使用
number,前端展示可以接受,但涉及精确金额计算时要谨慎。后端仍应使用BigDecimal。 getRelatedDocuments当前基于详情 mock 派生,并不是真实后端关联单据 API。
9. Composable:前端复用业务逻辑
9.1 useAsyncState.ts
export function useAsyncState() {
const loading = ref(false);
const errorMessage = ref('');
async function run<T>(action: () => Promise<T>): Promise<T | undefined> {
loading.value = true;
errorMessage.value = '';
try {
return await action();
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '操作失败';
return undefined;
} finally {
loading.value = false;
}
}
return { loading, errorMessage, run };
}
后端类比:这是一个通用执行包装器,像 Service 层里的 try/catch 模板,只不过它维护的是页面 loading 和错误消息。
页面使用方式:
const { loading, errorMessage, run } = useAsyncState();
const response = await run(() => listOrders({ ...query }));
9.2 useOrderForm.ts
这是新增/编辑订单共用的表单逻辑,类似后端 ApplicationService + Command 组装。
它负责:
- 初始化表单。
- 编辑页加载详情。
- 判断是否可编辑。
- 选择合同。
- 保存订单。
- 取消返回。
关键状态:
const form = reactive<SaveOrderRequest>(createInitialForm());
const detail = ref<OrderDetailDTO>();
const contracts = ref<ContractSummaryDTO[]>([]);
const editable = ref(true);
返回给页面:
return {
form,
detail,
contracts,
pickerOpen,
editable,
loading,
errorMessage,
title,
platformOrderNo,
selectedContractIds,
load,
markDirty,
chooseContracts,
cancel,
save,
};
这就是组合式 API 的典型思路:把页面可复用逻辑抽到函数里,页面组件只负责布局和事件绑定。
10. 页面层:pages/order
10.1 OrderListPage.vue
订单列表页负责:
- 采购/销售 tab。
- 查询条件。
- 分页。
- 完结确认。
- 批量导入弹窗。
- loading/error/empty 状态。
核心状态:
const activeType = ref<OrderType>('PURCHASE');
const items = ref<OrderListItemDTO[]>([]);
const total = ref(0);
const query = reactive<OrderListQuery>({
orderType: 'PURCHASE',
pageNo: 1,
pageSize: 10,
...
});
加载数据:
async function loadOrders(): Promise<void> {
const response = await run(() => listOrders({ ...query }));
items.value = response?.data.list || [];
total.value = response?.data.total || 0;
}
后端视角看,这个页面像一个 Controller 方法,接收用户查询条件,调用 listOrders,再把结果交给 OrderTable 展示。
10.2 OrderCreatePage.vue
新增页本身逻辑很薄,主要调用 useOrderForm():
const orderForm = useOrderForm();
页面组合了:
OrderBasicFormOrderLineEditorContractPickerModal- 底部保存栏
这说明 AI 生成代码时做了组件拆分,新增页没有把所有输入控件堆在一个大文件里。
10.3 OrderEditPage.vue
编辑页和新增页结构相似,但多了:
const props = defineProps<{ id: string }>();
const orderForm = useOrderForm(props.id);
onMounted(orderForm.load);
如果订单不可编辑:
- 表单组件传入
:disabled="!orderForm.editable.value"。 - 保存按钮禁用。
useOrderForm.save()中也会再次拦截。
这是合理的双层防护:UI 禁用 + 业务方法拦截。
10.4 OrderDetailPage.vue
详情页负责:
- 加载订单详情。
- 加载关联单据。
- 展示基本信息和清单。
- 根据权限展示编辑/关联合同/完结按钮。
- 完结和关联合同后刷新详情。
关键权限控制:
<RouterLink v-if="detail.permissions.canEdit" ...>编辑</RouterLink>
<button v-if="detail.permissions.canLinkContract" ...>关联合同</button>
<button v-if="detail.permissions.canComplete" ...>已完结</button>
这个修复点很关键:详情页编辑按钮不能只看状态,必须跟随后端返回的 permissions.canEdit。
11. 组件层:components/order
11.1 OrderTable.vue
这是列表表格组件。它通过 props 接收数据:
defineProps<{
items: OrderListItemDTO[];
orderType: OrderType;
loading: boolean;
errorMessage: string;
}>();
通过 emit 通知父组件:
const emit = defineEmits<{
complete: [order: OrderListItemDTO];
retry: [];
}>();
后端类比:props 是入参,emit 是回调事件。子组件不直接改父组件状态,而是发事件给父组件。
11.2 OrderFilterForm.vue
查询表单组件通常会使用 v-model 与父组件同步查询条件。
本项目列表页这样使用:
<OrderFilterForm
v-model="query"
:order-type="activeType"
@reset="resetQuery"
@search="query.pageNo = 1; loadOrders()"
/>
这表示:
- 查询条件对象由父组件持有。
- 子组件负责输入框展示和触发查询/重置事件。
11.3 OrderBasicForm.vue 与 OrderLineEditor.vue
这两个组件构成订单表单主体:
OrderBasicForm:订单基本信息、购方、供方、日期、备注等。OrderLineEditor:订单清单行,单价、数量、金额、税率联动。
从设计角度看,把“基本信息”和“明细行编辑器”拆开是合理的,因为它们复杂度不同,后续可以独立维护。
11.4 ContractPickerModal.vue
合同选择弹窗负责搜索和选择合同。它通过事件把选中的合同返回给父组件:
@confirm="orderForm.chooseContracts"
选择合同后,useOrderForm.chooseContracts 会同步:
contractsform.contractIdsform.project
这符合“关联合同后带入项目”的业务规则。
11.5 ImportOrderModal.vue
导入弹窗负责:
- 基本信息导入 / 清单导入 tab。
- 文件选择。
- 校验非
.xlsx。 - 显示导入失败明细。
- 成功后通知父页面刷新列表。
需要注意:前端导入当前依赖 API 层 Mock 或真实接口。真实模板下载按钮在历史记录里曾是入口展示,后端模板内容也曾是占位,这类功能需要重点联调。
12. TypeScript 类型怎么读
TypeScript 的关键价值是让前端在编译期发现字段错误。
例如:
export type OrderStatus = 'EXECUTING' | 'COMPLETED';
这表示订单状态只能是两个字符串之一。类似 Java enum,但运行时只是字符串。
export interface PageResult<T> {
list: T[];
pageNo: number;
pageSize: number;
total: number;
totalPages: number;
}
这是泛型接口,类似:
class PageResult<T> {
List<T> list;
int pageNo;
int pageSize;
long total;
int totalPages;
}
Promise<ApiResponse<OrderDetailDTO>>
类似 Java:
CompletableFuture<ApiResponse<OrderDetailDTO>>
但是浏览器里 Promise 是异步请求的标准模型。
13. 响应式状态怎么理解
Vue 的核心是“状态变了,页面自动重新渲染”。
ref
const total = ref(0);
total.value = 10;
ref 包装单个值。脚本里用 .value,模板里可以直接写 {{ total }}。
reactive
const query = reactive<OrderListQuery>({
pageNo: 1,
pageSize: 10,
});
query.pageNo = 2;
reactive 包装对象,适合表单、查询条件。
computed
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / query.pageSize)));
computed 是派生状态,依赖 total 和 query.pageSize,它们变化时自动更新。
14. 真实 API 与 Mock 的切换
本项目 API 层所有业务函数都先判断 USE_MOCK:
if (USE_MOCK) {
// 本地内存模拟
}
return request(...);
优点:
- 前端可以在后端没完全准备好时独立开发。
- 页面不需要知道数据来自 Mock 还是真后端。
风险:
- Mock 和真实后端字段可能漂移。
- Mock 逻辑可能比真实后端简单,掩盖权限、错误码、分页、导入等问题。
- 生产环境必须确保
VITE_USE_MOCK不为true。
后端开发审查 AI 前端代码时,要重点问:
- 页面是否真正调用
api/order/index.ts,而不是组件里直接fetch? - Mock 响应结构是否和后端
ApiResponse完全一致? - 错误码是否和后端设计一致?
- 真实 API 模式有没有跑过联调或 contract diff?
15. 当前 AI 生成前端代码的优点
从结构上看,这份前端代码有几个不错的点:
- 路由清晰,订单列表、新增、编辑、详情分离。
- Layout 和业务页面分离,导航高亮通过 route meta 实现。
- API 类型集中在
types.ts,不是到处散落。 - 页面没有直接写
fetch,统一通过订单 API 模块。 - 新增/编辑共用
useOrderForm,避免大量重复。 - 列表表格、筛选表单、清单编辑器、弹窗都有组件拆分。
- 权限按钮使用后端返回的
permissions控制。 - 有基础
build、契约、视觉、E2E 脚本。
16. 当前代码的不足和生产级改进建议
16.1 请求层偏轻
现在使用浏览器原生 fetch,没有统一超时、401 处理、token 刷新、全局错误提示、请求取消等能力。
生产建议:
- 引入统一 request client。
- 统一处理登录态、租户/企业 header、错误码、网络异常。
- 明确环境变量和后端 base URL 管理。
16.2 没有状态管理
当前没有 Pinia。订单模块还能接受,但如果系统扩展到用户、菜单、权限、企业上下文、字典缓存,纯组件传递会变复杂。
生产建议:
- 用户信息、企业上下文、菜单权限可用 Pinia。
- 页面局部状态仍保留在组件/composable。
16.3 Mock 与真实后端仍需持续校验
虽然已有 verify:contract,但真实 API 联调是另一个层级。
生产建议:
- 契约测试必须覆盖真实后端响应。
- Mock 数据应从 OpenAPI/契约生成或至少自动 diff。
- 前端 E2E 最好支持真实后端模式。
16.4 表单校验偏手写
当前很多校验在 API Mock 或组件事件里做,复杂后容易分散。
生产建议:
- 引入统一表单校验策略。
- 前端校验只做用户体验,后端校验仍是权威。
- 校验规则和错误码尽量从 API 设计同步。
16.5 金额计算要谨慎
前端使用 number 做金额展示和简单联动可以接受,但不要把它作为财务计算权威。
生产建议:
- 后端保存前重新计算金额。
- 前端只做展示和输入辅助。
- 如前端需要复杂金额计算,可考虑 decimal 库。
16.6 导入模板/文件下载要重点补齐
历史上该项目后端模板下载曾返回占位字节,前端导入模板按钮也曾偏入口展示。
生产建议:
- 前端点击下载后,应验证能得到真实
.xlsx。 - E2E 或人工验收覆盖下载、填写、上传、失败明细。
- 模板字段应与导入解析器一致。
17. 后端人员快速读前端代码的顺序
建议按这个顺序读:
-
package.json
先知道怎么启动、怎么构建、有哪些依赖。 -
src/main.ts和src/router/index.ts
理解应用入口和 URL 到页面的映射。 -
src/api/order/types.ts
对照后端 DTO/API 设计,看字段是否一致。 -
src/api/order/index.ts
看每个页面最终调用哪个 API,Mock 和真实 API 如何切换。 -
src/pages/order/OrderListPage.vue
从列表页理解 tab、查询、分页、完结、导入入口。 -
src/composables/useOrderForm.ts
理解新增/编辑共用逻辑。 -
src/pages/order/OrderCreatePage.vue、OrderEditPage.vue、OrderDetailPage.vue
看页面如何组合组件。 -
src/components/order/*
最后看表单、表格、弹窗这些细节组件。
18. 审查 AI 前端代码的 Checklist
后端同学可以用下面的问题判断前端代码质量:
- 页面是否通过 API wrapper 调接口,还是组件里散落
fetch? types.ts字段是否和后端02-api-design.md/ DTO 一致?- 真实 API 和 Mock API 返回结构是否一致?
- 页面是否处理 loading、empty、error、success 状态?
- 权限按钮是否由后端权限字段控制,而不是前端自己猜?
- 新增/编辑是否有前端基础校验,同时后端仍保留权威校验?
- 完结、关联合同这类状态动作是否有二次确认和失败提示?
- 分页参数
pageNo/pageSize/total/totalPages是否和后端一致? - 导入失败结构是否展示行号、字段和错误原因?
- 是否存在生产环境仍开启 Mock 的风险?
npm run build、npm run verify、npm run test:e2e是否通过?- 是否有真实后端 API 联调证据?
19. 一个请求从页面到后端的完整链路
以订单列表查询为例:
用户点击“查询”
-> OrderListPage.vue 触发 loadOrders()
-> useAsyncState.run 包装 loading/error
-> api/order/index.ts 的 listOrders(query)
-> Mock 模式:过滤内存 orders
-> Real API 模式:fetch /api/orders?...
-> 后端返回 ApiResponse<PageResult<OrderListItemDTO>>
-> 页面更新 items / total
-> OrderTable.vue 重新渲染表格
这条链路就是前端版的 Controller -> Service -> Client -> ViewModel -> View 更新。
20. 总结
这份 AI 生成的前端工程适合做订单管理模块的轻量演示和学习样例。它的基本分层是清楚的:路由、布局、页面、组件、API、composable、工具函数都有边界。
但如果按真实生产系统要求,还需要重点加强:
- 真实 API 联调和契约测试;
- 统一请求层;
- 权限/企业上下文管理;
- 导入模板和文件下载闭环;
- 更系统的表单校验;
- 浏览器级 E2E;
- Mock 与真实后端的自动化 diff。
对后端 Java 人员来说,最重要的理解方式是:不要把 Vue 页面看成一堆 HTML,而要把它看成“浏览器里的分层应用”。types.ts 是 DTO,api/index.ts 是客户端适配器,composables 是前端用例编排,pages 是路由入口,components 是可复用视图单元。这样读起来就会顺很多。

598

被折叠的 条评论
为什么被折叠?



