guest-reception-system/docs/frontend-architecture.md

12 KiB
Raw Blame History

前端架构与关键决策(招生协作 H5

面向后续开发 / AI 的speedrun 文档。记录本项目前端的结构、约定和「为什么这么做」,以及待后端配合的事项。配套接口文档见 api.md

技术栈

  • Vue 3 <script setup> + TypeScript + Vue Routerhistory 模式)
  • Piniapinia-plugin-persistedstate 持久化,存储经 AES 加密,见 src/store/secureStore.ts
  • Vant 4 组件库(按需引入,每个组件手动 import "vant/es/xxx/style"
  • UnoCSSpreset-wind4 + preset-rem-to-px
  • 企业微信 JS-SDK@wecom/jssdk
  • 运行环境:企业微信 / 微信 webview老内核 X5/TBS 兼容要求高)

1. HTTP 层

已弃用 axios,统一用 fetch 封装。不要再引入 axios 或恢复 customAxios.ts / encryptUrl.ts(已删除)。

  • src/api/customFetch.ts — 纯粹的请求组装:拼 headers / body / query、相对路径拼 baseUrl、跑拦截器管线。导出 getRequest / postRequest / putRequest / deleteRequest,均返回 Promise<ApiResponse>
  • src/api/interceptors.ts — 类 axios 的拦截器注册中心,interceptors.request/response/error.use()。内置默认拦截器:
    • 请求:注入 Authorization: Bearer <token>token 取自 useUserStore().getToken,本项目 store 字段是 token,不是 accessTokenFormData 直传不设 Content-Type非 GET JSON body 自动加 application/json
    • 全局 loading并发计数 + 800ms 延迟才显示 Vant loading toastoptions.silent 可跳过。
    • 响应:非 2xx 统一 showToast 报错并短路返回 {error, code};业务码 result.code === 2location.reload()(登录失效)。
    • 网络异常:关 loading + toast + 返回 {error, code:-1}
    • 扩展新行为请用 interceptors.*.use(),不要写进 customFetch。
  • src/api/fetchUrl.ts — 所有接口 URL 构造函数集中在此。baseUrl = import.meta.env.VITE_API_BASE_URL ?? "https://api.zsfz.jinzejk.com"。开发环境不设该变量 → 走相对 /api,由 vite.config.ts 的 dev proxy 转发到内网后端;生产由 .env.production 注入域名。

Admin.NET 统一响应{ code, type, message, result, extras, time }code === 200 为成功,业务数据在 result。各 API 服务里用 unwrapresult


2. API 服务层

  • src/api/admission.ts — 招生辅助数据:
    • fetchConsultStages()GET /options/consult-stages,返回排序后的 {label,value,disabled}[]
    • fetchSchoolRegions()GET /school/regions,返回 {label, value(=region 精确筛选值)}[]
    • searchSchools({page,pageSize,region,keyword})POST /school/search,分页。
    • (历史上有过 fetchInvitationFormOptions/fetchAllSchools,已废弃删除——改为各选择器按需请求。)
  • src/api/wecom.tsgetWecomSignature(url, type):换取企业微信 JS-SDK 签名,见第 5 节。

3. 选择器组件「数据自取」

要点:下拉/选择器的数据请求收敛在组件内部父页面TeacherHome / 家长端 InvitationForm不再重复写取数逻辑v-model 绑值 + @select 拿回显示名。

  • SingleSelectPopup.vue(咨询学段):
    • onBeforeMount 里请求一次 fetchConsultStages()(不用 watch(show)——被 keep-alive 缓存时不会随激活重复请求)。
    • 仍保留通用 options prop父级显式传 options 就用外部的、不请求(保持组件通用性)。
  • SchoolSelectPopup.vue(毕业院校):
    • 区域列表在 onBeforeMount 请求一次(loadRegions() 带 in-flight 去重)。
    • 兜底:区域为空(首次请求失败)时,watch(show) 在打开时补拉一次;补齐且列表确空已结束时才 resetList() 触发加载(避免与 VanList 自动加载重复)。
    • 学校按区域服务端分页VanList@load 每次请求下一页;切换区域 / 搜索 → resetList 重载;搜索态跨区域查询(不带 region
    • 选中通过 @select 回传 {label,value},父级存 selectedSchoolName 用于字段展示(父级不再持有完整学校列表)。

父页面里「毕业院校是否必填」的规则用 schoolOptionalStageLabelssrc/views/teacher/invitationOptions.ts)按学段显示名匹配(不依赖后端 value 是编码还是中文)。


