Compare commits

...

11 Commits

Author SHA1 Message Date
rain 24ccdf251e 交接文档 2026-09-17 16:37:00 +08:00
rain d040507cb8 修改完成 2026-09-13 19:41:18 +08:00
rain 15b301b358 修改页面,完成单页模式新增首页返回上一页按钮 2026-08-31 14:49:08 +08:00
rain aaed0584b1 修改功能 2026-08-29 19:06:07 +08:00
rain f94c652c7c 样式修改,添加名称 2026-08-05 14:37:00 +08:00
rain 5b79893650 等待后端更新接口 2026-08-03 16:49:38 +08:00
rain ac5823547a 验证20% 2026-07-31 11:31:26 +08:00
fizzleaf 309598bfa6 feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、
  siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法
- 添加帮助文章和站点资讯的参数验证逻辑
- 更新测试文件添加新的API方法测试用例
- 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用
- 更新加入家谱页面为完整的申请流程界面
- 修改资讯详情页面为站点资讯展示页面
- 更新AxiosRequestUtil中认证处理逻辑
- 添加世系树渲染的HTML生成函数用于页面复用
- 更新文档中的API契约说明和页面规划
2026-07-30 16:04:48 +08:00
fizzleaf fb1743aa2a feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
2026-07-29 16:57:27 +08:00
fizzleaf 59a72fb22b docs: plan genealogy creation first batch 2026-07-29 16:35:21 +08:00
fizzleaf 3c775e1359 docs: define remaining PC page integrations 2026-07-29 16:04:12 +08:00
164 changed files with 21310 additions and 16015 deletions
+7
View File
@@ -1,6 +1,7 @@
######################################################################
# Build Tools
/node_modules/
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
@@ -51,3 +52,9 @@ nbdist/
robots.txt
sitemap.xml
# Local browser audit screenshots and generated reports
/artifacts/
# Local design-audit runtime metadata
/.hallmark/
-115
View File
@@ -1,115 +0,0 @@
# AGENTS.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
1. Think Before Coding
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
State your assumptions explicitly. If uncertain, ask.
If multiple interpretations exist, present them instead of picking silently.
If a simpler approach exists, say so. Push back when warranted.
If something is unclear, stop. Name what is confusing. Ask.
2. Simplicity First
Minimum code that solves the problem. Nothing speculative.
No features beyond what was asked.
No abstractions for single-use code.
No "flexibility" or "configurability" that was not requested.
No error handling for impossible scenarios.
If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
3. Surgical Changes
Touch only what you must. Clean up only your own mess.
When editing existing code:
Do not "improve" adjacent code, comments, or formatting.
Do not refactor things that are not broken.
Match existing style, even if you would do it differently.
If you notice unrelated dead code, mention it instead of deleting it.
When your changes create orphans:
Remove imports, variables, functions, files, docs, and references that YOUR changes made unused.
Do not remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
4. Contract Discipline
When a new contract lands, make it the only contract in the same change.
A contract means any source-of-truth rule that code depends on: data shape, API boundary, schema, field name, config key, prompt format, file layout, runtime entrypoint, ownership boundary, or workflow.
When introducing or changing a contract, do all three in the same round:
Specify the single owner.
Identify the one module, function, schema, document, or service that owns the contract.
Do not leave the same rule duplicated across frontend, backend, scripts, prompts, docs, or tests.
If multiple places need the value, they should consume it from the owner rather than redefine it.
Delete old fields and old entrypoints.
Remove obsolete fields, fallback reads, compatibility branches, legacy routes, stale config keys, old scripts, and old docs.
Do not keep the old path "just in case" unless the user explicitly asks for a compatibility period.
If compatibility is required, name it as temporary, define the removal condition, and keep it narrow.
Tighten validators and runtime together.
Update schemas, validators, tests, fixtures, seed data, docs, and runtime code in the same change.
Make invalid old inputs fail early and loudly.
Do not allow runtime behavior to accept shapes that validators reject, or validators to allow shapes that runtime no longer supports.
The rule: New owner, old path removed, validator and runtime tightened. If one is missing, the contract migration is not complete.
5. Goal-Driven Execution
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
"Add validation" -> "Write tests for invalid inputs, then make them pass."
"Fix the bug" -> "Write a test that reproduces it, then make it pass."
"Refactor X" -> "Ensure tests pass before and after."
"Change a contract" -> "Identify owner, remove old paths, tighten validators and runtime, then verify old inputs fail."
For multi-step tasks, state a brief plan:
1. [Step] -> verify: [check]
2. [Step] -> verify: [check]
3. [Step] -> verify: [check]
Strong success criteria let you loop independently. Weak criteria like "make it work" require clarification.
6. Verification Before Closure
A change is not finished until the relevant behavior is checked.
Before reporting completion:
Run the smallest relevant test, script, typecheck, build, or manual verification.
Prefer focused verification over broad, noisy suites when the task is narrow.
If verification cannot be run, say exactly why.
If verification is only by inspection, say that clearly.
If the task changed a contract, verify both the new valid path and the old invalid path.
Do not claim success from intent. Claim success from evidence.
7. Reporting Results
Be concise, concrete, and honest.
When summarizing work:
Say what changed.
Say how it was verified.
Mention any remaining risk or skipped verification.
Do not bury important caveats in vague language.
Do not over-explain routine edits.
Good final answer shape:
Changed: [short summary]
Verified: [command/check]
Notes: [only if needed]
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, fewer hidden compatibility paths, clearer ownership of contracts, and clarifying questions happen before implementation rather than after mistakes.
-8604
View File
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -36,7 +36,7 @@
<section class="inner-hero about-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">About Us</div>
<div class="eyebrow">关于我们</div>
<h1>用数字化方式守护家族记忆</h1>
<p class="lead">
代代相传面向家庭、宗亲会与地方文化组织,提供家谱创建、资料管理、文化展示和亲人协作工具。
@@ -91,17 +91,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -110,6 +110,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+13 -11
View File
@@ -36,11 +36,10 @@
<section class="inner-hero app-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Mobile App</div>
<div class="eyebrow">移动应用</div>
<h1>手机端随时记录,电脑端清晰浏览</h1>
<p class="lead">
家族成员可以在手机端上传照片、查看家谱、发布动态;PC
端适合浏览门户内容和公开家谱。
家族成员可以在手机端上传照片、查看家谱、发布动态;电脑端适合浏览门户内容和公开家谱。
</p>
<div class="hero-buttons">
<a class="btn primary magnetic" href="download.html">下载应用</a
@@ -71,7 +70,7 @@
<div class="section-head">
<h2>移动端能力</h2>
<p>
承接 APP 里的主要功能,让用户理解手机端适合日常记录和共同维护。
手机端适合随时记录家族资料、上传影像并与亲人共同维护。
</p>
</div>
<div class="grid-4">
@@ -105,7 +104,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>
@@ -122,18 +121,18 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a
><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -142,11 +141,14 @@
</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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/page-effects.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/app-promotion-pages.js"></script>
</body>
</html>
+14 -40
View File
@@ -3,11 +3,11 @@
<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/css/article-detail.css" />
</head>
<body class="page-article-detail" data-article-detail-page>
<body class="page-article-detail" data-site-article-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.html"
@@ -32,39 +32,11 @@
</div>
</header>
<main>
<section class="inner-hero article-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Article Detail</div>
<h1 data-article-title>为什么家谱不只是一本名册</h1>
<p class="lead" data-article-summary>
家谱记录的是家族关系,也记录一个家庭如何理解自己的来处、秩序和传承。
</p>
<div class="article-meta" data-article-meta>
<span>家谱知识</span><span>2026.05</span><span>阅读 2,418</span>
</div>
</div>
<div class="hero-card article-cover-detail tilt-card">
<span></span>
<p>记录来处,也照见未来。</p>
</div>
</div>
</section>
<section class="section alt">
<div class="container article-layout">
<article class="article-content tilt-card" data-article-content>
<p>谱文详情加载中...</p>
</article>
<aside class="article-side tilt-card">
<h3>相关入口</h3>
<a href="genealogy.html">了解数字家谱</a>
<a href="create-genealogy.html">创建一本家谱</a>
<a href="culture.html">返回家族文化</a>
</aside>
</div>
</section>
<div class="container">
<div class="module-meta" data-site-article-status role="status" aria-live="polite"></div>
</div>
<main data-site-article-detail>
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取资讯详情…</div>
</main>
<footer class="footer">
@@ -86,14 +58,14 @@
<h4>文化内容</h4>
<a href="culture.html">家族文化</a
><a href="surname.html">姓氏百科</a
><a href="article-detail.html">家谱知识</a
><a href="news.html">新闻资讯</a
><a href="about.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -102,11 +74,13 @@
</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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/page-effects.js"></script>
<script src="public/js/site-news-pages.js"></script>
</body>
</html>
+2 -1
View File
@@ -29,7 +29,7 @@
<section class="site-info-hero">
<div class="container">
<div>
<div class="site-info-kicker">Children Privacy</div>
<div class="site-info-kicker">儿童隐私保护</div>
<h1>儿童个人信息处理规则</h1>
<p>用于说明儿童个人信息的处理边界、监护人权利和保护措施。</p>
</div>
@@ -67,6 +67,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+28 -1
View File
@@ -1,5 +1,5 @@
(function (root, factory) {
// 根配置负责运行环境接口基础地址。
// 根配置负责运行环境接口基础地址与全站跳转入口
if (typeof module === 'object' && module.exports) {
module.exports = factory;
return;
@@ -12,6 +12,31 @@
var CLIENT_ID = 'ced7e5f0498645c6ec642dcf450b036f';
var TENANT_ID = '000000';
var TOKEN_KEY = 'genealogy_auth_token';
var BLOCKED_NAVIGATION_PROTOCOL = /^(?:javascript|data|vbscript):/i;
function createNavigationUtil() {
return {
open: function (url) {
var targetUrl = String(url || '').trim();
if (!targetUrl || BLOCKED_NAVIGATION_PROTOCOL.test(targetUrl) || typeof root.open !== 'function') {
return null;
}
var openedWindow = root.open(targetUrl, '_blank', 'noopener');
if (openedWindow) openedWindow.opener = null;
return openedWindow;
},
replace: function (url) {
var targetUrl = String(url || '').trim();
if (!targetUrl || BLOCKED_NAVIGATION_PROTOCOL.test(targetUrl)) return;
if (root.location && typeof root.location.replace === 'function') {
root.location.replace(targetUrl);
}
}
};
}
// 这里只维护运行环境和后端基础地址,具体接口路径统一放在 ApiClient 中。
var ENVIRONMENTS = {
@@ -48,6 +73,8 @@
};
}
root.NavigationUtil = root.NavigationUtil || createNavigationUtil();
return {
getEnvironment: getEnvironment,
getApiBaseUrl: getApiBaseUrl,
+73 -31
View File
@@ -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"
@@ -36,7 +36,7 @@
<section class="inner-hero create-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Start Genealogy</div>
<div class="eyebrow">创建家谱</div>
<h1>创建属于家族的数字空间</h1>
<p class="lead">
填写姓氏谱名、地区与简介,先建立家谱主页,再逐步邀请亲人共同完善资料。
@@ -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">
先填写基本资料,后续可以继续补充成员、相册和谱文
标有 * 的字段为必填。登录后即可创建,成功后会进入新家谱主页
</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">创建完成后,可在家谱首页的“邀请家人”中生成一次性邀请链接。</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>
@@ -105,17 +143,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -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>
+8 -7
View File
@@ -36,7 +36,7 @@
<section class="inner-hero culture-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Family Culture</div>
<div class="eyebrow">家族文化</div>
<h1>让家风、族训与故事被认真保存</h1>
<p class="lead">
家族文化承接新闻、知识、公告和故事,让谱文之外的记忆也能被阅读、分享和传承。
@@ -104,17 +104,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -123,6 +123,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
-905
View File
@@ -1,905 +0,0 @@
# PC 接口字段与页面落地规划(供 AI 执行)
> 文档状态:规划,不代表当前代码已经完成
> 基线日期:2026-07-27
> 当前本地快照:根目录 `PC.openapi.json`OpenAPI 3.1.0
> 使用对象:另一台电脑上的执行 AI
> 本轮边界:只写规划;执行 AI 才能修改页面、脚本和测试
## 0. 强制要求:必须打开 Apifox 查看
执行 AI 在修改任何代码前,必须实际登录并打开 Apifox 的“家谱”项目,进入每一个本轮接口的详情页逐项查看。不得只读取 `PC.openapi.json`、Apifox 本地缓存、旧截图或历史文档。
每个接口都必须在 Apifox 详情页记录:
1. method、完整 path、接口状态和所属 PC 目录;
2. Path、Query、Header、Cookie 和 Body 的全部字段;
3. 每个字段的类型、必填/可选/条件必填、默认值、枚举、格式、长度和取值范围;
4. 字段说明、示例值、关联字典和条件显示规则;
5. 请求体中折叠对象、数组元素和嵌套字段的完整结构;
6. 成功响应、失败响应、列表元素、详情对象和分页结构的完整字段;
7. 登录要求、角色权限、业务前置条件和错误码;
8. Apifox 页面有但 OpenAPI 导出遗漏的字段或说明。
核对结果必须写入本规划对应模块的字段表,并为每个字段标明 U/S/F/A/R/I/V 分类、使用页面、数据来源和提交时机。Apifox 详情没有确认的字段不得进入代码;Apifox 与导出文件不一致时,以现场核对结果形成问题清单,先让接口 owner 修正并重新导出,再实施。
## 1. 最终目标
执行 AI 要把 Apifox 中的 PC 接口准确落到现有 PC 页面,最终同时满足:
1. 请求体中的必填字段、可选字段全部有明确页面归属。
2. 字段被明确分成:
- 用户输入;
- 用户选择;
- 文件选择后自动生成;
- 页面上下文自动带入;
- 接口响应后只读展示;
- 仅内部保存、不得展示或手工填写;
- 纯前端校验字段、不得发给后端。
3. ID、Token、`clientid``tenantId`、OSS ID、分页参数等系统字段不得让用户手工输入。
4. 下拉框和选择器必须有真实数据源;没有选项接口时不得改成“请输入 ID”。
5. Apifox 未展开的响应 DTO、未声明的枚举和互相冲突的路径不得靠前端猜测。
6. APP 接口不得用于填补 PC 接口缺口。
7. 所有家谱内业务必须使用真实 `genealogyId`,不得写死示例值。
8. 每一阶段必须有契约测试、页面行为测试和真实联调结果。
## 2. 执行 AI 的工作协议
执行 AI 开始前必须按顺序完成:
1. 阅读项目根目录 `AGENTS.md`
2. 检查工作树,只修改本计划点名的文件;保留用户已有改动。
3. 登录 Apifox,打开“家谱”项目,逐条核对实时 PC 接口详情。
4. 从 Apifox 重新导出最新 OpenAPI,和仓库 `PC.openapi.json` 做结构化差异比较。
5. 完成第 5 节“契约阻断项”;阻断项未解决的模块只保留页面设计,不接真实写操作。
6. 按第 8 节阶段顺序施工,每完成一阶段立即验证,不能把所有模块一次性混改。
遇到以下情况时,停止当前模块并整理成一份待后端确认清单,同时继续其他不受影响的模块:
- Apifox 页面和导出文件的 method、path、必填性、枚举或类型不一致;
- 响应仍是无属性的 `ObjectResult` / `ListResult`
- 选择字段没有合法选项来源;
- 更新接口没有说明“字段不传”和“传 null/空串”的区别;
- 页面需要的业务能力在 PC 目录中不存在。
不得通过兼容多个旧字段、读取 APP DTO、保留旧路径 fallback 或展示原始 JSON 绕过阻断。
## 3. 契约依据与优先级
发生冲突时按以下顺序处理:
1. Apifox “家谱”项目中当前 PC 接口详情;
2. 从该详情当场重新导出的 OpenAPI 文件;
3. 本规划中的字段和流程分类;
4. 当前仓库代码;
5. 旧规划和历史交接记录。
通常以 Apifox 的 PC 目录作为 PC 前端接口的正式契约源。2026-07-28 用户明确指定本轮直接以 `D:/WorkSpace/Java/Genealogy/doc/apifox/genealogy-pc-openapi.yaml` 对接,因此该 YAML 是本轮冻结契约;在线 Apifox 的旧重复记录不再覆盖它。`utils/ApiClient.js` 是前端路径和请求方法的唯一 owner。页面脚本只能调用 `ApiClient` 业务方法,不能直接拼 URL 或调用 Axios。
旧 OpenAPI 文件不再作为新增功能依据,只用于结构化差异比较和追踪导出遗漏。
如果 Apifox 在线内容发生变化,先更新本地 OpenAPI、本规划的受影响章节和契约测试,再修改运行时代码。不得让文档、测试和运行时同时保留两套契约。
## 4. 当前基线与复核结论
### 4.1 当前导出范围
当前 `PC.openapi.json` 共 96 个操作、15 个标签、68 个 Schema
| 模块 | 操作数 | 当前主要页面 |
| --- | ---: | --- |
| 验证中心 | 3 | 登录、注册、找回密码、安全设置 |
| 认证登录 | 11 | `login.html``register.html``forgot-password.html`、个人资料和安全页 |
| 文件上传 | 3 | 所有头像、图片、附件和视频选择控件 |
| 家谱 | 1 | `profile-families.html` |
| 家族圈 | 14 | `profile-feed.html``profile-feed-edit.html`、待新增动态详情页 |
| 行政区划 | 8 | 个人资料及以后需要地区的表单 |
| 字辈谱 | 6 | `profile-generation.html` |
| 世系人物 | 12 | `profile-tree.html` |
| 内容文章 | 1 | `profile-article.html``profile-article-edit.html` |
| 相册 | 2 | `profile-album.html` |
| 视频 | 5 | `profile-video.html`、待新增视频编辑页 |
| 贺礼邀约 | 4 | `profile-gift.html`、邀约管理面板 |
| 祭祀 | 2 | `profile-gift.html` |
| 族务记录 | 20 | 成长、亲友、备忘录、功德录页面 |
| 消息通知 | 4 | `profile-messages.html` |
| **合计** | **96** | |
### 4.2 已对照到的 Apifox 现状
本机 Apifox 缓存中的“家谱”项目包含 APP、PC、共享区划等 246 条接口记录。当前导出的 96 条路径均能在该项目缓存的接口树中找到,但存在以下差异:
1. Apifox 缓存同时保留了两条逻辑相同的短信接口:
- 错误旧路径:`genealogy/pc/auth/sms/{operationCode}/code`
- 正确路径:`/genealogy/pc/auth/sms/{operationCode}/code`
当前导出只保留正确路径。执行前必须在 Apifox 删除或正式废弃缺少 `/` 的旧定义,前端只实现正确路径。
2. 导出文件同时含 `/genealogy/region/*``/genealogy/pc/region/*` 两套共 8 个区划操作;两套的参数、响应和描述完全相同。执行前必须由后端选定一套唯一正式路径并删除另一套。选定后实际唯一操作数应从 96 收口为 92。
3. 旧版“85 接口规划”已经失效。新增范围主要包括 PC 区划 4 条、视频列表/新增/详情/修改 4 条、功德列表/新增/详情/修改 4 条。
4. 当前 96 个操作全部被声明为需要 `Authorization`,连注册、登录、短信发送和验证中心也不例外。这与未登录流程冲突,必须在 Apifox 逐项修正 security。
5. 当前有 21 个成功响应使用无业务属性的 `ObjectResult`,12 个使用无元素 Schema 的 `ListResult`;另有 `LineagePersonTreeView` 名义上是专用 DTO,但没有任何属性。
### 4.3 2026-07-28 补齐版 YAML 与在线 Apifox 复核
补齐文件 `genealogy-pc-openapi.yaml` 含 98 个 path、137 个 HTTP operation、17 个 tag、319 个 schema;旧 `PC.openapi.json` 含 71 个 path、96 个 operation、68 个 schema。补齐文件新增了家谱、成员、相册、文章、祭祀、通知、反馈、VIP 和官网内容等路径及 DTO,但它还不能替代在线 Apifox:
1. 已实际登录并打开 Apifox“家谱”项目的 PC 模块。在线概览显示 **142 个接口、87 个数据模型**,与补齐 YAML 的 137 个 HTTP operation 不一致。
2. 在线“认证登录”目录仍显示 **12 个接口**,同时存在:
- 错误旧路径:`genealogy/pc/auth/sms/{operationCode}/code`
- 正确路径:`/genealogy/pc/auth/sms/{operationCode}/code`
补齐 YAML 只包含正确路径,因此 C02 在线尚未完成。
3. 补齐 YAML 给注册、密码登录、短信登录和短信发送标了 `security: []`;在线密码登录详情仍展示必需的 `Authorization` API Key,并显示 `Auth 1`。因此 C03 的导出定义和在线详情冲突。
4. 在线 `GET /genealogy/pc/auth/profile` 成功响应仍为泛型 `ObjectResult`;展开后的 `data` 只有任意附加属性,没有 `ProfileView` 字段。补齐 YAML 同样引用泛型 `ObjectResult`C04/C06 未完成。
5. `ProfileUpdateBody.avatar` 仍为 `integer<int64>`,而 `FileUploadVo.ossId``LineagePersonView.avatarOssId` 等 OSS ID 为 `string`;部分头像字段也仍为 `integer<int64>`。C10 未完成。
6. 已用真实登录响应和后端 `AppLoginVo` 复核:token 的唯一 JSON 字段是 `access_token`;原 YAML 的 `token``accessToken``tokenValue``userId``tenantId``clientId` 均不是该响应的直接字段,现已删除。
7. `ProfileUpdateBody` 仍只有 `nickName``realName``avatar``sex``birthday``email`,没有地区字段,也没有说明缺省、`null`、空串的清空语义。C07/C18 未完成。
用户已明确要求本轮直接按补齐 YAML 施工,所以在线旧短信路径和公开接口 security 残留不再阻断本轮实现。YAML 自身仍存在的 profile 泛型响应、头像 ID 类型冲突和更新清空语义缺失继续保留为阻断项,前端不得自行补造。
## 5. 实施前必须解决的契约阻断项
以下事项不是前端自由选择,必须先在 Apifox 明确:
| 编号 | 问题 | 影响 | 完成条件 |
| --- | --- | --- | --- |
| C01 | 两套区划路径完全重复 | 页面和客户端会形成双路径 | Apifox 只保留一套;导出和客户端只出现一次 |
| C02 | 短信接口有缺少开头 `/` 的旧记录 | 可能生成错误 URL | 删除/废弃旧记录,只保留正确路径 |
| C03 | 所有操作均要求 Authorization | 未登录流程无法成立 | 注册、登录、找回、验证、短信等逐项标明公开;登录后接口明确鉴权 |
| C04 | 33 个操作使用泛型对象/列表响应 | 列表、详情、编辑回填和 ID 来源不确定 | 为每个业务资源增加明确 View/DTO Schema |
| C05 | `LineagePersonTreeView` 没有属性 | 世系树只能猜字段 | 补齐节点 ID、人物资料、配偶、子女和递归结构 |
| C06 | 用户资料响应未展开 | 个人资料无法可靠回填 | 增加 `ProfileView`,明确所有可读字段 |
| C07 | `ProfileUpdateBody` 没有地区字段 | 现居地区无法保存 | 明确增加地区编码字段或删除“保存地区”功能 |
| C08 | 区划响应没有元素 DTO | 下拉选项的 code/name/level 不受契约约束 | 增加 `RegionView` 及列表/路径响应 |
| C09 | 上传初始化响应未展开,却描述了 `instant=true` | 秒传、续传和 `uploadId` 状态不明确 | 增加初始化响应 DTO,明确 `instant`、已传分片和 OSS 信息 |
| C10 | OSS ID 类型冲突 | 可能发生 JS 精度丢失或校验失败 | 所有 `ossId``avatar``avatarOssId`、视频和附件 ID 统一为 string |
| C11 | `completed` 只有 string 类型,无枚举 | 不能决定复选框提交 `0/1`、true/false 或其他值 | Apifox 增加枚举和中文含义 |
| C12 | `feedType``recordType``registerSource` 等无枚举 | 无法判断文本输入还是选择项 | 明确为自由文本,或补全枚举 |
| C13 | 多个日期字段没有 format/时区规则 | 页面值和后端解析可能不一致 | 明确 date 或 date-time,并统一时区 |
| C14 | 只有配额,没有我的家谱/成员入口 | 无法取得 `genealogyId`、成员或受邀用户选项 | 增加 PC 家谱列表/选项和成员选项接口,或明确外部上下文契约 |
| C15 | `bindingMode=SPECIFIED` 需要 `appUserId`,但无业务用户选项接口 | 不能提供合法选择器 | 增加家谱成员/业务用户选项接口 |
| C16 | 邀约的 `inviteeUserIds` 没有选项接口 | 不能让管理员选择受邀人 | 增加同一家谱成员选项接口 |
| C17 | 视频、成长、亲友、备忘、功德、通知响应未展开 | 无法安全展示、编辑或单条操作 | 分别增加资源 View 和列表/详情响应 |
| C18 | 更新接口未说明缺省、null、空串语义 | 可选字段可能无法清空 | Apifox 对每个可清空字段写明更新规则 |
| C19 | 多数 View 没有 `canEdit` / `canDelete` 等权限字段 | 页面无法可靠决定按钮可见性 | 增加能力字段,或提供明确且可实现的角色规则 |
| C20 | 文章、相册、祭祀只有删除操作 | 没有真实 ID 来源和完整资源流程 | 补全 PC 列表/详情/新增/修改后才开放页面 |
当前复核状态:
| 阻断项 | 状态 | 在线/补齐文件证据 |
| --- | --- | --- |
| C02 | 本轮按 YAML 解决 | 只实现带 `/` 的正确路径,不保留旧路径 fallback |
| C03 | 本轮按 YAML 解决 | 验证、注册、登录、短信、找回使用 `security: []`;其他认证接口携带 Bearer |
| C04/C06 | 阻断 | profile GET 在线及 YAML 都只有泛型 `ObjectResult` |
| C07 | 阻断 | `ProfileUpdateBody` 没有地区字段 |
| C01/C08 | 前端按后端 PC 实链解决 | 只调用 `/genealogy/pc/region/*`;列表按 `RegionSelectVo``regionCode/regionName/regionLevel` 读取,不保留公共路径 fallback |
| C09 | 前端按后端 PC 实链解决 | `SysOssResumableInitVo` 明确提供 `uploadId/instant/ossId/url/fileName/uploadedChunks`,完成响应使用 `SysOssUploadVo` |
| C10 | 前端边界解决 | 后端初始化 `ossId` 为 Long、完成响应为 string;浏览器统一转十进制字符串,隐藏回填且不转 `Number` |
| C13 | 后续日期模块仍阻断 | 本阶段上传与区划响应不消费日期;其他模块仍需逐字段核验 Java 日期类型 |
| C18 | 阻断 | 未说明可选更新字段的省略、`null`、空串语义 |
阶段 0 验收标准:
- Apifox 中不存在旧短信路径和双区划路径;
- 重新导出的 OpenAPI 只包含一套正式契约;
- 公开与鉴权接口标记正确;
- 本阶段要实现的每个列表/详情都有明确响应 DTO;
- 所有枚举、日期和 ID 类型已确定;
- 将旧输入发送给后端时会明确失败,前端不保留兼容读取。
## 6. 页面字段分类标准
执行 AI 对每个请求字段都要采用下面一种处理类型:
| 类型 | 含义 | 页面处理 |
| --- | --- | --- |
| U:用户输入 | 姓名、标题、正文、手机号等 | 可见输入框/文本域,展示必填或选填 |
| S:用户选择 | 枚举、人员、字辈、地区、状态等 | 单选、下拉、多选、日期或开关;选项必须有真实来源 |
| F:文件派生 | OSS ID、文件名、MD5、大小等 | 用户只选文件;值由上传组件生成并隐藏 |
| A:自动填写 | header、租户、上下文 ID、分页、默认值等 | 页面不可编辑,由统一 owner 注入 |
| R:响应只读 | 创建时间、计数、发布人、状态文本等 | 列表、详情、徽标或只读信息 |
| I:内部状态 | token、challengeId、validToken、资源 ID 等 | 仅内存/安全存储/URL 中保存,不显示原值 |
| V:前端校验 | 确认密码、注销确认文字等 | 可见但不得发给后端 |
通用规则:
1. 必填字段使用明确的必填标识,并在提交前校验。
2. 可选字段也必须有页面归属;不填写时直接省略,除非 Apifox 明确要求传 null。
3. 枚举必须使用选择控件,禁止自由文本。
4. 外键 ID 必须使用搜索/选择控件,禁止“请输入人物 ID”“请输入 OSS ID”。
5. `sortOrder``status` 这类管理字段:
- 有管理权限时放在“高级设置”;
- 普通用户使用隐藏默认值;
- 后端未返回权限时不得自行开放高级设置。
6. 所有 int64 ID 在浏览器中按字符串保存和比较。
7. 响应中的 `code``msg` 由请求层统一处理;业务页只消费 `data``rows/total`
## 7. 字段、页面和流程规划
### 7.1 全局自动字段
| 字段 | 类型 | 唯一来源 | 规则 |
| --- | --- | --- | --- |
| `clientid` | A | `ApiClient` 配置 | 每个需要它的请求自动加 Header,不进入表单或 body |
| `Authorization` | I/A | 登录响应 token | 仅鉴权接口携带;注册登录等公开接口不携带 |
| `tenantId` | A | 环境配置 | 登录、注册、验证等自动带入,不让用户输入 |
| `genealogyId` | I/A | PC 家谱上下文 | `profile-common.js` 读取和传播;缺失时阻止家谱内请求 |
| `feedId``commentId``personId``poemId``articleId``albumId``photoId``videoId``ceremonyId``giftId``recordId``relativeId``memoId``meritId``notificationId` | I/A | 列表/详情响应或 URL | 由被点击记录带入,禁止文本框输入 |
| `pageNum``pageSize` | A/S | 分页组件 | 页码由组件维护;用户只操作翻页/每页条数 |
| `ossId` / `mediaOssIds` | F/I | 文件上传响应 | 页面展示预览和文件名,隐藏保存 ID |
| `sortOrder` | S/A | 高级设置或默认值 | 不得把空字符串转成 0;未填写时省略 |
| `status` | S/A | 权限化状态选择或默认值 | 枚举 0=正常、1=停用;不得把 1 标成“草稿” |
### 7.2 验证中心与认证登录
#### 页面字段矩阵
| 字段 | 必填性 | 类型 | 页面/流程 | 处理 |
| --- | --- | --- | --- | --- |
| `operationCode` | 必填 path | A | 所有认证动作 | 页面动作固定为 `password-login``sms-login``register``forgot-password``phone-change``account-deactivate` |
| `clientid` | 必填 header | A | 所有认证请求 | `ApiClient` 自动注入 |
| `tenantId` | 必填 | A | 所有登录/验证表单 | 环境配置自动注入 |
| `subject` | require 可选,challenge/verify 必填 | A | 验证流程 | 从当前手机号/账号派生,用户不重复填写 |
| `grantType` | 必填 | A | 注册、登录、短信、找回 | 密码动作固定 `password`,短信动作固定 `sms` |
| `phone` | 必填 | U | 登录、注册、找回、换绑 | 手机号输入和格式校验 |
| `password` | 必填 | U/A | 密码登录、注册 | 用户输入明文,提交前生成 32 位 MD5;日志不得记录 |
| `oldPassword` | 必填 | U/A | 修改密码 | 同上 |
| `newPassword` | 必填 | U/A | 修改/重置密码 | 同上 |
| `confirmPassword` | 前端字段 | V | 注册、修改、找回 | 只比较一致性,不发送 |
| `smsCode` | 必填 | U | 短信登录、注册、找回、换绑、注销 | 4 位验证码输入 |
| `nickName` | 可选 | U | 注册 | 昵称输入 |
| `registerSource` | 可选 | A | 注册 | 当前端固定 `PC`Apifox 需确认枚举 |
| `validToken` | 条件必填 | I/A | 密码登录、发送短信 | 验证成功后仅存内存,使用一次即清除 |
| `challengeId` | 必填 | I/A | 验证校验 | 挑战响应自动回填 |
| `providerCode``captchaType` | 可选 | A | 验证校验 | 挑战响应决定,用户不可修改 |
| `payload.track` | 条件必填 | A | 天爱验证码 | 控件原样生成轨迹、尺寸和时间 |
| `payload.uuid` | 条件必填 | I/A | 系统图形验证码 | 挑战响应回填 |
| `payload.code` | 条件必填 | U | 系统图形验证码 | 用户输入图片答案 |
天爱控件产生的嵌套字段全部属于 A,不得另做表单让用户填写:
- `bgImageWidth``bgImageHeight`:控件实际显示尺寸,必填;
- `templateImageWidth``templateImageHeight`:模板实际显示尺寸,可选;
- `startTime``stopTime`:操作开始和结束毫秒时间戳,必填;
- `left``top`:控件最终偏移,可选;
- `trackList`:必填轨迹数组;
- 每个轨迹点的 `x``y``t``type` 均由控件生成;
- `data`:不同验证码类型的扩展数据,原样提交。
验证和登录响应字段:
| 响应字段 | 类型 | 页面处理 |
| --- | --- | --- |
| `required` | R/A | 决定是否进入挑战流程 |
| `providerCode``captchaType` | I/A | 选择正确验证码控件 |
| `sceneCode` | I/R | 服务端解析结果,可用于只读诊断;不得回传 |
| `ttlSeconds``expireSeconds` | R/A | 倒计时和过期重试 |
| `challengeId``uuid` | I | 只在当前挑战内保存 |
| `img` | R | 系统图形验证码图片 |
| `payload` | I/A | 天爱控件初始化数据 |
| `passed` | R/A | 决定校验成功或失败 |
| `validToken` | I | 一次性票据,不显示、不落长期存储 |
| `message` | R | 安全地显示验证结果 |
| `access_token` | string / I | 登录接口 `data`;密码或短信登录成功时立即保存,后续仅用于 Bearer 鉴权 |
| `expire_in` | integer<int64> / I/R | 登录接口 `data`;登录成功时取得,可用于会话到期提示,不由用户填写 |
| `client_id``clientKey``deviceType``userType` | string / I/R | 登录接口 `data`;登录成功时取得,仅作会话上下文或诊断,不回传为用户输入 |
| `profile` | object / R | 登录接口 `data.profile`;登录成功后取得,可用于页面只读展示或资料页初始回填 |
| `profile.userId``profile.tenantId``profile.userNo` | integer/string、string、string / I/R | 后端自动产生;不得让用户手工填写,业务 ID 超出 JS 安全整数时保持字符串 |
| `profile.phone``profile.nickName``profile.realName``profile.email` | string/null / R | 后端用户资料;登录成功后只读展示或作为资料页回填,编辑时仍只提交 `ProfileUpdateBody` 允许字段 |
| `profile.avatar` | integer/string/null / R | 后端头像 OSS ID;只读回填,用户换头像必须经文件选择和上传组件产生,禁止手填 OSS ID |
| `profile.sex` | enum `0/1/2` / R | 后端用户资料;页面映射男/女/未知 |
| `profile.birthday` | date/null / R | 后端用户资料;页面按日期展示 |
| `profile.registerSource``profile.loginIp``profile.loginDate` | string/null / R/I | 后端自动产生;只读或内部诊断,不作为表单输入 |
| `profile.status` | string / R/I | 后端账号状态;只读或内部诊断,不作为表单输入 |
| `profile.clientKey``profile.deviceType` | string / I/R | 后端登录上下文;只读或内部诊断 |
#### 认证业务流程
密码登录:
1. 用户输入手机号和密码。
2.`password-login + tenantId + phone` 调用 `require`
3.`required=false`,直接提交密码登录。
4. 若需要验证,调用 challenge,展示服务端指定控件,再调用 verify。
5. 得到 `validToken` 后与 MD5 密码一起登录。
6. 成功后只读取并保存 `data.access_token``token``accessToken``tokenValue` 均视为非法旧响应,不做兼容读取。
短信类动作:
1. 用户输入手机号。
2. 按当前动作完成 require → challenge → verify。
3. 把一次性 `validToken` 提交给发送短信接口。
4. 用户输入短信码后执行短信登录、注册、找回、换绑或注销。
5. `validToken` 不进入最终注册、短信登录、换绑或注销 body,除非 Apifox 明确修改契约。
#### 个人资料字段
`ProfileUpdateBody` 的所有字段必须进入 `profile-data.html`
| 字段 | 必填性 | 类型 | 控件 |
| --- | --- | --- | --- |
| `nickName` | 可选 | U | 文本,最长 30 |
| `realName` | 可选 | U | 文本,最长 30 |
| `avatar` | 可选 | F/I | 图片上传、预览、删除;隐藏保存 OSS ID |
| `sex` | 可选 | S | 男 0、女 1、未知 2,三项都要有 |
| `birthday` | 可选 | S | 日期选择器,`yyyy-MM-dd` |
| `email` | 可选 | U | email 输入,最长 100 |
现有页面整改点:
- 当前 `avatarOssId` 字段名与请求契约 `avatar` 不一致;
- 补齐 YAML 中 `avatar` 仍是 `integer<int64>`,但示例值已超过 JavaScript 安全整数范围;浏览器边界必须按十进制字符串保留精度,且只能由上传响应自动回填,不能转 `Number` 或让用户手填;
- 缺少 `realName``email` 和性别“未知”;
- 页面现居地区、父亲、微信/QQ、学历/职业不在 `ProfileUpdateBody`,不得混入保存请求;
- 在后端增加地区字段前,“保存地区”按钮必须关闭或改成纯查询演示;
- `GET /auth/profile` 必须补齐 `ProfileView` 后才能严格回填。
### 7.3 文件上传
用户在任何业务页只操作“选择文件、取消、重试”;下面字段全部自动产生:
| 阶段 | 字段 | 必填性 | 类型 | 来源 |
| --- | --- | --- | --- | --- |
| 初始化 | `uploadId` | 必填 | I/A | 客户端生成一次并贯穿全过程 |
| 初始化 | `fileName` | 必填 | F | `File.name` |
| 初始化 | `fileMd5` | 必填 | F | 文件内容计算 |
| 初始化 | `totalSize` | 必填 | F | `File.size` |
| 初始化 | `totalChunks` | 必填 | F | 根据大小和分片大小计算 |
| 初始化 | `chunkSize` | 必填 | A | 上传组件统一配置,默认示例 4 MiB,最后一片除外 |
| 初始化 | `contentType` | 可选 | F | `File.type` |
| 分片 | `chunkIndex` | 必填 | A | 上传循环生成 |
| 分片 | `chunkMd5` | 必填 | F | 当前分片计算 |
| 分片 | `file` | 必填 | F | 当前 Blob |
| 完成 | `uploadId/fileName/fileMd5/totalSize/totalChunks` | 必填 | I/A/F | 必须与初始化一致 |
响应处理:
- `FileUploadVo.ossId`I,隐藏写入业务表单;
- `url``thumbnailUrl`R,用于预览;
- `fileName``originalName`R,用于文件列表;
- 初始化 `instant=true` 时直接读取 `ossId/url/fileName` 完成秒传;`instant=false` 时使用服务端 `uploadId``uploadedChunks` 跳过已上传分片;
- 初始化 Long `ossId` 和完成响应 string `ossId` 均在浏览器边界转为十进制字符串;
- 上传成功但业务保存失败时,当前契约没有释放引用接口,必须让后端补充生命周期规则。
统一由 `public/js/upload-pages.js` 管理分片、进度、重试、取消、MD5 和隐藏 ID。各业务页面不得复制上传算法,也不得出现“请输入 OSS ID”的可见输入框。
### 7.4 家谱上下文与配额
`GET /genealogy/pc/genealogies/quota` 只负责在 `profile-families.html` 展示:
| 响应字段 | 类型 | 页面呈现 |
| --- | --- | --- |
| `createUsed``createLimit``createRemaining` | R | 创建额度已用/上限/剩余 |
| `canCreate` | R | 控制“创建家谱”是否可用 |
| `joinUsed``joinLimit``joinRemaining` | R | 加入额度已用/上限/剩余 |
| `canJoin` | R | 控制“加入家谱”是否可用 |
`-1` 显示为“不限”,不能参与普通减法或显示负数。
配额响应不包含 `genealogyId`,不能替代“我的家谱”。在 C14 完成前:
- 所有家谱内页面缺少真实 `genealogyId` 时必须阻止请求;
- 不显示静态家谱卡片作为真实数据;
- 不从 APP 接口获取家谱;
- 不允许用户手工输入家谱 ID。
### 7.5 行政区划
本 PC 前端只实现 `/genealogy/pc/region/*`,不调用重复的 `/genealogy/region/*`,也不保留 fallback。
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `parentCode` | 可选 query | A/S | 省级不传;选择上一级后自动查询下级 |
| `regionCode` | 必填 path | I/A | 从被选择的区划记录带入 |
| `keyword` | 必填 query | U | 搜索框 |
| `level` | 可选 query | S | 1 省、2 市、3 区县、4 乡镇街道、5 村社区 |
| `limit` | 可选 query | A | 搜索组件固定合理上限,不让用户自由输入 |
| `clientid` | 可选 header | A | 统一请求层注入 |
真实 Java `RegionSelectVo` 已明确 `regionCode``regionName``regionLevel` 等字段。个人资料当前仍没有可保存的地区字段,因此区划组件只提供级联、搜索和路径回显,不提交到资料更新接口。
### 7.6 家族圈
页面归属:
- `profile-feed.html`:分页列表、点赞、轻量评论;
- `profile-feed-edit.html`:新增和编辑;
- 新增 `profile-feed-detail.html`:详情、完整评论树、回复、通知跳转和分享深链。
#### 写入字段
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `genealogyId` | 必填 path | I/A | 家谱上下文 |
| `feedId` | 详情/修改/删除必填 | I/A | 列表记录或 URL |
| `feedType` | 可选 | A 或 S | 当前默认隐藏 `text`;Apifox 补枚举后才能改成选择 |
| `feedContent` | 必填 | U | 正文编辑器 |
| `mediaOssIds` | 可选 | F/I | 多文件上传结果拼成英文逗号串,用户只看缩略图 |
| `sortOrder` | 可选 | S/A | 管理员高级设置;普通发布默认省略/0 |
| `status` | 可选 | S/A | 0 正常、1 停用;不得显示为“发布/草稿” |
| `parentCommentId` | 可选 | I/A | 点击“回复”后自动带入;一级评论省略或 null |
| `commentContent` | 必填 | U | 评论输入,最多 1000 字 |
| `pageNum/pageSize` | 可选 | A/S | 分页组件 |
#### 动态响应字段
| 字段 | 页面处理 |
| --- | --- |
| `feedId``genealogyId` | I,用于路由和后续操作 |
| `genealogyNo``genealogyName` | R,详情来源信息 |
| `publisherUserId` | I,用于权限判断,但不能单独代替后端鉴权 |
| `publisherNickName``publisherStatus` | R,发布人和状态 |
| `feedType``feedContent` | R,类型和正文 |
| `mediaOssIds` | I/F,解析后通过文件 URL 规则展示,不直接显示 ID |
| `likedByMe` | R,决定点赞按钮状态 |
| `likeCount``commentCount` | R,计数 |
| `pinned``pinnedTime` | R,置顶徽标和时间 |
| `sortOrder``status` | R/管理信息 |
| `remark` | R,仅在产品确认需要时展示,不能当正文 |
| `createTime``updateTime` | R,发布时间和编辑时间 |
评论响应字段:
- `commentId``genealogyId``feedId``parentCommentId``appUserId``parentAppUserId`I
- `appUserNickName``appUserAvatar``parentAppUserNickName`R
- `commentContent`R;为 null 且 `userDeleted=1` 时显示“该评论已删除”占位;
- `replyCount`R,控制“展开回复”;
- `commentLevel`R`root``reply`
- `status``createTime`R。
流程:
1. 列表首屏调用分页接口,不同时调用非分页接口重复取数。
2. 点击记录进入详情,以 `genealogyId + feedId` 取详情。
3. 一级评论调用 comments 分页;回复只调用对应 comment 的 replies 分页。
4. 点赞、取消、评论、删除成功后以接口返回和重新读取结果校正计数。
5. 删除有回复的评论后保留占位,不从 DOM 直接移除整棵回复。
### 7.7 字辈谱
`profile-generation.html` 同时提供“单条维护”和“批量维护”。
单条字段:
| 字段 | 必填性 | 类型 | 控件 |
| --- | --- | --- | --- |
| `generationNo` | 必填 | U/S | 正整数世代序号 |
| `generationText` | 必填 | U | 最长 50 |
| `description` | 可选 | U | 文本域,最长 500 |
| `sortOrder` | 可选 | U/S | 管理员高级数字字段 |
| `status` | 可选 | S | 0 正常、1 停用 |
| `poemId` | 修改必填 path | I/A | 列表记录 |
批量字段:
| 字段 | 必填性 | 类型 | 控件 |
| --- | --- | --- | --- |
| `poemText` | 必填 | U | 文本域,最长 26000;支持合同中列出的分隔符 |
| `disableMissing` | 可选 | S | 复选框,必须有“将停用后续世代、不删除历史”的确认说明 |
批量流程必须是 preview → 用户检查 → save
- 预览展示 `createCount``updateCount``keepCount``disableCount`
- 逐项结果来自 `items`;每项展示 `generationNo``oldGenerationText``newGenerationText``oldStatus``newStatus``action``warning`
- `poemId``genealogyId` 是内部字段;
- 保存前再次确认,因为服务端会基于最新数据重新计算差异。
普通展示列表使用正常字辈接口;管理页面使用 management 接口以包含停用项。
### 7.8 世系人物
全部操作集中在 `profile-tree.html`,详情和编辑可使用抽屉/弹窗,但所有字段都要有明确控件。
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `bindingMode` | 必填 | S | NONE 不绑定、SELF 当前账号、SPECIFIED 指定账号 |
| `appUserId` | SPECIFIED 时必填 | S/I | 从真实业务用户选项选择;没有接口时禁用 SPECIFIED,禁止文本 ID |
| `personNo` | 可选 | U/A | 高级设置可填;留空由服务端生成 |
| `name` | 必填 | U | 姓名 |
| `aliasName` | 可选 | U | 别名/曾用名 |
| `sex` | 可选 | S | 0 男、1 女、2 未知 |
| `generation` | 可选 | S/I | 从正常字辈列表选择世代 |
| `generationName` | 可选 | A/R | 随字辈选项自动回填并只读显示 |
| `fatherId` | 可选 | S/I | 世系人物搜索选择器 |
| `motherId` | 可选 | S/I | 世系人物搜索选择器 |
| `avatarOssId` | 可选 | F/I | 上传头像自动回填 |
| `birthDate` | 可选 | S | date-time 选择器 |
| `birthLunar` | 可选 | S | 0 公历、1 农历开关 |
| `birthPlace` | 可选 | U | 出生地文本 |
| `deathDate` | 可选 | S | date-time 选择器 |
| `deathLunar` | 可选 | S | 0 公历、1 农历开关 |
| `deathPlace` | 可选 | U | 逝世地 |
| `burialPlace` | 可选 | U | 安葬地 |
| `personStatus` | 可选 | S | 0 健在、1 已故、2 未知 |
| `biography` | 可选 | U | 人物简介 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `remark` | 可选 | U | 备注 |
| `relationName` | 可选 | U | 仅“添加配偶”快捷流程显示 |
人物响应字段按以下方式使用:
- `personId``genealogyId``appUserId``fatherId``motherId``avatarOssId`I
- `genealogyName``genealogyNo`R
- `appUserNickName`R,显示绑定账号;
- `personNo``name``aliasName``sex``generation``generationName`R/编辑回填;
- `fatherName``motherName``spouseNames`R
- 出生、逝世、安葬、状态、简介、备注、排序字段:R/编辑回填。
关系流程:
1. 先选中一个现有 `personId` 作为关系锚点。
2. 点击父母、子女、兄弟姐妹或配偶。
3. 打开完整 `LineagePersonBody` 表单,创建的是一个新人物并建立关系,不是绑定两个现有人物 ID。
4. 配偶流程才显示 `relationName`
5. 当前没有解除关系接口,页面不得伪造“解除关系”。
6. DELETE 是逻辑停用;存在正常子女时可能失败,确认文案不能写成物理删除。
列表筛选:
- `keyword`U,搜索姓名、别名或人物编号;
- `generation`S,从字辈接口选择;
- `personStatus`S0/1/2
- `pageNum/pageSize`:分页组件自动维护。
`LineagePersonTreeView` 补全前,不得继续依靠多个字段 fallback 猜树结构。
### 7.9 视频
计划新增 `profile-video-edit.html``profile-video.html` 负责列表。
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `videoTitle` | 必填 | U | 标题 |
| `videoDesc` | 可选 | U | 说明文本域 |
| `coverOssId` | 可选 | F/I | 封面上传、预览、隐藏 ID |
| `videoOssId` | 必填 | F/I | 视频文件上传;不得输入 ID |
| `durationSeconds` | 可选 | F/A/R | 从视频元数据读取,只读显示 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
| `genealogyId``videoId` | path | I/A | 上下文和记录 |
流程:选视频 → 分片上传 → 得到 `videoOssId` → 可选封面上传 → 填标题说明 → 保存 → 重新读取详情。
当前视频列表、详情、新增和修改均返回泛型对象/列表。C17 完成前可以完成静态表单布局和 `ApiClient` 契约测试,但不得把真实 CRUD 页面标记为已完成。
### 7.10 贺礼邀约与祭祀
邀约字段:
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `inviteeUserIds` | 必填 | S/I | 家谱成员多选;提交完整名单,空数组表示取消全部未响应邀请 |
| `inviteStatus` | 必填 | S | 当前用户只能选择 ACCEPTED 接受或 DECLINED 拒绝 |
| `genealogyId``ceremonyId` | path | I/A | 活动上下文 |
邀约响应在 `profile-gift.html` 展示:
- `invitationId``genealogyId``ceremonyId``inviteeUserId`I
- `inviteStatus`RPENDING/ACCEPTED/DECLINED/CANCELED 徽标;
- `inviteVersion`I/R,只在管理审计需要时显示;
- `deliveredTime``readTime``responseTime`R
- `ceremonyTitle``ceremonyTime``location``locationAddress`R
- `longitude``latitude`:地图定位内部值,页面显示地图/地址而非原始数字。
“我的邀请”接口有完整响应,可独立实现接受/拒绝流程。管理端替换受邀人依赖 C16,且必须先有真实活动 ID。
当前没有祭祀活动列表、详情、新增、修改和献礼列表/新增接口。现有 `profile-gift-edit.html` 中的 `ceremonyType``ceremonyTitle``ceremonyTime``location``ceremonyDesc``coverOssId` 不属于任何现有 PC 写入 DTO,必须继续保持禁用,不得向猜测路径提交。
两条删除接口只能在后端补齐列表和详情、产生稳定 ID 后开放:
- 删除活动是逻辑删除活动及祭品并释放封面引用;
- 删除祭品只操作被选中的真实 `giftId`
- 删除确认必须说明影响范围。
### 7.11 成长记录
页面:`profile-growth.html``profile-growth-edit.html`
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `lineagePersonId` | 可选 | S/I | 世系人物搜索选择器,禁止 ID 输入框 |
| `recordType` | 可选 | U 或 S | C12 未解决前按 Apifox 明确结果决定,不能猜枚举 |
| `recordTitle` | 必填 | U | 标题 |
| `recordContent` | 可选 | U | 正文编辑器 |
| `recordDate` | 可选 | S | 日期控件,等待 C13 明确格式 |
| `remindTime` | 可选 | S | 日期时间控件 |
| `mediaOssIds` | 可选 | F/I | 多附件上传 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
列表、详情、新增和修改响应仍是泛型对象。补齐 `GrowthRecordView` 前不得展示原始 JSON,也不得猜 `recordId` 开启编辑和删除。
### 7.12 亲友记录
页面:`profile-relative.html``profile-relative-edit.html`
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `relativeName` | 必填 | U | 亲友姓名 |
| `relationName` | 可选 | U | 关系名称 |
| `eventName` | 可选 | U | 事件名称 |
| `eventTime` | 可选 | S | 日期时间选择器 |
| `giftAmount` | 可选 | U | 金额输入,明确精度和非负规则后校验 |
| `recordContent` | 可选 | U | 内容 |
| `mediaOssIds` | 可选 | F/I | 多附件上传 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
补齐 `RelativeRecordView` 前只完成页面字段布局和请求契约,不开放真实列表、编辑和删除。
### 7.13 备忘录
页面:`profile-memo.html``profile-memo-edit.html`
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `memoTitle` | 必填 | U | 标题 |
| `memoContent` | 可选 | U | 内容 |
| `remindTime` | 可选 | S | 日期时间 |
| `completed` | 可选 | S | 应为开关;C11 明确提交值前保持禁用 |
| `mediaOssIds` | 可选 | F/I | 多附件上传 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
`completed` 表示业务完成状态,`status` 表示记录正常/停用,两者不能合并。补齐 `MemoView` 前不得猜 `memoId` 或响应字段。
### 7.14 功德记录
页面:`profile-merit.html``profile-merit-edit.html`
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `donorName` | 必填 | U | 功德人姓名 |
| `meritType` | 可选 | S | donation 捐赠、repair 修祠、public 公益、other 其他 |
| `meritTitle` | 必填 | U | 标题 |
| `meritContent` | 可选 | U | 内容 |
| `amount` | 可选 | U | 金额 |
| `meritTime` | 可选 | S | date-time |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
现有页面错误地使用了 `service`,但契约枚举中不存在该值;必须删除 `service`,增加 `repair``public`。现有页面还缺少 `sortOrder``status`
列表、详情、新增和修改仍是泛型响应。补齐 `MeritRecordView` 后再开放完整 CRUD。
### 7.15 消息通知
页面:`profile-messages.html`
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `readStatus` | 可选 query | S | 全部=省略、未读=0、已读=1 |
| `notificationId` | 单条已读必填 path | I/A | 从被点击通知记录带入 |
| 未读数量 `data` | 响应 | R | 顶部徽标,int64 按安全数值/字符串处理 |
通知列表当前是 `ListResult<object>`,没有通知 ID、标题、正文、类型、时间、已读状态和目标深链。补齐 `NotificationView` 前:
- 可以调用未读数量和全部已读;
- 不得展示原始对象;
- 不得猜 `notificationId` 开放单条已读;
- 不得根据未声明字段拼详情跳转。
### 7.16 当前只有删除能力的模块
| 模块 | 当前操作 | 页面策略 |
| --- | --- | --- |
| 内容文章 | 删除文章 | `profile-article*` 保持设计;没有列表/详情时不开放删除 |
| 相册 | 删除相册、删除照片 | `profile-album.html` 保持设计;不使用静态 ID 调删除 |
| 祭祀 | 删除活动、删除祭品 | 活动 CRUD 和礼物列表补齐前保持禁用 |
删除按钮必须由真实响应中的 ID 和权限字段产生。禁止用 DOM 序号、示例 ID 或 URL 猜测资源 ID。
## 8. 分阶段执行顺序
### 阶段 0:冻结契约
工作:
1. 完成第 5 节 C01-C20 的本阶段相关项。
2. 从 Apifox 重导 OpenAPI。
3. 写契约快照测试,统计操作、请求字段、required、枚举、response `$ref`
4. 确认旧路径和旧字段会失败,不保留兼容代码。
验证:
- JSON 可被解析;
- 实际接口数与收口后的 Apifox 一致;
- 0 个重复逻辑路径;
- 本轮实现模块不存在泛型业务响应;
- 登录注册等公开接口不带 Authorization。
### 阶段 1:请求基础设施、验证和认证
工作:
1. 收口 `config.js``AxiosRequestUtil.js``ApiClient.js` 的 header、token、tenant 和错误处理。
2. 完成验证中心、登录、注册、短信、找回、换绑、注销、退出。
3. 完成个人资料全部 6 个写入字段。
4. 为每条认证流程测试 body 中不存在 `clientId``sceneCode` 和旧验证码字段。
验证:
- 密码、短信两种登录成功;
- 验证策略开/关两条路径都成功;
- 一次性 `validToken` 不能复用;
- 401 清除登录态,403 保留登录态;
- 确认密码等 V 类字段未进入请求。
### 阶段 2:文件上传和区划
前提:C01、C08、C09、C10、C13 完成。
工作:
1. 实现单分片小文件和多分片大文件同一流程。
2. 实现秒传、进度、重试、重复提交锁。
3. 只保留一套区划 API,并实现级联、搜索、路径回显。
4. 所有业务页移除可见 OSS ID 输入框。
验证:
- 普通图片、最后一片不足 chunkSize、大视频、秒传分别通过;
- MD5、大小、分片数量与初始化一致;
- 页面刷新后的业务对象能重新显示文件;
- 区划不同时请求两套路径。
### 阶段 3:家谱上下文
前提:C14 完成,或产品正式给出可验证的外部上下文契约。
工作:
1. `profile-common.js` 成为 `genealogyId` 的唯一 owner。
2. 我的家谱页提供真实选择入口。
3. 所有家谱内链接传播同一个 ID,切换家谱时清空旧页面状态。
验证:
- 无 ID 不发请求;
- 切换两个家谱不串数据;
- ID 始终按字符串处理;
- 不存在示例 ID 和 APP 请求。
### 阶段 4:已具备明确 DTO 的家谱模块
顺序:
1. 家族圈;
2. 字辈谱;
3. 世系人物;
4. 我的贺礼邀请。
每个模块采用同一闭环:
1. `ApiClient` 方法和契约测试;
2. 列表/详情读取;
3. 新增;
4. 修改;
5. 删除/停用;
6. 权限、空状态、错误状态、重复提交;
7. 写入后重新读取验证。
### 阶段 5:补齐 DTO 后的资源模块
顺序:
1. 视频;
2. 成长记录;
3. 亲友记录;
4. 备忘录;
5. 功德记录;
6. 消息通知。
任何模块的 View/List DTO 未补齐时,只完成表单布局和契约测试,不宣称真实功能完成。
### 阶段 6:后端补齐后再开放的模块
范围:
- 文章;
- 相册/照片;
- 祭祀活动/祭品;
- 管理员邀约名单;
- 指定账号绑定世系人物;
- 家谱和成员管理;
- 世系关系解除。
这些模块不允许通过 APP 接口或猜测路径提前实现。
## 9. 文件级施工建议
执行 AI 应优先修改现有 owner,避免页面各自复制逻辑:
| 文件 | 职责 |
| --- | --- |
| `PC.openapi.json` | 当次 Apifox 导出快照,不手工补字段 |
| `config.js` | 环境、API 基础地址、非敏感客户端配置 |
| `utils/ApiClient.js` | 所有正式 PC method/path/query/body |
| `utils/AxiosRequestUtil.js` | header、鉴权、响应解包、401/403 |
| `utils/FormUtil.js` | 表单取值、空可选字段省略、前端校验 |
| `public/js/profile-common.js` | 登录页外的家谱上下文 |
| `public/js/upload-pages.js` | 唯一上传流程 |
| `public/js/auth-pages.js` / `security-pages.js` | 认证业务流程 |
| `public/js/feed-pages.js` | 家族圈 |
| `public/js/generation-pages.js` | 字辈 |
| `public/js/lineage-pages.js` | 世系 |
| `public/js/growth-pages.js``relative-pages.js``memo-pages.js` | 对应族务记录 |
| 新增专用视频/功德/邀约脚本 | 仅在 DTO 完整后新增 |
不得创建一个包揽所有业务的巨型页面脚本,也不得为了单一页面引入通用框架。
## 10. 测试与验收
### 10.1 必须新增或更新的测试
1. OpenAPI 契约快照:
- operation 数;
- method/path
- body Schema
- required
- enum
- response `$ref`
- 公开/鉴权标记。
2. `ApiClient` 契约测试:
- URL、query、body、header
- int64 ID 不转 Number
- 可选空字段被省略;
- 旧路径/旧字段不存在。
3. 页面字段覆盖测试:
- 每个请求 Schema 字段都能在本规划的 U/S/F/A/I/V 分类中找到;
- 用户可编辑字段有对应 `name`
- 自动字段没有可见文本输入;
- 枚举选项和值与 Apifox 一致。
4. 流程测试:
- 验证与短信;
- 上传;
- 家谱上下文;
- 列表 → 详情 → 编辑 → 重读;
- 删除/停用;
- 401/403/404/409/422/429。
5. 安全测试:
- 富文本经过净化;
- 密码、token、validToken 不写日志;
- 外链和上传文件类型受限;
- 错误消息不直接插入 HTML。
### 10.2 建议验证命令
先跑最小相关测试,再跑全量:
```powershell
node --test tests/api-client-contract.test.js
node --test tests/auth-pages.test.js tests/captcha-pages-contract.test.js
node --test tests/feed-pages.test.js tests/generation-pages.test.js tests/lineage-pages.test.js
node --test tests/growth-pages.test.js tests/relative-pages.test.js tests/memo-pages.test.js tests/notification-pages.test.js
node --test tests/*.test.js
```
执行 AI 若新增测试文件,应在本节补上精确命令。
### 10.3 模块完成定义
一个模块只有同时满足以下条件才可标记完成:
1. Apifox 契约无未决冲突。
2. 所有请求字段已分类并落到页面。
3. 列表和详情响应有明确 DTO。
4. `ApiClient` 不存在旧路径或 fallback。
5. 页面有加载、空、成功、校验失败、无权限和网络失败状态。
6. 写操作防重复提交。
7. 成功写入后通过重新读取验证。
8. 相关测试和全量测试通过。
9. 没有原始 JSON、手填 ID、示例业务数据或 APP 接口冒充真实能力。
## 11. 执行完成后的报告格式
执行 AI 每个阶段只报告:
```text
Changed: 修改了哪些 owner、页面和契约。
Verified: 运行了哪些测试,真实联调覆盖了哪些流程。
Blocked: 哪些字段或流程仍缺 Apifox/后端契约,具体缺什么。
Next: 下一阶段可以开始的最小范围。
```
不得用“应该可以”“大概完成”代替测试证据。
@@ -1,129 +0,0 @@
# 内容与协作页面状态收口 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让缺少正式接口的内容、协作、消息与提醒页面明确展示开发状态,同时保留家族圈动态的真实功能。
**Architecture:** 复用 `public/js/pending-pages.js` 的页面状态契约。每个待开发页面仅增加 `data-feature-status`、中文提示文案和状态脚本引用;脚本阻止主内容区未声明可用的业务链接,不新增接口调用、示例数据或业务脚本。
**Tech Stack:** 原生 JavaScript、静态 HTML/CSS、Node LTS `node:test`
## Global Constraints
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
- `PC.openapi2.json` 中缺少正式接口的功能不得伪造请求或静态数据为真实业务。
- 家族圈动态 `profile-feed.html``profile-feed-edit.html` 已接入正式接口,不能标记为待开发。
- 所有新增注释使用中文;不写入测试账号或密码。
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
---
### Task 1: 扩展页面状态测试
**Files:**
- Modify: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: 每页的 `data-feature-status="pending"``public/js/pending-pages.js`
- Produces: 内容与协作待开发页面的静态约束,以及家族圈动态未被误标记的约束。
- [x] **Step 1: 写入失败测试**
`contentPendingPages` 中列出以下页面:
```js
const contentPendingPages = [
'profile-content.html', 'profile-article.html', 'profile-article-edit.html',
'profile-album.html', 'profile-video.html', 'profile-gift.html',
'profile-gift-edit.html', 'profile-growth.html', 'profile-growth-edit.html',
'profile-memo.html', 'profile-memo-edit.html', 'profile-merit.html',
'profile-merit-edit.html', 'profile-messages.html', 'profile-feedback.html',
'profile-admin-permissions.html', 'profile-data-reminders.html'
];
```
同时断言 `profile-feed.html``profile-feed-edit.html` 不含待开发状态标记,并验证只有 `data-feature-link="available"` 的业务链接可跳转。
- [x] **Step 2: 运行失败测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
Expected: 新增页面尚未标记,测试失败。
### Task 2: 标记内容与协作页面
**Files:**
- Modify: `profile-content.html`
- Modify: `profile-article.html`
- Modify: `profile-article-edit.html`
- Modify: `profile-album.html`
- Modify: `profile-video.html`
- Modify: `profile-gift.html`
- Modify: `profile-gift-edit.html`
- Modify: `profile-growth.html`
- Modify: `profile-growth-edit.html`
- Modify: `profile-memo.html`
- Modify: `profile-memo-edit.html`
- Modify: `profile-merit.html`
- Modify: `profile-merit-edit.html`
- Modify: `profile-messages.html`
- Modify: `profile-feedback.html`
- Modify: `profile-admin-permissions.html`
- Modify: `profile-data-reminders.html`
- Test: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: `PageAvailability.init()` 读取的 `<body data-feature-status="pending" data-feature-message="..."></body>`
- Produces: 可见的“功能开发中”提示,并拦截主内容区提交与操作按钮。
- [x] **Step 1: 为每页增加状态契约**
在目标页面的 `body` 增加 `data-feature-status="pending"` 和与业务相符的中文 `data-feature-message`。例如谱文页面使用“谱文服务正在开发中,当前页面仅保留设计预览。”
- [x] **Step 2: 加载统一状态脚本**
`ApiClient.js` 的页面在其后加载:
```html
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
```
`profile-video.html``profile-data-reminders.html` 不依赖 `ApiClient.js`,在 `page-effects.js` 前加载状态脚本。
- [x] **Step 3: 收束业务入口跳转**
待开发页面主内容区的业务链接由状态脚本拦截并显示当前页面提示;侧栏保留页面浏览导航。`profile-content.html` 的家族圈动态入口使用 `data-feature-link="available"`,继续跳转到 `profile-feed.html`
- [x] **Step 4: 运行测试确认通过**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
Expected: 待开发页面均加载状态脚本,家族圈动态页面未被标记。
### Task 3: 更新进度并做静态验证
**Files:**
- Modify: `docs/规划.md`
- Modify: `docs/superpowers/plans/2026-07-11-content-page-state-closure.md`
- Test: `tests/*.test.js`
**Interfaces:**
- Consumes: 全部页面的本地脚本引用。
- Produces: 已完成第二轮进度与可重复的验证结果。
- [x] **Step 1: 更新总规划的第二轮状态**
`docs/规划.md` 中“第二轮:内容与协作页面状态收口”从未完成更新为已完成,并写明家族圈动态未受影响。
- [x] **Step 2: 执行完整测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 所有 Node 测试通过。
- [x] **Step 3: 执行静态检查**
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,随后运行 `git diff --check`
Expected: 每个本地脚本文件存在,且没有空白格式错误。
@@ -1,192 +0,0 @@
# 家族圈动态接口接入 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`PC.openapi2.json` 中已交付的 12 个家族圈动态接口接入既有动态页面,并保持未开发的家谱业务不调用不存在的接口。
**Architecture:** `PC.openapi2.json` 是接口唯一依据;`ApiClient` 提供一一对应的私有请求封装,`feed-pages.js` 只协调页面事件和已定义的接口方法。动态页从 URL 查询参数读取真实 `genealogyId`,不写入示例家谱 ID。
**Tech Stack:** 原生 JavaScript、Axios、Node LTS `node:test`、静态 HTML/CSS。
## Global Constraints
- 所有新增或修改的注释使用清晰中文。
- 不删除现有页面和样式;不接入尚未出现在 `PC.openapi2.json` 的业务。
- 不提交当前工作区:其中含用户已有的未提交修改。
- 动态读取响应目前只有通用 `ListResult`/`PageResult`,页面仅使用 `feedId``feedContent`;后端返回缺少这两个字段时展示明确错误,不猜测字段名。
---
### Task 1: 更新接口边界测试和动态请求测试
**Files:**
- Modify: `tests/api-client-contract.test.js`
- Modify: `tests/pc-scope.test.js`
- Create: `tests/feed-pages.test.js`
**Interfaces:**
- Consumes: `PC.openapi2.json` 的 12 个 `/genealogy/pc/genealogies/{genealogyId}/feeds` 操作。
- Produces: 对客户端路径、HTTP 方法、请求体和页面请求体转换的自动化约束。
- [ ] **Step 1: 写入失败测试**
```js
test('家族圈动态方法使用文档定义的路径和请求体', async () => {
const calls = [];
const client = createClientWithRecorder(calls);
await client.createFeed(900001001, { feedContent: '今天上传一张老照片。' });
await client.feedCommentsPage(900001001, 7001, { pageNum: 1, pageSize: 10 });
assert.deepEqual(calls.map((call) => [call.method, call.url]), [
['post', '/genealogy/pc/genealogies/900001001/feeds'],
['get', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/page']
]);
});
test('动态表单只构造 FamilyFeedBody 字段', () => {
assert.deepEqual(FeedPages.buildFeedBody({
feedContent: '家谱动态', mediaOssIds: '101,102', status: '0'
}), {
feedType: 'text', feedContent: '家谱动态', mediaOssIds: '101,102', status: '0'
});
});
```
- [ ] **Step 2: 运行失败测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 动态方法或 `feed-pages.js` 尚不存在导致失败。
- [ ] **Step 3: 调整范围检查**
```js
assert.match(contract, /\/genealogy\/pc\/genealogies\/\{genealogyId\}\/feeds/);
assert.equal(read('profile-feed.html').includes('src="public/js/feed-pages.js"'), true);
assert.equal(read('profile-feed-edit.html').includes('src="public/js/feed-pages.js"'), true);
```
- [ ] **Step 4: 重新运行测试,确认仍处于预期失败状态**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 仅动态实现相关断言失败。
### Task 2: 增加 12 个家族圈 API 方法
**Files:**
- Modify: `utils/ApiClient.js`
- Modify: `tests/api-client-contract.test.js`
**Interfaces:**
- Consumes: `genealogyId:number|string``feedId:number|string``commentId:number|string`
- Produces: `feeds``feedsPage``feedDetail``createFeed``updateFeed``deleteFeed``likeFeed``unlikeFeed``feedComments``feedCommentsPage``createFeedComment``deleteFeedComment`
- [ ] **Step 1: 保持失败测试不变**
```js
await client.createFeed(900001001, { feedContent: '家谱动态' });
assert.equal(calls[0].url, '/genealogy/pc/genealogies/900001001/feeds');
assert.deepEqual(calls[0].data, { feedContent: '家谱动态' });
```
- [ ] **Step 2: 实现最小路径构造**
```js
function feedPath(genealogyId, suffix) {
return '/genealogy/pc/genealogies/' + encodeURIComponent(genealogyId) + '/feeds' + suffix;
}
createFeed: function (genealogyId, body) {
return request('POST', feedPath(genealogyId, ''), { body: body });
}
```
- [ ] **Step 3: 运行测试,确认通过**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 动态客户端路径和请求体断言通过。
### Task 3: 恢复动态页的真实交互
**Files:**
- Create: `public/js/feed-pages.js`
- Modify: `profile-feed.html`
- Modify: `profile-feed-edit.html`
- Modify: `tests/feed-pages.test.js`
**Interfaces:**
- Consumes: `GenealogyApi.defaultClient` 的 12 个动态方法和 URL 中的 `genealogyId`、可选 `feedId`
- Produces: 动态列表、发布/编辑、点赞/取消点赞、评论查看/发布/删除、动态删除事件。
- [ ] **Step 1: 使页面测试失败**
```js
assert.equal(FeedPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(FeedPages.getCurrentGenealogyId(''), '');
assert.equal(FeedPages.getFeedId({ feedId: 7001 }), '7001');
```
- [ ] **Step 2: 实现页面数据转换和事件**
```js
function buildFeedBody(values) {
return {
feedType: 'text',
feedContent: String(values.feedContent || '').trim(),
mediaOssIds: trimOrUndefined(values.mediaOssIds),
status: trimOrUndefined(values.status)
};
}
```
列表只读取 `feedId``feedContent`,并在缺少其中任一字段时显示“动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO”。编辑页以 `feedId` 查询详情并调用 `updateFeed`;列表操作调用点赞、取消点赞、评论、删除接口后重新加载当前页。
- [ ] **Step 3: 挂载脚本和契约字段**
```html
<textarea id="feedContent" name="feedContent" required></textarea>
<script src="public/js/feed-pages.js"></script>
```
移除未在 `FamilyFeedBody` 中定义的“可见范围”提交字段;保持页面布局与既有视觉类名。
- [ ] **Step 4: 运行测试,确认通过**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: `feed-pages.test.js` 和已有测试全部通过。
### Task 4: 更新文档来源与最终验证
**Files:**
- Modify: `tests/pc-scope.test.js`
- Modify: `PC.openapi.yaml`
**Interfaces:**
- Consumes: `PC.openapi2.json` 的接口路径。
- Produces: 明确标记旧 YAML 不再为接口源,并使静态检查以 JSON 为准。
- [ ] **Step 1: 写入失败检查**
```js
const contract = JSON.parse(read('PC.openapi2.json'));
assert.ok(contract.paths['/genealogy/pc/genealogies/{genealogyId}/feeds']);
```
- [ ] **Step 2: 标记旧 YAML 失效**
`PC.openapi.yaml` 文件首行写入中文说明:该文件不再作为代码或测试接口源,正式接口源为 `PC.openapi2.json`。不复制或手改 JSON 中未定义的动态响应 DTO。
- [ ] **Step 3: 全量验证**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 全部测试通过,且所有动态页面加载的脚本文件都存在。
## Self-review
- 范围只覆盖最新文档已交付的家族圈动态接口,没有加入家谱创建、成员或世系树接口。
- 所有请求体字段来自 `FamilyFeedBody``FamilyFeedCommentBody`
- 读取响应不使用历史字段兜底,不把未知字段猜成有效数据。
- 任何 URL 缺少 `genealogyId` 的页面都会明确提示,不使用硬编码示例 ID。
@@ -1,140 +0,0 @@
# 家谱基础页面状态收口 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让尚未获得后端家谱接口的基础页面明确展示开发状态,并阻止表单和操作按钮伪造成功结果。
**Architecture:** 仅在家谱列表、创建/加入、审核、成员管理、邀请、世系和字辈页面加载一个轻量状态脚本。页面通过 `data-feature-status``data-feature-message` 提供文案;脚本统一插入提示条,并拦截页面主内容区的提交与按钮操作。
**Tech Stack:** 原生 JavaScript、静态 HTML/CSS、Node LTS `node:test`
## Global Constraints
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
- 不调用 `PC.openapi2.json` 中不存在的家谱、成员、世系或字辈接口。
- 所有新增注释使用中文;不写入测试账号或密码。
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
---
### Task 1: 建立失败测试
**Files:**
- Create: `tests/pending-pages.test.js`
- Modify: `tests/pc-scope.test.js`
**Interfaces:**
- Consumes: `PageAvailability.buildBanner(message)``PageAvailability.isPendingPage(body)`
- Produces: 对状态脚本、页面标记和脚本加载的约束。
- [x] **Step 1: 写入失败测试**
```js
test('待开发页面生成明确且转义后的状态提示', () => {
assert.match(PageAvailability.buildBanner('家谱基础服务正在开发中'), /家谱基础服务正在开发中/);
assert.match(PageAvailability.buildBanner('<script>'), /&lt;script&gt;/);
});
test('家谱基础页面显式加载状态脚本', () => {
assert.equal(read('profile-families.html').includes('data-feature-status="pending"'), true);
assert.equal(read('profile-tree.html').includes('src="public/js/pending-pages.js"'), true);
});
```
- [x] **Step 2: 运行失败测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js tests/pc-scope.test.js`
Expected: `pending-pages.js` 不存在且页面未标记导致失败。
### Task 2: 实现统一状态脚本与样式
**Files:**
- Create: `public/js/pending-pages.js`
- Modify: `public/css/profile-module.css`
- Test: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: `<body data-feature-status="pending" data-feature-message="..."></body>`
- Produces: `PageAvailability.buildBanner(message)``PageAvailability.init()`
- [x] **Step 1: 实现最小状态脚本**
```js
function buildBanner(message) {
return '<section class="feature-status-banner" role="status"><strong>功能开发中</strong><p>' + escapeHtml(message) + '</p></section>';
}
```
脚本只在 `data-feature-status="pending"` 页面运行,将提示条放在 `.module-main` 首位;拦截该页面 `main` 区域内的表单提交和非重置按钮,显示同一提示文案。
- [x] **Step 2: 增加复用样式**
```css
.feature-status-banner {
border: 1px solid rgba(196, 146, 69, .38);
border-radius: 16px;
background: #fff8e8;
}
```
- [x] **Step 3: 运行测试确认通过**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js tests/pc-scope.test.js`
Expected: 新增状态脚本和页面标记断言通过。
### Task 3: 标记家谱基础页面
**Files:**
- Modify: `profile-families.html`
- Modify: `profile-create-family.html`
- Modify: `profile-join-family.html`
- Modify: `profile-join-review.html`
- Modify: `profile-family-admin.html`
- Modify: `profile-invite.html`
- Modify: `profile-tree.html`
- Modify: `profile-generation.html`
**Interfaces:**
- Consumes: `pending-pages.js` 和每页 `data-feature-message`
- Produces: 明确状态提示,不提交文档外请求。
- [x] **Step 1: 在 body 标记状态**
```html
<body class="page-profile-module" data-feature-status="pending" data-feature-message="家谱成员与权限服务正在开发中,当前页面仅保留设计预览。">
```
- [x] **Step 2: 在 ApiClient 后加载状态脚本**
```html
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/page-effects.js"></script>
```
- [x] **Step 3: 运行完整测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 全部测试通过。
### Task 4: 静态验证
**Files:**
- Test: `tests/pc-scope.test.js`
**Interfaces:**
- Consumes: 8 个已标记页面。
- Produces: 所有待开发页面有状态文案,所有页面脚本路径存在。
- [x] **Step 1: 执行脚本引用检查**
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,确认每个本地脚本文件存在。
- [x] **Step 2: 执行格式检查**
Run: `git diff --check`
Expected: 无缺失脚本和空白格式错误。
@@ -1,139 +0,0 @@
# 官网静态页面补齐 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 补齐官网新闻、法律文档入口、通用状态与工单页面,使无接口的线上服务不再伪装为可用功能。
**Architecture:** 新增静态 HTML 页面共用 `public/css/site-info.css` 和现有官网头尾布局。新闻页只链接现有静态文章与公告详情;法律页面明确标明待法务确认;工单页面复用 `pending-pages.js` 阻止无接口提交。
**Tech Stack:** 静态 HTML、原生 CSS、原生 JavaScript、Node LTS `node:test`
## Global Constraints
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
- 不新增 `PC.openapi2.json` 外的接口调用,不伪造新闻接口、法律协议、工单列表或工单详情数据。
- 法律页面只能显示“待法务确认”说明,发布前必须替换为主体与法务确认的正式文本。
- 所有新增注释使用中文;不写入测试账号或密码。
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
---
### Task 1: 建立静态页面约束测试
**Files:**
- Create: `tests/public-static-pages.test.js`
**Interfaces:**
- Consumes: 静态页面文件、`data-feature-status="pending"` 和本地 `href`
- Produces: 页面存在、关键说明、状态脚本与本地链接的可重复校验。
- [x] **Step 1: 写入失败测试**
测试必须断言存在以下文件:
```js
const staticPages = [
'news.html', 'privacy.html', 'children-privacy.html', 'terms.html',
'not-found.html', 'forbidden.html', 'maintenance.html',
'my-tickets.html', 'ticket-detail.html'
];
```
同时断言:`news.html` 含有新闻分类锚点并链接 `article-detail.html``notice-detail.html`;三个法律页面含“待法务确认”;`submit-ticket.html``my-tickets.html``ticket-detail.html` 有待开发状态和 `pending-pages.js`;新页面中的本地 `href` 全部存在。
- [x] **Step 2: 运行失败测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/public-static-pages.test.js`
Expected: 新页面不存在导致测试失败。
### Task 2: 建立通用静态页面外观与新闻、法律入口
**Files:**
- Create: `public/css/site-info.css`
- Create: `news.html`
- Create: `privacy.html`
- Create: `children-privacy.html`
- Create: `terms.html`
- Modify: `index.html`
**Interfaces:**
- Consumes: 现有 `public/css/public.css``site-header``footer``container``btn``card` 样式。
- Produces: 新闻分类和详情跳转,以及能从首页底部访问的法律入口。
- [x] **Step 1: 创建复用样式**
`site-info.css` 提供 `.site-info-hero``.site-info-layout``.site-info-card``.site-info-status``.site-info-links`,仅处理本轮新增页面的排版和窄屏适配。
- [x] **Step 2: 创建新闻页面**
`news.html` 必须有“平台公告”“家谱文化”分类锚点,并将两条静态内容分别链接到 `notice-detail.html``article-detail.html`
- [x] **Step 3: 创建法律页面并连接首页**
三个法律页面均写明“待法务确认”,并互相提供跳转链接。首页 `footer-legal` 增加三个页面链接。
### Task 3: 建立状态与工单边界
**Files:**
- Create: `not-found.html`
- Create: `forbidden.html`
- Create: `maintenance.html`
- Create: `my-tickets.html`
- Create: `ticket-detail.html`
- Modify: `submit-ticket.html`
**Interfaces:**
- Consumes: `PageAvailability.init()` 读取的待开发页面属性。
- Produces: 三种无接口提交的工单状态,和可返回首页、帮助中心的静态状态页。
- [x] **Step 1: 创建三个通用状态页**
三个页面分别显示“页面不存在”“无访问权限”“系统维护中”,并提供 `index.html` 返回入口。
- [x] **Step 2: 创建工单列表与详情状态页**
`my-tickets.html``ticket-detail.html` 均使用:
```html
<body data-feature-status="pending" data-feature-message="在线工单查询服务正在开发中,当前页面仅保留设计预览。">
```
并在 `public/js/pending-pages.js` 后仅放行帮助中心链接。
- [x] **Step 3: 收束提交工单表单**
`submit-ticket.html` 增加:
```html
<body class="page-submit-ticket" data-feature-status="pending" data-feature-message="在线提交工单服务正在开发中,请先通过帮助中心了解常见问题。">
```
`ApiClient.js` 后加载状态脚本,并为“返回帮助中心”链接添加 `data-feature-link="available"`
### Task 4: 回写进度与验证
**Files:**
- Modify: `docs/规划.md`
- Modify: `docs/superpowers/plans/2026-07-11-public-static-pages.md`
- Test: `tests/*.test.js`
**Interfaces:**
- Consumes: 新增静态页面和全部 HTML 本地脚本引用。
- Produces: 第四轮进度记录和验证结果。
- [x] **Step 1: 写入第四轮完成状态**
`docs/规划.md` 的“实施进度”中增加静态官网页面补齐已完成,并注明法律文本需法务替换。
- [x] **Step 2: 执行完整测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 所有 Node 测试通过。
- [x] **Step 3: 执行静态检查**
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,随后运行 `git diff --check`
Expected: 每个本地脚本文件存在,且没有空白格式错误。
@@ -1,112 +0,0 @@
# 剩余用户 PC 业务入口收口 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让剩余未有正式接口支持的用户 PC 业务入口显示明确状态,避免跳转到看似可用的分享、会员或家谱管理流程。
**Architecture:** 复用 `public/js/pending-pages.js` 的状态契约和链接收束规则。仅标记 `profile-family-home.html``profile-share.html``profile-services.html`;账号主页、个人资料、安全设置和家族圈动态继续使用现有真实功能。
**Tech Stack:** 原生 JavaScript、静态 HTML/CSS、Node LTS `node:test`
## Global Constraints
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
- `PC.openapi2.json` 中不存在家谱主页、邀请奖励和会员订单的正式接口,不得伪造请求或静态数据为真实业务。
- `profile-family-home.html` 的家族圈动态入口继续跳转到 `profile-feed.html`
- 所有新增注释使用中文;不写入测试账号或密码。
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
---
### Task 1: 扩展入口状态测试
**Files:**
- Modify: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: 目标页面的 `data-feature-status="pending"`、状态脚本引用与 `data-feature-link="available"`
- Produces: 剩余业务入口的静态约束,及真实账户页面不被误标记的约束。
- [x] **Step 1: 写入失败测试**
新增以下数组并加入现有待开发页面检查:
```js
const residualPendingPages = [
'profile-family-home.html',
'profile-share.html',
'profile-services.html'
];
```
新增断言:`profile-family-home.html``profile-feed.html` 链接必须带有 `data-feature-link="available"``profile.html``profile-data.html``profile-security.html` 不得含待开发状态标记。
- [x] **Step 2: 运行失败测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
Expected: 三个剩余入口尚未标记,测试失败。
### Task 2: 标记剩余业务入口
**Files:**
- Modify: `profile-family-home.html`
- Modify: `profile-share.html`
- Modify: `profile-services.html`
- Test: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: `PageAvailability.init()` 读取的页面状态属性,和 `PageAvailability.shouldBlockLink(link)` 的可用入口约定。
- Produces: 状态提示、业务链接拦截,以及唯一保留的动态入口。
- [x] **Step 1: 标记页面状态**
分别在三个 `body` 中增加 `data-feature-status="pending"` 和对应中文提示:家谱主页使用“家谱概览与管理服务正在开发中,当前页面仅保留设计预览。”;应用分享使用“应用分享与邀请奖励服务正在开发中,当前页面仅保留设计预览。”;帮助与服务使用“会员与在线服务正在开发中,当前页面仅保留设计预览。”
- [x] **Step 2: 加载统一状态脚本并放行动态入口**
每页都在 `utils/ApiClient.js` 后增加:
```html
<script src="utils/ApiClient.js"></script>
<script src="public/js/pending-pages.js"></script>
```
`profile-family-home.html` 中将动态链接改为:
```html
<a class="btn primary magnetic" href="profile-feed.html" data-feature-link="available">发布动态</a>
```
- [x] **Step 3: 运行测试确认通过**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
Expected: 全部状态页面和真实账户页面断言通过。
### Task 3: 更新进度与验证
**Files:**
- Modify: `docs/规划.md`
- Modify: `docs/superpowers/plans/2026-07-11-residual-pc-entry-closure.md`
- Test: `tests/*.test.js`
**Interfaces:**
- Consumes: 全部 HTML 的本地脚本引用。
- Produces: 已完成第三轮进度和可重复验证记录。
- [x] **Step 1: 写入第三轮完成状态**
`docs/规划.md` 的“实施进度”中增加已完成第三轮,说明三类剩余 PC 业务入口已收口,家族圈动态未受影响。
- [x] **Step 2: 执行完整测试**
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
Expected: 所有 Node 测试通过。
- [x] **Step 3: 执行静态检查**
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,随后运行 `git diff --check`
Expected: 每个本地脚本文件存在,且没有空白格式错误。
+45 -160
View File
@@ -1,182 +1,67 @@
# PC 接口对接交接文档
# 家谱 PC 前端交接
> 更新:2026-07-25
> 历史交接记录:本文的 79/85 条接口数量已过期,不再作为 AI 施工依据。当前接口基线、字段分类、阻断项和执行顺序统一以 [PC接口对接规划.md](PC接口对接规划.md) 为准;执行前仍须在 Apifox 实时复核并重新导出。
更新日期2026-09-17
## 一、必须遵守的范围
## 项目是什么
1. Apifox 的 **PC 目录**是唯一正式契约源
2. 本地 OpenAPI 导出文件可能少接口,只能辅助查找,不能据此决定接口存在、路径或字段。
3. 不使用 APP 接口,也不能把 `/app/` 改为 `/pc/` 后猜测使用。
4. 继续任何新接口前,必须直接在 Apifox PC 详情核对 method、path、query、body、响应和权限。
5. 家谱业务只能使用真实 `?genealogyId=...`;不写死编号、不从 APP 获取。
6. 页面不直接调用 Axios、不自行拼请求路径,统一通过 `utils/ApiClient.js`
这是一个静态 HTML 多页面项目。页面直接加载 CSS 和 JavaScript,没有 React、Vue 或单页应用入口
## 二、当前目录与历史施工范围
官网页面在根目录,例如 `index.html``login.html``genealogy.html`。登录后的个人中心和家谱功能页面以 `profile-` 开头。
### 当前 PC 目录(2026-07-25
## 主要目录
| 分组 | 条数 | 当前处理状态 |
| --- | ---: | --- |
| 验证中心 | 3 | 已逐条复核并迁移 |
| 认证登录 | 12 | 已逐条复核并迁移;其中两条短信发送定义同为当前 `operationCode` 路径 |
| 文件上传 | 3 | 已逐条复核、`ApiClient` 仅保留分片三条;初始化响应未展开,头像上传保持阻止 |
| 家谱 | 1 | 已核验并映射配额查询;入口页仅展示配额并阻止进入需 `genealogyId` 的业务页,不能伪造编号 |
| 家族圈 | 14 | 已逐条复核;动态、点赞、评论、直接回复及两类分页均与当前实现一致 |
| 行政区划 | 4 | 已逐条复核;当前 PC 目录下实际路径均为 `/genealogy/region/*`,与 `ApiClient` 一致 |
| 字辈谱 | 6 | 已逐条复核;正常查询、维护、新增、批量预览/保存和修改/停用/恢复均与当前实现一致 |
| 世系人物 | 12 | 已逐条复核;人物列表/分页/选项/树、CRUD/停用和四种新增关系均与当前实现一致 |
| 内容文章 | 1 | 已核验;仅删除,页面继续关闭危险入口 |
| 相册 | 2 | 已核验;仅删除,页面继续关闭危险入口 |
| 视频 | 1 | 已核验;仅删除,页面继续关闭危险入口 |
| 贺礼邀约 | 4 | 已逐条核验并映射;缺少活动列表/创建/详情,页面保持预览 |
| 祭祀 | 2 | 已核验;仅删除活动/祭品,页面继续关闭危险入口 |
| 族务记录 | 16 | 已逐条复核;成长记录、亲友往来、备忘录各 5 条与功德删除 1 条均保持当前契约 |
| 消息通知 | 4 | 已逐条核验并映射;消息页已接入原始列表、未读数和全部已读 |
| 合计 | 85 | 以桌面版 PC 目录为准 |
### 历史 79 条施工清单(2026-07-24,已被当前目录基线替代)
| 分组 | 条数 | 状态 |
| --- | ---: | --- |
| 验证中心 | 4 | 已核验、已接入 |
| 认证登录 | 11 | 已核验、已接入 |
| 文件上传 | 6 | 已核验;仅单文件上传页面闭环开放 |
| 行政区划 | 4 | 已核验、已接入 |
| 家族圈 | 14 | 已核验、已接入 |
| 字辈谱 | 6 | 已核验、已接入 |
| 世系人物 | 12 | 已核验、已接入 |
| 族务记录·成长记录 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
| 族务记录·亲友往来 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
| 族务记录·备忘录 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
| 族务记录·功德记录 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 内容文章 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 相册 | 2 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 视频 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 祭祀 | 2 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
| 合计 | 79 | |
完整映射见 [PC接口对接规划.md](PC接口对接规划.md)。
### 已核验关键规则
- 验证中心现为三条 PC 接口:`GET /genealogy/pc/auth/verification/{operationCode}/require``POST .../{operationCode}/challenge``POST .../{operationCode}/verify``operationCode` 仅允许 `password-login``sms-login``register``forgot-password``phone-change``account-deactivate`;前端不得提交旧 `sceneCode`
- `require` 的 query 为必填 `tenantId` 和可选 `subject`challenge 的 body 为必填 `tenantId``subject`verify 的 body 在此基础上增加 `challengeId`,可附 `providerCode``captchaType``payload`。三条都由请求头传 `clientid`,不得在 query/body 传 `clientId`
- 短信发送为 `POST /genealogy/pc/auth/sms/{operationCode}/code`,支持除 `password-login` 外的五个短信动作。body 使用 `grantType: "sms"``tenantId``phone` 和策略开启时的 `validToken``clientid` 只走请求头。
- 当前认证登录的上述已核验接口一律要求 `clientid` 请求头,且 body 明确禁止 `clientId`。注册/密码登录/找回密码的 body 为 `grantType: "password"``tenantId` 加业务字段;密码登录在验证中心策略要求时还提交本次验证取得的 `validToken`;短信登录为 `grantType: "sms"``tenantId``phone``smsCode`;换绑为 `phone``smsCode`;注销为 `smsCode`
- 族务记录当前 16 条由成长记录、亲友往来、备忘录三套完整 CRUD(各 5 条)和 `DELETE .../merit-records/{meritId}` 组成。三套 CRUD 都要求 `genealogyId`,详情/修改/删除还要求各自记录 ID;列表元素与新增/详情响应 DTO 仍未展开,页面只展示原始记录并保持无稳定 ID 的编辑、详情、删除入口关闭。
- 行政区划四条当前路径为 `GET /genealogy/region/children`(可选 `parentCode`)、`GET /genealogy/region/path/{regionCode}``GET /genealogy/region/search`(必填 `keyword`,可选 `level``limit`)、`GET /genealogy/region/{regionCode}`;均已与 `ApiClient` 核对一致。
- 字辈谱 6 条当前路径分别为 `GET .../generation-poems``GET .../generation-poems/management``POST .../generation-poems``POST .../generation-poems/batch/preview``POST .../generation-poems/batch/save``PUT .../generation-poems/{poemId}`;实现仍只使用其明确 DTO 字段。
- 世系人物 12 条当前包含 `persons` 列表/新增、`persons/page``persons/options``tree`、人物详情/修改/停用,以及 `children``parents``siblings``spouses` 四种以完整 `LineagePersonBody` 新建关系人物的接口;`genealogyId``personId` 均保持真实入口传入。
- 内容文章、相册、视频和祭祀当前 6 条均为删除孤岛:`DELETE .../articles/{articleId}``.../albums/{albumId}``.../albums/{albumId}/photos/{photoId}``.../videos/{videoId}``.../ceremonies/{ceremonyId}``.../ceremonies/{ceremonyId}/gifts/{giftId}`。它们已映射但无当前 PC 列表/详情来源,不能开放页面删除。
- 文件上传现仅有 `POST /genealogy/pc/files/resumable/init``POST .../chunk``POST .../complete`。所有文件统一先初始化;普通小文件可按初始化响应的 `instant=true` 直接取 OSS 信息,但该响应的字段 Schema 尚未展开,页面不得猜测 `uploadId``ossId``instant`,头像上传继续阻止。
- 家谱当前只提供 `GET /genealogy/pc/genealogies/quota`,返回创建/加入的已用数量、上限、剩余额度以及 `canCreate`/`canJoin`。它不返回家谱对象或 `genealogyId`;只映射为 `genealogyQuota`,不据此开放家谱业务页面。
- 贺礼邀约四条为替换受邀人、查询活动邀请名单、当前用户接受/拒绝、查询我的邀请。受邀人 body 只允许 `inviteeUserIds`,当前用户响应 body 只允许 `inviteStatus: ACCEPTED|DECLINED`。缺少活动列表/创建/详情,`profile-gift*.html` 继续为明确的待开发预览。
- 消息通知为列表(可选 `readStatus: 0|1`)、未读数量、单条已读和全部已读。列表元素 DTO 未展开,`profile-messages.html` 只安全展示服务端原始记录,不猜测标题、内容、时间或单条 `notificationId`;未读数和全部已读已开放。
- 认证相关均使用 PC 路径;密码为 32 位 MD5。改绑/注销只使用 PC DTO 所需字段,不擅自加 `tenantId`
- 家族圈 14 条当前路径为动态列表/分页/详情/增改删、点赞/取消点赞、一级评论列表/分页/发表/删除和直接回复列表/分页;均要求真实 `genealogyId`,现有 `ApiClient` 与页面调用已逐项比对一致。新评论只提交必填 `commentContent` 和可选 `parentCommentId`,不能发旧字段 `content``replyUserId`
- 字辈管理列表使用 `GET .../generation-poems/management`;批量操作使用 `poemText` 和可选 `disableMissing`。单项最多 50 字符、一次最多 500 代、总长最多 26000。
- 世系分页 query`pageNum``pageSize``keyword``generation``personStatus``keyword` 只查姓名、别名、人物编号。
- 世系写入字段是 `name``generation``biography`,不是旧字段 `personName``generationNo``introduction``sortOrder``relationName` 是可选字段。
- `DELETE .../lineage/persons/{personId}` 是逻辑停用,不是物理删除;有正常子女时后端会拒绝。
- 新增父母/配偶/兄弟姐妹/子女四个关系接口均接收完整 `LineagePersonBody` 来新建关系人物,并非绑定两个已有 ID。
- 已末次回到 Apifox 复核:世系分页 query、创建人物 body、`POST .../lineage/persons/{personId}/spouses``relationName`
- 成长记录 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/growth-records``/{recordId}``genealogyId``recordId` 均是必填 int64 路径参数,`clientid` 为必填请求头。
- 成长记录的 `GrowthRecordBody` 只有 `recordTitle` 必填;可选字段为 `lineagePersonId``recordType``recordContent``recordDate``remindTime``mediaOssIds``sortOrder``status``mediaOssIds` 只接受英文逗号分隔的正整数 OSS ID。
- 成长记录列表响应是未展开元素 DTO 的 `ListResult`,详情/新增/修改为未展开元素 DTO 的 `ObjectResult`,删除为 `VoidResult`。不能据此猜测 `recordId`、标题或权限字段;页面仅作原始列表展示和新增,不开放编辑、详情或删除入口。
- 亲友往来 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/relative-records``/{relativeId}``RelativeRecordBody` 只有 `relativeName` 必填,可选 `relationName``eventName``eventTime``giftAmount``recordContent``mediaOssIds``sortOrder``status`。列表/详情元素 DTO 同样未展开,页面不开放编辑、详情或删除入口。
- 备忘录 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/memos``/{memoId}``genealogyId``memoId` 均是必填 int64 路径参数,`clientid` 为必填请求头。备忘录请求体只有 `memoTitle` 必填,可选 `memoContent``remindTime``completed``mediaOssIds``sortOrder``status``completed` 在 Apifox 中是 string,不能擅自改成布尔值。列表/详情元素 DTO 未展开,页面仅作原始列表展示和新增。
- 功德记录在 PC 目录中当前只存在 `DELETE /genealogy/pc/genealogies/{genealogyId}/merit-records/{meritId}`;两个路径参数均为必填 int64,登录和 `clientid` 必填,响应为 `VoidResult``deleteMeritRecord` 已映射,但页面没有真实列表、详情或稳定 `meritId` 来源,必须保持删除入口关闭。
- 其余 6 个删除孤岛接口也已核验并映射:`deleteArticle``deleteAlbum``deleteAlbumPhoto``deleteVideo``deleteCeremony``deleteCeremonyGift`。它们都要求登录、`clientid` 和 int64 路径 ID,响应均为 `VoidResult`;相册、视频与祭祀删除会由后端释放相应文件引用。由于没有 PC 列表/详情和稳定 ID 来源,所有相关页面继续保持预览,不能触发删除。
## 三、已经落地的页面
### 账号与资料
- `login.html`:密码登录、短信登录。
- `register.html`:短信注册。
- `forgot-password.html`:短信重置密码。
- `profile-security.html`:改密码、改绑手机、注销账号。
- `profile-data.html` / `profile.html`:资料读取保存、行政区划;头像上传等待当前 PC 分片初始化响应 Schema 补齐。
### 家谱业务
- `profile-feed.html` / `profile-feed-edit.html`:家族圈列表、详情、发布/修改、点赞、评论、回复。
- `profile-generation.html` + `public/js/generation-pages.js`:字辈管理列表、新增、编辑、停用/恢复、批量预览/保存。所有写操作有防重复提交锁;批量示例要求用空格或支持的标点分隔。
- `profile-tree.html` + `public/js/lineage-pages.js`:成员总览、世系树、分页搜索/翻页、下拉选择、详情、新增/编辑、逻辑停用、父母/配偶/兄弟姐妹/子女新增。所有写操作有防重复提交锁;响应 int64 ID 只以安全字符串用于后续请求。
- `profile-growth.html` / `profile-growth-edit.html` + `public/js/growth-pages.js`:成长记录列表和新增;写入仅使用 `GrowthRecordBody`,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
- `profile-relative.html` / `profile-relative-edit.html` + `public/js/relative-pages.js`:亲友往来列表和新增;写入仅使用 `RelativeRecordBody`,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
- `profile-memo.html` / `profile-memo-edit.html` + `public/js/memo-pages.js`:备忘录列表和新增;写入仅使用已核验的备忘录请求体字段,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
### 关键代码文件
| 文件 | 责任 |
| 位置 | 用途 |
| --- | --- |
| `utils/ApiClient.js` | 已核验 PC 路径、请求头、DTO |
| `public/js/profile-common.js` | `genealogyId` 读取和链接传播的唯一 owner |
| `public/js/genealogy-entry-pages.js` | 无真实家谱上下文时的统一入口页;仅查询 PC 配额并阻止业务跳转 |
| `public/js/auth-pages.js``captcha-pages.js``security-pages.js` | 登录、注册、验证码、账号安全 |
| `public/js/profile-pages.js``region-pages.js``upload-pages.js` | 资料、区划、头像 |
| `public/js/feed-pages.js` | 家族圈 |
| `public/js/generation-pages.js` | 字辈谱 |
| `public/js/lineage-pages.js` | 世系人物 |
| `public/js/growth-pages.js` | 成长记录 |
| `public/js/relative-pages.js` | 亲友往来 |
| `public/js/memo-pages.js` | 备忘录 |
| 根目录 `*.html` | 各页面入口 |
| `public/css/` | 页面样式,`public.css` 是全局样式 |
| `public/js/` | 页面逻辑、公共交互和第三方脚本 |
| `public/images/` | 官网图片素材 |
| `utils/` | 请求、存储、表单和提示工具 |
| `config.js` | 前端接口配置 |
| `scripts/build-minified.mjs` | 构建脚本 |
| `dist/` | 构建产物;当前已清理,需要时重新构建 |
## 四、当前阻塞和未完成范围
## 已清理内容
### PC 家谱入口缺失
本次已删除测试文件、测试截图、临时接口响应、设计审查记录和旧文档。`dist/`、接口快照和 `package.json` 也已清理。仓库不再保留自动化测试,也没有 `npm test` 命令。
PC 目录没有“我的家谱、家谱详情、创建/加入/选择家谱”接口。前端无法自行得到真实 `genealogyId`,因此当前正确行为是:个人中心和内容入口先统一进入 `profile-families.html`,该页只读取 `genealogyQuota()` 并明确说明当前不能选择家谱;缺少 ID 时阻止业务请求。静态家谱卡片已移除,不要伪造编号,也不要用 APP 补这个缺口
后续不要在仓库中保存测试截图、测试账号信息、接口返回样本或重复的过程记录。需要说明验证结果时,写在当前提交或任务交付里即可
### 已核验但仍缺业务闭环的页面范围
## 请求和登录
| 分组 | 已核验条数 | 当前原则 |
| --- | ---: | --- |
| 内容文章 | 1 | 仅删除;没有真实列表/详情来源时不开放删除 |
| 相册 | 2 | 仅删除;不猜 DTO 或文件引用来源 |
| 视频 | 1 | 仅删除;不猜 DTO 或文件引用来源 |
| 祭祀 | 2 | 仅删除;不猜实体和献礼字段 |
| 功德记录 | 1 | 仅删除;不猜实体和 `meritId` 来源 |
接口地址和请求封装在 `utils/ApiClient.js``utils/AxiosRequestUtil.js``config.js`
## 五、通用行为和安全约束
页面不要直接发 Axios 请求,也不要在页面里拼接口地址。新增接口时先在 `ApiClient` 中统一定义,再由页面调用。
- `ApiClient` 统一加 `clientid`;登录后统一加 `Authorization: Bearer <token>`
- 401 清 token 跳转 `login.html`;403 保留登录态并展示无权限状态。
- 浏览器中的 int64 ID 均按字符串保存,避免精度丢失。
- 当前接口没有定义的删除、解绑、关系解除和业务文件引用值,保持禁用/待开发,不能猜测实现。
- 这是脏工作树;不要使用 `git reset --hard``git checkout --`,也不要删除未跟踪文件。
登录信息由 `StorageUtil.js` 管理。登录后的请求会携带 Bearer Token401 会清除登录态并跳转到 `login.html`403 保留登录态并显示无权限状态
## 六、验证结果
后端返回的长整型 ID 在浏览器里按字符串处理,避免数字精度丢失。
本轮已直接完成当前 PC 目录 85/85 条的详情复核,并逐项比对 `ApiClient`、对应页面和契约测试;历史 79 条记录不作为本轮验收依据。完整测试结果以本轮末次运行记录为准。
## 家谱业务规则
已运行的检查包括:
家谱业务必须使用真实的 `genealogyId`。没有家谱上下文时,应阻止请求或提示用户选择家谱,不能写死示例 ID。
- `node --check utils/ApiClient.js`
- `node --check public/js/profile-common.js`
- `node --check public/js/genealogy-entry-pages.js`
- `node --check public/js/generation-pages.js`
- `node --check public/js/lineage-pages.js`
- `node --check public/js/growth-pages.js`
- `node --check public/js/memo-pages.js`
- `node --check public/js/captcha-pages.js`
- `node --check public/js/auth-pages.js`
- `node --check public/js/security-pages.js`
- `node --test tests/api-client-contract.test.js tests/captcha-pages-contract.test.js tests/auth-pages.test.js tests/security-upload-scope.test.js tests/profile-pages.test.js`
- `node --test tests/auth-pages.test.js tests/api-client-contract.test.js tests/profile-pages.test.js tests/security-upload-scope.test.js tests/feed-pages.test.js tests/generation-pages.test.js tests/lineage-pages.test.js tests/growth-pages.test.js tests/relative-pages.test.js tests/memo-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
- `npm.cmd test`91/91 通过)
- `git diff --check`
接口字段、权限和接口是否存在,以 Apifox 的 PC 目录为准。不要从 APP 接口推测 PC 接口,也不要根据旧文件猜字段。
本地运行态检查已使用 Chrome DevTools 逐页打开 `profile-feed*`、成长记录、亲友往来、备忘录、消息、资料、字辈和世系页面:12 个静态页面均返回 200;无登录态时需鉴权的页面转到登录且没有发送业务请求,三类族务编辑页缺少真实 `genealogyId` 时保持阻止且不发请求。已使用测试账号完成密码登录的真实滑块验证与登录;资料页实际修改昵称后已恢复原值,页面两次均显示保存成功;消息页实际完成列表和未读数读取,服务端返回空列表。个人中心到家谱入口页的真实点击已验证,入口页仅调用配额接口且在缺少真实 `genealogyId` 时不进入家族业务。当前仍没有 PC 家谱列表/详情返回的真实家谱上下文,因此未对需 `genealogyId` 的写接口发送提交
接口还缺少列表、详情或删除能力时,页面应保持禁用或提示状态,不要补造数据把入口打开
当前完整测试集已运行并通过。
## 常用位置
## 七、下一位 GPT 的执行顺序
- 个人中心通用布局和导航:`public/js/profile-common.js`
- 个人中心样式:`public/css/profile.css``public/css/profile-module.css`
- 富文本编辑:`public/js/rich-editor.js``public/js/wangeditor5/`
- 附件上传:`public/js/attachment-editor.js``public/js/upload-pages.js`
- Layui 配置:`public/js/lay-config.js`
1. 读本文和 `docs/PC接口对接规划.md`
2. 先检查桌面版 Apifox PC 目录是否新增或变更;只逐条复核受影响接口,不要借用 APP 接口或猜 DTO。
3. 每次只处理一个完整小分组:Apifox 核验 → `ApiClient` → 对应页面 → 聚焦测试;家谱配额接口不是入口,继续不写死 `genealogyId`
4. 只有删除能力、没有真实列表/详情来源的模块,不开放危险操作,只登记后端缺口。
5. 每完成一组,更新本文、`PC接口对接规划.md` 和相关测试,并报告改动、验证和剩余阻塞
改公共样式或公共脚本前,先检查相关 `profile-*.html` 的引用,避免影响其他页面
## 接手一个功能时
1. 找到页面入口、对应脚本和已有接口调用
2. 在 Apifox 的 PC 目录确认请求方法、参数、响应字段和权限。
3. 修改请求封装和页面逻辑,保持加载、空数据、失败、无权限和禁用状态完整。
4. JavaScript 改动后执行 `node --check 路径/文件.js`,并在浏览器走通受影响页面。
5. 需要生成发布文件时执行 `node scripts/build-minified.mjs`。当前 `package.json` 已删除;如果后续重新引入 npm 脚本,需要同时恢复并维护对应配置。
6. 提交前执行 `git diff --check`,确认没有格式问题。
工作区可能有未提交修改。不要使用 `git reset --hard``git checkout --` 覆盖现有文件。
-99
View File
@@ -1,99 +0,0 @@
# 规划
## 项目目标
将“代代相传”建设为由官网、用户 PC 管理端、用户 APP 端和平台后台组成的家谱服务。四个端共享家谱、成员、内容和权限规则,但分别交付,不能用用户 PC 页面替代 APP 或平台后台。
## 当前基线
- 当前工作区共有 55 个 HTML 页面,现有页面之间没有指向不存在 HTML 页的站内链接。
- 本文保留项目级历史规划,不再维护接口数量和字段契约。当前 PC 接口基线、字段页面归属和 AI 执行顺序统一见 [PC接口对接规划.md](PC接口对接规划.md)。
- 已完成真实接口闭环:注册、密码登录、短信登录、找回密码、个人资料、安全设置、头像上传、地区选择、家族圈动态。
- 家谱列表、家谱创建/加入、成员、权限、世系、谱文、相册、视频、贺礼、成长、备忘、功德、消息、反馈等页面已保留设计,但尚无对应正式接口,不得伪造请求或静态数据为真实业务。
## 实施进度
- [x] 第一轮:家谱基础页面状态收口。已覆盖家谱列表、创建与加入、审核、家族管理、邀请、世系和字辈页面;未开发功能会显示统一提示并阻止伪造操作。
- [x] 第二轮:内容与协作页面状态收口。已覆盖除家族圈动态外的内容、记录、消息、反馈、管理员权限与资料提醒页面;家族圈动态保持真实功能并保留跳转入口。
- [x] 第三轮:剩余用户 PC 业务入口收口。已覆盖家谱主页、应用分享和帮助与会员服务页面;家族圈动态入口保持真实跳转。
- [x] 第四轮:官网静态页面补齐。已新增新闻、法律文档入口、通用状态和工单页面;在线工单功能明确处于开发中,法律正文必须在发布前由法务替换确认。
## 产品边界
### 官网
保留首页、数字家谱、家谱广场、姓氏百科、家族文化、应用下载、帮助中心、关于我们、搜索、公告和工单入口。
需要补齐的独立页面:
1. 新闻列表、新闻分类、新闻详情。
2. 隐私政策、儿童个人信息处理规则、服务协议。
3. 我的工单、工单详情。
4. 404、无权限、系统维护等通用状态页。
### 用户 PC 管理端
现有页面覆盖了思维导图中的大部分视觉入口:我的家谱、家谱主页、世系图、字辈、谱文、相册、视频、家族圈、贺礼、成长日志、备忘录、功德录、家族管理、管理员权限、入谱审核、消息、个人资料和安全设置。
需要补齐的关键页面:
1. 家族成员名册与成员详情,用于按成员筛选、查看资料、维护角色;世系图不能替代成员名册。
2. 家族圈动态详情,用于承载可分享链接、完整评论和通知跳转。
3. 无家谱首次引导、无权限、空列表等状态视图。
### 用户 APP 端
APP 是独立交付物,负责移动端查看家谱、上传照片/视频、发布内容、消息和个人中心。当前 PC 项目只需共享接口契约和视觉规范,不直接复制为 APP 页面。
### 平台后台
平台后台是独立项目,负责全站家谱审核、内容治理、会员/VIP、系统设置和运营数据。它不应通过隐藏按钮混入用户 PC 端。
## 后端接口开发顺序
### 第一阶段:家谱基础
1. 我的家谱列表、家谱详情、创建家谱、加入家谱、加入审核。
2. 家谱成员列表、成员详情、邀请、移除、角色与管理员权限。
3. 家谱入口必须返回真实 `genealogyId`,让世系、内容和家族圈能从同一上下文进入。
### 第二阶段:世系与资料
1. 世系树查询。
2. 成员新增、编辑、删除。
3. 父母、配偶、子女等关系维护。
4. 字辈列表与维护。
### 第三阶段:内容与协作
1. 谱文及分类。
2. 相册、照片、视频。
3. 贺礼、成长日志、备忘录、功德录。
4. 消息中心、点赞评论通知、生日提醒、入谱审核通知。
5. 意见反馈、工单列表与详情。
### 第四阶段:官网内容与合规
1. 新闻资讯、公告、公开内容检索。
2. 隐私、未成年人保护和服务协议。
3. 客服工单的提交、查询和回复。
## 契约要求
每个业务对象都必须提供明确 DTO,至少包括列表、详情和写入请求模型。禁止前端依赖泛型 `object` 或猜测字段名。
最优先需要后端补充的模型:
1. `GenealogyDTO``GenealogyMemberDTO``MemberRoleDTO`
2. `LineagePersonDTO``LineageRelationDTO``GenerationDTO`
3. `FeedDTO``CommentDTO`
4. `ArticleDTO``AlbumDTO``VideoDTO``CeremonyDTO``GrowthLogDTO``MemoDTO``MeritDTO`
5. `NotificationDTO``TicketDTO``NewsDTO`
## 交付规则
1. 有正式接口和 DTO 的页面,才接入真实读写功能。
2. 未开发接口对应的页面保留设计,但明确显示“功能开发中”或空状态,不伪造成功数据。
3. 家谱业务全部从真实 `genealogyId` 进入,不写死示例家谱编号。
4. 访客、普通成员、家谱管理员、平台管理员必须有独立权限边界。
5. 测试账号仅用于运行时验收;账号和密码不写入仓库、文档、测试、日志或配置。
+8 -9
View File
@@ -32,7 +32,7 @@
<section class="inner-hero download-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Download</div>
<div class="eyebrow">应用下载</div>
<h1>下载代代相传 APP</h1>
<p class="lead">
手机端适合日常上传宣传相册、发布宣传视频、查看家谱和补充资料。
@@ -43,9 +43,8 @@
</div>
</div>
<div class="hero-card qr-card tilt-card">
<div class="qr-box"><span>扫码</span></div>
<h3>手机扫码下载</h3>
<p>支持 iOS 与 Android,实际项目可替换为真实下载二维码。</p>
<h3>选择下载方式</h3>
<p>请在下方选择平台当前提供的可用下载入口。</p>
</div>
</div>
</section>
@@ -53,7 +52,7 @@
<div class="container">
<div class="section-head">
<h2>下载方式</h2>
<p>前台先展示下载入口,后续可以接真实应用商店地址</p>
<p>下载地址会根据平台当前配置显示;没有可用入口时会明确提示</p>
</div>
<div class="grid-3" data-promotion-download-list>
<div class="api-empty">下载推广内容加载中</div>
@@ -84,10 +83,10 @@
><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
+5 -5
View File
@@ -36,7 +36,7 @@
<section class="inner-hero detail-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Family Detail</div>
<div class="eyebrow">家谱详情</div>
<h1 data-family-text="title">四川武胜汤氏族</h1>
<p class="lead" data-family-text="intro|description|summary" data-fallback="公开展示家族源流、世系概览、字辈排行、谱文资料和宣传相册,让访客快速理解这一本家谱的内容结构。">
公开展示家族源流、世系概览、字辈排行、谱文资料和宣传相册,让访客快速理解这一本家谱的内容结构。
@@ -119,10 +119,10 @@
><a href="about.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
+1
View File
@@ -48,6 +48,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+22 -36
View File
@@ -18,46 +18,31 @@
class="card form-card auth-card recover-card"
aria-labelledby="recover-title"
>
<div class="eyebrow">Password Recovery</div>
<div class="eyebrow">找回密码</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>
+8 -7
View File
@@ -36,7 +36,7 @@
<section class="inner-hero genealogy-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Digital Genealogy</div>
<div class="eyebrow">数字家谱</div>
<h1>把成员关系整理成清晰可查的数字家谱</h1>
<p class="lead">
从始祖、分支、排行到个人资料,数字家谱帮助家族建立长期可维护的成员档案和世系关系。
@@ -132,17 +132,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -151,6 +151,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+17 -29
View File
@@ -36,13 +36,13 @@
<section class="inner-hero help-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Help Center</div>
<div class="eyebrow">帮助中心</div>
<h1>从创建到协作,快速解决常见问题</h1>
<p class="lead">
覆盖注册登录、创建家谱、邀请成员、资料审核、隐私设置和内容发布等流程
帮助内容由平台持续维护,覆盖账号、家谱和内容使用中的常见问题
</p>
<div class="help-tags">
<span>创建家谱</span><span>邀请成员</span><span>隐私权限</span>
<span>账号安全</span><span>家谱使用</span><span>内容管理</span>
</div>
</div>
<div class="hero-card help-card tilt-card">
@@ -55,6 +55,7 @@
<a class="btn primary magnetic" href="submit-ticket.html"
>提交工单</a
>
<a class="btn ghost magnetic" href="my-tickets.html">我的工单</a>
</div>
</div>
</section>
@@ -63,27 +64,12 @@
<div class="container">
<div class="section-head">
<h2>常见问题</h2>
<p>把复杂流程拆成清楚步骤,便于用户第一次使用时快速上手</p>
<p>选择问题后读取平台发布的完整解答</p>
<button class="btn ghost" type="button" data-help-refresh>刷新帮助</button>
</div>
<div class="faq-list" data-help-list>
<details open class="tilt-card">
<summary>如何创建第一本家谱?</summary>
<p>
进入创建家谱页面,填写家族名称、姓氏、地区和简介,即可生成家谱空间。
</p>
</details>
<details class="tilt-card">
<summary>如何邀请亲人一起维护?</summary>
<p>在家谱管理中生成邀请码,亲人可通过加入家谱页面提交申请。</p>
</details>
<details class="tilt-card">
<summary>公开家谱会展示哪些内容?</summary>
<p>仅展示管理员允许公开的家族介绍、动态和部分资料。</p>
</details>
<details class="tilt-card">
<summary>资料填错了怎么办?</summary>
<p>管理员可以编辑成员资料、调整世系关系,也可以设置审核流程。</p>
</details>
<div class="muted" data-help-status role="status" aria-live="polite"></div>
<div class="faq-list is-loading" data-help-list>
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取帮助文章…</div>
</div>
</div>
</section>
@@ -100,17 +86,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -125,5 +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>
+556
View File
@@ -0,0 +1,556 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>代代相传家谱_在线修家谱_家谱查询_姓氏寻根_祠堂祭祀</title>
<meta
name="keywords"
content="电子家谱,在线修家谱,家谱制作,家谱查询,中国家谱网,寻根,祠堂,祭祀,祭拜,中华传统文化,生命文化,儒释道,姓氏源流;敬祖睦族,忠孝两全,传承家风;族谱网,族记;清明节家谱,过年修家谱,海外寻根"
/>
<meta
name="description"
content="相传相传家谱网,大型生命文化平台,综合儒释道思想,弘扬中华传统文化,提供电子家谱_在线修家谱_网上修家谱,家谱制作_族谱制作_修谱软件,家谱查询_姓氏寻根_寻根问祖_姓氏源流,祠堂祭祀_祭拜_祭奠,弘扬孝敬_忠孝两全_相夫教子_敬祖睦族_慎终追远_传承家风,覆盖清明节家谱_过年修家谱_90后修家谱_海外华人寻根_老家谱电子化,代代相传_薪火相传"
/>
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/css/index.css" />
</head>
<body class="page-home">
<div class="company-bar">
<div class="container">富众鑫科技</div>
</div>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.html"
><img
class="brand-logo"
src="public/images/logo.png"
alt="我们的家谱,代代相传"
/></a>
<nav class="nav-links">
<a class="active" href="index.html">首页</a>
<a href="genealogy.html">数字家谱</a>
<a href="plaza.html">家谱广场</a>
<a href="culture.html">家族文化</a>
<a href="surname.html">姓氏百科</a>
<a href="app.html">应用下载</a>
<a href="help.html">帮助中心</a>
<a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a href="login.html">登录</a>
<a href="register.html">注册</a>
<a class="btn primary magnetic" href="create-genealogy.html"
>创建家谱</a
>
</div>
</div>
</header>
<main>
<div class="hero">
<div class="container hero-inner">
<div class="hero-copy">
<div class="eyebrow">数字家谱与家族文化门户</div>
<h1>
一部会生长的<br /><span class="hero-title-mark">数字家谱</span>
</h1>
<p class="lead">
把姓氏源流、世系关系、字辈排行、宣传相册、谱文故事与亲人动态整理在一起,让家族的来处清晰可见,让记忆可以继续传下去。
</p>
<div class="word-scroll-card">
<div class="word-scroll-label">正在记录</div>
<div class="word-window">
<div class="word-track">
<div class="word">寻根问祖</div>
<div class="word">字辈排行</div>
<div class="word">世系关系</div>
<div class="word">宣传相册</div>
<div class="word">谱文故事</div>
<div class="word">亲人动态</div>
<div class="word">寻根问祖</div>
<div class="word">字辈排行</div>
<div class="word">世系关系</div>
<div class="word">宣传相册</div>
<div class="word">谱文故事</div>
<div class="word">亲人动态</div>
</div>
</div>
</div>
<div class="hero-path">
<span>1</span>创建家谱<i></i><span>2</span>邀请亲人<i></i
><span>3</span>共同传承
</div>
<div class="hero-action-card">
<div class="cta">
<a class="btn primary magnetic" href="create-genealogy.html"
>立即创建家谱</a
>
<a class="btn ghost magnetic" href="plaza.html">查看家谱示例</a>
</div>
</div>
<div class="metrics">
<div class="metric"><strong>家谱广场</strong>浏览公开家族</div>
<div class="metric"><strong>姓氏百科</strong>了解姓氏源流</div>
<div class="metric"><strong>家族文化</strong>阅读传承故事</div>
</div>
</div>
<div class="hero-visual">
<div class="archive-stage"></div>
<div class="book-cover"></div>
<div class="hall">
<img src="public/images/ancestral-hall.png" alt="家族祠堂" />
</div>
<a class="video-card" href="promo-video.html">
<small>宣传影像</small>
<span>观看宣传视频</span>
</a>
<div class="search-panel">
<h3>查找公开家谱</h3>
<a class="search-box" href="search-result.html">
<span>输入姓氏、地区或家谱名称</span><b>搜索</b>
</a>
<div class="genealogy-list">
<div class="genealogy-item">
<strong>四川成都陈氏族</strong>共修入 128 人
</div>
<div class="genealogy-item">
<strong>湖广刘氏家谱</strong>收录 6 代字辈
</div>
<div class="genealogy-item">
<strong>岭南张氏宗谱</strong>开放谱文 18 篇
</div>
</div>
</div>
</div>
</div>
</div>
<div class="quick-strip">
<div class="container quick-inner">
<a class="quick-card tilt-card" href="create-genealogy.html">
<b>创建家谱</b><span>填写姓氏谱名,建立属于家族的数字空间。</span>
</a>
<a class="quick-card tilt-card" href="join-genealogy.html">
<b>加入家谱</b><span>通过邀请码加入亲人创建的家谱。</span>
</a>
<a class="quick-card tilt-card" href="plaza.html">
<b>浏览广场</b><span>查看公开家谱示例与家族故事。</span>
</a>
<a
class="quick-card tilt-card"
id="promo-video"
href="promo-video.html"
><b>宣传视频</b
><span>进入视频列表,查看宣传影像和纪念视频。</span></a
>
</div>
</div>
<section class="functions">
<div class="container">
<div class="section-head">
<h2>数字家谱核心功能</h2>
</div>
<div class="function-grid">
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>世系图</h3>
<p>
以树谱方式展示成员关系,支持查看个人资料、添加亲属、调整排行。
</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>字辈谱</h3>
<p>记录各代字辈与人数,让家族世代脉络更加清楚。</p>
</div>
<a class="function-card tilt-card" href="promo-album.html">
<div class="icon"></div>
<h3>宣传相册</h3>
<p>按相册归档宣传影像,展示祖屋、活动、老照片和重要时刻。</p>
</a>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>谱文资料</h3>
<p>沉淀谱序、谱文、家训、恩荣录等文本内容。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>家族圈</h3>
<p>发布动态、分享图片、评论互动,让亲人之间保持连接。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>功德录</h3>
<p>记录家族贡献人物与事迹,让善行与纪念被看见。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>贺礼邀请</h3>
<p>发布婚礼、生日、升学等家族喜事,通知相关成员。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>权限管理</h3>
<p>设置管理员、内容可见范围与入谱审核,保护家族隐私。</p>
</div>
</div>
</div>
</section>
<section class="plaza">
<div class="container">
<div class="section-head">
<h2>家谱广场</h2>
</div>
<div class="plaza-layout">
<a class="featured-family tilt-card" href="family-detail.html">
<h3>四川成都陈氏族</h3>
<p>
已收录成员 128 人,整理字辈 6 代,公开谱文 18 篇,宣传相册 7
组。家族资料由成员共同维护,持续更新。
</p>
</a>
<div class="family-cards">
<a class="family-card tilt-card" href="family-detail.html">
<h4>湖广刘氏家谱</h4>
<p>从始祖世系到现代家庭分支,保留字辈与迁徙记录。</p>
<div class="tag-row">
<span class="tag">公开家谱</span
><span class="tag">字辈完整</span>
</div>
</a>
<a class="family-card tilt-card" href="family-detail.html">
<h4>岭南张氏宗谱</h4>
<p>以谱文、相册、家族动态为主,记录家风与重要纪念。</p>
<div class="tag-row">
<span class="tag">家族文化</span
><span class="tag">宣传相册</span>
</div>
</a>
<a class="family-card tilt-card" href="family-detail.html">
<h4>川东李氏家谱</h4>
<p>多人共同完善成员资料,形成清晰的家族成员档案。</p>
<div class="tag-row">
<span class="tag">多人共修</span
><span class="tag">成员资料</span>
</div>
</a>
</div>
</div>
</div>
</section>
<section class="culture">
<div class="container">
<div class="section-head">
<h2>家族文化</h2>
</div>
<div class="article-grid">
<a class="article-card tilt-card" href="article-detail.html">
<div class="article-cover"></div>
<div class="article-body">
<h3>为什么家谱不只是一本名册</h3>
<p>
家谱记录的是家族关系,也记录一个家庭如何理解自己的来处与传承。
</p>
<div class="article-meta">家谱知识 · 2026.05</div>
</div>
</a>
<a class="article-card tilt-card" href="article-detail.html">
<div class="article-cover"></div>
<div class="article-body">
<h3>字辈在家族传承中的作用</h3>
<p>字辈让不同世代的人能找到自己的位置,也让同族称谓更清晰。</p>
<div class="article-meta">姓氏文化 · 2026.05</div>
</div>
</a>
<a class="article-card tilt-card" href="article-detail.html">
<div class="article-cover"></div>
<div class="article-body">
<h3>如何邀请亲人共同完善家谱</h3>
<p>从创建家谱、生成邀请码到设置管理员,逐步建立协作流程。</p>
<div class="article-meta">使用指南 · 2026.05</div>
</div>
</a>
</div>
</div>
</section>
<section class="ad-slots">
<div class="container">
<div class="ad-grid">
<a
class="ad-card tilt-card"
href="https://3dyjs.cn/"
target="_blank"
rel="noopener"
aria-label="3D研究室广告"
>
<img src="public/images/mlogo.png" alt="3D研究室" />
</a>
<a
class="ad-card tilt-card"
href="promo-album.html"
aria-label="影像相册广告"
>
<img src="public/images/ad-album-small.png" alt="影像相册" />
</a>
<a
class="ad-card tilt-card"
href="promo-video.html"
aria-label="活动视频广告"
>
<img src="public/images/ad-video-small.png" alt="活动视频" />
</a>
<a
class="ad-card tilt-card"
href="genealogy.html"
aria-label="族谱印制广告"
>
<img src="public/images/ad-print.png" alt="族谱印制" />
</a>
<a
class="ad-card tilt-card"
href="culture.html"
aria-label="祠堂文化广告"
>
<img src="public/images/ad-hall.png" alt="祠堂文化" />
</a>
<a
class="ad-card tilt-card"
href="app.html"
aria-label="云端存档广告"
>
<img src="public/images/ad-cloud.png" alt="云端存档" />
</a>
<a
class="ad-card tilt-card"
href="culture.html"
aria-label="宗亲联谊广告"
>
<img src="public/images/ad-reunion.png" alt="宗亲联谊" />
</a>
<a
class="ad-card tilt-card"
href="surname.html"
aria-label="姓氏研究广告"
>
<img src="public/images/ad-research.png" alt="姓氏研究" />
</a>
<a
class="ad-card tilt-card"
href="culture.html"
aria-label="家风记忆广告"
>
<img src="public/images/ad-memory.png" alt="家风记忆" />
</a>
<a
class="ad-card tilt-card"
href="help.html"
aria-label="资料校对广告"
>
<img src="public/images/ad-data.png" alt="资料校对" />
</a>
</div>
</div>
</section>
<section class="surname">
<div class="container">
<div class="surname-panel tilt-card">
<div class="surname-intro">
<h2>姓氏百科</h2>
<p>
以姓氏为入口,整理姓氏源流、名人故事、地区分布、字辈参考和相关家谱。
</p>
<a class="btn primary magnetic" href="surname.html"
>进入姓氏百科</a
>
</div>
<div class="surname-grid">
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
</div>
</div>
</div>
</section>
<section class="recommend">
<div class="container">
<div class="section-head">
<h2>相关推荐</h2>
</div>
<div class="recommend-grid">
<a
class="recommend-card tilt-card"
href="article-detail.html"
>
<span>资料整理</span>
<h3>家庭档案整理指南</h3>
<p>参考照片、证件、谱牒、口述资料的归档方式。</p>
</a>
<a
class="recommend-card tilt-card"
href="surname.html"
>
<span>姓氏源流</span>
<h3>姓氏文化资料库</h3>
<p>查看姓氏源流、迁徙脉络、堂号和郡望参考资料。</p>
</a>
<a
class="recommend-card tilt-card"
href="culture.html"
>
<span>宗亲文化</span>
<h3>宗亲会活动参考</h3>
<p>了解宗亲联谊、祭祖活动和家风传播的组织方式。</p>
</a>
<a
class="recommend-card tilt-card"
href="help.html"
>
<span>工具服务</span>
<h3>修谱工具与服务</h3>
<p>查找谱书排版、影像扫描、云端存储等外部服务。</p>
</a>
<a
class="recommend-card tilt-card"
href="culture.html"
>
<span>地方志</span>
<h3>地方文献检索入口</h3>
<p>从地方志、县志和族群资料中寻找家族线索。</p>
</a>
<a
class="recommend-card tilt-card"
href="surname.html"
>
<span>人名研究</span>
<h3>字辈与命名参考</h3>
<p>参考传统字辈、排行、称谓和家族命名资料。</p>
</a>
<a
class="recommend-card tilt-card"
href="promo-album.html"
>
<span>影像修复</span>
<h3>老照片修复服务</h3>
<p>了解老照片扫描、修复、上色和数字归档服务。</p>
</a>
<a
class="recommend-card tilt-card"
href="article-detail.html"
>
<span>口述史</span>
<h3>家族故事采集方法</h3>
<p>参考长辈访谈、口述记录和家风故事整理方式。</p>
</a>
</div>
</div>
</section>
<section class="friend-links">
<div class="container">
<div class="friend-links-panel">
<h2>常用入口</h2>
<div class="friend-link-list">
<a href="https://3d.3dyjs.cn/" target="_blank" rel="noopener"
>3D研究室</a
>
<a href="surname.html">姓氏源流资料</a>
<a href="plaza.html">公开家谱资料</a>
<a href="article-detail.html">修谱知识库</a>
<a href="culture.html">地方文献</a>
<a href="promo-album.html">家族影像</a>
<a href="genealogy.html">数字家谱</a>
<a href="help.html">使用帮助</a>
</div>
</div>
</div>
</section>
<section class="download">
<div class="container">
<div class="download-panel tilt-card">
<div>
<h2>手机端随时记录,电脑端清晰浏览</h2>
<p>
家族成员可以在手机端上传照片、查看家谱、发布动态;电脑端适合浏览门户内容、查看公开家谱和进入家谱空间。
</p>
<div class="cta">
<a class="btn primary magnetic" href="download.html">下载应用</a
><a class="btn ghost magnetic" href="help.html">查看帮助</a>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div>
<a class="brand magnetic" href="index.html"
><img
class="brand-logo"
src="public/images/logo-light.png"
alt="我们的家谱,代代相传"
/></a>
<p>一个面向家庭和宗族的数字家谱与家族文化门户。</p>
</div>
<div>
<h4>产品入口</h4>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a
><a href="news.html">新闻资讯</a>
</div>
<div>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
<div>© 2026 代代相传. All rights reserved.</div>
<a href="privacy.html">隐私政策</a>
<a href="children-privacy.html">儿童个人信息处理规则</a>
<a href="terms.html">服务协议</a>
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener"
>ICP备案:蜀ICP备2024044594号-1</a
>
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+184 -544
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -15,7 +15,12 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/css/index.css" />
</head>
<body>
<body class="page-home page-home-portal">
<div class="company-bar">
<div class="container company-bar-inner">
<strong>富众鑫文化科技</strong>
</div>
</div>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.html"
@@ -24,19 +29,17 @@
src="public/images/logo.png"
alt="我们的家谱,代代相传"
/></a>
<nav class="nav-links">
<a class="active" href="index.html">首页</a>
<nav class="nav-links" aria-label="主导航">
<a class="active" href="index.html" aria-current="page">首页</a>
<a href="genealogy.html">数字家谱</a>
<a href="plaza.html">家谱广场</a>
<a href="culture.html">家族文化</a>
<a href="surname.html">姓氏百科</a>
<a href="app.html">应用下载</a>
<a href="help.html">帮助中心</a>
<a href="about.html">关于我们</a>
<a href="app.html">APP下载</a>
<a href="help.html">在线帮助</a>
<button type="button" data-add-favorite>收藏</button>
</nav>
<div class="nav-actions">
<a href="login.html">登录</a>
<a href="register.html">注册</a>
<a href="login.html">登录</a>
<a class="btn primary magnetic" href="create-genealogy.html"
>创建家谱</a
>
@@ -45,584 +48,221 @@
</header>
<main>
<div class="hero">
<div class="container hero-inner">
<div class="hero-copy">
<div class="eyebrow">数字家谱与家族文化门户</div>
<h1>
一部会生长的<br /><span class="hero-title-mark">数字家谱</span>
</h1>
<section class="portal-overview" aria-labelledby="home-title">
<div class="container portal-overview-grid">
<div class="hero-copy portal-copy">
<div class="portal-copy-heading">
<div>
<div class="eyebrow">数字家谱与家族文化门户</div>
<h1 id="home-title">一部会生长的<br /><span class="hero-title-mark">数字家谱</span></h1>
</div>
<a class="portal-qr-card" href="download.html" aria-label="进入应用下载页面">
<strong>扫码下载</strong>
<span id="home-app-qr" class="portal-qr" aria-hidden="true"></span>
<small>手机端随时记录</small>
</a>
</div>
<p class="lead">
把姓氏源流、世系关系、字辈排行、宣传相册、谱文故事与亲人动态整理在一起,让家族的来处清晰可见,让记忆可以继续传下去。
</p>
<div class="word-scroll-card">
<div class="word-scroll-label">正在记录</div>
<div class="word-scroll-card portal-word-scroll" aria-label="家谱内容范围">
<button class="word-scroll-label" type="button" data-toggle-word-scroll aria-label="暂停词条滚动" aria-pressed="false" title="点击暂停或继续滚动">正在记录</button>
<div class="word-window">
<div class="word-track">
<div class="word">寻根问祖</div>
<div class="word">字辈排行</div>
<div class="word">世系关系</div>
<div class="word">宣传相册</div>
<div class="word">谱文故事</div>
<div class="word">亲人动态</div>
<div class="word">寻根问祖</div>
<div class="word">字辈排行</div>
<div class="word">世系关系</div>
<div class="word">宣传相册</div>
<div class="word">谱文故事</div>
<div class="word">亲人动态</div>
<div class="portal-word-group">
<span class="word">寻根问祖</span><span class="word">字辈排行</span><span class="word">世系关系</span><span class="word">宣传相册</span><span class="word">谱文故事</span><span class="word">亲人动态</span>
</div>
<div class="portal-word-group" aria-hidden="true">
<span class="word">寻根问祖</span><span class="word">字辈排行</span><span class="word">世系关系</span><span class="word">宣传相册</span><span class="word">谱文故事</span><span class="word">亲人动态</span>
</div>
</div>
</div>
</div>
<div class="hero-path">
<span>1</span>创建家谱<i></i><span>2</span>邀请亲人<i></i
><span>3</span>共同传承
</div>
<div class="hero-action-card">
<div class="cta">
<a class="btn primary magnetic" href="create-genealogy.html"
>立即创建家谱</a
>
<a class="btn ghost magnetic" href="plaza.html">查看家谱示例</a>
</div>
</div>
<div class="metrics">
<div class="metric"><strong>家谱广场</strong>浏览公开家族</div>
<div class="metric"><strong>姓氏百科</strong>了解姓氏源流</div>
<div class="metric"><strong>家族文化</strong>阅读传承故事</div>
</div>
<ol class="hero-path" aria-label="修谱步骤">
<li><b>1</b>创建家谱</li>
<li><b>2</b>邀请亲人</li>
<li><b>3</b>共同传承</li>
</ol>
</div>
<div class="hero-visual">
<div class="archive-stage"></div>
<div class="book-cover"></div>
<div class="hall">
<img src="public/images/ancestral-hall.png" alt="家族祠堂" />
</div>
<a class="video-card" href="promo-video.html">
<small>宣传影像</small>
<span>观看宣传视频</span>
</a>
<div class="search-panel">
<h3>查找公开家谱</h3>
<a class="search-box" href="search-result.html">
<span>输入姓氏、地区或家谱名称</span><b>搜索</b>
</a>
<div class="genealogy-list">
<div class="genealogy-item">
<strong>四川成都陈氏族</strong>共修入 128 人
</div>
<div class="genealogy-item">
<strong>湖广刘氏家谱</strong>收录 6 代字辈
</div>
<div class="genealogy-item">
<strong>岭南张氏宗谱</strong>开放谱文 18 篇
</div>
</div>
</div>
<div class="portal-gallery" aria-label="家谱与家族影像">
<div class="book-cover portal-gallery-book" role="img" aria-label="张氏家谱封面"></div>
<img src="public/images/ancestral-hall.png" alt="家族祠堂" />
<img src="public/images/family-memory-cover.jpg" alt="家族照片与记忆档案" />
</div>
</div>
</div>
</section>
<div class="quick-strip">
<div class="container quick-inner">
<a class="quick-card tilt-card" href="create-genealogy.html">
<b>创建家谱</b><span>填写姓氏谱名,建立属于家族数字空间。</span>
<section class="portal-entry-section" aria-label="主要服务入口">
<div class="container portal-entry-grid">
<a class="portal-entry portal-service-entry tilt-card" href="create-genealogy.html" aria-label="修谱服务,创建家谱">
<img src="public/images/ad-genealogy-service.png" alt="家族数字化修谱服务" />
</a>
<a class="quick-card tilt-card" href="join-genealogy.html">
<b>加入家谱</b><span>通过邀请码加入亲人创建的家谱。</span>
<a class="portal-entry portal-family-entry tilt-card" href="profile-families.html">
<span>进入管理</span>
<strong>我的家谱</strong>
<small>查看、新建和管理全部家谱</small>
</a>
<a class="quick-card tilt-card" href="plaza.html">
<b>浏览广场</b><span>查看公开家谱示例与家族故事。</span>
<a class="video-card portal-entry portal-media-entry tilt-card" href="promo-video.html" aria-label="查看宣传视频介绍">
<small>宣传影像</small>
<span>观看宣传视频</span>
</a>
<a class="video-card portal-entry portal-media-entry tilt-card" href="promo-video.html" aria-label="查看宣传视频介绍">
<small>宣传影像</small>
<span>观看宣传视频</span>
</a>
<a
class="quick-card tilt-card"
id="promo-video"
href="promo-video.html"
><b>宣传视频</b
><span>进入视频列表,查看宣传影像和纪念视频。</span></a
>
</div>
</div>
<section class="functions">
<div class="container">
<div class="section-head">
<h2>数字家谱核心功能</h2>
</div>
<div class="function-grid">
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>世系图</h3>
<p>
以树谱方式展示成员关系,支持查看个人资料、添加亲属、调整排行。
</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>字辈谱</h3>
<p>记录各代字辈与人数,让家族世代脉络更加清楚。</p>
</div>
<a class="function-card tilt-card" href="promo-album.html">
<div class="icon"></div>
<h3>宣传相册</h3>
<p>按相册归档宣传影像,展示祖屋、活动、老照片和重要时刻。</p>
</a>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>谱文资料</h3>
<p>沉淀谱序、谱文、家训、恩荣录等文本内容。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>家族圈</h3>
<p>发布动态、分享图片、评论互动,让亲人之间保持连接。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>功德录</h3>
<p>记录家族贡献人物与事迹,让善行与纪念被看见。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>贺礼邀请</h3>
<p>发布婚礼、生日、升学等家族喜事,通知相关成员。</p>
</div>
<div class="function-card tilt-card">
<div class="icon"></div>
<h3>权限管理</h3>
<p>设置管理员、内容可见范围与入谱审核,保护家族隐私。</p>
</div>
</div>
</div>
</section>
<section class="plaza">
<div class="container">
<div class="section-head">
<h2>家谱广场</h2>
</div>
<div class="plaza-layout">
<a class="featured-family tilt-card" href="family-detail.html">
<h3>四川成都陈氏族</h3>
<p>
已收录成员 128 人,整理字辈 6 代,公开谱文 18 篇,宣传相册 7
组。家族资料由成员共同维护,持续更新。
</p>
</a>
<div class="family-cards">
<a class="family-card tilt-card" href="family-detail.html">
<h4>湖广刘氏家谱</h4>
<p>从始祖世系到现代家庭分支,保留字辈与迁徙记录。</p>
<div class="tag-row">
<span class="tag">公开家谱</span
><span class="tag">字辈完整</span>
</div>
</a>
<a class="family-card tilt-card" href="family-detail.html">
<h4>岭南张氏宗谱</h4>
<p>以谱文、相册、家族动态为主,记录家风与重要纪念。</p>
<div class="tag-row">
<span class="tag">家族文化</span
><span class="tag">宣传相册</span>
</div>
</a>
<a class="family-card tilt-card" href="family-detail.html">
<h4>川东李氏家谱</h4>
<p>多人共同完善成员资料,形成清晰的家族成员档案。</p>
<div class="tag-row">
<span class="tag">多人共修</span
><span class="tag">成员资料</span>
</div>
</a>
</div>
</div>
<section class="portal-ad-strip" aria-label="第一组广告栏">
<div class="container portal-ad-grid">
<a class="tilt-card" href="genealogy.html"><img src="public/images/ad-service.png" alt="家谱服务" /></a>
<a class="tilt-card" href="promo-album.html"><img src="public/images/ad-album-small.png" alt="影像相册" /></a>
<a class="tilt-card" href="promo-video.html"><img src="public/images/ad-video-small.png" alt="活动视频" /></a>
<a class="tilt-card" href="genealogy.html"><img src="public/images/ad-print.png" alt="族谱印制" /></a>
<a class="tilt-card" href="culture.html"><img src="public/images/ad-hall.png" alt="祠堂文化" /></a>
<a class="tilt-card" href="app.html"><img src="public/images/ad-cloud.png" alt="云端存档" /></a>
<a class="tilt-card" href="culture.html"><img src="public/images/ad-reunion.png" alt="宗亲联谊" /></a>
<a class="tilt-card" href="surname.html"><img src="public/images/ad-research.png" alt="姓氏研究" /></a>
<a class="tilt-card" href="article-detail.html"><img src="public/images/ad-memory.png" alt="家风记忆" /></a>
</div>
</section>
<section class="culture">
<section class="portal-directory" aria-label="家族文化与资讯">
<div class="container">
<div class="section-head">
<h2>家族文化</h2>
</div>
<div class="article-grid">
<a class="article-card tilt-card" href="article-detail.html">
<div class="article-cover"></div>
<div class="article-body">
<h3>为什么家谱不只是一本名册</h3>
<p>
家谱记录的是家族关系,也记录一个家庭如何理解自己的来处与传承。
</p>
<div class="article-meta">家谱知识 · 2026.05</div>
</div>
<div class="portal-directory-grid">
<a class="function-card portal-directory-card tilt-card" href="genealogy.html">
<span class="icon" aria-hidden="true"></span>
<h3>家族知识</h3>
<p>了解谱牒方式和成员关系,查看资料、添加亲属并完善家谱。</p>
</a>
<a class="article-card tilt-card" href="article-detail.html">
<div class="article-cover"></div>
<div class="article-body">
<h3>字辈在家族传承中的作用</h3>
<p>字辈让不同世代的人能找到自己的位置,也让同族称谓更清晰。</p>
<div class="article-meta">姓氏文化 · 2026.05</div>
</div>
<a class="function-card portal-directory-card tilt-card" href="culture.html">
<span class="icon" aria-hidden="true"></span>
<h3>宗亲文化</h3>
<p>了解宗亲礼俗、家风家训与传承故事,延续家族文化。</p>
</a>
<a class="article-card tilt-card" href="article-detail.html">
<div class="article-cover"></div>
<div class="article-body">
<h3>如何邀请亲人共同完善家谱</h3>
<p>从创建家谱、生成邀请码到设置管理员,逐步建立协作流程。</p>
<div class="article-meta">使用指南 · 2026.05</div>
</div>
<a class="function-card portal-directory-card tilt-card" href="notice-detail.html">
<span class="icon" aria-hidden="true"></span>
<h3>网站公告</h3>
<p>集中查看平台通知、服务调整与家谱相关公告。</p>
</a>
<a class="function-card portal-directory-card tilt-card" href="news.html">
<span class="icon" aria-hidden="true"></span>
<h3>最近新闻</h3>
<p>查看家族动态、平台资讯和近期发布的重要消息。</p>
</a>
<a class="function-card portal-directory-card tilt-card" href="surname.html">
<span class="icon" aria-hidden="true"></span>
<h3>姓氏源流</h3>
<p>追溯姓氏来处、迁徙脉络、堂号与郡望资料。</p>
</a>
<a class="function-card portal-directory-card tilt-card" href="culture.html">
<span class="icon" aria-hidden="true"></span>
<h3>网站活动</h3>
<p>查看婚礼、生日、升学等家族喜事和宗亲活动。</p>
</a>
</div>
</div>
</section>
<section class="ad-slots">
<div class="container">
<div class="ad-grid">
<a
class="ad-card tilt-card"
href="https://3dyjs.cn/"
target="_blank"
rel="noopener"
aria-label="3D研究室广告"
>
<img src="public/images/mlogo.png" alt="3D研究室" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/family-album"
target="_blank"
rel="noopener"
aria-label="影像相册广告"
>
<img src="public/images/ad-album-small.png" alt="影像相册" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/family-video"
target="_blank"
rel="noopener"
aria-label="活动视频广告"
>
<img src="public/images/ad-video-small.png" alt="活动视频" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/genealogy-print"
target="_blank"
rel="noopener"
aria-label="族谱印制广告"
>
<img src="public/images/ad-print.png" alt="族谱印制" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/ancestral-hall"
target="_blank"
rel="noopener"
aria-label="祠堂文化广告"
>
<img src="public/images/ad-hall.png" alt="祠堂文化" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/cloud-archive"
target="_blank"
rel="noopener"
aria-label="云端存档广告"
>
<img src="public/images/ad-cloud.png" alt="云端存档" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/clan-reunion"
target="_blank"
rel="noopener"
aria-label="宗亲联谊广告"
>
<img src="public/images/ad-reunion.png" alt="宗亲联谊" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/surname-research"
target="_blank"
rel="noopener"
aria-label="姓氏研究广告"
>
<img src="public/images/ad-research.png" alt="姓氏研究" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/family-memory"
target="_blank"
rel="noopener"
aria-label="家风记忆广告"
>
<img src="public/images/ad-memory.png" alt="家风记忆" />
</a>
<a
class="ad-card tilt-card"
href="https://example.com/data-check"
target="_blank"
rel="noopener"
aria-label="资料校对广告"
>
<img src="public/images/ad-data.png" alt="资料校对" />
</a>
</div>
</div>
</section>
<section class="surname">
<div class="container">
<div class="surname-panel tilt-card">
<div class="surname-intro">
<h2>姓氏百科</h2>
<p>
以姓氏为入口,整理姓氏源流、名人故事、地区分布、字辈参考和相关家谱。
</p>
<a class="btn primary magnetic" href="surname.html"
>进入姓氏百科</a
>
</div>
<div class="surname-grid">
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<a class="surname-name" href="surname-detail.html"></a>
<section class="portal-resources" id="site-map" aria-label="推荐与友情链接">
<div class="container portal-resource-grid">
<div class="portal-resource-group">
<h2>其他推荐</h2>
<div class="portal-resource-list">
<a href="news.html">新闻资讯</a>
<a href="culture.html">信息公开</a>
<a href="surname.html">地方动态</a>
<a href="article-detail.html">专题报道</a>
<a href="promo-album.html">家族影像</a>
<a href="promo-video.html">视频内容</a>
</div>
</div>
</div>
</section>
<section class="recommend">
<div class="container">
<div class="section-head">
<h2>相关推荐</h2>
</div>
<div class="recommend-grid">
<a
class="recommend-card tilt-card"
href="https://example.com/archive-guide"
target="_blank"
rel="noopener"
>
<span>资料整理</span>
<h3>家庭档案整理指南</h3>
<p>参考照片、证件、谱牒、口述资料的归档方式。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/surname-origin"
target="_blank"
rel="noopener"
>
<span>姓氏源流</span>
<h3>姓氏文化资料库</h3>
<p>查看姓氏源流、迁徙脉络、堂号和郡望参考资料。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/clan-culture"
target="_blank"
rel="noopener"
>
<span>宗亲文化</span>
<h3>宗亲会活动参考</h3>
<p>了解宗亲联谊、祭祖活动和家风传播的组织方式。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/genealogy-tools"
target="_blank"
rel="noopener"
>
<span>工具服务</span>
<h3>修谱工具与服务</h3>
<p>查找谱书排版、影像扫描、云端存储等外部服务。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/local-history"
target="_blank"
rel="noopener"
>
<span>地方志</span>
<h3>地方文献检索入口</h3>
<p>从地方志、县志和族群资料中寻找家族线索。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/name-study"
target="_blank"
rel="noopener"
>
<span>人名研究</span>
<h3>字辈与命名参考</h3>
<p>参考传统字辈、排行、称谓和家族命名资料。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/photo-restore"
target="_blank"
rel="noopener"
>
<span>影像修复</span>
<h3>老照片修复服务</h3>
<p>了解老照片扫描、修复、上色和数字归档服务。</p>
</a>
<a
class="recommend-card tilt-card"
href="https://example.com/oral-history"
target="_blank"
rel="noopener"
>
<span>口述史</span>
<h3>家族故事采集方法</h3>
<p>参考长辈访谈、口述记录和家风故事整理方式。</p>
</a>
</div>
</div>
</section>
<section class="friend-links">
<div class="container">
<div class="friend-links-panel">
<div class="portal-resource-group">
<h2>友情链接</h2>
<div class="friend-link-list">
<a href="https://3d.3dyjs.cn/" target="_blank" rel="noopener"
>3D研究室</a
>
<a
href="https://example.com/surnames"
target="_blank"
rel="noopener"
>姓氏源流资料</a
>
<a
href="https://example.com/genealogy"
target="_blank"
rel="noopener"
>公开家谱资料</a
>
<a href="https://example.com/wiki" target="_blank" rel="noopener"
>修谱知识库</a
>
<a
href="https://example.com/history"
target="_blank"
rel="noopener"
>地方文献</a
>
<a
href="https://example.com/archive"
target="_blank"
rel="noopener"
>档案服务</a
>
<a
href="https://example.com/printing"
target="_blank"
rel="noopener"
>谱书印制</a
>
<a href="https://example.com/photo" target="_blank" rel="noopener"
>老照片修复</a
>
<div class="portal-resource-list">
<a href="genealogy.html">代代相传家谱</a>
<a href="plaza.html">家谱广场</a>
<a href="surname.html">姓氏百科</a>
<a href="culture.html">家族文化</a>
<a href="help.html">在线帮助</a>
</div>
</div>
</div>
</section>
<section class="download">
<div class="container">
<div class="download-panel tilt-card">
<div>
<h2>手机端随时记录,电脑端清晰浏览</h2>
<p>
家族成员可以在手机端上传照片、查看家谱、发布动态;PC
端适合浏览门户内容、查看公开家谱和进入家谱空间。
</p>
<div class="cta">
<a class="btn primary magnetic" href="download.html">下载应用</a
><a class="btn ghost magnetic" href="help.html">查看帮助</a>
</div>
</div>
<div class="qr-wrap">
<div class="qr">
<div class="qr-box"></div>
<strong>小程序</strong>
<p class="qr-note">扫码体验</p>
</div>
<div class="qr">
<div class="qr-box"></div>
<strong>APP 下载</strong>
<p class="qr-note">手机查看</p>
</div>
</div>
</div>
<section class="portal-ad-strip portal-ad-strip-bottom" aria-label="第二组广告栏">
<div class="container portal-ad-grid">
<a class="tilt-card" href="help.html"><img src="public/images/ad-data.png" alt="资料校对" loading="lazy" /></a>
<a class="tilt-card" href="article-detail.html"><img src="public/images/ad-memory.png" alt="家风记忆" loading="lazy" /></a>
<a class="tilt-card" href="surname.html"><img src="public/images/ad-research.png" alt="姓氏研究" loading="lazy" /></a>
<a class="tilt-card" href="culture.html"><img src="public/images/ad-reunion.png" alt="宗亲联谊" loading="lazy" /></a>
<a class="tilt-card" href="app.html"><img src="public/images/ad-cloud.png" alt="云端存档" loading="lazy" /></a>
<a class="tilt-card" href="culture.html"><img src="public/images/ad-hall.png" alt="祠堂文化" loading="lazy" /></a>
<a class="tilt-card" href="genealogy.html"><img src="public/images/ad-print.png" alt="族谱印制" loading="lazy" /></a>
<a class="tilt-card" href="promo-video.html"><img src="public/images/ad-video-small.png" alt="活动视频" loading="lazy" /></a>
<a class="tilt-card" href="promo-album.html"><img src="public/images/ad-album-small.png" alt="影像相册" loading="lazy" /></a>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div>
<a class="brand magnetic" href="index.html"
><img
class="brand-logo"
src="public/images/logo-light.png"
alt="我们的家谱,代代相传"
/></a>
<p>一个面向家庭和宗族的数字家谱与家族文化门户。</p>
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a
><a href="news.html">新闻资讯</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
</div>
</div>
<div class="footer-legal">
<div>© 2026 代代相传. All rights reserved.</div>
<a href="privacy.html">隐私政策</a>
<a href="children-privacy.html">儿童个人信息处理规则</a>
<a href="terms.html">服务协议</a>
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener"
>ICP备案:蜀ICP备2024044594号-1</a
>
<footer class="footer portal-footer">
<nav class="container portal-footer-links" aria-label="页脚导航">
<a href="about.html">关于家谱</a>
<a href="help.html">联系客服</a>
<a href="submit-ticket.html">意见反馈</a>
<a href="about.html">免责声明</a>
<a href="terms.html">用户协议</a>
<a href="privacy.html">隐私政策</a>
<a href="index.html#site-map">网站地图</a>
</nav>
<div class="portal-footer-legal">
<div class="container">
<p>Copyright 20142026 代代相传家谱 版权所有</p>
<p>
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">工信部:蜀ICP备2024044594号-1</a>
<a href="children-privacy.html">儿童个人信息处理规则</a>
</p>
<p>家族文化资料请以家谱原件与当地正式记录为准</p>
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/qrcode.min.js"></script>
<script src="public/js/page-effects.js"></script>
<script>
(function () {
var qrTarget = document.getElementById('home-app-qr');
if (qrTarget && typeof window.QRCode === 'function') {
new window.QRCode(qrTarget, {
text: new URL('download.html', window.location.href).href,
width: 92,
height: 92
});
}
var scrollButton = document.querySelector('[data-toggle-word-scroll]');
var wordScroll = document.querySelector('.portal-word-scroll');
function syncWordScroll() {
var isPaused = wordScroll.classList.contains('is-paused');
scrollButton.setAttribute('aria-pressed', String(isPaused));
scrollButton.setAttribute('aria-label', isPaused ? '开始词条滚动' : '暂停词条滚动');
scrollButton.textContent = isPaused ? '开始滚动' : '正在记录';
}
scrollButton.addEventListener('click', function () {
var shouldPlay = wordScroll.classList.contains('is-paused');
wordScroll.classList.toggle('is-paused', !shouldPlay);
wordScroll.classList.toggle('is-playing', shouldPlay);
syncWordScroll();
});
syncWordScroll();
document.querySelectorAll('[data-add-favorite]').forEach(function (bookmarkButton) {
bookmarkButton.addEventListener('click', function () {
window.alert('请按 Ctrl+DMac 请按 Command+D)收藏本站');
});
});
})();
</script>
</body>
</html>
+60 -23
View File
@@ -1,40 +1,77 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<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" />
<link rel="stylesheet" href="public/css/join-genealogy.css" />
</head>
<body class="page-join-genealogy">
<main class="auth-layout">
<a class="brand auth-page-brand" href="index.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<section
class="card form-card auth-card join-card"
aria-labelledby="join-title"
>
<div class="eyebrow">Join Family</div>
<h1 id="join-title">加入家谱</h1>
<p class="lead">输入亲人分享的家谱ID或从详情页带入的链接,提交后等待管理员审核。</p>
<form class="form-grid" data-genealogy-form="join" data-success-url="profile-join-family.html">
<div class="invite-box">
<input class="input" name="genealogyId" inputmode="numeric" placeholder="请输入家谱ID或邀请码" />
<body class="page-profile-module page-join-genealogy" data-join-apply-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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" aria-label="个人中心主导航"></nav>
<div class="nav-actions"><a href="profile-join-family.html">查看我的申请</a></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">我的家谱</div>
<div class="module-title-row">
<div>
<h1>加入家谱</h1>
<p>先搜索并选择要加入的家谱,再补充申请资料;家谱编号由系统自动处理。</p>
</div>
<a class="btn ghost magnetic" href="profile-join-family.html">查看我的申请</a>
</div>
<input class="input" name="applicantName" autocomplete="name" placeholder="你的姓名" />
<input class="input" name="phone" autocomplete="tel" placeholder="手机号" />
<input class="input" name="relationDesc" placeholder="与家族关系说明" />
<textarea class="input" name="applyReason" rows="4" placeholder="申请说明(选填)"></textarea>
<button class="btn primary" type="submit">提交申请</button>
</form>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav" aria-label="个人中心功能导航"></aside>
<div class="module-main">
<section class="module-panel">
<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>
</form>
</div>
</section>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.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/join-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+24 -39
View File
@@ -15,7 +15,7 @@
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<section class="card form-card auth-card" aria-labelledby="login-title">
<div class="eyebrow">Welcome Back</div>
<div class="eyebrow">欢迎回来</div>
<h1 id="login-title">登录账号</h1>
<p class="lead">登录后可创建、加入和维护家谱。</p>
<div class="login-mode-tabs" role="tablist" aria-label="登录方式">
@@ -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>
+1
View File
@@ -48,6 +48,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+36 -46
View File
@@ -3,67 +3,57 @@
<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/css/site-info.css" />
<link rel="stylesheet" href="public/layui/css/layui.css" />
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body
data-feature-status="pending"
data-feature-message="在线工单查询服务正在开发中,当前页面仅保留设计预览。"
>
<body class="page-profile-module" data-feedback-list-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.html"
><img
class="brand-logo"
src="public/images/logo.png"
alt="我们的家谱,代代相传"
/></a>
<nav class="nav-links">
<a href="index.html">首页</a
><a class="active" href="help.html">帮助中心</a
><a href="news.html">新闻资讯</a>
</nav>
<div class="nav-actions"><a href="login.html">登录</a></div>
<a class="brand magnetic" href="index.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" aria-label="个人中心主导航"></nav>
<div class="nav-actions"><a href="help.html">帮助中心</a><a class="btn primary magnetic" href="submit-ticket.html">提交新工单</a></div>
</div>
</header>
<main>
<section class="site-info-hero">
<section class="module-hero">
<div class="container">
<div>
<div class="site-info-kicker">My Tickets</div>
<h1>我的工单</h1>
<p>工单列表、处理进度与客服回复将在后续工单接口交付后接入。</p>
<div class="module-kicker">服务与帮助</div>
<div class="module-title-row">
<div><h1>我的工单</h1><p>查看当前账号提交的问题说明和处理状态。</p></div>
<a class="btn primary magnetic" href="submit-ticket.html">提交新工单</a>
</div>
<div class="site-info-mark"></div>
</div>
</section>
<section class="site-info-section alt">
<div class="container">
<div class="site-info-status">
<h2>暂未开放在线查询</h2>
<p>当前不能读取或展示工单数据,也不会伪造历史记录。</p>
<div class="site-info-actions">
<a
class="btn ghost magnetic"
href="help.html"
data-feature-link="available"
>返回帮助中心</a
>
</div>
<section class="section">
<div class="container module-layout">
<aside class="module-nav" aria-label="个人中心功能导航"></aside>
<div class="module-main">
<section class="module-panel">
<div class="module-title-row">
<div><h2>反馈记录</h2><p>选择记录可以查看完整说明和处理状态。</p></div>
<button class="btn ghost" type="button" data-feedback-refresh>刷新</button>
</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>
</section>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="footer-legal">
<div>© 2026 代代相传. All rights reserved.</div>
<a href="privacy.html">隐私政策</a>
</div>
</div>
</footer>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/feedback-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+19 -20
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/css/site-info.css" />
</head>
<body>
<body data-site-news-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.html"
@@ -31,10 +31,10 @@
<section class="site-info-hero">
<div class="container">
<div>
<div class="site-info-kicker">News Center</div>
<div class="site-info-kicker">资讯中心</div>
<h1>新闻资讯</h1>
<p>
集中阅读平台公告与家谱文化内容。当前为静态展示,后续由新闻内容接口统一提供列表、分类和详情
集中阅读平台新闻、公告与下载内容
</p>
</div>
<div class="site-info-mark"></div>
@@ -42,27 +42,19 @@
</section>
<section class="site-info-section">
<div class="container site-info-layout">
<div class="site-info-list">
<a class="site-info-item" id="platform" href="notice-detail.html"
><small>平台公告</small>
<h2>平台内容公开展示规范说明</h2>
<p>
说明公开展示的家谱与文化内容范围,以及资料维护时应注意的边界。
</p></a
><a class="site-info-item" id="culture" href="article-detail.html"
><small>家谱文化</small>
<h2>为什么家谱不只是一本名册</h2>
<p>
从成员关系、家风记忆与共同维护三个角度理解数字家谱的价值。
</p></a
>
<div>
<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-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取资讯…</div>
</div>
</div>
<aside class="site-info-panel">
<h2>新闻分类</h2>
<div class="site-info-links">
<a href="#platform">平台公告</a><a href="#culture">家谱文化</a
><a href="notice-detail.html">公告详情</a
><a href="article-detail.html">文化文章</a>
<a href="news.html">全部</a
><a href="news.html?articleType=news">新闻</a
><a href="news.html?articleType=notice">公告</a
><a href="news.html?articleType=download">下载</a>
</div>
</aside>
</div>
@@ -78,6 +70,13 @@
</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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/page-effects.js"></script>
<script src="public/js/site-news-pages.js"></script>
</body>
</html>
+1
View File
@@ -46,6 +46,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+5 -5
View File
@@ -32,7 +32,7 @@
<section class="inner-hero notice-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Notice</div>
<div class="eyebrow">平台公告</div>
<h1 data-promotion-title>平台内容公开展示规范说明</h1>
<p class="lead" data-promotion-summary>
为了保护家族资料安全,平台仅展示管理员确认公开的家谱介绍、文章、宣传相册和宣传视频。
@@ -94,10 +94,10 @@
><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
+496
View File
@@ -0,0 +1,496 @@
{
"name": "jiapu",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"esbuild": "^0.28.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
}
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"private": true,
"scripts": {
"test": "node --test tests/*.test.js"
}
}
+7 -7
View File
@@ -36,7 +36,7 @@
<section class="inner-hero plaza-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Family Plaza</div>
<div class="eyebrow">家谱广场</div>
<h1>浏览公开家谱,寻找同宗线索</h1>
<p class="lead">
按姓氏、地区、堂号和家谱名称检索公开家谱,先看示例,再创建自己的家族空间。
@@ -119,17 +119,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
+2 -1
View File
@@ -29,7 +29,7 @@
<section class="site-info-hero">
<div class="container">
<div>
<div class="site-info-kicker">Privacy Policy</div>
<div class="site-info-kicker">隐私政策</div>
<h1>隐私政策</h1>
<p>说明平台处理个人信息的规则、用户权利与联系渠道。</p>
</div>
@@ -67,6 +67,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+22 -30
View File
@@ -8,10 +8,10 @@
<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"
<a class="brand magnetic" href="index.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">
@@ -19,20 +19,20 @@
><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>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Permissions</div>
<div class="module-kicker">权限设置</div>
<div class="module-title-row">
<div>
<h1>管理员权限</h1>
<p>
分配全权管理员、全系管理员、支系管理员,以及入谱审核等具体权限
在成员名册中维护管理员、编辑和成员角色;可执行的操作以当前账号权限为准
</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>
+125
View File
@@ -0,0 +1,125 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-album-detail-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-album.html">相册</a>
</nav>
<div class="nav-actions">
<a href="profile-album.html" data-genealogy-context-link>返回相册</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">相册详情</div>
<div class="module-title-row">
<div>
<h1 data-album-detail-title>相册详情</h1>
<p>查看相册信息和照片;照片暂不可在线预览时,仍会保留名称和上传记录。</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a>
<a href="profile-article.html">谱文</a>
<a class="active" href="profile-album.html">相册</a>
<a href="profile-video.html">视频</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>相册信息</h2>
<div data-album-detail>
<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-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 novalidate>
<div class="editor-two">
<div class="editor-field">
<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" role="status" aria-live="polite">未选择文件</p>
</div>
<div class="editor-field">
<label for="albumPhotoTitle">照片标题</label>
<input id="albumPhotoTitle" name="photoTitle" type="text" placeholder="请输入照片标题" />
</div>
</div>
<div class="editor-field">
<label for="albumPhotoDesc">照片说明</label>
<textarea id="albumPhotoDesc" name="photoDesc" placeholder="请输入照片说明"></textarea>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="albumPhotographer">拍摄人</label>
<input id="albumPhotographer" name="photographer" type="text" placeholder="请输入拍摄人" />
</div>
<div class="editor-field">
<label for="albumShootTime">拍摄时间</label>
<input id="albumShootTime" name="shootTime" type="datetime-local" />
</div>
</div>
<div class="editor-field">
<label for="albumPhotoSortOrder">排序</label>
<input id="albumPhotoSortOrder" name="sortOrder" type="number" step="1" placeholder="数字越小越靠前" />
</div>
<input name="status" type="hidden" value="0" />
<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>
</form>
</section>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.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/md5.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/album-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-album-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-album.html">相册</a>
</nav>
<div class="nav-actions">
<a href="profile-album.html" data-genealogy-context-link>返回相册</a>
<button class="btn primary magnetic" type="submit" form="album-edit-form" data-album-editor-action hidden>保存</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">编辑相册</div>
<div class="module-title-row">
<div>
<h1 data-album-editor-title>新建相册</h1>
<p>维护相册名称、说明和封面;封面编号由上传自动产生。</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a>
<a href="profile-article.html">谱文</a>
<a class="active" href="profile-album.html">相册</a>
<a href="profile-video.html">视频</a>
</aside>
<div class="module-main">
<section class="module-panel">
<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">相册名称 <span class="field-required" aria-hidden="true">*</span></label>
<input id="albumName" name="albumName" type="text" required placeholder="请输入相册名称" />
</div>
<div class="editor-field">
<label for="albumDesc">相册说明</label>
<textarea id="albumDesc" name="albumDesc" placeholder="请输入相册说明"></textarea>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="albumCoverFile">封面文件</label>
<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-attachment-editor data-attachment-replace-only data-upload-target="#albumCoverOssId" data-upload-status="#albumCoverStatus" />
<p id="albumCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
</div>
<div class="editor-field">
<label for="albumSortOrder">排序</label>
<input id="albumSortOrder" name="sortOrder" type="number" step="1" placeholder="数字越小越靠前" />
</div>
</div>
<input name="status" type="hidden" value="0" />
<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>
</div>
</form>
</section>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.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/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/album-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+22 -86
View File
@@ -8,33 +8,32 @@
<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-album-page data-feature-status="pending" data-feature-message="家族相册服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-album-page>
<header class="site-header">
<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
>
<a class="brand magnetic" href="index.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-album.html">相册</a
><a href="profile-video.html">视频</a>
<a href="profile-content.html">内容发布</a>
<a class="active" href="profile-album.html">相册</a>
<a href="profile-video.html">视频</a>
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="#album-form">新建相册</a>
<a href="profile-content.html">返回发布中心</a>
<a class="btn primary magnetic" href="profile-album-edit.html" data-album-create-link hidden>新建相册</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Album</div>
<div class="module-kicker">家族相册</div>
<div class="module-title-row">
<div>
<h1>家族相册</h1>
<p>
管理相册名称、封面、描述和照片,作为个人中心内的独立相册管理页。
</p>
<p>查看和管理家谱相册;照片文件由上传组件自动关联,无需填写内部编号。</p>
</div>
</div>
</div>
@@ -42,82 +41,21 @@
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a href="profile-article.html">谱文</a
><a class="active" href="profile-album.html">相册</a
><a href="profile-video.html">视频</a
><a href="profile-family-home.html">家谱主页</a>
<a href="profile-content.html">内容发布</a>
<a href="profile-article.html">谱文</a>
<a class="active" href="profile-album.html">相册</a>
<a href="profile-video.html">视频</a>
<a href="profile-family-home.html">家谱主页</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>相册列表</h2>
<!-- 相册列表由 /albums 接口渲染,点击后加载照片 -->
<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>
<section class="module-panel">
<h2>新建 / 编辑相册</h2>
<form id="album-form" class="editor-form" data-album-form>
<div class="editor-field">
<label for="albumName">相册名称</label>
<input id="albumName" name="albumName" type="text" placeholder="请输入相册名称" required />
</div>
<div class="editor-field">
<label for="albumDesc">相册描述</label>
<textarea id="albumDesc" name="albumDesc" placeholder="请输入相册描述"></textarea>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="albumCoverFile">封面文件</label>
<input id="coverOssId" name="coverOssId" type="hidden" />
<label class="upload-control" for="albumCoverFile">选择封面图片</label>
<input id="albumCoverFile" class="upload-input" type="file" accept="image/*" data-upload-target="#coverOssId" data-upload-status="#albumCoverStatus" />
<p id="albumCoverStatus" class="upload-status">未选择文件</p>
</div>
<div class="editor-field">
<label for="albumStatus">状态</label>
<select id="albumStatus" name="status">
<option value="0">启用</option>
<option value="1">停用</option>
</select>
</div>
</div>
<div class="bottom-actions">
<button class="btn primary" type="submit">保存相册</button>
<button class="btn ghost" type="reset">清空</button>
</div>
</form>
</section>
<section class="module-panel">
<h2>照片管理</h2>
<!-- 选择相册后由 /albums/{albumId}/photos 接口渲染 -->
<div class="module-list" data-album-photo-list>
<div class="api-empty">请选择相册查看照片</div>
</div>
<form class="editor-form album-photo-form" data-album-photo-form>
<div class="editor-two">
<div class="editor-field">
<label for="albumPhotoFile">照片文件</label>
<input id="photoOssId" name="ossId" type="hidden" required />
<label class="upload-control" for="albumPhotoFile">选择照片</label>
<input id="albumPhotoFile" class="upload-input" type="file" accept="image/*" data-upload-target="#photoOssId" data-upload-status="#albumPhotoStatus" />
<p id="albumPhotoStatus" class="upload-status">未选择文件</p>
</div>
<div class="editor-field">
<label for="photoTitle">照片标题</label>
<input id="photoTitle" name="photoTitle" type="text" placeholder="请输入照片标题" />
</div>
</div>
<div class="editor-field">
<label for="photoDesc">照片说明</label>
<textarea id="photoDesc" name="photoDesc" placeholder="请输入照片说明"></textarea>
</div>
<div class="bottom-actions">
<button class="btn primary" type="submit">添加照片</button>
</div>
</form>
</section>
</div>
</div>
</section>
@@ -128,13 +66,11 @@
<script src="public/js/profile-common.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/md5.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/album-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+28 -17
View File
@@ -9,10 +9,10 @@
<link rel="stylesheet" href="public/css/profile-module.css" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-article-edit-page data-feature-status="pending" data-feature-message="谱文服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-article-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -21,17 +21,17 @@
</nav>
<div class="nav-actions">
<a href="profile-article.html">返回谱文</a
><button class="btn primary magnetic" type="submit" form="article-edit-form">保存</button>
><button class="btn primary magnetic" type="submit" form="article-edit-form" data-article-editor-action hidden>保存</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Article Editor</div>
<div class="module-kicker">编辑谱文</div>
<div class="module-title-row">
<div>
<h1>编辑谱文</h1>
<h1 data-article-editor-title>新建谱文</h1>
<p>
使用富文本编辑器撰写谱文、谱序、谱论和恩荣录,支持图片、表格与排版。
</p>
@@ -50,9 +50,12 @@
<div class="module-main">
<section class="module-panel">
<h2>谱文内容</h2>
<form id="article-edit-form" class="editor-form layui-form" data-article-form>
<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"
@@ -62,12 +65,6 @@
/>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="articleCategory">谱文类型</label
><select id="articleCategory" name="categoryId" data-article-category>
<option value="">分类加载中...</option>
</select>
</div>
<div class="editor-field">
<label for="articleSignature">落款</label
><input
@@ -77,9 +74,19 @@
placeholder="请输入落款"
/>
</div>
<div class="editor-field">
<label for="articleSortOrder">排序</label
><input
id="articleSortOrder"
name="sortOrder"
type="number"
step="1"
placeholder="数字越小越靠前"
/>
</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"
@@ -96,9 +103,11 @@
<label for="articleCoverFile">封面文件</label>
<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>
<input id="articleCoverFile" class="upload-input" type="file" accept="image/*" data-attachment-editor data-upload-target="#articleCoverOssId" data-upload-status="#articleCoverStatus" />
<p id="articleCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
</div>
<input name="status" type="hidden" value="0" />
<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"
@@ -124,8 +133,10 @@
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/article-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+10 -27
View File
@@ -8,10 +8,10 @@
<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-article-page data-feature-status="pending" data-feature-message="谱文服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-article-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -20,7 +20,7 @@
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="profile-article-edit.html"
><a class="btn primary magnetic" href="profile-article-edit.html" data-article-create-link hidden
>添加谱文</a
>
</div>
@@ -29,7 +29,7 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Article</div>
<div class="module-kicker">家族谱文</div>
<div class="module-title-row">
<div>
<h1>谱文管理</h1>
@@ -51,32 +51,14 @@
<div class="module-main">
<section class="module-panel">
<h2>谱文列表</h2>
<!-- 谱文列表由 /articles 接口渲染 -->
<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 class="form-like">
<p>
<span>谱文标题</span><b>待输入</b
><a class="link-btn" href="profile-article-edit.html">填写</a>
</p>
<p>
<span>谱文内容</span><b>待输入</b
><a class="link-btn" href="profile-article-edit.html">编辑</a>
</p>
<p>
<span>图片质量</span><b>原图</b
><a
class="link-btn"
href="#"
data-layer-select="原图|高清|普通"
data-select-title="选择图片质量"
>选择</a
>
</p>
<h2>谱文详情</h2>
<div data-article-detail>
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一篇谱文查看详情</div>
</div>
</section>
</div>
@@ -92,7 +74,8 @@
<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/media-display.js"></script>
<script src="public/js/article-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-ceremony-detail-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-ceremony.html">祭祀活动</a></nav>
<div class="nav-actions"><a href="profile-ceremony.html">返回活动列表</a><a class="btn primary magnetic" href="profile-gift-edit.html" data-ceremony-editor-action hidden>编辑活动</a></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container"><div class="module-kicker">活动详情</div><div class="module-title-row"><div><h1 data-ceremony-detail-title>活动详情</h1><p>活动信息、祭品记录与管理员受邀名单。</p></div></div></div>
</section>
<section class="section">
<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-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-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">礼金金额 <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>
<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-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-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>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/ceremony-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-ceremony-list-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-ceremony.html">祭祀活动</a></nav>
<div class="nav-actions"><a href="profile-content.html">返回发布中心</a><a class="btn primary magnetic" href="profile-gift-edit.html" data-ceremony-create-link data-genealogy-context-link hidden>创建活动</a></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">祭祀活动</div>
<div class="module-title-row"><div><h1>祭祀活动</h1><p>查看当前家谱的活动;具备内容维护权限时可创建、编辑和删除。</p></div></div>
</div>
</section>
<section class="section">
<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">
<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-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
正在加载祭祀活动…
</div>
</div>
</section>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/ceremony-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+20 -25
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -8,22 +8,22 @@
<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-content-page data-feature-status="pending" data-feature-message="除家族圈动态外,其他内容服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-content-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<nav class="nav-links">
<a href="profile.html">首页</a
><a href="profile-families.html">数字家谱</a
<a href="profile.html">个人中心</a
><a href="profile-families.html">我的家谱</a
><a href="plaza.html">家谱广场</a
><a class="active" href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a><a href="app.html">应用下载</a
><a href="help.html">帮助中心</a><a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a href="profile.html">个人中心</a
<a href="profile-data.html">账户资料</a
><a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
@@ -33,16 +33,15 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Content Studio</div>
<div class="module-kicker">内容发布</div>
<div class="module-title-row">
<div>
<h1>内容发布</h1>
<p>
承接 APP
家谱主页里的谱文、视频、相册、功德录、贺礼邀请、成长日志、动态和备忘录。
集中管理谱文、视频、相册、功德录、贺礼邀请、成长记录、家族动态和备忘录。
</p>
</div>
<a class="btn primary magnetic" href="profile-article.html"
<a class="btn primary magnetic" href="profile-article.html" data-feature-link="available" data-genealogy-context-link
>添加谱文</a
>
</div>
@@ -62,27 +61,27 @@
<section class="module-panel">
<h2>发布入口</h2>
<div class="module-grid">
<a class="module-card tilt-card" href="profile-article.html"
<a class="module-card tilt-card" href="profile-article.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-album.html"
<a class="module-card tilt-card" href="profile-album.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-video.html"
<a class="module-card tilt-card" href="profile-video.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-merit.html"
<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
>
<a class="module-card tilt-card" href="profile-gift.html"
<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
@@ -99,22 +98,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>记录亲友关系、往来事项和礼金。</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>以标题、内容和提醒记录家族恩人等事项,统一归入备忘录管理</p></a
>
</div>
</section>
<section class="module-panel">
<h2>最近内容</h2>
<!-- 最近内容先接谱文接口,其他内容类型按后续编号逐步接入 -->
<div class="module-list" data-recent-content>
<div class="api-empty">最近内容加载中...</div>
</div>
<h2>使用说明</h2>
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择上方功能,继续查看和维护当前家谱内容。</div>
</section>
</div>
</div>
@@ -129,7 +125,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>
+50 -24
View File
@@ -8,10 +8,10 @@
<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-feature-status="pending" data-feature-message="创建家谱服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-genealogy-create-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -27,12 +27,12 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Create Family</div>
<div class="module-kicker">创建家谱</div>
<div class="module-title-row">
<div>
<h1>创建家谱</h1>
<p>
个人中心专用创建流程,用于录入姓氏、谱名、堂号、地望、祠堂和访问权限
填写家谱基本信息;标有 * 的字段为必填,创建后可进入家谱工作区继续维护
</p>
</div>
</div>
@@ -48,16 +48,16 @@
<div class="module-main">
<section class="module-panel">
<h2>基础信息</h2>
<!-- 个人中心创建家谱复用创建接口,字段对应 GenealogyCreateBody。 -->
<form class="editor-form profile-create-family-form" data-genealogy-form="create" data-success-url="profile-families.html">
<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>
@@ -93,6 +100,20 @@
<label for="intro">家谱简介</label>
<textarea id="intro" name="intro"></textarea>
</div>
<div class="editor-field">
<label for="familyCoverFile">家谱封面</label>
<input id="familyCoverOssId" name="coverOssId" type="hidden" />
<label class="upload-control" for="familyCoverFile">选择封面图片</label>
<input
id="familyCoverFile"
class="upload-input"
type="file"
accept="image/*"
data-upload-target="#familyCoverOssId"
data-upload-status="#familyCoverStatus"
/>
<p id="familyCoverStatus" class="upload-status" role="status" aria-live="polite">未选择文件</p>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="visibility">可见范围</label>
@@ -104,15 +125,17 @@
</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">创建完成后,可在家谱首页的“邀请家人”中生成一次性邀请链接。</p>
</div>
</div>
<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">确定创建</button>
<button class="btn primary magnetic" type="submit" data-genealogy-create-submit>确定创建</button>
<a class="btn ghost magnetic" href="profile-families.html">返回列表</a>
</div>
</form>
@@ -130,8 +153,11 @@
<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/profile-common.js"></script>
<script src="public/js/region-pages.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/genealogy-entry-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+13 -18
View File
@@ -8,10 +8,10 @@
<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-feature-status="pending" data-feature-message="资料提醒服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-profile-reminder-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -28,7 +28,7 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Data Reminders</div>
<div class="module-kicker">资料提醒</div>
<div class="module-title-row">
<div>
<h1>资料提醒</h1>
@@ -49,20 +49,10 @@
<div class="module-main">
<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="module-list" data-profile-reminder-list>
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
正在读取资料提醒…
</div>
</div>
</section>
</div>
@@ -73,7 +63,12 @@
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.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-reminder-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+55 -59
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -11,19 +11,19 @@
<body class="page-profile-module" data-profile-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<nav class="nav-links">
<a href="profile.html">首页</a
><a href="profile-families.html">数字家谱</a
<a href="profile.html">个人中心</a
><a href="profile-families.html">我的家谱</a
><a href="plaza.html">家谱广场</a
><a href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a><a href="app.html">应用下载</a
><a href="help.html">帮助中心</a><a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a class="active" href="profile.html">个人中心</a
<a class="active" href="profile-data.html">账户资料</a
><a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
@@ -33,12 +33,12 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Profile Data</div>
<div class="module-kicker">个人资料</div>
<div class="module-title-row">
<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">现居地区暂不支持保存到个人资料</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">
个人资料页不单独维护亲属关系;请先选择家谱,再到成员管理或世系图中查看和维护。
</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,12 @@
<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/media-display.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>
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><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-person-document-page>
<header class="site-header"><div class="container nav"><a class="brand" href="index.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-families.html">我的家谱</a><a class="active" href="profile-content.html" data-genealogy-context-link>家族文化</a></nav><div class="nav-actions"><a href="profile-family-home.html" data-genealogy-context-link>返回家谱主页</a></div></div></header>
<main><section class="module-hero"><div class="container"><div class="module-kicker">重要证件</div><div class="module-title-row"><div><h1>重要证件</h1><p>按人物维护证件档案、附件和访问密码。证件号码仅填写脱敏内容。</p></div></div></div></section>
<section class="section"><div class="container module-layout"><aside class="module-nav"><a href="profile-family-home.html" data-genealogy-context-link>家谱主页</a><a href="profile-content.html" data-genealogy-context-link>内容发布</a><a class="active" href="profile-documents.html" data-genealogy-context-link>重要证件</a></aside><div class="module-main">
<section class="module-panel"><h2>新增档案</h2><form class="editor-form" data-document-form novalidate><div class="editor-grid"><div class="editor-field"><label for="documentPerson">关联人物</label><select id="documentPerson" data-document-person><option>人物加载中</option></select></div><div class="editor-field"><label for="documentType">证件类型</label><select id="documentType" data-document-type><option>类型加载中</option></select></div><div class="editor-field"><label for="documentTitle">证件标题</label><input id="documentTitle" name="documentTitle" maxlength="100" /></div><div class="editor-field"><label for="maskedIdentifier">脱敏标识</label><input id="maskedIdentifier" name="maskedIdentifier" maxlength="100" placeholder="例如 310***********1234" /></div></div><div class="editor-field"><label for="documentDescription">说明</label><textarea id="documentDescription" name="description" maxlength="1000"></textarea></div><div class="editor-grid"><div class="editor-field"><label for="documentUsage">文件用途</label><select id="documentUsage" data-document-usage><option value="FRONT">正面</option><option value="BACK">反面</option><option value="ATTACHMENT">附件</option></select></div><div class="editor-field"><label for="documentFile">证件文件</label><input id="documentFile" type="file" accept="image/*,application/pdf" data-upload-target="#documentOssId" data-upload-status="#documentUploadStatus" /><input id="documentOssId" type="hidden" data-document-oss-id /><p id="documentUploadStatus" class="upload-status" role="status" aria-live="polite"></p></div></div><div class="bottom-actions"><button class="btn primary" type="submit">保存档案</button></div></form></section>
<section class="module-panel"><h2>档案列表</h2><div class="module-grid" data-document-list><div class="api-state api-state--loading" data-api-state="loading">正在读取重要证件…</div></div></section><div class="api-status" data-document-status role="status" aria-live="polite">正在初始化…</div>
</div></div></section>
</main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><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><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/upload-pages.js"></script><script src="public/js/person-document-pages.js"></script><script src="public/js/page-effects.js"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><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-earning-page>
<header class="site-header"><div class="container nav"><a class="brand" href="index.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.html">个人中心</a><a class="active" href="profile-services.html">帮助与服务</a></nav><div class="nav-actions"><a href="profile-services.html">返回服务</a></div></div></header>
<main><section class="module-hero"><div class="container"><div class="module-kicker">推广收益</div><div class="module-title-row"><div><h1>推广收益</h1><p>查看可用收益、不可变流水和提现处理状态。</p></div></div></div></section><section class="section"><div class="container module-layout"><aside class="module-nav"><a href="profile-services.html">帮助与服务</a><a class="active" href="profile-earnings.html">推广收益</a><a href="profile-share.html">应用分享</a></aside><div class="module-main">
<section class="module-panel"><h2>收益概览</h2><div data-earning-summary><div class="api-state api-state--loading" data-api-state="loading">正在读取收益…</div></div></section>
<section class="module-panel"><h2>申请提现</h2><form class="editor-form" data-withdrawal-form novalidate><div class="editor-grid"><div class="editor-field"><label for="withdrawalAmount">提现金额</label><input id="withdrawalAmount" name="amount" inputmode="decimal" placeholder="0.00" /></div><div class="editor-field"><label for="payoutAccountName">收款人姓名</label><input id="payoutAccountName" name="payoutAccountName" maxlength="64" /></div></div><div class="editor-field"><label for="payoutQrFile">微信收款码</label><input id="payoutQrFile" class="upload-input" type="file" accept="image/*" data-upload-target="#payoutQrOssId" data-upload-status="#payoutQrStatus" /><label class="upload-control" for="payoutQrFile">选择收款码图片</label><input id="payoutQrOssId" name="payoutQrOssId" type="hidden" /><p id="payoutQrStatus" class="upload-status" role="status" aria-live="polite"></p></div><div class="bottom-actions"><button class="btn primary" type="submit" data-withdrawal-submit>提交提现申请</button></div></form></section>
<section class="module-panel"><h2>提现记录</h2><div class="module-list" data-earning-withdrawals><div class="api-state api-state--loading" data-api-state="loading">正在读取提现记录…</div></div></section><section class="module-panel"><h2>收益流水</h2><div class="module-list" data-earning-ledger><div class="api-state api-state--loading" data-api-state="loading">正在读取收益流水…</div></div></section><div class="api-status" data-earning-status role="status" aria-live="polite">正在初始化…</div>
</div></div></section></main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><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><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/upload-pages.js"></script><script src="public/js/earning-pages.js"></script><script src="public/js/page-effects.js"></script>
</body>
</html>
+29 -16
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -8,37 +8,36 @@
<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-feature-status="pending" data-feature-message="家谱列表服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module">
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<nav class="nav-links">
<a href="profile.html">首页</a
><a class="active" href="profile-families.html">数字家谱</a
<a href="profile.html">个人中心</a
><a class="active" href="profile-families.html">我的家谱</a
><a href="plaza.html">家谱广场</a
><a href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a><a href="app.html">应用下载</a
><a href="help.html">帮助中心</a><a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a href="profile.html">个人中心</a
><a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
<a href="profile-data.html">账户资料</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Personal Center</div>
<nav class="profile-path" aria-label="当前位置">
<a href="profile.html">个人中心</a><span aria-hidden="true">/</span><strong aria-current="page">我的家谱</strong>
</nav>
<div class="module-title-row">
<div>
<h1>我的家谱</h1>
<p>
这里进入的家谱。当前 PC 只提供家谱额度查询,列表和详情入口等待后端补齐后再开放
真实列表选择要进入的家谱,后续页面会持续使用同一个家谱上下文
</p>
</div>
<a class="btn primary magnetic" href="profile-create-family.html"
@@ -60,9 +59,12 @@
<div class="module-main">
<section class="module-panel">
<h2>家谱入口</h2>
<p>对应 PC 的“我的家谱”入口;进入具体业务前必须取得真实家谱编号</p>
<p>先选择要处理的家谱,再进入主页、成员、世系和内容管理</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" data-genealogy-entry-status>正在读取家谱入口状态…</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">
@@ -78,8 +80,20 @@
<a class="module-card tilt-card" href="profile-join-family.html"
><span class="icon"></span>
<h3>加入家谱</h3>
<p>输入邀请码或由管理员邀请,加入已有家谱协作</p></a
<p>搜索并选择可加入的家谱,提交关系和申请说明等待审核</p></a
>
<div class="module-card">
<span class="icon"></span>
<h3>家谱排序</h3>
<p>选择家谱后上移或下移,保存个人的家谱展示顺序。</p>
<label for="genealogySortSelect">选择家谱</label>
<select id="genealogySortSelect" data-genealogy-sort-select disabled><option value="">家谱加载中</option></select>
<div class="row-actions">
<button class="btn outline" type="button" data-genealogy-sort-up disabled>上移</button>
<button class="btn outline" type="button" data-genealogy-sort-down disabled>下移</button>
<button class="btn primary" type="button" data-genealogy-sort-save disabled>保存顺序</button>
</div>
</div>
</div>
</section>
</div>
@@ -94,9 +108,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/pending-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>
+50 -78
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -8,101 +8,73 @@
<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-member-admin-page data-feature-status="pending" data-feature-message="家谱成员与权限服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-member-admin-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<nav class="nav-links">
<a href="profile.html">首页</a
><a class="active" href="profile-families.html">数字家谱</a
><a href="plaza.html">家谱广场</a
><a href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a><a href="app.html">应用下载</a
><a href="help.html">帮助中心</a><a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a href="profile.html">个人中心</a
><a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
</div>
<a class="brand magnetic" href="index.html"><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a>
<nav class="nav-links"><a href="profile.html">个人中心</a><a class="active" href="profile-families.html">我的家谱</a><a href="profile-content.html">家族文化</a></nav>
<div class="nav-actions"><a href="profile-data.html">账户资料</a><a class="btn primary magnetic" href="profile-tree.html" data-genealogy-context-link>世系人物</a></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Family Admin</div>
<div class="module-kicker">家族管理</div>
<div class="module-title-row">
<div>
<h1 data-member-title>家族管理</h1>
<p>
承接 APP
里的世代管理、管理员权限、入谱审核和邀请绑定等后台能力。
</p>
</div>
<a class="btn primary magnetic" href="profile-data.html"
>添加成员</a
>
<div><h1 data-member-title>家族管理</h1><p>查看家谱成员;具备管理权限时可调整成员信息、角色和世系人物绑定。</p></div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-families.html">我的家谱</a
><a href="profile-messages.html">消息中心</a
><a class="active" href="profile-family-admin.html">家族管理</a
><a href="profile-content.html">内容发布</a
><a href="profile-data.html">个人资料</a
><a href="profile-services.html">帮助与服务</a>
</aside>
<aside class="module-nav"><a href="profile-families.html">我的家谱</a><a href="profile-messages.html">消息中心</a><a class="active" href="profile-family-admin.html" data-genealogy-context-link>家族管理</a><a href="profile-tree.html" data-genealogy-context-link>世系图</a><a href="profile-generation.html" data-genealogy-context-link>字辈谱</a></aside>
<div class="module-main">
<section class="module-panel">
<h2>管理入口</h2>
<p data-member-overview>家谱概览加载中...</p>
<h2>家谱概览</h2>
<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
>
<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-messages.html"
><span class="icon"></span>
<h3>入谱审核</h3>
<p>审核加入家谱与绑定账号申请。</p></a
>
<a
class="module-card tilt-card"
href="profile-admin-permissions.html"
><span class="icon"></span>
<h3>管理员权限</h3>
<p>分配全权、全系、支系管理员。</p></a
>
<a class="module-card tilt-card" href="profile-invite.html"
><span class="icon"></span>
<h3>邀请家人</h3>
<p>生成邀请入口,让亲人加入协作。</p></a
>
<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-ceremony.html" data-genealogy-context-link><span class="icon"></span><h3>祭祀活动</h3><p>维护活动、祭品与受邀名单。</p></a>
<a class="module-card tilt-card" href="profile-article.html" data-genealogy-context-link><span class="icon"></span><h3>谱文</h3><p>查看或维护当前家谱谱文</p></a>
<a class="module-card tilt-card" href="profile-join-review.html" data-genealogy-context-link><span class="icon"></span><h3>入谱审核</h3><p>审核当前家谱的待处理加入申请。</p></a>
</div>
</section>
<section class="module-panel">
<h2>当前管理</h2>
<!-- 成员列表由 /members 接口渲染,权限按钮会提交 /members/{memberId} -->
<div class="module-list" data-member-list>
<div class="api-empty">成员加载中...</div>
</div>
<h2>正常成</h2>
<p>成员新增必须通过后端已有的加入或邀请流程;本页不伪造直接新增成员。</p>
<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 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>
</div>
<div class="editor-two">
<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">留空表示保持原信息不变;只填写本次需要修改的内容。</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 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>
<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>
<h2>退出家谱</h2>
<p>退出只停用当前账号的成员关系,不删除世系人物资料;谱主必须先转让谱主。</p>
<div class="bottom-actions"><button class="btn ghost" type="button" data-member-leave>退出当前家谱</button></div>
</section>
</div>
</div>
@@ -117,7 +89,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/member-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+111 -30
View File
@@ -8,21 +8,21 @@
<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-home-page data-feature-status="pending" data-feature-message="家谱概览与管理服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-family-home-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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 class="active" href="profile-families.html">我的家谱</a
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
><a href="profile-content.html">内容发布</a
><a href="profile-content.html" data-genealogy-context-link>内容发布</a
><a href="profile-data.html">个人资料</a
><a href="profile-services.html">帮助与服务</a>
</nav>
<div class="nav-actions">
<a href="profile.html">返回中心</a
<a href="profile.html">账户中心</a
><a class="btn primary magnetic" href="profile-feed.html" data-feature-link="available" data-genealogy-context-link>发布动态</a>
</div>
</div>
@@ -30,15 +30,17 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Family Home</div>
<nav class="profile-path" aria-label="当前位置">
<a href="profile.html">个人中心</a><span aria-hidden="true">/</span><a href="profile-families.html">我的家谱</a><span aria-hidden="true">/</span><strong data-family-home-context aria-current="page">当前家谱</strong>
</nav>
<div class="module-title-row">
<div>
<h1 data-family-home-title>四川武胜汤氏族</h1>
<h1 data-family-home-title>正在读取家谱</h1>
<p>
后台家谱主页,用于进入世系、相册、家族圈、内容管理和成员协作。
当前家谱的工作区,用于进入世系、相册、家族圈、内容管理和成员协作。
</p>
</div>
<a class="btn ghost magnetic" href="profile-family-admin.html" data-genealogy-context-link
<a class="btn ghost magnetic" href="profile-family-admin.html" data-family-home-management data-genealogy-context-link hidden
>管理家谱</a
>
</div>
@@ -49,54 +51,131 @@
<aside class="module-nav">
<a class="active" href="profile-family-home.html" data-genealogy-context-link>家谱主页</a
><a href="profile-tree.html" data-genealogy-context-link>世系图</a
><a href="profile-album.html">相册</a
><a href="profile-content.html">内容发布</a
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a
><a href="profile-album.html" data-feature-link="available" data-genealogy-context-link>相册</a
><a href="profile-content.html" data-genealogy-context-link>内容发布</a
><a href="profile-family-admin.html" data-family-home-management data-genealogy-context-link hidden>家族管理</a
><a href="profile-messages.html">消息中心</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>家谱概览</h2>
<div class="module-grid">
<div class="module-card">
<span class="icon"></span>
<h3>家谱编号</h3>
<p data-family-home-summary>家谱信息加载中...</p>
<div class="module-card" data-family-home-overview>
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
正在加载家谱概览…
</div>
</div>
<a class="module-card tilt-card" href="profile-tree.html" data-genealogy-context-link
><span class="icon"></span>
<h3>世系图</h3>
<p>查看树谱模式和成员关系。</p></a
><a class="module-card tilt-card" href="profile-invite.html"
><span class="icon"></span>
<div class="module-card">
<span class="icon"></span>
<h3>家谱成员</h3>
<p><strong data-family-home-member-count>--</strong></p>
</div>
<div class="module-card">
<span class="icon"></span>
<h3>世系人物</h3>
<p><strong data-family-home-person-count>--</strong></p>
</div>
<a class="module-card tilt-card" href="profile-invite.html" data-family-home-management data-genealogy-context-link hidden>
<span class="icon"></span>
<h3>邀请家人</h3>
<p>生成邀请入口,邀请亲人协作</p></a
>
<p>生成一次性邀请链接,指定人物并查看邀请状态</p>
</a>
</div>
<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"
<a class="module-card tilt-card" href="profile-article.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-video.html"
><a class="module-card tilt-card" href="profile-album.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-video.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-merit.html"
>
</div>
</section>
<section class="module-panel">
<h2>家族记录与协作</h2>
<p>按列表、详情和编辑页面管理动态、成长记录、亲友往来、备忘和活动。</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>记录亲友关系、往来事项和礼金。</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
><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
>
<a class="module-card tilt-card" href="profile-documents.html" data-feature-link="available" data-genealogy-context-link>
<span class="icon"></span>
<h3>重要证件</h3>
<p>按人物维护证件档案、附件和密码保护。</p>
</a>
</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#membership" 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-settings.html" data-feature-link="available" data-family-home-management data-genealogy-context-link hidden>
<span class="icon"></span>
<h3>家谱维护</h3>
<p>归档、恢复或在身份校验后永久删除家谱。</p>
</a>
</div>
</section>
</div>
@@ -112,7 +191,9 @@
<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/media-display.js"></script>
<script src="public/js/lineage-pages.js"></script>
<script src="public/js/family-home-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-genealogy-lifecycle-page>
<header class="site-header"><div class="container nav"><a class="brand" href="index.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-families.html">我的家谱</a><a class="active" href="profile-family-admin.html" data-genealogy-context-link>家族管理</a></nav><div class="nav-actions"><a href="profile-family-home.html" data-genealogy-context-link>返回家谱主页</a></div></div></header>
<main>
<section class="module-hero"><div class="container"><div class="module-kicker">家谱维护</div><div class="module-title-row"><div><h1>家谱维护</h1><p>归档、恢复或永久删除当前家谱。永久删除前会再次校验谱主身份和手机号。</p></div></div></div></section>
<section class="section"><div class="container module-layout"><aside class="module-nav"><a href="profile-family-home.html" data-genealogy-context-link>家谱主页</a><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a><a class="active" href="profile-family-settings.html" data-genealogy-context-link>家谱维护</a></aside><div class="module-main">
<section class="module-panel"><h2>归档与恢复</h2><p>归档用于暂时停止维护,不会永久删除数据。</p><div class="bottom-actions"><button class="btn outline" type="button" data-genealogy-archive>归档家谱</button><button class="btn outline" type="button" data-genealogy-restore>恢复家谱</button></div></section>
<section class="module-panel"><h2>永久删除</h2><p>此操作不可恢复。仅谱主且满足服务端删除条件时可执行。</p><form class="editor-form" data-genealogy-delete-form novalidate><div class="editor-field"><label for="deleteGenealogyName">家谱全名</label><input id="deleteGenealogyName" data-genealogy-delete-name autocomplete="off" /></div><div class="editor-field"><label for="deleteSmsCode">短信验证码</label><div class="bottom-actions"><input id="deleteSmsCode" data-genealogy-delete-sms inputmode="numeric" autocomplete="one-time-code" /><button class="btn outline" type="button" data-genealogy-delete-code>发送验证码</button></div></div><div class="bottom-actions"><button class="btn primary" type="submit" data-genealogy-delete-submit disabled>永久删除家谱</button></div></form></section>
<div class="api-status" data-lifecycle-status role="status" aria-live="polite">正在读取家谱维护能力…</div>
</div></div></section>
</main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><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><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/genealogy-lifecycle-pages.js"></script><script src="public/js/page-effects.js"></script>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-feed-detail-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-family-home.html">家谱主页</a
><a class="active" href="profile-feed.html" data-feed-list-link data-genealogy-context-link>家族圈</a>
</nav>
<div class="nav-actions">
<a href="profile-feed.html" data-feed-list-link data-genealogy-context-link>返回动态列表</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">动态详情</div>
<div class="module-title-row">
<div>
<h1>动态详情</h1>
<p>查看动态、评论与回复;本页链接可用于通知跳转和分享。</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-feed.html" data-feed-list-link data-genealogy-context-link>家族圈</a
><a href="profile-album.html">相册</a
><a href="profile-video.html">视频</a>
</aside>
<div class="module-main">
<section class="module-panel" data-feed-detail>
<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-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">
正在加载评论…
</div>
</div>
</section>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<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>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/feed-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+22 -19
View File
@@ -12,7 +12,7 @@
<body class="page-profile-module" data-feed-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -20,18 +20,18 @@
><a class="active" href="profile-feed.html">发布动态</a>
</nav>
<div class="nav-actions">
<a href="profile-feed.html" data-feed-list-link>返回动态</a
><button class="btn primary magnetic" type="submit" form="feed-edit-form">发布</button>
<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" data-feed-editor-action hidden disabled>发布</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Feed Editor</div>
<div class="module-kicker" data-feed-editor-kicker>发布动态</div>
<div class="module-title-row">
<div>
<h1>发布动态</h1>
<h1 data-feed-editor-title>发布动态</h1>
<p>面向家族圈发布文字和图片动态,与家人交流近况。</p>
</div>
</div>
@@ -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"
@@ -61,29 +61,29 @@
></textarea>
</div>
<div class="editor-field">
<label for="feedMedia">图片 OSS ID</label>
<input id="feedMedia" name="mediaOssIds" type="text" placeholder="多个图片 ID 用英文逗号分隔" />
<!-- 当前上传接口一次只接收一个文件;多张图片请使用逗号分隔 OSS ID。 -->
<label for="feedMediaFile">动态图片</label>
<input id="feedMedia" name="mediaOssIds" type="hidden" />
<label class="upload-control" for="feedMediaFile">选择动态图片</label>
<input id="feedMediaFile" class="upload-input" type="file" accept="image/*" data-upload-target="#feedMedia" data-upload-status="#feedMediaStatus" />
<p id="feedMediaStatus" class="upload-status">未选择文件</p>
<input id="feedMediaFile" class="upload-input" type="file" accept="image/*" multiple data-upload-multiple="true" data-attachment-editor data-upload-target="#feedMedia" data-upload-status="#feedMediaStatus" />
<p id="feedMediaStatus" class="upload-status" role="status" aria-live="polite">未选择图片</p>
</div>
<div class="editor-two">
<div class="editor-two" data-feed-advanced hidden>
<div class="editor-field">
<label for="feedStatus">状态</label>
<select id="feedStatus" name="status">
<option value="0">发布</option>
<option value="1">草稿</option>
<select id="feedStatus" name="status" disabled>
<option value="0">正常</option>
<option value="1">停用</option>
</select>
</div>
<div class="editor-field">
<label for="feedSortOrder">排序值</label>
<input id="feedSortOrder" name="sortOrder" type="number" min="0" value="0" />
<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
><a class="btn ghost magnetic" href="profile-feed.html" data-feed-list-link
<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
>
</div>
@@ -105,7 +105,10 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/feed-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+21 -15
View File
@@ -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" />
@@ -11,17 +11,17 @@
<body class="page-profile-module" data-feed-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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-family-home.html">家谱主页</a
><a href="profile-content.html">内容发布</a
><a class="active" href="profile-feed.html">发布动态</a>
><a class="active" href="profile-feed.html">家族圈</a>
</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
><a class="btn primary magnetic" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden
>发布</a
>
</div>
@@ -30,10 +30,10 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Family Feed</div>
<div class="module-kicker">家族圈</div>
<div class="module-title-row">
<div>
<h1>发布动态</h1>
<h1>家族圈</h1>
<p>向家族圈发布文字和图片动态,与家人交流近况。</p>
</div>
</div>
@@ -43,31 +43,31 @@
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-feed.html">发布动态</a
><a class="active" href="profile-feed.html">家族圈</a
><a href="profile-album.html">相册</a
><a href="profile-video.html">视频</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>动态发布</h2>
<h2>发布动态</h2>
<div class="form-like">
<p>
<span>内容</span><b>在发布页填写</b
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-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>添加图片 OSS ID</b
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link>添加</a
<span>图片</span><b>直接选择并上传多张图片</b
><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>编辑</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>设置</a
<span>发布状态</span><b>正常或停用</b
><a class="link-btn" href="profile-feed-edit.html" data-feed-create-link data-genealogy-context-link hidden>设置</a
>
</p>
</div>
@@ -76,7 +76,12 @@
<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>
<span data-feed-page-label>第 1 页</span>
<button class="pill" type="button" data-feed-page-next disabled>下一页</button>
</div>
</section>
</div>
@@ -92,6 +97,7 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/feed-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+18 -27
View File
@@ -8,10 +8,10 @@
<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-feature-status="pending" data-feature-message="意见反馈服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-feedback-page="profile">
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -19,15 +19,14 @@
><a class="active" href="profile-feedback.html">意见反馈</a>
</nav>
<div class="nav-actions">
<a href="profile-services.html">返回服务中心</a
><button class="btn primary magnetic" type="submit" form="profileFeedbackForm">提交</button>
<a href="profile-services.html">返回服务中心</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Feedback</div>
<div class="module-kicker">意见反馈</div>
<div class="module-title-row">
<div>
<h1>意见反馈</h1>
@@ -40,21 +39,19 @@
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-services.html">帮助与服务</a
><a href="profile-share.html">应用分享</a
><span class="module-nav-unavailable">应用分享<small>即将开放</small></span
><a class="active" href="profile-feedback.html">意见反馈</a
><a href="profile-security.html">安全设置</a>
</aside>
<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>
<select id="feedbackType" name="feedbackType">
<option value="suggestion">改进建议</option>
<option value="bug">问题反馈</option>
<option value="data">资料异常</option>
<select id="feedbackType" name="feedbackType" data-feedback-type-options>
<option value="">反馈类型加载中…</option>
</select>
</div>
<div class="editor-field">
@@ -63,28 +60,22 @@
</div>
</div>
<div class="editor-field">
<label for="feedbackTitle">反馈标题</label>
<input id="feedbackTitle" name="feedbackTitle" type="text" placeholder="请填写反馈标题" />
</div>
<div class="editor-field">
<label for="feedbackContent">反馈描述</label>
<textarea id="feedbackContent" name="feedbackContent" placeholder="请描述问题、页面位置和期望结果"></textarea>
<label for="feedbackContent">反馈描述 <span class="field-required" aria-hidden="true">*</span></label>
<textarea id="feedbackContent" name="feedbackContent" required placeholder="请描述问题、页面位置和期望结果"></textarea>
</div>
<div class="form-status" role="status" aria-live="polite" data-feedback-status></div>
<div class="bottom-actions">
<button class="btn primary magnetic" type="submit">提交反馈</button>
<button class="btn primary magnetic" type="submit" data-feedback-submit>提交反馈</button>
</div>
</form>
</section>
<section class="module-panel">
<h2>历史反馈</h2>
<div class="module-title-row">
<h2>历史反馈</h2>
<button class="btn ghost" type="button" data-feedback-refresh>刷新</button>
</div>
<div class="module-list" data-feedback-list>
<div class="module-row">
<div>
<h3>资料导入建议</h3>
<p>已受理 · 2026.05</p>
</div>
<span class="pill">查看</span>
</div>
<div class="api-state api-state--loading" data-api-state="loading" role="status" aria-live="polite" aria-busy="true">正在读取反馈记录…</div>
</div>
</section>
</div>
@@ -99,8 +90,8 @@
<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/profile-common.js"></script>
<script src="public/js/feedback-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+41 -8
View File
@@ -11,7 +11,7 @@
<body class="page-profile-module" data-generation-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -27,7 +27,7 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Generation</div>
<div class="module-kicker">字辈管理</div>
<div class="module-title-row">
<div>
<h1>字辈谱</h1>
@@ -37,6 +37,9 @@
class="btn primary magnetic"
type="button"
data-generation-add
data-generation-management
hidden
disabled
>新增字辈</button>
</div>
</div>
@@ -49,12 +52,12 @@
><a href="profile-family-admin.html" data-genealogy-context-link>家族管理</a>
</aside>
<div id="generation-content" class="module-main">
<section class="module-panel generation-batch-panel">
<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"
@@ -81,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>
+46 -30
View File
@@ -3,36 +3,36 @@
<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" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-ceremony-edit-page data-feature-status="pending" data-feature-message="当前 PC 未提供活动创建或编辑接口,页面保持设计预览。">
<body class="page-profile-module" data-ceremony-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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-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">活动编辑</div>
<div class="module-title-row">
<div>
<h1>创建贺礼</h1>
<p>维护贺礼类型、标题、时间地点、通知人员和邀请正文</p>
<h1 data-ceremony-editor-title>创建祭祀活动</h1>
<p>维护活动类型、标题、时间地点和说明;受邀名单在活动详情中选择家谱成员</p>
</div>
</div>
</div>
@@ -42,36 +42,31 @@
<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>
<form id="ceremony-edit-form" class="editor-form layui-form" data-ceremony-form>
<h2>活动信息</h2>
<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
><select id="giftType" name="ceremonyType">
<option value="wedding">婚礼</option>
<option value="education">升学</option>
<option value="birthday">生日</option>
<option value="ancestor">祭祀</option>
<option value="other">其他</option>
</select>
<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">
<label for="giftDate">举办时间</label
><input
id="giftDate"
name="ceremonyTime"
type="text"
type="datetime-local"
placeholder="请选择时间"
/>
</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">
@@ -84,24 +79,43 @@
/>
</div>
<div class="editor-field">
<label for="giftContent">贺礼内容</label
><textarea
<label for="giftLocationAddress">详细地址</label
><input id="giftLocationAddress" name="locationAddress" type="text" maxlength="300" placeholder="最多 300 个字符" />
</div>
<div class="editor-two">
<div class="editor-field">
<label for="giftLongitude">经度</label
><input id="giftLongitude" name="longitude" type="number" min="-180" max="180" step="any" placeholder="与纬度同时填写" />
</div>
<div class="editor-field">
<label for="giftLatitude">纬度</label
><input id="giftLatitude" name="latitude" type="number" min="-90" max="90" step="any" placeholder="与经度同时填写" />
</div>
</div>
<div class="editor-field">
<label for="giftContent">活动说明</label
><textarea class="js-rich-editor"
id="giftContent"
name="ceremonyDesc"
class="js-rich-editor"
placeholder="请输入贺礼内容"
placeholder="请输入活动说明"
></textarea>
</div>
<div class="editor-field">
<label for="giftCoverFile">封面文件</label>
<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>
<input id="giftCoverFile" class="upload-input" type="file" accept="image/*" data-attachment-editor data-upload-target="#giftCoverOssId" data-upload-status="#giftCoverStatus" />
<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" />
<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>
@@ -124,8 +138,10 @@
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/ceremony-admin-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+23 -36
View File
@@ -8,10 +8,10 @@
<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-ceremony-page data-feature-status="pending" data-feature-message="当前 PC 仅提供活动邀约接口,缺少活动列表、创建和详情,页面保持设计预览。">
<body class="page-profile-module" data-ceremony-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -19,22 +19,19 @@
><a class="active" href="profile-gift.html">贺礼邀请</a>
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="profile-gift-edit.html"
>创建贺礼</a
>
<a href="profile-content.html">返回发布中心</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Invitation</div>
<div class="module-kicker">贺礼邀请</div>
<div class="module-title-row">
<div>
<h1>贺礼邀请</h1>
<p>
当前仅核验活动邀约、响应及“我的邀请”;活动列表、创建和详情接口尚未提供
查看发给当前账号的活动邀请,并直接接受或拒绝;具备权限时可进入活动管理
</p>
</div>
</div>
@@ -50,37 +47,27 @@
</aside>
<div class="module-main">
<section class="module-panel">
<h2>邀请列表</h2>
<!-- 当前 PC 未提供活动列表接口,保持待开发状态。 -->
<div class="module-list" data-ceremony-list>
<div class="api-empty">贺礼邀请加载中...</div>
<h2>我的邀请</h2>
<div class="module-list" data-my-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>
<div class="form-status" data-my-invitation-status role="status" aria-live="polite"></div>
</section>
<section class="module-panel">
<div class="module-panel-head">
<h2>关联活动</h2>
<a class="pill" href="profile-ceremony.html" data-genealogy-context-link>管理关联活动</a>
</div>
</section>
<section class="module-panel">
<h2>献礼记录</h2>
<!-- 当前 PC 未提供活动详情或献礼记录接口,保持待开发状态。 -->
<div class="module-list" data-ceremony-gift-list>
<div class="api-empty">请选择贺礼查看献礼记录</div>
<h2>邀请详情</h2>
<div data-my-invitation-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
请选择一条邀请查看详情
</div>
</div>
<form class="editor-form ceremony-gift-form" data-ceremony-gift-form>
<div class="editor-two">
<div class="editor-field">
<label for="giftGiverName">献礼人</label>
<input id="giftGiverName" name="giverName" type="text" placeholder="请输入献礼人姓名" />
</div>
<div class="editor-field">
<label for="giftAmount">礼金金额</label>
<input id="giftAmount" name="giftAmount" type="number" step="0.01" placeholder="请输入金额" />
</div>
</div>
<div class="editor-field">
<label for="giftMessage">献礼留言</label>
<textarea id="giftMessage" name="giftMessage" placeholder="请输入留言"></textarea>
</div>
<div class="bottom-actions">
<button class="btn primary" type="submit">保存献礼</button>
</div>
</form>
</section>
</div>
</div>
@@ -95,7 +82,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/ceremony-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+24 -20
View File
@@ -12,7 +12,7 @@
<body class="page-profile-module" data-growth-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -21,18 +21,18 @@
</nav>
<div class="nav-actions">
<a href="profile-growth.html" data-genealogy-context-link>返回记录</a
><button class="btn primary magnetic" type="submit" form="growth-edit-form">保存</button>
><button class="btn primary magnetic" type="submit" form="growth-edit-form" data-growth-editor-action hidden>保存</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Growth Record</div>
<div class="module-kicker">编辑成长记录</div>
<div class="module-title-row">
<div>
<h1>新建成长记录</h1>
<p>仅提交 Apifox PC 的 GrowthRecordBody 字段</p>
<h1 data-growth-editor-title>新建成长记录</h1>
<p>人物从当前家谱世系中选择,附件由上传组件自动生成内部编号</p>
</div>
</div>
</div>
@@ -48,16 +48,17 @@
<div class="module-main">
<section class="module-panel">
<h2>记录内容</h2>
<form id="growth-edit-form" class="editor-form layui-form" data-growth-form>
<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">世系人物 ID</label
><input
<label for="growthPersonId">关联世系人物</label
><select
id="growthPersonId"
name="lineagePersonId"
type="text"
placeholder="可选;请输入世系人物 ID"
/>
data-growth-lineage-person
>
<option value="">正在加载世系人物…</option>
</select>
</div>
<div class="editor-field">
<label for="growthDate">日期</label
@@ -71,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">
@@ -90,33 +91,34 @@
</div>
<div class="editor-field">
<label for="growthRemindTime">提醒时间</label>
<input id="growthRemindTime" name="remindTime" type="text" placeholder="可选;使用带时区的 ISO 8601 时间" />
<input id="growthRemindTime" name="remindTime" type="datetime-local" />
</div>
<div class="editor-two">
<div class="editor-field">
<label for="growthMediaFiles">附件文件</label>
<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" />
<p id="growthMediaStatus" class="upload-status">未选择文件</p>
<input id="growthMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-attachment-editor data-upload-target="#growthMediaOssIds" data-upload-status="#growthMediaStatus" data-growth-media-input />
<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">
<label for="growthSortOrder">排序值</label>
<input id="growthSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" />
</div>
</div>
<div class="editor-field">
<label for="growthStatus">状态</label>
<input id="growthStatus" name="status" type="text" placeholder="可选;按后端定义填写状态" />
</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 data-growth-editor-placeholder>
<div class="api-state api-state--loading" role="status" aria-live="polite" aria-busy="true">正在加载成长记录编辑信息…</div>
</div>
</section>
</div>
</div>
@@ -135,7 +137,9 @@
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/growth-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+11 -9
View File
@@ -11,7 +11,7 @@
<body class="page-profile-module" data-growth-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -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>
@@ -29,11 +29,11 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Growth Records</div>
<div class="module-kicker">成长记录</div>
<div class="module-title-row">
<div>
<h1>成长记录</h1>
<p>记录成员成长节点、日期、提醒和附件 OSS ID</p>
<p>记录家族成员成长节点、日期、提醒和附件。</p>
</div>
</div>
</div>
@@ -49,14 +49,15 @@
<div class="module-main">
<section class="module-panel">
<h2>记录列表</h2>
<!-- 成长记录由 ApiClient.growthRecords 渲染。 -->
<div class="module-list" data-growth-list>
<div class="api-empty">成长记录加载中...</div>
<div class="module-list" data-growth-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">
<h2>接口状态</h2>
<p>Apifox 当前未展开成长记录列表和详情的元素 DTO,页面不猜测 <code>recordId</code>、标题或权限字段,因此不开放编辑和删除入口。</p>
<h2 tabindex="-1" data-growth-detail-heading>记录详情</h2>
<div data-growth-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条成长记录查看详情。</div>
</div>
</section>
</div>
</div>
@@ -71,6 +72,7 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/growth-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+18 -17
View File
@@ -8,10 +8,10 @@
<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-invite-page data-feature-status="pending" data-feature-message="家谱成员邀请服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-family-invite-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -26,13 +26,13 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Invite</div>
<div class="module-kicker">邀请家人</div>
<div class="module-title-row">
<div>
<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>
@@ -45,19 +45,20 @@
</aside>
<div class="module-main">
<section class="module-panel">
<h2>邀请方式</h2>
<div class="module-grid two">
<div class="module-card">
<span class="icon"></span>
<h3>邀请码</h3>
<p>当前邀请码:<b data-invite-code>加载中...</b></p>
</div>
<div class="module-card">
<span class="icon"></span>
<h3>邀请链接</h3>
<p data-invite-link>邀请链接加载中...</p>
</div>
<h2>生成邀请</h2>
<form class="editor-form" data-invite-form novalidate>
<div class="editor-field"><label for="invitePerson">关联人物(可选)</label><select id="invitePerson" data-invite-person><option value="">人物加载中</option></select></div>
<div class="bottom-actions"><button class="btn primary" type="submit">生成邀请链接</button><div class="form-status" data-invite-status role="status" aria-live="polite">正在读取邀请服务…</div></div>
</form>
<div class="editor-form" data-invite-result hidden>
<div class="editor-field"><label for="inviteLink">一次性邀请链接</label><input id="inviteLink" data-invite-link readonly /></div>
<button class="btn outline" type="button" data-invite-copy>复制链接</button>
</div>
<div class="module-card" data-invite-redeem hidden><h3>接受家谱邀请</h3><p>邀请令牌:<span data-invite-token></span></p><button class="btn primary" type="button" data-invite-redeem-submit>确认加入</button></div>
</section>
<section class="module-panel">
<h2>我发出的邀请</h2>
<div class="module-list" data-invite-list><div class="api-state api-state--loading" data-api-state="loading">正在读取邀请记录…</div></div>
</section>
</div>
</div>
@@ -72,7 +73,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/family-invite-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+10 -16
View File
@@ -8,10 +8,10 @@
<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-feature-status="pending" data-feature-message="加入家谱服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-my-join-applies-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -26,13 +26,11 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Join Family</div>
<div class="module-kicker">加入家谱</div>
<div class="module-title-row">
<div>
<h1>加入家谱</h1>
<p>
输入亲人分享的邀请码,提交后进入个人中心消息流程等待管理员审核。
</p>
<p>查看加入申请进度;待审核申请可以撤销后重新提交。</p>
</div>
<a
class="btn primary magnetic"
@@ -51,16 +49,12 @@
</aside>
<div class="module-main">
<section class="module-panel">
<h2>我的加入申请</h2>
<div class="module-list" data-join-apply-list="mine">
<a class="module-row" href="join-genealogy.html">
<div>
<h3>提交新的加入申请</h3>
<p>输入家谱 ID 和关系说明,等待管理员审核。</p>
</div>
<span class="pill">去申请</span>
</a>
<div class="module-title-row">
<h2>我的加入申请</h2>
<button class="btn ghost" type="button" data-join-apply-refresh>刷新</button>
</div>
<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>
</div>
@@ -75,7 +69,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/join-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+32 -50
View File
@@ -8,10 +8,10 @@
<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-feature-status="pending" data-feature-message="入谱审核服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-join-review-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -20,27 +20,18 @@
><a class="active" href="profile-join-review.html">入谱审核</a>
</nav>
<div class="nav-actions">
<a href="profile-messages.html">返回消息</a
><a
class="btn primary magnetic"
href="#"
data-layer-confirm="确认批量处理当前审核申请?"
data-confirm-title="全部处理"
>全部处理</a
>
<a href="profile-family-admin.html" data-genealogy-context-link>返回家族管理</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Review</div>
<div class="module-kicker">入谱审核</div>
<div class="module-title-row">
<div>
<h1>入谱审核</h1>
<p>
个人中心专用审核页,处理加入家谱、绑定账号和成员资料补全申请。
</p>
<p>审核当前家谱的加入申请。只有家谱管理者可以执行通过或拒绝。</p>
</div>
</div>
</div>
@@ -49,47 +40,38 @@
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-messages.html">消息中心</a
><a class="active" href="profile-join-review.html">入谱审核</a
><a href="profile-invite.html">邀请家人</a
><a href="profile-admin-permissions.html">管理员权限</a>
><a class="active" href="profile-join-review.html" data-genealogy-context-link>入谱审核</a
><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">
<h2>待审核申请</h2>
<div class="module-title-row">
<h2>待审核申请</h2>
<button class="btn ghost" type="button" data-join-review-refresh>刷新</button>
</div>
<div class="form-note" role="status" aria-live="polite" data-join-review-status></div>
<div class="module-list" data-join-apply-list="pending">
<a class="module-row" href="profile-join-review.html"
><div>
<h3>某某人申请加入四川武胜汤氏族</h3>
<p>提交时间:2026.05.26 · 关系说明待确认</p>
</div>
<span class="pill">审核</span></a
><a class="module-row" href="profile-join-review.html"
><div>
<h3>张三申请绑定成员账号</h3>
<p>需要核对手机号和成员资料</p>
</div>
<span class="pill">处理</span></a
>
<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="bottom-actions">
<a
class="btn primary magnetic"
href="#"
data-layer-confirm="同意当前入谱申请?"
data-confirm-title="审核通过"
>同意</a
><a
class="btn ghost magnetic"
href="#"
data-layer-prompt="请输入拒绝原因"
data-prompt-title="拒绝申请"
data-prompt-type="textarea"
>拒绝</a
>
</div>
<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>
@@ -104,7 +86,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/join-review-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+14 -10
View File
@@ -11,14 +11,14 @@
<body class="page-profile-module" data-memo-edit-page>
<header class="site-header">
<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>
<a class="brand magnetic" href="index.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-memo.html" data-genealogy-context-link>返回记录</a><button class="btn primary magnetic" type="submit" form="memo-edit-form">保存</button></div>
<div class="nav-actions"><a href="profile-memo.html" data-genealogy-context-link>返回记录</a><button class="btn primary magnetic" type="submit" form="memo-edit-form" data-memo-editor-action hidden>保存</button></div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container"><div class="module-kicker">Memo Editor</div><div class="module-title-row"><div><h1>新建备忘录</h1><p>仅提交 Apifox PC 已核验的请求体字段</p></div></div></div>
<div class="container"><div class="module-kicker">编辑备忘录</div><div class="module-title-row"><div><h1 data-memo-editor-title>新建备忘录</h1><p>附件由上传组件自动生成内部编号,页面不允许手工填写</p></div></div></div>
</section>
<section class="section">
<div class="container module-layout">
@@ -26,20 +26,22 @@
<div class="module-main">
<section class="module-panel">
<h2>备忘内容</h2>
<form id="memo-edit-form" class="editor-form layui-form" data-memo-form>
<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="text" placeholder="可选;按后端定义填写" /></div>
<div class="editor-field"><label for="memoCompleted">是否已完成</label><input id="memoCompleted" name="completed" type="text" placeholder="可选;按后端定义填写" /></div>
<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" /><p id="memoMediaStatus" class="upload-status">未选择文件</p></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-attachment-editor 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>
<div class="editor-field"><label for="memoStatus">状态</label><input id="memoStatus" name="status" type="text" placeholder="可选;按后端定义填写" /></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><span class="form-status" data-memo-form-status></span></div>
<input name="status" type="hidden" value="0" />
<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 data-memo-editor-placeholder><div class="api-state api-state--loading" role="status" aria-live="polite" aria-busy="true">正在加载备忘录编辑信息…</div></div>
</section>
</div>
</div>
@@ -55,7 +57,9 @@
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/memo-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+11 -7
View File
@@ -11,15 +11,15 @@
<body class="page-profile-module" data-memo-page>
<header class="site-header">
<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>
<a class="brand magnetic" href="index.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>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Memo</div>
<div class="module-kicker">备忘录</div>
<div class="module-title-row"><div><h1>备忘录</h1><p>记录家族事务、纪念事项、提醒时间和完成状态。</p></div></div>
</div>
</section>
@@ -29,12 +29,15 @@
<div class="module-main">
<section class="module-panel">
<h2>备忘列表</h2>
<!-- 备忘录由 ApiClient.memos 渲染。 -->
<div class="module-list" data-memo-list><div class="api-empty">备忘录加载中...</div></div>
<div class="module-list" data-memo-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">
<h2>接口状态</h2>
<p>Apifox 当前未展开备忘录列表和详情的元素 DTO,页面不猜测 <code>memoId</code>、标题或权限字段,因此不开放编辑和删除入口。</p>
<h2 tabindex="-1" data-memo-detail-heading>备忘详情</h2>
<div data-memo-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条备忘录查看详情。</div>
</div>
</section>
</div>
</div>
@@ -49,6 +52,7 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/memo-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+42 -50
View File
@@ -3,38 +3,37 @@
<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" />
<link rel="stylesheet" href="public/js/wangeditor5/css/style.css" />
</head>
<body class="page-profile-module" data-merit-edit-page data-feature-status="pending" data-feature-message="功德录服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-merit-edit-page>
<header class="site-header">
<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
>
<a class="brand magnetic" href="index.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-merit.html">功德录</a>
<a href="profile-content.html">内容发布</a>
<a class="active" href="profile-merit.html" data-genealogy-context-link>功德录</a>
</nav>
<div class="nav-actions">
<a href="profile-merit.html">返回功德录</a
><button class="btn primary magnetic" type="submit" form="merit-edit-form">保存</button>
<a href="profile-merit.html" data-genealogy-context-link>返回功德录</a>
<button class="btn primary magnetic" type="submit" form="merit-edit-form" data-merit-editor-action hidden>保存</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Merit Editor</div>
<div class="module-kicker">编辑功德记录</div>
<div class="module-title-row">
<div>
<h1>添加功德人</h1>
<p>
记录功德人姓名、事迹、时间和图片资料,正文使用富文本编辑器。
</p>
<h1 data-merit-editor-title>新增功德记录</h1>
<p>选择功德类型并填写金额和说明;标有 * 的项目为必填。</p>
</div>
</div>
</div>
@@ -42,70 +41,62 @@
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-merit.html">功德录</a
><a href="profile-gift.html">贺礼邀请</a
><a href="profile-growth.html">成长日志</a>
<a href="profile-content.html">内容发布</a>
<a class="active" href="profile-merit.html" data-genealogy-context-link>功德录</a>
<a href="profile-gift.html" data-genealogy-context-link>贺礼邀请</a>
<a href="profile-growth.html" data-genealogy-context-link>成长日志</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>功德资料</h2>
<form id="merit-edit-form" class="editor-form layui-form" data-merit-form>
<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
><input
id="meritName"
name="donorName"
type="text"
placeholder="请输入姓名"
required
/>
<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">
<label for="meritDate">日期</label
><input
id="meritDate"
name="meritTime"
type="text"
placeholder="请选择日期"
/>
<label for="meritTime">功德时间</label>
<input id="meritTime" name="meritTime" type="datetime-local" />
</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">
<label for="meritContent">事迹内容</label
><textarea
id="meritContent"
name="meritContent"
class="js-rich-editor"
placeholder="请输入内容"
></textarea>
<label for="meritContent">事迹内容</label>
<textarea id="meritContent" name="meritContent" class="js-rich-editor" placeholder="可选,记录功德事迹"></textarea>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="meritType">功德类型</label>
<select id="meritType" name="meritType">
<option value="donation">捐赠</option>
<option value="service">义务服务</option>
<option value="repair">修缮</option>
<option value="public">公益</option>
<option value="other">其他</option>
</select>
</div>
<div class="editor-field">
<label for="meritAmount">金额</label>
<input id="meritAmount" name="amount" type="number" step="0.01" placeholder="请输入金额" />
<input id="meritAmount" name="amount" type="number" step="any" placeholder="可选;后端未限定精度、范围或正负" />
</div>
</div>
<div class="editor-field">
<label for="meritSortOrder">排序值</label>
<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"
>取消</a
>
<button class="btn primary magnetic" type="submit">保存</button>
<a class="btn ghost magnetic" href="profile-merit.html" data-genealogy-context-link>取消</a>
</div>
</form>
<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>
@@ -116,13 +107,14 @@
<script src="public/js/lay-config.js"></script>
<script src="public/js/wangeditor5/index.js"></script>
<script src="public/js/rich-editor.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<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/media-display.js"></script>
<script src="public/js/merit-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+25 -42
View File
@@ -8,32 +8,31 @@
<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-merit-page data-feature-status="pending" data-feature-message="功德录服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-merit-page>
<header class="site-header">
<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
>
<a class="brand magnetic" href="index.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-merit.html">功德录</a>
<a href="profile-content.html">内容发布</a>
<a class="active" href="profile-merit.html" data-genealogy-context-link>功德录</a>
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="profile-merit-edit.html"
>添加功德人</a
>
<a href="profile-content.html">返回发布中心</a>
<a class="btn primary magnetic" href="profile-merit-edit.html" data-genealogy-context-link data-merit-create-link hidden>新增功德记录</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Merit</div>
<div class="module-kicker">功德录</div>
<div class="module-title-row">
<div>
<h1>功德录</h1>
<p>记录功德人、事迹、时间和相关图片,沉淀家族贡献。</p>
<p>记录功德人、事迹、时间和金额,沉淀家族贡献。</p>
</div>
</div>
</div>
@@ -41,40 +40,23 @@
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a class="active" href="profile-merit.html">功德录</a
><a href="profile-gift.html">贺礼邀请</a
><a href="profile-growth.html">成长日志</a
><a href="profile-memo.html">备忘录</a>
<a href="profile-content.html">内容发布</a>
<a class="active" href="profile-merit.html" data-genealogy-context-link>功德录</a>
<a href="profile-gift.html" data-genealogy-context-link>贺礼邀请</a>
<a href="profile-growth.html" data-genealogy-context-link>成长日志</a>
<a href="profile-memo.html" data-genealogy-context-link>备忘录</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>功德</h2>
<!-- 功德记录由 /merit-records 接口渲染 -->
<div class="module-list" data-merit-list>
<div class="api-empty">功德记录加载中...</div>
<h2>功德记录</h2>
<div class="module-list" data-merit-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">
<h2>添加功德人</h2>
<div class="form-like">
<p>
<span>姓名</span><b>待输入</b
><a class="link-btn" href="profile-merit-edit.html">填写</a>
</p>
<p>
<span>内容</span><b>待输入</b
><a class="link-btn" href="profile-merit-edit.html">编辑</a>
</p>
<p>
<span>附件</span><b>可添加图片</b
><a
class="link-btn"
href="#"
data-layer-msg="请选择要上传的附件"
>添加</a
>
</p>
<h2 tabindex="-1" data-merit-detail-heading>记录详情</h2>
<div data-merit-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条功德记录查看详情。</div>
</div>
</section>
</div>
@@ -84,13 +66,14 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<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/media-display.js"></script>
<script src="public/js/merit-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+49 -33
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -11,35 +11,33 @@
<body class="page-profile-module">
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<a class="brand magnetic" href="index.html">
<img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" />
</a>
<nav class="nav-links">
<a href="profile.html">首页</a
><a href="profile-families.html">数字家谱</a
><a href="plaza.html">家谱广场</a
><a href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a><a href="app.html">应用下载</a
><a href="help.html">帮助中心</a><a href="about.html">关于我们</a>
<a href="profile.html">个人中心</a>
<a href="profile-families.html">我的家谱</a>
<a href="plaza.html">家谱广场</a>
<a href="profile-content.html">家族文化</a>
<a href="surname.html">姓氏百科</a>
<a href="app.html">应用下载</a>
<a href="help.html">帮助中心</a>
<a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a class="active" href="profile.html">个人中心</a
><a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
<a class="active" href="profile-messages.html">消息中心</a>
<a class="btn primary magnetic" href="profile-create-family.html">创建家谱</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Messages</div>
<div class="module-kicker">消息中心</div>
<div class="module-title-row">
<div>
<h1>消息中心</h1>
<p>
查看当前账号的消息通知,并同步未读数量。
</p>
<p>查看当前账号的通知详情,并管理未读状态。</p>
</div>
<a class="btn ghost magnetic" href="profile.html">返回个人中心</a>
</div>
@@ -48,34 +46,52 @@
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-families.html">我的家谱</a
><a class="active" href="profile-messages.html">消息中心</a
><a href="profile-family-admin.html">家族管理</a
><a href="profile-content.html">内容发布</a
><a href="profile-data.html">个人资料</a
><a href="profile-services.html">帮助与服务</a>
<a href="profile-families.html">我的家谱</a>
<a class="active" href="profile-messages.html">消息中心</a>
<a href="profile-family-admin.html">家族管理</a>
<a href="profile-content.html">内容发布</a>
<a href="profile-data.html">个人资料</a>
<a href="profile-services.html">帮助与服务</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>通知列表</h2>
<p class="api-empty" data-notification-status>正在加载消息…</p>
<p class="module-note">未读数量:<b data-notification-unread-count>0</b></p>
<div class="module-list" data-notification-list>
<div class="api-empty">正在加载消息…</div>
<div class="module-title-row">
<div>
<h2>通知列表</h2>
<p class="module-note">未读数量:<b data-notification-unread-count>0</b></p>
</div>
<label class="editor-field">
<span>阅读状态</span>
<select data-notification-filter>
<option value="">全部通知</option>
<option value="0">仅未读</option>
<option value="1">仅已读</option>
</select>
</label>
</div>
<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-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>
<div data-notification-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条通知查看详情。</div>
</div>
</section>
<section class="module-panel">
<h2>阅读管理</h2>
<div class="module-grid two">
<button class="module-card module-action-card" type="button" data-notification-read-all>
<span class="icon"></span>
<h3>全部标记已读</h3>
<p>使用当前 PC 的全部已读接口</p>
<p>将当前账号所有未读通知标记为已读</p>
</button>
<button class="module-card module-action-card" type="button" data-notification-refresh>
<span class="icon"></span>
<h3>刷新消息</h3>
<p>重新读取通知列表与未读数量。</p>
<p>重新读取通知列表、详情与未读数量。</p>
</button>
</div>
</section>
@@ -86,7 +102,7 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
+120 -21
View File
@@ -3,33 +3,132 @@
<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-relative-edit-page>
<header class="site-header"><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-relative.html" data-genealogy-context-link>亲友往来</a></nav>
<div class="nav-actions"><a href="profile-relative.html" data-genealogy-context-link>返回记录</a><button class="btn primary magnetic" type="submit" form="relative-edit-form">保存</button></div>
</div></header>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-relative.html" data-genealogy-context-link>亲友往来</a>
</nav>
<div class="nav-actions">
<a href="profile-relative.html" data-genealogy-context-link>返回记录</a>
<button class="btn primary magnetic" type="submit" form="relative-edit-form" data-relative-editor-action hidden>保存</button>
</div>
</div>
</header>
<main>
<section class="module-hero"><div class="container"><div class="module-kicker">Relative Record</div><div class="module-title-row"><div><h1>新建亲友往来</h1><p>仅提交 Apifox PC 的 RelativeRecordBody 字段。</p></div></div></div></section>
<section class="section"><div class="container module-layout">
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-growth.html" data-genealogy-context-link>成长记录</a><a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a><a href="profile-memo.html">备忘录</a></aside>
<div class="module-main"><section class="module-panel"><h2>记录内容</h2>
<form id="relative-edit-form" class="editor-form layui-form" data-relative-form>
<div class="editor-two"><div class="editor-field"><label for="relativeName">亲友姓名</label><input id="relativeName" name="relativeName" type="text" required /></div><div class="editor-field"><label for="relationName">关系名称</label><input id="relationName" name="relationName" type="text" placeholder="可选" /></div></div>
<div class="editor-two"><div class="editor-field"><label for="eventName">事件名称</label><input id="eventName" name="eventName" type="text" placeholder="可选" /></div><div class="editor-field"><label for="eventTime">事件时间</label><input id="eventTime" name="eventTime" type="text" placeholder="可选;使用带时区的 ISO 8601 时间" /></div></div>
<div class="editor-two"><div class="editor-field"><label for="giftAmount">礼金金额</label><input id="giftAmount" name="giftAmount" type="number" step="any" placeholder="可选" /></div><div class="editor-field"><label for="relativeSortOrder">排序值</label><input id="relativeSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" /></div></div>
<div class="editor-field"><label for="relativeContent">记录内容</label><textarea id="relativeContent" name="recordContent" placeholder="可选"></textarea></div>
<div class="editor-two"><div class="editor-field"><label for="relativeMediaFiles">附件文件</label><input id="relativeMediaOssIds" name="mediaOssIds" type="hidden" /><label class="upload-control" for="relativeMediaFiles">选择附件</label><input id="relativeMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-upload-target="#relativeMediaOssIds" data-upload-status="#relativeMediaStatus" /><p id="relativeMediaStatus" class="upload-status">未选择文件</p></div><div class="editor-field"><label for="relativeStatus">状态</label><input id="relativeStatus" name="status" type="text" placeholder="可选;按后端定义填写" /></div></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>
</section></div>
</div></section>
<section class="module-hero">
<div class="container">
<div class="module-kicker">编辑亲友往来</div>
<div class="module-title-row">
<div>
<h1 data-relative-editor-title>新建亲友记录</h1>
<p>附件由上传组件自动生成内部编号,页面不允许手工填写。</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a>
<a href="profile-growth.html" data-genealogy-context-link>成长记录</a>
<a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a>
<a href="profile-memo.html">备忘录</a>
</aside>
<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 novalidate>
<div class="editor-two">
<div class="editor-field">
<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">
<label for="relationName">关系名称</label>
<input id="relationName" name="relationName" type="text" placeholder="可选,例如长辈、同学" />
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="eventName">事件名称</label>
<input id="eventName" name="eventName" type="text" placeholder="可选,例如寿宴、婚礼" />
</div>
<div class="editor-field">
<label for="eventTime">事件时间</label>
<input id="eventTime" name="eventTime" type="datetime-local" />
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="giftAmount">礼金金额</label>
<input id="giftAmount" name="giftAmount" type="number" step="any" placeholder="可选;后端未限定精度或正负" />
</div>
<div class="editor-field">
<label for="relativeSortOrder">排序值</label>
<input id="relativeSortOrder" name="sortOrder" type="number" step="1" placeholder="可选;安全整数" />
</div>
</div>
<div class="editor-field">
<label for="relativeContent">记录内容</label>
<textarea id="relativeContent" name="recordContent" placeholder="可选,记录往来详情"></textarea>
</div>
<div class="editor-field">
<label for="relativeMediaFiles">附件文件</label>
<input id="relativeMediaOssIds" name="mediaOssIds" type="hidden" />
<label class="upload-control" for="relativeMediaFiles">选择附件</label>
<input
id="relativeMediaFiles"
class="upload-input"
type="file"
multiple
data-upload-multiple="true"
data-attachment-editor data-upload-target="#relativeMediaOssIds"
data-upload-status="#relativeMediaStatus"
data-relative-media-input
/>
<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>
</div>
</form>
<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>
</section>
</main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><script src="public/js/profile-common.js?v=20260725-entry"></script><script src="config.js"></script><script src="utils/StorageUtil.js"></script><script src="utils/axios.js"></script><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/md5.js"></script><script src="public/js/upload-pages.js"></script><script src="public/js/relative-pages.js"></script><script src="public/js/page-effects.js"></script>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/relative-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+63 -14
View File
@@ -9,21 +9,70 @@
<link rel="stylesheet" href="public/css/profile-module.css" />
</head>
<body class="page-profile-module" data-relative-page>
<header class="site-header"><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-relative.html" data-genealogy-context-link>亲友往来</a></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></div>
</div></header>
<main>
<section class="module-hero"><div class="container"><div class="module-kicker">Relative Records</div><div class="module-title-row"><div><h1>亲友往来</h1><p>记录亲友关系、事项、时间、礼金和附件 OSS ID。</p></div></div></div></section>
<section class="section"><div class="container module-layout">
<aside class="module-nav"><a href="profile-content.html">内容发布</a><a href="profile-growth.html" data-genealogy-context-link>成长记录</a><a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a><a href="profile-memo.html">备忘录</a></aside>
<div class="module-main">
<section class="module-panel"><h2>记录列表</h2><div class="module-list" data-relative-list><div class="api-empty">亲友往来加载中...</div></div></section>
<section class="module-panel"><h2>接口状态</h2><p>Apifox 未展开列表和详情元素 DTO,页面不猜测 <code>relativeId</code>、展示字段或权限字段,因此不开放编辑和删除入口。</p></section>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-relative.html" data-genealogy-context-link>亲友往来</a>
</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 data-relative-create-link hidden>新建记录</a>
</div>
</div></section>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">亲友往来</div>
<div class="module-title-row">
<div>
<h1>亲友往来</h1>
<p>记录亲友关系、往来事项、时间、礼金和附件。</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a>
<a href="profile-growth.html" data-genealogy-context-link>成长记录</a>
<a class="active" href="profile-relative.html" data-genealogy-context-link>亲友往来</a>
<a href="profile-memo.html">备忘录</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>记录列表</h2>
<div class="module-list" data-relative-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">
<h2 tabindex="-1" data-relative-detail-heading>记录详情</h2>
<div data-relative-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">请选择一条亲友记录查看详情。</div>
</div>
</section>
</div>
</div>
</section>
</main>
<script src="public/js/jquery360.js"></script><script src="public/layui/layui.js"></script><script src="public/js/lay-config.js"></script><script src="public/js/profile-common.js?v=20260725-entry"></script><script src="config.js"></script><script src="utils/StorageUtil.js"></script><script src="utils/axios.js"></script><script src="utils/AxiosRequestUtil.js"></script><script src="utils/ApiClient.js"></script><script src="public/js/relative-pages.js"></script><script src="public/js/page-effects.js"></script>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/relative-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+22 -23
View File
@@ -11,7 +11,7 @@
<body class="page-profile-module" data-security-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -20,18 +20,18 @@
</nav>
<div class="nav-actions">
<a href="profile-services.html">返回服务中心</a
><a class="btn primary magnetic" href="#" data-security-logout>退出登录</a>
><button class="btn primary magnetic" type="button" data-security-logout>退出登录</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Security</div>
<div class="module-kicker">安全设置</div>
<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,16 @@
<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 ghost magnetic" type="button" data-security-logout>退出登录</button>
<span class="api-status" data-security-status>等待验证</span>
<button class="btn danger magnetic" type="submit" data-security-submit>提交注销</button>
<div class="form-status" data-security-status role="status" aria-live="polite"></div>
</div>
</form>
</section>
@@ -155,10 +154,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>
+70 -43
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -8,15 +8,15 @@
<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-vip-page data-feature-status="pending" data-feature-message="会员与在线服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-vip-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<nav class="nav-links">
<a href="profile.html">首页</a
><a href="profile-families.html">数字家谱</a
<a href="profile.html">个人中心</a
><a href="profile-families.html">我的家谱</a
><a href="plaza.html">家谱广场</a
><a href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a><a href="app.html">应用下载</a
@@ -24,7 +24,7 @@
><a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a href="profile.html">个人中心</a
<a href="profile-data.html">账户资料</a
><a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
@@ -34,16 +34,13 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Services</div>
<div class="module-kicker">帮助与服务</div>
<div class="module-title-row">
<div>
<h1>帮助与服务</h1>
<p>
整合 APP
“我的”页中的应用推广、意见反馈、安全设置、帮助中心和应用分享。
</p>
<p>集中处理推广收益、意见反馈、账号安全、帮助说明和会员服务。</p>
</div>
<a class="btn primary magnetic" href="profile-feedback.html"
<a class="btn primary magnetic" href="profile-feedback.html" data-feature-link="available"
>提交反馈</a
>
</div>
@@ -63,12 +60,18 @@
<section class="module-panel">
<h2>常用服务</h2>
<div class="module-grid">
<a class="module-card tilt-card" href="profile-share.html"
><span class="icon"></span>
<h3>应用分享</h3>
<p>邀请好友使用,了解应用下载和分享奖励</p></a
>
<a class="module-card tilt-card" href="profile-feedback.html"
<a class="module-card tilt-card" href="profile-earnings.html" data-feature-link="available">
<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>推荐设置正在完善,目前不会保存开关状态。</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>
<p>提交产品问题、资料异常和改进建议。</p></a
@@ -96,40 +99,63 @@
</div>
</section>
<section class="module-panel">
<h2>会员服务</h2>
<p>选择适合当前家谱的会员套餐,创建订单后继续完成支付</p>
<div class="vip-package-list" data-vip-package-list>
<div class="api-empty">会员套餐加载中</div>
<h2>平台设置与协议</h2>
<p>集中查看平台说明、服务支持和协议入口;正式内容以平台发布版本为准</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>
<form class="editor-form vip-order-form" data-vip-order-form>
<div class="editor-two">
<div class="editor-field">
<label for="packageId">套餐 ID</label>
<input id="packageId" name="packageId" type="number" placeholder="请先选择套餐" required />
</div>
<div class="editor-field">
<label for="genealogyId">家谱 ID</label>
<input id="genealogyId" name="genealogyId" type="number" placeholder="可选,指定要开通的家谱" />
</div>
</div>
</section>
<section class="module-panel" id="membership" tabindex="-1">
<h2>会员服务</h2>
<p>选择适合的会员套餐并创建微信扫码支付订单。</p>
<div class="vip-package-list" data-vip-package-list>
<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 novalidate>
<p class="form-status" data-vip-selected-package role="status" aria-live="polite">尚未选择套餐</p>
<div class="editor-field">
<label for="payType">支付方式</label>
<select id="payType" name="payType">
<option value="wechat">微信支付</option>
<option value="alipay">支付宝</option>
<option value="balance">余额支付</option>
<label for="vipGenealogy">关联家谱(可选)</label>
<select id="vipGenealogy" data-vip-genealogy-options>
<option value="">家谱选项加载中</option>
</select>
</div>
<div class="bottom-actions">
<button class="btn primary magnetic" type="submit">创建会员订单</button>
<span class="api-status" data-vip-order-status>等待选择套餐</span>
<button class="btn primary magnetic" type="submit" data-vip-order-submit>创建会员订单</button>
<div class="form-status" data-vip-order-status role="status" aria-live="polite"></div>
</div>
</form>
</section>
<section class="module-panel" data-vip-payment-panel hidden>
<h2>微信扫码支付</h2>
<div data-vip-payment-qr></div>
<p data-vip-payment-text role="status" aria-live="polite"></p>
</section>
<section class="module-panel">
<h2>会员订单</h2>
<div class="module-title-row">
<h2>会员订单</h2>
<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">
@@ -162,8 +188,9 @@
<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/qrcode.min.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>
+31 -22
View File
@@ -3,25 +3,24 @@
<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"
<a class="brand magnetic" href="index.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-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>
@@ -29,11 +28,11 @@
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Share</div>
<div class="module-kicker">应用分享</div>
<div class="module-title-row">
<div>
<h1>应用分享</h1>
<p>邀请好友创建和维护家谱,查看应用下载、分享奖励和推广入口</p>
<h1>应用分享与收益</h1>
<p>应用邀请暂以移动端为主;这里可查看推广收益、流水和提现记录</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,30 +49,41 @@
<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>邀请奖励规则正在完善,当前暂不展示奖励金额。</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>分享链接和二维码暂未开放,请先使用移动端分享。</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">
分享记录暂未开放;推广收益和提现结果可在下方查看。
</div>
</div>
</section>
<section class="module-panel">
<h2>收益与提现</h2>
<div class="module-grid two">
<a class="module-card tilt-card" href="profile-earnings.html" data-feature-link="available">
<span class="icon"></span>
<h3>推广收益</h3>
<p>查看可用余额、冻结金额和不可变收益流水。</p>
</a>
<a class="module-card tilt-card" href="profile-earnings.html" data-feature-link="available">
<span class="icon"></span>
<h3>提现记录</h3>
<p>上传微信收款码,提交提现并跟踪处理状态。</p>
</a>
</div>
</section>
</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>
+147 -39
View File
@@ -11,7 +11,7 @@
<body class="page-profile-module" data-lineage-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.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">
@@ -21,14 +21,14 @@
</nav>
<div class="nav-actions">
<a href="profile-family-home.html" data-genealogy-context-link>返回家谱主页</a
><a class="btn primary magnetic" href="#lineage-member-form">添加成员</a>
><a class="btn primary magnetic" href="#lineage-member-form" data-lineage-management hidden>添加成员</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Tree Mode</div>
<div class="module-kicker">世系管理</div>
<div class="module-title-row">
<div>
<h1>世系图</h1>
@@ -37,7 +37,7 @@
</p>
</div>
<a class="btn ghost magnetic" href="profile-generation.html" data-genealogy-context-link
>管理世代</a
>管理字辈</a
>
</div>
</div>
@@ -53,7 +53,13 @@
<div class="module-main">
<section class="module-panel">
<h2>树谱操作</h2>
<div class="module-grid">
<div class="lineage-toolbar lineage-current-toolbar">
<label for="lineageCurrentPerson">当前成员</label>
<select id="lineageCurrentPerson" data-lineage-person-options>
<option value="">请选择当前成员</option>
</select>
</div>
<div class="module-grid" data-lineage-management hidden>
<button class="module-card module-action-card tilt-card" type="button" data-lineage-relation="parents"
><span class="icon"></span>
<h3>添加父亲 / 母亲</h3>
@@ -76,33 +82,38 @@
<p>先选择成员,再为当前成员新增同辈关系。</p></button
>
</div>
<div class="lineage-toolbar">
<select data-lineage-person-options aria-label="选择当前成员">
<option value="">请选择当前成员</option>
</select>
</div>
</section>
<section class="module-panel">
<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">
<input type="search" data-lineage-keyword placeholder="输入姓名、别名或人物编号搜索" />
<div class="lineage-toolbar lineage-filter-toolbar">
<input type="search" data-lineage-keyword aria-label="按姓名、别名或人物编号搜索成员" placeholder="输入姓名、别名或人物编号搜索" />
<select data-lineage-generation-filter aria-label="按世代筛选">
<option value="">全部世代</option>
</select>
<select data-lineage-status-filter aria-label="按人物状态筛选">
<option value="">全部人物状态</option>
<option value="0">健在</option>
<option value="1">已故</option>
<option value="2">未知</option>
</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>
@@ -110,15 +121,49 @@
<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">
<h2>新增成员</h2>
<form id="lineage-member-form" class="editor-form" data-lineage-form>
<section class="module-panel" data-lineage-management hidden>
<h2>新增 / 编辑成员</h2>
<form id="lineage-member-form" class="editor-form" data-lineage-form novalidate>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-person-name">姓名</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>
<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>
<input id="lineage-avatar-oss-id" name="avatarOssId" type="hidden" />
<label class="upload-control" for="lineage-avatar-file">选择头像文件</label>
<input
id="lineage-avatar-file"
class="upload-input"
type="file"
accept="image/*"
data-upload-target="#lineage-avatar-oss-id"
data-upload-status="#lineage-avatar-status"
data-upload-mode="resumable"
data-upload-biz-type="avatar"
data-upload-usage-scene="lineage_avatar"
/>
<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">姓名 <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">
@@ -126,55 +171,116 @@
<select id="lineage-sex" name="sex">
<option value="0"></option>
<option value="1"></option>
<option value="2" selected>未知</option>
</select>
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-generation-no">世代序号</label>
<input id="lineage-generation-no" name="generation" type="number" min="1" placeholder="例如 3" />
<label for="lineage-generation-no">世代</label>
<select id="lineage-generation-no" name="generation" data-lineage-generation>
<option value="">请选择世代</option>
</select>
</div>
<div class="editor-field">
<label for="lineage-generation-name">字辈</label>
<input id="lineage-generation-name" name="generationName" type="text" placeholder="例如 忠" />
<input id="lineage-generation-name" name="generationName" type="text" readonly />
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-person-no">人物编号</label>
<input id="lineage-person-no" name="personNo" type="text" placeholder="可选" />
<input id="lineage-person-no" name="personNo" type="text" placeholder="可选;留空由系统生成" />
</div>
<div class="editor-field">
<label for="lineage-alias-name">别名</label>
<input id="lineage-alias-name" name="aliasName" type="text" placeholder="可选" />
</div>
</div>
<div class="editor-field">
<label for="lineage-sort-order">排序值</label>
<input id="lineage-sort-order" name="sortOrder" type="number" step="1" placeholder="可选,数值越小越靠前" />
</div>
<div class="editor-field">
<label for="lineage-relation-name">关系称谓</label>
<input id="lineage-relation-name" name="relationName" type="text" placeholder="添加配偶时可选,例如 妻、夫" />
<div class="editor-two">
<div class="editor-field">
<label for="lineage-father-id">父亲</label>
<select id="lineage-father-id" name="fatherId" data-lineage-parent-option data-empty-label="请选择父亲">
<option value="">请选择父亲</option>
</select>
</div>
<div class="editor-field">
<label for="lineage-mother-id">母亲</label>
<select id="lineage-mother-id" name="motherId" data-lineage-parent-option data-empty-label="请选择母亲">
<option value="">请选择母亲</option>
</select>
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-birth-date">出生日期</label>
<input id="lineage-birth-date" name="birthDate" type="date" />
<label for="lineage-birth-date">出生时间</label>
<input id="lineage-birth-date" name="birthDate" type="datetime-local" />
</div>
<div class="editor-field">
<label for="lineage-death-date">逝世日期</label>
<input id="lineage-death-date" name="deathDate" type="date" />
<label for="lineage-birth-lunar">出生历法</label>
<select id="lineage-birth-lunar" name="birthLunar">
<option value="0" selected>公历</option>
<option value="1">农历</option>
</select>
</div>
</div>
<div class="editor-field">
<label for="lineage-introduction">简介</label>
<label for="lineage-birth-place">出生地</label>
<input id="lineage-birth-place" name="birthPlace" type="text" placeholder="可选" />
</div>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-death-date">逝世时间</label>
<input id="lineage-death-date" name="deathDate" type="datetime-local" />
</div>
<div class="editor-field">
<label for="lineage-death-lunar">逝世历法</label>
<select id="lineage-death-lunar" name="deathLunar">
<option value="0" selected>公历</option>
<option value="1">农历</option>
</select>
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-death-place">逝世地</label>
<input id="lineage-death-place" name="deathPlace" type="text" placeholder="可选" />
</div>
<div class="editor-field">
<label for="lineage-burial-place">安葬地</label>
<input id="lineage-burial-place" name="burialPlace" type="text" placeholder="可选" />
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="lineage-person-status">人物状态</label>
<select id="lineage-person-status" name="personStatus">
<option value="0">健在</option>
<option value="1">已故</option>
<option value="2" selected>未知</option>
</select>
</div>
<div class="editor-field">
<label for="lineage-sort-order">排序值</label>
<input id="lineage-sort-order" name="sortOrder" type="number" step="1" placeholder="可选,数值越小越靠前" />
</div>
</div>
<div class="editor-field" data-lineage-spouse-field hidden>
<label for="lineage-relation-name">配偶关系称谓</label>
<input id="lineage-relation-name" name="relationName" type="text" placeholder="可选,例如 妻、夫" />
</div>
<div class="editor-field">
<label for="lineage-introduction">人物简介</label>
<textarea id="lineage-introduction" name="biography" placeholder="记录成员生平简介"></textarea>
</div>
<div class="editor-field">
<label for="lineage-remark">备注</label>
<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>
@@ -191,6 +297,8 @@
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/lineage-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
+130
View File
@@ -0,0 +1,130 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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-video-edit-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="index.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-video.html" data-genealogy-context-link>视频</a>
</nav>
<div class="nav-actions">
<a href="profile-video.html" data-genealogy-context-link>返回视频列表</a>
<button class="btn primary magnetic" type="submit" form="video-edit-form" data-video-management hidden>保存视频</button>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">编辑视频</div>
<div class="module-title-row">
<div>
<h1 data-video-editor-title>发布视频</h1>
<p>视频文件由上传组件自动关联,无需填写内部编号。</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a>
<a class="active" href="profile-video.html" data-genealogy-context-link>视频</a>
<a href="profile-family-home.html" data-genealogy-context-link>家谱主页</a>
</aside>
<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 novalidate>
<div class="editor-field">
<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">
<label for="video-description">视频说明</label>
<textarea id="video-description" name="videoDesc" placeholder="可选,记录视频内容"></textarea>
</div>
<div class="editor-two">
<div class="editor-field">
<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
id="video-file"
class="upload-input"
type="file"
accept="video/*"
data-attachment-editor data-upload-target="#video-oss-id"
data-upload-status="#video-file-status"
data-video-duration-target="#video-duration"
/>
<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>
<input id="video-cover-oss-id" name="coverOssId" type="hidden" />
<label class="upload-control" for="video-cover-file">选择封面图片</label>
<input
id="video-cover-file"
class="upload-input"
type="file"
accept="image/*"
data-attachment-editor 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 role="status" aria-live="polite">未选择封面</p>
</div>
</div>
<div class="editor-two">
<div class="editor-field">
<label for="video-duration">视频时长(秒)</label>
<input id="video-duration" name="durationSeconds" type="number" step="1" min="0" readonly />
<p class="upload-status">选择视频后自动读取,不会要求手工填写。</p>
</div>
<div class="editor-field">
<label for="video-sort-order">排序值</label>
<input id="video-sort-order" name="sortOrder" type="number" step="1" placeholder="可选,数值越小越靠前" />
</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>
</div>
</form>
<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>
</section>
</main>
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/md5.js"></script>
<script src="public/js/attachment-editor.js"></script>
<script src="public/js/upload-pages.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/video-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+33 -47
View File
@@ -8,33 +8,32 @@
<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-feature-status="pending" data-feature-message="家族视频服务正在开发中,当前页面仅保留设计预览。">
<body class="page-profile-module" data-video-page>
<header class="site-header">
<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
>
<a class="brand magnetic" href="index.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 href="profile-album.html">相册</a
><a class="active" href="profile-video.html">视频</a>
<a href="profile-content.html">内容发布</a>
<a href="profile-album.html">相册</a>
<a class="active" href="profile-video.html" data-genealogy-context-link>视频</a>
</nav>
<div class="nav-actions">
<a href="profile-content.html">返回发布中心</a
><a class="btn primary magnetic" href="profile-video.html"
>发布视频</a
>
<a href="profile-content.html">返回发布中心</a>
<a class="btn primary magnetic" href="profile-video-edit.html" data-genealogy-context-link data-video-management hidden>发布视频</a>
</div>
</div>
</header>
<main>
<section class="module-hero">
<div class="container">
<div class="module-kicker">Video</div>
<div class="module-kicker">家族视频</div>
<div class="module-title-row">
<div>
<h1>家族视频</h1>
<p>发布和管理家族视频,支持标题、描述、封面和上传状态维护</p>
<p>查看家族视频资料;有内容维护权限的成员可以发布、编辑和删除视频</p>
</div>
</div>
</div>
@@ -42,45 +41,26 @@
<section class="section">
<div class="container module-layout">
<aside class="module-nav">
<a href="profile-content.html">内容发布</a
><a href="profile-album.html">相册</a
><a class="active" href="profile-video.html">视频</a
><a href="profile-family-home.html">家谱主页</a>
<a href="profile-content.html">内容发布</a>
<a href="profile-album.html">相册</a>
<a class="active" href="profile-video.html" data-genealogy-context-link>视频</a>
<a href="profile-family-home.html" data-genealogy-context-link>家谱主页</a>
</aside>
<div class="module-main">
<section class="module-panel">
<h2>视频列表</h2>
<div class="module-list">
<a class="module-row" href="profile-video.html"
><div>
<h3>四川武胜汤氏族视频</h3>
<p>已发布 · 2026.05 · 可编辑</p>
</div>
<span class="pill">管理</span></a
><a class="module-row" href="profile-video.html"
><div>
<h3>祭祖活动记录</h3>
<p>草稿 · 等待上传视频</p>
</div>
<span class="pill">继续</span></a
>
<div class="module-list" data-video-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">
<h2>发布视频</h2>
<div class="form-like">
<p>
<span>视频标题</span><b>必填</b
><a class="link-btn" href="profile-video.html">填写</a>
</p>
<p>
<span>视频描述</span><b>0/200</b
><a class="link-btn" href="profile-video.html">编辑</a>
</p>
<p>
<span>上传视频</span><b>未上传</b
><a class="link-btn" href="profile-video.html">选择</a>
</p>
<h2>视频详情</h2>
<div data-video-detail aria-live="polite">
<div class="api-state api-state--empty" data-api-state="empty" role="status" aria-live="polite">
请选择一个视频查看详情
</div>
</div>
</section>
</div>
@@ -90,8 +70,14 @@
<script src="public/js/jquery360.js"></script>
<script src="public/layui/layui.js"></script>
<script src="public/js/lay-config.js"></script>
<script src="public/js/profile-common.js"></script>
<script src="public/js/pending-pages.js"></script>
<script src="public/js/profile-common.js?v=20260725-entry"></script>
<script src="config.js"></script>
<script src="utils/StorageUtil.js"></script>
<script src="utils/axios.js"></script>
<script src="utils/AxiosRequestUtil.js"></script>
<script src="utils/ApiClient.js"></script>
<script src="public/js/media-display.js"></script>
<script src="public/js/video-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+71 -233
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -11,12 +11,12 @@
<body class="page-profile" data-profile-page>
<header class="site-header">
<div class="container nav">
<a class="brand magnetic" href="profile.html"
<a class="brand magnetic" href="index.html"
><img class="brand-logo" src="public/images/logo.png" alt="我们的家谱,代代相传" /></a
>
<nav class="nav-links">
<a href="profile.html">首页</a>
<a href="profile-families.html">数字家谱</a>
<a class="active" href="profile.html">个人中心</a>
<a href="profile-families.html">我的家谱</a>
<a href="plaza.html">家谱广场</a>
<a href="profile-content.html">家族文化</a>
<a href="surname.html">姓氏百科</a>
@@ -25,10 +25,8 @@
<a href="about.html">关于我们</a>
</nav>
<div class="nav-actions">
<a class="active" href="profile.html">个人中心</a>
<a class="btn primary magnetic" href="profile-create-family.html"
>创建家谱</a
>
<a href="profile-data.html">账户资料</a>
<a class="btn primary magnetic" href="profile-create-family.html">创建家谱</a>
</div>
</div>
</header>
@@ -36,29 +34,33 @@
<main>
<section class="profile-hero">
<div class="container profile-hero-grid">
<div class="profile-card tilt-card">
<div class="profile-card">
<div class="avatar-wrap">
<div class="avatar" data-profile-text="avatarText">鹿</div>
<div class="avatar" data-profile-avatar data-profile-text="avatarText"></div>
<span></span>
</div>
<div class="profile-info">
<div class="eyebrow">Personal Center</div>
<h1 data-profile-text="displayName">鹿野 Loyel</h1>
<p>
管理家谱、亲属资料、家族内容和协作消息。APP
里的核心功能都收纳在这里,适合 PC 端集中处理。
</p>
<div class="eyebrow">个人中心</div>
<h1 data-profile-text="displayName">正在加载账户</h1>
<p>账户资料与家谱工作区分开呈现。进入家谱后,再处理成员、内容和协作事务。</p>
<div class="profile-tags">
<span>血亲</span><span>第二代 顺字辈</span
><span>已绑定账号</span>
<span>已登录账户</span><span>资料与账号同步</span>
</div>
<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>
</div>
</div>
</div>
<div class="profile-stats tilt-card">
<div><strong>2</strong><span>我的家谱</span></div>
<div><strong>7</strong><span>待处理</span></div>
<div><strong>18</strong><span>已发布内容</span></div>
<div><strong>86%</strong><span>资料完善度</span></div>
<div class="profile-next-card">
<div class="eyebrow">下一步</div>
<h2>从家谱开始</h2>
<p>没有家谱时可创建或申请加入;有多个家谱时,请先选择本次要处理的家谱。</p>
<div class="toolbar">
<a class="link-btn" href="profile-create-family.html">创建家谱</a>
<a class="link-btn" href="profile-join-family.html">申请加入</a>
</div>
</div>
</div>
</section>
@@ -66,216 +68,70 @@
<section class="section dashboard-section">
<div class="container dashboard-layout">
<aside class="dashboard-side">
<div class="side-card user-actions tilt-card">
<div class="side-card user-actions">
<h2>账户入口</h2>
<a href="profile-data.html"
><span>个人资料</span><b>完善成员档案</b></a
>
<a href="profile-messages.html"
><span>消息中心</span><b>审核、邀请与提醒</b></a
>
<a href="profile-services.html"
><span>帮助与服务</span><b>帮助、反馈、推广</b></a
>
<a class="logout-link" href="#logout" data-logout-open
><span>退出登录</span><b>返回首页</b></a
>
</div>
<div class="side-card todo-card tilt-card" id="messages">
<div class="card-head">
<h2>待办消息</h2>
<span>7 条</span>
</div>
<div class="todo-list">
<a href="profile-join-review.html"
><b>入谱申请</b><span>3 人等待审核</span></a
>
<a href="profile-gift.html"
><b>贺礼邀请</b><span>2 条需要确认</span></a
>
<a href="profile-data-reminders.html"
><b>资料提醒</b><span>2 位成员待完善</span></a
>
</div>
<a href="profile-data.html"><span>个人资料</span><b>查看并更新账户资料</b></a>
<a href="profile-messages.html"><span>消息中心</span><b>查看真实通知与业务提醒</b></a>
<a href="profile-services.html"><span>帮助与服务</span><b>反馈、安全设置与会员服务</b></a>
<button class="logout-link" type="button" data-logout-open><span>退出登录</span><b>返回首页</b></button>
</div>
</aside>
<div class="dashboard-main">
<section class="panel my-family-panel" id="families">
<section class="panel my-family-panel">
<div class="panel-head">
<div>
<h2>我的家谱</h2>
<p>
对应 APP
首页和家谱主页,进入后可查看家谱主页、世系图、相册和家族圈。
</p>
<p>先从真实列表选择家谱,再进入对应的主页、世系图、成员和内容管理。</p>
</div>
<a class="link-btn" href="profile-families.html">查看全部</a>
<div class="toolbar">
<a
class="btn primary magnetic"
href="profile-create-family.html"
>创建家谱</a
><a class="btn ghost magnetic" href="profile-join-family.html"
>加入家谱</a
>
</div>
</div>
<div class="family-grid">
<a
class="family-card tilt-card"
href="profile-families.html"
>
<div class="book-cover">我 的<br />家 谱</div>
<a class="family-card" href="profile-families.html">
<div class="book-cover">选 择<br />家 谱</div>
<div>
<h3>进入我的家谱</h3>
<p>
从家谱入口选择真实家谱后,再查看主页、成员、世系图和家族内容。
</p>
<p>列表只展示当前账户真实可访问的家谱,避免把示例数据当成自己的内容。</p>
<div class="mini-actions">
<span>家谱主页</span><span>家族圈</span><span>成员管理</span>
<span>选择家谱</span><span>进入主页</span><span>处理事务</span>
</div>
</div>
</a>
</div>
</section>
<section class="panel" id="management">
<section class="panel">
<div class="panel-head compact">
<div>
<h2>家族管理</h2>
<p>
把 APP 里的世代、管理员、权限、入谱审核集中到一个管理区。
</p>
<h2>账户资料</h2>
<p>以下内容来自你的账户资料;未填写的信息会显示为“待填写”。</p>
</div>
<a class="link-btn" href="profile-family-admin.html" data-genealogy-context-link
>进入管理</a
>
<a class="link-btn" href="profile-data.html">编辑资料</a>
</div>
<div class="feature-grid management-grid">
<a class="feature-tile tilt-card" href="profile-families.html"
><span></span>
<h3>世系图</h3>
<p>查看成员世系、搜索成员、进入树谱模式。</p></a
>
<a class="feature-tile tilt-card" href="profile-families.html"
><span></span>
<h3>字辈谱</h3>
<p>管理字辈、统计各代人数和成员分布。</p></a
>
<a class="feature-tile tilt-card" href="profile-generation.html" data-genealogy-context-link
><span></span>
<h3>管理世代</h3>
<p>设置世代、调整始祖世代、维护排行。</p></a
>
<a class="feature-tile tilt-card" href="profile-messages.html"
><span></span>
<h3>入谱审核</h3>
<p>处理加入家谱、成员绑定和资料审核。</p></a
>
<a
class="feature-tile tilt-card"
href="profile-admin-permissions.html"
><span></span>
<h3>管理员权限</h3>
<p>分配全权、全系、支系管理员权限。</p></a
>
<a class="feature-tile tilt-card" href="profile-invite.html"
><span></span>
<h3>邀请家人</h3>
<p>生成邀请入口,引导亲人激活使用。</p></a
>
<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="email">加载中</b></p>
</div>
</section>
<section class="panel" id="content">
<section class="panel">
<div class="panel-head compact">
<div>
<h2>内容发布</h2>
<p>
对应 APP
家谱主页中的谱文、视频、相册、功德录、贺礼邀请、成长日志等内容入口。
</p>
<h2>账户与服务</h2>
<p>将 APP 的“我的”功能整理为桌面服务入口;家谱内容仍需从具体家谱进入。</p>
</div>
<a class="link-btn" href="profile-content.html">发布中心</a>
<a class="link-btn" href="profile-services.html">查看服务中心</a>
</div>
<div class="content-grid">
<a
class="content-tile large tilt-card"
href="profile-article.html"
><span>谱文</span>
<h3>撰写谱文与族史</h3>
<p>沉淀族训、谱序、谱论、恩荣录等家族文献。</p></a
>
<a class="content-tile tilt-card" href="profile-album.html"
><span>相册</span>
<h3>家族相册</h3>
<p>新建相册、上传照片、编辑封面。</p></a
>
<a class="content-tile tilt-card" href="profile-video.html"
><span>视频</span>
<h3>家族视频</h3>
<p>发布视频、管理家族影像。</p></a
>
<a class="content-tile tilt-card" href="profile-merit.html"
><span>功德</span>
<h3>功德录</h3>
<p>记录功德人、事迹与纪念内容。</p></a
>
<a class="content-tile tilt-card" href="profile-gift.html"
><span>贺礼</span>
<h3>贺礼邀请</h3>
<p>管理婚礼、生日、升学等贺礼。</p></a
>
<a class="content-tile tilt-card" href="profile-families.html?next=profile-feed.html"
><span>动态</span>
<h3>发布动态</h3>
<p>面向家族圈发布图文近况。</p></a
>
<a class="content-tile tilt-card" href="profile-families.html?next=profile-growth.html"
><span>日志</span>
<h3>成长日志</h3>
<p>记录成员成长和人生节点。</p></a
>
<a class="content-tile tilt-card" href="profile-families.html?next=profile-memo.html"
><span>备忘</span>
<h3>人亲簿 / 备忘录</h3>
<p>保存亲友往来和家族事务。</p></a
>
</div>
</section>
<section class="profile-data-layout" id="profile-data">
<div class="panel data-panel">
<div class="panel-head compact">
<div>
<h2>个人资料</h2>
<p>来自 APP 的个人资料、亲属、完善资料和绑定账号流程。</p>
</div>
<a class="link-btn" href="profile-data.html">编辑资料</a>
</div>
<div class="data-list">
<p><span>绑定账号</span><b data-profile-text="phone">已绑定</b></p>
<p><span>姓名</span><b data-profile-text="displayName">鹿野 Loyel</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>父亲 2 人</b></p>
<p><span>补充资料</span><b>待完善 4 项</b></p>
</div>
</div>
<div class="panel service-panel" id="services">
<div class="panel-head compact">
<div>
<h2>帮助与服务</h2>
<p>我的页面中的常用功能。</p>
</div>
</div>
<div class="service-grid">
<a href="profile-share.html">应用分享</a>
<a href="profile-feedback.html">意见反馈</a>
<a href="help.html">帮助中心</a>
<a href="profile-security.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#membership"><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>
@@ -287,29 +143,24 @@
<div class="container">
<div class="footer-grid">
<div>
<a class="brand" href="profile.html"
<a class="brand" href="index.html"
><img class="brand-logo" src="public/images/logo-light.png" alt="我们的家谱,代代相传" /></a
>
<p>一个面向家庭和宗族的数字家谱与家族文化门户。</p>
</div>
<div>
<h4>产品入口</h4>
<a href="profile-families.html">数字家谱</a
><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a
><a href="help.html">帮助中心</a>
<a href="profile-families.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a href="profile-content.html">家族文化</a
><a href="surname.html">姓氏百科</a
><a href="profile-article.html">家谱知识</a
><a href="profile-messages.html">平台公告</a>
<a href="profile-content.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="profile-article.html">家谱知识</a><a href="profile-messages.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -318,29 +169,15 @@
</div>
</div>
</footer>
<div class="logout-modal" id="logoutModal" aria-hidden="true">
<div class="logout-modal" id="logoutModal" aria-hidden="true" hidden>
<div class="logout-modal-backdrop" data-logout-close></div>
<div
class="logout-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="logoutTitle"
>
<button
class="logout-close"
type="button"
data-logout-close
aria-label="关闭"
>
×
</button>
<div class="logout-dialog" role="dialog" aria-modal="true" aria-labelledby="logoutTitle">
<button class="logout-close" type="button" data-logout-close aria-label="关闭">×</button>
<span class="logout-icon">退</span>
<h2 id="logoutTitle">确认退出登录?</h2>
<p>退出后需要重新登录,才能继续管理家谱、审核消息和编辑资料。</p>
<div class="logout-actions">
<button class="btn ghost magnetic" type="button" data-logout-close>
取消
</button>
<button class="btn ghost magnetic" type="button" data-logout-close>取消</button>
<a class="btn primary magnetic" href="index.html" data-logout-confirm>确认退出</a>
</div>
</div>
@@ -354,8 +191,9 @@
<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/media-display.js"></script>
<script src="public/js/profile-pages.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+7 -7
View File
@@ -31,7 +31,7 @@
<section class="inner-hero promo-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Public Album</div>
<div class="eyebrow">宣传相册</div>
<h1>用公开相册展示家族故事</h1>
<p class="lead">
宣传相册用于前台展示祖屋、祠堂、家族活动、老照片和公开纪念内容,帮助访客更直观理解家族文化。
@@ -74,17 +74,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
+8 -7
View File
@@ -31,7 +31,7 @@
<section class="inner-hero promo-hero">
<div class="container">
<div class="hero-copy">
<div class="eyebrow">Public Video</div>
<div class="eyebrow">宣传视频</div>
<h1>用宣传视频讲述家族文化</h1>
<p class="lead">
宣传视频用于前台展示家族活动、祠堂风貌、修谱故事和公开纪念内容,让访问者更快进入家族叙事。
@@ -81,17 +81,17 @@
</div>
<div>
<h4>产品入口</h4>
<a>数字家谱</a><a>家谱广场</a><a>应用下载</a><a>帮助中心</a>
<a href="genealogy.html">数字家谱</a><a href="plaza.html">家谱广场</a><a href="app.html">应用下载</a><a href="help.html">帮助中心</a>
</div>
<div>
<h4>文化内容</h4>
<a>家族文化</a><a>姓氏百科</a><a>家谱知识</a><a>平台公告</a>
<a href="culture.html">家族文化</a><a href="surname.html">姓氏百科</a><a href="article-detail.html">家谱知识</a><a href="notice-detail.html">平台公告</a>
</div>
<div>
<h4>联系我们</h4>
<p>客服电话:400-000-0000</p>
<p>邮箱:service@example.com</p>
<p>地址:中国 · 成都</p>
<h4>服务支持</h4>
<a href="help.html">帮助中心</a>
<a href="submit-ticket.html">提交问题</a>
<a href="about.html">关于平台</a>
</div>
</div>
<div class="footer-legal">
@@ -100,6 +100,7 @@
</div>
</div>
</footer>
<script src="config.js"></script>
<script src="public/js/page-effects.js"></script>
</body>
</html>
+3
View File
@@ -183,10 +183,13 @@
}
.promotion-card {
display: block;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
background: #fffaf0;
color: inherit;
text-decoration: none;
box-shadow: 0 18px 42px rgba(57, 48, 36, .08);
transition: transform .24s ease, border-color .24s ease, box-shadow .24s ease;
}
+12
View File
@@ -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;
+1155 -3
View File
File diff suppressed because it is too large Load Diff
+81 -1
View File
@@ -1,6 +1,86 @@
/* 加入家谱页只保留单页差异,公共认证卡片样式在 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-page-grid .field {
display: grid;
gap: 10px;
}
.join-page-grid .field > span {
color: var(--ink);
font-weight: 900;
}
.join-page-grid .field input,
.join-page-grid .field textarea {
width: 100%;
min-height: 48px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fffdf8;
color: var(--ink);
font: inherit;
outline: none;
transition: border-color .2s ease, box-shadow .2s ease;
}
.join-page-grid .field textarea {
min-height: 128px;
resize: vertical;
}
.join-page-grid .field input:focus,
.join-page-grid .field textarea:focus {
border-color: rgba(200, 59, 50, .45);
box-shadow: 0 0 0 4px rgba(200, 59, 50, .08);
}
.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 {
+529 -3
View File
@@ -5,6 +5,8 @@
.module-hero {
position: relative;
overflow: hidden;
min-height: 257px;
box-sizing: border-box;
padding: 58px 0 42px;
background:
radial-gradient(circle at 76% 18%, rgba(196, 146, 69, .16), transparent 25%),
@@ -78,9 +80,15 @@
top: 106px;
display: grid;
gap: 8px;
max-height: calc(100vh - 126px);
overflow-y: auto;
padding: 16px;
}
.page-profile-module .module-nav:not([data-workspace-navigation]) {
visibility: hidden;
}
.module-nav a {
padding: 14px 16px;
border-radius: 14px;
@@ -96,9 +104,53 @@
transform: translateX(4px);
}
.workspace-nav-group {
border: 1px solid transparent;
border-radius: 14px;
}
.workspace-nav-group[open] {
border-color: rgba(224, 211, 189, .72);
background: rgba(255, 250, 242, .72);
}
.workspace-nav-group summary {
padding: 14px 16px;
border-radius: 14px;
color: #5e665f;
font-weight: 900;
cursor: pointer;
transition: color .22s ease, background .22s ease;
}
.workspace-nav-group summary:hover,
.workspace-nav-group.is-open > summary {
color: var(--red);
background: #fbefea;
}
.workspace-nav-group summary:focus-visible,
.module-nav a:focus-visible {
outline: 3px solid rgba(200, 59, 50, .32);
outline-offset: 2px;
}
.workspace-nav-items {
display: grid;
gap: 4px;
padding: 2px 8px 8px 18px;
}
.module-nav .workspace-nav-items a {
padding: 10px 12px;
font-size: 14px;
font-weight: 800;
}
.module-main {
display: grid;
gap: 20px;
min-width: 0;
}
.feature-status-banner {
@@ -197,6 +249,26 @@
align-items: center;
}
[data-generation-management][hidden] {
display: none !important;
}
[data-lineage-management][hidden] {
display: none !important;
}
[data-lineage-spouse-field][hidden] {
display: none !important;
}
[data-lineage-pagination][hidden] {
display: none !important;
}
[data-video-management][hidden] {
display: none !important;
}
.generation-row .pill {
border: 0;
cursor: pointer;
@@ -224,6 +296,10 @@
margin-bottom: 16px;
}
.lineage-filter-toolbar {
grid-template-columns: minmax(220px, 1fr) minmax(140px, .5fr) minmax(140px, .5fr) auto;
}
.lineage-toolbar input,
.lineage-toolbar select {
min-height: 46px;
@@ -295,6 +371,23 @@
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
margin-top: 16px;
}
.row-actions button.pill {
border: 0;
font: inherit;
cursor: pointer;
}
.row-actions .pill:focus-visible {
outline: 3px solid rgba(200, 59, 50, .28);
outline-offset: 2px;
}
.module-panel h2[tabindex="-1"] {
scroll-margin-top: 96px;
outline: none;
}
.pill.is-danger {
@@ -302,6 +395,34 @@
background: #fde8e2;
}
.ceremony-invitation-row .pill {
border: 0;
cursor: pointer;
font: inherit;
}
.ceremony-invitation-row .pill:disabled {
cursor: wait;
opacity: .58;
}
.invitation-detail b {
overflow-wrap: anywhere;
}
.invitation-detail .pill {
display: inline-block;
margin-left: 8px;
font-size: 13px;
}
.video-row .pill,
.video-detail + .bottom-actions button {
border: 0;
cursor: pointer;
font: inherit;
}
.article-row {
/* 谱文列表行复用模块列表视觉,单独保留类名方便后续文章样式调整 */
align-items: center;
@@ -344,12 +465,17 @@
cursor: pointer;
}
.upload-input {
.editor-field .upload-input {
position: absolute;
width: 1px;
height: 1px;
min-height: 0;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
@@ -447,13 +573,19 @@
.security-form {
/* 安全设置表单复用编辑表单结构,只补模块间距 */
margin-top: 16px;
max-width: 880px;
}
.security-danger {
border-left: 4px solid #a13b32;
border-color: rgba(138, 50, 43, .28);
background: #fff7f2;
}
.security-danger h2 {
color: #7e2d27;
}
.security-danger .editor-field input,
.security-danger .editor-field textarea {
background: #fffdf8;
@@ -470,6 +602,11 @@
gap: 12px;
}
.profile-region-picker > label {
display: grid;
gap: 6px;
}
.profile-region-picker select {
min-height: 48px;
padding: 12px 14px;
@@ -491,6 +628,68 @@
box-shadow: 0 24px 60px rgba(57, 48, 36, .11);
}
.module-card.is-unavailable {
cursor: not-allowed;
opacity: .68;
}
.module-card.is-unavailable:after {
display: none;
}
.module-card.is-unavailable .pill {
display: inline-flex;
margin-top: 12px;
color: #6b542d;
background: #fff0cf;
}
.module-nav-unavailable {
padding: 14px 16px;
border-radius: 14px;
color: var(--muted);
background: #f4f1eb;
font-weight: 900;
}
.profile-path {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 18px;
color: #6f766f;
font-size: 14px;
font-weight: 800;
}
.profile-path a {
color: var(--red);
}
.profile-path a:hover {
text-decoration: underline;
}
.profile-path span {
color: #a89f91;
}
.profile-path strong {
min-width: 0;
overflow: hidden;
color: var(--ink);
text-overflow: ellipsis;
white-space: nowrap;
}
.module-nav-unavailable small {
display: block;
margin-top: 4px;
color: #8a5a3b;
font-size: 12px;
font-weight: 700;
}
.module-card.is-selected {
border-color: rgba(200, 59, 50, .5);
background: #fff6f1;
@@ -614,6 +813,16 @@
display: flex;
gap: 14px;
flex-wrap: wrap;
margin-top: 16px;
}
/* 网格和横向行已经通过 gap 管理间距,避免操作区重复叠加外边距。 */
.editor-form > .bottom-actions,
.editor-field > .bottom-actions,
.editor-two .bottom-actions,
.module-list > .row-actions,
.module-row > .row-actions {
margin-top: 0;
}
.editor-form {
@@ -631,11 +840,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 {
@@ -667,6 +881,11 @@
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
align-items: start;
}
.editor-field > .btn {
width: fit-content;
}
.profile-form-actions {
@@ -679,6 +898,26 @@
font-weight: 800;
}
[data-growth-editor][hidden],
[data-growth-editor-action][hidden] {
display: none !important;
}
[data-relative-editor][hidden],
[data-relative-editor-action][hidden] {
display: none !important;
}
[data-memo-editor][hidden],
[data-memo-editor-action][hidden] {
display: none !important;
}
[data-merit-editor][hidden],
[data-merit-editor-action][hidden] {
display: none !important;
}
.generation-batch-form {
/* 批量字辈表单复用编辑表单,只补充本页预览前的间距。 */
margin-top: 14px;
@@ -745,10 +984,24 @@
.module-nav {
position: static;
grid-template-columns: repeat(2, minmax(0, 1fr));
max-height: none;
overflow: visible;
}
.lineage-filter-toolbar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.module-hero {
min-height: 0;
}
.page-profile-module .module-nav {
display: none;
}
.module-title-row {
display: grid;
}
@@ -757,7 +1010,7 @@
font-size: 38px;
}
.module-nav {
.lineage-filter-toolbar {
grid-template-columns: 1fr;
}
@@ -768,3 +1021,276 @@
grid-template-columns: 1fr;
}
}
/* 个人中心工作台的统一密度和媒体布局。所有业务页沿用同一侧栏宽度与内容节奏。 */
.page-profile-module {
background: var(--color-canvas);
}
.page-profile-module .nav {
height: 72px;
}
.page-profile-module .section {
padding: var(--space-7) 0;
}
.module-hero {
height: 168px;
min-height: 168px;
padding: var(--space-5) 0;
}
.lineage-current-toolbar {
margin-bottom: var(--space-4);
}
.lineage-current-toolbar label {
flex: 0 0 auto;
font-weight: 800;
}
.module-kicker {
margin-bottom: var(--space-3);
padding: 6px 11px;
}
.module-title-row h1 {
margin-bottom: var(--space-2);
font-size: var(--text-2xl);
line-height: 1.18;
}
.module-title-row p {
font-size: var(--text-body-lg);
line-height: 1.75;
}
.module-layout {
grid-template-columns: 248px minmax(0, 1fr);
gap: var(--space-5);
}
.module-nav,
.module-panel,
.module-card {
border-radius: var(--radius-lg);
box-shadow: var(--shadow-panel);
}
.module-nav {
top: 88px;
gap: var(--space-2);
max-height: calc(100vh - 108px);
padding: var(--space-3);
scrollbar-width: thin;
scrollbar-color: rgba(171, 65, 54, .34) transparent;
}
.module-nav::-webkit-scrollbar {
width: 6px;
}
.module-nav::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(171, 65, 54, .34);
}
.module-nav::-webkit-scrollbar-track {
background: transparent;
}
.module-nav a,
.workspace-nav-group summary {
padding: 11px 13px;
border-radius: var(--radius-md);
}
.module-nav a:hover,
.module-nav a.active {
transform: translateX(2px);
}
.module-main {
gap: var(--space-4);
}
.module-panel {
padding: var(--space-5);
}
.module-panel h2 {
font-size: var(--text-title);
}
.module-grid {
gap: var(--space-4);
}
.module-card {
min-height: 144px;
padding: var(--space-4);
}
.module-card:after {
content: none;
}
.module-card h3,
.module-row h3 {
font-size: var(--text-heading);
}
.module-row {
gap: var(--space-4);
padding: var(--space-4);
border-radius: var(--radius-md);
}
.module-row[data-join-genealogy-id] > span:first-child {
display: grid;
gap: var(--space-1);
text-align: left;
}
.module-row[data-join-genealogy-id] small {
color: var(--text-muted);
font-weight: 500;
}
.form-like {
border-radius: var(--radius-md);
}
.form-like p {
grid-template-columns: 132px minmax(0, 1fr) auto;
gap: var(--space-4);
padding: 14px var(--space-4);
}
.module-row > .media-frame,
.album-photo-row > .media-frame {
width: 152px;
aspect-ratio: 4 / 3;
}
.family-cover-media,
.article-cover-media,
.album-cover-media,
.ceremony-cover-media,
.video-detail-media {
margin-bottom: var(--space-4);
aspect-ratio: 16 / 7;
}
.family-cover-media img,
.article-cover-media img,
.album-cover-media img,
.ceremony-cover-media img,
.video-detail-media video {
aspect-ratio: 16 / 7;
}
:is(.article-cover-media, .album-cover-media, .ceremony-cover-media) img {
object-fit: contain;
}
.video-detail-media video {
object-fit: contain;
}
.lineage-row {
grid-template-columns: auto minmax(0, 1fr) auto;
}
.lineage-node {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
column-gap: var(--space-2);
}
.lineage-node > .media-avatar {
grid-row: 1 / 3;
}
@media (max-width: 1060px) {
.module-layout {
grid-template-columns: 1fr;
}
.module-nav {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.module-hero {
height: auto;
padding: var(--space-5) 0 var(--space-4);
}
.module-title-row h1 {
font-size: var(--text-title);
}
.module-panel {
padding: var(--space-4);
}
.module-row > .media-frame,
.album-photo-row > .media-frame {
width: 100%;
}
.form-like p,
.module-row {
grid-template-columns: 1fr;
}
.row-actions {
justify-content: flex-start;
}
.row-actions .pill {
display: inline-flex;
min-height: 44px;
padding: 10px 14px;
align-items: center;
justify-content: center;
}
.bottom-actions .form-status {
flex-basis: 100%;
}
}
.attachment-edit-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(180px, 100%), 220px));
gap: 12px;
margin-top: 12px;
}
.attachment-edit-list:empty { display: none; }
.attachment-edit-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
min-width: 0;
padding: 10px;
border: 1px solid var(--border, #e6ddcf);
border-radius: 8px;
}
.attachment-edit-item :is(img, video) {
width: 100%;
height: 140px;
object-fit: contain;
background: var(--paper, #faf7f0);
}
.attachment-edit-name { overflow-wrap: anywhere; font-size: 13px; }
.attachment-edit-remove { margin-top: auto; }
.attachment-edit-remove:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
.attachment-edit-remove:disabled { opacity: .5; cursor: not-allowed; }
+114
View File
@@ -33,6 +33,7 @@
.profile-card,
.profile-stats,
.profile-next-card,
.panel,
.side-card {
border: 1px solid rgba(224, 211, 189, .84);
@@ -94,6 +95,17 @@
line-height: 1.9;
}
.profile-info .profile-load-status {
margin: 14px 0 0;
color: var(--muted);
font-size: 14px;
font-weight: 800;
}
.profile-info .profile-load-status:empty {
display: none;
}
.profile-tags,
.mini-actions {
display: flex;
@@ -101,6 +113,10 @@
gap: 10px;
}
.mini-actions {
margin-top: 12px;
}
.profile-tags span,
.mini-actions span {
padding: 7px 12px;
@@ -118,6 +134,29 @@
gap: 14px;
}
.profile-next-card {
padding: 32px;
}
.profile-next-card .eyebrow {
margin-bottom: 14px;
}
.profile-next-card h2 {
margin-bottom: 12px;
font-size: 30px;
}
.profile-next-card p {
margin-bottom: 22px;
color: var(--muted);
line-height: 1.8;
}
.profile-hero-actions {
margin-top: 22px;
}
.profile-stats div {
min-height: 116px;
padding: 20px;
@@ -180,16 +219,23 @@
}
.user-actions a,
.user-actions button,
.todo-list a {
display: block;
width: 100%;
padding: 15px 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fbf7ed;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
transition: transform .22s ease, border-color .22s ease, background .22s ease, color .22s ease;
}
.user-actions a:hover,
.user-actions button:hover,
.todo-list a:hover {
transform: translateX(5px);
border-color: rgba(71, 111, 99, .28);
@@ -427,6 +473,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;
@@ -448,6 +514,10 @@
transition: opacity .22s ease;
}
.logout-modal[hidden] {
display: none;
}
.logout-modal.show {
opacity: 1;
pointer-events: auto;
@@ -611,3 +681,47 @@
grid-template-columns: 1fr;
}
}
.page-profile {
background: var(--color-canvas);
}
.profile-hero {
padding: var(--space-7) 0 var(--space-6);
}
.profile-card,
.profile-stats,
.profile-next-card,
.panel,
.side-card {
border-radius: var(--radius-lg);
box-shadow: var(--shadow-panel);
}
.profile-info h1 {
font-size: var(--text-display);
}
.avatar .media-avatar,
.avatar .media-avatar img {
width: 100%;
height: 100%;
border-radius: inherit;
}
.avatar .media-avatar {
color: #fff;
background: transparent;
font: inherit;
}
@media (max-width: 760px) {
.profile-hero {
padding: var(--space-5) 0 var(--space-4);
}
.profile-info h1 {
font-size: var(--text-title);
}
}
+593 -21
View File
@@ -1,16 +1,18 @@
@import url("../../tokens.css");
:root {
--red: #c83b32;
--red-soft: #f4ded8;
--green: #476f63;
--green-dark: #213f38;
--gold: #c49245;
--ink: #26302c;
--muted: #737b76;
--paper: #fbf7ed;
--paper-2: #f7efe2;
--line: #eadfce;
--white: #fffdf8;
--shadow: 0 24px 70px rgba(69, 53, 35, .12);
--red: var(--color-accent);
--red-soft: var(--color-accent-soft);
--green: var(--color-green);
--green-dark: var(--color-ink);
--gold: var(--color-gold);
--ink: var(--color-ink);
--muted: var(--color-ink-2);
--paper: var(--color-paper);
--paper-2: var(--color-paper-2);
--line: var(--color-rule);
--white: var(--color-panel);
--shadow: var(--shadow-card);
}
.auth-layer-message {
@@ -34,10 +36,19 @@
box-sizing: border-box;
}
html,
body {
overflow-x: clip;
}
html {
scrollbar-gutter: stable;
}
body {
margin: 0;
color: var(--ink);
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
font-family: var(--font-body);
background: var(--paper);
}
@@ -63,7 +74,7 @@ img {
}
.container {
width: min(1280px, calc(100% - 72px));
width: min(var(--layout-max), calc(100% - 72px));
margin: 0 auto;
}
@@ -142,7 +153,7 @@ img {
display: flex;
align-items: center;
gap: 26px;
color: rgba(255, 253, 248, .78);
color: #fffdf8;
font-size: 15px;
white-space: nowrap;
}
@@ -154,6 +165,7 @@ img {
.nav-links a,
.nav-actions a {
color: inherit;
position: relative;
transition: color .22s ease, transform .22s ease;
}
@@ -181,11 +193,92 @@ img {
display: flex;
align-items: center;
gap: 14px;
color: rgba(255, 253, 248, .86);
color: #fffdf8;
font-size: 15px;
white-space: nowrap;
}
.nav-user-entry {
min-width: 0;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
.nav-user-avatar {
width: 32px;
height: 32px;
position: relative;
flex: 0 0 auto;
display: inline-grid;
place-items: center;
overflow: hidden;
border: 1px solid rgba(255, 248, 230, .56);
border-radius: 50%;
color: var(--red-deep);
background: #fff4dc;
font-size: 13px;
font-weight: 900;
}
.nav-user-avatar img {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
object-fit: cover;
}
.nav-user-avatar-fallback {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
.nav-user-name {
max-width: 96px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-current-genealogy {
min-width: 0;
max-width: 240px;
display: grid;
gap: 2px;
margin-left: auto;
padding: 7px 12px;
border: 1px solid rgba(255, 248, 230, .28);
border-radius: 12px;
color: #fff8e6;
background: rgba(79, 29, 26, .18);
line-height: 1.2;
}
.profile-current-genealogy span {
color: rgba(255, 248, 230, .72);
font-size: 11px;
font-weight: 700;
}
.profile-current-genealogy strong {
overflow: hidden;
color: #fff8e6;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-mobile-menu-toggle,
.profile-mobile-menu,
.site-menu-toggle,
.site-mobile-menu {
display: none;
}
.btn {
min-width: 118px;
height: 44px;
@@ -230,6 +323,18 @@ img {
background: rgba(255, 255, 255, .68);
}
.btn.danger {
border-color: #8a322b;
color: #fffdf8;
background: #8a322b;
box-shadow: 0 10px 22px rgba(138, 50, 43, .18);
}
.btn.danger:hover {
border-color: #71251f;
background: #71251f;
}
.inner-hero {
position: relative;
overflow: hidden;
@@ -518,6 +623,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;
@@ -604,8 +724,9 @@ textarea {
opacity: .72;
}
/* 接口列表为空时的统一占位样式 */
.api-empty {
/* 业务请求的加载、空数据、失败和无权限状态 */
.api-empty,
.api-state {
padding: 22px;
border: 1px dashed var(--line);
border-radius: 14px;
@@ -615,6 +736,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);
@@ -790,11 +922,187 @@ textarea {
background: #fff4ef;
}
@media (max-width: 1320px) {
.page-profile .nav-links,
.page-profile-module .nav-links {
display: none;
}
.profile-mobile-menu-toggle {
min-width: 58px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 12px;
border: 1px solid rgba(255, 248, 230, .38);
border-radius: 10px;
color: #fff8e6;
background: rgba(79, 29, 26, .2);
font: inherit;
font-size: 13px;
font-weight: 800;
}
.page-profile .nav-actions,
.page-profile-module .nav-actions {
display: none;
}
.profile-mobile-menu.is-open {
display: block;
position: absolute;
top: 100%;
right: 0;
left: 0;
max-height: calc(100vh - 72px);
overflow-y: auto;
padding: 14px 0 18px;
border-bottom: 1px solid rgba(255, 253, 248, .24);
background: rgba(121, 42, 35, .99);
box-shadow: 0 22px 38px rgba(83, 32, 26, .28);
}
.profile-mobile-menu-inner {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.profile-mobile-workspace {
display: grid;
gap: 8px;
}
.profile-mobile-workspace .workspace-nav-group {
border-color: rgba(255, 248, 230, .16);
background: transparent;
}
.profile-mobile-workspace .workspace-nav-group[open] {
background: rgba(255, 253, 248, .08);
}
.profile-mobile-menu .profile-mobile-workspace summary {
min-height: 46px;
display: flex;
align-items: center;
padding: 10px 14px;
color: #fff8e6;
font-weight: 900;
cursor: pointer;
}
.profile-mobile-menu .profile-mobile-workspace .workspace-nav-group.is-open > summary {
color: #fff7d8;
background: rgba(255, 253, 248, .16);
}
.profile-mobile-workspace .workspace-nav-items,
.profile-mobile-account-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
padding: 8px;
}
.profile-mobile-menu a,
.profile-mobile-menu button {
min-height: 46px;
display: flex;
align-items: center;
padding: 10px 14px;
border: 1px solid rgba(255, 248, 230, .16);
border-radius: 10px;
color: #fff8e6;
background: rgba(255, 253, 248, .08);
font: inherit;
font-weight: 800;
text-align: left;
}
.profile-mobile-menu button {
width: 100%;
cursor: pointer;
}
.profile-mobile-menu a.is-current {
border-color: rgba(255, 241, 202, .76);
color: #fff7d8;
background: rgba(255, 253, 248, .2);
}
}
@media (max-width: 620px) {
.profile-mobile-workspace .workspace-nav-items,
.profile-mobile-account-actions {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 1120px) {
.nav-links {
display: none;
}
.site-menu-toggle {
min-width: 58px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 12px;
border: 1px solid rgba(255, 248, 230, .38);
border-radius: 10px;
color: #fff8e6;
background: rgba(79, 29, 26, .2);
font: inherit;
font-size: 13px;
font-weight: 800;
}
.has-site-mobile-menu .nav-actions {
display: none;
}
.site-mobile-menu.is-open {
display: block;
position: absolute;
top: 100%;
right: 0;
left: 0;
padding: 14px 0 18px;
border-bottom: 1px solid rgba(255, 253, 248, .24);
background: rgba(121, 42, 35, .99);
box-shadow: 0 22px 38px rgba(83, 32, 26, .28);
}
.site-mobile-menu-inner {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.site-mobile-menu a {
min-height: 46px;
display: flex;
align-items: center;
padding: 10px 14px;
border: 1px solid rgba(255, 248, 230, .16);
border-radius: 10px;
color: #fff8e6;
background: rgba(255, 253, 248, .08);
font: inherit;
font-weight: 800;
text-align: left;
}
.site-mobile-menu a.active {
border-color: rgba(255, 241, 202, .76);
color: #fff7d8;
background: rgba(255, 253, 248, .2);
}
.inner-hero .container,
.grid-2 {
grid-template-columns: 1fr;
@@ -819,6 +1127,61 @@ textarea {
display: none;
}
.page-profile-module .nav,
.page-profile .nav {
height: 70px;
gap: 10px;
}
.page-profile-module .brand,
.page-profile .brand {
padding: 7px 10px;
}
.page-profile-module .brand-logo,
.page-profile .brand-logo {
width: 114px;
}
.page-profile-module .brand-logo-mark {
width: 38px;
height: 38px;
}
.page-profile-module .brand-text {
display: none;
}
.page-profile-module .nav-actions {
display: none;
}
.profile-current-genealogy {
max-width: 112px;
padding: 7px 9px;
}
.profile-current-genealogy span {
display: none;
}
.profile-current-genealogy strong {
font-size: 13px;
}
.profile-mobile-menu-toggle {
min-width: 50px;
padding: 0 9px;
}
.profile-mobile-menu-inner {
grid-template-columns: 1fr;
}
.site-mobile-menu-inner {
grid-template-columns: 1fr;
}
.inner-hero .container {
padding: 46px 0 54px;
}
@@ -851,11 +1214,220 @@ textarea {
}
}
/* 统一业务媒体:只承载后端授权地址,加载失败时保留可理解的回退状态。 */
.media-frame {
position: relative;
min-width: 0;
margin: 0;
overflow: clip;
border: var(--rule-thin) solid var(--color-rule);
border-radius: var(--radius-card);
background: var(--color-panel-soft);
}
.media-preview {
width: 100%;
display: block;
padding: 0;
border: 0;
background: transparent;
font: inherit;
}
.media-frame img,
.media-frame video {
width: 100%;
display: block;
object-fit: cover;
background: var(--color-paper-2);
}
.media-frame img {
aspect-ratio: 4 / 3;
}
.media-frame video {
max-height: min(72vh, 720px);
aspect-ratio: 16 / 9;
}
.media-frame figcaption {
padding: var(--space-xs) var(--space-sm);
color: var(--color-ink-2);
font-size: var(--text-sm);
line-height: 1.6;
}
.media-fallback {
min-height: 132px;
display: grid;
place-items: center;
padding: var(--space-md);
color: var(--color-ink-2);
background: var(--color-paper-2);
text-align: center;
font-size: var(--text-sm);
font-weight: 600;
}
.media-fallback[hidden] {
display: none;
}
.media-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 280px));
gap: var(--space-sm);
margin-top: var(--space-md);
justify-content: start;
}
.media-gallery .media-frame img {
object-fit: contain;
}
.media-download {
min-height: var(--control-height);
display: flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
border: var(--rule-thin) solid var(--color-rule);
border-radius: var(--radius-sm);
color: var(--color-accent-strong);
background: var(--color-panel);
font-size: var(--text-sm);
font-weight: 700;
overflow-wrap: anywhere;
}
.media-avatar {
width: 48px;
height: 48px;
position: relative;
flex: 0 0 auto;
display: inline-grid;
place-items: center;
overflow: clip;
border-radius: 50%;
color: var(--color-accent-strong);
background: var(--color-accent-soft);
font-weight: 800;
}
.media-avatar img {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
object-fit: cover;
}
.media-avatar-fallback {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
.media-avatar img:not([hidden]) + .media-avatar-fallback {
visibility: hidden;
}
.media-avatar.is-media-error .media-avatar-fallback {
visibility: visible;
}
.media-dialog {
width: min(1040px, calc(100% - 32px));
max-height: calc(100vh - 32px);
padding: var(--space-lg);
border: var(--rule-thin) solid var(--color-rule);
border-radius: var(--radius-dialog);
color: var(--color-ink);
background: var(--color-panel);
box-shadow: var(--shadow-dialog);
}
.media-dialog::backdrop {
background: var(--color-overlay);
backdrop-filter: blur(4px);
}
.media-dialog img {
width: 100%;
max-height: calc(100vh - 120px);
object-fit: contain;
}
.media-dialog-close {
min-height: 40px;
display: block;
margin: 0 0 var(--space-sm) auto;
padding: 0 var(--space-sm);
border: var(--rule-thin) solid var(--color-rule);
border-radius: var(--radius-pill);
color: var(--color-ink);
background: var(--color-panel);
font: inherit;
font-weight: 700;
}
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
outline: 3px solid var(--color-focus);
outline-offset: 3px;
}
:where(button, .btn, input, select, textarea):disabled,
:where(button, .btn)[aria-disabled="true"] {
cursor: not-allowed;
opacity: .52;
box-shadow: none;
transform: none;
}
:where(button, .btn)[aria-busy="true"] {
cursor: progress;
opacity: .72;
pointer-events: none;
}
.api-state,
.api-empty {
padding: var(--space-md);
border-radius: var(--radius-md);
background: var(--color-panel-soft);
}
.api-state--error,
.api-state--forbidden,
[data-state="error"] {
border-color: color-mix(in oklch, var(--color-error) 32%, var(--color-rule));
color: var(--color-error);
background: var(--color-error-soft);
}
.api-state--success,
[data-state="success"] {
border-color: color-mix(in oklch, var(--color-success) 30%, var(--color-rule));
color: var(--color-success);
background: var(--color-success-soft);
}
@media (max-width: 768px) {
.media-gallery {
grid-template-columns: minmax(0, 280px);
}
.media-dialog {
padding: var(--space-sm);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*:before,
*:after {
*:not(:where(.page-home-portal, .page-home-portal *)),
*:not(:where(.page-home-portal, .page-home-portal *))::before,
*:not(:where(.page-home-portal, .page-home-portal *))::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

+169
View File
@@ -0,0 +1,169 @@
(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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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();
root.NavigationUtil.open('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
};
});
+814
View File
@@ -0,0 +1,814 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.AlbumPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.AlbumPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function confirmAction(message) {
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
return root.ProfileUI.confirmAction(message);
}
return Promise.resolve(!root['confirm'] || root['confirm'](message));
}
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
var documentRef = root.document;
var albumsById = Object.create(null);
var photosById = Object.create(null);
var currentAlbum = null;
var canEditContent = false;
var writePending = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, node) {
return documentRef ? (node || documentRef).querySelector(selector) : null;
}
function queryAll(selector, node) {
return documentRef ? Array.prototype.slice.call((node || documentRef).querySelectorAll(selector)) : [];
}
function stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function trimOrUndefined(value) {
var text = stringValue(value).trim();
return text || undefined;
}
function queryParam(search, name) {
return new URLSearchParams(stringValue(search).replace(/^\?/, '')).get(name) || '';
}
function normalizeId(value) {
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
return /^[1-9][0-9]*$/.test(String(value)) ? String(value) : '';
}
function normalizeNullableId(value) {
if (value === undefined || value === null || value === '') return '';
return normalizeId(value);
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function getCurrentGenealogyId(search) {
var source = search === undefined && root.location ? root.location.search : search;
var contextId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
if (contextId) return normalizeId(contextId);
}
return normalizeId(queryParam(source, 'genealogyId'));
}
function getCurrentAlbumId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'albumId'));
}
function toBackendDateTime(value) {
var text = trimOrUndefined(value);
if (!text) return undefined;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ') + ':00';
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ');
return text;
}
function toDateTimeInputValue(value) {
var text = stringValue(value);
if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) return '';
return text.slice(0, 16).replace(' ', 'T');
}
function isValidBackendDateTime(value) {
var match = stringValue(value).match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
var year;
var month;
var day;
var days;
if (!match) return false;
year = Number(match[1]);
month = Number(match[2]);
day = Number(match[3]);
if (month < 1 || month > 12 || Number(match[4]) > 23 || Number(match[5]) > 59 || Number(match[6]) > 59) return false;
days = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return day >= 1 && day <= days[month - 1];
}
function buildAlbumBody(values) {
var source = values || {};
var body = {
albumName: stringValue(source.albumName).trim(),
status: '0'
};
var albumDesc = trimOrUndefined(source.albumDesc);
var coverOssId = normalizeNullableId(source.coverOssId);
var sortOrder = optionalSafeInteger(source.sortOrder);
if (albumDesc !== undefined) body.albumDesc = albumDesc;
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateAlbumBody(body) {
if (!body || !body.albumName) return '请填写相册名称';
if (body.invalidCoverOssId) return '封面文件编号无效,请重新选择文件';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用相册,暂不开放停用';
if (body.status !== '0') return '相册状态只能是 0';
return '';
}
function buildAlbumPhotoBody(values) {
var source = values || {};
var body = { status: '0' };
var ossId = normalizeNullableId(source.ossId);
var photoTitle = trimOrUndefined(source.photoTitle);
var photoDesc = trimOrUndefined(source.photoDesc);
var photographer = trimOrUndefined(source.photographer);
var shootTime = toBackendDateTime(source.shootTime);
var sortOrder = optionalSafeInteger(source.sortOrder);
if (ossId) body.ossId = ossId;
else if (source.ossId !== undefined && source.ossId !== null && source.ossId !== '') body.invalidOssId = true;
if (photoTitle !== undefined) body.photoTitle = photoTitle;
if (photoDesc !== undefined) body.photoDesc = photoDesc;
if (photographer !== undefined) body.photographer = photographer;
if (shootTime !== undefined) body.shootTime = shootTime;
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateAlbumPhotoBody(body) {
if (!body || !body.ossId || body.invalidOssId) return '请选择并上传照片';
if (body.shootTime !== undefined && !isValidBackendDateTime(body.shootTime)) return '拍摄时间格式无效';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用照片,暂不开放停用';
if (body.status !== '0') return '照片状态只能是 0';
return '';
}
function normalizeAlbum(item) {
var albumId = normalizeId(item && item.albumId);
var genealogyId = normalizeId(item && item.genealogyId);
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.coverFile);
var albumName = stringValue(item && item.albumName).trim();
var photoCount = optionalSafeInteger(item && item.photoCount);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var album;
if (!albumId || !genealogyId || !albumName) return null;
if (item.coverFile !== undefined && item.coverFile !== null && !coverFile) return null;
if (item.photoCount !== undefined && item.photoCount !== null && item.photoCount !== '' && photoCount === undefined) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
album = {
albumId: albumId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
albumName: albumName,
albumDesc: stringValue(item.albumDesc),
status: status,
remark: stringValue(item.remark)
};
if (coverFile) album.coverFile = coverFile;
if (photoCount !== undefined) album.photoCount = photoCount;
if (sortOrder !== undefined) album.sortOrder = sortOrder;
return album;
}
function normalizeAlbumPhoto(item) {
var photoId = normalizeId(item && item.photoId);
var genealogyId = normalizeId(item && item.genealogyId);
var albumId = normalizeId(item && item.albumId);
var photoFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.photoFile);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var photo;
if (!photoId || !genealogyId || !albumId || !photoFile) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
photo = {
photoId: photoId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
albumId: albumId,
albumName: stringValue(item.albumName),
photoFile: photoFile,
photoTitle: stringValue(item.photoTitle),
photoDesc: stringValue(item.photoDesc),
photographer: stringValue(item.photographer),
shootTime: stringValue(item.shootTime),
status: status,
remark: stringValue(item.remark)
};
if (sortOrder !== undefined) photo.sortOrder = sortOrder;
return photo;
}
function normalizeAlbums(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeAlbum);
return normalized.some(function (album) { return !album; }) ? [] : normalized;
}
function normalizeAlbumPhotos(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeAlbumPhoto);
return normalized.some(function (photo) { return !photo; }) ? [] : normalized;
}
function findAlbumById(data, albumId) {
var albums = normalizeAlbums(data);
var expectedId = normalizeId(albumId);
return albums.find(function (album) { return album.albumId === expectedId; }) || null;
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildListUrl(genealogyId) {
return 'profile-album.html?genealogyId=' + encodeURIComponent(genealogyId);
}
function buildEditUrl(genealogyId, albumId) {
var url = 'profile-album-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
return albumId ? url + '&albumId=' + encodeURIComponent(albumId) : url;
}
function buildDetailUrl(genealogyId, albumId) {
return 'profile-album-detail.html?genealogyId=' + encodeURIComponent(genealogyId) +
'&albumId=' + encodeURIComponent(albumId);
}
function detailValue(label, value) {
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
}
function renderAlbumDetail(album) {
var actions = '';
if (!album) return '';
if (canEditContent) {
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(album.genealogyId, album.albumId)) + '">编辑相册</a>' +
'<button class="btn danger" type="button" data-album-delete-id="' +
escapeHtml(album.albumId) + '">删除相册</button></div>';
}
return (MediaDisplay ? MediaDisplay.renderImage(album.coverFile, {
alt: album.albumName + '封面', className: 'album-cover-media'
}) : '') + '<div class="form-like album-detail">' +
detailValue('相册名称', album.albumName) +
detailValue('相册说明', album.albumDesc) +
detailValue('照片数量', album.photoCount === undefined ? '' : album.photoCount) +
detailValue('备注', album.remark) +
'</div>' + actions;
}
function renderAlbumRow(album) {
var actions = '<a class="pill" href="' + escapeHtml(buildDetailUrl(album.genealogyId, album.albumId)) +
'">查看照片</a>';
if (canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(album.genealogyId, album.albumId)) +
'">编辑</a><button class="pill is-danger" type="button" data-album-delete-id="' +
escapeHtml(album.albumId) + '">删除</button>';
}
return '<article class="module-row album-row"><div><h3>' + escapeHtml(album.albumName) +
'</h3><p>' + escapeHtml(album.albumDesc || ('照片数量:' + (album.photoCount || 0))) +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
function renderPhotoList(data) {
var photos = Array.isArray(data) ? data.map(normalizeAlbumPhoto) : [];
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="' +
escapeHtml(photo.photoId) + '">删除</button>'
: '';
return '<article class="module-row album-photo-row">' +
(MediaDisplay ? MediaDisplay.renderImage(photo.photoFile, {
alt: photo.photoTitle || '家族照片', className: 'album-photo-media'
}) : '') + '<div><h3>' +
escapeHtml(photo.photoTitle || '未命名照片') + '</h3><p>' +
escapeHtml([photo.photoDesc, photo.photographer, photo.shootTime].filter(Boolean).join(' · ') || '文件已上传') +
'</p></div><div class="row-actions">' + action + '</div></article>';
}).join('');
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
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();
root.NavigationUtil.open('login.html');
return true;
}
function showMessage(message) {
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
else if (root.alert) root.alert(message);
}
function syncLinks() {
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
}
function getApiStateType(error) {
return isForbidden(error) ? 'forbidden' : 'error';
}
function setStatus(selector, message, type) {
var target = query(selector);
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) {
var placeholder = query('[data-album-editor-placeholder]');
queryAll('[data-album-editor], [data-album-editor-action], [data-album-photo-editor]').forEach(function (element) {
element.hidden = !enabled;
});
if (placeholder) {
placeholder.hidden = Boolean(enabled);
if (message) placeholder.textContent = message;
}
}
function setWritePending(value) {
writePending = Boolean(value);
queryAll('[data-album-form] button, [data-album-form] input, [data-album-form] textarea, [data-album-photo-form] button, [data-album-photo-form] input, [data-album-photo-form] textarea, [data-album-delete-id], [data-album-photo-delete-id]').forEach(function (control) {
control.disabled = writePending;
});
}
function renderNoContext() {
var message = '请先选择家谱,再查看或维护相册。';
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, 'forbidden');
setStatus('[data-album-photo-status]', message, 'forbidden');
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
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 = canEditAlbums(detail);
queryAll('[data-album-create-link], [data-album-editor-action], [data-album-photo-editor]').forEach(function (element) {
element.hidden = !canEditContent;
});
return detail;
}
function renderAlbums(data) {
var container = query('[data-album-list]');
var albums = normalizeAlbums(data);
albumsById = Object.create(null);
albums.forEach(function (album) { albumsById[album.albumId] = album; });
if (!container) return albums;
if (!Array.isArray(data) || (data.length && !albums.length)) {
setStatus('[data-album-list]', '相册响应缺少稳定 AlbumVo 字段,请联系后端核对。', 'error');
return [];
}
if (!albums.length) {
setStatus('[data-album-list]', '暂无相册', 'empty');
return [];
}
container.innerHTML = albums.map(renderAlbumRow).join('');
return albums;
}
async function loadAlbums() {
var api = getApi();
var genealogyId;
if (redirectUnauthorized(api)) return;
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;
setStatus(
'[data-album-list]',
isForbidden(error) ? '当前账号无权查看该家谱的相册。' : '相册加载失败,请稍后重试。',
getApiStateType(error)
);
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的相册。' : (error.message || '相册加载失败'));
}
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) { values[field.name] = field.value; });
return values;
}
function setFieldValue(formSelector, name, value) {
var field = query(formSelector + ' [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillAlbumForm(album) {
if (!album || album.status !== '0') throw new Error('停用相册无法通过当前 PC 接口重新读取或编辑');
setFieldValue('[data-album-form]', 'albumName', album.albumName);
setFieldValue('[data-album-form]', 'albumDesc', album.albumDesc);
setFieldValue('[data-album-form]', 'sortOrder', album.sortOrder);
setFieldValue('[data-album-form]', 'status', '0');
}
async function loadAlbumEditor() {
var api = getApi();
var genealogyId;
var albumId;
var albums;
var album;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
albumId = getCurrentAlbumId();
syncLinks();
setEditorEnabled(false, '正在加载相册编辑信息…');
setStatus('[data-album-form-status]', '正在读取家谱权限…', 'loading');
try {
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的相册。' };
if (albumId) {
albums = await api.albums(genealogyId);
album = findAlbumById(albums, albumId);
if (!album) throw new Error('相册列表未返回当前相册');
fillAlbumForm(album);
var albumResponse = albums.find(function (entry) { return String(entry.albumId) === albumId; });
root.AttachmentEditor.setFiles('#albumCoverOssId', albumResponse.coverFile ? [albumResponse.coverFile] : []);
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 || '相册编辑信息加载失败'),
getApiStateType(error)
);
}
}
async function loadPhotos(api, genealogyId, albumId) {
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) 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;
}
async function loadAlbumDetailPage() {
var api = getApi();
var genealogyId;
var albumId;
var albums;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
albumId = getCurrentAlbumId();
if (!genealogyId || !albumId) {
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);
currentAlbum = findAlbumById(albums, albumId);
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;
setStatus(
'[data-album-photo-list]',
isForbidden(error) ? '当前账号无权查看该相册照片。' : (error.message || '照片加载失败'),
getApiStateType(error)
);
showMessage(isForbidden(error) ? '当前账号无权查看该相册照片。' : (error.message || '照片加载失败'));
}
}
async function submitAlbum(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var albumId = getCurrentAlbumId();
var body;
var validation;
var result;
var saved;
var savedId;
var verified;
if (writePending || !canEditContent || !genealogyId || redirectUnauthorized(api)) return;
body = buildAlbumBody(getFormValues(form));
validation = validateAlbumBody(body);
if (validation) {
setStatus('[data-album-form-status]', validation, 'error');
return;
}
delete body.invalidCoverOssId;
delete body.invalidSortOrder;
setWritePending(true);
setStatus('[data-album-form-status]', '正在保存相册…', 'loading');
try {
result = albumId
? await api.updateAlbum(genealogyId, albumId, body)
: await api.createAlbum(genealogyId, body);
saved = normalizeAlbum(result);
savedId = albumId || (saved && saved.albumId);
if (!savedId) throw new Error('保存响应缺少 albumId');
verified = findAlbumById(await api.albums(genealogyId), savedId);
if (!verified) throw new Error('保存后相册列表未返回同一相册');
root.NavigationUtil.open(buildDetailUrl(genealogyId, savedId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(
'[data-album-form-status]',
isForbidden(error) ? '当前账号无权保存该相册。' : (error.message || '相册保存失败'),
getApiStateType(error)
);
} finally {
setWritePending(false);
}
}
async function submitPhoto(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var albumId = getCurrentAlbumId();
var body;
var validation;
var saved;
if (writePending || !canEditContent || !genealogyId || !albumId || redirectUnauthorized(api)) return;
body = buildAlbumPhotoBody(getFormValues(form));
validation = validateAlbumPhotoBody(body);
if (validation) {
setStatus('[data-album-photo-status]', validation, 'error');
return;
}
delete body.invalidOssId;
delete body.invalidSortOrder;
setWritePending(true);
setStatus('[data-album-photo-status]', '正在添加照片…', 'loading');
try {
saved = normalizeAlbumPhoto(await api.createAlbumPhoto(genealogyId, albumId, body));
if (!saved || saved.albumId !== albumId) throw new Error('添加照片响应缺少稳定 photoId');
if (!(await loadPhotos(api, genealogyId, albumId)).some(function (photo) { return photo.photoId === saved.photoId; })) {
throw new Error('添加后照片列表未返回同一照片');
}
form.reset();
setStatus('[data-album-photo-status]', '照片已添加');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setStatus(
'[data-album-photo-status]',
isForbidden(error) ? '当前账号无权添加照片。' : (error.message || '照片添加失败'),
getApiStateType(error)
);
} finally {
setWritePending(false);
}
}
async function deleteAlbum(albumId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var album = albumsById[albumId] || currentAlbum;
if (writePending || !canEditContent || !genealogyId || !normalizeId(albumId) || redirectUnauthorized(api)) return;
if (!await confirmAction('确认删除“' + (album ? album.albumName : '该相册') + '”及其中全部照片吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteAlbum(genealogyId, albumId);
if (query('[data-album-page]')) await loadAlbums();
else root.NavigationUtil.open(buildListUrl(genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该相册。' : (error.message || '相册删除失败'));
} finally {
setWritePending(false);
}
}
async function deletePhoto(photoId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var albumId = getCurrentAlbumId();
var photo = photosById[photoId];
if (writePending || !canEditContent || !genealogyId || !albumId || !normalizeId(photoId) || redirectUnauthorized(api)) return;
if (!await confirmAction('确认删除“' + (photo && photo.photoTitle || '该照片') + '”吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteAlbumPhoto(genealogyId, albumId, photoId);
await loadPhotos(api, genealogyId, albumId);
showMessage('照片已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该照片。' : (error.message || '照片删除失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var albumDelete = event.target.closest('[data-album-delete-id]');
var photoDelete = event.target.closest('[data-album-photo-delete-id]');
if (albumDelete) {
event.preventDefault();
deleteAlbum(albumDelete.getAttribute('data-album-delete-id'));
return;
}
if (photoDelete) {
event.preventDefault();
deletePhoto(photoDelete.getAttribute('data-album-photo-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var albumForm = event.target.closest('[data-album-form]');
var photoForm = event.target.closest('[data-album-photo-form]');
if (albumForm) {
event.preventDefault();
submitAlbum(albumForm);
} else if (photoForm) {
event.preventDefault();
submitPhoto(photoForm);
}
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-album-page]')) loadAlbums();
if (query('[data-album-edit-page]')) loadAlbumEditor();
if (query('[data-album-detail-page]')) loadAlbumDetailPage();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentAlbumId: getCurrentAlbumId,
buildAlbumBody: buildAlbumBody,
validateAlbumBody: validateAlbumBody,
buildAlbumPhotoBody: buildAlbumPhotoBody,
validateAlbumPhotoBody: validateAlbumPhotoBody,
canEditAlbums: canEditAlbums,
normalizeAlbum: normalizeAlbum,
normalizeAlbumPhoto: normalizeAlbumPhoto,
normalizeAlbums: normalizeAlbums,
normalizeAlbumPhotos: normalizeAlbumPhotos,
findAlbumById: findAlbumById,
renderAlbumDetail: renderAlbumDetail,
renderPhotoList: renderPhotoList,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+176
View File
@@ -0,0 +1,176 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.AppPromotionPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.AppPromotionPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function normalizeId(value) {
var normalized;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
normalized = String(value).trim();
return /^[1-9][0-9]*$/.test(normalized) ? normalized : '';
}
function text(value) {
return value === undefined || value === null ? '' : String(value).trim();
}
function normalizeTargetUrl(value) {
var normalized = text(value);
var parsed;
if (!normalized) return '';
try {
parsed = new URL(normalized);
} catch (error) {
return '';
}
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : '';
}
function normalizePromotion(item) {
var source = item || {};
var promotionId = normalizeId(source.promotionId);
var promotionTitle = text(source.promotionTitle);
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(source.coverFile);
var promotion;
if (!promotionId || !promotionTitle || String(source.status) !== '0') return null;
if (source.coverFile !== undefined && source.coverFile !== null && !coverFile) return null;
promotion = {
promotionId: promotionId,
promotionTitle: promotionTitle,
promotionDesc: text(source.promotionDesc),
targetUrl: normalizeTargetUrl(source.targetUrl)
};
if (coverFile) promotion.coverFile = coverFile;
return promotion;
}
function normalizePromotionList(data) {
var items;
if (!Array.isArray(data)) return [];
items = data.map(normalizePromotion);
return items.some(function (item) { return !item; }) ? [] : items;
}
function renderPromotionCard(item) {
var content = (MediaDisplay ? MediaDisplay.renderImage(item.coverFile, {
alt: item.promotionTitle + '封面', className: 'promotion-cover-media'
}) : '') + '<div><h3>' + escapeHtml(item.promotionTitle) + '</h3>' +
'<p>' + escapeHtml(item.promotionDesc) + '</p></div>';
if (!item.targetUrl) {
return '<article class="promotion-card" data-promotion-id="' +
escapeHtml(item.promotionId) + '">' + content + '</article>';
}
return '<a class="promotion-card" data-promotion-id="' + escapeHtml(item.promotionId) +
'" href="' + escapeHtml(item.targetUrl) +
'" target="_blank" rel="noopener noreferrer">' + content + '</a>';
}
function renderPromotionList(data) {
var items = normalizePromotionList(data);
return items.map(renderPromotionCard).join('');
}
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) {
var data;
var items;
if (!api || typeof api.promotions !== 'function') throw new Error('应用推广接口不可用');
data = await api.promotions({ platform: 'pc' });
items = normalizePromotionList(data);
if (!Array.isArray(data) || items.length !== data.length) {
throw new Error('推广列表响应无效');
}
return items;
}
function isUnauthorized(error) {
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]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
if (!page || !list) return;
if (!api || !api.getToken || !api.getToken()) {
setPromotionState(list, 'forbidden', '登录后可查看应用推广。');
return;
}
setPromotionState(list, 'loading', '正在读取应用推广…');
try {
var items = await loadPromotions(api);
if (!items.length) {
setPromotionState(list, 'empty', '当前暂无应用推广。');
return;
}
list.innerHTML = items.map(renderPromotionCard).join('');
} catch (error) {
if (isUnauthorized(error)) {
if (api.clearToken) api.clearToken();
setPromotionState(list, 'forbidden', '登录后可查看应用推广。');
return;
}
setPromotionState(
list,
isForbidden(error) ? 'forbidden' : 'error',
isForbidden(error) ? '当前账号无权查看应用推广。' : '应用推广读取失败,请稍后重试。'
);
}
}
return {
normalizePromotion: normalizePromotion,
normalizePromotionList: normalizePromotionList,
normalizeTargetUrl: normalizeTargetUrl,
renderPromotionList: renderPromotionList,
setPromotionState: setPromotionState,
loadPromotions: loadPromotions,
isUnauthorized: isUnauthorized,
isForbidden: isForbidden,
init: init
};
});
+630
View File
@@ -0,0 +1,630 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.ArticlePages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.ArticlePages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function confirmAction(message) {
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
return root.ProfileUI.confirmAction(message);
}
return Promise.resolve(!root['confirm'] || root['confirm'](message));
}
var MediaDisplay = root.MediaDisplay || (typeof require === 'function' ? require('./media-display.js') : null);
var documentRef = root.document;
var articlesById = Object.create(null);
var writePending = false;
var canEditContent = false;
function getApi() {
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
function query(selector, node) {
return documentRef ? (node || documentRef).querySelector(selector) : null;
}
function queryAll(selector, node) {
return documentRef ? Array.prototype.slice.call((node || documentRef).querySelectorAll(selector)) : [];
}
function stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function trimOrUndefined(value) {
var text = stringValue(value).trim();
return text || undefined;
}
function queryParam(search, name) {
return new URLSearchParams(stringValue(search).replace(/^\?/, '')).get(name) || '';
}
function normalizeId(value) {
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
return /^[1-9][0-9]*$/.test(String(value)) ? String(value) : '';
}
function normalizeNullableId(value) {
if (value === undefined || value === null || value === '') return '';
return normalizeId(value);
}
function getCurrentGenealogyId(search) {
var source = search === undefined && root.location ? root.location.search : search;
var contextId;
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
contextId = root.ProfileUI.getGenealogyId();
if (contextId) return normalizeId(contextId);
}
return normalizeId(queryParam(source, 'genealogyId'));
}
function getCurrentArticleId(search) {
var source = search === undefined && root.location ? root.location.search : search;
return normalizeId(queryParam(source, 'articleId'));
}
function optionalSafeInteger(value) {
var number;
if (value === undefined || value === null || value === '') return undefined;
number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
function buildArticleBody(values) {
var source = values || {};
var body = {
articleTitle: stringValue(source.articleTitle).trim(),
articleContent: stringValue(source.articleContent).trim(),
status: '0'
};
var articleSummary = trimOrUndefined(source.articleSummary);
var coverOssId = normalizeNullableId(source.coverOssId);
var authorName = trimOrUndefined(source.authorName);
var sortOrder = optionalSafeInteger(source.sortOrder);
if (articleSummary !== undefined) body.articleSummary = articleSummary;
if (source.coverOssId === null) body.coverOssId = null;
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
if (coverOssId) body.coverOssId = coverOssId;
else body.invalidCoverOssId = true;
}
if (authorName !== undefined) body.authorName = authorName;
if (source.sortOrder !== undefined && source.sortOrder !== null && source.sortOrder !== '') {
if (sortOrder !== undefined) body.sortOrder = sortOrder;
else body.invalidSortOrder = true;
}
return body;
}
function validateArticleBody(body) {
if (!body || !body.articleTitle) return '请填写谱文标题';
if (!body.articleContent) return '请填写谱文正文';
if (body.invalidCoverOssId) return '封面文件编号无效,请重新选择文件';
if (body.invalidSortOrder) return '排序值必须是安全整数';
if (body.status === '1') return '当前 PC 无法重新读取停用谱文,暂不开放停用';
if (body.status !== '0') return '谱文状态只能是 0';
return '';
}
function normalizeArticle(item) {
var articleId = normalizeId(item && item.articleId);
var genealogyId = normalizeId(item && item.genealogyId);
var categoryId = normalizeNullableId(item && item.categoryId);
var coverFile = MediaDisplay && MediaDisplay.normalizeFileAccess(item && item.coverFile);
var articleTitle = stringValue(item && item.articleTitle).trim();
var articleContent = stringValue(item && item.articleContent).trim();
var viewCount = optionalSafeInteger(item && item.viewCount);
var sortOrder = optionalSafeInteger(item && item.sortOrder);
var status = stringValue(item && item.status);
var article;
if (!articleId || !genealogyId || !articleTitle || !articleContent) return null;
if (item.categoryId !== undefined && item.categoryId !== null && item.categoryId !== '' && !categoryId) return null;
if (item.coverFile !== undefined && item.coverFile !== null && !coverFile) return null;
if (item.viewCount !== undefined && item.viewCount !== null && item.viewCount !== '' && viewCount === undefined) return null;
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
if (status !== '0' && status !== '1') return null;
article = {
articleId: articleId,
genealogyId: genealogyId,
genealogyNo: stringValue(item.genealogyNo),
genealogyName: stringValue(item.genealogyName),
surname: stringValue(item.surname),
categoryName: stringValue(item.categoryName),
categoryCode: stringValue(item.categoryCode),
articleTitle: articleTitle,
articleSummary: stringValue(item.articleSummary),
articleContent: articleContent,
authorName: stringValue(item.authorName),
publishTime: stringValue(item.publishTime),
status: status,
remark: stringValue(item.remark)
};
if (categoryId) article.categoryId = categoryId;
if (coverFile) article.coverFile = coverFile;
if (viewCount !== undefined) article.viewCount = viewCount;
if (sortOrder !== undefined) article.sortOrder = sortOrder;
return article;
}
function normalizeArticles(data) {
var normalized;
if (!Array.isArray(data)) return [];
normalized = data.map(normalizeArticle);
return normalized.some(function (article) { return !article; }) ? [] : normalized;
}
function matchesSavedArticle(item, expectedArticleId) {
var article = normalizeArticle(item);
return Boolean(article && article.articleId === normalizeId(expectedArticleId));
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildListUrl(genealogyId, articleId) {
var url = 'profile-article.html?genealogyId=' + encodeURIComponent(genealogyId);
return articleId ? url + '&articleId=' + encodeURIComponent(articleId) : url;
}
function buildEditUrl(genealogyId, articleId) {
var url = 'profile-article-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
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>';
}
function renderArticleDetail(article) {
var actions = '';
if (!article) return stateHtml('empty', '请选择一篇谱文查看详情');
if (canEditContent) {
actions = '<div class="bottom-actions"><a class="btn ghost" href="' +
escapeHtml(buildEditUrl(article.genealogyId, article.articleId)) + '">编辑谱文</a>' +
'<button class="btn danger" type="button" data-article-delete-id="' +
escapeHtml(article.articleId) + '">删除谱文</button></div>';
}
return (MediaDisplay ? MediaDisplay.renderImage(article.coverFile, {
alt: article.articleTitle + '封面', className: 'article-cover-media'
}) : '') + '<div class="form-like article-detail">' +
detailValue('标题', article.articleTitle) +
detailValue('摘要', article.articleSummary) +
detailValue('正文', article.articleContent) +
detailValue('作者', article.authorName) +
detailValue('分类', article.categoryName) +
detailValue('发布时间', article.publishTime) +
detailValue('浏览量', article.viewCount === undefined ? '' : article.viewCount) +
detailValue('备注', article.remark) +
'</div>' + actions;
}
function renderArticleRow(article) {
var actions = '<button class="pill" type="button" data-article-detail-id="' +
escapeHtml(article.articleId) + '">查看详情</button>';
if (canEditContent) {
actions += '<a class="pill" href="' + escapeHtml(buildEditUrl(article.genealogyId, article.articleId)) +
'">编辑</a><button class="pill is-danger" type="button" data-article-delete-id="' +
escapeHtml(article.articleId) + '">删除</button>';
}
return '<article class="module-row article-row"><div><h3>' + escapeHtml(article.articleTitle) +
'</h3><p>' + escapeHtml(article.articleSummary || article.authorName || article.publishTime || '暂无摘要') +
'</p></div><div class="row-actions">' + actions + '</div></article>';
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
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();
root.NavigationUtil.open('login.html');
return true;
}
function showMessage(message) {
if (root.layui && root.layui.layer) root.layui.layer.msg(message);
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, type) {
var target = query('[data-article-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-article-editor-placeholder]');
queryAll('[data-article-editor], [data-article-editor-action]').forEach(function (element) {
element.hidden = !enabled;
});
if (placeholder) {
placeholder.hidden = Boolean(enabled);
if (message) setState(placeholder, type || getStateType(message), message);
}
}
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() {
var message = '请先选择家谱,再查看或维护谱文。';
var list = query('[data-article-list]');
var detail = query('[data-article-detail]');
setState(list, 'empty', message);
setState(detail, 'empty', '当前没有家谱上下文。');
setEditorEnabled(false, message, 'empty');
setFormStatus(message, 'empty');
}
function requireGenealogyId() {
var genealogyId = getCurrentGenealogyId();
if (!genealogyId) renderNoContext();
return genealogyId;
}
async function loadCapability(api, genealogyId) {
var detail = await api.genealogyDetail(genealogyId);
canEditContent = canEditArticles(detail);
queryAll('[data-article-create-link], [data-article-editor-action]').forEach(function (element) {
element.hidden = !canEditContent;
});
return detail;
}
function renderArticles(data) {
var container = query('[data-article-list]');
var articles = normalizeArticles(data);
articlesById = Object.create(null);
articles.forEach(function (article) {
articlesById[article.articleId] = article;
});
if (!container) return articles;
if (!Array.isArray(data) || (data.length && !articles.length)) {
setState(container, 'error', '谱文响应缺少稳定 ArticleVo 字段,请联系后端核对。');
return [];
}
if (!articles.length) {
setState(container, 'empty', '暂无谱文');
return [];
}
container.innerHTML = articles.map(renderArticleRow).join('');
return articles;
}
function renderDetail(article) {
var container = query('[data-article-detail]');
if (container) container.innerHTML = renderArticleDetail(article);
}
async function loadArticleDetail(articleId) {
var api = getApi();
var genealogyId = requireGenealogyId();
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)) {
throw new Error('谱文详情响应缺少稳定 ArticleVo 字段');
}
articlesById[article.articleId] = article;
renderDetail(article);
return article;
} catch (error) {
if (redirectUnauthorized(api, error)) return null;
setState(query('[data-article-detail]'), isForbidden(error) ? 'forbidden' : 'error',
isForbidden(error) ? '当前账号无权查看该谱文。' : '谱文详情加载失败,请稍后重试。');
showMessage(isForbidden(error) ? '当前账号无权查看该谱文。' : (error.message || '谱文详情加载失败'));
return null;
}
}
async function loadArticles() {
var api = getApi();
var genealogyId;
var articles;
var requestedId;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncLinks();
setState(query('[data-article-list]'), 'loading', '正在加载谱文…');
try {
await loadCapability(api, genealogyId);
articles = renderArticles(await api.articles(genealogyId));
requestedId = getCurrentArticleId();
if (requestedId && articlesById[requestedId]) await loadArticleDetail(requestedId);
else if (articles.length) await loadArticleDetail(articles[0].articleId);
else renderDetail(null);
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setState(query('[data-article-list]'), isForbidden(error) ? 'forbidden' : 'error',
isForbidden(error) ? '当前账号无权查看该家谱的谱文。' : '谱文加载失败,请稍后重试。');
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的谱文。' : (error.message || '谱文加载失败'));
}
}
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = root.AttachmentEditor ? root.AttachmentEditor.readField(field) : field.value;
});
return values;
}
function setFieldValue(name, value) {
var field = query('[data-article-form] [name="' + name + '"]');
if (field) field.value = value === undefined || value === null ? '' : String(value);
}
function fillArticleForm(article) {
var contentField;
var contentEditor;
if (!article || article.status !== '0') throw new Error('停用谱文无法通过当前 PC 接口重新读取或编辑');
setFieldValue('articleTitle', article.articleTitle);
setFieldValue('articleSummary', article.articleSummary);
setFieldValue('articleContent', article.articleContent);
setFieldValue('authorName', article.authorName);
setFieldValue('sortOrder', article.sortOrder);
setFieldValue('status', '0');
contentField = query('[name="articleContent"]');
if (root.AppRichEditor && root.AppRichEditor.init && contentField) {
contentEditor = root.AppRichEditor.init(contentField);
if (contentEditor && contentEditor.editor && contentEditor.editor.setHtml) {
contentEditor.editor.setHtml(article.articleContent);
contentEditor.sync();
}
}
}
async function loadArticleEditor() {
var api = getApi();
var genealogyId;
var articleId;
var article;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
articleId = getCurrentArticleId();
syncLinks();
setEditorEnabled(false, '正在加载谱文编辑信息…', 'loading');
setFormStatus('正在读取家谱权限…', 'loading');
try {
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的谱文。' };
if (articleId) {
var articleResponse = await api.articleDetail(genealogyId, articleId);
article = normalizeArticle(articleResponse);
if (!article || article.articleId !== articleId) throw new Error('谱文详情响应缺少稳定 ArticleVo 字段');
fillArticleForm(article);
root.AttachmentEditor.setFiles('#articleCoverOssId', articleResponse.coverFile ? [articleResponse.coverFile] : []);
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 || '谱文编辑信息加载失败'), isForbidden(error) ? 'forbidden' : 'error');
setFormStatus(
isForbidden(error) ? '当前账号无权维护该家谱的谱文。' : (error.message || '谱文编辑信息加载失败'),
isForbidden(error) ? 'forbidden' : 'error'
);
}
}
async function submitArticle(form) {
var api = getApi();
var genealogyId = requireGenealogyId();
var articleId = getCurrentArticleId();
var body;
var validation;
var result;
var saved;
var savedId;
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, 'error');
return;
}
delete body.invalidCoverOssId;
delete body.invalidSortOrder;
setWritePending(true, '正在保存…');
setFormStatus('正在保存谱文…', 'loading');
try {
result = articleId
? await api.updateArticle(genealogyId, articleId, body)
: await api.createArticle(genealogyId, body);
saved = normalizeArticle(result);
savedId = articleId || (saved && saved.articleId);
if (!savedId) throw new Error('保存响应缺少 articleId');
if (!matchesSavedArticle(await api.articleDetail(genealogyId, savedId), savedId)) {
throw new Error('保存后重读未返回同一篇谱文');
}
root.NavigationUtil.open(buildListUrl(genealogyId, savedId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFormStatus(
isForbidden(error) ? '当前账号无权保存该谱文。' : (error.message || '谱文保存失败'),
isForbidden(error) ? 'forbidden' : 'error'
);
} finally {
setWritePending(false);
}
}
async function deleteArticle(articleId) {
var api = getApi();
var genealogyId = requireGenealogyId();
var article = articlesById[articleId];
if (writePending || !canEditContent || !genealogyId || !normalizeId(articleId) || redirectUnauthorized(api)) return;
if (!await confirmAction('确认删除“' + (article ? article.articleTitle : '该谱文') + '”吗?删除后不可恢复。')) return;
setWritePending(true);
try {
await api.deleteArticle(genealogyId, articleId);
await loadArticles();
showMessage('谱文已删除');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
showMessage(isForbidden(error) ? '当前账号无权删除该谱文。' : (error.message || '谱文删除失败'));
} finally {
setWritePending(false);
}
}
function bindActions() {
if (!documentRef) return;
documentRef.addEventListener('click', function (event) {
var detailButton = event.target.closest('[data-article-detail-id]');
var deleteButton = event.target.closest('[data-article-delete-id]');
if (detailButton) {
event.preventDefault();
loadArticleDetail(detailButton.getAttribute('data-article-detail-id'));
return;
}
if (deleteButton) {
event.preventDefault();
deleteArticle(deleteButton.getAttribute('data-article-delete-id'));
}
});
documentRef.addEventListener('submit', function (event) {
var form = event.target.closest('[data-article-form]');
if (!form) return;
event.preventDefault();
submitArticle(form);
});
}
function init() {
if (!documentRef) return;
bindActions();
if (query('[data-article-page]')) loadArticles();
if (query('[data-article-edit-page]')) loadArticleEditor();
}
return {
getCurrentGenealogyId: getCurrentGenealogyId,
getCurrentArticleId: getCurrentArticleId,
buildArticleBody: buildArticleBody,
validateArticleBody: validateArticleBody,
normalizeArticle: normalizeArticle,
canEditArticles: canEditArticles,
normalizeArticles: normalizeArticles,
matchesSavedArticle: matchesSavedArticle,
renderArticleDetail: renderArticleDetail,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,
init: init
};
});
+182
View File
@@ -0,0 +1,182 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.AttachmentEditor = factory(root);
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
var editors = new Map();
function normalizeFiles(files) {
var seen = new Set();
return (files || []).map(function (file) {
var ossId = file && file.ossId;
if ((typeof ossId === 'number' && !Number.isSafeInteger(ossId)) || !/^[1-9][0-9]*$/.test(String(ossId))) {
throw new Error('原附件缺少有效的上传标识,暂不能保存,请刷新后重试');
}
return {
ossId: String(ossId),
fileName: String(file.fileName || '附件'),
mediaType: String(file.mediaType || '').toLowerCase(),
accessUrl: root.MediaDisplay ? (root.MediaDisplay.normalizeFileAccess(file) || {}).accessUrl || '' : ''
};
}).filter(function (file) {
if (seen.has(file.ossId)) return false;
seen.add(file.ossId);
return true;
});
}
function release(files) {
files.forEach(function (file) {
if (file.objectUrl) root.URL.revokeObjectURL(file.objectUrl);
});
}
function getEditor(target) {
if (typeof target === 'string') target = root.document.querySelector(target);
return editors.get(target);
}
function sync(editor) {
editor.target.value = editor.files.map(function (file) { return file.ossId; }).join(',');
editor.preview.replaceChildren();
editor.files.forEach(function (file) {
var card = root.document.createElement('div');
var url = file.objectUrl || file.accessUrl;
var name = root.document.createElement('span');
var remove = root.document.createElement('button');
var media;
card.className = 'attachment-edit-item';
if (url && /^(image|video)(\/|$)/.test(file.mediaType)) {
media = root.document.createElement(/^image/.test(file.mediaType) ? 'img' : 'video');
media.src = url;
if (media.tagName === 'IMG') media.alt = file.fileName;
else { media.controls = true; media.preload = 'metadata'; }
media.addEventListener('error', function () {
media.hidden = true;
name.textContent = file.fileName + '(预览不可用,附件仍保留)';
});
card.appendChild(media);
}
name.className = 'attachment-edit-name';
name.textContent = file.fileName;
card.appendChild(name);
remove.type = 'button';
remove.className = 'pill attachment-edit-remove';
remove.textContent = '移除';
remove.setAttribute('aria-label', '移除 ' + file.fileName);
remove.disabled = editor.busy || editor.input.disabled;
// 相册接口尚不支持可靠清空封面,仅允许替换。
if (editor.input.hasAttribute('data-attachment-replace-only')) {
remove.hidden = true;
}
remove.addEventListener('click', function () {
if (editor.busy || editor.input.disabled) return;
editor.files = editor.files.filter(function (existing) { return existing !== file; });
release([file]);
editor.cleared = true;
sync(editor);
});
card.appendChild(remove);
editor.preview.appendChild(card);
});
if (editor.status && !editor.busy) {
editor.status.textContent = editor.files.length
? '已选择 ' + editor.files.length + ' 个文件;移除或替换将在保存后生效'
: (editor.cleared ? '附件已移除,保存后生效' : '未选择文件');
}
}
function init() {
root.document.querySelectorAll('[data-attachment-editor]').forEach(function (input) {
var target = root.document.querySelector(input.getAttribute('data-upload-target'));
var preview;
var editor;
if (!target || editors.has(target)) return;
preview = root.document.createElement('div');
preview.className = 'attachment-edit-list';
preview.setAttribute('aria-label', '已选择的附件');
input.insertAdjacentElement('afterend', preview);
editor = {
target: target, input: input, preview: preview,
status: root.document.querySelector(input.getAttribute('data-upload-status')),
multiple: input.getAttribute('data-upload-multiple') === 'true',
files: [], cleared: false, busy: false
};
editors.set(target, editor);
new root.MutationObserver(function () {
preview.querySelectorAll('button').forEach(function (button) { button.disabled = input.disabled || editor.busy; });
}).observe(input, { attributes: true, attributeFilter: ['disabled'] });
});
}
function setFiles(target, files) {
init();
var editor = getEditor(target);
if (!editor) throw new Error('附件编辑控件未初始化');
editor.invalid = true;
var normalized = normalizeFiles(files);
release(editor.files);
editor.files = normalized;
editor.cleared = false;
editor.invalid = false;
sync(editor);
}
function addUploaded(target, upload, file) {
var editor = getEditor(target);
if (!editor) return false;
var attachment = normalizeFiles([{ ossId: upload.ossId, fileName: file.name, mediaType: file.type }])[0];
if (editor.files.some(function (existing) { return existing.ossId === attachment.ossId; })) return true;
attachment.objectUrl = root.URL.createObjectURL(file);
if (!editor.multiple) { release(editor.files); editor.files = []; }
editor.files.push(attachment);
editor.cleared = false;
sync(editor);
return true;
}
function clear(target) {
var editor = getEditor(target);
if (!editor || editor.busy || editor.input.disabled) return;
release(editor.files);
editor.files = [];
editor.cleared = true;
sync(editor);
}
function readField(field) {
var editor = getEditor(field);
// nullable 单封面 PATCH:空值仅在用户主动移除时提交 null。
if (editor && !editor.multiple && editor.cleared && !editor.files.length) return null;
return field.value;
}
function setBusy(target, busy) {
var editor = getEditor(target);
if (!editor) return;
editor.busy = busy;
editor.preview.querySelectorAll('button').forEach(function (button) { button.disabled = busy || editor.input.disabled; });
}
if (root.document) {
root.document.addEventListener('DOMContentLoaded', init);
root.document.addEventListener('submit', function (event) {
var pending = Array.from(editors.values()).some(function (editor) { return editor.input.form === event.target && editor.busy; });
var invalid = Array.from(editors.values()).some(function (editor) { return editor.input.form === event.target && editor.invalid; });
if (!pending && !invalid) return;
event.preventDefault();
event.stopImmediatePropagation();
var status = event.target.querySelector('.upload-status');
if (status) status.textContent = invalid ? '原附件读取不完整,请刷新后重试,暂不能保存' : '文件正在上传,请等待上传完成后再保存';
}, true);
root.addEventListener('pagehide', function (event) {
if (!event.persisted) editors.forEach(function (editor) { release(editor.files); });
});
}
return { init: init, normalizeFiles: normalizeFiles, setFiles: setFiles, addUploaded: addUploaded, clear: clear, readField: readField, setBusy: setBusy };
});
+121 -40
View File
@@ -91,6 +91,17 @@
return root.GenealogyApi && root.GenealogyApi.defaultClient;
}
async function cacheCurrentUser(api) {
if (!root.PublicNavigation) return;
root.PublicNavigation.clearUser();
try {
root.PublicNavigation.storeUser(await api.currentProfile());
} catch (error) {
// 账户摘要加载失败不应阻断已经成功的登录,个人中心会再次读取并同步资料。
root.PublicNavigation.clearUser();
}
}
function getCaptchaOperation(formType) {
// PC 验证中心按业务动作解析服务端已激活的验证场景。
var config = FORM_CONFIG[formType] || {};
@@ -104,6 +115,39 @@
return config.successUrl || '';
}
function normalizeGenealogyId(value) {
var id = value === undefined || value === null ? '' : String(value).trim();
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
return /^[1-9][0-9]*$/.test(id) ? id : '';
}
function getPostLoginUrl(genealogies) {
var ids = (Array.isArray(genealogies) ? genealogies : []).map(function (item) {
return normalizeGenealogyId(item && item.genealogyId);
}).filter(Boolean).filter(function (id, index, values) {
return values.indexOf(id) === index;
});
if (ids.length === 1) {
return 'profile-family-home.html?genealogyId=' + encodeURIComponent(ids[0]);
}
return 'profile-families.html';
}
async function resolvePostLoginUrl(api, formType) {
if ((formType !== 'login' && formType !== 'sms-login') || !api || !api.genealogiesMine) {
return getSuccessUrl(formType);
}
try {
return getPostLoginUrl(await api.genealogiesMine());
} catch (error) {
return getSuccessUrl(formType);
}
}
function getFormCaptchaOperation(form, formType) {
// 业务动作由当前脚本统一管理,避免散落在 HTML 属性中。
var operationCode = getCaptchaOperation(formType);
@@ -196,6 +240,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);
@@ -259,14 +330,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 || {};
@@ -275,11 +338,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;
}
@@ -317,21 +382,26 @@
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 = getSuccessUrl('login');
await cacheCurrentUser(api);
root.NavigationUtil.open(await resolvePostLoginUrl(api, 'login'));
} catch (error) {
showMessage(error.message || '登录失败');
setFormStatus(form, error.message || '登录失败', getApiStateType(error));
} finally {
setBusy(button, false);
setSubmitPending(button, false, '正在登录…');
}
}
@@ -341,18 +411,20 @@
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 = getSuccessUrl('sms-login');
await cacheCurrentUser(api);
root.NavigationUtil.open(await resolvePostLoginUrl(api, 'sms-login'));
} catch (error) {
showMessage(error.message || '短信登录失败');
setFormStatus(form, error.message || '短信登录失败', getApiStateType(error));
} finally {
setBusy(button, false);
setSubmitPending(button, false, '正在登录…');
}
}
@@ -362,18 +434,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');
root.NavigationUtil.open(getSuccessUrl('register'));
} catch (error) {
showMessage(error.message || '注册失败');
setFormStatus(form, error.message || '注册失败', getApiStateType(error));
} finally {
setBusy(button, false);
setSubmitPending(button, false, '正在注册…');
}
}
@@ -386,26 +459,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);
}
}
@@ -416,19 +494,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');
root.NavigationUtil.open(getSuccessUrl('password-reset'));
} catch (error) {
showMessage(error.message || '重设密码失败');
setFormStatus(form, error.message || '重设密码失败', getApiStateType(error));
} finally {
setBusy(button, false);
setSubmitPending(button, false, '正在重设…');
}
}
@@ -489,6 +568,8 @@
getCaptchaOperation: getCaptchaOperation,
getFormCaptchaOperation: getFormCaptchaOperation,
getSuccessUrl: getSuccessUrl,
getPostLoginUrl: getPostLoginUrl,
resolvePostLoginUrl: resolvePostLoginUrl,
validateAuthValues: validateAuthValues,
init: init
};
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More