视觉审核完成30%

This commit is contained in:
rain
2026-07-16 18:12:59 +08:00
parent cb25317412
commit 23cfd365a1
89 changed files with 3740 additions and 403 deletions
+2
View File
@@ -9,3 +9,5 @@
/docs/design/assets/a01-vnext/candidates/
/docs/design/assets/a01-vnext/review/
/.superpowers/
**/__pycache__/
*.py[cod]
@@ -0,0 +1,41 @@
{
"schemaVersion": 2,
"page": "A01",
"runtime": {
"url": "http://localhost:5173/#/pages/auth/a01-entry",
"chromePort": 9222
},
"assets": [
{
"id": "a01-primary-button-v2",
"assetClass": "fixed-bitmap",
"source": "static/assets/foundation/opaque/a01-primary-button.png",
"output": "static/assets/foundation/transparent/a01-primary-button-v2.png",
"logicalSlot": { "widthRpx": 622, "heightRpx": 92 },
"outputPixels": { "width": 1866, "height": 276 },
"render": { "scalePolicy": "uniform-only", "uniMode": "aspectFit", "allowDistortion": false },
"alpha": { "required": true, "transparentOuterPadding": 12, "cornerMaxAlpha": 0 },
"edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true },
"quality": { "maxBytes": 1000000, "symmetry": "horizontal", "colorSpace": "sRGB" },
"processing": { "trim": 8, "capWidth": 180 },
"runtime": { "selector": ".login-submit", "imageSelector": ".button-skin img" },
"consumers": ["pages/auth/a01-entry.vue"]
},
{
"id": "a01-secondary-button-v2",
"assetClass": "fixed-bitmap",
"source": "static/assets/foundation/opaque/a01-secondary-button.png",
"output": "static/assets/foundation/transparent/a01-secondary-button-v2.png",
"logicalSlot": { "widthRpx": 622, "heightRpx": 100 },
"outputPixels": { "width": 1866, "height": 300 },
"render": { "scalePolicy": "uniform-only", "uniMode": "aspectFit", "allowDistortion": false },
"alpha": { "required": true, "transparentOuterPadding": 12, "cornerMaxAlpha": 0 },
"edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true },
"quality": { "maxBytes": 1200000, "symmetry": "horizontal", "colorSpace": "sRGB" },
"processing": { "trim": 12, "capWidth": 200 },
"runtime": { "selector": ".wechat-login", "imageSelector": ".button-skin img" },
"consumers": ["pages/auth/a01-entry.vue"]
}
]
}
@@ -0,0 +1,70 @@
{
"schemaVersion": 2,
"page": "A01",
"runtime": {
"url": "http://localhost:5173/#/pages/auth/a01-entry",
"chromePort": 9222
},
"assets": [
{
"id": "a01-scroll-primary-v3",
"assetClass": "fixed-bitmap",
"source": "docs/design/assets/a01-vnext/masters/a01-scroll-primary-master-v3.png",
"output": "static/assets/foundation/transparent/a01-scroll-primary-v3.png",
"logicalSlot": { "widthRpx": 622, "heightRpx": 92 },
"outputPixels": { "width": 1866, "height": 276 },
"render": { "scalePolicy": "uniform-only", "uniMode": "aspectFit", "allowDistortion": false },
"alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 },
"edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true },
"quality": { "maxBytes": 1800000, "symmetry": "horizontal", "colorSpace": "sRGB" },
"processing": { "trim": 1, "capWidth": 380, "keyColor": "#00FF00", "keyTolerance": 96 },
"runtime": { "selector": ".login-submit", "imageSelector": ".button-skin img" },
"consumers": ["pages/auth/a01-entry.vue"]
},
{
"id": "a01-scroll-secondary-v3",
"assetClass": "fixed-bitmap",
"source": "docs/design/assets/a01-vnext/masters/a01-scroll-secondary-master-v3.png",
"output": "static/assets/foundation/transparent/a01-scroll-secondary-v3.png",
"logicalSlot": { "widthRpx": 622, "heightRpx": 100 },
"outputPixels": { "width": 1866, "height": 300 },
"render": { "scalePolicy": "uniform-only", "uniMode": "aspectFit", "allowDistortion": false },
"alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 },
"edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true },
"quality": { "maxBytes": 1800000, "symmetry": "horizontal", "colorSpace": "sRGB" },
"processing": { "trim": 1, "capWidth": 380, "keyColor": "#00FF00", "keyTolerance": 96 },
"runtime": { "selector": ".wechat-login", "imageSelector": ".button-skin img" },
"consumers": ["pages/auth/a01-entry.vue"]
},
{
"id": "a01-scroll-toast-v3",
"assetClass": "nine-slice",
"source": "docs/design/assets/a01-vnext/masters/a01-scroll-toast-master-v3.png",
"output": "static/assets/foundation/transparent/a01-scroll-toast-v3.png",
"logicalSlot": { "widthRpx": 590, "heightRpx": 82 },
"outputPixels": { "width": 1770, "height": 246 },
"render": { "scalePolicy": "nine-slice", "uniMode": "aspectFit", "allowDistortion": false },
"alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 },
"edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true },
"quality": { "maxBytes": 1600000, "symmetry": "horizontal", "colorSpace": "sRGB" },
"processing": { "trim": 1, "capWidth": 340, "keyColor": "#00FF00", "keyTolerance": 96 },
"runtime": { "selector": ".feedback-toast", "imageSelector": ".feedback-toast__skin img" },
"consumers": ["pages/auth/a01-entry.vue"]
},
{
"id": "a01-scroll-dialog-v3",
"assetClass": "fixed-bitmap",
"source": "docs/design/assets/a01-vnext/masters/a01-scroll-dialog-master-v3-r2.png",
"output": "static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png",
"logicalSlot": { "widthRpx": 620, "heightRpx": 520 },
"outputPixels": { "width": 1860, "height": 1560 },
"render": { "scalePolicy": "uniform-only", "uniMode": "aspectFit", "allowDistortion": false },
"alpha": { "required": true, "transparentOuterPadding": 6, "cornerMaxAlpha": 0 },
"edge": { "forbidChromaResidue": true, "forbidLightFringe": true, "premultipliedAlphaCheck": true },
"quality": { "maxBytes": 3000000, "symmetry": "horizontal", "colorSpace": "sRGB" },
"processing": { "trim": 1, "capWidth": 260, "keyColor": "#00FF00", "keyTolerance": 96, "paletteColors": 128, "indexedPng": true },
"runtime": { "selector": ".verification-dialog", "imageSelector": ".verification-dialog__skin img" },
"consumers": ["pages/auth/a01-entry.vue"]
}
]
}
@@ -0,0 +1,49 @@
{
"schemaVersion": 1,
"page": "G01",
"status": "candidates-pending-user-selection",
"generatedDate": "2026-07-16",
"referenceAssets": [
"static/assets/foundation/transparent/footer-mountain-bamboo.png",
"tmp/g01-audit-02-application-gap-focused-412x915.png"
],
"determinism": {
"modelRegeneration": "direction-only-not-pixel-identical",
"savedMasterProcessing": "pixel-identical-with-locked-dependencies"
},
"chromaKey": {
"color": "#00ff00",
"dominanceStart": 30,
"dominanceEnd": 220,
"despillAllowance": 10
},
"candidates": [
{
"id": "g01-list-background-a",
"title": "上山下亭纵向框景",
"master": "docs/design/assets/g01-background/masters/g01-list-background-direction-a-chroma-master.png",
"generatedOutput": "design-pipeline/generated/g01-background/g01-list-background-direction-a.png",
"processingMode": "chroma-key",
"sourcePixels": { "width": 1024, "height": 1536 },
"prompt": "1024x1536 纵向 G01 列表背景独立资产;暖灰淡金中国水墨,远山从上方约 10% 开始,左中下亭台,右侧竹影贯穿,中央低对比度供列表文字阅读;无文字、无 UI、无卡片、无红色;非画面区域必须为纯 #00ff00 绿幕,画面本身不得使用绿色。"
},
{
"id": "g01-list-background-b",
"title": "上下山水环抱框景",
"master": "docs/design/assets/g01-background/masters/g01-list-background-direction-b-chroma-master.png",
"generatedOutput": "design-pipeline/generated/g01-background/g01-list-background-direction-b.png",
"processingMode": "chroma-key",
"sourcePixels": { "width": 1024, "height": 1536 },
"prompt": "1024x1536 纵向 G01 列表背景独立资产;上方连续远山与雾带、左中亭台、右侧淡金竹影、下方第二层山水,三个纵向区段都有内容但中央保持安静;无文字、无 UI、无卡片、无红色;非画面区域必须为纯 #00ff00 绿幕,画面本身不得使用绿色。"
},
{
"id": "g01-list-background-c",
"title": "云竹亭台连续宣纸景",
"master": "docs/design/assets/g01-background/masters/g01-list-background-direction-c-paper-master.png",
"generatedOutput": "design-pipeline/generated/g01-background/g01-list-background-direction-c.png",
"processingMode": "opaque-paper",
"sourcePixels": { "width": 1024, "height": 1536 },
"prompt": "1024x1536 纵向 G01 列表背景独立资产;暖宣纸底,顶部 5% 即出现云气、枝叶和淡金竹影,左侧约 40% 高度放置亭台,山水从中部连续延伸到底部,中央低对比度;无文字、无 UI、无卡片、无标志、无红色。"
}
]
}
+9 -1
View File
@@ -3,7 +3,15 @@
"private": true,
"type": "module",
"scripts": {
"build:a01": "node scripts/build-a01.mjs"
"build:a01": "node scripts/build-a01.mjs",
"test:v2": "node --test tests/*.test.mjs",
"validate:a01-buttons": "node scripts/validate-manifest-v2.mjs manifests/a01-buttons-v2.json",
"validate:a01-scroll-skins": "node scripts/validate-manifest-v2.mjs manifests/a01-scroll-skins-v3.json",
"rebuild:a01-buttons": "node scripts/rebuild-a01-buttons.mjs",
"verify:assets": "node scripts/verify-assets.mjs",
"build:a01-scroll-skins": "node scripts/build-a01-scroll-skins.mjs",
"verify:a01-scroll-skins": "node scripts/verify-a01-scroll-skins.mjs",
"build:g01-background-candidates": "node scripts/build-g01-backgrounds.mjs"
},
"dependencies": {
"sharp": "0.34.5"
+1
View File
@@ -0,0 +1 @@
Pillow==12.3.0
+98
View File
@@ -0,0 +1,98 @@
"""Deterministic PNG quality checks for generated design assets."""
from pathlib import Path
from typing import Any
from PIL import Image
def _outer_ring_pixels(image: Image.Image, thickness: int):
width, height = image.size
thickness = max(0, min(thickness, width // 2, height // 2))
for y in range(height):
for x in range(width):
if x < thickness or x >= width - thickness or y < thickness or y >= height - thickness:
yield image.getpixel((x, y))
def analyze_asset(path: Path, spec: dict[str, Any]) -> dict[str, Any]:
"""Analyze one PNG against its manifest v2 specification."""
path = Path(path)
errors: list[str] = []
warnings: list[str] = []
metrics: dict[str, int] = {}
with Image.open(path) as opened:
width, height = opened.size
mode = opened.mode
info = dict(opened.info)
has_alpha = "A" in opened.getbands() or "transparency" in opened.info
image = opened.convert("RGBA")
expected = spec["outputPixels"]
if (width, height) != (expected["width"], expected["height"]):
errors.append(
f"dimensions expected {expected['width']}x{expected['height']}, got {width}x{height}"
)
alpha = spec.get("alpha", {})
if alpha.get("required") and not has_alpha:
errors.append(f"alpha-required asset has no alpha channel or transparency table, got {mode}")
padding = int(alpha.get("transparentOuterPadding", 0))
max_alpha = int(alpha.get("cornerMaxAlpha", 0))
opaque_padding = sum(1 for pixel in _outer_ring_pixels(image, padding) if pixel[3] > max_alpha)
metrics["outerPaddingViolations"] = opaque_padding
if opaque_padding:
errors.append(f"outer padding contains {opaque_padding} pixels above alpha {max_alpha}")
edge = spec.get("edge", {})
pixels = list(image.get_flattened_data())
chroma_residue = sum(
1
for red, green, blue, pixel_alpha in pixels
if pixel_alpha > 8 and green > 120 and green - max(red, blue) > 80
)
metrics["chromaResiduePixels"] = chroma_residue
if edge.get("forbidChromaResidue") and chroma_residue:
errors.append(f"chroma residue detected in {chroma_residue} visible pixels")
light_fringe = sum(
1
for red, green, blue, pixel_alpha in pixels
if 0 < pixel_alpha < 255 and red > 235 and green > 235 and blue > 235
)
metrics["lightFringePixels"] = light_fringe
if edge.get("forbidLightFringe") and light_fringe:
errors.append(f"light fringe detected in {light_fringe} partially transparent pixels")
transparent_rgb = sum(
1
for red, green, blue, pixel_alpha in pixels
if pixel_alpha == 0 and (red != 0 or green != 0 or blue != 0)
)
metrics["transparentRgbPixels"] = transparent_rgb
if edge.get("premultipliedAlphaCheck") and transparent_rgb:
errors.append(f"transparent RGB detected in {transparent_rgb} fully transparent pixels")
byte_size = path.stat().st_size
max_bytes = int(spec.get("quality", {}).get("maxBytes", 0))
if max_bytes and byte_size > max_bytes:
errors.append(f"maxBytes {max_bytes} exceeded by file size {byte_size}")
expected_color_space = spec.get("quality", {}).get("colorSpace")
if expected_color_space == "sRGB" and not ("srgb" in info or "icc_profile" in info):
warnings.append("PNG does not declare an sRGB chunk or ICC profile")
return {
"id": spec.get("id", path.stem),
"path": str(path),
"width": width,
"height": height,
"mode": mode,
"bytes": byte_size,
"errors": errors,
"warnings": warnings,
"metrics": metrics,
}
@@ -0,0 +1,28 @@
import fs from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { loadAndValidateManifest } from './manifest-v2.mjs'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(scriptDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestPath = path.join(pipelineDirectory, 'manifests', 'a01-scroll-skins-v3.json')
const reportPath = path.join(pipelineDirectory, 'generated', 'a01-scroll-skins-v3', 'quality-report.json')
const localPython = path.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe')
const python = process.env.PYTHON || (fs.existsSync(localPython) ? localPython : 'python')
await loadAndValidateManifest(manifestPath, workspace)
function run(script, args) {
const result = spawnSync(python, [path.join(scriptDirectory, script), ...args], {
cwd: workspace,
encoding: 'utf8',
stdio: 'inherit',
})
if (result.error) throw new Error(`无法启动 Python${python}):${result.error.message}`)
if (result.status !== 0) process.exit(result.status ?? 1)
}
run('build_scroll_skins.py', [manifestPath, '--workspace', workspace])
run('verify-assets.py', [manifestPath, '--workspace', workspace, '--report', reportPath])
@@ -0,0 +1,33 @@
import fs from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(scriptDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestPath = path.join(pipelineDirectory, 'manifests', 'g01-background-candidates.json')
const reportPath = path.join(pipelineDirectory, 'generated', 'g01-background', 'build-report.json')
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
if (manifest.schemaVersion !== 1 || manifest.page !== 'G01' || manifest.candidates?.length !== 3) {
throw new Error('G01 background manifest must declare exactly three schema v1 candidates')
}
for (const candidate of manifest.candidates) {
const source = path.resolve(workspace, candidate.master)
const relative = path.relative(workspace, source)
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`${candidate.id} master escapes workspace`)
if (!fs.existsSync(source)) throw new Error(`${candidate.id} master is missing: ${candidate.master}`)
}
const localPython = path.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe')
const python = process.env.PYTHON || (fs.existsSync(localPython) ? localPython : 'python')
const result = spawnSync(
python,
[path.join(scriptDirectory, 'build_g01_backgrounds.py'), manifestPath, '--workspace', workspace, '--report', reportPath],
{ cwd: workspace, encoding: 'utf8', stdio: 'inherit' }
)
if (result.error) throw new Error(`无法启动 Python${python}):${result.error.message}`)
if (result.status !== 0) process.exit(result.status ?? 1)
@@ -0,0 +1,122 @@
"""Deterministically build G01 background candidates from saved ImageGen masters."""
import argparse
import hashlib
import json
from pathlib import Path
from typing import Any
from PIL import Image, PngImagePlugin
def process_chroma_image(source: Image.Image, config: dict[str, Any]) -> Image.Image:
"""Convert a green-screen master to clean RGBA with edge despill."""
start = int(config["dominanceStart"])
end = int(config["dominanceEnd"])
allowance = int(config["despillAllowance"])
if end <= start:
raise ValueError("dominanceEnd must be greater than dominanceStart")
output = Image.new("RGBA", source.size, (0, 0, 0, 0))
converted = []
for red, green, blue, source_alpha in source.convert("RGBA").get_flattened_data():
dominance = green - max(red, blue)
if green >= 100 and dominance > start:
removal = min(1.0, (dominance - start) / (end - start))
alpha = round(source_alpha * (1.0 - removal))
if alpha <= 2:
converted.append((0, 0, 0, 0))
continue
green = min(green, max(red, blue) + allowance)
converted.append((red, green, blue, alpha))
else:
converted.append((red, green, blue, source_alpha))
output.putdata(converted)
return output
def process_opaque_image(source: Image.Image) -> Image.Image:
"""Normalize a paper-backed master to deterministic opaque RGBA."""
output = source.convert("RGBA")
output.putalpha(255)
return output
def save_png(image: Image.Image, path: Path) -> None:
"""Save a deterministic PNG with an explicit standard-sRGB chunk."""
path.parent.mkdir(parents=True, exist_ok=True)
png_info = PngImagePlugin.PngInfo()
png_info.add(b"sRGB", b"\x00")
image.save(path, format="PNG", optimize=True, compress_level=9, pnginfo=png_info)
def _resolve_inside(workspace: Path, relative_path: str) -> Path:
absolute = (workspace / relative_path).resolve()
try:
absolute.relative_to(workspace.resolve())
except ValueError as error:
raise ValueError(f"path escapes workspace: {relative_path}") from error
return absolute
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def build(manifest_path: Path, workspace: Path, report_path: Path) -> list[dict[str, Any]]:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
chroma = manifest["chromaKey"]
reports = []
for candidate in manifest["candidates"]:
source_path = _resolve_inside(workspace, candidate["master"])
output_path = _resolve_inside(workspace, candidate["generatedOutput"])
with Image.open(source_path) as opened:
expected = candidate["sourcePixels"]
if opened.size != (expected["width"], expected["height"]):
raise ValueError(
f"{candidate['id']} expected {expected['width']}x{expected['height']}, got {opened.width}x{opened.height}"
)
if candidate["processingMode"] == "chroma-key":
output = process_chroma_image(opened, chroma)
elif candidate["processingMode"] == "opaque-paper":
output = process_opaque_image(opened)
else:
raise ValueError(f"unsupported processingMode: {candidate['processingMode']}")
save_png(output, output_path)
reports.append(
{
"id": candidate["id"],
"mode": candidate["processingMode"],
"source": candidate["master"],
"output": candidate["generatedOutput"],
"width": output.width,
"height": output.height,
"bytes": output_path.stat().st_size,
"sha256": _sha256(output_path),
}
)
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(reports, ensure_ascii=False, indent=2), encoding="utf-8")
return reports
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("manifest", type=Path)
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
reports = build(args.manifest, args.workspace.resolve(), args.report)
for report in reports:
print(f"G01-BACKGROUND-BUILD PASS {report['id']} {report['width']}x{report['height']} {report['sha256']}")
if __name__ == "__main__":
main()
@@ -0,0 +1,176 @@
"""Build exact A01 scroll-skin outputs from chroma-backed visual masters."""
import argparse
import json
import math
from pathlib import Path
from PIL import Image, PngImagePlugin
def remove_chroma_background(
image: Image.Image,
key: tuple[int, int, int],
tolerance: int,
feather: int = 72,
) -> Image.Image:
source = image.convert("RGBA")
cleaned = Image.new("RGBA", source.size, (0, 0, 0, 0))
result = []
key_red, key_green, key_blue = key
for red, green, blue, source_alpha in source.get_flattened_data():
distance = math.sqrt(
(red - key_red) ** 2 + (green - key_green) ** 2 + (blue - key_blue) ** 2
)
alpha_fraction = max(0.0, min(1.0, (distance - tolerance) / feather))
alpha_fraction *= source_alpha / 255
if alpha_fraction <= 0:
result.append((0, 0, 0, 0))
continue
recovered_red = round((red - (1 - alpha_fraction) * key_red) / alpha_fraction)
recovered_green = round((green - (1 - alpha_fraction) * key_green) / alpha_fraction)
recovered_blue = round((blue - (1 - alpha_fraction) * key_blue) / alpha_fraction)
result.append(
(
max(0, min(255, recovered_red)),
max(0, min(255, recovered_green)),
max(0, min(255, recovered_blue)),
round(alpha_fraction * 255),
)
)
cleaned.putdata(result)
return cleaned
def trim_transparent(image: Image.Image) -> Image.Image:
alpha = image.getchannel("A")
bounds = alpha.getbbox()
if bounds is None:
raise ValueError("chroma removal left no visible artwork")
return image.crop(bounds)
def stretch_safe_center(
image: Image.Image,
*,
output_size: tuple[int, int],
cap_width: int,
padding: int,
) -> Image.Image:
output_width, output_height = output_size
inner_width = output_width - padding * 2
inner_height = output_height - padding * 2
if inner_width <= 0 or inner_height <= 0:
raise ValueError("transparent padding leaves no drawable area")
if cap_width <= 0 or cap_width * 2 >= image.width:
raise ValueError(f"invalid capWidth {cap_width} for artwork width {image.width}")
target_cap_width = max(1, round(cap_width * inner_height / image.height))
center_width = inner_width - target_cap_width * 2
if center_width <= 0:
raise ValueError("scaled caps leave no room for the stretch-safe center")
left = image.crop((0, 0, cap_width, image.height))
center = image.crop((cap_width, 0, image.width - cap_width, image.height))
right = image.crop((image.width - cap_width, 0, image.width, image.height))
resampling = Image.Resampling.LANCZOS
left = left.resize((target_cap_width, inner_height), resampling)
center = center.resize((center_width, inner_height), resampling)
right = right.resize((target_cap_width, inner_height), resampling)
output = Image.new("RGBA", output_size, (0, 0, 0, 0))
output.alpha_composite(left, (padding, padding))
output.alpha_composite(center, (padding + target_cap_width, padding))
output.alpha_composite(right, (output_width - padding - target_cap_width, padding))
return output
def sanitize_output_edges(image: Image.Image) -> Image.Image:
cleaned = []
for red, green, blue, alpha in image.convert("RGBA").get_flattened_data():
chroma_residue = alpha > 0 and green > 120 and green - max(red, blue) > 80
light_fringe = 0 < alpha < 255 and red > 235 and green > 235 and blue > 235
if alpha == 0 or chroma_residue or light_fringe:
cleaned.append((0, 0, 0, 0))
else:
cleaned.append((red, green, blue, alpha))
output = Image.new("RGBA", image.size, (0, 0, 0, 0))
output.putdata(cleaned)
return output
def reduce_png_palette(image: Image.Image, colors: int = 192) -> Image.Image:
alpha = image.getchannel("A")
rgb = image.convert("RGB").quantize(
colors=colors,
method=Image.Quantize.MEDIANCUT,
dither=Image.Dither.NONE,
).convert("RGB")
output = rgb.convert("RGBA")
output.putalpha(alpha)
return sanitize_output_edges(output)
def parse_hex_color(value: str) -> tuple[int, int, int]:
if len(value) != 7 or not value.startswith("#"):
raise ValueError(f"invalid RGB hex color: {value}")
return tuple(int(value[index:index + 2], 16) for index in (1, 3, 5))
def build_asset(asset: dict, workspace: Path) -> None:
processing = asset["processing"]
alpha = asset["alpha"]
pixels = asset["outputPixels"]
source_path = workspace / asset["source"]
output_path = workspace / asset["output"]
with Image.open(source_path) as source:
cleaned = remove_chroma_background(
source,
parse_hex_color(processing["keyColor"]),
int(processing["keyTolerance"]),
)
artwork = trim_transparent(cleaned)
output = stretch_safe_center(
artwork,
output_size=(pixels["width"], pixels["height"]),
cap_width=int(processing["capWidth"]),
padding=int(alpha["transparentOuterPadding"]),
)
palette_colors = int(processing.get("paletteColors", 192))
output = sanitize_output_edges(output)
if processing.get("indexedPng"):
output = output.quantize(
colors=palette_colors,
method=Image.Quantize.FASTOCTREE,
dither=Image.Dither.NONE,
)
else:
output = reduce_png_palette(output, colors=palette_colors)
output_path.parent.mkdir(parents=True, exist_ok=True)
png_info = PngImagePlugin.PngInfo()
png_info.add(b"sRGB", b"\x00")
output.save(output_path, format="PNG", optimize=True, pnginfo=png_info)
print(f"BUILT {asset['id']} -> {asset['output']}")
def build_manifest(manifest_path: Path, workspace: Path) -> None:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
for asset in manifest["assets"]:
build_asset(asset, workspace)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("manifest", type=Path)
parser.add_argument("--workspace", type=Path, required=True)
args = parser.parse_args()
build_manifest(args.manifest.resolve(), args.workspace.resolve())
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
const allowedAssetClasses = new Set([
'code-native',
'fixed-bitmap',
'transparent-overlay',
'nine-slice',
'tile-texture'
])
const requireObject = (value, label) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be an object`)
}
return value
}
const requireString = (value, label) => {
if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} must be a non-empty string`)
return value
}
const requirePositiveNumber = (value, label) => {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
throw new Error(`${label} must be a positive number`)
}
return value
}
const resolveWorkspacePath = (workspace, relativePath, label) => {
requireString(relativePath, label)
const absolutePath = path.resolve(workspace, relativePath)
const relative = path.relative(workspace, absolutePath)
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error(`${label} escapes workspace: ${relativePath}`)
}
return absolutePath
}
const requireBoolean = (value, label) => {
if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`)
return value
}
export const validateManifest = (manifest, workspace) => {
requireObject(manifest, 'manifest')
if (manifest.schemaVersion !== 2) throw new Error('schemaVersion must be 2')
requireString(manifest.page, 'page')
requireObject(manifest.runtime, 'runtime')
requireString(manifest.runtime.url, 'runtime.url')
requirePositiveNumber(manifest.runtime.chromePort, 'runtime.chromePort')
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) throw new Error('assets must be a non-empty array')
const ids = new Set()
for (const asset of manifest.assets) {
requireObject(asset, 'asset')
const id = requireString(asset.id, 'asset.id')
if (ids.has(id)) throw new Error(`duplicate asset id: ${id}`)
ids.add(id)
if (!allowedAssetClasses.has(asset.assetClass)) throw new Error(`unsupported assetClass: ${asset.assetClass}`)
resolveWorkspacePath(workspace, asset.source, `${id}.source`)
resolveWorkspacePath(workspace, asset.output, `${id}.output`)
const logicalSlot = requireObject(asset.logicalSlot, `${id}.logicalSlot`)
const outputPixels = requireObject(asset.outputPixels, `${id}.outputPixels`)
const widthRpx = requirePositiveNumber(logicalSlot.widthRpx, `${id}.logicalSlot.widthRpx`)
const heightRpx = requirePositiveNumber(logicalSlot.heightRpx, `${id}.logicalSlot.heightRpx`)
const width = requirePositiveNumber(outputPixels.width, `${id}.outputPixels.width`)
const height = requirePositiveNumber(outputPixels.height, `${id}.outputPixels.height`)
const render = requireObject(asset.render, `${id}.render`)
requireString(render.scalePolicy, `${id}.render.scalePolicy`)
requireString(render.uniMode, `${id}.render.uniMode`)
requireBoolean(render.allowDistortion, `${id}.render.allowDistortion`)
if (render.scalePolicy === 'uniform-only' && render.uniMode === 'scaleToFill') {
throw new Error(`${id} uniform-only asset cannot use scaleToFill`)
}
const ratioDrift = Math.abs((width / height) / (widthRpx / heightRpx) - 1)
if (asset.assetClass === 'fixed-bitmap' && ratioDrift > 0.005) {
throw new Error(`${id} ratio drift exceeds 0.5%`)
}
const alpha = requireObject(asset.alpha, `${id}.alpha`)
requireBoolean(alpha.required, `${id}.alpha.required`)
requirePositiveNumber(alpha.transparentOuterPadding, `${id}.alpha.transparentOuterPadding`)
if (!Number.isInteger(alpha.cornerMaxAlpha) || alpha.cornerMaxAlpha < 0 || alpha.cornerMaxAlpha > 255) {
throw new Error(`${id}.alpha.cornerMaxAlpha must be an integer from 0 to 255`)
}
const edge = requireObject(asset.edge, `${id}.edge`)
requireBoolean(edge.forbidChromaResidue, `${id}.edge.forbidChromaResidue`)
requireBoolean(edge.forbidLightFringe, `${id}.edge.forbidLightFringe`)
requireBoolean(edge.premultipliedAlphaCheck, `${id}.edge.premultipliedAlphaCheck`)
const quality = requireObject(asset.quality, `${id}.quality`)
requirePositiveNumber(quality.maxBytes, `${id}.quality.maxBytes`)
requireString(quality.colorSpace, `${id}.quality.colorSpace`)
const processing = requireObject(asset.processing, `${id}.processing`)
requirePositiveNumber(processing.trim, `${id}.processing.trim`)
requirePositiveNumber(processing.capWidth, `${id}.processing.capWidth`)
const runtime = requireObject(asset.runtime, `${id}.runtime`)
requireString(runtime.selector, `${id}.runtime.selector`)
requireString(runtime.imageSelector, `${id}.runtime.imageSelector`)
if (!Array.isArray(asset.consumers) || asset.consumers.length === 0) {
throw new Error(`${id}.consumers must be a non-empty array`)
}
for (const consumer of asset.consumers) resolveWorkspacePath(workspace, consumer, `${id}.consumer`)
}
return manifest
}
export const loadAndValidateManifest = async (filePath, workspace) => {
const manifest = JSON.parse(await readFile(filePath, 'utf8'))
return validateManifest(manifest, workspace)
}
@@ -0,0 +1,31 @@
import fs from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { loadAndValidateManifest } from './manifest-v2.mjs'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(scriptDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestPath = path.join(pipelineDirectory, 'manifests', 'a01-buttons-v2.json')
const reportPath = path.join(pipelineDirectory, 'generated', 'a01-buttons-v2', 'quality-report.json')
await loadAndValidateManifest(manifestPath, workspace)
const localPython = path.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe')
const python = process.env.PYTHON || (fs.existsSync(localPython) ? localPython : 'python')
function runPython(script, args) {
const result = spawnSync(python, [path.join(scriptDirectory, script), ...args], {
cwd: workspace,
encoding: 'utf8',
stdio: 'inherit',
})
if (result.error) {
throw new Error(`无法启动 Python${python}):${result.error.message}`)
}
if (result.status !== 0) process.exit(result.status ?? 1)
}
runPython('rebuild_a01_buttons.py', [manifestPath, '--workspace', workspace])
runPython('verify-assets.py', [manifestPath, '--workspace', workspace, '--report', reportPath])
@@ -0,0 +1,89 @@
"""Rebuild A01 button skins with deterministic horizontal three-slice scaling."""
import argparse
import json
from pathlib import Path
from PIL import Image, PngImagePlugin
def rebuild_button(
source_path: Path,
output_path: Path,
*,
output_size: tuple[int, int],
trim: int,
padding: int,
cap_width: int,
) -> None:
"""Trim contaminated edges, preserve both caps, and stretch only the center."""
with Image.open(source_path) as opened:
source = opened.convert("RGBA")
if trim < 0 or trim * 2 >= min(source.size):
raise ValueError(f"invalid trim {trim} for source size {source.size}")
if trim:
source = source.crop((trim, trim, source.width - trim, source.height - trim))
if cap_width <= 0 or cap_width * 2 >= source.width:
raise ValueError(f"invalid capWidth {cap_width} for cropped width {source.width}")
output_width, output_height = output_size
inner_width = output_width - padding * 2
inner_height = output_height - padding * 2
if inner_width <= 0 or inner_height <= 0:
raise ValueError(f"padding {padding} leaves no drawable area in {output_size}")
target_cap_width = max(1, round(cap_width * inner_height / source.height))
if target_cap_width * 2 >= inner_width:
raise ValueError("scaled caps leave no room for the center slice")
left = source.crop((0, 0, cap_width, source.height))
center = source.crop((cap_width, 0, source.width - cap_width, source.height))
right = source.crop((source.width - cap_width, 0, source.width, source.height))
resampling = Image.Resampling.LANCZOS
left = left.resize((target_cap_width, inner_height), resampling)
center = center.resize((inner_width - target_cap_width * 2, inner_height), resampling)
right = right.resize((target_cap_width, inner_height), resampling)
output = Image.new("RGBA", output_size, (0, 0, 0, 0))
output.alpha_composite(left, (padding, padding))
output.alpha_composite(center, (padding + target_cap_width, padding))
output.alpha_composite(right, (output_width - padding - target_cap_width, padding))
output_path.parent.mkdir(parents=True, exist_ok=True)
png_info = PngImagePlugin.PngInfo()
png_info.add(b"sRGB", b"\x00")
output.save(output_path, format="PNG", optimize=True, pnginfo=png_info)
def rebuild_manifest(manifest_path: Path, workspace_root: Path) -> None:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
for asset in manifest["assets"]:
processing = asset["processing"]
alpha = asset["alpha"]
pixels = asset["outputPixels"]
source = workspace_root / asset["source"]
output = workspace_root / asset["output"]
rebuild_button(
source,
output,
output_size=(pixels["width"], pixels["height"]),
trim=processing["trim"],
padding=alpha["transparentOuterPadding"],
cap_width=processing["capWidth"],
)
print(f"REBUILT {asset['id']} -> {asset['output']}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("manifest", type=Path)
parser.add_argument("--workspace", type=Path, required=True)
args = parser.parse_args()
rebuild_manifest(args.manifest.resolve(), args.workspace.resolve())
if __name__ == "__main__":
main()
@@ -0,0 +1,15 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { loadAndValidateManifest } from './manifest-v2.mjs'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(scriptDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestArgument = process.argv[2]
if (!manifestArgument) throw new Error('Usage: node validate-manifest-v2.mjs <manifest>')
const manifestPath = path.resolve(pipelineDirectory, manifestArgument)
await loadAndValidateManifest(manifestPath, workspace)
process.stdout.write(`MANIFEST-V2 PASS ${path.relative(workspace, manifestPath).replaceAll('\\', '/')}\n`)
@@ -0,0 +1,22 @@
import fs from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { loadAndValidateManifest } from './manifest-v2.mjs'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(scriptDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestPath = path.join(pipelineDirectory, 'manifests', 'a01-scroll-skins-v3.json')
const reportPath = path.join(pipelineDirectory, 'generated', 'a01-scroll-skins-v3', 'quality-report.json')
const localPython = path.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe')
const python = process.env.PYTHON || (fs.existsSync(localPython) ? localPython : 'python')
await loadAndValidateManifest(manifestPath, workspace)
const result = spawnSync(
python,
[path.join(scriptDirectory, 'verify-assets.py'), manifestPath, '--workspace', workspace, '--report', reportPath],
{ cwd: workspace, encoding: 'utf8', stdio: 'inherit' }
)
if (result.error) throw new Error(`无法启动 Python${python}):${result.error.message}`)
if (result.status !== 0) process.exit(result.status ?? 1)
+24
View File
@@ -0,0 +1,24 @@
import fs from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { loadAndValidateManifest } from './manifest-v2.mjs'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(scriptDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestPath = path.join(pipelineDirectory, 'manifests', 'a01-buttons-v2.json')
const reportPath = path.join(pipelineDirectory, 'generated', 'a01-buttons-v2', 'quality-report.json')
await loadAndValidateManifest(manifestPath, workspace)
const localPython = path.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe')
const python = process.env.PYTHON || (fs.existsSync(localPython) ? localPython : 'python')
const result = spawnSync(
python,
[path.join(scriptDirectory, 'verify-assets.py'), manifestPath, '--workspace', workspace, '--report', reportPath],
{ cwd: workspace, encoding: 'utf8', stdio: 'inherit' }
)
if (result.error) throw new Error(`无法启动 Python${python}):${result.error.message}`)
if (result.status !== 0) process.exit(result.status ?? 1)
+47
View File
@@ -0,0 +1,47 @@
"""Verify generated assets declared in a manifest v2 file."""
import argparse
import json
import sys
from pathlib import Path
from asset_quality import analyze_asset
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("manifest", type=Path)
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
reports = []
failed = False
for asset in manifest["assets"]:
output = args.workspace / asset["output"]
if not output.is_file():
report = {
"id": asset["id"],
"path": str(output),
"errors": ["output file is missing"],
"warnings": [],
"metrics": {},
}
else:
report = analyze_asset(output, asset)
reports.append(report)
if report["errors"]:
failed = True
print(f"ASSET-QUALITY FAIL {asset['id']}: {'; '.join(report['errors'])}")
else:
print(f"ASSET-QUALITY PASS {asset['id']}")
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(reports, ensure_ascii=False, indent=2), encoding="utf-8")
if failed:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const testDirectory = path.dirname(fileURLToPath(import.meta.url))
const workspace = path.resolve(testDirectory, '..', '..')
const manifestPath = path.join(workspace, 'design-pipeline', 'manifests', 'g01-background-candidates.json')
const wrapperPath = path.join(workspace, 'design-pipeline', 'scripts', 'build-g01-backgrounds.mjs')
test('G01 background candidate manifest preserves three portable masters', async () => {
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
assert.equal(manifest.schemaVersion, 1)
assert.equal(manifest.page, 'G01')
assert.equal(manifest.status, 'candidates-pending-user-selection')
assert.equal(manifest.candidates.length, 3)
const ids = new Set()
for (const candidate of manifest.candidates) {
assert.ok(!ids.has(candidate.id), `duplicate candidate id: ${candidate.id}`)
ids.add(candidate.id)
assert.match(candidate.prompt, /1024x1536/)
assert.deepEqual(candidate.sourcePixels, { width: 1024, height: 1536 })
assert.ok(['chroma-key', 'opaque-paper'].includes(candidate.processingMode))
for (const key of ['master', 'generatedOutput']) {
const absolute = path.resolve(workspace, candidate[key])
const relative = path.relative(workspace, absolute)
assert.ok(!relative.startsWith('..') && !path.isAbsolute(relative), `${key} escapes workspace`)
}
}
})
test('G01 Node wrapper delegates pixel decoding to locked Pillow without Sharp', async () => {
const wrapper = await readFile(wrapperPath, 'utf8')
assert.doesNotMatch(wrapper, /from ['"]sharp['"]/)
assert.match(wrapper, /build_g01_backgrounds\.py/)
})
@@ -0,0 +1,72 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { validateManifest } from '../scripts/manifest-v2.mjs'
const testDirectory = path.dirname(fileURLToPath(import.meta.url))
const workspace = path.resolve(testDirectory, '..', '..')
const validAsset = () => ({
id: 'a01-primary-button-v2',
assetClass: 'fixed-bitmap',
source: 'static/assets/foundation/opaque/a01-primary-button.png',
output: 'static/assets/foundation/transparent/a01-primary-button-v2.png',
logicalSlot: { widthRpx: 622, heightRpx: 92 },
outputPixels: { width: 1866, height: 276 },
render: { scalePolicy: 'uniform-only', uniMode: 'aspectFit', allowDistortion: false },
alpha: { required: true, transparentOuterPadding: 12, cornerMaxAlpha: 0 },
edge: { forbidChromaResidue: true, forbidLightFringe: true, premultipliedAlphaCheck: true },
quality: { maxBytes: 500000, symmetry: 'horizontal', colorSpace: 'sRGB' },
processing: { trim: 8, capWidth: 180 },
runtime: { selector: '.login-submit', imageSelector: '.button-skin img' },
consumers: ['pages/auth/a01-entry.vue']
})
const validManifest = () => ({
schemaVersion: 2,
page: 'A01',
runtime: { url: 'http://localhost:5173/#/pages/auth/a01-entry', chromePort: 9222 },
assets: [validAsset()]
})
test('accepts a complete manifest v2', () => {
const manifest = validManifest()
assert.equal(validateManifest(manifest, workspace), manifest)
})
test('rejects duplicate asset ids', () => {
const manifest = validManifest()
manifest.assets.push(validAsset())
assert.throws(() => validateManifest(manifest, workspace), /duplicate asset id/i)
})
test('rejects paths that escape the workspace', () => {
const manifest = validManifest()
manifest.assets[0].output = '../outside.png'
assert.throws(() => validateManifest(manifest, workspace), /escapes workspace/i)
})
test('rejects a fixed bitmap whose output ratio differs from its slot', () => {
const manifest = validManifest()
manifest.assets[0].outputPixels.height = 300
assert.throws(() => validateManifest(manifest, workspace), /ratio drift/i)
})
test('rejects scaleToFill for uniform-only assets', () => {
const manifest = validManifest()
manifest.assets[0].render.uniMode = 'scaleToFill'
assert.throws(() => validateManifest(manifest, workspace), /scaleToFill/i)
})
test('rejects unknown asset classes', () => {
const manifest = validManifest()
manifest.assets[0].assetClass = 'mystery'
assert.throws(() => validateManifest(manifest, workspace), /assetClass/i)
})
test('rejects assets without consumers', () => {
const manifest = validManifest()
manifest.assets[0].consumers = []
assert.throws(() => validateManifest(manifest, workspace), /consumers/i)
})
@@ -0,0 +1,30 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { validateManifest } from '../scripts/manifest-v2.mjs'
const testDirectory = path.dirname(fileURLToPath(import.meta.url))
const pipelineDirectory = path.resolve(testDirectory, '..')
const workspace = path.resolve(pipelineDirectory, '..')
const manifestPath = path.join(pipelineDirectory, 'manifests', 'a01-scroll-skins-v3.json')
test('declares the approved A01 scroll-skin family', async () => {
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
validateManifest(manifest, workspace)
assert.equal(manifest.assets.length, 4)
assert.deepEqual(
manifest.assets.map(({ id }) => id),
['a01-scroll-primary-v3', 'a01-scroll-secondary-v3', 'a01-scroll-toast-v3', 'a01-scroll-dialog-v3']
)
assert.deepEqual(
manifest.assets.map(({ outputPixels }) => [outputPixels.width, outputPixels.height]),
[[1866, 276], [1866, 300], [1770, 246], [1860, 1560]]
)
assert.equal(manifest.assets[2].assetClass, 'nine-slice')
assert.equal(manifest.assets[2].render.scalePolicy, 'nine-slice')
assert.equal(manifest.assets[3].render.uniMode, 'aspectFit')
assert(manifest.assets.every(({ consumers }) => consumers.includes('pages/auth/a01-entry.vue')))
})
+100
View File
@@ -0,0 +1,100 @@
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
from asset_quality import analyze_asset
class AssetQualityTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
def spec(self):
return {
"id": "test-button",
"outputPixels": {"width": 32, "height": 16},
"alpha": {"required": True, "transparentOuterPadding": 2, "cornerMaxAlpha": 0},
"edge": {
"forbidChromaResidue": True,
"forbidLightFringe": True,
"premultipliedAlphaCheck": True,
},
"quality": {"maxBytes": 10000, "colorSpace": "sRGB"},
}
def clean_image(self):
image = Image.new("RGBA", (32, 16), (0, 0, 0, 0))
for y in range(2, 14):
for x in range(2, 30):
image.putpixel((x, y), (150, 20, 12, 255))
return image
def save(self, image, name="asset.png"):
path = self.root / name
image.save(path, format="PNG")
return path
def error_text(self, report):
return " ".join(report["errors"])
def test_accepts_clean_rgba_asset(self):
report = analyze_asset(self.save(self.clean_image()), self.spec())
self.assertEqual([], report["errors"])
self.assertEqual(32, report["width"])
self.assertEqual(16, report["height"])
def test_accepts_indexed_png_with_real_transparency(self):
indexed = self.clean_image().quantize(
colors=16,
method=Image.Quantize.FASTOCTREE,
dither=Image.Dither.NONE,
)
report = analyze_asset(self.save(indexed), self.spec())
self.assertEqual([], report["errors"])
self.assertEqual("P", report["mode"])
def test_rejects_wrong_dimensions(self):
report = analyze_asset(self.save(Image.new("RGBA", (31, 16), (0, 0, 0, 0))), self.spec())
self.assertIn("dimensions", self.error_text(report))
def test_rejects_opaque_outer_padding(self):
image = self.clean_image()
image.putpixel((0, 0), (120, 30, 20, 255))
report = analyze_asset(self.save(image), self.spec())
self.assertIn("outer padding", self.error_text(report))
def test_rejects_visible_green_residue(self):
image = self.clean_image()
image.putpixel((16, 8), (0, 255, 0, 255))
report = analyze_asset(self.save(image), self.spec())
self.assertIn("chroma residue", self.error_text(report))
def test_rejects_light_partially_transparent_fringe(self):
image = self.clean_image()
image.putpixel((2, 8), (250, 250, 250, 128))
report = analyze_asset(self.save(image), self.spec())
self.assertIn("light fringe", self.error_text(report))
def test_rejects_hidden_rgb_in_fully_transparent_pixels(self):
image = self.clean_image()
image.putpixel((0, 0), (255, 255, 255, 0))
report = analyze_asset(self.save(image), self.spec())
self.assertIn("transparent RGB", self.error_text(report))
def test_rejects_file_over_max_bytes(self):
spec = self.spec()
spec["quality"]["maxBytes"] = 8
report = analyze_asset(self.save(self.clean_image()), spec)
self.assertIn("maxBytes", self.error_text(report))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,64 @@
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
from build_g01_backgrounds import process_chroma_image, process_opaque_image, save_png
class G01BackgroundBuilderTests(unittest.TestCase):
def config(self):
return {
"dominanceStart": 30,
"dominanceEnd": 220,
"despillAllowance": 10,
}
def test_pure_green_becomes_clean_transparency(self):
source = Image.new("RGB", (2, 1), (0, 255, 0))
output = process_chroma_image(source, self.config())
self.assertEqual((0, 0, 0, 0), output.getpixel((0, 0)))
def test_warm_artwork_remains_opaque(self):
source = Image.new("RGB", (1, 1), (210, 190, 150))
output = process_chroma_image(source, self.config())
self.assertEqual((210, 190, 150, 255), output.getpixel((0, 0)))
def test_antialiased_green_edge_is_translucent_and_despilled(self):
source = Image.new("RGB", (1, 1), (100, 200, 90))
output = process_chroma_image(source, self.config())
red, green, blue, alpha = output.getpixel((0, 0))
self.assertGreater(alpha, 0)
self.assertLess(alpha, 255)
self.assertLessEqual(green, max(red, blue) + 10)
def test_opaque_master_is_normalized_to_rgba_without_transparency(self):
source = Image.new("RGB", (1, 1), (240, 235, 220))
output = process_opaque_image(source)
self.assertEqual("RGBA", output.mode)
self.assertEqual((240, 235, 220, 255), output.getpixel((0, 0)))
def test_saved_png_declares_srgb_color_profile(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "asset.png"
save_png(Image.new("RGBA", (1, 1), (240, 235, 220, 255)), path)
with Image.open(path) as opened:
self.assertIn("srgb", opened.info)
def test_saved_png_bytes_are_deterministic(self):
with tempfile.TemporaryDirectory() as directory:
first = Path(directory) / "first.png"
second = Path(directory) / "second.png"
image = Image.new("RGBA", (2, 2), (240, 235, 220, 255))
save_png(image, first)
save_png(image, second)
self.assertEqual(first.read_bytes(), second.read_bytes())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,92 @@
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
from build_scroll_skins import build_asset, remove_chroma_background, sanitize_output_edges, stretch_safe_center
class BuildScrollSkinsTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
def test_chroma_background_becomes_transparent_black(self):
image = Image.new("RGB", (4, 2), (0, 255, 0))
image.putpixel((1, 0), (180, 30, 20))
cleaned = remove_chroma_background(image, (0, 255, 0), tolerance=80)
self.assertEqual((0, 0, 0, 0), cleaned.getpixel((0, 0)))
self.assertEqual((180, 30, 20, 255), cleaned.getpixel((1, 0)))
def test_safe_center_stretch_preserves_caps_and_exact_size(self):
image = Image.new("RGBA", (30, 10), (150, 20, 10, 255))
for y in range(10):
for x in range(5):
image.putpixel((x, y), (220, 170, 40, 255))
image.putpixel((29 - x, y), (220, 170, 40, 255))
output = stretch_safe_center(
image,
output_size=(60, 20),
cap_width=5,
padding=2,
)
self.assertEqual((60, 20), output.size)
self.assertEqual((0, 0, 0, 0), output.getpixel((0, 0)))
self.assertEqual((220, 170, 40, 255), output.getpixel((3, 10)))
self.assertEqual((150, 20, 10, 255), output.getpixel((30, 10)))
self.assertEqual((220, 170, 40, 255), output.getpixel((56, 10)))
def test_sanitizes_only_forbidden_edge_artifacts(self):
image = Image.new("RGBA", (3, 1), (248, 240, 220, 255))
image.putpixel((0, 0), (0, 255, 0, 128))
image.putpixel((1, 0), (250, 250, 250, 96))
cleaned = sanitize_output_edges(image)
self.assertEqual((0, 0, 0, 0), cleaned.getpixel((0, 0)))
self.assertEqual((0, 0, 0, 0), cleaned.getpixel((1, 0)))
self.assertEqual((248, 240, 220, 255), cleaned.getpixel((2, 0)))
def test_build_asset_honors_declared_palette_size(self):
source = Image.new("RGB", (32, 16), (0, 255, 0))
for y in range(1, 15):
for x in range(1, 31):
source.putpixel((x, y), (120 + (x % 30) * 4, 20 + y, 10))
source_path = self.root / "source.png"
output_path = self.root / "output.png"
source.save(source_path)
asset = {
"id": "palette-test",
"source": "source.png",
"output": "output.png",
"outputPixels": {"width": 60, "height": 30},
"alpha": {"transparentOuterPadding": 1},
"processing": {
"capWidth": 4,
"keyColor": "#00FF00",
"keyTolerance": 80,
"paletteColors": 8,
},
}
build_asset(asset, self.root)
with Image.open(output_path) as output:
visible_colors = {
pixel[:3] for pixel in output.convert("RGBA").get_flattened_data() if pixel[3] > 0
}
self.assertLessEqual(len(visible_colors), 8)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,49 @@
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image, ImageDraw
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
from rebuild_a01_buttons import rebuild_button
class RebuildA01ButtonTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
def make_source(self):
image = Image.new("RGBA", (60, 30), (0, 255, 0, 255))
draw = ImageDraw.Draw(image)
draw.rectangle((4, 4, 55, 25), fill=(150, 20, 12, 255))
draw.rectangle((4, 4, 13, 25), fill=(210, 160, 40, 255))
draw.rectangle((46, 4, 55, 25), fill=(210, 160, 40, 255))
return image
def test_rebuilds_three_slice_into_clean_transparent_canvas(self):
source = self.root / "source.png"
output = self.root / "output.png"
self.make_source().save(source)
rebuild_button(source, output, output_size=(180, 60), trim=4, padding=6, cap_width=10)
with Image.open(output) as image:
self.assertEqual("RGBA", image.mode)
self.assertEqual((180, 60), image.size)
self.assertEqual((0, 0, 0, 0), image.getpixel((0, 0)))
self.assertEqual((0, 0, 0, 0), image.getpixel((179, 59)))
visible_pixels = [pixel for pixel in image.get_flattened_data() if pixel[3] > 8]
self.assertFalse(
any(green > 120 and green - max(red, blue) > 80 for red, green, blue, green_alpha in visible_pixels)
)
self.assertGreater(image.getpixel((7, 30))[0], image.getpixel((90, 30))[0])
self.assertGreater(image.getpixel((172, 30))[0], image.getpixel((90, 30))[0])
if __name__ == "__main__":
unittest.main()
+71 -33
View File
@@ -1,3 +1,37 @@
# Design QA — A01 卷轴祥云四件套 v3
- approved visual source: `docs/design/assets/a01-vnext/A01-shared-scroll-skins-with-dialog-approved.png`
- H5 runtime evidence: `tmp/a01-scroll-v3-password-412x915.png``tmp/a01-scroll-v3-toast-412x915.png``tmp/a01-scroll-v3-dialog-412x915.png`
- viewport: 412×915;自动运行 smoke 另覆盖 320×568、360×640、360×800
- scope: 登录主按钮、微信次按钮、反馈 Toast;安全验证弹窗已按用户最新决定降为流程占位,不属于最终视觉
## Findings
当前真实 H5 运行图未发现四件套相关的 P0/P1/P2 问题。
- 视觉统一:四件套均采用用户选定的“方案 2”卷轴祥云体系,主按钮为朱砂底,次按钮、Toast 与弹窗为暖宣纸底,古金边框和角饰属于同一套装饰语言。
- 资产质量:四张运行资产外角透明,没有旧按钮的白边/浅绿边;按钮与弹窗使用等比显示,Toast 使用固定切片的 `border-image`,没有整体拉伸角饰。
- 内容分层:按钮文字、微信图标、Toast 文案和弹窗内容仍由 Vue 承载,位图只负责复杂装饰表面;文字可访问性和状态切换不被烘焙进图片。
- 真实运行:412×915 的密码/短信、掩码/明文、无效手机号 Toast、协议错误和协议勾选状态均已实际触发;320×568 继续允许自然纵向滚动。
- 用户决定:2026-07-16 已明确通过除真实滑动验证外的 7 类 A01 H5 视觉状态,并确认缩小后的品牌 Logo;真实滑动验证内部视觉使用接口组件自带样式,延期到接入阶段审核。
## Evidence limits
除真实滑动验证外的 7 类 H5 状态已取得用户视觉确认,但这仍不是 A01 整页冻结。`tmp/` 截图不作为长期交接代表图;真实滑动验证组件、Android/HBuilderX 的状态栏、安全区、软键盘、字体放大、触摸和返回手势仍未复核,因此 A01 保持 `[~]`
## Implementation checklist
- [x] 四张独立 v3 资产通过 manifest、Node/Python 测试与像素质量门
- [x] A01 静态合同、透明角审计和四档 H5 运行 smoke
- [x] 用户确认 7 类非滑动验证 H5 视觉状态与缩小后的品牌 Logo
- [ ] 接入真实滑动验证组件并审核其自带样式
- [ ] Android/HBuilderX 真机或模拟器复核
- [ ] 用户验收并冻结 A01 整页
final result: partially accepted (7 H5 visual states accepted; slider verification and Android review pending)
---
# Design QA — T04 新增亲属
- representative contact sheet: `docs/design/screens/handoff/2026-07-16/T04-current-form-success-contact-720x800.png`
@@ -316,9 +350,10 @@ final result: passed (internal H5 candidate QA only; user review pending)
# Design QA — A06 阻断登录状态
- archive status: 2026-07-16 用户确认当前样式没有问题,但当前流程暂时不用该页;已从 `pages.json` 和活动验收队列移除,源码、测试和证据保留。
- 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
- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A06-current-risk-v2-412x915.png`
- viewport: 412×915;运行 smoke 另查 320×568、360×640、360×800
- state: 账号冻结、账号停用、风险限制、未知旧状态回落、自定义恢复说明层
## Findings
@@ -327,12 +362,12 @@ final result: passed (internal H5 candidate QA only; user review pending)
- 产品边界:旧 normal/failed/register/wechat 状态已移除;A06 仅保留冻结、停用、风险限制,普通登录错误继续由 A01 就近承接。
- 信息完整性:每种状态明确展示限制原因、影响范围和恢复方式,不只给出模糊失败提示。
- 视觉与反馈:保持认证系列底板和品牌印;恢复入口使用自定义说明层,不调用原生 Toast/Modal
- 尺寸适配:档无横向溢出,320×568 按正常比例自然滚动。
- 视觉与反馈:Logo 已同步 A01 的 `184×221rpx` 槽位;页面继承全局楷体,标题分隔、两处朱砂按钮和恢复卷轴均使用 A01 新资产,不再引用旧按钮或旧面板
- 尺寸适配:档无横向溢出,320×568 与 360×640 按正常比例自然滚动。
## Evidence limits
当前结论仅为 HBuilderX H5 + Chrome 内部 QA,不等于用户验收;真实状态来源、账号申诉服务、Android 返回手势和字体放大仍待功能/真机阶段
当前结论仅说明用户对封存前 H5 样式没有异议,不等于完整页面验收或冻结;真实状态来源、账号申诉服务、Android 返回手势和字体放大均未验证。封存期间运行 smoke 明确 SKIP;恢复路由后必须重新执行
## Implementation checklist
@@ -340,79 +375,82 @@ final result: passed (internal H5 candidate QA only; user review pending)
- [x] 原因、影响范围、恢复入口完整
- [x] 未知旧状态安全回落到风险限制
- [x] 自定义恢复说明层与返回 A01
- [x] 三档 H5 响应式和运行烟测
- [x] A01 新 Logo、全局字体、标题分隔、两处按钮和恢复卷轴
- [x] 封存前四档 H5 响应式和运行烟测
- [x] 从活动路由和验收表移除,源码/测试/证据保留
- [ ] Android/HBuilderX 真机或模拟器复核
- [ ] 用户验收
- [ ] 恢复路由后的用户逐态验收
final result: passed (internal H5 candidate QA only; user review pending)
final result: archived (H5 style had no user objection; not an accepted or frozen active page)
---
# 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: 默认表单、手机号错误、发送验证码前验证、验证关闭保值、发送反馈、密码不一致、自定义成功结果
- source visual truth path: A01/A04 已确定的红色宗祠水墨认证系列`docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png`
- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A05-current-reset-v2-412x915.png`
- viewport: 412×915;运行 smoke 另查 320×568、360×640、360×800
- state: 默认表单、手机号错误、滑动验证待接入 Toast、全字段错误、密码不一致、自定义成功结果
## Findings
没有遗留的 H5 P0/P1/P2 问题。
- 视觉连续性:使用同一宗祠水墨底板、独立品牌印、朱砂主按钮和宣纸表单,旧大面板已移除
- 流程顺序:获取验证码先校验手机号,再打开自定义行为验证;重设提交不重复验证
- 错误与结果:四字段错误就近显示,验证码发送反馈和重设成功结果均为项目内自定义组件,不使用原生 UniApp 提示。
- 尺寸适配:档无横向溢出320×568 保持正常比例并自然滚动。
- 视觉连续性:Logo 已同步 A01 的 `184×221rpx` 槽位;页面继承全局楷体,标题分隔、页面主按钮、成功结果按钮和 Toast 均迁移到 A01 新资产体系
- 流程边界:获取验证码先校验手机号;通过后只提示“滑动验证待接口接入”,不再用旧面板伪造验证完成、验证码已发送或重新获取状态
- 错误与结果:四字段错误继续就近显示;成功结果使用新卷轴容器和新版朱砂按钮,不用原生 UniApp 提示。
- 尺寸适配:档无横向溢出320×568 和 360×640 保持正常比例并自然纵向滚动。
## Evidence limits
当前结论仅为 HBuilderX H5 + Chrome 内部 QA,不等于用户验收;真实短信、行为验证令牌、重设接口、Android 软键盘与成功后手机号回填属于后续功能/真机复核
2026-07-16 用户已逐态确认当前 7 类 H5 视觉状态。真实滑动验证使用接口组件自带样式;真实短信、重设接口、Android 状态栏、安全区、软键盘、字体放大和返回手势属于后续功能/真机复核。返回 A01 后回填手机号并聚焦密码框由用户明确延期到真实重置接口链路接入时解决,不阻塞当前样式验收
## Implementation checklist
- [x] A01/A04 同系列视觉与真实资产
- [x] 发送验证码前自定义行为验证
- [x] 关闭验证保留表单与自定义发送反馈
- [x] A01/A04 同系列 Logo、全局字体、标题分隔、按钮与 Toast
- [x] 移除旧验证面板和伪验证码发送状态
- [x] 手机号通过后使用滑动验证待接入反馈并保留表单
- [x] 四字段页内校验与自定义成功结果
- [x] 档 H5 响应式和运行烟测
- [x] 档 H5 响应式和真实运行烟测
- [x] 用户确认当前 7 类 H5 视觉状态(接口回流数据与焦点延期)
- [ ] Android/HBuilderX 真机或模拟器复核
- [ ] 用户验收
final result: passed (internal H5 candidate QA only; user review pending)
final result: user H5 visual review passed for implemented states; real slider, interface return data/focus and Android 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: 默认注册、空表单错误、协议错误、自定义行为验证、自定义反馈
- implementation screenshot path: `docs/design/screens/runtime/2026-07-16/A04-current-register-v2-412x915.png`
- viewport: 412×915;运行 smoke 另查 320×568、360×640、360×800
- state: 默认注册、空表单错误、协议错误、协议勾选、新版自定义反馈
## Findings
没有遗留的 H5 P0/P1/P2 问题。
- 视觉连续性:沿用 A01 的红色宗祠、水墨宣纸、独立 `brand-seal.png`、朱砂按钮与古金分隔,不再使用旧大厅与大面板。
- 视觉连续性:Logo 已同步 A01 的 `184×221rpx` 槽位;标题分隔、朱砂按钮和 Toast 已迁移到 A01 新资产体系,页面统一继承全局楷体,不再用旧按钮、旧 Toast 图片层或旧验证面板。
- 表单层级:手机号、设置密码、确认密码三行保持清晰标签和输入提示;错误信息就近显示,不调用原生 UniApp 提示。
- 尺寸适配:三档宽高无横向溢出;320×568 按最新决定保持正常比例并自然纵向滚动。
- 流程验证:A01 注册入口、三字段校验、协议校验、自定义验证层、自定义反馈以及返回登录均通过浏览器运行烟测。
- 流程验证:A01 注册入口、三字段校验、协议校验、新版 Toast 反馈以及返回登录均通过浏览器运行烟测;完整本地校验后只提示“滑动验证待接口接入”
## Evidence limits
当前结论仅为 HBuilderX H5 + Chrome 内部 QA,通过不等于用户验收;Android 状态栏、安全区、软键盘、字体放大和返回手势仍需真机或模拟器复核。
2026-07-16 用户已逐态确认当前可实现的 8 类 H5 视觉状态;真实滑动验证使用接口组件自带样式,注册成功/失败真实链路留到接口阶段审核。Android 状态栏、安全区、软键盘、字体放大和返回手势仍需真机或模拟器复核,因此 A04 继续保持 `[~]`,不冻结
## Implementation checklist
- [x] A01 同系列视觉与真实资产
- [x] 三字段就近校验与协议页内错误
- [x] 自定义验证层与反馈提示
- [x] 320×568 自然滚动、360×800 与 412×915 无横向溢出
- [x] A01 新 Logo、标题分隔、主按钮、Toast 与全局字体
- [x] 移除非最终自定义验证层,保留滑动验证待接入反馈
- [x] 320×568 自然滚动、360×640、360×800 与 412×915 无横向溢出
- [x] H5 静态契约与真实运行烟测
- [ ] Android/HBuilderX 真机或模拟器复核
- [ ] 用户验收
- [x] 用户确认当前 8 类 H5 视觉状态(真实滑动验证与接口结果态除外)
final result: passed (internal H5 candidate QA only; user review pending)
final result: user H5 visual review passed for implemented states; real slider, interface result states and Android review pending
---
@@ -0,0 +1,72 @@
# G01 列表背景候选与换机重建
> 日期:2026-07-16
> 状态:三个母版已生成并通过本机确定性构建;尚未由用户选择,不得接入页面或写成已验收。
## 1. 为什么重做
现有 `footer-mountain-bamboo.png``750×360`,视觉主体集中在底部,上半部留白较大;页面又把它定位到整个内容末尾并使用 `scaleToFill` 拉伸,因此列表变长时山水被推到“添加家谱”下方。用户提出背景应更早出现,并进一步提出 G01 可能只滚动下方列表、上方当前家谱与快捷入口固定。
滚动结构暂不实施。新背景先作为列表滚动窗口候选资产保存,下一轮选择方向并确认滚动边界后再接入。
## 2. 稳定输入
三个母版位于:
- `docs/design/assets/g01-background/masters/g01-list-background-direction-a-chroma-master.png`
- `docs/design/assets/g01-background/masters/g01-list-background-direction-b-chroma-master.png`
- `docs/design/assets/g01-background/masters/g01-list-background-direction-c-paper-master.png`
准确提示词、标题、处理模式、源尺寸和生成输出路径由 `design-pipeline/manifests/g01-background-candidates.json` 单一维护。A/B 使用纯 `#00ff00` 绿幕并提取透明叠加层;C 保留完整暖宣纸底。
## 3. 跨电脑重建
要求:Windows、Node.js 22+、npm、Python 3.12+。本机验证环境为 Node `24.15.0`、npm `11.12.1`、Python `3.14.6`、Pillow `12.3.0`
在项目根目录执行:
```powershell
python --version
node --version
npm.cmd --version
python -m venv design-pipeline/.venv
design-pipeline/.venv/Scripts/python.exe -m pip install --upgrade pip
design-pipeline/.venv/Scripts/python.exe -m pip install -r design-pipeline/requirements.txt
npm.cmd --prefix design-pipeline run build:g01-background-candidates
design-pipeline/.venv/Scripts/python.exe -m unittest design-pipeline/tests/test_build_g01_backgrounds.py -v
node --test design-pipeline/tests/g01-background-candidates.test.mjs
```
构建输出进入已忽略目录 `design-pipeline/generated/g01-background/`
- `g01-list-background-direction-a.png`
- `g01-list-background-direction-b.png`
- `g01-list-background-direction-c.png`
- `build-report.json`
Node 入口只使用内置模块做路径与清单编排;图片解码、尺寸检查和像素处理由锁定版本的 Pillow 统一负责,因此 G01 候选构建不依赖已删除的 `node_modules`。项目其他使用 Sharp 的旧流水线仍需执行 `npm.cmd ci --prefix design-pipeline`
## 4. 当前质量基线
本机输出:
| 候选 | 模式 | 像素 | 字节 | SHA-256 |
| --- | --- | ---: | ---: | --- |
| A | chroma-key | 1024×1536 | 2,280,297 | `7b8a8abecc651b1bc4c589607780c8ff00ea0f13de018ca4b27a479b2a870fe4` |
| B | chroma-key | 1024×1536 | 1,890,556 | `3a0420eaaac9a2821da0f9a2ddbbbd6fb29b463fa6f41be7fa5c23948f782e2a` |
| C | opaque-paper | 1024×1536 | 2,407,477 | `f14aeed81046d2d5e7ae78b1e62f572689c38261a2cc043142bef9f70200dc5b` |
三张输出均满足:可见绿幕残留 0、透明 RGB 0、尺寸错误 0、体积错误 0、sRGB 警告 0。本机连续构建两次的报告与文件 SHA-256 完全一致。若另一台电脑因 Python、Pillow 或压缩实现差异产生不同文件哈希,应先核对锁定依赖,再比较解码后的 RGBA 像素和质量报告,不能只凭压缩字节差异判定视觉失败。
## 5. 可复现边界
- 精确可复现:从仓库内保存的母版执行清单、Python 和 Node 流程,得到相同 RGBA 像素处理结果。
- 方向可复现但非逐像素:在另一台电脑上使用清单中的提示词重新调用图像模型。
- 不可依赖:Codex 本机生成目录、`tmp/``node_modules/``.venv/``generated/` 或未进入仓库的聊天图片。
## 6. 下一步
1. 用户比较 A/B/C 后选择一个方向,或提出组合要求。
2. 选择后只保留一个正式母版合同,创建版本化 `static/assets/` 运行输出。
3. 再讨论并实现:标题栏、当前家谱大卡片和快捷入口固定;从“我创建的”开始独立滚动;背景固定在列表滚动窗口背后。
4. 完成 320×568、360×640、360×800、412×915 H5 截图和 Android/HBuilderX 复核。
+29 -12
View File
@@ -1,6 +1,6 @@
# P00 页面结构与资产清单
> 状态:54 条最终编号路由已落地;A01/A02/A04/A05/A06/G01 两种状态与 G03 `create|ancestor` 已验收;其余 47 条路由均已有 `[~]` 视觉候选和真实运行截图,等待用户逐页审核
> 状态覆盖:当前实际为 52 条活动路由、52 页全部 `[~]` 待审核、0 页冻结;A06 已封存且保留实现。页面状态以 `docs/交接记录.md` 与 `docs/验收规划.md` 为准
> 目的:让页面、路由和资产都有唯一、可读、可审计的归属;本文件是 P-00 的执行清单。
## 1. 页面命名规则
@@ -20,7 +20,10 @@
| 当前路由 | 当前可确认用途 | 目标文件 | 处理状态 |
| --- | --- | --- | --- |
| `pages/auth/a01-entry` | A01 启动/登录引导 | `pages/auth/a01-entry.vue` | 已迁移;视觉冻结;完整按钮资产待补 |
| `pages/auth/a01-entry` | A01 启动/登录引导 | `pages/auth/a01-entry.vue` | 7 类非滑动验证视觉状态已获用户确认;真实滑动验证用接口组件自带样式,待接入审核;Android 复核前保持 `[~]` |
| `pages/auth/a04-register` | A04 注册账号 | `pages/auth/a04-register.vue` | 当前 8 类 H5 视觉状态已获用户确认;真实滑动验证、接口结果态与 Android/HBuilderX 待审核;保持 `[~]` |
| `pages/auth/a05-reset-password` | A05 重置密码 | `pages/auth/a05-reset-password.vue` | 当前 7 类 H5 视觉状态已获用户确认;接口回流手机号/焦点、真实滑动验证与 Android/HBuilderX 待审核;保持 `[~]` |
| (已从 `pages.json` 移除) | A06 登录阻断状态(封存) | `pages/auth/a06-auth-status.vue` | 用户确认当前样式没有问题,但当前流程暂时不用;源码、测试和证据保留,不计入活动验收统计,恢复时重新接入真实入口并审核 |
| `pages/auth/a02-login` | A02 账号登录 | `pages/auth/a02-login.vue` | 已迁移;已验收,视觉冻结 |
| `pages/genealogy/g01-my-genealogies` | G01 我的家谱 | `pages/genealogy/g01-my-genealogies.vue` | 已迁移;视觉冻结 |
| `pages/genealogy/g03-create-genealogy` | G03 创建家谱/始祖录入 | `pages/genealogy/g03-create-genealogy.vue` | `step=create|ancestor`;已获用户验收,视觉冻结 |
@@ -83,7 +86,7 @@ static/assets/
transparent/
```
当前已迁入的资产以第 4 节台账为准。A01 已使用 `a01-primary-button.png``a01-secondary-button.png` 两张完整按钮皮肤替换旧角标拼接实现;是否维持用户验收效果仍以运行时截图回归为准
当前已迁入的资产以第 4 节台账为准。A01 已引用版本化的卷轴祥云四件套 v3:主/次按钮与弹窗使用 `aspectFit`Toast 使用固定切片的 `border-image`;文字和交互由代码承载。旧资产与 v2 试点资产保留,不覆盖、不删除。v3 已通过本机自动质量门和 H5 四尺寸检查,但尚未获得 Android 与用户验收
## 4. 当前资产台账
@@ -98,11 +101,17 @@ static/assets/
| `foundation/transparent/auth-ink-scenery.png` | transparent / 750×1334 | 认证页淡墨叠层 | A01、A02、A04、A05、A06 | 保留 |
| `foundation/transparent/auth-title-cloud.png` | transparent / 200×120 | 认证标题右侧云纹 | A01、A02、A04、A05、A06 | 保留 |
| `foundation/transparent/auth-divider-knot.png` | transparent / 160×96 | 认证标题与正文结饰 | A01、A04、A05、A06 | 保留 |
| `foundation/opaque/a01-primary-button.png` | opaque / 1877×418 | A01 朱砂主按钮完整皮肤 | A01、A02、A04、A05、A06 | 保留;A01、A02、A04、A05、A06 已验收 |
| `foundation/opaque/a01-secondary-button.png` | opaque / 1881×419 | A01 宣纸次按钮完整皮肤 | A01 | 保留;待运行截图回归 |
| `modules/auth/opaque/a02-login-panel.png` | opaque / 940×1672 | 认证完整卷轴面板,含纸纹、双金线、角纹、云纹与底部山水 | A02、A04、A05、A06 | 保留;A02、A04、A05、A06 已验收 |
| `modules/auth/transparent/a02-agreement-unchecked.png` | transparent / 96×96 | 认证未同意协议图标 | A02、A04 协议确认行,叠在卷轴面板上 | 保留;A02、A04 已验收 |
| `modules/auth/transparent/a02-agreement-checked.png` | transparent / 96×96 | 认证已同意协议图标 | A02、A04 协议确认行,叠在卷轴面板上 | 保留;A02、A04 已验收 |
| `foundation/opaque/a01-primary-button.png` | opaque / 1877×418 | 朱砂主按钮皮肤 | 其他仍实际引用的模块页面 | 暂时保留;A01、A04、A05、A06 已停止引用,其他页面需逐页迁移 |
| `foundation/opaque/a01-secondary-button.png` | opaque / 1881×419 | 宣纸次按钮皮肤 | 其他仍实际引用的页面 | 暂时保留;A01 可见按钮和 Toast 已停止引用,其他页面未逐页迁移 |
| `foundation/transparent/a01-primary-button-v2.png` | transparent / 1866×276 | A01 固定槽位主按钮历史试点 | 当前 A01 已停止引用 | 保留,不覆盖、不删除 |
| `foundation/transparent/a01-secondary-button-v2.png` | transparent / 1866×300 | A01 固定槽位次按钮历史试点 | 当前 A01 已停止引用 | 保留,不覆盖、不删除 |
| `foundation/transparent/a01-scroll-primary-v3.png` | transparent / 1866×276 | 卷轴祥云朱砂主按钮皮肤 | A01 登录、A04 注册、A05 重设、A06 恢复入口及弹层按钮 | A01/A04/A05 当前 H5 状态已获用户确认;A06 待逐态审核;均待 Android 复核 |
| `foundation/transparent/a01-scroll-secondary-v3.png` | transparent / 1866×300 | 卷轴祥云宣纸次按钮皮肤 | A01 微信按钮 | 用户 H5 视觉审核通过;待 Android 复核 |
| `foundation/transparent/a01-scroll-toast-v3.png` | transparent / 1770×246 | 卷轴祥云 Toast 可切片表面 | A01、A04、A05 自定义反馈 Toast | A01/A04 用户 H5 审核通过;A05 待逐态审核;均待 Android 复核 |
| `modules/auth/transparent/a01-scroll-dialog-v3.png` | transparent indexed PNG / 1860×1560 | 卷轴祥云对话容器 | A01 历史验证占位、A05 成功结果、A06 恢复说明 | A01 不得用于最终滑动验证;A05/A06 仅用于本地结果或说明候选 |
| `modules/auth/opaque/a02-login-panel.png` | opaque / 940×1672 | 旧认证完整卷轴面板 | 当前认证页无引用 | 历史保留,不覆盖、不删除 |
| `modules/auth/transparent/a02-agreement-unchecked.png` | transparent / 96×96 | 认证未同意协议图标 | A01、A04 协议确认行 | 保留;A01/A04 用户 H5 审核通过 |
| `modules/auth/transparent/a02-agreement-checked.png` | transparent / 96×96 | 认证已同意协议图标 | A01、A04 协议确认行 | 保留;A01/A04 用户 H5 审核通过 |
| `foundation/transparent/auth-login-outline.png` | transparent / 96×96 | 登录/状态图标 | A01、A06 | 保留 |
| `foundation/transparent/auth-wechat.png` | transparent / 96×96 | 微信图标 | A01 | 保留 |
| `foundation/transparent/brand-seal.png` | transparent / 240×288 | 品牌谱印 | A01、A02、A04、A05、A06、G06、`PageHeader` | 保留 |
@@ -110,6 +119,9 @@ static/assets/
| `foundation/transparent/footer-mountain-bamboo.png` | transparent / 750×360 | 根页底部山水竹影 | G01、G06 | 保留 |
| `foundation/transparent/notice.png` | transparent / 96×96 | 通知图标 | `PageHeader` | 保留 |
| `foundation/transparent/chevron-right.png` | transparent / 96×96 | 返回与列表箭头 | A02、A04、A05、A06、`GenealogyCard` | 保留 |
| `docs/design/assets/g01-background/masters/g01-list-background-direction-a-chroma-master.png` | candidate master / 1024×1536 | G01 列表背景 A,绿幕透明候选 | G01 待选 | 母版保留;未接入页面 |
| `docs/design/assets/g01-background/masters/g01-list-background-direction-b-chroma-master.png` | candidate master / 1024×1536 | G01 列表背景 B,绿幕透明候选 | G01 待选 | 母版保留;未接入页面 |
| `docs/design/assets/g01-background/masters/g01-list-background-direction-c-paper-master.png` | candidate master / 1024×1536 | G01 列表背景 C,完整宣纸候选 | G01 待选 | 母版保留;未接入页面 |
| `modules/genealogy/opaque/g01-empty-panel.png` | opaque / 1122×1402 | 无家谱空态完整宣纸卡,双金线、角饰、云纹与下沿山水 | G01 `state=empty` | 保留;由 ImageGen 生成,已获用户验收 |
| `modules/genealogy/opaque/g03-create-flow-panel.png` | opaque / 1122×1500 | 完整宣纸任务面板,双金线、回纹角饰、云纹与下沿淡墨山水 | G03、G08、G11、G12、T01、T03 至 T06、T08、F/R/N/M 表单/详情/设置/状态 | 保留;由 ImageGen 生成,G03 已获用户验收,复用页待审 |
| `modules/genealogy/opaque/g05-overview-surface.png` | opaque / 1122×1506 | 家谱总览完整视觉表面,含墨蓝谱名区、四宫格宣纸槽、金线近况分隔与底部访问说明面 | G05 `loading|ready|empty|error` | 保留;由 ImageGen 参考 G05 改前截图生成,页面只叠放真实文字和点击层;待集中审美确认 |
@@ -142,14 +154,19 @@ static/assets/
本轮已迁入 38 项在用资产,删除 24 项运行时零引用的旧副本。A01 的两张完整按钮皮肤与 G06 检索资产均由图像生成工具制作,不是设计图裁切;G06 不再使用大型检索面板,当前使用朱砂题签、宽版输入框与搜索按钮三张独立位图,页面只叠放真实品牌印、文字与交互控件。旧版设计记录中的历史路径不再是资产依据;后续只按本台账引用和维护。
## 5. 图片属性规则
## 5. 混合资产属性规则
完整生产、质量门和换机恢复方法以 `docs/design/设计资产生产流水线规范.md` 为准。P00 只登记最终资产归属,不重复维护算法和阈值。
| 类型 | 文件格式与内容 | 典型用途 | 页面使用规则 |
| --- | --- | --- | --- |
| 不透明底图 | JPG 或无透明区域 PNG;包含完整底色、纹理、边框和四角 | 页面纸纹、朱砂页头、完整主/次按钮、卡片、导航 | 直接作为对应区域的完整视觉面,不再靠 CSS 补色或叠加边角 |
| 透明叠加图 | RGBA PNG;四周透明,主体边界干净 | Logo、图标、祥云、谱印、分隔纹、边框 | 明确记录叠在何种不透明底图上;不单独承担底色 |
| 代码原生 | Vue/SCSS,不产生图片 | 布局、文字、间距、普通分隔线、纯色和交互状态 | 不伪造复杂品牌装饰;普通内容保持正常布局流 |
| 固定比例位图 | JPG、PNG 或 WebP;比例与槽位一致 | 宗祠、水墨背景、固定比例复杂视觉面 | manifest 声明槽位和 `uniform-only`;禁止变形拉伸 |
| 透明叠加图 | RGBA PNG/WebP;外缘透明、主体边界干净 | Logo、图标、祥云、谱印、分隔纹 | manifest 声明 Alpha 与边缘策略;禁止白边、绿边 |
| 可伸缩装饰框 | 九宫格或经过验证的等价组件资产 | 多宽度按钮框、卡片框、弹窗框 | 必须先验证 H5 与 Android;四角和边线不得被整体拉伸 |
| 平铺纹理 | 可无缝重复的小图 | 宣纸、无方向纹理 | manifest 明确平铺/覆盖策略,不承担文字或边框 |
禁止项:整页截图、设计图局部裁切、半边边框、按钮角标拼接、CSS/SVG 绘制可见装饰、同一语义的多版本长期共存。
禁止项:整页截图、设计图局部页面裁切、半边边框、按钮角标拼接、临时 CSSSVG 伪造复杂品牌装饰、未声明的 `scaleToFill``fit: 'fill'`、同一语义的多版本长期共存。
## 6. 资产清理登记规则
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

@@ -0,0 +1,28 @@
# 上传前文件清理审计(2026-07-16)
## 审计边界
- 只检查仓库长期交接截图、未跟踪二进制资产和已忽略的本地生成目录。
- 不清理 `tmp/``runtime/`、测试、脚本、候选母版或用户已有修改。
- 不修改仓库截图体积阈值,不执行任何 Git 暂存、提交、推送、重置或检出操作。
## 本轮已清理
以下两张旧 A01 全页截图没有被任何文档、索引、清单、代码或测试引用;逐张查看后确认其视觉方案已被 2026-07-16 的 A01 当前代表图和状态联系表替代,因此删除:
- `docs/design/screens/handoff/2026-07-15/A01-selected-password-brand-seal-841x1870.png`1,805,168 字节)
- `docs/design/screens/handoff/2026-07-15/A01-selected-sms-brand-seal-841x1870.png`1,785,574 字节)
合计减少 3,590,742 字节。没有删除其他历史 handoff 证据。
## 保留结论
- `docs/design/assets/g01-background/masters/` 下三张 G01 背景母版均被候选清单和换机重建文档引用,必须保留。
- 当前其他未跟踪二进制资产均有文档、清单、代码或测试引用,不按“未跟踪”直接删除。
- `design-pipeline/generated/``design-pipeline/.venv/``docs/design/screens/runtime/``tmp/` 等目录处于 Git 忽略状态,不会进入正常提交;本轮不做破坏性本地清理。
## 仍未解决的仓库合同
清理后,`docs/design/screens/handoff/` 仍有 50 个文件、29,887,097 字节,继续超过 35 个文件/20MB 的交接截图合同。剩余文件存在有效引用或属于长期证据,不能为了让测试变绿而擅自删除;后续仍需用户决定归档策略。
`tests/repository-handoff-size-contract.ps1` 还会把已忽略的 runtime 截图统计在内,因此必须同时如实报告脚本结果与 handoff 专项目录结果,不能放宽阈值掩盖问题。
+19 -13
View File
@@ -1,30 +1,31 @@
# 家谱 APP 视觉设计交接手册
> **2026-07-15 状态覆盖说明:** 本手册中的截图方法、资产规则和验证流程继续有效;“54 条路由、7 页已验收/冻结”的旧状态不再有效。当前为 53最终路由、53 页全部重新待审核、0 页冻结,正在返工 A01。接管时先读 `docs/交接记录.md` 与 `docs/验收规划.md`。
> **2026-07-16 状态覆盖说明:** 本手册中的截图方法、资产规则和验证流程继续有效;旧路由与验收数字不再有效。当前为 52活动路由、52 页全部 `[~]` 待审核、0 页冻结;A06 已从路由与活动验收队列封存但保留实现。接管时先读 `docs/交接记录.md` 与 `docs/验收规划.md`。
> 更新日期:2026-07-14
> 用途:在新电脑或交给新的 GPT 后,按本手册恢复当前工作;它只记录已发生的事实,不把待验收写成已完成。
## 0. 给新 GPT 的可直接执行指令
你接手的是一个 **uni-app Android 家谱项目的高质量视觉设计任务**,全程中文工作;它不是只改 CSS 的任务。先不修改代码,完整阅读:`AGENTS.md``docs/交接记录.md`、本手册、`docs/规划.md``docs/design/D1_安卓视觉规范与页面壳.md``docs/design/P00_页面结构与资产清单.md``docs/superpowers/plans/2026-07-13-product-design-visual-refinement.md``design-qa.md`。随后只向用户简洁复述当前停点和准备如何处理反馈。
你接手的是一个 **uni-app Android 家谱项目的高质量视觉设计任务**,全程中文工作;它不是只改 CSS 的任务。先不修改代码,完整阅读:`AGENTS.md``docs/交接记录.md`、本手册、`docs/规划.md``docs/design/D1_安卓视觉规范与页面壳.md``docs/design/P00_页面结构与资产清单.md``docs/design/设计资产生产流水线规范.md``docs/superpowers/plans/2026-07-13-product-design-visual-refinement.md``design-qa.md`。随后只向用户简洁复述当前停点和准备如何处理反馈。
必须保留已有未提交改动;不使用 worktree 或多代理;不执行 `git add``git commit``git push`、上传、`git reset``git checkout`。A01、A02、A04、A05、A06、G01、G03 的可见效果已获用户验收冻结,不能以重构或顺手优化的名义改变。G07 是 G06 的页面内状态,不得重新建为独立页。
必须保留已有未提交改动;不使用 worktree;不执行 `git add``git commit``git push`、上传、`git reset``git checkout`旧阶段的 A01、A02、A04、A05、A06、G01、G03 验收冻结记录只作历史证据;本轮 52 个活动页面全部重新待审核。A06 已封存,G07 是 G06 的页面内状态,不得重新建为独立页。
用户已授权先完成全部页面候选、后集中审核。54 条最终路由均已覆盖:7 个已验收冻结,其余 47 个统一标 `[~]`;不得把候选写成用户已验收。下一阶段由用户按规划逐页勾选,接口只作功能布局参考,本轮没有对接。
当前实际为 52 条活动路由,52 页全部标 `[~]`,0 页冻结;已有 14 个活动页面内部 H5 候选,A06 候选另行封存,均不得写成用户已正式验收。用户下一活动审核页是 G01;无人审核时下一施工页是 T07。接口只作功能布局参考,本轮没有对接。
新环境必须可用并实际调用:`product-design:index``product-design:audit``imagegen``superpowers:test-driven-development``superpowers:systematic-debugging``superpowers:verification-before-completion`。没有这些能力时,先安装/启用同等能力,不得假装已经完成设计审视或位图生成。
## 1. 当前停点:先看这一节
项目有 54最终编号路由;T02 已收敛到 T01 同页状态。这不等于 54页面已完成视觉验收
项目实际有 52活动路由;A02 已合并进 A01,A06 已封存,T02 已合并进 T01且遗留占位文件已删除。`docs/验收规划.md` 52活动页面全部为 `[~]`0 页已验收,0 页冻结
- 用户已经验收的视觉基准有七个:**A01 启动/登录引导**、**A02 账号登录**、**A04 注册账号**、**A05 重置密码**、**A06 登录状态**、**G01 我的家谱**、**G03 创建家谱**。它们的可见效果已冻结
- **A02 账号登录**已经完成资产化重做、功能回归和多尺寸截图,并于 2026-07-14 获用户审美验收。A03 已删除;A02 的短信 Tab 是唯一手机号验证码登录入口。A04、A05、A06 均于同日完成独立重做并获用户审美验收;G02 已按用户确认合并为 G01 的 `?state=empty`G04 已合并为 G03 的 `?step=ancestor`;G07 已合并为 G06 的初始/结果/无结果状态。G06 三态候选截图和内部审视已经完成,但尚未获得用户视觉验收。
- G05、G06、G08G12、T01、T03T08、F01F10、R01R11、N01N02、M01–M10 已完成候选截图和审视;共享 `ModulePage.vue` 已升级为全位图高规格母版,不再是旧 CSS 过渡壳
- 已有 14 个活动页面内部 H5 候选:A01、A04、A05、G01、G03、G05、G06、G08、G09、G10、G11、G12、T01、T04。A06 的 H5 候选、源码、测试和证据已封存保留
- 这些候选、内部 smoke、GPT 审视和旧 QA 都不等于用户正式验收。
- 用户下一活动审核页是 G01;无人审核时下一施工页是 T07 成员目录
- 基础版本只做浅色国风主题;深色和跟随系统延期。
- 不提交、不推送、不上传 Git;由用户自行处理版本库操作。
下一位执行者先读取当前停点与已验收页面,然后按 `docs/规划.md` `[~]` 清单协助用户逐页审核。默认只`docs/design/视觉证据索引.md` 打开一张代表图或一张状态联系表;多尺寸、before/after 与历史废稿仅在发现问题时展开。`G06-no-panel-*` 图只能帮助理解迭代原因,不能用于当前验收。全部 47 个候选都等待用户逐页确认,不再按模块批量推进新页面。
下一位执行者先读 `docs/交接记录.md` `docs/验收规划.md`,再按 `[~]` 清单协助用户逐页审核。默认只打开一张代表图或一张复杂状态联系表;多尺寸、before/after 与历史废稿仅在发现问题时展开。
## 1.1 迁移前必须保留的文件
@@ -40,6 +41,7 @@
| `docs/规划.md` | 54 条独立路由的范围、模块顺序和“完成”的定义。 |
| `docs/design/D1_安卓视觉规范与页面壳.md` | Android 页面、资产、布局与中文注释的硬约束。 |
| `docs/design/P00_页面结构与资产清单.md` | 路由、页面文件和资产台账的唯一清单。 |
| `docs/design/设计资产生产流水线规范.md` | 混合代码/位图落地、manifest、Node/Sharp/Python 分工、质量门和跨电脑恢复合同。 |
| `docs/superpowers/plans/2026-07-13-product-design-visual-refinement.md` | 已批准的分模块视觉精修计划和验收顺序。 |
| `docs/design/2026-07-13-A01-G01-A02-视觉审视.md` | A01/G01 视觉证据与旧 A02 的问题定位。 |
| `docs/design/A02_账号登录_设计记录.md` | A02 当前实现、资产、截图和测试记录。 |
@@ -59,14 +61,16 @@
必须遵守:
1. 完整按钮、卡片、标题签、页面头图和有边框的装饰面,使用完整的**不透明位图资产**承载底色、纹理和边框
2. 透明 PNG 仅承担 Logo、图标、祥云、纹样等叠加元素;CSS 只承担布局、文字、状态与交互,不能伪造完整框、祥云、按钮或卡片
1. 最新生产方法以 `docs/design/设计资产生产流水线规范.md` 为准:代码承担布局、文字、间距、状态和普通结构;复杂水墨、品牌装饰和代码无法忠实表达的视觉使用真实位图
2. 固定比例完整位图、透明叠加资产和经 H5/Android 验证的九宫格必须在 manifest 中声明真实槽位、缩放和 Alpha 策略;不得默认使用 `scaleToFill` 或 Sharp `fit: 'fill'` 掩盖比例错误
3. 不能把整张设计截图或局部截图裁进运行页面;运行资产必须是可复用的独立元素。
4. 不要修改 A01/G01 已验收的可见布局、比例、配色或装饰。可读其实现和资产,但不能借“整理”改变效果
4. A01/G01 历史证据仍是品牌观察真源,但不等于本轮已验收或冻结;当前状态以 `docs/交接记录.md``docs/验收规划.md` 为准
5. 不要用一个通用卡片母版覆盖所有模块。家谱、世系树、内容档案、人物记录、通知和设置必须在同一视觉语言下有各自的信息密度与层级。
## 4. A02 的精确状态
> 本节是 2026-07-14 的历史实现记录,不是当前路由或验收状态。当前 A02 已删除并合入 A01,本轮不得恢复 A02 路由;最新状态以 `docs/交接记录.md` 为准。
### 实现内容
- 路由与页面:`pages/auth/a02-login.vue`
@@ -124,7 +128,9 @@ node scripts/capture-chrome-page.js `
## 6. 后续执行顺序与每轮产物
### 门禁 0A02 与 A04 已通过
### 历史门禁记录(本轮已失效)
以下内容只解释旧阶段为何保留某些资产,不得据此把任何页面写成当前已验收或冻结。
1. A02 与 A04 均已通过;不因后续页面改动而触碰其可见效果。
2. 若用户提出明确调整:只在对应页面修正,先调整对应契约测试,再改页面/资产,重新截图复核。
+2 -2
View File
@@ -5,7 +5,7 @@
- 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)
- 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)
@@ -17,7 +17,7 @@
- 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 页全部重新待审核;下方旧“已验收”图片只作为历史视觉基础,不计入本轮完成状态。
- 当前实际为 52 个活动页面全部 `[~]` 待审核;A06 证据封存保留,不计入活动验收统计。下方旧“已验收”图片只作为历史视觉基础,不计入本轮完成状态。
> 本仓库只保留低负担交接证据:每页一张代表图,复杂模块使用联系表。完整 before/after、多尺寸、历史废稿和全部运行截图保存在项目外本地归档,不进入 Git。
@@ -0,0 +1,374 @@
# 家谱 APP 设计资产生产流水线规范
> 状态:已批准并完成 A01 卷轴祥云四件套 v3 的本机 H5 试点;仍待 Android/HBuilderX、跨电脑复现和用户正式验收。
> 更新日期:2026-07-16。
> 适用范围:本项目后续页面重设计、独立视觉资产生产、跨电脑恢复与质量审计。
> 当前边界:manifest v2、Python/Pillow 质量门和 A01 主按钮、次按钮、Toast、验证弹窗四张 v3 资产已经落地并通过本机 H5 四尺寸检查;这不代表 Android 已复核、用户已验收或流水线可以推广到其他页面。
## 1. 目标
本项目采用“混合落地”方式,不把所有视觉都做成整张位图,也不把复杂国风装饰退化为临时 CSS 或手绘 SVG。
目标是:
1. 设计稿中的布局、文字、间距、状态和普通结构由 Vue/SCSS 负责。
2. 复杂水墨、宗祠、品牌印、专用图标和难以代码复现的装饰由真实位图资产负责。
3. 可复用且需要伸缩的装饰框优先进入经过 Android/H5 验证的九宫格或组件化资产方案。
4. Node.js、Sharp 与 Python 读取同一份 JSON manifest,禁止在脚本、页面和文档中重复维护尺寸规则。
5. 自动化负责消除机械错误;真实运行截图和用户逐页审核仍是最终视觉判断来源。
流水线不能保证任意扁平设计图自动拆成一比一资产。要获得稳定质量,页面设计阶段必须同时明确组件边界、目标槽位、伸缩策略和独立资产任务。
## 2. 已有雏形与升级原则
仓库现有可复现雏形:
- `design-pipeline/package.json`
- `design-pipeline/package-lock.json`
- `design-pipeline/manifests/a01.json`
- `design-pipeline/scripts/build-a01.mjs`
- `tests/a01-code-pipeline-contract.ps1`
原 schema v1 雏形已经具备 Node.js、Sharp、JSON 清单、路径约束、尺寸/Alpha/体积检查和 A01 预览组合能力,但单独使用时仍不是合格的资产生产线,原因包括:
- 对所有资产统一使用 `fit: 'fill'`,不能阻止比例拉伸。
- manifest 只记录文件像素,不记录页面真实槽位和允许的缩放策略。
- 没有检查白边、绿边、半透明脏边、预乘 Alpha、色彩空间和边缘污染。
- 代码预览不是 uni-app 真实运行截图。
- `source === output` 主要保证换机可验证,不能替代新资产的生产与质量处理。
升级时保留已有目录、Node/Sharp 依赖和清单入口,不另建一条互相冲突的流水线。新合同升级为 manifest v2;A01 试点通过前不得批量迁移其他页面。
2026-07-16 已实现的 v2 试点包括:
- `design-pipeline/manifests/a01-buttons-v2.json`:尺寸、槽位、缩放、透明边、质量阈值和消费者的单一机器合同。
- `design-pipeline/scripts/rebuild-a01-buttons.mjs``rebuild_a01_buttons.py`Node 编排、Pillow 横向三段式离线重建。
- `design-pipeline/scripts/verify-assets.mjs``verify-assets.py``asset_quality.py`:尺寸、Alpha 外圈、绿边、浅色半透明边、透明 RGB、体积和色彩声明检查。
- `design-pipeline/requirements.txt`:锁定 `Pillow==12.3.0`
- Node/Python 单元测试、A01 静态契约、资产审计和四档 H5 运行时检查。
在双按钮试点基础上,用户选择“方案 2:卷轴祥云”并批准扩展为四件套 v3:
- `design-pipeline/manifests/a01-scroll-skins-v3.json`:四件套源母图、输出槽位、缩放、透明边、体积和消费者的唯一机器合同。
- `build-a01-scroll-skins.mjs``build_scroll_skins.py`Node 编排与 Pillow 确定性构建;构建会清除外缘绿边、浅色半透明脏边和透明 RGB 残留。
- 主/次按钮和弹窗按固定比例等比输出;Toast 输出可切片透明 PNG,由页面 `border-image` 承担自适应宽度。
- 弹窗采用带透明度表的索引 PNG 控制体积,质量门读取真实透明通道,不通过放宽阈值换取通过。
旧 schema v1 仍保留给现有 A01 预览流程;不得把它的 `fit: 'fill'` 规则复制到 v2 固定比例资产。
## 3. 资产分类与唯一落地方式
每个设计元素在 manifest 中只能属于以下一种主要类型:
| 类型 | `assetClass` | 正确落地 | 典型内容 |
| --- | --- | --- | --- |
| 代码原生 | `code-native` | Vue/SCSS | 布局、文字、间距、纯色、普通分隔线、状态与交互 |
| 固定比例位图 | `fixed-bitmap` | 精确槽位比例的 PNG/WebP,不允许变形 | 宗祠头图、复杂水墨、固定比例品牌视觉面 |
| 透明叠加资产 | `transparent-overlay` | 干净 RGBA PNG/WebP | Logo、印章、云纹、专用图标、结饰 |
| 可伸缩装饰框 | `nine-slice` | 经 H5/Android 验证的九宫格或等价组件 | 多宽度按钮框、卡片框、弹窗框 |
| 平铺纹理 | `tile-texture` | 明确的重复或覆盖策略 | 小尺寸宣纸纹、无方向噪声纹理 |
禁止:
- 把整页设计截图或局部页面截图直接裁入运行页面。
- 用 emoji、文本符号、临时第三方 SVG、CSS 拼画代替缺失的品牌装饰或图标。
- 同一资产既声明固定比例又在页面中随意拉伸。
- 未经 manifest 授权使用 `scaleToFill` 或 Sharp `fit: 'fill'`
- 把候选图、审阅图、runtime 截图或 `generated/` 中间文件当成唯一正式源。
## 4. 单一 manifest v2 合同
manifest 是资产生产、页面接入和质量验证的唯一机器可读所有者。建议结构:
```json
{
"schemaVersion": 2,
"page": "A01",
"assets": [
{
"id": "a01-primary-button-v2",
"assetClass": "fixed-bitmap",
"source": "static/assets/foundation/transparent/a01-primary-button-v2.png",
"output": "static/assets/foundation/transparent/a01-primary-button-v2.png",
"logicalSlot": {
"widthRpx": 622,
"heightRpx": 92
},
"outputPixels": {
"width": 1866,
"height": 276
},
"render": {
"scalePolicy": "uniform-only",
"uniMode": "aspectFit",
"allowDistortion": false
},
"alpha": {
"required": true,
"transparentOutsideFrame": true,
"cornerMaxAlpha": 0
},
"edge": {
"forbidChromaResidue": true,
"forbidLightFringe": true,
"premultipliedAlphaCheck": true
},
"quality": {
"maxBytes": 500000,
"symmetry": "horizontal",
"colorSpace": "sRGB"
},
"consumers": [
"pages/auth/a01-entry.vue"
]
}
]
}
```
正式实现时应为 schema v2 编写 JSON Schema 或等价的严格校验器。未知字段、缺失字段、越出工作区的路径、重复 ID 和未登记消费者都应尽早失败。
## 5. Node.js 与 Sharp 的职责
Node.js 是流水线编排层,继续复用当前 `design-pipeline/`
1. 解析并校验 manifest。
2. 检查路径只能位于工作区内。
3. 调用 Sharp 读取元数据、裁切、等比缩放、格式转换、压缩和预览组合。
4. 根据 `scalePolicy` 选择算法;`uniform-only` 禁止 `fit: 'fill'`
5. 扫描 Vue/SCSS 中的资产引用、`mode` 和显示槽位。
6. 检查资源比例与页面逻辑槽位比例,默认容差不超过 0.5%。
7. 禁止页面引用未进入 manifest 的新视觉资产。
8. 生成包含 SHA-256、尺寸、字节数、Alpha、消费者和检查结果的构建报告。
9. 使用现有 Chrome CDP 9222 对真实 H5 页面做尺寸、资源加载和局部截图验证。
Node/Sharp 不负责艺术判断,也不把代码组合预览当作用户验收证据。
## 6. Python 的职责
Python 是像素质量层,建议使用 Pillow;只有 Pillow 无法稳定完成的边缘分析再引入 OpenCV,避免不必要的重依赖。
职责:
1. 验证 PNG/WebP 解码、实际尺寸、通道数和 sRGB 色彩空间。
2. 检查透明角、外框外 Alpha、可见内容边界和空白边距。
3. 检测绿幕残留、异常高亮边缘和半透明脏边。
4. 对透明边缘执行去污染与正确的 Alpha 处理,避免 Android 缩放后出现白边或绿边。
5. 检查预乘 Alpha 风险;完全透明像素不得保留会渗出的异常 RGB。
6. 对要求对称的按钮和装饰框检查左右差异。
7. 生成局部边缘放大图、资产联系表和机器可读报告。
8. 质量不达标时返回非零退出码,禁止继续接入页面。
白边检测不能简单地“禁止白色”,因为次按钮内部可能本来就是暖白宣纸。检查必须只针对外框外区域、半透明边界和 manifest 声明的允许色板。
## 7. 资产生产流程
### 7.1 设计阶段
设计或 ImageGen 输出页面方向时,同时登记:
- 页面逻辑尺寸和目标视口。
- 组件边界与逻辑槽位。
- 哪些内容必须由代码渲染。
- 哪些内容需要独立透明资产。
- 哪些装饰允许伸缩以及不可伸缩安全区。
- 字体、字号、颜色和状态。
不接受只有一张扁平页面图而没有资产任务单的“最终设计”。
### 7.2 候选生产
1. 只有缺少完整位图资产时使用 ImageGen。
2. 输入图必须明确角色:编辑目标、风格参考或组合参考。
3. 生成内容不包含应由代码渲染的按钮文字、表单文字和状态文案。
4. 中间候选进入忽略目录,不覆盖正式资产。
5. 选择一个母图后再进入确定性处理;候选图不长期堆入仓库。
ImageGen 的艺术输出不能保证跨电脑逐像素复现。跨电脑可复现的是“已选择母图之后的处理、验证和页面接入”。需要长期重建的唯一母图必须进入受控正式路径或由最终资产本身承担稳定输入。
### 7.3 确定性处理
1. 按 manifest 裁切或补透明边距。
2. 以目标槽位比例导出 2×/3×文件。
3. 清理 Alpha、白边、绿边和异常 RGB。
4. 转为 sRGB,使用高质量等比缩放。
5. 输出新的版本化文件,不直接覆盖当前正式资产。
6. Python 质量门通过后,才允许更新页面引用。
### 7.4 页面接入
1. 先写或调整资产合同并看到预期失败。
2. 页面使用 manifest 指定的渲染模式。
3. 文字、图标和交互状态保持独立可访问节点。
4. 只在真正需要叠层的地方使用定位;普通内容保持正常布局流。
5. 禁止为了匹配截图而恢复错误比例或无依据的定位。
### 7.5 运行验证
最低验证层级:
1. manifest/schema 合同。
2. Python 像素质量检查。
3. Node/Sharp 构建和引用检查。
4. H5 Chrome 真实运行:320×568、360×640、360×800、412×915。
5. 一张代表图或一张复杂状态联系表。
6. Android/HBuilderX 真机或模拟器复核。
7. 用户明确逐页验收。
H5、自动截图差异、内部 smoke 和 GPT 审视都不能替代第 6、7 层。
## 8. 质量门槛
### 8.1 通用门槛
- 输出像素必须与 manifest 完全一致。
- 固定比例资产与逻辑槽位比例误差默认不超过 0.5%。
- `uniform-only` 资产不得出现非等比缩放。
- PNG 声明透明时必须真正包含 Alpha 通道。
- 声明透明角时,指定角点 Alpha 必须为 0。
- 禁止绿幕残留和未授权的浅色外缘。
- 文件必须为 sRGB;不得依赖另一台电脑上的专有字体、PSD 或本机临时目录。
- 体积超过 `maxBytes` 时失败,不通过降低清晰度或删除证据掩盖。
### 8.2 固定按钮门槛
- 图片中不包含按钮文字和微信图标。
- 外框之外透明,内部视觉面完整。
- 四角、双边和装饰连续,不得出现切口或拼接缝。
- 3×输出缩小到真实槽位后,金线仍连续且不过度发虚。
- 按钮点击区域由代码控制,不由图片透明区决定。
### 8.3 自动差异的边界
截图差异只比较同视口、同状态和同字体环境。字体抗锯齿、WebView 与 Chrome 的细小差异不能用单一全屏像素阈值判死刑;应优先比较资产局部裁图、尺寸、边缘和结构,再由人工判断整体视觉。
## 9. A01 卷轴祥云资产试点
流水线首先用两张 v2 按钮验证固定槽位,随后按用户选定方向升级为四件套 v3。当前 A01 实际引用:
- `a01-scroll-primary-v3.png``1866×276px`,登录主按钮。
- `a01-scroll-secondary-v3.png``1866×300px`,微信次按钮。
- `a01-scroll-toast-v3.png``1770×246px`,反馈 Toast 九宫格/切片表面。
- `a01-scroll-dialog-v3.png``1860×1560px`,安全验证弹窗表面。
当前 `a01-primary-button.png``a01-secondary-button.png` 的原始比例与页面槽位不一致,且次按钮外缘存在高亮白/浅绿色像素,不能作为新流水线质量样板。
本机 H5 试点结果:
- 四张 v3 输出通过尺寸、Alpha 外圈、透明 RGB、绿边、浅色半透明边、体积和色彩声明检查;旧资产和 v2 试点资产原样保留。
- A01 主/次按钮与弹窗使用 v3 路径和 `aspectFit`;Toast 使用 v3 切片资产;所有文字、微信图标和交互仍由页面代码独立承载。
- 自动质量门与 `320×568``360×640``360×800``412×915` H5 运行检查通过。
- 412×915 默认态、Toast 和验证弹窗真实运行截图未见原来的外缘白/浅绿边、角饰断裂或整体比例拉伸;仍需 Android/HBuilderX 与用户确认。
试点规则:
1. 保留当前资产,不覆盖、不删除。
2. 新资产使用版本化路径。
3. 先通过 Python/Node 自动质量门,再接入 A01。
4. 接入后只保存一张 412×915 代表图;四尺寸可以自动检查但不连续保存大量相似截图。
5. H5 通过只能写“内部候选”;Android 和用户确认之前,A01 仍为 `[~]`
6. 只有用户确认试点资产质量达到标准,才把 manifest v2 和混合流水线推广到其他页面。
## 10. 跨电脑环境合同
### G01 列表背景候选扩展
2026-07-16 已把混合流水线扩展到 G01 背景候选,但尚未推广为正式运行资产。三个 ImageGen 母版、准确提示和处理模式保存在 `docs/design/assets/g01-background/masters/``design-pipeline/manifests/g01-background-candidates.json``build-g01-backgrounds.mjs` 使用 Node 内置模块编排,`build_g01_backgrounds.py` 使用锁定的 Pillow 完成绿幕转 Alpha、去绿边、透明 RGB 清理、sRGB 写入和确定性 PNG 输出。
执行 `npm.cmd --prefix design-pipeline run build:g01-background-candidates` 可在另一台电脑从仓库母版重建三个候选。模型重新生成不能保证逐像素一致;精确重建必须使用仓库内母版。完整步骤、当前哈希和质量基线见 `docs/design/G01_列表背景候选与换机重建.md`
### 10.1 当前已可运行的 Node 雏形
新电脑需要 Windows、Node.js 22 或更高版本、npm、HBuilderX、Chrome 和项目完整工作区。当前 Node 24 已验证可用。
```powershell
node --version
npm --version
npm.cmd ci --prefix design-pipeline
npm.cmd --prefix design-pipeline run build:a01
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-code-pipeline-contract.ps1
```
这些命令只证明现有 schema v1 雏形可运行,不证明新质量门已经实现。
### 10.2 manifest v2 的 Python 环境
`design-pipeline/` 已提供锁定依赖、统一 Node 入口和 Python 质量脚本;不得要求接管者凭经验安装不确定版本。当前结构:
```text
design-pipeline/
package.json
package-lock.json
requirements.txt
manifests/
scripts/
rebuild-a01-buttons.mjs
rebuild_a01_buttons.py
verify-assets.mjs
verify-assets.py
generated/ # 忽略,可再生成
```
恢复和验证命令:
```powershell
python --version
python -m venv design-pipeline/.venv
design-pipeline/.venv/Scripts/python.exe -m pip install --upgrade pip
design-pipeline/.venv/Scripts/python.exe -m pip install -r design-pipeline/requirements.txt
npm.cmd ci --prefix design-pipeline
npm.cmd --prefix design-pipeline run test:v2
npm.cmd --prefix design-pipeline run validate:a01-buttons
npm.cmd --prefix design-pipeline run rebuild:a01-buttons
npm.cmd --prefix design-pipeline run verify:assets
npm.cmd --prefix design-pipeline run validate:a01-scroll-skins
npm.cmd --prefix design-pipeline run build:a01-scroll-skins
npm.cmd --prefix design-pipeline run verify:a01-scroll-skins
design-pipeline/.venv/Scripts/python.exe -m unittest discover -s design-pipeline/tests -p "test_*.py" -v
```
Node 入口优先使用 `PYTHON` 环境变量,其次自动使用 `design-pipeline/.venv/Scripts/python.exe`,最后才回退到系统 `python`。本机已按以上依赖完成验证;跨电脑仍必须真实执行并保存输出,不能只因文件齐全就写成换机复现通过。
### 10.3 可迁移文件
至少保留:
- `design-pipeline/package.json`
- `design-pipeline/package-lock.json`
- 实施后新增的 Python 锁定依赖文件
- manifest、JSON Schema、构建/质量脚本和对应测试
- manifest 引用的正式资产
- 页面源码和真实截图工具
- 本规范、`docs/交接记录.md` 与 P00 资产台账
不得依赖:
- `node_modules/`
- Python 虚拟环境
- `design-pipeline/generated/`
- `tmp/`
- 本机 Chrome 用户目录
- Photoshop/PSD
- 未进入仓库的候选图或 runtime 截图
## 11. 失败处理
- 依赖安装失败:停止并报告缺失环境,不绕过锁文件。
- manifest 不合法:修正单一数据源,不在页面里增加例外。
- 比例不匹配:回到目标槽位或重新导出,禁止 `fill` 掩盖。
- 白边/绿边:回到 Alpha 去污染步骤,禁止用页面背景色遮盖。
- H5 与组合预览不同:以真实 H5 为准并查明差异。
- H5 与 Android 不同:不得冻结资产,记录 WebView/字体/缩放差异后修正。
- 自动检查通过但用户认为不好看:状态仍为待审核,视觉判断以用户最新决定为准。
## 12. 推广条件
以下条件全部满足后,才能从 A01 推广:
1. A01 四张 v3 卷轴祥云资产通过 manifest、Python、Node 和四尺寸检查。
2. 412×915 真实 H5 代表图没有白边、绿边、拉伸或模糊。
3. Android/HBuilderX 已复核。
4. 用户明确确认四件套资产质量和 A01 整页达到标准。
5. 新电脑按文档能够从锁文件恢复环境并得到一致的质量报告。
当前仅满足第 1、2 项;第 3、4、5 项尚未满足。因此本规范和 A01 试点仍不是整项目推广或用户验收完成证明。
@@ -0,0 +1,52 @@
# A01 品牌 Logo 尺寸微调实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 A01 顶部品牌 Logo 精确缩小到 `184×221rpx`,其他布局保持不变。
**Architecture:** 继续使用现有 `brand-seal.png` 和绝对定位叠层,只调整页面内唯一的尺寸所有者 `.brand-seal`。静态契约负责防止尺寸回退,真实 H5 截图负责视觉判断。
**Tech Stack:** uni-app Vue、SCSS、PowerShell 契约测试、Chrome CDP 截图与运行 smoke。
## Global Constraints
- 只修改 A01 Logo 尺寸和对应契约。
- 不使用 worktree,不执行 Git 暂存、提交、推送、重置或清理。
- 不把 H5 截图写成 Android 或用户整页验收。
---
### Task 1: 锁定并实现 Logo 尺寸
**Files:**
- Modify: `tests/a01-a02-ui-contract.ps1`
- Modify: `pages/auth/a01-entry.vue`
**Interfaces:**
- Consumes: `.brand-seal` 现有样式规则和 `brand-seal.png`
- Produces: 精确的 `184×221rpx` H5 显示槽位。
- [ ] **Step 1: 写入失败契约**
在测试中解析 `.brand-seal`,要求 `width: 184rpx;``height: 221rpx;``top: 64rpx;``left: 50%;`
- [ ] **Step 2: 确认 RED**
运行:`powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1`
预期:因页面仍为 `220×264rpx` 而失败。
- [ ] **Step 3: 最小实现**
`.brand-seal` 的宽高改为:
```scss
width: 184rpx;
height: 221rpx;
```
- [ ] **Step 4: 验证 GREEN 与真实运行**
运行 A01 静态合同、四档运行 smoke、412×915 截图和 `git diff --check`;人工确认 Logo 无拉伸、裁切或新的层级问题。
项目禁止 Git 操作,因此本计划不包含提交步骤。
@@ -0,0 +1,197 @@
# A01 Scroll Skins V3 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. The user forbids subagents, worktrees, commits, and all Git mutation commands, so execution stays inline in the current worktree and every commit step is intentionally omitted.
**Goal:** Replace A01's current button, Toast, and verification-panel visuals with the user-approved option-2 scroll-and-cloud family while preserving live text, exact slot ratios, interaction behavior, and old assets.
**Architecture:** Keep the approved board as the immutable visual target. Generate one clean master per surface, then use the existing Node/Pillow pipeline to remove presentation backgrounds, preserve fixed decorative caps/corners, stretch only safe centers, and emit exact versioned outputs. A01 consumes the new outputs only after manifest and pixel tests pass; H5 comparison uses the approved board plus real runtime screenshots.
**Tech Stack:** uni-app/Vue, SCSS, Node.js 24, native `node:test`, Python 3.14, Pillow 12.3.0, PowerShell contracts, Chrome CDP 9222, built-in ImageGen.
## Global Constraints
- Scope is A01 only; do not edit other page implementations.
- Preserve `STKaiti`, `KaiTi`, live button copy, WeChat icon, verification copy, interactions, and current logical slots.
- Keep all old assets; write only versioned v3 outputs.
- Do not use `scaleToFill` to hide a ratio mismatch.
- Master and output assets contain no text, icons, logo, white halo, green fringe, or presentation shadow.
- A01 remains `[~]`; H5 output is internal evidence only until Android/HBuilderX and user review.
- Do not run worktree, subagent, `git add`, `commit`, `push`, `reset`, `checkout`, upload, delete, or cleanup commands.
---
### Task 1: Preserve Exact Individual Masters
**Files:**
- Reference: `docs/design/assets/a01-vnext/A01-shared-scroll-skins-with-dialog-approved.png`
- Create: `docs/design/assets/a01-vnext/masters/a01-scroll-primary-master-v3.png`
- Create: `docs/design/assets/a01-vnext/masters/a01-scroll-secondary-master-v3.png`
- Create: `docs/design/assets/a01-vnext/masters/a01-scroll-toast-master-v3.png`
- Create: `docs/design/assets/a01-vnext/masters/a01-scroll-dialog-master-v3.png`
**Interfaces:**
- Consumes: the approved four-component board and the measured slots from the design spec.
- Produces: four committed-quality visual masters with flat chroma presentation backgrounds, no text, and quiet safe centers.
- [ ] **Step 1: Generate the four masters independently**
Use ImageGen once per master. Attach the approved board every time. Require the exact family, one isolated component, a flat `#00ff00` background for deterministic extraction, no external shadow, and these target ratios: `622:92`, `622:100`, `590:82`, `620:520`.
- [ ] **Step 2: Inspect every master**
Open all four with `view_image`. Reject any master with text, asymmetric clouds, cropped scroll rods, missing gold lines, landscape above the dialog's lower 22%, or decoration crossing the live-content safe center.
- [ ] **Step 3: Save only the approved individual masters**
Copy each generated result to the exact master path without overwriting an existing file. Keep ImageGen originals untouched.
### Task 2: Declare the V3 Machine Contract
**Files:**
- Create: `design-pipeline/manifests/a01-scroll-skins-v3.json`
- Modify: `design-pipeline/tests/manifest-v2.test.mjs`
- Modify: `design-pipeline/package.json`
**Interfaces:**
- Consumes: four master paths from Task 1.
- Produces: manifest assets `a01-scroll-primary-v3`, `a01-scroll-secondary-v3`, `a01-scroll-toast-v3`, and `a01-scroll-dialog-v3`.
- [ ] **Step 1: Write failing manifest tests**
Add assertions that the manifest declares exactly four unique outputs, exact logical/output dimensions, `uniform-only` for fixed button/dialog outputs, `nine-slice` for Toast, and A01 consumer selectors.
- [ ] **Step 2: Run RED**
Run `npm.cmd --prefix design-pipeline run test:v2`. Expected: failure because `a01-scroll-skins-v3.json` does not exist.
- [ ] **Step 3: Add the manifest and scripts**
Declare outputs:
```text
static/assets/foundation/transparent/a01-scroll-primary-v3.png 1866×276
static/assets/foundation/transparent/a01-scroll-secondary-v3.png 1866×300
static/assets/foundation/transparent/a01-scroll-toast-v3.png 1770×246
static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png 1860×1560
```
Add `build:a01-scroll-skins` and `verify:a01-scroll-skins` package scripts. All paths must remain inside the workspace.
- [ ] **Step 4: Run GREEN**
Run the Node tests and manifest validator. Expected: all tests pass and `MANIFEST-V2 PASS` names the v3 manifest.
### Task 3: Build Exact Clean Outputs
**Files:**
- Create: `design-pipeline/tests/test_build_scroll_skins.py`
- Create: `design-pipeline/scripts/build_scroll_skins.py`
- Create: `design-pipeline/scripts/build-a01-scroll-skins.mjs`
- Modify: `design-pipeline/scripts/asset_quality.py`
**Interfaces:**
- Produces: `remove_chroma_background(image, key, tolerance)`, `stretch_safe_center(image, output_size, insets)`, and `build_manifest(manifest, workspace)`.
- [ ] **Step 1: Write failing synthetic tests**
Tests must prove that chroma pixels become transparent black, decorative caps/corners remain pixel-identical after resizing, only the declared center stretches, exact output dimensions are returned, and a dialog with `alpha.required=false` remains opaque.
- [ ] **Step 2: Run RED**
Run the Python unittest discovery command. Expected: import failure for `build_scroll_skins`.
- [ ] **Step 3: Implement minimal Pillow processing**
Use RGBA conversion, chroma-distance Alpha removal, bounding-box trim, manifest insets, Pillow LANCZOS only for target-height normalization, center-only stretching, transparent-black cleanup, sRGB chunk writing, and optimized PNG output.
- [ ] **Step 4: Run GREEN and build actual assets**
Run Python tests, `npm.cmd --prefix design-pipeline run build:a01-scroll-skins`, and the quality verifier. Expected for every asset: exact dimensions, zero visible chroma residue, zero light fringe, zero transparent RGB, and no manifest errors.
- [ ] **Step 5: Inspect all four outputs**
Use `view_image`; reject seams, cropped rods, distorted clouds, weak gold contrast, center-knot drift, or dialog landscape entering the content region.
### Task 4: Integrate A01 Test-First
**Files:**
- Modify: `tests/a01-a02-ui-contract.ps1`
- Modify: `tests/a01-asset-alpha-audit.ps1`
- Modify: `tests/a01-responsive-runtime-smoke.js`
- Modify: `pages/auth/a01-entry.vue`
**Interfaces:**
- Consumes: four verified v3 outputs.
- Produces: A01 using v3 visuals without changing behavior or live content.
- [ ] **Step 1: Extend contracts before page changes**
Require the four v3 paths, exact sizes, no old A01 button/Toast/dialog references in A01, no `scaleToFill` on these skins, and the approved runtime selectors.
- [ ] **Step 2: Run RED**
Run both PowerShell contracts. Expected: failure because A01 still uses v2 buttons and the old dialog/code-native Toast.
- [ ] **Step 3: Make the minimal A01 change**
Replace only the decorative skin layers. Preserve copy, icon, state, click handlers, normal-flow layout, overlay positioning, and slot dimensions. Use `aspectFit` for exact button/dialog assets; use the Toast v3 asset through its declared adaptive rendering strategy.
- [ ] **Step 4: Run GREEN**
Run the PowerShell contracts and Python quality audit. Expected: pass.
### Task 5: Real Runtime and Visual Comparison
**Files:**
- Modify: `scripts/capture-chrome-page.js`
- Create ignored evidence under: `tmp/a01-scroll-skins-v3-*.png`
- Modify: `design-qa.md`
**Interfaces:**
- Produces: password, Toast, and verification-dialog runtime evidence at `412×915`, plus four-viewport structural verification.
- [ ] **Step 1: Add deterministic capture states**
The capture tool must support `password-tab`, `invalid-login`, and `verification-dialog` without arbitrary click timing. Runtime smoke must assert all four v3 assets load at expected natural dimensions.
- [ ] **Step 2: Run four viewports**
Run `node tests/a01-responsive-runtime-smoke.js`. Expected: pass at `320×568`, `360×640`, `360×800`, and `412×915`, with natural scrolling at 320×568 and no horizontal overflow.
- [ ] **Step 3: Capture and compare**
Capture the three meaningful A01 states. Compare the approved visual board and runtime evidence together. Fix P0/P1/P2 mismatches; do not loop on P3 polish.
- [ ] **Step 4: Update QA truthfully**
Record the exact source and runtime images in `design-qa.md`. The result may say H5 passed only; Android and user acceptance remain pending.
### Task 6: Documentation and Final Verification
**Files:**
- Modify: `docs/design/设计资产生产流水线规范.md`
- Modify: `docs/design/P00_页面结构与资产清单.md`
- Modify: `docs/交接记录.md`
- [ ] **Step 1: Record implemented facts**
Document the selected visual source, four v3 outputs, build/verify commands, A01-only scope, preserved old assets, and remaining Android/user gates.
- [ ] **Step 2: Run the focused verification set**
```powershell
npm.cmd --prefix design-pipeline run test:v2
npm.cmd --prefix design-pipeline run build:a01-scroll-skins
npm.cmd --prefix design-pipeline run verify:a01-scroll-skins
design-pipeline/.venv/Scripts/python.exe -m unittest discover -s design-pipeline/tests -p "test_*.py" -v
powershell -NoProfile -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/a01-asset-alpha-audit.ps1
node tests/a01-responsive-runtime-smoke.js
git diff --check
```
Expected: all focused checks pass; `git diff --check` has no whitespace error. CRLF notices alone are not failures.
- [ ] **Step 3: Report remaining gates**
State explicitly: H5 internal evidence completed or blocked, Android/HBuilderX pending, user page acceptance pending, A01 remains `[~]`, no Git mutation commands executed.
@@ -0,0 +1,117 @@
# A04 同步 A01 新视觉体系实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 A04 页面内旧认证视觉全部替换为 A01 已审核的新体系,并移除非最终的自定义滑动验证面板。
**Architecture:** A04 继续独立拥有注册字段和校验逻辑,只复用 A01 已生产的位图资产与全局字体合同。主按钮按固定比例等比显示,Toast 使用同一张可切片资产;真实滑动验证由后续接口组件负责,当前有效提交只显示待接入反馈。
**Tech Stack:** uni-app Vue 3、SCSS、PowerShell 静态契约、Chrome CDP 运行 smoke。
## Global Constraints
- A04 Logo 必须为 `184×221rpx``top: 64rpx`、水平居中。
- 禁止 A04 引用 `a01-primary-button.png``a01-secondary-button.png``a02-login-panel.png` 或局部 `STSong/SimSun`
- 注册主按钮使用 `a01-scroll-primary-v3.png``aspectFit`Toast 使用 `a01-scroll-toast-v3.png``border-image`
- 协议图标、返回箭头、宗祠底板保留。
- 不使用 worktree,不执行 Git 暂存、提交、推送、重置或清理。
- 不修改 A01 或其他页面,不对接接口,不把 H5 写成 Android 或用户整页验收。
---
### Task 1: 用契约锁定 A04 新视觉和验证边界
**Files:**
- Modify: `tests/a04-registration-contract.ps1`
- Modify: `tests/a04-registration-runtime-smoke.js`
**Interfaces:**
- Consumes: A04 当前页面结构与 A01 v3 资产路径。
- Produces: 旧资产禁止、Logo 精确尺寸、Toast 切片和有效提交反馈的回归合同。
- [ ] **Step 1: 写入失败静态契约**
要求 A04 包含 `a01-vnext-divider-v1.png``a01-scroll-primary-v3.png``a01-scroll-toast-v3.png``width: 184rpx;``height: 221rpx;``border-image-slice:` 和“滑动验证待接口接入”;禁止三个旧资产、`verificationVisible``verification-layer``feedback-toast__skin``font-family:`
- [ ] **Step 2: 更新运行 smoke 的期望**
有效表单提交后等待 `.feedback-toast`,断言文案为“滑动验证待接口接入”,并断言页面不存在 `.verification-layer`;四档尺寸改为 `320×568``360×640``360×800``412×915`
- [ ] **Step 3: 运行测试确认 RED**
运行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a04-registration-contract.ps1
```
预期:因 A04 仍引用旧主按钮和旧验证面板而失败。
---
### Task 2: 最小迁移 A04 页面
**Files:**
- Modify: `pages/auth/a04-register.vue`
**Interfaces:**
- Consumes: `a01-vnext-divider-v1.png``a01-scroll-primary-v3.png``a01-scroll-toast-v3.png` 与全局字体。
- Produces: A04 新默认态、错误态、协议态和滑动验证待接入反馈。
- [ ] **Step 1: 替换模板资产**
将标题分隔和主按钮改为新资产与 `aspectFit`,删除验证弹层模板及 Toast 内部拉伸图片。
- [ ] **Step 2: 收敛脚本状态**
删除 `verificationVisible``closeVerification``confirmVerification`;有效提交改为:
```js
showFeedback('滑动验证待接口接入')
```
- [ ] **Step 3: 同步样式**
`.brand-seal` 改为 `184×221rpx`;移除所有局部 `font-family`;主按钮保持现有点击槽但等比承载 v3 皮肤;Toast 使用:
```scss
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
```
- [ ] **Step 4: 运行静态合同确认 GREEN**
运行静态合同,预期输出 `A04-REGISTRATION-CONTRACT PASS`
---
### Task 3: 真实运行验证与浏览器交付
**Files:**
- Modify: `design-qa.md`
**Interfaces:**
- Consumes: A04 已迁移页面与 CDP 调试页。
- Produces: 四档响应式结果和浏览器中的 A04 默认态。
- [ ] **Step 1: 运行 A04 smoke**
运行 `node tests/a04-registration-runtime-smoke.js`,预期输出 `A04-REGISTRATION-RUNTIME-SMOKE PASS`
- [ ] **Step 2: 捕获并检查 412×915**
运行 `capture-chrome-page.js` 捕获 A04 默认态,人工检查 Logo、标题、字段、按钮、协议和底部留白。
- [ ] **Step 3: 更新 QA 边界**
`design-qa.md` 的 A04 段记录新资产和真实滑动验证延期;不得写成用户已验收。
- [ ] **Step 4: 最终聚焦验证**
重新运行静态合同、运行 smoke、截图合同和 `git diff --check`;将浏览器停在 A04 默认态供用户审核。
项目禁止 Git 操作,因此本计划不包含提交步骤。
@@ -0,0 +1,167 @@
# A05 向 A01 视觉体系收拢实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` inline. 本任务集中修改同一 A05 页面,不派发子代理;项目禁止 worktree、Git 暂存、提交和推送。
**Goal:** 让 A05 的 Logo、字体、标题分隔、按钮、Toast 和成功结果容器统一使用 A01 已确定的新视觉资产,同时保留四字段重设流程。
**Architecture:** `pages/auth/a05-reset-password.vue` 继续独立拥有表单和本地状态。页面直接消费 A01 正式资产;静态 PowerShell 合同负责禁止旧资产回退,Chrome CDP 烟测负责验证交互与多尺寸运行态。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell、Node.js、Chrome CDP。
## Global Constraints
- 只修改 A05、A05 聚焦测试、A05 QA/验收记录和必要计划文档。
- 不接接口;真实滑动验证使用后续组件自带样式,本轮只显示待接入 Toast。
- 不使用原生 UniApp Toast、Modal、Loading、ActionSheet。
- 320×568 允许自然纵向滚动;同时复核 360×640、360×800、412×915。
- A05 保持 `[~]`H5 证据不等于 Android/HBuilderX 或完整用户验收。
---
### Task 1: 收紧 A05 静态视觉合同
**Files:**
- Modify: `tests/a05-reset-password-contract.ps1`
- Test: `tests/a05-reset-password-contract.ps1`
**Interfaces:**
- Consumes: `pages/auth/a05-reset-password.vue` UTF-8 源码。
- Produces: 新资产必需项、旧资产禁用项、Logo/字体/九宫格渲染约束。
- [x] **Step 1: 写失败合同**
要求以下源码片段存在:
```powershell
'a01-vnext-divider-v1.png'
'a01-scroll-primary-v3.png'
'a01-scroll-toast-v3.png'
'a01-scroll-dialog-v3.png'
'width: 184rpx;'
'height: 221rpx;'
"border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');"
'border-image-slice: 58 280 fill;'
```
禁止以下旧路径和结构:
```powershell
'a01-primary-button.png'
'a01-secondary-button.png'
'a02-login-panel.png'
'auth-divider-knot.png'
'feedback-toast__skin'
'verificationVisible'
'verification-layer'
'font-family:'
```
- [x] **Step 2: 运行合同并确认 RED**
Run: `powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a05-reset-password-contract.ps1`
Expected: FAIL,首个失败原因是 A05 尚未引用新标题分隔或新按钮资产。
---
### Task 2: 最小迁移 A05 页面
**Files:**
- Modify: `pages/auth/a05-reset-password.vue`
- Test: `tests/a05-reset-password-contract.ps1`
**Interfaces:**
- Consumes: A01 的 `brand-seal.png``a01-vnext-divider-v1.png``a01-scroll-primary-v3.png``a01-scroll-toast-v3.png``a01-scroll-dialog-v3.png`
- Produces: 原选择器和表单 API 保持可用;获取验证码入口改为待接入 Toast。
- [x] **Step 1: 替换模板资产**
```vue
<image class="reset-divider" src="/static/assets/modules/auth/transparent/a01-vnext-divider-v1.png" mode="aspectFit" />
<image class="reset-submit__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<image class="success-dialog__skin" src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png" mode="aspectFit" />
<image class="success-action__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
```
删除旧验证层和 Toast 图片子层;Toast 只保留真实文字节点。
- [x] **Step 2: 收敛行为验证占位**
```js
const prepareGetCode = () => {
if (!/^1\d{10}$/.test(phone.value)) {
fieldErrors.value.phone = '请输入正确手机号'
return
}
fieldErrors.value.phone = ''
showFeedback('滑动验证待接口接入')
}
```
删除 `verificationVisible``closeVerification``confirmVerification`,不伪造验证码已发送。
- [x] **Step 3: 对齐样式**
Logo 改为 `184×221rpx`;删除全部局部 `font-family`;按钮图片使用 `aspectFit`;Toast 复制 A04 已验证的九宫格 `border-image` 参数;成功卷轴按现有遮罩居中,不改变返回登录逻辑。
- [x] **Step 4: 运行合同并确认 GREEN**
Run: `powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a05-reset-password-contract.ps1`
Expected: `A05-RESET-PASSWORD-CONTRACT PASS`
---
### Task 3: 更新运行烟测并完成 RED/GREEN
**Files:**
- Modify: `tests/a05-reset-password-runtime-smoke.js`
- Test: `tests/a05-reset-password-runtime-smoke.js`
**Interfaces:**
- Consumes: Chrome `9222` 调试端口与 `http://localhost:5173` H5 页面。
- Produces: 默认态、手机号错误、待接入 Toast、全字段错误、密码不一致和成功结果证据。
- [x] **Step 1: 将旧验证层断言改为新 Toast 断言**
点击“获取验证码”后等待 `.feedback-toast`,断言文字为“滑动验证待接口接入”;同时断言不存在 `.verification-layer`,表单值未清空。
- [x] **Step 2: 运行烟测**
Run: `node tests/a05-reset-password-runtime-smoke.js http://localhost:5173`
Expected: `A05-RESET-PASSWORD-RUNTIME-SMOKE PASS`
---
### Task 4: 真实截图、文档和最终验证
**Files:**
- Modify: `design-qa.md`
- Modify: `docs/验收规划.md`
- Modify: `docs/交接记录.md`
- Modify: `docs/design/P00_页面结构与资产清单.md`
- Update ignored evidence: `tmp/a05-*.png`
**Interfaces:**
- Consumes: 实际 H5 运行页与静态/运行测试输出。
- Produces: A05 待审核候选证据,不产生正式验收或冻结结论。
- [x] **Step 1: 捕获四档视口并审视**
用 Chrome CDP 捕获 320×568、360×640、360×800、412×915,检查 Logo、标题、四字段、验证码入口、按钮边缘、Toast、成功卷轴、滚动和横向溢出。
- [x] **Step 2: 更新 A05 记录**
只记录新视觉迁移与内部 H5 证据;A05 仍为 `[~]`,真实滑动验证、接口结果和 Android/HBuilderX 仍待审核。
- [x] **Step 3: 运行最终验证**
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a05-reset-password-contract.ps1
node tests/a05-reset-password-runtime-smoke.js http://localhost:5173
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/capture-chrome-page-contract.ps1
git diff --check
```
Expected: 三项测试 PASS`git diff --check` 退出码 0,允许已有 CRLF 提示。
@@ -0,0 +1,17 @@
# A06 向 A01 视觉体系收拢实施计划
> 当前会话内联执行;不使用子代理、worktree 或任何 Git 暂存、提交、推送操作。
**Goal:** 在不改变 A06 三种阻断状态逻辑的前提下,统一 Logo、字体、标题分隔、按钮和恢复说明层。
**Architecture:** `pages/auth/a06-auth-status.vue``statusConfig` 继续作为状态合同唯一所有者;只替换展示层资产与尺寸。PowerShell 合同禁止旧视觉回退,Chrome CDP 验证多状态和四档尺寸。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell、Node.js、Chrome CDP。
## Tasks
- [x]`tests/a06-auth-status-contract.ps1` 要求 `a01-vnext-divider-v1.png`、两次 `a01-scroll-primary-v3.png``a01-scroll-dialog-v3.png``184×221rpx``aspectFit`;禁止旧三项资产与 `font-family:`,运行得到 RED。
- [x]`pages/auth/a06-auth-status.vue` 最小替换资产和样式,保持 `statusConfig`、查询参数、恢复层与导航逻辑不变,运行合同得到 GREEN。
- [x]`tests/a06-auth-status-runtime-smoke.js` 尺寸补齐为 320×568、360×640、360×800、412×915,运行三状态、未知回落、弹层与返回 A01 烟测。
- [x] 捕获并检查 412×915 风险默认态和恢复说明层,以及 320×568 短屏;更新 `design-qa.md`、验收规划、交接记录和资产清单。
- [x] 运行 A06/A05/A04/A01 聚焦合同、A06 运行烟测、截图合同、状态统计和 `git diff --check`A06 保持 `[~]`
@@ -0,0 +1,16 @@
# A06 路由封存实施计划
> 用户于 2026-07-16 选择从正式路由移除 A06,但保留页面源码、测试、视觉证据与文档。禁止删除文件、worktree 和任何 Git 写操作。
**Goal:** 将 A06 从当前基础版本路由和验收队列中封存,使正式路由从 53 条变为 52 条,同时保留可恢复实现。
**Contract owner:** `pages.json` 是当前可发布路由的唯一所有者;`docs/交接记录.md` 记录 A06 封存原因、保留路径与恢复条件。
## Tasks
- [x] 先修改路由合同:A06 源码必须存在,但 `pages.json` 不得注册 A06A01/A02/A03/full-page 合同统一期待 52 条,运行得到 RED。
- [x]`pages.json` 删除 A06 单一路由项,不移动或删除 `pages/auth/a06-auth-status.vue`、A06 测试和证据,运行静态合同得到 GREEN。
- [x] 让 A06 runtime smoke 在路由封存时明确输出 SKIP 并成功退出;恢复路由后仍执行原多状态烟测。
- [x] 从 52 页活动验收表移出 A06,后续序号减一;新增“封存页面”记录,不计入 `[~]/[x]/[!]` 统计。
- [x] 更新交接记录、视觉手册、P00、证据索引和 Design QA:A06 保留用途为未来阻断登录流程,恢复条件为产品重新启用并补真实入口/接口/Android 复核。
- [x] 验证 52 条路由、52 个 `[~]`、0 `[x]`、0 `[!]`、A06 源码/测试/证据存在、活动路由不存在 A06,并运行相关合同及 `git diff --check`
@@ -0,0 +1,284 @@
# Design Asset Pipeline v2 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Subagents and worktrees are prohibited for this project.
**Goal:** Upgrade the existing Node/Sharp A01 pipeline with a strict manifest v2, a portable Python/Pillow pixel-quality gate, and two clean, exact-ratio A01 button v2 assets verified in the real uni-app page.
**Architecture:** Keep `design-pipeline/` as the single orchestration root. Node validates the manifest, checks consumers and runtime slots, and invokes Python; Pillow deterministically rebuilds versioned assets from committed masters and analyzes Alpha/edge contamination. A new pilot manifest is added beside schema v1 so the current reproducible pipeline is not broken before the pilot passes.
**Tech Stack:** Node.js 24 / native `node:test`, Sharp 0.34.5, Python 3.14, Pillow 12.3.0, PowerShell contracts, Chrome CDP 9222, uni-app/HBuilderX.
**Execution record (2026-07-16):** All six implementation tasks were executed in the current worktree. The importable Python module is `rebuild_a01_buttons.py` and the Node entry is `rebuild-a01-buttons.mjs`. The focused automated gates and four H5 viewports passed; Android/HBuilderX, a second-computer rebuild, and user acceptance remain outstanding. Existing handoff evidence was not overwritten; the latest internal 412×915 check stays under ignored `tmp/`.
## Global Constraints
- Do not use a worktree, subagent, `git add`, `git commit`, `git push`, `git reset`, `git checkout`, upload, or destructive cleanup.
- Preserve the current A01 and documentation changes already in the dirty worktree.
- Do not overwrite or delete `a01-primary-button.png` or `a01-secondary-button.png`; v2 outputs use new paths.
- Do not promote the pipeline beyond A01 in this implementation.
- A01 stays `[~]`; H5 evidence is internal only and Android/user verification remains outstanding.
- Fixed assets must match the actual logical slots: primary `622×92rpx`, secondary `622×100rpx`.
- Produce 3× files: primary `1866×276px`, secondary `1866×300px`.
- Button text and the WeChat icon remain code/UI nodes and must not be baked into the bitmaps.
- Every production-code task follows RED → GREEN and ends with a focused diff/status checkpoint instead of a commit.
---
### Task 1: Strict Manifest v2 Validator
**Files:**
- Create: `design-pipeline/manifests/a01-buttons-v2.json`
- Create: `design-pipeline/scripts/manifest-v2.mjs`
- Create: `design-pipeline/scripts/validate-manifest-v2.mjs`
- Create: `design-pipeline/tests/manifest-v2.test.mjs`
- Modify: `design-pipeline/package.json`
**Interfaces:**
- Consumes: a JSON file path supplied on the command line.
- Produces: `loadAndValidateManifest(filePath): Promise<ManifestV2>` and CLI output `MANIFEST-V2 PASS <path>`.
- [ ] **Step 1: Write failing Node tests**
Use `node:test` to assert that a valid fixture returns schema version 2 and that validators reject duplicate IDs, path escape, ratio mismatch over 0.5%, `uniform-only` with `scaleToFill`, unknown asset classes, and missing consumers.
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { validateManifest } from '../scripts/manifest-v2.mjs'
test('rejects a fixed bitmap whose output ratio differs from its slot', () => {
const manifest = validManifest()
manifest.assets[0].outputPixels.height = 300
assert.throws(() => validateManifest(manifest, workspace), /ratio/i)
})
```
- [ ] **Step 2: Run RED**
Run: `node --test design-pipeline/tests/manifest-v2.test.mjs`
Expected: FAIL because `manifest-v2.mjs` does not exist.
- [ ] **Step 3: Implement the minimal strict validator**
Validate these exact properties: `schemaVersion`, `page`, unique `assets[].id`, `assetClass`, workspace-safe `source/output`, `logicalSlot`, `outputPixels`, `render.scalePolicy`, `render.uniMode`, `alpha`, `edge`, `quality.maxBytes`, and non-empty `consumers`. Reject unknown top-level asset classes and calculate ratio drift as:
```js
const slotRatio = widthRpx / heightRpx
const outputRatio = width / height
const ratioDrift = Math.abs(outputRatio / slotRatio - 1)
if (ratioDrift > 0.005) throw new Error('asset ratio drift exceeds 0.5%')
```
- [ ] **Step 4: Add pilot manifest and package scripts**
Add scripts:
```json
{
"test:v2": "node --test tests/manifest-v2.test.mjs",
"validate:a01-buttons": "node scripts/validate-manifest-v2.mjs manifests/a01-buttons-v2.json"
}
```
The manifest declares the two old committed masters as `source`, versioned v2 outputs, exact slot/output dimensions, `uniform-only`, transparent outer padding, no chroma residue, sRGB, and the A01 consumer selectors `.login-submit` / `.wechat-login`.
- [ ] **Step 5: Run GREEN and checkpoint**
Run:
```powershell
npm.cmd ci --prefix design-pipeline
npm.cmd --prefix design-pipeline run test:v2
npm.cmd --prefix design-pipeline run validate:a01-buttons
git diff --check
git status --short
```
Expected: Node tests and manifest validation pass; only scoped files plus pre-existing changes appear.
### Task 2: Portable Python Pixel Quality Gate
**Files:**
- Create: `design-pipeline/requirements.txt`
- Create: `design-pipeline/scripts/asset_quality.py`
- Create: `design-pipeline/tests/test_asset_quality.py`
- Create: `design-pipeline/scripts/verify-assets.py`
**Interfaces:**
- Consumes: manifest v2 and its output PNG files.
- Produces: `analyze_asset(path: Path, spec: dict) -> dict`, non-zero CLI exit on violations, and `ASSET-QUALITY PASS <id>` per asset.
- [ ] **Step 1: Pin the supported dependency**
Create:
```text
Pillow==12.3.0
```
Pillow 12.3.0 supports Python 3.10+ including 3.14; do not add OpenCV unless a failing test proves Pillow is insufficient.
- [ ] **Step 2: Write failing Python unit tests with synthetic PNGs**
Use `unittest` and temporary images to cover exact dimensions, RGBA requirement, transparent corners, transparent outer padding, green residue, light fringe on partially transparent edge pixels, sRGB metadata warning/reporting, and maximum bytes.
```python
def test_rejects_visible_green_residue(self):
image = Image.new("RGBA", (32, 16), (0, 0, 0, 0))
image.putpixel((16, 8), (0, 255, 0, 255))
report = analyze_asset(self.save(image), self.spec())
self.assertIn("chroma residue", " ".join(report["errors"]))
```
- [ ] **Step 3: Run RED**
Run outside the sandbox if required:
```powershell
python -m venv design-pipeline/.venv
design-pipeline/.venv/Scripts/python.exe -m pip install -r design-pipeline/requirements.txt
design-pipeline/.venv/Scripts/python.exe -m unittest discover -s design-pipeline/tests -p 'test_*.py' -v
```
Expected: FAIL because `asset_quality.py` does not exist.
- [ ] **Step 4: Implement the minimal analyzer**
Implement row-wise Pillow analysis without OpenCV. Count chroma residue only where Alpha is visible. Evaluate light fringe only on pixels with `0 < alpha < 255`, so the intentionally warm-white secondary-button interior is not rejected. Require the declared transparent outer padding and exact dimensions.
- [ ] **Step 5: Implement manifest CLI and run GREEN**
`verify-assets.py` imports the analyzer, reads the manifest, validates each existing output, writes an ignored JSON report under `design-pipeline/generated/a01-buttons-v2/quality-report.json`, and exits non-zero on any error.
Run unit tests and `git diff --check`; expected unit-test PASS. Output verification remains RED until Task 3 creates v2 assets.
### Task 3: Deterministic A01 Button Rebuilder
**Files:**
- Create: `design-pipeline/scripts/rebuild-a01-buttons.py`
- Create: `design-pipeline/scripts/rebuild-a01-buttons.mjs`
- Create: `design-pipeline/tests/test_rebuild_a01_buttons.py`
- Modify: `design-pipeline/package.json`
**Interfaces:**
- Consumes: the two committed v1 master PNGs plus manifest v2.
- Produces: versioned v2 PNGs at exactly `1866×276` and `1866×300`, with transparent outer padding and no baked text/icons.
- [ ] **Step 1: Write failing reconstruction tests**
Generate synthetic framed masters with deliberately contaminated outer rows. Assert that `rebuild_button(source, output_size, trim, padding, cap_width)` removes the contaminated border, keeps transparent corners/padding, preserves left/right caps, and returns exact output size.
- [ ] **Step 2: Run RED**
Run the Python unittest command. Expected: FAIL because the rebuilder module does not exist.
- [ ] **Step 3: Implement nine-slice-like offline rebuilding**
Use Pillow only:
1. Crop the manifest-declared contaminated outer trim.
2. Split into left cap, stretchable center, and right cap.
3. Resize caps uniformly to the inner target height.
4. Resize only the center horizontally to fill the remaining width.
5. Paste onto an RGBA target with the declared transparent padding.
6. Save optimized sRGB PNG.
This is offline raster production, not runtime nine-slice; the page receives a complete exact-ratio bitmap.
- [ ] **Step 4: Add a Node orchestration script**
Add `rebuild:a01-buttons` to `package.json`, calling a small Node launcher that selects `PYTHON` or `python`, validates the manifest, runs the Python rebuilder, then runs `verify-assets.py`. A non-zero Python exit must propagate.
- [ ] **Step 5: Build actual v2 assets and run GREEN**
Run the rebuild command with Python available outside the sandbox if required. Inspect both output images with `view_image`, then run Python unit tests and output verification. Expected: exact sizes, transparent outer padding, no detected fringe/chroma residue.
### Task 4: A01 Asset Contract and Page Integration
**Files:**
- Modify: `tests/a01-a02-ui-contract.ps1`
- Modify: `tests/a01-asset-alpha-audit.ps1`
- Modify: `pages/auth/a01-entry.vue`
**Interfaces:**
- Consumes: v2 button paths and manifest render policy.
- Produces: A01 references v2 skins with no text/icon baked in and no ratio distortion.
- [ ] **Step 1: Extend the PowerShell contracts first**
Require v2 asset references, forbid the two v1 paths in A01, require `mode="aspectFit"` on both button skins, and verify exact v2 pixel sizes/Alpha corners. Keep v1 files present because other pages may still consume them.
- [ ] **Step 2: Run RED**
Run both A01 contracts. Expected: FAIL because A01 still references v1 assets with `scaleToFill`.
- [ ] **Step 3: Make the minimal page change**
Change only the two `button-skin` sources and their `mode`; do not change button copy, WeChat icon, click behavior, layout heights, or unrelated A01 styles.
- [ ] **Step 4: Run GREEN**
Run the A01 contracts and `git diff --check`. Expected: PASS.
### Task 5: Runtime Slot and Visual Verification
**Files:**
- Create: `design-pipeline/scripts/verify-runtime-slots.mjs`
- Create: `design-pipeline/tests/runtime-slot-contract.test.mjs`
- Modify: `design-pipeline/package.json`
**Interfaces:**
- Consumes: manifest v2 runtime URL, selectors, Chrome port 9222.
- Produces: `RUNTIME-ASSET-SLOTS PASS` after checking loaded sources, slot dimensions, ratio, overflow, and four viewports.
- [ ] **Step 1: Write failing Node tests for pure slot comparison**
Extract `compareSlot(actual, expected, tolerance)` and test exact match, tolerance, and mismatch. Run RED before implementation.
- [ ] **Step 2: Implement the CDP verifier**
Use the same native WebSocket request pattern as `scripts/capture-chrome-page.js`. Navigate without the existing reload race, wait for both images to load, and inspect 320×568, 360×640, 360×800, and 412×915. Assert no horizontal overflow and natural scroll at 320×568.
- [ ] **Step 3: Run Node tests and real runtime verification**
Keep HBuilderX H5 and the dedicated Chrome 9222 page running. Expected: pure tests PASS and `RUNTIME-ASSET-SLOTS PASS`.
- [ ] **Step 4: Capture one representative screenshot**
Overwrite the existing ignored A01 412×915 runtime representative instead of generating multiple similar images. Open it with `view_image` and inspect both button edges, corners, line sharpness, text/icon separation, and page layout.
### Task 6: Portable Handoff and Final Verification
**Files:**
- Modify: `docs/design/设计资产生产流水线规范.md`
- Modify: `docs/交接记录.md`
- Modify: `docs/design/P00_页面结构与资产清单.md`
**Interfaces:**
- Consumes: actual commands, paths, reports, and observed verification results.
- Produces: another Windows computer can restore Node/Python dependencies and repeat the verified static pipeline.
- [ ] **Step 1: Update documentation with facts only**
Replace “target contract” wording only for components actually implemented. Record Pillow 12.3.0, Python 3.10+ support, environment commands, manifest path, script names, v2 asset dimensions, and remaining Android/user-validation gap.
- [ ] **Step 2: Run the complete focused verification set**
Run:
```powershell
npm.cmd --prefix design-pipeline run test:v2
npm.cmd --prefix design-pipeline run validate:a01-buttons
npm.cmd --prefix design-pipeline run rebuild:a01-buttons
design-pipeline/.venv/Scripts/python.exe -m unittest discover -s design-pipeline/tests -p 'test_*.py' -v
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-asset-alpha-audit.ps1
npm.cmd --prefix design-pipeline run verify:a01-buttons:runtime
git diff --check
git status --short
```
- [ ] **Step 3: Report exact verification layers**
Report static pipeline, Python tests, H5 runtime and screenshot evidence separately. Do not claim Android or user acceptance. Do not commit, stage, push, upload, delete v1 assets, or promote the pipeline to other pages.
@@ -0,0 +1,34 @@
# G01 Application Card Spacing Implementation Plan
> **For agentic workers:** 本计划在当前会话内执行;用户禁止 worktree、多代理及任何 Git 写操作。
**Goal:** 让 G01“加入申请”组三张独立卷轴卡片之间保持 `12rpx` 间距。
**Architecture:** 保留现有模板和资产,仅使用相邻兄弟选择器为第二张及后续申请卡增加上间距。静态合同锁定精确值,运行烟测与真实截图确认页面未溢出且视觉接缝消失。
**Tech Stack:** Vue 3、uni-app、SCSS、PowerShell 静态合同、Chrome CDP 截图。
## Global Constraints
- 只修改 G01 当前审核区域。
- 不新增或替换资产。
- 不执行 Git 暂存、提交、推送、重置或检出。
- H5 截图只是当前视觉审核证据,Android/HBuilderX 仍待复核。
---
### Task 1: 锁定并实现申请卡间距
**Files:**
- Modify: `tests/g01-visual-contract.ps1`
- Modify: `pages/genealogy/g01-my-genealogies.vue`
**Interfaces:**
- Consumes: 现有 `.application-record` 相邻兄弟结构。
- Produces: `.application-record + .application-record { margin-top: 12rpx; }`
- [x]`tests/g01-visual-contract.ps1` 增加精确间距断言。
- [x] 运行合同,确认因缺少 `12rpx` 间距而失败。
- [x] 在 G01 样式中加入相邻兄弟选择器,不调整其他样式。
- [x] 重新运行 G01 静态合同与运行烟测并确认通过。
- [x] 捕获并检查 412×915 默认状态,确认三张申请卡之间有小间隔且没有横向溢出。
@@ -0,0 +1,53 @@
# G01 List Background Assets Implementation Plan
> **For agentic workers:** 当前会话内执行;禁止 worktree、多代理和任何 Git 写操作。
**Goal:** 保存三个 G01 背景母版,并建立可在另一台电脑确定性重建候选输出的 Python+Node 流水线。
**Architecture:** ImageGen 输出作为受控母版;候选清单是生成提示、源路径和处理模式的唯一所有者。Python 进行像素处理,Node 校验路径并编排,生成结果只进入可再生的忽略目录。
**Tech Stack:** Node.js 22+、Python 3、Pillow 12.3.0、JSON、PNG。
## Global Constraints
- 不修改 G01 页面滚动结构或运行资产引用。
- 不覆盖现有 `footer-mountain-bamboo.png`
- 不把候选写入 handoff 截图目录。
- 不执行 Git 暂存、提交、推送、重置或检出。
---
### Task 1: 候选母版与机器清单
**Files:**
- Create: `design-pipeline/manifests/g01-background-candidates.json`
- Create: `design-pipeline/tests/g01-background-candidates.test.mjs`
- [ ] 先写清单合同测试并确认缺少清单时失败。
- [ ] 写入三个母版、完整提示、源尺寸、处理模式和输出路径。
- [ ] 运行 Node 测试并确认清单通过。
### Task 2: 确定性像素构建
**Files:**
- Create: `design-pipeline/scripts/build_g01_backgrounds.py`
- Create: `design-pipeline/scripts/build-g01-backgrounds.mjs`
- Create: `design-pipeline/tests/test_build_g01_backgrounds.py`
- Modify: `design-pipeline/package.json`
- [ ] 先写纯绿、抗锯齿绿边和不透明母版测试并确认失败。
- [ ] 实现绿幕转 Alpha、去绿污染、透明 RGB 清零和不透明母版标准化。
- [ ] 添加 Node 编排入口和 npm 命令。
- [ ] 运行 Python/Node 测试与实际候选构建。
### Task 3: 交接与清理审计
**Files:**
- Modify: `docs/design/设计资产生产流水线规范.md`
- Modify: `docs/design/P00_页面结构与资产清单.md`
- Modify: `docs/交接记录.md`
- Create: `docs/design/G01_列表背景候选与换机重建.md`
- [ ] 记录三母版、构建命令、依赖、不可逐像素重新生成的边界和页面滚动待讨论事项。
- [ ] 扫描截图、候选和未跟踪文件的文档引用,生成精确清理清单。
- [ ] 不删除历史 handoff 证据;任何含糊项留给用户确认。
@@ -0,0 +1,16 @@
# A01 品牌 Logo 尺寸微调设计
## 目标
将 A01 顶部现有 `brand-seal.png``220×264rpx` 缩小为 `184×221rpx`,降低它对宗祠主体的遮挡感,同时保持“谱”字清晰可辨。
## 已确认方案
- 资产、宽高比、水平中心线和 `top: 64rpx` 均不变。
- 只修改 `.brand-seal``width``height`
- 不移动标题、宗祠底图、登录内容或其他页面。
- 412×915 真实运行截图中,Logo 应完整位于宗祠区域、没有拉伸或裁切,并与标题保持清楚的上下层级。
## 验证边界
先用 A01 静态契约锁定精确尺寸,再运行四档 H5 smoke,并人工查看 412×915 截图。H5 通过仍不代表 Android/HBuilderX 或用户整页验收完成。
@@ -0,0 +1,101 @@
# A01 公用卷轴云纹皮肤设计规格
> 日期:2026-07-16
> 状态:用户已确认第 2 套卷轴云纹四件套视觉,待用户复核本规格后进入实施计划。
> 初选视觉源:`docs/design/assets/a01-vnext/A01-shared-skin-family-option-2-selected.png`
> 最终视觉源:`docs/design/assets/a01-vnext/A01-shared-scroll-skins-with-dialog-approved.png`
## 1. 目标与范围
本轮只重新设计并落地 A01 的公用视觉皮肤,不改登录流程、文案、图标、字号、按钮槽位或接口状态。
需要形成同一视觉家族中的四个独立成品:
1. 朱砂主按钮皮肤:用于登录等主操作。
2. 暖宣纸次按钮皮肤:用于微信登录等次操作。
3. 暖宣纸提示框皮肤:用于项目自定义 Toast;与次按钮同族,但更轻、更简洁并允许内容增高。
4. 暖宣纸验证弹窗皮肤:用于安全验证弹窗;使用同族卷轴金线、祥云和底部淡墨山水。
按钮版和提示版不强行共用同一张最终 PNG。它们共享纹样语言、色板和生产组件,但分别适配自己的槽位与伸缩行为。
## 2. 选定视觉语言
- 方向:第 2 套卷轴云纹。
- 主按钮使用深朱砂纸面、古金双线、左右卷轴端头、清晰祥云角花和上下中心盘长结。
- 次按钮使用暖宣纸纸面和同构古金纹样,边线对比度必须高于当前按钮,缩到 320px 屏幕仍能识别花纹。
- Toast 使用同族暖宣纸和祥云,但减少卷轴端头与边线层级,避免抢占错误文案;不能看成缩小的微信按钮。
- 验证弹窗使用清晰的祥云角花和古金双线,淡墨山水仅占底部约 22%,中央保持安静,不能干扰标题、说明和操作按钮。
- 文字、微信图标和提示文案全部由 Vue 保持为活内容,不烘焙进资产。
- 不引入现代渐变、玻璃拟态、高亮白外圈、绿色残边或与宗祠头图冲突的重阴影。
## 3. 公用资产结构
### 3.1 按钮皮肤
按钮采用“横向三段式主皮肤 + 独立中心结饰”的结构:
- 左端:固定宽度,保存卷轴端头、左侧祥云和角线。
- 中段:只保存连续朱砂/宣纸纹理与直线,可横向伸缩。
- 右端:固定宽度,保存卷轴端头、右侧祥云和角线。
- 中心结饰:单独透明叠加,始终几何居中,不随中段拉伸。
这样既忠实保留选定图中的端头、祥云和中心结,又不会把花纹整体横向拉扁。定位只用于皮肤层、结饰层和文字层叠放;按钮本身仍参与正常文档流。
### 3.2 Toast 皮肤
Toast 使用独立的轻量九宫格/三段式皮肤:
- 左右祥云和折角固定。
- 中央纸面与直线允许横向伸缩。
- 上下方向允许在字体放大或文案换行时增高。
- 不使用按钮卷轴端柱,避免提示框显得过重。
如果 Android WebView 对九宫格表现不一致,则由同一 master 通过 Python 离线导出项目已知槽位尺寸,不允许回退到 `scaleToFill` 整图拉伸。
### 3.3 验证弹窗皮肤
验证弹窗使用独立九宫格皮肤:
- 四角祥云、折角和双层金线固定,不随弹窗宽高变化而拉伸。
- 中央暖宣纸区域允许双向伸缩,用于活标题、说明文案和操作区。
- 底部淡墨山水作为独立安全区,只能在水平方向延展或按目标槽位离线导出,不能侵入正文区域。
- 弹窗内容与操作按钮保持代码原生;遮罩、关闭交互和验证流程不改变。
- 现有 `a02-login-panel.png` 约为 `944×1668px` 竖版图,却被 `scaleToFill` 铺进 `620×520rpx` 横向槽位,属于明确比例变形;新资产不得沿用该比例和渲染方式。
## 4. 尺寸合同
| 成品 | 逻辑槽位 | 3× 基准输出 | 伸缩策略 |
| --- | --- | --- | --- |
| 主按钮 | `622×92rpx` | `1866×276px` | 左右端固定,中段横向伸缩,结饰独立居中 |
| 次按钮 | `622×100rpx` | `1866×300px` | 左右端固定,中段横向伸缩,结饰独立居中 |
| Toast | `590×82rpx` 起 | `1770×246px` 起 | 四角固定,中部可横向和纵向扩展 |
| 验证弹窗 | `620×520rpx` | `1860×1560px` | 四角与底部山水固定安全区,中央双向伸缩 |
320×568 下按钮允许随页面自然滚动,不为同屏展示压缩按钮高度。Toast 宽度不得超出安全区,字体放大后不得裁字。
## 5. 生产方法
1. 以选定视觉图作为艺术方向,不直接从带展示背景的整图粗裁最终资产。
2. 使用 ImageGen 分别生成无文字、无图标、无展示阴影的主按钮、次按钮、Toast 和验证弹窗 master。
3. 用 Python/Pillow 完成透底、去白边/绿边、对称校正、切片、sRGB 与精确尺寸导出。
4. Node manifest 记录 master、切片安全区、消费者、槽位和输出;禁止页面重复定义同一合同。
5. A01 先作为唯一试点,Android 和用户确认前不推广到其他页面。
## 6. 验收标准
- 320px 宽真实运行截图中,卷轴端头、祥云和金线能够清楚识别。
- 左右纹样对称;中心结不偏移、不拉伸;按钮文字与微信图标不被纹样干扰。
- 主/次按钮没有白边、绿边、黑边、透明 RGB 或比例变形。
- Toast 在短文案、长文案和字体放大状态下均不裁字、不拉坏角花。
- 验证弹窗在 `620×520rpx` 槽位中不得出现横向压扁、模糊角花或山水侵入正文;标题、说明和两个操作入口必须保持清晰。
- `320×568``360×640``360×800``412×915` H5 检查通过。
- Android/HBuilderX 真机或模拟器复核通过后,才具备公用推广资格。
- 用户没有明确说“通过”前,A01 始终保持 `[~]`
## 7. 非目标
- 不修改 A01 以外页面的按钮。
- 不清理旧按钮资产或历史截图。
- 不对接登录接口。
- 不制作深色主题。
- 不把内部 H5 截图写成用户验收证据。
@@ -0,0 +1,39 @@
# A04 同步 A01 新视觉体系设计
## 目标
将 A04 注册账号页内仍在使用的旧认证视觉替换为 A01 已经通过用户审核的新体系,同时保持 A04 的注册字段、校验逻辑、协议入口和返回登录流程不变。
## 已确认方案
用户选择“方案 1:完整同步 A01 新体系”。本次只修改 `pages/auth/a04-register.vue`、A04 聚焦测试和必要文档,不修改 A01 或其他页面。
### 视觉合同
- 继续使用现有宗祠水墨底板和 `brand-seal.png`
- A04 Logo 与 A01 完全一致:`184×221rpx``top: 64rpx`、水平居中。
- 页面不再声明 `STSong/SimSun`,统一继承全局 `STKaiti / KaiTi`
- 标题分隔改用 `a01-vnext-divider-v1.png``aspectFit`
- 注册主按钮改用 `a01-scroll-primary-v3.png``aspectFit`;文字继续由 Vue 独立承载。
- 反馈 Toast 改用 `a01-scroll-toast-v3.png``border-image` 切片;文案继续由 Vue 承载。
- A04 协议勾选/未勾选图标继续使用 A01 当前已审核的同款资产,不因文件名含 `a02` 而重复造图。
- 返回箭头和宗祠底板属于仍在使用的共享资产,不列为旧视觉替换对象。
### 滑动验证边界
- 删除 A04 当前 `a02-login-panel.png` 自定义安全验证占位视觉及对应开关状态。
- 完整本地校验通过后,只用新版自定义 Toast 提示“滑动验证待接口接入”。
- 后续接口阶段由真实滑动验证组件的自带滑块、轨道、拼图和结果样式接管;本轮不制作新的自定义验证面板。
## 状态与交互
- 保留手机号、设置密码、确认密码三字段和就近错误提示。
- 保留协议未勾选错误、勾选后即时清除、返回 A01 和协议入口。
- 不接注册接口,不伪造注册成功,不新增页面或路由。
## 验证
- 测试先锁定新资产、全局字体、Logo 尺寸和旧资产禁用,再实施页面修改。
- 检查默认、字段错误、协议错误、协议勾选和滑动验证待接入 Toast。
- H5 覆盖 320×568、360×640、360×800、412×915412×915 在用户当前 Chrome 中逐状态审核。
- H5 通过不代表 Android/HBuilderX 或用户整页验收完成,A04 保持 `[~]`
@@ -0,0 +1,48 @@
# A05 向 A01 视觉体系收拢设计
> 设计决定:2026-07-16 用户确认采用完整收拢方案。本文只约束 A05 的视觉资产与字体迁移,不改变接口边界和重置密码业务流程。
## 目标
把 A05 重设密码页仍在使用的旧认证视觉替换为 A01 已确定的新体系,使 A01、A04、A05 在品牌 Logo、字体、标题分隔、主按钮和项目自定义 Toast 上保持一致。
## 唯一视觉真源
- A01 当前真实运行视觉与 `docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png`
- 全局字体合同:`styles/global.scss` 中的 `"STKaiti", "KaiTi", serif`
- Logo`static/assets/foundation/transparent/brand-seal.png`A05 使用与 A01 相同的 `184×221rpx` 槽位、`top: 64rpx` 和水平居中规则。
- 标题分隔:`static/assets/modules/auth/transparent/a01-vnext-divider-v1.png`,使用 `aspectFit`,不得拉伸。
- 朱砂主按钮:`static/assets/foundation/transparent/a01-scroll-primary-v3.png`,用于“确认重设”和成功结果中的“返回登录”,使用 `aspectFit`
- 浅色卷轴 Toast`static/assets/foundation/transparent/a01-scroll-toast-v3.png`,使用九宫格 `border-image`,不得用绝对定位图片层拉伸。
## 页面结构
1. 保留现有红色宗祠水墨底图、返回按钮、标题、副标题、四字段表单、获取验证码入口、重设提示和返回登录入口。
2. 删除 A05 内所有局部 `font-family`,统一继承项目全局楷体。
3. Logo 缩小到 A01 已确认尺寸;不改底图,不把 Logo 烘焙进背景。
4. 标题分隔线、页面主按钮和成功结果按钮换成 A01 新资产;按钮文字仍由真实 DOM 承担。
5. Toast 删除旧 `feedback-toast__skin` 图片层,改用新浅色卷轴九宫格背景。
## 行为验证与结果态边界
- A05 仍在“获取验证码”前校验手机号。
- 真实滑动验证必须使用后续接口组件自带样式;旧 `a02-login-panel.png` 验证面板不作为最终视觉证据。
- 本轮移除旧自定义验证面板的最终视觉依赖。手机号通过后只显示“滑动验证待接口接入”的项目自定义 Toast,不伪造滑块完成、验证码发送成功或倒计时。
- 四字段本地校验和成功结果层继续保留,用于当前页面视觉流程审核;成功结果层换用 `a01-scroll-dialog-v3.png` 作为国风卷轴容器,但该资产在滑动验证场景仍不得冒充真实组件。
- 返回登录仍使用 `/pages/auth/a01-entry`,不恢复已删除的 A02。
## 不变范围
- 不接短信、滑动验证、重置密码或登录接口。
- 不新增路由,不修改 A01、A04 或其他页面。
- 不改变手机号、验证码、新密码、确认新密码的字段和现有就近错误文案。
- 不新增深色主题,不压缩 320×568;短屏继续自然纵向滚动。
- 不使用 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet`
## 验收标准
- 静态合同要求 A05 引用新 Logo 尺寸、标题分隔、两处朱砂按钮与九宫格 Toast,且不再引用旧主按钮、旧次按钮、旧登录面板或局部字体。
- 运行时覆盖默认空表单、手机号错误、滑动验证待接入 Toast、全字段错误、密码不一致和成功结果层。
- 320×568、360×640、360×800、412×915 无横向溢出;320×568 允许纵向滚动。
- 只保存一张 412×915 代表图或一张复杂状态联系表;H5 只作为候选证据。
- Android/HBuilderX、真实滑动验证和真实接口结果继续待审,A05 保持 `[~]`,不冻结。
@@ -0,0 +1,26 @@
# A06 向 A01 视觉体系收拢设计
> 2026-07-16 用户确认直接收拢。本文复用 A01/A04/A05 已确定的认证视觉合同,不探索新方向。
## 目标与真源
- A06 保留 `frozen|disabled|risk` 三种阻断状态、未知状态回落、原因/影响/恢复信息、恢复说明层与返回 A01 行为。
- Logo 使用 `brand-seal.png` 与 A01 相同的 `184×221rpx``top: 64rpx` 居中槽位。
- 页面删除全部局部 `font-family`,继承 `styles/global.scss` 的全局楷体。
- 标题分隔使用 `a01-vnext-divider-v1.png``aspectFit`
- 页面“查看恢复方式”和恢复层“我知道了”使用 `a01-scroll-primary-v3.png``aspectFit`
- 恢复说明层使用 `a01-scroll-dialog-v3.png``aspectFit`,仅作为项目自定义信息层,不冒充接口结果。
## 不变范围
- 不改状态文案、查询参数、状态回落、打开/关闭恢复层和返回 A01 的逻辑。
- 不接账号申诉、身份核验或恢复接口。
- 不修改其他页面,不清理旧资产文件。
- 基础版本仅浅色;320×568 自然纵向滚动。
- A06 保持 `[~]`,真实入口、接口和 Android/HBuilderX 仍待复核。
## 验收
- 静态合同要求四项新资产、Logo 尺寸、两处新版按钮和全局字体继承;禁止旧按钮、旧面板、旧分隔与局部字体。
- 运行态覆盖冻结、停用、风险限制、未知回落、恢复说明层关闭和返回 A01。
- 复核 320×568、360×640、360×800、412×915,无横向溢出,短屏可滚动。
@@ -0,0 +1,23 @@
# G01 加入申请卡片间距设计
## 决定
G01“加入申请”组三张记录继续使用现有独立卷轴卡片资产。相邻卡片之间增加 `12rpx` 垂直间距,第一张卡片与分组标题之间保持现状。
## 原因
- 每张卡片代表一条独立申请,适度留白能强化独立点击与状态扫读。
- 现有卷轴卡片包含完整上下边框和四角装饰;直接相连会造成双边框和角花碰撞。
- `12rpx` 能消除接缝拥挤,同时保持三条记录属于同一分组,不明显增加页面长度。
## 范围
- 只修改 `pages/genealogy/g01-my-genealogies.vue` 中相邻 `.application-record` 的垂直间距。
- 不修改卡片资产、卡片高度、内边距、文案、状态颜色、分组标题、其他家谱卡片和页面导航。
- 在 412×915 Chrome H5 真实运行画面复核;Android/HBuilderX 仍留在最终设备验收阶段。
## 验收
- `.application-record + .application-record``margin-top``12rpx`
- 第一张申请卡没有新增顶部间距。
- 页面不存在横向溢出,三张卡片边框不再相接。
@@ -0,0 +1,31 @@
# G01 列表背景资产设计
## 当前决定
先生成并保留三个 G01 列表背景母版,不修改页面滚动结构,也不替换现有运行资产。下一轮先由用户选择方向,再创建唯一正式运行资产并接入页面。
## 已生成方向
- A:上方远山、左下亭台、右侧竹影,纯绿幕背景,适合提取透明叠加层。
- B:上下远山形成纵向框景、左中亭台、右侧竹影,纯绿幕背景,适合提取透明叠加层。
- C:云气、树枝、亭台、竹影和山水从上到下连续分布,带完整暖宣纸底,适合直接作为不透明列表背景。
三个母版都只作为候选,不等于用户已选择,也不进入页面引用。母版位于 `docs/design/assets/g01-background/masters/`
## 生产合同
1. ImageGen 只负责艺术母版;准确提示词、母版路径和处理模式写入 `design-pipeline/manifests/g01-background-candidates.json`
2. Python/Pillow 负责解码、绿幕转 Alpha、去绿边、清理透明 RGB、sRGB 输出和确定性压缩。
3. Node 负责校验清单路径与源尺寸、调用 Python,并把结果写入已忽略的 `design-pipeline/generated/g01-background/`
4. 另一台电脑使用仓库内母版构建时应得到相同像素结果;再次调用模型只能得到同方向的新母版,不能保证逐像素一致。
5. 用户选择方向前,不创建 `static/assets/` 正式输出,不修改 G01 页面。
## 下一轮页面结构待讨论
用户提出 G01 应只滚动下方列表:红色标题栏、当前家谱大卡片、四个快捷入口和分隔线固定;从“我创建的”开始独立滚动。新背景预计固定在列表滚动窗口背后,而不是作为列表末尾内容。该结构尚未批准实施,下一轮继续讨论并进行四档尺寸验证。
## 质量与仓库边界
- 候选生成结果不写入 `docs/design/screens/handoff/`,避免扩大交接截图合同。
- `generated/`、虚拟环境、Node 依赖和 `tmp/` 不进入 Git。
- 三个母版是当前选择尚未完成时的必要设计输入;选定正式方向后,再由用户决定是否保留未选母版。
+43 -16
View File
@@ -3,20 +3,25 @@
> 最后更新:2026-07-16
> 用途:更换电脑、重新打开 Codex/GPT 后的唯一接管入口。
> 当前阶段:只做页面样式与视觉流程验收;不对接接口,不做功能验收。
> 当前施工停点:A01 至 T04 共 15 页已有内部 H5 候选,均尚未通过用户验收;下一页是 T07 成员目录。
> 当前施工停点:14 个活动页面已有内部 H5 候选;A06 另行封存并保留实现。用户下一审核页是 G01,无人审核时下一施工页是 T07 成员目录。
## 1. 接管后先复述的准确停点
- `pages.json` 当前实际注册 **53最终路由**
- `pages.json` 当前实际注册 **52活动路由**A06 已从路由表移除但源码保留
- A02 已删除并合入 A01;不存在需要保留的 A02 兼容入口。
- `docs/验收规划.md` 当前统计为:**53 个待审核、0 个已验收、0 个返工标记**
- 历史上曾标记通过的 A01、A02、A04、A05、A06、G01、G03 只保留为历史视觉证据**本轮全部重新审核**
- `docs/验收规划.md` 当前活动页统计为:**52 个待审核、0 个已验收、0 个返工标记、0 个冻结**;A06 属于封存页,不计入这些数量
- 历史上曾标记通过的 A01、A02、A04、A05、A06、G01、G03 只保留为历史视觉证据;活动页必须在本轮重新审核,A06 按最新决定封存
- 当前没有任何页面处于视觉冻结状态;只有用户在本轮明确说“通过”后,对应页面才能改为 `[x]` 并冻结。
- 2026-07-16 已按用户“继续按验收规划自主推进”的最新授权,完成 **A01、A04、A05、A06、G01、G03、G05、G06、G08、G09、G10、G11、G12、T01、T04** 的内部 H5 候选、聚焦合同和运行时检查;这些结果都只是待审候选,不能写成“用户已验收”或“已冻结”。
- 2026-07-16 已完成 **A01、A04、A05、G01、G03、G05、G06、G08、G09、G10、G11、G12、T01、T04** 共 14 个活动页面的内部 H5 候选、聚焦合同和运行时检查;这些结果都只是待审候选,不能写成“用户已验收”或“已冻结”。A06 的候选实现和证据另行封存。
- A01 当前候选已落实:闭眼图标保留中心瞳孔;密码行使用项目现有锁图标;短信验证码输入图标使用自生成的“消息气泡 + 三点”;不得恢复勾选图标或临时第三方 SVG。密码/短信两态代表联系表为 `docs/design/screens/handoff/2026-07-16/A01-current-password-sms-contact-824x915.png`
- 当前施工停在 **T04 新增亲属之后**,下一页严格按 `docs/验收规划.md` 进入 **T07 成员目录**。T01 收敛时删除了未注册路由但仍被 Git 跟踪的旧占位文件 `pages/tree/t02-tree-states.vue`;T04 复核时把共用成员表单残留的原生 `uni.showModal` 替换为项目自定义冲突说明层。用户明早仍需从 A01 开始逐页审核;未明确说“通过”的页面一律保持 `[~]`
- 2026-07-16 用户已逐态确认 A04 当前可实现的 8 类 H5 视觉状态;A04 继续保持 `[~]`,因为真实滑动验证必须使用接口组件自带样式,注册成功/失败真实链路和 Android/HBuilderX 复核尚未完成。当前用户逐页审核位置已推进到 **A05 重置密码**
- A05 已按用户决定向 A01 完整收拢 Logo、全局楷体、标题分隔、两处朱砂按钮、浅色卷轴 Toast 和成功结果卷轴;旧验证面板、伪“验证码已发送”和“重新获取”状态已移除。2026-07-16 用户已逐态确认当前 7 类 H5 视觉状态;返回 A01 后“回填手机号、聚焦密码框”由用户明确延期到真实重置接口链路接入时解决,不阻塞当前样式验收。真实滑动验证、接口结果和 Android/HBuilderX 仍待复核,A05 继续保持 `[~]`、不冻结。
- A06 已向 A01 完整收拢 Logo、全局楷体、标题分隔、页面主按钮、恢复层按钮和恢复卷轴。2026-07-16 用户确认样式没有问题,但判断当前流程暂时不用该页,并选择第二种封存方式:从 `pages.json` 和活动验收表移除,保留 `pages/auth/a06-auth-status.vue`、测试、设计记录与截图证据。A06 不标 `[x]`、不冻结,也不计入活动页 `[~]` 数量;运行 smoke 在封存期间明确 SKIP。
- G01 审核中已把“加入申请”三张卡改为独立 `12rpx` 间距并通过 H5 合同与运行截图;页面尚未整体通过。用户随后提出顶部标题、当前家谱大卡片和四个快捷入口应固定,仅从“我创建的”开始滚动;该结构留到下一轮继续讨论,尚未改代码。
- G01 旧底部山水存在主体过低和上部空白问题。2026-07-16 已生成 A/B/C 三个 `1024×1536` 背景母版并保存到 `docs/design/assets/g01-background/masters/`,建立 `g01-background-candidates.json`、Python 绿幕/色彩处理和 Node 编排。本机三候选构建的尺寸、可见绿幕、透明 RGB、体积和 sRGB 检查均为 0 错误;尚未由用户选择,不得接入页面。换机命令和质量哈希见 `docs/design/G01_列表背景候选与换机重建.md`
- 当前施工停在 **T04 新增亲属之后**,无人审核时下一页严格按 `docs/验收规划.md` 进入 **T07 成员目录**。T01 收敛时删除了未注册路由但仍被 Git 跟踪的旧占位文件 `pages/tree/t02-tree-states.vue`;T04 复核时把共用成员表单残留的原生 `uni.showModal` 替换为项目自定义冲突说明层。当前用户逐页审核已完成 A01、A04、A05 的可实现 H5 状态审视并封存 A06,下一活动审核页为 **G01 我的家谱**;尚缺的真实接口状态与 Android/HBuilderX 复核继续保留为待办。
如果其他文档仍写“54 条路由、7 个已验收或冻结”,那是旧阶段记录。当前状态和验收数量以本文件、实际 `pages.json``docs/验收规划.md` 为准。
如果其他文档仍写“5354 条路由、7 个已验收或冻结”,那是旧阶段记录。当前状态和验收数量以本文件、实际 `pages.json``docs/验收规划.md` 为准。
## 2. 新电脑启动时必须完整阅读
@@ -30,7 +35,8 @@
6. `docs/design/视觉证据索引.md`
7. `docs/design/D1_安卓视觉规范与页面壳.md`
8. `docs/design/P00_页面结构与资产清单.md`
9. `design-qa.md`(历史内部 QA 证据不等于本轮用户验收)
9. `docs/design/设计资产生产流水线规范.md`
10. `design-qa.md`(历史内部 QA 证据不等于本轮用户验收)
阅读后先运行:
@@ -40,7 +46,7 @@ git status --short
必须保留全部已有未提交改动。不要因为文件是未跟踪状态就删除,也不要用旧提交覆盖当前工作区。
随后先向用户复述:53最终路由、53 页全部待审核、当前无冻结页、内部候选施工停在 T01 之后、用户逐页审核仍从 A01 开始、下一施工页是 T04,以及准备使用的技能。复述准确前不要继续修改页面。
随后先向用户复述:52活动路由、52 页全部待审核、当前无冻结页、已有 14 页活动 H5 候选、A06 封存保留、用户下一审核页是 G01、无人审核时下一施工页是 T07,以及准备使用的技能。复述准确前不要继续修改页面。
## 3. 文档权威顺序
@@ -48,7 +54,7 @@ git status --short
1. 用户在当前对话中的最新明确决定。
2. `docs/交接记录.md`:真实停点、风险、下一步。
3. `docs/验收规划.md`53 页流程顺序、状态合同、视觉验收规则和勾选状态。
3. `docs/验收规划.md`52活动流程顺序、封存页记录、状态合同、视觉验收规则和勾选状态。
4. 实际代码、`pages.json` 和测试:当前实现事实。
5. `docs/design/视觉证据索引.md`:默认展示证据。
6. `docs/design/视觉设计交接手册.md``docs/规划.md`:历史方法、范围和背景。
@@ -83,6 +89,10 @@ git status --short
- 睁眼图标:`static/assets/modules/auth/transparent/a01-icon-eye-open-v1.png`
- 闭眼图标:`static/assets/modules/auth/transparent/a01-icon-eye-closed-pupil-v2.png`
- 短信验证码图标:`static/assets/modules/auth/transparent/a01-icon-sms-code-v2.png`
- 卷轴祥云主按钮:`static/assets/foundation/transparent/a01-scroll-primary-v3.png`
- 卷轴祥云微信按钮:`static/assets/foundation/transparent/a01-scroll-secondary-v3.png`
- 卷轴祥云 Toast`static/assets/foundation/transparent/a01-scroll-toast-v3.png`
- 卷轴祥云安全验证弹窗:`static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png`
- 最新可跨电脑查看的代表图:`docs/design/screens/handoff/2026-07-15/A01-current-sms-generated-412x915.png`
`static/assets/modules/auth/transparent/a01-icon-sms-code-v1.svg``a01-icon-verification-v1.png` 等未引用候选仍可能存在于工作区。当前不要顺手清理;页面样式全部完成后再单独做文档与资产清理。
@@ -91,11 +101,17 @@ git status --short
- `tests/a01-a02-ui-contract.ps1` 已要求页面引用 `a01-icon-sms-code-v2.png`,并禁止回退到勾选图标和临时 SVG。
- 最新真实运行截图已确认短信验证码行显示棕色消息气泡三点图标。
- 2026-07-16 用户已在浏览器中逐状态确认 A01 的密码默认、密码掩码、密码明文、短信默认、字段错误 Toast、协议错误和协议已勾选共 7 类视觉状态;顶部 Logo 缩小为 `184×221rpx` 后也已确认。以上子项标记为“用户视觉审核通过”。
- 用户同时决定:真实滑动验证的滑块、轨道、拼图和结果状态使用接口组件自带样式,延期到接口接入阶段审核;当前卷轴安全验证弹窗只是流程占位,不是最终视觉,不计入通过项。
- A01 整页仍缺少本轮用户最终确认;当前浅色国风主题的四档尺寸完整复核和 Android 真机复核均不能写成已通过。深色与跟随系统已由用户决定延期到下一版本,不是基础版本验收门槛。
- 旧的 `A01-password-icons-412x915.png` 实际仍是短信 Tab 且使用旧临时图标,不能当密码态证据。
### 4.4 最新资产生产决策
- 2026-07-16 用户批准将现有 Node.js/Sharp 雏形升级为混合资产流水线:Node.js 负责 manifest、编排、代码引用和 Chrome CDP 运行验证;Sharp 负责确定性裁切、等比缩放、格式和压缩;Python/Pillow(必要时 OpenCV)负责 Alpha、白边、绿边、预乘 Alpha 和像素边缘质量。唯一规范见 `docs/design/设计资产生产流水线规范.md`
- 后续页面采用混合落地:代码负责布局、文字、间距、状态和普通结构;真实位图负责复杂水墨、品牌装饰和代码无法忠实表达的视觉;可复用装饰框只有通过 H5/Android 验证后才进入九宫格方案。禁止把所有元素机械做成整张位图,也禁止用临时 CSS/SVG 伪造复杂资产。
- schema v1 仍只是旧预览雏形,统一 `fit: 'fill'`、基础尺寸/Alpha 检查和代码组合预览不足以证明资产质量。2026-07-16 已新增 manifest v2、`Pillow==12.3.0`、Node/Python 重建与质量脚本;A01 卷轴祥云四件套 v3 已通过自动质量门、静态契约、资产审计和 H5 四尺寸检查。跨电脑、Android/HBuilderX 与用户正式验收仍未完成。
- 第一试点从登录/微信双按钮扩展为用户选定并批准的“方案 2:卷轴祥云”四件套。当前实际资产为 `a01-scroll-primary-v3.png`1866×276)、`a01-scroll-secondary-v3.png`1866×300)、`a01-scroll-toast-v3.png`1770×246)和 `a01-scroll-dialog-v3.png`(1860×1560)。主/次按钮和弹窗等比接入;Toast 使用固定切片 `border-image` 自适应文案宽度。文字、微信图标和交互仍由代码承载。旧原图与 v2 试点资产保留不覆盖。试点未获 Android 与用户整页确认前,不推广到其他页面。
- 用户已决定停止扩展 PhotoshopPSD 生产链,改用“ImageGen 原始母图 + 纯代码处理与渲染”的新流水线。
- 2026-07-16 用户最新决定:不保留 `design-pipeline/node_modules/``design-pipeline/generated/``docs/design/assets/a01-vnext/candidates/``docs/design/assets/a01-vnext/review/`;这些目录既不进入 Git,也不留在当前电脑。Photoshop 历史脚本仍作为文字/实现历史保留,但新流水线不得依赖已删除的 PSD、候选图或审阅中间稿。
- 新流水线使用独立目录,以 JSON 作为页面图层、状态、尺寸和导出路径的唯一机器可读所有者;Node.js 负责组合,Sharp 负责裁切、透明处理、缩放、体积检查和 PNG 导出。
@@ -108,6 +124,7 @@ git status --short
仓库必须保留以下可复现文件:
- `docs/design/设计资产生产流水线规范.md`
- `design-pipeline/package.json`
- `design-pipeline/package-lock.json`
- `design-pipeline/manifests/a01.json`
@@ -124,6 +141,8 @@ npm.cmd --prefix design-pipeline run build:a01
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-code-pipeline-contract.ps1
```
以上是 schema v1 雏形的恢复命令。manifest v2 与 Python 质量门的换机命令已写入 `docs/design/设计资产生产流水线规范.md`;本机已验证,但新电脑必须真实安装并执行后才能宣称跨电脑复现通过。
预期输出分别包含 `A01-CODE-PIPELINE BUILD PASS``A01-CODE-PIPELINE-CONTRACT PASS`。构建结果生成在被忽略的 `design-pipeline/generated/a01/`;它只用于本机预览和验证,不得提交。需要再次腾出空间时可以删除 `design-pipeline/node_modules/``design-pipeline/generated/`,下次重新执行 `npm.cmd ci` 与构建命令即可。不得把已删除的 `candidates/``review/` 恢复为清单输入。
## 5. 下一步严格执行顺序
@@ -156,13 +175,12 @@ http://localhost:5173/#/pages/auth/a01-entry
### 第三步:无人审核期间从 T01 继续施工;用户审核时以当前审核页优先
截至 2026-07-16,以下 15 页已生成内部候选并完成聚焦检查,但仍全部为 `[~]`
截至 2026-07-16,以下 14 个活动页面已生成内部候选并完成聚焦检查,但仍全部为 `[~]`
```text
A01 登录
→ A04 注册
→ A05 重置密码
→ A06 阻断登录状态
→ G01 我的家谱
→ G03 创建家谱/录入始祖
→ G05 家谱总览
@@ -176,7 +194,7 @@ A01 登录
→ T04 新增亲属
```
下一施工页是 `T07 成员目录`,随后继续 T03、T05 等验收表顺序。若用户开始逐页审核或指出返工项,立即停下后续施工,回到用户当前指定页面;只有用户明确说“通过”后才能把该页改为 `[x]` 并冻结。
用户下一活动审核页是 `G01 我的家谱`下一施工页是 `T07 成员目录`,随后继续 T03、T05 等验收表顺序。若用户开始逐页审核或指出返工项,立即停下后续施工,回到用户当前指定页面;只有用户明确说“通过”后才能把活动页面改为 `[x]` 并冻结。A06 不在该队列内,其源码、测试和证据保留供未来恢复。
真实入口优先。开发者直达路由只用于排错,不能冒充流程可达证据。不同用户身份、空态、加载、失败、权限、审核中、被拒绝、已退出/移除等状态必须在同一页面验收项下覆盖。
@@ -265,9 +283,9 @@ node scripts/capture-chrome-page.js `
2026-07-16 已取得的内部验证证据如下(均不等于用户验收或 Android 通过):
- 静态合同通过:A01/A02、A04、A05、A06、G01、G03、G05、G06、G08-G10、G11-G12、T01、T03-T08,以及截图脚本和整页视觉合同。
- H5 运行时 smoke 分别通过:A01、A04、A05、A06、G01、G03、G05、G06、G08-G10、G11-G12、T01、T03-T08。G05 与 T03-T08 在串联 CDP 测试中曾因页面目标重建瞬时失败,独立重跑均已通过;不要把该诊断噪声写成功能缺陷,也不要隐瞒独立复核方式。
- 实际路由统计为 53`git diff --check` 退出码为 0,仅有 CRLF 警告
- 静态合同通过:A01/A02、A04、A05、A06 封存合同、G01、G03、G05、G06、G08-G10、G11-G12、T01、T03-T08,以及截图脚本和整页视觉合同。
- H5 运行时 smoke 分别通过:A01、A04、A05、G01、G03、G05、G06、G08-G10、G11-G12、T01、T03-T08A06 因路由封存明确输出 `SKIP archived route`。G05 与 T03-T08 在串联 CDP 测试中曾因页面目标重建瞬时失败,独立重跑均已通过;不要把该诊断噪声写成功能缺陷,也不要隐瞒独立复核方式。
- 实际活动路由统计为 52A06 源码保留但未注册
- 长期代表图集中在 `docs/design/screens/handoff/2026-07-16/`,并已登记到 `docs/design/视觉证据索引.md`;这些仍是 H5 候选,Android/HBuilderX 真机或模拟器复核尚未完成。
- 仓库交接截图体积合同未通过,具体数据与处置边界见第 12 节。
@@ -316,8 +334,17 @@ GPT 不操作 Git。用户切换电脑前应自行确认以下内容已纳入版
`docs/design/screens/runtime/``tmp/` 和本机 Chrome 调试目录不能作为换机后的唯一资料来源。最终运行资产必须在 `static/assets/`,长期代表证据必须在 `docs/design/screens/handoff/`,决策与下一步必须写进本文件或 `docs/验收规划.md`
项目全部页面、接口与 Android 复核结束时,必须把本文件整理为最终交接文档,至少记录:最终活动路由与验收数量、A06 等封存页面及恢复步骤、延期到接口阶段的问题、Android/HBuilderX 验证结论、长期证据索引、遗留风险,以及交接当时已有修改和未跟踪内容。A06 恢复时必须先由产品确认独立阻断页仍有必要,再恢复路由、接入真实登录结果与恢复服务,并重新完成四档 H5、Android/HBuilderX 和用户审核。
## 12. 仓库体积与本地缓存规则
### 2026-07-16 下班前资产与清理交接
- G01 已保存 A/B/C 三个列表背景候选母版;候选尚未由用户选择,也未接入页面。页面“上方固定、下方列表独立滚动”的结构问题留待下一轮继续讨论。
- 跨电脑重建入口为 `npm.cmd --prefix design-pipeline run build:g01-background-candidates`,完整环境、依赖、输出哈希与可复现边界见 `docs/design/G01_列表背景候选与换机重建.md`
- 上传前审计只删除了两张无任何引用且已被新版证据替代的 2026-07-15 A01 旧全页截图,共减少 3,590,742 字节;精确清单见 `docs/design/上传前文件清理审计_2026-07-16.md`
- 清理后 handoff 专项目录仍为 50 个文件、29,887,097 字节,继续超过 35 个文件/20MB 合同。未删除其他被引用历史证据,未放宽阈值,归档策略仍需用户决定。
2026-07-16 推送失败根因已处理:原本地提交包含约 **229,085,446 字节**新 Blob,混入嵌套 `node_modules`、生成预览、4 个大型 PSD 和候选中间 PNG,压缩上传包达到 81.39MiB 后被远端断开。清单改为正式运行资产自包含后,上述四类目录已从提交和本机删除;最新待推送提交的新 Blob 总量复核约 **20,518,032 字节**。旧大对象可能暂时存在于本机 `.git` reflog/对象库,但不再由最新提交引用,也不会随本次正常推送上传;不要为清理它们执行 reset 或手工删除 `.git` 对象。
2026-07-16 最新检查:`tests/repository-handoff-size-contract.ps1` **未通过**,报告 `too many handoff screenshots: 217`(该次检查发生在新增 T04 代表图之前)。该脚本当前统计了 `docs/design/screens/` 下包含已忽略 runtime 在内的全部截图;最终单独统计长期交接目录 `docs/design/screens/handoff/`**52 个文件、33,477,839 字节(约 31.93MB**,同样超过合同的 35 个文件/20MB门槛。本轮遵守用户禁令,没有删除、覆盖或清理任何历史证据、候选资产、runtime、tmp 或测试,也没有为了变绿而擅自放宽阈值。换机后应先让用户决定历史证据的归档策略,再单独处理体积合同;在此之前必须如实保留失败状态。
+1 -1
View File
@@ -1,6 +1,6 @@
# 家谱 APP 总规划
> **2026-07-15 状态覆盖说明:** 本文件保留旧阶段范围与历史记录,其中“54 条路由、7 页已验收/冻结”的状态已经失效。当前实际为 `pages.json` 53最终路由、`docs/验收规划.md` 中 53 页全部 `[~]` 待审核、0 页冻结;准确停点与下一步以 `docs/交接记录.md` 和 `docs/验收规划.md` 为准。
> **2026-07-16 状态覆盖说明:** 本文件保留旧阶段范围与历史记录,其中旧路由和验收数字已经失效。当前实际为 `pages.json` 52活动路由、`docs/验收规划.md` 中 52 页全部 `[~]` 待审核、0 页冻结;A06 已封存并保留实现。准确停点与下一步以 `docs/交接记录.md` 和 `docs/验收规划.md` 为准。
> 电脑或 GPT 更换时,先阅读:[项目交接记录](交接记录.md)。
+67 -64
View File
@@ -2,7 +2,7 @@
> 更新日期:2026-07-15
> 适用范围:Android `uni-app` 用户端
> 当前实现:`pages.json` 已收敛为 53最终路由;A02 已并入 A01。历史上的 7 个验收结果仅作为旧视觉证据,不计入本轮完成状态。
> 当前实现:`pages.json` 已收敛为 52活动路由;A02 已并入 A01A06 已从活动路由与验收队列封存。历史上的 7 个验收结果仅作为旧视觉证据,不计入本轮完成状态。
> 本文件只管理逐页视觉、页面状态外观、弹窗与加载样式、手机适配和表现质量;不替代 `docs/规划.md` 对项目范围和最终路由的管理,也不承担接口与业务功能验收。功能验收规则后续单独编写。
## 1. 标记与审核规则
@@ -19,7 +19,7 @@
1. 页面在真实运行环境中打开;流程页优先从上游可见入口进入,尚未实现入口时明确标记 `⚠`,不能冒充流程设计完整。
2. 正常、加载、空、失败、权限、校验和成功等适用状态的视觉呈现均已检查;本阶段只验状态外观,不验证后台结果真伪。
3. 基础版本只验收当前浅色国风主题,并在 320×568、360×640、360×800、412×915 四个尺寸确认无溢出、裁切、遮挡或错位;深色与跟随系统整体延期到下一版本,不作为本轮 53 页通过条件。
3. 基础版本只验收当前浅色国风主题,并在 320×568、360×640、360×800、412×915 四个尺寸确认无溢出、裁切、遮挡或错位;深色与跟随系统整体延期到下一版本,不作为本轮 52 页通过条件。
4. 默认系统字号和约 1.3 倍系统字号均可完整操作;关键按钮、表单错误、姓名、关系和操作结果不得截断,空间不足时允许合理换行或滚动,不得强行缩小文字或遮挡主操作,并使用超长族谱名、生僻姓名完成边界检查。
5. 状态栏、底部手势区、软键盘、系统返回和长文本不会破坏页面。
6. 点击反馈、加载、滚动、动画和图片解码在当前静态/Mock 数据下无明显卡顿。
@@ -35,7 +35,7 @@
接口尚未提供不阻止基础页面样式标记 `[x]`。微信登录、行为验证、邀请码、真实审核、上传、支付、权限控制和数据提交均留到后续功能验收;当前可以审核其入口、容器、加载、成功、失败和禁用状态是否好看,但不得把静态/Mock 画面描述为功能已通过。
本轮不沿用任何页面的历史完成标记。A01、A04、A05、A06、G01、G03 也必须重新完成页面结构、视觉状态、兼容性、弹窗加载样式和表现质量检查;用户重新确认后才能标记 `[x]`
本轮不沿用任何页面的历史完成标记。A01、A04、A05、G01、G03 也必须重新完成页面结构、视觉状态、兼容性、弹窗加载样式和表现质量检查;用户重新确认后才能标记 `[x]`A06 已封存,不参与本轮活动页面统计。
审核证据默认每页只展示一张 360×800 代表图;复杂状态合成一张联系表,不连续展示多张前后对比图。其余尺寸在内部完成检查,只有发现问题或用户要求时才展开。
@@ -52,11 +52,11 @@
- A01 首次安装默认展示密码登录;用户主动切换密码/短信方式后,本机只记录上次选择的登录方式,下次打开恢复该方式。不得保存密码;手机号只允许按后续隐私与安全规则合规保存或脱敏展示。微信登录始终保持为独立入口,不参与密码/短信方式记忆。
- A04 注册成功后直接建立登录状态并进入 G01“我的家谱”,不得要求用户返回 A01 重复输入账号密码;注册接口能否返回有效登录凭证在后续功能验收核对,当前页面样式验收先覆盖提交中、成功过渡和进入 G01 的视觉连续性。
- A05 重置密码成功后不自动登录:先显示短暂的自定义成功结果,再返回 A01,自动保留刚才使用的手机号、切换到密码登录并聚焦密码框,让用户使用新密码完成登录;不得清空手机号或要求重新寻找登录方式。
- A01 内部直接处理密码错误、验证码错误和手机号未注册,使用就近校验,不跳转 A06;微信授权取消仍留在 A01,视为用户取消而非登录失败。A06 只承接账号冻结、停用、风险限制或其他无法继续登录的阻断性状态,必须说明原因、影响范围和明确恢复入口,不得成为普通错误的汇总页。
- A01 内部直接处理密码错误、验证码错误和手机号未注册,使用就近校验;微信授权取消仍留在 A01,视为用户取消而非登录失败。A06 已封存,不再作为当前登录结果入口。若未来恢复,只承接账号冻结、停用、风险限制或其他无法继续登录的阻断性状态,必须说明原因、影响范围和明确恢复入口,不得成为普通错误的汇总页。
- A01 在 320×568 最小验收尺寸下允许自然纵向滚动,不为追求全部核心入口同屏而压缩品牌头、表单、字号或有效触控区;必须保证首屏层级清楚、滚动顺畅、无横向溢出,底部协议和主操作可完整滚动到达。
- A01 未勾选协议时采用页内校验:协议区域轻微震动并高亮,下方显示明确错误文案;不弹原生 Toast 或确认弹窗,不跳转。用户勾选后错误样式立即消失。
- A02 从最终路由和验收表移除,不保留兼容入口;A04、A05、A06 的登录返回统一指向 A01。实际代码收敛完成前,`pages.json` 的 54 条是实施现状,不是最终验收数量
- A01、A04、A05 共用同一行为验证弹层,但触发时机不同。A01 登录在账号信息、协议等本地校验通过并点击“登录”后弹出,验证成功后自动继续登录;A05 忘记密码在用户请求发送短信验证码前弹出,验证成功后自动发送短信,重置提交时不重复验证。A04 注册当前因短信注册接口尚未提供,临时按 A01 方式在完整填写并点击“注册”后弹出;注册短信接口提供即为临时契约的移除条件,届时必须迁移为“发送短信验证码前验证”,并删除注册提交前的旧触发路径,不保留双重验证。用户关闭或验证失败时必须保留原表单内容。当前页面样式阶段只审核弹层初始、拖动、成功、失败和关闭状态,不审核真实滑动轨迹、`validToken` 或接口自动续接。原始 TAC 运行库保存在 `static/vendor/tac/`,留待功能阶段使用。
- A02 从最终路由和验收表移除,不保留兼容入口;A04、A05 的登录返回统一指向 A01。A06 源码仍保留返回 A01 的逻辑,但已从 `pages.json` 和活动验收表移除
- A01、A04、A05 后续接入同一真实滑动验证组件,但触发时机不同。A01 登录在账号信息、协议等本地校验通过并点击“登录”后触发,验证成功后自动继续登录;A05 忘记密码在用户请求发送短信验证码前触发,验证成功后自动发送短信,重置提交时不重复验证。A04 注册当前因短信注册接口尚未提供,临时按 A01 方式在完整填写并点击“注册”后触发;注册短信接口提供即为临时契约的移除条件,届时必须迁移为“发送短信验证码前验证”,并删除注册提交前的旧触发路径,不保留双重验证。用户关闭或验证失败时必须保留原表单内容。2026-07-16 用户决定滑块、轨道、拼图及成功/失败等内部视觉采用接入组件的自带样式,延期到接口阶段结合真实组件审核;当前卷轴验证弹窗仅为流程占位,不作为最终视觉或用户验收证据。原始 TAC 运行库保存在 `static/vendor/tac/`,留待功能阶段使用。
- 短信验证码按钮只设计两种稳定状态:“获取验证码”和“Ns 后重试”倒计时。发送请求期间仅在原按钮内部显示短暂进度并阻止重复点击,不新增独立按钮形态;发送失败立即恢复可发送状态并在按钮附近就近说明原因;频繁限制有明确剩余秒数时进入倒计时,没有剩余秒数时保持可发送并显示限制说明。倒计时的真实到期时间、前后台恢复和接口限流结果归入后续功能验收。
- 登录成功后统一进入 G01“我的家谱”,不直接跳入上次浏览的家谱。
- APP 启动时按登录状态分流:首次打开或没有有效凭证进入 A01;已登录且凭证有效时直接进入 G01,不重复显示登录页;凭证过期时先回到 A01,再显示项目自定义单按钮信息对话框,标题“登录状态已过期”,正文“为保障账号安全,请重新登录”,按钮“重新登录”,点击后关闭并聚焦登录表单,不使用 Toast 或原生 `uni.showModal`;主动退出登录后清除登录凭证,但可保留用户上次选择的密码/短信登录方式。凭证有效性、过期检测和启动恢复归入后续功能验收。
@@ -172,106 +172,109 @@
| `[ ]` | C19 | Android 真机性能 | 以 4GB 内存中低端 Android 为底线,真机复核启动、键盘、500 条长列表、图片解码、页面切换和系统返回手势,无肉眼可见的持续掉帧 |
| `[ ]` | C20 | 运行与构建洁净度 | 无控制台错误、资源 404、未处理 Promise、明显重复渲染和构建错误 |
## 5. 53 页真实用户流程验收表
## 5. 52 页真实用户流程验收表
### 5.1 首次打开与认证
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 1 | A01 登录 | APP 首次打开;首次默认密码登录、后续恢复上次密码/短信方式,品牌区、微信登录入口、注册、忘记密码、协议未勾选/已勾选、空输入、错误提示、完整输入后点击登录再显示行为验证、按钮加载、键盘遮挡;320×568 保持正常比例并允许自然纵向滚动 |
| `[~]` | 2 | A04 注册账号 | A01→注册;协议、手机号错误、密码不一致、注册中、成功/失败页面样式;当前临时在完整填写并点击注册后显示行为验证,注册短信接口提供后迁移为发送验证码前验证;注册成功后自动建立登录状态并进入 G01 |
| `[~]` | 3 | A05 重置密码 | A01→忘记密码;手机号、发送短信前行为验证、验证码、新密码、两次不一致、重设中、成功/失败页面样式;成功后返回 A01,保留手机号、切到密码登录并聚焦密码框,不自动登录 |
| `[~]` ⚠ | 4 | A06 登录状态 | A01 登录结果→阻断状态;仅覆盖账号冻结、停用、风险限制等无法继续登录的状态,必须展示原因、影响范围和恢复入口;普通密码/验证码错误、未注册及微信取消均留在 A01;当前缺少真实结果跳转 |
| `[~]` | 1 | A01 登录 | **2026-07-16 用户已确认通过除真实滑动验证外的 7 类 H5 视觉状态**:密码默认密码掩码、密码明文、短信默认、字段错误 Toast、协议错误、协议已勾选;品牌区、双按钮和 Logo 缩小版同时通过本轮审核。真实滑动验证采用接口组件自带样式,待接入后单独审核;Android/HBuilderX 复核前页面不冻结 |
| `[~]` | 2 | A04 注册账号 | **2026-07-16 用户已确认通过当前可实现的 8 类 H5 视觉状态**:默认空表单、空提交全量错误、手机号单项错误、两次密码不一致、协议未勾选、填写完成待提交、滑动验证待接入 Toast、协议入口 Toast;Logo、全局楷体、标题分隔、朱砂主按钮和浅色卷轴 Toast 同时通过本轮审核。真实滑动验证采用接口组件自带样式,注册成功/失败和进入 G01 的真实链路待接口阶段审核;Android/HBuilderX 复核前页面不冻结 |
| `[~]` | 3 | A05 重置密码 | A01→忘记密码;**2026-07-16 用户已确认通过当前 7 类 H5 视觉状态**:默认空表单、空提交全量错误、手机号单项错误、滑动验证待接入 Toast、两次新密码不一致、密码重设成功卷轴、返回 A01 密码登录画面;Logo、全局楷体、标题分隔、两处朱砂按钮和 Toast 同时通过本轮审核。用户决定“成功后回填手机号并聚焦密码框”不阻塞当前样式验收,随真实重置接口链路接入时解决;真实滑动验证、真实短信/重设结果及 Android/HBuilderX 仍待复核,页面保持 `[~]`、不冻结 |
### 5.2 登录后进入我的家谱并完成首次分流
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 5 | G01 我的家谱 | 登录成功→同页多状态;空态保留搜索/邀请码/创建三入口,以搜索为主、邀请码快捷、创建为次并提示先确认无现有家谱;三分组列表、顶部当前项切换、上次选择恢复、待录入始祖/继续完善、审核中、被拒绝、已退出/被移除、加载、失败、添加家谱弹层、通知入口 |
| `[~]` | 6 | G03 创建家谱 | G01 空态或“添加家谱”→创建独立家谱;姓氏、谱名、堂号、所在地、访问规则,创建前提示搜索潜在已有家谱;覆盖必填校验、创建中、失败和已创建但始祖未完成的中断恢复;成功后直接成为创建者→录入始祖→进入 G05 总览,不直接跳 T01;下属支系创建待接口提供上级谱与权限字段 |
| `[~]` | 7 | G06 加入家谱 | G01 空态或“添加家谱”→加入;搜索/邀请码双模式、地区与谱名筛选;结果卡展示谱名、姓氏、地区、堂号、所属上级谱、当前支系、管理/认证信息、成员规模和更新时间,并覆盖未加入、已加入、审核中、被拒绝、已退出/移除、我创建的六种操作状态;邀请码验证、初始、加载、结果、无结果、无效/过期邀请码、失败、清空输入 |
| `[~]` | 8 | G08 关系确认与入谱 | G06 选定家谱→填写真实姓名和以已知长辈为参照的文字关系;搜索态显示申请说明/提交申请并在成功后进入 G09,邀请码态显示可选补充信息/确认加入并明确无需审核,成功后返回 G01 选中新家谱;覆盖校验、重复提交、提交中、成功/失败、返回保护;结构化关系待接口提供后迁移 |
| `[~]` | 9 | G09 我的申请 | G08 搜索来源提交成功→申请记录;待审、通过、拒绝、修改后重新提交、撤回、空、失败;邀请码来源不进入本页 |
| `[~]` | 10 | G05 家谱总览 | G06 未加入结果卡→公开预览,或 G01 顶部可用家谱大卡片/G09 已通过→成员总览;公开态仅展示身份、来源、地区、堂号、支系、公开说明和申请入口;成员态展示首屏三层家谱信息;覆盖加载、正常、空、失败、所有者/成员入口差异、无权限,世系图降为次级入口 |
| `[~]` | 4 | G01 我的家谱 | 登录成功→同页多状态;空态保留搜索/邀请码/创建三入口,以搜索为主、邀请码快捷、创建为次并提示先确认无现有家谱;三分组列表、顶部当前项切换、上次选择恢复、待录入始祖/继续完善、审核中、被拒绝、已退出/被移除、加载、失败、添加家谱弹层、通知入口 |
| `[~]` | 5 | G03 创建家谱 | G01 空态或“添加家谱”→创建独立家谱;姓氏、谱名、堂号、所在地、访问规则,创建前提示搜索潜在已有家谱;覆盖必填校验、创建中、失败和已创建但始祖未完成的中断恢复;成功后直接成为创建者→录入始祖→进入 G05 总览,不直接跳 T01;下属支系创建待接口提供上级谱与权限字段 |
| `[~]` | 6 | G06 加入家谱 | G01 空态或“添加家谱”→加入;搜索/邀请码双模式、地区与谱名筛选;结果卡展示谱名、姓氏、地区、堂号、所属上级谱、当前支系、管理/认证信息、成员规模和更新时间,并覆盖未加入、已加入、审核中、被拒绝、已退出/移除、我创建的六种操作状态;邀请码验证、初始、加载、结果、无结果、无效/过期邀请码、失败、清空输入 |
| `[~]` | 7 | G08 关系确认与入谱 | G06 选定家谱→填写真实姓名和以已知长辈为参照的文字关系;搜索态显示申请说明/提交申请并在成功后进入 G09,邀请码态显示可选补充信息/确认加入并明确无需审核,成功后返回 G01 选中新家谱;覆盖校验、重复提交、提交中、成功/失败、返回保护;结构化关系待接口提供后迁移 |
| `[~]` | 8 | G09 我的申请 | G08 搜索来源提交成功→申请记录;待审、通过、拒绝、修改后重新提交、撤回、空、失败;邀请码来源不进入本页 |
| `[~]` | 9 | G05 家谱总览 | G06 未加入结果卡→公开预览,或 G01 顶部可用家谱大卡片/G09 已通过→成员总览;公开态仅展示身份、来源、地区、堂号、支系、公开说明和申请入口;成员态展示首屏三层家谱信息;覆盖加载、正常、空、失败、所有者/成员入口差异、无权限,世系图降为次级入口 |
### 5.3 所有者管理家谱
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 11 | G10 入谱审核 | G05/G01/N01→审核;列表、空、失败、通过/拒绝确认、无权限 |
| `[~]` | 12 | G11 家谱设置 | G05→设置;名称校验、公开范围、访问说明、保存中、成功/失败、无权限 |
| `[~]` | 13 | G12 字辈诗 | G01/G05→字辈诗;列表、空、编辑、校验、保存失败、无权限 |
| `[~]` | 10 | G10 入谱审核 | G05/G01/N01→审核;列表、空、失败、通过/拒绝确认、无权限 |
| `[~]` | 11 | G11 家谱设置 | G05→设置;名称校验、公开范围、访问说明、保存中、成功/失败、无权限 |
| `[~]` | 12 | G12 字辈诗 | G01/G05→字辈诗;列表、空、编辑、校验、保存失败、无权限 |
### 5.4 世系树与成员
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 14 | T01 世系树 | G05 新建引导卡或常规世系入口→世系树;G03 始祖保存不得直接跳入;覆盖加载、树、空、失败、横向提示、节点选中 |
| `[~]` | 15 | T04 新增亲属 | T01→新增;首位成员/普通亲属、关系选择、必填、保存中、结果 |
| `[~]` | 16 | T07 成员目录 | T01→成员目录;列表、搜索、无结果、失败、成员选择 |
| `[~]` | 17 | T03 成员档案 | T01/T07→成员;正常、失败、隐私字段、编辑入口 |
| `[~]` | 18 | T05 编辑成员 | T03→编辑;字段校验、保存中、成功/失败、无权限 |
| `[~]` | 19 | T06 编辑关系 | T01→编辑关系;冲突、循环关系、说明弹窗、保存失败 |
| `[~]` ⚠ | 20 | T08 成员状态 | 应由 T03 进入;去世、隐私隐藏、无权限;当前无真实入口 |
| `[~]` | 13 | T01 世系树 | G05 新建引导卡或常规世系入口→世系树;G03 始祖保存不得直接跳入;覆盖加载、树、空、失败、横向提示、节点选中 |
| `[~]` | 14 | T04 新增亲属 | T01→新增;首位成员/普通亲属、关系选择、必填、保存中、结果 |
| `[~]` | 15 | T07 成员目录 | T01→成员目录;列表、搜索、无结果、失败、成员选择 |
| `[~]` | 16 | T03 成员档案 | T01/T07→成员;正常、失败、隐私字段、编辑入口 |
| `[~]` | 17 | T05 编辑成员 | T03→编辑;字段校验、保存中、成功/失败、无权限 |
| `[~]` | 18 | T06 编辑关系 | T01→编辑关系;冲突、循环关系、说明弹窗、保存失败 |
| `[~]` ⚠ | 19 | T08 成员状态 | 应由 T03 进入;去世、隐私隐藏、无权限;当前无真实入口 |
### 5.5 家族动态与内容
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 21 | F01 家族首页 | 底部“家族”Tab;加载、列表、空、失败、发布及内容入口 |
| `[~]` | 22 | F02 发布动态 | F01→发布;空内容、键盘、提交中、成功/失败、重复提交 |
| `[~]` | 23 | F03 动态详情 | F01 动态卡→详情;正文、评论、操作结果、内容失效 |
| `[~]` | 24 | F04 谱文列表 | F01→谱文;分类、列表、空、失败、新建入口 |
| `[~]` ⚠ | 25 | F05 谱文详情 | 应由 F04 文章进入;详情、收藏、失效内容;当前卡片只弹提示 |
| `[~]` ⚠ | 26 | F06 编辑谱文 | 应由 F04 新建/F05 编辑进入;草稿、校验、保存、失败 |
| `[~]` | 27 | F07 相册列表 | F01→相册;列表、空、失败、新建相册 |
| `[~]` ⚠ | 28 | F08 相册详情 | 应由 F07 相册进入;照片墙、空相册、预览、失效;当前无跳转 |
| `[~]` ⚠ | 29 | F09 上传照片 | 应由 F08→添加照片;权限、选择、上传进度、失败重试 |
| `[~]` ⚠ | 30 | F10 家族视频 | 应由 F01 内容入口进入;未开放、空、列表、失败;当前无入口 |
| `[~]` | 20 | F01 家族首页 | 底部“家族”Tab;加载、列表、空、失败、发布及内容入口 |
| `[~]` | 21 | F02 发布动态 | F01→发布;空内容、键盘、提交中、成功/失败、重复提交 |
| `[~]` | 22 | F03 动态详情 | F01 动态卡→详情;正文、评论、操作结果、内容失效 |
| `[~]` | 23 | F04 谱文列表 | F01→谱文;分类、列表、空、失败、新建入口 |
| `[~]` ⚠ | 24 | F05 谱文详情 | 应由 F04 文章进入;详情、收藏、失效内容;当前卡片只弹提示 |
| `[~]` ⚠ | 25 | F06 编辑谱文 | 应由 F04 新建/F05 编辑进入;草稿、校验、保存、失败 |
| `[~]` | 26 | F07 相册列表 | F01→相册;列表、空、失败、新建相册 |
| `[~]` ⚠ | 27 | F08 相册详情 | 应由 F07 相册进入;照片墙、空相册、预览、失效;当前无跳转 |
| `[~]` ⚠ | 28 | F09 上传照片 | 应由 F08→添加照片;权限、选择、上传进度、失败重试 |
| `[~]` ⚠ | 29 | F10 家族视频 | 应由 F01 内容入口进入;未开放、空、列表、失败;当前无入口 |
### 5.6 人物、礼仪与档案
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` ⚠ | 31 | R01 人物录 | 应由家族档案入口进入;列表、搜索、空、失败、新建 |
| `[~]` ⚠ | 32 | R02 人物详情 | 应由 R01 人物进入;详情、编辑、隐私、内容失效 |
| `[~]` ⚠ | 33 | R03 贺礼列表 | 应由家族工具进入;列表、空、失败、新增 |
| `[~]` ⚠ | 34 | R04 编辑贺礼 | 应由 R03 进入;新增/编辑、校验、保存、删除确认 |
| `[~]` | 35 | R05 礼仪列表 | F01→礼仪;列表、空、失败、新建 |
| `[~]` ⚠ | 36 | R06 礼仪详情 | 应由 R05 活动进入;详情、编辑、活动失效 |
| `[~]` ⚠ | 37 | R07 编辑礼仪 | 应由 R05 新建/R06 编辑进入;校验、保存、失败 |
| `[~]` ⚠ | 38 | R08 成长日志 | 应由人物/成员档案进入;时间轴、空、失败、新增 |
| `[~]` ⚠ | 39 | R09 人生事 | 应由人物/成员档案进入;时间轴、空、失败、新增 |
| `[~]` | 40 | R10 家族备忘 | F01→备忘;列表、空、失败、新增 |
| `[~]` ⚠ | 41 | R11 功德记录 | 应由家族档案入口进入;列表、空、失败、新增 |
| `[~]` ⚠ | 30 | R01 人物录 | 应由家族档案入口进入;列表、搜索、空、失败、新建 |
| `[~]` ⚠ | 31 | R02 人物详情 | 应由 R01 人物进入;详情、编辑、隐私、内容失效 |
| `[~]` ⚠ | 32 | R03 贺礼列表 | 应由家族工具进入;列表、空、失败、新增 |
| `[~]` ⚠ | 33 | R04 编辑贺礼 | 应由 R03 进入;新增/编辑、校验、保存、删除确认 |
| `[~]` | 34 | R05 礼仪列表 | F01→礼仪;列表、空、失败、新建 |
| `[~]` ⚠ | 35 | R06 礼仪详情 | 应由 R05 活动进入;详情、编辑、活动失效 |
| `[~]` ⚠ | 36 | R07 编辑礼仪 | 应由 R05 新建/R06 编辑进入;校验、保存、失败 |
| `[~]` ⚠ | 37 | R08 成长日志 | 应由人物/成员档案进入;时间轴、空、失败、新增 |
| `[~]` ⚠ | 38 | R09 人生事 | 应由人物/成员档案进入;时间轴、空、失败、新增 |
| `[~]` | 39 | R10 家族备忘 | F01→备忘;列表、空、失败、新增 |
| `[~]` ⚠ | 40 | R11 功德记录 | 应由家族档案入口进入;列表、空、失败、新增 |
### 5.7 消息
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 42 | N01 消息中心 | G01/M01→消息;加载、未读、已读、全部已读、空、失败、审核跳转 |
| `[~]` ⚠ | 43 | N02 消息详情 | 应由 N01 消息卡进入;详情、已读、失效消息、业务跳转;当前点击只改已读 |
| `[~]` | 41 | N01 消息中心 | G01/M01→消息;加载、未读、已读、全部已读、空、失败、审核跳转 |
| `[~]` ⚠ | 42 | N02 消息详情 | 应由 N01 消息卡进入;详情、已读、失效消息、业务跳转;当前点击只改已读 |
### 5.8 个人中心
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 44 | M01 我的首页 | 底部“我的”Tab;资料、提醒、菜单、失败状态 |
| `[~]` | 45 | M02 个人资料 | M01→资料;头像权限、必填、保存中、成功/失败 |
| `[~]` | 46 | M03 账号与安全 | M01→安全;密码、手机号、设备、异常提醒 |
| `[~]` ⚠ | 47 | M04 修改密码 | 应由 M03 进入;旧密码、新密码、不一致、成功/失败 |
| `[~]` ⚠ | 48 | M05 修改手机号 | 应由 M03 进入;验证码、错误号码、成功/失败 |
| `[~]` | 49 | M06 帮助中心 | M01→帮助;常见问题、无结果、联系入口 |
| `[~]` ⚠ | 50 | M07 意见反馈 | 应由 M06 进入;校验、提交中、成功/失败 |
| `[~]` ⚠ | 51 | M08 应用推广 | 应由 M01 进入;分享权限、生成海报、取消、失败 |
| `[~]` ⚠ | 52 | M09 VIP 与订单 | 应由 M01 进入;无订单、权益、订单列表、加载失败 |
| `[~]` | 53 | M10 设置与关于 | M01→设置;协议、隐私、版本、退出确认/取消;基础版本不展示外观模式入口,深色与跟随系统延期到下一版本 |
| `[~]` | 43 | M01 我的首页 | 底部“我的”Tab;资料、提醒、菜单、失败状态 |
| `[~]` | 44 | M02 个人资料 | M01→资料;头像权限、必填、保存中、成功/失败 |
| `[~]` | 45 | M03 账号与安全 | M01→安全;密码、手机号、设备、异常提醒 |
| `[~]` ⚠ | 46 | M04 修改密码 | 应由 M03 进入;旧密码、新密码、不一致、成功/失败 |
| `[~]` ⚠ | 47 | M05 修改手机号 | 应由 M03 进入;验证码、错误号码、成功/失败 |
| `[~]` | 48 | M06 帮助中心 | M01→帮助;常见问题、无结果、联系入口 |
| `[~]` ⚠ | 49 | M07 意见反馈 | 应由 M06 进入;校验、提交中、成功/失败 |
| `[~]` ⚠ | 50 | M08 应用推广 | 应由 M01 进入;分享权限、生成海报、取消、失败 |
| `[~]` ⚠ | 51 | M09 VIP 与订单 | 应由 M01 进入;无订单、权益、订单列表、加载失败 |
| `[~]` | 52 | M10 设置与关于 | M01→设置;协议、隐私、版本、退出确认/取消;基础版本不展示外观模式入口,深色与跟随系统延期到下一版本 |
### 5.9 封存页面(不计入 52 页活动验收统计)
| 页面 | 封存决定 | 保留内容 | 恢复条件 |
| --- | --- | --- | --- |
| A06 登录状态 | 2026-07-16 用户确认当前样式没有问题,但当前产品流程暂时不用该页;采用“从 `pages.json` 和活动验收表移除”的封存方式 | 保留 `pages/auth/a06-auth-status.vue`、静态合同、运行测试、设计记录与截图证据;不删除、不冻结,也不标记为 `[x]` | 产品重新需要独立阻断登录页时,将路由重新加入 `pages.json`,接入真实登录结果与恢复服务,再执行四档 H5、Android/HBuilderX 和用户逐态审核 |
## 6. 当前视觉流程断点与后续功能依赖
除 APP 启动页 A01 外,目前有 22 个最终页面没有被其他页面真实跳转到:
- A06。
- T08。
- F05、F06、F08、F09、F10。
- R01、R02、R03、R04、R06、R07、R08、R09、R11。
@@ -317,8 +320,8 @@
## 9. 全量结束条件
- C01C20 全部为 `[x]`
- A02 已并入 A01`pages.json` 最终路由数为 53,且不存在旧入口、重定向或兼容分支
- 53 个页面全部为 `[x]`,不存在 `[~]``[!]` 或未解释的 `⚠`
- A02 已并入 A01A06 已封存,`pages.json` 活动路由数为 52,且不存在 A02 旧入口或 A06 活动入口
- 52活动页面全部为 `[x]`,不存在 `[~]``[!]` 或未解释的 `⚠`
- 每个页面的适用状态子项全部通过,不以单张正常态截图代替状态验收。
- 22 个现有流程断点均已通过真实入口和返回路径验证。
- 40 处原生 UniApp 提示均已替换,运行页面不出现原生弹窗或 Toast 样式。
-6
View File
@@ -19,12 +19,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/auth/a06-auth-status",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/genealogy/g01-my-genealogies",
"style": {
+20 -28
View File
@@ -8,7 +8,7 @@
<view class="login-content">
<view class="login-heading">
<text class="login-title">登录家谱</text>
<image class="title-divider" src="/static/assets/modules/auth/opaque/a01-title-divider-v2.png" mode="scaleToFill" />
<image class="title-divider" src="/static/assets/modules/auth/transparent/a01-vnext-divider-v1.png" mode="aspectFit" />
</view>
<view class="login-tabs">
@@ -42,7 +42,7 @@
</view>
<view class="login-submit" hover-class="button-pressed" @click="submitLogin">
<image class="button-skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<image class="button-skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text class="login-submit__copy">登录</text>
</view>
@@ -55,7 +55,7 @@
</view>
<view class="wechat-login" hover-class="button-pressed" @click="prepareWechatLogin">
<image class="button-skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<image class="button-skin" src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="aspectFit" />
<view class="wechat-login__content">
<image class="wechat-icon" src="/static/assets/foundation/transparent/auth-wechat.png" mode="aspectFit" />
<text>微信登录</text>
@@ -85,7 +85,7 @@
<!-- 当前只验收自定义容器外观真实行为验证数据留到接口阶段接入 -->
<view v-if="verificationVisible" class="verification-layer" @click="closeVerification">
<view class="verification-dialog" @click.stop>
<image class="verification-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<image class="verification-dialog__skin" src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png" mode="aspectFit" />
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy">{{ verificationPurpose }}前需要完成行为验证</text>
@@ -99,7 +99,6 @@
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<image class="feedback-toast__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
@@ -248,8 +247,8 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
z-index: 1;
top: 64rpx;
left: 50%;
width: 220rpx;
height: 264rpx;
width: 184rpx;
height: 221rpx;
transform: translateX(-50%);
pointer-events: none;
}
@@ -263,16 +262,15 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
}
.login-heading {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 118rpx;
height: 142rpx;
}
.login-title {
color: #9f170f;
font-family: STSong, SimSun, serif;
font-size: 82rpx;
font-weight: 700;
letter-spacing: 8rpx;
@@ -280,12 +278,10 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
}
.title-divider {
position: absolute;
bottom: -4rpx;
left: 50%;
width: 286rpx;
height: 32rpx;
transform: translateX(-50%);
flex: none;
width: 360rpx;
height: 60rpx;
filter: brightness(0.68) saturate(1.5) contrast(1.2);
}
.login-tabs {
@@ -303,7 +299,6 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
justify-content: center;
min-height: 88rpx;
color: #80684f;
font-family: STSong, SimSun, serif;
font-size: 40rpx;
letter-spacing: 2rpx;
}
@@ -430,7 +425,6 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
position: relative;
z-index: 1;
color: #fffaf1;
font-family: STSong, SimSun, serif;
font-size: 52rpx;
letter-spacing: 8rpx;
}
@@ -440,7 +434,6 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
align-items: center;
height: 100rpx;
color: #9f6a27;
font-family: STSong, SimSun, serif;
font-size: 32rpx;
white-space: nowrap;
}
@@ -466,7 +459,6 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
.wechat-login {
height: 100rpx;
color: #493323;
font-family: STSong, SimSun, serif;
font-size: 40rpx;
letter-spacing: 3rpx;
}
@@ -563,18 +555,19 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 590rpx;
min-height: 82rpx;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
background: transparent;
transform: translateX(-50%);
}
.feedback-toast__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.feedback-toast__copy {
position: relative;
z-index: 1;
@@ -621,7 +614,6 @@ const prepareAgreement = () => showFeedback('协议页面准备中')
.verification-title {
color: #6f140f;
font-family: STSong, SimSun, serif;
font-size: 42rpx;
font-weight: 700;
}
+17 -101
View File
@@ -12,7 +12,7 @@
</view>
<text class="page-title">注册账号</text>
<text class="page-subtitle">创建属于你的家谱账号</text>
<image class="register-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<image class="register-divider" src="/static/assets/modules/auth/transparent/a01-vnext-divider-v1.png" mode="aspectFit" />
</view>
<view class="form-content">
@@ -42,7 +42,7 @@
</view>
<view class="register-submit" hover-class="button-hover" @click="submitRegister">
<image class="register-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<image class="register-submit__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text class="register-submit__content">注册账号</text>
</view>
@@ -66,24 +66,7 @@
</view>
</view>
<!-- 注册短信接口未提供前完整本地校验通过后使用同系列行为验证容器 -->
<view v-if="verificationVisible" class="verification-layer" @click="closeVerification">
<view class="verification-dialog" @click.stop>
<image class="verification-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy">注册前需要完成行为验证</text>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view class="verification-action" hover-class="tap-fade" @click="closeVerification">取消</view>
<view class="verification-action verification-action--primary" hover-class="tap-fade" @click="confirmVerification">知道了</view>
</view>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<image class="feedback-toast__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
@@ -103,7 +86,6 @@ const fieldErrors = ref({
password: '',
confirmPassword: ''
})
const verificationVisible = ref(false)
const feedbackVisible = ref(false)
const feedbackMessage = ref('')
let feedbackTimer = null
@@ -150,20 +132,12 @@ const validateForm = () => {
return !Object.values(nextErrors).some(Boolean)
}
const closeVerification = () => {
verificationVisible.value = false
}
const confirmVerification = () => {
closeVerification()
showFeedback('注册服务待接入')
}
const submitRegister = () => {
const formValid = validateForm()
if (!agreed.value) agreementError.value = true
if (!formValid || !agreed.value) return
verificationVisible.value = true
//
showFeedback('滑动验证待接口接入')
}
</script>
@@ -200,8 +174,8 @@ const submitRegister = () => {
z-index: 1;
top: 64rpx;
left: 50%;
width: 220rpx;
height: 264rpx;
width: 184rpx;
height: 221rpx;
transform: translateX(-50%);
pointer-events: none;
}
@@ -240,7 +214,6 @@ const submitRegister = () => {
.page-title {
color: #9f170f;
font-family: STSong, SimSun, serif;
font-size: 66rpx;
font-weight: 700;
letter-spacing: 7rpx;
@@ -254,9 +227,9 @@ const submitRegister = () => {
}
.register-divider {
width: 154rpx;
height: 34rpx;
margin-top: 10rpx;
width: 300rpx;
height: 50rpx;
filter: brightness(0.68) saturate(1.5) contrast(1.2);
}
.form-content { margin-top: 12rpx; }
@@ -273,7 +246,6 @@ const submitRegister = () => {
.input-label {
flex: 0 0 152rpx;
color: #674b35;
font-family: STSong, SimSun, serif;
font-size: 31rpx;
}
@@ -319,7 +291,6 @@ const submitRegister = () => {
position: relative;
z-index: 1;
color: #fffaf0;
font-family: STSong, SimSun, serif;
font-size: 39rpx;
letter-spacing: 6rpx;
}
@@ -388,19 +359,19 @@ const submitRegister = () => {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 590rpx;
min-height: 82rpx;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
background: transparent;
transform: translateX(-50%);
}
.feedback-toast__skin,
.verification-dialog__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.feedback-toast__copy {
position: relative;
z-index: 1;
@@ -410,59 +381,4 @@ const submitRegister = () => {
text-align: center;
}
.verification-layer {
position: fixed;
z-index: 40;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
background: rgba(31, 20, 13, 0.56);
}
.verification-dialog {
position: relative;
width: 620rpx;
height: 520rpx;
}
.verification-dialog__content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
height: 100%;
padding: 90rpx 64rpx 54rpx;
}
.verification-title {
color: #6f140f;
font-family: STSong, SimSun, serif;
font-size: 42rpx;
font-weight: 700;
}
.verification-copy { margin-top: 42rpx; color: #493323; font-size: 28rpx; }
.verification-note { margin-top: 20rpx; color: #8d7a68; font-size: 23rpx; }
.verification-actions {
display: flex;
width: 100%;
margin-top: auto;
}
.verification-action {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 88rpx;
color: #745b46;
font-size: 28rpx;
}
.verification-action--primary { color: #a9160d; font-weight: 700; }
</style>
+20 -60
View File
@@ -12,7 +12,7 @@
</view>
<text class="page-title">重设密码</text>
<text class="page-subtitle">验证手机号后设置新的登录密码</text>
<image class="reset-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<image class="reset-divider" src="/static/assets/modules/auth/transparent/a01-vnext-divider-v1.png" mode="aspectFit" />
</view>
<view class="form-content">
@@ -28,7 +28,7 @@
<view class="input-row code-row">
<text class="input-label">验证码</text>
<input v-model.trim="verificationCode" class="auth-input" type="number" maxlength="6" placeholder="请输入验证码" placeholder-class="placeholder" @input="clearFieldError('verificationCode')" />
<view class="get-code" hover-class="tap-fade" @click="prepareGetCode">{{ codeRequested ? '重新获取' : '获取验证码' }}</view>
<view class="get-code" hover-class="tap-fade" @click="prepareGetCode">获取验证码</view>
</view>
<text v-if="fieldErrors.verificationCode" class="field-error">{{ fieldErrors.verificationCode }}</text>
</view>
@@ -51,7 +51,7 @@
</view>
<view class="reset-submit" hover-class="button-hover" @click="submitReset">
<image class="reset-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<image class="reset-submit__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text class="reset-submit__content">确认重设</text>
</view>
@@ -63,31 +63,15 @@
</view>
</view>
<!-- 发送短信验证码前进行行为验证关闭时不清空表单 -->
<view v-if="verificationVisible" class="verification-layer" @click="closeVerification">
<view class="verification-dialog" @click.stop>
<image class="verification-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy">发送验证码前需要完成行为验证</text>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view class="verification-action" hover-class="tap-fade" @click="closeVerification">取消</view>
<view class="verification-action verification-action--primary" hover-class="tap-fade" @click="confirmVerification">完成验证</view>
</view>
</view>
</view>
</view>
<view v-if="successVisible" class="success-layer">
<view class="success-dialog">
<image class="success-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<image class="success-dialog__skin" src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png" mode="aspectFit" />
<view class="success-dialog__content">
<text class="success-mark"></text>
<text class="success-title">密码已重设</text>
<text class="success-copy">请返回登录页使用新密码登录</text>
<view class="success-action" hover-class="button-hover" @click="prepareLogin">
<image class="success-action__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<image class="success-action__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text class="success-action__copy">返回登录</text>
</view>
</view>
@@ -95,7 +79,6 @@
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<image class="feedback-toast__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
@@ -109,8 +92,6 @@ const phone = ref('')
const verificationCode = ref('')
const password = ref('')
const confirmPassword = ref('')
const codeRequested = ref(false)
const verificationVisible = ref(false)
const successVisible = ref(false)
const feedbackVisible = ref(false)
const feedbackMessage = ref('')
@@ -151,17 +132,7 @@ const prepareGetCode = () => {
return
}
fieldErrors.value.phone = ''
verificationVisible.value = true
}
const closeVerification = () => {
verificationVisible.value = false
}
const confirmVerification = () => {
closeVerification()
codeRequested.value = true
showFeedback('验证码已发送')
showFeedback('滑动验证待接口接入')
}
const validateForm = () => {
@@ -214,8 +185,8 @@ const submitReset = () => {
z-index: 1;
top: 64rpx;
left: 50%;
width: 220rpx;
height: 264rpx;
width: 184rpx;
height: 221rpx;
transform: translateX(-50%);
pointer-events: none;
}
@@ -247,9 +218,9 @@ const submitReset = () => {
}
.back-button__icon { width: 42rpx; height: 42rpx; transform: scaleX(-1); }
.page-title { color: #9f170f; font-family: STSong, SimSun, serif; font-size: 66rpx; font-weight: 700; letter-spacing: 7rpx; }
.page-title { color: #9f170f; font-size: 66rpx; font-weight: 700; letter-spacing: 7rpx; }
.page-subtitle { margin-top: 12rpx; color: #806c58; font-size: 25rpx; letter-spacing: 1rpx; }
.reset-divider { width: 154rpx; height: 34rpx; margin-top: 10rpx; }
.reset-divider { width: 300rpx; height: 50rpx; margin-top: 2rpx; filter: brightness(0.68) saturate(1.5) contrast(1.2); }
.form-content { margin-top: 8rpx; }
.field-block { min-height: 114rpx; }
@@ -260,7 +231,7 @@ const submitReset = () => {
border-bottom: 1rpx solid #cfaa70;
}
.input-label { flex: 0 0 162rpx; color: #674b35; font-family: STSong, SimSun, serif; font-size: 29rpx; }
.input-label { flex: 0 0 162rpx; color: #674b35; font-size: 29rpx; }
.auth-input { flex: 1; min-width: 0; height: 82rpx; color: #3f2c1d; font-size: 27rpx; }
.placeholder { color: #a79e94; }
.get-code { flex: 0 0 auto; padding-left: 14rpx; border-left: 1rpx solid #d7bd94; color: #a7160c; font-size: 23rpx; }
@@ -279,12 +250,11 @@ const submitReset = () => {
.reset-submit__skin,
.success-action__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.reset-submit__content { position: relative; z-index: 1; color: #fffaf0; font-family: STSong, SimSun, serif; font-size: 35rpx; letter-spacing: 6rpx; }
.reset-submit__content { position: relative; z-index: 1; color: #fffaf0; font-size: 39rpx; letter-spacing: 6rpx; }
.reset-tip { display: block; margin-top: 22rpx; color: #806c58; font-size: 22rpx; text-align: center; }
.login-entry { display: flex; align-items: center; justify-content: center; min-height: 78rpx; color: #6b5a4a; font-size: 24rpx; }
.login-entry__link { display: flex; align-items: center; min-height: 78rpx; margin-left: 8rpx; color: #a7160c; }
.verification-layer,
.success-layer {
position: fixed;
z-index: 20;
@@ -297,27 +267,17 @@ const submitReset = () => {
background: rgba(35, 18, 10, 0.62);
}
.verification-dialog,
.success-dialog { position: relative; width: 100%; max-width: 670rpx; min-height: 500rpx; }
.verification-dialog__skin,
.success-dialog { position: relative; width: 620rpx; height: 520rpx; }
.success-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.verification-dialog__content,
.success-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; min-height: 500rpx; padding: 92rpx 70rpx 64rpx; text-align: center; }
.verification-title,
.success-title { color: #8f160f; font-family: STSong, SimSun, serif; font-size: 45rpx; font-weight: 700; letter-spacing: 4rpx; }
.verification-copy,
.success-copy { margin-top: 32rpx; color: #513a28; font-size: 27rpx; line-height: 42rpx; }
.verification-note { margin-top: 14rpx; color: #8d7965; font-size: 22rpx; }
.verification-actions { display: flex; width: 100%; margin-top: 56rpx; border-top: 1rpx solid #d6bb91; }
.verification-action { display: flex; flex: 1; align-items: center; justify-content: center; min-height: 86rpx; color: #73533a; font-size: 27rpx; }
.verification-action--primary { border-left: 1rpx solid #d6bb91; color: #a7160c; }
.success-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; height: 100%; padding: 64rpx 64rpx 42rpx; text-align: center; }
.success-title { color: #8f160f; font-size: 43rpx; font-weight: 700; letter-spacing: 4rpx; }
.success-copy { margin-top: 18rpx; color: #513a28; font-size: 26rpx; line-height: 40rpx; }
.success-mark { display: flex; align-items: center; justify-content: center; width: 82rpx; height: 82rpx; margin-bottom: 20rpx; border: 2rpx solid #a7160c; border-radius: 50%; color: #a7160c; font-size: 48rpx; }
.success-action { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 92rpx; margin-top: 38rpx; overflow: hidden; }
.success-action__copy { position: relative; z-index: 1; color: #fffaf0; font-family: STSong, SimSun, serif; font-size: 30rpx; letter-spacing: 4rpx; }
.success-action { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 82rpx; margin-top: 24rpx; overflow: hidden; }
.success-action__copy { position: relative; z-index: 1; color: #fffaf0; font-size: 30rpx; letter-spacing: 4rpx; }
.feedback-toast { position: fixed; z-index: 30; top: calc(40rpx + env(safe-area-inset-top)); left: 50%; display: flex; align-items: center; justify-content: center; min-width: 280rpx; min-height: 80rpx; padding: 0 42rpx; transform: translateX(-50%); }
.feedback-toast__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.feedback-toast__copy { position: relative; z-index: 1; color: #5e402b; font-size: 24rpx; }
.feedback-toast { position: fixed; z-index: 30; top: calc(env(safe-area-inset-top) + 24rpx); left: 50%; display: flex; align-items: center; justify-content: center; box-sizing: border-box; width: 590rpx; min-height: 82rpx; border: 16rpx solid transparent; border-width: 16rpx 74rpx; border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png'); border-image-slice: 58 280 fill; border-image-width: 16rpx 74rpx; border-image-repeat: stretch; background: transparent; transform: translateX(-50%); }
.feedback-toast__copy { position: relative; z-index: 1; padding: 16rpx 36rpx; color: #5c4330; font-size: 25rpx; text-align: center; }
.tap-fade,
.button-hover { opacity: 0.72; }
+16 -16
View File
@@ -12,7 +12,7 @@
</view>
<text class="page-title">登录受限</text>
<text class="page-subtitle">请根据当前账号状态完成恢复</text>
<image class="status-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<image class="status-divider" src="/static/assets/modules/auth/transparent/a01-vnext-divider-v1.png" mode="aspectFit" />
</view>
<view class="status-body">
@@ -38,7 +38,7 @@
</view>
<view class="status-primary" hover-class="button-hover" @click="openRecovery">
<image class="status-primary__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<image class="status-primary__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text class="status-primary__copy">查看恢复方式</text>
</view>
<view class="status-secondary" hover-class="tap-fade" @click="goLogin">返回登录</view>
@@ -47,13 +47,13 @@
<view v-if="recoveryVisible" class="recovery-layer" @click="closeRecovery">
<view class="recovery-dialog" @click.stop>
<image class="recovery-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<image class="recovery-dialog__skin" src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png" mode="aspectFit" />
<view class="recovery-dialog__content">
<text class="recovery-title">{{ currentState.recoveryTitle }}</text>
<text class="recovery-copy">{{ currentState.recoveryDetail }}</text>
<text class="recovery-note">账号恢复服务将在功能阶段接入当前仅审核页面样式与状态位置</text>
<view class="recovery-action" hover-class="button-hover" @click="closeRecovery">
<image class="recovery-action__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<image class="recovery-action__skin" src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text class="recovery-action__copy">我知道了</text>
</view>
</view>
@@ -150,19 +150,19 @@ const goBack = () => uni.navigateBack()
.page-canvas { position: relative; box-sizing: border-box; width: 100%; min-height: calc(1665rpx + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); }
.page-backdrop { position: absolute; inset: 0 0 auto; width: 100%; height: 1665rpx; pointer-events: none; }
.brand-seal { position: absolute; z-index: 1; top: 64rpx; left: 50%; width: 220rpx; height: 264rpx; transform: translateX(-50%); pointer-events: none; }
.brand-seal { position: absolute; z-index: 1; top: 64rpx; left: 50%; width: 184rpx; height: 221rpx; transform: translateX(-50%); pointer-events: none; }
.status-content { position: relative; z-index: 2; box-sizing: border-box; padding: 430rpx 64rpx 46rpx; }
.page-heading { position: relative; display: flex; flex-direction: column; align-items: center; min-height: 154rpx; }
.back-button { position: absolute; top: 10rpx; left: 0; display: flex; align-items: center; justify-content: center; width: 88rpx; height: 88rpx; }
.back-button__icon { width: 42rpx; height: 42rpx; transform: scaleX(-1); }
.page-title { color: #9f170f; font-family: STSong, SimSun, serif; font-size: 66rpx; font-weight: 700; letter-spacing: 7rpx; }
.page-title { color: #9f170f; font-size: 66rpx; font-weight: 700; letter-spacing: 7rpx; }
.page-subtitle { margin-top: 12rpx; color: #806c58; font-size: 25rpx; letter-spacing: 1rpx; }
.status-divider { width: 154rpx; height: 34rpx; margin-top: 10rpx; }
.status-divider { width: 300rpx; height: 50rpx; margin-top: 2rpx; filter: brightness(0.68) saturate(1.5) contrast(1.2); }
.status-body { display: flex; flex-direction: column; align-items: center; padding-top: 6rpx; text-align: center; }
.status-icon { width: 94rpx; height: 94rpx; opacity: 0.82; }
.status-eyebrow { margin-top: 10rpx; color: #a7160c; font-size: 22rpx; letter-spacing: 3rpx; }
.status-title { margin-top: 10rpx; color: #3f2c1d; font-family: STSong, SimSun, serif; font-size: 40rpx; font-weight: 700; letter-spacing: 3rpx; }
.status-title { margin-top: 10rpx; color: #3f2c1d; font-size: 40rpx; font-weight: 700; letter-spacing: 3rpx; }
.status-description { box-sizing: border-box; width: 100%; margin-top: 12rpx; color: #786654; font-size: 24rpx; line-height: 1.65; }
.status-details { width: 100%; margin-top: 22rpx; border-top: 1rpx solid #cfaa70; }
.status-detail { display: flex; box-sizing: border-box; min-height: 78rpx; padding: 16rpx 0; border-bottom: 1rpx solid rgba(207, 170, 112, 0.72); text-align: left; }
@@ -172,18 +172,18 @@ const goBack = () => uni.navigateBack()
.status-primary { position: relative; display: flex; align-items: center; justify-content: center; height: 104rpx; margin-top: 28rpx; overflow: hidden; }
.status-primary__skin,
.recovery-action__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.status-primary__copy { position: relative; z-index: 1; color: #fffaf0; font-family: STSong, SimSun, serif; font-size: 34rpx; letter-spacing: 5rpx; }
.status-primary__copy { position: relative; z-index: 1; color: #fffaf0; font-size: 38rpx; letter-spacing: 5rpx; }
.status-secondary { display: flex; align-items: center; justify-content: center; min-height: 82rpx; color: #a7160c; font-size: 24rpx; }
.recovery-layer { position: fixed; z-index: 20; inset: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 42rpx; background: rgba(35, 18, 10, 0.62); }
.recovery-dialog { position: relative; width: 100%; max-width: 670rpx; min-height: 560rpx; }
.recovery-dialog { position: relative; width: 620rpx; height: 520rpx; }
.recovery-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.recovery-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; min-height: 560rpx; padding: 92rpx 70rpx 64rpx; text-align: center; }
.recovery-title { color: #8f160f; font-family: STSong, SimSun, serif; font-size: 43rpx; font-weight: 700; letter-spacing: 4rpx; }
.recovery-copy { margin-top: 32rpx; color: #513a28; font-size: 27rpx; line-height: 43rpx; }
.recovery-note { margin-top: 18rpx; color: #8d7965; font-size: 21rpx; line-height: 34rpx; }
.recovery-action { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 92rpx; margin-top: 38rpx; overflow: hidden; }
.recovery-action__copy { position: relative; z-index: 1; color: #fffaf0; font-family: STSong, SimSun, serif; font-size: 30rpx; letter-spacing: 4rpx; }
.recovery-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; height: 100%; padding: 58rpx 58rpx 36rpx; text-align: center; }
.recovery-title { color: #8f160f; font-size: 42rpx; font-weight: 700; letter-spacing: 4rpx; }
.recovery-copy { margin-top: 20rpx; color: #513a28; font-size: 26rpx; line-height: 40rpx; }
.recovery-note { margin-top: 12rpx; color: #8d7965; font-size: 20rpx; line-height: 31rpx; }
.recovery-action { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 80rpx; margin-top: 22rpx; overflow: hidden; }
.recovery-action__copy { position: relative; z-index: 1; color: #fffaf0; font-size: 29rpx; letter-spacing: 4rpx; }
.tap-fade,
.button-hover { opacity: 0.72; }
+1
View File
@@ -630,6 +630,7 @@ const openShortcut = (key) => {
box-sizing: border-box;
padding: 22rpx 26rpx;
}
.application-record + .application-record { margin-top: 12rpx; }
.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; }
+44 -4
View File
@@ -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 <url> <selector> <output> [width] [height] [password-tab|sms-tab|g06-results|g06-empty|g06-invite|fresh-navigation]')
throw new Error('Usage: node scripts/capture-chrome-page.js <url> <selector> <output> [width] [height] [password-tab|sms-tab|invalid-login|verification-dialog|g06-results|g06-empty|g06-invite|fresh-navigation]')
}
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
@@ -55,8 +55,8 @@ const waitForLoadedImages = async (send) => {
}
const prepareA01LoginState = async (send, action) => {
if (action !== 'password-tab' && action !== 'sms-tab') return
const tabIndex = action === 'password-tab' ? 0 : 1
if (action !== 'password-tab' && action !== 'sms-tab' && action !== 'invalid-login' && action !== 'verification-dialog') return
const tabIndex = action === 'sms-tab' ? 1 : 0
for (let attempt = 0; attempt < 30; attempt += 1) {
const prepared = await send('Runtime.evaluate', {
@@ -78,10 +78,50 @@ const prepareA01LoginState = async (send, action) => {
expression: `document.querySelectorAll('.login-tab')[${tabIndex}]?.classList.contains('active') === true`,
returnByValue: true
})
if (active.result?.value) return
if (active.result?.value) break
if (attempt === 29) throw new Error(`A01 did not enter the requested ${action} state`)
await sleep(100)
}
if (action === 'invalid-login') {
await send('Runtime.evaluate', { expression: "document.querySelector('.login-submit').click()" })
for (let attempt = 0; attempt < 30; attempt += 1) {
const visible = await send('Runtime.evaluate', {
expression: "Boolean(document.querySelector('.feedback-toast'))",
returnByValue: true
})
if (visible.result?.value) return
if (attempt === 29) throw new Error('A01 invalid-login Toast did not render')
await sleep(100)
}
}
if (action === 'verification-dialog') {
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const inputs = document.querySelectorAll('.auth-input input')
if (inputs.length < 2) return false
inputs[0].value = '13800138000'
inputs[0].dispatchEvent(new Event('input', { bubbles: true }))
inputs[1].value = 'demo-password'
inputs[1].dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.agreement-row').click()
document.querySelector('.login-submit').click()
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error('Could not prepare A01 verification dialog state')
for (let attempt = 0; attempt < 30; attempt += 1) {
const visible = await send('Runtime.evaluate', {
expression: "Boolean(document.querySelector('.verification-dialog'))",
returnByValue: true
})
if (visible.result?.value) return
if (attempt === 29) throw new Error('A01 verification dialog did not render')
await sleep(100)
}
}
}
const waitForRequestedUrl = async (send, url) => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

+69 -5
View File
@@ -33,6 +33,7 @@ $root = Split-Path -Parent $PSScriptRoot
$entryPath = Join-Path $root 'pages/auth/a01-entry.vue'
$legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue'
$pagesPath = Join-Path $root 'pages.json'
$globalStylesPath = Join-Path $root 'styles/global.scss'
if (-not (Test-Path -LiteralPath $entryPath)) {
throw 'Missing the single A01 login page.'
@@ -48,19 +49,59 @@ if ($pages.pages[0].path -ne 'pages/auth/a01-entry') {
if ($pages.pages.path -contains 'pages/auth/a02-login') {
throw 'pages.json must not retain the legacy A02 route.'
}
if ($pages.pages.Count -ne 53) {
throw "Expected 53 final routes after the A02 merge, found $($pages.pages.Count)."
if ($pages.pages.Count -ne 52) {
throw "Expected 52 active routes after the A02 merge and A06 archive, found $($pages.pages.Count)."
}
$entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8
$globalStyles = Get-Content -LiteralPath $globalStylesPath -Raw -Encoding utf8
# Typography has one owner, and the title decoration participates in normal flow.
Assert-Contains -Content $globalStyles -Expected 'font-family: "STKaiti", "KaiTi", serif' -Message 'Global styles must own the STKaiti/KaiTi font contract.'
Assert-NotContains -Content $entry -Unexpected 'font-family:' -Message 'A01 must inherit the global font contract instead of overriding it locally.'
$headingRule = [regex]::Match($entry, '(?ms)^\.login-heading\s*\{(?<Body>.*?)^\}')
if (-not $headingRule.Success) {
throw 'A01 is missing the login heading style rule.'
}
Assert-Contains -Content $headingRule.Groups['Body'].Value -Expected 'flex-direction: column;' -Message 'A01 login heading must lay out its title and divider vertically in normal flow.'
Assert-NotContains -Content $headingRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 login heading must not use positioning for ordinary vertical layout.'
$titleRule = [regex]::Match($entry, '(?ms)^\.login-title\s*\{(?<Body>.*?)^\}')
if (-not $titleRule.Success) {
throw 'A01 is missing the login title style rule.'
}
Assert-NotContains -Content $titleRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 login title must remain in normal flow.'
$dividerRule = [regex]::Match($entry, '(?ms)^\.title-divider\s*\{(?<Body>.*?)^\}')
if (-not $dividerRule.Success) {
throw 'A01 is missing the title divider style rule.'
}
foreach ($positioning in @('position: absolute', 'left: 50%', 'translateX(')) {
Assert-NotContains -Content $dividerRule.Groups['Body'].Value -Unexpected $positioning -Message "A01 title divider must not use manual positioning: $positioning"
}
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'width: 360rpx;' -Message 'A01 title divider must be wide enough to remain visible on paper.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 60rpx;' -Message 'A01 title divider must retain a legible knot and line weight.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'filter: brightness(0.68) saturate(1.5) contrast(1.2);' -Message 'A01 title divider must keep sufficient contrast against the paper background.'
$brandSealRule = [regex]::Match($entry, '(?ms)^\.brand-seal\s*\{(?<Body>.*?)^\}')
if (-not $brandSealRule.Success) {
throw 'A01 is missing the brand seal style rule.'
}
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'width: 184rpx;' -Message 'A01 brand seal must use the approved smaller width.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'height: 221rpx;' -Message 'A01 brand seal must preserve its aspect ratio at the approved smaller height.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'top: 64rpx;' -Message 'A01 brand seal must keep its approved vertical anchor.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'left: 50%;' -Message 'A01 brand seal must remain horizontally centered.'
# Final decorative surfaces must come from real bitmap assets.
foreach ($asset in @(
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'a01-title-divider-v2.png',
'a01-primary-button.png',
'a01-secondary-button.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-secondary-v3.png',
'a01-scroll-toast-v3.png',
'a01-scroll-dialog-v3.png',
'auth-wechat.png',
'a01-icon-phone-v1.png',
'a01-icon-lock-v1.png',
@@ -80,10 +121,24 @@ foreach ($obsoleteVisualAsset in @(
'a01-login-scroll-frame-v1.png',
'a01-vnext-scroll-v1.png',
'auth-ink-scenery.png'
'a01-title-divider-v2.png',
'a01-primary-button-v2.png',
'a01-secondary-button-v2.png',
'a02-login-panel.png'
)) {
Assert-NotContains -Content $entry -Unexpected $obsoleteVisualAsset -Message "A01 must not retain the rejected header, scenery, or scroll asset: $obsoleteVisualAsset"
}
$buttonSkinTags = [regex]::Matches($entry, '<image class="button-skin"[^>]+>')
if ($buttonSkinTags.Count -ne 2) {
throw "A01 must have exactly two visible login button skins, found $($buttonSkinTags.Count)."
}
foreach ($buttonSkinTag in $buttonSkinTags) {
Assert-Contains -Content $buttonSkinTag.Value -Expected 'mode="aspectFit"' -Message 'A01 button skins must use uniform aspectFit rendering.'
Assert-NotContains -Content $buttonSkinTag.Value -Unexpected '/opaque/' -Message 'A01 visible button skins must use cleaned transparent v2 assets.'
Assert-NotContains -Content $buttonSkinTag.Value -Unexpected 'mode="scaleToFill"' -Message 'A01 button skins must not distort fixed artwork with scaleToFill.'
}
$requiredCopy = @(
'55m75b2V5a626LCx',
'5a+G56CB55m75b2V',
@@ -137,4 +192,13 @@ Assert-NotContains -Content $entry -Unexpected 'a01-icon-eye-closed-v1.png' -Mes
Assert-NotContains -Content $entry -Unexpected 'a01-icon-verification-v1.png' -Message 'A01 SMS field must use a message-code icon instead of a success-state checkmark.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-sms-code-v1.svg' -Message 'A01 SMS field must use the custom generated icon instead of the temporary third-party SVG.'
Assert-NotContains -Content $entry -Unexpected '@media (max-height:' -Message 'A01 must scroll naturally on short screens instead of compressing the selected design with height breakpoints.'
Assert-NotContains -Content $entry -Unexpected 'feedback-toast__skin' -Message 'A01 feedback Toast must use the dedicated nine-slice border asset instead of an absolutely stretched image.'
$feedbackToastRule = [regex]::Match($entry, '(?ms)^\.feedback-toast\s*\{(?<Body>.*?)^\}')
if (-not $feedbackToastRule.Success) {
throw 'A01 is missing the custom feedback Toast style rule.'
}
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected "border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');" -Message 'A01 feedback Toast must use the approved v3 nine-slice asset.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'border-image-slice:' -Message 'A01 feedback Toast must declare fixed decorative slices.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'fill' -Message 'A01 feedback Toast nine-slice must fill the paper center.'
Write-Output 'A01-LOGIN-MERGE-CONTRACT PASS'
+55 -1
View File
@@ -18,13 +18,67 @@ $buttons = @(
'static/assets/foundation/opaque/a01-primary-button.png',
'static/assets/foundation/opaque/a01-secondary-button.png'
)
$v2Buttons = @(
'static/assets/foundation/transparent/a01-primary-button-v2.png',
'static/assets/foundation/transparent/a01-secondary-button-v2.png'
)
$v3Skins = @(
'static/assets/foundation/transparent/a01-scroll-primary-v3.png',
'static/assets/foundation/transparent/a01-scroll-secondary-v3.png',
'static/assets/foundation/transparent/a01-scroll-toast-v3.png',
'static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png'
)
foreach ($asset in $backgrounds + $icons + $buttons) {
foreach ($asset in $backgrounds + $icons + $buttons + $v2Buttons + $v3Skins) {
if (-not (Test-Path -LiteralPath (Join-Path $root $asset))) {
throw "Missing A01 asset: $asset"
}
}
foreach ($asset in $v3Skins) {
$bitmap = [System.Drawing.Bitmap]::FromFile((Join-Path $root $asset))
$width = [int]$bitmap.Width
$height = [int]$bitmap.Height
$corners = @(
$bitmap.GetPixel(0, 0).A,
$bitmap.GetPixel(($width - 1), 0).A,
$bitmap.GetPixel(0, ($height - 1)).A,
$bitmap.GetPixel(($width - 1), ($height - 1)).A
)
$bitmap.Dispose()
$expectedSize = switch ($asset) {
'static/assets/foundation/transparent/a01-scroll-primary-v3.png' { @(1866, 276) }
'static/assets/foundation/transparent/a01-scroll-secondary-v3.png' { @(1866, 300) }
'static/assets/foundation/transparent/a01-scroll-toast-v3.png' { @(1770, 246) }
'static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png' { @(1860, 1560) }
}
if ($width -ne $expectedSize[0] -or $height -ne $expectedSize[1]) { throw "$asset must be $($expectedSize[0]) x $($expectedSize[1])px." }
if (($corners | Where-Object { $_ -ne 0 }).Count -ne 0) { throw "$asset must have transparent outer corners." }
Write-Output "PASS $asset transparent corners and exact v3 slot ratio"
}
foreach ($asset in $v2Buttons) {
$bitmap = [System.Drawing.Bitmap]::FromFile((Join-Path $root $asset))
$width = [int]$bitmap.Width
$height = [int]$bitmap.Height
$corners = @(
$bitmap.GetPixel(0, 0).A,
$bitmap.GetPixel(($width - 1), 0).A,
$bitmap.GetPixel(0, ($height - 1)).A,
$bitmap.GetPixel(($width - 1), ($height - 1)).A
)
$bitmap.Dispose()
$expectedSize = switch ($asset) {
'static/assets/foundation/transparent/a01-primary-button-v2.png' { @(1866, 276) }
'static/assets/foundation/transparent/a01-secondary-button-v2.png' { @(1866, 300) }
}
if ($width -ne $expectedSize[0] -or $height -ne $expectedSize[1]) { throw "$asset must be $($expectedSize[0]) x $($expectedSize[1])px." }
if (($corners | Where-Object { $_ -ne 0 }).Count -ne 0) { throw "$asset must have transparent outer corners." }
Write-Output "PASS $asset transparent corners and exact runtime ratio"
}
foreach ($asset in $icons) {
$bitmap = [System.Drawing.Bitmap]::FromFile((Join-Path $root $asset))
$width = [int]$bitmap.Width
+41
View File
@@ -56,6 +56,14 @@ const waitFor = async (send, expression, message) => {
throw new Error(message)
}
const waitForFreshDocument = async (send, previousTimeOrigin) => {
await waitFor(
send,
`performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`,
'A01 navigation did not create a fresh document'
)
}
const run = async () => {
const { socket, send, exceptions } = await connect()
try {
@@ -70,7 +78,10 @@ const run = async () => {
mobile: true
})
await send('Page.navigate', { url })
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `A01 did not navigate at ${size.width}x${size.height}`)
const previousTimeOrigin = await valueOf(send, 'performance.timeOrigin')
await send('Page.reload')
await waitForFreshDocument(send, previousTimeOrigin)
await waitFor(send, "document.querySelectorAll('.login-tab').length === 2", `A01 did not render at ${size.width}x${size.height}`)
if (size.width === 320 && size.height === 568) {
await waitFor(send, "document.querySelector('.auth-page')?.scrollHeight > document.querySelector('.auth-page')?.clientHeight", 'A01 did not establish natural scrolling at 320x568')
@@ -86,11 +97,30 @@ const run = async () => {
})`)
assert.strictEqual(metrics.innerWidth, size.width, `A01 viewport width mismatch at ${size.width}x${size.height}`)
assert(metrics.scrollWidth <= size.width + 1, `A01 has horizontal overflow at ${size.width}x${size.height}: ${metrics.scrollWidth}`)
const buttonAssets = await valueOf(send, `Array.from(document.querySelectorAll('.button-skin img')).map((image) => ({
src: image.currentSrc || image.src,
naturalWidth: image.naturalWidth,
naturalHeight: image.naturalHeight
}))`)
assert.strictEqual(buttonAssets.length, 2, `A01 button skins did not render at ${size.width}x${size.height}`)
assert(buttonAssets[0].src.includes('a01-scroll-primary-v3.png'), `A01 primary v3 skin is missing at ${size.width}x${size.height}`)
assert(buttonAssets[1].src.includes('a01-scroll-secondary-v3.png'), `A01 secondary v3 skin is missing at ${size.width}x${size.height}`)
assert.deepStrictEqual(
buttonAssets.map(({ naturalWidth, naturalHeight }) => [naturalWidth, naturalHeight]),
[[1866, 276], [1866, 300]],
`A01 v3 button assets have unexpected runtime dimensions at ${size.width}x${size.height}`
)
if (size.width === 320 && size.height === 568) {
assert(metrics.pageScrollHeight > metrics.pageClientHeight, 'A01 must scroll naturally at 320x568')
}
}
await valueOf(send, "document.querySelector('.login-submit').click()")
await waitFor(send, "Boolean(document.querySelector('.feedback-toast'))", 'A01 invalid phone Toast did not render')
const toastBorderImage = await valueOf(send, "getComputedStyle(document.querySelector('.feedback-toast')).borderImageSource")
assert(toastBorderImage.includes('a01-scroll-toast-v3.png'), `A01 Toast did not render the v3 nine-slice asset: ${toastBorderImage}`)
await valueOf(send, "document.querySelectorAll('.login-tab')[1].click()")
await waitFor(send, "document.querySelectorAll('.login-tab')[1].classList.contains('active')", 'A01 SMS tab did not activate')
await waitFor(send, "Boolean(document.querySelector('.input-icon--sms img'))", 'A01 SMS field image did not render after tab activation')
@@ -117,6 +147,17 @@ const run = async () => {
await waitFor(send, "!document.querySelector('.agreement-error')", 'A01 agreement error did not clear after selection')
await valueOf(send, "document.querySelector('.login-submit').click()")
await waitFor(send, "Boolean(document.querySelector('.verification-layer'))", 'A01 did not open the custom verification layer after local validation')
await waitFor(send, "document.querySelector('.verification-dialog__skin img')?.naturalWidth === 1860", 'A01 verification dialog v3 asset did not finish loading')
const dialogAsset = await valueOf(send, `(() => {
const image = document.querySelector('.verification-dialog__skin img')
return image ? { src: image.currentSrc || image.src, naturalWidth: image.naturalWidth, naturalHeight: image.naturalHeight } : null
})()`)
assert(dialogAsset?.src.includes('a01-scroll-dialog-v3.png'), `A01 verification dialog did not load the v3 asset: ${dialogAsset?.src}`)
assert.deepStrictEqual(
[dialogAsset.naturalWidth, dialogAsset.naturalHeight],
[1860, 1560],
'A01 verification dialog loaded unexpected natural dimensions'
)
assert.deepStrictEqual(exceptions, [], `A01 raised browser exceptions: ${exceptions.join('; ')}`)
process.stdout.write('A01-RESPONSIVE-RUNTIME-SMOKE PASS\n')
+1 -1
View File
@@ -4,7 +4,7 @@ $root = Split-Path -Parent $PSScriptRoot
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
$legacyPage = Join-Path $root 'pages/auth/a02-login.vue'
if ($pages.pages.Count -ne 53) { throw "Expected 53 routes, found $($pages.pages.Count)." }
if ($pages.pages.Count -ne 52) { throw "Expected 52 active routes, found $($pages.pages.Count)." }
if ($pages.pages.path -contains 'pages/auth/a02-login') { throw 'Legacy A02 route still exists.' }
if (Test-Path -LiteralPath $legacyPage) { throw 'Legacy A02 page file still exists.' }
+1 -1
View File
@@ -8,6 +8,6 @@ $a03File = Join-Path $root 'pages/auth/a03-mobile-verify.vue'
if ($pages.pages.path -contains 'pages/auth/a03-mobile-verify') { throw 'A-03 must not remain in pages.json' }
if (Test-Path -LiteralPath $a03File) { throw 'A-03 page file must be deleted' }
if ($catalog -match '(?m)^\s*a03\s*:') { throw 'A-03 catalog entry must be deleted' }
if ($pages.pages.Count -ne 53) { throw "Expected 53 final routes after the approved page-state merges, found $($pages.pages.Count)." }
if ($pages.pages.Count -ne 52) { throw "Expected 52 active routes after the approved page-state merges and A06 archive, found $($pages.pages.Count)." }
Write-Output 'A03-ROUTE-REMOVAL-CONTRACT PASS'
+23 -6
View File
@@ -20,16 +20,19 @@ $registerPath = Join-Path $root 'pages/auth/a04-register.vue'
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
$catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Raw -Encoding utf8
$register = Get-Content -LiteralPath $registerPath -Raw -Encoding utf8
$globalStyles = Get-Content -LiteralPath (Join-Path $root 'styles/global.scss') -Raw -Encoding utf8
$registerCopy = ConvertFrom-Utf8Base64 '5rOo5YaM6LSm5Y+3'
$loginCopy = ConvertFrom-Utf8Base64 '55m75b2V'
$sliderPendingCopy = ConvertFrom-Utf8Base64 '5ruR5Yqo6aqM6K+B5b6F5o6l5Y+j5o6l5YWl'
if (-not ($pages.pages.path -contains 'pages/auth/a04-register')) { throw 'A-04 route must remain declared in pages.json' }
Assert-NotContains -Content $register -Unexpected 'ModulePage' -Message 'A-04 must not remain a ModulePage shell'
foreach ($required in @(
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'a02-login-panel.png',
'a01-primary-button.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-toast-v3.png',
'a02-agreement-unchecked.png',
'a02-agreement-checked.png',
'v-model.trim="phone"',
@@ -38,11 +41,10 @@ foreach ($required in @(
'const agreed = ref(false)',
'const agreementError = ref(false)',
'const fieldErrors = ref({',
'const verificationVisible = ref(false)',
'const toggleAgreement = () =>',
'const submitRegister = () =>',
'class="feedback-toast"',
'class="verification-layer"',
$sliderPendingCopy,
"const prepareLogin = () => uni.redirectTo({ url: '/pages/auth/a01-entry' })",
'if (!/^1\d{10}$/.test(phone.value))',
'if (!password.value)',
@@ -56,13 +58,28 @@ Assert-Contains -Content $register -Expected $loginCopy -Message 'A-04 must prov
Assert-NotContains -Content $catalog -Unexpected 'a04:' -Message 'A-04 must not remain in the temporary page catalog'
Assert-NotContains -Content $register -Unexpected 'register-intro' -Message 'A-04 must not retain the redundant registration intro copy'
Assert-NotContains -Content $register -Unexpected 'auth-design-scenery-v1.png' -Message 'A-04 must use the shared red-hall ink backdrop instead of the rejected scenery layer'
Assert-Contains -Content $globalStyles -Expected 'font-family: "STKaiti", "KaiTi", serif' -Message 'Global styles must own the A04 font contract.'
Assert-NotContains -Content $register -Unexpected 'font-family:' -Message 'A04 must inherit the global font contract instead of overriding it locally.'
foreach ($obsoleteVisual in @(
'a01-primary-button.png',
'a01-secondary-button.png',
'a02-login-panel.png',
'verificationVisible',
'verification-layer',
'feedback-toast__skin'
)) {
Assert-NotContains -Content $register -Unexpected $obsoleteVisual -Message "A04 must not retain obsolete visual or placeholder state: $obsoleteVisual"
}
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
Assert-NotContains -Content $register -Unexpected $nativeUi -Message "A-04 must not use native UniApp feedback: $nativeUi"
}
foreach ($required in @(
'auth-divider-knot.png',
'register-divider',
'width: 154rpx'
'mode="aspectFit"',
'width: 184rpx;',
'height: 221rpx;',
"border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');",
'border-image-slice: 58 280 fill;'
)) {
Assert-Contains -Content $register -Expected $required -Message "Missing A-04 visual balance contract: $required"
}
+6 -4
View File
@@ -68,7 +68,7 @@ const run = async () => {
await valueOf(send, "document.querySelector('.register-link').click()")
await waitFor(send, "location.href.includes('/pages/auth/a04-register')", 'A01 registration entry did not open A04')
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 640 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
await navigate(send, a04Url, '.register-submit')
const metrics = await valueOf(send, `({
@@ -94,9 +94,11 @@ const run = async () => {
document.querySelector('.agreement-row').click()
document.querySelector('.register-submit').click()
})()`)
await waitFor(send, "Boolean(document.querySelector('.verification-layer'))", 'A04 valid submit did not open custom verification')
await valueOf(send, "document.querySelector('.verification-action--primary').click()")
await waitFor(send, "Boolean(document.querySelector('.feedback-toast'))", 'A04 verification result did not use custom feedback')
await waitFor(send, "Boolean(document.querySelector('.feedback-toast'))", 'A04 valid submit did not show slider integration feedback')
assert.strictEqual(await valueOf(send, "document.querySelector('.feedback-toast__copy')?.textContent"), '滑动验证待接口接入', 'A04 valid submit showed unexpected feedback')
assert.strictEqual(await valueOf(send, "Boolean(document.querySelector('.verification-layer'))"), false, 'A04 must not retain the non-final custom verification placeholder')
const toastBorderImage = await valueOf(send, "getComputedStyle(document.querySelector('.feedback-toast')).borderImageSource")
assert(toastBorderImage.includes('a01-scroll-toast-v3.png'), `A04 Toast did not render the v3 nine-slice asset: ${toastBorderImage}`)
await navigate(send, a04Url, '.login-entry__link')
await valueOf(send, "document.querySelector('.login-entry__link').click()")
+29 -6
View File
@@ -24,15 +24,17 @@ $resetCopy = ConvertFrom-Utf8Base64 '6YeN6K6+5a+G56CB'
$confirmPasswordCopy = ConvertFrom-Utf8Base64 '56Gu6K6k5paw5a+G56CB'
$getCodeCopy = ConvertFrom-Utf8Base64 '6I635Y+W6aqM6K+B56CB'
$loginCopy = ConvertFrom-Utf8Base64 '6L+U5Zue55m75b2V'
$sliderPendingCopy = ConvertFrom-Utf8Base64 '5ruR5Yqo6aqM6K+B5b6F5o6l5Y+j5o6l5YWl'
if (-not ($pages.pages.path -contains 'pages/auth/a05-reset-password')) { throw 'A-05 route must remain declared in pages.json' }
Assert-NotContains -Content $page -Unexpected 'ModulePage' -Message 'A-05 must not remain a ModulePage shell'
foreach ($required in @(
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'a02-login-panel.png',
'a01-primary-button.png',
'auth-divider-knot.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-toast-v3.png',
'a01-scroll-dialog-v3.png',
'chevron-right.png',
'v-model.trim="phone"',
'v-model.trim="verificationCode"',
@@ -41,16 +43,18 @@ foreach ($required in @(
'const prepareGetCode = () =>',
'const submitReset = () =>',
'const fieldErrors = ref({',
'const verificationVisible = ref(false)',
'const successVisible = ref(false)',
'class="verification-layer"',
'class="feedback-toast"',
'class="success-layer"',
"const prepareLogin = () => uni.redirectTo({ url: '/pages/auth/a01-entry' })",
'if (!/^1\d{10}$/.test(phone.value))',
'if (!/^\d{6}$/.test(verificationCode.value))',
'if (!password.value)',
'if (password.value !== confirmPassword.value)'
'if (password.value !== confirmPassword.value)',
'width: 184rpx;',
'height: 221rpx;',
"border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');",
'border-image-slice: 58 280 fill;'
)) {
Assert-Contains -Content $page -Expected $required -Message "Missing A-05 reset-password contract: $required"
}
@@ -58,8 +62,27 @@ Assert-Contains -Content $page -Expected $resetCopy -Message 'A-05 must visibly
Assert-Contains -Content $page -Expected $confirmPasswordCopy -Message 'A-05 must require password confirmation'
Assert-Contains -Content $page -Expected $getCodeCopy -Message 'A-05 must expose the verification-code action'
Assert-Contains -Content $page -Expected $loginCopy -Message 'A-05 must provide the A01 return path'
Assert-Contains -Content $page -Expected $sliderPendingCopy -Message 'A-05 must defer the real slider to the interface stage'
Assert-NotContains -Content $catalog -Unexpected 'a05:' -Message 'A-05 must not remain in the temporary page catalog'
Assert-NotContains -Content $page -Unexpected 'auth-design-scenery-v1.png' -Message 'A-05 must use the selected red-hall ink backdrop'
foreach ($obsoleteVisual in @(
'a01-primary-button.png',
'a01-secondary-button.png',
'a02-login-panel.png',
'auth-divider-knot.png',
'feedback-toast__skin',
'codeRequested',
'verificationVisible',
'verification-layer',
'closeVerification',
'confirmVerification',
'font-family:'
)) {
Assert-NotContains -Content $page -Unexpected $obsoleteVisual -Message "A-05 must not retain obsolete visual or placeholder state: $obsoleteVisual"
}
$primarySkinCount = ([regex]::Matches($page, 'a01-scroll-primary-v3\.png')).Count
if ($primarySkinCount -ne 2) { throw "A-05 must use the approved primary skin exactly twice, found $primarySkinCount" }
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
Assert-NotContains -Content $page -Unexpected $nativeUi -Message "A-05 must not use native UniApp feedback: $nativeUi"
}
+9 -12
View File
@@ -69,11 +69,13 @@ const run = async () => {
await send('Page.enable')
await send('Runtime.enable')
await navigate(send, a01Url, '.forgot-password')
await navigate(send, a01Url, '.login-tab')
await valueOf(send, "document.querySelectorAll('.login-tab')[0].click()")
await waitFor(send, "Boolean(document.querySelector('.forgot-password'))", 'A01 password state did not expose forgot-password entry')
await valueOf(send, "document.querySelector('.forgot-password').click()")
await waitFor(send, "location.href.includes('/pages/auth/a05-reset-password')", 'A01 forgot-password entry did not open A05')
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 640 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
await navigate(send, a05Url, '.reset-submit')
const metrics = await valueOf(send, `({
@@ -89,16 +91,11 @@ const run = async () => {
await waitFor(send, "Boolean(document.querySelector('.field-error'))", 'A05 invalid phone did not show inline error')
await valueOf(send, setInputs(['13800138000', '', '', '']))
await valueOf(send, "document.querySelector('.get-code').click()")
await waitFor(send, "Boolean(document.querySelector('.verification-layer'))", 'A05 code request did not open custom verification')
await valueOf(send, "document.querySelector('.verification-action').click()")
await waitFor(send, "!document.querySelector('.verification-layer')", 'A05 verification did not close')
assert.strictEqual(await valueOf(send, "document.querySelector('.auth-input input').value"), '13800138000', 'A05 verification close cleared the form')
await valueOf(send, "document.querySelector('.get-code').click()")
await waitFor(send, "Boolean(document.querySelector('.verification-layer'))", 'A05 verification did not reopen')
await valueOf(send, "document.querySelector('.verification-action--primary').click()")
await waitFor(send, "Boolean(document.querySelector('.feedback-toast'))", 'A05 verified code request did not use custom feedback')
assert.strictEqual(await valueOf(send, "document.querySelector('.get-code').textContent"), '重新获取', 'A05 code action did not enter requested state')
await waitFor(send, "Boolean(document.querySelector('.feedback-toast'))", 'A05 code request did not use custom pending feedback')
assert.strictEqual(await valueOf(send, "document.querySelector('.feedback-toast').textContent.trim()"), '滑动验证待接口接入', 'A05 code request showed the wrong pending feedback')
assert.strictEqual(await valueOf(send, "Boolean(document.querySelector('.verification-layer'))"), false, 'A05 must not retain the old verification panel')
assert.strictEqual(await valueOf(send, "document.querySelector('.auth-input input').value"), '13800138000', 'A05 pending slider feedback cleared the form')
assert.strictEqual(await valueOf(send, "document.querySelector('.get-code').textContent.trim()"), '获取验证码', 'A05 must not fake a requested-code state before the real slider and SMS interface')
await valueOf(send, "document.querySelector('.reset-submit').click()")
await waitFor(send, "document.querySelectorAll('.field-error').length === 3", 'A05 incomplete submit did not show three remaining field errors')
+22 -5
View File
@@ -14,9 +14,12 @@ $root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/auth/a06-auth-status.vue') -Raw -Encoding utf8
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
$catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Raw -Encoding utf8
$runtime = Get-Content -LiteralPath (Join-Path $root 'tests/a06-auth-status-runtime-smoke.js') -Raw -Encoding utf8
if (-not ($pages.pages.path -contains 'pages/auth/a06-auth-status')) { throw 'A-06 route must remain declared in pages.json' }
if ($pages.pages.path -contains 'pages/auth/a06-auth-status') { throw 'A-06 is archived and must not remain declared in pages.json' }
if (-not (Test-Path -LiteralPath (Join-Path $root 'pages/auth/a06-auth-status.vue'))) { throw 'Archived A-06 source must be retained' }
if (-not ($pages.pages.path -contains 'pages/auth/a01-entry')) { throw 'A-06 must retain the A01 return target' }
Assert-Contains -Content $runtime -Expected 'A06-AUTH-STATUS-RUNTIME-SMOKE SKIP archived route' -Message 'A-06 runtime smoke must explicitly skip while the route is archived'
Assert-NotContains -Content $page -Unexpected 'ModulePage' -Message 'A-06 must not remain a ModulePage shell'
Assert-NotContains -Content $catalog -Unexpected 'a06:' -Message 'A-06 must not remain in the temporary page catalog'
@@ -37,12 +40,15 @@ foreach ($required in @(
'const goBack = () =>',
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'a02-login-panel.png',
'a01-primary-button.png',
'auth-divider-knot.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-dialog-v3.png',
'chevron-right.png',
'auth-login-outline.png',
'class="recovery-layer"'
'class="recovery-layer"',
'width: 184rpx;',
'height: 221rpx;',
'mode="aspectFit"'
)) {
Assert-Contains -Content $page -Expected $required -Message "Missing A-06 auth-status contract: $required"
}
@@ -54,6 +60,17 @@ foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'un
Assert-NotContains -Content $page -Unexpected $nativeUi -Message "A-06 must not use native UniApp feedback: $nativeUi"
}
Assert-NotContains -Content $page -Unexpected 'auth-design-scenery-v1.png' -Message 'A-06 must use the selected red-hall ink backdrop'
foreach ($obsoleteVisual in @(
'a01-primary-button.png',
'a02-login-panel.png',
'auth-divider-knot.png',
'font-family:'
)) {
Assert-NotContains -Content $page -Unexpected $obsoleteVisual -Message "A-06 must not retain obsolete visual: $obsoleteVisual"
}
$primarySkinCount = ([regex]::Matches($page, 'a01-scroll-primary-v3\.png')).Count
if ($primarySkinCount -ne 2) { throw "A-06 must use the approved primary skin exactly twice, found $primarySkinCount" }
foreach ($forbidden in @(
'(?s)\.status-panel\s*\{[^}]*\bborder\s*:',
+10 -1
View File
@@ -1,4 +1,13 @@
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const routeConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'pages.json'), 'utf8'))
const isActiveRoute = routeConfig.pages.some((item) => item.path === 'pages/auth/a06-auth-status')
if (!isActiveRoute) {
process.stdout.write('A06-AUTH-STATUS-RUNTIME-SMOKE SKIP archived route\n')
process.exit(0)
}
const baseUrl = 'http://localhost:5173/#/pages/auth/a06-auth-status'
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
@@ -60,7 +69,7 @@ const run = async () => {
await send('Page.enable')
await send('Runtime.enable')
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 640 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
await navigate(send, 'risk', '账号存在安全风险')
const metrics = await valueOf(send, `({
+4 -4
View File
@@ -9,7 +9,7 @@ $root = Split-Path -Parent $PSScriptRoot
$pagesPath = Join-Path $root 'pages.json'
$pageHeader = ConvertFrom-Utf8Base64 '6aG16Z2i57yW5Y+377ya'
$expectedPages = @(
'pages/auth/a01-entry', 'pages/auth/a04-register', 'pages/auth/a05-reset-password', 'pages/auth/a06-auth-status',
'pages/auth/a01-entry', 'pages/auth/a04-register', 'pages/auth/a05-reset-password',
'pages/genealogy/g01-my-genealogies', 'pages/genealogy/g03-create-genealogy', 'pages/genealogy/g05-genealogy-overview', 'pages/genealogy/g06-search-genealogies', 'pages/genealogy/g08-join-application', 'pages/genealogy/g09-my-applications', 'pages/genealogy/g10-application-review', 'pages/genealogy/g11-genealogy-settings', 'pages/genealogy/g12-generation-poems',
'pages/tree/t01-tree-overview', 'pages/tree/t03-member-profile', 'pages/tree/t04-add-relative', 'pages/tree/t05-edit-member', 'pages/tree/t06-edit-relationship', 'pages/tree/t07-member-directory', 'pages/tree/t08-member-states',
'pages/family/f01-family-feed', 'pages/family/f02-publish-feed', 'pages/family/f03-feed-detail', 'pages/family/f04-article-list', 'pages/family/f05-article-detail', 'pages/family/f06-article-editor', 'pages/family/f07-album-list', 'pages/family/f08-album-detail', 'pages/family/f09-media-upload', 'pages/family/f10-video-list',
@@ -20,11 +20,11 @@ $expectedPages = @(
$pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json
$actualPaths = @($pages.pages.path)
if ($actualPaths.Count -ne 53) {
throw "Expected 53 visual routes after the approved page-state merges, found $($actualPaths.Count)."
if ($actualPaths.Count -ne 52) {
throw "Expected 52 active visual routes after the approved page-state merges and A06 archive, found $($actualPaths.Count)."
}
if (Compare-Object -ReferenceObject $expectedPages -DifferenceObject $actualPaths) {
throw 'pages.json routes do not match the 53-route visual delivery contract.'
throw 'pages.json routes do not match the 52-route active visual delivery contract.'
}
foreach ($page in $expectedPages) {
+1
View File
@@ -117,6 +117,7 @@ if ($card -notmatch '(?s)\.card-title-row\s*\{.*?justify-content:\s*space-betwee
if ($card -notmatch '(?s)\.card-meta\s*\{.*?font-size:\s*26rpx') { throw 'G-01 list metadata is below the approved readable size.' }
if ($card -notmatch '(?s)\.card-updated\s*\{.*?font-size:\s*22rpx') { throw 'G-01 list update date is below the approved readable size.' }
if ($page -notmatch '(?s)\.create-action\s*>\s*text\s*\{.*?white-space:\s*nowrap') { throw 'G-01 create action text can wrap onto multiple lines.' }
if ($page -notmatch '(?s)\.application-record\s*\+\s*\.application-record\s*\{[^}]*margin-top:\s*12rpx') { throw 'G-01 adjacent application cards must keep the approved 12rpx separation.' }
$manifest = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json')
if ($manifest -notmatch '"statusbar"\s*:\s*\{\s*"immersed"\s*:\s*"supportedDevice"\s*,\s*"style"\s*:\s*"light"\s*,\s*"background"\s*:\s*"#B52E22"') { throw 'Android status bar is not configured for the cinnabar root header.' }