Appearance
Agreement 用户协议(前端调用视角)
契约权威 → 手册 · agreement:字段表 / 类型 / 可空 / curl 一律在那,本页零复述。 本页回答:哪个页面在什么时机调、响应从哪一层读、失败了前端做什么。 封装
src/api/agreement.ts(api 层 6 文件之一,最小的一份:2 个端点)· 消费页 pages/agreement + 登录页两协议链接
一、域特征(读端点前先看这四条)
| # | 特征 | 对前端意味着什么 |
|---|---|---|
| 1 | 两个端点全免登录 | auth:'none' —— 协议在认证之前就要能看(登录页底部链接)。别在这里做 token 前置判断 |
| 2 | 恒 HTTP 200,无业务错误码 | 唯一失败形态是 data: null(非法 type / 无记录)—— 没有 catch 可抓、没有码值可分支,只能判空 |
| 3 | type 白名单只有两个值 | user_agreement(用户协议)/ privacy(隐私政策)。传白名单外的值不报错,返回 data: null → 协议页静默空白(最坏形态:用户以为协议是空的) |
| 4 | 🔴 两个响应字段组是全项目唯一的 snake_case | updated_at —— 其余 28 个端点一律 camelCase。勿"顺手改成 camelCase",前端类型声明已对齐 |
| 5 | 🆕 content_format 决定渲染方式 | plain=纯文本按段(存量);html=服务端已 sanitize 的富文本 → 用 rich-text 组件渲染(勿 v-html)。旧版 App 不认该字段 → 行为不变(向后兼容) |
⚠️ 失败是静默的(本页的头号坑)
后端对非法 type 走 cz__ok(None):HTTP 200 + code "0" + data: null,不 500、不报错。 ⇒ 前端必须显式判空,不能把"请求成功"当成"内容拿到了"。
ts
// 正确:code 与 data 一起判(协议页现有写法,保持)
if (res?.code === '0' && res.data) content.value = res.data;
// ❌ 反例:直接 res.data.content → 非法 type 时抛 TypeError,页面白屏二、端点明细
getAgreement(type)(src/api/agreement.ts → getAgreement('user_agreement' | 'privacy'))
一句话:协议详情页 onLoad 按 type 拉单条正文 —— 后台可更新内容,App 不必发版。
前端函数签名
| 项 | 值 |
|---|---|
| 封装 | src/api/agreement.ts → getAgreement(type) |
| 路径 | GET api/v1/content/agreements/{type} |
| 触发时机 | pages/agreement/index.vue 的 onLoad(从登录页链接或「我的」页跳入) |
| 鉴权 | none(公开) |
必需 payload 键
| 键 | 从哪来 | 必填 |
|---|---|---|
{type} (path) | 页面 onLoad 的 query.type(归一后再传,见坑位) | ✔ |
响应读法
ts
getAgreement(type).then((res: any) => {
if (res?.code === '0' && res.data) content.value = res.data; // 中文注解:判空是必须的,见上「静默失败」
});
// 🆕 富文本(2026-09-24):按 content_format 分支渲染
const isHtml = computed(() => content.value?.content_format === 'html');
// html → rich-text 内置组件(白名单渲染、不执行 script);勿 v-html
const richNodes = computed(() => content.value?.content || '');
// plain(存量)→ 照旧按空行切段
const paragraphs = computed(() =>
isHtml.value ? [] : (content.value?.content || '').split(/\n{2,}/).filter(p => p.trim()), // 中文注解:plain 才是纯文本
);响应真值
| 路径 | 含义 |
|---|---|
data(整体) | 可能是 null —— 非法 type / 无记录 |
data.title | 协议标题(页面 title 优先用跳转参数,拿不到才用它) |
data.content | 纯文本全文,空行分段 —— ⚠️ 不是 HTML/富文本,别直接塞 rich-text |
data.version | 版本号(后台每次更新自增) |
data.updated_at | 更新时间(snake_case) |
字段类型/可空性见 手册 · getAgreement。
失败时的前端动作
| 情形 | 前端动作 |
|---|---|
data === null(HTTP 200) | 不要报错弹窗 —— 页面显示「协议内容暂不可用」兜底文案;这是"内容缺失"不是"网络故障" |
| 网络/5xx | 同上兜底 + 日志;协议页不阻断用户返回(登录页链接必须能退回) |
⚠️ 坑位
type由标题文案反推:登录页showAgreement(title)用title.includes('隐私') ? 'privacy' : 'user_agreement'生成 —— ⇒ 改登录页协议链接文案时别动「隐私」二字,否则隐私政策会静默变成用户协议(页面不报错,内容却是错的)。- 协议页
onLoad自己又归一了一次(query.type === 'privacy' ? 'privacy' : 'user_agreement')—— 这是有意为之的兜底:外部跳转传了非法type也不会打到data: null分支。 新调用点请沿用同一归一写法,别把query.type直接透传。
listAgreements()(src/api/agreement.ts → listAgreements())
一句话:一次拉全部协议(用户协议 + 隐私政策)。当前前端无任何调用点 —— 保留给"启动时预热协议内容"之类的场景。
前端函数签名
| 项 | 值 |
|---|---|
| 封装 | src/api/agreement.ts → listAgreements()(无入参) |
| 路径 | GET api/v1/content/agreements |
| 触发时机 | 无(2026-09-20 实查:全仓零调用;登录页是两个 navigateTo 链接,不是一次拉两类) |
| 鉴权 | none(公开) |
必需 payload 键:无。
响应读法(顶层直接是数组)
ts
const res = await listAgreements();
const items = res?.data ?? []; // 中文注解:data 就是数组
const privacy = items.find(i => i.type === 'privacy'); // 中文注解:按 type 取,不要按 index 取响应真值
| 路径 | 含义 |
|---|---|
data[].type | user_agreement / privacy(唯一可用的取用键) |
data[].title / .content | 同单条端点 |
data[].version / .updated_at | 同单条端点(snake_case) |
失败时的前端动作
| 情形 | 前端动作 |
|---|---|
| 任意失败 | 保空数组(当前无调用点,新接入方自行兜底) |
| 空数组 | 正常态 —— 与单条端点的 data: null 含义不同,别混用判空逻辑 |
三、一页速查
| 端点 | 前端函数 | 调用点 | data 形态 | 失败形态 |
|---|---|---|---|---|
GET /api/v1/content/agreements/{type} | getAgreement(type) | pages/agreement/index.vue | 对象 或 null | HTTP 200 + data:null(非法 type / 无记录) |
GET /api/v1/content/agreements | listAgreements() | 无(零调用) | 数组 | 同上,data 为 [] |
字段表 / 类型 / 可空 / curl → 手册 · agreement。 本页与手册的分工边界 → api/ 总览。