4. 路由与布局

  • 两套布局 + 底部 tabbar
    • TeacherLayout/teacher):表单 / 我的。
    • ReceptionLayout/reception):任务 / 我的。
    • tabbar 图标用 OSS 图片按 active 态切换(#icon="{ active }" 插槽)。
  • 主要页面:教师端 TeacherHome(邀约表) / TeacherMine / InvitationData;接待端 ReceptionTask / ReceptionMine / ReceptionData / ReceptionRecord;家长端(公开路由 /invite/:tokenInvitationForm / SubmissionSuccess / PassCode
  • router/index.tsuserRole 目前写死 "admin",登录 / 角色路由待接(见待办)。

5. 企业微信 JS-SDKww.register

main.ts 初始化(app.use(store/router) 之后、mount 之前)。未配置 corpId 或非微信/企微环境时跳过。

register({
  corpId: VITE_WECOM_CORP_ID,
  agentId: VITE_WECOM_AGENT_ID,
  jsApiList: ['shareWechatMessage','shareAppMessage','shareToExternalContact','getLocation','openLocation'],
  getConfigSignature:      (url) => getWecomSignature(url, 'config'),
  getAgentConfigSignature: (url) => getWecomSignature(url, 'agent'),
})

corpId / agentId / Secret 的区别

级别 从哪拿 放哪
corpId企业ID 企业级,所有自建应用共用 管理后台 → 我的企业 → 企业信息 → 底部「企业ID」 前端 VITE_WECOM_CORP_ID(非敏感)
AgentId 每个应用不同 应用管理 → 该应用页 前端 VITE_WECOM_AGENT_ID(非敏感)
Secret 每个应用不同 应用页 只放后端,绝不进前端

getConfigSignature vs getAgentConfigSignature —— 后端要做什么

两者都是「对当前页面 URL 生成 JS-SDK 签名」,算法完全相同,只是用的 jsapi_ticket 不同

  • getConfigSignature → wx.config(企业级鉴权):用企业级 jsapi_ticket。基础 JS-SDK 能力的入口,必须先成功。
  • getAgentConfigSignature → agentConfig(应用级鉴权):用应用级 jsapi_ticket。企业微信很多「应用级」接口shareToExternalContact、外部联系人/客户群相关,以及需要应用身份的分享)必须在 config 之后再做 agentConfig 才能调用。@wecom/jssdkregister 会自动先 config 后 agentConfig。

后端每次收到 { url, type } 要做的type = config | agent

  1. corpId + 应用SecretGET /cgi-bin/gettokenaccess_token(缓存 ~7200s
  2. 用 access_token 取对应 jsapi_ticket都缓存 ~7200s
    • type=configGET /cgi-bin/get_jsapi_ticket?access_token=...企业 ticket
    • type=agentGET /cgi-bin/ticket/get?access_token=...&type=agent_config应用 ticket
  3. 生成 noncestrtimestamp;取前端传来的 url去掉 # 及其后面部分)。
  4. signature = sha1("jsapi_ticket=<ticket>&noncestr=<nonce>&timestamp=<ts>&url=<url>")(小写十六进制)。
  5. 返回 { timestamp, nonceStr, signature }

关键坑

  • 签名的 url 必须是前端传来的当前页面 URL工厂参数不能用固定值。SPA/iOS 上企业微信按「进入页 URL」签名——若线上 iOS 报签名无效,需前端在入口缓存 URL 后传给后端。
  • 一个应用的 Secret 就能算出这两种 ticket企业 ticket 和 agent_config ticket 都用该应用的 access_token 换)。
  • 签名接口本身不依赖用户 JWT是企业/应用级),可做成公开或轻鉴权;前端 getWecomSignaturepostRequest,若已登录会自动带 token未登录则不带。
  • 前端签名接口路径暂定 POST /api/admission/wecom/jsapi-signaturefetchUrl.tsgetWecomSignatureUrl待与后端确认

