From c7f278fe794832ebaff9502d4687863c2f691488 Mon Sep 17 00:00:00 2001
From: rain <982766701@qq.com>
Date: Fri, 24 Jul 2026 07:56:22 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=9070%?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components/TacVerification.vue | 227 +--
.../manifests/application-runtime-assets.json | 1 -
docs/Apifox逐页业务接口与页面展示核对台账.md | 221 +++
docs/产品参考页面功能映射表.md | 751 +++++++++
docs/今晚全量联调与明早测试执行计划.md | 575 +++++--
pages/auth/a01-entry.vue | 2 +-
pages/auth/a04-register.vue | 2 +-
pages/auth/a05-reset-password.vue | 2 +-
pages/family/f01-family-feed.vue | 309 +---
pages/family/f02-publish-feed.vue | 239 +--
pages/family/f03-feed-detail.vue | 141 +-
pages/family/f04-article-list.vue | 159 +-
pages/family/f05-article-detail.vue | 120 +-
pages/family/f06-article-editor.vue | 516 ++----
pages/family/f07-album-list.vue | 246 +--
pages/family/f08-album-detail.vue | 348 +----
pages/family/f09-media-upload.vue | 629 +-------
pages/family/f10-video-list.vue | 139 +-
pages/genealogy/g03-create-genealogy.vue | 739 +--------
pages/genealogy/g06-search-genealogies.vue | 722 +--------
pages/genealogy/g08-join-application.vue | 466 +-----
pages/genealogy/g09-my-applications.vue | 391 +----
pages/genealogy/g10-application-review.vue | 475 +-----
pages/genealogy/g11-genealogy-settings.vue | 482 +-----
pages/genealogy/g12-generation-poems.vue | 807 +++-------
pages/notification/n01-message-center.vue | 258 +--
pages/notification/n02-message-detail.vue | 138 +-
pages/profile/m01-profile-home.vue | 426 +----
pages/profile/m02-edit-profile.vue | 121 +-
pages/profile/m03-security-settings.vue | 47 +-
pages/profile/m04-change-password.vue | 32 +-
pages/profile/m05-change-phone.vue | 95 +-
pages/profile/m06-help-center.vue | 3 +-
pages/profile/m10-about-settings.vue | 25 +-
pages/records/r01-people-list.vue | 110 +-
pages/records/r02-person-detail.vue | 295 +---
pages/records/r03-gift-list.vue | 208 +--
pages/records/r04-gift-editor.vue | 406 ++---
pages/records/r05-ritual-list.vue | 217 +--
pages/records/r06-ritual-detail.vue | 249 +--
pages/records/r07-ritual-editor.vue | 320 +---
pages/records/r08-growth-journal.vue | 411 +----
pages/records/r09-life-events.vue | 109 +-
pages/records/r10-memo-list.vue | 250 +--
pages/records/r11-merit-records.vue | 287 +---
pages/tree/t01-tree-overview.vue | 220 ++-
pages/tree/t03-member-profile.vue | 100 +-
pages/tree/t04-add-relative.vue | 189 ++-
pages/tree/t05-edit-member.vue | 177 ++-
pages/tree/t06-edit-relationship.vue | 325 +---
pages/tree/t07-member-directory.vue | 98 +-
pages/tree/t08-member-states.vue | 109 +-
.../tree/transparent/t01-member-drawer.png | Bin 254885 -> 0 bytes
...ctive-page-business-ownership-contract.ps1 | 8 +-
tests/auth-accessibility-static-contract.ps1 | 7 +-
tests/auth-code-action-visual-contract.ps1 | 4 +-
tests/auth-tac-integration-contract.ps1 | 11 +-
tests/auth-tac-openapi-contract.ps1 | 150 +-
tests/core-flow-contract.ps1 | 6 +-
tests/current-documentation-contract.ps1 | 5 +-
tests/data-driven-layout-risk-allowlist.json | 24 +
tests/document-flow-position-allowlist.json | 10 -
tests/family-feed-read-openapi-contract.ps1 | 1381 +----------------
tests/family-fixture-runtime-smoke.js | 4 +-
tests/g03-bootstrap-client-release-gate.ps1 | 143 +-
tests/g03-bootstrap-openapi-contract.ps1 | 1017 +-----------
tests/help-center-openapi-contract.ps1 | 138 +-
tests/interface-page-mapping-contract.ps1 | 2 +-
tests/lineage-write-apifox-contract.ps1 | 32 +
tests/logout-openapi-contract.ps1 | 186 +--
tests/navigation-flow-contract.ps1 | 26 +-
tests/night-run.manifest.json | 75 +
tests/night-run.ps1 | 182 +++
tests/nm-page-business-contract.ps1 | 30 +-
tests/password-change-openapi-contract.ps1 | 268 +---
tests/responsive-layout-allowlist.json | 1 +
tests/t01-all-states-visual-contract.ps1 | 11 +-
tests/t01-person-action-panel-contract.ps1 | 49 +
...t01-sheet-state-document-flow-contract.ps1 | 3 +-
tests/t01-tree-state-contract.ps1 | 7 +-
tests/t01-tree-state-runtime-smoke.js | 2 +-
tests/t03-t06-all-states-visual-contract.ps1 | 40 +-
...3-t08-business-specialization-contract.ps1 | 139 +-
tests/t03-t08-member-flow-contract.ps1 | 308 +---
tests/t04-relative-remote-close-contract.ps1 | 35 +
tests/t05-member-remote-close-contract.ps1 | 33 +
tests/t06-rank-remote-close-contract.ps1 | 33 +
tests/tac-renderjs-runtime-smoke.js | 56 +-
tests/tac-shell-accessibility-contract.ps1 | 49 +-
tests/tree-member-fixture-runtime-smoke.js | 4 +-
utils/api.js | 763 ++++++++-
utils/navigation-routes.js | 5 +-
92 files changed, 4741 insertions(+), 14440 deletions(-)
create mode 100644 docs/Apifox逐页业务接口与页面展示核对台账.md
create mode 100644 docs/产品参考页面功能映射表.md
delete mode 100644 static/assets/modules/tree/transparent/t01-member-drawer.png
create mode 100644 tests/lineage-write-apifox-contract.ps1
create mode 100644 tests/night-run.manifest.json
create mode 100644 tests/night-run.ps1
create mode 100644 tests/t01-person-action-panel-contract.ps1
create mode 100644 tests/t04-relative-remote-close-contract.ps1
create mode 100644 tests/t05-member-remote-close-contract.ps1
create mode 100644 tests/t06-rank-remote-close-contract.ps1
diff --git a/components/TacVerification.vue b/components/TacVerification.vue
index 6fafcb8..2bf23b3 100644
--- a/components/TacVerification.vue
+++ b/components/TacVerification.vue
@@ -2,59 +2,20 @@
-
-
- 安全验证
- 拖动滑块完成验证;可刷新当前挑战或关闭返回。
-
-
-
-
-
-
-
-
+ id="jiapu-tac-host"
+ :prop="renderContext"
+ :change:prop="tacRenderer.onContextChange"
+ />
+
diff --git a/pages/family/f02-publish-feed.vue b/pages/family/f02-publish-feed.vue
index 4997ddf..ec68614 100644
--- a/pages/family/f02-publish-feed.vue
+++ b/pages/family/f02-publish-feed.vue
@@ -1,43 +1,23 @@
-
+
- 记录此刻分享通知、活动、家族故事或一段共同记忆。{{ resultCopy.title }}{{ resultCopy.copy }}
+
+
+
+
+ 记录此刻
+ 当前接口只明确文本动态的 `feedContent` 请求字段;媒体、排序和状态均不由本页猜测或提交。
+
+ {{ submitError }}
+
+
+
+ {{ resultCopy.title }}
+ {{ resultCopy.copy }}
+
+
+
+
- >
+
+
+
diff --git a/pages/family/f03-feed-detail.vue b/pages/family/f03-feed-detail.vue
index 5c7bfa6..da7b9c6 100644
--- a/pages/family/f03-feed-detail.vue
+++ b/pages/family/f03-feed-detail.vue
@@ -1,20 +1,19 @@
-
+
-
+
+ 动态详情接口尚未声明标题、正文、发布人和发布时间字段;本页不猜测这些字段,只展示该动态的真实一级评论。
- {{ currentFeed.tag }} · {{ currentFeed.time }}
- {{ currentFeed.title }}
- {{ currentFeed.content }}
- 发布人:{{ currentFeed.author }}
+ 动态正文待后端字段合同
+ 已根据当前动态 ID 读取评论。正文内容会在详情响应提供可消费 DTO 后再接入。
-
@@ -47,7 +48,7 @@
-
@@ -65,11 +65,10 @@ import { onBackPress, onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
-import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
-import { findFamilyFeedFixture } from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
+import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
goBack,
handleBackPress,
@@ -79,29 +78,28 @@ import {
const genealogyId = ref("");
const feedId = ref("");
-const currentFeed = ref(null);
const feedState = ref("loading");
const commentState = ref("idle");
const commentDraft = ref("");
const commentError = ref("");
-const toastVisible = ref(false);
+const commentNotice = ref("");
const discardVisible = ref(false);
const feedComments = ref([]);
-let submitTimer = null;
-let toastTimer = null;
+const feedRequestController = createRequestController();
+let loadSequence = 0;
const isDirty = computed(() => Boolean(commentDraft.value.trim()));
const stateCopy = computed(() => {
- if (feedState.value === "error" && currentFeed.value) {
+ if (feedState.value === "error") {
return {
- title: "动态暂不可用",
- copy: "请稍后重新查看,已有家族记录不会受到影响。",
- action: "重新查看",
+ title: "评论暂不可用",
+ copy: "评论读取没有得到可消费的服务端响应;页面不会用本地数据替代。",
+ action: "重新读取",
};
}
return {
- title: "动态已失效或入口无效",
- copy: "没有找到当前家谱中的这条动态,页面不会回退到其他记录。",
- action: genealogyId.value ? "返回家族圈" : "返回上一页",
+ title: "动态入口无效",
+ copy: "没有取得当前家谱和动态标识,页面不会回退到其他动态。",
+ action: genealogyId.value ? "返回家族动态" : "返回上一页",
};
});
const discardConfirmation = createDiscardConfirmation((visible) => {
@@ -111,45 +109,69 @@ const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
+const loadComments = async () => {
+ if (!genealogyId.value || !feedId.value) {
+ feedState.value = "expired";
+ return false;
+ }
+ const sequence = ++loadSequence;
+ feedState.value = "loading";
+ try {
+ const comments = await appApi.getFeedComments(genealogyId.value, feedId.value, {
+ requestController: feedRequestController,
+ });
+ if (sequence !== loadSequence) return false;
+ feedComments.value = comments;
+ feedState.value = "ready";
+ return true;
+ } catch (error) {
+ if (isRequestCancelled(error) || sequence !== loadSequence) return false;
+ feedState.value = "error";
+ return false;
+ }
+};
+
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
feedId.value = String(query.feedId || "");
- const selected = findFamilyFeedFixture(genealogyId.value, feedId.value);
- currentFeed.value = selected;
- feedComments.value = selected?.comments || [];
- feedState.value = ["loading", "error", "expired"].includes(query.state)
- ? selected
- ? query.state
- : "expired"
- : selected
- ? "ready"
- : "expired";
+ loadComments();
});
-const submitComment = () => {
- if (commentState.value === "validating" || !currentFeed.value) return;
+const submitComment = async () => {
+ if (commentState.value === "submitting" || feedState.value !== "ready") return;
const content = commentDraft.value.trim();
if (!content) {
commentError.value = "请先写下评论内容";
+ commentNotice.value = "";
return;
}
commentError.value = "";
- commentState.value = "validating";
- const timer = setTimeout(() => {
- if (submitTimer !== timer) return;
- submitTimer = null;
- commentState.value = "preview";
- toastVisible.value = true;
- toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
- }, 320);
- submitTimer = timer;
+ commentNotice.value = "";
+ commentState.value = "submitting";
+ try {
+ await appApi.createFeedComment(genealogyId.value, feedId.value, { commentContent: content }, {
+ requestController: feedRequestController,
+ });
+ commentDraft.value = "";
+ const refreshed = await loadComments();
+ if (!refreshed) {
+ commentState.value = "error";
+ commentError.value = "评论已提交,但列表刷新失败;请稍后重新查看。";
+ return;
+ }
+ commentState.value = "idle";
+ commentNotice.value = "评论已提交,并已从服务端刷新。";
+ } catch (error) {
+ if (isRequestCancelled(error)) return;
+ commentState.value = "error";
+ commentError.value = error?.message || "评论提交失败,请稍后重试。";
+ }
};
-const restoreFeed = () => { feedState.value = "ready"; };
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
- submitting: commentState.value === "validating",
+ submitting: commentState.value === "submitting",
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
@@ -161,14 +183,14 @@ const backToFamily = async () => {
return returnTo("F01", { genealogyId: genealogyId.value });
};
const handleStateAction = () => {
- if (feedState.value === "error" && currentFeed.value) return restoreFeed();
+ if (feedState.value === "error") return loadComments();
return backToFamily();
};
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => {
- if (submitTimer) clearTimeout(submitTimer);
- if (toastTimer) clearTimeout(toastTimer);
+ ++loadSequence;
+ feedRequestController.abort();
discardConfirmation.dispose();
});
@@ -180,26 +202,29 @@ onUnmounted(() => {
.feed-detail-header, .feed-detail-loading, .feed-detail-content { z-index: 1; }
.feed-detail-loading { min-height: calc(100vh - 100rpx); }
.feed-detail-content { padding: 18rpx 24rpx 72rpx; }
+.feed-source-note { margin-top: 12rpx; padding: 14rpx 18rpx; border: 1rpx solid rgba(128, 106, 81, .22); border-radius: 10rpx; background: rgba(255,255,255,.56); }
+.feed-source-note text { display: block; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.feed-article-card, .feed-comments-panel, .feed-comment-form, .feed-state-card { @include adaptive.adaptive-family-content; width: 100%; }
-.feed-article-card { padding: 46rpx 48rpx 42rpx; }
+.feed-article-card { padding: 42rpx 48rpx; }
.feed-article-card text, .feed-state-card > text { display: block; }
-.feed-article-card__meta, .feed-article-card__author { color: $ink-muted; font-size: 22rpx; }
-.feed-article-card__title { margin-top: 12rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 36rpx; font-weight: 700; }
-.feed-article-card__body { margin-top: 18rpx; color: $ink; font-size: 25rpx; line-height: 1.75; }
-.feed-article-card__author { margin-top: 22rpx; }
+.feed-article-card__title { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; }
+.feed-article-card__body { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.7; }
.feed-comments-panel { margin-top: 18rpx; padding: 38rpx 38rpx 34rpx; }
.feed-comments-heading { display: flex; align-items: center; justify-content: space-between; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.feed-comments-heading text:last-child { color: $ink-muted; font-size: 21rpx; font-weight: 400; }
.feed-comment-card { @include adaptive.adaptive-family-field; margin-top: 16rpx; padding: 20rpx 22rpx; }
.feed-comment-card > view { display: flex; justify-content: space-between; gap: 18rpx; color: $ink-muted; font-size: 20rpx; }
.feed-comment-card > text { display: block; margin-top: 9rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
+.feed-comment-card > .feed-comment-card__replies { color: $ink-muted; font-size: 20rpx; }
.feed-comments-empty { padding: 34rpx 10rpx 20rpx; text-align: center; }
.feed-comments-empty text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; }
.feed-comments-empty text:first-child { color: $ink; font-size: 28rpx; font-weight: 700; }
.feed-comment-form { margin-top: 18rpx; padding: 38rpx 40rpx 42rpx; }
.feed-comment-form > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
.feed-comment-form textarea { @include adaptive.adaptive-family-field; width: auto; min-width: 0; min-height: 126rpx; margin-top: 16rpx; padding: 20rpx 22rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
-.feed-comment-error { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
+.feed-comment-error, .feed-comment-notice { display: block; margin-top: 10rpx; font-size: 22rpx; }
+.feed-comment-error { color: $brand-red; }
+.feed-comment-notice { color: $ink-muted; }
.feed-comment-form .app-button { margin-top: 22rpx; }
.feed-state-card { min-height: 340rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
.feed-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
diff --git a/pages/family/f04-article-list.vue b/pages/family/f04-article-list.vue
index 32e6891..44d8c23 100644
--- a/pages/family/f04-article-list.vue
+++ b/pages/family/f04-article-list.vue
@@ -1,45 +1,13 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
- {{ category }}
-
-
-
-
-
- {{ article.category }}
- {{ article.title }}
- {{ article.summary }}
- {{ article.author }}{{ article.updatedAt }}
-
-
-
-
-
- {{ keyword || activeCategory !== '全部' ? '没有找到相关谱文' : '还没有谱文' }}
- {{ keyword || activeCategory !== '全部' ? '换一个关键词或分类继续查找。' : '从第一篇家训、序言或家族往事开始记录。' }}
-
-
-
-
-
+
+
{{ stateCopy.title }}
{{ stateCopy.copy }}
-
+
@@ -49,115 +17,42 @@
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
-import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
-import {
- getGenealogyFixtureAccess,
- listFamilyArticleFixtures,
-} from "@/data/mock.js";
import { goBack, openPage } from "@/utils/navigation.js";
-const articleCategories = ["全部", "家风家训", "家族往事", "族谱序言"];
const genealogyId = ref("");
const hasValidContext = ref(false);
-const articles = ref([]);
-const activeCategory = ref("全部");
-const keyword = ref("");
-const listState = ref("loading");
-const filteredArticles = computed(() => {
- const term = keyword.value.trim().toLowerCase();
- return articles.value.filter((article) => {
- const categoryMatched = activeCategory.value === "全部" || article.category === activeCategory.value;
- const keywordMatched = !term || `${article.title} ${article.summary} ${article.author}`.toLowerCase().includes(term);
- return categoryMatched && keywordMatched;
- });
-});
-const stateCopy = computed(() => ({
- empty: {
- title: "还没有谱文",
- copy: "当前家谱尚无可读取谱文,真实写接口接入后才能新增。",
- action: "填写谱文预览",
- },
- error: {
- title: "谱文列表暂不可用",
- copy: "请稍后重新查看,已有谱文不会受到影响。",
- action: "重新查看",
- },
- invalid: {
- title: "谱文入口无效",
- copy: "没有找到可访问的成员家谱,页面不会展示其他家谱内容。",
- action: "返回上一页",
- },
-}[listState.value]));
-
+const listState = ref("unavailable");
+const stateCopy = computed(() => hasValidContext.value
+ ? {
+ title: "谱文列表待后端字段合同",
+ copy: "列表接口只声明通用对象数组,没有文章 ID、分类、标题、摘要、作者或更新时间字段;页面已停止展示本地文章和本地筛选。",
+ action: "新建谱文",
+ }
+ : {
+ title: "谱文入口无效",
+ copy: "没有取得有效家谱标识,页面不会展示其他家谱内容。",
+ action: "返回上一页",
+ },
+);
onLoad((query) => {
- genealogyId.value = String(query.genealogyId || "");
- hasValidContext.value = ["owner", "member"].includes(
- getGenealogyFixtureAccess(genealogyId.value).accessRole,
- );
- if (!genealogyId.value || !hasValidContext.value) {
- listState.value = "invalid";
- return;
- }
- articles.value = listFamilyArticleFixtures(genealogyId.value);
- listState.value = ["loading", "empty", "error"].includes(query.state)
- ? query.state
- : articles.value.length
- ? "ready"
- : "empty";
+ genealogyId.value = String(query?.genealogyId || "");
+ hasValidContext.value = /^[1-9]\d*$/.test(genealogyId.value);
+ listState.value = hasValidContext.value ? "unavailable" : "invalid";
});
-
-const openArticle = (article) =>
- openPage(
- "F05",
- { genealogyId: genealogyId.value, articleId: String(article.id) },
- "F04",
- );
-const createArticle = () =>
- hasValidContext.value
- ? openPage(
- "F06",
- { genealogyId: genealogyId.value, mode: "create" },
- "F04",
- )
- : Promise.resolve(false);
-const restoreArticles = () => {
- articles.value = listFamilyArticleFixtures(genealogyId.value);
- listState.value = articles.value.length ? "ready" : "empty";
-};
-const handleStateAction = () => {
- if (listState.value === "invalid") return goBack();
- if (listState.value === "error") return restoreArticles();
- return createArticle();
-};
-const resetFilters = () => { keyword.value = ""; activeCategory.value = "全部"; };
+const createArticle = () => hasValidContext.value
+ ? openPage("F06", { genealogyId: genealogyId.value, mode: "create" }, "F04")
+ : Promise.resolve(false);
+const handleStateAction = () => hasValidContext.value ? createArticle() : goBack();
diff --git a/pages/family/f06-article-editor.vue b/pages/family/f06-article-editor.vue
index aa05938..6f15531 100644
--- a/pages/family/f06-article-editor.vue
+++ b/pages/family/f06-article-editor.vue
@@ -1,26 +1,27 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ 服务端创建
+ 把值得传承的故事写下来
+ 本页只发送 Apifox 已声明且可映射的文章标题与正文。分类需要 `categoryId`,当前没有可用分类 owner,故不展示或猜测分类。
+
+ 文章标题
+
+
+
+ 正文内容
+
+
+ {{ submitError }}
+
+
+
+
{{ resultCopy.eyebrow }}
{{ resultCopy.title }}
@@ -29,89 +30,6 @@
-
-
-
-
- 草稿 · 未发布
- 把值得传承的故事写下来
- 补充标题、分类和正文;当前只校验并生成本地预览。
-
-
- 文章标题
-
-
-
- {{
- fieldErrors.title
- }}
-
-
-
- 文章分类
-
-
-
- {{
- fieldErrors.category
- }}
-
-
-
- 正文内容
-
-
-
- {{
- fieldErrors.content
- }}
-
-
- 当前为未发布草稿;真实草稿保存将在接口阶段接入。
- 保存失败,已填写内容仍保留,请稍后重试。
-
-
-
-
diff --git a/pages/family/f07-album-list.vue b/pages/family/f07-album-list.vue
index 5f95e42..ed7d07b 100644
--- a/pages/family/f07-album-list.vue
+++ b/pages/family/f07-album-list.vue
@@ -1,63 +1,22 @@
-
+
-
+
-
-
-
-
-
-
-
-
- 让每一张照片都回到家人身边共 {{ albums.length }} 本相册
-
- 本地预览 · 尚未提交服务器
- {{ localAlbumPreview.name }}
- 这本相册不会加入正式列表,离开页面后不保存。
-
-
-
-
-
- {{ album.name }}
- {{ album.photoCount }} 张照片 · {{ album.updatedAt }}
- {{ album.description }}
-
-
-
-
-
- 还没有相册创建一本相册,把团圆、成长与祖居旧影整理在一起。
-
-
-
-
-
+
+
+
{{ stateCopy.title }}
{{ stateCopy.copy }}
-
+
-
-
+
相册名称
-
- {{ albumNameError }}
+
+ {{ submitError }}
-
@@ -66,162 +25,74 @@ import { computed, onUnmounted, ref } from "vue";
import { onBackPress, onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
-import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
-import {
- getGenealogyFixtureAccess,
- listFamilyAlbumFixtures,
-} from "@/data/mock.js";
-import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
-import {
- goBack,
- handleBackPress,
- openPage,
- runBackGuard,
-} from "@/utils/navigation.js";
+import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
+import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const hasValidContext = ref(false);
-const albums = ref([]);
-const albumState = ref("loading");
const dialogVisible = ref(false);
const albumNameDraft = ref("");
-const albumNameError = ref("");
-const localAlbumPreview = ref(null);
-const discardVisible = ref(false);
-const albumStateClasses = computed(() => ({
- [`album-list-state--${albumState.value}`]: true,
- "album-state--empty": albumState.value === "empty",
- "album-list-state--loading": albumState.value === "loading",
- "album-list-state--error": albumState.value === "error",
-}));
-const isDirty = computed(() =>
- Boolean(albumNameDraft.value.trim() || localAlbumPreview.value),
+const submitError = ref("");
+const isSubmitting = ref(false);
+const created = ref(false);
+const requestController = createRequestController();
+const stateCopy = computed(() => !hasValidContext.value
+ ? { title: "相册入口无效", copy: "没有取得有效家谱标识,页面不会展示其他家谱相册。", action: "返回上一页" }
+ : created.value
+ ? { title: "相册已提交服务端", copy: "服务端已返回成功信封;相册列表仍没有条目 DTO,页面不会生成本地相册卡片。", action: "新建相册" }
+ : { title: "相册列表待后端字段合同", copy: "列表响应没有声明相册 ID、封面、名称、照片数、描述或更新时间字段;页面已停止展示本地相册。", action: "新建相册" },
);
-const stateCopy = computed(() => ({
- empty: {
- title: "还没有相册",
- copy: "当前家谱尚无可读取相册,可先生成不会保存的本地预览。",
- action: "填写相册预览",
- },
- error: {
- title: "相册暂不可用",
- copy: "请稍后重新查看,已有照片不会受到影响。",
- action: "重新查看",
- },
- invalid: {
- title: "相册入口无效",
- copy: "没有找到可访问的成员家谱,页面不会展示其他家谱相册。",
- action: "返回上一页",
- },
-}[albumState.value]));
-const discardConfirmation = createDiscardConfirmation((visible) => {
- discardVisible.value = visible;
-});
-const requestDiscardConfirmation = discardConfirmation.request;
-const confirmDiscard = discardConfirmation.confirm;
-const cancelDiscard = discardConfirmation.cancel;
-
onLoad((query) => {
- genealogyId.value = String(query.genealogyId || "");
- hasValidContext.value = ["owner", "member"].includes(
- getGenealogyFixtureAccess(genealogyId.value).accessRole,
- );
- if (!genealogyId.value || !hasValidContext.value) {
- albumState.value = "invalid";
+ genealogyId.value = String(query?.genealogyId || "");
+ hasValidContext.value = /^[1-9]\d*$/.test(genealogyId.value);
+});
+const openCreateDialog = () => {
+ if (!hasValidContext.value || isSubmitting.value) return;
+ albumNameDraft.value = "";
+ submitError.value = "";
+ dialogVisible.value = true;
+};
+const closeCreateDialog = () => {
+ if (!isSubmitting.value) dialogVisible.value = false;
+};
+const submitAlbum = async () => {
+ if (isSubmitting.value) return;
+ const albumName = albumNameDraft.value.trim();
+ if (!albumName) {
+ submitError.value = "请填写相册名称";
return;
}
- albums.value = listFamilyAlbumFixtures(genealogyId.value);
- albumState.value = ["loading", "empty", "error"].includes(query.state)
- ? query.state
- : albums.value.length
- ? "ready"
- : "empty";
+ isSubmitting.value = true;
+ submitError.value = "";
+ try {
+ await appApi.createAlbum(genealogyId.value, { albumName }, { requestController });
+ dialogVisible.value = false;
+ created.value = true;
+ } catch (error) {
+ if (!isRequestCancelled(error)) submitError.value = error?.message || "相册提交失败,请稍后重试。";
+ } finally {
+ isSubmitting.value = false;
+ }
+};
+const requestBack = () => runBackGuard({
+ transientOpen: dialogVisible.value,
+ submitting: isSubmitting.value,
+ "close-transient": closeCreateDialog,
+ "block-submitting": () => true,
});
-
-const openAlbum = (album) =>
- openPage(
- "F08",
- { genealogyId: genealogyId.value, albumId: String(album.id) },
- "F07",
- );
-const createAlbum = () => { albumNameDraft.value = ""; albumNameError.value = ""; dialogVisible.value = true; };
-const closeCreateDialog = () => { dialogVisible.value = false; };
-const requestCloseCreateDialog = async () => {
- if (!albumNameDraft.value.trim()) {
- closeCreateDialog();
- return true;
- }
- const confirmed = await requestDiscardConfirmation();
- if (!confirmed) return false;
- albumNameDraft.value = "";
- albumNameError.value = "";
- closeCreateDialog();
- return true;
-};
-const confirmCreateAlbum = () => {
- const name = albumNameDraft.value.trim();
- if (!name) { albumNameError.value = "请填写相册名称"; return; }
- localAlbumPreview.value = Object.freeze({ name });
- albumNameDraft.value = "";
- dialogVisible.value = false;
-};
-const restoreAlbums = () => {
- albums.value = listFamilyAlbumFixtures(genealogyId.value);
- albumState.value = albums.value.length ? "ready" : "empty";
-};
-const handleStateAction = () => {
- if (albumState.value === "invalid") return goBack();
- if (albumState.value === "error") return restoreAlbums();
- return createAlbum();
-};
-const requestBack = () => {
- if (discardVisible.value) {
- return runBackGuard({
- transientOpen: true,
- "close-transient": cancelDiscard,
- });
- }
- if (dialogVisible.value) {
- return runBackGuard({
- transientOpen: true,
- "close-transient": requestCloseCreateDialog,
- });
- }
- return runBackGuard({
- dirty: isDirty.value,
- "confirm-discard": requestDiscardConfirmation,
- });
-};
+const handleStateAction = () => hasValidContext.value ? openCreateDialog() : goBack();
onBackPress((event) => handleBackPress(event, requestBack));
-onUnmounted(() => { discardConfirmation.dispose(); });
+onUnmounted(() => requestController.abort());
diff --git a/pages/family/f08-album-detail.vue b/pages/family/f08-album-detail.vue
index 495833d..23ad27c 100644
--- a/pages/family/f08-album-detail.vue
+++ b/pages/family/f08-album-detail.vue
@@ -1,342 +1,50 @@
-
+
-
+
-
-
+
-
-
- 相册状态
- 相册已失效或入口无效
- 当前家谱中没有找到这本相册,页面不会回退到其他相册。
-
-
-
-
-
-
- 家族影像
- {{ album.name }}
- {{ album.description }}
- {{ albumState === "empty" ? "0 张照片" : `${photos.length} 张照片` }}
-
-
-
-
- 照片墙
- 相册里还没有照片
- 添加第一张团圆影像,把值得珍藏的时刻留在这里。
-
-
-
-
-
-
- {{ photos[0].caption }}
-
-
-
-
-
- {{ photo.caption }}
-
-
-
-
-
-
-
-
-
-
-
-
- 照片预览
- 关闭
-
-
-
-
diff --git a/pages/family/f09-media-upload.vue b/pages/family/f09-media-upload.vue
index 5173344..4e786eb 100644
--- a/pages/family/f09-media-upload.vue
+++ b/pages/family/f09-media-upload.vue
@@ -1,627 +1,48 @@
-
+
-
+
-
-
+
-
- 相册入口无效
- 没有找到当前相册
- 页面不会把照片归入其他家谱或其他相册。
-
-
-
+
+ 文件服务待接入
+ 照片上传暂不可用
+ 照片写入接口要求已有 `ossId`。当前没有经 Apifox 核实的文件上传 owner、真实 OSS 回执或访问 URL,页面不再从本地图片库选择图片,也不会生成上传预览。
+
-
-
-
-
- 当前相册
- {{ album.name }}
- 最多可选择 9 张
-
-
-
-
- 照片访问权限
- 需要照片访问权限
- 授权后才能选择照片;当前只验证选择与说明流程,不会上传。
-
-
-
-
-
-
- 本地流程预览
- {{ selectedPhotos.length }} 张照片已完成本地校验
- 照片尚未上传,也没有加入“{{ album.name }}”;返回后不会保存。
-
-
-
-
-
-
-
- 选择照片
- {{ selectedPhotos.length }} / {{ MAX_PHOTOS }}
-
-
-
-
-
- {{ index + 1 }}
- 当前
-
- 删除
-
-
-
-
- {{
- selectedPhotos.length ? "继续添加" : "选择照片"
- }}
- {{
- selectedPhotos.length ? `还可选择 ${MAX_PHOTOS - selectedPhotos.length} 张` : "最多 9 张"
- }}
-
-
-
-
-
-
- 整批说明必填
-
-
-
- {{
- validationMessage
- }}
-
-
-
-
-
- 第 {{ activePhotoIndex + 1 }} 张 / 共
- {{ selectedPhotos.length }} 张
- 正在编辑此照片的说明
-
-
-
- 当前照片说明选填
-
-
-
-
-
-
-
-
-
-
diff --git a/pages/family/f10-video-list.vue b/pages/family/f10-video-list.vue
index 436de0b..09e0082 100644
--- a/pages/family/f10-video-list.vue
+++ b/pages/family/f10-video-list.vue
@@ -1,142 +1,11 @@
-
+
-
-
-
-
-
-
-
- 记录会动的家族记忆
-
-
-
-
- {{ hasValidContext ? '视频 · 服务说明' : '页面入口' }}
- {{ hasValidContext ? '视频服务暂未开放' : '家谱身份无效' }}
- {{ hasValidContext ? '开放后可在这里浏览当前家谱的影像与纪念视频。' : '请从一个可访问的成员家谱重新进入,页面不会展示其他家谱内容。' }}
-
-
-
-
-
-
-
+ 视频服务暂未开放Apifox 当前只有删除视频 operation,未声明视频列表、详情、播放地址、发布、编辑、评论、点赞或分享 owner。页面不以相册、参考项目或本地数据补造视频能力。
-
-
diff --git a/pages/genealogy/g03-create-genealogy.vue b/pages/genealogy/g03-create-genealogy.vue
index 3977da1..6e79afd 100644
--- a/pages/genealogy/g03-create-genealogy.vue
+++ b/pages/genealogy/g03-create-genealogy.vue
@@ -1,735 +1,4 @@
-
-
-
-
-
-
-
-
-
-
- 第一步 · 立谱信息
- 为家族立一部可传承的谱
- 家谱建立后可继续完善,名称与访问规则由创建者维护。
-
-
-
- 姓氏
-
-
- {{
- fieldErrors.surname
- }}
-
- 谱名
-
-
- {{
- fieldErrors.name
- }}
-
- 堂号
-
-
-
- 所在地
-
-
- {{
- fieldErrors.location
- }}
-
-
-
- 访问规则
-
- {{ option.label }}
-
-
- 保护族人资料;创建后可在家谱设置中开放搜索与申请加入。
- 创建样式模拟失败,请检查信息后重试。
-
-
- {{
- createState === "submitting" ? "正在创建…" : "创建并录入首代"
- }}
-
-
-
-
- 第二步 · 首代人物
- 家谱从这一位开始
- 可先录入姓名、世代和简要生平,其他资料后续随时补充。
-
-
-
- 姓名
-
-
- {{
- fieldErrors.personName
- }}
-
- 世代
- 第一世
-
-
-
- 性别
- {{ sexLabel }}
-
-
-
- 出生日期
-
- {{
- ancestorForm.birthDate || "请选择日期"
- }}
-
-
-
- 首代人物保存失败,请稍后重试。
-
-
- 生平简述
-
-
-
-
- {{
- ancestorState === "submitting" ? "正在保存…" : "保存并进入家谱"
- }}
-
-
-
-
-
-
-
-
- 先确认是否已有家谱
- 同一家族可能已经建谱。建议先搜索谱名、地区和堂号,避免重复创建。
-
- 先搜索已有家谱
-
-
- 确认没有,继续创建
-
- 返回修改
-
-
-
-
-
-
-
- 家谱创建完成
- 当前为本地流程预览,资料尚未提交服务器,可进入总览继续检查页面。
-
- 进入家谱总览
-
-
-
-
-
-
-
-
-
-
-
-
+
+创建家谱待结果恢复合同创建家谱后还必须用真实响应中的 `genealogyId` 创建首位人物;当前没有可靠的创建结果恢复查询 owner,且不能从泛型“我的家谱”列表按名称猜 ID。页面不再生成本地家谱或本地首位人物预览。
+
+
diff --git a/pages/genealogy/g06-search-genealogies.vue b/pages/genealogy/g06-search-genealogies.vue
index b29f088..0bc2903 100644
--- a/pages/genealogy/g06-search-genealogies.vue
+++ b/pages/genealogy/g06-search-genealogies.vue
@@ -1,718 +1,4 @@
-
-
-
-
-
-
-
-
- 搜索家谱
- 邀请码加入
-
-
-
-
-
-
- 清空
-
-
- 搜索
-
-
-
- 地区
- {{ area }}
-
-
-
-
-
- 先确认家族是否已有家谱
- 可按姓氏、谱名、地区或堂号查找,避免重复创建。
-
-
-
- 检索到 {{ results.length }} 部家谱请核对支系与管理信息
-
-
-
-
- {{ item.name }}
- {{ item.surname }}氏 · {{ item.hall }}
-
- {{ resultRelationLabel(item) }}
-
-
- 地区:{{ item.location }}
- 所属上级谱:{{ item.parentName }}
- 当前支系:{{ item.branchName }}
- 管理信息:{{ item.manager }} · {{ item.certification }}
- {{ item.memberCount }} 位成员 · 更新于
- {{ item.updatedAt }}
-
- {{ resultActionLabel(item) }}
-
-
-
-
-
- 暂未找到匹配家谱
- 可调整地区或关键词后重试,也可使用邀请码加入。
-
-
- 检索暂时失败
- 当前仅展示失败样式,请稍后重新搜索。
-
- 重新搜索
-
-
-
-
-
-
- 输入家谱邀请码
- 邀请码用于定位指定家谱,加入前仍需确认真实姓名和关系。
-
-
-
-
-
- 验证
-
-
- 样式验证可输入 JP2026
-
-
-
-
- 通过邀请码直接定位家谱
- 验证有效后仅显示目标家谱,仍需填写身份关系;本地验证不会变更成员身份。
-
-
- 邀请码无效或已过期
- 请核对邀请码,或向家谱管理员重新获取。
-
-
- 已定位目标家谱
-
-
- {{ inviteTarget.name }}
-
- {{ inviteTarget.surname }}氏 · {{ inviteTarget.hall }} ·
- {{ inviteTarget.location }}
- 所属上级谱:{{ inviteTarget.parentName }}
- 当前支系:{{ inviteTarget.branchName }}
- {{ inviteTarget.manager }} ·
- {{ inviteTarget.certification }}
-
- 填写关系信息
-
-
- 当前为样式验证,不会变更成员身份
-
-
-
-
-
-
-
-
-
+
+公开家谱搜索待后端字段合同公开家谱读取没有声明家谱 ID、名称、地区、简介、成员数或可申请状态字段;页面不再展示 fixture 搜索结果,也不从猜测条目进入申请。
+
+
diff --git a/pages/genealogy/g08-join-application.vue b/pages/genealogy/g08-join-application.vue
index a4d2879..1bb33cd 100644
--- a/pages/genealogy/g08-join-application.vue
+++ b/pages/genealogy/g08-join-application.vue
@@ -1,462 +1,4 @@
-
-
-
-
-
-
-
-
- {{ sourceContract.eyebrow }}
- {{ sourceContract.formTitle }} {{ genealogyName }}
- {{ sourceContract.formCopy }}
-
- 真实姓名
-
- {{
- fieldErrors.realName
- }}
-
- 与家谱关系
-
- {{
- fieldErrors.relation
- }}
- 请以家谱中一位已知长辈为参照,例如:某某某堂侄
-
- {{ sourceContract.thirdFieldLabel }}
-
-
- {{ sourceContract.note }}
-
- {{
- isSubmitting
- ? sourceContract.submittingLabel
- : sourceContract.submitLabel
- }}
-
-
-
-
- {{
- joinState === "success"
- ? sourceContract.successEyebrow
- : joinState === "ineligible"
- ? "当前不可申请"
- : sourceContract.errorEyebrow
- }}
- {{
- joinState === "success"
- ? sourceContract.successTitle
- : joinState === "ineligible"
- ? ineligibleTitle
- : sourceContract.errorTitle
- }}
- {{ resultCopy }}
-
- {{
- joinState === "success"
- ? sourceContract.nextLabel
- : joinState === "ineligible"
- ? ineligibleActionLabel
- : "重新填写"
- }}
-
-
-
-
-
-
-
-
-
-
-
+
+加入申请待后端字段合同申请提交 operation 不等于可安全发起申请:当前没有公开家谱详情、可申请权限或稳定家谱条目 ID 的消费投影。页面不再使用 fixture 家谱或本地申请成功提示。
+
+
diff --git a/pages/genealogy/g09-my-applications.vue b/pages/genealogy/g09-my-applications.vue
index a2a2a2e..6012d40 100644
--- a/pages/genealogy/g09-my-applications.vue
+++ b/pages/genealogy/g09-my-applications.vue
@@ -1,394 +1,11 @@
-
+
-
-
-
-
-
-
-
- 入谱申请进度审核结果会同步到消息中心
-
-
-
- {{ item.genealogyName }}
- {{ item.appliedAt }}
- {{ item.relation }}
- {{ statusLabel(item.status) }}
- {{
- statusHint(item.status)
- }}
- {{ actionFor(item) }}
-
-
-
-
-
-
-
- {{ stateTitle }}
- {{ stateCopy }}
-
-
-
- {{
- applicationState === "error" ? "重新查看" : "查找公开家谱"
- }}
-
-
-
-
-
+ 申请列表待后端字段合同申请读取响应没有声明申请 ID、家谱名称、申请时间、状态或审核备注字段;页面已停止展示本地申请和本地撤回结果。
-
-
diff --git a/pages/genealogy/g10-application-review.vue b/pages/genealogy/g10-application-review.vue
index f30b92f..bc72d0b 100644
--- a/pages/genealogy/g10-application-review.vue
+++ b/pages/genealogy/g10-application-review.vue
@@ -1,478 +1,11 @@
-
+
-
-
-
-
-
-
- 核实亲属关系后再决定
- 当前只预览审核交互,不会提交服务器
-
-
-
-
-
- {{ item.name }}
- {{ item.phone }}
- {{ item.appliedAt }}
- {{ item.relation }}
-
-
-
-
-
- {{
- item.status === "APPROVED" ? "已通过" : "已拒绝"
- }}
-
-
-
-
-
-
-
-
- {{ stateTitle }}
- {{ stateCopy }}
-
-
-
-
-
-
-
- 拒绝原因
-
- {{ rejectionError }}
-
-
-
-
- {{ feedbackMessage }}
-
-
+ 申请审核待后端字段合同审核 operation 存在,但没有可消费的申请列表 DTO 或稳定申请 ID 来源;页面不再展示 fixture 申请或本地通过/拒绝预览。
-
-
diff --git a/pages/genealogy/g11-genealogy-settings.vue b/pages/genealogy/g11-genealogy-settings.vue
index 92f6264..40a15f0 100644
--- a/pages/genealogy/g11-genealogy-settings.vue
+++ b/pages/genealogy/g11-genealogy-settings.vue
@@ -1,485 +1,11 @@
-
+
-
-
-
-
-
-
-
- 谱主可见 · 基础设置
- 完善家谱访问规则
- 这里维护名称、访问规则与家谱简介;访问规则会同时决定公开范围和加入方式,管理权转让在成员场景中单独处理。
-
-
- 家谱名称
-
-
- {{
- nameError
- }}
-
-
- 访问规则
-
-
-
- {{ option.label }}
-
-
- {{ visibilityHint }}
-
-
-
- 家谱简介
-
-
-
-
- 保存设置
-
-
-
-
- {{
- settingsState === "success"
- ? "设置已保存"
- : settingsState === "no-permission"
- ? "权限不足"
- : "设置未保存"
- }}
- {{
- settingsState === "success"
- ? "本页设置草稿已更新"
- : settingsState === "no-permission"
- ? "当前账号不能修改家谱"
- : "暂时无法打开家谱设置"
- }}
- {{
- settingsState === "success"
- ? `本地预览已更新,尚未提交服务器;当前只保留在本页,返回总览不会改变原资料 · ${genealogyDraft.name} · ${accessPresetLabel}`
- : settingsState === "no-permission"
- ? "只有家谱所有者可以修改名称、访问规则和家谱简介。"
- : "请从家谱总览重新进入,当前修改不会被保留。"
- }}
-
- {{
- settingsState === "success" ? "继续调整" : "重新查看"
- }}
-
-
-
-
-
- 本页草稿已更新
-
-
+ 家谱设置待字段合同更新 operation 已声明名称、姓氏、祠堂、地区、简介、可见性等字段,但当前没有可消费的概览 DTO 来安全预填,也没有封面 `coverOssId` 的上传闭环。页面不再以 fixture 编辑或本地预览保存。
-
-
diff --git a/pages/genealogy/g12-generation-poems.vue b/pages/genealogy/g12-generation-poems.vue
index 3ea0ec1..662ac3d 100644
--- a/pages/genealogy/g12-generation-poems.vue
+++ b/pages/genealogy/g12-generation-poems.vue
@@ -1,176 +1,76 @@
-
+
-
+
+
+
+
-
-
{{ genealogyName }} · 传承字序
{{ generationRangeTitle }}
- {{ currentGenerationCopy }}
+ 接口未声明“当前世代”字段,页面只按后端返回的世代、文字和状态展示。
-
+
第 {{ item.generationNo }} 世
{{ item.generationText }}
- {{
- item.current
- ? "当前字辈"
- : item.status === GENERATION_POEM_STATUS.DISABLED
- ? "已停用·记录保留"
- : "传承字序"
- }}
+ {{ item.status === GENERATION_POEM_STATUS.DISABLED ? '已停用·记录保留' : '传承字序' }}
-
+
继续加载后续字辈(剩余 {{ remainingPoemCount }} 代)
-
- 维护字辈诗
+
+ {{ canManage ? '继续维护字辈诗' : '读取维护列表' }}
- 批量维护
+ 服务端批量维护
录入完整字辈序列
- 无分隔符时每个字符对应一代;也可用空格、逗号、分号、顿号、斜杠或竖线分隔多字字辈。一次最多
- {{ MAX_GENERATION_COUNT }} 代,每代最多
- {{ MAX_GENERATION_TEXT_LENGTH }} 个字符。本页只更新本地预览,尚未提交服务器。
+ 提交前会先请求服务端批量预览。无分隔符时每个字符对应一代;也可用空格、逗号、分号、顿号、斜杠或竖线分隔多字字辈。
字辈内容
-
+
{{ poemError }}
未被新文本覆盖的后续世代
-
-
- {{ disableMissing ? "停用并保留记录" : "保持原状态" }}
+
+
+ {{ disableMissing ? '停用并保留记录' : '保持原状态' }}
- 预览:{{ previewSummary }}
+ {{ previewSummary }}
- 取消
+ 取消
-
- 保存字辈
+
+ {{ previewing ? '正在预览' : '服务端预览' }}
+
+
+ {{ saving ? '正在保存' : '保存字辈' }}
- {{
- poemState === "empty"
- ? "尚未建立字辈"
- : poemState === "no-permission"
- ? "权限不足"
- : "字辈暂不可用"
- }}
- {{
- poemState === "empty"
- ? "从第一段传承字序开始"
- : poemState === "no-permission"
- ? "当前账号不能维护字辈"
- : "暂时无法读取字辈诗"
- }}
- {{
- poemState === "empty"
- ? "管理员可以一次录入连续字辈,系统会按世代拆分展示。"
- : poemState === "no-permission"
- ? "普通成员可以查看字辈,但只有具备管理权限的成员可以维护。"
- : "请从家谱总览重新进入,或稍后再试。"
- }}
-
- {{
- poemState === "empty"
- ? "开始录入"
- : poemState === "no-permission"
- ? "返回家谱总览"
- : "重新查看"
- }}
+ {{ poemState === 'empty' ? '尚未建立字辈' : '字辈暂不可用' }}
+ {{ poemState === 'empty' ? '当前读取没有正常状态字辈' : '暂时无法读取字辈诗' }}
+ {{ stateCopy }}
+
+ {{ poemState === 'empty' ? '读取维护列表' : '重新读取' }}
-
- 本地字辈预览已更新
-
+ {{ feedbackMessage }}
@@ -192,85 +90,61 @@ import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
-import {
- findGenealogyFixture,
- getGenealogyFixtureAccess,
-} from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
+import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
- MAX_GENERATION_COUNT,
MAX_GENERATION_POEM_INPUT_LENGTH,
- MAX_GENERATION_TEXT_LENGTH,
GENERATION_POEM_STATUS,
- findFirstGenerationGap,
- mergeGenerationPoemRows,
validateGenerationPoemText,
} from "@/utils/generation-poem.js";
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
-const INITIAL_POEM_TEXT = "启宗敦本";
const POEM_RENDER_BATCH_SIZE = 50;
-const PREVIEW_GENERATION_LIMIT = 12;
-const LOCAL_PREVIEW_START_GENERATION = 1;
-const LOCAL_CURRENT_GENERATION = 3;
const genealogyId = ref("");
-const genealogyName = ref("家谱");
+const genealogyName = ref("当前家谱");
const poemState = ref("loading");
const poemError = ref("");
-const feedbackVisible = ref(false);
+const feedbackMessage = ref("");
const discardVisible = ref(false);
const canManage = ref(false);
-let feedbackTimer = null;
-const poemDraft = ref(INITIAL_POEM_TEXT);
-// OpenAPI 的 disableMissing 示例为 false;本地同样采用安全默认 false。停用后续世代是高影响动作,
-// 必须由谱主主动选择,不能把缩短一次本地草稿解释为默认停用历史记录。
+const previewing = ref(false);
+const saving = ref(false);
+const poemDraft = ref("");
const disableMissing = ref(false);
-const editorOrigin = ref("list");
const editorSnapshot = ref(null);
const poemRows = ref([]);
const visiblePoemCount = ref(POEM_RENDER_BATCH_SIZE);
-const visiblePoemRows = computed(() =>
- poemRows.value.slice(0, visiblePoemCount.value),
-);
-const remainingPoemCount = computed(() =>
- poemRows.value.length > visiblePoemCount.value
- ? poemRows.value.length - visiblePoemCount.value
- : 0,
-);
-const pageTitle = computed(() =>
- poemState.value === "edit" ? "维护字辈诗" : "字辈诗",
-);
+const preview = ref(null);
+const previewSignature = ref("");
+const requestController = createRequestController();
+let requestSequence = 0;
+
+const visiblePoemRows = computed(() => poemRows.value.slice(0, visiblePoemCount.value));
+const remainingPoemCount = computed(() => Math.max(0, poemRows.value.length - visiblePoemCount.value));
+const pageTitle = computed(() => poemState.value === "edit" ? "维护字辈诗" : "字辈诗");
const generationRangeTitle = computed(() => {
if (!poemRows.value.length) return "尚未建立字辈";
const first = poemRows.value[0].generationNo;
const last = poemRows.value[poemRows.value.length - 1].generationNo;
return first === last ? `第 ${first} 世字辈` : `第 ${first}—${last} 世字辈`;
});
-const currentGenerationCopy = computed(() => {
- const current = poemRows.value.find((item) => item.current);
- return current
- ? `当前为第 ${current.generationNo} 世“${current.generationText}”字辈。`
- : `当前第 ${LOCAL_CURRENT_GENERATION} 世尚未被有效字辈覆盖。`;
-});
+const draftSignature = computed(() => `${poemDraft.value}\u0000${disableMissing.value}`);
+const canSave = computed(() => Boolean(preview.value) && previewSignature.value === draftSignature.value);
const previewSummary = computed(() => {
- const validation = validateGenerationPoemText(poemDraft.value);
- if (!validation.valid) {
- return poemDraft.value.trim() ? validation.message : "尚未录入字辈";
- }
- const visible = validation.generations
- .slice(0, PREVIEW_GENERATION_LIMIT)
- .join(" · ");
- const remaining = validation.generations.length - PREVIEW_GENERATION_LIMIT;
- return remaining > 0 ? `${visible} · …另 ${remaining} 代` : visible;
+ if (previewing.value) return "正在请求服务端预览。";
+ if (!preview.value) return "尚未请求服务端预览;预览成功后才可保存。";
+ return `服务端预览:新增 ${preview.value.createCount} 条,更新 ${preview.value.updateCount} 条,保留 ${preview.value.keepCount} 条,停用 ${preview.value.disableCount} 条。`;
});
-const isDirty = computed(() =>
- Boolean(
- poemState.value === "edit" &&
- editorSnapshot.value &&
- (poemDraft.value !== editorSnapshot.value.poemDraft ||
- disableMissing.value !== editorSnapshot.value.disableMissing),
- ),
+const stateCopy = computed(() => poemState.value === "empty"
+ ? "正常字辈读取未返回条目。维护列表由后端单独鉴权,点击后才会判断是否可维护。"
+ : "未取得可消费的服务端响应,页面不会用本地字辈或本地权限替代。",
);
+const isDirty = computed(() => Boolean(
+ poemState.value === "edit" && editorSnapshot.value && (
+ poemDraft.value !== editorSnapshot.value.poemDraft ||
+ disableMissing.value !== editorSnapshot.value.disableMissing
+ ),
+));
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
@@ -278,439 +152,186 @@ const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
-const resetPoemRows = () => {
- const seed = validateGenerationPoemText(INITIAL_POEM_TEXT).generations;
- poemDraft.value = INITIAL_POEM_TEXT;
- poemRows.value = mergeGenerationPoemRows({
- existingRows: [],
- generationTexts: seed,
- startGeneration: LOCAL_PREVIEW_START_GENERATION,
- currentGeneration: LOCAL_CURRENT_GENERATION,
- disableMissing: false,
- });
+const applyRows = (rows) => {
+ poemRows.value = rows;
visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
+ const named = rows.find((item) => item.genealogyName);
+ genealogyName.value = named?.genealogyName || "当前家谱";
};
-
-const loadMorePoems = () => {
- visiblePoemCount.value = Math.min(
- poemRows.value.length,
- visiblePoemCount.value + POEM_RENDER_BATCH_SIZE,
- );
-};
-
-const loadPoems = (query = {}) => {
- genealogyId.value = String(query.genealogyId || "");
- const fixture = findGenealogyFixture(genealogyId.value);
- if (!fixture) {
- canManage.value = false;
+const loadPoems = async ({ management = false } = {}) => {
+ if (!genealogyId.value) {
poemState.value = "error";
- return;
+ return false;
}
- genealogyName.value = fixture.name || "家谱";
- const accessRole = getGenealogyFixtureAccess(genealogyId.value).accessRole;
- canManage.value = accessRole === "owner";
- if (accessRole === "guest") {
- poemState.value = "no-permission";
- return;
+ const sequence = ++requestSequence;
+ poemState.value = "loading";
+ poemError.value = "";
+ try {
+ const rows = management
+ ? await appApi.getGenerationPoemManagement(genealogyId.value, { requestController })
+ : await appApi.getGenerationPoems(genealogyId.value, { requestController });
+ if (sequence !== requestSequence) return false;
+ applyRows(rows);
+ canManage.value = management;
+ poemState.value = rows.length ? "list" : "empty";
+ return true;
+ } catch (error) {
+ if (isRequestCancelled(error) || sequence !== requestSequence) return false;
+ poemState.value = "error";
+ return false;
}
- resetPoemRows();
- const requestedState =
- query.state === "loading"
- ? "loading"
- : query.state === "empty"
- ? "empty"
- : query.state === "edit"
- ? "edit"
- : query.state === "no-permission"
- ? "no-permission"
- : query.state === "error"
- ? "error"
- : "list";
- if (requestedState === "empty") {
- poemDraft.value = "";
- poemRows.value = [];
- }
- if (requestedState === "edit") {
- if (!canManage.value) {
- poemState.value = "no-permission";
- return;
- }
- poemState.value = "list";
- openEditor("list");
- return;
- }
- poemState.value = requestedState;
};
-
-onLoad(loadPoems);
-onUnload(() => {
- const timer = feedbackTimer;
- feedbackTimer = null;
- if (timer) clearTimeout(timer);
- discardConfirmation.dispose();
-});
-
-const openEditor = (origin = poemState.value) => {
+const loadMorePoems = () => {
+ visiblePoemCount.value = Math.min(poemRows.value.length, visiblePoemCount.value + POEM_RENDER_BATCH_SIZE);
+};
+const invalidatePreview = () => {
+ poemError.value = "";
+ preview.value = null;
+ previewSignature.value = "";
+};
+const toggleDisableMissing = () => {
+ disableMissing.value = !disableMissing.value;
+ invalidatePreview();
+};
+const openEditor = () => {
if (!canManage.value) return false;
- editorOrigin.value = origin === "empty" ? "empty" : "list";
- editorSnapshot.value = Object.freeze({
- poemDraft: poemDraft.value,
- disableMissing: disableMissing.value,
- });
+ poemDraft.value = poemRows.value
+ .filter((item) => item.status === GENERATION_POEM_STATUS.ACTIVE)
+ .map((item) => item.generationText)
+ .join(" ");
+ disableMissing.value = false;
+ preview.value = null;
+ previewSignature.value = "";
+ editorSnapshot.value = Object.freeze({ poemDraft: poemDraft.value, disableMissing: disableMissing.value });
poemState.value = "edit";
return true;
};
-const restoreEditorSnapshot = () => {
- if (!editorSnapshot.value) return;
- poemDraft.value = editorSnapshot.value.poemDraft;
- disableMissing.value = editorSnapshot.value.disableMissing;
+const enterManagement = async () => {
+ if (previewing.value || saving.value) return;
+ const loaded = await loadPoems({ management: true });
+ if (loaded) openEditor();
};
const requestLeaveEditor = async () => {
if (isDirty.value) {
const confirmed = await requestDiscardConfirmation();
if (!confirmed) return false;
}
- restoreEditorSnapshot();
- poemState.value = editorOrigin.value;
+ poemState.value = poemRows.value.length ? "list" : "empty";
editorSnapshot.value = null;
poemError.value = "";
return true;
};
-const requestBack = () =>
- runBackGuard({
- transientOpen: discardVisible.value,
- internalTrail: poemState.value === "edit",
- "close-transient": cancelDiscard,
- "pop-internal-trail": requestLeaveEditor,
- });
-
-onBackPress((event) => handleBackPress(event, requestBack));
-
-const handleStateAction = () => {
- if (poemState.value === "empty") return openEditor("empty");
- if (poemState.value === "error")
- return loadPoems({ genealogyId: genealogyId.value });
- return requestBack();
-};
-const savePoems = () => {
+const validateDraft = () => {
const validation = validateGenerationPoemText(poemDraft.value);
if (!validation.valid) {
poemError.value = validation.message;
- return;
+ return false;
}
- const lastGeneration =
- LOCAL_PREVIEW_START_GENERATION + validation.generations.length - 1;
- if (LOCAL_CURRENT_GENERATION > lastGeneration) {
- poemError.value = `请至少录入到当前第 ${LOCAL_CURRENT_GENERATION} 世`;
- return;
- }
- const nextRows = mergeGenerationPoemRows({
- existingRows: poemRows.value,
- generationTexts: validation.generations,
- startGeneration: LOCAL_PREVIEW_START_GENERATION,
- currentGeneration: LOCAL_CURRENT_GENERATION,
- disableMissing: disableMissing.value,
- });
- const activeRows = nextRows.filter(
- (item) => item.status === GENERATION_POEM_STATUS.ACTIVE,
- );
- const lastActiveGeneration = activeRows.length
- ? activeRows[activeRows.length - 1].generationNo
- : null;
- const firstGap = lastActiveGeneration === null
- ? null
- : findFirstGenerationGap(
- nextRows,
- lastActiveGeneration + 1,
- LOCAL_PREVIEW_START_GENERATION,
- );
- if (firstGap !== null) {
- poemError.value = `第 ${firstGap} 世字辈缺失;请补齐完整序列,或选择停用未覆盖的后续记录`;
- return;
- }
- poemRows.value = nextRows;
- visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
- editorSnapshot.value = null;
- poemState.value = "list";
- feedbackVisible.value = true;
- if (feedbackTimer) clearTimeout(feedbackTimer);
- const timer = setTimeout(() => {
- if (feedbackTimer !== timer) return;
- feedbackVisible.value = false;
- feedbackTimer = null;
- }, 1800);
- feedbackTimer = timer;
+ return true;
};
+const previewPoems = async () => {
+ if (previewing.value || saving.value || !validateDraft()) return;
+ previewing.value = true;
+ poemError.value = "";
+ try {
+ preview.value = await appApi.previewGenerationPoemBatch(genealogyId.value, {
+ poemText: poemDraft.value,
+ disableMissing: disableMissing.value,
+ }, { requestController });
+ previewSignature.value = draftSignature.value;
+ } catch (error) {
+ if (!isRequestCancelled(error)) poemError.value = error?.message || "服务端预览失败,请稍后重试。";
+ } finally {
+ previewing.value = false;
+ }
+};
+const savePoems = async () => {
+ if (!canSave.value || saving.value || !validateDraft()) return;
+ saving.value = true;
+ poemError.value = "";
+ try {
+ await appApi.saveGenerationPoemBatch(genealogyId.value, {
+ poemText: poemDraft.value,
+ disableMissing: disableMissing.value,
+ }, { requestController });
+ feedbackMessage.value = "服务端字辈已保存,正在刷新维护列表。";
+ const loaded = await loadPoems({ management: true });
+ if (!loaded) {
+ poemError.value = "字辈已提交,但维护列表刷新失败;请稍后重新查看。";
+ return;
+ }
+ editorSnapshot.value = null;
+ feedbackMessage.value = "服务端字辈已保存并已刷新。";
+ } catch (error) {
+ if (!isRequestCancelled(error)) poemError.value = error?.message || "字辈保存失败,请稍后重试。";
+ } finally {
+ saving.value = false;
+ }
+};
+const requestBack = () => runBackGuard({
+ transientOpen: discardVisible.value,
+ internalTrail: poemState.value === "edit",
+ submitting: previewing.value || saving.value,
+ "close-transient": cancelDiscard,
+ "pop-internal-trail": requestLeaveEditor,
+ "block-submitting": () => true,
+});
+const handleStateAction = () => poemState.value === "empty" ? enterManagement() : loadPoems();
+
+onLoad((query) => {
+ genealogyId.value = String(query?.genealogyId || "");
+ loadPoems();
+});
+onBackPress((event) => handleBackPress(event, requestBack));
+onUnload(() => {
+ ++requestSequence;
+ requestController.abort();
+ discardConfirmation.dispose();
+});
diff --git a/pages/notification/n01-message-center.vue b/pages/notification/n01-message-center.vue
index d4af8f7..67aae9c 100644
--- a/pages/notification/n01-message-center.vue
+++ b/pages/notification/n01-message-center.vue
@@ -1,260 +1,12 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
- {{ item.unread ? "未读提醒" : "已读" }} · {{ item.time }}
- {{ item.title }}
- {{ item.content }}
-
-
-
-
-
-
-
-
- {{
- noticeState === "empty" ? "暂时没有新消息" : "消息中心暂不可用"
- }}
- {{
- noticeState === "empty"
- ? "家谱申请、审核结果和家族提醒会留在这里。"
- : "请稍后重新进入,已读状态不会受到影响。"
- }}
-
-
-
-
-
-
-
+ 消息列表待后端字段合同通知列表没有声明通知 ID、已读状态、标题、时间、正文、类型或跳转参数;页面已停止展示本地消息,也不会以 fixture 修改已读状态。
-
-
diff --git a/pages/notification/n02-message-detail.vue b/pages/notification/n02-message-detail.vue
index 91b4a73..6427ffc 100644
--- a/pages/notification/n02-message-detail.vue
+++ b/pages/notification/n02-message-detail.vue
@@ -1,141 +1,11 @@
-
+
-
-
-
-
-
-
-
-
- 这条消息已失效
- 消息可能已撤回或超过保留期限,请返回消息中心查看其他内容。
-
-
-
-
-
-
- {{ noticeDetail.unread ? "未读提醒" : "已读" }}
- {{ noticeDetail.time }}
-
- {{ noticeDetail.title }}
- {{ noticeDetail.body }}
- 来自:{{ noticeDetail.source }}
-
-
-
-
- {{ targetError }}
-
-
-
-
-
+ 消息详情暂未开放Apifox 当前只有通知列表、单条标已读和全部标已读 operation,没有消息详情读取 owner;页面不再从 fixture 展示正文、来源或业务跳转。
-
-
diff --git a/pages/profile/m01-profile-home.vue b/pages/profile/m01-profile-home.vue
index af3645c..0c1917c 100644
--- a/pages/profile/m01-profile-home.vue
+++ b/pages/profile/m01-profile-home.vue
@@ -1,429 +1,11 @@
-
+
-
-
-
-
-
-
-
-
-
-
- 个人谱牒
-
-
-
- {{ currentUser.name }}
- {{ currentUser.role }}
- {{ currentUser.phone }}
-
-
-
-
-
-
-
- 待你处理
- {{ unreadCount }} 条家谱提醒与审核通知
-
-
-
-
-
-
- 服务与设置
-
-
-
-
-
-
-
-
- 个人资料暂不可用
- 请稍后重新进入,账号和家谱资料不会受到影响。
-
-
-
-
-
-
-
+ 个人资料待后端字段合同当前资料读取接口只返回通用对象,未声明昵称、角色、手机号或脱敏规则;页面已停止展示 mock 用户与 fixture 未读数。
-
-
diff --git a/pages/profile/m02-edit-profile.vue b/pages/profile/m02-edit-profile.vue
index a3c5ede..f69b0bc 100644
--- a/pages/profile/m02-edit-profile.vue
+++ b/pages/profile/m02-edit-profile.vue
@@ -1,124 +1,11 @@
-
+
-
-
-
-
-
-
- {{ profileForm.nickName || "未填写昵称" }}头像将在上传接口批次接入
- 选择头像
-
-
-
- 昵称
- {{ errors.nickName }}
- 真实姓名
- 邮箱
- {{ errors.email }}
-
-
-
-
-
-
+ 个人资料编辑待字段合同读取接口没有资料 DTO;更新接口允许昵称、头像、性别、生日、地区和地址,但当前页面原有真实姓名、邮箱等字段不属于该合同。页面不再用 mock 预填或提交未知字段。
-
-
diff --git a/pages/profile/m03-security-settings.vue b/pages/profile/m03-security-settings.vue
index e6f2f93..f41ae4f 100644
--- a/pages/profile/m03-security-settings.vue
+++ b/pages/profile/m03-security-settings.vue
@@ -1,50 +1,11 @@
-
+
-
-
-
-
-
- 账号安全操作
- 修改密码和换绑手机号目前只提供本地校验预览,不会变更服务器账号信息。
-
-
-
-
- {{ item.label }}{{ item.note }}
-
-
-
-
-
-
-
+ 安全资料待后端字段合同没有独立安全概览、设备或登录记录读取 owner;页面不再显示 mock 账号摘要。可进入已独立核对的修改密码页面,换绑手机号仍需人工 TAC/短信窗口。
-
-
diff --git a/pages/profile/m04-change-password.vue b/pages/profile/m04-change-password.vue
index 23bcdd9..d95d73d 100644
--- a/pages/profile/m04-change-password.vue
+++ b/pages/profile/m04-change-password.vue
@@ -15,7 +15,7 @@
{{ errors[field.key] }}
-
+
JSON.stringify(passwordForm));
const baseline = ref(formSnapshot.value);
const isDirty = computed(() => formSnapshot.value !== baseline.value);
@@ -79,13 +83,27 @@ const validateForm = () => {
errors.confirm = !passwordForm.confirm ? "请再次输入新密码" : passwordForm.confirm !== passwordForm.next ? "两次输入的新密码不一致" : "";
return !Object.values(errors).some(Boolean);
};
-const savePassword = () => {
+const savePassword = async () => {
if (!validateForm() || passwordState.value === "saving") return;
passwordState.value = "saving";
- timer = setTimeout(() => {
- passwordState.value = "ready";
- showToast("本地校验通过,尚未提交服务器");
- }, 500);
+ try {
+ await appApi.changePassword({
+ oldPasswordHash: calcMD5(passwordForm.current),
+ newPasswordHash: calcMD5(passwordForm.next),
+ }, { requestController: passwordRequestController });
+ if (!pageActive) return;
+ passwordForm.current = "";
+ passwordForm.next = "";
+ passwordForm.confirm = "";
+ baseline.value = formSnapshot.value;
+ showToast("密码修改成功");
+ } catch (error) {
+ if (pageActive && !isRequestCancelled(error)) {
+ showToast(error?.message || "密码修改失败,请稍后重试");
+ }
+ } finally {
+ if (pageActive) passwordState.value = "ready";
+ }
};
const requestBack = () =>
runBackGuard({
@@ -98,6 +116,8 @@ const requestBack = () =>
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
+ pageActive = false;
+ passwordRequestController.abort();
clearTimeout(timer);
discardConfirmation.dispose();
});
diff --git a/pages/profile/m05-change-phone.vue b/pages/profile/m05-change-phone.vue
index 88de000..d415fc3 100644
--- a/pages/profile/m05-change-phone.vue
+++ b/pages/profile/m05-change-phone.vue
@@ -1,98 +1,11 @@
-
+
-
-
-
-
- 当前绑定手机号{{ phoneForm.currentPhone }}更换后,新手机号将用于登录与安全验证。
-
- 新手机号
- {{ errors.newPhone }}
- 验证码发送条件
- {{ errors.code }}
-
-
-
-
-
-
+ 换绑手机号待人工验证换绑接口要求新手机号、四位短信码和 `clientId`,但没有可安全消费的当前手机号 DTO,也没有已核实的专用受保护发码链。页面不显示 mock 手机号、不发送验证码、不在无人值守时换绑。
-
-
diff --git a/pages/profile/m06-help-center.vue b/pages/profile/m06-help-center.vue
index 1f975ed..07efa02 100644
--- a/pages/profile/m06-help-center.vue
+++ b/pages/profile/m06-help-center.vue
@@ -4,6 +4,7 @@
+ 当前展示的是本地使用说明,不代表已读取服务端帮助文章;需要最新支持请提交意见反馈。
{{ filteredQuestions.length }} 条
{{ category }}
@@ -46,5 +47,5 @@ const contactSupport = () => openPage("M07", {}, "M06");
diff --git a/pages/profile/m10-about-settings.vue b/pages/profile/m10-about-settings.vue
index a28c73a..281a4da 100644
--- a/pages/profile/m10-about-settings.vue
+++ b/pages/profile/m10-about-settings.vue
@@ -1,6 +1,6 @@
-
+
@@ -11,18 +11,19 @@
-
+
diff --git a/pages/records/r04-gift-editor.vue b/pages/records/r04-gift-editor.vue
index cbd5e71..6d4b9a5 100644
--- a/pages/records/r04-gift-editor.vue
+++ b/pages/records/r04-gift-editor.vue
@@ -1,130 +1,47 @@
-
+
-
+
-
-
-
-
-
-
- 本地预览,尚未提交服务器
- 这份往来内容只存在于当前页面,不会插入、修改或删除正式记录。
-
- {{ item.label }}{{ item.value }}
-
-
-
-
- {{ relativeForm.eventName }}
-
- {{ item.label }}
- {{ item.value }}
-
-
-
-
-
-
- {{ mode === "create" ? "填写一份人情往来预览" : "调整往来记录预览" }}
-
+
+
+
+ 记录一份家人往来
+ 页面只发送已声明且可映射的字段;媒体需要 `mediaOssIds`,没有上传 owner 时不提交。
{{ field.label }}
-
- {{ relativeErrors[field.key] }}
+
+
-
-
+ {{ submitError }}
+
- 往来记录不可用
- 记录不存在、缺少身份或不属于当前家谱,页面不会回退到其他记录。
-
+ {{ resultCopy.title }}
+ {{ resultCopy.copy }}
+
-
+
+
+
diff --git a/pages/records/r05-ritual-list.vue b/pages/records/r05-ritual-list.vue
index 75c828b..8575331 100644
--- a/pages/records/r05-ritual-list.vue
+++ b/pages/records/r05-ritual-list.vue
@@ -1,216 +1,31 @@
-
+
-
+
-
-
-
-
-
-
-
- {{ ceremony.ceremonyType }}
- {{ ceremony.ceremonyTitle }}
-
- {{ ceremony.ceremonyTime }} ·
- {{ ceremony.location || "地点待定" }}
-
- 查看活动与受邀信息
-
-
-
-
- {{ stateCopy.title }}
- {{ stateCopy.copy }}
-
-
-
+
+ 礼仪活动列表暂未开放Apifox 只有活动详情、修改、献礼和删除相关 operation,没有活动列表或新建活动 owner;页面不再展示本地礼仪活动。
diff --git a/pages/records/r06-ritual-detail.vue b/pages/records/r06-ritual-detail.vue
index bd794aa..06463d5 100644
--- a/pages/records/r06-ritual-detail.vue
+++ b/pages/records/r06-ritual-detail.vue
@@ -1,252 +1,21 @@
-
+
-
-
-
-
-
-
-
-
-
- {{ ceremonyDetail.ceremonyType }}
- {{ ceremonyDetail.ceremonyTitle }}
-
- {{ ceremonyDetail.ceremonyTime }} ·
- {{ ceremonyDetail.location || "地点待定" }}
-
- {{ ceremonyDetail.ceremonyDesc || "暂无活动说明" }}
-
-
-
- 受邀家人
- {{ invitees.length }} 人
-
-
- {{ invitee.displayName }} · {{ invitee.relationName }}
- {{ invitee.statusText }}
-
- 尚无受邀记录—
-
-
-
-
- {{ stateCopy.title }}
- {{ stateCopy.copy }}
-
-
-
-
+ {{ stateCopy.title }}{{ stateCopy.copy }}
diff --git a/pages/records/r07-ritual-editor.vue b/pages/records/r07-ritual-editor.vue
index 0543212..942c044 100644
--- a/pages/records/r07-ritual-editor.vue
+++ b/pages/records/r07-ritual-editor.vue
@@ -1,321 +1,19 @@
-
+
-
-
-
-
-
-
-
-
- 本地预览,尚未提交服务器
- 这份礼仪内容只存在于当前页面,不会新增、覆盖或删除正式活动。
-
- {{ item.label }}
- {{ item.value }}
-
-
-
-
-
- {{ mode === "create" ? "填写一份家族礼仪预览" : "调整活动预览" }}
-
-
- {{ field.label }}
-
-
-
- {{ ritualErrors[field.key] }}
-
-
-
-
-
-
-
- 礼仪活动不可用
- 活动不存在、缺少身份或不属于当前家谱,页面不会回退到其他活动。
-
-
-
-
-
+ 礼仪活动维护暂未开放当前没有新建活动 operation;修改虽有 operation,但没有可消费的详情 DTO 或可靠活动 ID 来源。本页不再生成本地创建或编辑预览。
diff --git a/pages/records/r08-growth-journal.vue b/pages/records/r08-growth-journal.vue
index 6a565c6..7c822a1 100644
--- a/pages/records/r08-growth-journal.vue
+++ b/pages/records/r08-growth-journal.vue
@@ -1,407 +1,22 @@
-
+
-
-
-
-
-
-
-
-
- {{ memberRecord.name }}
- 成长中的每一个瞬间
-
-
- 本地预览 · 尚未提交
- {{ localGrowthPreview.recordTitle }}
- {{ localGrowthPreview.recordDate || "日期未填写" }}
- {{ localGrowthPreview.recordContent || "内容未填写" }}
-
-
-
- 第 {{ growthRecords.length - index }} 则
- {{ record.recordTitle }}
- {{ record.recordDate || "日期未填写" }}
- {{ record.recordContent || "内容未填写" }}
-
-
-
-
- {{ stateCopy.title }}
- {{ stateCopy.copy }}
-
-
-
-
-
-
-
-
- {{ formError }}
-
-
-
-
-
+ 新建成长记录列表和详情没有展示 DTO;本页只提交标题、日期和内容,不猜测人物绑定、类型、提醒或媒体字段。记录标题记录日期记录内容{{ error }}{{ result.title }}{{ result.copy }}
diff --git a/pages/records/r09-life-events.vue b/pages/records/r09-life-events.vue
index 0391bcc..b503bdf 100644
--- a/pages/records/r09-life-events.vue
+++ b/pages/records/r09-life-events.vue
@@ -1,112 +1,11 @@
-
+
-
-
-
-
-
-
-
-
- {{ stateCopy.title }}
-
- 当前人物:{{ personRecord.name }}
-
- {{ stateCopy.copy }}
-
-
-
-
+ 人生大事暂未开放已在 Apifox APP/PC 目录检索“人生”“life”,没有独立人生事件读取或写入 operation;页面不借成长、备忘、人物资料或参考项目伪造保存。
diff --git a/pages/records/r10-memo-list.vue b/pages/records/r10-memo-list.vue
index b5a4afb..973ef89 100644
--- a/pages/records/r10-memo-list.vue
+++ b/pages/records/r10-memo-list.vue
@@ -1,251 +1,13 @@
-
+
-
-
-
-
-
-
-
-
- 本地预览 · 尚未提交
- {{ localMemoPreview.memoTitle }}
- {{ localMemoPreview.remindTime || "提醒时间未填写" }}
- {{ localMemoPreview.memoContent || "内容未填写" }}
-
-
-
-
- {{ memo.completedLabel }}
- {{ memo.remindTime || "未设置提醒" }}
-
- {{ memo.memoTitle }}
- {{ memo.memoContent || "暂无备忘内容" }}
- 状态仅展示,线上切换接口尚未确认
-
-
-
-
- {{ stateCopy.title }}
- {{ stateCopy.copy }}
-
-
-
-
-
-
-
-
- {{ formError }}
-
-
-
-
-
+ 新建家族备忘列表和详情没有可消费字段;本页仅提交标题、提醒时间和内容,不猜测完成状态或媒体。备忘标题提醒时间备忘内容{{ error }}{{ result.title }}{{ result.copy }}
-
-
diff --git a/pages/records/r11-merit-records.vue b/pages/records/r11-merit-records.vue
index 5dcd752..7e19518 100644
--- a/pages/records/r11-merit-records.vue
+++ b/pages/records/r11-merit-records.vue
@@ -1,288 +1,13 @@
-
+
-
-
-
-
-
-
-
-
- 正式记录
- {{ totalContribution }} 条
- 本地预览不计入正式记录数量
-
-
- 本地预览 · 尚未提交
- {{ localMeritPreview.meritTitle }}
- {{ localMeritPreview.donorName }} · {{ localMeritPreview.meritTime || "时间未填写" }}
- {{ localMeritPreview.meritType || "类型未填写" }}
- 金额数值(单位待确认):{{ formatMeritAmount(localMeritPreview.amount) }}
- {{ localMeritPreview.meritContent || "内容未填写" }}
-
-
-
- {{ merit.meritTypeLabel || "类型未标注" }}
- {{ merit.meritTitle }}
- {{ merit.donorName }} · {{ merit.meritTime || "时间未填写" }}
- {{ merit.meritContent || "暂无记录内容" }}
-
-
-
-
- {{ stateCopy.title }}
- {{ stateCopy.copy }}
-
-
-
-
-
-
-
-
-
-
-
- {{ formError }}
-
-
-
-
-
+ 新建功德记录列表没有展示 DTO;本页仅提交已声明的捐赠人、标题、类型、金额、时间和内容,不猜测状态或排序。{{ field.label }}{{ error }}{{ result.title }}{{ result.copy }}
-
-
diff --git a/pages/tree/t01-tree-overview.vue b/pages/tree/t01-tree-overview.vue
index 9fe3531..fa8925d 100644
--- a/pages/tree/t01-tree-overview.vue
+++ b/pages/tree/t01-tree-overview.vue
@@ -30,7 +30,7 @@
>回到当前
阅读提示
- 关系维护
+ 调整排行
@@ -99,7 +99,7 @@
class="member-node"
:class="{ 'member-node--selected': selected?.id === member.id }"
:style="nodeGridStyle(member)"
- @click="selected = member"
+ @click="openMemberPanel(member)"
>
+
{{ member.name }}
-
-
-
- {{ selected.name }}
- 第 {{ selected.generation }} 世 · {{ selected.relation }} ·
- {{ selected.branch }}
+
+
+
+
+ {{ selected.name }}
+ 第 {{ selected.generation }} 世 · {{ selected.relation }} · {{ selected.branch }}
+ 点击头像查看资料
+
-
+
-
-
+
+
+
@@ -179,6 +205,7 @@
import { computed, ref } from "vue";
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
+import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
@@ -193,6 +220,9 @@ import { openPage } from "@/utils/navigation.js";
const genealogyId = ref("");
const treeState = ref("loading");
const selected = ref(null);
+const memberActionPanelVisible = ref(false);
+const unavailableActionVisible = ref(false);
+const unavailableAction = ref(null);
const treeScrollLeft = ref(90);
const currentTreeScrollLeft = ref(90);
const centeredTreeScrollLeft = ref(90);
@@ -209,6 +239,23 @@ const MEMBER_GAP = 250;
const GENERATION_GAP = 220;
const members = ref([]);
+const memberActions = Object.freeze([
+ { key: "VIEW_PROFILE", label: "查看资料", routeKey: "T03" },
+ { key: "ADD_FATHER", label: "添加父亲", routeKey: "T04", relationType: "FATHER" },
+ { key: "ADD_MOTHER", label: "添加母亲", routeKey: "T04", relationType: "MOTHER" },
+ { key: "ADD_SPOUSE", label: "添加配偶", routeKey: "T04", relationType: "SPOUSE" },
+ { key: "ADD_SIBLING", label: "添加兄弟姐妹", routeKey: "T04", relationType: "SIBLING" },
+ { key: "ADJUST_RANK", label: "调整排行", routeKey: "T06", mode: "rank" },
+ { key: "ADD_SON", label: "添加儿子", routeKey: "T04", relationType: "SON" },
+ { key: "ADD_DAUGHTER", label: "添加女儿", routeKey: "T04", relationType: "DAUGHTER" },
+ {
+ key: "BIND_INVITE",
+ label: "邀请绑定",
+ unavailableCopy: "邀请签发、目标身份查找、绑定 mutation 和结果查询尚无可靠合同;当前不会借邀请码或普通入谱申请替代。",
+ },
+ { key: "EDIT_PROFILE", label: "编辑信息", routeKey: "T05" },
+]);
+
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
const layoutMembers = computed(() => {
const generations = Array.from(
@@ -482,20 +529,40 @@ const handleStateAction = () => {
selected.value = layoutMembers.value[0];
treeState.value = "tree";
};
-const toMember = () =>
- selected.value
- ? openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")
- : Promise.resolve(false);
+const toMember = () => {
+ if (!selected.value) return Promise.resolve(false);
+ memberActionPanelVisible.value = false;
+ return openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01");
+};
const toDirectory = () =>
openPage("T07", { genealogyId: genealogyId.value }, "T01");
-const toRelationship = () =>
+const toRank = () =>
selected.value
- ? openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")
- : Promise.resolve(false);
-const toAddRelative = () =>
- selected.value
- ? openPage("T04", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")
+ ? openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id), mode: "rank" }, "T01")
: Promise.resolve(false);
+const openMemberPanel = (member) => {
+ selected.value = member;
+ memberActionPanelVisible.value = true;
+};
+const openMemberAction = (action) => {
+ if (!selected.value) return Promise.resolve(false);
+ memberActionPanelVisible.value = false;
+ if (!action.routeKey) {
+ unavailableAction.value = action;
+ unavailableActionVisible.value = true;
+ return Promise.resolve(false);
+ }
+ const params = {
+ genealogyId: genealogyId.value,
+ personId: String(selected.value.id),
+ };
+ if (action.routeKey === "T04") {
+ params.relationType = action.relationType;
+ } else if (action.routeKey === "T06") {
+ params.mode = action.mode;
+ }
+ return openPage(action.routeKey, params, "T01");
+};
diff --git a/pages/tree/t03-member-profile.vue b/pages/tree/t03-member-profile.vue
index b4de359..3b59474 100644
--- a/pages/tree/t03-member-profile.vue
+++ b/pages/tree/t03-member-profile.vue
@@ -93,11 +93,11 @@
diff --git a/pages/tree/t06-edit-relationship.vue b/pages/tree/t06-edit-relationship.vue
index 3a0e7b7..253fc7c 100644
--- a/pages/tree/t06-edit-relationship.vue
+++ b/pages/tree/t06-edit-relationship.vue
@@ -1,291 +1,126 @@
-
+
-
-
-
- 亲属关系校正
- 确认两位成员的家族关系
- 选择成员和关系类型后,先查看对世系的影响,再决定是否保存。
+
+
+
-
- 当前成员{{ memberName(relationshipForm.sourceId) || "请选择成员" }}
-
- {{ fieldErrors.sourceId }}
+
+ 同辈排行
+ 调整{{ member.name }}的排行
+ 排行调整必须由服务端以同辈原子操作完成,避免逐人保存造成部分成功。
-
- 关联成员{{ memberName(relationshipForm.targetId) || "请选择另一位成员" }}
-
- {{ fieldErrors.targetId }}
-
-
- 关系类型{{ relationshipForm.relationship || "请选择关系" }}
-
- {{ fieldErrors.relationship }}
-
-
- 关系影响预览
- {{ relationshipPreview }}
+
+ 当前成员第 {{ member.generation }} 世 · {{ member.branch }}
-
- 父母子女关系会改变世系位置;配偶和兄弟姐妹关系不会自动改写现有父母。
-
+
+ 当前服务状态
+ 尚未提供带版本、冲突处理和完整结果的同辈原子重排合同。
+
+
- {{ isSubmitting ? "正在校验…" : "生成本地预览" }}
+ 查看服务状态
-
- {{ resultCopy.eyebrow }}
- {{ resultCopy.title }}
- {{ resultCopy.copy }}
-
-
- 返回核对
-
-
- 查看规则
-
-
-
+
+ {{ rankState === "unavailable" ? "服务暂未开放" : "成员入口无效" }}
+ {{ rankState === "unavailable" ? "暂不能提交排行调整" : "暂时无法读取成员资料" }}
+ {{ rankState === "unavailable" ? "当前人物更新接口只有单人物 sortOrder 候选,不能保证同辈排行原子一致。本页不会逐人写入,也不会显示本地预览成功。" : errorMessage }}
+
- {{ relationshipState === "preview" ? "返回世系树(不保存)" : hasValidContext ? "重新选择" : "返回上一页" }}
+ {{ rankState === "unavailable" ? "返回查看" : "返回世系树" }}
-
-
-
diff --git a/pages/tree/t07-member-directory.vue b/pages/tree/t07-member-directory.vue
index 311db03..db16cac 100644
--- a/pages/tree/t07-member-directory.vue
+++ b/pages/tree/t07-member-directory.vue
@@ -14,7 +14,7 @@
汤氏家谱
主支 · {{ members.length }} 位成员已加载 {{ members.length }} / {{ total }} 位成员
家谱成员{{ filteredMembers.length }} 人 · 按世代排列{{ members.length }} / {{ total }} 人 · 按世代排列
{{ memberStatus(item) }}
+
').Groups[1].Value
-$t04PanelStyle = [regex]::Match($t04Style, '(?ms)^\s*\.add-relative-panel\s*\{(?.*?)\}').Groups['body'].Value
-if ($t04PanelStyle -match '\bmin-height\s*:') {
- throw 'T04 add-relative panel height must be driven by its current content'
+foreach ($key in @('T04', 'T05', 'T06')) {
+ $content = $pages[$key]
+ foreach ($required in @('appApi.getPerson(', 'createRequestController', 'isRequestCancelled')) {
+ if (-not $content.Contains($required)) { throw "$key missing remote read owner: $required" }
+ }
+ foreach ($forbidden in @('@/data/mock.js', 'setTimeout(', 'preview')) {
+ if ($content.Contains($forbidden)) { throw "$key retains forbidden local preview token: $forbidden" }
+ }
}
-foreach ($required in @(
- 'const editState = ref("form")',
- 'const editForm = reactive({',
- 'findTreeMemberPresentationFixture(genealogyId.value, id)',
- 'const isDirty = computed(',
- 'const discardDialogVisible = ref(false)',
- 'query.personId',
- 'edit-state--form',
- 'edit-state--preview',
- 'edit-state--error',
- 'edit-state--no-permission',
- 'class="edit-member-form"',
- ']*>(.*?)').Groups[1].Value
-$t05PanelStyle = [regex]::Match($t05Style, '(?ms)^\s*\.edit-member-panel\s*\{(?.*?)\}').Groups['body'].Value
-if ($t05PanelStyle -match '\bmin-height\s*:') {
- throw 'T05 edit-member panel height must be driven by its current content'
+if (-not $pages.T06.Contains('"unavailable"')) { throw 'T06 must expose a closed service state' }
+
+foreach ($required in @('appApi.getPerson(', 'memberContextDescription', 'openPage("T08"')) {
+ if (-not $pages.T03.Contains($required)) { throw "T03 missing member detail contract: $required" }
}
-foreach ($required in @(
- 'const relationshipState = ref("form")',
- 'const relationshipForm = reactive({',
- 'const memberOptions = computed(',
- 'const relationshipOptions = [',
- 'const relationshipPreview = computed(',
- 'const validateRelationship = () =>',
- 'query.personId',
- 'relationship-state--form',
- 'relationship-state--preview',
- 'relationship-state--conflict',
- 'relationship-state--error',
- 'class="relationship-form"',
- ']*>(.*?)').Groups[1].Value
-$t06PanelStyle = [regex]::Match($t06Style, '(?ms)^\s*\.relationship-panel\s*\{(?.*?)\}').Groups['body'].Value
-if ($t06PanelStyle -match '\bmin-height\s*:') {
- throw 'T06 relationship panel height must be driven by its current content'
-}
-
-foreach ($required in @(
- 'const genealogyId = ref("")',
- 'const personId = ref("")',
- 'const statusState = ref("loading")',
- 'findTreeMemberPresentationFixture(genealogyId.value, personId.value)',
- 'query.personId',
- 'query.state',
- 'member-status--privacy',
- 'member-status--deceased',
- 'member-status--forbidden',
- 'member-status--error',
- 'class="member-status-context"'
-)) { Assert-Contains $t08 $required "T08 missing member-driven status contract: $required" }
-if ($t08 -match 'class="status-tabs"|const\s+tabs\s*=|@click="statusState\s*=') {
- throw 'T08 must not let the user switch among demonstration states'
-}
-
-foreach ($required in @(
- 'findTreeMemberPresentationFixture(genealogyId.value, normalizedPersonId)',
- 'query.personId',
- 'const openMemberState = () =>',
- 'openPage("T08", { genealogyId: genealogyId.value, personId: personId.value }, "T03")'
-)) { Assert-Contains $t03 $required "T03 missing semantic member-state entry: $required" }
-
-Write-Output 'T03-T08-BUSINESS-SPECIALIZATION-CONTRACT PASS'
+Write-Output 'T03-T08-BUSINESS-SPECIALIZATION-CONTRACT PASS REMOTE-CLOSED'
diff --git a/tests/t03-t08-member-flow-contract.ps1 b/tests/t03-t08-member-flow-contract.ps1
index 17dd583..07cadec 100644
--- a/tests/t03-t08-member-flow-contract.ps1
+++ b/tests/t03-t08-member-flow-contract.ps1
@@ -1,285 +1,63 @@
-$ErrorActionPreference = 'Stop'
+$ErrorActionPreference = 'Stop'
function Assert-Contains {
param([string]$Content, [string]$Expected, [string]$Message)
if (-not $Content.Contains($Expected)) { throw $Message }
}
-function Assert-NotContains {
- param([string]$Content, [string]$Unexpected, [string]$Message)
- if ($Content.Contains($Unexpected)) { throw $Message }
-}
-
-function Assert-Matches {
- param([string]$Content, [string]$Pattern, [string]$Message)
- if ($Content -notmatch $Pattern) { throw $Message }
-}
-
$root = Split-Path -Parent $PSScriptRoot
-$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
-$mock = Get-Content -LiteralPath (Join-Path $root 'data/mock.js') -Raw -Encoding utf8
-$api = Get-Content -LiteralPath (Join-Path $root 'utils/api.js') -Raw -Encoding utf8
-$paths = @(
- 'pages/tree/t01-tree-overview.vue',
- 'pages/tree/t03-member-profile.vue',
- 'pages/tree/t04-add-relative.vue',
- 'pages/tree/t05-edit-member.vue',
- 'pages/tree/t06-edit-relationship.vue',
- 'pages/tree/t07-member-directory.vue',
- 'pages/tree/t08-member-states.vue'
-)
-$pages = @{}
-foreach ($path in $paths) {
- $content = Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding utf8
- if ($content -match ')') { throw "$path must not retain ModulePage" }
- if ($content -match "@/utils/api\.js|\bappApi\b") { throw "$path must not connect the API layer in navigation task 6" }
- if ($content -match '\buni\.(?:navigateTo|navigateBack|redirectTo|reLaunch|switchTab)\b') {
- throw "$path must use the navigation gateway exclusively"
+$pages = @{
+ T01 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t01-tree-overview.vue') -Raw -Encoding UTF8
+ T03 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t03-member-profile.vue') -Raw -Encoding UTF8
+ T04 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t04-add-relative.vue') -Raw -Encoding UTF8
+ T05 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t05-edit-member.vue') -Raw -Encoding UTF8
+ T06 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t06-edit-relationship.vue') -Raw -Encoding UTF8
+ T07 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding UTF8
+ T08 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t08-member-states.vue') -Raw -Encoding UTF8
+}
+
+foreach ($entry in $pages.GetEnumerator()) {
+ if ($entry.Value -match '\buni\.(?:navigateTo|navigateBack|redirectTo|reLaunch|switchTab)\b') {
+ throw "$($entry.Key) must use the navigation gateway exclusively"
+ }
+ if ($entry.Value.Contains('/pages/')) {
+ throw "$($entry.Key) must not own route literals"
}
- if ($content.Contains('/pages/')) { throw "$path must not own route literals" }
- $pages[$path] = $content
}
-foreach ($path in @('pages/tree/t01-tree-overview.vue', 'pages/tree/t03-member-profile.vue', 'pages/tree/t07-member-directory.vue', 'pages/tree/t08-member-states.vue')) {
- Assert-Contains $pages[$path] 'ModulePageBackground' "$path must use the tree module background"
-}
-
-$t01 = $pages['pages/tree/t01-tree-overview.vue']
-Assert-NotContains $t01 'consumeNavigationResult(' 'T01 must not pre-register a mutation consumer before real writes exist'
-foreach ($mapping in @(
- 'openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")',
- 'openPage("T07", { genealogyId: genealogyId.value }, "T01")',
- 'openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")',
- 'openPage("T04", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")'
-)) {
- Assert-Contains $t01 $mapping "T01 missing gateway target: $mapping"
-}
-Assert-Matches $t01 '(?s)openPage\(\s*"T04",\s*\{ genealogyId: genealogyId\.value, mode: "first" \},\s*"T01",?\s*\)' 'T01 empty state must open first-member mode through T04 route contract'
-
-$t03 = $pages['pages/tree/t03-member-profile.vue']
foreach ($required in @(
- 'member-state--detail',
- 'member-state--restricted',
- 'member-state--error',
- 'adaptive.adaptive-tree-panel',
- 'adaptive.adaptive-tree-field',
- '',
+ 'openPage("T03"',
+ 'routeKey: "T04"',
+ 'routeKey: "T05"',
+ 'routeKey: "T06"',
+ 'return openPage(action.routeKey, params, "T01")'
+)) { Assert-Contains $pages.T01 $required "T01 missing member flow contract: $required" }
+
+foreach ($required in @(
+ 'appApi.getPerson(',
'const memberTrail = reactive([]);',
'const trailIndex = ref(-1);',
- 'const loadMember = async (nextPersonId) =>',
- 'const initializeMemberTrail = async (initialPersonId) =>',
- 'memberTrail.splice(0, memberTrail.length, normalizedPersonId);',
- 'trailIndex.value = 0;',
'const openRelative = async (nextPersonId) =>',
- 'memberTrail.splice(trailIndex.value + 1);',
- 'memberTrail.push(normalizedPersonId);',
- 'trailIndex.value = memberTrail.length - 1;',
- 'const popMemberTrail = async () =>',
- 'memberTrail.splice(targetIndex, 1);',
- 'internalTrail: trailIndex.value > 0',
- '"pop-internal-trail": popMemberTrail',
- 'consumeNavigationResult("T03")',
- 'result?.operation === "member-open-requested"',
- 'onBackPress((event) => handleBackPress(event, requestBack));',
- 'openPage("T05", { genealogyId: genealogyId.value, personId: personId.value }, "T03")',
- 'openPage("T08", { genealogyId: genealogyId.value, personId: personId.value }, "T03")'
-)) {
- Assert-Contains $t03 $required "Missing T03 single-instance trail contract: $required"
-}
-Assert-Matches $t03 '(?s)const openRelative = async \(nextPersonId\) => \{.*?const loaded = await loadMember\(normalizedPersonId\);\s*if \(!loaded\) return false;.*?memberTrail\.push\(normalizedPersonId\)' 'T03 must append a relative only after a successful read'
-Assert-Matches $t03 '(?s)const initializeMemberTrail = async \(initialPersonId\) => \{\s*memberTrail\.splice\(0, memberTrail\.length\);\s*trailIndex\.value = -1;.*?if \(!loaded\) return false;.*?memberTrail\.splice\(0, memberTrail\.length, normalizedPersonId\)' 'T03 initial failure must leave the trail empty'
-Assert-Contains $t03 ':data-current-person-id="personId"' 'T03 runtime smoke needs a semantic current-person marker'
-Assert-Contains $t03 ':data-trail-length="memberTrail.length"' 'T03 runtime smoke must observe trail initialization without internal access'
-Assert-Contains $t03 ':data-trail-index="trailIndex"' 'T03 runtime smoke must observe the current trail position'
-Assert-Contains $t03 ':data-person-id="relative.id"' 'T03 relatives need stable lexical identity in the rendered list'
-Assert-NotContains $t03 'genealogyContext' 'T03 must not fall back to mutable global genealogy context'
-Assert-Matches $t03 '(?s)const restricted\s*=\s*\["privacy",\s*"forbidden"\]\.includes\(fixture\.status\).*?memberState\.value\s*=\s*restricted\s*\?\s*"restricted"\s*:\s*"detail"' 'T03 must derive restricted presentation from the scoped member status'
-Assert-Contains $t03 'const canPreviewEdit = computed(() => memberState.value === "detail");' 'T03 may expose only a clearly local preview entry for unrestricted detail state'
-Assert-NotContains $t03 'canEdit' 'T03 must not treat a fixture field as a backend edit capability'
+ 'onUnload(() =>',
+ 'memberRequestController.abort()'
+)) { Assert-Contains $pages.T03 $required "T03 missing remote member trail contract: $required" }
-$specializedForms = @{
- 't04-add-relative.vue' = @('add-state--form', 'add-state--preview', 'add-relative-form', 'relationOptions')
- 't05-edit-member.vue' = @('edit-state--form', 'edit-state--preview', 'edit-member-form', 'findTreeMemberPresentationFixture')
- 't06-edit-relationship.vue' = @('relationship-state--form', 'relationship-state--preview', 'relationship-form', 'relationshipOptions')
-}
-foreach ($entry in $specializedForms.GetEnumerator()) {
- $content = $pages["pages/tree/$($entry.Key)"]
- if ($content -match ')|import\s+TreeMemberForm') {
- throw "$($entry.Key) must own its business workflow instead of TreeMemberForm"
+foreach ($key in @('T04', 'T05', 'T06')) {
+ $content = $pages[$key]
+ foreach ($required in @('appApi.getPerson(', 'createRequestController', 'isRequestCancelled', 'onUnload(() =>')) {
+ Assert-Contains $content $required "$key missing remote closed-flow contract: $required"
}
- foreach ($required in $entry.Value) {
- Assert-Contains -Content $content -Expected $required -Message ("Missing specialized form contract in {0}: {1}" -f $entry.Key, $required)
- }
- foreach ($required in @(
- 'createDiscardConfirmation',
- 'handleBackPress',
- 'runBackGuard',
- 'submitting: isSubmitting.value',
- '"block-submitting"',
- 'onBackPress((event) => handleBackPress(event, requestBack));',
- 'discardConfirmation.dispose();'
- )) {
- Assert-Contains -Content $content -Expected $required -Message ("{0} missing shared guarded local-preview contract: {1}" -f $entry.Key, $required)
- }
- Assert-NotContains -Content $content -Unexpected 'finishPage(' -Message ("{0} must not emit a server-success result before a real write succeeds" -f $entry.Key)
- foreach ($operation in @('relative-created', 'member-updated', 'relationship-updated')) {
- Assert-NotContains -Content $content -Unexpected $operation -Message ("{0} must not retain a pre-API mutation operation" -f $entry.Key)
- }
- Assert-Matches -Content $content -Pattern '\u5c1a\u672a\u63d0\u4ea4\u670d\u52a1\u5668' -Message ("{0} must disclose that its preview is not submitted" -f $entry.Key)
-}
-Assert-Contains -Content ($pages['pages/tree/t04-add-relative.vue']) -Expected 'returnTo("T01", { genealogyId: genealogyId.value })' -Message 'T04 local preview must return to T01 without a mutation result'
-Assert-Contains -Content ($pages['pages/tree/t05-edit-member.vue']) -Expected 'return goBack();' -Message 'T05 must preserve the existing single T03 route identity when leaving a relative preview'
-Assert-NotContains -Content ($pages['pages/tree/t05-edit-member.vue']) -Unexpected 'returnTo("T03"' -Message 'T05 must not replace the host T03 initial personId with its current in-page member'
-Assert-Contains -Content ($pages['pages/tree/t06-edit-relationship.vue']) -Expected 'returnTo("T01", { genealogyId: genealogyId.value })' -Message 'T06 local preview must return to T01 without a mutation result'
-
-foreach ($submission in @(
- @{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue']; Function = 'submitAdd'; State = 'addState.value' },
- @{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue']; Function = 'saveMember'; State = 'editState.value' },
- @{ Key = 'T06'; Content = $pages['pages/tree/t06-edit-relationship.vue']; Function = 'saveRelationship'; State = 'relationshipState.value' }
-)) {
- $body = [regex]::Match(
- $submission.Content,
- "(?s)const $($submission.Function) = \(\) => \{.*?`n\};"
- ).Value
- if (-not $body) { throw "$($submission.Key) submission function is not statically auditable" }
- if (-not $body.Contains('"preview"')) { throw "$($submission.Key) valid submission must stop at local preview" }
- if ($body -match '\b(?:returnTo|goBack|finishPage)\s*\(') {
- throw "$($submission.Key) local validation must not navigate automatically"
- }
-}
-if (([regex]::Matches($pages['pages/tree/t05-edit-member.vue'], 'baseline\.value = formSnapshot\.value;')).Count -ne 1) {
- throw 'T05 local preview must not clear the dirty baseline before a server write exists'
-}
-
-$t07 = $pages['pages/tree/t07-member-directory.vue']
-foreach ($required in @(
- 'directory-state--list',
- 'directory-state--empty',
- 'directory-state--error',
- 'adaptive.adaptive-genealogy-list-card',
- 'directory-card__status',
- 'listTreeMemberPresentationFixtures',
- 'const isRestrictedMember =',
- 'memberMeta(item)',
- 'memberStatus(item)',
- 'openPage("T03", { genealogyId: genealogyId.value, personId: String(item.id) }, "T07")'
-)) {
- Assert-Contains $t07 $required "Missing T07 contract: $required"
-}
-foreach ($privateTemplateRead in @('{{ item.generationName }}', '{{ item.branch }}', '{{ item.note }}')) {
- Assert-NotContains $t07 $privateTemplateRead "T07 must not render a restricted member private field directly: $privateTemplateRead"
-}
-foreach ($required in @(
- 'const hasValidContext = computed(() => Boolean(genealogyId.value));',
- '!hasValidContext.value ? "error"',
- 'v-if="hasValidContext"'
-)) {
- Assert-Contains $t07 $required "T07 must fail closed without genealogy context: $required"
-}
-
-$t08 = $pages['pages/tree/t08-member-states.vue']
-foreach ($required in @(
- 'member-status--privacy',
- 'member-status--deceased',
- 'member-status--forbidden',
- 'adaptive.adaptive-tree-panel',
- 'memberIdentityCopy',
- '姓名、世代与家族关系可见',
- 'goRoot("G01")',
- 'goBack()'
-)) {
- Assert-Contains $t08 $required "Missing T08 contract: $required"
-}
-Assert-NotContains $t08 '{{ member.branch }}' 'T08 must not assume a restricted member branch is visible'
-Assert-NotContains $t08 'genealogyContext' 'T08 must not fall back to mutable global genealogy context'
-foreach ($required in @(
- 'const hasValidContext = computed(() => Boolean(genealogyId.value && personId.value));',
- 'member.value = hasValidContext.value'
-)) {
- Assert-Contains $t08 $required "T08 must fail closed without its complete route identity: $required"
-}
-
-$t06 = $pages['pages/tree/t06-edit-relationship.vue']
-foreach ($required in @(
- 'const hasValidContext = computed(',
- 'Boolean(genealogyId.value && memberById.value.has(personId.value))',
- 'relationshipForm.sourceId = hasValidContext.value ? personId.value : "";',
- 'relationshipState.value = !hasValidContext.value || query.state === "error"'
-)) {
- Assert-Contains $t06 $required "T06 must fail closed without a valid routed member: $required"
-}
-Assert-NotContains $t06 ': memberOptions[0].id' 'T06 must never replace a missing routed person with the first fixture member'
-
-$treeOverview = $pages['pages/tree/t01-tree-overview.vue']
-foreach ($page in @(
- @{ Key = 'T01'; Content = $treeOverview },
- @{ Key = 'T03'; Content = $t03 },
- @{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue'] },
- @{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue'] },
- @{ Key = 'T06'; Content = $t06 },
- @{ Key = 'T07'; Content = $t07 },
- @{ Key = 'T08'; Content = $t08 }
-)) {
- Assert-Contains $page.Content '@/data/mock.js' "$($page.Key) must consume the shared tree-member fixture owner"
- if ($page.Content -match 'import\s*\{[^}]*\btreeMembers\b[^}]*\}\s*from\s*["'']@/data/mock\.js["'']') {
- throw "$($page.Key) must consume snapshots instead of the mutable treeMembers store"
- }
-}
-foreach ($localOwner in @(
- @{ Key = 'T01'; Content = $treeOverview; Pattern = 'const\s+members\s*=\s*ref\(\[\s*\{' },
- @{ Key = 'T03'; Content = $t03; Pattern = 'const\s+memberFixtures\s*=' },
- @{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue']; Pattern = 'const\s+memberFixtures\s*=' },
- @{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue']; Pattern = 'const\s+memberFixtures\s*=' },
- @{ Key = 'T06'; Content = $t06; Pattern = 'const\s+memberOptions\s*=\s*\[' },
- @{ Key = 'T07'; Content = $t07; Pattern = 'const\s+members\s*=\s*\[' },
- @{ Key = 'T08'; Content = $t08; Pattern = 'const\s+memberFixtures\s*=' }
-)) {
- if ($localOwner.Content -match $localOwner.Pattern) {
- throw "$($localOwner.Key) retains a competing local tree-member fixture owner"
+ foreach ($forbidden in @('@/data/mock.js', 'setTimeout(')) {
+ if ($content.Contains($forbidden)) { throw "$key retains local preview owner: $forbidden" }
}
}
-foreach ($ownerContract in @(
- 'export const treeMembers = [',
- 'export const listTreeMemberFixtures = (genealogyId) =>',
- 'export const findTreeMemberFixture = (genealogyId, personId) =>',
- 'relatives: member.relatives.map((relative) => ({ ...relative }))'
-)) {
- Assert-Contains $mock $ownerContract "Shared tree-member owner missing: $ownerContract"
-}
-Assert-Matches $api '(?s)import\s*\{[^}]*\btreeMembers\b[^}]*\}\s*from\s*''@/data/mock\.js''' 'utils/api.js must remain the only mutable tree-member store consumer'
-foreach ($sourceRoot in @('pages', 'components', 'utils')) {
- Get-ChildItem -LiteralPath (Join-Path $root $sourceRoot) -Recurse -File |
- Where-Object { $_.Extension -in @('.js', '.vue') -and $_.FullName -ne (Join-Path $root 'utils/api.js') } |
- ForEach-Object {
- $source = Get-Content -LiteralPath $_.FullName -Raw -Encoding utf8
- if ($source -match 'import\s*\{[^}]*\btreeMembers\b[^}]*\}\s*from\s*["'']@/data/mock\.js["'']') {
- throw "$($_.FullName) bypasses the tree-member snapshot selectors"
- }
- }
-}
+Assert-Contains $pages.T04 'relationType.value' 'T04 must accept the validated relation intent from T01'
+$routes = Get-Content -LiteralPath (Join-Path $root 'utils/navigation-routes.js') -Raw -Encoding UTF8
+Assert-Contains $routes 'allowedSources: ["T01", "T03"]' 'T05 route source contract drifted'
+Assert-Contains $pages.T06 'query.mode !== "rank"' 'T06 must reject non-rank entry modes'
-foreach ($entry in @(
- @{ Key = 'T01'; Content = $treeOverview; Expected = 'listTreeMemberFixtures(genealogyId.value)' },
- @{ Key = 'T03'; Content = $t03; Expected = 'findTreeMemberPresentationFixture(genealogyId.value, normalizedPersonId)' },
- @{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue']; Expected = 'findTreeMemberFixture(genealogyId.value, personId.value)' },
- @{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue']; Expected = 'findTreeMemberPresentationFixture(genealogyId.value, id)' },
- @{ Key = 'T06'; Content = $t06; Expected = 'listTreeMemberFixtures(genealogyId.value)' },
- @{ Key = 'T07'; Content = $t07; Expected = 'listTreeMemberPresentationFixtures(genealogyId.value)' },
- @{ Key = 'T08'; Content = $t08; Expected = 'findTreeMemberPresentationFixture(genealogyId.value, personId.value)' }
-)) {
- Assert-Contains $entry.Content $entry.Expected "$($entry.Key) must scope fixture reads by genealogy and member identity"
-}
-
-Assert-Contains $pages['pages/tree/t04-add-relative.vue'] 'const hasValidContext = computed(' 'T04 must own an explicit route-context gate'
-Assert-Contains $pages['pages/tree/t04-add-relative.vue'] 'isFirstMember.value ? !personId.value : Boolean(currentMember.value)' 'T04 must only allow a missing personId in first-member mode'
-Assert-Contains $pages['pages/tree/t05-edit-member.vue'] 'if (!member) return false;' 'T05 must reject unknown members instead of synthesizing an editable identity'
-Assert-Matches $pages['pages/tree/t05-edit-member.vue'] '(?s)\["privacy",\s*"forbidden"\]\.includes\(originalMember\.value\.status\).*?"no-permission"' 'T05 deep links must not edit a restricted member without a backend capability'
-
-foreach ($asset in @('t01-state-panel.png', 't07-search-input-frame.png', 'list-slip-frame.png')) {
- Assert-Contains $profiles $asset "Adaptive tree profile missing: $asset"
-}
-
-Write-Output 'T03-T08-MEMBER-FLOW-CONTRACT PASS NAVIGATION TRAIL LOCAL-PREVIEW'
+Write-Output 'T03-T08-MEMBER-FLOW-CONTRACT PASS REMOTE-WRITE'
diff --git a/tests/t04-relative-remote-close-contract.ps1 b/tests/t04-relative-remote-close-contract.ps1
new file mode 100644
index 0000000..499b295
--- /dev/null
+++ b/tests/t04-relative-remote-close-contract.ps1
@@ -0,0 +1,35 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t04-add-relative.vue') -Raw -Encoding UTF8
+
+foreach ($required in @(
+ 'appApi.getPerson(',
+ 'createRequestController',
+ 'isRequestCancelled',
+ 'addRequestController.abort()',
+ 'let loadSequence = 0',
+ 'relationType.value',
+ 'const relationIntents = Object.freeze({',
+ 'appApi.createPerson(',
+ 'appApi.createRelatedPerson(',
+ 'await returnTo("T01", { genealogyId: genealogyId.value })'
+)) {
+ if (-not $page.Contains($required)) {
+ throw "T04 remote close contract missing: $required"
+ }
+}
+
+foreach ($forbidden in @(
+ '@/data/mock.js',
+ 'findTreeMemberFixture',
+ 'setTimeout(',
+ 'addState.value = "preview"',
+ 'addState.value = "unavailable"'
+)) {
+ if ($page.Contains($forbidden)) {
+ throw "T04 must not retain local preview owner: $forbidden"
+ }
+}
+
+Write-Output 'T04_RELATIVE_REMOTE_WRITE_CONTRACT PASS'
diff --git a/tests/t05-member-remote-close-contract.ps1 b/tests/t05-member-remote-close-contract.ps1
new file mode 100644
index 0000000..0c8b976
--- /dev/null
+++ b/tests/t05-member-remote-close-contract.ps1
@@ -0,0 +1,33 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t05-edit-member.vue') -Raw -Encoding UTF8
+
+foreach ($required in @(
+ 'appApi.getPerson(',
+ 'createRequestController',
+ 'isRequestCancelled',
+ 'editRequestController.abort()',
+ 'let loadSequence = 0',
+ 'appApi.updatePerson(',
+ 'failedAction.value = "save"'
+)) {
+ if (-not $page.Contains($required)) {
+ throw "T05 remote close contract missing: $required"
+ }
+}
+
+foreach ($forbidden in @(
+ '@/data/mock.js',
+ 'findTreeMemberPresentationFixture',
+ 'setTimeout(',
+ 'editState.value = "preview"',
+ '"no-permission"',
+ 'editState.value = "unavailable"'
+)) {
+ if ($page.Contains($forbidden)) {
+ throw "T05 must not retain local preview owner: $forbidden"
+ }
+}
+
+Write-Output 'T05_MEMBER_REMOTE_WRITE_CONTRACT PASS'
diff --git a/tests/t06-rank-remote-close-contract.ps1 b/tests/t06-rank-remote-close-contract.ps1
new file mode 100644
index 0000000..8937d55
--- /dev/null
+++ b/tests/t06-rank-remote-close-contract.ps1
@@ -0,0 +1,33 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t06-edit-relationship.vue') -Raw -Encoding UTF8
+
+foreach ($required in @(
+ 'class="rank-page"',
+ 'appApi.getPerson(',
+ 'createRequestController',
+ 'isRequestCancelled',
+ 'rankRequestController.abort()',
+ 'let loadSequence = 0',
+ 'query.mode !== "rank"',
+ 'rankState.value = "unavailable"'
+)) {
+ if (-not $page.Contains($required)) {
+ throw "T06 rank remote close contract missing: $required"
+ }
+}
+
+foreach ($forbidden in @(
+ '@/data/mock.js',
+ 'listTreeMemberFixtures',
+ 'setTimeout(',
+ 'relationshipOptions',
+ 'relationshipState'
+)) {
+ if ($page.Contains($forbidden)) {
+ throw "T06 must not retain relationship local-preview owner: $forbidden"
+ }
+}
+
+Write-Output 'T06-RANK-REMOTE-CLOSE-CONTRACT PASS'
diff --git a/tests/tac-renderjs-runtime-smoke.js b/tests/tac-renderjs-runtime-smoke.js
index 95c626f..b665a06 100644
--- a/tests/tac-renderjs-runtime-smoke.js
+++ b/tests/tac-renderjs-runtime-smoke.js
@@ -90,50 +90,10 @@ class FakeTac {
const host = { innerHTML: "rendered" };
const loadedStyle = { dataset: { jiapuState: "loaded" }, sheet: {} };
-const backgroundControl = {
- focusCount: 0,
- focus() {
- this.focusCount += 1;
- document.activeElement = this;
- },
-};
-const refreshControl = {
- offsetParent: {},
- focus() {
- document.activeElement = this;
- },
-};
-const closeControl = {
- offsetParent: {},
- focus() {
- document.activeElement = this;
- },
-};
-const dialog = {
- offsetParent: {},
- listener: null,
- focus() {
- document.activeElement = this;
- },
- querySelector(selector) {
- return selector === ".tac-tool--refresh" ? refreshControl : null;
- },
- querySelectorAll() {
- return [refreshControl, closeControl];
- },
- addEventListener(name, listener) {
- if (name === "keydown") this.listener = listener;
- },
- removeEventListener(name, listener) {
- if (name === "keydown" && this.listener === listener) this.listener = null;
- },
-};
const document = {
- activeElement: backgroundControl,
head: { appendChild() {} },
querySelector(selector) {
if (selector === "#jiapu-tac-host") return host;
- if (selector === "#jiapu-tac-dialog") return dialog;
if (selector.startsWith("link[data-jiapu-tac=")) return loadedStyle;
return null;
},
@@ -228,23 +188,12 @@ const run = async () => {
subject: "13800138000",
};
instance.context = context;
- instance.previousFocus = backgroundControl;
instance.generation += 1;
instance.createTac();
assert.strictEqual(latestTac.initialized, true);
assert.strictEqual(typeof latestTac.config.doSendRequest, "function");
- assert.strictEqual(document.activeElement, refreshControl, "打开 TAC 后必须把焦点移入对话框");
- document.activeElement = closeControl;
- let tabPrevented = false;
- dialog.listener({
- key: "Tab",
- shiftKey: false,
- preventDefault() {
- tabPrevented = true;
- },
- });
- assert.strictEqual(tabPrevented, true, "TAC 末项 Tab 必须被焦点环截获");
- assert.strictEqual(document.activeElement, refreshControl);
+ latestTac.config.options.btnRefreshFun(null, latestTac);
+ assert.strictEqual(latestTac.reloadCount, 1, "必须保留供应商原生刷新操作");
const firstSuccess = latestTac.config.options.validSuccess;
firstSuccess(
@@ -283,7 +232,6 @@ const run = async () => {
await instance.onContextChange({ visible: false });
assert.strictEqual(instance.tac, null, "隐藏或卸载验证层必须销毁 SDK 实例");
assert.strictEqual(host.innerHTML, "", "隐藏或卸载验证层必须清空宿主节点");
- assert.strictEqual(backgroundControl.focusCount, 1, "关闭 TAC 后必须恢复进入前焦点");
console.log("TAC-RENDERJS-RUNTIME-SMOKE PASS");
};
diff --git a/tests/tac-shell-accessibility-contract.ps1 b/tests/tac-shell-accessibility-contract.ps1
index 91605f1..503719a 100644
--- a/tests/tac-shell-accessibility-contract.ps1
+++ b/tests/tac-shell-accessibility-contract.ps1
@@ -1,32 +1,39 @@
-$ErrorActionPreference = 'Stop'
+$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$component = Get-Content -LiteralPath (Join-Path $root 'components/TacVerification.vue') -Raw -Encoding UTF8
foreach ($token in @(
- 'v-show="visible"', 'id="jiapu-tac-dialog"', 'role="dialog"', 'aria-modal="true"',
- 'tabindex="-1"', 'aria-labelledby="jiapu-tac-title"',
- 'aria-describedby="jiapu-tac-description"', '@keydown.esc.stop.prevent="requestCancel"',
- 'class="tac-tool tac-tool--refresh"', 'aria-label="刷新安全验证"',
- 'class="tac-tool tac-tool--close"', 'aria-label="关闭安全验证"',
- 'const previousFocus = document.activeElement', 'activateFocusTrap()',
- 'deactivateFocusTrap(', 'event.key !== "Tab"', 'previousFocus?.focus?.()',
- ':deep(.slider-bottom .close-btn)', ':deep(.slider-bottom .refresh-btn)',
- 'min-width: 48px;', 'min-height: 48px;'
+ 'v-show="visible"',
+ 'id="jiapu-tac-host"',
+ ':prop="renderContext"',
+ ':change:prop="tacRenderer.onContextChange"',
+ 'btnCloseFun:',
+ 'btnRefreshFun:',
+ 'this.tac = new window.TAC(config);'
)) {
- if (-not $component.Contains($token)) { throw "TAC 外壳无障碍预检缺少:$token" }
+ if (-not $component.Contains($token)) { throw "Missing native TAC shell token: $token" }
}
-$logicalScript = [regex]::Match($component, '(?s)').Groups['Body'].Value
-foreach ($browserOnly in @('document.', 'window.', 'querySelector(')) {
- if ($logicalScript.Contains($browserOnly)) {
- throw "TAC 逻辑层不得访问浏览器专属对象:$browserOnly"
- }
+foreach ($forbidden in @(
+ 'tac-panel',
+ 'tac-heading',
+ 'tac-tool',
+ 'slider-bottom .close-btn',
+ 'slider-bottom .refresh-btn',
+ 'logoUrl:',
+ 'i18n:',
+ 'new window.TAC(config, {'
+)) {
+ if ($component.Contains($forbidden)) { throw "TAC overrides vendor rendering: $forbidden" }
}
-$toolButtons = [regex]::Matches($component, '(?s)