修改完成

This commit is contained in:
2026-09-13 19:41:13 +08:00
parent 15b301b358
commit d040507cb8
27 changed files with 2238 additions and 551 deletions
+39
View File
@@ -0,0 +1,39 @@
# 首页设计还原 QA
## 对照基准
- 设计源:用户在当前对话提供的桌面首页设计图,红色文字和箭头作为替换与结构调整标注。
- 实现基线:原首页 `index-modern.html` 的品牌色、字体、卡片质感和 `public/js/page-effects.js` 交互。
- 桌面审查截图:`artifacts/homepage-final-review-full.png`CSS 视口 1440 × 2200DPR 1。
- 移动审查截图:`artifacts/homepage-final-review-mobile.png`CSS 视口 500 × 1600DPR 1。
- 页面状态:未登录初始状态。
## 结构对照
- 页面沿用原首页 1280px 宽屏容器,顺序为双层导航、首屏、四个主入口、第一条九格广告栏、六个内容入口、推荐与友情链接、第二条九格广告栏、页脚。
- 已删除设计图没有的家谱广场、文章列表、姓氏百科、相关推荐大卡、下载大区块。
- “我的家谱”是独立强调入口,链接 `profile-families.html`,承担查看、新建和管理全部家谱。
- 红色标注按替换文案处理;被替换的旧标题没有在同一卡片中保留。
## 视觉与交互对照
- 保留原首页的暖纸色、朱红渐变导航、宋体标题、黑体正文、宽屏留白、圆角卡片、层次阴影与半透明面板。
- 首屏按设计图重组为正文、二维码、原有蓝色家谱封面、祠堂和家族记忆图片。
- 保留原首页的 `tilt-card` 指针倾斜、按钮磁吸、图片悬停缩放、移动菜单和 reduced-motion 行为。
- 桌面页脚链接与备案信息已恢复居中,不受原全局链接布局影响。
- 主入口图片按左侧内容定位,避免服务、视频和相册标题被居中裁切;入口区、内容卡和广告栏已压缩无效留白。
- 两组九格广告栏均使用项目已有广告图片,不再显示文字占位块。
- 页脚链接区改为完整浅色横条,消除了深色背景在两侧提前露出的断层。
## 响应式与可访问性
- 1120px 以下主入口和广告栏分栏;720px 以下首屏、入口和内容卡改为单列。
- 浏览器导航结构专项检查通过,移动断点继续沿用原首页规则。
- 图片提供替代文本;二维码入口、导航、广告位和内容卡均使用链接语义;焦点样式沿用公共样式。
## 剩余差异
- P3:设计图右下角为多人家族合影,仓库没有对应素材,当前继续使用原有家族记忆图片。
- P3:原首页交互动效在截图中不可见,但已通过复用原脚本和交互类保留。
final result: passed
+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>
+183 -471
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
@@ -15,9 +15,11 @@
<link rel="stylesheet" href="public/css/public.css" />
<link rel="stylesheet" href="public/css/index.css" />
</head>
<body class="page-home">
<body class="page-home page-home-portal">
<div class="company-bar">
<div class="container">富众鑫科技</div>
<div class="container company-bar-inner">
<strong>富众鑫文化科技</strong>
</div>
</div>
<header class="site-header">
<div class="container nav">
@@ -27,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
>
@@ -48,509 +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="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>
<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="genealogy.html">数字家谱</a>
<a href="help.html">使用帮助</a>
<a href="promo-video.html">视频内容</a>
</div>
</div>
<div class="portal-resource-group">
<h2>友情链接</h2>
<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>
家族成员可以在手机端上传照片、查看家谱、发布动态;电脑端适合浏览门户内容、查看公开家谱和进入家谱空间。
</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>
<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 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
>
<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>
+2 -1
View File
@@ -62,7 +62,7 @@
<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-upload-target="#albumCoverOssId" data-upload-status="#albumCoverStatus" />
<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">
@@ -93,6 +93,7 @@
<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>
+2 -1
View File
@@ -103,7 +103,7 @@
<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" />
<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" />
@@ -133,6 +133,7 @@
<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/article-pages.js"></script>
+3 -1
View File
@@ -64,7 +64,7 @@
<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/*" multiple data-upload-multiple="true" data-upload-target="#feedMedia" data-upload-status="#feedMediaStatus" />
<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" data-feed-advanced hidden>
@@ -105,6 +105,8 @@
<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>
+2 -1
View File
@@ -104,7 +104,7 @@
<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" />
<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">
@@ -138,6 +138,7 @@
<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/ceremony-admin-pages.js"></script>
+2 -1
View File
@@ -98,7 +98,7 @@
<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" data-growth-media-input />
<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>
@@ -137,6 +137,7 @@
<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>
+2 -1
View File
@@ -34,7 +34,7 @@
<div class="editor-field"><label for="memoCompleted">完成状态</label><select id="memoCompleted" name="completed"><option value="0">未完成</option><option value="1">已完成</option></select></div>
</div>
<div class="editor-two">
<div class="editor-field"><label for="memoMediaFiles">附件文件</label><input id="memoMediaOssIds" name="mediaOssIds" type="hidden" /><label class="upload-control" for="memoMediaFiles">选择附件</label><input id="memoMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-upload-target="#memoMediaOssIds" data-upload-status="#memoMediaStatus" data-memo-media-input /><p id="memoMediaStatus" class="upload-status" data-memo-media-status role="status" aria-live="polite">未选择文件</p><button class="pill" type="button" data-memo-clear-media>清除附件</button></div>
<div class="editor-field"><label for="memoMediaFiles">附件文件</label><input id="memoMediaOssIds" name="mediaOssIds" type="hidden" /><label class="upload-control" for="memoMediaFiles">选择附件</label><input id="memoMediaFiles" class="upload-input" type="file" multiple data-upload-multiple="true" data-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>
<input name="status" type="hidden" value="0" />
@@ -57,6 +57,7 @@
<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>
+2 -1
View File
@@ -93,7 +93,7 @@
type="file"
multiple
data-upload-multiple="true"
data-upload-target="#relativeMediaOssIds"
data-attachment-editor data-upload-target="#relativeMediaOssIds"
data-upload-status="#relativeMediaStatus"
data-relative-media-input
/>
@@ -125,6 +125,7 @@
<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>
+3 -2
View File
@@ -66,7 +66,7 @@
class="upload-input"
type="file"
accept="video/*"
data-upload-target="#video-oss-id"
data-attachment-editor data-upload-target="#video-oss-id"
data-upload-status="#video-file-status"
data-video-duration-target="#video-duration"
/>
@@ -81,7 +81,7 @@
class="upload-input"
type="file"
accept="image/*"
data-upload-target="#video-cover-oss-id"
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>
@@ -121,6 +121,7 @@
<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>
+972 -3
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -1263,3 +1263,34 @@
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; }
+3 -3
View File
@@ -1425,9 +1425,9 @@ textarea {
@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;
+2 -1
View File
@@ -510,7 +510,6 @@
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]', 'coverOssId', album.coverOssId);
setFieldValue('[data-album-form]', 'sortOrder', album.sortOrder);
setFieldValue('[data-album-form]', 'status', '0');
}
@@ -537,6 +536,8 @@
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);
+5 -3
View File
@@ -102,6 +102,7 @@
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;
@@ -446,7 +447,7 @@
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
values[field.name] = root.AttachmentEditor ? root.AttachmentEditor.readField(field) : field.value;
});
return values;
}
@@ -464,7 +465,6 @@
if (!article || article.status !== '0') throw new Error('停用谱文无法通过当前 PC 接口重新读取或编辑');
setFieldValue('articleTitle', article.articleTitle);
setFieldValue('articleSummary', article.articleSummary);
setFieldValue('coverOssId', article.coverOssId);
setFieldValue('articleContent', article.articleContent);
setFieldValue('authorName', article.authorName);
setFieldValue('sortOrder', article.sortOrder);
@@ -496,9 +496,11 @@
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的谱文。' };
if (articleId) {
article = normalizeArticle(await api.articleDetail(genealogyId, 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);
+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 };
});
+6 -3
View File
@@ -166,6 +166,7 @@
if (latitude !== undefined) body.latitude = latitude;
else body.invalidLatitude = true;
}
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;
@@ -615,7 +616,7 @@
function getFormValues(form) {
var values = {};
queryAll('[name]', form).forEach(function (field) { values[field.name] = field.value; });
queryAll('[name]', form).forEach(function (field) { values[field.name] = root.AttachmentEditor ? root.AttachmentEditor.readField(field) : field.value; });
return values;
}
@@ -630,7 +631,7 @@
var contentEditor;
if (!ceremony || ceremony.status !== '0') throw new Error('停用活动无法通过当前 PC 接口重新读取或编辑');
['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'location', 'locationAddress', 'longitude', 'latitude', 'coverOssId', 'sortOrder'].forEach(function (name) {
['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'location', 'locationAddress', 'longitude', 'latitude', 'sortOrder'].forEach(function (name) {
setFieldValue('[data-ceremony-form]', name, ceremony[name]);
});
setFieldValue('[data-ceremony-form]', 'ceremonyTime', toDateTimeInputValue(ceremony.ceremonyTime));
@@ -662,9 +663,11 @@
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的祭祀活动。' };
if (ceremonyId) {
ceremony = normalizeCeremony(await api.ceremonyDetail(genealogyId, ceremonyId));
var ceremonyResponse = await api.ceremonyDetail(genealogyId, ceremonyId);
ceremony = normalizeCeremony(ceremonyResponse);
if (!ceremony || ceremony.ceremonyId !== ceremonyId) throw new Error('活动详情响应缺少稳定 ceremonyId');
fillCeremonyForm(ceremony);
root.AttachmentEditor.setFiles('#giftCoverOssId', ceremonyResponse.coverFile ? [ceremonyResponse.coverFile] : []);
if (query('[data-ceremony-editor-title]')) query('[data-ceremony-editor-title]').textContent = '编辑祭祀活动';
}
setEditorEnabled(true);
+13 -1
View File
@@ -302,7 +302,7 @@
if (trimOrUndefined(source.sortOrder) !== undefined && sortOrder === undefined) {
throw new Error('排序值必须是非负整数');
}
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (source.mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds || '';
if (status !== undefined) body.status = status;
if (sortOrder !== undefined) body.sortOrder = sortOrder;
return body;
@@ -618,11 +618,22 @@
function fillFeedForm(data) {
var feed = normalizeFeed(data);
var contentField;
var contentEditor;
if (!feed) throw new Error('动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO。');
queryAll('[data-feed-form] [name]').forEach(function (field) {
if (feed[field.name] !== undefined && feed[field.name] !== null) field.value = feed[field.name];
});
contentField = query('[data-feed-form] [name="feedContent"]');
if (contentField && root.AppRichEditor && root.AppRichEditor.init) {
contentEditor = root.AppRichEditor.init(contentField);
if (contentEditor && contentEditor.editor && contentEditor.editor.setHtml) {
contentEditor.editor.setHtml(feed.feedContent);
contentEditor.sync();
}
}
root.AttachmentEditor.setFiles('#feedMedia', data.mediaFiles || []);
}
function canEditFeeds(genealogy) {
@@ -788,6 +799,7 @@
setFeedFormStatus('');
} catch (error) {
if (redirectUnauthorized(api, error)) return;
setFeedFormEnabled(false);
setFeedFormStatus(
getApiStateType(error) === 'forbidden' ? '当前账号无权维护该家谱的动态。' : (error.message || '动态详情加载失败'),
getApiStateType(error)
+4 -15
View File
@@ -124,7 +124,7 @@
if (recordContent !== undefined) body.recordContent = recordContent;
if (recordDate !== undefined) body.recordDate = recordDate;
if (remindTime !== undefined) body.remindTime = remindTime;
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (source.mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds || '';
if (sortOrderText !== undefined) {
body.sortOrder = toSafeInteger(sortOrderText);
if (body.sortOrder === undefined) body.invalidSortOrder = true;
@@ -136,7 +136,7 @@
function validateGrowthRecordBody(body) {
if (!body || !body.recordTitle) return '请填写记录标题';
if (body.lineagePersonId !== undefined && !normalizeId(body.lineagePersonId)) return '请选择有效的世系人物';
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
if (body.mediaOssIds !== undefined && body.mediaOssIds !== '' && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件上传结果无效';
}
if (body.invalidSortOrder || (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && !Number.isSafeInteger(body.sortOrder))) {
@@ -554,7 +554,6 @@
setFieldValue('recordContent', record.recordContent);
setFieldValue('recordDate', toDateInputValue(record.recordDate));
setFieldValue('remindTime', toDateTimeInputValue(record.remindTime));
setFieldValue('mediaOssIds', record.mediaOssIds);
setFieldValue('sortOrder', record.sortOrder);
setFieldValue('status', '0');
contentField = query('[data-growth-form] [name="recordContent"]');
@@ -564,11 +563,6 @@
editorInstance.editor.setHtml(record.recordContent || '');
}
}
if (query('[data-growth-media-status]')) {
query('[data-growth-media-status]').textContent = record.mediaOssIds
? '已保留 ' + attachmentCount(record.mediaOssIds) + ' 个附件,可继续选择追加'
: '未选择文件';
}
}
function applyLineageOptions(data, selectedId, fallbackName) {
@@ -603,6 +597,7 @@
applyLineageOptions(result[0], record && record.lineagePersonId, record && record.lineagePersonName);
if (record) {
fillGrowthForm(record);
root.AttachmentEditor.setFiles('#growthMediaOssIds', result[1].mediaFiles || []);
if (query('[data-growth-editor-title]')) query('[data-growth-editor-title]').textContent = '编辑成长记录';
}
setEditorEnabled(true);
@@ -683,13 +678,7 @@
}
function clearMediaSelection() {
var target = query('[data-growth-form] [name="mediaOssIds"]');
var status = query('[data-growth-media-status]');
var fileInput = query('[data-growth-media-input]');
if (target) target.value = '';
if (fileInput) fileInput.value = '';
if (status) status.textContent = '未选择文件';
root.AttachmentEditor.clear('#growthMediaOssIds');
}
function bindActions() {
+6 -16
View File
@@ -110,7 +110,7 @@
if (memoContent !== undefined) body.memoContent = memoContent;
if (remindTime !== undefined) body.remindTime = remindTime;
if (completed !== undefined) body.completed = completed;
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (source.mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds || '';
if (sortOrderText !== undefined) {
body.sortOrder = toSafeInteger(sortOrderText);
if (body.sortOrder === undefined) body.invalidSortOrder = true;
@@ -141,7 +141,7 @@
if (body.completed !== undefined && body.completed !== '0' && body.completed !== '1') {
return '完成状态只能是未完成或已完成';
}
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
if (body.mediaOssIds !== undefined && body.mediaOssIds !== '' && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件上传结果无效';
}
if (body.invalidSortOrder || (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && !Number.isSafeInteger(body.sortOrder))) {
@@ -518,14 +518,8 @@
setFieldValue('memoContent', memo.memoContent);
setFieldValue('remindTime', toDateTimeInputValue(memo.remindTime));
setFieldValue('completed', memo.completed);
setFieldValue('mediaOssIds', memo.mediaOssIds);
setFieldValue('sortOrder', memo.sortOrder);
setFieldValue('status', '0');
if (query('[data-memo-media-status]')) {
query('[data-memo-media-status]').textContent = memo.mediaOssIds
? '已保留 ' + attachmentCount(memo.mediaOssIds) + ' 个附件,可继续选择追加'
: '未选择文件';
}
}
async function loadMemoEditor() {
@@ -545,9 +539,11 @@
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的备忘录。' };
if (memoId) {
memo = normalizeMemo(await api.memoDetail(genealogyId, memoId));
var memoResponse = await api.memoDetail(genealogyId, memoId);
memo = normalizeMemo(memoResponse);
if (!memo) throw new Error('备忘录详情响应缺少稳定 MemoVo 字段');
fillMemoForm(memo);
root.AttachmentEditor.setFiles('#memoMediaOssIds', memoResponse.mediaFiles || []);
if (query('[data-memo-editor-title]')) query('[data-memo-editor-title]').textContent = '编辑备忘录';
}
setEditorEnabled(true);
@@ -628,13 +624,7 @@
}
function clearMediaSelection() {
var target = query('[data-memo-form] [name="mediaOssIds"]');
var status = query('[data-memo-media-status]');
var fileInput = query('[data-memo-media-input]');
if (target) target.value = '';
if (fileInput) fileInput.value = '';
if (status) status.textContent = '未选择文件';
root.AttachmentEditor.clear('#memoMediaOssIds');
}
function bindActions() {
+4 -3
View File
@@ -252,7 +252,7 @@
menu.setAttribute("aria-hidden", "true");
inner.className = "container site-mobile-menu-inner";
[...navLinks.querySelectorAll("a"), ...(navActions ? navActions.querySelectorAll("a") : [])].forEach((link) => {
[...navLinks.querySelectorAll("a, button"), ...(navActions ? navActions.querySelectorAll("a") : [])].forEach((link) => {
inner.appendChild(link.cloneNode(true));
});
menu.appendChild(inner);
@@ -288,9 +288,10 @@
initSiteMenu();
refreshPublicUser();
// 只在精确指针设备上启用动效,触屏和减少动效偏好下保持静态
// 鼠标反馈仅用于精确指针;首页独立启用动效,其他页面遵循系统偏好
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const reduceMotion = !document.body.classList.contains("page-home-portal") &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!finePointer || reduceMotion) return;
+6 -16
View File
@@ -150,7 +150,7 @@
else if (!isExactJsonNumber(amountText, body.giftAmount)) body.invalidGiftPrecision = true;
}
if (recordContent !== undefined) body.recordContent = recordContent;
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
if (source.mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds || '';
if (sortOrderText !== undefined) {
body.sortOrder = toSafeInteger(sortOrderText);
if (body.sortOrder === undefined) body.invalidSortOrder = true;
@@ -168,7 +168,7 @@
if (body.eventTime !== undefined && !isValidBackendDateTime(body.eventTime)) {
return '事件时间格式无效';
}
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
if (body.mediaOssIds !== undefined && body.mediaOssIds !== '' && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
return '附件上传结果无效';
}
if (body.invalidSortOrder || (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && !Number.isSafeInteger(body.sortOrder))) {
@@ -576,14 +576,8 @@
setFieldValue('eventTime', toDateTimeInputValue(record.eventTime));
setFieldValue('giftAmount', record.giftAmount);
setFieldValue('recordContent', record.recordContent);
setFieldValue('mediaOssIds', record.mediaOssIds);
setFieldValue('sortOrder', record.sortOrder);
setFieldValue('status', '0');
if (query('[data-relative-media-status]')) {
query('[data-relative-media-status]').textContent = record.mediaOssIds
? '已保留 ' + attachmentCount(record.mediaOssIds) + ' 个附件,可继续选择追加'
: '未选择文件';
}
}
async function loadRelativeEditor() {
@@ -603,9 +597,11 @@
await loadCapability(api, genealogyId);
if (!canEditContent) throw { status: 403, message: '当前账号无权维护该家谱的亲友记录。' };
if (relativeId) {
record = normalizeRelativeRecord(await api.relativeRecordDetail(genealogyId, relativeId));
var relativeResponse = await api.relativeRecordDetail(genealogyId, relativeId);
record = normalizeRelativeRecord(relativeResponse);
if (!record) throw new Error('亲友记录详情响应缺少稳定 RelativeRecordVo 字段');
fillRelativeForm(record);
root.AttachmentEditor.setFiles('#relativeMediaOssIds', relativeResponse.mediaFiles || []);
if (query('[data-relative-editor-title]')) query('[data-relative-editor-title]').textContent = '编辑亲友记录';
}
setEditorEnabled(true);
@@ -688,13 +684,7 @@
}
function clearMediaSelection() {
var target = query('[data-relative-form] [name="mediaOssIds"]');
var status = query('[data-relative-media-status]');
var fileInput = query('[data-relative-media-input]');
if (target) target.value = '';
if (fileInput) fileInput.value = '';
if (status) status.textContent = '未选择文件';
root.AttachmentEditor.clear('#relativeMediaOssIds');
}
function bindActions() {
+5 -1
View File
@@ -225,6 +225,7 @@
if (!files.length || !target || redirectUnauthorized(api)) return;
input.disabled = true;
if (root.AttachmentEditor) root.AttachmentEditor.setBusy(target, true);
try {
for (index = 0; index < files.length; index += 1) {
@@ -235,7 +236,9 @@
}
}));
if (!result.ossId) throw new Error('上传响应缺少 ossId');
target.value = mergeUploadTargetValue(target.value, result.ossId, multiple);
if (!root.AttachmentEditor || !root.AttachmentEditor.addUploaded(target, result, files[index])) {
target.value = mergeUploadTargetValue(target.value, result.ossId, multiple);
}
setUploadStatus(status, '', buildUploadStatus(result.fileName || files[index].name, result.ossId));
}
} catch (error) {
@@ -244,6 +247,7 @@
showMessage(isForbidden(error) ? '当前账号无权上传文件。' : (error.message || '上传失败'));
} finally {
input.disabled = false;
if (root.AttachmentEditor) root.AttachmentEditor.setBusy(target, false);
input.value = '';
}
}
+5 -6
View File
@@ -97,6 +97,7 @@
if (videoDesc !== undefined) body.videoDesc = videoDesc;
if (coverOssId !== undefined) body.coverOssId = coverOssId;
if (source.coverOssId === null) body.coverOssId = null;
if (videoOssId !== undefined) body.videoOssId = videoOssId;
if (durationText !== undefined) {
body.durationSeconds = toSafeInteger(durationText);
@@ -114,7 +115,7 @@
if (!body || !body.videoTitle) return '请填写视频标题';
if (!body.videoOssId) return '请先选择并上传视频文件';
if (!normalizeId(body.videoOssId)) return '视频文件上传结果无效';
if (body.coverOssId !== undefined && !normalizeId(body.coverOssId)) return '封面上传结果无效';
if (body.coverOssId !== undefined && body.coverOssId !== null && !normalizeId(body.coverOssId)) return '封面上传结果无效';
if (body.invalidDuration || (body.durationSeconds !== undefined && !Number.isSafeInteger(body.durationSeconds))) {
return '视频时长必须是安全整数';
}
@@ -488,7 +489,7 @@
var values = {};
queryAll('[name]', form).forEach(function (field) {
values[field.name] = field.value;
values[field.name] = root.AttachmentEditor ? root.AttachmentEditor.readField(field) : field.value;
});
return values;
}
@@ -502,13 +503,9 @@
function fillVideoForm(video) {
setFieldValue('videoTitle', video.videoTitle);
setFieldValue('videoDesc', video.videoDesc);
setFieldValue('coverOssId', video.coverOssId);
setFieldValue('videoOssId', video.videoOssId);
setFieldValue('durationSeconds', video.durationSeconds);
setFieldValue('sortOrder', video.sortOrder);
setFieldValue('status', '0');
if (query('[data-video-file-status]')) query('[data-video-file-status]').textContent = '已保留原视频文件,可重新选择替换';
if (query('[data-video-cover-status]')) query('[data-video-cover-status]').textContent = video.coverOssId ? '已保留原封面,可重新选择替换' : '未选择封面';
}
async function loadVideoEditor() {
@@ -540,6 +537,8 @@
video = normalizeVideo(result[1]);
if (!video) throw new Error('视频详情响应缺少稳定 VideoVo 字段');
fillVideoForm(video);
root.AttachmentEditor.setFiles('#video-oss-id', result[1].videoFile ? [result[1].videoFile] : []);
root.AttachmentEditor.setFiles('#video-cover-oss-id', result[1].coverFile ? [result[1].coverFile] : []);
if (query('[data-video-editor-title]')) query('[data-video-editor-title]').textContent = '编辑视频';
}
setFormStatus('');
+49
View File
@@ -0,0 +1,49 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { resolve } = require('node:path');
const Feed = require('../public/js/feed-pages');
const Article = require('../public/js/article-pages');
const Video = require('../public/js/video-pages');
const Ceremony = require('../public/js/ceremony-admin-pages');
const Editor = require('../public/js/attachment-editor');
const Growth = require('../public/js/growth-pages');
const Memo = require('../public/js/memo-pages');
const Relative = require('../public/js/relative-pages');
test('附件编辑页在上传组件之前加载真实 MD5 依赖', () => {
for (const page of ['feed', 'article', 'album', 'video', 'gift', 'growth', 'memo', 'relative']) {
const html = readFileSync(resolve(__dirname, '../profile-' + page + '-edit.html'), 'utf8');
const scripts = Array.from(html.matchAll(/<script\s+src="([^"]+)"/g), match => match[1]);
const md5Index = scripts.indexOf('public/js/md5.js');
const uploadIndex = scripts.indexOf('public/js/upload-pages.js');
assert.ok(md5Index >= 0 && md5Index < uploadIndex, page + ' 缺少上传所需的 MD5 依赖');
}
});
test('删除全部动态附件时明确提交空列表', () => {
assert.equal(Feed.buildFeedBody({ feedContent: '保留文字', mediaOssIds: '' }).mediaOssIds, '');
});
test('编辑附件保留精确的长 ID,拒绝缺失或已丢失精度的 ID', () => {
const file = { ossId: '2062179707935264771', fileName: '照片.png', mediaType: 'image/png' };
assert.equal(Editor.normalizeFiles([file, file]).length, 1);
assert.equal(Editor.normalizeFiles([file])[0].ossId, file.ossId);
assert.throws(() => Editor.normalizeFiles([{ fileName: '照片.png' }]));
assert.throws(() => Editor.normalizeFiles([{ ossId: 2062179707935264771 }]));
});
test('多附件表单清空后显式发送空值', () => {
for (const build of [Growth.buildGrowthRecordBody, Memo.buildMemoBody, Relative.buildRelativeRecordBody]) {
assert.equal(build({ mediaOssIds: '' }).mediaOssIds, '');
assert.equal(build({ mediaOssIds: '2062179707935264771' }).mediaOssIds, '2062179707935264771');
}
});
test('封面更新区分未操作、清空和替换', () => {
for (const build of [Article.buildArticleBody, Video.buildVideoBody, Ceremony.buildCeremonyBody]) {
assert.equal(build({}).coverOssId, undefined);
assert.equal(build({ coverOssId: null }).coverOssId, null);
assert.equal(build({ coverOssId: '2062179707935264771' }).coverOssId, '2062179707935264771');
}
});
+149
View File
@@ -0,0 +1,149 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { createServer } from 'node:http';
import { readFile, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { resolve, extname, join, dirname, basename } from 'node:path';
const project = resolve(import.meta.dirname, '..');
const errors = [];
const server = createServer(async (req, res) => {
try {
const path = new URL(req.url, 'http://localhost').pathname;
let content = await readFile(join(project, path));
if (path.endsWith('.html')) {
// 使用真实编辑页和页面控制器,仅替换网络、导航及富文本依赖。
content = content.toString().replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, (tag) =>
/(?:wangeditor5\/index|rich-editor|md5|attachment-editor|upload-pages|media-display|feed-pages|article-pages|video-pages|memo-pages|growth-pages|relative-pages|album-pages|ceremony-admin-pages)\.js/.test(tag) ? tag : '');
}
res.setHeader('Content-Type', { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.png': 'image/png' }[extname(path)] || 'application/octet-stream');
res.end(content);
} catch { res.writeHead(404); res.end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
const userData = await mkdtemp(join(tmpdir(), 'jiapu-attachment-test-'));
const chrome = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-gpu-sandbox', '--use-angle=swiftshader',
'--disable-features=Vulkan', '--remote-debugging-port=0', `--user-data-dir=${userData}`, 'about:blank'
], { windowsHide: true });
let socket;
try {
const endpoint = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Chrome 启动超时')), 15000);
chrome.stderr.on('data', chunk => {
const match = chunk.toString().match(/DevTools listening on (ws:\/\/\S+)/);
if (match) { clearTimeout(timer); resolve(match[1]); }
});
chrome.on('error', reject);
});
const tabs = await (await fetch(endpoint.replace('ws:', 'http:').replace(/\/devtools\/browser\/.*/, '/json'))).json();
socket = new WebSocket(tabs.find(tab => tab.type === 'page').webSocketDebuggerUrl);
await new Promise(resolve => socket.addEventListener('open', resolve, { once: true }));
const pending = new Map();
let id = 0;
socket.addEventListener('message', event => {
const message = JSON.parse(event.data);
if (message.method === 'Runtime.exceptionThrown') errors.push(message.params.exceptionDetails);
if (pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id); }
});
async function send(method, params = {}) {
const requestId = ++id;
const promise = new Promise(resolve => pending.set(requestId, resolve));
socket.send(JSON.stringify({ id: requestId, method, params }));
const response = await promise;
if (response.error) throw new Error(JSON.stringify(response.error));
return response.result;
}
async function evaluate(expression) {
const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
return result.result.value;
}
await send('Runtime.enable');
await send('Page.enable');
await send('Page.addScriptToEvaluateOnNewDocument', { source: `
window.saved = []; window.alert = function () {}; window.NavigationUtil = { open: function () {} };
window.fixtureFiles = [
{ ossId: '2062179707935264771', fileName: '原照片.png', mediaType: 'image/png', accessUrl: '${origin}/public/images/logo-mark.png' },
{ ossId: '2062179707935264772', fileName: '说明.pdf', mediaType: 'application/pdf', accessUrl: '${origin}/document.pdf' }
];
const record = { genealogyId: '2', feedId: '3', feedContent: '原动态', feedType: 'text',
articleId: '3', articleTitle: '谱文', articleContent: '正文',
videoId: '3', videoTitle: '影像', durationSeconds: 5,
memoId: '3', memoTitle: '备忘', memoContent: '正文', completed: '0',
recordId: '3', recordTitle: '成长', recordContent: '正文', recordType: 'other',
relativeId: '3', relativeName: '亲友', ceremonyId: '3', ceremonyTitle: '活动', ceremonyType: 'other',
albumId: '3', albumName: '相册', status: '0', mediaFiles: fixtureFiles, coverFile: fixtureFiles[0],
videoFile: { ossId: '2062179707935264773', fileName: '原视频.mp4', mediaType: 'video/mp4', accessUrl: '${origin}/video.mp4' }
};
window.GenealogyApi = { defaultClient: {
getToken: () => 'mock', genealogyDetail: async () => ({ canEditContent: true, canManage: true }),
currentProfile: async () => ({ userId: '1' }),
feedDetail: async () => record, articleDetail: async () => record, videoDetail: async () => record,
memoDetail: async () => record, relativeRecordDetail: async () => record, growthRecordDetail: async () => record,
ceremonyDetail: async () => record, albums: async () => [record], lineagePersonOptions: async () => [],
updateFeed: async (g, f, body) => { saved.push(body); return record; },
updateArticle: async (g, f, body) => { saved.push(body); return record; },
updateVideo: async (g, f, body) => { saved.push(body); return record; },
initResumableUpload: async () => { await new Promise(r => setTimeout(r, 150)); return { instant: true, ossId: '2062179707935264774' }; }
} };
` });
async function open(page, query) {
await send('Page.navigate', { url: `${origin}/${page}?genealogyId=2&${query}=3` });
for (let attempt = 0; attempt < 100; attempt++) {
if (await evaluate('document.querySelectorAll(".attachment-edit-item").length > 0')) return;
await new Promise(resolve => setTimeout(resolve, 50));
}
throw new Error(page + ': 附件没有回显 ' + await evaluate('document.body.innerText'));
}
await open('profile-feed-edit.html', 'feedId');
assert.equal(await evaluate('AppRichEditor.init(document.querySelector("[name=feedContent]")).editor.getText().trim()'), '原动态', '编辑器应回显原正文');
assert.equal(await evaluate('document.querySelector("#feedMedia").value'), '2062179707935264771,2062179707935264772');
assert.equal(await evaluate('document.querySelectorAll(".attachment-edit-item img").length'), 1);
await evaluate('AppRichEditor.init(document.querySelector("[name=feedContent]")).editor.setHtml("<p>仅改文字</p>"); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved[0].mediaOssIds'), '2062179707935264771,2062179707935264772');
await evaluate('document.querySelector(".attachment-edit-remove").click()');
assert.equal(await evaluate('document.querySelector("#feedMedia").value'), '2062179707935264772');
await evaluate(`const input=document.querySelector('#feedMediaFile'); const transfer=new DataTransfer(); transfer.items.add(new File(['image'], '新增.png', {type:'image/png'})); input.files=transfer.files; input.dispatchEvent(new Event('change',{bubbles:true})); document.querySelector('form').requestSubmit();`);
assert.equal(await evaluate('saved.length'), 1, '上传中应阻止保存');
await evaluate('new Promise(r=>setTimeout(r,250))');
assert.equal(await evaluate('document.querySelector("#feedMedia").value'), '2062179707935264772,2062179707935264774');
await evaluate('document.querySelectorAll(".attachment-edit-remove").forEach(button=>button.click()); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.at(-1).mediaOssIds'), '');
await open('profile-article-edit.html', 'articleId');
await evaluate('document.querySelector(".attachment-edit-remove").click(); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.at(-1).coverOssId'), null);
await open('profile-video-edit.html', 'videoId');
assert.equal(await evaluate('document.querySelectorAll(".attachment-edit-item video").length'), 1);
assert.equal(await evaluate('document.querySelector("#video-oss-id").value'), '2062179707935264773');
await evaluate('document.querySelector("#video-cover-oss-id").closest(".editor-field").querySelector(".attachment-edit-remove").click(); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.at(-1).coverOssId'), null);
assert.equal(await evaluate('saved.at(-1).videoOssId'), '2062179707935264773');
await evaluate('document.querySelector(".attachment-edit-item video").parentElement.querySelector("button").click(); document.querySelector("form").requestSubmit()');
assert.equal(await evaluate('saved.length'), 1, '必填视频移除后不能保存,必须先重新上传');
for (const [page, query] of [['memo','memoId'], ['growth','recordId'], ['relative','relativeId'], ['album','albumId'], ['gift','ceremonyId']]) {
await open('profile-' + page + '-edit.html', query);
if (page === 'album') assert.equal(await evaluate('document.querySelector(".attachment-edit-remove").hidden'), true);
if (['memo', 'growth', 'relative'].includes(page)) {
await evaluate(`document.querySelector('[data-${page}-clear-media]').click()`);
assert.equal(await evaluate('document.querySelector("[name=mediaOssIds]").value'), '');
assert.equal(await evaluate('document.querySelectorAll(".attachment-edit-item").length'), 0);
}
}
await send('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
await open('profile-feed-edit.html', 'feedId');
assert.equal(await evaluate('document.documentElement.scrollWidth <= innerWidth'), true, '移动端不能横向溢出');
await evaluate(`try { AttachmentEditor.setFiles('#feedMedia', [{fileName:'缺少标识.png'}]); } catch {} document.querySelector('form').requestSubmit();`);
assert.equal(await evaluate('saved.length'), 0, '原附件信息不完整时应阻止保存');
assert.deepEqual(errors, []);
console.log('PASS: 编辑页原附件回显、逐项移除、追加上传、上传中阻止保存、全部移除、封面 null、视频预览与移动端布局(模拟 API)');
} finally {
if (socket) socket.close();
chrome.kill();
server.close();
await new Promise(resolve => chrome.exitCode !== null ? resolve() : chrome.once('exit', resolve));
if (dirname(userData) === resolve(tmpdir()) && basename(userData).startsWith('jiapu-attachment-test-')) {
await rm(userData, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
}