harness-demo 前端 Vue + TypeScript 代码解析(后端开发工程师视角)

该文章已生成可运行项目,

harness-demo 前端 Vue + TypeScript 代码解析

面向读者:熟悉 Java/Spring 后端,但对 Vue、TypeScript、Vite 前端体系不成系统的研发人员。

分析对象:harness-demo/frontend

1. 先建立整体心智模型

这个前端工程是一个轻量 Vue 3 单页应用,技术栈是:

技术类比后端本项目作用
Vue 3Spring MVC + 模板渲染思想,但运行在浏览器组织页面、组件、事件、状态
TypeScriptJava 的静态类型能力给前端对象、接口响应、组件参数加类型
ViteMaven/Gradle + Dev Server本地启动、热更新、打包
Vue RouterSpring 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.tsSpring Boot 启动类创建 Vue 应用,挂载路由
router/index.tsController 路由注册定义 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);

这几行做了三件事:

  1. 创建 Vue 应用实例。
  2. 注册路由插件。
  3. 挂载到 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> 中使用。
  • refreactivecomputed 是最常见的响应式 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 存放导航高亮需要的元信息,比如 modulemenutitle

后端类比:

@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 入口。它同时做了两件事:

  1. Mock 模式:用内存数组模拟后端。
  2. 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,可读性比较好。
  • 请求函数按业务语义命名,如 listOrderscreateOrdercompleteOrder
  • 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();

页面组合了:

  • OrderBasicForm
  • OrderLineEditor
  • ContractPickerModal
  • 底部保存栏

这说明 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 会同步:

  • contracts
  • form.contractIds
  • form.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 是派生状态,依赖 totalquery.pageSize,它们变化时自动更新。

14. 真实 API 与 Mock 的切换

本项目 API 层所有业务函数都先判断 USE_MOCK

if (USE_MOCK) {
  // 本地内存模拟
}
return request(...);

优点:

  • 前端可以在后端没完全准备好时独立开发。
  • 页面不需要知道数据来自 Mock 还是真后端。

风险:

  • Mock 和真实后端字段可能漂移。
  • Mock 逻辑可能比真实后端简单,掩盖权限、错误码、分页、导入等问题。
  • 生产环境必须确保 VITE_USE_MOCK 不为 true

后端开发审查 AI 前端代码时,要重点问:

  1. 页面是否真正调用 api/order/index.ts,而不是组件里直接 fetch
  2. Mock 响应结构是否和后端 ApiResponse 完全一致?
  3. 错误码是否和后端设计一致?
  4. 真实 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. 后端人员快速读前端代码的顺序

建议按这个顺序读:

  1. package.json
    先知道怎么启动、怎么构建、有哪些依赖。

  2. src/main.tssrc/router/index.ts
    理解应用入口和 URL 到页面的映射。

  3. src/api/order/types.ts
    对照后端 DTO/API 设计,看字段是否一致。

  4. src/api/order/index.ts
    看每个页面最终调用哪个 API,Mock 和真实 API 如何切换。

  5. src/pages/order/OrderListPage.vue
    从列表页理解 tab、查询、分页、完结、导入入口。

  6. src/composables/useOrderForm.ts
    理解新增/编辑共用逻辑。

  7. src/pages/order/OrderCreatePage.vueOrderEditPage.vueOrderDetailPage.vue
    看页面如何组合组件。

  8. 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 buildnpm run verifynpm 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 是可复用视图单元。这样读起来就会顺很多。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值