等待后端更新接口
This commit is contained in:
@@ -105,7 +105,7 @@
|
||||
<p>展示后台维护的应用推广、公告和下载引导内容。</p>
|
||||
</div>
|
||||
<div class="promotion-list" data-promotion-list>
|
||||
<div class="api-empty">应用推广内容加载中</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取应用推广…</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -142,6 +142,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="utils/StorageUtil.js"></script>
|
||||
<script src="utils/axios.js"></script>
|
||||
|
||||
+3
-2
@@ -33,10 +33,10 @@
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<p class="module-meta" data-site-article-status></p>
|
||||
<div class="module-meta" data-site-article-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<main data-site-article-detail>
|
||||
<div class="api-empty">资讯详情加载中</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取资讯详情…</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
@@ -74,6 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="utils/StorageUtil.js"></script>
|
||||
<script src="utils/axios.js"></script>
|
||||
|
||||
+66
-24
@@ -7,7 +7,7 @@
|
||||
<link rel="stylesheet" href="public/css/public.css" />
|
||||
<link rel="stylesheet" href="public/css/create-genealogy.css" />
|
||||
</head>
|
||||
<body class="page-create-genealogy">
|
||||
<body class="page-create-genealogy" data-genealogy-create-page>
|
||||
<header class="site-header">
|
||||
<div class="container nav">
|
||||
<a class="brand magnetic" href="index.html"
|
||||
@@ -59,35 +59,73 @@
|
||||
|
||||
<section class="section alt create-form-section">
|
||||
<div class="container">
|
||||
<form class="card form-card create-form tilt-card" data-genealogy-form="create" data-success-url="profile-families.html">
|
||||
<form class="card form-card create-form tilt-card" data-genealogy-create-form novalidate aria-describedby="public-create-status">
|
||||
<div class="form-ornament"></div>
|
||||
<h2>家谱信息</h2>
|
||||
<p class="form-note">
|
||||
先填写基本资料,后续可以继续补充成员、相册和谱文。
|
||||
标有 * 的字段为必填。登录后使用当前 PC 家谱服务创建,创建成功后进入对应家谱主页。
|
||||
</p>
|
||||
<p class="form-note" data-genealogy-create-quota role="status" aria-live="polite">正在读取家谱额度…</p>
|
||||
<div class="form-grid">
|
||||
<input
|
||||
class="input"
|
||||
name="genealogyName"
|
||||
placeholder="家谱名称,如:四川武胜汤氏族谱"
|
||||
/>
|
||||
<input class="input" name="surname" placeholder="主要姓氏" />
|
||||
<!-- 地区选择器:最终行政区编码回填到 regionCode,供创建家谱接口使用。 -->
|
||||
<div class="region-picker create-region-picker" data-region-picker>
|
||||
<select class="input" name="provinceCode" data-region-level="province">
|
||||
<option value="">请选择省</option>
|
||||
</select>
|
||||
<select class="input" name="cityCode" data-region-level="city">
|
||||
<option value="">请选择市</option>
|
||||
</select>
|
||||
<select class="input" name="districtCode" data-region-level="district">
|
||||
<option value="">请选择区县</option>
|
||||
</select>
|
||||
<input type="hidden" name="regionCode" data-region-code />
|
||||
<label class="field" for="publicFamilyName">谱名 <b aria-hidden="true">*</b>
|
||||
<input id="publicFamilyName" class="input" name="genealogyName" placeholder="例如:四川武胜汤氏族谱" required />
|
||||
</label>
|
||||
<label class="field" for="publicFamilySurname">主要姓氏 <b aria-hidden="true">*</b>
|
||||
<input id="publicFamilySurname" class="input" name="surname" placeholder="例如:汤" required />
|
||||
</label>
|
||||
<div class="field" role="group" aria-labelledby="public-family-region-label" aria-describedby="public-region-status public-create-status">
|
||||
<span id="public-family-region-label">家谱所在地 <b aria-hidden="true">*</b></span>
|
||||
<div class="region-picker create-region-picker" data-region-picker>
|
||||
<label for="publicFamilyProvince">省
|
||||
<select id="publicFamilyProvince" class="input" name="provinceCode" data-region-level="province">
|
||||
<option value="">请选择省</option>
|
||||
</select>
|
||||
</label>
|
||||
<label for="publicFamilyCity">市
|
||||
<select id="publicFamilyCity" class="input" name="cityCode" data-region-level="city">
|
||||
<option value="">请选择市</option>
|
||||
</select>
|
||||
</label>
|
||||
<label for="publicFamilyDistrict">区县
|
||||
<select id="publicFamilyDistrict" class="input" name="districtCode" data-region-level="district">
|
||||
<option value="">请选择区县</option>
|
||||
</select>
|
||||
</label>
|
||||
<input type="hidden" name="regionCode" data-region-code />
|
||||
</div>
|
||||
<div id="public-region-status" class="form-note" data-region-picker-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<label class="field" for="publicAncestralHall">堂号/祠堂
|
||||
<input id="publicAncestralHall" class="input" name="ancestralHall" placeholder="选填" />
|
||||
</label>
|
||||
<label class="field" for="publicOriginPlace">籍贯/起源地
|
||||
<input id="publicOriginPlace" class="input" name="originPlace" placeholder="选填" />
|
||||
</label>
|
||||
<label class="field" for="publicAddressDetail">详细地址
|
||||
<input id="publicAddressDetail" class="input" name="addressDetail" placeholder="选填" />
|
||||
</label>
|
||||
<label class="field" for="publicIntro">家谱简介
|
||||
<textarea id="publicIntro" name="intro" placeholder="选填"></textarea>
|
||||
</label>
|
||||
<label class="field" for="publicVisibility">可见范围
|
||||
<select id="publicVisibility" class="input" name="visibility">
|
||||
<option value="1">公开</option>
|
||||
<option value="2">成员可见</option>
|
||||
<option value="0">私密</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field" for="publicJoinMode">加入方式
|
||||
<select id="publicJoinMode" class="input" name="joinMode" aria-describedby="public-invite-mode-note">
|
||||
<option value="1">审核加入</option>
|
||||
<option value="2">邀请码加入</option>
|
||||
<option value="0">关闭加入</option>
|
||||
</select>
|
||||
<span id="public-invite-mode-note" class="form-note">当前 PC 未提供邀请码生成、发送或管理接口;选择邀请模式后不能在本页面继续生成邀请码。</span>
|
||||
</label>
|
||||
<div id="public-create-status" class="form-note" data-genealogy-create-status role="status" aria-live="polite"></div>
|
||||
<div>
|
||||
<button class="btn primary magnetic submit-btn" type="submit" data-genealogy-create-submit>提交创建</button>
|
||||
</div>
|
||||
<input class="input" name="addressDetail" placeholder="家族所在地" />
|
||||
<textarea name="intro" placeholder="家族简介"></textarea>
|
||||
<button class="btn primary magnetic submit-btn" type="submit">提交创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -124,12 +162,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="public/js/jquery360.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="utils/StorageUtil.js"></script>
|
||||
<script src="utils/FormUtil.js"></script>
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/region-pages.js"></script>
|
||||
<script src="public/js/genealogy-entry-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# PC 后端补齐清单(交给后端 GPT)
|
||||
|
||||
> 状态:待后端确认与实施
|
||||
>
|
||||
> 前端基线:`ac58235`(“验证20%”)之后的 PC 工作区。当前前端回归为 362 项通过。
|
||||
>
|
||||
> 本文目的:让后端直接知道 PC 端还缺什么能力、每项最小需要补什么,以及如何验收。它是 `PC接口对接规划.md` 中 C01–C46 的精简交接版;详细的历史证据仍以该文档为准。
|
||||
|
||||
## 0. 不可违反的边界
|
||||
|
||||
1. 这是 **PC** 契约补齐,不是把 `Jiapu-App` 的接口、参数、缓存键或数据结构搬过来。路径和 DTO 以 PC 后端为唯一 owner。
|
||||
2. 请先在 PC 的 Apifox/OpenAPI 中定义完整请求、响应、权限、枚举和错误码,再实现 Controller/Service;完成后重新导出契约。前端只会通过 `utils/ApiClient.js` 接入已确认的 PC 契约。
|
||||
3. 不要以泛型 `ObjectResult`、无元素类型的列表或“前端自行猜字段”作为交付。每个列表、详情和写入结果都必须有明确 DTO。
|
||||
4. 所有资源 ID、OSS ID、订单号等可能超过 JavaScript 安全整数的值,统一以十进制字符串传输和返回;不得要求用户手填内部 ID。
|
||||
5. 不返回或展示成员、邀请人、通知发送人的原始手机号。确有业务必要的内部用户 ID 只在受权限保护的选项或写入场景返回,PC 页面不会展示它。
|
||||
6. 不记录、更不要求提交真实测试账号、密码、令牌或真实家谱数据。角色验收使用既有隔离身份或后端准备的隔离夹具。
|
||||
|
||||
## 1. 已有能力,不要重复实现
|
||||
|
||||
以下 PC 能力已被当前前端使用:登录/账户基础资料、家谱列表与创建、申请加入与审核、成员名册与角色维护、成员选项、世系人物及四类关系新增、字辈、家族圈常规 CRUD、视频、成长/亲友/备忘/功德记录、谱文、相册/照片、祭祀活动/祭品/活动邀约、通知已读、反馈工单、会员套餐与会员订单、帮助文章、PC 推广和官网资讯。
|
||||
|
||||
本清单要求补的是这些能力的缺口或契约精度,不要求重新设计上述已有流程。已有写入接口仍必须由后端做最终权限校验,不能只依赖前端隐藏按钮。
|
||||
|
||||
## 2. P0:PC 当前完全缺失的业务契约
|
||||
|
||||
| 编号 | 前端位置与业务目标 | 后端必须补齐的最小能力 | 验收标准 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0-01 | `profile-data-reminders.html`:查看当前家谱中待完善的成员资料 | 只读“资料缺项提醒”列表,按当前家谱和当前操作者权限过滤。每项至少返回稳定的成员/世系人物定位 ID、脱敏展示名、缺失字段枚举、可处理状态和更新时间;缺失字段至少能区分头像、出生日期、关系等。联系方式只能表达“缺失/已填写”,不能下发原始号码。 | 有权限角色能读取真实空列表和非空列表;无权限、无家谱、资源不存在分别有可区分失败;前端能从响应跳到已有成员/世系编辑页。 |
|
||||
| P0-02 | `profile-invite.html`、家谱主页:邀请家人加入家谱 | 家谱成员邀请的创建、活动邀请列表、撤销/失效、受邀人校验和受邀人接受流程。邀请方式如为链接或邀请码,响应需给出 `inviteId`、受限邀请凭据/链接、过期时间、使用次数或上限、状态及可展示的创建时间;不得把可枚举的内部 ID 当邀请码。 | 仅后端定义的可管理角色可创建和撤销;过期、撤销、次数用尽、非受邀人、重复接受均有稳定业务错误;接受后能在家谱成员/审核链路中重读结果。 |
|
||||
| P0-03 | `profile-family-home.html`:重要证件 | 家谱重要证件的列表、详情、新增、修改、删除(或明确仅支持其中一部分)。证件 DTO 至少含稳定 ID、家谱 ID、标题/类型、说明、文件对象、创建/更新时间、状态和权限能力。文件必须使用第 3.1 节的安全文件对象。 | 无权用户不能读写他人家谱证件;上传后重读同一条记录;删除后的可见性和是否可恢复有明确定义。 |
|
||||
| P0-04 | `profile-families.html`:我的家谱自定义排序 | 读取当前账号的家谱顺序及保存顺序的接口。保存请求应提交完整有序的“当前账号可见家谱 ID”数组,后端在一个事务内校验归属、去重和完整性,不接受跨账号或不可见家谱 ID。 | 重排后重新读取顺序一致;重复、缺失、越权 ID 返回明确校验/权限错误;并发保存不会产生重复排序值。 |
|
||||
| P0-05 | `profile-family-home.html`:删除家谱 | 先由产品/后端确定唯一语义:**逻辑归档** 或 **物理删除**,不能接口名叫删除而实际效果不明确。接口需只允许谱主执行,并返回影响范围(成员、人物、内容、文件、订单等)、最终状态和可否恢复;如需二次确认,应采用后端签发的一次性确认令牌或明确确认字段。 | 非谱主必定拒绝;成功后“我的家谱”重读不再出现或以归档状态出现;重复请求幂等;关联资源处理与定义一致。 |
|
||||
| P0-06 | `profile-share.html`、`profile-services.html`:应用分享、奖励、推广收益、提现 | 一套完整账户权益契约:分享规则/分享凭据、分享记录、奖励或收益流水、账户可用/冻结/累计余额、提现方式选项、提现申请及提现记录。金额和状态必须由服务端计算与驱动,前端不能传入奖励金额、余额或审核状态。 | 普通用户只能读取自己的记录;提现前校验可用余额、金额精度、最低/最高额、风控状态和收款方式;重复提交幂等;流水、余额与提现状态可重读且一致。 |
|
||||
| P0-07 | `profile-services.html`:个性化推荐开关 | 当前用户的推荐偏好读取与保存。至少定义可配置项枚举、默认值、保存后的版本/更新时间,以及未登录、无权限或功能未开通的处理。 | 未知枚举和值非法会被拒绝;保存后重读一致;不允许操作其他用户的偏好。 |
|
||||
| P0-08 | `profile-services.html`:会员订单在线支付 | 现有“创建订单/读取订单”之外,补支付发起、支付参数、安全回跳/异步通知后的订单状态查询和关闭/超时语义。`payType` 必须有正式枚举,订单状态机由后端维护,不能让前端提交“已支付”。 | 创建支付后返回可安全使用的支付会话参数;支付成功、失败、取消、超时、退款均能重读到准确状态;重复通知和用户刷新幂等。 |
|
||||
|
||||
## 3. P1:已有 PC 页面被契约限制的能力
|
||||
|
||||
### 3.1 统一安全文件访问能力(C21、C23、C25、C28、C30、C36、C38、C40)
|
||||
|
||||
下列资源目前大多只返回 `ossId` 或 `mediaOssIds`,PC 无法安全回显图片、封面、视频、附件名称或下载入口:家族圈、视频、成长记录、亲友记录、备忘录、谱文、相册/照片、祭祀活动。
|
||||
|
||||
请在资源 View 内直接返回文件对象数组,或提供一个受资源权限保护的 PC 文件解析接口。两种方案只保留一种,推荐统一文件对象:
|
||||
|
||||
| 字段 | 要求 |
|
||||
| --- | --- |
|
||||
| `ossId` | string,原始文件稳定标识 |
|
||||
| `fileName` | 可安全展示的文件名 |
|
||||
| `mediaType` | 明确枚举,例如 image/video/document |
|
||||
| `accessUrl` | 短时授权 URL;不能由前端用 OSS ID 拼接 |
|
||||
| `expiresAt` | 授权 URL 失效时间,使用统一日期时间格式 |
|
||||
|
||||
文件解析必须根据“该文件所属业务资源 + 当前操作者”校验权限,不能做成任意登录用户可按 OSS ID 探测或下载文件的接口。
|
||||
|
||||
### 3.2 停用记录的管理、详情与恢复(C22、C24、C26、C29、C31、C33、C37、C39、C41)
|
||||
|
||||
以下资源可以或可能写入 `status=1`,但普通列表不返回、普通详情拒绝读取,导致管理员无法恢复或验证:家族圈、视频、成长记录、亲友记录、备忘录、功德、谱文、相册/照片、祭祀活动。
|
||||
|
||||
请为每类资源提供唯一且一致的方案:
|
||||
|
||||
1. 给有管理权限的用户提供 `management` 列表和详情,包含正常/停用状态;或
|
||||
2. 提供受权限保护的独立状态读取与恢复接口。
|
||||
|
||||
必须明确 `status=0/1` 的中文业务含义、谁可停用/恢复、停用后文件和关联数据如何处理、普通用户是否仍可读取。前端在此完成前会持续固定提交正常状态,避免产生不可重读数据。
|
||||
|
||||
### 3.3 金额、分类与可清空字段(C18、C27、C32、C35、C42)
|
||||
|
||||
| 范围 | 缺失内容 | 必须定义 |
|
||||
| --- | --- | --- |
|
||||
| 亲友往来 `giftAmount`、功德金额、祭品金额 | 只有 number/BigDecimal,缺少精度和边界 | 货币单位、`precision`、`scale`、`minimum`、`maximum`、是否允许负数及其业务含义。建议金额响应使用十进制字符串,避免浮点误差。 |
|
||||
| 谱文分类 | 列表的 `categoryId` 未进入正式契约,且没有分类选项来源 | 正式 query、当前家谱文章分类选项接口、分类 DTO、是否允许无分类和分类停用后的处理。 |
|
||||
| 所有可选更新字段 | 未定义“不传字段”、`null`、空字符串、空数组的差别 | 每个更新 DTO 对每个可清空字段写明语义,并用后端测试覆盖。特别是备忘录提醒时间、说明、附件和成员绑定。 |
|
||||
|
||||
### 3.4 个人资料、成员、通知与关系契约(C04–C08、C11–C13、C19、C34、C43–C46)
|
||||
|
||||
| 范围 | 当前问题 | 后端补齐要求 |
|
||||
| --- | --- | --- |
|
||||
| 当前用户资料 | 资料读取仍可能是泛型响应;更新 DTO 没有地区字段 | 提供完整 `ProfileView`;如产品保留现居地区,更新/读取都使用 `regionCode`;明确头像 ID 类型和所有可清空字段语义。 |
|
||||
| 行政区划 | 历史上存在双路径和无元素 DTO | 只保留一套 PC 正式路径;`RegionView` 固定 `regionCode`、`regionName`、`regionLevel`、父级关系;删除旧路径。 |
|
||||
| 世系树 | `LineagePersonTreeView` 曾无属性 | OpenAPI 展开递归节点:人物 ID、基础展示资料、配偶、子女及必要的状态/权限。所有日期必须声明 `date` 或带时区规则的 `date-time`。 |
|
||||
| 枚举与能力 | `completed`、`feedType`、`recordType` 等可能无完整枚举;多数 View 缺 `canEdit/canDelete` | 明确自由文本或完整枚举;对页面需要的操作返回稳定的严格布尔能力字段,例如 `canManage`、`canEditContent`、`canEdit`、`canDelete`。后端仍做最终鉴权。 |
|
||||
| 通知 | 声明的家谱名、发送人、业务摘要等字段实际常为空,且含隐私风险 | 补齐真正可用的非敏感字段,或从 Schema 删除不会返回的字段;提供安全、明确的 `bizType/bizId` 深链规则,不能让前端猜路径;始终不返回发送人手机号。 |
|
||||
| 家谱成员 | `GenealogyMemberVo` 声明字段与实际填充不一致,角色枚举也不一致 | View 补齐真实需要的非敏感展示字段,或收紧 Schema;可分配角色正式收敛为后端实际支持的 `admin`、`editor`、`member`,不能接受 `owner`、`visitor`。 |
|
||||
| 世系关系解除 | 只有新增关系,没有解除关系 | 新增带家谱权限与关系完整性校验的解除接口;需明确父母、配偶、子女、兄弟姐妹解除后的双向影响。 |
|
||||
| 成员与世系人物解绑 | 当前更新中空 `lineagePersonId` 只代表不修改 | 提供专用解绑动作,或正式约定 `null`/空值表示解绑并给出可验证示例;不能由前端猜测。 |
|
||||
| 个人亲属关系 | `profile-data.html` 当前无个人关系数据 | 这是独立于家谱世系的产品决策:若保留个人关系页,请提供当前用户私有的列表/维护契约;若不做,请明确下线该需求,继续引导到家谱世系维护。 |
|
||||
|
||||
## 4. P2:契约质量与一致性必须同时修复
|
||||
|
||||
这些事项不一定新增页面,却会使 PC 前后端长期出现“接口能调但不知道如何正确用”的问题:
|
||||
|
||||
1. 删除/废弃缺少开头 `/` 的旧短信路径,只保留正式 PC 路径。
|
||||
2. 未登录可调用的注册、登录、短信、找回密码、验证接口在 OpenAPI 中标为公开;登录后接口标明 Bearer 鉴权。不要让公开接口错误继承 Authorization。
|
||||
3. 所有列表、详情、上传初始化/完成、写入结果都展开具体 Schema,尤其不能保留无元素的通用列表。上传初始化要明确 `uploadId`、秒传、已传分片、完成后的 OSS 信息。
|
||||
4. 用统一错误响应表达:未登录、无权限、资源不存在、字段校验失败、业务冲突、状态不可操作、频率/幂等冲突。每一类要有稳定 code 和用户可读 message;不要只返回模糊失败文本。
|
||||
5. 统一日期格式、时区、分页字段和排序字段;同一资源的列表、详情、创建、更新应使用同一 ID 类型和状态枚举。
|
||||
6. 更新 Apifox/OpenAPI、DTO、Controller、Service 和集成测试必须在同一提交完成;不要只改其中一处,也不要保留 APP 路径 fallback。
|
||||
|
||||
## 5. 权限与数据安全验收矩阵
|
||||
|
||||
后端实施每个新增或补齐接口后,至少用既有隔离的谱主、管理员/编辑、普通成员、非成员和未登录身份验证。不要为了验收在真实家谱中创建成员或写入数据。
|
||||
|
||||
| 场景 | 必须验证 |
|
||||
| --- | --- |
|
||||
| 未登录 | 仅公开认证接口可用;所有用户/家谱资源拒绝。 |
|
||||
| 非成员 | 不能读取私有家谱内容、成员、文件、邀请或管理列表。 |
|
||||
| 普通成员 | 只能使用后端允许的只读或本人操作,不能修改成员角色、家谱设置、排序、删除、证件或全局邀请。 |
|
||||
| 管理员/编辑 | 仅获得已声明能力范围内的内容或成员维护权限;不得越过谱主做家谱删除、谱主转让等操作。 |
|
||||
| 谱主 | 可执行后端定义的家谱级管理动作,但仍受资源状态、确认和业务完整性约束。 |
|
||||
|
||||
每个写入接口还要验证:重复请求幂等、写后可精确重读、越权返回明确错误、停用/恢复状态可验证、敏感字段不会越权泄漏。
|
||||
|
||||
## 6. 建议实施顺序
|
||||
|
||||
1. 先完成第 4 节的 OpenAPI/DTO 基础收口,并冻结唯一 PC 契约。
|
||||
2. 其次完成 3.1 文件访问和 3.2 停用管理;这两项能同时解除多个已有内容页的展示和状态闭环。
|
||||
3. 再完成 3.3、3.4 的金额、分类、资料、成员和关系精度。
|
||||
4. 最后按 P0-01 至 P0-08 补齐新业务;其中家谱删除、提现和支付必须先确认产品状态机与风控规则,不能只给一个无约束写接口。
|
||||
|
||||
## 7. 给后端 GPT 的执行指令
|
||||
|
||||
```text
|
||||
你负责为现有“家谱”项目补齐 PC API,不得复用或复制 Jiapu-App API。
|
||||
|
||||
先阅读 docs/PC后端补齐清单.md 与 docs/PC接口对接规划.md,确认当前 PC OpenAPI 和 Controller 的实际差异;不要重做“已有能力”章节列出的功能。
|
||||
|
||||
按第 6 节顺序实施。每个模块都要在同一改动中完成:PC OpenAPI/Apifox、请求 DTO、响应 View、Controller/Service、权限校验、错误码和集成测试。不要返回泛型 Object,不要让前端手填内部 ID,不要返回原始手机号,也不要新增 APP 路径兼容层。
|
||||
|
||||
实施前先输出:将新增/修改的 PC 接口、每个请求和响应字段、角色权限、状态机、数据库迁移(如有)以及与本清单的对应编号。实施后用隔离角色完成读写、越权、重复提交和写后重读验证,并重新导出 OpenAPI。
|
||||
```
|
||||
|
||||
## 8. 交付回执模板
|
||||
|
||||
后端完成后,请按以下格式回传,便于前端逐项接入:
|
||||
|
||||
| 清单编号 | 状态 | 正式 PC method/path | 请求 DTO | 响应 View | 权限/状态机 | OpenAPI 位置 | 集成测试证据 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 例如 P0-01 | 已完成/不做(附产品结论) | 仅填 PC 路径 | 字段和必填性 | 字段和类型 | 可用角色与错误码 | tag/schema 名 | 隔离测试名称,不含真实凭据 |
|
||||
|
||||
如某项决定不做,必须给出明确产品结论和对应前端页面应继续保留的不可操作文案;不能以“暂时没有接口”结束。
|
||||
+1
-1
@@ -1557,7 +1557,7 @@ PC 端只开放三条业务用户接口:
|
||||
|
||||
- C35–C46 中记录的分类选项、文件 URL、停用记录、金额业务精度/上限、响应 enrichment 与解绑契约;
|
||||
- 世系关系解除没有 PC 接口;成员移出/退出不会伪造成世系关系解除或成员与世系人物解绑;
|
||||
- 真实账号 `19181970173` 已验证登录和无家谱上下文分支:阶段 6 页面均阻止业务请求、隐藏管理操作并返回家谱选择页,浏览器控制台无错误。该账号当前无家谱数据,因此列表有数据态、详情以及真实写操作仍待有数据账号复核,未制造家谱或业务记录。
|
||||
- 隔离测试账号已验证登录和无家谱上下文分支:阶段 6 页面均阻止业务请求、隐藏管理操作并返回家谱选择页,浏览器控制台无错误。该账号当前无家谱数据,因此列表有数据态、详情以及真实写操作仍待有数据账号复核,未制造家谱或业务记录。
|
||||
|
||||
### 阶段 7:剩余 PC 页面分批开放
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
await client.submitFeedback({
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173',
|
||||
contactInfo: '<测试联系电话>',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
});
|
||||
@@ -50,7 +50,7 @@ assert.deepEqual(calls, [
|
||||
body: {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
contactInfo: '<测试联系电话>'
|
||||
}
|
||||
},
|
||||
{ method: 'GET', url: '/genealogy/pc/feedback' }
|
||||
@@ -120,13 +120,13 @@ Expected: PASS。
|
||||
assert.deepEqual(FeedbackPages.buildFeedbackBody({
|
||||
feedbackType: ' bug ',
|
||||
feedbackContent: ' 页面按钮无响应 ',
|
||||
contactInfo: ' 19181970173 ',
|
||||
contactInfo: ' <测试联系电话> ',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
}), {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
contactInfo: '<测试联系电话>'
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ await client.createGenealogy({
|
||||
|
||||
await client.applyToGenealogy(genealogyId, {
|
||||
applicantName: '申请人',
|
||||
phone: '19100000000',
|
||||
phone: '<测试联系电话>',
|
||||
relationDesc: '族亲',
|
||||
applyReason: '申请加入',
|
||||
inviterUserId: 'drop-without-safe-source'
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
```js
|
||||
assert.deepEqual(JoinPages.buildJoinApplyBody({
|
||||
applicantName: ' 叶子 ',
|
||||
phone: ' 19181970173 ',
|
||||
phone: ' <测试联系电话> ',
|
||||
relationDesc: ' 族亲 ',
|
||||
applyReason: ' 申请加入 ',
|
||||
inviterUserId: 'must-drop'
|
||||
}), {
|
||||
applicantName: '叶子',
|
||||
phone: '19181970173',
|
||||
phone: '<测试联系电话>',
|
||||
relationDesc: '族亲',
|
||||
applyReason: '申请加入'
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# 个人中心能力映射
|
||||
|
||||
## 使用边界
|
||||
|
||||
`Jiapu-App` 只作为产品信息架构参考。本项目的真实读取、写入和权限判断只使用当前 PC 契约及 `utils/ApiClient.js`;不得复制 App 请求路径、参数、缓存编号或测试身份信息。
|
||||
|
||||
没有当前 PC 接口契约的能力必须保留明确的不可操作状态,不能以静态数据、二维码、金额或成功提示替代。
|
||||
|
||||
## 账户与服务
|
||||
|
||||
| 参考能力 | PC 页面 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| 账户概览、个人资料 | `profile.html`、`profile-data.html` | 读取并编辑当前 PC 资料字段;未声明的地区和个人亲属列表不伪造。 |
|
||||
| 修改密码、换绑手机、注销、退出登录 | `profile-security.html` | 使用认证 PC 契约;表单具备校验、提交中和失败反馈。 |
|
||||
| 意见反馈、我的工单 | `profile-feedback.html`、`submit-ticket.html`、`my-tickets.html`、`ticket-detail.html` | 使用当前账号的反馈集合,提交后重读确认。 |
|
||||
| 帮助中心、平台联系与协议 | `help.html`、`profile-services.html` | 帮助文章读取使用 PC 契约;平台说明使用站内页面。 |
|
||||
| 会员套餐与订单 | `profile-services.html` | 可选择服务端套餐和我的家谱创建订单并重读;PC 未开放在线支付。 |
|
||||
| 分享奖励、推广收益、提现、推荐偏好 | `profile-share.html`、`profile-services.html` | 当前 PC 无对应读取或写入契约,明确不可操作。 |
|
||||
|
||||
## 家谱工作台
|
||||
|
||||
| 参考能力 | PC 页面 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| 家谱列表、创建、加入、我的申请 | `profile-families.html`、`profile-create-family.html`、`profile-join-family.html` | 使用真实家谱列表、额度和申请契约;家谱编号只来自上下文或响应。 |
|
||||
| 当前家谱主页 | `profile-family-home.html` | 以概览和世系树组成桌面工作台,并透传当前家谱上下文。 |
|
||||
| 成员名册、角色、谱主转移、入谱审核 | `profile-family-admin.html`、`profile-admin-permissions.html`、`profile-join-review.html` | 成员维护和审核只由服务端能力字段决定。 |
|
||||
| 世系人物与父母、配偶、子女、兄弟姐妹关系 | `profile-tree.html` | 列表、详情、编辑和四类关系维护使用世系 PC 契约。 |
|
||||
| 字辈 | `profile-generation.html` | 字辈列表、单条和批量维护使用当前 PC 契约。 |
|
||||
| 家族圈 | `profile-feed.html`、`profile-feed-detail.html`、`profile-feed-edit.html` | 动态、评论、回复和点赞使用当前家谱上下文。 |
|
||||
| 谱文、相册、视频 | `profile-article.html`、`profile-album.html`、`profile-video.html` 及各自编辑/详情页 | 已提供列表、详情和编辑闭环;没有文件访问 URL 时只展示安全业务元数据。 |
|
||||
| 祭祀、贺礼邀约 | `profile-ceremony.html`、`profile-ceremony-detail.html`、`profile-gift.html`、`profile-gift-edit.html` | 活动、祭品和当前账号邀约使用当前 PC 契约。 |
|
||||
| 成长、亲友、备忘、功德记录 | 对应 `profile-*-edit.html` 与列表页 | 各自使用当前 PC CRUD 契约和真实家谱上下文。 |
|
||||
| 家谱证件、成员邀请、家谱排序、家谱删除 | `profile-family-home.html`、`profile-invite.html`、`profile-families.html` | 当前 PC 没有完整契约,明确不可操作。 |
|
||||
| 个人亲属关系页 | `profile-data.html` | 当前 PC 未提供个人关系列表;页面明确引导至当前家谱的成员管理与世系维护。 |
|
||||
|
||||
## 验收边界
|
||||
|
||||
- 页面统一提供加载、空数据、请求失败和无权限状态;写入表单提供字段标签、校验、提交中和失败反馈。
|
||||
- 所有家谱内操作由同一当前家谱上下文提供稳定字符串 ID,不接受手工填写业务 ID 或 OSS ID。
|
||||
- 权限以 PC 家谱详情的严格布尔能力字段和服务端写入结果为准;没有隔离管理员或普通成员测试实体时,运行时角色验收记录原因后跳过,不创建测试数据。
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
> 更新:2026-07-25
|
||||
> 历史交接记录:本文的 79/85 条接口数量已过期,不再作为 AI 施工依据。当前接口基线、字段分类、阻断项和执行顺序统一以 [PC接口对接规划.md](PC接口对接规划.md) 为准;执行前仍须在 Apifox 实时复核并重新导出。
|
||||
> **失效范围:** 本文第二至第七节均为当日历史快照,可能包含已被现行 PC 契约和页面实现替代的接口、页面状态与验收结论;不得据此新增、关闭或阻断任何功能。
|
||||
|
||||
## 一、必须遵守的范围
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
- 当前工作区共有 55 个 HTML 页面,现有页面之间没有指向不存在 HTML 页的站内链接。
|
||||
- 本文保留项目级历史规划,不再维护接口数量和字段契约。当前 PC 接口基线、字段页面归属和 AI 执行顺序统一见 [PC接口对接规划.md](PC接口对接规划.md)。
|
||||
- 已完成真实接口闭环:注册、密码登录、短信登录、找回密码、个人资料、安全设置、头像上传、地区选择、家族圈动态。
|
||||
- 家谱列表、家谱创建/加入、成员、权限、世系、谱文、相册、视频、贺礼、成长、备忘、功德、消息、反馈等页面已保留设计,但尚无对应正式接口,不得伪造请求或静态数据为真实业务。
|
||||
- 家谱列表、家谱创建/加入、成员、权限、世系、谱文、相册、视频、贺礼、成长、备忘、功德、消息、反馈等页面已接入当前 PC 契约;仍未声明 PC 契约的能力以 [个人中心能力映射.md](个人中心能力映射.md) 标注为不可操作,不得伪造请求或静态数据为真实业务。
|
||||
|
||||
## 实施进度
|
||||
|
||||
|
||||
+11
-1
@@ -19,10 +19,20 @@
|
||||
|
||||
## 验证方式
|
||||
|
||||
- 自动回归:`npm test`,298 项通过。
|
||||
- 自动回归:`npm test`,362 项通过。
|
||||
- 真实流程:登录后进入真实家谱列表、选择家谱主页,并在 1249px 桌面与 390px 手机视口检查导航和首屏。
|
||||
- 验收使用隔离浏览器配置;测试账号、登录令牌及真实家谱标识不写入仓库。
|
||||
|
||||
## 20% 后续维护记录
|
||||
|
||||
- 个人中心继续以 Jiapu-App 的账户与家谱能力为信息架构参考;真实读写仅使用当前 PC 客户端契约。
|
||||
- 已补齐当前家谱上下文传递、业务页统一加载/空数据/失败/无权限状态,以及账户、家谱和内容表单的标签与失败反馈。
|
||||
- PC 尚未声明契约的资料提醒、家谱邀请、家谱删除、家谱排序、分享奖励、推广收益与提现,保留明确的不可操作状态;不展示模拟金额、记录、二维码或成功结果。
|
||||
- 交给后端的完整补齐范围、最小 DTO、权限和验收要求见 `docs/PC后端补齐清单.md`;该清单只描述 PC 能力,不迁用 App 接口。
|
||||
- 本次当前全量回归为 362 项通过;测试账号、密码、令牌和真实家谱信息仍不写入代码、文档或 Git。
|
||||
- 角色运行时验收受阻:已用隔离身份只读确认谱主能力和未认证访问闭环,但该身份可访问的家谱中没有管理员或普通成员实体。未创建成员、未提交申请或写入家谱数据;解除条件是提供已有的隔离管理员、普通成员身份,或预先在隔离家谱中准备对应角色关系。
|
||||
- 最新只读复核跳过:隔离登录响应未提供可明确消费的令牌字段;为避免打印原始响应或猜测字段,未继续请求角色接口。该项不影响现有静态权限回归,待测试环境恢复标准 PC 登录响应后再验收。
|
||||
|
||||
## 后续优先级
|
||||
|
||||
1. 统一业务页的加载中、空数据、请求失败和无权限状态,减少页面之间的割裂感。
|
||||
|
||||
+21
-35
@@ -21,43 +21,28 @@
|
||||
<div class="eyebrow">Password Recovery</div>
|
||||
<h1 id="recover-title">找回密码</h1>
|
||||
<p class="lead">输入注册手机号,获取验证码后重设登录密码。</p>
|
||||
<form id="password-reset-form" class="form-grid">
|
||||
<input
|
||||
class="input"
|
||||
name="phone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
maxlength="11"
|
||||
placeholder="手机号"
|
||||
/>
|
||||
<div class="code-row">
|
||||
<input
|
||||
class="input"
|
||||
name="smsCode"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
maxlength="4"
|
||||
pattern="\d{4}"
|
||||
placeholder="验证码"
|
||||
/>
|
||||
<button class="btn ghost" type="button" data-api-send-code>获取验证码</button>
|
||||
<form id="password-reset-form" class="form-grid" novalidate>
|
||||
<label class="auth-field" for="reset-phone">
|
||||
<span>手机号 <b aria-hidden="true">*</b></span>
|
||||
<input id="reset-phone" class="input" name="phone" type="tel" inputmode="numeric" autocomplete="tel" maxlength="11" placeholder="请输入注册手机号" required />
|
||||
</label>
|
||||
<div class="auth-field">
|
||||
<label for="reset-sms-code">短信验证码 <b aria-hidden="true">*</b></label>
|
||||
<div class="code-row">
|
||||
<input id="reset-sms-code" class="input" name="smsCode" autocomplete="one-time-code" inputmode="numeric" maxlength="4" pattern="\d{4}" placeholder="请输入四位验证码" required />
|
||||
<button class="btn ghost" type="button" data-api-send-code>获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="input"
|
||||
name="newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="新密码"
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="确认新密码"
|
||||
/>
|
||||
<label class="auth-field" for="reset-password">
|
||||
<span>新密码 <b aria-hidden="true">*</b></span>
|
||||
<input id="reset-password" class="input" name="newPassword" type="password" autocomplete="new-password" placeholder="至少六位" required />
|
||||
</label>
|
||||
<label class="auth-field" for="reset-confirm-password">
|
||||
<span>确认新密码 <b aria-hidden="true">*</b></span>
|
||||
<input id="reset-confirm-password" class="input" name="confirmPassword" type="password" autocomplete="new-password" placeholder="请再次输入新密码" required />
|
||||
</label>
|
||||
<input type="hidden" name="validToken" />
|
||||
<div id="reset-status" class="auth-form-status" role="status" aria-live="polite" data-auth-form-status></div>
|
||||
<button class="btn primary" type="submit">重设密码</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -73,6 +58,7 @@
|
||||
<script src="public/layui/layui.js"></script>
|
||||
<script src="public/tac/js/tac.min.js"></script>
|
||||
<script src="public/js/captcha-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/auth-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
<p>选择问题后读取平台发布的完整解答。</p>
|
||||
<button class="btn ghost" type="button" data-help-refresh>刷新帮助</button>
|
||||
</div>
|
||||
<p class="muted" data-help-status aria-live="polite">正在读取帮助文章…</p>
|
||||
<div class="muted" data-help-status role="status" aria-live="polite"></div>
|
||||
<div class="faq-list is-loading" data-help-list>
|
||||
<div class="api-empty">正在读取帮助文章…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取帮助文章…</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -111,6 +111,7 @@
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/help-pages.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+39
-30
@@ -19,36 +19,44 @@
|
||||
<div class="eyebrow">Join Family</div>
|
||||
<h1 id="join-title">加入家谱</h1>
|
||||
<p class="lead">搜索并选择要加入的家谱,再补充申请资料。家谱编号由系统自动处理。</p>
|
||||
<form class="form-grid" data-join-genealogy-search>
|
||||
<label class="field">
|
||||
<span>搜索家谱</span>
|
||||
<input name="keyword" type="search" maxlength="50" placeholder="输入谱名或姓氏" />
|
||||
</label>
|
||||
<button class="btn ghost" type="submit">搜索</button>
|
||||
</form>
|
||||
<div class="module-list" data-join-genealogy-options></div>
|
||||
<p class="form-note" data-join-selected-genealogy>尚未选择家谱</p>
|
||||
<form class="form-grid" data-join-apply-form>
|
||||
<label class="field">
|
||||
<span>申请人姓名(选填)</span>
|
||||
<input name="applicantName" maxlength="50" autocomplete="name" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>联系电话(选填)</span>
|
||||
<input name="phone" maxlength="30" autocomplete="tel" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>与家谱关系(选填)</span>
|
||||
<input name="relationDesc" maxlength="100" placeholder="例如:族亲" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>申请理由(选填)</span>
|
||||
<textarea name="applyReason" maxlength="500" rows="4"></textarea>
|
||||
</label>
|
||||
<p class="form-note" aria-live="polite" data-join-apply-status></p>
|
||||
<button class="btn primary" type="submit" data-join-apply-submit>提交申请</button>
|
||||
<a class="btn ghost" href="profile-join-family.html">查看我的申请</a>
|
||||
</form>
|
||||
<div class="join-page-grid">
|
||||
<section class="join-search-panel" aria-labelledby="join-search-title">
|
||||
<h2 id="join-search-title">选择家谱</h2>
|
||||
<form class="form-grid" data-join-genealogy-search novalidate aria-describedby="join-options-status">
|
||||
<label class="field">
|
||||
<span>搜索家谱</span>
|
||||
<input name="keyword" type="search" maxlength="50" placeholder="输入谱名或姓氏" />
|
||||
</label>
|
||||
<button class="btn ghost" type="submit">搜索</button>
|
||||
</form>
|
||||
<div id="join-options-status" class="form-note" data-join-options-status role="status" aria-live="polite"></div>
|
||||
<div class="module-list" data-join-genealogy-options aria-live="polite"></div>
|
||||
<p class="form-note" data-join-selected-genealogy role="status" aria-live="polite">尚未选择家谱</p>
|
||||
</section>
|
||||
<form class="form-grid join-apply-form" data-join-apply-form novalidate aria-describedby="join-apply-status">
|
||||
<h2>补充申请资料</h2>
|
||||
<p class="form-note">以下资料均为选填,审核人将据此确认您的加入申请。</p>
|
||||
<label class="field">
|
||||
<span>申请人姓名(选填)</span>
|
||||
<input name="applicantName" maxlength="50" autocomplete="name" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>联系电话(选填)</span>
|
||||
<input name="phone" maxlength="30" autocomplete="tel" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>与家谱关系(选填)</span>
|
||||
<input name="relationDesc" maxlength="100" placeholder="例如:族亲" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>申请理由(选填)</span>
|
||||
<textarea name="applyReason" maxlength="500" rows="4"></textarea>
|
||||
</label>
|
||||
<div id="join-apply-status" class="form-note" data-join-apply-status role="status" aria-live="polite"></div>
|
||||
<button class="btn primary" type="submit" data-join-apply-submit>提交申请</button>
|
||||
<a class="btn ghost" href="profile-join-family.html">查看我的申请</a>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="config.js"></script>
|
||||
@@ -56,6 +64,7 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/join-pages.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+23
-38
@@ -29,55 +29,39 @@
|
||||
<form
|
||||
id="login-password-form"
|
||||
class="form-grid login-mode-panel"
|
||||
novalidate
|
||||
>
|
||||
<input
|
||||
class="input"
|
||||
name="phone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
maxlength="11"
|
||||
placeholder="手机号"
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="密码"
|
||||
/>
|
||||
<label class="auth-field" for="login-password-phone">
|
||||
<span>手机号 <b aria-hidden="true">*</b></span>
|
||||
<input id="login-password-phone" class="input" name="phone" type="tel" inputmode="numeric" autocomplete="tel" maxlength="11" placeholder="请输入手机号" required />
|
||||
</label>
|
||||
<label class="auth-field" for="login-password-value">
|
||||
<span>密码 <b aria-hidden="true">*</b></span>
|
||||
<input id="login-password-value" class="input" name="password" type="password" autocomplete="current-password" placeholder="请输入密码" required />
|
||||
</label>
|
||||
<input type="hidden" name="validToken" />
|
||||
<div id="login-password-status" class="auth-form-status" role="status" aria-live="polite" data-auth-form-status></div>
|
||||
<button class="btn primary" type="submit">登录</button>
|
||||
</form>
|
||||
<form
|
||||
id="login-sms-form"
|
||||
class="form-grid login-mode-panel"
|
||||
hidden
|
||||
novalidate
|
||||
>
|
||||
<input
|
||||
class="input"
|
||||
name="phone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
maxlength="11"
|
||||
placeholder="手机号"
|
||||
/>
|
||||
<div class="code-row">
|
||||
<input
|
||||
class="input"
|
||||
name="smsCode"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
maxlength="4"
|
||||
pattern="\d{4}"
|
||||
placeholder="短信验证码"
|
||||
/>
|
||||
<button class="btn ghost" type="button" data-api-send-code>
|
||||
获取验证码
|
||||
</button>
|
||||
<label class="auth-field" for="login-sms-phone">
|
||||
<span>手机号 <b aria-hidden="true">*</b></span>
|
||||
<input id="login-sms-phone" class="input" name="phone" type="tel" inputmode="numeric" autocomplete="tel" maxlength="11" placeholder="请输入手机号" required />
|
||||
</label>
|
||||
<div class="auth-field">
|
||||
<label for="login-sms-code">短信验证码 <b aria-hidden="true">*</b></label>
|
||||
<div class="code-row">
|
||||
<input id="login-sms-code" class="input" name="smsCode" autocomplete="one-time-code" inputmode="numeric" maxlength="4" pattern="\d{4}" placeholder="请输入四位验证码" required />
|
||||
<button class="btn ghost" type="button" data-api-send-code>获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="validToken" />
|
||||
<div id="login-sms-status" class="auth-form-status" role="status" aria-live="polite" data-auth-form-status></div>
|
||||
<button class="btn primary" type="submit">短信登录</button>
|
||||
</form>
|
||||
<div class="auth-links">
|
||||
@@ -97,6 +81,7 @@
|
||||
<script src="public/layui/layui.js"></script>
|
||||
<script src="public/tac/js/tac.min.js"></script>
|
||||
<script src="public/js/captcha-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/auth-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
|
||||
+4
-2
@@ -42,8 +42,10 @@
|
||||
<h2>我的反馈记录</h2>
|
||||
<button class="btn ghost" type="button" data-feedback-refresh>刷新</button>
|
||||
</div>
|
||||
<p class="form-note" aria-live="polite" data-feedback-status></p>
|
||||
<div class="module-list" data-feedback-list></div>
|
||||
<div class="form-status" role="status" aria-live="polite" data-feedback-status></div>
|
||||
<div class="module-list" data-feedback-list>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取反馈记录…</div>
|
||||
</div>
|
||||
<div class="site-info-actions">
|
||||
<a class="btn primary magnetic" href="submit-ticket.html">提交新工单</a>
|
||||
<a class="btn ghost magnetic" href="help.html">返回帮助中心</a>
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
<section class="site-info-section">
|
||||
<div class="container site-info-layout">
|
||||
<div>
|
||||
<p class="module-meta" data-site-news-status></p>
|
||||
<div class="module-meta" data-site-news-status role="status" aria-live="polite"></div>
|
||||
<div class="site-info-list" data-site-news-list>
|
||||
<div class="api-empty">资讯内容加载中</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取资讯…</div>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="site-info-panel">
|
||||
@@ -70,6 +70,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="utils/StorageUtil.js"></script>
|
||||
<script src="utils/axios.js"></script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="public/layui/css/layui.css" />
|
||||
<link rel="stylesheet" href="public/css/profile-module.css" />
|
||||
</head>
|
||||
<body class="page-profile-module" data-admin-permission-page data-feature-status="pending" data-feature-message="管理员权限服务正在开发中,当前页面仅保留设计预览。">
|
||||
<body class="page-profile-module" data-admin-permission-page>
|
||||
<header class="site-header">
|
||||
<div class="container nav">
|
||||
<a class="brand magnetic" href="profile.html"
|
||||
@@ -19,8 +19,8 @@
|
||||
><a class="active" href="profile-family-admin.html">家族管理</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-family-admin.html">返回管理</a
|
||||
><button class="btn primary magnetic" type="submit" form="admin-permission-form">确定</button>
|
||||
<a href="profile-family-admin.html" data-genealogy-context-link>返回管理</a
|
||||
><a class="btn primary magnetic" href="profile-family-admin.html" data-genealogy-context-link>管理成员</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div>
|
||||
<h1>管理员权限</h1>
|
||||
<p>
|
||||
分配全权管理员、全系管理员、支系管理员,以及入谱审核等具体权限。
|
||||
当前 PC 端通过成员名册维护管理员、编辑和成员角色;具体可操作范围以服务端返回的当前角色为准。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,34 +41,26 @@
|
||||
<section class="section">
|
||||
<div class="container module-layout">
|
||||
<aside class="module-nav">
|
||||
<a href="profile-family-admin.html">家族管理</a
|
||||
<a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
|
||||
><a class="active" href="profile-admin-permissions.html"
|
||||
>管理员权限</a
|
||||
><a href="profile-messages.html">入谱审核</a>
|
||||
><a href="profile-join-review.html" data-genealogy-context-link>入谱审核</a>
|
||||
</aside>
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>权限设置</h2>
|
||||
<form id="admin-permission-form" class="editor-form" data-permission-form>
|
||||
<!-- 成员下拉由 /members/options 接口渲染 -->
|
||||
<div class="editor-field">
|
||||
<label for="permission-member">选择成员</label>
|
||||
<select id="permission-member" data-member-select>
|
||||
<option value="">成员加载中...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="permission-role">角色权限</label>
|
||||
<select id="permission-role" name="roleType">
|
||||
<option value="admin">管理员</option>
|
||||
<option value="member">普通成员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary" type="submit">保存权限</button>
|
||||
<button class="btn ghost" type="button" data-owner-transfer>转让所有者</button>
|
||||
</div>
|
||||
</form>
|
||||
<h2>当前权限</h2>
|
||||
<div data-admin-permission-current>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在核对当前家谱角色与权限…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>角色与权限</h2>
|
||||
<div class="module-list">
|
||||
<article class="module-row"><div><h3>谱主</h3><p>可维护管理员、编辑和成员角色,并可转让谱主。</p></div></article>
|
||||
<article class="module-row"><div><h3>管理员</h3><p>可维护编辑和成员角色,不能修改谱主或其他管理员。</p></div></article>
|
||||
<article class="module-row"><div><h3>编辑、成员</h3><p>不显示成员角色编辑入口;最终权限由服务端校验。</p></div></article>
|
||||
</div>
|
||||
<div class="bottom-actions"><a class="btn primary magnetic" href="profile-family-admin.html" data-genealogy-context-link>进入成员名册管理</a></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +75,7 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/pending-pages.js"></script>
|
||||
<script src="public/js/admin-permission-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -48,25 +48,29 @@
|
||||
<section class="module-panel">
|
||||
<h2>相册信息</h2>
|
||||
<div data-album-detail>
|
||||
<div class="api-empty">相册加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
相册加载中...
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>照片列表</h2>
|
||||
<div class="module-list" data-album-photo-list>
|
||||
<div class="api-empty">照片加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
照片加载中...
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel" data-album-photo-editor hidden>
|
||||
<h2>添加照片</h2>
|
||||
<form class="editor-form" data-album-photo-form>
|
||||
<form class="editor-form" data-album-photo-form novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="albumPhotoFile">照片文件</label>
|
||||
<label for="albumPhotoFile">照片文件 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="albumPhotoOssId" name="ossId" type="hidden" />
|
||||
<label class="upload-control" for="albumPhotoFile">选择照片</label>
|
||||
<input id="albumPhotoFile" class="upload-input" type="file" accept="image/*" data-upload-target="#albumPhotoOssId" data-upload-status="#albumPhotoUploadStatus" />
|
||||
<p id="albumPhotoUploadStatus" class="upload-status">未选择文件</p>
|
||||
<p id="albumPhotoUploadStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="albumPhotoTitle">照片标题</label>
|
||||
@@ -92,7 +96,7 @@
|
||||
<input id="albumPhotoSortOrder" name="sortOrder" type="number" step="1" placeholder="数字越小越靠前" />
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<p class="api-status" data-album-photo-status></p>
|
||||
<div class="api-status" data-album-photo-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary" type="submit">添加照片</button>
|
||||
</div>
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
</aside>
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<div class="api-empty" data-album-editor-placeholder>正在读取家谱权限...</div>
|
||||
<form id="album-edit-form" class="editor-form" data-album-form data-album-editor hidden>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" data-album-editor-placeholder role="status" aria-live="polite" aria-busy="true">正在读取家谱权限...</div>
|
||||
<form id="album-edit-form" class="editor-form" data-album-form data-album-editor hidden novalidate>
|
||||
<div class="editor-field">
|
||||
<label for="albumName">相册名称</label>
|
||||
<label for="albumName">相册名称 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="albumName" name="albumName" type="text" required placeholder="请输入相册名称" />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -63,7 +63,7 @@
|
||||
<input id="albumCoverOssId" name="coverOssId" type="hidden" />
|
||||
<label class="upload-control" for="albumCoverFile">选择封面图片</label>
|
||||
<input id="albumCoverFile" class="upload-input" type="file" accept="image/*" data-upload-target="#albumCoverOssId" data-upload-status="#albumCoverStatus" />
|
||||
<p id="albumCoverStatus" class="upload-status">未选择文件</p>
|
||||
<p id="albumCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="albumSortOrder">排序</label>
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<p class="api-status" data-album-form-status></p>
|
||||
<div class="api-status" data-album-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary" type="submit">保存相册</button>
|
||||
<a class="btn ghost" href="profile-album.html" data-genealogy-context-link>取消</a>
|
||||
|
||||
+3
-1
@@ -51,7 +51,9 @@
|
||||
<section class="module-panel">
|
||||
<h2>相册列表</h2>
|
||||
<div class="module-list" data-album-list>
|
||||
<div class="api-empty">相册加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
相册加载中...
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -50,10 +50,12 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>谱文内容</h2>
|
||||
<div class="api-empty" data-article-editor-placeholder>正在读取家谱权限...</div>
|
||||
<form id="article-edit-form" class="editor-form layui-form" data-article-form data-article-editor hidden>
|
||||
<div data-article-editor-placeholder>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取家谱权限…</div>
|
||||
</div>
|
||||
<form id="article-edit-form" class="editor-form layui-form" data-article-form data-article-editor hidden novalidate>
|
||||
<div class="editor-field">
|
||||
<label for="articleTitle">谱文标题</label
|
||||
<label for="articleTitle">谱文标题<span class="field-required" aria-hidden="true"> *</span></label
|
||||
><input
|
||||
id="articleTitle"
|
||||
name="articleTitle"
|
||||
@@ -84,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="articleContent">正文</label
|
||||
<label for="articleContent">正文<span class="field-required" aria-hidden="true"> *</span></label
|
||||
><textarea
|
||||
id="articleContent"
|
||||
name="articleContent"
|
||||
@@ -102,10 +104,10 @@
|
||||
<input id="articleCoverOssId" name="coverOssId" type="hidden" />
|
||||
<label class="upload-control" for="articleCoverFile">选择封面图片</label>
|
||||
<input id="articleCoverFile" class="upload-input" type="file" accept="image/*" data-upload-target="#articleCoverOssId" data-upload-status="#articleCoverStatus" />
|
||||
<p id="articleCoverStatus" class="upload-status">未选择文件</p>
|
||||
<p id="articleCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<p class="api-status" data-article-form-status></p>
|
||||
<div class="form-status" data-article-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">保存</button
|
||||
><a class="btn ghost magnetic" href="profile-article.html"
|
||||
|
||||
@@ -52,13 +52,13 @@
|
||||
<section class="module-panel">
|
||||
<h2>谱文列表</h2>
|
||||
<div class="module-list" data-article-list>
|
||||
<div class="api-empty">谱文加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载谱文…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>谱文详情</h2>
|
||||
<div data-article-detail>
|
||||
<div class="api-empty">请选择一篇谱文查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一篇谱文查看详情</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -24,27 +24,46 @@
|
||||
<div class="container module-layout">
|
||||
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-gift.html">我的邀请</a><a class="active" href="profile-ceremony.html">祭祀活动</a><a href="profile-merit.html">功德录</a></aside>
|
||||
<div class="module-main">
|
||||
<section class="module-panel"><h2>活动信息</h2><div data-ceremony-detail aria-live="polite"><div class="api-empty">正在加载活动详情…</div></div></section>
|
||||
<section class="module-panel">
|
||||
<h2>活动信息</h2>
|
||||
<div data-ceremony-detail aria-live="polite">
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载活动详情…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>祭品记录</h2>
|
||||
<div class="module-list" data-ceremony-gift-list aria-live="polite"><div class="api-empty">正在加载祭品…</div></div>
|
||||
<form class="editor-form" data-ceremony-gift-form data-ceremony-admin-editor hidden>
|
||||
<div class="module-list" data-ceremony-gift-list aria-live="polite">
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载祭品…
|
||||
</div>
|
||||
</div>
|
||||
<form class="editor-form" data-ceremony-gift-form data-ceremony-admin-editor hidden novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field"><label for="ceremonyGiverName">赠送人姓名</label><input id="ceremonyGiverName" name="giverName" type="text" placeholder="可选" /></div>
|
||||
<div class="editor-field"><label for="ceremonyGiftAmount">礼金金额</label><input id="ceremonyGiftAmount" name="giftAmount" type="number" min="0" step="any" required /></div>
|
||||
<div class="editor-field"><label for="ceremonyGiftAmount">礼金金额 <span class="field-required" aria-hidden="true">*</span></label><input id="ceremonyGiftAmount" name="giftAmount" type="number" min="0" step="any" required /></div>
|
||||
</div>
|
||||
<div class="editor-field"><label for="ceremonyGiftMessage">祭品留言</label><textarea id="ceremonyGiftMessage" name="giftMessage" placeholder="可选"></textarea></div>
|
||||
<p class="upload-status" data-ceremony-gift-status aria-live="polite"></p>
|
||||
<div class="upload-status" data-ceremony-gift-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">添加祭品</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel" data-ceremony-admin-editor hidden>
|
||||
<h2>受邀名单</h2>
|
||||
<div class="module-list" data-ceremony-invitation-list aria-live="polite"><div class="api-empty">正在加载邀请记录…</div></div>
|
||||
<form class="editor-form" data-ceremony-invitee-form>
|
||||
<div class="module-list" data-ceremony-invitation-list aria-live="polite">
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载邀请记录…
|
||||
</div>
|
||||
</div>
|
||||
<form class="editor-form" data-ceremony-invitee-form data-ceremony-admin-editor hidden novalidate>
|
||||
<p>仅显示当前家谱中已绑定业务账号的正常成员;提交完整勾选名单,取消勾选会撤销尚未响应的邀请。</p>
|
||||
<div class="editor-choice-list" data-ceremony-invitee-options><div class="api-empty">正在加载成员选项…</div></div>
|
||||
<p class="upload-status" data-ceremony-invitee-status aria-live="polite"></p>
|
||||
<div class="editor-choice-list" data-ceremony-invitee-options>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载成员选项…
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-status" data-ceremony-invitee-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">更新受邀名单</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<div class="module-panel-head"><h2>活动列表</h2><a class="pill" href="profile-gift-edit.html" data-ceremony-create-link data-genealogy-context-link hidden>创建活动</a></div>
|
||||
<div class="module-list" data-ceremony-list aria-live="polite"><div class="api-empty">正在加载祭祀活动…</div></div>
|
||||
<div class="module-list" data-ceremony-list aria-live="polite">
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载祭祀活动…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,19 +99,19 @@
|
||||
>
|
||||
<a class="module-card tilt-card" href="profile-relative.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">亲</span>
|
||||
<h3>亲友往来</h3>
|
||||
<p>记录亲友关系、事项和礼金。</p></a
|
||||
<h3>亲友往来(贺礼簿)</h3>
|
||||
<p>记录亲友关系、往来事项和礼金,承接 APP 贺礼簿。</p></a
|
||||
>
|
||||
<a class="module-card tilt-card" href="profile-memo.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">备</span>
|
||||
<h3>备忘录</h3>
|
||||
<p>记录家族事务、提醒和完成状态。</p></a
|
||||
<h3>备忘录(家族恩人)</h3>
|
||||
<p>以标题、内容和提醒记录家族恩人等事项;当前 PC 不区分独立分类。</p></a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>使用说明</h2>
|
||||
<div class="api-empty">请先选择家谱,再进入对应模块查看和维护真实内容。</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请先选择家谱,再进入对应模块查看和维护真实内容。</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+29
-21
@@ -32,7 +32,7 @@
|
||||
<div>
|
||||
<h1>创建家谱</h1>
|
||||
<p>
|
||||
个人中心专用创建流程,用于录入姓氏、谱名、堂号、地望、祠堂和访问权限。
|
||||
填写当前 PC 服务支持的家谱信息;标有 * 的字段为必填,创建后可进入家谱工作区继续维护。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,16 +48,16 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>基础信息</h2>
|
||||
<p class="upload-status" data-genealogy-create-quota>正在读取家谱额度…</p>
|
||||
<form class="editor-form profile-create-family-form" data-genealogy-create-form>
|
||||
<p class="upload-status" data-genealogy-create-quota role="status" aria-live="polite">正在读取家谱额度…</p>
|
||||
<form class="editor-form profile-create-family-form" data-genealogy-create-form novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="familySurname">姓氏</label>
|
||||
<input id="familySurname" name="surname" type="text" required />
|
||||
<label for="familySurname">姓氏 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="familySurname" name="surname" type="text" required placeholder="例如:王" />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="familyName">谱名</label>
|
||||
<input id="familyName" name="genealogyName" type="text" required />
|
||||
<label for="familyName">谱名 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="familyName" name="genealogyName" type="text" required placeholder="例如:王氏家谱" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
@@ -70,20 +70,27 @@
|
||||
<input id="originPlace" name="originPlace" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label>家族所在地</label>
|
||||
<div class="editor-field" role="group" aria-labelledby="family-region-label" aria-describedby="family-create-status">
|
||||
<span id="family-region-label" class="editor-label">家族所在地 <span class="field-required" aria-hidden="true">*</span></span>
|
||||
<div class="profile-region-picker" data-region-picker>
|
||||
<select name="provinceCode" data-region-level="province">
|
||||
<option value="">请选择省</option>
|
||||
</select>
|
||||
<select name="cityCode" data-region-level="city">
|
||||
<option value="">请选择市</option>
|
||||
</select>
|
||||
<select name="districtCode" data-region-level="district">
|
||||
<option value="">请选择区县</option>
|
||||
</select>
|
||||
<label for="familyProvinceCode">省
|
||||
<select id="familyProvinceCode" name="provinceCode" data-region-level="province">
|
||||
<option value="">请选择省</option>
|
||||
</select>
|
||||
</label>
|
||||
<label for="familyCityCode">市
|
||||
<select id="familyCityCode" name="cityCode" data-region-level="city">
|
||||
<option value="">请选择市</option>
|
||||
</select>
|
||||
</label>
|
||||
<label for="familyDistrictCode">区县
|
||||
<select id="familyDistrictCode" name="districtCode" data-region-level="district">
|
||||
<option value="">请选择区县</option>
|
||||
</select>
|
||||
</label>
|
||||
<input type="hidden" name="regionCode" data-region-code />
|
||||
</div>
|
||||
<div class="upload-status" data-region-picker-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="addressDetail">详细地址</label>
|
||||
@@ -105,7 +112,7 @@
|
||||
data-upload-target="#familyCoverOssId"
|
||||
data-upload-status="#familyCoverStatus"
|
||||
/>
|
||||
<p id="familyCoverStatus" class="upload-status">未选择文件</p>
|
||||
<p id="familyCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
@@ -118,14 +125,15 @@
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="joinMode">加入方式</label>
|
||||
<select id="joinMode" name="joinMode">
|
||||
<select id="joinMode" name="joinMode" aria-describedby="family-invite-mode-note">
|
||||
<option value="1">审核加入</option>
|
||||
<option value="2">邀请码加入</option>
|
||||
<option value="0">关闭加入</option>
|
||||
</select>
|
||||
<p id="family-invite-mode-note" class="form-status">当前 PC 未提供邀请码生成、发送或管理接口;选择邀请模式后不能在本页面继续生成邀请码。</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="upload-status" data-genealogy-create-status aria-live="polite"></p>
|
||||
<div id="family-create-status" class="upload-status" data-genealogy-create-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit" data-genealogy-create-submit>确定创建</button>
|
||||
<a class="btn ghost magnetic" href="profile-families.html">返回列表</a>
|
||||
|
||||
@@ -50,19 +50,9 @@
|
||||
<section class="module-panel">
|
||||
<h2>待完善成员</h2>
|
||||
<div class="module-list">
|
||||
<a class="module-row" href="profile-data.html"
|
||||
><div>
|
||||
<h3>鹿野 Loyel</h3>
|
||||
<p>缺少出生日期、出生地址和微信信息</p>
|
||||
</div>
|
||||
<span class="pill">完善</span></a
|
||||
><a class="module-row" href="profile-data.html"
|
||||
><div>
|
||||
<h3>张三</h3>
|
||||
<p>缺少头像和亲属关系</p>
|
||||
</div>
|
||||
<span class="pill">编辑</span></a
|
||||
>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
|
||||
当前 PC 接口未提供资料缺项提醒列表,暂不能展示或处理成员提醒。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+48
-53
@@ -38,7 +38,7 @@
|
||||
<div>
|
||||
<h1>个人资料</h1>
|
||||
<p>
|
||||
管理个人资料、亲属关系和账号安全信息。
|
||||
管理个人资料和账号安全信息;家谱成员关系请在当前家谱中维护。
|
||||
</p>
|
||||
</div>
|
||||
<a class="btn primary magnetic" href="#profile-edit-form"
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>编辑资料</h2>
|
||||
<form class="editor-form profile-data-form" id="profile-edit-form" data-profile-form>
|
||||
<form class="editor-form profile-data-form" id="profile-edit-form" data-profile-form novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="profileNickName">昵称</label>
|
||||
@@ -105,7 +105,7 @@
|
||||
data-upload-biz-type="avatar"
|
||||
data-upload-usage-scene="profile_avatar"
|
||||
/>
|
||||
<p id="profileAvatarStatus" class="upload-status">请选择头像文件</p>
|
||||
<p id="profileAvatarStatus" class="upload-status" role="status" aria-live="polite">请选择头像文件</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
@@ -135,30 +135,35 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="bottom-actions profile-form-actions">
|
||||
<button class="btn primary magnetic" type="submit">保存资料</button>
|
||||
<span class="form-status" data-profile-status></span>
|
||||
<button class="btn primary magnetic" type="submit" data-profile-submit>保存资料</button>
|
||||
<div class="form-status" data-profile-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>基本资料</h2>
|
||||
<div data-profile-load-status><div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取账户资料…</div></div>
|
||||
<div class="form-like">
|
||||
<p>
|
||||
<span>头像</span><b>已上传</b
|
||||
<span>头像</span><b data-profile-text="avatarStatus">正在读取</b
|
||||
><a class="link-btn" href="profile-data.html">更换</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>姓名</span><b data-profile-text="displayName">鹿野 Loyel</b
|
||||
<span>昵称</span><b data-profile-text="displayName">正在读取</b
|
||||
><a class="link-btn" href="profile-data.html">编辑</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>绑定账号</span><b data-profile-text="phone">已绑定</b
|
||||
<span>真实姓名</span><b data-profile-text="realName">正在读取</b
|
||||
><a class="link-btn" href="#profile-edit-form">编辑</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>绑定账号</span><b data-profile-text="phone">正在读取</b
|
||||
><a class="link-btn" href="profile-security.html">安全设置</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>血亲关系</span><b>第二代 顺字辈</b
|
||||
><a class="link-btn" href="profile-family-admin.html"
|
||||
>查看世系</a
|
||||
<span>家谱关系</span><b>请在当前家谱中查看</b
|
||||
><a class="link-btn" href="profile-family-admin.html" data-genealogy-context-link
|
||||
>成员管理</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
@@ -167,78 +172,68 @@
|
||||
<h2>个人信息</h2>
|
||||
<div class="form-like">
|
||||
<p>
|
||||
<span>父亲</span><b>某某</b
|
||||
><a class="link-btn" href="profile-data.html">编辑</a>
|
||||
<span>性别</span><b data-profile-text="sex">正在读取</b
|
||||
><a class="link-btn" href="#profile-edit-form">编辑</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>出生日期</span><b data-profile-text="birthday">待选择</b
|
||||
><a class="link-btn" href="profile-data.html">选择</a>
|
||||
><a class="link-btn" href="#profile-edit-form">编辑</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>现居地址</span><b data-profile-text="regionText">待填写</b
|
||||
><a class="link-btn" href="profile-data.html">填写</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>微信 / QQ</span><b>待填写</b
|
||||
><a class="link-btn" href="profile-data.html">填写</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>学历 / 职业</span><b>待填写</b
|
||||
><a class="link-btn" href="profile-data.html">填写</a>
|
||||
<span>邮箱</span><b data-profile-text="email">正在读取</b
|
||||
><a class="link-btn" href="#profile-edit-form">编辑</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>现居地区</h2>
|
||||
<div class="editor-form region-profile-form">
|
||||
<h2 id="profile-region-heading">现居地区</h2>
|
||||
<div class="editor-form region-profile-form" role="group" aria-labelledby="profile-region-heading" aria-describedby="profile-region-contract-note profile-region-picker-status">
|
||||
<div class="region-picker profile-region-picker" data-region-picker>
|
||||
<select class="input" name="provinceCode" data-region-level="province">
|
||||
<option value="">请选择省</option>
|
||||
</select>
|
||||
<select class="input" name="cityCode" data-region-level="city">
|
||||
<option value="">请选择市</option>
|
||||
</select>
|
||||
<select class="input" name="districtCode" data-region-level="district">
|
||||
<option value="">请选择区县</option>
|
||||
</select>
|
||||
<label for="profileProvinceCode">省
|
||||
<select id="profileProvinceCode" class="input" name="provinceCode" data-region-level="province">
|
||||
<option value="">请选择省</option>
|
||||
</select>
|
||||
</label>
|
||||
<label for="profileCityCode">市
|
||||
<select id="profileCityCode" class="input" name="cityCode" data-region-level="city">
|
||||
<option value="">请选择市</option>
|
||||
</select>
|
||||
</label>
|
||||
<label for="profileDistrictCode">区县
|
||||
<select id="profileDistrictCode" class="input" name="districtCode" data-region-level="district">
|
||||
<option value="">请选择区县</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="button" disabled>暂不支持保存地区</button>
|
||||
<span class="form-status">当前 YAML 的 ProfileUpdateBody 没有地区字段</span>
|
||||
<span id="profile-region-contract-note" class="form-status">当前 YAML 的 ProfileUpdateBody 没有地区字段</span>
|
||||
</div>
|
||||
<div id="profile-region-picker-status" class="form-status" data-region-picker-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<form class="editor-form" data-region-search-form data-region-search-panel>
|
||||
<form class="editor-form" data-region-search-form data-region-search-panel novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="profileRegionKeyword">搜索地区</label>
|
||||
<input id="profileRegionKeyword" name="regionKeyword" type="search" placeholder="地区名称或编码" />
|
||||
<input id="profileRegionKeyword" name="regionKeyword" type="search" placeholder="地区名称或编码" aria-describedby="profile-region-search-status" required />
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn ghost magnetic" type="submit">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="profile-region-search-status" class="form-status" data-region-search-status role="status" aria-live="polite"></div>
|
||||
</form>
|
||||
<p class="api-status" data-region-search-path></p>
|
||||
<div class="module-list" data-region-search-results></div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>亲属</h2>
|
||||
<h2>亲属关系</h2>
|
||||
<div class="module-list">
|
||||
<div class="module-row">
|
||||
<div>
|
||||
<h3>张三</h3>
|
||||
<p>父亲 · 已绑定成员资料</p>
|
||||
</div>
|
||||
<span class="pill">查看</span>
|
||||
</div>
|
||||
<div class="module-row">
|
||||
<div>
|
||||
<h3>张三</h3>
|
||||
<p>父亲 · 未绑定账号</p>
|
||||
</div>
|
||||
<span class="pill">邀请激活</span>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
|
||||
当前 PC 资料接口未提供个人亲属关系列表;请在选定家谱的成员管理中查看和维护。
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-actions"><a class="btn ghost magnetic" href="profile-family-admin.html" data-genealogy-context-link>前往成员管理</a></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,11 +248,11 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/md5.js"></script>
|
||||
<script src="public/js/upload-pages.js"></script>
|
||||
<script src="public/js/region-pages.js"></script>
|
||||
<script src="public/js/profile-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-3
@@ -60,9 +60,11 @@
|
||||
<section class="module-panel">
|
||||
<h2>家谱入口</h2>
|
||||
<p>对应 PC 的“我的家谱”入口;进入具体业务前必须取得真实家谱编号。</p>
|
||||
<p class="api-status" data-genealogy-entry-status>正在读取我的家谱…</p>
|
||||
<div class="api-status" data-genealogy-entry-status role="status" aria-live="polite">正在读取家谱额度…</div>
|
||||
<div class="module-list" data-genealogy-list="mine">
|
||||
<div class="api-empty">正在加载列表…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在读取我的家谱…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
@@ -80,6 +82,11 @@
|
||||
<h3>加入家谱</h3>
|
||||
<p>搜索并选择可加入的家谱,提交关系和申请说明等待审核。</p></a
|
||||
>
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">序</span>
|
||||
<h3>家谱排序</h3>
|
||||
<p>当前 PC 家谱契约未提供自定义排序的读取或保存接口,暂不能调整家谱显示顺序。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -94,8 +101,8 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/genealogy-entry-pages.js"></script>
|
||||
<script src="public/js/profile-common.js?v=20260725-entry"></script>
|
||||
<script src="public/js/genealogy-entry-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-10
@@ -31,7 +31,7 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>家谱概览</h2>
|
||||
<p data-member-overview>正在加载家谱和当前权限…</p>
|
||||
<div data-member-overview><div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载家谱和当前权限…</div></div>
|
||||
<div class="module-grid">
|
||||
<a class="module-card tilt-card" href="profile-tree.html" data-feature-link="available" data-genealogy-context-link><span class="icon">世</span><h3>世系图</h3><p>查看或维护世系人物与亲属关系。</p></a>
|
||||
<a class="module-card tilt-card" href="profile-generation.html" data-feature-link="available" data-genealogy-context-link><span class="icon">辈</span><h3>字辈谱</h3><p>查看或维护家谱字辈。</p></a>
|
||||
@@ -43,11 +43,12 @@
|
||||
<section class="module-panel">
|
||||
<h2>正常成员</h2>
|
||||
<p>成员新增必须通过后端已有的加入或邀请流程;本页不伪造直接新增成员。</p>
|
||||
<div class="module-list" data-member-list aria-live="polite"><div class="api-empty">成员加载中…</div></div>
|
||||
<div class="form-status" role="status" aria-live="polite" data-member-action-status></div>
|
||||
<div class="module-list" data-member-list aria-live="polite"><div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">成员加载中…</div></div>
|
||||
</section>
|
||||
<section class="module-panel" data-member-management hidden>
|
||||
<h2 data-member-form-title>编辑成员</h2>
|
||||
<form class="editor-form" data-member-form hidden>
|
||||
<form class="editor-form" data-member-form novalidate hidden>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field"><label for="memberAdminName">成员名称</label><input id="memberAdminName" name="memberName" type="text" maxlength="50" /></div>
|
||||
<div class="editor-field"><label for="memberAdminRelation">与家谱关系</label><input id="memberAdminRelation" name="relationName" type="text" maxlength="100" /></div>
|
||||
@@ -56,18 +57,18 @@
|
||||
<div class="editor-field"><label for="memberAdminRole">成员角色</label><select id="memberAdminRole" name="roleType" data-member-role-options></select></div>
|
||||
<div class="editor-field"><label for="memberAdminLineage">绑定世系人物</label><select id="memberAdminLineage" name="lineagePersonId" data-member-lineage-options><option value="">不修改世系人物绑定</option></select></div>
|
||||
</div>
|
||||
<p class="upload-status">当前 PC 更新接口不提供解除世系人物绑定;留空表示不修改,不会发送空 ID。</p>
|
||||
<p class="upload-status" data-member-form-status aria-live="polite"></p>
|
||||
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">保存成员</button><button class="btn ghost" type="reset">取消</button></div>
|
||||
<p class="upload-status">当前 PC 更新接口未声明空值清除语义;成员名称、关系和世系人物绑定留空均表示不修改,不会发送空值。</p>
|
||||
<div class="upload-status" role="status" aria-live="polite" data-member-form-status></div>
|
||||
<div class="bottom-actions"><button class="btn primary magnetic" type="submit" data-member-submit>保存成员</button><button class="btn ghost" type="reset">取消</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel" data-owner-transfer-section hidden>
|
||||
<h2>转让谱主</h2>
|
||||
<form class="editor-form" data-owner-transfer-form>
|
||||
<div class="editor-field"><label for="ownerTransferTarget">新谱主</label><select id="ownerTransferTarget" name="targetMemberId" data-owner-transfer-target><option value="">请选择新谱主</option></select></div>
|
||||
<form class="editor-form" data-owner-transfer-form novalidate>
|
||||
<div class="editor-field"><label for="ownerTransferTarget">新谱主 <span class="field-required" aria-hidden="true">*</span></label><select id="ownerTransferTarget" name="targetMemberId" data-owner-transfer-target required><option value="">请选择新谱主</option></select></div>
|
||||
<p>只能选择当前正常成员。转让后当前账号变为管理员,新谱主受其账号家谱拥有额度约束。</p>
|
||||
<p class="upload-status" data-owner-transfer-status aria-live="polite"></p>
|
||||
<div class="bottom-actions"><button class="btn ghost" type="submit">确认转让谱主</button></div>
|
||||
<div class="upload-status" role="status" aria-live="polite" data-owner-transfer-status></div>
|
||||
<div class="bottom-actions"><button class="btn ghost" type="submit" data-owner-transfer-submit>确认转让谱主</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel" data-member-leave-section hidden>
|
||||
|
||||
@@ -61,9 +61,9 @@
|
||||
<h2>家谱概览</h2>
|
||||
<div class="module-grid">
|
||||
<div class="module-card" data-family-home-overview>
|
||||
<span class="icon">谱</span>
|
||||
<h3>家谱信息</h3>
|
||||
<p>家谱概览加载中...</p>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载家谱概览…
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<span class="icon">员</span>
|
||||
@@ -75,23 +75,27 @@
|
||||
<h3>世系人物</h3>
|
||||
<p><strong data-family-home-person-count>--</strong> 人</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">邀</span>
|
||||
<h3>邀请家人</h3>
|
||||
<p>PC 暂未开放邀请,请等待后端提供正式接口。</p>
|
||||
<span class="pill">暂不可操作</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="api-status" data-family-home-status>正在读取家谱概览</p>
|
||||
<div class="api-status" data-family-home-status role="status" aria-live="polite">正在读取家谱概览</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>世系预览</h2>
|
||||
<!-- 家谱主页只展示世系树预览,完整维护进入世系图页面 -->
|
||||
<div class="lineage-tree-wrap is-compact" data-lineage-home-tree>
|
||||
<div class="api-empty">世系树加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载世系树…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>常用入口</h2>
|
||||
<h2>家族文化</h2>
|
||||
<p>围绕当前家谱集中维护谱文、影像与家族资料。</p>
|
||||
<div class="module-grid">
|
||||
<a class="module-card tilt-card" href="profile-article.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">文</span>
|
||||
@@ -105,14 +109,76 @@
|
||||
><span class="icon">影</span>
|
||||
<h3>视频</h3>
|
||||
<p>发布和编辑家族视频。</p></a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>家族记录与协作</h2>
|
||||
<p>以列表、详情与编辑分工承接 APP 的动态、日志、人情簿、备忘和活动能力。</p>
|
||||
<div class="module-grid">
|
||||
<a class="module-card tilt-card" href="profile-feed.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">动</span>
|
||||
<h3>家族圈</h3>
|
||||
<p>发布图文动态、查看评论和家族互动。</p></a
|
||||
><a class="module-card tilt-card" href="profile-growth.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">志</span>
|
||||
<h3>成长日志</h3>
|
||||
<p>记录成员成长节点、日期和附件。</p></a
|
||||
><a class="module-card tilt-card" href="profile-relative.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">亲</span>
|
||||
<h3>亲友往来(贺礼簿)</h3>
|
||||
<p>记录亲友关系、往来事项和礼金,承接 APP 贺礼簿。</p></a
|
||||
><a class="module-card tilt-card" href="profile-memo.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">备</span>
|
||||
<h3>备忘录(家族恩人)</h3>
|
||||
<p>以标题、内容和提醒记录家族恩人等事项;当前 PC 不区分独立分类。</p></a
|
||||
><a class="module-card tilt-card" href="profile-merit.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">德</span>
|
||||
<h3>功德录</h3>
|
||||
<p>记录功德人和事迹。</p></a
|
||||
<p>记录功德人、事迹和相关金额。</p></a
|
||||
><a class="module-card tilt-card" href="profile-ceremony.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">祭</span>
|
||||
<h3>祭祀活动</h3>
|
||||
<p>维护活动、祭品和受邀名单。</p></a
|
||||
><a class="module-card tilt-card" href="profile-gift.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">礼</span>
|
||||
<h3>贺礼邀请</h3>
|
||||
<p>查看和响应当前家谱的活动邀请。</p></a
|
||||
>
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">证</span>
|
||||
<h3>重要证件</h3>
|
||||
<p>当前 PC 接口未提供家谱证件的列表、上传或维护能力,暂不能查看或保存。</p>
|
||||
<span class="pill">暂不可操作</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>家谱维护</h2>
|
||||
<p>世系、字辈和成员维护均与当前家谱绑定;管理权限始终以服务端结果为准。</p>
|
||||
<div class="module-grid">
|
||||
<a class="module-card tilt-card" href="profile-tree.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">世</span>
|
||||
<h3>世系图</h3>
|
||||
<p>查看世系结构并维护人物与关系。</p></a
|
||||
><a class="module-card tilt-card" href="profile-generation.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">辈</span>
|
||||
<h3>字辈谱</h3>
|
||||
<p>查询和维护家谱字辈。</p></a
|
||||
><a class="module-card tilt-card" href="profile-family-admin.html" data-feature-link="available" data-family-home-management data-genealogy-context-link hidden
|
||||
><span class="icon">管</span>
|
||||
<h3>家族管理</h3>
|
||||
<p>管理成员、入谱审核和谱主转移。</p></a
|
||||
><a class="module-card tilt-card" href="profile-services.html" data-feature-link="available" data-genealogy-context-link
|
||||
><span class="icon">会</span>
|
||||
<h3>会员与订单</h3>
|
||||
<p>关联当前家谱创建会员订单,并查看已有订单。</p></a
|
||||
><div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">删</span>
|
||||
<h3>删除家谱</h3>
|
||||
<p>当前 PC 家谱契约未提供删除接口,暂不能查看或执行删除操作。</p>
|
||||
<span class="pill">暂不可操作</span>
|
||||
</div>
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -45,12 +45,34 @@
|
||||
</aside>
|
||||
<div class="module-main">
|
||||
<section class="module-panel" data-feed-detail>
|
||||
<div class="api-empty">动态详情加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载动态详情…
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>发表评论</h2>
|
||||
<form class="editor-form" data-feed-comment-form novalidate>
|
||||
<input name="parentCommentId" type="hidden" />
|
||||
<p class="module-meta" data-feed-comment-form-target>发表公开评论</p>
|
||||
<div class="editor-field">
|
||||
<label for="feedCommentContent">评论内容 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<textarea id="feedCommentContent" name="commentContent" rows="3" maxlength="1000" required disabled placeholder="写下想说的话"></textarea>
|
||||
</div>
|
||||
<div class="form-status" role="status" aria-live="polite" data-feed-comment-form-status>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取动态评论权限…</div>
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit" disabled>发表评论</button>
|
||||
<button class="btn ghost" type="button" data-feed-comment-cancel disabled>取消回复</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>评论与回复</h2>
|
||||
<div data-feed-detail-comments>
|
||||
<div class="api-empty">评论加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载评论…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-feed.html" data-feed-list-link data-genealogy-context-link>返回动态</a
|
||||
><button class="btn primary magnetic" type="submit" form="feed-edit-form">发布</button>
|
||||
><button class="btn primary magnetic" type="submit" form="feed-edit-form" data-feed-editor-action hidden disabled>发布</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>动态内容</h2>
|
||||
<form id="feed-edit-form" class="editor-form layui-form" data-feed-form>
|
||||
<form id="feed-edit-form" class="editor-form layui-form" data-feed-form novalidate>
|
||||
<input name="feedType" type="hidden" value="text" />
|
||||
<div class="editor-field">
|
||||
<label for="feedContent">内容</label
|
||||
<label for="feedContent">内容 <span class="field-required" aria-hidden="true">*</span></label
|
||||
><textarea
|
||||
id="feedContent"
|
||||
name="feedContent"
|
||||
@@ -65,7 +65,7 @@
|
||||
<input id="feedMedia" name="mediaOssIds" type="hidden" />
|
||||
<label class="upload-control" for="feedMediaFile">选择动态图片</label>
|
||||
<input id="feedMediaFile" class="upload-input" type="file" accept="image/*" multiple data-upload-multiple="true" data-upload-target="#feedMedia" data-upload-status="#feedMediaStatus" />
|
||||
<p id="feedMediaStatus" class="upload-status">未选择图片</p>
|
||||
<p id="feedMediaStatus" class="upload-status" role="status" aria-live="polite">未选择图片</p>
|
||||
</div>
|
||||
<div class="editor-two" data-feed-advanced hidden>
|
||||
<div class="editor-field">
|
||||
@@ -80,8 +80,9 @@
|
||||
<input id="feedSortOrder" name="sortOrder" type="number" min="0" value="0" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-status" role="status" aria-live="polite" data-feed-form-status></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">发布</button
|
||||
<button class="btn primary magnetic" type="submit" data-feed-editor-action hidden disabled>发布</button
|
||||
><a class="btn ghost magnetic" href="profile-feed.html" data-feed-list-link data-genealogy-context-link
|
||||
>取消</a
|
||||
>
|
||||
|
||||
+6
-6
@@ -21,7 +21,7 @@
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-family-home.html">返回家谱主页</a
|
||||
><a class="btn primary magnetic" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link
|
||||
><a class="btn primary magnetic" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden
|
||||
>发布</a
|
||||
>
|
||||
</div>
|
||||
@@ -53,21 +53,21 @@
|
||||
<div class="form-like">
|
||||
<p>
|
||||
<span>内容</span><b>在发布页填写</b
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link>编辑</a>
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden>编辑</a>
|
||||
</p>
|
||||
<p>
|
||||
<span>图片</span><b>可直接选择并上传多张图片</b
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link>添加</a
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden>添加</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span>动态类型</span><b>图文动态</b
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link>编辑</a
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden>编辑</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span>发布状态</span><b>正常或停用</b
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link>设置</a
|
||||
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden>设置</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@
|
||||
<h2>最近动态</h2>
|
||||
<!-- 动态列表由 /feeds/page 接口渲染,点赞和评论走对应子接口 -->
|
||||
<div class="module-list" data-feed-list>
|
||||
<div class="api-empty">动态加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载家族动态…</div>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<button class="pill" type="button" data-feed-page-prev disabled>上一页</button>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>提交反馈</h2>
|
||||
<form class="editor-form" id="profileFeedbackForm" data-feedback-form>
|
||||
<form class="editor-form" id="profileFeedbackForm" data-feedback-form novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="feedbackType">反馈类型</label>
|
||||
@@ -63,10 +63,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="feedbackContent">反馈描述</label>
|
||||
<label for="feedbackContent">反馈描述 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<textarea id="feedbackContent" name="feedbackContent" required placeholder="请描述问题、页面位置和期望结果"></textarea>
|
||||
</div>
|
||||
<p class="form-note" aria-live="polite" data-feedback-status></p>
|
||||
<div class="form-status" role="status" aria-live="polite" data-feedback-status></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit" data-feedback-submit>提交反馈</button>
|
||||
</div>
|
||||
@@ -77,7 +77,9 @@
|
||||
<h2>历史反馈</h2>
|
||||
<button class="btn ghost" type="button" data-feedback-refresh>刷新</button>
|
||||
</div>
|
||||
<div class="module-list" data-feedback-list></div>
|
||||
<div class="module-list" data-feedback-list>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取反馈记录…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,8 +93,8 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/feedback-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/feedback-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+35
-5
@@ -55,9 +55,9 @@
|
||||
<section class="module-panel generation-batch-panel" data-generation-management hidden>
|
||||
<h2>批量字辈</h2>
|
||||
<!-- 批量字辈表单:字段对应 Apifox 的 GenerationPoemBatchBody。 -->
|
||||
<form class="editor-form generation-batch-form" data-generation-batch-form>
|
||||
<form class="editor-form generation-batch-form" data-generation-batch-form novalidate>
|
||||
<div class="editor-field">
|
||||
<label for="generationBatchContent">字辈内容</label>
|
||||
<label for="generationBatchContent">字辈内容 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<textarea
|
||||
id="generationBatchContent"
|
||||
name="poemText"
|
||||
@@ -84,17 +84,47 @@
|
||||
type="button"
|
||||
data-generation-batch-action="save"
|
||||
>批量保存</button>
|
||||
<span class="form-status" data-generation-batch-status></span>
|
||||
<span class="form-status" role="status" aria-live="polite" data-generation-batch-status></span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="module-list generation-batch-preview" data-generation-batch-preview>
|
||||
<div class="api-empty">批量预览结果将在这里显示</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">批量预览结果将在这里显示。</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="generation-editor" class="module-panel" data-generation-editor hidden>
|
||||
<h2 data-generation-editor-title>新增字辈</h2>
|
||||
<form class="editor-form" data-generation-form novalidate>
|
||||
<input name="poemId" type="hidden" />
|
||||
<input name="status" type="hidden" />
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="generationNo">世代序号 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="generationNo" name="generationNo" type="number" min="1" max="2147483647" step="1" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="generationText">字辈文字 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="generationText" name="generationText" type="text" maxlength="50" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="generationDescription">说明</label>
|
||||
<textarea id="generationDescription" name="description" rows="3" maxlength="500" placeholder="可选,最多 500 个字符"></textarea>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="generationSortOrder">排序值</label>
|
||||
<input id="generationSortOrder" name="sortOrder" type="number" min="-2147483648" max="2147483647" step="1" placeholder="可留空,越小越靠前" />
|
||||
</div>
|
||||
<div class="form-status" role="status" aria-live="polite" data-generation-form-status></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit" data-generation-editor-submit>保存字辈</button>
|
||||
<button class="btn ghost" type="button" data-generation-editor-cancel>取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>字辈列表</h2>
|
||||
<div class="module-list" data-generation-list>
|
||||
<div class="api-empty">正在加载字辈记录…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载字辈记录…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+15
-15
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>创建贺礼 - 个人中心 - 代代相传</title>
|
||||
<title>创建祭祀活动 - 个人中心 - 代代相传</title>
|
||||
<link rel="stylesheet" href="public/css/public.css" />
|
||||
<link rel="stylesheet" href="public/layui/css/layui.css" />
|
||||
<link rel="stylesheet" href="public/css/profile-module.css" />
|
||||
@@ -17,18 +17,18 @@
|
||||
>
|
||||
<nav class="nav-links">
|
||||
<a href="profile-content.html">内容发布</a
|
||||
><a class="active" href="profile-gift.html">贺礼邀请</a>
|
||||
><a class="active" href="profile-ceremony.html">祭祀活动</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-gift.html">返回贺礼</a
|
||||
><button class="btn primary magnetic" type="submit" form="ceremony-edit-form">确定创建</button>
|
||||
<a href="profile-ceremony.html">返回活动列表</a
|
||||
><button class="btn primary magnetic" type="submit" form="ceremony-edit-form" data-ceremony-editor-action hidden>确定创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="module-hero">
|
||||
<div class="container">
|
||||
<div class="module-kicker">Gift Editor</div>
|
||||
<div class="module-kicker">Ceremony Editor</div>
|
||||
<div class="module-title-row">
|
||||
<div>
|
||||
<h1 data-ceremony-editor-title>创建祭祀活动</h1>
|
||||
@@ -42,17 +42,17 @@
|
||||
<aside class="module-nav">
|
||||
<a href="profile-content.html">内容发布</a
|
||||
><a href="profile-merit.html">功德录</a
|
||||
><a class="active" href="profile-gift.html">贺礼邀请</a
|
||||
><a class="active" href="profile-ceremony.html">祭祀活动</a
|
||||
><a href="profile-feed.html">发布动态</a>
|
||||
</aside>
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>活动信息</h2>
|
||||
<div class="api-empty" data-ceremony-editor-placeholder>正在加载活动编辑信息…</div>
|
||||
<form id="ceremony-edit-form" class="editor-form layui-form" data-ceremony-form data-ceremony-editor hidden>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" data-ceremony-editor-placeholder role="status" aria-live="polite" aria-busy="true">正在加载活动编辑信息…</div>
|
||||
<form id="ceremony-edit-form" class="editor-form layui-form" data-ceremony-form data-ceremony-editor hidden novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="giftType">活动类型</label
|
||||
<label for="giftType">活动类型 <span class="field-required" aria-hidden="true">*</span></label
|
||||
><input id="giftType" name="ceremonyType" type="text" placeholder="请输入后端约定的活动类型" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="giftTitle">贺礼标题</label
|
||||
<label for="giftTitle">活动标题 <span class="field-required" aria-hidden="true">*</span></label
|
||||
><input id="giftTitle" name="ceremonyTitle" type="text" placeholder="请输入标题" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -93,11 +93,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="giftContent">贺礼内容</label
|
||||
<label for="giftContent">活动说明</label
|
||||
><textarea class="js-rich-editor"
|
||||
id="giftContent"
|
||||
name="ceremonyDesc"
|
||||
placeholder="请输入贺礼内容"
|
||||
placeholder="请输入活动说明"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -105,17 +105,17 @@
|
||||
<input id="giftCoverOssId" name="coverOssId" type="hidden" />
|
||||
<label class="upload-control" for="giftCoverFile">选择封面图片</label>
|
||||
<input id="giftCoverFile" class="upload-input" type="file" accept="image/*" data-upload-target="#giftCoverOssId" data-upload-status="#giftCoverStatus" />
|
||||
<p id="giftCoverStatus" class="upload-status">未选择文件</p>
|
||||
<p id="giftCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="giftSortOrder">排序值</label
|
||||
><input id="giftSortOrder" name="sortOrder" type="number" step="1" placeholder="可选,数值越小越靠前" />
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<p class="upload-status" data-ceremony-form-status aria-live="polite"></p>
|
||||
<div class="upload-status" data-ceremony-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">确定创建</button
|
||||
><a class="btn ghost magnetic" href="profile-gift.html"
|
||||
><a class="btn ghost magnetic" href="profile-ceremony.html"
|
||||
>取消</a
|
||||
>
|
||||
</div>
|
||||
|
||||
+7
-2
@@ -49,8 +49,11 @@
|
||||
<section class="module-panel">
|
||||
<h2>我的邀请</h2>
|
||||
<div class="module-list" data-my-invitation-list aria-live="polite">
|
||||
<div class="api-empty">正在加载我的邀请…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载我的邀请…
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-status" data-my-invitation-status role="status" aria-live="polite"></div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<div class="module-panel-head">
|
||||
@@ -61,7 +64,9 @@
|
||||
<section class="module-panel">
|
||||
<h2>邀请详情</h2>
|
||||
<div data-my-invitation-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一条邀请查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
|
||||
请选择一条邀请查看详情
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>记录内容</h2>
|
||||
<form id="growth-edit-form" class="editor-form" data-growth-form data-growth-editor hidden>
|
||||
<form id="growth-edit-form" class="editor-form" data-growth-form data-growth-editor hidden novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="growthPersonId">关联世系人物</label
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="growthTitle">标题</label>
|
||||
<label for="growthTitle">标题<span class="field-required" aria-hidden="true"> *</span></label>
|
||||
<input id="growthTitle" name="recordTitle" type="text" placeholder="请输入成长记录标题" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -99,7 +99,7 @@
|
||||
<input id="growthMediaOssIds" name="mediaOssIds" type="hidden" />
|
||||
<label class="upload-control" for="growthMediaFiles">选择附件</label>
|
||||
<input id="growthMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-upload-target="#growthMediaOssIds" data-upload-status="#growthMediaStatus" data-growth-media-input />
|
||||
<p id="growthMediaStatus" class="upload-status" data-growth-media-status>未选择文件</p>
|
||||
<p id="growthMediaStatus" class="upload-status" data-growth-media-status role="status" aria-live="polite">未选择文件</p>
|
||||
<button class="pill" type="button" data-growth-clear-media>清除附件</button>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -108,15 +108,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<input id="growthStatus" name="status" type="hidden" value="0" />
|
||||
<div class="form-status" data-growth-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">保存</button
|
||||
><a class="btn ghost magnetic" href="profile-growth.html" data-genealogy-context-link
|
||||
>取消</a
|
||||
>
|
||||
<span class="form-status" data-growth-form-status></span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="api-empty" data-growth-editor-placeholder>正在加载成长记录编辑信息…</div>
|
||||
<div data-growth-editor-placeholder>
|
||||
<div class="api-state api-state--loading" role="status" aria-live="polite" aria-busy="true">正在加载成长记录编辑信息…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-content.html">返回发布中心</a
|
||||
><a class="btn primary magnetic" href="profile-growth-edit.html" data-genealogy-context-link
|
||||
><a class="btn primary magnetic" href="profile-growth-edit.html" data-genealogy-context-link data-growth-create-link hidden
|
||||
>新建记录</a
|
||||
>
|
||||
</div>
|
||||
@@ -50,13 +50,13 @@
|
||||
<section class="module-panel">
|
||||
<h2>记录列表</h2>
|
||||
<div class="module-list" data-growth-list aria-live="polite">
|
||||
<div class="api-empty">正在加载成长记录…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载成长记录…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>记录详情</h2>
|
||||
<div data-growth-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一条成长记录查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条成长记录查看详情。</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@
|
||||
<h1>邀请家人</h1>
|
||||
<p>生成家谱邀请链接或邀请码,引导亲人加入家谱并激活账号。</p>
|
||||
</div>
|
||||
<a class="btn primary magnetic" href="join-genealogy.html">打开加入页</a>
|
||||
<a class="btn ghost magnetic" href="profile-family-admin.html" data-feature-link="available" data-genealogy-context-link>返回家族管理</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -50,12 +50,12 @@
|
||||
<div class="module-card">
|
||||
<span class="icon">码</span>
|
||||
<h3>邀请码</h3>
|
||||
<p>当前邀请码:<b data-invite-code>加载中...</b></p>
|
||||
<p>当前 PC 接口未提供邀请码,暂不能生成或展示。</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<span class="icon">链</span>
|
||||
<h3>邀请链接</h3>
|
||||
<p data-invite-link>邀请链接加载中...</p>
|
||||
<p>当前 PC 接口未提供邀请链接,暂不能生成或复制。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<h2>我的加入申请</h2>
|
||||
<button class="btn ghost" type="button" data-join-apply-refresh>刷新</button>
|
||||
</div>
|
||||
<p class="form-note" aria-live="polite" data-my-join-applies-status></p>
|
||||
<div class="form-note" role="status" aria-live="polite" data-my-join-applies-status></div>
|
||||
<div class="module-list" data-join-apply-list="mine"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<a href="profile-messages.html">消息中心</a
|
||||
><a class="active" href="profile-join-review.html" data-genealogy-context-link>入谱审核</a
|
||||
><span class="module-nav-unavailable">邀请家人<small>即将开放</small></span
|
||||
><span class="module-nav-unavailable">管理员权限<small>即将开放</small></span>
|
||||
><a href="profile-admin-permissions.html" data-genealogy-context-link>管理员权限</a>
|
||||
</aside>
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
@@ -50,8 +50,28 @@
|
||||
<h2>待审核申请</h2>
|
||||
<button class="btn ghost" type="button" data-join-review-refresh>刷新</button>
|
||||
</div>
|
||||
<p class="form-note" aria-live="polite" data-join-review-status></p>
|
||||
<div class="module-list" data-join-apply-list="pending"></div>
|
||||
<div class="form-note" role="status" aria-live="polite" data-join-review-status></div>
|
||||
<div class="module-list" data-join-apply-list="pending">
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在读取待审核申请…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="join-review-form" class="module-panel" data-join-review-editor hidden>
|
||||
<h2 data-join-review-form-title>审核加入申请</h2>
|
||||
<form class="editor-form" data-join-review-form novalidate>
|
||||
<input name="applyId" type="hidden" />
|
||||
<input name="status" type="hidden" />
|
||||
<div class="editor-field">
|
||||
<label for="joinAuditRemark">审核说明</label>
|
||||
<textarea id="joinAuditRemark" name="auditRemark" rows="4" maxlength="500" disabled placeholder="可选;拒绝时可说明原因,最多 500 个字符"></textarea>
|
||||
</div>
|
||||
<div class="form-status" role="status" aria-live="polite" data-join-review-form-status></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit" data-join-review-submit disabled>提交审核</button>
|
||||
<button class="btn ghost" type="button" data-join-review-cancel disabled>取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,21 +26,22 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>备忘内容</h2>
|
||||
<form id="memo-edit-form" class="editor-form" data-memo-form data-memo-editor hidden>
|
||||
<div class="editor-field"><label for="memoTitle">备忘标题</label><input id="memoTitle" name="memoTitle" type="text" required /></div>
|
||||
<form id="memo-edit-form" class="editor-form" data-memo-form data-memo-editor hidden novalidate>
|
||||
<div class="editor-field"><label for="memoTitle">备忘标题<span class="field-required" aria-hidden="true"> *</span></label><input id="memoTitle" name="memoTitle" type="text" required /></div>
|
||||
<div class="editor-field"><label for="memoContent">备忘内容</label><textarea id="memoContent" name="memoContent" placeholder="可选"></textarea></div>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field"><label for="memoRemindTime">提醒时间</label><input id="memoRemindTime" name="remindTime" type="datetime-local" /></div>
|
||||
<div class="editor-field"><label for="memoCompleted">完成状态</label><select id="memoCompleted" name="completed"><option value="0">未完成</option><option value="1">已完成</option></select></div>
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field"><label for="memoMediaFiles">附件文件</label><input id="memoMediaOssIds" name="mediaOssIds" type="hidden" /><label class="upload-control" for="memoMediaFiles">选择附件</label><input id="memoMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-upload-target="#memoMediaOssIds" data-upload-status="#memoMediaStatus" data-memo-media-input /><p id="memoMediaStatus" class="upload-status" data-memo-media-status>未选择文件</p><button class="pill" type="button" data-memo-clear-media>清除附件</button></div>
|
||||
<div class="editor-field"><label for="memoMediaFiles">附件文件</label><input id="memoMediaOssIds" name="mediaOssIds" type="hidden" /><label class="upload-control" for="memoMediaFiles">选择附件</label><input id="memoMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-upload-target="#memoMediaOssIds" data-upload-status="#memoMediaStatus" data-memo-media-input /><p id="memoMediaStatus" class="upload-status" data-memo-media-status role="status" aria-live="polite">未选择文件</p><button class="pill" type="button" data-memo-clear-media>清除附件</button></div>
|
||||
<div class="editor-field"><label for="memoSortOrder">排序值</label><input id="memoSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" /></div>
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">保存</button><a class="btn ghost magnetic" href="profile-memo.html" data-genealogy-context-link>取消</a><span class="form-status" data-memo-form-status></span></div>
|
||||
<div class="form-status" data-memo-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions"><button class="btn primary magnetic" type="submit">保存</button><a class="btn ghost magnetic" href="profile-memo.html" data-genealogy-context-link>取消</a></div>
|
||||
</form>
|
||||
<div class="api-empty" data-memo-editor-placeholder>正在加载备忘录编辑信息…</div>
|
||||
<div data-memo-editor-placeholder><div class="api-state api-state--loading" role="status" aria-live="polite" aria-busy="true">正在加载备忘录编辑信息…</div></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@
|
||||
<div class="container nav">
|
||||
<a class="brand magnetic" href="profile.html"><img class="brand-logo brand-logo-mark" src="public/images/logo-mark.png" alt="" /><span class="brand-text">个人中心</span></a>
|
||||
<nav class="nav-links"><a href="profile-content.html">内容发布</a><a class="active" href="profile-memo.html" data-genealogy-context-link>备忘录</a></nav>
|
||||
<div class="nav-actions"><a href="profile-content.html">返回发布中心</a><a class="btn primary magnetic" href="profile-memo-edit.html" data-genealogy-context-link>新建备忘</a></div>
|
||||
<div class="nav-actions"><a href="profile-content.html">返回发布中心</a><a class="btn primary magnetic" href="profile-memo-edit.html" data-genealogy-context-link data-memo-create-link hidden>新建备忘</a></div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
@@ -30,13 +30,13 @@
|
||||
<section class="module-panel">
|
||||
<h2>备忘列表</h2>
|
||||
<div class="module-list" data-memo-list aria-live="polite">
|
||||
<div class="api-empty">正在加载备忘录…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载备忘录…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>备忘详情</h2>
|
||||
<div data-memo-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一条备忘录查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条备忘录查看详情。</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>功德资料</h2>
|
||||
<form id="merit-edit-form" class="editor-form" data-merit-form data-merit-editor hidden>
|
||||
<form id="merit-edit-form" class="editor-form" data-merit-form data-merit-editor hidden novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="meritName">功德人姓名</label>
|
||||
<label for="meritName">功德人姓名<span class="field-required" aria-hidden="true"> *</span></label>
|
||||
<input id="meritName" name="donorName" type="text" placeholder="请输入功德人姓名" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="meritTitle">功德标题</label>
|
||||
<label for="meritTitle">功德标题<span class="field-required" aria-hidden="true"> *</span></label>
|
||||
<input id="meritTitle" name="meritTitle" type="text" placeholder="请输入功德标题" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -88,13 +88,15 @@
|
||||
<input id="meritSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" />
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<div class="form-status" data-merit-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">保存</button>
|
||||
<a class="btn ghost magnetic" href="profile-merit.html" data-genealogy-context-link>取消</a>
|
||||
<span class="form-status" data-merit-form-status></span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="api-empty" data-merit-editor-placeholder>正在加载功德记录编辑信息…</div>
|
||||
<div data-merit-editor-placeholder>
|
||||
<div class="api-state api-state--loading" role="status" aria-live="polite" aria-busy="true">正在加载功德记录编辑信息…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-content.html">返回发布中心</a>
|
||||
<a class="btn primary magnetic" href="profile-merit-edit.html" data-genealogy-context-link>新增功德记录</a>
|
||||
<a class="btn primary magnetic" href="profile-merit-edit.html" data-genealogy-context-link data-merit-create-link hidden>新增功德记录</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -50,13 +50,13 @@
|
||||
<section class="module-panel">
|
||||
<h2>功德记录</h2>
|
||||
<div class="module-list" data-merit-list aria-live="polite">
|
||||
<div class="api-empty">正在加载功德记录…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载功德记录…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>记录详情</h2>
|
||||
<div data-merit-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一条功德记录查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条功德记录查看详情。</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -69,15 +69,15 @@
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p class="api-empty" data-notification-status>正在加载消息…</p>
|
||||
<div class="form-status" data-notification-status role="status" aria-live="polite"></div>
|
||||
<div class="module-list" data-notification-list aria-live="polite">
|
||||
<div class="api-empty">正在加载消息…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载消息…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>通知详情</h2>
|
||||
<div data-notification-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一条通知查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条通知查看详情。</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>记录内容</h2>
|
||||
<form id="relative-edit-form" class="editor-form" data-relative-form data-relative-editor hidden>
|
||||
<form id="relative-edit-form" class="editor-form" data-relative-form data-relative-editor hidden novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="relativeName">亲友姓名</label>
|
||||
<label for="relativeName">亲友姓名<span class="field-required" aria-hidden="true"> *</span></label>
|
||||
<input id="relativeName" name="relativeName" type="text" placeholder="请输入亲友姓名" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -97,17 +97,19 @@
|
||||
data-upload-status="#relativeMediaStatus"
|
||||
data-relative-media-input
|
||||
/>
|
||||
<p id="relativeMediaStatus" class="upload-status" data-relative-media-status>未选择文件</p>
|
||||
<p id="relativeMediaStatus" class="upload-status" data-relative-media-status role="status" aria-live="polite">未选择文件</p>
|
||||
<button class="pill" type="button" data-relative-clear-media>清除附件</button>
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<div class="form-status" data-relative-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">保存</button>
|
||||
<a class="btn ghost magnetic" href="profile-relative.html" data-genealogy-context-link>取消</a>
|
||||
<span class="form-status" data-relative-form-status></span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="api-empty" data-relative-editor-placeholder>正在加载亲友记录编辑信息…</div>
|
||||
<div data-relative-editor-placeholder>
|
||||
<div class="api-state api-state--loading" role="status" aria-live="polite" aria-busy="true">正在加载亲友记录编辑信息…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-content.html">返回发布中心</a>
|
||||
<a class="btn primary magnetic" href="profile-relative-edit.html" data-genealogy-context-link>新建记录</a>
|
||||
<a class="btn primary magnetic" href="profile-relative-edit.html" data-genealogy-context-link data-relative-create-link hidden>新建记录</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -49,13 +49,13 @@
|
||||
<section class="module-panel">
|
||||
<h2>记录列表</h2>
|
||||
<div class="module-list" data-relative-list aria-live="polite">
|
||||
<div class="api-empty">正在加载亲友记录…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在加载亲友记录…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>记录详情</h2>
|
||||
<div data-relative-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一条亲友记录查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条亲友记录查看详情。</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+19
-19
@@ -31,7 +31,7 @@
|
||||
<div class="module-title-row">
|
||||
<div>
|
||||
<h1>安全设置</h1>
|
||||
<p>维护登录密码、手机号、微信快捷登录和账号安全提醒。</p>
|
||||
<p>维护登录密码、手机号与账号注销安全操作。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,34 +48,34 @@
|
||||
<section class="module-panel">
|
||||
<h2>账号安全</h2>
|
||||
<!-- 修改密码表单:字段对应 PasswordChangeBody。 -->
|
||||
<form class="editor-form security-form" data-security-form="password">
|
||||
<form class="editor-form security-form" data-security-form="password" novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="oldPassword">当前密码</label>
|
||||
<label for="oldPassword">当前密码 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="oldPassword" name="oldPassword" type="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="newPassword">新密码</label>
|
||||
<label for="newPassword">新密码 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="newPassword" name="newPassword" type="password" autocomplete="new-password" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="confirmPassword">确认新密码</label>
|
||||
<label for="confirmPassword">确认新密码 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="confirmPassword" name="confirmPassword" type="password" autocomplete="new-password" required />
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">修改密码</button>
|
||||
<span class="api-status" data-security-status>等待提交</span>
|
||||
<button class="btn primary magnetic" type="submit" data-security-submit>修改密码</button>
|
||||
<div class="form-status" data-security-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>换绑手机号</h2>
|
||||
<!-- 换绑手机号表单:字段对应 PhoneChangeBody。 -->
|
||||
<form class="editor-form security-form" data-security-form="phone">
|
||||
<form class="editor-form security-form" data-security-form="phone" novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="newPhone">新手机号</label>
|
||||
<label for="newPhone">新手机号 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input
|
||||
id="newPhone"
|
||||
name="phone"
|
||||
@@ -87,7 +87,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="phoneSmsCode">短信验证码</label>
|
||||
<label for="phoneSmsCode">短信验证码 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input
|
||||
id="phoneSmsCode"
|
||||
name="smsCode"
|
||||
@@ -103,17 +103,17 @@
|
||||
</div>
|
||||
<input id="phoneValidToken" name="validToken" type="hidden" />
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">确认换绑</button>
|
||||
<span class="api-status" data-security-status>等待验证码</span>
|
||||
<button class="btn primary magnetic" type="submit" data-security-submit>确认换绑</button>
|
||||
<div class="form-status" data-security-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="module-panel security-danger">
|
||||
<h2>注销账号</h2>
|
||||
<p>验证已绑定手机号的短信验证码后提交注销申请。</p>
|
||||
<form class="editor-form security-form" data-security-form="deactivate">
|
||||
<form class="editor-form security-form" data-security-form="deactivate" novalidate>
|
||||
<div class="editor-field">
|
||||
<label for="deactivateSmsCode">短信验证码</label>
|
||||
<label for="deactivateSmsCode">短信验证码 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input
|
||||
id="deactivateSmsCode"
|
||||
name="smsCode"
|
||||
@@ -127,17 +127,17 @@
|
||||
<button class="btn ghost magnetic" type="button" data-security-send-deactivate-code>
|
||||
获取验证码
|
||||
</button>
|
||||
<p class="api-status" data-security-bound-phone>正在读取已绑定手机号</p>
|
||||
<div class="form-status" data-security-bound-phone role="status" aria-live="polite"><div class="api-state api-state--loading" data-api-state="loading" aria-busy="true">正在读取已绑定手机号…</div></div>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="confirmText">确认文本</label>
|
||||
<label for="confirmText">确认文本 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="confirmText" name="confirmText" type="text" placeholder="请输入:确认注销" required />
|
||||
</div>
|
||||
<input name="validToken" type="hidden" />
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">提交注销</button>
|
||||
<button class="btn primary magnetic" type="submit" data-security-submit>提交注销</button>
|
||||
<button class="btn ghost magnetic" type="button" data-security-logout>退出登录</button>
|
||||
<span class="api-status" data-security-status>等待验证</span>
|
||||
<div class="form-status" data-security-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -155,10 +155,10 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/tac/js/tac.min.js"></script>
|
||||
<script src="public/js/captcha-pages.js"></script>
|
||||
<script src="public/js/security-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+40
-8
@@ -65,10 +65,16 @@
|
||||
<div class="module-grid">
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">享</span>
|
||||
<h3>应用分享</h3>
|
||||
<p>邀请奖励服务正在开发中,当前不提供不可用的跳转入口。</p>
|
||||
<h3>应用分享与收益</h3>
|
||||
<p>分享奖励、推广收益与提现服务正在开发中,当前不提供不可用的跳转入口。</p>
|
||||
<span class="pill">即将开放</span>
|
||||
</div>
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">荐</span>
|
||||
<h3>个性化推荐</h3>
|
||||
<p>当前 PC 接口未提供推荐偏好的读取或保存,暂不能设置推荐开关。</p>
|
||||
<span class="pill">暂不可设置</span>
|
||||
</div>
|
||||
<a class="module-card tilt-card" href="profile-feedback.html" data-feature-link="available"
|
||||
><span class="icon">馈</span>
|
||||
<h3>意见反馈</h3>
|
||||
@@ -96,14 +102,40 @@
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>平台设置与协议</h2>
|
||||
<p>集中查看 APP“设置”中的平台说明、联系方式及协议入口;正式生效内容以平台和法务发布版本为准。</p>
|
||||
<div class="module-grid">
|
||||
<a class="module-card tilt-card" href="about.html"
|
||||
><span class="icon">关</span>
|
||||
<h3>关于我们与联系我们</h3>
|
||||
<p>查看平台介绍、客服信息和服务说明。</p></a
|
||||
>
|
||||
<a class="module-card tilt-card" href="privacy.html"
|
||||
><span class="icon">隐</span>
|
||||
<h3>隐私政策</h3>
|
||||
<p>查看个人信息处理说明。</p></a
|
||||
>
|
||||
<a class="module-card tilt-card" href="children-privacy.html"
|
||||
><span class="icon">童</span>
|
||||
<h3>儿童信息保护</h3>
|
||||
<p>查看儿童个人信息处理规则。</p></a
|
||||
>
|
||||
<a class="module-card tilt-card" href="terms.html"
|
||||
><span class="icon">约</span>
|
||||
<h3>服务协议</h3>
|
||||
<p>查看平台服务协议入口。</p></a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>会员服务</h2>
|
||||
<p>选择适合的会员套餐并创建订单。当前 PC 暂未开放在线支付。</p>
|
||||
<div class="vip-package-list" data-vip-package-list>
|
||||
<div class="api-empty">会员套餐加载中</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取会员套餐…</div>
|
||||
</div>
|
||||
<form class="editor-form vip-order-form" data-vip-order-form>
|
||||
<p class="api-status" data-vip-selected-package>尚未选择套餐</p>
|
||||
<form class="editor-form vip-order-form" data-vip-order-form novalidate>
|
||||
<p class="form-status" data-vip-selected-package role="status" aria-live="polite">尚未选择套餐</p>
|
||||
<div class="editor-field">
|
||||
<label for="vipGenealogy">关联家谱(可选)</label>
|
||||
<select id="vipGenealogy" data-vip-genealogy-options>
|
||||
@@ -112,7 +144,7 @@
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit" data-vip-order-submit>创建会员订单</button>
|
||||
<span class="api-status" data-vip-order-status>等待选择套餐</span>
|
||||
<div class="form-status" data-vip-order-status role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -122,7 +154,7 @@
|
||||
<button class="btn outline" type="button" data-vip-order-refresh>刷新订单</button>
|
||||
</div>
|
||||
<div class="module-list" data-vip-order-list>
|
||||
<div class="api-empty">会员订单加载中</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取会员订单…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
@@ -155,8 +187,8 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/vip-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/vip-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+29
-20
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>应用分享 - 个人中心 - 代代相传</title>
|
||||
<title>应用分享与收益 - 个人中心 - 代代相传</title>
|
||||
<link rel="stylesheet" href="public/css/public.css" />
|
||||
<link rel="stylesheet" href="public/layui/css/layui.css" />
|
||||
<link rel="stylesheet" href="public/css/profile-module.css" />
|
||||
</head>
|
||||
<body class="page-profile-module" data-family-share-page data-feature-status="pending" data-feature-message="应用分享与邀请奖励服务正在开发中,当前页面仅保留设计预览。">
|
||||
<body class="page-profile-module" data-family-share-page>
|
||||
<header class="site-header">
|
||||
<div class="container nav">
|
||||
<a class="brand magnetic" href="profile.html"
|
||||
@@ -16,12 +16,11 @@
|
||||
>
|
||||
<nav class="nav-links">
|
||||
<a href="profile-services.html">帮助与服务</a
|
||||
><a class="active" href="profile-share.html">应用分享</a>
|
||||
><a class="active" href="profile-share.html">应用分享与收益</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<a href="profile-services.html">返回服务中心</a
|
||||
><a class="btn primary magnetic" href="profile-share.html"
|
||||
>立即分享</a
|
||||
<a class="btn ghost magnetic" href="profile-services.html"
|
||||
>返回服务中心</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,8 +31,8 @@
|
||||
<div class="module-kicker">Share</div>
|
||||
<div class="module-title-row">
|
||||
<div>
|
||||
<h1>应用分享</h1>
|
||||
<p>邀请好友创建和维护家谱,查看应用下载、分享奖励和推广入口。</p>
|
||||
<h1>应用分享与收益</h1>
|
||||
<p>当前 PC 未声明应用分享、奖励、收益或提现的正式接口;本页仅说明不可操作边界。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +41,7 @@
|
||||
<div class="container module-layout">
|
||||
<aside class="module-nav">
|
||||
<a href="profile-services.html">帮助与服务</a
|
||||
><a class="active" href="profile-share.html">应用分享</a
|
||||
><a class="active" href="profile-share.html">应用分享与收益</a
|
||||
><a href="profile-feedback.html">意见反馈</a
|
||||
><a href="help.html">帮助中心</a>
|
||||
</aside>
|
||||
@@ -50,27 +49,38 @@
|
||||
<section class="module-panel">
|
||||
<h2>邀请好友奖励</h2>
|
||||
<div class="module-grid">
|
||||
<div class="module-card">
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">享</span>
|
||||
<h3>每邀请一位好友</h3>
|
||||
<p data-member-overview>分享概览加载中...</p>
|
||||
<p>当前 PC 接口未提供邀请奖励规则,暂不能展示奖励。</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">码</span>
|
||||
<h3>分享二维码</h3>
|
||||
<p data-invite-link>分享链接加载中...</p>
|
||||
<p>当前 PC 接口未提供分享链接或二维码,暂不能生成。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>分享记录</h2>
|
||||
<div class="module-list">
|
||||
<div class="module-row">
|
||||
<div>
|
||||
<h3>本月邀请</h3>
|
||||
<p>3 人访问 · 1 人完成注册</p>
|
||||
</div>
|
||||
<span class="pill">查看</span>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
|
||||
当前 PC 接口未提供分享记录,暂不能展示邀请访问或注册结果。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>收益与提现</h2>
|
||||
<div class="module-grid two">
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">收</span>
|
||||
<h3>推广收益</h3>
|
||||
<p>当前 PC 接口未提供收益账户或余额,暂不能展示收益金额。</p>
|
||||
</div>
|
||||
<div class="module-card is-unavailable" aria-disabled="true">
|
||||
<span class="icon">提</span>
|
||||
<h3>提现记录</h3>
|
||||
<p>当前 PC 接口未提供提现方式、申请或记录,暂不能提交提现。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -87,7 +97,6 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/pending-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+20
-16
@@ -86,13 +86,13 @@
|
||||
<h2>世系树</h2>
|
||||
<!-- 世系树由 public/js/lineage-pages.js 按接口数据渲染 -->
|
||||
<div class="lineage-tree-wrap" data-lineage-tree>
|
||||
<div class="api-empty">世系树加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">世系树加载中…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>成员搜索</h2>
|
||||
<div class="lineage-toolbar lineage-filter-toolbar">
|
||||
<input type="search" data-lineage-keyword placeholder="输入姓名、别名或人物编号搜索" />
|
||||
<input type="search" data-lineage-keyword aria-label="按姓名、别名或人物编号搜索成员" placeholder="输入姓名、别名或人物编号搜索" />
|
||||
<select data-lineage-generation-filter aria-label="按世代筛选">
|
||||
<option value="">全部世代</option>
|
||||
</select>
|
||||
@@ -104,14 +104,15 @@
|
||||
</select>
|
||||
<button class="btn primary" type="button" data-lineage-search>搜索</button>
|
||||
</div>
|
||||
<p class="form-status" data-lineage-list-summary></p>
|
||||
<p class="form-status" role="status" aria-live="polite" data-lineage-list-summary></p>
|
||||
<div class="form-status" role="status" aria-live="polite" data-lineage-action-status></div>
|
||||
<!-- 成员列表由世系人物分页接口渲染 -->
|
||||
<div class="module-list" data-lineage-list>
|
||||
<div class="api-empty">成员加载中...</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">世系人物加载中…</div>
|
||||
</div>
|
||||
<div class="bottom-actions" data-lineage-pagination hidden>
|
||||
<button class="btn ghost" type="button" data-lineage-page-action="previous">上一页</button>
|
||||
<span class="form-status" data-lineage-page-status></span>
|
||||
<span class="form-status" data-lineage-page-status role="status" aria-live="polite"></span>
|
||||
<button class="btn ghost" type="button" data-lineage-page-action="next">下一页</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -119,24 +120,27 @@
|
||||
<h2>成员详情</h2>
|
||||
<!-- 点击成员列表或世系树节点后渲染详情 -->
|
||||
<div data-lineage-detail>
|
||||
<div class="api-empty">请选择一个成员查看资料</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一个成员查看资料</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel" data-lineage-management hidden>
|
||||
<h2>新增 / 编辑成员</h2>
|
||||
<form id="lineage-member-form" class="editor-form" data-lineage-form>
|
||||
<form id="lineage-member-form" class="editor-form" data-lineage-form novalidate>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="lineage-binding-mode">账号绑定</label>
|
||||
<label for="lineage-binding-mode">账号绑定 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<select id="lineage-binding-mode" name="bindingMode" required>
|
||||
<option value="NONE" selected>不绑定账号</option>
|
||||
<option value="SELF">绑定当前登录账号</option>
|
||||
<option value="SPECIFIED">指定其他账号</option>
|
||||
</select>
|
||||
<select name="appUserId" data-lineage-member-options hidden>
|
||||
<option value="">请选择已绑定账号的家谱成员</option>
|
||||
</select>
|
||||
<p class="upload-status" data-lineage-binding-status>指定账号仅可从当前家谱正常成员中选择。</p>
|
||||
<div class="editor-field" data-lineage-specified-member hidden>
|
||||
<label for="lineage-specified-member">指定账号</label>
|
||||
<select id="lineage-specified-member" name="appUserId" data-lineage-member-options hidden>
|
||||
<option value="">请选择已绑定账号的家谱成员</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="upload-status" data-lineage-binding-status role="status" aria-live="polite">指定账号仅可从当前家谱正常成员中选择。</p>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="lineage-avatar-file">头像</label>
|
||||
@@ -153,12 +157,12 @@
|
||||
data-upload-biz-type="avatar"
|
||||
data-upload-usage-scene="lineage_avatar"
|
||||
/>
|
||||
<p id="lineage-avatar-status" class="upload-status">请选择头像文件</p>
|
||||
<p id="lineage-avatar-status" class="upload-status" role="status" aria-live="polite">请选择头像文件</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="lineage-person-name">姓名</label>
|
||||
<label for="lineage-person-name">姓名 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="lineage-person-name" name="name" type="text" placeholder="请输入成员姓名" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -273,9 +277,9 @@
|
||||
<textarea id="lineage-remark" name="remark" placeholder="可选"></textarea>
|
||||
</div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary" type="submit">保存成员</button>
|
||||
<button class="btn primary" type="submit" data-lineage-submit>保存成员</button>
|
||||
<button class="btn ghost" type="reset">清空</button>
|
||||
<span class="form-status" data-lineage-form-status></span>
|
||||
<span class="form-status" role="status" aria-live="polite" data-lineage-form-status></span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
<div class="module-main">
|
||||
<section class="module-panel">
|
||||
<h2>视频内容</h2>
|
||||
<form id="video-edit-form" class="editor-form" data-video-form data-video-management hidden>
|
||||
<form id="video-edit-form" class="editor-form" data-video-form data-video-management hidden novalidate>
|
||||
<div class="editor-field">
|
||||
<label for="video-title">视频标题</label>
|
||||
<label for="video-title">视频标题 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="video-title" name="videoTitle" type="text" placeholder="请输入视频标题" required />
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
<div class="editor-field">
|
||||
<label for="video-file">视频文件</label>
|
||||
<label for="video-file">视频文件 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<input id="video-oss-id" name="videoOssId" type="hidden" />
|
||||
<label class="upload-control" for="video-file">选择视频文件</label>
|
||||
<input
|
||||
@@ -70,7 +70,7 @@
|
||||
data-upload-status="#video-file-status"
|
||||
data-video-duration-target="#video-duration"
|
||||
/>
|
||||
<p id="video-file-status" class="upload-status" data-video-file-status>请选择视频文件</p>
|
||||
<p id="video-file-status" class="upload-status" data-video-file-status role="status" aria-live="polite">请选择视频文件</p>
|
||||
</div>
|
||||
<div class="editor-field">
|
||||
<label for="video-cover-file">封面图片</label>
|
||||
@@ -84,7 +84,7 @@
|
||||
data-upload-target="#video-cover-oss-id"
|
||||
data-upload-status="#video-cover-status"
|
||||
/>
|
||||
<p id="video-cover-status" class="upload-status" data-video-cover-status>未选择封面</p>
|
||||
<p id="video-cover-status" class="upload-status" data-video-cover-status role="status" aria-live="polite">未选择封面</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-two">
|
||||
@@ -99,13 +99,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<input name="status" type="hidden" value="0" />
|
||||
<div class="form-status" data-video-form-status role="status" aria-live="polite"></div>
|
||||
<div class="bottom-actions">
|
||||
<button class="btn primary magnetic" type="submit">保存视频</button>
|
||||
<a class="btn ghost magnetic" href="profile-video.html" data-genealogy-context-link>取消</a>
|
||||
<span class="form-status" data-video-form-status></span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="api-empty" data-video-permission-placeholder>正在核对视频维护权限…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" data-video-permission-placeholder role="status" aria-live="polite" aria-busy="true">正在核对视频维护权限…</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-2
@@ -50,13 +50,17 @@
|
||||
<section class="module-panel">
|
||||
<h2>视频列表</h2>
|
||||
<div class="module-list" data-video-list aria-live="polite">
|
||||
<div class="api-empty">正在加载视频…</div>
|
||||
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
|
||||
正在加载视频…
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module-panel">
|
||||
<h2>视频详情</h2>
|
||||
<div data-video-detail aria-live="polite">
|
||||
<div class="api-empty">请选择一个视频查看详情</div>
|
||||
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
|
||||
请选择一个视频查看详情
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+22
-3
@@ -46,7 +46,7 @@
|
||||
<div class="profile-tags">
|
||||
<span>已登录账户</span><span>资料以实际接口为准</span>
|
||||
</div>
|
||||
<p class="profile-load-status" data-profile-load-status role="status" aria-live="polite">正在读取账户资料…</p>
|
||||
<div class="profile-load-status" data-profile-load-status role="status" aria-live="polite">正在读取账户资料…</div>
|
||||
<div class="toolbar profile-hero-actions">
|
||||
<a class="btn primary magnetic" href="profile-families.html">进入我的家谱</a>
|
||||
<a class="btn ghost magnetic" href="profile-data.html">完善个人资料</a>
|
||||
@@ -111,8 +111,27 @@
|
||||
<div class="data-list">
|
||||
<p><span>绑定账号</span><b data-profile-text="phone">加载中</b></p>
|
||||
<p><span>昵称</span><b data-profile-text="displayName">加载中</b></p>
|
||||
<p><span>真实姓名</span><b data-profile-text="realName">加载中</b></p>
|
||||
<p><span>性别</span><b data-profile-text="sex">加载中</b></p>
|
||||
<p><span>出生日期</span><b data-profile-text="birthday">加载中</b></p>
|
||||
<p><span>现居地区</span><b data-profile-text="regionText">加载中</b></p>
|
||||
<p><span>邮箱</span><b data-profile-text="email">加载中</b></p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="panel-head compact">
|
||||
<div>
|
||||
<h2>账户与服务</h2>
|
||||
<p>将 APP 的“我的”功能整理为桌面服务入口;家谱内容仍需从具体家谱进入。</p>
|
||||
</div>
|
||||
<a class="link-btn" href="profile-services.html">查看服务中心</a>
|
||||
</div>
|
||||
<div class="service-grid" aria-label="账户与服务入口">
|
||||
<a href="profile-messages.html"><strong>消息中心</strong><span>查看真实通知与业务提醒</span></a>
|
||||
<a href="profile-data.html"><strong>个人资料</strong><span>维护昵称、实名和联系方式</span></a>
|
||||
<a href="profile-security.html"><strong>账户安全</strong><span>修改密码、换绑手机或注销账号</span></a>
|
||||
<a href="profile-services.html"><strong>会员服务</strong><span>查看套餐、订单、帮助与服务说明</span></a>
|
||||
<a href="profile-feedback.html"><strong>意见反馈</strong><span>提交产品问题和改进建议</span></a>
|
||||
<a href="app.html"><strong>移动端</strong><span>下载 APP,处理移动端场景</span></a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -172,8 +191,8 @@
|
||||
<script src="utils/axios.js"></script>
|
||||
<script src="utils/AxiosRequestUtil.js"></script>
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/profile-pages.js"></script>
|
||||
<script src="public/js/profile-common.js?v=20260725-entry"></script>
|
||||
<script src="public/js/profile-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -169,6 +169,18 @@
|
||||
transition: border-color .22s ease, box-shadow .22s ease, background .22s ease, transform .22s ease;
|
||||
}
|
||||
|
||||
.create-form .field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--ink);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.create-region-picker label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.create-region-picker {
|
||||
/* 创建家谱页地区选择器占满表单宽度,编码通过隐藏字段提交 */
|
||||
display: grid;
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
/* 加入家谱页只保留单页差异,公共认证卡片样式在 public.css */
|
||||
.page-join-genealogy .auth-card {
|
||||
width: min(500px, 100%);
|
||||
width: min(1080px, 100%);
|
||||
}
|
||||
|
||||
.join-page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(340px, .85fr);
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.join-search-panel,
|
||||
.join-apply-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.join-search-panel {
|
||||
padding-right: 28px;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.join-page-grid h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.join-page-grid .module-list {
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.page-join-genealogy .auth-card {
|
||||
width: min(500px, 100%);
|
||||
}
|
||||
|
||||
.join-page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.join-search-panel {
|
||||
padding-right: 0;
|
||||
padding-bottom: 24px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-box {
|
||||
|
||||
@@ -522,6 +522,11 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-region-picker > label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.profile-region-picker select {
|
||||
min-height: 48px;
|
||||
padding: 12px 14px;
|
||||
@@ -745,11 +750,16 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.editor-field label {
|
||||
.editor-field label,
|
||||
.editor-label {
|
||||
color: var(--ink);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.field-required {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.editor-field input,
|
||||
.editor-field select,
|
||||
.editor-field textarea {
|
||||
|
||||
@@ -458,6 +458,26 @@
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.service-grid strong,
|
||||
.service-grid span {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.service-grid strong {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.service-grid span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.service-grid a:hover span {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
}
|
||||
|
||||
.tilt-card {
|
||||
transform: perspective(900px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg)) translateY(var(--lift, 0));
|
||||
transition: transform .18s ease, box-shadow .24s ease, border-color .24s ease;
|
||||
|
||||
+29
-2
@@ -551,6 +551,21 @@ textarea {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.auth-card .auth-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--ink);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.auth-card .auth-field b {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.auth-card .auth-form-status:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-card .input {
|
||||
height: 50px;
|
||||
background: #fffaf1;
|
||||
@@ -637,8 +652,9 @@ textarea {
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
/* 接口列表为空时的统一占位样式 */
|
||||
.api-empty {
|
||||
/* 业务请求的加载、空数据、失败和无权限状态 */
|
||||
.api-empty,
|
||||
.api-state {
|
||||
padding: 22px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 14px;
|
||||
@@ -648,6 +664,17 @@ textarea {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.api-state--loading {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.api-state--error,
|
||||
.api-state--forbidden {
|
||||
border-color: rgba(160, 50, 45, .28);
|
||||
color: var(--red);
|
||||
background: rgba(160, 50, 45, .06);
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 60px 0 36px;
|
||||
color: rgba(255, 255, 255, .78);
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
|
||||
root.AdminPermissionPages = factory(root);
|
||||
if (root.document) {
|
||||
root.document.addEventListener('DOMContentLoaded', function () {
|
||||
root.AdminPermissionPages.init();
|
||||
});
|
||||
}
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var documentRef = root.document;
|
||||
var roleLabels = {
|
||||
owner: '谱主',
|
||||
admin: '管理员',
|
||||
editor: '编辑',
|
||||
member: '成员',
|
||||
visitor: '访客'
|
||||
};
|
||||
|
||||
function getApi() {
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function query(selector) {
|
||||
return documentRef ? documentRef.querySelector(selector) : null;
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text;
|
||||
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
if (value === undefined || value === null) return '';
|
||||
text = String(value).trim();
|
||||
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function getCurrentGenealogyId(search) {
|
||||
var source = search === undefined && root.location ? root.location.search : search;
|
||||
var params;
|
||||
var contextId;
|
||||
|
||||
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
|
||||
contextId = normalizeId(root.ProfileUI.getGenealogyId());
|
||||
if (contextId) return contextId;
|
||||
}
|
||||
params = new URLSearchParams(String(source || '').replace(/^\?/, ''));
|
||||
return normalizeId(params.get('genealogyId'));
|
||||
}
|
||||
|
||||
function normalizePermissionOverview(data, expectedGenealogyId) {
|
||||
var source = data || {};
|
||||
var genealogyId = normalizeId(source.genealogyId);
|
||||
var roleType = String(source.roleType === undefined || source.roleType === null ? '' : source.roleType).trim();
|
||||
var expectedId = normalizeId(expectedGenealogyId);
|
||||
|
||||
if (!genealogyId || !expectedId || genealogyId !== expectedId || !Object.prototype.hasOwnProperty.call(roleLabels, roleType)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
genealogyId: genealogyId,
|
||||
roleType: roleType,
|
||||
canManage: source.canManage === true,
|
||||
canEditContent: source.canEditContent === true
|
||||
};
|
||||
}
|
||||
|
||||
function canMaintainContent(access) {
|
||||
return Boolean(access && (access.canEditContent === true || access.canManage === true));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderCurrentPermissions(access) {
|
||||
if (!access) return '';
|
||||
return '<div class="form-like admin-permission-current">' +
|
||||
'<p><span>当前角色</span><b>' + escapeHtml(roleLabels[access.roleType]) + '</b></p>' +
|
||||
'<p><span>成员角色维护</span><b>' + (access.canManage ? '可管理' : '不可管理') + '</b></p>' +
|
||||
'<p><span>入谱审核</span><b>' + (access.canManage ? '可审核' : '不可审核') + '</b></p>' +
|
||||
'<p><span>内容维护</span><b>' + (canMaintainContent(access) ? '可维护' : '不可维护') + '</b></p>' +
|
||||
'</div><p class="module-meta">实际写入操作仍由服务端按当前角色与家谱权限校验。</p>';
|
||||
}
|
||||
|
||||
function setState(type, message) {
|
||||
var container = query('[data-admin-permission-current]');
|
||||
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.textContent = message;
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function redirectToLogin(api) {
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
if (!root.location) return;
|
||||
if (typeof root.location.replace === 'function') root.location.replace('login.html');
|
||||
else root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
async function loadPage() {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var overview;
|
||||
var container;
|
||||
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
genealogyId = getCurrentGenealogyId();
|
||||
if (!genealogyId) {
|
||||
setState('empty', '请先选择家谱,再查看当前角色权限。');
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
|
||||
setState('loading', '正在核对当前家谱角色与权限…');
|
||||
try {
|
||||
overview = normalizePermissionOverview(await api.genealogyDetail(genealogyId), genealogyId);
|
||||
if (!overview) throw new Error('家谱权限响应缺少稳定角色或能力字段');
|
||||
container = query('[data-admin-permission-current]');
|
||||
if (container) container.innerHTML = renderCurrentPermissions(overview);
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setState(
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱权限。' : '当前角色权限读取失败,请稍后重试。'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!documentRef || !query('[data-admin-permission-page]')) return;
|
||||
loadPage();
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentGenealogyId: getCurrentGenealogyId,
|
||||
normalizePermissionOverview: normalizePermissionOverview,
|
||||
canMaintainContent: canMaintainContent,
|
||||
renderCurrentPermissions: renderCurrentPermissions,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
+104
-39
@@ -295,7 +295,7 @@
|
||||
function renderAlbumDetail(album) {
|
||||
var actions = '';
|
||||
|
||||
if (!album) return '<div class="api-empty">未找到该相册</div>';
|
||||
if (!album) return '';
|
||||
if (canEditContent) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(album.genealogyId, album.albumId)) + '">编辑相册</a>' +
|
||||
@@ -327,8 +327,8 @@
|
||||
function renderPhotoList(data) {
|
||||
var photos = Array.isArray(data) ? data.map(normalizeAlbumPhoto) : [];
|
||||
|
||||
if (photos.some(function (photo) { return !photo; })) return '<div class="api-empty">照片响应字段不完整</div>';
|
||||
if (!photos.length) return '<div class="api-empty">暂无照片</div>';
|
||||
if (photos.some(function (photo) { return !photo; })) return '';
|
||||
if (!photos.length) return '';
|
||||
return photos.map(function (photo) {
|
||||
var action = canEditContent
|
||||
? '<button class="pill is-danger" type="button" data-album-photo-delete-id="' +
|
||||
@@ -369,10 +369,23 @@
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
|
||||
}
|
||||
|
||||
function setStatus(selector, message) {
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setStatus(selector, message, type) {
|
||||
var target = query(selector);
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
@@ -397,12 +410,12 @@
|
||||
function renderNoContext() {
|
||||
var message = '请先选择家谱,再查看或维护相册。';
|
||||
|
||||
if (query('[data-album-list]')) query('[data-album-list]').innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (query('[data-album-detail]')) query('[data-album-detail]').innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
if (query('[data-album-photo-list]')) query('[data-album-photo-list]').innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setStatus('[data-album-list]', message, 'empty');
|
||||
setStatus('[data-album-detail]', '当前没有家谱上下文。', 'empty');
|
||||
setStatus('[data-album-photo-list]', '当前没有家谱上下文。', 'empty');
|
||||
setEditorEnabled(false, message);
|
||||
setStatus('[data-album-form-status]', message);
|
||||
setStatus('[data-album-photo-status]', message);
|
||||
setStatus('[data-album-form-status]', message, 'forbidden');
|
||||
setStatus('[data-album-photo-status]', message, 'forbidden');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -412,10 +425,14 @@
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function canEditAlbums(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var detail = await api.genealogyDetail(genealogyId);
|
||||
|
||||
canEditContent = Boolean(detail && (detail.canEditContent || detail.canManage));
|
||||
canEditContent = canEditAlbums(detail);
|
||||
queryAll('[data-album-create-link], [data-album-editor-action], [data-album-photo-editor]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
@@ -430,11 +447,11 @@
|
||||
albums.forEach(function (album) { albumsById[album.albumId] = album; });
|
||||
if (!container) return albums;
|
||||
if (!Array.isArray(data) || (data.length && !albums.length)) {
|
||||
container.innerHTML = '<div class="api-empty">相册响应缺少稳定 AlbumVo 字段,请联系后端核对。</div>';
|
||||
setStatus('[data-album-list]', '相册响应缺少稳定 AlbumVo 字段,请联系后端核对。', 'error');
|
||||
return [];
|
||||
}
|
||||
if (!albums.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无相册</div>';
|
||||
setStatus('[data-album-list]', '暂无相册', 'empty');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = albums.map(renderAlbumRow).join('');
|
||||
@@ -449,16 +466,17 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncLinks();
|
||||
setStatus('[data-album-list]', '正在加载相册…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
renderAlbums(await api.albums(genealogyId));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-album-list]')) {
|
||||
query('[data-album-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的相册。' : '相册加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
setStatus(
|
||||
'[data-album-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的相册。' : '相册加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的相册。' : (error.message || '相册加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -498,6 +516,7 @@
|
||||
albumId = getCurrentAlbumId();
|
||||
syncLinks();
|
||||
setEditorEnabled(false, '正在加载相册编辑信息…');
|
||||
setStatus('[data-album-form-status]', '正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的相册。' };
|
||||
@@ -509,25 +528,38 @@
|
||||
if (query('[data-album-editor-title]')) query('[data-album-editor-title]').textContent = '编辑相册';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setStatus('[data-album-form-status]', '');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditorEnabled(false, isForbidden(error) ? '当前账号无权维护该家谱的相册。' : (error.message || '相册编辑信息加载失败'));
|
||||
setStatus('[data-album-form-status]', isForbidden(error) ? '当前账号无权维护该家谱的相册。' : (error.message || '相册编辑信息加载失败'));
|
||||
setStatus(
|
||||
'[data-album-form-status]',
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的相册。' : (error.message || '相册编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPhotos(api, genealogyId, albumId) {
|
||||
var data = await api.albumPhotos(genealogyId, albumId);
|
||||
var photos = normalizeAlbumPhotos(data);
|
||||
var data;
|
||||
var photos;
|
||||
var container = query('[data-album-photo-list]');
|
||||
|
||||
setStatus('[data-album-photo-list]', '正在加载照片…', 'loading');
|
||||
data = await api.albumPhotos(genealogyId, albumId);
|
||||
photos = normalizeAlbumPhotos(data);
|
||||
photosById = Object.create(null);
|
||||
photos.forEach(function (photo) { photosById[photo.photoId] = photo; });
|
||||
if (container) {
|
||||
container.innerHTML = !Array.isArray(data) || (data.length && !photos.length)
|
||||
? '<div class="api-empty">照片响应缺少稳定 AlbumPhotoVo 字段,请联系后端核对。</div>'
|
||||
: renderPhotoList(photos);
|
||||
if (!container) return photos;
|
||||
if (!Array.isArray(data) || (data.length && !photos.length)) {
|
||||
setStatus('[data-album-photo-list]', '照片响应缺少稳定 AlbumPhotoVo 字段,请联系后端核对。', 'error');
|
||||
return [];
|
||||
}
|
||||
if (!photos.length) {
|
||||
setStatus('[data-album-photo-list]', '暂无照片', 'empty');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = renderPhotoList(photos);
|
||||
return photos;
|
||||
}
|
||||
|
||||
@@ -541,10 +573,15 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
albumId = getCurrentAlbumId();
|
||||
if (!genealogyId || !albumId) {
|
||||
if (genealogyId && query('[data-album-detail]')) query('[data-album-detail]').innerHTML = '<div class="api-empty">缺少有效相册编号。</div>';
|
||||
if (genealogyId) {
|
||||
setStatus('[data-album-detail]', '缺少有效相册编号。', 'error');
|
||||
setStatus('[data-album-photo-list]', '缺少有效相册编号。', 'empty');
|
||||
}
|
||||
return;
|
||||
}
|
||||
syncLinks();
|
||||
setStatus('[data-album-detail]', '正在加载相册信息…', 'loading');
|
||||
setStatus('[data-album-photo-list]', '正在加载照片…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
albums = await api.albums(genealogyId);
|
||||
@@ -552,12 +589,31 @@
|
||||
if (!currentAlbum) throw new Error('相册列表未返回当前相册');
|
||||
if (query('[data-album-detail]')) query('[data-album-detail]').innerHTML = renderAlbumDetail(currentAlbum);
|
||||
if (query('[data-album-detail-title]')) query('[data-album-detail-title]').textContent = currentAlbum.albumName;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(
|
||||
'[data-album-detail]',
|
||||
isForbidden(error) ? '当前账号无权查看该相册。' : (error.message || '相册详情加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
setStatus(
|
||||
'[data-album-photo-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该相册。' : '照片加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该相册。' : (error.message || '相册详情加载失败'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadPhotos(api, genealogyId, albumId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-album-detail]')) query('[data-album-detail]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该相册。' : escapeHtml(error.message || '相册详情加载失败')) + '</div>';
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该相册。' : (error.message || '相册详情加载失败'));
|
||||
setStatus(
|
||||
'[data-album-photo-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该相册照片。' : (error.message || '照片加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该相册照片。' : (error.message || '照片加载失败'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,17 +628,17 @@
|
||||
var savedId;
|
||||
var verified;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
body = buildAlbumBody(getFormValues(form));
|
||||
validation = validateAlbumBody(body);
|
||||
if (validation) {
|
||||
setStatus('[data-album-form-status]', validation);
|
||||
setStatus('[data-album-form-status]', validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidCoverOssId;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setStatus('[data-album-form-status]', '正在保存相册…');
|
||||
setStatus('[data-album-form-status]', '正在保存相册…', 'loading');
|
||||
try {
|
||||
result = albumId
|
||||
? await api.updateAlbum(genealogyId, albumId, body)
|
||||
@@ -595,7 +651,11 @@
|
||||
if (root.location) root.location.href = buildDetailUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-album-form-status]', isForbidden(error) ? '当前账号无权保存该相册。' : (error.message || '相册保存失败'));
|
||||
setStatus(
|
||||
'[data-album-form-status]',
|
||||
isForbidden(error) ? '当前账号无权保存该相册。' : (error.message || '相册保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -609,17 +669,17 @@
|
||||
var validation;
|
||||
var saved;
|
||||
|
||||
if (writePending || !genealogyId || !albumId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !albumId || redirectUnauthorized(api)) return;
|
||||
body = buildAlbumPhotoBody(getFormValues(form));
|
||||
validation = validateAlbumPhotoBody(body);
|
||||
if (validation) {
|
||||
setStatus('[data-album-photo-status]', validation);
|
||||
setStatus('[data-album-photo-status]', validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidOssId;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setStatus('[data-album-photo-status]', '正在添加照片…');
|
||||
setStatus('[data-album-photo-status]', '正在添加照片…', 'loading');
|
||||
try {
|
||||
saved = normalizeAlbumPhoto(await api.createAlbumPhoto(genealogyId, albumId, body));
|
||||
if (!saved || saved.albumId !== albumId) throw new Error('添加照片响应缺少稳定 photoId');
|
||||
@@ -630,7 +690,11 @@
|
||||
setStatus('[data-album-photo-status]', '照片已添加');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-album-photo-status]', isForbidden(error) ? '当前账号无权添加照片。' : (error.message || '照片添加失败'));
|
||||
setStatus(
|
||||
'[data-album-photo-status]',
|
||||
isForbidden(error) ? '当前账号无权添加照片。' : (error.message || '照片添加失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -641,7 +705,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var album = albumsById[albumId] || currentAlbum;
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(albumId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(albumId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (album ? album.albumName : '该相册') + '”及其中全部照片吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -662,7 +726,7 @@
|
||||
var albumId = getCurrentAlbumId();
|
||||
var photo = photosById[photoId];
|
||||
|
||||
if (writePending || !genealogyId || !albumId || !normalizeId(photoId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !albumId || !normalizeId(photoId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (photo && photo.photoTitle || '该照片') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -722,6 +786,7 @@
|
||||
validateAlbumBody: validateAlbumBody,
|
||||
buildAlbumPhotoBody: buildAlbumPhotoBody,
|
||||
validateAlbumPhotoBody: validateAlbumPhotoBody,
|
||||
canEditAlbums: canEditAlbums,
|
||||
normalizeAlbum: normalizeAlbum,
|
||||
normalizeAlbumPhoto: normalizeAlbumPhoto,
|
||||
normalizeAlbums: normalizeAlbums,
|
||||
|
||||
@@ -86,12 +86,16 @@
|
||||
function renderPromotionList(data) {
|
||||
var items = normalizePromotionList(data);
|
||||
|
||||
if (!items.length) return '<div class="api-empty">当前暂无应用推广</div>';
|
||||
return items.map(renderPromotionCard).join('');
|
||||
}
|
||||
|
||||
function renderPromotionLoginRequired() {
|
||||
return '<div class="api-empty">登录后可查看应用推广,<a href="login.html">立即登录</a></div>';
|
||||
function setPromotionState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.textContent = message;
|
||||
}
|
||||
|
||||
async function loadPromotions(api) {
|
||||
@@ -111,6 +115,10 @@
|
||||
return Number(error && (error.status || error.code)) === 401;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var page = root.document && root.document.querySelector('[data-promotion-page]');
|
||||
var list = root.document && root.document.querySelector('[data-promotion-list]');
|
||||
@@ -118,22 +126,30 @@
|
||||
|
||||
if (!page || !list) return;
|
||||
if (!api || !api.getToken || !api.getToken()) {
|
||||
list.innerHTML = renderPromotionLoginRequired();
|
||||
setPromotionState(list, 'forbidden', '登录后可查看应用推广。');
|
||||
return;
|
||||
}
|
||||
|
||||
setPromotionState(list, 'loading', '正在读取应用推广…');
|
||||
try {
|
||||
list.classList.add('is-loading');
|
||||
list.innerHTML = renderPromotionList(await loadPromotions(api));
|
||||
var items = await loadPromotions(api);
|
||||
|
||||
if (!items.length) {
|
||||
setPromotionState(list, 'empty', '当前暂无应用推广。');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = renderPromotionList(items);
|
||||
} catch (error) {
|
||||
if (isUnauthorized(error)) {
|
||||
if (api.clearToken) api.clearToken();
|
||||
list.innerHTML = renderPromotionLoginRequired();
|
||||
setPromotionState(list, 'forbidden', '登录后可查看应用推广。');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div class="api-empty">应用推广读取失败,请稍后重试</div>';
|
||||
} finally {
|
||||
list.classList.remove('is-loading');
|
||||
setPromotionState(
|
||||
list,
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看应用推广。' : '应用推广读取失败,请稍后重试。'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +158,10 @@
|
||||
normalizePromotionList: normalizePromotionList,
|
||||
normalizeTargetUrl: normalizeTargetUrl,
|
||||
renderPromotionList: renderPromotionList,
|
||||
renderPromotionLoginRequired: renderPromotionLoginRequired,
|
||||
setPromotionState: setPromotionState,
|
||||
loadPromotions: loadPromotions,
|
||||
isUnauthorized: isUnauthorized,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
+80
-28
@@ -192,6 +192,10 @@
|
||||
return articleId ? url + '&articleId=' + encodeURIComponent(articleId) : url;
|
||||
}
|
||||
|
||||
function canEditArticles(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function detailValue(label, value) {
|
||||
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
|
||||
}
|
||||
@@ -199,7 +203,7 @@
|
||||
function renderArticleDetail(article) {
|
||||
var actions = '';
|
||||
|
||||
if (!article) return '<div class="api-empty">请选择一篇谱文查看详情</div>';
|
||||
if (!article) return stateHtml('empty', '请选择一篇谱文查看详情');
|
||||
if (canEditContent) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(article.genealogyId, article.articleId)) + '">编辑谱文</a>' +
|
||||
@@ -256,17 +260,49 @@
|
||||
else if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function stateHtml(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) return root.ProfileUI.renderApiState(type, message);
|
||||
return '<div class="api-state api-state--' + type + '" role="' +
|
||||
(type === 'error' || type === 'forbidden' ? 'alert' : 'status') +
|
||||
'" aria-live="polite">' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.innerHTML = stateHtml(type, message);
|
||||
}
|
||||
|
||||
function getStateType(message) {
|
||||
if (message.indexOf('无权') >= 0) return 'forbidden';
|
||||
if (/失败|缺少/.test(message)) return 'error';
|
||||
if (/正在|读取中|加载中/.test(message)) return 'loading';
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
function syncLinks() {
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
function setFormStatus(message, type) {
|
||||
var target = query('[data-article-form-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
function setEditorEnabled(enabled, message, type) {
|
||||
var placeholder = query('[data-article-editor-placeholder]');
|
||||
|
||||
queryAll('[data-article-editor], [data-article-editor-action]').forEach(function (element) {
|
||||
@@ -274,15 +310,22 @@
|
||||
});
|
||||
if (placeholder) {
|
||||
placeholder.hidden = Boolean(enabled);
|
||||
if (message) placeholder.textContent = message;
|
||||
if (message) setState(placeholder, type || getStateType(message), message);
|
||||
}
|
||||
}
|
||||
|
||||
function setWritePending(value) {
|
||||
function setWritePending(value, label) {
|
||||
var submit = query('[data-article-form] [type="submit"]');
|
||||
|
||||
writePending = Boolean(value);
|
||||
queryAll('[data-article-form] button, [data-article-form] input, [data-article-form] textarea, [data-article-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
if (submit) {
|
||||
if (!submit.dataset.defaultLabel) submit.dataset.defaultLabel = submit.textContent;
|
||||
submit.textContent = writePending && label ? label : submit.dataset.defaultLabel;
|
||||
submit.setAttribute('aria-busy', writePending ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function renderNoContext() {
|
||||
@@ -290,10 +333,10 @@
|
||||
var list = query('[data-article-list]');
|
||||
var detail = query('[data-article-detail]');
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setEditorEnabled(false, message);
|
||||
setFormStatus(message);
|
||||
setState(list, 'empty', message);
|
||||
setState(detail, 'empty', '当前没有家谱上下文。');
|
||||
setEditorEnabled(false, message, 'empty');
|
||||
setFormStatus(message, 'empty');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -306,7 +349,7 @@
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var detail = await api.genealogyDetail(genealogyId);
|
||||
|
||||
canEditContent = Boolean(detail && (detail.canEditContent || detail.canManage));
|
||||
canEditContent = canEditArticles(detail);
|
||||
queryAll('[data-article-create-link], [data-article-editor-action]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
@@ -323,11 +366,11 @@
|
||||
});
|
||||
if (!container) return articles;
|
||||
if (!Array.isArray(data) || (data.length && !articles.length)) {
|
||||
container.innerHTML = '<div class="api-empty">谱文响应缺少稳定 ArticleVo 字段,请联系后端核对。</div>';
|
||||
setState(container, 'error', '谱文响应缺少稳定 ArticleVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!articles.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无谱文</div>';
|
||||
setState(container, 'empty', '暂无谱文');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = articles.map(renderArticleRow).join('');
|
||||
@@ -346,6 +389,7 @@
|
||||
var article;
|
||||
|
||||
if (!genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return null;
|
||||
setState(query('[data-article-detail]'), 'loading', '正在加载谱文详情…');
|
||||
try {
|
||||
article = normalizeArticle(await api.articleDetail(genealogyId, articleId));
|
||||
if (!article || article.articleId !== normalizeId(articleId)) {
|
||||
@@ -356,7 +400,8 @@
|
||||
return article;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
setState(query('[data-article-detail]'), isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看该谱文。' : '谱文详情加载失败,请稍后重试。');
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该谱文。' : (error.message || '谱文详情加载失败'));
|
||||
return null;
|
||||
}
|
||||
@@ -372,6 +417,7 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncLinks();
|
||||
setState(query('[data-article-list]'), 'loading', '正在加载谱文…');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
articles = renderArticles(await api.articles(genealogyId));
|
||||
@@ -381,11 +427,8 @@
|
||||
else renderDetail(null);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-article-list]')) {
|
||||
query('[data-article-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的谱文。' : '谱文加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
setState(query('[data-article-list]'), isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的谱文。' : '谱文加载失败,请稍后重试。');
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的谱文。' : (error.message || '谱文加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -438,7 +481,8 @@
|
||||
if (!genealogyId) return;
|
||||
articleId = getCurrentArticleId();
|
||||
syncLinks();
|
||||
setEditorEnabled(false, '正在加载谱文编辑信息…');
|
||||
setEditorEnabled(false, '正在加载谱文编辑信息…', 'loading');
|
||||
setFormStatus('正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的谱文。' };
|
||||
@@ -449,12 +493,16 @@
|
||||
if (query('[data-article-editor-title]')) query('[data-article-editor-title]').textContent = '编辑谱文';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditorEnabled(false, isForbidden(error)
|
||||
? '当前账号无权维护该家谱的谱文。'
|
||||
: (error.message || '谱文编辑信息加载失败'));
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权维护该家谱的谱文。' : (error.message || '谱文编辑信息加载失败'));
|
||||
: (error.message || '谱文编辑信息加载失败'), isForbidden(error) ? 'forbidden' : 'error');
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的谱文。' : (error.message || '谱文编辑信息加载失败'),
|
||||
isForbidden(error) ? 'forbidden' : 'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,18 +516,18 @@
|
||||
var saved;
|
||||
var savedId;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
|
||||
body = buildArticleBody(getFormValues(form));
|
||||
validation = validateArticleBody(body);
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidCoverOssId;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存谱文…');
|
||||
setWritePending(true, '正在保存…');
|
||||
setFormStatus('正在保存谱文…', 'loading');
|
||||
try {
|
||||
result = articleId
|
||||
? await api.updateArticle(genealogyId, articleId, body)
|
||||
@@ -493,7 +541,10 @@
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权保存该谱文。' : (error.message || '谱文保存失败'));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权保存该谱文。' : (error.message || '谱文保存失败'),
|
||||
isForbidden(error) ? 'forbidden' : 'error'
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -504,7 +555,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var article = articlesById[articleId];
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (article ? article.articleTitle : '该谱文') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -557,6 +608,7 @@
|
||||
buildArticleBody: buildArticleBody,
|
||||
validateArticleBody: validateArticleBody,
|
||||
normalizeArticle: normalizeArticle,
|
||||
canEditArticles: canEditArticles,
|
||||
normalizeArticles: normalizeArticles,
|
||||
matchesSavedArticle: matchesSavedArticle,
|
||||
renderArticleDetail: renderArticleDetail,
|
||||
|
||||
+69
-36
@@ -229,6 +229,33 @@
|
||||
button.setAttribute('aria-busy', busy ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function getApiStateType(error) {
|
||||
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setFormStatus(form, message, type) {
|
||||
var status = query('[data-auth-form-status]', form);
|
||||
|
||||
if (!status) return;
|
||||
if (!message) {
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(status, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function setSubmitPending(button, pending, pendingLabel) {
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = Boolean(pending);
|
||||
button.textContent = pending ? pendingLabel : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
if (root.MessageUtil && root.MessageUtil.show) {
|
||||
root.MessageUtil.show(message);
|
||||
@@ -292,14 +319,6 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateBeforeSubmit(formType, values) {
|
||||
var message = validateAuthValues(formType, values);
|
||||
|
||||
if (!message) return true;
|
||||
showMessage(message);
|
||||
return false;
|
||||
}
|
||||
|
||||
function validatePhoneOnly(values) {
|
||||
var data = values || {};
|
||||
|
||||
@@ -308,11 +327,13 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateBeforeSendCode(values) {
|
||||
var message = validatePhoneOnly(values);
|
||||
function validateForm(form, formType, values) {
|
||||
var message = formType === 'send-code'
|
||||
? validatePhoneOnly(values)
|
||||
: validateAuthValues(formType, values);
|
||||
|
||||
if (!message) return true;
|
||||
showMessage(message);
|
||||
setFormStatus(form, message, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -350,21 +371,25 @@
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
setFormStatus(form, '接口初始化失败,请刷新后重试', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateForm(form, 'login', values)) return;
|
||||
if (!await ensureCaptcha(form, getFormCaptchaOperation(form, 'login'), values.phone)) {
|
||||
setFormStatus(form, '请完成验证后再提交登录。', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('login', values)) return;
|
||||
if (!await ensureCaptcha(form, getFormCaptchaOperation(form, 'login'), values.phone)) return;
|
||||
values = readForm(form);
|
||||
values.validToken = takeCaptchaToken(form);
|
||||
setBusy(button, true);
|
||||
setSubmitPending(button, true, '正在登录…');
|
||||
setFormStatus(form, '正在验证账号…', 'loading');
|
||||
try {
|
||||
await api.login(buildLoginBody(values));
|
||||
root.location.href = await resolvePostLoginUrl(api, 'login');
|
||||
} catch (error) {
|
||||
showMessage(error.message || '登录失败');
|
||||
setFormStatus(form, error.message || '登录失败', getApiStateType(error));
|
||||
} finally {
|
||||
setBusy(button, false);
|
||||
setSubmitPending(button, false, '正在登录…');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,18 +399,19 @@
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
setFormStatus(form, '接口初始化失败,请刷新后重试', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('sms-login', values)) return;
|
||||
setBusy(button, true);
|
||||
if (!validateForm(form, 'sms-login', values)) return;
|
||||
setSubmitPending(button, true, '正在登录…');
|
||||
setFormStatus(form, '正在验证短信验证码…', 'loading');
|
||||
try {
|
||||
await api.loginBySms(buildSmsLoginBody(values));
|
||||
root.location.href = await resolvePostLoginUrl(api, 'sms-login');
|
||||
} catch (error) {
|
||||
showMessage(error.message || '短信登录失败');
|
||||
setFormStatus(form, error.message || '短信登录失败', getApiStateType(error));
|
||||
} finally {
|
||||
setBusy(button, false);
|
||||
setSubmitPending(button, false, '正在登录…');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,18 +421,19 @@
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
setFormStatus(form, '接口初始化失败,请刷新后重试', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('register', values)) return;
|
||||
setBusy(button, true);
|
||||
if (!validateForm(form, 'register', values)) return;
|
||||
setSubmitPending(button, true, '正在注册…');
|
||||
setFormStatus(form, '正在创建账号…', 'loading');
|
||||
try {
|
||||
await api.register(buildRegisterBody(values));
|
||||
root.location.href = getSuccessUrl('register');
|
||||
} catch (error) {
|
||||
showMessage(error.message || '注册失败');
|
||||
setFormStatus(form, error.message || '注册失败', getApiStateType(error));
|
||||
} finally {
|
||||
setBusy(button, false);
|
||||
setSubmitPending(button, false, '正在注册…');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,26 +446,31 @@
|
||||
var operationCode = getFormCaptchaOperation(form, formType);
|
||||
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
setFormStatus(form, '接口初始化失败,请刷新后重试', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateForm(form, 'send-code', values)) return;
|
||||
if (!await ensureCaptcha(form, operationCode, values.phone)) {
|
||||
setFormStatus(form, '请完成验证后再获取验证码。', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSendCode(values)) return;
|
||||
if (!await ensureCaptcha(form, operationCode, values.phone)) return;
|
||||
|
||||
values = readForm(form);
|
||||
values.validToken = takeCaptchaToken(form);
|
||||
|
||||
setBusy(button, true);
|
||||
setFormStatus(form, '正在发送验证码…', 'loading');
|
||||
try {
|
||||
await api.sendSmsCode(operationCode, {
|
||||
phone: values.phone,
|
||||
validToken: values.validToken || ''
|
||||
});
|
||||
showMessage('验证码已发送');
|
||||
setFormStatus(form, '');
|
||||
setBusy(button, false);
|
||||
startSmsCooldown(button);
|
||||
} catch (error) {
|
||||
showMessage(error.message || '验证码发送失败');
|
||||
setFormStatus(form, error.message || '验证码发送失败', getApiStateType(error));
|
||||
setBusy(button, false);
|
||||
}
|
||||
}
|
||||
@@ -449,19 +481,20 @@
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
setFormStatus(form, '接口初始化失败,请刷新后重试', 'error');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('password-reset', values)) return;
|
||||
setBusy(button, true);
|
||||
if (!validateForm(form, 'password-reset', values)) return;
|
||||
setSubmitPending(button, true, '正在重设…');
|
||||
setFormStatus(form, '正在重设密码…', 'loading');
|
||||
try {
|
||||
await api.resetPassword(buildPasswordResetBody(values));
|
||||
showMessage('密码已重设,请重新登录');
|
||||
root.location.href = getSuccessUrl('password-reset');
|
||||
} catch (error) {
|
||||
showMessage(error.message || '重设密码失败');
|
||||
setFormStatus(form, error.message || '重设密码失败', getApiStateType(error));
|
||||
} finally {
|
||||
setBusy(button, false);
|
||||
setSubmitPending(button, false, '正在重设…');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
var mapLink = '';
|
||||
var actions = '';
|
||||
|
||||
if (!ceremony) return '<div class="api-empty">未找到该活动</div>';
|
||||
if (!ceremony) return '';
|
||||
if (ceremony.longitude !== undefined && ceremony.latitude !== undefined) {
|
||||
mapLink = ' <a class="pill" target="_blank" rel="noopener noreferrer" href="https://uri.amap.com/marker?position=' +
|
||||
encodeURIComponent(ceremony.longitude + ',' + ceremony.latitude) + '&name=' +
|
||||
@@ -432,8 +432,8 @@
|
||||
function renderGiftList(data) {
|
||||
var gifts = Array.isArray(data) ? data.map(normalizeCeremonyGift) : [];
|
||||
|
||||
if (gifts.some(function (gift) { return !gift; })) return '<div class="api-empty">祭品响应字段不完整</div>';
|
||||
if (!gifts.length) return '<div class="api-empty">暂无祭品记录</div>';
|
||||
if (gifts.some(function (gift) { return !gift; })) return '';
|
||||
if (!gifts.length) return '';
|
||||
return gifts.map(function (gift) {
|
||||
var action = canEditContent
|
||||
? '<button class="pill is-danger" type="button" data-ceremony-gift-delete-id="' +
|
||||
@@ -450,8 +450,8 @@
|
||||
function renderInvitationList(data) {
|
||||
var invitations = normalizeList(data, normalizeInvitation);
|
||||
|
||||
if (!Array.isArray(data) || (data.length && !invitations.length)) return '<div class="api-empty">邀请响应字段不完整</div>';
|
||||
if (!invitations.length) return '<div class="api-empty">暂无邀请记录</div>';
|
||||
if (!Array.isArray(data) || (data.length && !invitations.length)) return '';
|
||||
if (!invitations.length) return '';
|
||||
return invitations.map(function (invitation) {
|
||||
var option = inviteeOptionsByUserId[invitation.inviteeUserId];
|
||||
var label = option && (option.memberName || option.appUserNickName) || '已邀请成员';
|
||||
@@ -486,10 +486,23 @@
|
||||
else if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function setStatus(selector, message) {
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setStatus(selector, message, type) {
|
||||
var target = query(selector);
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function syncLinks() {
|
||||
@@ -518,10 +531,13 @@
|
||||
function renderNoContext() {
|
||||
var message = '请先选择家谱,再查看或维护祭祀活动。';
|
||||
|
||||
if (query('[data-ceremony-list]')) query('[data-ceremony-list]').innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setStatus('[data-ceremony-list]', message, 'empty');
|
||||
setStatus('[data-ceremony-detail]', '当前没有家谱上下文。', 'empty');
|
||||
setStatus('[data-ceremony-gift-list]', '当前没有家谱上下文。', 'empty');
|
||||
setStatus('[data-ceremony-invitation-list]', '当前没有家谱上下文。', 'empty');
|
||||
setStatus('[data-ceremony-invitee-options]', '当前没有家谱上下文。', 'empty');
|
||||
setEditorEnabled(false, message);
|
||||
setStatus('[data-ceremony-form-status]', message);
|
||||
setStatus('[data-ceremony-form-status]', message, 'forbidden');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -531,10 +547,14 @@
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function canEditCeremonies(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var detail = await api.genealogyDetail(genealogyId);
|
||||
|
||||
canEditContent = Boolean(detail && (detail.canEditContent || detail.canManage));
|
||||
canEditContent = canEditCeremonies(detail);
|
||||
queryAll('[data-ceremony-create-link], [data-ceremony-editor-action], [data-ceremony-admin-editor]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
@@ -549,12 +569,14 @@
|
||||
ceremonies.forEach(function (ceremony) { ceremoniesById[ceremony.ceremonyId] = ceremony; });
|
||||
if (!container) return ceremonies;
|
||||
if (!Array.isArray(data) || (data.length && !ceremonies.length)) {
|
||||
container.innerHTML = '<div class="api-empty">活动响应缺少稳定 CeremonyVo 字段,请联系后端核对。</div>';
|
||||
setStatus('[data-ceremony-list]', '活动响应缺少稳定 CeremonyVo 字段,请联系后端核对。', 'error');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = ceremonies.length
|
||||
? ceremonies.map(renderCeremonyRow).join('')
|
||||
: '<div class="api-empty">暂无祭祀活动</div>';
|
||||
if (!ceremonies.length) {
|
||||
setStatus('[data-ceremony-list]', '暂无祭祀活动', 'empty');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = ceremonies.map(renderCeremonyRow).join('');
|
||||
return ceremonies;
|
||||
}
|
||||
|
||||
@@ -566,13 +588,17 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncLinks();
|
||||
setStatus('[data-ceremony-list]', '正在加载祭祀活动…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
renderCeremonies(await api.ceremonies(genealogyId));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-ceremony-list]')) query('[data-ceremony-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的祭祀活动。' : '活动加载失败,请稍后重试。') + '</div>';
|
||||
setStatus(
|
||||
'[data-ceremony-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的祭祀活动。' : '活动加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的祭祀活动。' : (error.message || '活动加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -622,6 +648,7 @@
|
||||
ceremonyId = getCurrentCeremonyId();
|
||||
syncLinks();
|
||||
setEditorEnabled(false, '正在加载活动编辑信息…');
|
||||
setStatus('[data-ceremony-form-status]', '正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的祭祀活动。' };
|
||||
@@ -632,25 +659,38 @@
|
||||
if (query('[data-ceremony-editor-title]')) query('[data-ceremony-editor-title]').textContent = '编辑祭祀活动';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setStatus('[data-ceremony-form-status]', '');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditorEnabled(false, isForbidden(error) ? '当前账号无权维护该家谱的祭祀活动。' : (error.message || '活动编辑信息加载失败'));
|
||||
setStatus('[data-ceremony-form-status]', isForbidden(error) ? '当前账号无权维护该家谱的祭祀活动。' : (error.message || '活动编辑信息加载失败'));
|
||||
setStatus(
|
||||
'[data-ceremony-form-status]',
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的祭祀活动。' : (error.message || '活动编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGifts(api, genealogyId, ceremonyId) {
|
||||
var data = await api.ceremonyGifts(genealogyId, ceremonyId);
|
||||
var gifts = normalizeList(data, normalizeCeremonyGift);
|
||||
var data;
|
||||
var gifts;
|
||||
var container = query('[data-ceremony-gift-list]');
|
||||
|
||||
setStatus('[data-ceremony-gift-list]', '正在加载祭品…', 'loading');
|
||||
data = await api.ceremonyGifts(genealogyId, ceremonyId);
|
||||
gifts = normalizeList(data, normalizeCeremonyGift);
|
||||
giftsById = Object.create(null);
|
||||
gifts.forEach(function (gift) { giftsById[gift.giftId] = gift; });
|
||||
if (container) {
|
||||
container.innerHTML = !Array.isArray(data) || (data.length && !gifts.length)
|
||||
? '<div class="api-empty">祭品响应缺少稳定 CeremonyGiftVo 字段,请联系后端核对。</div>'
|
||||
: renderGiftList(gifts);
|
||||
if (!container) return gifts;
|
||||
if (!Array.isArray(data) || (data.length && !gifts.length)) {
|
||||
setStatus('[data-ceremony-gift-list]', '祭品响应缺少稳定 CeremonyGiftVo 字段,请联系后端核对。', 'error');
|
||||
return [];
|
||||
}
|
||||
if (!gifts.length) {
|
||||
setStatus('[data-ceremony-gift-list]', '暂无祭品记录', 'empty');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = renderGiftList(gifts);
|
||||
return gifts;
|
||||
}
|
||||
|
||||
@@ -662,31 +702,48 @@
|
||||
if (invitation.inviteStatus !== 'CANCELED') selected[invitation.inviteeUserId] = true;
|
||||
});
|
||||
if (!container) return;
|
||||
container.innerHTML = options.length ? options.map(function (option) {
|
||||
if (!options.length) {
|
||||
setStatus('[data-ceremony-invitee-options]', '暂无已绑定账号的正常成员', 'empty');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = options.map(function (option) {
|
||||
var label = option.memberName || option.appUserNickName || '家谱成员';
|
||||
return '<label class="editor-choice"><input type="checkbox" data-invitee-user-id="' +
|
||||
escapeHtml(option.appUserId) + '"' + (selected[option.appUserId] ? ' checked' : '') +
|
||||
' /> <span>' + escapeHtml(label) + '</span></label>';
|
||||
}).join('') : '<div class="api-empty">暂无已绑定账号的正常成员</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadInvitees(api, genealogyId, ceremonyId) {
|
||||
var results = await Promise.all([
|
||||
var results;
|
||||
var options;
|
||||
var invitations;
|
||||
var list = query('[data-ceremony-invitation-list]');
|
||||
|
||||
setStatus('[data-ceremony-invitation-list]', '正在加载邀请记录…', 'loading');
|
||||
setStatus('[data-ceremony-invitee-options]', '正在加载成员选项…', 'loading');
|
||||
results = await Promise.all([
|
||||
api.genealogyMemberOptions(genealogyId),
|
||||
api.ceremonyInvitations(genealogyId, ceremonyId)
|
||||
]);
|
||||
var options = normalizeList(results[0], normalizeInviteeOption);
|
||||
var invitations = normalizeList(results[1], normalizeInvitation);
|
||||
var list = query('[data-ceremony-invitation-list]');
|
||||
options = normalizeList(results[0], normalizeInviteeOption);
|
||||
invitations = normalizeList(results[1], normalizeInvitation);
|
||||
|
||||
inviteeOptionsByUserId = Object.create(null);
|
||||
options.forEach(function (option) { inviteeOptionsByUserId[option.appUserId] = option; });
|
||||
if (!Array.isArray(results[0]) || (results[0].length && !options.length)) {
|
||||
if (query('[data-ceremony-invitee-options]')) query('[data-ceremony-invitee-options]').innerHTML = '<div class="api-empty">成员选项响应字段不完整</div>';
|
||||
setStatus('[data-ceremony-invitee-options]', '成员选项响应字段不完整', 'error');
|
||||
} else {
|
||||
renderInviteeOptions(options, invitations);
|
||||
}
|
||||
if (list) list.innerHTML = renderInvitationList(results[1]);
|
||||
if (!list) return { options: options, invitations: invitations };
|
||||
if (!Array.isArray(results[1]) || (results[1].length && !invitations.length)) {
|
||||
setStatus('[data-ceremony-invitation-list]', '邀请响应字段不完整', 'error');
|
||||
} else if (!invitations.length) {
|
||||
setStatus('[data-ceremony-invitation-list]', '暂无邀请记录', 'empty');
|
||||
} else {
|
||||
list.innerHTML = renderInvitationList(results[1]);
|
||||
}
|
||||
return { options: options, invitations: invitations };
|
||||
}
|
||||
|
||||
@@ -699,10 +756,17 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
ceremonyId = getCurrentCeremonyId();
|
||||
if (!genealogyId || !ceremonyId) {
|
||||
if (genealogyId && query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = '<div class="api-empty">缺少有效活动编号。</div>';
|
||||
if (genealogyId) {
|
||||
setStatus('[data-ceremony-detail]', '缺少有效活动编号。', 'error');
|
||||
setStatus('[data-ceremony-gift-list]', '缺少有效活动编号。', 'empty');
|
||||
setStatus('[data-ceremony-invitation-list]', '缺少有效活动编号。', 'empty');
|
||||
setStatus('[data-ceremony-invitee-options]', '缺少有效活动编号。', 'empty');
|
||||
}
|
||||
return;
|
||||
}
|
||||
syncLinks();
|
||||
setStatus('[data-ceremony-detail]', '正在加载活动详情…', 'loading');
|
||||
setStatus('[data-ceremony-gift-list]', '正在加载祭品…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
currentCeremony = normalizeCeremony(await api.ceremonyDetail(genealogyId, ceremonyId));
|
||||
@@ -710,13 +774,62 @@
|
||||
if (query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = renderCeremonyDetail(currentCeremony);
|
||||
if (query('[data-ceremony-detail-title]')) query('[data-ceremony-detail-title]').textContent = currentCeremony.ceremonyTitle;
|
||||
if (query('[data-ceremony-editor-action]')) query('[data-ceremony-editor-action]').href = buildEditUrl(genealogyId, ceremonyId);
|
||||
await loadGifts(api, genealogyId, ceremonyId);
|
||||
if (canEditContent) await loadInvitees(api, genealogyId, ceremonyId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-ceremony-detail]')) query('[data-ceremony-detail]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该活动。' : escapeHtml(error.message || '活动详情加载失败')) + '</div>';
|
||||
setStatus(
|
||||
'[data-ceremony-detail]',
|
||||
isForbidden(error) ? '当前账号无权查看该活动。' : (error.message || '活动详情加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
setStatus(
|
||||
'[data-ceremony-gift-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该活动。' : '祭品加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
setStatus(
|
||||
'[data-ceremony-invitation-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该活动。' : '邀请记录加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
setStatus(
|
||||
'[data-ceremony-invitee-options]',
|
||||
isForbidden(error) ? '当前账号无权查看该活动。' : '成员选项加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该活动。' : (error.message || '活动详情加载失败'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadGifts(api, genealogyId, ceremonyId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(
|
||||
'[data-ceremony-gift-list]',
|
||||
isForbidden(error) ? '当前账号无权查看该活动祭品。' : (error.message || '祭品加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该活动祭品。' : (error.message || '祭品加载失败'));
|
||||
}
|
||||
if (!canEditContent) {
|
||||
setStatus('[data-ceremony-invitation-list]', '当前账号无权查看或维护受邀名单。', 'forbidden');
|
||||
setStatus('[data-ceremony-invitee-options]', '当前账号无权查看或维护受邀名单。', 'forbidden');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadInvitees(api, genealogyId, ceremonyId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(
|
||||
'[data-ceremony-invitation-list]',
|
||||
isForbidden(error) ? '当前账号无权查看或维护受邀名单。' : (error.message || '邀请记录加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
setStatus(
|
||||
'[data-ceremony-invitee-options]',
|
||||
isForbidden(error) ? '当前账号无权查看或维护受邀名单。' : '成员选项加载失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看或维护受邀名单。' : (error.message || '邀请记录加载失败'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,11 +843,12 @@
|
||||
var savedId;
|
||||
var verified;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
|
||||
body = buildCeremonyBody(getFormValues(form));
|
||||
validation = validateCeremonyBody(body);
|
||||
if (validation) {
|
||||
setStatus('[data-ceremony-form-status]', validation);
|
||||
setStatus('[data-ceremony-form-status]', validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidLongitude;
|
||||
@@ -742,7 +856,7 @@
|
||||
delete body.invalidCoverOssId;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setStatus('[data-ceremony-form-status]', '正在保存活动…');
|
||||
setStatus('[data-ceremony-form-status]', '正在保存活动…', 'loading');
|
||||
try {
|
||||
saved = normalizeCeremony(ceremonyId
|
||||
? await api.updateCeremony(genealogyId, ceremonyId, body)
|
||||
@@ -754,7 +868,11 @@
|
||||
if (root.location) root.location.href = buildDetailUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-ceremony-form-status]', isForbidden(error) ? '当前账号无权保存该活动。' : (error.message || '活动保存失败'));
|
||||
setStatus(
|
||||
'[data-ceremony-form-status]',
|
||||
isForbidden(error) ? '当前账号无权保存该活动。' : (error.message || '活动保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -768,16 +886,16 @@
|
||||
var validation;
|
||||
var saved;
|
||||
|
||||
if (writePending || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
|
||||
body = buildCeremonyGiftBody(getFormValues(form));
|
||||
validation = validateCeremonyGiftBody(body);
|
||||
if (validation) {
|
||||
setStatus('[data-ceremony-gift-status]', validation);
|
||||
setStatus('[data-ceremony-gift-status]', validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidGiftAmount;
|
||||
setWritePending(true);
|
||||
setStatus('[data-ceremony-gift-status]', '正在添加祭品…');
|
||||
setStatus('[data-ceremony-gift-status]', '正在添加祭品…', 'loading');
|
||||
try {
|
||||
saved = normalizeCeremonyGift(await api.createCeremonyGift(genealogyId, ceremonyId, body));
|
||||
if (!saved || saved.ceremonyId !== ceremonyId) throw new Error('添加祭品响应缺少稳定 giftId');
|
||||
@@ -788,7 +906,11 @@
|
||||
setStatus('[data-ceremony-gift-status]', '祭品已添加');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-ceremony-gift-status]', isForbidden(error) ? '当前账号无权添加祭品。' : (error.message || '祭品添加失败'));
|
||||
setStatus(
|
||||
'[data-ceremony-gift-status]',
|
||||
isForbidden(error) ? '当前账号无权添加祭品。' : (error.message || '祭品添加失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -800,19 +922,23 @@
|
||||
var ceremonyId = getCurrentCeremonyId();
|
||||
var body;
|
||||
|
||||
if (writePending || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !ceremonyId || redirectUnauthorized(api)) return;
|
||||
body = buildInviteesBody(queryAll('[data-invitee-user-id]:checked').map(function (input) {
|
||||
return input.getAttribute('data-invitee-user-id');
|
||||
}));
|
||||
setWritePending(true);
|
||||
setStatus('[data-ceremony-invitee-status]', '正在更新受邀人…');
|
||||
setStatus('[data-ceremony-invitee-status]', '正在更新受邀人…', 'loading');
|
||||
try {
|
||||
await api.replaceCeremonyInvitees(genealogyId, ceremonyId, body.inviteeUserIds);
|
||||
await loadInvitees(api, genealogyId, ceremonyId);
|
||||
setStatus('[data-ceremony-invitee-status]', '受邀名单已更新');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-ceremony-invitee-status]', isForbidden(error) ? '当前账号无权维护受邀名单。' : (error.message || '受邀名单更新失败'));
|
||||
setStatus(
|
||||
'[data-ceremony-invitee-status]',
|
||||
isForbidden(error) ? '当前账号无权维护受邀名单。' : (error.message || '受邀名单更新失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -823,7 +949,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var ceremony = ceremoniesById[ceremonyId] || currentCeremony;
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(ceremonyId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(ceremonyId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (ceremony ? ceremony.ceremonyTitle : '该活动') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -844,7 +970,7 @@
|
||||
var ceremonyId = getCurrentCeremonyId();
|
||||
var gift = giftsById[giftId];
|
||||
|
||||
if (writePending || !genealogyId || !ceremonyId || !normalizeId(giftId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !ceremonyId || !normalizeId(giftId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (gift && (gift.giverName || gift.giverNickName) || '该祭品') + '”的记录吗?')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -911,6 +1037,7 @@
|
||||
normalizeInvitation: normalizeInvitation,
|
||||
normalizeInviteeOption: normalizeInviteeOption,
|
||||
buildInviteesBody: buildInviteesBody,
|
||||
canEditCeremonies: canEditCeremonies,
|
||||
renderCeremonyDetail: renderCeremonyDetail,
|
||||
renderGiftList: renderGiftList,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
|
||||
+46
-12
@@ -40,6 +40,10 @@
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
@@ -171,7 +175,7 @@
|
||||
function renderInvitationDetail(invitation) {
|
||||
var mapLink = '';
|
||||
|
||||
if (!invitation) return '<div class="api-empty">请选择一条邀请查看详情</div>';
|
||||
if (!invitation) return '';
|
||||
if (invitation.longitude !== undefined && invitation.latitude !== undefined) {
|
||||
mapLink = ' <a class="pill" target="_blank" rel="noopener noreferrer" href="https://uri.amap.com/marker?position=' +
|
||||
encodeURIComponent(invitation.longitude + ',' + invitation.latitude) + '&name=' +
|
||||
@@ -194,7 +198,12 @@
|
||||
var container = query('[data-my-invitation-detail]');
|
||||
|
||||
selectedInvitationId = invitation ? invitation.invitationId : '';
|
||||
if (container) container.innerHTML = renderInvitationDetail(invitation);
|
||||
if (!container) return;
|
||||
if (!invitation) {
|
||||
setInvitationState('[data-my-invitation-detail]', 'empty', '请选择一条邀请查看详情');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = renderInvitationDetail(invitation);
|
||||
}
|
||||
|
||||
function renderInvitations(data) {
|
||||
@@ -209,12 +218,12 @@
|
||||
});
|
||||
if (!container) return invitations;
|
||||
if (invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">邀请响应缺少稳定 ID 或合法状态,请联系后端核对 DTO。</div>';
|
||||
renderDetail(null);
|
||||
setInvitationState('[data-my-invitation-list]', 'error', '邀请响应缺少稳定 ID 或合法状态,请联系后端核对 DTO。');
|
||||
setInvitationState('[data-my-invitation-detail]', 'error', '邀请详情暂不可用,请联系后端核对 DTO。');
|
||||
return [];
|
||||
}
|
||||
if (!invitations.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无贺礼邀请</div>';
|
||||
setInvitationState('[data-my-invitation-list]', 'empty', '暂无贺礼邀请');
|
||||
renderDetail(null);
|
||||
return [];
|
||||
}
|
||||
@@ -234,23 +243,41 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setListState(message) {
|
||||
var container = query('[data-my-invitation-list]');
|
||||
function setInvitationState(selector, type, message) {
|
||||
var container = query(selector);
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">' + escapeHtml(message) + '</div>';
|
||||
if (!container) return;
|
||||
if (!message) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.textContent = message;
|
||||
}
|
||||
|
||||
async function loadMyInvitations() {
|
||||
var api = getApi();
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
setListState('正在加载我的邀请…');
|
||||
setInvitationState('[data-my-invitation-list]', 'loading', '正在加载我的邀请…');
|
||||
setInvitationState('[data-my-invitation-detail]', 'loading', '正在加载邀请详情…');
|
||||
try {
|
||||
renderInvitations(await api.myCeremonyInvitations());
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setListState(Number(error && (error.status || error.code)) === 403 ? '当前账号无权查看邀请' : (error.message || '邀请加载失败'));
|
||||
renderDetail(null);
|
||||
setInvitationState(
|
||||
'[data-my-invitation-list]',
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看邀请。' : (error.message || '邀请加载失败')
|
||||
);
|
||||
setInvitationState(
|
||||
'[data-my-invitation-detail]',
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看邀请。' : '邀请详情加载失败,请稍后重试。'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,13 +290,19 @@
|
||||
if (writePending || redirectUnauthorized(api) || !invitation || !canRespondToInvitation(invitation)) return;
|
||||
if (validation) return;
|
||||
setWritePending(true);
|
||||
setInvitationState('[data-my-invitation-status]', 'loading', '正在更新邀请状态…');
|
||||
try {
|
||||
await api.respondCeremonyInvitation(invitation.genealogyId, invitation.ceremonyId, body);
|
||||
selectedInvitationId = invitation.invitationId;
|
||||
await loadMyInvitations();
|
||||
setInvitationState('[data-my-invitation-status]', '', '邀请状态已更新。');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setListState(Number(error && (error.status || error.code)) === 403 ? '当前账号无权响应该邀请' : (error.message || '邀请响应失败'));
|
||||
setInvitationState(
|
||||
'[data-my-invitation-status]',
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权响应该邀请。' : (error.message || '邀请响应失败')
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -310,6 +343,7 @@
|
||||
renderInvitationRow: renderInvitationRow,
|
||||
renderInvitationDetail: renderInvitationDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
loadMyInvitations: loadMyInvitations,
|
||||
init: init
|
||||
};
|
||||
|
||||
@@ -94,6 +94,10 @@
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
async function loadFamilyHome(api, genealogyId) {
|
||||
var normalizedId = normalizeId(genealogyId);
|
||||
var results;
|
||||
@@ -135,11 +139,38 @@
|
||||
if (element) element.textContent = value;
|
||||
}
|
||||
|
||||
function setHomeState(selector, type, message) {
|
||||
var element = query(selector);
|
||||
|
||||
if (!element) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(element, type, message);
|
||||
return;
|
||||
}
|
||||
element.textContent = message;
|
||||
}
|
||||
|
||||
function setHomeStatus(message, type) {
|
||||
var element = query('[data-family-home-status]');
|
||||
|
||||
if (!element) return;
|
||||
if (!message) {
|
||||
element.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(element, type, message);
|
||||
return;
|
||||
}
|
||||
element.textContent = message;
|
||||
}
|
||||
|
||||
async function initFamilyHomePage() {
|
||||
var page = query('[data-family-home-page]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId;
|
||||
var result;
|
||||
var treeHtml;
|
||||
|
||||
if (!page) return;
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
@@ -157,36 +188,72 @@
|
||||
root.ProfileUI.syncGenealogyContextLinks();
|
||||
}
|
||||
if (!root.LineagePages || !root.LineagePages.renderLineageTreeHtml) {
|
||||
setText('[data-family-home-status]', '世系预览初始化失败,请刷新后重试');
|
||||
setHomeState('[data-family-home-overview]', 'error', '家谱主页初始化失败,请刷新后重试。');
|
||||
setHomeState('[data-lineage-home-tree]', 'error', '世系预览初始化失败,请刷新后重试。');
|
||||
setHomeStatus('世系预览初始化失败,请刷新后重试。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setHomeState('[data-family-home-overview]', 'loading', '正在加载家谱概览…');
|
||||
setHomeState('[data-lineage-home-tree]', 'loading', '正在加载世系树…');
|
||||
setHomeStatus('正在读取家谱概览…', 'loading');
|
||||
try {
|
||||
result = await loadFamilyHome(api, genealogyId);
|
||||
result = normalizeFamilyOverview(await api.genealogyOverview(genealogyId), genealogyId);
|
||||
if (!result) throw new Error('家谱概览响应无效');
|
||||
if (root.ProfileUI && root.ProfileUI.setCurrentGenealogyContext) {
|
||||
root.ProfileUI.setCurrentGenealogyContext(result.overview.genealogyId, result.overview.genealogyName);
|
||||
root.ProfileUI.setCurrentGenealogyContext(result.genealogyId, result.genealogyName);
|
||||
}
|
||||
setText('[data-family-home-title]', result.overview.genealogyName);
|
||||
setText('[data-family-home-context]', result.overview.genealogyName);
|
||||
query('[data-family-home-overview]').innerHTML = renderFamilyOverview(result.overview);
|
||||
setText('[data-family-home-member-count]', String(result.overview.memberCount));
|
||||
setText('[data-family-home-person-count]', String(result.overview.personCount));
|
||||
setText('[data-family-home-title]', result.genealogyName);
|
||||
setText('[data-family-home-context]', result.genealogyName);
|
||||
query('[data-family-home-overview]').innerHTML = renderFamilyOverview(result);
|
||||
setText('[data-family-home-member-count]', String(result.memberCount));
|
||||
setText('[data-family-home-person-count]', String(result.personCount));
|
||||
queryAll('[data-family-home-management]').forEach(function (element) {
|
||||
element.hidden = !shouldShowFamilyManagement(result.overview);
|
||||
element.hidden = !shouldShowFamilyManagement(result);
|
||||
});
|
||||
query('[data-lineage-home-tree]').innerHTML =
|
||||
root.LineagePages.renderLineageTreeHtml(result.tree);
|
||||
setText('[data-family-home-status]', '家谱概览已更新');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
query('[data-family-home-overview]').innerHTML =
|
||||
'<div class="api-empty">家谱概览读取失败,请稍后重试</div>';
|
||||
query('[data-lineage-home-tree]').innerHTML =
|
||||
'<div class="api-empty">世系树读取失败,请稍后重试</div>';
|
||||
setText('[data-family-home-status]', error.message || '家谱主页读取失败,请稍后重试');
|
||||
setHomeState(
|
||||
'[data-family-home-overview]',
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱。' : '家谱概览读取失败,请稍后重试。'
|
||||
);
|
||||
setHomeState(
|
||||
'[data-lineage-home-tree]',
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱。' : '世系树读取失败,请稍后重试。'
|
||||
);
|
||||
setHomeStatus(
|
||||
isForbidden(error) ? '当前账号无权查看该家谱。' : (error.message || '家谱主页读取失败,请稍后重试。'),
|
||||
isForbidden(error) ? 'forbidden' : 'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
result = await api.lineageTree(genealogyId);
|
||||
treeHtml = Array.isArray(result) ? root.LineagePages.renderLineageTreeHtml(result) : '';
|
||||
if (!treeHtml) {
|
||||
setHomeState('[data-lineage-home-tree]', 'empty', '暂无世系树');
|
||||
} else {
|
||||
query('[data-lineage-home-tree]').innerHTML = treeHtml;
|
||||
}
|
||||
setHomeStatus('家谱概览已更新');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setHomeState(
|
||||
'[data-lineage-home-tree]',
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看该家谱世系树。' : '世系树读取失败,请稍后重试。'
|
||||
);
|
||||
setHomeStatus(
|
||||
isForbidden(error) ? '当前账号无权查看该家谱世系树。' : (error.message || '世系树读取失败,请稍后重试。'),
|
||||
isForbidden(error) ? 'forbidden' : 'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +266,7 @@
|
||||
renderFamilyOverview: renderFamilyOverview,
|
||||
shouldShowFamilyManagement: shouldShowFamilyManagement,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
loadFamilyHome: loadFamilyHome,
|
||||
initFamilyHomePage: initFamilyHomePage,
|
||||
init: init
|
||||
|
||||
+307
-59
@@ -65,6 +65,125 @@
|
||||
if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function getApiStateType(error) {
|
||||
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setFeedFormStatus(message, type) {
|
||||
var target = query('[data-feed-form-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setFeedContainerState(selector, type, message) {
|
||||
var target = query(selector);
|
||||
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.innerHTML = '<div class="api-state api-state--' + type + '" data-api-state="' + type + '">' +
|
||||
escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setFeedFormEnabled(enabled) {
|
||||
var form = query('[data-feed-form]');
|
||||
|
||||
if (form) {
|
||||
queryAll('input, textarea, select, button', form).forEach(function (field) {
|
||||
field.disabled = !enabled;
|
||||
});
|
||||
}
|
||||
queryAll('[data-feed-editor-action]').forEach(function (action) {
|
||||
action.hidden = !enabled;
|
||||
action.disabled = !enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function setFeedSubmitPending(pending) {
|
||||
var selectors = '[data-feed-form] [type="submit"], [type="submit"][form="feed-edit-form"]';
|
||||
|
||||
queryAll(selectors).forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = Boolean(pending);
|
||||
button.textContent = pending ? '正在保存…' : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function setFeedCommentFormStatus(message, type) {
|
||||
var target = query('[data-feed-comment-form-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setFeedCommentFormEnabled(enabled) {
|
||||
var form = query('[data-feed-comment-form]');
|
||||
|
||||
if (!form) return;
|
||||
queryAll('input, textarea, button', form).forEach(function (field) {
|
||||
field.disabled = !enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function setFeedCommentSubmitPending(pending) {
|
||||
queryAll('[data-feed-comment-form] [type="submit"]').forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.textContent = pending ? '正在发布…' : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function setFeedCommentTarget(parentCommentId) {
|
||||
var form = query('[data-feed-comment-form]');
|
||||
var target = query('[data-feed-comment-form-target]');
|
||||
var parent = parentCommentId ? toId(parentCommentId) : '';
|
||||
|
||||
if (!form) return;
|
||||
if (query('[name="parentCommentId"]', form)) query('[name="parentCommentId"]', form).value = parent;
|
||||
if (target) target.textContent = parent ? '正在回复这条评论' : '发表公开评论';
|
||||
}
|
||||
|
||||
function openFeedCommentForm(feedId, parentCommentId) {
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
var form;
|
||||
var submitButton;
|
||||
|
||||
if (!feedId || !genealogyId) return;
|
||||
if (!query('[data-feed-detail-page]')) {
|
||||
if (root.location) root.location.href = buildFeedUrl('detail', genealogyId, feedId);
|
||||
return;
|
||||
}
|
||||
if (getCurrentFeedId() !== toId(feedId)) return;
|
||||
form = query('[data-feed-comment-form]');
|
||||
submitButton = form && query('[type="submit"]', form);
|
||||
if (!form || !submitButton || submitButton.disabled) return;
|
||||
setFeedCommentTarget(parentCommentId);
|
||||
setFeedCommentFormStatus('');
|
||||
if (query('[name="commentContent"]', form) && query('[name="commentContent"]', form).focus) {
|
||||
query('[name="commentContent"]', form).focus();
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryParam(search, name) {
|
||||
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
||||
|
||||
@@ -280,6 +399,39 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderFeedState(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) {
|
||||
return root.ProfileUI.renderApiState(type, message);
|
||||
}
|
||||
return '<div class="api-state api-state--' + escapeHtml(type) + '" data-api-state="' +
|
||||
escapeHtml(type) + '">' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function replaceInlineCommentList(feedId, content) {
|
||||
var row = query('[data-feed-id="' + feedId + '"]');
|
||||
var previous = row && row.nextElementSibling;
|
||||
|
||||
if (!row) return;
|
||||
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
|
||||
row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
|
||||
}
|
||||
|
||||
function replaceReplyList(parentCommentId, content) {
|
||||
var row = query('[data-feed-comment-node="' + parentCommentId + '"]');
|
||||
var previous = row && row.nextElementSibling;
|
||||
|
||||
if (!row) return;
|
||||
if (previous && previous.getAttribute('data-feed-reply-list') === parentCommentId) previous.remove();
|
||||
row.insertAdjacentHTML('afterend', content);
|
||||
}
|
||||
|
||||
function setReplyState(parentCommentId, type, message) {
|
||||
replaceReplyList(
|
||||
parentCommentId,
|
||||
'<div data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + renderFeedState(type, message) + '</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function getListUrl(genealogyId, feedId) {
|
||||
return buildFeedUrl(feedId ? 'edit' : 'list', genealogyId, feedId);
|
||||
}
|
||||
@@ -323,11 +475,11 @@
|
||||
|
||||
if (!container) return;
|
||||
if (invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。</div>';
|
||||
setFeedContainerState('[data-feed-list]', 'error', '动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
|
||||
return;
|
||||
}
|
||||
if (!feeds.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无家族动态</div>';
|
||||
setFeedContainerState('[data-feed-list]', 'empty', '暂无家族动态');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -393,21 +545,19 @@
|
||||
var row = query('[data-feed-id="' + feedId + '"]');
|
||||
var result = normalizeComments(data);
|
||||
var list = result.comments;
|
||||
var previous = row && row.nextElementSibling;
|
||||
var content;
|
||||
|
||||
if (!row && !detailContainer) return;
|
||||
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
|
||||
if (result.invalidCount) {
|
||||
content = '<div class="api-empty">评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>';
|
||||
if (detailContainer) detailContainer.innerHTML = content;
|
||||
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
|
||||
content = renderFeedState('error', '评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。');
|
||||
if (detailContainer) setFeedContainerState('[data-feed-detail-comments]', 'error', '评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。');
|
||||
else replaceInlineCommentList(feedId, content);
|
||||
return;
|
||||
}
|
||||
if (!list.length) {
|
||||
content = '<div class="api-empty">暂无评论</div>';
|
||||
if (detailContainer) detailContainer.innerHTML = content;
|
||||
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
|
||||
content = renderFeedState('empty', '暂无评论');
|
||||
if (detailContainer) setFeedContainerState('[data-feed-detail-comments]', 'empty', '暂无评论');
|
||||
else replaceInlineCommentList(feedId, content);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -415,26 +565,24 @@
|
||||
return renderCommentRow(feedId, comment);
|
||||
}).join('') + '</div>' + renderPageControls('comments', feedId, '', currentCommentPages[feedId] || 1, getPageTotal(data));
|
||||
if (detailContainer) detailContainer.innerHTML = content;
|
||||
else row.insertAdjacentHTML('afterend', '<div data-feed-comment-list="' + escapeHtml(feedId) + '">' + content + '</div>');
|
||||
else replaceInlineCommentList(feedId, content);
|
||||
}
|
||||
|
||||
function renderCommentReplies(feedId, parentCommentId, data) {
|
||||
var row = query('[data-feed-comment-node="' + parentCommentId + '"]');
|
||||
var result = normalizeComments(data);
|
||||
var previous = row && row.nextElementSibling;
|
||||
|
||||
if (!row) return;
|
||||
if (previous && previous.getAttribute('data-feed-reply-list') === parentCommentId) previous.remove();
|
||||
if (result.invalidCount) {
|
||||
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">回复响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>');
|
||||
setReplyState(parentCommentId, 'error', '回复响应缺少 commentId 或 commentContent,请联系后端补充 DTO。');
|
||||
return;
|
||||
}
|
||||
if (!result.comments.length) {
|
||||
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">暂无直接回复</div>');
|
||||
setReplyState(parentCommentId, 'empty', '暂无直接回复');
|
||||
return;
|
||||
}
|
||||
|
||||
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + result.comments.map(function (comment) {
|
||||
replaceReplyList(parentCommentId, '<div class="module-list" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + result.comments.map(function (comment) {
|
||||
return renderCommentRow(feedId, comment);
|
||||
}).join('') + renderPageControls('replies', feedId, parentCommentId, currentReplyPages[parentCommentId] || 1, getPageTotal(data)) + '</div>');
|
||||
}
|
||||
@@ -471,11 +619,21 @@
|
||||
});
|
||||
}
|
||||
|
||||
function canEditFeeds(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function setFeedContentEditingEnabled(enabled) {
|
||||
queryAll('[data-feed-create-link]').forEach(function (link) {
|
||||
link.hidden = !enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeViewerContext(genealogy, profile) {
|
||||
return {
|
||||
userId: toId(profile && profile.userId),
|
||||
canEditContent: genealogy && genealogy.canEditContent === true,
|
||||
canManage: genealogy && genealogy.canManage === true
|
||||
canEditContent: canEditFeeds(genealogy),
|
||||
canManage: Boolean(genealogy && genealogy.canManage === true)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -494,6 +652,7 @@
|
||||
profile = null;
|
||||
}
|
||||
viewerContext = normalizeViewerContext(genealogy, profile);
|
||||
setFeedContentEditingEnabled(viewerContext.canEditContent);
|
||||
return viewerContext;
|
||||
}
|
||||
|
||||
@@ -544,7 +703,11 @@
|
||||
|
||||
if (!genealogyId) {
|
||||
container = query('[data-feed-list]');
|
||||
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入家族圈动态</div>';
|
||||
if (container) setFeedContainerState('[data-feed-list]', 'forbidden', '请从具体家谱进入家族圈动态。');
|
||||
setFeedFormEnabled(false);
|
||||
setFeedFormStatus('请从具体家谱进入动态发布。', 'forbidden');
|
||||
setFeedCommentFormEnabled(false);
|
||||
setFeedCommentFormStatus('请从具体家谱进入动态详情后发表评论。', 'forbidden');
|
||||
showMessage('请从具体家谱进入家族圈动态');
|
||||
}
|
||||
return genealogyId;
|
||||
@@ -560,6 +723,7 @@
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
currentFeedPage = buildPageQuery(pageNum || getQueryParam(root.location && root.location.search, 'pageNum')).pageNum;
|
||||
setFeedContainerState('[data-feed-list]', 'loading', '正在加载家族动态…');
|
||||
try {
|
||||
await loadViewerContext(api, genealogyId);
|
||||
data = await api.feedsPage(genealogyId, buildPageQuery(currentFeedPage));
|
||||
@@ -567,7 +731,11 @@
|
||||
updateListPager(data);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-feed-list]')) query('[data-feed-list]').innerHTML = '<div class="api-empty">家族动态加载失败,请稍后重试。</div>';
|
||||
setFeedContainerState(
|
||||
'[data-feed-list]',
|
||||
getApiStateType(error),
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看该家谱的动态。' : '家族动态加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(error.message || '家族动态加载失败');
|
||||
}
|
||||
}
|
||||
@@ -581,19 +749,33 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
setFeedFormEnabled(false);
|
||||
setFeedFormStatus('正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadViewerContext(api, genealogyId);
|
||||
if (!viewerContext.canEditContent) {
|
||||
setFeedFormStatus('当前账号无权发布或编辑该家谱的动态。', 'forbidden');
|
||||
return;
|
||||
}
|
||||
setFeedFormEnabled(true);
|
||||
if (query('[data-feed-advanced]')) {
|
||||
query('[data-feed-advanced]').hidden = !viewerContext.canManage;
|
||||
queryAll('[name]', query('[data-feed-advanced]')).forEach(function (field) {
|
||||
field.disabled = !viewerContext.canManage;
|
||||
});
|
||||
}
|
||||
if (!feedId) return;
|
||||
if (!feedId) {
|
||||
setFeedFormStatus('');
|
||||
return;
|
||||
}
|
||||
fillFeedForm(await api.feedDetail(genealogyId, feedId));
|
||||
setFeedFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '动态详情加载失败');
|
||||
setFeedFormStatus(
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权维护该家谱的动态。' : (error.message || '动态详情加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,21 +786,35 @@
|
||||
var feed;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
setFeedCommentFormEnabled(false);
|
||||
setFeedCommentFormStatus('正在读取动态评论权限…', 'loading');
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) {
|
||||
if (query('[data-feed-detail]')) query('[data-feed-detail]').innerHTML = '<div class="api-empty">动态链接缺少有效编号</div>';
|
||||
setFeedContainerState('[data-feed-detail]', 'error', '动态链接缺少有效编号。');
|
||||
setFeedCommentFormStatus('动态链接缺少有效编号。', 'error');
|
||||
return;
|
||||
}
|
||||
syncGenealogyLinks(genealogyId);
|
||||
setFeedContainerState('[data-feed-detail]', 'loading', '正在加载动态详情…');
|
||||
try {
|
||||
await loadViewerContext(api, genealogyId);
|
||||
feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
|
||||
if (!feed) throw new Error('动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
|
||||
renderFeedDetail(feed, genealogyId);
|
||||
setFeedCommentFormEnabled(true);
|
||||
setFeedCommentFormStatus('');
|
||||
await showComments(feedId, currentCommentPages[feedId] || 1);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-feed-detail]')) query('[data-feed-detail]').innerHTML = '<div class="api-empty">动态详情加载失败,请返回列表重试。</div>';
|
||||
setFeedContainerState(
|
||||
'[data-feed-detail]',
|
||||
getApiStateType(error),
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看该动态。' : '动态详情加载失败,请返回列表重试。'
|
||||
);
|
||||
setFeedCommentFormStatus(
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权评论该动态。' : '动态详情加载失败,暂不能发表评论。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
showMessage(error.message || '动态详情加载失败');
|
||||
}
|
||||
}
|
||||
@@ -632,24 +828,28 @@
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
if (!viewerContext.canEditContent) {
|
||||
setFeedFormStatus('当前账号无权发布或编辑该家谱的动态。', 'forbidden');
|
||||
return;
|
||||
}
|
||||
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
|
||||
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
|
||||
try {
|
||||
body = buildFeedBody(getFormValues(form));
|
||||
} catch (error) {
|
||||
showMessage(error.message || '动态表单校验失败');
|
||||
setFeedFormStatus(error.message || '动态表单校验失败', 'error');
|
||||
return;
|
||||
}
|
||||
if (!body.feedContent) {
|
||||
showMessage('请填写动态内容');
|
||||
setFeedFormStatus('请填写动态内容', 'error');
|
||||
return;
|
||||
}
|
||||
return withActionLock('save:' + (feedId || 'new'), async function () {
|
||||
var saved;
|
||||
var savedFeedId;
|
||||
var buttons = queryAll('[type="submit"]', form);
|
||||
|
||||
buttons.forEach(function (button) { button.disabled = true; });
|
||||
setFeedSubmitPending(true);
|
||||
setFeedFormStatus('正在保存动态…', 'loading');
|
||||
try {
|
||||
if (feedId) {
|
||||
saved = await api.updateFeed(genealogyId, feedId, body);
|
||||
@@ -665,9 +865,12 @@
|
||||
root.location.href = buildPostSaveUrl(genealogyId, savedFeedId, body.status);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '动态保存失败');
|
||||
setFeedFormStatus(
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权保存该家谱的动态。' : (error.message || '动态保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
buttons.forEach(function (button) { button.disabled = false; });
|
||||
setFeedSubmitPending(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -723,6 +926,11 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
currentCommentPages[feedId] = buildPageQuery(pageNum).pageNum;
|
||||
if (query('[data-feed-detail-comments]')) {
|
||||
setFeedContainerState('[data-feed-detail-comments]', 'loading', '正在加载评论…');
|
||||
} else {
|
||||
replaceInlineCommentList(feedId, renderFeedState('loading', '正在加载评论…'));
|
||||
}
|
||||
try {
|
||||
renderComments(
|
||||
feedId,
|
||||
@@ -731,7 +939,19 @@
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-feed-detail-comments]')) {
|
||||
query('[data-feed-detail-comments]').innerHTML = '<div class="api-empty">评论加载失败,请稍后重试。</div>';
|
||||
setFeedContainerState(
|
||||
'[data-feed-detail-comments]',
|
||||
getApiStateType(error),
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看评论。' : '评论加载失败,请稍后重试。'
|
||||
);
|
||||
} else {
|
||||
replaceInlineCommentList(
|
||||
feedId,
|
||||
renderFeedState(
|
||||
getApiStateType(error),
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看评论。' : '评论加载失败,请稍后重试。'
|
||||
)
|
||||
);
|
||||
}
|
||||
showMessage(error.message || '评论加载失败');
|
||||
}
|
||||
@@ -745,6 +965,7 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId || !commentId) return;
|
||||
currentReplyPages[commentId] = buildPageQuery(pageNum).pageNum;
|
||||
setReplyState(commentId, 'loading', '正在加载回复…');
|
||||
try {
|
||||
renderCommentReplies(
|
||||
feedId,
|
||||
@@ -758,46 +979,60 @@
|
||||
);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-feed-comment-node="' + commentId + '"]')) {
|
||||
query('[data-feed-comment-node="' + commentId + '"]').insertAdjacentHTML(
|
||||
'afterend',
|
||||
'<div class="api-empty">回复加载失败,请稍后重试。</div>'
|
||||
);
|
||||
}
|
||||
setReplyState(
|
||||
commentId,
|
||||
getApiStateType(error),
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看回复。' : '回复加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(error.message || '回复加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function createComment(feedId, parentCommentId) {
|
||||
async function submitComment(form) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var content;
|
||||
var feedId = getCurrentFeedId();
|
||||
var body;
|
||||
var values;
|
||||
var forbidden = false;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (redirectUnauthorized(api) || !form) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
content = root.prompt ? root.prompt(parentCommentId ? '请输入回复内容' : '请输入评论内容', '') : '';
|
||||
body = buildCommentBody({ commentContent: content, parentCommentId: parentCommentId });
|
||||
if (!body.commentContent) return;
|
||||
if (body.commentContent.length > 1000) {
|
||||
showMessage('评论不能超过 1000 字');
|
||||
values = getFormValues(form);
|
||||
body = buildCommentBody(values);
|
||||
if (!body.commentContent) {
|
||||
setFeedCommentFormStatus('请填写评论内容。', 'error');
|
||||
return;
|
||||
}
|
||||
return withActionLock('comment:' + feedId + ':' + (parentCommentId || 'root'), async function () {
|
||||
if (body.commentContent.length > 1000) {
|
||||
setFeedCommentFormStatus('评论不能超过 1000 字。', 'error');
|
||||
return;
|
||||
}
|
||||
return withActionLock('comment:' + feedId + ':' + (body.parentCommentId || 'root'), async function () {
|
||||
setFeedCommentFormEnabled(false);
|
||||
setFeedCommentSubmitPending(true);
|
||||
setFeedCommentFormStatus('正在发布评论…', 'loading');
|
||||
try {
|
||||
await api.createFeedComment(genealogyId, feedId, body);
|
||||
await showComments(feedId, currentCommentPages[feedId] || 1);
|
||||
if (parentCommentId) await showCommentReplies(feedId, parentCommentId, currentReplyPages[parentCommentId] || 1);
|
||||
if (query('[data-feed-detail-page]')) {
|
||||
var feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
|
||||
if (feed) renderFeedDetail(feed, genealogyId);
|
||||
} else {
|
||||
await loadFeeds(currentFeedPage);
|
||||
}
|
||||
if (body.parentCommentId) await showCommentReplies(feedId, body.parentCommentId, currentReplyPages[body.parentCommentId] || 1);
|
||||
form.reset();
|
||||
setFeedCommentTarget('');
|
||||
setFeedCommentFormStatus('');
|
||||
var feed = normalizeFeed(await api.feedDetail(genealogyId, feedId));
|
||||
if (feed) renderFeedDetail(feed, genealogyId);
|
||||
showMessage('评论已发布');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '评论发布失败');
|
||||
forbidden = getApiStateType(error) === 'forbidden';
|
||||
setFeedCommentFormStatus(
|
||||
forbidden ? '当前账号无权评论该动态。' : (error.message || '评论发布失败,请稍后重试。'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setFeedCommentSubmitPending(false);
|
||||
if (!forbidden) setFeedCommentFormEnabled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -832,9 +1067,16 @@
|
||||
documentRef.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('[data-feed-form]');
|
||||
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
submitFeed(form);
|
||||
if (form) {
|
||||
event.preventDefault();
|
||||
submitFeed(form);
|
||||
return;
|
||||
}
|
||||
form = event.target.closest('[data-feed-comment-form]');
|
||||
if (form) {
|
||||
event.preventDefault();
|
||||
submitComment(form);
|
||||
}
|
||||
});
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var target = event.target;
|
||||
@@ -858,13 +1100,18 @@
|
||||
if (target.closest('[data-feed-comment]')) {
|
||||
event.preventDefault();
|
||||
feedId = target.closest('[data-feed-comment]').getAttribute('data-feed-comment');
|
||||
createComment(feedId);
|
||||
openFeedCommentForm(feedId);
|
||||
}
|
||||
if (target.closest('[data-feed-comment-reply]')) {
|
||||
event.preventDefault();
|
||||
commentId = target.closest('[data-feed-comment-reply]').getAttribute('data-feed-comment-reply');
|
||||
feedId = target.closest('[data-feed-comment-reply]').getAttribute('data-feed-id');
|
||||
createComment(feedId, commentId);
|
||||
openFeedCommentForm(feedId, commentId);
|
||||
}
|
||||
if (target.closest('[data-feed-comment-cancel]')) {
|
||||
event.preventDefault();
|
||||
setFeedCommentTarget('');
|
||||
setFeedCommentFormStatus('');
|
||||
}
|
||||
if (target.closest('[data-feed-comment-replies]')) {
|
||||
event.preventDefault();
|
||||
@@ -937,6 +1184,7 @@
|
||||
withActionLock: withActionLock,
|
||||
normalizeFeed: normalizeFeed,
|
||||
normalizeComment: normalizeComment,
|
||||
canEditFeeds: canEditFeeds,
|
||||
normalizeViewerContext: normalizeViewerContext,
|
||||
normalizeComments: normalizeComments,
|
||||
normalizeList: normalizeList,
|
||||
|
||||
+75
-29
@@ -181,6 +181,10 @@
|
||||
else root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
var values = {};
|
||||
|
||||
@@ -190,10 +194,35 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
function setApiState(target, type, message) {
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message || '';
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
var status = root.document && root.document.querySelector('[data-feedback-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
setApiState(status, type, message);
|
||||
}
|
||||
|
||||
function setFeedbackList(list, items, detailPage) {
|
||||
if (!list) return;
|
||||
if (!items.length) {
|
||||
setApiState(list, 'empty', '当前没有反馈记录。');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = renderFeedbackList(items, { detailPage: detailPage || 'ticket-detail.html' });
|
||||
}
|
||||
|
||||
function setPendingButton(button, pending, label) {
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = pending;
|
||||
button.textContent = pending ? label : button.dataset.defaultLabel;
|
||||
}
|
||||
|
||||
async function readMyFeedback(api) {
|
||||
@@ -225,24 +254,32 @@
|
||||
var items;
|
||||
|
||||
if (!list) return [];
|
||||
setStatus('正在读取反馈记录…');
|
||||
setApiState(list, 'loading', '正在读取反馈记录…');
|
||||
setStatus('loading', '正在读取反馈记录…');
|
||||
try {
|
||||
items = await readMyFeedback(api);
|
||||
list.innerHTML = renderFeedbackList(items, { detailPage: 'ticket-detail.html' });
|
||||
setStatus('');
|
||||
setFeedbackList(list, items, 'ticket-detail.html');
|
||||
setStatus('', '');
|
||||
return items;
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return [];
|
||||
}
|
||||
list.innerHTML = '<div class="api-empty">读取反馈记录失败</div>';
|
||||
setStatus(error.message || '读取反馈记录失败,请稍后重试');
|
||||
setApiState(list, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看反馈记录。' : '读取反馈记录失败,请稍后重试。');
|
||||
setStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看反馈记录。' : (error.message || '读取反馈记录失败,请稍后重试。'));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (refresh) refresh.addEventListener('click', loadList);
|
||||
if (refresh) refresh.addEventListener('click', async function () {
|
||||
setPendingButton(refresh, true, '正在刷新…');
|
||||
try {
|
||||
await loadList();
|
||||
} finally {
|
||||
setPendingButton(refresh, false, '正在刷新…');
|
||||
}
|
||||
});
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body;
|
||||
var validation;
|
||||
@@ -255,12 +292,12 @@
|
||||
body = buildFeedbackBody(getFormValues(form));
|
||||
validation = validateFeedbackBody(body);
|
||||
if (validation) {
|
||||
setStatus(validation);
|
||||
setStatus('error', validation);
|
||||
return;
|
||||
}
|
||||
writePending = true;
|
||||
if (button) button.disabled = true;
|
||||
setStatus('正在提交反馈…');
|
||||
setPendingButton(button, true, '正在提交…');
|
||||
setStatus('loading', '正在提交反馈…');
|
||||
try {
|
||||
created = normalizeFeedback(await api.submitFeedback(body));
|
||||
if (!created) throw new Error('提交响应缺少有效反馈信息');
|
||||
@@ -272,18 +309,18 @@
|
||||
if (root.location) root.location.href = buildFeedbackDetailUrl(created.feedbackId);
|
||||
return;
|
||||
}
|
||||
if (list) list.innerHTML = renderFeedbackList(refreshed, { detailPage: 'ticket-detail.html' });
|
||||
setFeedbackList(list, refreshed, 'ticket-detail.html');
|
||||
form.reset();
|
||||
setStatus('反馈已提交');
|
||||
setStatus('', '反馈已提交');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '提交反馈失败,请稍后重试');
|
||||
setStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限提交反馈。' : (error.message || '提交反馈失败,请稍后重试。'));
|
||||
} finally {
|
||||
writePending = false;
|
||||
if (button) button.disabled = false;
|
||||
setPendingButton(button, false, '正在提交…');
|
||||
}
|
||||
});
|
||||
await loadList();
|
||||
@@ -303,22 +340,28 @@
|
||||
|
||||
async function loadList() {
|
||||
try {
|
||||
setStatus('正在读取反馈记录…');
|
||||
list.innerHTML = renderFeedbackList(await readMyFeedback(api), {
|
||||
detailPage: 'ticket-detail.html'
|
||||
});
|
||||
setStatus('');
|
||||
setApiState(list, 'loading', '正在读取反馈记录…');
|
||||
setStatus('loading', '正在读取反馈记录…');
|
||||
setFeedbackList(list, await readMyFeedback(api), 'ticket-detail.html');
|
||||
setStatus('', '');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div class="api-empty">读取反馈记录失败</div>';
|
||||
setStatus(error.message || '读取反馈记录失败,请稍后重试');
|
||||
setApiState(list, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看反馈记录。' : '读取反馈记录失败,请稍后重试。');
|
||||
setStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看反馈记录。' : (error.message || '读取反馈记录失败,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
if (refresh) refresh.addEventListener('click', loadList);
|
||||
if (refresh) refresh.addEventListener('click', async function () {
|
||||
setPendingButton(refresh, true, '正在刷新…');
|
||||
try {
|
||||
await loadList();
|
||||
} finally {
|
||||
setPendingButton(refresh, false, '正在刷新…');
|
||||
}
|
||||
});
|
||||
await loadList();
|
||||
}
|
||||
|
||||
@@ -331,7 +374,7 @@
|
||||
|
||||
if (!page || !detail) return;
|
||||
if (!feedbackId) {
|
||||
detail.innerHTML = '<div class="api-empty">反馈记录不存在或无权查看</div>';
|
||||
setApiState(detail, 'empty', '反馈记录不存在或无权查看。');
|
||||
return;
|
||||
}
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
@@ -339,17 +382,19 @@
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setStatus('正在读取反馈详情…');
|
||||
setApiState(detail, 'loading', '正在读取反馈详情…');
|
||||
setStatus('loading', '正在读取反馈详情…');
|
||||
item = findFeedbackById(await api.myFeedback(), feedbackId);
|
||||
detail.innerHTML = renderFeedbackDetail(item);
|
||||
setStatus(item ? '' : '反馈记录不存在或无权查看');
|
||||
if (item) detail.innerHTML = renderFeedbackDetail(item);
|
||||
else setApiState(detail, 'empty', '反馈记录不存在或无权查看。');
|
||||
setStatus('', item ? '' : '反馈记录不存在或无权查看。');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
detail.innerHTML = '<div class="api-empty">反馈详情读取失败</div>';
|
||||
setStatus(error.message || '反馈详情读取失败,请稍后重试');
|
||||
setApiState(detail, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看该反馈详情。' : '反馈详情读取失败,请稍后重试。');
|
||||
setStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看该反馈详情。' : (error.message || '反馈详情读取失败,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +422,7 @@
|
||||
buildFeedbackDetailUrl: buildFeedbackDetailUrl,
|
||||
renderFeedbackList: renderFeedbackList,
|
||||
renderFeedbackDetail: renderFeedbackDetail,
|
||||
isForbidden: isForbidden,
|
||||
initFeedbackFormPage: initFeedbackFormPage,
|
||||
initFeedbackListPage: initFeedbackListPage,
|
||||
initFeedbackDetailPage: initFeedbackDetailPage,
|
||||
|
||||
@@ -13,11 +13,25 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
function normalizeTargetPage(value) {
|
||||
var target = String(value || '');
|
||||
var hashIndex = target.indexOf('#');
|
||||
var hash = hashIndex < 0 ? '' : target.slice(hashIndex);
|
||||
var parts;
|
||||
var params;
|
||||
|
||||
if (hashIndex >= 0) target = target.slice(0, hashIndex);
|
||||
parts = target.split('?');
|
||||
if (!/^profile-[a-z-]+\.html$/.test(parts[0])) return 'profile-family-home.html';
|
||||
params = new URLSearchParams(parts[1] || '');
|
||||
params.delete('genealogyId');
|
||||
return parts[0] + (params.toString() ? '?' + params.toString() : '') + hash;
|
||||
}
|
||||
|
||||
function getTargetPage(search) {
|
||||
var params = new URLSearchParams(String(search === undefined && root.location ? root.location.search : search || '').replace(/^\?/, ''));
|
||||
var target = params.get('next') || '';
|
||||
|
||||
return /^profile-[a-z-]+\.html$/.test(target) ? target : 'profile-family-home.html';
|
||||
return normalizeTargetPage(params.get('next'));
|
||||
}
|
||||
|
||||
function buildStatus(quota) {
|
||||
@@ -142,16 +156,57 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function setCreateStatus(message) {
|
||||
function getApiStateType(error) {
|
||||
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setCreateStatus(message, type) {
|
||||
var status = root.document && root.document.querySelector('[data-genealogy-create-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
if (!status) return;
|
||||
if (!message) {
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(status, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function setEntryStatus(message, type) {
|
||||
var status = root.document && root.document.querySelector('[data-genealogy-entry-status]');
|
||||
|
||||
if (!status) return;
|
||||
if (!message) {
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(status, type, message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function setGenealogyListState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.textContent = message;
|
||||
}
|
||||
|
||||
function setCreatePending(pending) {
|
||||
var button = root.document && root.document.querySelector('[data-genealogy-create-submit]');
|
||||
|
||||
if (button) button.disabled = Boolean(pending);
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = Boolean(pending);
|
||||
button.textContent = pending ? '正在创建…' : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
}
|
||||
|
||||
async function initGenealogyCreatePage() {
|
||||
@@ -166,22 +221,24 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setCreateStatus('正在读取创建额度…', 'loading');
|
||||
try {
|
||||
quota = await api.genealogyQuota();
|
||||
if (quotaElement) quotaElement.textContent = buildStatus(quota) || '当前额度信息不可用';
|
||||
if (!canCreateGenealogy(quota)) {
|
||||
setCreateStatus('当前没有可用的家谱创建额度');
|
||||
setCreatePending(true);
|
||||
setCreateStatus('当前没有可用的家谱创建额度', 'empty');
|
||||
form.querySelector('[data-genealogy-create-submit]').disabled = true;
|
||||
return;
|
||||
}
|
||||
setCreateStatus('');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
if (quotaElement) quotaElement.textContent = '家谱额度读取失败';
|
||||
setCreateStatus(error.message || '无法读取家谱创建额度');
|
||||
setCreatePending(true);
|
||||
setCreateStatus(error.message || '无法读取家谱创建额度', getApiStateType(error));
|
||||
form.querySelector('[data-genealogy-create-submit]').disabled = true;
|
||||
return;
|
||||
}
|
||||
if (form.__genealogyCreateBound) return;
|
||||
@@ -197,13 +254,13 @@
|
||||
body = buildGenealogyCreateBody(getFormValues(form));
|
||||
validation = validateGenealogyCreateBody(body);
|
||||
if (validation) {
|
||||
setCreateStatus(validation);
|
||||
setCreateStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidCoverOssId;
|
||||
writePending = true;
|
||||
setCreatePending(true);
|
||||
setCreateStatus('正在创建家谱…');
|
||||
setCreateStatus('正在创建家谱…', 'loading');
|
||||
try {
|
||||
created = normalizeCreatedGenealogy(await api.createGenealogy(body));
|
||||
if (!created) throw new Error('创建响应缺少有效家谱信息');
|
||||
@@ -218,7 +275,7 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setCreateStatus(error.message || '创建家谱失败,请稍后重试');
|
||||
setCreateStatus(error.message || '创建家谱失败,请稍后重试', getApiStateType(error));
|
||||
} finally {
|
||||
writePending = false;
|
||||
setCreatePending(false);
|
||||
@@ -246,19 +303,28 @@
|
||||
}
|
||||
|
||||
function buildEntryUrl(genealogyId, target) {
|
||||
var params = new URLSearchParams();
|
||||
var normalizedTarget = normalizeTargetPage(target);
|
||||
var parts = normalizedTarget.split('#');
|
||||
var pathAndQuery = parts[0];
|
||||
var hash = parts.length > 1 ? '#' + parts.slice(1).join('#') : '';
|
||||
var pathParts = pathAndQuery.split('?');
|
||||
var params = new URLSearchParams(pathParts[1] || '');
|
||||
|
||||
params.set('genealogyId', String(genealogyId));
|
||||
return (target || 'profile-family-home.html') + '?' + params.toString();
|
||||
return pathParts[0] + '?' + params.toString() + hash;
|
||||
}
|
||||
|
||||
function renderGenealogies(container, data, target) {
|
||||
var items = Array.isArray(data) ? data.map(normalizeGenealogy).filter(Boolean) : [];
|
||||
|
||||
if (!container) return;
|
||||
if (!container) return items;
|
||||
if (!Array.isArray(data) || (data.length && !items.length)) {
|
||||
setGenealogyListState(container, 'error', '家谱响应字段不完整,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!items.length) {
|
||||
container.innerHTML = '<div class="api-empty">当前账号还没有可进入的家谱</div>';
|
||||
return;
|
||||
setGenealogyListState(container, 'empty', '当前账号还没有可进入的家谱');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = items.map(function (item) {
|
||||
var meta = [
|
||||
@@ -274,6 +340,7 @@
|
||||
'<div><h3>' + escapeHtml(item.genealogyName) + '</h3><p>' + escapeHtml(meta) +
|
||||
'</p></div><span class="pill">' + (item.canManage ? '管理并进入' : '进入家谱') + '</span></a>';
|
||||
}).join('');
|
||||
return items;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
@@ -293,24 +360,43 @@
|
||||
return;
|
||||
}
|
||||
if (!api || !api.genealogyQuota || !api.genealogiesMine) {
|
||||
status.textContent = '家谱入口接口初始化失败,请刷新后重试。';
|
||||
setEntryStatus('家谱入口接口初始化失败,请刷新后重试。', 'error');
|
||||
setGenealogyListState(container, 'error', '家谱列表接口初始化失败,请刷新后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
setGenealogyListState(container, 'loading', '正在读取我的家谱…');
|
||||
setEntryStatus('正在读取家谱额度…', 'loading');
|
||||
try {
|
||||
result = await Promise.all([api.genealogiesMine(), api.genealogyQuota()]);
|
||||
renderGenealogies(container, result[0], target);
|
||||
status.textContent = buildStatus(result[1]);
|
||||
result = await api.genealogiesMine();
|
||||
renderGenealogies(container, result, target);
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
status.textContent = '无法读取我的家谱,请稍后重试。';
|
||||
setGenealogyListState(
|
||||
container,
|
||||
getApiStateType(error),
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权查看我的家谱。' : '无法读取我的家谱,请稍后重试。'
|
||||
);
|
||||
}
|
||||
try {
|
||||
result = await api.genealogyQuota();
|
||||
setEntryStatus(buildStatus(result));
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setEntryStatus(
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号无权读取家谱额度。' : '家谱额度读取失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeTargetPage: normalizeTargetPage,
|
||||
getTargetPage: getTargetPage,
|
||||
buildStatus: buildStatus,
|
||||
buildGenealogyCreateBody: buildGenealogyCreateBody,
|
||||
|
||||
+126
-41
@@ -63,6 +63,33 @@
|
||||
if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function setGenerationState(selector, type, message) {
|
||||
var target = query(selector);
|
||||
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.innerHTML = '<div class="api-state api-state--' + escapeHtml(type) + '" data-api-state="' + escapeHtml(type) + '">' +
|
||||
escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setGenerationFormStatus(message, type) {
|
||||
var target = query('[data-generation-form-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function getQueryParam(search, name) {
|
||||
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
||||
|
||||
@@ -196,8 +223,12 @@
|
||||
return poem;
|
||||
}
|
||||
|
||||
function canEditGenerations(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function normalizeGenerationAccess(genealogy) {
|
||||
var canEditContent = Boolean(genealogy && genealogy.canEditContent === true);
|
||||
var canEditContent = canEditGenerations(genealogy);
|
||||
|
||||
return {
|
||||
canEditContent: canEditContent,
|
||||
@@ -251,11 +282,11 @@
|
||||
});
|
||||
if (!container) return;
|
||||
if (invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">字辈响应缺少 poemId、generationNo、generationText 或 status,请联系后端补充 DTO。</div>';
|
||||
setGenerationState('[data-generation-list]', 'error', '字辈响应缺少 poemId、generationNo、generationText 或 status,请联系后端补充 DTO。');
|
||||
return;
|
||||
}
|
||||
if (!poems.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无字辈记录</div>';
|
||||
setGenerationState('[data-generation-list]', 'empty', '暂无字辈记录');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = poems.map(renderGenerationPoem).join('');
|
||||
@@ -335,7 +366,7 @@
|
||||
|
||||
if (!container) return false;
|
||||
if (!preview) {
|
||||
container.innerHTML = '<div class="api-empty">批量预览响应缺少 items,请联系后端补充 DTO。</div>';
|
||||
setGenerationState('[data-generation-batch-preview]', 'error', '批量预览响应缺少 items,请联系后端补充 DTO。');
|
||||
return false;
|
||||
}
|
||||
summary = '新增 ' + (preview.createCount === undefined ? '-' : preview.createCount) +
|
||||
@@ -386,18 +417,35 @@
|
||||
if (target) target.textContent = message || '';
|
||||
}
|
||||
|
||||
function setGenerationEditorVisible(visible) {
|
||||
var editor = query('[data-generation-editor]');
|
||||
|
||||
if (editor) editor.hidden = !visible;
|
||||
}
|
||||
|
||||
function setGenerationSubmitPending(pending) {
|
||||
queryAll('[data-generation-editor-submit]').forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = Boolean(pending) || !managementAccess;
|
||||
button.textContent = pending ? '正在保存…' : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function refreshManagementControls() {
|
||||
var disabled = !managementAccess || writePending;
|
||||
|
||||
queryAll('[data-generation-management]').forEach(function (element) {
|
||||
element.hidden = !managementAccess;
|
||||
});
|
||||
if (!managementAccess) setGenerationEditorVisible(false);
|
||||
queryAll('[data-generation-add], [data-generation-batch-action], [data-generation-edit], [data-generation-status]').forEach(function (control) {
|
||||
control.disabled = disabled;
|
||||
});
|
||||
queryAll('[data-generation-management] textarea, [data-generation-management] input').forEach(function (control) {
|
||||
queryAll('[data-generation-management] textarea, [data-generation-management] input, [data-generation-editor] textarea, [data-generation-editor] input, [data-generation-editor] select, [data-generation-editor] button').forEach(function (control) {
|
||||
control.disabled = disabled;
|
||||
});
|
||||
setGenerationSubmitPending(writePending);
|
||||
}
|
||||
|
||||
function setManagementEnabled(enabled) {
|
||||
@@ -411,12 +459,11 @@
|
||||
}
|
||||
|
||||
function renderForbiddenState() {
|
||||
var container = query('[data-generation-list]');
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">当前账号没有字辈维护权限</div>';
|
||||
setGenerationState('[data-generation-list]', 'forbidden', '当前账号没有字辈维护权限');
|
||||
lastPreviewSignature = '';
|
||||
setManagementEnabled(false);
|
||||
setBatchStatus('当前账号没有字辈维护权限');
|
||||
setGenerationFormStatus('当前账号没有字辈维护权限', 'forbidden');
|
||||
}
|
||||
|
||||
function syncGenealogyLinks(genealogyId) {
|
||||
@@ -440,34 +487,47 @@
|
||||
|
||||
if (!genealogyId) {
|
||||
container = query('[data-generation-list]');
|
||||
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入字辈管理</div>';
|
||||
if (container) setGenerationState('[data-generation-list]', 'error', '请从具体家谱进入字辈管理');
|
||||
setGenerationEditorVisible(false);
|
||||
setGenerationFormStatus('请从具体家谱进入字辈管理', 'error');
|
||||
showMessage('请从具体家谱进入字辈管理');
|
||||
}
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function promptForPoem(existing) {
|
||||
var generationNo;
|
||||
var generationText;
|
||||
var description;
|
||||
var sortOrder;
|
||||
function openPoemEditor(poemId) {
|
||||
var form = query('[data-generation-form]');
|
||||
var existing = poemId ? poemsById[poemId] : null;
|
||||
var title = query('[data-generation-editor-title]');
|
||||
|
||||
if (!root.prompt) return null;
|
||||
generationNo = root.prompt('世代序号(从 1 开始)', existing ? String(existing.generationNo) : '');
|
||||
if (generationNo === null) return null;
|
||||
generationText = root.prompt('字辈文字(最多 50 个字符)', existing ? existing.generationText : '');
|
||||
if (generationText === null) return null;
|
||||
description = root.prompt('说明(最多 500 个字符,可留空)', existing && existing.description ? existing.description : '');
|
||||
if (description === null) return null;
|
||||
sortOrder = root.prompt('排序值(可留空,越小越靠前)', existing && existing.sortOrder !== undefined ? String(existing.sortOrder) : '');
|
||||
if (sortOrder === null) return null;
|
||||
return buildGenerationPoemBody({
|
||||
generationNo: generationNo,
|
||||
generationText: generationText,
|
||||
description: description,
|
||||
sortOrder: sortOrder,
|
||||
status: existing && existing.status
|
||||
if (!managementAccess || writePending || !form || (poemId && !existing)) return;
|
||||
form.reset();
|
||||
queryAll('[name]', form).forEach(function (field) {
|
||||
var value;
|
||||
|
||||
if (field.name === 'poemId') value = poemId || '';
|
||||
if (field.name === 'generationNo') value = existing ? existing.generationNo : '';
|
||||
if (field.name === 'generationText') value = existing ? existing.generationText : '';
|
||||
if (field.name === 'description') value = existing && existing.description ? existing.description : '';
|
||||
if (field.name === 'sortOrder') value = existing && existing.sortOrder !== undefined ? existing.sortOrder : '';
|
||||
if (field.name === 'status') value = existing ? existing.status : '';
|
||||
if (value !== undefined) field.value = value;
|
||||
});
|
||||
if (title) title.textContent = existing ? '编辑字辈' : '新增字辈';
|
||||
setGenerationFormStatus('');
|
||||
setGenerationEditorVisible(true);
|
||||
if (root.location && root.location.hash !== '#generation-editor') root.location.hash = 'generation-editor';
|
||||
if (query('[name="generationNo"]', form) && query('[name="generationNo"]', form).focus) {
|
||||
query('[name="generationNo"]', form).focus();
|
||||
}
|
||||
}
|
||||
|
||||
function closePoemEditor() {
|
||||
var form = query('[data-generation-form]');
|
||||
|
||||
if (form) form.reset();
|
||||
setGenerationFormStatus('');
|
||||
setGenerationEditorVisible(false);
|
||||
}
|
||||
|
||||
async function loadGenerationPoems() {
|
||||
@@ -479,6 +539,7 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
setGenerationState('[data-generation-list]', 'loading', '正在加载字辈记录…');
|
||||
try {
|
||||
access = normalizeGenerationAccess(await api.genealogyDetail(genealogyId));
|
||||
setManagementEnabled(access.canEditContent);
|
||||
@@ -489,27 +550,37 @@
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
setGenerationState('[data-generation-list]', 'error', '字辈列表加载失败,请稍后重试。');
|
||||
showMessage(error.message || '字辈列表加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function savePoem(poemId) {
|
||||
async function submitPoem(form) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var existing = poemId ? poemsById[poemId] : null;
|
||||
var poemId;
|
||||
var body;
|
||||
var validation;
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api) || !form) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || (poemId && !existing)) return;
|
||||
body = promptForPoem(existing);
|
||||
if (!body) return;
|
||||
validation = validateGenerationPoemBody(body);
|
||||
if (validation) {
|
||||
showMessage(validation);
|
||||
if (!genealogyId) return;
|
||||
poemId = normalizeId(getFormValues(form).poemId);
|
||||
if (getFormValues(form).poemId && !poemId) {
|
||||
setGenerationFormStatus('字辈编号无效,请返回列表重新编辑。', 'error');
|
||||
return;
|
||||
}
|
||||
if (poemId && !poemsById[poemId]) {
|
||||
setGenerationFormStatus('当前字辈不存在或已更新,请返回列表刷新后重试。', 'error');
|
||||
return;
|
||||
}
|
||||
body = buildGenerationPoemBody(getFormValues(form));
|
||||
validation = validateGenerationPoemBody(body);
|
||||
if (validation) {
|
||||
setGenerationFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
setGenerationFormStatus('正在保存字辈…', 'loading');
|
||||
setWritePending(true);
|
||||
try {
|
||||
if (poemId) {
|
||||
@@ -518,6 +589,7 @@
|
||||
await api.createGenerationPoem(genealogyId, body);
|
||||
}
|
||||
showMessage('字辈已保存');
|
||||
closePoemEditor();
|
||||
await loadGenerationPoems();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
@@ -525,7 +597,7 @@
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '字辈保存失败');
|
||||
setGenerationFormStatus(error.message || '字辈保存失败', 'error');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -645,6 +717,13 @@
|
||||
|
||||
function bindActions() {
|
||||
if (!documentRef) return;
|
||||
documentRef.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('[data-generation-form]');
|
||||
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
submitPoem(form);
|
||||
});
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var target = event.target;
|
||||
var action = target.closest('[data-generation-batch-action]');
|
||||
@@ -658,12 +737,17 @@
|
||||
}
|
||||
if (target.closest('[data-generation-add]')) {
|
||||
event.preventDefault();
|
||||
savePoem('');
|
||||
openPoemEditor('');
|
||||
return;
|
||||
}
|
||||
if (target.closest('[data-generation-editor-cancel]')) {
|
||||
event.preventDefault();
|
||||
closePoemEditor();
|
||||
return;
|
||||
}
|
||||
if (!poemButton) return;
|
||||
event.preventDefault();
|
||||
if (poemButton.hasAttribute('data-generation-edit')) savePoem(poemButton.getAttribute('data-generation-edit'));
|
||||
if (poemButton.hasAttribute('data-generation-edit')) openPoemEditor(poemButton.getAttribute('data-generation-edit'));
|
||||
if (poemButton.hasAttribute('data-generation-status')) togglePoemStatus(poemButton.getAttribute('data-generation-status'));
|
||||
});
|
||||
}
|
||||
@@ -680,6 +764,7 @@
|
||||
buildBatchBody: buildBatchBody,
|
||||
validateGenerationPoemBody: validateGenerationPoemBody,
|
||||
validateBatchBody: validateBatchBody,
|
||||
canEditGenerations: canEditGenerations,
|
||||
splitPoemText: splitPoemText,
|
||||
normalizeGenerationPoem: normalizeGenerationPoem,
|
||||
normalizeGenerationAccess: normalizeGenerationAccess,
|
||||
|
||||
+124
-37
@@ -15,6 +15,7 @@
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
var writePending = false;
|
||||
|
||||
function getApi() {
|
||||
@@ -253,19 +254,26 @@
|
||||
return recordId ? url + '&recordId=' + encodeURIComponent(recordId) : url;
|
||||
}
|
||||
|
||||
function canEditRecords(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function renderGrowthRow(record) {
|
||||
var summary = [];
|
||||
var actions = '<button class="pill" type="button" data-growth-detail-id="' +
|
||||
escapeHtml(record.recordId) + '">查看详情</button>';
|
||||
|
||||
if (record.lineagePersonName) summary.push(record.lineagePersonName);
|
||||
if (record.recordType) summary.push(record.recordType);
|
||||
if (record.recordDate) summary.push(record.recordDate);
|
||||
if (!summary.length) summary.push('未填写日期和关联人物');
|
||||
if (canEditContent) {
|
||||
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) +
|
||||
'">编辑</a><button class="pill is-danger" type="button" data-growth-delete-id="' +
|
||||
escapeHtml(record.recordId) + '">删除</button>';
|
||||
}
|
||||
return '<article class="module-row growth-row"><div><h3>' + escapeHtml(record.recordTitle) + '</h3><p>' +
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
|
||||
'<button class="pill" type="button" data-growth-detail-id="' + escapeHtml(record.recordId) + '">查看详情</button>' +
|
||||
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑</a>' +
|
||||
'<button class="pill is-danger" type="button" data-growth-delete-id="' + escapeHtml(record.recordId) + '">删除</button>' +
|
||||
'</div></article>';
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' + actions + '</div></article>';
|
||||
}
|
||||
|
||||
function detailValue(label, value) {
|
||||
@@ -274,9 +282,16 @@
|
||||
|
||||
function renderGrowthDetail(record) {
|
||||
var count;
|
||||
var actions = '';
|
||||
|
||||
if (!record) return '<div class="api-empty">请选择一条成长记录查看详情</div>';
|
||||
if (!record) return stateHtml('empty', '请选择一条成长记录查看详情');
|
||||
count = attachmentCount(record.mediaOssIds);
|
||||
if (canEditContent) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-growth-delete-id="' +
|
||||
escapeHtml(record.recordId) + '">删除记录</button></div>';
|
||||
}
|
||||
return '<div class="form-like growth-detail">' +
|
||||
detailValue('标题', record.recordTitle) +
|
||||
detailValue('关联人物', record.lineagePersonName || record.lineagePersonNo) +
|
||||
@@ -288,9 +303,7 @@
|
||||
detailValue('记录人', record.appUserNickName) +
|
||||
detailValue('状态', record.status === '0' ? '正常' : '停用') +
|
||||
detailValue('备注', record.remark) +
|
||||
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-growth-delete-id="' + escapeHtml(record.recordId) + '">删除记录</button></div>';
|
||||
'</div>' + actions;
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
@@ -311,6 +324,10 @@
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
@@ -327,13 +344,38 @@
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var status = query('[data-growth-form-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
function stateHtml(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) return root.ProfileUI.renderApiState(type, message);
|
||||
return '<div class="api-state api-state--' + type + '" role="' +
|
||||
(type === 'error' || type === 'forbidden' ? 'alert' : 'status') +
|
||||
'" aria-live="polite">' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
function setState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.innerHTML = stateHtml(type, message);
|
||||
}
|
||||
|
||||
function setFormStatus(message, type) {
|
||||
var status = query('[data-growth-form-status]');
|
||||
|
||||
if (!status) return;
|
||||
if (!message) {
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(status, type, message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message, type) {
|
||||
var placeholder = query('[data-growth-editor-placeholder]');
|
||||
|
||||
queryAll('[data-growth-editor], [data-growth-editor-action]').forEach(function (element) {
|
||||
@@ -341,19 +383,37 @@
|
||||
});
|
||||
if (placeholder) {
|
||||
placeholder.hidden = Boolean(enabled);
|
||||
if (message) placeholder.textContent = message;
|
||||
if (!enabled && message) {
|
||||
if (type) setState(placeholder, type, message);
|
||||
else placeholder.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setEditAccess(enabled) {
|
||||
canEditContent = Boolean(enabled);
|
||||
queryAll('[data-growth-create-link]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var genealogy = await api.genealogyDetail(genealogyId);
|
||||
|
||||
setEditAccess(canEditRecords(genealogy));
|
||||
return genealogy;
|
||||
}
|
||||
|
||||
function renderNoContext() {
|
||||
var list = query('[data-growth-list]');
|
||||
var detail = query('[data-growth-detail]');
|
||||
var message = '请先选择家谱,再查看或维护成长记录。';
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setEditorEnabled(false, message);
|
||||
setFormStatus(message);
|
||||
setState(list, 'empty', message);
|
||||
setState(detail, 'empty', '当前没有家谱上下文。');
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, message, 'empty');
|
||||
setFormStatus(message, 'empty');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -363,11 +423,18 @@
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function setWritePending(pending) {
|
||||
function setWritePending(pending, label) {
|
||||
var submit = query('[data-growth-form] [type="submit"]');
|
||||
|
||||
writePending = Boolean(pending);
|
||||
queryAll('[data-growth-form] input, [data-growth-form] textarea, [data-growth-form] select, [data-growth-form] button, [data-growth-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
if (submit) {
|
||||
if (!submit.dataset.defaultLabel) submit.dataset.defaultLabel = submit.textContent;
|
||||
submit.textContent = writePending && label ? label : submit.dataset.defaultLabel;
|
||||
submit.setAttribute('aria-busy', writePending ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function renderGrowthRecords(data) {
|
||||
@@ -382,11 +449,11 @@
|
||||
});
|
||||
if (!container) return records;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">成长记录响应缺少稳定 GrowthRecordVo 字段,请联系后端核对。</div>';
|
||||
setState(container, 'error', '成长记录响应缺少稳定 GrowthRecordVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!records.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
|
||||
setState(container, 'empty', '暂无成长记录');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = records.map(renderGrowthRow).join('');
|
||||
@@ -405,6 +472,7 @@
|
||||
var record;
|
||||
|
||||
if (!genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return null;
|
||||
setState(query('[data-growth-detail]'), 'loading', '正在加载成长记录详情…');
|
||||
try {
|
||||
record = normalizeGrowthRecord(await api.growthRecordDetail(genealogyId, recordId));
|
||||
if (!record) throw new Error('成长记录详情响应缺少稳定 GrowthRecordVo 字段');
|
||||
@@ -413,7 +481,11 @@
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
setState(
|
||||
query('[data-growth-detail]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该成长记录。' : '成长记录详情加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该成长记录。' : (error.message || '成长记录详情加载失败'));
|
||||
return null;
|
||||
}
|
||||
@@ -429,7 +501,9 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks();
|
||||
setState(query('[data-growth-list]'), 'loading', '正在加载成长记录…');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
records = renderGrowthRecords(await api.growthRecords(genealogyId));
|
||||
requestedRecordId = getCurrentRecordId();
|
||||
if (requestedRecordId && recordsById[requestedRecordId]) {
|
||||
@@ -441,11 +515,12 @@
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-growth-list]')) {
|
||||
query('[data-growth-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : '成长记录加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
setEditAccess(false);
|
||||
setState(
|
||||
query('[data-growth-list]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : '成长记录加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : (error.message || '成长记录加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -510,8 +585,11 @@
|
||||
if (!genealogyId) return;
|
||||
recordId = getCurrentRecordId();
|
||||
syncGenealogyLinks();
|
||||
setEditorEnabled(false, '正在加载成长记录编辑信息…');
|
||||
setEditorEnabled(false, '正在加载成长记录编辑信息…', 'loading');
|
||||
setFormStatus('正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的成长记录。' };
|
||||
result = await Promise.all([
|
||||
api.lineagePersonOptions(genealogyId),
|
||||
recordId ? api.growthRecordDetail(genealogyId, recordId) : Promise.resolve(null)
|
||||
@@ -524,12 +602,17 @@
|
||||
if (query('[data-growth-editor-title]')) query('[data-growth-editor-title]').textContent = '编辑成长记录';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, isForbidden(error)
|
||||
? '当前账号无权维护该家谱的成长记录。'
|
||||
: (error.message || '成长记录编辑信息加载失败'));
|
||||
setFormStatus(error.message || '成长记录编辑信息加载失败');
|
||||
: (error.message || '成长记录编辑信息加载失败'), getApiStateType(error));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的成长记录。' : (error.message || '成长记录编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,16 +627,16 @@
|
||||
var savedId;
|
||||
var verifiedDetail;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
body = buildGrowthRecordBody(getFormValues(form));
|
||||
validation = validateGrowthRecordBody(body);
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存成长记录…');
|
||||
setWritePending(true, '正在保存…');
|
||||
setFormStatus('正在保存成长记录…', 'loading');
|
||||
try {
|
||||
result = recordId
|
||||
? await api.updateGrowthRecord(genealogyId, recordId, body)
|
||||
@@ -566,7 +649,10 @@
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权保存该成长记录。' : (error.message || '成长记录保存失败'));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权保存该成长记录。' : (error.message || '成长记录保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -577,7 +663,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var record = recordsById[recordId];
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.recordTitle : '该成长记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -647,6 +733,7 @@
|
||||
validateGrowthRecordBody: validateGrowthRecordBody,
|
||||
normalizeGrowthRecord: normalizeGrowthRecord,
|
||||
matchesSavedRecord: matchesSavedRecord,
|
||||
canEditRecords: canEditRecords,
|
||||
renderLineageOptions: renderLineageOptions,
|
||||
renderGrowthDetail: renderGrowthDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
|
||||
+47
-10
@@ -133,6 +133,10 @@
|
||||
else root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var list = root.document && root.document.querySelector('[data-help-list]');
|
||||
var status = root.document && root.document.querySelector('[data-help-status]');
|
||||
@@ -145,23 +149,48 @@
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
if (status) status.textContent = message || '';
|
||||
function setApiState(target, type, message) {
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message || '';
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
setApiState(status, type, message);
|
||||
}
|
||||
|
||||
function setHelpList(items) {
|
||||
if (!items.length) {
|
||||
setApiState(list, 'empty', '当前暂无帮助文章。');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = renderHelpArticleList(items);
|
||||
}
|
||||
|
||||
function setPendingButton(button, pending, label) {
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = pending;
|
||||
button.textContent = pending ? label : button.dataset.defaultLabel;
|
||||
}
|
||||
|
||||
async function refreshList() {
|
||||
try {
|
||||
list.classList.add('is-loading');
|
||||
setStatus('正在读取帮助文章…');
|
||||
list.innerHTML = renderHelpArticleList(await loadHelpArticles(api));
|
||||
setStatus('');
|
||||
setApiState(list, 'loading', '正在读取帮助文章…');
|
||||
setStatus('loading', '正在读取帮助文章…');
|
||||
setHelpList(await loadHelpArticles(api));
|
||||
setStatus('', '');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div class="api-empty">帮助文章读取失败,请稍后重试</div>';
|
||||
setStatus(error.message || '帮助文章读取失败');
|
||||
setApiState(list, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看帮助文章。' : '帮助文章读取失败,请稍后重试。');
|
||||
setStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看帮助文章。' : (error.message || '帮助文章读取失败,请稍后重试。'));
|
||||
} finally {
|
||||
list.classList.remove('is-loading');
|
||||
}
|
||||
@@ -177,7 +206,7 @@
|
||||
panel = list.querySelector('[data-help-detail-panel="' + helpId + '"]');
|
||||
if (!panel) return;
|
||||
button.disabled = true;
|
||||
panel.textContent = '正在读取完整解答…';
|
||||
setApiState(panel, 'loading', '正在读取完整解答…');
|
||||
try {
|
||||
panel.innerHTML = renderHelpArticleDetail(await loadHelpArticleDetail(api, helpId));
|
||||
} catch (error) {
|
||||
@@ -185,11 +214,18 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
panel.textContent = error.message || '帮助文章详情读取失败';
|
||||
setApiState(panel, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看该帮助文章。' : (error.message || '帮助文章详情读取失败,请稍后重试。'));
|
||||
}
|
||||
});
|
||||
|
||||
if (refresh) refresh.addEventListener('click', refreshList);
|
||||
if (refresh) refresh.addEventListener('click', async function () {
|
||||
setPendingButton(refresh, true, '正在刷新…');
|
||||
try {
|
||||
await refreshList();
|
||||
} finally {
|
||||
setPendingButton(refresh, false, '正在刷新…');
|
||||
}
|
||||
});
|
||||
await refreshList();
|
||||
}
|
||||
|
||||
@@ -200,6 +236,7 @@
|
||||
renderHelpArticleDetail: renderHelpArticleDetail,
|
||||
loadHelpArticleDetail: loadHelpArticleDetail,
|
||||
loadHelpArticles: loadHelpArticles,
|
||||
isForbidden: isForbidden,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
init: init
|
||||
};
|
||||
|
||||
+67
-22
@@ -29,6 +29,14 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderApiState(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) {
|
||||
return root.ProfileUI.renderApiState(type, message);
|
||||
}
|
||||
return '<div class="api-state api-state--' + type + '" data-api-state="' + type + '">' +
|
||||
escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text;
|
||||
|
||||
@@ -129,9 +137,9 @@
|
||||
function renderJoinGenealogyOptions(data) {
|
||||
var items;
|
||||
|
||||
if (!Array.isArray(data)) return '<div class="api-empty">家谱列表数据无效,请刷新重试</div>';
|
||||
if (!Array.isArray(data)) return renderApiState('error', '家谱列表数据无效,请刷新重试');
|
||||
items = data.map(normalizeJoinGenealogy).filter(Boolean);
|
||||
if (!items.length) return '<div class="api-empty">没有找到可申请加入的家谱</div>';
|
||||
if (!items.length) return renderApiState('empty', '没有找到可申请加入的家谱');
|
||||
return items.map(function (item) {
|
||||
var meta = [
|
||||
item.surname + '氏',
|
||||
@@ -150,7 +158,7 @@
|
||||
function renderMyJoinApplies(data) {
|
||||
var items = normalizeJoinApplies(data);
|
||||
|
||||
if (!items.length) return '<div class="api-empty">当前没有加入申请</div>';
|
||||
if (!items.length) return renderApiState('empty', '当前没有加入申请');
|
||||
return items.map(function (item) {
|
||||
var details = [
|
||||
item.surname ? item.surname + '氏' : '',
|
||||
@@ -184,10 +192,40 @@
|
||||
else root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function setText(selector, message) {
|
||||
function getApiStateType(error) {
|
||||
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setText(selector, message, type) {
|
||||
var element = root.document && root.document.querySelector(selector);
|
||||
|
||||
if (element) element.textContent = message || '';
|
||||
if (!element) return;
|
||||
if (!message) {
|
||||
element.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(element, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
element.textContent = message;
|
||||
}
|
||||
|
||||
function setOptionsState(options, type, message) {
|
||||
if (!options) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(options, type, message);
|
||||
return;
|
||||
}
|
||||
options.innerHTML = renderApiState(type, message);
|
||||
}
|
||||
|
||||
function setSubmitPending(button, pending, pendingLabel) {
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = Boolean(pending);
|
||||
button.textContent = pending ? pendingLabel : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
@@ -222,27 +260,34 @@
|
||||
return;
|
||||
}
|
||||
|
||||
function clearSelectedGenealogy() {
|
||||
selectedGenealogyId = '';
|
||||
if (selected) selected.textContent = '尚未选择家谱';
|
||||
}
|
||||
|
||||
async function loadOptions(keyword) {
|
||||
var data;
|
||||
|
||||
setText('[data-join-apply-status]', '正在读取可申请家谱…');
|
||||
clearSelectedGenealogy();
|
||||
setText('[data-join-options-status]', '正在读取可申请家谱…', 'loading');
|
||||
setOptionsState(options, 'loading', '正在读取可申请家谱…');
|
||||
try {
|
||||
data = await api.genealogyOptions(keyword ? { keyword: keyword } : {});
|
||||
options.innerHTML = renderJoinGenealogyOptions(data);
|
||||
setText('[data-join-apply-status]', '');
|
||||
setText('[data-join-options-status]', '');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
options.innerHTML = '<div class="api-empty">读取家谱列表失败</div>';
|
||||
setText('[data-join-apply-status]', error.message || '读取家谱列表失败,请稍后重试');
|
||||
setOptionsState(options, getApiStateType(error), '读取家谱列表失败');
|
||||
setText('[data-join-options-status]', error.message || '读取家谱列表失败,请稍后重试', getApiStateType(error));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!canJoinGenealogy(await api.genealogyQuota())) {
|
||||
setText('[data-join-apply-status]', '当前没有可用的家谱加入额度');
|
||||
setText('[data-join-apply-status]', '当前没有可用的家谱加入额度', 'empty');
|
||||
applyForm.querySelector('[data-join-apply-submit]').disabled = true;
|
||||
return;
|
||||
}
|
||||
@@ -251,7 +296,7 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setText('[data-join-apply-status]', error.message || '无法读取家谱加入额度');
|
||||
setText('[data-join-apply-status]', error.message || '无法读取家谱加入额度', getApiStateType(error));
|
||||
applyForm.querySelector('[data-join-apply-submit]').disabled = true;
|
||||
return;
|
||||
}
|
||||
@@ -280,18 +325,18 @@
|
||||
event.preventDefault();
|
||||
if (writePending) return;
|
||||
if (!selectedGenealogyId) {
|
||||
setText('[data-join-apply-status]', '请先从家谱列表中选择要加入的家谱');
|
||||
setText('[data-join-apply-status]', '请先从家谱列表中选择要加入的家谱', 'error');
|
||||
return;
|
||||
}
|
||||
body = buildJoinApplyBody(getFormValues(applyForm));
|
||||
validation = validateJoinApplyBody(body);
|
||||
if (validation) {
|
||||
setText('[data-join-apply-status]', validation);
|
||||
setText('[data-join-apply-status]', validation, 'error');
|
||||
return;
|
||||
}
|
||||
writePending = true;
|
||||
applyForm.querySelector('[data-join-apply-submit]').disabled = true;
|
||||
setText('[data-join-apply-status]', '正在提交申请…');
|
||||
setSubmitPending(applyForm.querySelector('[data-join-apply-submit]'), true, '正在提交…');
|
||||
setText('[data-join-apply-status]', '正在提交申请…', 'loading');
|
||||
try {
|
||||
created = normalizeJoinApply(await api.applyToGenealogy(selectedGenealogyId, body));
|
||||
if (!created) throw new Error('申请响应缺少有效申请信息');
|
||||
@@ -305,10 +350,10 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setText('[data-join-apply-status]', error.message || '提交加入申请失败,请稍后重试');
|
||||
setText('[data-join-apply-status]', error.message || '提交加入申请失败,请稍后重试', getApiStateType(error));
|
||||
} finally {
|
||||
writePending = false;
|
||||
applyForm.querySelector('[data-join-apply-submit]').disabled = false;
|
||||
setSubmitPending(applyForm.querySelector('[data-join-apply-submit]'), false, '正在提交…');
|
||||
}
|
||||
});
|
||||
await loadOptions('');
|
||||
@@ -330,7 +375,7 @@
|
||||
async function loadApplies() {
|
||||
var data;
|
||||
|
||||
setText('[data-my-join-applies-status]', '正在读取申请记录…');
|
||||
setText('[data-my-join-applies-status]', '正在读取申请记录…', 'loading');
|
||||
try {
|
||||
data = await api.myGenealogyJoinApplies();
|
||||
list.innerHTML = renderMyJoinApplies(data);
|
||||
@@ -341,8 +386,8 @@
|
||||
redirectToLogin(api);
|
||||
return [];
|
||||
}
|
||||
list.innerHTML = '<div class="api-empty">读取加入申请失败</div>';
|
||||
setText('[data-my-join-applies-status]', error.message || '读取加入申请失败,请稍后重试');
|
||||
list.innerHTML = renderApiState(getApiStateType(error), '读取加入申请失败');
|
||||
setText('[data-my-join-applies-status]', error.message || '读取加入申请失败,请稍后重试', getApiStateType(error));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -361,7 +406,7 @@
|
||||
if (root.confirm && !root.confirm('确认撤销这条加入申请吗?')) return;
|
||||
writePending = true;
|
||||
button.disabled = true;
|
||||
setText('[data-my-join-applies-status]', '正在撤销申请…');
|
||||
setText('[data-my-join-applies-status]', '正在撤销申请…', 'loading');
|
||||
try {
|
||||
await api.cancelGenealogyJoinApply(applyId);
|
||||
refreshed = await loadApplies();
|
||||
@@ -373,7 +418,7 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setText('[data-my-join-applies-status]', error.message || '撤销加入申请失败,请稍后重试');
|
||||
setText('[data-my-join-applies-status]', error.message || '撤销加入申请失败,请稍后重试', getApiStateType(error));
|
||||
} finally {
|
||||
writePending = false;
|
||||
}
|
||||
|
||||
+161
-24
@@ -91,7 +91,7 @@
|
||||
function renderPendingJoinApplies(data, canManage) {
|
||||
var items = normalizePendingApplies(data);
|
||||
|
||||
if (!items.length) return '<div class="api-empty">当前没有待审核申请</div>';
|
||||
if (!items.length) return '';
|
||||
return items.map(function (item) {
|
||||
var details = [
|
||||
item.phone ? '申请联系电话:' + item.phone : '',
|
||||
@@ -124,16 +124,103 @@
|
||||
else root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
function getApiStateType(error) {
|
||||
return Number(error && (error.status || error.code)) === 403 ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
var element = root.document && root.document.querySelector('[data-join-review-status]');
|
||||
|
||||
if (element) element.textContent = message || '';
|
||||
if (!element) return;
|
||||
if (!message) {
|
||||
element.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(element, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
element.textContent = message;
|
||||
}
|
||||
|
||||
function setReviewFormStatus(message, type) {
|
||||
var element = root.document && root.document.querySelector('[data-join-review-form-status]');
|
||||
|
||||
if (!element) return;
|
||||
if (!message) {
|
||||
element.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(element, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
element.textContent = message;
|
||||
}
|
||||
|
||||
function setPendingListState(message, type) {
|
||||
var element = root.document && root.document.querySelector('[data-join-apply-list="pending"]');
|
||||
|
||||
if (!element) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(element, type || 'error', message);
|
||||
return;
|
||||
}
|
||||
element.textContent = message;
|
||||
}
|
||||
|
||||
function setReviewFormEnabled(enabled) {
|
||||
var form = root.document && root.document.querySelector('[data-join-review-form]');
|
||||
|
||||
if (!form) return;
|
||||
Array.prototype.slice.call(form.querySelectorAll('textarea, button')).forEach(function (field) {
|
||||
field.disabled = !enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function setReviewSubmitPending(pending) {
|
||||
var button = root.document && root.document.querySelector('[data-join-review-submit]');
|
||||
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.textContent = pending ? '正在提交…' : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function openReviewForm(applyId, status) {
|
||||
var form = root.document && root.document.querySelector('[data-join-review-form]');
|
||||
var panel = root.document && root.document.querySelector('[data-join-review-editor]');
|
||||
var title = root.document && root.document.querySelector('[data-join-review-form-title]');
|
||||
var submit = root.document && root.document.querySelector('[data-join-review-submit]');
|
||||
|
||||
if (!form || !panel || !applyId || ['1', '2'].indexOf(status) < 0) return;
|
||||
form.reset();
|
||||
form.querySelector('[name="applyId"]').value = applyId;
|
||||
form.querySelector('[name="status"]').value = status;
|
||||
if (title) title.textContent = status === '1' ? '通过加入申请' : '拒绝加入申请';
|
||||
if (submit) submit.textContent = status === '1' ? '确认通过' : '确认拒绝';
|
||||
setReviewFormStatus('');
|
||||
panel.hidden = false;
|
||||
if (root.location && root.location.hash !== '#join-review-form') root.location.hash = 'join-review-form';
|
||||
if (form.querySelector('[name="auditRemark"]') && form.querySelector('[name="auditRemark"]').focus) {
|
||||
form.querySelector('[name="auditRemark"]').focus();
|
||||
}
|
||||
}
|
||||
|
||||
function closeReviewForm() {
|
||||
var form = root.document && root.document.querySelector('[data-join-review-form]');
|
||||
var panel = root.document && root.document.querySelector('[data-join-review-editor]');
|
||||
|
||||
if (form) form.reset();
|
||||
if (panel) panel.hidden = true;
|
||||
setReviewFormStatus('');
|
||||
}
|
||||
|
||||
async function initJoinReviewPage() {
|
||||
var page = root.document && root.document.querySelector('[data-join-review-page]');
|
||||
var list = root.document && root.document.querySelector('[data-join-apply-list="pending"]');
|
||||
var refresh = root.document && root.document.querySelector('[data-join-review-refresh]');
|
||||
var form = root.document && root.document.querySelector('[data-join-review-form]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
var canManage = false;
|
||||
@@ -141,7 +228,8 @@
|
||||
|
||||
if (!page) return;
|
||||
if (!genealogyId) {
|
||||
setStatus('请先从“我的家谱”选择要管理的家谱');
|
||||
setStatus('请先从“我的家谱”选择要管理的家谱', 'forbidden');
|
||||
setReviewFormEnabled(false);
|
||||
list.innerHTML = '<a class="module-row" href="profile-families.html?next=profile-join-review.html">选择家谱后继续</a>';
|
||||
return;
|
||||
}
|
||||
@@ -156,19 +244,32 @@
|
||||
async function loadPending() {
|
||||
var data;
|
||||
|
||||
setStatus('正在读取待审核申请…');
|
||||
setStatus('正在读取待审核申请…', 'loading');
|
||||
try {
|
||||
data = await api.pendingGenealogyJoinApplies(genealogyId);
|
||||
if (!Array.isArray(data) || (data.length && !normalizePendingApplies(data).length)) {
|
||||
setPendingListState('待审核申请响应字段不完整,请联系后端核对。', 'error');
|
||||
setStatus('待审核申请响应字段不完整,请联系后端核对。', 'error');
|
||||
return [];
|
||||
}
|
||||
if (!data.length) {
|
||||
setPendingListState('当前没有待审核申请', 'empty');
|
||||
setStatus('');
|
||||
return [];
|
||||
}
|
||||
list.innerHTML = renderPendingJoinApplies(data, canManage);
|
||||
setStatus(canManage ? '' : '当前账号没有审核该家谱申请的权限');
|
||||
setStatus(canManage ? '' : '当前账号没有审核该家谱申请的权限', canManage ? undefined : 'forbidden');
|
||||
return normalizePendingApplies(data);
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return [];
|
||||
}
|
||||
list.innerHTML = '<div class="api-empty">读取待审核申请失败</div>';
|
||||
setStatus(error.message || '读取待审核申请失败,请稍后重试');
|
||||
setPendingListState(
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号没有审核该家谱申请的权限。' : '读取待审核申请失败,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
setStatus(error.message || '读取待审核申请失败,请稍后重试', getApiStateType(error));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -178,8 +279,9 @@
|
||||
|
||||
canManage = detail && detail.canManage === true;
|
||||
if (!canManage) {
|
||||
list.innerHTML = '<div class="api-empty">当前账号没有审核该家谱申请的权限</div>';
|
||||
setStatus('当前账号没有审核该家谱申请的权限');
|
||||
setPendingListState('当前账号没有审核该家谱申请的权限。', 'forbidden');
|
||||
setStatus('当前账号没有审核该家谱申请的权限', 'forbidden');
|
||||
setReviewFormEnabled(false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -187,55 +289,90 @@
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '无法核验当前家谱管理权限');
|
||||
setStatus(error.message || '无法核验当前家谱管理权限', getApiStateType(error));
|
||||
setPendingListState(
|
||||
getApiStateType(error) === 'forbidden' ? '当前账号没有审核该家谱申请的权限。' : '无法读取待审核申请,请稍后重试。',
|
||||
getApiStateType(error)
|
||||
);
|
||||
setReviewFormEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setReviewFormEnabled(true);
|
||||
|
||||
refresh.addEventListener('click', function () {
|
||||
loadPending();
|
||||
});
|
||||
list.addEventListener('click', async function (event) {
|
||||
list.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-join-audit-id]');
|
||||
var applyId;
|
||||
var status;
|
||||
var auditRemark = '';
|
||||
var body;
|
||||
var validation;
|
||||
var refreshed;
|
||||
|
||||
if (!button || !list.contains(button) || writePending || !canManage) return;
|
||||
applyId = normalizeId(button.getAttribute('data-join-audit-id'));
|
||||
status = button.getAttribute('data-join-audit-status');
|
||||
if (!applyId || ['1', '2'].indexOf(status) < 0) return;
|
||||
if (status === '1' && root.confirm && !root.confirm('确认通过这条加入申请吗?')) return;
|
||||
if (status === '2' && root.prompt) {
|
||||
auditRemark = root.prompt('可填写拒绝原因(最多 500 个字符)', '') || '';
|
||||
openReviewForm(applyId, status);
|
||||
});
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var values;
|
||||
var applyId;
|
||||
var body;
|
||||
var validation;
|
||||
var refreshed;
|
||||
var forbidden = false;
|
||||
|
||||
event.preventDefault();
|
||||
if (writePending || !canManage) return;
|
||||
values = {};
|
||||
Array.prototype.slice.call(form.querySelectorAll('[name]')).forEach(function (field) {
|
||||
values[field.name] = field.value;
|
||||
});
|
||||
applyId = normalizeId(values.applyId);
|
||||
body = buildJoinAuditBody(values);
|
||||
if (!applyId) {
|
||||
setReviewFormStatus('申请编号无效,请返回列表重新操作。', 'error');
|
||||
return;
|
||||
}
|
||||
body = buildJoinAuditBody({ status: status, auditRemark: auditRemark });
|
||||
validation = validateJoinAuditBody(body);
|
||||
if (validation) {
|
||||
setStatus(validation);
|
||||
setReviewFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
writePending = true;
|
||||
button.disabled = true;
|
||||
setStatus('正在提交审核结果…');
|
||||
setReviewFormEnabled(false);
|
||||
setReviewSubmitPending(true);
|
||||
setReviewFormStatus('正在提交审核结果…', 'loading');
|
||||
try {
|
||||
await api.auditGenealogyJoinApply(genealogyId, applyId, body);
|
||||
refreshed = await loadPending();
|
||||
if (refreshed.some(function (item) { return item.applyId === applyId; })) {
|
||||
throw new Error('审核后申请仍在待审核列表');
|
||||
}
|
||||
closeReviewForm();
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '提交审核结果失败,请稍后重试');
|
||||
forbidden = getApiStateType(error) === 'forbidden';
|
||||
setReviewFormStatus(
|
||||
forbidden ? '当前账号无权审核该家谱申请。' : (error.message || '提交审核结果失败,请稍后重试。'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
if (forbidden) {
|
||||
canManage = false;
|
||||
setStatus('当前账号无权审核该家谱申请。', 'forbidden');
|
||||
}
|
||||
} finally {
|
||||
writePending = false;
|
||||
setReviewSubmitPending(false);
|
||||
if (!forbidden) setReviewFormEnabled(true);
|
||||
}
|
||||
});
|
||||
root.document.querySelector('[data-join-review-cancel]').addEventListener('click', function () {
|
||||
if (!writePending) closeReviewForm();
|
||||
});
|
||||
await loadPending();
|
||||
}
|
||||
|
||||
|
||||
+86
-25
@@ -304,11 +304,11 @@
|
||||
});
|
||||
if (!container) return;
|
||||
if (invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">人物响应缺少可安全使用的 personId 或 name,请联系后端补充 DTO。</div>';
|
||||
setLineageState('[data-lineage-list]', 'error', '人物响应缺少可安全使用的 personId 或 name,请联系后端补充 DTO。');
|
||||
return;
|
||||
}
|
||||
if (!people.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无世系人物</div>';
|
||||
setLineageState('[data-lineage-list]', 'empty', '暂无世系人物');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = people.map(function (person) {
|
||||
@@ -344,16 +344,20 @@
|
||||
function renderLineageTreeHtml(data) {
|
||||
var nodes = normalizeList(data).map(function (item) { return renderTreeNode(item, {}); }).filter(Boolean);
|
||||
|
||||
if (!nodes.length) {
|
||||
return '<div class="api-empty">暂无世系树</div>';
|
||||
}
|
||||
if (!nodes.length) return '';
|
||||
return '<ul class="lineage-tree">' + nodes.join('') + '</ul>';
|
||||
}
|
||||
|
||||
function renderTree(data) {
|
||||
var container = query('[data-lineage-tree]');
|
||||
var html = renderLineageTreeHtml(data);
|
||||
|
||||
if (container) container.innerHTML = renderLineageTreeHtml(data);
|
||||
if (!container) return;
|
||||
if (!html) {
|
||||
setLineageState('[data-lineage-tree]', 'empty', '暂无世系树');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderOptions(data) {
|
||||
@@ -397,9 +401,11 @@
|
||||
function updateBindingFields() {
|
||||
var bindingMode = query('[name="bindingMode"]');
|
||||
var memberSelect = query('[data-lineage-member-options]');
|
||||
var specifiedMember = query('[data-lineage-specified-member]');
|
||||
var specified = Boolean(bindingMode && bindingMode.value === 'SPECIFIED');
|
||||
|
||||
if (!memberSelect) return;
|
||||
if (specifiedMember) specifiedMember.hidden = !specified;
|
||||
memberSelect.hidden = !specified;
|
||||
memberSelect.required = specified;
|
||||
if (!specified) memberSelect.value = '';
|
||||
@@ -469,7 +475,7 @@
|
||||
|
||||
if (!container) return;
|
||||
if (!person) {
|
||||
container.innerHTML = '<div class="api-empty">人物详情缺少可安全使用的 personId 或 name,请联系后端补充 DTO。</div>';
|
||||
setLineageState('[data-lineage-detail]', 'error', '人物详情缺少可安全使用的 personId 或 name,请联系后端补充 DTO。');
|
||||
return;
|
||||
}
|
||||
selectedPersonId = person.personId;
|
||||
@@ -513,10 +519,61 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
function setLineageState(selector, type, message) {
|
||||
var target = query(selector);
|
||||
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.innerHTML = '<div class="api-state api-state--' + escapeHtml(type) + '" data-api-state="' + escapeHtml(type) + '">' +
|
||||
escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setFormStatus(message, type) {
|
||||
var target = query('[data-lineage-form-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setActionStatus(message, type) {
|
||||
var target = query('[data-lineage-action-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setLineageSubmitPending(pending) {
|
||||
queryAll('[data-lineage-submit]').forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.textContent = pending ? '正在保存…' : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function setLineageSubmitLabel(label) {
|
||||
queryAll('[data-lineage-submit]').forEach(function (button) {
|
||||
button.dataset.defaultLabel = label;
|
||||
if (!writePending) button.textContent = label;
|
||||
});
|
||||
}
|
||||
|
||||
function relationLabel(mode) {
|
||||
@@ -545,13 +602,12 @@
|
||||
|
||||
function renderNoContext() {
|
||||
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
|
||||
var container = query(selector);
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入世系管理</div>';
|
||||
setLineageState(selector, 'error', '请从具体家谱进入世系管理');
|
||||
});
|
||||
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
|
||||
currentPageTotal = null;
|
||||
setLineageEnabled(false);
|
||||
setActionStatus('请从具体家谱进入世系管理。', 'error');
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
@@ -588,18 +644,18 @@
|
||||
function setWritePending(pending) {
|
||||
writePending = Boolean(pending);
|
||||
refreshControls();
|
||||
setLineageSubmitPending(writePending);
|
||||
}
|
||||
|
||||
function renderForbiddenState() {
|
||||
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
|
||||
var container = query(selector);
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">当前账号没有世系人物管理权限</div>';
|
||||
setLineageState(selector, 'forbidden', '当前账号没有世系人物管理权限');
|
||||
});
|
||||
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
|
||||
currentPageTotal = null;
|
||||
setLineageEnabled(false);
|
||||
setFormStatus('当前账号没有世系人物管理权限');
|
||||
setFormStatus('当前账号没有世系人物管理权限', 'forbidden');
|
||||
setActionStatus('当前账号没有世系人物管理权限', 'forbidden');
|
||||
}
|
||||
|
||||
function getCurrentKeyword() {
|
||||
@@ -648,6 +704,9 @@
|
||||
if (!genealogyId) return;
|
||||
currentPageNum = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
setLineageState('[data-lineage-tree]', 'loading', '正在加载世系树…');
|
||||
setLineageState('[data-lineage-list]', 'loading', '正在加载世系人物…');
|
||||
setActionStatus('');
|
||||
try {
|
||||
results = await Promise.all([
|
||||
api.genealogyDetail(genealogyId),
|
||||
@@ -681,9 +740,7 @@
|
||||
return;
|
||||
}
|
||||
['[data-lineage-tree]', '[data-lineage-list]'].forEach(function (selector) {
|
||||
var container = query(selector);
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">世系人物加载失败,请稍后重试</div>';
|
||||
setLineageState(selector, 'error', '世系人物加载失败,请稍后重试');
|
||||
});
|
||||
showMessage(error.message || '世系人物加载失败');
|
||||
}
|
||||
@@ -696,6 +753,7 @@
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !personId) return;
|
||||
setLineageState('[data-lineage-detail]', 'loading', '正在加载成员详情…');
|
||||
try {
|
||||
renderDetail(await api.lineagePersonDetail(genealogyId, personId));
|
||||
} catch (error) {
|
||||
@@ -704,6 +762,7 @@
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
setLineageState('[data-lineage-detail]', 'error', '人物详情加载失败,请稍后重试。');
|
||||
showMessage(error.message || '人物详情加载失败');
|
||||
}
|
||||
}
|
||||
@@ -720,7 +779,7 @@
|
||||
relationMode = mode;
|
||||
editingPersonId = '';
|
||||
setRelationFieldVisibility(mode === 'spouses');
|
||||
if (submitButton) submitButton.textContent = '新增' + relationLabel(mode);
|
||||
if (submitButton) setLineageSubmitLabel('新增' + relationLabel(mode));
|
||||
setFormStatus('当前将为“' + person.name + '”新增' + relationLabel(mode));
|
||||
}
|
||||
|
||||
@@ -777,7 +836,7 @@
|
||||
editingPersonId = person.personId;
|
||||
setRelationFieldVisibility(false);
|
||||
submitButton = query('[data-lineage-form] button[type="submit"]');
|
||||
if (submitButton) submitButton.textContent = '保存修改';
|
||||
if (submitButton) setLineageSubmitLabel('保存修改');
|
||||
setFormStatus('正在编辑“' + person.name + '”');
|
||||
}
|
||||
|
||||
@@ -787,7 +846,7 @@
|
||||
relationMode = '';
|
||||
editingPersonId = '';
|
||||
setRelationFieldVisibility(false);
|
||||
if (submitButton) submitButton.textContent = '保存成员';
|
||||
if (submitButton) setLineageSubmitLabel('保存成员');
|
||||
setFormStatus('');
|
||||
if (root.setTimeout) root.setTimeout(updateBindingFields, 0);
|
||||
}
|
||||
@@ -803,11 +862,11 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存成员...');
|
||||
setFormStatus('正在保存成员…', 'loading');
|
||||
try {
|
||||
if (editingPersonId) {
|
||||
result = await api.updateLineagePerson(genealogyId, editingPersonId, body);
|
||||
@@ -833,7 +892,7 @@
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
setFormStatus(error.message || '成员保存失败');
|
||||
setFormStatus(error.message || '成员保存失败', 'error');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -849,6 +908,7 @@
|
||||
if (!genealogyId || !personId) return;
|
||||
if (root.confirm && !root.confirm('确认停用“' + (person ? person.name : '该成员') + '”吗?有正常子女时后端会拒绝停用。')) return;
|
||||
setWritePending(true);
|
||||
setActionStatus('正在停用成员…', 'loading');
|
||||
try {
|
||||
await api.disableLineagePerson(genealogyId, personId);
|
||||
selectedPersonId = '';
|
||||
@@ -861,6 +921,7 @@
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
setActionStatus(error.message || '成员停用失败', 'error');
|
||||
showMessage(error.message || '成员停用失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
|
||||
@@ -133,9 +133,7 @@
|
||||
var source = values || {};
|
||||
var body = {};
|
||||
var memberName = trimOrUndefined(source.memberName);
|
||||
var relationName = source.relationName === undefined || source.relationName === null
|
||||
? undefined
|
||||
: stringValue(source.relationName).trim();
|
||||
var relationName = trimOrUndefined(source.relationName);
|
||||
var roleType = trimOrUndefined(source.roleType);
|
||||
var lineagePersonId = normalizeId(source.lineagePersonId);
|
||||
|
||||
@@ -175,8 +173,8 @@
|
||||
return Boolean(role && role !== 'owner');
|
||||
}
|
||||
|
||||
function canTransferOwner(role) {
|
||||
return role === 'owner';
|
||||
function canTransferOwner(role, canManage) {
|
||||
return role === 'owner' && canManage === true;
|
||||
}
|
||||
|
||||
function buildOwnerTransferBody(targetMemberId) {
|
||||
@@ -246,10 +244,39 @@
|
||||
else if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function setStatus(selector, message) {
|
||||
function setStatus(selector, message, type) {
|
||||
var target = query(selector);
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setMemberListState(type, message) {
|
||||
var target = query('[data-member-list]');
|
||||
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.innerHTML = '<div class="api-state api-state--' + escapeHtml(type) + '" data-api-state="' + escapeHtml(type) + '">' +
|
||||
escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setPendingButton(selector, pending, pendingLabel) {
|
||||
queryAll(selector).forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.textContent = pending ? pendingLabel : button.dataset.defaultLabel;
|
||||
button.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function syncLinks() {
|
||||
@@ -261,6 +288,9 @@
|
||||
queryAll('[data-member-form] input, [data-member-form] select, [data-member-form] button, [data-owner-transfer-form] select, [data-owner-transfer-form] button, [data-member-edit-id], [data-member-remove-id], [data-member-leave]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
setPendingButton('[data-member-submit]', writePending, '正在保存…');
|
||||
setPendingButton('[data-owner-transfer-submit]', writePending, '正在转让…');
|
||||
setPendingButton('[data-member-leave]', writePending, '正在退出…');
|
||||
}
|
||||
|
||||
function renderLineageOptions(data) {
|
||||
@@ -299,16 +329,21 @@
|
||||
members.forEach(function (member) { membersById[member.memberId] = member; });
|
||||
if (!container) return members;
|
||||
if (!Array.isArray(data) || (data.length && !members.length)) {
|
||||
container.innerHTML = '<div class="api-empty">成员响应缺少稳定 GenealogyMemberVo 字段,请联系后端核对。</div>';
|
||||
setMemberListState('error', '成员响应缺少稳定 GenealogyMemberVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = members.length ? members.map(function (member) {
|
||||
if (!members.length) {
|
||||
setMemberListState('empty', '暂无正常成员');
|
||||
renderTransferOptions(members);
|
||||
return members;
|
||||
}
|
||||
container.innerHTML = members.map(function (member) {
|
||||
return renderMemberRow(member, {
|
||||
actorRole: actorRole,
|
||||
currentMemberId: currentMemberId,
|
||||
canManage: managementAccess
|
||||
});
|
||||
}).join('') : '<div class="api-empty">暂无正常成员</div>';
|
||||
}).join('');
|
||||
renderTransferOptions(members);
|
||||
return members;
|
||||
}
|
||||
@@ -358,8 +393,9 @@
|
||||
}
|
||||
|
||||
function renderNoContext() {
|
||||
if (query('[data-member-list]')) query('[data-member-list]').innerHTML = '<div class="api-empty">请先选择家谱,再管理成员。</div>';
|
||||
setStatus('[data-member-overview]', '当前没有家谱上下文。');
|
||||
setMemberListState('forbidden', '请先选择家谱,再管理成员。');
|
||||
setStatus('[data-member-overview]', '当前没有家谱上下文。', 'forbidden');
|
||||
setStatus('[data-member-action-status]', '请先选择家谱,再进行成员操作。', 'forbidden');
|
||||
queryAll('[data-member-management], [data-member-leave-section], [data-owner-transfer-section]').forEach(function (element) {
|
||||
element.hidden = true;
|
||||
});
|
||||
@@ -381,6 +417,9 @@
|
||||
return;
|
||||
}
|
||||
syncLinks();
|
||||
setStatus('[data-member-overview]', '正在加载家谱和当前权限…', 'loading');
|
||||
setMemberListState('loading', '正在加载成员…');
|
||||
setStatus('[data-member-action-status]', '');
|
||||
try {
|
||||
results = await Promise.all([
|
||||
api.genealogyDetail(genealogyId),
|
||||
@@ -399,13 +438,13 @@
|
||||
setStatus('[data-member-overview]', (detail.genealogyName || '当前家谱') + ' · ' + members.length + ' 位正常成员 · 当前角色:' + (roleLabels[actorRole] || '成员'));
|
||||
queryAll('[data-member-management]').forEach(function (element) { element.hidden = !managementAccess; });
|
||||
queryAll('[data-member-leave-section]').forEach(function (element) { element.hidden = !currentMember || !canLeaveGenealogy(actorRole); });
|
||||
queryAll('[data-owner-transfer-section]').forEach(function (element) { element.hidden = !currentMember || !canTransferOwner(actorRole); });
|
||||
queryAll('[data-owner-transfer-section]').forEach(function (element) { element.hidden = !currentMember || !canTransferOwner(actorRole, managementAccess); });
|
||||
renderLineageOptions(results[3]);
|
||||
renderMembers(results[2]);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-member-list]')) query('[data-member-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱成员。' : '成员加载失败,请稍后重试。') + '</div>';
|
||||
setMemberListState(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '当前账号无权查看该家谱成员。' : '成员加载失败,请稍后重试。');
|
||||
setStatus('[data-member-overview]', isForbidden(error) ? '当前账号无权查看该家谱成员。' : '成员加载失败,请稍后重试。', isForbidden(error) ? 'forbidden' : 'error');
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱成员。' : (error.message || '成员加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -429,12 +468,12 @@
|
||||
body = buildMemberUpdateBody(getFormValues(form));
|
||||
validation = validateMemberUpdateBody(body);
|
||||
if (validation) {
|
||||
setStatus('[data-member-form-status]', validation);
|
||||
setStatus('[data-member-form-status]', validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidLineagePersonId;
|
||||
setWritePending(true);
|
||||
setStatus('[data-member-form-status]', '正在保存成员…');
|
||||
setStatus('[data-member-form-status]', '正在保存成员…', 'loading');
|
||||
try {
|
||||
saved = normalizeMember(await api.updateGenealogyMember(genealogyId, selectedMemberId, body));
|
||||
if (!saved || saved.memberId !== selectedMemberId) throw new Error('成员修改响应缺少稳定 memberId');
|
||||
@@ -445,7 +484,7 @@
|
||||
showMessage('成员信息已更新');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-member-form-status]', isForbidden(error) ? '当前账号无权修改该成员。' : (error.message || '成员修改失败'));
|
||||
setStatus('[data-member-form-status]', isForbidden(error) ? '当前账号无权修改该成员。' : (error.message || '成员修改失败'), isForbidden(error) ? 'forbidden' : 'error');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -459,6 +498,7 @@
|
||||
if (writePending || !genealogyId || !target || !managementAccess || !canRemoveMember(actorRole, target.roleType) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认将“' + memberDisplayName(target) + '”移出当前家谱吗?该操作不会删除其世系人物资料。')) return;
|
||||
setWritePending(true);
|
||||
setStatus('[data-member-action-status]', '正在移出成员…', 'loading');
|
||||
try {
|
||||
await api.removeGenealogyMember(genealogyId, memberId);
|
||||
if ((await fetchAndRenderMembers(api, genealogyId)).some(function (member) { return member.memberId === memberId; })) {
|
||||
@@ -468,6 +508,7 @@
|
||||
showMessage('成员已移出家谱');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-member-action-status]', isForbidden(error) ? '当前账号无权移出该成员。' : (error.message || '移出成员失败'), isForbidden(error) ? 'forbidden' : 'error');
|
||||
showMessage(isForbidden(error) ? '当前账号无权移出该成员。' : (error.message || '移出成员失败'));
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
@@ -481,11 +522,13 @@
|
||||
if (writePending || !genealogyId || !currentMemberId || !canLeaveGenealogy(actorRole) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认退出当前家谱吗?退出后需重新受邀才能进入。世系人物资料不会被删除。')) return;
|
||||
setWritePending(true);
|
||||
setStatus('[data-member-action-status]', '正在退出当前家谱…', 'loading');
|
||||
try {
|
||||
await api.leaveGenealogy(genealogyId);
|
||||
if (root.location) root.location.href = 'profile-families.html';
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-member-action-status]', isForbidden(error) ? '当前账号无权退出该家谱。' : (error.message || '退出家谱失败'), isForbidden(error) ? 'forbidden' : 'error');
|
||||
showMessage(isForbidden(error) ? '当前账号无权退出该家谱。' : (error.message || '退出家谱失败'));
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
@@ -499,21 +542,21 @@
|
||||
var validation = validateOwnerTransferBody(body, currentMemberId);
|
||||
var target = membersById[body.targetMemberId];
|
||||
|
||||
if (writePending || !genealogyId || !canTransferOwner(actorRole) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !genealogyId || !canTransferOwner(actorRole, managementAccess) || redirectUnauthorized(api)) return;
|
||||
if (validation || !target) {
|
||||
setStatus('[data-owner-transfer-status]', validation || '请选择成员列表中的新谱主');
|
||||
setStatus('[data-owner-transfer-status]', validation || '请选择成员列表中的新谱主', 'error');
|
||||
return;
|
||||
}
|
||||
if (root.confirm && !root.confirm('确认将谱主转让给“' + memberDisplayName(target) + '”吗?转让后当前账号将变为管理员。')) return;
|
||||
setWritePending(true);
|
||||
setStatus('[data-owner-transfer-status]', '正在转让谱主…');
|
||||
setStatus('[data-owner-transfer-status]', '正在转让谱主…', 'loading');
|
||||
try {
|
||||
await api.transferGenealogyOwner(genealogyId, body);
|
||||
await loadPage();
|
||||
setStatus('[data-owner-transfer-status]', '谱主已转让');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus('[data-owner-transfer-status]', isForbidden(error) ? '只有当前谱主可以执行转让。' : (error.message || '谱主转让失败'));
|
||||
setStatus('[data-owner-transfer-status]', isForbidden(error) ? '只有当前谱主可以执行转让。' : (error.message || '谱主转让失败'), isForbidden(error) ? 'forbidden' : 'error');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
|
||||
+124
-37
@@ -15,6 +15,7 @@
|
||||
|
||||
var documentRef = root.document;
|
||||
var memosById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
var writePending = false;
|
||||
|
||||
function getApi() {
|
||||
@@ -228,18 +229,25 @@
|
||||
return memoId ? url + '&memoId=' + encodeURIComponent(memoId) : url;
|
||||
}
|
||||
|
||||
function canEditRecords(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function renderMemoRow(memo) {
|
||||
var summary = [];
|
||||
var actions = '<button class="pill" type="button" data-memo-detail-id="' +
|
||||
escapeHtml(memo.memoId) + '">查看详情</button>';
|
||||
|
||||
summary.push(memo.completed === '1' ? '已完成' : '未完成');
|
||||
if (memo.remindTime) summary.push('提醒:' + memo.remindTime);
|
||||
if (memo.memoContent) summary.push(memo.memoContent);
|
||||
if (canEditContent) {
|
||||
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(memo.genealogyId, memo.memoId)) +
|
||||
'">编辑</a><button class="pill is-danger" type="button" data-memo-delete-id="' +
|
||||
escapeHtml(memo.memoId) + '">删除</button>';
|
||||
}
|
||||
return '<article class="module-row memo-row"><div><h3>' + escapeHtml(memo.memoTitle) + '</h3><p>' +
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
|
||||
'<button class="pill" type="button" data-memo-detail-id="' + escapeHtml(memo.memoId) + '">查看详情</button>' +
|
||||
'<a class="pill" href="' + escapeHtml(buildEditUrl(memo.genealogyId, memo.memoId)) + '">编辑</a>' +
|
||||
'<button class="pill is-danger" type="button" data-memo-delete-id="' + escapeHtml(memo.memoId) + '">删除</button>' +
|
||||
'</div></article>';
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' + actions + '</div></article>';
|
||||
}
|
||||
|
||||
function detailValue(label, value) {
|
||||
@@ -248,9 +256,16 @@
|
||||
|
||||
function renderMemoDetail(memo) {
|
||||
var count;
|
||||
var actions = '';
|
||||
|
||||
if (!memo) return '<div class="api-empty">请选择一条备忘录查看详情</div>';
|
||||
if (!memo) return stateHtml('empty', '请选择一条备忘录查看详情');
|
||||
count = attachmentCount(memo.mediaOssIds);
|
||||
if (canEditContent) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(memo.genealogyId, memo.memoId)) + '">编辑备忘录</a>' +
|
||||
'<button class="btn ghost" type="button" data-memo-delete-id="' +
|
||||
escapeHtml(memo.memoId) + '">删除备忘录</button></div>';
|
||||
}
|
||||
return '<div class="form-like memo-detail">' +
|
||||
detailValue('备忘标题', memo.memoTitle) +
|
||||
detailValue('备忘内容', memo.memoContent) +
|
||||
@@ -259,9 +274,7 @@
|
||||
detailValue('附件', count ? count + ' 个附件' : '未上传') +
|
||||
detailValue('记录人', memo.appUserNickName) +
|
||||
detailValue('备注', memo.remark) +
|
||||
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(memo.genealogyId, memo.memoId)) + '">编辑备忘录</a>' +
|
||||
'<button class="btn ghost" type="button" data-memo-delete-id="' + escapeHtml(memo.memoId) + '">删除备忘录</button></div>';
|
||||
'</div>' + actions;
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
@@ -279,6 +292,10 @@
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
@@ -288,13 +305,38 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var target = query('[data-memo-form-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
function stateHtml(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) return root.ProfileUI.renderApiState(type, message);
|
||||
return '<div class="api-state api-state--' + type + '" role="' +
|
||||
(type === 'error' || type === 'forbidden' ? 'alert' : 'status') +
|
||||
'" aria-live="polite">' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
function setState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.innerHTML = stateHtml(type, message);
|
||||
}
|
||||
|
||||
function setFormStatus(message, type) {
|
||||
var target = query('[data-memo-form-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message, type) {
|
||||
var placeholder = query('[data-memo-editor-placeholder]');
|
||||
|
||||
queryAll('[data-memo-editor], [data-memo-editor-action]').forEach(function (element) {
|
||||
@@ -302,15 +344,39 @@
|
||||
});
|
||||
if (placeholder) {
|
||||
placeholder.hidden = Boolean(enabled);
|
||||
if (message) placeholder.textContent = message;
|
||||
if (!enabled && message) {
|
||||
if (type) setState(placeholder, type, message);
|
||||
else placeholder.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWritePending(value) {
|
||||
function setEditAccess(enabled) {
|
||||
canEditContent = Boolean(enabled);
|
||||
queryAll('[data-memo-create-link]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var genealogy = await api.genealogyDetail(genealogyId);
|
||||
|
||||
setEditAccess(canEditRecords(genealogy));
|
||||
return genealogy;
|
||||
}
|
||||
|
||||
function setWritePending(value, label) {
|
||||
var submit = query('[data-memo-form] [type="submit"]');
|
||||
|
||||
writePending = Boolean(value);
|
||||
queryAll('[data-memo-form] button, [data-memo-form] input, [data-memo-form] textarea, [data-memo-form] select, [data-memo-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
if (submit) {
|
||||
if (!submit.dataset.defaultLabel) submit.dataset.defaultLabel = submit.textContent;
|
||||
submit.textContent = writePending && label ? label : submit.dataset.defaultLabel;
|
||||
submit.setAttribute('aria-busy', writePending ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function syncLinks() {
|
||||
@@ -322,10 +388,11 @@
|
||||
var detail = query('[data-memo-detail]');
|
||||
var message = '请先选择家谱,再查看或维护备忘录。';
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setEditorEnabled(false, message);
|
||||
setFormStatus(message);
|
||||
setState(list, 'empty', message);
|
||||
setState(detail, 'empty', '当前没有家谱上下文。');
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, message, 'empty');
|
||||
setFormStatus(message, 'empty');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -347,11 +414,11 @@
|
||||
});
|
||||
if (!container) return memos;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">备忘录响应缺少稳定 MemoVo 字段,请联系后端核对。</div>';
|
||||
setState(container, 'error', '备忘录响应缺少稳定 MemoVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!memos.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无备忘录</div>';
|
||||
setState(container, 'empty', '暂无备忘录');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = memos.map(renderMemoRow).join('');
|
||||
@@ -370,6 +437,7 @@
|
||||
var memo;
|
||||
|
||||
if (!genealogyId || !normalizeId(memoId) || redirectUnauthorized(api)) return null;
|
||||
setState(query('[data-memo-detail]'), 'loading', '正在加载备忘录详情…');
|
||||
try {
|
||||
memo = normalizeMemo(await api.memoDetail(genealogyId, memoId));
|
||||
if (!memo) throw new Error('备忘录详情响应缺少稳定 MemoVo 字段');
|
||||
@@ -378,7 +446,11 @@
|
||||
return memo;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
setState(
|
||||
query('[data-memo-detail]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该备忘录。' : '备忘录详情加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该备忘录。' : (error.message || '备忘录详情加载失败'));
|
||||
return null;
|
||||
}
|
||||
@@ -394,7 +466,9 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncLinks();
|
||||
setState(query('[data-memo-list]'), 'loading', '正在加载备忘录…');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
memos = renderMemos(await api.memos(genealogyId));
|
||||
requestedMemoId = getCurrentMemoId();
|
||||
if (requestedMemoId && memosById[requestedMemoId]) {
|
||||
@@ -406,11 +480,12 @@
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-memo-list]')) {
|
||||
query('[data-memo-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : '备忘录加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
setEditAccess(false);
|
||||
setState(
|
||||
query('[data-memo-list]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : '备忘录加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的备忘录。' : (error.message || '备忘录加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -457,8 +532,11 @@
|
||||
if (!genealogyId) return;
|
||||
memoId = getCurrentMemoId();
|
||||
syncLinks();
|
||||
setEditorEnabled(false, '正在加载备忘录编辑信息…');
|
||||
setEditorEnabled(false, '正在加载备忘录编辑信息…', 'loading');
|
||||
setFormStatus('正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的备忘录。' };
|
||||
if (memoId) {
|
||||
memo = normalizeMemo(await api.memoDetail(genealogyId, memoId));
|
||||
if (!memo) throw new Error('备忘录详情响应缺少稳定 MemoVo 字段');
|
||||
@@ -466,12 +544,17 @@
|
||||
if (query('[data-memo-editor-title]')) query('[data-memo-editor-title]').textContent = '编辑备忘录';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, isForbidden(error)
|
||||
? '当前账号无权维护该家谱的备忘录。'
|
||||
: (error.message || '备忘录编辑信息加载失败'));
|
||||
setFormStatus(error.message || '备忘录编辑信息加载失败');
|
||||
: (error.message || '备忘录编辑信息加载失败'), getApiStateType(error));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的备忘录。' : (error.message || '备忘录编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,16 +569,16 @@
|
||||
var savedId;
|
||||
var verifiedDetail;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
body = buildMemoBody(getFormValues(form));
|
||||
validation = validateMemoBody(body);
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存备忘录…');
|
||||
setWritePending(true, '正在保存…');
|
||||
setFormStatus('正在保存备忘录…', 'loading');
|
||||
try {
|
||||
result = memoId
|
||||
? await api.updateMemo(genealogyId, memoId, body)
|
||||
@@ -508,7 +591,10 @@
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权保存该备忘录。' : (error.message || '备忘录保存失败'));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权保存该备忘录。' : (error.message || '备忘录保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -519,7 +605,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var memo = memosById[memoId];
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(memoId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(memoId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (memo ? memo.memoTitle : '该备忘录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -589,6 +675,7 @@
|
||||
validateMemoBody: validateMemoBody,
|
||||
normalizeMemo: normalizeMemo,
|
||||
matchesSavedMemo: matchesSavedMemo,
|
||||
canEditRecords: canEditRecords,
|
||||
isEditableMemo: isEditableMemo,
|
||||
renderMemoDetail: renderMemoDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
|
||||
+126
-37
@@ -15,6 +15,7 @@
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
var writePending = false;
|
||||
var meritTypes = {
|
||||
donation: '捐赠',
|
||||
@@ -284,17 +285,24 @@
|
||||
return meritId ? url + '&meritId=' + encodeURIComponent(meritId) : url;
|
||||
}
|
||||
|
||||
function canEditRecords(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function renderMeritRow(record) {
|
||||
var summary = [meritTypes[record.meritType]];
|
||||
var actions = '<button class="pill" type="button" data-merit-detail-id="' +
|
||||
escapeHtml(record.meritId) + '">查看详情</button>';
|
||||
|
||||
if (record.meritTime) summary.push(record.meritTime);
|
||||
if (record.amount) summary.push('金额:' + record.amount);
|
||||
if (canEditContent) {
|
||||
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.meritId)) +
|
||||
'">编辑</a><button class="pill is-danger" type="button" data-merit-delete-id="' +
|
||||
escapeHtml(record.meritId) + '">删除</button>';
|
||||
}
|
||||
return '<article class="module-row merit-row"><div><h3>' + escapeHtml(record.meritTitle) + '</h3><p>' +
|
||||
escapeHtml(record.donorName + ' · ' + summary.join(' · ')) + '</p></div><div class="row-actions">' +
|
||||
'<button class="pill" type="button" data-merit-detail-id="' + escapeHtml(record.meritId) + '">查看详情</button>' +
|
||||
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.meritId)) + '">编辑</a>' +
|
||||
'<button class="pill is-danger" type="button" data-merit-delete-id="' + escapeHtml(record.meritId) + '">删除</button>' +
|
||||
'</div></article>';
|
||||
escapeHtml(record.donorName + ' · ' + summary.join(' · ')) + '</p></div><div class="row-actions">' + actions + '</div></article>';
|
||||
}
|
||||
|
||||
function detailValue(label, value) {
|
||||
@@ -302,7 +310,15 @@
|
||||
}
|
||||
|
||||
function renderMeritDetail(record) {
|
||||
if (!record) return '<div class="api-empty">请选择一条功德记录查看详情</div>';
|
||||
var actions = '';
|
||||
|
||||
if (!record) return stateHtml('empty', '请选择一条功德记录查看详情');
|
||||
if (canEditContent) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.meritId)) + '">编辑功德记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-merit-delete-id="' +
|
||||
escapeHtml(record.meritId) + '">删除功德记录</button></div>';
|
||||
}
|
||||
return '<div class="form-like merit-detail">' +
|
||||
detailValue('功德人', record.donorName) +
|
||||
detailValue('功德类型', meritTypes[record.meritType]) +
|
||||
@@ -312,9 +328,7 @@
|
||||
detailValue('功德时间', record.meritTime) +
|
||||
detailValue('记录人', record.appUserNickName) +
|
||||
detailValue('备注', record.remark) +
|
||||
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.meritId)) + '">编辑功德记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-merit-delete-id="' + escapeHtml(record.meritId) + '">删除功德记录</button></div>';
|
||||
'</div>' + actions;
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
@@ -332,6 +346,10 @@
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
@@ -341,13 +359,38 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var target = query('[data-merit-form-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
function stateHtml(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) return root.ProfileUI.renderApiState(type, message);
|
||||
return '<div class="api-state api-state--' + type + '" role="' +
|
||||
(type === 'error' || type === 'forbidden' ? 'alert' : 'status') +
|
||||
'" aria-live="polite">' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
function setState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.innerHTML = stateHtml(type, message);
|
||||
}
|
||||
|
||||
function setFormStatus(message, type) {
|
||||
var target = query('[data-merit-form-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message, type) {
|
||||
var placeholder = query('[data-merit-editor-placeholder]');
|
||||
|
||||
queryAll('[data-merit-editor], [data-merit-editor-action]').forEach(function (element) {
|
||||
@@ -355,15 +398,39 @@
|
||||
});
|
||||
if (placeholder) {
|
||||
placeholder.hidden = Boolean(enabled);
|
||||
if (message) placeholder.textContent = message;
|
||||
if (!enabled && message) {
|
||||
if (type) setState(placeholder, type, message);
|
||||
else placeholder.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWritePending(value) {
|
||||
function setEditAccess(enabled) {
|
||||
canEditContent = Boolean(enabled);
|
||||
queryAll('[data-merit-create-link]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var genealogy = await api.genealogyDetail(genealogyId);
|
||||
|
||||
setEditAccess(canEditRecords(genealogy));
|
||||
return genealogy;
|
||||
}
|
||||
|
||||
function setWritePending(value, label) {
|
||||
var submit = query('[data-merit-form] [type="submit"]');
|
||||
|
||||
writePending = Boolean(value);
|
||||
queryAll('[data-merit-form] button, [data-merit-form] input, [data-merit-form] textarea, [data-merit-form] select, [data-merit-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
if (submit) {
|
||||
if (!submit.dataset.defaultLabel) submit.dataset.defaultLabel = submit.textContent;
|
||||
submit.textContent = writePending && label ? label : submit.dataset.defaultLabel;
|
||||
submit.setAttribute('aria-busy', writePending ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function syncLinks() {
|
||||
@@ -375,10 +442,11 @@
|
||||
var detail = query('[data-merit-detail]');
|
||||
var message = '请先选择家谱,再查看或维护功德记录。';
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setEditorEnabled(false, message);
|
||||
setFormStatus(message);
|
||||
setState(list, 'empty', message);
|
||||
setState(detail, 'empty', '当前没有家谱上下文。');
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, message, 'empty');
|
||||
setFormStatus(message, 'empty');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -400,11 +468,11 @@
|
||||
});
|
||||
if (!container) return records;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">功德记录响应缺少稳定 MeritRecordVo 字段,请联系后端核对。</div>';
|
||||
setState(container, 'error', '功德记录响应缺少稳定 MeritRecordVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!records.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无功德记录</div>';
|
||||
setState(container, 'empty', '暂无功德记录');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = records.map(renderMeritRow).join('');
|
||||
@@ -423,6 +491,7 @@
|
||||
var record;
|
||||
|
||||
if (!genealogyId || !normalizeId(meritId) || redirectUnauthorized(api)) return null;
|
||||
setState(query('[data-merit-detail]'), 'loading', '正在加载功德记录详情…');
|
||||
try {
|
||||
record = normalizeMeritRecord(await api.meritRecordDetail(genealogyId, meritId));
|
||||
if (!record) throw new Error('功德记录详情响应缺少稳定 MeritRecordVo 字段');
|
||||
@@ -431,7 +500,11 @@
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
setState(
|
||||
query('[data-merit-detail]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该功德记录。' : '功德记录详情加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该功德记录。' : (error.message || '功德记录详情加载失败'));
|
||||
return null;
|
||||
}
|
||||
@@ -447,7 +520,9 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncLinks();
|
||||
setState(query('[data-merit-list]'), 'loading', '正在加载功德记录…');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
records = renderMeritRecords(await api.meritRecords(genealogyId));
|
||||
requestedMeritId = getCurrentMeritId();
|
||||
if (requestedMeritId && recordsById[requestedMeritId]) {
|
||||
@@ -459,11 +534,12 @@
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-merit-list]')) {
|
||||
query('[data-merit-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的功德记录。' : '功德记录加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
setEditAccess(false);
|
||||
setState(
|
||||
query('[data-merit-list]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的功德记录。' : '功德记录加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的功德记录。' : (error.message || '功德记录加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -517,8 +593,11 @@
|
||||
if (!genealogyId) return;
|
||||
meritId = getCurrentMeritId();
|
||||
syncLinks();
|
||||
setEditorEnabled(false, '正在加载功德记录编辑信息…');
|
||||
setEditorEnabled(false, '正在加载功德记录编辑信息…', 'loading');
|
||||
setFormStatus('正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的功德记录。' };
|
||||
if (meritId) {
|
||||
record = normalizeMeritRecord(await api.meritRecordDetail(genealogyId, meritId));
|
||||
if (!record) throw new Error('功德记录详情响应缺少稳定 MeritRecordVo 字段');
|
||||
@@ -526,12 +605,17 @@
|
||||
if (query('[data-merit-editor-title]')) query('[data-merit-editor-title]').textContent = '编辑功德记录';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, isForbidden(error)
|
||||
? '当前账号无权维护该家谱的功德记录。'
|
||||
: (error.message || '功德记录编辑信息加载失败'));
|
||||
setFormStatus(error.message || '功德记录编辑信息加载失败');
|
||||
: (error.message || '功德记录编辑信息加载失败'), getApiStateType(error));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的功德记录。' : (error.message || '功德记录编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,18 +630,19 @@
|
||||
var savedId;
|
||||
var verifiedDetail;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (root.AppRichEditor && root.AppRichEditor.syncAll) root.AppRichEditor.syncAll();
|
||||
body = buildMeritRecordBody(getFormValues(form));
|
||||
validation = validateMeritRecordBody(body);
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidAmount;
|
||||
delete body.invalidAmountPrecision;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存功德记录…');
|
||||
setWritePending(true, '正在保存…');
|
||||
setFormStatus('正在保存功德记录…', 'loading');
|
||||
try {
|
||||
result = meritId
|
||||
? await api.updateMeritRecord(genealogyId, meritId, body)
|
||||
@@ -570,7 +655,10 @@
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权保存该功德记录。' : (error.message || '功德记录保存失败'));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权保存该功德记录。' : (error.message || '功德记录保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -581,7 +669,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var record = recordsById[meritId];
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(meritId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(meritId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.meritTitle : '该功德记录') + '”吗?删除后不可恢复。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -635,6 +723,7 @@
|
||||
validateMeritRecordBody: validateMeritRecordBody,
|
||||
normalizeMeritRecord: normalizeMeritRecord,
|
||||
matchesSavedMerit: matchesSavedMerit,
|
||||
canEditRecords: canEditRecords,
|
||||
isEditableMerit: isEditableMerit,
|
||||
renderMeritDetail: renderMeritDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
|
||||
@@ -178,10 +178,23 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateStatus(message) {
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function setNotificationState(target, type, message) {
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message || '';
|
||||
}
|
||||
|
||||
function updateStatus(type, message) {
|
||||
var status = query('[data-notification-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
setNotificationState(status, type, message);
|
||||
}
|
||||
|
||||
function setWritePending(value) {
|
||||
@@ -203,21 +216,27 @@
|
||||
});
|
||||
if (!container) return notifications;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">通知列表响应缺少稳定 NotificationView 字段,请联系后端核对。</div>';
|
||||
setNotificationState(container, 'error', '通知列表响应无效,请稍后重试。');
|
||||
return [];
|
||||
}
|
||||
if (!notifications.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无通知</div>';
|
||||
setNotificationState(container, 'empty', '当前暂无通知。');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = notifications.map(renderNotificationRow).join('');
|
||||
if (writePending) setWritePending(true);
|
||||
return notifications;
|
||||
}
|
||||
|
||||
function renderDetail(notification) {
|
||||
var container = query('[data-notification-detail]');
|
||||
|
||||
if (container) container.innerHTML = renderNotificationDetail(notification);
|
||||
if (!container) return;
|
||||
if (!notification) {
|
||||
setNotificationState(container, 'empty', '请选择一条通知查看详情。');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = renderNotificationDetail(notification);
|
||||
}
|
||||
|
||||
async function loadNotificationDetail(notificationId) {
|
||||
@@ -225,6 +244,7 @@
|
||||
var notification;
|
||||
|
||||
if (!normalizeId(notificationId) || redirectUnauthorized(api)) return null;
|
||||
setNotificationState(query('[data-notification-detail]'), 'loading', '正在读取通知详情…');
|
||||
try {
|
||||
notification = normalizeNotification(await api.notificationDetail(notificationId));
|
||||
if (!notification || !matchesNotification(notification, notificationId)) {
|
||||
@@ -236,8 +256,8 @@
|
||||
return notification;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
updateStatus(error.message || '通知详情加载失败');
|
||||
setNotificationState(query('[data-notification-detail]'), isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看该通知详情。' : '通知详情加载失败,请稍后重试。');
|
||||
updateStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看该通知详情。' : (error.message || '通知详情加载失败,请稍后重试。'));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -257,10 +277,12 @@
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!api.notifications || !api.notificationDetail || !api.unreadNotificationCount) {
|
||||
updateStatus('消息接口初始化失败,请刷新后重试');
|
||||
setNotificationState(query('[data-notification-list]'), 'error', '消息服务初始化失败,请刷新后重试。');
|
||||
updateStatus('error', '消息服务初始化失败,请刷新后重试。');
|
||||
return;
|
||||
}
|
||||
updateStatus('正在同步消息…');
|
||||
setNotificationState(query('[data-notification-list]'), 'loading', '正在同步消息…');
|
||||
updateStatus('loading', '正在同步消息…');
|
||||
try {
|
||||
result = await Promise.all([
|
||||
api.notifications(buildNotificationQuery(currentFilter())),
|
||||
@@ -273,12 +295,12 @@
|
||||
: (notifications[0] && notifications[0].notificationId);
|
||||
if (nextSelectedId) await loadNotificationDetail(nextSelectedId);
|
||||
else renderDetail(null);
|
||||
updateStatus('消息已同步');
|
||||
updateStatus('', '消息已同步');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
renderNotifications(null);
|
||||
renderDetail(null);
|
||||
updateStatus(error.message || '消息同步失败');
|
||||
setNotificationState(query('[data-notification-detail]'), isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看通知详情。' : '消息同步失败,请稍后重试。');
|
||||
updateStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限查看通知列表。' : (error.message || '消息同步失败,请稍后重试。'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +310,7 @@
|
||||
|
||||
if (writePending || !normalizeId(notificationId) || !canMarkRead(notification) || redirectUnauthorized(api)) return;
|
||||
setWritePending(true);
|
||||
updateStatus('loading', '正在标记通知为已读…');
|
||||
try {
|
||||
await api.markNotificationRead(notificationId);
|
||||
selectedNotificationId = notificationId;
|
||||
@@ -295,7 +318,7 @@
|
||||
await loadNotifications();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
updateStatus(error.message || '标记通知已读失败');
|
||||
updateStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限标记该通知为已读。' : (error.message || '标记通知已读失败,请稍后重试。'));
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -306,17 +329,18 @@
|
||||
|
||||
if (writePending || redirectUnauthorized(api)) return;
|
||||
if (!api.markAllNotificationsRead) {
|
||||
updateStatus('消息接口初始化失败,请刷新后重试');
|
||||
updateStatus('error', '消息服务初始化失败,请刷新后重试。');
|
||||
return;
|
||||
}
|
||||
setWritePending(true);
|
||||
updateStatus('loading', '正在将全部通知标记为已读…');
|
||||
try {
|
||||
await api.markAllNotificationsRead();
|
||||
showMessage('已全部标记为已读');
|
||||
await loadNotifications();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
updateStatus(error.message || '全部标记已读失败');
|
||||
updateStatus(isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '暂无权限标记全部通知为已读。' : (error.message || '全部标记已读失败,请稍后重试。'));
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -368,6 +392,7 @@
|
||||
renderNotificationRow: renderNotificationRow,
|
||||
renderNotificationDetail: renderNotificationDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
loadNotifications: loadNotifications,
|
||||
init: init
|
||||
};
|
||||
|
||||
@@ -1,12 +1,43 @@
|
||||
(function (window, $) {
|
||||
'use strict';
|
||||
|
||||
if (!$) return;
|
||||
|
||||
var profileUI = window.ProfileUI || {};
|
||||
var layuiReady = false;
|
||||
var layerApi = null;
|
||||
var genealogyContextStorageKey = 'genealogy_current_context_v1';
|
||||
var mobileMenuId = 'profile-mobile-menu';
|
||||
var apiStateTypes = ['loading', 'empty', 'error', 'forbidden'];
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderApiState(type, message) {
|
||||
var role;
|
||||
var busy;
|
||||
|
||||
if (apiStateTypes.indexOf(type) < 0) throw new TypeError('不支持的业务状态:' + type);
|
||||
role = type === 'error' || type === 'forbidden' ? 'alert' : 'status';
|
||||
busy = type === 'loading' ? ' aria-busy="true"' : '';
|
||||
return '<div class="api-state api-state--' + type + '" data-api-state="' + type +
|
||||
'" role="' + role + '" aria-live="polite"' + busy + '>' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setApiState(element, type, message) {
|
||||
if (!element) return;
|
||||
element.innerHTML = renderApiState(type, message);
|
||||
}
|
||||
|
||||
profileUI.renderApiState = renderApiState;
|
||||
profileUI.setApiState = setApiState;
|
||||
window.ProfileUI = profileUI;
|
||||
|
||||
if (!$) return;
|
||||
|
||||
function getSessionStorage() {
|
||||
try {
|
||||
@@ -231,13 +262,25 @@
|
||||
|
||||
if (!value || value.charAt(0) === '#' || /^(?:https?:|mailto:|tel:)/i.test(value)) return value;
|
||||
if (value.indexOf('profile-families.html') === 0) return value;
|
||||
return 'profile-families.html?next=' + encodeURIComponent(value.split('?')[0]);
|
||||
return 'profile-families.html?next=' + encodeURIComponent(value);
|
||||
}
|
||||
|
||||
var genealogyContextPagePattern = /(?:^|\/)profile-(?:admin-permissions|album(?:-detail|-edit)?|article(?:-edit)?|ceremony(?:-detail)?|content|family-admin|family-home|feed(?:-detail|-edit)?|generation|gift(?:-edit)?|growth(?:-edit)?|join-review|memo(?:-edit)?|merit(?:-edit)?|relative(?:-edit)?|tree|video(?:-edit)?)\.html(?:[?#]|$)/;
|
||||
|
||||
function isGenealogyContextHref(href) {
|
||||
var value = String(href || '');
|
||||
|
||||
return !/^(?:https?:|mailto:|tel:)/i.test(value) && genealogyContextPagePattern.test(value);
|
||||
}
|
||||
|
||||
function syncGenealogyContextLinks() {
|
||||
var genealogyId = getGenealogyId();
|
||||
|
||||
$('[data-genealogy-context-link]').each(function () {
|
||||
$('[data-genealogy-context-link], a[href]').filter(function () {
|
||||
var $link = $(this);
|
||||
|
||||
return $link.is('[data-genealogy-context-link]') || isGenealogyContextHref($link.attr('href'));
|
||||
}).each(function () {
|
||||
var $link = $(this);
|
||||
|
||||
$link.attr('href', genealogyId ? withGenealogyId($link.attr('href'), genealogyId) : getGenealogyEntryUrl($link.attr('href')));
|
||||
@@ -463,7 +506,7 @@
|
||||
syncGenealogyContextLinks();
|
||||
});
|
||||
|
||||
window.ProfileUI = {
|
||||
Object.assign(profileUI, {
|
||||
confirm: openConfirm,
|
||||
closeLogout: closeLegacyLogout,
|
||||
initLayui: initLayui,
|
||||
@@ -471,10 +514,13 @@
|
||||
normalizeGenealogyId: normalizeGenealogyId,
|
||||
withGenealogyId: withGenealogyId,
|
||||
getGenealogyEntryUrl: getGenealogyEntryUrl,
|
||||
isGenealogyContextHref: isGenealogyContextHref,
|
||||
getCurrentGenealogyContext: getCurrentGenealogyContext,
|
||||
setCurrentGenealogyContext: setCurrentGenealogyContext,
|
||||
clearCurrentGenealogyContext: clearCurrentGenealogyContext,
|
||||
refreshCurrentGenealogyContext: refreshCurrentGenealogyContext,
|
||||
syncGenealogyContextLinks: syncGenealogyContextLinks
|
||||
};
|
||||
syncGenealogyContextLinks: syncGenealogyContextLinks,
|
||||
renderApiState: renderApiState,
|
||||
setApiState: setApiState
|
||||
});
|
||||
})(window, window.jQuery || (window.layui && window.layui.$));
|
||||
|
||||
+60
-72
@@ -33,25 +33,24 @@
|
||||
return String(displayName).charAt(0) || '家';
|
||||
}
|
||||
|
||||
function getFinalRegionCode(profile) {
|
||||
var source = profile || {};
|
||||
function getSexText(value) {
|
||||
var labels = { '0': '男', '1': '女', '2': '未知' };
|
||||
|
||||
return source.districtCode || source.cityCode || source.provinceCode || '';
|
||||
}
|
||||
|
||||
function buildRegionText(profile) {
|
||||
return getFinalRegionCode(profile) ? '加载中...' : '待填写';
|
||||
return labels[String(value)] || '待填写';
|
||||
}
|
||||
|
||||
function buildProfileView(profile) {
|
||||
// 页面展示模型和接口返回分开,后续改文案不影响接口契约。
|
||||
return {
|
||||
displayName: profileValue(profile, 'nickName', '未设置昵称'),
|
||||
realName: profileValue(profile, 'realName', '待填写'),
|
||||
avatarText: getAvatarText(profile),
|
||||
avatarStatus: profile && profile.avatar ? '已设置' : '未设置',
|
||||
phone: profileValue(profile, 'phone', '未绑定'),
|
||||
sex: profileValue(profile, 'sex', '待填写'),
|
||||
sex: getSexText(profile && profile.sex),
|
||||
birthday: profileValue(profile, 'birthday', '待填写'),
|
||||
regionText: buildRegionText(profile || {})
|
||||
email: profileValue(profile, 'email', '待填写'),
|
||||
regionText: '当前资料接口未提供'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +81,12 @@
|
||||
return body;
|
||||
}
|
||||
|
||||
function validateProfileUpdateBody(body) {
|
||||
if (!body || Object.keys(body).length === 0) return '请至少填写一项资料';
|
||||
if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) return '请输入正确的邮箱地址';
|
||||
return '';
|
||||
}
|
||||
|
||||
function getApi() {
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
@@ -129,17 +134,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(form, message) {
|
||||
function setStatus(form, type, message) {
|
||||
var statusNode = query('[data-profile-status]', form);
|
||||
|
||||
if (statusNode) {
|
||||
statusNode.textContent = message || '';
|
||||
if (!statusNode) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(statusNode, type, message);
|
||||
return;
|
||||
}
|
||||
statusNode.textContent = message || '';
|
||||
}
|
||||
|
||||
function setSubmitting(form, isSubmitting) {
|
||||
queryAll('button[type="submit"]', form).forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = isSubmitting;
|
||||
button.textContent = isSubmitting ? '保存中…' : button.dataset.defaultLabel;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,18 +165,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setProfileLoadStatus(message) {
|
||||
function setProfileLoadStatus(type, message) {
|
||||
var statusNode = query('[data-profile-load-status]');
|
||||
|
||||
if (statusNode) statusNode.textContent = message || '';
|
||||
if (!statusNode) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(statusNode, type, message);
|
||||
return;
|
||||
}
|
||||
statusNode.textContent = message || '';
|
||||
}
|
||||
|
||||
function buildUnavailableProfileView() {
|
||||
return {
|
||||
displayName: '账户资料暂时无法读取',
|
||||
realName: '暂无数据',
|
||||
avatarText: '!',
|
||||
avatarStatus: '暂无数据',
|
||||
phone: '暂无数据',
|
||||
birthday: '暂无数据',
|
||||
email: '暂无数据',
|
||||
sex: '暂无数据',
|
||||
regionText: '暂无数据'
|
||||
};
|
||||
}
|
||||
@@ -194,53 +213,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function buildRegionPathText(items) {
|
||||
return (Array.isArray(items) ? items : []).map(function (item) {
|
||||
return item && item.regionName || '';
|
||||
}).filter(Boolean).join(' / ');
|
||||
}
|
||||
|
||||
async function renderRegionText(profile, requestId) {
|
||||
var api = getApi();
|
||||
var regionCode = getFinalRegionCode(profile);
|
||||
var path;
|
||||
var text = '待填写';
|
||||
|
||||
if (!regionCode || !api || !api.regionPath) {
|
||||
renderProfile({ regionText: text });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
path = await api.regionPath(regionCode);
|
||||
text = buildRegionPathText(path) || text;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
}
|
||||
|
||||
if (requestId === undefined || requestId === latestProfileRequestId) {
|
||||
renderProfile({ regionText: text });
|
||||
}
|
||||
}
|
||||
|
||||
async function fillRegionPicker(profile) {
|
||||
var regionPages = root.RegionPages;
|
||||
|
||||
if (!regionPages || !regionPages.applyProfileRegionSelection) return;
|
||||
|
||||
try {
|
||||
await regionPages.applyProfileRegionSelection({
|
||||
provinceCode: trimOrUndefined(profile && profile.provinceCode),
|
||||
cityCode: trimOrUndefined(profile && profile.cityCode),
|
||||
districtCode: trimOrUndefined(profile && profile.districtCode)
|
||||
});
|
||||
} catch (error) {
|
||||
var api = getApi();
|
||||
|
||||
if (!redirectUnauthorized(api, error)) showMessage(error.message || '地区资料加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
var latestProfileRequestId = 0;
|
||||
|
||||
async function loadProfile() {
|
||||
@@ -252,40 +224,52 @@
|
||||
|
||||
if (!api || !documentRef || !documentRef.querySelector('[data-profile-page]')) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
setProfileLoadStatus('正在读取账户资料…');
|
||||
setProfileLoadStatus('loading', '正在读取账户资料…');
|
||||
|
||||
try {
|
||||
profile = await api.currentProfile();
|
||||
if (requestId !== latestProfileRequestId) return;
|
||||
renderProfile(buildProfileView(profile));
|
||||
fillProfileForm(profile);
|
||||
fillRegionPicker(profile);
|
||||
renderRegionText(profile, requestId);
|
||||
setProfileLoadStatus('');
|
||||
setProfileLoadStatus('', '');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
renderProfile(buildUnavailableProfileView());
|
||||
setProfileLoadStatus('资料读取失败,请刷新页面后重试。');
|
||||
setProfileLoadStatus('error', '资料读取失败,请刷新页面后重试。');
|
||||
showMessage(error.message || '个人资料加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitProfileForm(form) {
|
||||
var api = getApi();
|
||||
var body;
|
||||
var validation;
|
||||
|
||||
if (!api || !form) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
|
||||
try {
|
||||
body = buildProfileUpdateBody(getFormValues(form));
|
||||
} catch (error) {
|
||||
setStatus(form, 'error', error.message || '资料校验失败');
|
||||
return;
|
||||
}
|
||||
validation = validateProfileUpdateBody(body);
|
||||
if (validation) {
|
||||
setStatus(form, 'error', validation);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '保存中...');
|
||||
await api.updateProfile(buildProfileUpdateBody(getFormValues(form)));
|
||||
setStatus(form, '已保存');
|
||||
setStatus(form, 'loading', '正在保存资料…');
|
||||
await api.updateProfile(body);
|
||||
setStatus(form, '', '已保存');
|
||||
showMessage('个人资料已保存');
|
||||
await loadProfile();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '保存失败');
|
||||
setStatus(form, 'error', '保存失败,请稍后重试。');
|
||||
showMessage(error.message || '个人资料保存失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
@@ -299,6 +283,11 @@
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
if (!form.checkValidity()) {
|
||||
setStatus(form, 'error', '请检查资料填写是否正确');
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
submitProfileForm(form);
|
||||
});
|
||||
}
|
||||
@@ -310,11 +299,10 @@
|
||||
|
||||
return {
|
||||
getAvatarText: getAvatarText,
|
||||
getFinalRegionCode: getFinalRegionCode,
|
||||
buildRegionText: buildRegionText,
|
||||
buildProfileView: buildProfileView,
|
||||
buildUnavailableProfileView: buildUnavailableProfileView,
|
||||
buildProfileUpdateBody: buildProfileUpdateBody,
|
||||
validateProfileUpdateBody: validateProfileUpdateBody,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
reloadProfile: loadProfile,
|
||||
init: init
|
||||
|
||||
+84
-17
@@ -107,6 +107,10 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
if (!documentRef && !rootNode) return null;
|
||||
return (rootNode || documentRef).querySelector(selector);
|
||||
@@ -136,6 +140,21 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function setApiState(target, type, message) {
|
||||
if (!target) return;
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message || '';
|
||||
}
|
||||
|
||||
function setPickerState(picker, type, message) {
|
||||
var status = picker && query('[data-region-picker-status]', picker.parentNode);
|
||||
|
||||
setApiState(status, type, message);
|
||||
}
|
||||
|
||||
function setOptions(select, items, placeholder) {
|
||||
var list = normalizeList(items);
|
||||
|
||||
@@ -186,10 +205,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handlePickerError(error) {
|
||||
function handlePickerError(picker, error) {
|
||||
var api = getApi();
|
||||
var message;
|
||||
|
||||
if (!redirectUnauthorized(api, error)) showMessage(error.message || '地区加载失败');
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
message = isForbidden(error) ? '当前账号无权读取地区数据。' : (error.message || '地区加载失败');
|
||||
setPickerState(picker, isForbidden(error) ? 'forbidden' : 'error', message);
|
||||
showMessage(message);
|
||||
}
|
||||
|
||||
function bindPickerEvents(picker, province, city, district) {
|
||||
@@ -200,17 +223,33 @@
|
||||
resetSelect(city, '请选择市');
|
||||
resetSelect(district, '请选择区县');
|
||||
updateTargetCode(picker);
|
||||
if (!province.value) return;
|
||||
if (!province.value) {
|
||||
setPickerState(picker, '', '');
|
||||
return;
|
||||
}
|
||||
|
||||
loadChildrenInto(city, province.value, '请选择市').catch(handlePickerError);
|
||||
setPickerState(picker, 'loading', '正在加载市…');
|
||||
loadChildrenInto(city, province.value, '请选择市').then(function () {
|
||||
setPickerState(picker, '', '');
|
||||
}).catch(function (error) {
|
||||
handlePickerError(picker, error);
|
||||
});
|
||||
});
|
||||
|
||||
city.addEventListener('change', function () {
|
||||
resetSelect(district, '请选择区县');
|
||||
updateTargetCode(picker);
|
||||
if (!city.value) return;
|
||||
if (!city.value) {
|
||||
setPickerState(picker, '', '');
|
||||
return;
|
||||
}
|
||||
|
||||
loadChildrenInto(district, city.value, '请选择区县').catch(handlePickerError);
|
||||
setPickerState(picker, 'loading', '正在加载区县…');
|
||||
loadChildrenInto(district, city.value, '请选择区县').then(function () {
|
||||
setPickerState(picker, '', '');
|
||||
}).catch(function (error) {
|
||||
handlePickerError(picker, error);
|
||||
});
|
||||
});
|
||||
|
||||
district.addEventListener('change', function () {
|
||||
@@ -229,7 +268,11 @@
|
||||
resetSelect(city, '请选择市');
|
||||
resetSelect(district, '请选择区县');
|
||||
bindPickerEvents(picker, province, city, district);
|
||||
picker.__regionInitPromise = loadChildrenInto(province, '0', '请选择省').catch(function (error) {
|
||||
setPickerState(picker, 'loading', '正在加载省…');
|
||||
picker.__regionInitPromise = loadChildrenInto(province, '0', '请选择省').then(function (result) {
|
||||
setPickerState(picker, '', '');
|
||||
return result;
|
||||
}).catch(function (error) {
|
||||
picker.__regionInitPromise = null;
|
||||
throw error;
|
||||
});
|
||||
@@ -280,12 +323,16 @@
|
||||
var list = normalizeList(items);
|
||||
|
||||
if (!container) return;
|
||||
container.innerHTML = list.length ? list.map(function (item) {
|
||||
if (!list.length) {
|
||||
setApiState(container, 'empty', '未找到匹配地区');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = list.map(function (item) {
|
||||
var code = getRegionCode(item);
|
||||
var name = getRegionName(item);
|
||||
|
||||
return '<button class="btn ghost magnetic" type="button" data-region-search-result="' + escapeHtml(code) + '">' + escapeHtml(name) + ' ' + escapeHtml(code) + '</button>';
|
||||
}).join('') : '<p class="api-empty">未找到匹配地区</p>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function submitRegionSearch(form) {
|
||||
@@ -293,24 +340,35 @@
|
||||
var values = getFormValues(form);
|
||||
var results = query('[data-region-search-results]', form.parentNode);
|
||||
var pathText = query('[data-region-search-path]', form.parentNode);
|
||||
var status = query('[data-region-search-status]', form);
|
||||
var items;
|
||||
var message;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!values.regionKeyword || !values.regionKeyword.trim()) {
|
||||
setApiState(status, 'error', '请输入地区名称或编码');
|
||||
showMessage('请输入地区名称或编码');
|
||||
return;
|
||||
}
|
||||
|
||||
setApiState(status, 'loading', '正在搜索地区…');
|
||||
setApiState(results, 'loading', '正在搜索地区…');
|
||||
if (pathText) pathText.textContent = '搜索中...';
|
||||
try {
|
||||
renderSearchResults(results, await api.regionSearch({
|
||||
items = await api.regionSearch({
|
||||
keyword: values.regionKeyword.trim(),
|
||||
limit: 20
|
||||
}));
|
||||
if (pathText) pathText.textContent = '请选择一个地区';
|
||||
});
|
||||
renderSearchResults(results, items);
|
||||
setApiState(status, '', '');
|
||||
if (pathText) pathText.textContent = normalizeList(items).length ? '请选择一个地区' : '未找到匹配地区';
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (pathText) pathText.textContent = error.message || '地区搜索失败';
|
||||
showMessage(error.message || '地区搜索失败');
|
||||
message = isForbidden(error) ? '当前账号无权搜索地区。' : (error.message || '地区搜索失败');
|
||||
setApiState(results, isForbidden(error) ? 'forbidden' : 'error', message);
|
||||
setApiState(status, isForbidden(error) ? 'forbidden' : 'error', message);
|
||||
if (pathText) pathText.textContent = message;
|
||||
showMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,21 +378,27 @@
|
||||
var section = button.closest('.module-panel');
|
||||
var picker = section && query('[data-region-picker]', section);
|
||||
var pathText = section && query('[data-region-search-path]', section);
|
||||
var status = section && query('[data-region-search-status]', section);
|
||||
var detail;
|
||||
var path;
|
||||
var message;
|
||||
|
||||
if (!code || !picker || redirectUnauthorized(api)) return;
|
||||
|
||||
setApiState(status, 'loading', '正在定位地区…');
|
||||
if (pathText) pathText.textContent = '正在定位地区...';
|
||||
try {
|
||||
detail = await api.regionDetail(code);
|
||||
path = await api.regionPath(getRegionCode(detail) || code);
|
||||
await applyRegionSelection(picker, buildRegionSelection(path));
|
||||
if (pathText) pathText.textContent = buildPathText(path);
|
||||
setApiState(status, '', '');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (pathText) pathText.textContent = error.message || '地区定位失败';
|
||||
showMessage(error.message || '地区定位失败');
|
||||
message = isForbidden(error) ? '当前账号无权定位地区。' : (error.message || '地区定位失败');
|
||||
setApiState(status, isForbidden(error) ? 'forbidden' : 'error', message);
|
||||
if (pathText) pathText.textContent = message;
|
||||
showMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +428,9 @@
|
||||
if (redirectUnauthorized(api)) return;
|
||||
|
||||
queryAll('[data-region-picker]').forEach(function (picker) {
|
||||
initPicker(picker).catch(handlePickerError);
|
||||
initPicker(picker).catch(function (error) {
|
||||
handlePickerError(picker, error);
|
||||
});
|
||||
});
|
||||
bindSearchForms();
|
||||
}
|
||||
@@ -381,6 +447,7 @@
|
||||
applyRegionSelection: applyRegionSelection,
|
||||
applyProfileRegionSelection: applyProfileRegionSelection,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
+124
-37
@@ -15,6 +15,7 @@
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var canEditContent = false;
|
||||
var writePending = false;
|
||||
|
||||
function getApi() {
|
||||
@@ -280,19 +281,26 @@
|
||||
return relativeId ? url + '&relativeId=' + encodeURIComponent(relativeId) : url;
|
||||
}
|
||||
|
||||
function canEditRecords(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
function renderRelativeRow(record) {
|
||||
var summary = [];
|
||||
var actions = '<button class="pill" type="button" data-relative-detail-id="' +
|
||||
escapeHtml(record.relativeId) + '">查看详情</button>';
|
||||
|
||||
if (record.relationName) summary.push(record.relationName);
|
||||
if (record.eventName) summary.push(record.eventName);
|
||||
if (record.eventTime) summary.push(record.eventTime);
|
||||
if (!summary.length) summary.push('未填写关系和事件');
|
||||
if (canEditContent) {
|
||||
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.relativeId)) +
|
||||
'">编辑</a><button class="pill is-danger" type="button" data-relative-delete-id="' +
|
||||
escapeHtml(record.relativeId) + '">删除</button>';
|
||||
}
|
||||
return '<article class="module-row relative-row"><div><h3>' + escapeHtml(record.relativeName) + '</h3><p>' +
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
|
||||
'<button class="pill" type="button" data-relative-detail-id="' + escapeHtml(record.relativeId) + '">查看详情</button>' +
|
||||
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.relativeId)) + '">编辑</a>' +
|
||||
'<button class="pill is-danger" type="button" data-relative-delete-id="' + escapeHtml(record.relativeId) + '">删除</button>' +
|
||||
'</div></article>';
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' + actions + '</div></article>';
|
||||
}
|
||||
|
||||
function detailValue(label, value) {
|
||||
@@ -301,9 +309,16 @@
|
||||
|
||||
function renderRelativeDetail(record) {
|
||||
var count;
|
||||
var actions = '';
|
||||
|
||||
if (!record) return '<div class="api-empty">请选择一条亲友记录查看详情</div>';
|
||||
if (!record) return stateHtml('empty', '请选择一条亲友记录查看详情');
|
||||
count = attachmentCount(record.mediaOssIds);
|
||||
if (canEditContent) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.relativeId)) + '">编辑记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-relative-delete-id="' +
|
||||
escapeHtml(record.relativeId) + '">删除记录</button></div>';
|
||||
}
|
||||
return '<div class="form-like relative-detail">' +
|
||||
detailValue('亲友姓名', record.relativeName) +
|
||||
detailValue('关系名称', record.relationName) +
|
||||
@@ -315,9 +330,7 @@
|
||||
detailValue('记录人', record.appUserNickName) +
|
||||
detailValue('状态', record.status === '0' ? '正常' : '停用') +
|
||||
detailValue('备注', record.remark) +
|
||||
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.relativeId)) + '">编辑记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-relative-delete-id="' + escapeHtml(record.relativeId) + '">删除记录</button></div>';
|
||||
'</div>' + actions;
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
@@ -335,6 +348,10 @@
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
@@ -344,13 +361,38 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var target = query('[data-relative-form-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
function stateHtml(type, message) {
|
||||
if (root.ProfileUI && root.ProfileUI.renderApiState) return root.ProfileUI.renderApiState(type, message);
|
||||
return '<div class="api-state api-state--' + type + '" role="' +
|
||||
(type === 'error' || type === 'forbidden' ? 'alert' : 'status') +
|
||||
'" aria-live="polite">' + escapeHtml(message) + '</div>';
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
function setState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.innerHTML = stateHtml(type, message);
|
||||
}
|
||||
|
||||
function setFormStatus(message, type) {
|
||||
var target = query('[data-relative-form-status]');
|
||||
|
||||
if (!target) return;
|
||||
if (!message) {
|
||||
target.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message;
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message, type) {
|
||||
var placeholder = query('[data-relative-editor-placeholder]');
|
||||
|
||||
queryAll('[data-relative-editor], [data-relative-editor-action]').forEach(function (element) {
|
||||
@@ -358,15 +400,39 @@
|
||||
});
|
||||
if (placeholder) {
|
||||
placeholder.hidden = Boolean(enabled);
|
||||
if (message) placeholder.textContent = message;
|
||||
if (!enabled && message) {
|
||||
if (type) setState(placeholder, type, message);
|
||||
else placeholder.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWritePending(value) {
|
||||
function setEditAccess(enabled) {
|
||||
canEditContent = Boolean(enabled);
|
||||
queryAll('[data-relative-create-link]').forEach(function (element) {
|
||||
element.hidden = !canEditContent;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCapability(api, genealogyId) {
|
||||
var genealogy = await api.genealogyDetail(genealogyId);
|
||||
|
||||
setEditAccess(canEditRecords(genealogy));
|
||||
return genealogy;
|
||||
}
|
||||
|
||||
function setWritePending(value, label) {
|
||||
var submit = query('[data-relative-form] [type="submit"]');
|
||||
|
||||
writePending = Boolean(value);
|
||||
queryAll('[data-relative-form] button, [data-relative-form] input, [data-relative-form] textarea, [data-relative-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
if (submit) {
|
||||
if (!submit.dataset.defaultLabel) submit.dataset.defaultLabel = submit.textContent;
|
||||
submit.textContent = writePending && label ? label : submit.dataset.defaultLabel;
|
||||
submit.setAttribute('aria-busy', writePending ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function syncLinks() {
|
||||
@@ -378,10 +444,11 @@
|
||||
var detail = query('[data-relative-detail]');
|
||||
var message = '请先选择家谱,再查看或维护亲友记录。';
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setEditorEnabled(false, message);
|
||||
setFormStatus(message);
|
||||
setState(list, 'empty', message);
|
||||
setState(detail, 'empty', '当前没有家谱上下文。');
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, message, 'empty');
|
||||
setFormStatus(message, 'empty');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
@@ -403,11 +470,11 @@
|
||||
});
|
||||
if (!container) return records;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">亲友记录响应缺少稳定 RelativeRecordVo 字段,请联系后端核对。</div>';
|
||||
setState(container, 'error', '亲友记录响应缺少稳定 RelativeRecordVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!records.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无亲友往来记录</div>';
|
||||
setState(container, 'empty', '暂无亲友往来记录');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = records.map(renderRelativeRow).join('');
|
||||
@@ -426,6 +493,7 @@
|
||||
var record;
|
||||
|
||||
if (!genealogyId || !normalizeId(relativeId) || redirectUnauthorized(api)) return null;
|
||||
setState(query('[data-relative-detail]'), 'loading', '正在加载亲友记录详情…');
|
||||
try {
|
||||
record = normalizeRelativeRecord(await api.relativeRecordDetail(genealogyId, relativeId));
|
||||
if (!record) throw new Error('亲友记录详情响应缺少稳定 RelativeRecordVo 字段');
|
||||
@@ -434,7 +502,11 @@
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
setState(
|
||||
query('[data-relative-detail]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该亲友记录。' : '亲友记录详情加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该亲友记录。' : (error.message || '亲友记录详情加载失败'));
|
||||
return null;
|
||||
}
|
||||
@@ -450,7 +522,9 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncLinks();
|
||||
setState(query('[data-relative-list]'), 'loading', '正在加载亲友记录…');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
records = renderRelativeRecords(await api.relativeRecords(genealogyId));
|
||||
requestedRelativeId = getCurrentRelativeId();
|
||||
if (requestedRelativeId && recordsById[requestedRelativeId]) {
|
||||
@@ -462,11 +536,12 @@
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-relative-list]')) {
|
||||
query('[data-relative-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的亲友记录。' : '亲友记录加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
setEditAccess(false);
|
||||
setState(
|
||||
query('[data-relative-list]'),
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该家谱的亲友记录。' : '亲友记录加载失败,请稍后重试。'
|
||||
);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的亲友记录。' : (error.message || '亲友记录加载失败'));
|
||||
}
|
||||
}
|
||||
@@ -515,8 +590,11 @@
|
||||
if (!genealogyId) return;
|
||||
relativeId = getCurrentRelativeId();
|
||||
syncLinks();
|
||||
setEditorEnabled(false, '正在加载亲友记录编辑信息…');
|
||||
setEditorEnabled(false, '正在加载亲友记录编辑信息…', 'loading');
|
||||
setFormStatus('正在读取家谱权限…', 'loading');
|
||||
try {
|
||||
await loadCapability(api, genealogyId);
|
||||
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的亲友记录。' };
|
||||
if (relativeId) {
|
||||
record = normalizeRelativeRecord(await api.relativeRecordDetail(genealogyId, relativeId));
|
||||
if (!record) throw new Error('亲友记录详情响应缺少稳定 RelativeRecordVo 字段');
|
||||
@@ -524,12 +602,17 @@
|
||||
if (query('[data-relative-editor-title]')) query('[data-relative-editor-title]').textContent = '编辑亲友记录';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
setFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setEditAccess(false);
|
||||
setEditorEnabled(false, isForbidden(error)
|
||||
? '当前账号无权维护该家谱的亲友记录。'
|
||||
: (error.message || '亲友记录编辑信息加载失败'));
|
||||
setFormStatus(error.message || '亲友记录编辑信息加载失败');
|
||||
: (error.message || '亲友记录编辑信息加载失败'), getApiStateType(error));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权维护该家谱的亲友记录。' : (error.message || '亲友记录编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,18 +627,18 @@
|
||||
var savedId;
|
||||
var verifiedDetail;
|
||||
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
body = buildRelativeRecordBody(getFormValues(form));
|
||||
validation = validateRelativeRecordBody(body);
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidGiftAmount;
|
||||
delete body.invalidGiftPrecision;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存亲友记录…');
|
||||
setWritePending(true, '正在保存…');
|
||||
setFormStatus('正在保存亲友记录…', 'loading');
|
||||
try {
|
||||
result = relativeId
|
||||
? await api.updateRelativeRecord(genealogyId, relativeId, body)
|
||||
@@ -568,7 +651,10 @@
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权保存该亲友记录。' : (error.message || '亲友记录保存失败'));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号无权保存该亲友记录。' : (error.message || '亲友记录保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -579,7 +665,7 @@
|
||||
var genealogyId = requireGenealogyId();
|
||||
var record = recordsById[relativeId];
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(relativeId) || redirectUnauthorized(api)) return;
|
||||
if (writePending || !canEditContent || !genealogyId || !normalizeId(relativeId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.relativeName : '该亲友记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
@@ -649,6 +735,7 @@
|
||||
validateRelativeRecordBody: validateRelativeRecordBody,
|
||||
normalizeRelativeRecord: normalizeRelativeRecord,
|
||||
matchesSavedRecord: matchesSavedRecord,
|
||||
canEditRecords: canEditRecords,
|
||||
isEditableRecord: isEditableRecord,
|
||||
renderRelativeDetail: renderRelativeDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
|
||||
+52
-19
@@ -143,15 +143,32 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function setStatus(form, message) {
|
||||
function setStatus(form, type, message) {
|
||||
var status = query('[data-security-status]', form);
|
||||
|
||||
if (status) status.textContent = message;
|
||||
if (!status) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(status, type, message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message || '';
|
||||
}
|
||||
|
||||
function setSubmitting(form, isSubmitting) {
|
||||
queryAll('button[type="submit"]', form).forEach(function (button) {
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = isSubmitting;
|
||||
button.textContent = isSubmitting ? '提交中…' : button.dataset.defaultLabel;
|
||||
});
|
||||
}
|
||||
|
||||
function setBoundPhoneStatus(type, message) {
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(node, type, message);
|
||||
return;
|
||||
}
|
||||
node.textContent = message || '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,6 +228,7 @@
|
||||
|
||||
if (!api || !documentRef) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
setBoundPhoneStatus('loading', '正在读取已绑定手机号…');
|
||||
|
||||
try {
|
||||
profile = await api.currentProfile();
|
||||
@@ -218,14 +236,10 @@
|
||||
message = isPhone(boundPhone)
|
||||
? '验证码将发送至 ' + maskPhone(boundPhone)
|
||||
: '未获取到已绑定手机号';
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
node.textContent = message;
|
||||
});
|
||||
setBoundPhoneStatus('', message);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
node.textContent = '读取已绑定手机号失败';
|
||||
});
|
||||
setBoundPhoneStatus('error', '读取已绑定手机号失败,请刷新后重试。');
|
||||
showMessage(error.message || '账号资料加载失败');
|
||||
}
|
||||
}
|
||||
@@ -236,28 +250,31 @@
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!values.oldPassword || !values.newPassword) {
|
||||
setStatus(form, 'error', '请填写当前密码和新密码');
|
||||
showMessage('请填写原密码和新密码');
|
||||
return;
|
||||
}
|
||||
if (String(values.newPassword).length < 6) {
|
||||
setStatus(form, 'error', '新密码至少需要 6 位');
|
||||
showMessage('新密码至少需要 6 位');
|
||||
return;
|
||||
}
|
||||
if (values.newPassword !== values.confirmPassword) {
|
||||
setStatus(form, 'error', '两次新密码不一致');
|
||||
showMessage('两次新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '提交中...');
|
||||
setStatus(form, 'loading', '正在修改密码…');
|
||||
await api.changePassword(buildPasswordChangeBody(values));
|
||||
setStatus(form, '密码已修改');
|
||||
setStatus(form, '', '密码已修改');
|
||||
showMessage('密码已修改');
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '修改失败');
|
||||
setStatus(form, 'error', '修改失败,请稍后重试。');
|
||||
showMessage(error.message || '密码修改失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
@@ -270,32 +287,33 @@
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!body.phone || !body.smsCode) {
|
||||
setStatus(form, 'error', '请填写新手机号和验证码');
|
||||
showMessage('请填写新手机号和验证码');
|
||||
return;
|
||||
}
|
||||
if (!isPhone(body.phone)) {
|
||||
setStatus(form, 'error', '请输入正确的新手机号');
|
||||
showMessage('请输入正确的新手机号');
|
||||
return;
|
||||
}
|
||||
if (!isSmsCode(body.smsCode)) {
|
||||
setStatus(form, 'error', '请输入正确的短信验证码');
|
||||
showMessage('请输入正确的短信验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '提交中...');
|
||||
setStatus(form, 'loading', '正在换绑手机号…');
|
||||
await api.changePhone(body);
|
||||
boundPhone = body.phone;
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
node.textContent = '验证码将发送至 ' + maskPhone(boundPhone);
|
||||
});
|
||||
setStatus(form, '手机号已换绑');
|
||||
setBoundPhoneStatus('', '验证码将发送至 ' + maskPhone(boundPhone));
|
||||
setStatus(form, '', '手机号已换绑');
|
||||
showMessage('手机号已换绑');
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '换绑失败');
|
||||
setStatus(form, 'error', '换绑失败,请稍后重试。');
|
||||
showMessage(error.message || '手机号换绑失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
@@ -311,6 +329,7 @@
|
||||
if (!form || redirectUnauthorized(api)) return;
|
||||
values = getFormValues(form);
|
||||
if (!isPhone(values.phone)) {
|
||||
setStatus(form, 'error', '请输入正确的新手机号');
|
||||
showMessage('请输入正确的新手机号');
|
||||
return;
|
||||
}
|
||||
@@ -318,16 +337,19 @@
|
||||
values = getFormValues(form);
|
||||
values.validToken = takeCaptchaToken(form);
|
||||
button.disabled = true;
|
||||
setStatus(form, 'loading', '正在发送验证码…');
|
||||
|
||||
try {
|
||||
await api.sendSmsCode(getCaptchaOperation('phone'), {
|
||||
phone: values.phone,
|
||||
validToken: values.validToken || ''
|
||||
});
|
||||
setStatus(form, '', '验证码已发送,请查收短信。');
|
||||
showMessage('验证码已发送');
|
||||
startSmsCooldown(button);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, 'error', '验证码发送失败,请稍后重试。');
|
||||
showMessage(error.message || '验证码发送失败');
|
||||
button.disabled = false;
|
||||
}
|
||||
@@ -340,6 +362,7 @@
|
||||
|
||||
if (!form || redirectUnauthorized(api)) return;
|
||||
if (!isPhone(boundPhone)) {
|
||||
setStatus(form, 'error', '未获取到已绑定手机号,请刷新后重试。');
|
||||
showMessage('未获取到已绑定手机号,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
@@ -347,16 +370,19 @@
|
||||
values = getFormValues(form);
|
||||
values.validToken = takeCaptchaToken(form);
|
||||
button.disabled = true;
|
||||
setStatus(form, 'loading', '正在发送验证码…');
|
||||
|
||||
try {
|
||||
await api.sendSmsCode(getCaptchaOperation('deactivate'), {
|
||||
phone: boundPhone,
|
||||
validToken: values.validToken || ''
|
||||
});
|
||||
setStatus(form, '', '验证码已发送,请查收短信。');
|
||||
showMessage('验证码已发送');
|
||||
startSmsCooldown(button);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, 'error', '验证码发送失败,请稍后重试。');
|
||||
showMessage(error.message || '验证码发送失败');
|
||||
button.disabled = false;
|
||||
}
|
||||
@@ -368,10 +394,12 @@
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!isSmsCode(values.smsCode)) {
|
||||
setStatus(form, 'error', '请输入正确的短信验证码');
|
||||
showMessage('请输入正确的短信验证码');
|
||||
return;
|
||||
}
|
||||
if (!isDangerConfirmed(values.confirmText)) {
|
||||
setStatus(form, 'error', '请完整输入“确认注销”');
|
||||
showMessage('请完整输入“确认注销”');
|
||||
return;
|
||||
}
|
||||
@@ -379,13 +407,13 @@
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '提交中...');
|
||||
setStatus(form, 'loading', '正在提交注销申请…');
|
||||
await api.deactivateAccount(buildDeactivateBody(values));
|
||||
showMessage('账号已提交注销');
|
||||
root.location.href = 'index.html';
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '注销失败');
|
||||
setStatus(form, 'error', '注销失败,请稍后重试。');
|
||||
showMessage(error.message || '账号注销失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
@@ -415,6 +443,11 @@
|
||||
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
if (!form.checkValidity()) {
|
||||
setStatus(form, 'error', '请检查表单填写是否正确');
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
formType = form.getAttribute('data-security-form');
|
||||
|
||||
if (formType === 'password') submitPassword(form);
|
||||
|
||||
@@ -91,7 +91,6 @@
|
||||
}
|
||||
|
||||
function renderNormalizedArticleList(items) {
|
||||
if (!items.length) return '<div class="api-empty">当前暂无资讯</div>';
|
||||
return items.map(function (item) {
|
||||
return '<a class="site-info-item" href="article-detail.html?articleId=' +
|
||||
encodeURIComponent(item.articleId) + '">' +
|
||||
@@ -131,9 +130,7 @@
|
||||
function renderArticleDetail(item) {
|
||||
var article = normalizeArticle(item);
|
||||
|
||||
return article
|
||||
? renderNormalizedArticleDetail(article)
|
||||
: '<div class="api-empty">资讯不存在或已下线</div>';
|
||||
return article ? renderNormalizedArticleDetail(article) : '';
|
||||
}
|
||||
|
||||
function readQueryValue(search, key) {
|
||||
@@ -178,6 +175,27 @@
|
||||
return match;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function isMissingArticleError(error) {
|
||||
return error && error.message === '资讯不存在或已下线';
|
||||
}
|
||||
|
||||
function setSiteState(container, type, message) {
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.textContent = message;
|
||||
}
|
||||
|
||||
function setSiteStatus(container, message) {
|
||||
if (container) container.textContent = message || '';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var document = root.document;
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
@@ -196,19 +214,24 @@
|
||||
requestedType = readQueryValue(search, 'articleType');
|
||||
if (!list) return;
|
||||
if (requestedType && !normalizeArticleType(requestedType)) {
|
||||
list.innerHTML = '<div class="api-empty">资讯分类无效</div>';
|
||||
if (status) status.textContent = '资讯分类无效';
|
||||
setSiteState(list, 'error', '资讯分类无效。');
|
||||
setSiteStatus(status, '资讯分类无效。');
|
||||
return;
|
||||
}
|
||||
setSiteState(list, 'loading', '正在读取资讯…');
|
||||
try {
|
||||
list.classList.add('is-loading');
|
||||
list.innerHTML = renderNormalizedArticleList(await loadArticles(api, requestedType));
|
||||
if (status) status.textContent = '';
|
||||
var items = await loadArticles(api, requestedType);
|
||||
|
||||
if (!items.length) setSiteState(list, 'empty', '当前暂无资讯。');
|
||||
else list.innerHTML = renderNormalizedArticleList(items);
|
||||
setSiteStatus(status, '');
|
||||
} catch (error) {
|
||||
list.innerHTML = '<div class="api-empty">资讯读取失败,请稍后重试</div>';
|
||||
if (status) status.textContent = error.message || '资讯读取失败';
|
||||
} finally {
|
||||
list.classList.remove('is-loading');
|
||||
setSiteState(
|
||||
list,
|
||||
isForbidden(error) ? 'forbidden' : 'error',
|
||||
isForbidden(error) ? '当前账号无权查看资讯。' : '资讯读取失败,请稍后重试。'
|
||||
);
|
||||
setSiteStatus(status, error.message || '资讯读取失败,请稍后重试。');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -219,20 +242,23 @@
|
||||
articleId = readArticleId(search);
|
||||
if (!detail) return;
|
||||
if (!articleId) {
|
||||
detail.innerHTML = '<div class="api-empty">资讯编号无效</div>';
|
||||
if (status) status.textContent = '资讯编号无效';
|
||||
setSiteState(detail, 'error', '资讯编号无效。');
|
||||
setSiteStatus(status, '资讯编号无效。');
|
||||
return;
|
||||
}
|
||||
setSiteState(detail, 'loading', '正在读取资讯详情…');
|
||||
try {
|
||||
detail.classList.add('is-loading');
|
||||
detail.innerHTML = renderNormalizedArticleDetail(await loadArticleDetail(api, articleId));
|
||||
if (status) status.textContent = '';
|
||||
setSiteStatus(status, '');
|
||||
} catch (error) {
|
||||
detail.innerHTML = '<div class="api-empty">' +
|
||||
escapeHtml(error.message || '资讯读取失败,请稍后重试') + '</div>';
|
||||
if (status) status.textContent = error.message || '资讯读取失败';
|
||||
} finally {
|
||||
detail.classList.remove('is-loading');
|
||||
setSiteState(
|
||||
detail,
|
||||
isForbidden(error) ? 'forbidden' : (isMissingArticleError(error) ? 'empty' : 'error'),
|
||||
isForbidden(error)
|
||||
? '当前账号无权查看该资讯。'
|
||||
: (isMissingArticleError(error) ? '资讯不存在或已下线。' : '资讯详情读取失败,请稍后重试。')
|
||||
);
|
||||
setSiteStatus(status, error.message || '资讯详情读取失败,请稍后重试。');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,6 +273,9 @@
|
||||
renderArticleDetail: renderArticleDetail,
|
||||
loadArticles: loadArticles,
|
||||
loadArticleDetail: loadArticleDetail,
|
||||
isForbidden: isForbidden,
|
||||
isMissingArticleError: isMissingArticleError,
|
||||
setSiteState: setSiteState,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,10 +60,29 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
return (rootNode || documentRef).querySelector(selector);
|
||||
}
|
||||
|
||||
function queryAll(selector, rootNode) {
|
||||
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
|
||||
}
|
||||
|
||||
function setUploadStatus(status, type, message) {
|
||||
if (!status) return;
|
||||
status.setAttribute('role', 'status');
|
||||
status.setAttribute('aria-live', 'polite');
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(status, type, message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message || '';
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
if (root.layui && root.layui.layer) {
|
||||
root.layui.layer.msg(message);
|
||||
@@ -214,22 +233,20 @@
|
||||
|
||||
try {
|
||||
for (index = 0; index < files.length; index += 1) {
|
||||
if (status) status.textContent = files[index].name + ' 正在上传...';
|
||||
setUploadStatus(status, 'loading', files[index].name + ' 正在上传…');
|
||||
result = normalizeUploadResult(await uploadFileForPage(api, files[index], {
|
||||
onProgress: function (progress) {
|
||||
if (status) {
|
||||
status.textContent = '正在上传 ' + progress.uploadedChunks + '/' + progress.totalChunks + ' 分片';
|
||||
}
|
||||
setUploadStatus(status, 'loading', '正在上传 ' + progress.uploadedChunks + '/' + progress.totalChunks + ' 分片');
|
||||
}
|
||||
}));
|
||||
if (!result.ossId) throw new Error('上传响应缺少 ossId');
|
||||
target.value = mergeUploadTargetValue(target.value, result.ossId, multiple);
|
||||
if (status) status.textContent = buildUploadStatus(result.fileName || files[index].name, result.ossId);
|
||||
setUploadStatus(status, '', buildUploadStatus(result.fileName || files[index].name, result.ossId));
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (status) status.textContent = error.message || '上传失败';
|
||||
showMessage(error.message || '上传失败');
|
||||
setUploadStatus(status, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '当前账号无权上传文件。' : (error.message || '上传失败'));
|
||||
showMessage(isForbidden(error) ? '当前账号无权上传文件。' : (error.message || '上传失败'));
|
||||
} finally {
|
||||
input.disabled = false;
|
||||
input.value = '';
|
||||
@@ -248,6 +265,10 @@
|
||||
}
|
||||
|
||||
function init() {
|
||||
queryAll('[data-upload-status]').forEach(function (status) {
|
||||
status.setAttribute('role', 'status');
|
||||
status.setAttribute('aria-live', 'polite');
|
||||
});
|
||||
bindActions();
|
||||
}
|
||||
|
||||
@@ -259,6 +280,7 @@
|
||||
uploadFileForPage: uploadFileForPage,
|
||||
mergeUploadTargetValue: mergeUploadTargetValue,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
+107
-36
@@ -232,7 +232,7 @@
|
||||
function renderVideoDetail(video, managementAccess) {
|
||||
var actions = '';
|
||||
|
||||
if (!video) return '<div class="api-empty">请选择一个视频查看详情</div>';
|
||||
if (!video) return '';
|
||||
if (managementAccess) {
|
||||
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(video.genealogyId, video.videoId)) + '">编辑视频</a>' +
|
||||
@@ -287,15 +287,15 @@
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
|
||||
}
|
||||
|
||||
function renderNoContext() {
|
||||
var list = query('[data-video-list]');
|
||||
var detail = query('[data-video-detail]');
|
||||
var placeholder = query('[data-video-permission-placeholder]');
|
||||
function canEditVideos(genealogy) {
|
||||
return Boolean(genealogy && (genealogy.canEditContent === true || genealogy.canManage === true));
|
||||
}
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">请先选择家谱,再查看家族视频。</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
if (placeholder) placeholder.textContent = '请先选择家谱,再发布视频。';
|
||||
setFormStatus('请先选择家谱,再发布视频。');
|
||||
function renderNoContext() {
|
||||
setVideoListState('empty', '请先选择家谱,再查看家族视频。');
|
||||
setVideoDetailState('empty', '当前没有家谱上下文。');
|
||||
setPermissionPlaceholder('empty', '请先选择家谱,再发布视频。');
|
||||
setFormStatus('请先选择家谱,再发布视频。', 'forbidden');
|
||||
setManagementAccess(false);
|
||||
}
|
||||
|
||||
@@ -316,17 +316,67 @@
|
||||
if (placeholder) placeholder.hidden = canEditContent;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var status = query('[data-video-form-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
function getApiStateType(error) {
|
||||
return isForbidden(error) ? 'forbidden' : 'error';
|
||||
}
|
||||
|
||||
function setWritePending(pending) {
|
||||
function setVideoState(selector, type, message) {
|
||||
var container = query(selector);
|
||||
|
||||
if (!container) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(container, type, message);
|
||||
return;
|
||||
}
|
||||
container.textContent = message;
|
||||
}
|
||||
|
||||
function setVideoListState(type, message) {
|
||||
setVideoState('[data-video-list]', type, message);
|
||||
}
|
||||
|
||||
function setVideoDetailState(type, message) {
|
||||
setVideoState('[data-video-detail]', type, message);
|
||||
}
|
||||
|
||||
function setPermissionPlaceholder(type, message) {
|
||||
var placeholder = query('[data-video-permission-placeholder]');
|
||||
|
||||
if (!placeholder) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(placeholder, type, message);
|
||||
return;
|
||||
}
|
||||
placeholder.textContent = message;
|
||||
}
|
||||
|
||||
function setFormStatus(message, type) {
|
||||
var status = query('[data-video-form-status]');
|
||||
|
||||
if (!status) return;
|
||||
if (!message) {
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (type && root.ProfileUI && root.ProfileUI.setApiState) {
|
||||
root.ProfileUI.setApiState(status, type, message);
|
||||
return;
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function setWritePending(pending, label) {
|
||||
var submit = query('[data-video-form] [type="submit"]');
|
||||
|
||||
writePending = Boolean(pending);
|
||||
queryAll('[data-video-form] input, [data-video-form] textarea, [data-video-form] button, [data-video-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
if (submit) {
|
||||
if (!submit.dataset.defaultLabel) submit.dataset.defaultLabel = submit.textContent;
|
||||
submit.textContent = writePending && label ? label : submit.dataset.defaultLabel;
|
||||
submit.setAttribute('aria-busy', writePending ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function renderVideos(data) {
|
||||
@@ -341,11 +391,11 @@
|
||||
});
|
||||
if (!container) return videos;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">视频响应缺少稳定 VideoVo 字段,请联系后端核对。</div>';
|
||||
setVideoListState('error', '视频响应缺少稳定 VideoVo 字段,请联系后端核对。');
|
||||
return [];
|
||||
}
|
||||
if (!videos.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无家族视频</div>';
|
||||
setVideoListState('empty', '暂无家族视频');
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = videos.map(function (video) {
|
||||
@@ -357,7 +407,12 @@
|
||||
function renderDetail(video) {
|
||||
var container = query('[data-video-detail]');
|
||||
|
||||
if (container) container.innerHTML = renderVideoDetail(video, canEditContent);
|
||||
if (!container) return;
|
||||
if (!video) {
|
||||
setVideoDetailState('empty', '请选择一个视频查看详情');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = renderVideoDetail(video, canEditContent);
|
||||
}
|
||||
|
||||
async function loadVideoDetail(videoId) {
|
||||
@@ -366,6 +421,7 @@
|
||||
var video;
|
||||
|
||||
if (!genealogyId || !normalizeId(videoId) || redirectUnauthorized(api)) return null;
|
||||
setVideoDetailState('loading', '正在加载视频详情…');
|
||||
try {
|
||||
video = normalizeVideo(await api.videoDetail(genealogyId, videoId));
|
||||
if (!video) throw new Error('视频详情响应缺少稳定 VideoVo 字段');
|
||||
@@ -374,7 +430,10 @@
|
||||
return video;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
setVideoDetailState(
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号无权查看该视频。' : (error.message || '视频详情加载失败')
|
||||
);
|
||||
showMessage(error.message || '视频详情加载失败');
|
||||
return null;
|
||||
}
|
||||
@@ -391,9 +450,11 @@
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks();
|
||||
setVideoListState('loading', '正在加载视频…');
|
||||
setVideoDetailState('loading', '正在加载视频详情…');
|
||||
try {
|
||||
result = await Promise.all([api.genealogyDetail(genealogyId), api.videos(genealogyId)]);
|
||||
setManagementAccess(Boolean(result[0] && result[0].canEditContent === true));
|
||||
setManagementAccess(canEditVideos(result[0]));
|
||||
videos = renderVideos(result[1]);
|
||||
requestedVideoId = getCurrentVideoId();
|
||||
if (requestedVideoId && videosById[requestedVideoId]) {
|
||||
@@ -407,10 +468,12 @@
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
setManagementAccess(false);
|
||||
if (query('[data-video-list]')) query('[data-video-list]').innerHTML = '<div class="api-empty">当前账号无权查看该家谱视频。</div>';
|
||||
setVideoListState('forbidden', '当前账号无权查看该家谱视频。');
|
||||
setVideoDetailState('forbidden', '当前账号无权查看该家谱视频。');
|
||||
return;
|
||||
}
|
||||
if (query('[data-video-list]')) query('[data-video-list]').innerHTML = '<div class="api-empty">视频加载失败,请稍后重试。</div>';
|
||||
setVideoListState('error', '视频加载失败,请稍后重试。');
|
||||
setVideoDetailState('error', '视频详情加载失败,请稍后重试。');
|
||||
showMessage(error.message || '视频加载失败');
|
||||
}
|
||||
}
|
||||
@@ -454,17 +517,17 @@
|
||||
if (!genealogyId) return;
|
||||
videoId = getCurrentVideoId();
|
||||
syncGenealogyLinks();
|
||||
setFormStatus('正在读取家谱权限…', 'loading');
|
||||
setPermissionPlaceholder('loading', '正在核对视频维护权限…');
|
||||
try {
|
||||
result = await Promise.all([
|
||||
api.genealogyDetail(genealogyId),
|
||||
videoId ? api.videoDetail(genealogyId, videoId) : Promise.resolve(null)
|
||||
]);
|
||||
setManagementAccess(Boolean(result[0] && result[0].canEditContent === true));
|
||||
setManagementAccess(canEditVideos(result[0]));
|
||||
if (!canEditContent) {
|
||||
if (query('[data-video-permission-placeholder]')) {
|
||||
query('[data-video-permission-placeholder]').textContent = '当前账号没有视频维护权限。';
|
||||
}
|
||||
setFormStatus('当前账号没有视频维护权限。');
|
||||
setPermissionPlaceholder('forbidden', '当前账号没有视频维护权限。');
|
||||
setFormStatus('当前账号没有视频维护权限。', 'forbidden');
|
||||
return;
|
||||
}
|
||||
if (videoId) {
|
||||
@@ -473,15 +536,18 @@
|
||||
fillVideoForm(video);
|
||||
if (query('[data-video-editor-title]')) query('[data-video-editor-title]').textContent = '编辑视频';
|
||||
}
|
||||
setFormStatus('');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setManagementAccess(false);
|
||||
if (query('[data-video-permission-placeholder]')) {
|
||||
query('[data-video-permission-placeholder]').textContent = isForbidden(error)
|
||||
? '当前账号没有视频维护权限。'
|
||||
: (error.message || '视频编辑信息加载失败');
|
||||
}
|
||||
setFormStatus(isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频编辑信息加载失败'));
|
||||
setPermissionPlaceholder(
|
||||
getApiStateType(error),
|
||||
isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频编辑信息加载失败')
|
||||
);
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频编辑信息加载失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,13 +563,13 @@
|
||||
|
||||
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
setFormStatus(validation, 'error');
|
||||
return;
|
||||
}
|
||||
delete body.invalidDuration;
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存视频…');
|
||||
setWritePending(true, '正在保存…');
|
||||
setFormStatus('正在保存视频…', 'loading');
|
||||
try {
|
||||
result = videoId ? await api.updateVideo(genealogyId, videoId, body) : await api.createVideo(genealogyId, body);
|
||||
saved = normalizeVideo(result);
|
||||
@@ -513,7 +579,10 @@
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频保存失败'));
|
||||
setFormStatus(
|
||||
isForbidden(error) ? '当前账号没有视频维护权限。' : (error.message || '视频保存失败'),
|
||||
getApiStateType(error)
|
||||
);
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
@@ -607,8 +676,10 @@
|
||||
validateVideoBody: validateVideoBody,
|
||||
normalizeVideo: normalizeVideo,
|
||||
normalizeDurationSeconds: normalizeDurationSeconds,
|
||||
canEditVideos: canEditVideos,
|
||||
renderVideoRow: renderVideoRow,
|
||||
renderVideoDetail: renderVideoDetail,
|
||||
setPermissionPlaceholder: setPermissionPlaceholder,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
|
||||
+82
-22
@@ -119,6 +119,20 @@
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGenealogyOptions(data) {
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map(normalizeGenealogyOption).filter(Boolean);
|
||||
}
|
||||
|
||||
function resolveContextGenealogyId(options, contextId) {
|
||||
var normalizedContextId = normalizeId(contextId);
|
||||
|
||||
if (!normalizedContextId || !Array.isArray(options)) return '';
|
||||
return options.some(function (option) {
|
||||
return option.genealogyId === normalizedContextId;
|
||||
}) ? normalizedContextId : '';
|
||||
}
|
||||
|
||||
function buildVipOrderBody(values) {
|
||||
var source = values || {};
|
||||
var body = {};
|
||||
@@ -214,7 +228,7 @@
|
||||
}
|
||||
|
||||
function renderGenealogyOptions(data) {
|
||||
var items = Array.isArray(data) ? data.map(normalizeGenealogyOption).filter(Boolean) : [];
|
||||
var items = normalizeGenealogyOptions(data);
|
||||
|
||||
return '<option value="">不关联具体家谱</option>' + items.map(function (item) {
|
||||
return '<option value="' + escapeHtml(item.genealogyId) + '">' +
|
||||
@@ -271,6 +285,9 @@
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var packages = [];
|
||||
var selectedPackageId = '';
|
||||
var contextGenealogyId = root.ProfileUI && typeof root.ProfileUI.getGenealogyId === 'function'
|
||||
? root.ProfileUI.getGenealogyId()
|
||||
: '';
|
||||
var writePending = false;
|
||||
|
||||
if (!page) return;
|
||||
@@ -280,20 +297,52 @@
|
||||
}
|
||||
if (!packageList || !selectedText || !genealogySelect || !form || !orderList ||
|
||||
!api.vipPackages || !api.genealogiesMine || !api.vipOrders || !api.createVipOrder) {
|
||||
if (status) status.textContent = '会员服务接口初始化失败,请刷新后重试';
|
||||
setListState(packageList, 'error', '会员套餐服务初始化失败,请刷新后重试。');
|
||||
setListState(orderList, 'error', '会员订单服务初始化失败,请刷新后重试。');
|
||||
setStatus('error', '会员服务接口初始化失败,请刷新后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
if (status) status.textContent = message;
|
||||
function setApiState(target, type, message) {
|
||||
if (!target) return;
|
||||
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
|
||||
root.ProfileUI.setApiState(target, type, message);
|
||||
return;
|
||||
}
|
||||
target.textContent = message || '';
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
setApiState(status, type, message);
|
||||
}
|
||||
|
||||
function setListState(target, type, message) {
|
||||
setApiState(target, type, message);
|
||||
}
|
||||
|
||||
function setPendingButton(button, pending, label) {
|
||||
if (!button) return;
|
||||
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
|
||||
button.disabled = pending;
|
||||
button.textContent = pending ? label : button.dataset.defaultLabel;
|
||||
}
|
||||
|
||||
function renderPackages() {
|
||||
if (!packages.length) {
|
||||
setListState(packageList, 'empty', '当前没有可用会员套餐。');
|
||||
return;
|
||||
}
|
||||
packageList.innerHTML = renderVipPackages(packages, selectedPackageId);
|
||||
}
|
||||
|
||||
function renderOrders(orders) {
|
||||
orderList.innerHTML = renderVipOrders(orders);
|
||||
var items = normalizeVipOrders(orders);
|
||||
|
||||
if (!items.length) {
|
||||
setListState(orderList, 'empty', '当前没有会员订单。');
|
||||
return;
|
||||
}
|
||||
orderList.innerHTML = renderVipOrders(items);
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
@@ -302,21 +351,30 @@
|
||||
return orders;
|
||||
}
|
||||
|
||||
setListState(packageList, 'loading', '正在读取会员套餐…');
|
||||
setListState(orderList, 'loading', '正在读取会员订单…');
|
||||
setStatus('loading', '正在读取会员服务…');
|
||||
try {
|
||||
var initial = await Promise.all([api.vipPackages(), api.genealogiesMine(), api.vipOrders()]);
|
||||
var genealogyOptions = normalizeGenealogyOptions(initial[1]);
|
||||
var selectedContextGenealogyId = resolveContextGenealogyId(genealogyOptions, contextGenealogyId);
|
||||
|
||||
packages = normalizeVipPackages(initial[0]);
|
||||
renderPackages();
|
||||
genealogySelect.innerHTML = renderGenealogyOptions(initial[1]);
|
||||
genealogySelect.innerHTML = renderGenealogyOptions(genealogyOptions);
|
||||
if (selectedContextGenealogyId) genealogySelect.value = selectedContextGenealogyId;
|
||||
renderOrders(initial[2]);
|
||||
setStatus(packages.length ? '请选择会员套餐' : '当前没有可用会员套餐');
|
||||
setStatus('', packages.length
|
||||
? (selectedContextGenealogyId ? '已关联当前家谱,请选择会员套餐' : '请选择会员套餐')
|
||||
: '当前没有可用会员套餐');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
packageList.innerHTML = '<div class="api-empty">会员套餐读取失败,请稍后重试</div>';
|
||||
orderList.innerHTML = '<div class="api-empty">会员订单读取失败,请稍后重试</div>';
|
||||
setStatus(error.message || '会员服务读取失败,请稍后重试');
|
||||
setListState(packageList, 'error', '会员套餐读取失败,请稍后重试。');
|
||||
setListState(orderList, 'error', '会员订单读取失败,请稍后重试。');
|
||||
setStatus('error', error.message || '会员服务读取失败,请稍后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,25 +390,25 @@
|
||||
selectedPackageId = selected.packageId;
|
||||
selectedText.textContent = '已选择:' + selected.packageName + '(¥' + selected.price + ')';
|
||||
renderPackages();
|
||||
setStatus('可以创建会员订单');
|
||||
setStatus('', '可以创建会员订单');
|
||||
});
|
||||
|
||||
if (refresh) {
|
||||
refresh.addEventListener('click', async function () {
|
||||
if (writePending) return;
|
||||
refresh.disabled = true;
|
||||
setStatus('正在刷新订单…');
|
||||
setPendingButton(refresh, true, '正在刷新…');
|
||||
setStatus('loading', '正在刷新会员订单…');
|
||||
try {
|
||||
await loadOrders();
|
||||
setStatus('会员订单已刷新');
|
||||
setStatus('', '会员订单已刷新');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '会员订单刷新失败,请稍后重试');
|
||||
setStatus('error', error.message || '会员订单刷新失败,请稍后重试。');
|
||||
} finally {
|
||||
refresh.disabled = false;
|
||||
setPendingButton(refresh, false, '正在刷新…');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -370,13 +428,13 @@
|
||||
});
|
||||
validation = validateVipOrderBody(body);
|
||||
if (validation) {
|
||||
setStatus(validation);
|
||||
setStatus('error', validation);
|
||||
return;
|
||||
}
|
||||
|
||||
writePending = true;
|
||||
if (submit) submit.disabled = true;
|
||||
setStatus('正在创建会员订单…');
|
||||
setPendingButton(submit, true, '正在创建订单…');
|
||||
setStatus('loading', '正在创建会员订单…');
|
||||
try {
|
||||
created = normalizeVipOrder(await api.createVipOrder(body));
|
||||
if (!created) throw new Error('创建响应缺少有效会员订单信息');
|
||||
@@ -384,16 +442,16 @@
|
||||
verified = refreshed.find(function (item) { return item.orderId === created.orderId; });
|
||||
if (!verified) throw new Error('订单已提交,但重新读取未找到对应记录');
|
||||
renderOrders(refreshed);
|
||||
setStatus('订单已创建;当前 PC 暂未开放在线支付');
|
||||
setStatus('', '订单已创建;当前 PC 暂未开放在线支付。');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '会员订单创建失败,请稍后重试');
|
||||
setStatus('error', error.message || '会员订单创建失败,请稍后重试。');
|
||||
} finally {
|
||||
writePending = false;
|
||||
if (submit) submit.disabled = false;
|
||||
setPendingButton(submit, false, '正在创建订单…');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -402,6 +460,8 @@
|
||||
normalizeVipPackage: normalizeVipPackage,
|
||||
normalizeVipPackages: normalizeVipPackages,
|
||||
normalizeGenealogyOption: normalizeGenealogyOption,
|
||||
normalizeGenealogyOptions: normalizeGenealogyOptions,
|
||||
resolveContextGenealogyId: resolveContextGenealogyId,
|
||||
buildVipOrderBody: buildVipOrderBody,
|
||||
validateVipOrderBody: validateVipOrderBody,
|
||||
normalizeVipOrder: normalizeVipOrder,
|
||||
|
||||
+26
-44
@@ -20,52 +20,33 @@
|
||||
>
|
||||
<div class="eyebrow">Create Account</div>
|
||||
<h1 id="register-title">注册账号</h1>
|
||||
<p class="lead">注册后即可创建家谱或通过邀请码加入家族。</p>
|
||||
<form id="register-form" class="form-grid">
|
||||
<input
|
||||
class="input"
|
||||
name="phone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
maxlength="11"
|
||||
placeholder="手机号"
|
||||
/>
|
||||
<div class="code-row">
|
||||
<input
|
||||
class="input"
|
||||
name="smsCode"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
maxlength="4"
|
||||
pattern="\d{4}"
|
||||
placeholder="短信验证码"
|
||||
/>
|
||||
<button class="btn ghost" type="button" data-api-send-code>
|
||||
获取验证码
|
||||
</button>
|
||||
<p class="lead">注册后即可创建家谱或申请加入家族。</p>
|
||||
<form id="register-form" class="form-grid" novalidate>
|
||||
<label class="auth-field" for="register-phone">
|
||||
<span>手机号 <b aria-hidden="true">*</b></span>
|
||||
<input id="register-phone" class="input" name="phone" type="tel" inputmode="numeric" autocomplete="tel" maxlength="11" placeholder="请输入手机号" required />
|
||||
</label>
|
||||
<div class="auth-field">
|
||||
<label for="register-sms-code">短信验证码 <b aria-hidden="true">*</b></label>
|
||||
<div class="code-row">
|
||||
<input id="register-sms-code" class="input" name="smsCode" autocomplete="one-time-code" inputmode="numeric" maxlength="4" pattern="\d{4}" placeholder="请输入四位验证码" required />
|
||||
<button class="btn ghost" type="button" data-api-send-code>获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="input"
|
||||
name="nickName"
|
||||
autocomplete="nickname"
|
||||
placeholder="昵称(选填)"
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="设置密码"
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="确认密码"
|
||||
/>
|
||||
<label class="auth-field" for="register-nickname">
|
||||
<span>昵称(选填)</span>
|
||||
<input id="register-nickname" class="input" name="nickName" autocomplete="nickname" placeholder="请输入昵称" />
|
||||
</label>
|
||||
<label class="auth-field" for="register-password">
|
||||
<span>设置密码 <b aria-hidden="true">*</b></span>
|
||||
<input id="register-password" class="input" name="password" type="password" autocomplete="new-password" placeholder="至少六位" required />
|
||||
</label>
|
||||
<label class="auth-field" for="register-confirm-password">
|
||||
<span>确认密码 <b aria-hidden="true">*</b></span>
|
||||
<input id="register-confirm-password" class="input" name="confirmPassword" type="password" autocomplete="new-password" placeholder="请再次输入密码" required />
|
||||
</label>
|
||||
<input type="hidden" name="validToken" />
|
||||
<div id="register-status" class="auth-form-status" role="status" aria-live="polite" data-auth-form-status></div>
|
||||
<button class="btn primary" type="submit">注册账号</button>
|
||||
<div class="auth-links">
|
||||
<a href="login.html">已有账号,去登录</a>
|
||||
@@ -84,6 +65,7 @@
|
||||
<script src="public/layui/layui.js"></script>
|
||||
<script src="public/tac/js/tac.min.js"></script>
|
||||
<script src="public/js/captcha-pages.js"></script>
|
||||
<script src="public/js/profile-common.js"></script>
|
||||
<script src="public/js/auth-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
</body>
|
||||
|
||||
+8
-4
@@ -46,21 +46,25 @@
|
||||
</section>
|
||||
<section class="section alt">
|
||||
<div class="container ticket-layout">
|
||||
<form class="card form-card ticket-form tilt-card" data-feedback-form>
|
||||
<form class="card form-card ticket-form tilt-card" data-feedback-form novalidate>
|
||||
<h2>工单信息</h2>
|
||||
<input class="input" name="contactInfo" placeholder="手机号 / 邮箱" />
|
||||
<select name="feedbackType">
|
||||
<label for="ticketContactInfo">联系方式(可选)</label>
|
||||
<input id="ticketContactInfo" class="input" name="contactInfo" placeholder="手机号 / 邮箱" />
|
||||
<label for="ticketFeedbackType">反馈类型</label>
|
||||
<select id="ticketFeedbackType" name="feedbackType">
|
||||
<option value="advice">意见建议</option>
|
||||
<option value="bug">问题反馈</option>
|
||||
<option value="complaint">投诉反馈</option>
|
||||
<option value="other">其他反馈</option>
|
||||
</select>
|
||||
<label for="ticketFeedbackContent">问题描述 <span class="field-required" aria-hidden="true">*</span></label>
|
||||
<textarea
|
||||
id="ticketFeedbackContent"
|
||||
name="feedbackContent"
|
||||
required
|
||||
placeholder="请描述遇到的问题、页面位置和希望处理的结果"
|
||||
></textarea>
|
||||
<p class="form-note" aria-live="polite" data-feedback-status></p>
|
||||
<div class="form-status" role="status" aria-live="polite" data-feedback-status></div>
|
||||
<button class="btn primary magnetic" type="submit" data-feedback-submit>提交工单</button>
|
||||
</form>
|
||||
<aside class="ticket-side tilt-card">
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const AdminPermissionPages = require('../public/js/admin-permission-pages.js');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('管理员权限页只从当前家谱上下文读取安全编号', () => {
|
||||
assert.equal(AdminPermissionPages.getCurrentGenealogyId('?genealogyId=2062179707935264769'), '2062179707935264769');
|
||||
assert.equal(AdminPermissionPages.getCurrentGenealogyId('?genealogyId=0'), '');
|
||||
assert.equal(AdminPermissionPages.getCurrentGenealogyId('?genealogyId=unsafe'), '');
|
||||
});
|
||||
|
||||
test('管理员权限概览只接受当前家谱的角色与严格布尔能力字段', () => {
|
||||
assert.deepEqual(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false,
|
||||
ownerUserId: '2062179707935264701'
|
||||
}, '2062179707935264769'), {
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
});
|
||||
assert.equal(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264768',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
}, '2062179707935264769'), null);
|
||||
assert.equal(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: 'true',
|
||||
canEditContent: false
|
||||
}, '2062179707935264769').canManage, false);
|
||||
assert.equal(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'legacy-role',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
}, '2062179707935264769'), null);
|
||||
});
|
||||
|
||||
test('权限概览依据当前能力展示可管理、审核与内容维护状态', () => {
|
||||
const admin = {
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
};
|
||||
const member = {
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'member',
|
||||
canManage: false,
|
||||
canEditContent: false
|
||||
};
|
||||
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(admin), /当前角色[\s\S]*管理员/);
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(admin), /成员角色维护[\s\S]*可管理/);
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(admin), /内容维护[\s\S]*可维护/);
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(member), /入谱审核[\s\S]*不可审核/);
|
||||
assert.equal(AdminPermissionPages.canMaintainContent({ canManage: 'true', canEditContent: false }), false);
|
||||
});
|
||||
|
||||
test('管理员权限页使用既有家谱详情接口和统一状态,不提供 App 粒度授权表单', () => {
|
||||
const page = read('profile-admin-permissions.html');
|
||||
const script = read('public/js/admin-permission-pages.js');
|
||||
|
||||
assert.match(page, /data-admin-permission-current[\s\S]*data-api-state="loading"/);
|
||||
assert.match(page, /src="public\/js\/admin-permission-pages\.js"/);
|
||||
assert.match(script, /api\.genealogyDetail\(genealogyId\)/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /当前账号无权查看该家谱权限。/);
|
||||
assert.doesNotMatch(page, /<input|<select|<form/);
|
||||
assert.doesNotMatch(script, /addManageUser|getManagePower|auth_str/);
|
||||
});
|
||||
@@ -184,6 +184,9 @@ test('相册页面拆分列表、编辑和照片详情且不允许手填 OSS ID
|
||||
});
|
||||
assert.match(listPage, /data-album-list/);
|
||||
assert.match(editPage, /name="albumName"/);
|
||||
assert.match(editPage, /data-album-form[^>]+novalidate/);
|
||||
assert.match(editPage, /class="field-required"/);
|
||||
assert.match(editPage, /<div class="api-status" data-album-form-status/);
|
||||
assert.match(editPage, /name="coverOssId" type="hidden"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(detailPage, /data-album-detail/);
|
||||
@@ -191,5 +194,42 @@ test('相册页面拆分列表、编辑和照片详情且不允许手填 OSS ID
|
||||
assert.match(detailPage, /name="ossId" type="hidden"/);
|
||||
assert.match(detailPage, /name="shootTime" type="datetime-local"/);
|
||||
assert.match(detailPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(detailPage, /data-album-photo-form novalidate/);
|
||||
assert.match(detailPage, /<div class="api-status" data-album-photo-status/);
|
||||
assert.doesNotMatch(editPage + detailPage, /type="text"[^>]+(?:coverOssId|ossId)|name="albumId"|name="photoId"/);
|
||||
});
|
||||
|
||||
test('相册列表、详情和表单将加载、空数据、403 与请求失败写入统一状态组件', () => {
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-album.html'), 'utf8');
|
||||
const detailPage = fs.readFileSync(path.join(projectRoot, 'profile-album-detail.html'), 'utf8');
|
||||
const source = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.match(listPage, /data-album-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-album-detail[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-album-photo-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setStatus\('\[data-album-list\]', '正在加载相册…', 'loading'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-list\]', '暂无相册', 'empty'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-photo-list\]', '正在加载照片…', 'loading'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-photo-list\]', '暂无照片', 'empty'\)/);
|
||||
assert.match(source, /当前账号无权查看该相册照片。/);
|
||||
assert.match(source, /正在读取家谱权限…', 'loading'/);
|
||||
assert.match(source, /setStatus\('\[data-album-form-status\]', validation, 'error'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-photo-status\]', validation, 'error'\)/);
|
||||
assert.match(source, /getApiStateType\(error\)/);
|
||||
});
|
||||
|
||||
test('相册与照片只接受后端布尔能力字段,并在所有写路径上门控', () => {
|
||||
const source = fs.readFileSync(modulePath, 'utf8');
|
||||
const canEditAlbums = requireFunction('canEditAlbums');
|
||||
|
||||
assert.equal(canEditAlbums({ canEditContent: true }), true);
|
||||
assert.equal(canEditAlbums({ canManage: true }), true);
|
||||
assert.equal(canEditAlbums({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.match(source, /canEditContent = canEditAlbums\(detail\)/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !albumId \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !normalizeId\(albumId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !albumId \|\| !normalizeId\(photoId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
});
|
||||
|
||||
@@ -737,7 +737,7 @@ test('feedback methods use the PC collection path and AppFeedbackBody whitelist'
|
||||
await client.submitFeedback({
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173',
|
||||
contactInfo: '测试联系方式',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
});
|
||||
@@ -747,7 +747,7 @@ test('feedback methods use the PC collection path and AppFeedbackBody whitelist'
|
||||
['post', '/genealogy/pc/feedback', {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
contactInfo: '测试联系方式'
|
||||
}],
|
||||
['get', '/genealogy/pc/feedback', undefined]
|
||||
]);
|
||||
|
||||
@@ -119,17 +119,23 @@ test('推广区域只把 401 视为登录失效', () => {
|
||||
assert.equal(AppPromotionPages.isUnauthorized(new Error('网络失败')), false);
|
||||
});
|
||||
|
||||
test('推广区域提供明确的未登录和空列表状态', () => {
|
||||
assert.match(
|
||||
AppPromotionPages.renderPromotionLoginRequired(),
|
||||
/<a href="login\.html">立即登录<\/a>/
|
||||
);
|
||||
assert.match(AppPromotionPages.renderPromotionList([]), /当前暂无应用推广/);
|
||||
test('推广区域将空、无权限与请求失败交给统一状态组件呈现', () => {
|
||||
const source = fs.readFileSync(path.resolve(__dirname, '../public/js/app-promotion-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(AppPromotionPages.renderPromotionList([]), '');
|
||||
assert.equal(AppPromotionPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(AppPromotionPages.isForbidden({ status: 401 }), false);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setPromotionState\(list, 'loading', '正在读取应用推广…'\)/);
|
||||
assert.match(source, /setPromotionState\(list, 'empty', '当前暂无应用推广。'\)/);
|
||||
assert.match(source, /isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
});
|
||||
|
||||
test('应用下载页只通过页面标记和新推广模块接入,不暴露业务 ID 输入', () => {
|
||||
assert.match(appPage, /<body[^>]*data-promotion-page/);
|
||||
assert.match(appPage, /data-promotion-list/);
|
||||
assert.match(appPage, /data-api-state="loading"/);
|
||||
assert.match(appPage, /src="public\/js\/profile-common\.js"/);
|
||||
assert.match(appPage, /src="public\/js\/app-promotion-pages\.js"/);
|
||||
assert.doesNotMatch(appPage, /src="public\/js\/promotion-pages\.js"/);
|
||||
assert.doesNotMatch(appPage, /name="(?:promotionId|coverOssId)"/);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user