diff --git a/.gitignore b/.gitignore
index d30063c..0e41af8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,8 @@
/docs/design/screens/runtime/
/tmp-g01-icon-audit.png
/node_modules/
+**/node_modules/
+/design-pipeline/generated/
+/docs/design/assets/a01-vnext/candidates/
+/docs/design/assets/a01-vnext/review/
/.superpowers/
diff --git a/components/tree/TreeMemberForm.vue b/components/tree/TreeMemberForm.vue
index 6cebac6..7ce6d8d 100644
--- a/components/tree/TreeMemberForm.vue
+++ b/components/tree/TreeMemberForm.vue
@@ -43,6 +43,20 @@
+
+
+
+
+
+ 关系校验规则
+ 关系冲突说明
+ 同一成员不能同时存在两个生父,也不能形成上下代循环。请返回世系树核对原关系后再调整。
+
+ 我知道了
+
+
+
+
@@ -91,6 +105,7 @@ const query = (() => {
return pages[pages.length - 1]?.options || {}
})()
const formState = ref(query.state === 'success' ? 'success' : query.state === 'conflict' ? 'conflict' : query.state === 'error' ? 'error' : 'form')
+const conflictHelpVisible = ref(false)
const resultCopy = computed(() => ({
success: { eyebrow: '本地预览已更新', title: `${config.value.pageTitle}内容已保存`, copy: '返回世系树后可查看新的展示位置;正式数据将在接口阶段接入。' },
conflict: { eyebrow: '发现关系冲突', title: '这段关系会造成代际矛盾', copy: '目标成员已经存在父级关系,请先核对原关系,再决定是否调整。' },
@@ -104,7 +119,7 @@ const saveForm = () => {
}
formState.value = 'success'
}
-const showConflictHelp = () => uni.showModal({ title: '关系冲突说明', content: '同一成员不能同时存在两个生父或形成上下代循环。请返回世系树核对原关系。', showCancel: false })
+const showConflictHelp = () => { conflictHelpVisible.value = true }
diff --git a/design-pipeline/manifests/a01.json b/design-pipeline/manifests/a01.json
new file mode 100644
index 0000000..da11926
--- /dev/null
+++ b/design-pipeline/manifests/a01.json
@@ -0,0 +1,119 @@
+{
+ "schemaVersion": 1,
+ "canvas": {
+ "width": 412,
+ "height": 915
+ },
+ "shared": {
+ "brandSeal": "static/assets/foundation/transparent/brand-seal.png",
+ "lock": "static/assets/modules/auth/transparent/a01-icon-lock-v1.png",
+ "wechat": "static/assets/foundation/transparent/auth-wechat.png",
+ "agreementUnchecked": "static/assets/modules/auth/transparent/a02-agreement-unchecked.png"
+ },
+ "states": [
+ { "name": "password-hidden", "mode": "password", "passwordVisible": false },
+ { "name": "password-visible", "mode": "password", "passwordVisible": true },
+ { "name": "sms-default", "mode": "sms", "countdown": 0 },
+ { "name": "sms-countdown", "mode": "sms", "countdown": 56 }
+ ],
+ "assets": [
+ {
+ "id": "header",
+ "source": "static/assets/modules/auth/opaque/a01-vnext-header-v1.png",
+ "output": "static/assets/modules/auth/opaque/a01-vnext-header-v1.png",
+ "width": 824,
+ "height": 340,
+ "alpha": false,
+ "maxBytes": 1200000
+ },
+ {
+ "id": "scroll",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-scroll-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-scroll-v1.png",
+ "width": 784,
+ "height": 1544,
+ "alpha": true,
+ "maxBytes": 4500000
+ },
+ {
+ "id": "titleOrnament",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-title-ornament-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-title-ornament-v1.png",
+ "width": 376,
+ "height": 130,
+ "alpha": true,
+ "maxBytes": 350000
+ },
+ {
+ "id": "divider",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-divider-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-divider-v1.png",
+ "width": 540,
+ "height": 90,
+ "alpha": true,
+ "maxBytes": 350000
+ },
+ {
+ "id": "primaryButton",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-primary-button-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-primary-button-v1.png",
+ "width": 584,
+ "height": 130,
+ "alpha": true,
+ "maxBytes": 350000
+ },
+ {
+ "id": "secondaryButton",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-secondary-button-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-secondary-button-v1.png",
+ "width": 584,
+ "height": 120,
+ "alpha": true,
+ "maxBytes": 350000
+ },
+ {
+ "id": "phone",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-phone-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-phone-v1.png",
+ "width": 120,
+ "height": 120,
+ "alpha": true,
+ "maxBytes": 120000
+ },
+ {
+ "id": "eyeOpen",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-eye-open-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-eye-open-v1.png",
+ "width": 168,
+ "height": 128,
+ "alpha": true,
+ "maxBytes": 150000
+ },
+ {
+ "id": "eyeClosedPupil",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-eye-closed-pupil-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-eye-closed-pupil-v1.png",
+ "width": 168,
+ "height": 140,
+ "alpha": true,
+ "maxBytes": 160000
+ },
+ {
+ "id": "smsThreeDots",
+ "source": "static/assets/modules/auth/transparent/a01-vnext-sms-three-dots-v1.png",
+ "output": "static/assets/modules/auth/transparent/a01-vnext-sms-three-dots-v1.png",
+ "width": 120,
+ "height": 120,
+ "alpha": true,
+ "maxBytes": 120000
+ }
+ ],
+ "previews": [
+ { "state": "password-hidden", "output": "design-pipeline/generated/a01/A01-password-412x915.png", "width": 412, "height": 915 },
+ { "state": "sms-default", "output": "design-pipeline/generated/a01/A01-sms-412x915.png", "width": 412, "height": 915 },
+ { "state": "contact", "output": "design-pipeline/generated/a01/A01-password-sms-contact-824x915.png", "width": 824, "height": 915 },
+ { "state": "password-hidden", "output": "design-pipeline/generated/a01/A01-password-320x568.png", "width": 320, "height": 568 },
+ { "state": "password-hidden", "output": "design-pipeline/generated/a01/A01-password-360x640.png", "width": 360, "height": 640 },
+ { "state": "password-hidden", "output": "design-pipeline/generated/a01/A01-password-360x800.png", "width": 360, "height": 800 }
+ ]
+}
diff --git a/design-pipeline/package-lock.json b/design-pipeline/package-lock.json
new file mode 100644
index 0000000..f675a61
--- /dev/null
+++ b/design-pipeline/package-lock.json
@@ -0,0 +1,608 @@
+{
+ "name": "jiapu-design-pipeline",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "jiapu-design-pipeline",
+ "dependencies": {
+ "sharp": "0.34.5"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.7.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ }
+ }
+}
diff --git a/design-pipeline/package.json b/design-pipeline/package.json
new file mode 100644
index 0000000..9eb86a4
--- /dev/null
+++ b/design-pipeline/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "jiapu-design-pipeline",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build:a01": "node scripts/build-a01.mjs"
+ },
+ "dependencies": {
+ "sharp": "0.34.5"
+ }
+}
diff --git a/design-pipeline/scripts/build-a01.mjs b/design-pipeline/scripts/build-a01.mjs
new file mode 100644
index 0000000..512fd31
--- /dev/null
+++ b/design-pipeline/scripts/build-a01.mjs
@@ -0,0 +1,159 @@
+import { createHash } from 'node:crypto'
+import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import sharp from 'sharp'
+
+const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
+const workspace = path.resolve(scriptDirectory, '..', '..')
+const manifestPath = path.join(workspace, 'design-pipeline', 'manifests', 'a01.json')
+const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
+const generatedDirectory = path.join(workspace, 'design-pipeline', 'generated', 'a01')
+
+const resolveWorkspacePath = (relativePath) => {
+ const absolutePath = path.resolve(workspace, relativePath)
+ const relative = path.relative(workspace, absolutePath)
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
+ throw new Error(`路径越出工作区:${relativePath}`)
+ }
+ return absolutePath
+}
+
+const sha256 = (buffer) => createHash('sha256').update(buffer).digest('hex')
+
+const writeVersionedFile = async (outputPath, buffer) => {
+ await mkdir(path.dirname(outputPath), { recursive: true })
+ try {
+ const current = await readFile(outputPath)
+ if (sha256(current) !== sha256(buffer)) {
+ throw new Error(`拒绝覆盖内容不同的版本化输出:${path.relative(workspace, outputPath)}`)
+ }
+ return
+ } catch (error) {
+ if (error.code !== 'ENOENT') throw error
+ }
+ await writeFile(outputPath, buffer)
+}
+
+const runtimeAssets = new Map()
+const reportAssets = []
+
+for (const asset of manifest.assets) {
+ const sourcePath = resolveWorkspacePath(asset.source)
+ const outputPath = resolveWorkspacePath(asset.output)
+ await stat(sourcePath)
+ const pipeline = sharp(sourcePath).resize(asset.width, asset.height, { fit: 'fill' })
+ if (!asset.alpha) pipeline.flatten({ background: '#f4eadc' })
+ const buffer = await pipeline.png({ compressionLevel: 9, adaptiveFiltering: true, palette: false }).toBuffer()
+ await writeVersionedFile(outputPath, buffer)
+ runtimeAssets.set(asset.id, buffer)
+ const metadata = await sharp(buffer).metadata()
+ reportAssets.push({
+ id: asset.id,
+ output: asset.output,
+ width: metadata.width,
+ height: metadata.height,
+ bytes: buffer.length,
+ sha256: sha256(buffer)
+ })
+}
+
+const loadShared = async (key) => readFile(resolveWorkspacePath(manifest.shared[key]))
+const shared = {
+ brandSeal: await loadShared('brandSeal'),
+ lock: await loadShared('lock'),
+ wechat: await loadShared('wechat'),
+ agreementUnchecked: await loadShared('agreementUnchecked')
+}
+
+const svgLayer = (mode) => Buffer.from(`
+`)
+
+const sized = async (input, width, height) => sharp(input).resize(width, height, { fit: 'fill' }).png().toBuffer()
+
+const buildBasePreview = async (mode) => {
+ const stateIcon = mode === 'password' ? shared.lock : runtimeAssets.get('smsThreeDots')
+ const rightIcon = mode === 'password' ? runtimeAssets.get('eyeClosedPupil') : null
+ const layers = [
+ { input: await sized(runtimeAssets.get('header'), 412, 170), left: 0, top: 0 },
+ { input: await sized(runtimeAssets.get('scroll'), 392, 772), left: 10, top: 143 },
+ { input: await sized(shared.brandSeal, 72, 86), left: 170, top: 35 },
+ { input: await sized(runtimeAssets.get('titleOrnament'), 126, 44), left: 143, top: 235 },
+ { input: await sized(runtimeAssets.get('divider'), 180, 30), left: 116, top: 330 },
+ { input: await sized(runtimeAssets.get('phone'), 40, 40), left: 68, top: 420 },
+ { input: await sized(stateIcon, 40, 40), left: 68, top: 522 },
+ { input: await sized(runtimeAssets.get('primaryButton'), 292, 65), left: 60, top: 608 },
+ { input: await sized(runtimeAssets.get('secondaryButton'), 292, 60), left: 60, top: 718 },
+ { input: await sized(shared.wechat, 36, 36), left: 126, top: 730 },
+ { input: await sized(shared.agreementUnchecked, 24, 24), left: 62, top: 831 },
+ { input: svgLayer(mode), left: 0, top: 0 }
+ ]
+ if (rightIcon) layers.splice(7, 0, { input: await sized(rightIcon, 42, 35), left: 312, top: 525 })
+ return sharp({ create: { width: 412, height: 915, channels: 4, background: '#f4eadc' } })
+ .composite(layers)
+ .png({ compressionLevel: 9, adaptiveFiltering: true })
+ .toBuffer()
+}
+
+await mkdir(generatedDirectory, { recursive: true })
+const passwordPreview = await buildBasePreview('password')
+const smsPreview = await buildBasePreview('sms')
+
+const writeGenerated = async (relativePath, buffer) => {
+ const outputPath = resolveWorkspacePath(relativePath)
+ await mkdir(path.dirname(outputPath), { recursive: true })
+ await writeFile(outputPath, buffer)
+}
+
+await writeGenerated('design-pipeline/generated/a01/A01-password-412x915.png', passwordPreview)
+await writeGenerated('design-pipeline/generated/a01/A01-sms-412x915.png', smsPreview)
+const contact = await sharp({ create: { width: 824, height: 915, channels: 4, background: '#ffffff' } })
+ .composite([{ input: passwordPreview, left: 0, top: 0 }, { input: smsPreview, left: 412, top: 0 }])
+ .png({ compressionLevel: 9, adaptiveFiltering: true })
+ .toBuffer()
+await writeGenerated('design-pipeline/generated/a01/A01-password-sms-contact-824x915.png', contact)
+
+for (const preview of manifest.previews.filter((item) => item.width !== 412 && item.width !== 824)) {
+ const compact = await sharp(passwordPreview)
+ .resize(preview.width, preview.height, { fit: 'fill' })
+ .png({ compressionLevel: 9, adaptiveFiltering: true })
+ .toBuffer()
+ await writeGenerated(preview.output, compact)
+}
+
+const buildReport = {
+ schemaVersion: 1,
+ manifest: path.relative(workspace, manifestPath).replaceAll('\\', '/'),
+ assets: reportAssets,
+ generatedAt: new Date().toISOString()
+}
+await writeGenerated('design-pipeline/generated/a01/build-report.json', Buffer.from(`${JSON.stringify(buildReport, null, 2)}\n`))
+
+console.log('A01-CODE-PIPELINE BUILD PASS')
diff --git a/design-qa.md b/design-qa.md
index 6cb4350..07770a4 100644
--- a/design-qa.md
+++ b/design-qa.md
@@ -1,3 +1,467 @@
+# Design QA — T04 新增亲属
+
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/T04-current-form-success-contact-720x800.png`
+- viewport: 360×800;成员链路 smoke 覆盖 T04–T08 关键路由与状态
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。新增亲属表单的关系、姓名、出生日期和性别层级清楚,成功结果与表单使用同一纸面体系。共用成员表单中残留的原生 `uni.showModal` 已替换为项目自定义冲突说明层。
+
+## Evidence limits
+
+当前是 H5 内部候选;关系选择器、真实保存、键盘与 Android 返回手势仍待功能阶段和设备复核,用户视觉验收尚未进行。
+
+# Design QA — T01 世系树
+
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/T01-current-tree-empty-contact-720x800.png`
+- viewport: 360×800;运行 smoke 覆盖横向提示、空、失败、树态、节点选择和成员档案跳转
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 树态使用横向可拖动画布表达同代支系,页面本身无横向溢出;首屏保留阅读提示、代际标识、节点层级与选中成员操作条。
+- 空态说明内容产生方式,只突出“录入首代成员”;加载失败与横向提示继续由 T01 同页状态承接。
+- 移除了未注册路由但仍残留的 T02 旧占位文件,T01 成为世系阅读状态的唯一所有者。
+
+## Evidence limits
+
+当前是 H5 内部候选,不证明真实世系接口、超大族谱性能或 Android 手势已通过;用户验收和 Android/HBuilderX 复核仍待进行。
+
+## Implementation checklist
+
+- [x] T01 聚焦静态合同
+- [x] T01 H5 运行 smoke
+- [x] 树态/空态真实运行联系表及人工审视
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户视觉验收
+
+# Design QA — G11 家谱设置/G12 字辈诗
+
+- G11 representative contact sheet: `docs/design/screens/handoff/2026-07-16/G11-current-form-permission-contact-720x800.png`
+- G12 representative contact sheet: `docs/design/screens/handoff/2026-07-16/G12-current-list-edit-contact-720x800.png`
+- viewport: 360×800;运行 smoke 覆盖列表、空、编辑、成功、失败与无权限
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- G11:名称、公开范围、访问说明保持接口可解释边界;名称错误就近显示,保存反馈为项目自定义,无权限独立表达。
+- G12:列表、空、编辑、校验、保存失败、无权限均有位置;连续文本预览与缺失旧世代策略保持清晰。
+- 两页均移除原生 Toast,保留现有完整纸面板、按钮和输入资产。
+
+## Evidence limits
+
+当前保存只更新本地候选,不证明设置/字辈接口、权限或并发编辑已经接通;Android 仍待设备复核。
+
+## Implementation checklist
+
+- [x] G11 表单/成功/错误/无权限与自定义反馈
+- [x] G12 列表/空/编辑/校验/失败/无权限与自定义反馈
+- [x] H5 真实运行烟测和联系表
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G10 入谱审核
+
+- source visual truth path: 现有审核记录卡视觉与 `docs/验收规划.md` G10 状态合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G10-current-list-360x800.png`、`G10-current-no-permission-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G10-current-list-permission-contact-720x800.png`
+- viewport: 360×800;运行 smoke 覆盖说明、确认和结果反馈
+- state: 列表、空、失败、无权限、通过确认、拒绝确认、说明
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 审核操作:每条待审记录保留拒绝/通过,二者都必须进入自定义二次确认后才更新结果。
+- 自定义反馈:顶栏审核说明、确认弹层和处理结果全部使用项目组件,已移除原生 Modal/Toast。
+- 权限状态:无权限与网络失败分开表达,避免把角色限制误报成加载错误。
+- 卡片信息:姓名、脱敏手机号、关系说明和提交时间保持同屏可核对。
+
+## Evidence limits
+
+当前审核结果只更新本地候选卡片,不证明服务端权限、并发审核、消息通知或成员身份写入已经完成。
+
+## Implementation checklist
+
+- [x] 列表/空/失败/无权限
+- [x] 自定义通过/拒绝确认
+- [x] 自定义审核说明与结果反馈
+- [x] H5 真实运行烟测
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G05 成员总览/公开预览
+
+- source visual truth path: 已选 G05 总览位图与 `docs/验收规划.md` 公开/成员权限合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G05-current-member-owner-360x800.png`、`G05-current-public-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G05-current-member-public-contact-720x800.png`
+- viewport: 360×800;运行 smoke 另查 320×568、412×915
+- state: 公开预览、成员/所有者、加载、空、失败、无权限
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 公开边界:只展示身份、来源、地区、堂号、支系、上级谱和公开说明;不渲染成员统计、世系或管理入口。
+- 申请入口:公开态底部唯一主操作进入 G08 搜索来源。
+- 角色差异:所有者显示四个业务入口与顶栏管理;普通成员只显示世系和字辈,不显示录入、审核或设置。
+- 视觉层级:世系入口不再单独用朱砂强调,保持“总览优先、世系次级”的产品层级。
+- 实现边界:移除远程 API,加载/错误/无权限均为同页本地样式状态。
+
+## Evidence limits
+
+当前公开字段和角色由本地候选决定,不证明真实接口字段级权限、成员身份、管理权限或全局家谱上下文已接通。
+
+## Implementation checklist
+
+- [x] 公开预览最小字段与申请入口
+- [x] 成员/所有者入口差异
+- [x] loading/empty/error/no-permission 同页状态
+- [x] 三档 H5 响应式与真实运行烟测
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G03 创建家谱/首代人物
+
+- source visual truth path: 现有 G03 两步题签设计与 `docs/验收规划.md` 最新创建合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G03-current-create-360x800.png`、`G03-current-ancestor-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G03-current-create-ancestor-contact-720x800.png`
+- viewport: 360×800;运行 smoke 另查 320×568、412×915
+- state: 创建表单/校验/重复建谱提醒/提交中/失败,首代恢复/校验/保存中/失败/成功
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 创建字段:姓氏、谱名、堂号、所在地和访问规则齐全,访问规则提供仅成员可见/可搜索申请两个明确候选。
+- 防重复建谱:本地校验通过后先打开自定义提醒层,可先去 G06 搜索或确认继续创建。
+- 中断恢复:已创建未录始祖继续使用同一路由 `step=ancestor&genealogyId=`,不重复建谱。
+- 完成路径:首代保存成功显示自定义结果并进入 G05 总览,不再错误直跳 T01。
+- 实现边界:移除本页远程 API 和原生 UniApp 提示,当前只审核页面样式状态。
+
+## Evidence limits
+
+当前创建 ID、保存结果和失败状态均为本地候选,不证明服务端创建、重复谱检索、始祖持久化或断点恢复已经接通。
+
+## Implementation checklist
+
+- [x] 创建字段与页内校验
+- [x] 自定义已有谱提醒和访问规则候选
+- [x] 两步提交中/失败/成功与中断恢复外观
+- [x] 完成后进入 G05
+- [x] 三档 H5 响应式与真实运行烟测
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G09 我的申请
+
+- source visual truth path: 现有申请状态题签视觉与 `docs/验收规划.md` G09 状态合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G09-current-list-360x800.png`、`G09-current-empty-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G09-current-list-empty-contact-720x800.png`
+- viewport: 360×800;运行 smoke 同时验证交互路径
+- state: 待审、通过、拒绝、撤回、修改后重提、空、失败、加载
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 状态动作:待审只显示撤回,通过只显示进入家谱,拒绝只显示修改后重新提交;状态与动作分行显示,不再重叠。
+- 撤回流程:使用自定义确认层,确认后本地状态变为已撤回且不再显示审核动作。
+- 来源边界:空态明确只有搜索申请进入本页,邀请码直接加入不生成审核记录。
+- 路由语义:通过进入 G05;拒绝携带 `source=search&previous=rejected` 返回 G08。
+
+## Evidence limits
+
+当前记录和撤回结果为本地样式状态,不证明服务端撤回、审核同步、消息通知或列表现场恢复已经完成。
+
+## Implementation checklist
+
+- [x] 待审/通过/拒绝/撤回状态
+- [x] 状态唯一动作与自定义撤回确认
+- [x] 修改后重提和通过后进入家谱路径
+- [x] loading/empty/error 样式与运行烟测
+- [ ] 320/412 Android 真机专项复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G08 搜索申请/邀请码直加入谱
+
+- source visual truth path: 现有入谱表单题签视觉与 `docs/验收规划.md` 双来源合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G08-current-search-360x800.png`、`G08-current-invite-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G08-current-search-invite-contact-720x800.png`
+- viewport: 360×800;运行 smoke 另查 320×568、412×915
+- state: 搜索申请/邀请直加表单、页内错误、提交中、成功、失败、来源对应下一步
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 单一来源合同:页面通过 `sourceContract` 集中管理两种来源的标题、说明、第三字段、按钮、结果和下一步,避免“申请/直接加入”文案混用。
+- 字段边界:真实姓名、以已知长辈为参照的文字关系必填;没有虚构接口尚不支持的成员选择器或结构化关系。
+- 搜索来源:第三字段为申请说明,成功后去 G09 查看审核进度。
+- 邀请来源:第三字段为可选补充信息,明确无需审核;成功后返回 G01 并携带选中家谱。
+- 错误反馈:必填错误就近显示,重复点击提交被拦截,没有原生 UniApp 提示。
+
+## Evidence limits
+
+当前提交结果为本地样式状态,不证明真实申请、邀请码直加、重复提交防护服务端逻辑或 G01 全局上下文刷新已经接通。
+
+## Implementation checklist
+
+- [x] 搜索/邀请双来源单一合同
+- [x] 真实姓名与参照长辈文字关系校验
+- [x] 来源对应第三字段、结果和下一步
+- [x] 本地提交中/成功/失败与三档响应式
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G06 搜索/邀请码加入
+
+- source visual truth path: 已选 G06 题签检索视觉与 `docs/验收规划.md` 最新双模式/关系状态合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G06-current-initial-360x800.png`、`G06-current-results-360x800.png`、`G06-current-invite-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G06-current-initial-results-invite-contact-1080x800.png`
+- viewport: 360×800;运行 smoke 另查 320×568、412×915
+- state: 搜索初始/加载/结果/空/失败/清空,邀请码初始/无效/有效目标,六种用户关系动作
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 双模式:同一路由明确区分“搜索家谱”和“邀请码加入”,从 G01 邀请入口可直接进入邀请码状态。
+- 识别信息:结果卡同时展示谱名、姓氏、地区、堂号、上级谱、支系、管理/认证、成员数和更新时间,降低同名谱误选。
+- 关系动作:未加入、已加入、审核中、被拒绝、已退出/移除、我创建的六种状态各只有一个明确动作,不再统一显示“可申请”。
+- 公开预览:未加入用户点结果卡进入 G05 `mode=public`;申请按钮单独进入 G08 搜索来源。
+- 邀请流程:无效邀请码就近显示;有效目标明确提示确认关系后直接加入、无需等待审核。
+- 实现边界:移除远程 `appApi`,当前只用本地展示数据审核样式;没有原生 UniApp 提示。
+
+## Evidence limits
+
+当前邀请码 `JP2026` 和六种结果均为样式数据,不证明真实搜索、邀请码有效性、成员继承规则、认证信息或接口权限已经接通。
+
+## Implementation checklist
+
+- [x] 搜索/邀请码双模式
+- [x] 详细结果字段和六种关系动作
+- [x] 搜索五类状态、清空与地区筛选位置
+- [x] 邀请码无效/有效目标与 G08 来源合同
+- [x] 三档 H5 响应式、真实点击与联系表
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — G01 我的家谱重新审核
+
+- source visual truth path: 历史 G01 成熟视觉基准与 `docs/验收规划.md` 最新 G01 状态合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/G01-current-default-360x800.png`、`docs/design/screens/runtime/2026-07-16/G01-current-empty-360x800.png`
+- representative contact sheet: `docs/design/screens/handoff/2026-07-16/G01-current-default-empty-contact-720x800.png`
+- viewport: 360×800;运行 smoke 另查 320×568、412×915
+- state: 默认三分组、空态三入口、loading、error、添加家谱弹层、当前谱切换层
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 视觉策略:保留现有成熟的朱砂顶栏、宣纸、题签、金线图标和水墨页脚,不为重审无必要地更换风格。
+- 空态分流:搜索家谱为朱砂主入口,邀请码为快捷入口,创建为三级入口并明确提示先确认无现有家谱;三者始终同屏可见。
+- 默认状态:我创建的、我加入的、加入申请三组均存在,加入申请示例覆盖审核中、被拒绝和已退出的位置与语义。
+- 页面操作:添加家谱不再直接跳创建页,而是打开自定义三入口弹层;当前大卡支持自定义切换层。
+- 兼容性:三档无横向溢出,底部 Tabbar 保留内容安全距离;短屏正常滚动。
+
+## Evidence limits
+
+当前使用本地展示数据验证页面样式,不代表全局家谱上下文已经持久化,也不代表真实申请状态、接口加载、Android 返回现场恢复或用户验收完成。
+
+## Implementation checklist
+
+- [x] 空态搜索/邀请码/创建三入口及优先级
+- [x] 默认三分组与关键申请状态
+- [x] loading/error 与自定义重新加载
+- [x] 添加家谱弹层和当前谱切换层
+- [x] 三档 H5 响应式、真实点击与截图 QA
+- [ ] 全局家谱上下文真实持久化
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — A06 阻断登录状态
+
+- source visual truth path: A01/A04/A05 红色宗祠水墨认证系列与 `docs/验收规划.md` A06 最新产品合同
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A06-current-risk-412x915.png`
+- viewport: 412×915;运行 smoke 另查 320×568、360×800
+- state: 账号冻结、账号停用、风险限制、未知旧状态回落、自定义恢复说明层
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 产品边界:旧 normal/failed/register/wechat 状态已移除;A06 仅保留冻结、停用、风险限制,普通登录错误继续由 A01 就近承接。
+- 信息完整性:每种状态明确展示限制原因、影响范围和恢复方式,不只给出模糊失败提示。
+- 视觉与反馈:保持认证系列底板和品牌印;恢复入口使用自定义说明层,不调用原生 Toast/Modal。
+- 尺寸适配:三档无横向溢出,320×568 按正常比例自然滚动。
+
+## Evidence limits
+
+当前结论仅为 HBuilderX H5 + Chrome 内部 QA,不等于用户验收;真实状态来源、账号申诉服务、Android 返回手势和字体放大仍待功能/真机阶段。
+
+## Implementation checklist
+
+- [x] 只保留冻结、停用、风险限制
+- [x] 原因、影响范围、恢复入口完整
+- [x] 未知旧状态安全回落到风险限制
+- [x] 自定义恢复说明层与返回 A01
+- [x] 三档 H5 响应式和运行烟测
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — A05 重设密码
+
+- source visual truth path: A01/A04 红色宗祠水墨认证系列
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A05-current-reset-412x915.png`
+- viewport: 412×915;运行 smoke 另查 320×568、360×800
+- state: 默认表单、手机号错误、发送验证码前验证、验证关闭保值、发送反馈、密码不一致、自定义成功结果
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 视觉连续性:使用同一宗祠水墨底板、独立品牌印、朱砂主按钮和宣纸表单,旧大面板已移除。
+- 流程顺序:获取验证码先校验手机号,再打开自定义行为验证;重设提交不重复验证。
+- 错误与结果:四字段错误就近显示,验证码发送反馈和重设成功结果均为项目内自定义组件,不使用原生 UniApp 提示。
+- 尺寸适配:三档无横向溢出,320×568 保持正常比例并自然滚动。
+
+## Evidence limits
+
+当前结论仅为 HBuilderX H5 + Chrome 内部 QA,不等于用户验收;真实短信、行为验证令牌、重设接口、Android 软键盘与成功后手机号回填属于后续功能/真机复核。
+
+## Implementation checklist
+
+- [x] A01/A04 同系列视觉与真实资产
+- [x] 发送验证码前自定义行为验证
+- [x] 关闭验证保留表单与自定义发送反馈
+- [x] 四字段页内校验与自定义成功结果
+- [x] 三档 H5 响应式和运行烟测
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — A04 注册账号
+
+- source visual truth path: A01 已选红色宗祠水墨视觉体系与 `docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png`
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A04-current-register-412x915.png`
+- viewport: 412×915;运行 smoke 另查 320×568、360×800
+- state: 默认注册、空表单错误、协议错误、自定义行为验证、自定义反馈
+
+## Findings
+
+没有遗留的 H5 P0/P1/P2 问题。
+
+- 视觉连续性:沿用 A01 的红色宗祠、水墨宣纸、独立 `brand-seal.png`、朱砂按钮与古金分隔,不再使用旧大厅与大面板。
+- 表单层级:手机号、设置密码、确认密码三行保持清晰标签和输入提示;错误信息就近显示,不调用原生 UniApp 提示。
+- 尺寸适配:三档宽高无横向溢出;320×568 按最新决定保持正常比例并自然纵向滚动。
+- 流程验证:A01 注册入口、三字段校验、协议校验、自定义验证层、自定义反馈以及返回登录均通过浏览器运行烟测。
+
+## Evidence limits
+
+当前结论仅为 HBuilderX H5 + Chrome 内部 QA,通过不等于用户验收;Android 状态栏、安全区、软键盘、字体放大和返回手势仍需真机或模拟器复核。
+
+## Implementation checklist
+
+- [x] A01 同系列视觉与真实资产
+- [x] 三字段就近校验与协议页内错误
+- [x] 自定义验证层与反馈提示
+- [x] 320×568 自然滚动、360×800 与 412×915 无横向溢出
+- [x] H5 静态契约与真实运行烟测
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed (internal H5 candidate QA only; user review pending)
+
+---
+
+# Design QA — A01 红色宗祠水墨登录页
+
+- source visual truth path: `docs/design/screens/handoff/2026-07-16/A01-selected-password-red-hall-ink-412x915.png`、`docs/design/screens/handoff/2026-07-16/A01-selected-sms-red-hall-ink-412x915.png`
+- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A01-password-final-412x915.png`、`docs/design/screens/runtime/2026-07-16/A01-sms-final-412x915.png`
+- viewport: 412×915;另查 320×568、360×640、360×800
+- state: 密码登录、短信验证码登录;运行 smoke 另查协议页内错误与自定义行为验证层
+- full-view comparison evidence: `docs/design/screens/runtime/2026-07-16/A01-password-compare-final.png`、`docs/design/screens/runtime/2026-07-16/A01-sms-compare-final.png`
+- focused region comparison evidence: 全页比较以原始 412px 宽度并排显示,顶部宗祠、Logo、输入图标和协议文字均可辨认;没有需要额外放大的高密度区域。
+
+## Findings
+
+没有遗留的 P0/P1/P2 问题。
+
+- 字体与层级:标题、Tab、输入文案、主按钮和辅助文案已形成与选定稿一致的层级;中文字体受 H5 系统宋体渲染影响存在轻微字形差异,归为 P3。
+- 间距与布局:第二轮已扩大标题和表单节奏,协议区不再过早结束;四档宽度无横向溢出,320×568 按用户最新决定自然纵向滚动。
+- 颜色与令牌:朱砂红、暗红宗祠、象牙宣纸、淡墨山水和旧金线条与选定稿一致。
+- 图片质量:宗祠水墨底板为 824×1830 PNG;`brand-seal.png` 独立叠放,未让生成模型重绘“谱”字;锁、闭眼瞳孔、短信气泡三点和微信均使用真实资产。
+- 文案与内容:密码/短信标签、短信验证码、获取验证码、忘记密码、微信登录、注册和协议文案齐全。
+- 交互与可达性:密码/短信状态切换、密码显隐、协议页内错误、自定义行为验证层均通过浏览器 smoke;截图不能证明读屏顺序和 Android 真机触摸体验。
+
+## Comparison history
+
+1. v1 P1:标题、Tab、输入和按钮文字明显偏小,表单整体比设计稿提前约 90px 结束;已扩大字号并重排垂直节奏。
+2. v2 P2:宗祠区比设计稿高约 17px;已对新底板分段缩放,保持总尺寸不变。
+3. v3 P2:短信气泡、微信图标偏小且输入图标槽位偏右;已在固定槽位内调整显示比例和视觉位移。
+4. final:密码/短信同尺寸比较未发现新的 P0/P1/P2。
+
+## Evidence limits
+
+本轮是 HBuilderX H5 + Chrome 调试截图,不能替代 Android/HBuilderX 真机或模拟器对状态栏、安全区、软键盘、系统字体放大、读屏和返回手势的最终复核,也不代表用户已经验收。
+
+## Implementation checklist
+
+- [x] 密码/短信共用红色宗祠水墨底板
+- [x] 独立使用现有 `brand-seal.png`
+- [x] 密码锁、闭眼瞳孔和横线下方忘记密码
+- [x] 短信气泡三点和获取验证码
+- [x] 320×568 自然滚动与四档无横向溢出
+- [x] 协议页内错误和自定义行为验证层 smoke
+- [ ] Android/HBuilderX 真机或模拟器复核
+- [ ] 用户验收
+
+final result: passed
+
+---
+
# Design QA — G05 家谱总览
- source visual truth path: `docs/design/screens/runtime/2026-07-14/g-module-audit/before/01-g05-before-360x800.png`、`static/assets/modules/genealogy/opaque/g05-overview-surface.png`、外部思维导图与 `家谱主页.png` 的信息层级参考
diff --git a/docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd b/docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd
new file mode 100644
index 0000000..4784907
Binary files /dev/null and b/docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd differ
diff --git a/docs/design/assets/a01-vnext/source/a01-psd-manifest.json b/docs/design/assets/a01-vnext/source/a01-psd-manifest.json
new file mode 100644
index 0000000..0a16d8b
--- /dev/null
+++ b/docs/design/assets/a01-vnext/source/a01-psd-manifest.json
@@ -0,0 +1,45 @@
+{
+ "schemaVersion": 1,
+ "canvas": {
+ "width": 1236,
+ "height": 2745,
+ "logicalWidth": 412,
+ "logicalHeight": 915,
+ "scale": 3,
+ "resolution": 72
+ },
+ "groups": [
+ { "name": "00-参考", "visible": false },
+ { "name": "10-背景", "visible": true },
+ { "name": "20-卷轴", "visible": true },
+ { "name": "30-品牌与标题装饰", "visible": true },
+ { "name": "40-公共控件皮肤", "visible": true },
+ { "name": "50-密码登录", "visible": true },
+ { "name": "60-验证码登录", "visible": false },
+ { "name": "70-内容与标注", "visible": true }
+ ],
+ "layers": [],
+ "states": [
+ {
+ "name": "password-hidden",
+ "show": ["50-密码登录"],
+ "hide": ["60-验证码登录"]
+ },
+ {
+ "name": "password-visible",
+ "show": ["50-密码登录"],
+ "hide": ["60-验证码登录"]
+ },
+ {
+ "name": "sms-default",
+ "show": ["60-验证码登录"],
+ "hide": ["50-密码登录"]
+ },
+ {
+ "name": "sms-countdown",
+ "show": ["60-验证码登录"],
+ "hide": ["50-密码登录"]
+ }
+ ],
+ "exports": []
+}
diff --git a/docs/design/screens/handoff/2026-07-15/A01-selected-password-brand-seal-841x1870.png b/docs/design/screens/handoff/2026-07-15/A01-selected-password-brand-seal-841x1870.png
new file mode 100644
index 0000000..8a204cc
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-15/A01-selected-password-brand-seal-841x1870.png differ
diff --git a/docs/design/screens/handoff/2026-07-15/A01-selected-sms-brand-seal-841x1870.png b/docs/design/screens/handoff/2026-07-15/A01-selected-sms-brand-seal-841x1870.png
new file mode 100644
index 0000000..e820828
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-15/A01-selected-sms-brand-seal-841x1870.png differ
diff --git a/docs/design/screens/handoff/2026-07-15/A01-visual-source-password-selected-v2-841x1870.png b/docs/design/screens/handoff/2026-07-15/A01-visual-source-password-selected-v2-841x1870.png
new file mode 100644
index 0000000..0497fb2
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-15/A01-visual-source-password-selected-v2-841x1870.png differ
diff --git a/docs/design/screens/handoff/2026-07-15/A01-visual-source-sms-selected-v1-841x1870.png b/docs/design/screens/handoff/2026-07-15/A01-visual-source-sms-selected-v1-841x1870.png
new file mode 100644
index 0000000..e820828
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-15/A01-visual-source-sms-selected-v1-841x1870.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png b/docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png
new file mode 100644
index 0000000..1ba9a64
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/A01-selected-password-red-hall-ink-412x915.png b/docs/design/screens/handoff/2026-07-16/A01-selected-password-red-hall-ink-412x915.png
new file mode 100644
index 0000000..d45b046
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/A01-selected-password-red-hall-ink-412x915.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/A01-selected-sms-red-hall-ink-412x915.png b/docs/design/screens/handoff/2026-07-16/A01-selected-sms-red-hall-ink-412x915.png
new file mode 100644
index 0000000..b51c70c
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/A01-selected-sms-red-hall-ink-412x915.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/A04-current-register-412x915.png b/docs/design/screens/handoff/2026-07-16/A04-current-register-412x915.png
new file mode 100644
index 0000000..bdbb0b6
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/A04-current-register-412x915.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/A05-current-reset-412x915.png b/docs/design/screens/handoff/2026-07-16/A05-current-reset-412x915.png
new file mode 100644
index 0000000..56cef30
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/A05-current-reset-412x915.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/A06-current-risk-412x915.png b/docs/design/screens/handoff/2026-07-16/A06-current-risk-412x915.png
new file mode 100644
index 0000000..2c6ee72
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/A06-current-risk-412x915.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G01-current-default-empty-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G01-current-default-empty-contact-720x800.png
new file mode 100644
index 0000000..a5f344f
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G01-current-default-empty-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G03-current-create-ancestor-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G03-current-create-ancestor-contact-720x800.png
new file mode 100644
index 0000000..f2cff3f
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G03-current-create-ancestor-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G05-current-member-public-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G05-current-member-public-contact-720x800.png
new file mode 100644
index 0000000..85334cd
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G05-current-member-public-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G06-current-initial-results-invite-contact-1080x800.png b/docs/design/screens/handoff/2026-07-16/G06-current-initial-results-invite-contact-1080x800.png
new file mode 100644
index 0000000..81f24a2
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G06-current-initial-results-invite-contact-1080x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G08-current-search-invite-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G08-current-search-invite-contact-720x800.png
new file mode 100644
index 0000000..96f9da4
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G08-current-search-invite-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G09-current-list-empty-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G09-current-list-empty-contact-720x800.png
new file mode 100644
index 0000000..d242a61
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G09-current-list-empty-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G10-current-list-permission-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G10-current-list-permission-contact-720x800.png
new file mode 100644
index 0000000..0d8ad7d
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G10-current-list-permission-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G11-current-form-permission-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G11-current-form-permission-contact-720x800.png
new file mode 100644
index 0000000..abe7d17
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G11-current-form-permission-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/G12-current-list-edit-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/G12-current-list-edit-contact-720x800.png
new file mode 100644
index 0000000..688d72c
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/G12-current-list-edit-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/T01-current-tree-empty-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/T01-current-tree-empty-contact-720x800.png
new file mode 100644
index 0000000..e78a480
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/T01-current-tree-empty-contact-720x800.png differ
diff --git a/docs/design/screens/handoff/2026-07-16/T04-current-form-success-contact-720x800.png b/docs/design/screens/handoff/2026-07-16/T04-current-form-success-contact-720x800.png
new file mode 100644
index 0000000..e7c6ad1
Binary files /dev/null and b/docs/design/screens/handoff/2026-07-16/T04-current-form-success-contact-720x800.png differ
diff --git a/docs/design/视觉证据索引.md b/docs/design/视觉证据索引.md
index a29e102..000f4a9 100644
--- a/docs/design/视觉证据索引.md
+++ b/docs/design/视觉证据索引.md
@@ -2,7 +2,21 @@
## 当前换机检查点
-- A01 当前短信登录态(待审核,不代表通过):[412×915 真实运行代表图](screens/handoff/2026-07-15/A01-current-sms-generated-412x915.png)
+- A01 当前密码/短信登录联系表(待审核,不代表通过):[824×915 真实运行代表图](screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png)
+- A04 当前注册页(待审核,不代表通过):[412×915 真实运行代表图](screens/handoff/2026-07-16/A04-current-register-412x915.png)
+- A05 当前重设密码页(待审核,不代表通过):[412×915 真实运行代表图](screens/handoff/2026-07-16/A05-current-reset-412x915.png)
+- A06 当前风险限制页(待审核,不代表通过):[412×915 真实运行代表图](screens/handoff/2026-07-16/A06-current-risk-412x915.png)
+- G01 当前默认/空态联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G01-current-default-empty-contact-720x800.png)
+- G06 当前搜索初始/结果/邀请码联系表(待审核,不代表通过):[1080×800 真实运行联系表](screens/handoff/2026-07-16/G06-current-initial-results-invite-contact-1080x800.png)
+- G08 当前搜索申请/邀请码直加联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G08-current-search-invite-contact-720x800.png)
+- G09 当前申请列表/空态联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G09-current-list-empty-contact-720x800.png)
+- G03 当前创建/首代人物联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G03-current-create-ancestor-contact-720x800.png)
+- G05 当前成员总览/公开预览联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G05-current-member-public-contact-720x800.png)
+- G10 当前审核列表/无权限联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G10-current-list-permission-contact-720x800.png)
+- G11 当前设置/无权限联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G11-current-form-permission-contact-720x800.png)
+- G12 当前字辈列表/编辑联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/G12-current-list-edit-contact-720x800.png)
+- T01 当前世系树/空态联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/T01-current-tree-empty-contact-720x800.png)
+- T04 当前新增亲属表单/成功态联系表(待审核,不代表通过):[720×800 真实运行联系表](screens/handoff/2026-07-16/T04-current-form-success-contact-720x800.png)
- 当前实际为 53 页全部重新待审核;下方旧“已验收”图片只作为历史视觉基础,不计入本轮完成状态。
> 本仓库只保留低负担交接证据:每页一张代表图,复杂模块使用联系表。完整 before/after、多尺寸、历史废稿和全部运行截图保存在项目外本地归档,不进入 Git。
diff --git a/docs/superpowers/plans/2026-07-15-a01-asset-first-design.md b/docs/superpowers/plans/2026-07-15-a01-asset-first-design.md
new file mode 100644
index 0000000..255d005
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-15-a01-asset-first-design.md
@@ -0,0 +1,102 @@
+# A01 资产优先设计落地计划
+
+> 本计划由 `superpowers:writing-plans` 生成并在当前会话内直接执行。遵守用户最新约束:不使用子代理、不使用 worktree、不执行 `git add/commit/push/reset/checkout`,资产与设计验收通过前不修改 A01 页面代码。
+
+**目标:** 以冻结的密码/验证码视觉真源为依据,制作一套可投入 uni-app Android 的 A01 最终资产,并使用同一批资产组装可编辑 Figma 设计。
+
+**架构:** 密码与验证码状态共用唯一的背景、宗祠、卷轴、纹理、按钮和排版组件,仅切换标签、第二输入行和辅助操作。复杂装饰使用最终 PNG,文字、输入框布局、点击区域和状态切换保持可编辑;Figma 和应用后续必须引用相同资产文件与尺寸规则。
+
+**技术栈:** Figma Design、Figma Plugin API、ImageGen 位图资产、uni-app/Vue、Android WebView/HBuilderX。
+
+## 全局约束
+
+- 唯一 Logo:`static/assets/foundation/transparent/brand-seal.png`;后续 Android 桌面图标也以它为源。
+- 密码状态:左侧使用 `static/assets/modules/auth/transparent/a01-icon-lock-v1.png`,右侧是眼睛按钮,“忘记密码”在输入横线下方右对齐。
+- 验证码状态:使用“消息气泡+三点”图标和“获取验证码”,不显示眼睛或忘记密码。
+- 除 `brand-seal` 与 `a01-icon-lock-v1` 外,A01 可见装饰资产全部制作新版本;旧文件保留但最终不引用。
+- 不使用原生 UniApp 提示组件作为最终视觉;本阶段不做接口对接。
+- 基础版本只覆盖当前浅色国风主题,以及 320×568、360×640、360×800、412×915;深色与跟随系统延期到下一版本。
+- 设计审核必须基于真实截图;H5 仅为候选证据,最终需要 Android/HBuilderX 复核。
+
+---
+
+### 任务 1:冻结视觉真源
+
+**文件:**
+
+- 密码:`docs/design/screens/handoff/2026-07-15/A01-visual-source-password-selected-v2-841x1870.png`
+- 验证码:`docs/design/screens/handoff/2026-07-15/A01-visual-source-sms-selected-v1-841x1870.png`
+
+**验收:**
+
+- 密码稿 SHA256:`A93F201349C1BB1F2DFAB50BA0B5372A6FC1C4762661326ADE17AA29F31960EF`
+- 验证码稿 SHA256:`31D023A523DA8CA25DF00736793FAF40086B5B46F32664B724E570F432BF9D9E`
+- 两张均为 `841×1870`,后续按 `412×915` 等比参考,不从聊天缩略图反推。
+
+### 任务 2:建立 Figma 视觉真源与资产板
+
+**目标文件:** [A01 登录页重设计(可编辑)](https://www.figma.com/design/fWhC0maHCxh5STQsjlOoWw)
+
+**页面结构:**
+
+- `00 视觉真源`:密码与验证码全尺寸参考图并排放置,标注文件名和 SHA256。
+- `01 资产板`:Logo、宗祠、卷轴分片、纹理、标题纹样、按钮分片、输入图标、Android 图标安全区。
+- `02 页面状态`:密码隐藏、密码显示、验证码默认、验证码倒计时。
+- `03 响应式验收`:当前浅色国风主题的四档尺寸状态。
+
+**验收:** 每个 Figma 写入步骤返回节点 ID,并对新增 Frame 生成截图检查裁切、比例、文字和图标。
+
+### 任务 3:制作新资产
+
+**候选目录:** `docs/design/assets/a01-vnext/candidates/`
+
+**最终目录:**
+
+- `static/assets/modules/auth/opaque/`
+- `static/assets/modules/auth/transparent/`
+- `static/assets/foundation/opaque/`
+
+**资产边界:**
+
+- 宗祠头图:独立不透明宽图,包含 `brand-seal` 安全槽,但 Logo 本体仍单独引用。
+- 卷轴:顶部卷轴、中央纸纹理、左右边框、底部卷轴分离,禁止整张卷轴跨尺寸压缩。
+- 按钮:左端、中部可延展纹理、右端分离,避免边角被拉伸。
+- 图标:手机、开眼、闭眼保留瞳孔、消息气泡三点制作新版本;锁继续使用 `a01-icon-lock-v1.png`。
+- Android 图标:以 `brand-seal.png` 生成自适应图标前景、安全区预览与纯色背景方案,不改变 Logo 本体。
+
+**验收:** 检查像素尺寸、ARGB/Alpha、透明边角、主体覆盖率、边缘杂色和小尺寸可读性;不合格资产不进入 `static`。
+
+### 任务 4:用最终资产组装可编辑状态
+
+**Figma Frame:**
+
+- `A01 / Password / Hidden / 412×915`
+- `A01 / Password / Visible / 412×915`
+- `A01 / SMS / Default / 412×915`
+- `A01 / SMS / Countdown / 412×915`
+
+**状态差异:**
+
+- 密码隐藏:掩码文本、闭眼保留瞳孔。
+- 密码显示:明文文本、开眼图标。
+- 验证码默认:气泡三点图标、“获取验证码”。
+- 验证码倒计时:气泡三点图标、不可点击倒计时文案。
+
+**验收:** 背景和共用资产必须为同一组件实例;切换状态时公共区域位置不得漂移。
+
+### 任务 5:设计验收后进入代码实现
+
+**后续才允许修改:**
+
+- `pages/auth/a01-entry.vue`
+- A01 最终资产引用
+- `manifest.json` 或 HBuilderX Android 图标配置
+- `tests/a01-a02-ui-contract.ps1`
+
+**验证命令:**
+
+```powershell
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
+```
+
+**完成标准:** 合同测试通过,四档 H5 截图通过视觉比较,并取得 Android/HBuilderX 真机或模拟器截图;在此之前不得声明 A01 完成或验收通过。
diff --git a/docs/superpowers/plans/2026-07-15-a01-code-first-implementation.md b/docs/superpowers/plans/2026-07-15-a01-code-first-implementation.md
new file mode 100644
index 0000000..06e4797
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-15-a01-code-first-implementation.md
@@ -0,0 +1,199 @@
+# A01 纯代码资产流水线与页面落地实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. 本项目明确禁止 worktree、子代理和任何 Git 提交操作,执行时以项目规则为准。
+
+**Goal:** 不依赖 Photoshop,用同一份清单和源资产生成 A01 正式 PNG、设计预览与状态联系表,并将选定视觉一比一落到 `pages/auth/a01-entry.vue`。
+
+**Architecture:** `design-pipeline/manifests/a01.json` 是 A01 源资产、正式输出、状态和尺寸的唯一机器可读所有者。Node.js 调用 Sharp 完成缩放、PNG 优化、同源预览与联系表生成;A01 页面只引用确认后的正式资产,真实运行截图仍是最终视觉证据。
+
+**Tech Stack:** Node.js 24、Sharp、PowerShell 契约测试、Vue 3 `
diff --git a/pages/auth/a05-reset-password.vue b/pages/auth/a05-reset-password.vue
index f5d8f0e..d3c3553 100644
--- a/pages/auth/a05-reset-password.vue
+++ b/pages/auth/a05-reset-password.vue
@@ -1,328 +1,329 @@
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 重设密码
- 验证手机号后重新设置登录密码
-
+
+
+
+
-
+ 重设密码
+ 验证手机号后设置新的登录密码
+
-
-
+
+
手机号
-
+
+ {{ fieldErrors.phone }}
+
+
+
验证码
-
- 获取验证码
+
+ {{ codeRequested ? '重新获取' : '获取验证码' }}
+ {{ fieldErrors.verificationCode }}
+
+
+
新密码
-
+
+ {{ fieldErrors.password }}
+
+
+
确认新密码
-
+
+ {{ fieldErrors.confirmPassword }}
+
-
-
- 确认重设
-
+
+
+ 确认重设
+
- 重设完成后,请使用新密码登录
-
- 想起密码了?
- 返回登录
+ 重设成功后,请使用新密码登录
+
+ 想起密码了?
+ 返回登录
+
+
+
+
+
+
+
+
+
+ 安全验证
+ 发送验证码前需要完成行为验证
+ 验证数据将在接口接入后启用
+
+ 取消
+ 完成验证
+
+
+
+
+
+ ✓
+ 密码已重设
+ 请返回登录页,使用新密码登录
+
+
+ 返回登录
+
+
+
+
+
+
+
+ {{ feedbackMessage }}
+
diff --git a/pages/auth/a06-auth-status.vue b/pages/auth/a06-auth-status.vue
index 469f177..1a76dfe 100644
--- a/pages/auth/a06-auth-status.vue
+++ b/pages/auth/a06-auth-status.vue
@@ -1,46 +1,60 @@
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- 登录提示
- 请确认当前状态后继续操作
-
+
+
+
+
-
+ 登录受限
+ 请根据当前账号状态完成恢复
+
-
-
- {{ currentState.eyebrow }}
- {{ currentState.title }}
- {{ currentState.description }}
+
+
+ {{ currentState.eyebrow }}
+ {{ currentState.title }}
+ {{ currentState.description }}
+
+
+
+ {{ currentState.reasonLabel }}
+ {{ currentState.reason }}
+
+
+ {{ currentState.impactLabel }}
+ {{ currentState.impact }}
+
+
+ {{ currentState.recoveryLabel }}
+ {{ currentState.recovery }}
+
+
-
-
- {{ currentState.primaryLabel }}
-
+
+
+ 查看恢复方式
+
+ 返回登录
+
+
-
- {{ currentState.secondaryLabel }}
+
+
+
+
+ {{ currentState.recoveryTitle }}
+ {{ currentState.recoveryDetail }}
+ 账号恢复服务将在功能阶段接入,当前仅审核页面样式与状态位置。
+
+
+ 我知道了
@@ -52,320 +66,130 @@
import { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
-const status = ref('normal')
+const status = ref('risk')
+const recoveryVisible = ref(false)
+// A06 阻断状态的唯一合同;普通登录错误必须留在 A01 就近处理。
const statusConfig = {
- normal: {
- eyebrow: '登录提示',
- title: '登录未完成',
- description: '登录流程尚未完成,请选择一种方式继续。',
- primaryLabel: '返回登录',
- action: 'login',
- secondaryLabel: ''
+ frozen: {
+ eyebrow: '账号冻结',
+ title: '账号已被冻结',
+ description: '当前账号因安全审核暂时无法登录。',
+ reasonLabel: '限制原因',
+ reason: '账号进入人工安全审核',
+ impactLabel: '影响范围',
+ impact: '暂时无法进入家谱及管理资料',
+ recoveryLabel: '恢复方式',
+ recovery: '提交账号申诉并等待审核结果',
+ recoveryTitle: '申请解除冻结',
+ recoveryDetail: '请准备注册手机号与身份说明,提交后由平台进行人工核验。'
},
- failed: {
- eyebrow: '登录提示',
- title: '登录失败',
- description: '当前无法完成登录,请稍后重试或改用其他方式。',
- primaryLabel: '返回登录',
- action: 'login',
- secondaryLabel: ''
+ disabled: {
+ eyebrow: '账号停用',
+ title: '账号已被停用',
+ description: '当前账号处于停用状态,登录入口已关闭。',
+ reasonLabel: '限制原因',
+ reason: '账号已执行停用处理',
+ impactLabel: '影响范围',
+ impact: '无法登录,原有家谱资料不会被删除',
+ recoveryLabel: '恢复方式',
+ recovery: '联系平台核实停用原因与恢复条件',
+ recoveryTitle: '联系平台核实',
+ recoveryDetail: '请提供注册手机号和账号归属信息,平台核实后告知是否可以恢复。'
},
- restricted: {
- eyebrow: '账号安全',
- title: '账号暂时受限',
- description: '为保护家谱资料,当前账号暂不能登录。',
- primaryLabel: '查看帮助',
- action: 'help',
- secondaryLabel: '返回登录'
- },
- 'register-pending': {
- eyebrow: '注册提示',
- title: '注册尚未完成',
- description: '请返回注册页完善信息后再登录。',
- primaryLabel: '继续注册',
- action: 'register',
- secondaryLabel: '返回登录'
- },
- 'wechat-cancelled': {
- eyebrow: '微信登录',
- title: '微信授权已取消',
- description: '未获得微信授权,不会影响账号安全。',
- primaryLabel: '返回登录',
- action: 'login',
- secondaryLabel: ''
- },
- 'wechat-failed': {
- eyebrow: '微信登录',
- title: '微信授权失败',
- description: '当前无法完成微信授权,请使用账号密码登录。',
- primaryLabel: '返回登录',
- action: 'login',
- secondaryLabel: ''
+ risk: {
+ eyebrow: '风险限制',
+ title: '账号存在安全风险',
+ description: '系统检测到异常登录,为保护家谱资料已暂停本次登录。',
+ reasonLabel: '限制原因',
+ reason: '登录环境或操作行为存在异常',
+ impactLabel: '影响范围',
+ impact: '本次登录被阻断,账号资料保持不变',
+ recoveryLabel: '恢复方式',
+ recovery: '完成身份核验后重新登录',
+ recoveryTitle: '完成身份核验',
+ recoveryDetail: '请使用注册手机号完成身份核验;核验通过后可重新尝试登录。'
}
}
const currentState = computed(() => statusConfig[status.value])
const resolveStatus = () => {
- let requestedStatus = 'normal'
-
+ let requestedStatus = 'risk'
if (typeof location !== 'undefined') {
const query = new URLSearchParams(location.hash.split('?')[1] || '')
- requestedStatus = query.get('status') || 'normal'
+ requestedStatus = query.get('status') || 'risk'
} else {
const pages = getCurrentPages()
- const currentPage = pages[pages.length - 1]
- requestedStatus = currentPage?.options?.status || 'normal'
+ requestedStatus = pages[pages.length - 1]?.options?.status || 'risk'
}
-
- status.value = Object.prototype.hasOwnProperty.call(statusConfig, requestedStatus) ? requestedStatus : 'normal'
+ status.value = Object.prototype.hasOwnProperty.call(statusConfig, requestedStatus) ? requestedStatus : 'risk'
+ recoveryVisible.value = false
}
-onLoad(() => {
- resolveStatus()
-})
+onLoad(resolveStatus)
+onShow(resolveStatus)
-onShow(() => {
- resolveStatus()
-})
-
-const goLogin = () => {
- uni.navigateTo({ url: '/pages/auth/a01-entry' })
-}
-
-const goRegister = () => {
- uni.navigateTo({ url: '/pages/auth/a04-register' })
-}
-
-const handlePrimary = () => {
- if (currentState.value.action === 'register') {
- goRegister()
- return
- }
- if (currentState.value.action === 'help') {
- uni.showToast({ title: '账号申诉功能待接入', icon: 'none' })
- return
- }
- goLogin()
-}
-
-const goBack = () => {
- uni.navigateBack()
-}
+const goLogin = () => uni.redirectTo({ url: '/pages/auth/a01-entry' })
+const openRecovery = () => { recoveryVisible.value = true }
+const closeRecovery = () => { recoveryVisible.value = false }
+const goBack = () => uni.navigateBack()
diff --git a/pages/genealogy/g01-my-genealogies.vue b/pages/genealogy/g01-my-genealogies.vue
index 206c81c..7bded2e 100644
--- a/pages/genealogy/g01-my-genealogies.vue
+++ b/pages/genealogy/g01-my-genealogies.vue
@@ -1,4 +1,4 @@
-
+
请稍候,家族记忆正在归卷。
+
+
+
+ 暂时无法读取家谱
+ 网络或服务暂不可用,请稍后重新查看。
+
+
+ 重新加载
+
+
+
+
-
+
家谱
{{ currentGenealogy.name }}
+ 切换
@@ -118,7 +131,28 @@
/>
-
+
+ 加入申请
+
+
+
+
+ {{ item.name }}
+ {{ item.statusLabel }}
+
+ {{ item.description }}
+
+
+
+
+
+
- 新建家谱
+ 添加家谱
家谱
还没有加入任何家谱
- 先为自己的家族立一部谱,或搜索家谱申请加入
+ 先搜索家族是否已有家谱,再选择加入或创建
-
-
-
-
- 新建家谱
-
-
-
+ 搜索家谱
+
+
+
- 搜索并申请加入
+ 邀请码加入
+
+ 创建家谱
+
+ 确认没有现有家谱后再创建,避免重复建谱
+
+
+
+
+ 添加家谱
+ 建议先搜索已有家谱,避免重复创建
+
+
+ 搜索家谱
+
+
+
+ 邀请码加入
+
+ 确认没有现有家谱,继续创建
+ 取消
+
+
+
+
+
+
+
+
+ 切换当前家谱
+
+
+ {{ item.name }}
+ {{ item.location }} · {{ item.memberCount }} 位成员
+
+ {{ item.id === selectedGenealogyId ? '当前' : '选择' }}
+
+ 关闭
+
+
+
+
@@ -216,22 +285,29 @@ import PageHeader from "@/components/PageHeader.vue";
import { genealogies, notifications } from "@/data/mock.js";
const isLoading = ref(false);
+const hasError = ref(false);
const list = ref(genealogies);
const forceEmptyState = ref(false);
+const addDialogVisible = ref(false);
+const switcherVisible = ref(false);
+const selectedGenealogyId = ref(genealogies[0]?.id || null);
-const isEmptyStateRoute = () => {
+const readPresentationState = () => {
if (typeof location !== "undefined") {
const query = new URLSearchParams(location.hash.split("?")[1] || "");
- return query.get("state") === "empty";
+ return query.get("state") || "default";
}
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
- return currentPage?.options?.state === "empty";
+ return currentPage?.options?.state || "default";
};
const syncEmptyStateFromRoute = () => {
- forceEmptyState.value = isEmptyStateRoute();
+ const presentationState = readPresentationState();
+ forceEmptyState.value = presentationState === "empty";
+ isLoading.value = presentationState === "loading";
+ hasError.value = presentationState === "error";
};
onLoad(() => {
@@ -252,7 +328,17 @@ const createdGenealogies = computed(() =>
const joinedGenealogies = computed(() =>
list.value.filter((item, index) => index > 0),
);
-const currentGenealogy = computed(() => list.value[0]);
+const availableGenealogies = computed(() => list.value);
+const currentGenealogy = computed(() =>
+ availableGenealogies.value.find((item) => item.id === selectedGenealogyId.value) || availableGenealogies.value[0],
+);
+
+// 仅用于页面样式阶段覆盖加入申请的关键状态,不作为接口数据。
+const applicationRecords = [
+ { id: "pending", name: "汤氏南阳宗谱", statusLabel: "审核中", tone: "pending", description: "申请已提交,等待管理员审核" },
+ { id: "rejected", name: "汤氏清河家谱", statusLabel: "被拒绝", tone: "rejected", description: "可修改关系说明后重新申请" },
+ { id: "removed", name: "汤氏汝南支谱", statusLabel: "已退出", tone: "muted", description: "如需恢复成员身份,可重新申请加入" },
+];
const shortcuts = [
{
@@ -282,9 +368,27 @@ const openGenealogy = (genealogy) =>
const createGenealogy = () =>
uni.navigateTo({ url: "/pages/genealogy/g03-create-genealogy" });
const applyToJoin = () => uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
+const joinByInvite = () => uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies?mode=invite" });
const toNotifications = () =>
uni.navigateTo({ url: "/pages/notification/n01-message-center" });
+const openAddDialog = () => { addDialogVisible.value = true; };
+const closeAddDialog = () => { addDialogVisible.value = false; };
+const openSwitcher = () => { switcherVisible.value = true; };
+const closeSwitcher = () => { switcherVisible.value = false; };
+const selectGenealogy = (genealogy) => {
+ selectedGenealogyId.value = genealogy.id;
+ closeSwitcher();
+};
+const openApplication = (record) => {
+ const statusQuery = record.id === "pending" ? "pending" : record.id;
+ uni.navigateTo({ url: `/pages/genealogy/g09-my-applications?status=${statusQuery}` });
+};
+const retryLoad = () => {
+ hasError.value = false;
+ isLoading.value = false;
+};
+
const openShortcut = (key) => {
const paths = {
tree: "/pages/tree/t01-tree-overview",
@@ -405,6 +509,12 @@ const openShortcut = (key) => {
text-overflow: ellipsis;
white-space: nowrap;
}
+.current-switch-copy {
+ flex: 0 0 auto;
+ margin-left: auto;
+ color: $brand-red;
+ font-size: 23rpx;
+}
.current-info-divider {
position: relative;
z-index: 1;
@@ -512,6 +622,24 @@ const openShortcut = (key) => {
margin-top: 18rpx;
}
+.application-record {
+ position: relative;
+ display: flex;
+ min-height: 138rpx;
+ align-items: center;
+ box-sizing: border-box;
+ padding: 22rpx 26rpx;
+}
+.application-record__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
+.application-record__main { position: relative; z-index: 1; min-width: 0; flex: 1; }
+.application-record__title-row { display: flex; align-items: center; justify-content: space-between; }
+.application-record__name { overflow: hidden; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
+.application-record__status { flex: 0 0 auto; margin-left: 16rpx; color: #9a641f; font-size: 23rpx; }
+.application-record__status--rejected { color: #a7160c; }
+.application-record__status--muted { color: $ink-muted; }
+.application-record__copy { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 23rpx; }
+.application-record__chevron { position: relative; z-index: 1; width: 32rpx; height: 32rpx; margin-left: 14rpx; }
+
.create-action {
display: flex;
min-height: 96rpx;
@@ -558,16 +686,15 @@ const openShortcut = (key) => {
}
.state-panel {
+ position: relative;
display: flex;
min-height: 540rpx;
flex-direction: column;
align-items: center;
justify-content: center;
+ overflow: hidden;
padding: 42rpx;
box-sizing: border-box;
- border: 2rpx solid $gold-soft;
- border-radius: 18rpx;
- background: rgba(255, 249, 238, 0.76);
text-align: center;
}
@@ -676,8 +803,8 @@ const openShortcut = (key) => {
margin-top: 16rpx;
}
-.empty-create-action,
-.empty-search-action {
+.empty-search-action,
+.empty-invite-action {
position: relative;
display: flex;
width: 468rpx;
@@ -687,47 +814,24 @@ const openShortcut = (key) => {
overflow: hidden;
}
-.empty-create-action {
+.empty-search-action {
margin-top: 20rpx;
}
-.empty-search-action {
- margin-top: 18rpx;
+.empty-invite-action {
+ margin-top: 14rpx;
}
-.empty-create-action__skin,
-.empty-search-action__skin {
+.empty-search-action__skin,
+.empty-invite-action__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
-.empty-create-action__content {
- position: relative;
- z-index: 1;
- display: flex;
- align-items: center;
- color: #fff9ec;
- font-family: "STKaiti", "KaiTi", serif;
- font-size: 31rpx;
- font-weight: 700;
- letter-spacing: 4rpx;
-}
-
-.empty-create-action__icon {
- width: 34rpx;
- height: 34rpx;
- margin-right: 8rpx;
-}
-
-.empty-create-action__cloud {
- width: 46rpx;
- height: 24rpx;
- margin-left: 8rpx;
-}
-
-.empty-search-action__copy {
+.empty-search-action__copy,
+.empty-invite-action__copy {
position: relative;
z-index: 1;
color: #7b4e24;
@@ -736,6 +840,44 @@ const openShortcut = (key) => {
font-weight: 700;
letter-spacing: 3rpx;
}
+.empty-search-action__copy--primary { color: #fff9ec; }
+.empty-create-action { display: flex; align-items: center; justify-content: center; min-height: 64rpx; margin-top: 8rpx; color: $brand-red; }
+.empty-create-action__copy { font-family: "STKaiti", "KaiTi", serif; font-size: 27rpx; text-decoration: underline; text-underline-offset: 8rpx; }
+.empty-create-note { display: block; color: $ink-muted; font-size: 20rpx; line-height: 30rpx; }
+
+.state-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
+.state-panel__content { position: relative; z-index: 1; display: flex; min-height: 540rpx; flex-direction: column; align-items: center; justify-content: center; padding: 42rpx; box-sizing: border-box; }
+.state-retry { position: relative; display: flex; width: 360rpx; height: 96rpx; align-items: center; justify-content: center; margin-top: 30rpx; overflow: hidden; }
+.state-retry__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
+.state-retry__copy { position: relative; z-index: 1; color: #fff9ec; font-family: "STKaiti", "KaiTi", serif; font-size: 29rpx; }
+
+.add-dialog-layer,
+.genealogy-switcher-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 40rpx; background: rgba(34, 20, 12, 0.58); }
+.add-dialog,
+.genealogy-switcher { position: relative; width: 100%; max-width: 670rpx; min-height: 720rpx; }
+.add-dialog__skin,
+.genealogy-switcher__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
+.add-dialog__content,
+.genealogy-switcher__content { position: relative; z-index: 1; display: flex; min-height: 720rpx; flex-direction: column; align-items: center; box-sizing: border-box; padding: 78rpx 58rpx 50rpx; }
+.dialog-title { color: $brand-red; font-family: "STKaiti", "KaiTi", serif; font-size: 42rpx; font-weight: 700; letter-spacing: 4rpx; }
+.dialog-copy { margin-top: 18rpx; color: $ink-muted; font-size: 23rpx; }
+.dialog-primary,
+.dialog-secondary { position: relative; display: flex; width: 100%; height: 98rpx; align-items: center; justify-content: center; overflow: hidden; font-family: "STKaiti", "KaiTi", serif; font-size: 29rpx; }
+.dialog-primary { margin-top: 42rpx; color: #fff9ec; }
+.dialog-secondary { margin-top: 16rpx; color: #7b4e24; }
+.dialog-action__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
+.dialog-primary__copy,
+.dialog-secondary > text { position: relative; z-index: 1; }
+.dialog-create { display: flex; min-height: 76rpx; align-items: center; margin-top: 8rpx; color: $brand-red; font-size: 23rpx; }
+.dialog-close { display: flex; min-height: 72rpx; align-items: center; justify-content: center; margin-top: auto; color: $ink-muted; font-size: 24rpx; }
+.switcher-item { display: flex; width: 100%; min-height: 108rpx; align-items: center; justify-content: space-between; box-sizing: border-box; padding: 18rpx 12rpx; border-bottom: 1rpx solid rgba(181, 138, 75, 0.42); }
+.switcher-item:first-of-type { margin-top: 28rpx; }
+.switcher-item__name,
+.switcher-item__meta { display: block; }
+.switcher-item__name { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 31rpx; font-weight: 700; }
+.switcher-item__meta { margin-top: 6rpx; color: $ink-muted; font-size: 21rpx; }
+.switcher-item__state { color: $brand-red; font-size: 23rpx; }
+.switcher-item--active .switcher-item__name { color: $brand-red; }
.action-hover {
opacity: 0.82;
diff --git a/pages/genealogy/g03-create-genealogy.vue b/pages/genealogy/g03-create-genealogy.vue
index 86e05a6..fd366c7 100644
--- a/pages/genealogy/g03-create-genealogy.vue
+++ b/pages/genealogy/g03-create-genealogy.vue
@@ -49,27 +49,34 @@
姓氏
-
+
+ {{ fieldErrors.surname }}
谱名
-
+
+ {{ fieldErrors.name }}
堂号
所在地
-
+
+ {{ fieldErrors.location }}
访问规则
- 默认仅成员可见
+
+ 仅成员可见
+ 可搜索申请
+
保护族人资料;创建后可在家谱设置中开放搜索与申请加入。
+ 创建样式模拟失败,请检查信息后重试。
- 创建并录入首代
+ {{ createState === 'submitting' ? '正在创建…' : '创建并录入首代' }}
@@ -89,8 +96,9 @@
姓名
-
+
+ {{ fieldErrors.personName }}
世代
第一世
@@ -100,6 +108,7 @@
+ 首代人物保存失败,请稍后重试。
生平简述
@@ -112,7 +121,40 @@
src="/static/assets/foundation/opaque/a01-primary-button.png"
mode="scaleToFill"
/>
- 保存并查看世系树
+ {{ ancestorState === 'submitting' ? '正在保存…' : '保存并进入家谱' }}
+
+
+
+
+
+
+
+
+
+ 先确认是否已有家谱
+ 同一家族可能已经建谱。建议先搜索谱名、地区和堂号,避免重复创建。
+
+
+ 先搜索已有家谱
+
+
+
+ 确认没有,继续创建
+
+ 返回修改
+
+
+
+
+
+
+
+
+ 家谱创建完成
+ 首代人物已保存,接下来进入家谱总览继续完善资料。
+
+
+ 进入家谱总览
@@ -123,12 +165,15 @@
diff --git a/pages/genealogy/g05-genealogy-overview.vue b/pages/genealogy/g05-genealogy-overview.vue
index a79da35..55abbc8 100644
--- a/pages/genealogy/g05-genealogy-overview.vue
+++ b/pages/genealogy/g05-genealogy-overview.vue
@@ -4,13 +4,13 @@
-
+
{{ genealogy.hall || '堂号待补' }} · {{ genealogy.location || '所在地待补' }}
@@ -23,17 +23,17 @@
-
+
世系树查看家脉关系
-
+
录入族人从首代开始完善
字辈谱传承行辈次序
-
+
入谱审核处理加入申请
@@ -48,10 +48,34 @@
+
+
+ 公开家谱
+ {{ genealogy.name }}
+ {{ genealogy.source }}
+
+
+ 姓氏{{ genealogy.surname }}氏
+ 地区{{ genealogy.location }}
+ 堂号{{ genealogy.hall }}
+ 当前支系{{ genealogy.branchName }}
+ 所属上级谱{{ genealogy.parentName }}
+
+
+ 公开说明
+ {{ genealogy.publicDescription }}
+
+
+
+ 申请加入这部家谱
+
+
+
{{ stateTitle }}
{{ stateCopy }}
@@ -68,30 +92,49 @@
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
-import { appApi } from '@/utils/api.js'
-import { genealogyContext } from '@/utils/genealogy-context.js'
const genealogy = ref(null)
const genealogyId = ref('')
const overviewState = ref('loading')
const loadError = ref('')
+const viewMode = ref('member')
+const accessRole = ref('owner')
+const overviewFixture = {
+ id: '2001',
+ surname: '汤',
+ name: '汤氏南阳宗谱',
+ hall: '敦睦堂',
+ location: '河南·南阳',
+ parentName: '汤氏中华总谱',
+ branchName: '南阳主支',
+ source: '由南阳汤氏族人整理并维护',
+ publicDescription: '公开展示家谱身份、地区、堂号与支系信息;成员资料和世系详情仅向已加入成员开放。',
+ motto: '敦亲睦族,敬祖传家。',
+ memberCount: 428,
+ activeCount: 316,
+ visibility: '仅成员可见'
+}
const stateTitle = computed(() => ({
loading: '正在展开家谱…',
empty: '还没有可查看的家谱',
- error: '家谱暂时无法打开'
+ error: '家谱暂时无法打开',
+ 'no-permission': '当前账号无权查看'
}[overviewState.value] || ''))
const stateCopy = computed(() => loadError.value || ({
loading: '请稍候,正在读取家谱概览。',
empty: '请从“我的家谱”选择一部家谱后再进入。',
- error: '可能是网络波动、家谱不存在或当前账号无访问权限。'
+ error: '可能是网络波动或家谱不存在。',
+ 'no-permission': '这部家谱未向当前账号开放,请返回我的家谱或联系管理员。'
}[overviewState.value] || ''))
-const loadGenealogy = async (query = {}) => {
+const loadGenealogy = (query = {}) => {
overviewState.value = 'loading'
loadError.value = ''
genealogy.value = null
- genealogyId.value = query.genealogyId || genealogyId.value || genealogyContext.getCurrentGenealogyId()
+ genealogyId.value = query.genealogyId || genealogyId.value || ''
+ viewMode.value = query.mode === 'public' ? 'public' : 'member'
+ accessRole.value = query.role === 'member' ? 'member' : 'owner'
if (query.state === 'empty' || !genealogyId.value) {
overviewState.value = 'empty'
@@ -101,16 +144,14 @@ const loadGenealogy = async (query = {}) => {
overviewState.value = 'error'
return
}
-
- genealogyContext.setCurrentGenealogyId(genealogyId.value)
- try {
- genealogy.value = await appApi.getOverview(genealogyId.value)
- overviewState.value = genealogy.value ? 'ready' : 'error'
- if (!genealogy.value) loadError.value = '家谱不存在或当前账号无权访问。'
- } catch (error) {
- loadError.value = error.message || '家谱加载失败,请稍后重试。'
- overviewState.value = 'error'
+ if (query.state === 'no-permission') {
+ overviewState.value = 'no-permission'
+ return
}
+ if (query.state === 'loading') return
+
+ genealogy.value = { ...overviewFixture, id: genealogyId.value }
+ overviewState.value = 'ready'
}
onLoad(loadGenealogy)
@@ -122,10 +163,11 @@ const toFamily = () => uni.reLaunch({ url: '/pages/family/f01-family-feed' })
const toApplications = () => uni.navigateTo({ url: `/pages/genealogy/g10-application-review?genealogyId=${genealogyId.value}` })
const toSettings = () => uni.navigateTo({ url: `/pages/genealogy/g11-genealogy-settings?genealogyId=${genealogyId.value}` })
const toGenerationPoems = () => uni.navigateTo({ url: `/pages/genealogy/g12-generation-poems?genealogyId=${genealogyId.value}` })
+const applyToJoin = () => uni.navigateTo({ url: `/pages/genealogy/g08-join-application?source=search&genealogyId=${genealogyId.value}` })
diff --git a/pages/genealogy/g08-join-application.vue b/pages/genealogy/g08-join-application.vue
index a589424..bda7e18 100644
--- a/pages/genealogy/g08-join-application.vue
+++ b/pages/genealogy/g08-join-application.vue
@@ -3,7 +3,7 @@
-
+
- 公开家谱入谱申请
- 申请加入 {{ genealogyName }}
- 请填写真实身份和亲属关系,管理员审核后会通过消息告知结果。
+ {{ sourceContract.eyebrow }}
+ {{ sourceContract.formTitle }} {{ genealogyName }}
+ {{ sourceContract.formCopy }}
- 真实姓名
+ 真实姓名
+ {{ fieldErrors.realName }}
- 与家谱关系
+ 与家谱关系
+ {{ fieldErrors.relation }}
+ 请以家谱中一位已知长辈为参照,例如:汤正华堂侄
- 申请说明
+ {{ sourceContract.thirdFieldLabel }}
- 提交后可在“我的申请”中查看审核进度。
+ {{ sourceContract.note }}
- {{ isSubmitting ? '正在提交…' : '提交申请' }}
+ {{ isSubmitting ? sourceContract.submittingLabel : sourceContract.submitLabel }}
- {{ joinState === 'success' ? '申请已送达' : '申请未提交' }}
- {{ joinState === 'success' ? '等待管理员核实亲属关系' : '暂时无法提交申请' }}
+ {{ joinState === 'success' ? sourceContract.successEyebrow : sourceContract.errorEyebrow }}
+ {{ joinState === 'success' ? sourceContract.successTitle : sourceContract.errorTitle }}
{{ resultCopy }}
-
+
- {{ joinState === 'success' ? '查看我的申请' : '重新填写' }}
+ {{ joinState === 'success' ? sourceContract.nextLabel : '重新填写' }}
@@ -56,6 +59,7 @@ import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
const genealogyId = ref('')
+const source = ref('search')
const genealogyName = ref('这部家谱')
const genealogyPreview = {
'1001': '汤氏家谱',
@@ -65,12 +69,47 @@ const joinState = ref('form')
const isSubmitting = ref(false)
const errorMessage = ref('')
const form = reactive({ realName: '', relation: '', message: '' })
+const fieldErrors = reactive({ realName: '', relation: '' })
+const sourceContract = computed(() => source.value === 'invite' ? {
+ headerTitle: '确认关系并加入',
+ eyebrow: '邀请码直接加入',
+ formTitle: '确认加入',
+ formCopy: '请填写真实身份和亲属关系;提交后直接加入,无需等待管理员审核。',
+ thirdFieldLabel: '补充信息',
+ thirdFieldPlaceholder: '选填:补充祖居地、长辈姓名等信息',
+ note: '邀请码来源提交后直接加入,无需等待审核。',
+ submitLabel: '确认加入',
+ submittingLabel: '正在加入…',
+ successEyebrow: '已加入家谱',
+ successTitle: '关系信息已提交',
+ errorEyebrow: '加入未完成',
+ errorTitle: '暂时无法加入家谱',
+ nextLabel: '返回我的家谱',
+ successCopy: `已直接加入“${genealogyName.value}”,无需等待审核;返回后将刷新并选中这部家谱。`
+} : {
+ headerTitle: '申请加入家谱',
+ eyebrow: '公开家谱入谱申请',
+ formTitle: '申请加入',
+ formCopy: '请填写真实身份和亲属关系,管理员审核后会通过消息告知结果。',
+ thirdFieldLabel: '申请说明',
+ thirdFieldPlaceholder: '补充祖居地、长辈姓名等核验信息',
+ note: '提交后可在“我的申请”中查看审核进度。',
+ submitLabel: '提交申请',
+ submittingLabel: '正在提交…',
+ successEyebrow: '申请已送达',
+ successTitle: '等待管理员核实亲属关系',
+ errorEyebrow: '申请未提交',
+ errorTitle: '暂时无法提交申请',
+ nextLabel: '查看我的申请',
+ successCopy: `“${genealogyName.value}”的管理员会在核实后给出结果,请留意消息中心。`
+})
const resultCopy = computed(() => joinState.value === 'success'
- ? `“${genealogyName.value}”的管理员会在核实后给出结果,请留意消息中心。`
+ ? sourceContract.value.successCopy
: errorMessage.value || '请检查网络后重新填写;未成功提交的内容不会进入审核列表。')
onLoad((query) => {
genealogyId.value = query.genealogyId || ''
+ source.value = query.source === 'invite' ? 'invite' : 'search'
if (query.state === 'success') {
joinState.value = 'success'
return
@@ -85,22 +124,25 @@ onLoad((query) => {
const submitJoin = () => {
if (isSubmitting.value) return
- if (!form.realName.trim() || !form.relation.trim()) {
- uni.showToast({ title: '请填写真实姓名和亲属关系', icon: 'none' })
- return
- }
+ fieldErrors.realName = form.realName.trim() ? '' : '请填写真实姓名'
+ fieldErrors.relation = form.relation.trim() ? '' : '请填写与家谱的关系'
+ if (fieldErrors.realName || fieldErrors.relation) return
isSubmitting.value = true
setTimeout(() => {
- joinState.value = 'success'
+ joinState.value = form.realName.trim() === '失败' ? 'error' : 'success'
+ if (joinState.value === 'error') errorMessage.value = source.value === 'invite' ? '邀请码加入暂未完成,请稍后重试。' : '申请暂未提交,请稍后重试。'
isSubmitting.value = false
}, 280)
}
+const clearFieldError = (field) => { fieldErrors[field] = '' }
const retryForm = () => { joinState.value = 'form'; errorMessage.value = '' }
const toMyApplications = () => uni.redirectTo({ url: '/pages/genealogy/g09-my-applications' })
+const toMyGenealogies = () => uni.reLaunch({ url: `/pages/genealogy/g01-my-genealogies?genealogyId=${genealogyId.value}` })
+const completeFlow = () => source.value === 'invite' ? toMyGenealogies() : toMyApplications()
diff --git a/pages/genealogy/g10-application-review.vue b/pages/genealogy/g10-application-review.vue
index 4cd33eb..ec5c0a8 100644
--- a/pages/genealogy/g10-application-review.vue
+++ b/pages/genealogy/g10-application-review.vue
@@ -8,7 +8,8 @@
核实亲属关系后再决定
@@ -40,20 +41,46 @@
- {{ reviewState === 'empty' ? '暂无待审核申请' : '审核列表暂时无法读取' }}
- {{ reviewState === 'empty' ? '新的入谱申请会在这里出现,并同步发送消息提醒。' : errorMessage || '请检查网络或家谱权限后重试。' }}
+ {{ stateTitle }}
+ {{ stateCopy }}
重新查看
+
+
+
+
+
+
+ 审核说明
+ 请核对申请人的姓名、亲属关系和补充说明,仅确认与本家谱存在真实关系的申请。
+ 我知道了
+
+
+ {{ confirmation.approved ? '确认通过申请?' : '确认拒绝申请?' }}
+ {{ confirmation.approved ? '通过后,申请人将成为本家谱成员。' : '拒绝后,申请人会收到审核结果,并可修改后重新申请。' }}
+
+ 取消
+ {{ confirmation.approved ? '确认通过' : '确认拒绝' }}
+
+
+
+
+
+
+
+
+ {{ feedbackMessage }}
+
diff --git a/pages/genealogy/g11-genealogy-settings.vue b/pages/genealogy/g11-genealogy-settings.vue
index 9c00fa9..68d41af 100644
--- a/pages/genealogy/g11-genealogy-settings.vue
+++ b/pages/genealogy/g11-genealogy-settings.vue
@@ -8,7 +8,8 @@
@@ -20,8 +21,9 @@
家谱名称
-
+
+ {{ nameError }}
公开范围
@@ -47,25 +49,32 @@
- {{ settingsState === 'success' ? '设置已保存' : '设置未保存' }}
- {{ settingsState === 'success' ? '新的访问规则已经生效' : '暂时无法打开家谱设置' }}
- {{ settingsState === 'success' ? `${genealogyDraft.name} · ${visibilityLabel}` : '请从家谱总览重新进入,当前修改不会被保留。' }}
+ {{ settingsState === 'success' ? '设置已保存' : settingsState === 'no-permission' ? '权限不足' : '设置未保存' }}
+ {{ settingsState === 'success' ? '新的访问规则已经生效' : settingsState === 'no-permission' ? '当前账号不能修改家谱' : '暂时无法打开家谱设置' }}
+ {{ settingsState === 'success' ? `${genealogyDraft.name} · ${visibilityLabel}` : settingsState === 'no-permission' ? '只有家谱所有者可以修改名称、公开范围和访问说明。' : '请从家谱总览重新进入,当前修改不会被保留。' }}
{{ settingsState === 'success' ? '继续调整' : '重新查看' }}
+
+
+ 设置已保存
+
diff --git a/pages/genealogy/g12-generation-poems.vue b/pages/genealogy/g12-generation-poems.vue
index 3546c10..bf5ddc1 100644
--- a/pages/genealogy/g12-generation-poems.vue
+++ b/pages/genealogy/g12-generation-poems.vue
@@ -9,7 +9,8 @@
'poem-state--list': poemState === 'list',
'poem-state--empty': poemState === 'empty',
'poem-state--edit': poemState === 'edit',
- 'poem-state--error': poemState === 'error'
+ 'poem-state--error': poemState === 'error',
+ 'poem-state--no-permission': poemState === 'no-permission'
}">
@@ -37,8 +38,9 @@
字辈内容
-
+
+ {{ poemError }}
缺失旧世代时
@@ -58,24 +60,31 @@
- {{ poemState === 'empty' ? '尚未建立字辈' : '字辈暂不可用' }}
- {{ poemState === 'empty' ? '从第一段传承字序开始' : '暂时无法读取字辈诗' }}
- {{ poemState === 'empty' ? '管理员可以一次录入连续字辈,系统会按世代拆分展示。' : '请从家谱总览重新进入,或稍后再试。' }}
+ {{ poemState === 'empty' ? '尚未建立字辈' : poemState === 'no-permission' ? '权限不足' : '字辈暂不可用' }}
+ {{ poemState === 'empty' ? '从第一段传承字序开始' : poemState === 'no-permission' ? '当前账号不能维护字辈' : '暂时无法读取字辈诗' }}
+ {{ poemState === 'empty' ? '管理员可以一次录入连续字辈,系统会按世代拆分展示。' : poemState === 'no-permission' ? '普通成员可以查看字辈,但只有具备管理权限的成员可以维护。' : '请从家谱总览重新进入,或稍后再试。' }}
{{ poemState === 'empty' ? '开始录入' : '重新查看' }}
+
+
+ 字辈预览已更新
+
diff --git a/pages/tree/t02-tree-states.vue b/pages/tree/t02-tree-states.vue
deleted file mode 100644
index 3073a14..0000000
--- a/pages/tree/t02-tree-states.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/scripts/build-a01-layered-psd.ps1 b/scripts/build-a01-layered-psd.ps1
new file mode 100644
index 0000000..ad0799f
--- /dev/null
+++ b/scripts/build-a01-layered-psd.ps1
@@ -0,0 +1,65 @@
+param(
+ [string]$ManifestPath = 'docs/design/assets/a01-vnext/source/a01-psd-manifest.json',
+ [string]$OutputPath = 'docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd'
+)
+
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$rootFullPath = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
+
+function Resolve-WorkspacePath {
+ param(
+ [string]$Path,
+ [bool]$MustExist
+ )
+
+ $candidate = if ([IO.Path]::IsPathRooted($Path)) { $Path } else { Join-Path $root $Path }
+ $fullPath = [IO.Path]::GetFullPath($candidate)
+ if (-not $fullPath.StartsWith($rootFullPath, [StringComparison]::OrdinalIgnoreCase)) {
+ throw "Path must remain inside the workspace: $Path"
+ }
+ if ($MustExist -and -not (Test-Path -LiteralPath $fullPath -PathType Leaf)) {
+ throw "Required file does not exist: $fullPath"
+ }
+ return $fullPath
+}
+
+function ConvertTo-JavaScriptString {
+ param([string]$Value)
+ $escaped = $Value.Replace('\', '\\').Replace("'", "\'").Replace("`r", '\r').Replace("`n", '\n')
+ return "'$escaped'"
+}
+
+$resolvedManifest = Resolve-WorkspacePath -Path $ManifestPath -MustExist $true
+$resolvedOutput = Resolve-WorkspacePath -Path $OutputPath -MustExist $false
+$outputName = [IO.Path]::GetFileName($resolvedOutput)
+if ($outputName -notmatch '^A01-layered-source-v[0-9]+\.psd$') {
+ throw "PSD output must use a versioned A01 source name: $outputName"
+}
+if (Test-Path -LiteralPath $resolvedOutput) {
+ throw "Refusing to overwrite an existing PSD: $resolvedOutput"
+}
+if (-not (Get-Process -Name Photoshop -ErrorAction SilentlyContinue)) {
+ throw 'Adobe Photoshop must already be running.'
+}
+
+$outputDirectory = Split-Path -Parent $resolvedOutput
+if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) {
+ New-Item -ItemType Directory -Path $outputDirectory | Out-Null
+}
+
+$jsxPath = Resolve-WorkspacePath -Path 'scripts/photoshop/a01-build-layered-psd.jsx' -MustExist $true
+$app = New-Object -ComObject 'Photoshop.Application.150'
+$bootstrap = @"
+var JIAPU_A01_ROOT = $(ConvertTo-JavaScriptString $rootFullPath.TrimEnd([IO.Path]::DirectorySeparatorChar));
+var JIAPU_A01_MANIFEST = $(ConvertTo-JavaScriptString $resolvedManifest);
+var JIAPU_A01_PSD = $(ConvertTo-JavaScriptString $resolvedOutput);
+$.evalFile(new File($(ConvertTo-JavaScriptString $jsxPath)));
+"@
+$app.DoJavaScript($bootstrap, @(), 1)
+
+if (-not (Test-Path -LiteralPath $resolvedOutput -PathType Leaf)) {
+ throw "Photoshop did not create the expected PSD: $resolvedOutput"
+}
+
+Write-Output "A01-LAYERED-PSD BUILD PASS: $resolvedOutput"
diff --git a/scripts/capture-chrome-page.js b/scripts/capture-chrome-page.js
index c4e6a1d..ce3d7e8 100644
--- a/scripts/capture-chrome-page.js
+++ b/scripts/capture-chrome-page.js
@@ -6,7 +6,7 @@ const renderSettleMilliseconds = 900
const chromeDebuggingPort = process.env.CHROME_DEBUGGING_PORT || '9222'
if (!url || !selector || !output) {
- throw new Error('Usage: node scripts/capture-chrome-page.js