From cc706378c2a5778af776f3534ebbfea19d3c1771 Mon Sep 17 00:00:00 2001
From: rain <982766701@qq.com>
Date: Wed, 12 Aug 2026 18:22:59 +0800
Subject: [PATCH] feat: migrate app routes and business modules
---
components/AppButton.vue | 10 +-
components/AppDialog.vue | 4 +-
components/AppPromotionStrip.vue | 240 ++
components/AppTabbar.vue | 3 +-
components/AppToast.vue | 4 +-
components/PageHeader.vue | 3 +-
.../{AuthPageShell.vue => auth/PageShell.vue} | 4 +-
components/{ => auth}/TacVerification.vue | 14 +-
components/family/FeedCommentSection.vue | 718 ++++
components/family/FeedMedia.vue | 68 +
components/genealogy/AddDialog.vue | 156 +
.../{GenealogyCard.vue => genealogy/Card.vue} | 1 -
components/genealogy/InvitationManager.vue | 404 ++
components/genealogy/OrderDialog.vue | 339 ++
.../PageBackground.vue} | 0
components/genealogy/RegionPickerDialog.vue | 208 +
components/genealogy/RegionPickerSheet.vue | 187 +
components/genealogy/SwitcherDialog.vue | 189 +
.../profile/EarningWithdrawalDialog.vue | 304 ++
.../records/CeremonyInvitationManager.vue | 397 ++
.../records/GrowthRecordDetailDialog.vue | 399 ++
components/tree/MemberActionPanel.vue | 374 ++
components/tree/PersonDocumentDialog.vue | 1030 +++++
composables/auth/use-sms-verification.js | 196 +
data/mock.js | 695 ----
data/preview/family-content.js | 98 +
data/preview/genealogies.js | 128 +
data/preview/genealogy-membership.js | 16 +
data/preview/records.js | 60 +
pages.json | 130 +-
pages/auth/a06-auth-status.vue | 414 --
pages/auth/{a04-register.vue => register.vue} | 246 +-
...-reset-password.vue => reset-password.vue} | 236 +-
pages/auth/{a01-entry.vue => sign-in.vue} | 97 +-
.../{f09-media-upload.vue => add-photo.vue} | 98 +-
pages/family/album-detail.vue | 266 ++
.../family/{f07-album-list.vue => albums.vue} | 267 +-
pages/family/article-detail.vue | 457 +++
...-article-editor.vue => article-editor.vue} | 277 +-
.../{f04-article-list.vue => articles.vue} | 117 +-
pages/family/f05-article-detail.vue | 232 --
pages/family/f08-album-detail.vue | 180 -
pages/family/f10-video-list.vue | 150 -
.../{f03-feed-detail.vue => feed-detail.vue} | 346 +-
.../{f02-publish-feed.vue => feed-editor.vue} | 230 +-
.../family/{f01-family-feed.vue => feed.vue} | 167 +-
pages/family/videos.vue | 660 +++
pages/genealogy/application-review.vue | 258 ++
.../{g03-create-genealogy.vue => create.vue} | 432 +-
pages/genealogy/g06-search-genealogies.vue | 232 --
pages/genealogy/g09-my-applications.vue | 116 -
pages/genealogy/g10-application-review.vue | 128 -
...eration-poems.vue => generation-poems.vue} | 175 +-
...n-application.vue => join-application.vue} | 96 +-
pages/genealogy/members.vue | 728 ++++
pages/genealogy/my-applications.vue | 265 ++
...-my-genealogies.vue => my-genealogies.vue} | 666 +--
...05-genealogy-overview.vue => overview.vue} | 351 +-
pages/genealogy/search.vue | 433 ++
...11-genealogy-settings.vue => settings.vue} | 428 +-
pages/notification/message-center.vue | 211 +
pages/notification/message-detail.vue | 282 ++
pages/notification/n01-message-center.vue | 187 -
pages/notification/n02-message-detail.vue | 205 -
pages/profile/ceremony-invitations.vue | 301 ++
...hange-password.vue => change-password.vue} | 54 +-
...{m05-change-phone.vue => change-phone.vue} | 210 +-
pages/profile/compliance-document.vue | 222 +
pages/profile/earnings.vue | 445 ++
...{m02-edit-profile.vue => edit-profile.vue} | 124 +-
.../{m07-feedback.vue => feedback.vue} | 422 +-
pages/profile/help.vue | 353 ++
.../{m01-profile-home.vue => home.vue} | 229 +-
pages/profile/m06-help-center.vue | 328 --
pages/profile/m08-promotion.vue | 212 -
pages/profile/m09-vip-orders.vue | 614 ---
pages/profile/m10-about-settings.vue | 234 --
pages/profile/promotions.vue | 242 ++
...m03-security-settings.vue => security.vue} | 36 +-
pages/profile/settings.vue | 615 +++
pages/profile/vip.vue | 358 ++
.../{r05-ritual-list.vue => ceremonies.vue} | 57 +-
pages/records/ceremony-detail.vue | 657 +++
...-ritual-editor.vue => ceremony-editor.vue} | 291 +-
pages/records/growth-journal.vue | 932 +++++
pages/records/life-events.vue | 623 +++
pages/records/memos.vue | 734 ++++
pages/records/merit-records.vue | 716 ++++
.../{r01-people-list.vue => people.vue} | 29 +-
...02-person-detail.vue => person-detail.vue} | 71 +-
pages/records/r03-gift-list.vue | 192 -
pages/records/r06-ritual-detail.vue | 165 -
pages/records/r08-growth-journal.vue | 602 ---
pages/records/r09-life-events.vue | 73 -
pages/records/r10-memo-list.vue | 497 ---
pages/records/r11-merit-records.vue | 489 ---
...-editor.vue => relative-record-editor.vue} | 248 +-
pages/records/relative-records.vue | 333 ++
...{t04-add-relative.vue => add-relative.vue} | 374 +-
.../{t05-edit-member.vue => edit-member.vue} | 324 +-
...ber-directory.vue => member-directory.vue} | 33 +-
...-member-profile.vue => member-profile.vue} | 240 +-
pages/tree/member-rank.vue | 310 ++
...08-member-states.vue => member-states.vue} | 37 +-
pages/tree/overview.vue | 866 ++++
...t02-pedigree-overview.vue => pedigree.vue} | 49 +-
pages/tree/t01-tree-overview.vue | 1493 -------
pages/tree/t06-edit-relationship.vue | 248 --
services/api/auth-contract.js | 42 +
services/api/auth-service.js | 161 +
services/api/business-dictionary-contract.js | 44 +
services/api/business-dictionary-service.js | 25 +
services/api/business-file-contract.js | 63 +
services/api/ceremony-contract.js | 325 ++
services/api/ceremony-service.js | 174 +
services/api/earning-contract.js | 129 +
services/api/earning-service.js | 85 +
services/api/family-article-contract.js | 155 +
services/api/family-article-service.js | 166 +
services/api/family-feed-contract.js | 252 ++
services/api/family-feed-service.js | 220 +
services/api/family-media-contract.js | 191 +
services/api/family-media-service.js | 187 +
services/api/feedback-contract.js | 32 +
services/api/feedback-service.js | 39 +
services/api/file-upload-contract.js | 105 +
services/api/file-upload-service.js | 92 +
services/api/genealogy-contract.js | 414 ++
services/api/genealogy-member-contract.js | 164 +
services/api/genealogy-member-service.js | 116 +
services/api/genealogy-membership-contract.js | 117 +
services/api/genealogy-membership-service.js | 236 ++
services/api/genealogy-service.js | 169 +
services/api/generation-poem-contract.js | 106 +
services/api/generation-poem-service.js | 76 +
services/api/life-record-contract.js | 399 ++
services/api/life-record-service.js | 348 ++
services/api/lineage-person-contract.js | 224 ++
services/api/lineage-person-options.js | 42 +
services/api/lineage-service.js | 176 +
services/api/lineage-tree-contract.js | 119 +
services/api/lineage-write-contract.js | 127 +
services/api/normal-disable-status.js | 8 +
services/api/notification-contract.js | 79 +
services/api/notification-service.js | 82 +
services/api/person-document-contract.js | 150 +
services/api/person-document-service.js | 209 +
services/api/profile-contract.js | 129 +
services/api/profile-service.js | 77 +
services/api/protected-content-contract.js | 24 +
services/api/region-contract.js | 36 +
services/api/region-service.js | 43 +
services/api/request-client.js | 347 ++
services/api/request-controller.js | 31 +
services/api/request-error-message.js | 12 +
services/api/request-normalizers.js | 56 +
services/api/response-normalizers.js | 57 +
services/api/site-content-contract.js | 125 +
services/api/site-content-service.js | 55 +
services/api/vip-contract.js | 101 +
services/api/vip-service.js | 50 +
.../transparent/auth-login-outline.png | Bin 1973 -> 0 bytes
...a93b5@2x.png => default-avatar-female.png} | Bin
...nese__0@2x.png => default-avatar-male.png} | Bin
...roll-primary-v3.png => scroll-primary.png} | Bin
...-secondary-v3.png => scroll-secondary.png} | Bin
...1-scroll-toast-v3.png => scroll-toast.png} | Bin
...01-vnext-header-v1.png => auth-header.png} | Bin
...k-backdrop-v1.png => sign-in-backdrop.png} | Bin
...ment-checked.png => agreement-checked.png} | Bin
...-unchecked.png => agreement-unchecked.png} | Bin
...scroll-dialog-v3.png => dialog-scroll.png} | Bin
...a01-icon-lock-v1.png => icon-password.png} | Bin
.../{a01-icon-phone-v1.png => icon-phone.png} | Bin
...code-v2.png => icon-verification-code.png} | Bin
...losed-pupil-v2.png => password-hidden.png} | Bin
...n-eye-open-v1.png => password-visible.png} | Bin
...vnext-divider-v1.png => title-divider.png} | Bin
.../ancestral-home.png} | Bin
.../ancestral-portrait.png} | Bin
.../family-portrait.png} | Bin
.../reunion-hero.png} | Bin
.../reunion-table.png} | Bin
...letter-card.png => family-letter-card.png} | Bin
...rview-surface.png => overview-surface.png} | Bin
...06-search-button.png => search-button.png} | Bin
...h-input-wide.png => search-input-wide.png} | Bin
...ground-v3.png => add-sheet-background.png} | Bin
...{g01-dialog-close.png => dialog-close.png} | Bin
...-panel-frame.png => empty-panel-frame.png} | Bin
...m-field-frame.png => form-field-frame.png} | Bin
...05-settings-gear.png => settings-gear.png} | Bin
.../{n01-notice-card.png => notice-card.png} | Bin
...e-archive-hero-v2.png => archive-hero.png} | Bin
...d-courtyard.png => featured-courtyard.png} | Bin
...r-mountains-v1.png => paper-mountains.png} | Bin
...o-landscape.png => vip-hero-landscape.png} | Bin
...ile-edit-button-v2.png => edit-button.png} | Bin
...01-icon-calendar.png => icon-calendar.png} | Bin
...01-icon-envelope.png => icon-envelope.png} | Bin
...01-icon-feedback.png => icon-feedback.png} | Bin
.../{m01-icon-help.png => icon-help.png} | Bin
...{m01-icon-message.png => icon-message.png} | Bin
.../{m01-icon-person.png => icon-person.png} | Bin
...-icon-promotion.png => icon-promotion.png} | Bin
...01-icon-security.png => icon-security.png} | Bin
...01-icon-settings.png => icon-settings.png} | Bin
.../{m01-icon-vip.png => icon-vip.png} | Bin
...file-summary-card.png => summary-card.png} | Bin
...-tree-medallion.png => tree-medallion.png} | Bin
...son-name-card.png => person-name-card.png} | Bin
...1-action-arrow-v2.png => action-arrow.png} | Bin
...1-action-close-v2.png => action-close.png} | Bin
...el-paper-v3.png => action-panel-paper.png} | Bin
...nt-marker-v2.png => management-marker.png} | Bin
...-medallion-v2.png => member-medallion.png} | Bin
...-selected.png => member-node-selected.png} | Bin
...-standard.png => member-node-standard.png} | Bin
...frame-v4.png => relation-button-frame.png} | Bin
...1-relation-map-v5.png => relation-map.png} | Bin
...tion-marker-v2.png => relation-marker.png} | Bin
...input-frame.png => search-input-frame.png} | Bin
.../{t01-state-panel.png => state-panel.png} | Bin
styles/adaptive-frame-profiles.scss | 44 +-
utils/api.js | 3561 -----------------
.../password-policy.js} | 0
.../sms-cooldown.js} | 0
.../verification.js} | 22 +-
utils/avatar.js | 4 +-
utils/config.js | 15 -
utils/display-time.js | 7 +
.../access-policy.js} | 31 +-
.../context.js} | 0
utils/genealogy/generation-poem.js | 71 +
utils/generation-poem.js | 216 -
...umable-image-upload.js => media-upload.js} | 50 +-
.../{navigation.js => navigation/gateway.js} | 112 +-
.../routes.js} | 147 +-
utils/non-idempotent-write-guard.js | 18 +
utils/profile/compliance-content.js | 52 +
utils/profile/earning-money.js | 14 +
utils/profile/feedback-submission.js | 144 +
utils/request-outcome.js | 18 +
utils/runtime-config.js | 34 +
utils/session.js | 2 +-
utils/tree/layout.js | 323 ++
utils/tree/member-form.js | 41 +
247 files changed, 28623 insertions(+), 14988 deletions(-)
create mode 100644 components/AppPromotionStrip.vue
rename components/{AuthPageShell.vue => auth/PageShell.vue} (93%)
rename components/{ => auth}/TacVerification.vue (95%)
create mode 100644 components/family/FeedCommentSection.vue
create mode 100644 components/family/FeedMedia.vue
create mode 100644 components/genealogy/AddDialog.vue
rename components/{GenealogyCard.vue => genealogy/Card.vue} (98%)
create mode 100644 components/genealogy/InvitationManager.vue
create mode 100644 components/genealogy/OrderDialog.vue
rename components/{GenealogyPageBackground.vue => genealogy/PageBackground.vue} (100%)
create mode 100644 components/genealogy/RegionPickerDialog.vue
create mode 100644 components/genealogy/RegionPickerSheet.vue
create mode 100644 components/genealogy/SwitcherDialog.vue
create mode 100644 components/profile/EarningWithdrawalDialog.vue
create mode 100644 components/records/CeremonyInvitationManager.vue
create mode 100644 components/records/GrowthRecordDetailDialog.vue
create mode 100644 components/tree/MemberActionPanel.vue
create mode 100644 components/tree/PersonDocumentDialog.vue
create mode 100644 composables/auth/use-sms-verification.js
delete mode 100644 data/mock.js
create mode 100644 data/preview/family-content.js
create mode 100644 data/preview/genealogies.js
create mode 100644 data/preview/genealogy-membership.js
create mode 100644 data/preview/records.js
delete mode 100644 pages/auth/a06-auth-status.vue
rename pages/auth/{a04-register.vue => register.vue} (77%)
rename pages/auth/{a05-reset-password.vue => reset-password.vue} (74%)
rename pages/auth/{a01-entry.vue => sign-in.vue} (88%)
rename pages/family/{f09-media-upload.vue => add-photo.vue} (80%)
create mode 100644 pages/family/album-detail.vue
rename pages/family/{f07-album-list.vue => albums.vue} (54%)
create mode 100644 pages/family/article-detail.vue
rename pages/family/{f06-article-editor.vue => article-editor.vue} (53%)
rename pages/family/{f04-article-list.vue => articles.vue} (56%)
delete mode 100644 pages/family/f05-article-detail.vue
delete mode 100644 pages/family/f08-album-detail.vue
delete mode 100644 pages/family/f10-video-list.vue
rename pages/family/{f03-feed-detail.vue => feed-detail.vue} (50%)
rename pages/family/{f02-publish-feed.vue => feed-editor.vue} (56%)
rename pages/family/{f01-family-feed.vue => feed.vue} (55%)
create mode 100644 pages/family/videos.vue
create mode 100644 pages/genealogy/application-review.vue
rename pages/genealogy/{g03-create-genealogy.vue => create.vue} (58%)
delete mode 100644 pages/genealogy/g06-search-genealogies.vue
delete mode 100644 pages/genealogy/g09-my-applications.vue
delete mode 100644 pages/genealogy/g10-application-review.vue
rename pages/genealogy/{g12-generation-poems.vue => generation-poems.vue} (73%)
rename pages/genealogy/{g08-join-application.vue => join-application.vue} (64%)
create mode 100644 pages/genealogy/members.vue
create mode 100644 pages/genealogy/my-applications.vue
rename pages/genealogy/{g01-my-genealogies.vue => my-genealogies.vue} (61%)
rename pages/genealogy/{g05-genealogy-overview.vue => overview.vue} (66%)
create mode 100644 pages/genealogy/search.vue
rename pages/genealogy/{g11-genealogy-settings.vue => settings.vue} (63%)
create mode 100644 pages/notification/message-center.vue
create mode 100644 pages/notification/message-detail.vue
delete mode 100644 pages/notification/n01-message-center.vue
delete mode 100644 pages/notification/n02-message-detail.vue
create mode 100644 pages/profile/ceremony-invitations.vue
rename pages/profile/{m04-change-password.vue => change-password.vue} (83%)
rename pages/profile/{m05-change-phone.vue => change-phone.vue} (65%)
create mode 100644 pages/profile/compliance-document.vue
create mode 100644 pages/profile/earnings.vue
rename pages/profile/{m02-edit-profile.vue => edit-profile.vue} (77%)
rename pages/profile/{m07-feedback.vue => feedback.vue} (50%)
create mode 100644 pages/profile/help.vue
rename pages/profile/{m01-profile-home.vue => home.vue} (67%)
delete mode 100644 pages/profile/m06-help-center.vue
delete mode 100644 pages/profile/m08-promotion.vue
delete mode 100644 pages/profile/m09-vip-orders.vue
delete mode 100644 pages/profile/m10-about-settings.vue
create mode 100644 pages/profile/promotions.vue
rename pages/profile/{m03-security-settings.vue => security.vue} (78%)
create mode 100644 pages/profile/settings.vue
create mode 100644 pages/profile/vip.vue
rename pages/records/{r05-ritual-list.vue => ceremonies.vue} (74%)
create mode 100644 pages/records/ceremony-detail.vue
rename pages/records/{r07-ritual-editor.vue => ceremony-editor.vue} (51%)
create mode 100644 pages/records/growth-journal.vue
create mode 100644 pages/records/life-events.vue
create mode 100644 pages/records/memos.vue
create mode 100644 pages/records/merit-records.vue
rename pages/records/{r01-people-list.vue => people.vue} (91%)
rename pages/records/{r02-person-detail.vue => person-detail.vue} (85%)
delete mode 100644 pages/records/r03-gift-list.vue
delete mode 100644 pages/records/r06-ritual-detail.vue
delete mode 100644 pages/records/r08-growth-journal.vue
delete mode 100644 pages/records/r09-life-events.vue
delete mode 100644 pages/records/r10-memo-list.vue
delete mode 100644 pages/records/r11-merit-records.vue
rename pages/records/{r04-gift-editor.vue => relative-record-editor.vue} (58%)
create mode 100644 pages/records/relative-records.vue
rename pages/tree/{t04-add-relative.vue => add-relative.vue} (67%)
rename pages/tree/{t05-edit-member.vue => edit-member.vue} (71%)
rename pages/tree/{t07-member-directory.vue => member-directory.vue} (92%)
rename pages/tree/{t03-member-profile.vue => member-profile.vue} (67%)
create mode 100644 pages/tree/member-rank.vue
rename pages/tree/{t08-member-states.vue => member-states.vue} (84%)
create mode 100644 pages/tree/overview.vue
rename pages/tree/{t02-pedigree-overview.vue => pedigree.vue} (91%)
delete mode 100644 pages/tree/t01-tree-overview.vue
delete mode 100644 pages/tree/t06-edit-relationship.vue
create mode 100644 services/api/auth-contract.js
create mode 100644 services/api/auth-service.js
create mode 100644 services/api/business-dictionary-contract.js
create mode 100644 services/api/business-dictionary-service.js
create mode 100644 services/api/business-file-contract.js
create mode 100644 services/api/ceremony-contract.js
create mode 100644 services/api/ceremony-service.js
create mode 100644 services/api/earning-contract.js
create mode 100644 services/api/earning-service.js
create mode 100644 services/api/family-article-contract.js
create mode 100644 services/api/family-article-service.js
create mode 100644 services/api/family-feed-contract.js
create mode 100644 services/api/family-feed-service.js
create mode 100644 services/api/family-media-contract.js
create mode 100644 services/api/family-media-service.js
create mode 100644 services/api/feedback-contract.js
create mode 100644 services/api/feedback-service.js
create mode 100644 services/api/file-upload-contract.js
create mode 100644 services/api/file-upload-service.js
create mode 100644 services/api/genealogy-contract.js
create mode 100644 services/api/genealogy-member-contract.js
create mode 100644 services/api/genealogy-member-service.js
create mode 100644 services/api/genealogy-membership-contract.js
create mode 100644 services/api/genealogy-membership-service.js
create mode 100644 services/api/genealogy-service.js
create mode 100644 services/api/generation-poem-contract.js
create mode 100644 services/api/generation-poem-service.js
create mode 100644 services/api/life-record-contract.js
create mode 100644 services/api/life-record-service.js
create mode 100644 services/api/lineage-person-contract.js
create mode 100644 services/api/lineage-person-options.js
create mode 100644 services/api/lineage-service.js
create mode 100644 services/api/lineage-tree-contract.js
create mode 100644 services/api/lineage-write-contract.js
create mode 100644 services/api/normal-disable-status.js
create mode 100644 services/api/notification-contract.js
create mode 100644 services/api/notification-service.js
create mode 100644 services/api/person-document-contract.js
create mode 100644 services/api/person-document-service.js
create mode 100644 services/api/profile-contract.js
create mode 100644 services/api/profile-service.js
create mode 100644 services/api/protected-content-contract.js
create mode 100644 services/api/region-contract.js
create mode 100644 services/api/region-service.js
create mode 100644 services/api/request-client.js
create mode 100644 services/api/request-controller.js
create mode 100644 services/api/request-error-message.js
create mode 100644 services/api/request-normalizers.js
create mode 100644 services/api/response-normalizers.js
create mode 100644 services/api/site-content-contract.js
create mode 100644 services/api/site-content-service.js
create mode 100644 services/api/vip-contract.js
create mode 100644 services/api/vip-service.js
delete mode 100644 static/assets/foundation/transparent/auth-login-outline.png
rename static/assets/foundation/transparent/{loyel003_Ancient_Beauty_Wearing_Tang_Dynasty_ClothingLooking_to_07cb98a9-ec37-4620-a032-ebe511fa93b5@2x.png => default-avatar-female.png} (100%)
rename static/assets/foundation/transparent/{mjpc0703_A_cartoon_illustration_of_a_boy_wearing_a_red_Chinese__0@2x.png => default-avatar-male.png} (100%)
rename static/assets/foundation/transparent/{a01-scroll-primary-v3.png => scroll-primary.png} (100%)
rename static/assets/foundation/transparent/{a01-scroll-secondary-v3.png => scroll-secondary.png} (100%)
rename static/assets/foundation/transparent/{a01-scroll-toast-v3.png => scroll-toast.png} (100%)
rename static/assets/modules/auth/opaque/{a01-vnext-header-v1.png => auth-header.png} (100%)
rename static/assets/modules/auth/opaque/{a01-red-hall-ink-backdrop-v1.png => sign-in-backdrop.png} (100%)
rename static/assets/modules/auth/transparent/{a02-agreement-checked.png => agreement-checked.png} (100%)
rename static/assets/modules/auth/transparent/{a02-agreement-unchecked.png => agreement-unchecked.png} (100%)
rename static/assets/modules/auth/transparent/{a01-scroll-dialog-v3.png => dialog-scroll.png} (100%)
rename static/assets/modules/auth/transparent/{a01-icon-lock-v1.png => icon-password.png} (100%)
rename static/assets/modules/auth/transparent/{a01-icon-phone-v1.png => icon-phone.png} (100%)
rename static/assets/modules/auth/transparent/{a01-icon-sms-code-v2.png => icon-verification-code.png} (100%)
rename static/assets/modules/auth/transparent/{a01-icon-eye-closed-pupil-v2.png => password-hidden.png} (100%)
rename static/assets/modules/auth/transparent/{a01-icon-eye-open-v1.png => password-visible.png} (100%)
rename static/assets/modules/auth/transparent/{a01-vnext-divider-v1.png => title-divider.png} (100%)
rename static/assets/modules/family/{f08/f08-ancestral-home.png => albums/ancestral-home.png} (100%)
rename static/assets/modules/family/{f08/f08-ancestral-portrait.png => albums/ancestral-portrait.png} (100%)
rename static/assets/modules/family/{f08/f08-family-portrait.png => albums/family-portrait.png} (100%)
rename static/assets/modules/family/{f08/f08-reunion-hero.png => albums/reunion-hero.png} (100%)
rename static/assets/modules/family/{f08/f08-reunion-table.png => albums/reunion-table.png} (100%)
rename static/assets/modules/family/transparent/{f01-family-letter-card.png => family-letter-card.png} (100%)
rename static/assets/modules/genealogy/opaque/{g05-overview-surface.png => overview-surface.png} (100%)
rename static/assets/modules/genealogy/opaque/{g06-search-button.png => search-button.png} (100%)
rename static/assets/modules/genealogy/opaque/{g06-search-input-wide.png => search-input-wide.png} (100%)
rename static/assets/modules/genealogy/transparent/{g01-add-sheet-background-v3.png => add-sheet-background.png} (100%)
rename static/assets/modules/genealogy/transparent/{g01-dialog-close.png => dialog-close.png} (100%)
rename static/assets/modules/genealogy/transparent/{g01-empty-panel-frame.png => empty-panel-frame.png} (100%)
rename static/assets/modules/genealogy/transparent/{g-form-field-frame.png => form-field-frame.png} (100%)
rename static/assets/modules/genealogy/transparent/{g05-settings-gear.png => settings-gear.png} (100%)
rename static/assets/modules/notification/transparent/{n01-notice-card.png => notice-card.png} (100%)
rename static/assets/modules/profile/opaque/{m01-profile-archive-hero-v2.png => archive-hero.png} (100%)
rename static/assets/modules/profile/opaque/{m09-featured-courtyard.png => featured-courtyard.png} (100%)
rename static/assets/modules/profile/opaque/{m01-profile-paper-mountains-v1.png => paper-mountains.png} (100%)
rename static/assets/modules/profile/opaque/{m09-vip-hero-landscape.png => vip-hero-landscape.png} (100%)
rename static/assets/modules/profile/transparent/{m01-profile-edit-button-v2.png => edit-button.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-calendar.png => icon-calendar.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-envelope.png => icon-envelope.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-feedback.png => icon-feedback.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-help.png => icon-help.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-message.png => icon-message.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-person.png => icon-person.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-promotion.png => icon-promotion.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-security.png => icon-security.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-settings.png => icon-settings.png} (100%)
rename static/assets/modules/profile/transparent/{m01-icon-vip.png => icon-vip.png} (100%)
rename static/assets/modules/profile/transparent/{m01-profile-summary-card.png => summary-card.png} (100%)
rename static/assets/modules/profile/transparent/{m01-profile-tree-medallion.png => tree-medallion.png} (100%)
rename static/assets/modules/records/transparent/{r01-person-name-card.png => person-name-card.png} (100%)
rename static/assets/modules/tree/transparent/{t01-action-arrow-v2.png => action-arrow.png} (100%)
rename static/assets/modules/tree/transparent/{t01-action-close-v2.png => action-close.png} (100%)
rename static/assets/modules/tree/transparent/{t01-action-panel-paper-v3.png => action-panel-paper.png} (100%)
rename static/assets/modules/tree/transparent/{t01-management-marker-v2.png => management-marker.png} (100%)
rename static/assets/modules/tree/transparent/{t01-member-medallion-v2.png => member-medallion.png} (100%)
rename static/assets/modules/tree/transparent/{t01-member-node-selected.png => member-node-selected.png} (100%)
rename static/assets/modules/tree/transparent/{t01-member-node-standard.png => member-node-standard.png} (100%)
rename static/assets/modules/tree/transparent/{t01-relation-button-frame-v4.png => relation-button-frame.png} (100%)
rename static/assets/modules/tree/transparent/{t01-relation-map-v5.png => relation-map.png} (100%)
rename static/assets/modules/tree/transparent/{t01-relation-marker-v2.png => relation-marker.png} (100%)
rename static/assets/modules/tree/transparent/{t07-search-input-frame.png => search-input-frame.png} (100%)
rename static/assets/modules/tree/transparent/{t01-state-panel.png => state-panel.png} (100%)
delete mode 100644 utils/api.js
rename utils/{validation.js => auth/password-policy.js} (100%)
rename utils/{auth-sms-cooldown.js => auth/sms-cooldown.js} (100%)
rename utils/{auth-verification.js => auth/verification.js} (88%)
delete mode 100644 utils/config.js
create mode 100644 utils/display-time.js
rename utils/{genealogy-contracts.js => genealogy/access-policy.js} (64%)
rename utils/{genealogy-context.js => genealogy/context.js} (100%)
create mode 100644 utils/genealogy/generation-poem.js
delete mode 100644 utils/generation-poem.js
rename utils/{resumable-image-upload.js => media-upload.js} (84%)
rename utils/{navigation.js => navigation/gateway.js} (90%)
rename utils/{navigation-routes.js => navigation/routes.js} (72%)
create mode 100644 utils/non-idempotent-write-guard.js
create mode 100644 utils/profile/compliance-content.js
create mode 100644 utils/profile/earning-money.js
create mode 100644 utils/profile/feedback-submission.js
create mode 100644 utils/request-outcome.js
create mode 100644 utils/runtime-config.js
create mode 100644 utils/tree/layout.js
create mode 100644 utils/tree/member-form.js
diff --git a/components/AppButton.vue b/components/AppButton.vue
index 3fd0052..a526382 100644
--- a/components/AppButton.vue
+++ b/components/AppButton.vue
@@ -35,8 +35,8 @@ const emit = defineEmits(["click"]);
const skin = computed(() =>
props.type === "secondary"
- ? "/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
- : "/static/assets/foundation/transparent/a01-scroll-primary-v3.png",
+ ? "/static/assets/foundation/transparent/scroll-secondary.png"
+ : "/static/assets/foundation/transparent/scroll-primary.png",
);
const handleClick = (event) => {
@@ -45,7 +45,7 @@ const handleClick = (event) => {
diff --git a/components/AppTabbar.vue b/components/AppTabbar.vue
index 50f6564..655dee2 100644
--- a/components/AppTabbar.vue
+++ b/components/AppTabbar.vue
@@ -1,4 +1,3 @@
-
-
+ {{ item.publisher }}{{ item.content }}{{ feedTypeLabel(item.type) }} · {{ item.time }}
+ >
+ {{ item.publisher }}
+ {{ feedPreview(item.content) }}
+
+ {{ feedTypeLabel(item.type) }} · {{ formatMinuteTimestamp(item.time) }}
+ {{ item.recommendationReason }}
+
+ {{ loadMoreLabel }}
+
{{ stateCopy.title }}
import { computed, ref } from "vue";
-import { onLoad, onShow } from "@dcloudio/uni-app";
+import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppTabbar from "@/components/AppTabbar.vue";
+import FamilyFeedMedia from "@/components/family/FeedMedia.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
-import { appApi, isRequestCancelled } from "@/utils/api.js";
-import { genealogyContext } from "@/utils/genealogy-context.js";
-import { goRoot, openPage } from "@/utils/navigation.js";
+import {
+ createRequestController,
+ isRequestCancelled
+} from "@/services/api/request-controller.js";
+import { familyFeedApi } from "@/services/api/family-feed-service.js";
+import { formatMinuteTimestamp } from "@/utils/display-time.js";
+import { genealogyContext } from "@/utils/genealogy/context.js";
+import { goRoot, openPage } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const hasValidContext = ref(false);
const feedState = ref("loading");
const feeds = ref([]);
+const FEED_PAGE_SIZE = 20;
+const totalFeedCount = ref(0);
+const currentFeedPage = ref(1);
+const loadMoreState = ref("idle");
+const hasMoreFeeds = computed(
+ () => feedState.value === "list" && feeds.value.length < totalFeedCount.value,
+);
+const loadMoreLabel = computed(() => {
+ if (loadMoreState.value === "loading") return "正在加载";
+ if (loadMoreState.value === "error") return "加载失败,重新加载";
+ return "继续加载";
+});
let active = true;
+let skipInitialShowRefresh = true;
+const feedListRequestController = createRequestController();
+const recommendationRequestController = createRequestController();
const feedTypeLabel = (type) =>
String(type || "")
.trim()
.toLowerCase() === "text"
? "文字动态"
: "家族动态";
+const feedPreview = (content) => {
+ const text = String(content || "").trim();
+ return text.length > 96 ? `${text.slice(0, 96).trimEnd()}…` : text;
+};
const shortcuts = [
{ key: "articles", label: "谱文" },
{ key: "albums", label: "相册" },
@@ -109,16 +142,67 @@ const stateCopy = computed(() =>
const loadFeeds = async () => {
if (!hasValidContext.value) return;
feedState.value = "loading";
+ loadMoreState.value = "idle";
+ currentFeedPage.value = 1;
+ feedListRequestController.abort();
+ recommendationRequestController.abort();
try {
- const rows = await appApi.getFeeds(genealogyId.value);
+ const [pageResult, recommendationResult] = await Promise.allSettled([
+ familyFeedApi.getFeedPage(
+ genealogyId.value,
+ { pageNum: 1, pageSize: FEED_PAGE_SIZE },
+ { requestController: feedListRequestController },
+ ),
+ familyFeedApi.getFeedRecommendations(genealogyId.value, {
+ requestController: recommendationRequestController,
+ }),
+ ]);
+ if (pageResult.status === "rejected") throw pageResult.reason;
if (!active) return;
- feeds.value = rows;
+ const recommendations =
+ recommendationResult.status === "fulfilled"
+ ? recommendationResult.value
+ : [];
+ const reasons = new Map(
+ recommendations.map((item) => [String(item.id), item.recommendationReason]),
+ );
+ feeds.value = pageResult.value.rows.map((item) => ({
+ ...item,
+ recommendationReason: reasons.get(String(item.id)) || "",
+ }));
+ totalFeedCount.value = pageResult.value.total;
+ loadMoreState.value =
+ feeds.value.length < totalFeedCount.value ? "idle" : "done";
feedState.value = feeds.value.length ? "list" : "empty";
} catch (error) {
if (!active || isRequestCancelled(error)) return;
feedState.value = "error";
}
};
+const loadMoreFeeds = async () => {
+ if (!hasMoreFeeds.value || loadMoreState.value === "loading") return;
+ loadMoreState.value = "loading";
+ try {
+ const nextPage = currentFeedPage.value + 1;
+ const page = await familyFeedApi.getFeedPage(
+ genealogyId.value,
+ { pageNum: nextPage, pageSize: FEED_PAGE_SIZE },
+ { requestController: feedListRequestController },
+ );
+ if (!active) return;
+ const knownIds = new Set(feeds.value.map((item) => String(item.id)));
+ feeds.value = feeds.value.concat(
+ page.rows.filter((item) => !knownIds.has(String(item.id))),
+ );
+ currentFeedPage.value = nextPage;
+ totalFeedCount.value = page.total;
+ loadMoreState.value =
+ feeds.value.length < totalFeedCount.value ? "idle" : "done";
+ } catch (error) {
+ if (!active || isRequestCancelled(error)) return;
+ loadMoreState.value = "error";
+ }
+};
onLoad((query) => {
const supplied = Object.prototype.hasOwnProperty.call(
query || {},
@@ -132,11 +216,20 @@ onLoad((query) => {
else feedState.value = "error";
});
onShow(() => {
+ if (skipInitialShowRefresh) {
+ skipInitialShowRefresh = false;
+ return;
+ }
if (hasValidContext.value) void loadFeeds();
});
+onUnload(() => {
+ active = false;
+ feedListRequestController.abort();
+ recommendationRequestController.abort();
+});
const toPublish = () =>
hasValidContext.value
- ? openPage("F02", { genealogyId: genealogyId.value }, "F01")
+ ? openPage("F02", { genealogyId: genealogyId.value, mode: "create" }, "F01")
: goRoot("G01");
const openFeed = (item) =>
openPage("F03", { genealogyId: genealogyId.value, feedId: item.id }, "F01");
@@ -190,10 +283,17 @@ const handlePrimaryAction = () =>
.feed-shortcuts {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8rpx;
margin-top: 15rpx;
}
.feed-shortcut {
+ box-sizing: border-box;
min-height: 48px;
+ margin: 0;
+ padding: 0;
+ border: 1rpx solid rgba(181, 137, 63, 0.45);
+ border-radius: 8rpx;
+ background: rgba(255, 252, 244, 0.58);
display: flex;
align-items: center;
justify-content: center;
@@ -237,6 +337,37 @@ const handlePrimaryAction = () =>
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
}
+.feed-shortcut::after {
+ border: 0;
+}
+.feed-shortcut:active {
+ background: rgba(181, 137, 63, 0.1);
+}
+.feed-card__reason {
+ margin-top: 8rpx;
+ color: #9a6555;
+ font-size: clamp(12px, 20rpx, 15px);
+}
+.feed-more {
+ @include adaptive-scroll-button(secondary);
+ display: block;
+ width: 420rpx;
+ max-width: 100%;
+ min-height: 76rpx;
+ margin: 22rpx auto 0;
+ padding: 0 24rpx;
+ border: 0;
+ color: $ink;
+ font-size: clamp(14px, 22rpx, 17px);
+ font-weight: 700;
+ line-height: 76rpx;
+}
+.feed-more::after {
+ border: 0;
+}
+.feed-more[disabled] {
+ opacity: 0.62;
+}
.feed-state-card {
min-height: 230rpx;
display: flex;
diff --git a/pages/family/videos.vue b/pages/family/videos.vue
new file mode 100644
index 0000000..8ad2922
--- /dev/null
+++ b/pages/family/videos.vue
@@ -0,0 +1,660 @@
+
+
+
+
+
+
+ {{
+ isEdit ? "编辑家族视频" : "发布家族视频"
+ }}
+ {{
+ isEdit
+ ? "原视频、封面和相关设置会保留。"
+ : "视频上传成功后会自动关联,无需额外填写。"
+ }}
+
+
+ *视频文件
+
+ {{
+ isEdit
+ ? "当前视频已关联"
+ : uploading
+ ? "上传中…"
+ : receipt
+ ? "重新选择视频"
+ : "选择视频"
+ }}
+
+ 已上传:{{ receipt.fileName || "视频" }}
+
+ {{ uploadError }}
+
+
+ 视频封面
+
+ {{
+ coverUploading
+ ? "上传中…"
+ : coverReceipt
+ ? "重新选择封面"
+ : "选择图片"
+ }}
+
+ 已上传:{{ coverReceipt.fileName || "视频封面" }}
+
+ {{
+ coverUploadError
+ }}
+
+
+ *视频标题
+
+
+
+ 视频说明
+
+
+ {{ submitError }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂时无法读取视频
+ 请检查网络后重新加载。
+
+
+
+ 还没有家族视频
+ 可以先发布一段值得留存的影像。
+
+
+
+
+
+ {{ video.title }}
+ {{
+ video.description
+ }}
+ {{
+ video.publishTime || "刚刚发布"
+ }}
+
+
+
+
+
+
+ {{
+ videoActionError
+ }}
+
+
+ {{ stateCopy.title }}
+ {{ stateCopy.copy }}
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/genealogy/application-review.vue b/pages/genealogy/application-review.vue
new file mode 100644
index 0000000..7021dd9
--- /dev/null
+++ b/pages/genealogy/application-review.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+ 暂时无法打开审核页面
+ 未找到家谱信息,请返回后重新进入。
+
+
+
+
+
+
+ 暂时无法读取待审核申请
+ {{ applicationReviewError || "请检查网络后重试。" }}
+
+
+
+ 暂无待审核申请
+ 新的加入申请会显示在这里。
+
+
+
+ 共 {{ applications.length }} 条待审核申请
+ {{ feedbackMessage }}
+
+
+ {{ applicantName(item) }}
+ {{ applicationTime(item) }}
+
+ {{ applicantPhone(item) }}
+ {{ applicationRelation(item) }}
+ {{ applicationReason(item) }}
+
+
+
+
+
+
+
+
+
+ 拒绝原因
+
+ {{ rejectionError }}
+
+ {{ operationError }}
+
+
+
+
+
+
+
diff --git a/pages/genealogy/g03-create-genealogy.vue b/pages/genealogy/create.vue
similarity index 58%
rename from pages/genealogy/g03-create-genealogy.vue
rename to pages/genealogy/create.vue
index 968ee92..700741c 100644
--- a/pages/genealogy/g03-create-genealogy.vue
+++ b/pages/genealogy/create.vue
@@ -1,4 +1,3 @@
-
@@ -9,7 +8,7 @@
立谱信息
为家族创建一部家谱
创建成功后会回到我的家谱;首代人物可在后续合同开放后再录入。创建成功后会回到“我的家谱”;可在世系树中录入首位成员。
@@ -44,7 +43,12 @@
fieldErrors.genealogyName
}}
-
+
*所在地区
@@ -59,6 +63,12 @@
: regionPickerError || "请选择所在地区"
}}
+
{{
fieldErrors.regionCode
@@ -109,7 +119,7 @@
封面图片
选择图片后会取得真实上传回执,并在创建家谱时关联。图片上传成功后,会作为家谱封面保存。
访问规则
-
+
{{ option.label }}
@@ -146,60 +163,20 @@
-
-
-
-
- 选择地区
-
-
-
- {{ label }}
-
-
-
- {{ option.label }}
-
-
-
-
-
-
+
@@ -312,118 +292,36 @@ const validate = () => {
);
};
-const fetchRegionChildren = async (parentCode) => {
- regionLoading.value = true;
- regionPickerError.value = "";
- try {
- return await appApi.getRegionChildren(parentCode, {
- requestController: regionController,
- });
- } catch (error) {
- if (!isRequestCancelled(error))
- regionPickerError.value = error?.message || "地区列表加载失败,请重试";
- return [];
- } finally {
- regionLoading.value = false;
- }
-};
-
-const loadRegionRoot = async () => {
- if (isSubmitting.value || isUploading.value || regionLoading.value) return;
- const roots = await fetchRegionChildren("0");
- if (!roots.length || regionPickerError.value) return;
- regionPickerColumns.value = [roots];
- regionPickerIndexes.value = [0];
- regionPickerTrail.value = [];
- await loadPickerColumns();
-};
-
-const loadPickerColumns = async (provinceIndex = 0, cityIndex = 0) => {
- const provinces =
- regionPickerColumns.value[0] || (await fetchRegionChildren("0"));
- const province = provinces[provinceIndex];
- if (!province) return;
-
- const cities = await fetchRegionChildren(province.regionCode);
- const city = cities[cityIndex];
- if (!city) {
- regionPickerColumns.value = [provinces];
- regionPickerIndexes.value = [provinceIndex];
- return;
- }
-
- const districts = await fetchRegionChildren(city.regionCode);
- if (!districts.length || regionPickerError.value) return;
- regionPickerColumns.value = [provinces, cities, districts];
- regionPickerIndexes.value = [provinceIndex, cityIndex, 0];
-};
-
-const handleRegionPickerChange = async (event) => {
- if (regionLoading.value) return;
- const nextIndexes = (event?.detail?.value || []).map(
- (index) => Number(index) || 0,
- );
- const indexes = regionPickerIndexes.value;
- const provinceIndex = nextIndexes[0] || 0;
- const cityIndex = nextIndexes[1] || 0;
- const districtIndex = nextIndexes[2] || 0;
-
- if (provinceIndex !== (indexes[0] || 0)) {
- await loadPickerColumns(provinceIndex, 0);
- return;
- }
- if (cityIndex !== (indexes[1] || 0)) {
- await loadPickerColumns(indexes[0] || 0, cityIndex);
- return;
- }
- regionPickerIndexes.value = [indexes[0] || 0, indexes[1] || 0, districtIndex];
-};
-
const openRegionPicker = async () => {
if (isSubmitting.value || isUploading.value || regionLoading.value) return;
- if (!regionPickerColumns.value.length) await loadRegionRoot();
- if (regionPickerColumns.value.length) {
- regionPickerOpen.value = true;
- }
+ await regionPickerDialog.value?.open(selectedRegion.value?.regionCode || "");
};
-
-const closeRegionPicker = () => {
- regionPickerOpen.value = false;
-};
-
-const confirmRegionSelection = () => {
- const indexes = regionPickerIndexes.value;
- const trail = [];
- regionPickerColumns.value.forEach((column, index) => {
- const option = column[Number(indexes[index])];
- if (option) trail.push(option);
- });
- const region = trail[trail.length - 1];
- if (!region) return;
- regionPickerIndexes.value = trail.map(
- (_, index) => Number(indexes[index]) || 0,
- );
+const selectRegion = ({ region, trail }) => {
selectedRegion.value = region;
regionPickerTrail.value = trail;
fieldErrors.regionCode = "";
regionPickerError.value = "";
- regionPickerOpen.value = false;
};
onMounted(() => {
- loadRegionRoot();
+ void regionPickerDialog.value?.prepare();
});
const requestBack = () =>
- runBackGuard({
- transientOpen: regionPickerOpen.value || discardDialogVisible.value,
- dirty: hasDraft.value,
- submitting: isSubmitting.value || isUploading.value,
- "close-transient": () =>
- regionPickerOpen.value ? closeRegionPicker() : cancelDiscard(),
- "block-submitting": () => true,
- "confirm-discard": requestDiscardConfirmation,
- });
+ createdGenealogyId.value
+ ? returnTo("G01")
+ : runBackGuard({
+ transientOpen:
+ regionPickerTransientOpen.value || discardDialogVisible.value,
+ dirty: hasDraft.value,
+ submitting: isSubmitting.value || isUploading.value,
+ "close-transient": () =>
+ regionPickerTransientOpen.value
+ ? regionPickerDialog.value?.close()
+ : cancelDiscard(),
+ "block-submitting": () => true,
+ "confirm-discard": requestDiscardConfirmation,
+ });
onBackPress((event) => handleBackPress(event, requestBack));
@@ -433,7 +331,7 @@ const uploadCover = async () => {
uploadError.value = "";
try {
const receipt = await pickAndUploadImage({
- requestController: createController,
+ requestController: coverUploadRequestController,
});
if (!pageActive) return;
coverOssId.value = receipt.ossId;
@@ -444,7 +342,7 @@ const uploadCover = async () => {
!isImagePickCancelled(error) &&
!isRequestCancelled(error)
) {
- uploadError.value = error?.message || "封面图片上传失败,请稍后重试";
+ uploadError.value = getRequestErrorMessage(error, "封面图片上传失败,请稍后重试");
}
} finally {
if (pageActive) isUploading.value = false;
@@ -452,43 +350,71 @@ const uploadCover = async () => {
};
const submitCreate = async () => {
- if (isSubmitting.value || isUploading.value || !validate()) return;
- const access = toApiGenealogyAccess(form.accessPreset);
- if (!access) {
- submitError.value = "访问规则无效,请重新选择";
- return;
+ if (isSubmitting.value || isUploading.value) return;
+ if (!createdGenealogyId.value && !validate()) return;
+
+ let createPayload = null;
+ let createAttempt = null;
+ if (!createdGenealogyId.value) {
+ const access = toApiGenealogyAccess(form.accessPreset);
+ if (!access) {
+ submitError.value = "请选择家谱开放方式";
+ return;
+ }
+ createPayload = {
+ surname: form.surname,
+ genealogyName: form.genealogyName,
+ regionCode: selectedRegion.value.regionCode,
+ ancestralHall: form.ancestralHall,
+ originPlace: form.originPlace,
+ addressDetail: form.addressDetail,
+ intro: form.intro,
+ coverOssId: coverOssId.value,
+ ...access,
+ };
+ createAttempt = genealogyCreateGuard.begin(createPayload);
+ if (createAttempt === null) {
+ submitError.value =
+ "上次创建结果暂时无法确认,请先返回“我的家谱”检查,避免重复创建。";
+ return;
+ }
}
isSubmitting.value = true;
submitError.value = "";
try {
- const created = await appApi.createGenealogy(
- {
- surname: form.surname,
- genealogyName: form.genealogyName,
- regionCode: selectedRegion.value.regionCode,
- ancestralHall: form.ancestralHall,
- originPlace: form.originPlace,
- addressDetail: form.addressDetail,
- intro: form.intro,
- coverOssId: coverOssId.value,
- ...access,
- },
- { requestController: createController },
- );
- if (!pageActive) return;
+ if (!createdGenealogyId.value) {
+ const created = await genealogyApi.createGenealogy(
+ createPayload,
+ { requestController: genealogyCreateRequestController },
+ );
+ if (!pageActive) return;
+ createdGenealogyId.value = created.id;
+ }
await finishPage(
"G01",
{},
{
operation: "genealogy-created",
- entityId: created.id,
+ entityId: createdGenealogyId.value,
refresh: true,
},
);
} catch (error) {
- if (!pageActive || isRequestCancelled(error)) return;
- submitError.value = error?.message || "创建失败,请稍后重试";
+ if (!pageActive) return;
+ if (
+ !createdGenealogyId.value &&
+ createAttempt &&
+ genealogyCreateGuard.recordFailure(createAttempt, error)
+ ) {
+ submitError.value =
+ "创建结果暂时无法确认,请先返回“我的家谱”检查,避免重复创建。";
+ return;
+ }
+ if (isRequestCancelled(error)) return;
+ submitError.value = createdGenealogyId.value
+ ? "家谱已经创建,但页面返回失败。请再次点击“返回我的家谱”,不要重复创建。"
+ : getRequestErrorMessage(error, "创建失败,请稍后重试");
} finally {
if (pageActive) isSubmitting.value = false;
}
@@ -496,8 +422,8 @@ const submitCreate = async () => {
onUnload(() => {
pageActive = false;
- createController.abort();
- regionController.abort();
+ coverUploadRequestController.abort();
+ genealogyCreateRequestController.abort();
discardConfirmation.dispose();
});
@@ -590,6 +516,13 @@ onUnload(() => {
text-align: right;
overflow-wrap: anywhere;
}
+.region-selector-chevron {
+ width: 28rpx;
+ height: 28rpx;
+ margin-left: 12rpx;
+ flex: 0 0 auto;
+ opacity: 0.7;
+}
.region-selector-value--placeholder {
color: #ab9a86;
}
@@ -644,7 +577,7 @@ onUnload(() => {
}
.upload-button {
justify-self: start;
- min-height: 60rpx;
+ min-height: 88rpx;
margin: 0;
padding: 0 20rpx;
border: 1rpx solid rgba(159, 23, 15, 0.42);
@@ -681,6 +614,11 @@ onUnload(() => {
}
.access-rule__option {
flex: 1;
+ display: flex;
+ min-height: 88rpx;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
padding: 20rpx 12rpx;
border: 1rpx solid rgba(128, 89, 49, 0.34);
border-radius: 12rpx;
@@ -697,108 +635,4 @@ onUnload(() => {
.create-card .app-button {
margin-top: 32rpx;
}
-.region-sheet {
- position: fixed;
- z-index: 10;
- inset: 0;
- display: flex;
- align-items: flex-end;
-}
-.region-sheet__mask {
- position: absolute;
- inset: 0;
- background: rgba(43, 30, 20, 0.42);
-}
-.region-sheet__panel {
- position: relative;
- width: 100%;
- padding: 22rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
- border-radius: 30rpx 30rpx 0 0;
- background: #fdf9ef;
- box-shadow: 0 -12rpx 36rpx rgba(43, 30, 20, 0.2);
-}
-.region-sheet__intro {
- padding: 0 10rpx 16rpx;
- text-align: center;
-}
-.region-sheet__title {
- display: block;
-}
-.region-sheet__title {
- color: $ink;
- font-family: STKaiti, KaiTi, serif;
- font-size: clamp(19px, 36rpx, 24px);
- font-weight: 700;
-}
-.region-sheet__picker {
- overflow: hidden;
- border: 1rpx solid rgba(128, 89, 49, 0.28);
- border-radius: 16rpx;
- background: rgba(255, 252, 245, 0.8);
-}
-.region-sheet__column-headings {
- display: flex;
- height: 84rpx;
- border-bottom: 1rpx solid rgba(128, 89, 49, 0.18);
-}
-.region-sheet__column-heading {
- box-sizing: border-box;
- width: 33.333%;
- padding: 24rpx 12rpx;
- color: $ink-muted;
- font-size: clamp(16px, 26rpx, 20px);
- text-align: center;
-}
-.region-sheet__column-heading + .region-sheet__column-heading {
- border-left: 1rpx solid rgba(128, 89, 49, 0.16);
-}
-.region-sheet__picker-view {
- width: 100%;
- height: 520rpx;
-}
-.region-sheet__picker-item {
- box-sizing: border-box;
- height: 104rpx;
- overflow: hidden;
- padding: 0 6rpx;
- color: $ink-muted;
- font-size: clamp(16px, 26rpx, 20px);
- line-height: 104rpx;
- text-align: center;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.region-sheet__picker-item--selected {
- color: $brand-red;
- font-weight: 700;
-}
-.region-sheet__footer {
- display: flex;
- align-items: center;
- margin-top: 22rpx;
- gap: 12rpx;
-}
-.region-sheet__cancel {
- min-width: 116rpx;
- padding: 20rpx 10rpx;
- color: $ink-muted;
- font-size: clamp(16px, 28rpx, 20px);
- text-align: center;
-}
-.region-sheet__confirm {
- flex: 1;
- height: 82rpx;
- margin: 0;
- padding: 0;
- border: 0;
- border-radius: 10rpx;
- background: $brand-red;
- color: #fff;
- font-size: clamp(16px, 29rpx, 20px);
- font-weight: 700;
- line-height: 82rpx;
-}
-.region-sheet__confirm::after {
- display: none;
-}
diff --git a/pages/genealogy/g06-search-genealogies.vue b/pages/genealogy/g06-search-genealogies.vue
deleted file mode 100644
index d581c44..0000000
--- a/pages/genealogy/g06-search-genealogies.vue
+++ /dev/null
@@ -1,232 +0,0 @@
-
-
-
-
-
- 公开家谱以下结果来自服务端公开家谱列表。
-
- 暂时无法读取公开家谱
- 暂未找到公开家谱
-
-
- {{ item.name }}{{ item.surname }}氏
- {{
- item.location
- }}
- {{ item.intro }}
-
-
-
-
-
-
-
-
-
-
diff --git a/pages/genealogy/g09-my-applications.vue b/pages/genealogy/g09-my-applications.vue
deleted file mode 100644
index 0c14e3f..0000000
--- a/pages/genealogy/g09-my-applications.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
-
-
-
-
-
- 暂时无法读取我的申请
- 暂无加入申请申请记录会直接从服务端读取。
- 已读取 {{ applications.length }} 条申请申请详情接口尚未开放,当前不会用本地数据补造申请内容。
-
-
-
-
-
-
-
diff --git a/pages/genealogy/g10-application-review.vue b/pages/genealogy/g10-application-review.vue
deleted file mode 100644
index c2148f6..0000000
--- a/pages/genealogy/g10-application-review.vue
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
-
-
- 申请审核入口无效
-
- 暂时无法读取待审核申请
- 暂无待审核申请待审核记录会直接从服务端读取。
- 已读取 {{ applications.length }} 条待审核申请审核详情接口尚未开放,当前不会用本地数据补造申请内容或审核结果。
-
-
-
-
-
-
-
diff --git a/pages/genealogy/g12-generation-poems.vue b/pages/genealogy/generation-poems.vue
similarity index 73%
rename from pages/genealogy/g12-generation-poems.vue
rename to pages/genealogy/generation-poems.vue
index e78a2e9..13f2a76 100644
--- a/pages/genealogy/g12-generation-poems.vue
+++ b/pages/genealogy/generation-poems.vue
@@ -1,4 +1,3 @@
-
@@ -45,29 +44,37 @@
继续加载后续字辈(剩余 {{ remainingPoemCount }} 代)
-
+
{{ canManage ? "继续维护字辈诗" : "读取维护列表" }}
+ />{{ canManage ? "继续维护字辈诗" : "查看字辈列表" }}
- 服务端批量维护
- 录入完整字辈序列
+ 统一维护
+ 填写完整字辈
提交前会先请求服务端批量预览。无分隔符时每个字符对应一代;也可用空格、逗号、分号、顿号、斜杠或竖线分隔多字字辈。保存前可先查看调整结果。没有分隔符时,每个字对应一代;多个字可用空格、逗号、分号、顿号、斜杠或竖线分开。
字辈内容