已接的 JS-SDK 能力

  • TeacherMine.shareToParent()shareWechatMessage(转发到微信,把邀约链接发给家长);未 register 成功时 catch 降级为「右上角菜单分享」提示。
  • TeacherMine.saveImage()previewImage打开原生图片预览长按保存到相册。webview 里 <a download> 无效,故改用它;未 config / 不支持时降级提示长按保存。注意WeChat/WeCom 的 previewImage 对 base64 dataURL 支持不稳定(官方要求 http/https二维码目前是前端 canvas 生成的 dataURL——要稳妥保存应让后端把二维码作为可公网访问的图片 URL 返回(或用 uploadImage 换 URL再传给 previewImage。
  • PassCode.openSchoolLocation()openLocation(导航到校)。

6. 样式 / 资源约定

  • UnoCSS px 缩放preset-rem-to-px baseFontSize=4 + spacing DEFAULT=1px → 「数字即 px」。间距类 px-15=15px、rounded-12=12px、h-22=22px字号用 text-[Nrem]N×4=pxtext-[4.5rem]=18px。渐变/复杂值用字面量 bg-[linear-gradient(...)]别用 preset-wind4 的 from-*/to-*(会生成 oklab/CSS 变量,老版微信 webview 不支持)。
  • 远程图片资源:用户给的 OSS 图片 URLlw-zk.oss-cn-hangzhou.aliyuncs.com/...直接引用,不要下载到 public/images。验证 URL / 读尺寸可临时 curl用完删。
  • 字体public/fonts/din-bold.ttf 已在 src/style.css 注册 @font-facefamily DIN-Bold),用全局类 .din-bold(故意不叫 font-din,避开 UnoCSS font- 命名空间)给数字用。
  • 禁用态按钮:邀约表单提交按钮未填完/提交中禁用,禁用色 #A1CCFF(覆盖 Vant 默认半透明降级)。

7. 本地测试(后端未就绪)

分两层:

① 普通页面 / 表单流程(浏览器即可测) —— 用 dev-only 的 mock 中间件拦截接口,不需要真后端:

  • plugins/mockServer.tsVite 插件(apply: "serve"mock 了 options/consult-stagesschool/regionsschool/search(含分页 + 关键字过滤,「市中区」特意补到 24 条便于测 VanList 滚动分页)。返回 Admin.NET 统一响应结构。
  • 开关:.envVITE_USE_MOCK=truevite.config.ts 才挂载该插件;后端就绪后置 false 或删除。
  • 前提dev 的 VITE_API_BASE_URL 必须留空(走相对 /api才能被本地中间件拦截若填了绝对域名请求会直连该域名、mock 拦不到。线上域名放在 .env.production
  • 用法:pnpm dev 打开表单页 → 咨询学段 / 毕业院校下拉、区域切换、滚动加载、搜索都能跑。接待端页面本就用本地 mock ref直接可看。

② 企业微信能力(分享 / 导航) —— 普通浏览器测不了真实调起:

  • 浏览器里 env.isWeCom 为 false → main.ts 跳过 registershareToParent / openLocation 走 catch 降级 toast页面不报错、可验证 UI。
  • 真正调起需在企业微信开发者工具或真机企微里打开,且后端签名接口就绪 + 可信域名配置好。

8. 待办 / 需后端或配置配合

  • VITE_WECOM_CORP_ID / VITE_WECOM_AGENT_ID.env.production)。
  • 后端实现 JS-SDK 签名接口config 用企业 ticket、agent 用 agent_config ticket确认路径。
  • 登录流程:企业微信免登 / Mock 登录(POST /api/admission/wecom/logincode 可传 MOCK:<SysUser.Id>),拿 accessTokenuserStore.setTokenrouter/index.ts 按角色路由(现写死 admin
  • 各页面 mock 数据替换为真实接口:教师端提交邀约(invitation/create)、接待端任务(checkin/reception/*)、接待数据 / 记录、数据看板等。
  • 家长端 /invite/:token 是公开链接、无用户 tokenconsult-stages/school/* 等辅助数据接口按现有文档需登录授权,需后端提供按 token 的公开取数方式,否则家长端下拉取不到数据。
  • 分享缩略图 imgUrl 换成公网可访问的线上 logolink 域名要在企业微信后台配为可信域名。