家谱APP首页样式设计完成,落地80%

This commit is contained in:
2026-07-13 00:23:04 +08:00
commit 1da4d8cf1f
117 changed files with 9476 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$errors = [System.Collections.Generic.List[string]]::new()
function Test-LocalImport {
param(
[string]$SourceFile,
[string]$Target,
[string[]]$Extensions
)
if ($Target -match '^(https?:|sass:)') { return $true }
if ($Target.StartsWith('@/')) {
$candidate = Join-Path $root $Target.Substring(2)
} elseif ($Target.StartsWith('./') -or $Target.StartsWith('../')) {
$candidate = Join-Path (Split-Path -Parent $SourceFile) $Target
} else {
return $true
}
if (Test-Path -LiteralPath $candidate) { return $true }
foreach ($extension in $Extensions) {
if (Test-Path -LiteralPath ($candidate + $extension)) { return $true }
}
return $false
}
$pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json') | ConvertFrom-Json
foreach ($page in $pages.pages) {
$pageFile = Join-Path $root ($page.path + '.vue')
if (-not (Test-Path -LiteralPath $pageFile)) {
$errors.Add("Route has no page file: $($page.path)")
}
}
$manifest = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json') | ConvertFrom-Json
if ($manifest.vueVersion -eq '3' -and -not (Test-Path -LiteralPath (Join-Path $root 'index.html'))) {
$errors.Add('Vue 3 project is missing index.html.')
}
$sourceFiles = Get-ChildItem -Path $root -Recurse -File | Where-Object {
$_.FullName -notmatch '\\unpackage\\' -and $_.Extension -in '.vue', '.js', '.scss'
}
foreach ($source in $sourceFiles) {
$content = Get-Content -Raw -Encoding UTF8 $source.FullName
foreach ($match in [regex]::Matches($content, '@(?:import|use|forward)\s+["''](?<target>[^"'']+)["'']')) {
$target = $match.Groups['target'].Value
if (-not (Test-LocalImport -SourceFile $source.FullName -Target $target -Extensions @('.scss'))) {
$errors.Add("Missing Sass import in $($source.FullName.Substring($root.Length + 1)): $target")
}
}
foreach ($match in [regex]::Matches($content, '(?:from|import)\s+["''](?<target>[^"'']+)["'']')) {
$target = $match.Groups['target'].Value
if (-not (Test-LocalImport -SourceFile $source.FullName -Target $target -Extensions @('.js', '.vue', '.json'))) {
$errors.Add("Missing module import in $($source.FullName.Substring($root.Length + 1)): $target")
}
}
}
if ($errors.Count -gt 0) {
throw "Compile audit failed:`n$($errors -join "`n")"
}
Write-Output 'PASS compile audit'
+114
View File
@@ -0,0 +1,114 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$requiredFiles = @('utils/config.js', 'utils/session.js', 'utils/genealogy-context.js', 'utils/api.js', 'utils/md5.js', 'pages/auth/login.vue', 'pages/lineage/first-person.vue', 'pages/content/list.vue', 'pages/content/editor.vue')
foreach ($file in $requiredFiles) {
if (-not (Test-Path (Join-Path $root $file))) {
throw "Missing core-flow file: $file"
}
}
$config = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/config.js')
foreach ($token in @('mode:', 'isMockMode')) {
if ($config -notmatch [regex]::Escape($token)) {
throw "Missing runtime mode contract: $token"
}
}
$context = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/genealogy-context.js')
foreach ($method in @('getCurrentGenealogyId', 'setCurrentGenealogyId', 'clearCurrentGenealogyId')) {
if ($context -notmatch [regex]::Escape($method)) {
throw "Missing genealogy context method: $method"
}
}
$api = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/api.js')
foreach ($method in @('unwrapResponse', 'sendSmsCode', 'loginWithPassword', 'loginWithSms')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing auth API method: $method"
}
}
foreach ($method in @('createPerson', 'getPerson')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing lineage API method: $method"
}
}
foreach ($method in @('getArticles', 'getAlbums', 'getCeremonies', 'getGrowthRecords', 'createFeed')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing family content API method: $method"
}
}
if ($api -notmatch 'getProfile') {
throw 'Missing current-user profile API method'
}
$login = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/auth/login.vue')
foreach ($token in @('calcMD5', 'loginWithPassword', 'password', 'isSubmitting')) {
if ($login -notmatch [regex]::Escape($token)) {
throw "Missing password login behavior: $token"
}
}
$pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json')
if ($pages -notmatch 'pages/lineage/first-person') {
throw 'Missing first-person page route'
}
foreach ($route in @('pages/content/list', 'pages/content/editor')) {
if ($pages -notmatch [regex]::Escape($route)) {
throw "Missing content page route: $route"
}
}
foreach ($pageFile in @('pages/genealogy/detail.vue', 'pages/tree/index.vue', 'pages/member/detail.vue')) {
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root $pageFile)
if ($page -match "id=1001|id=3") {
throw "Hard-coded route ID remains in $pageFile"
}
if ($page -notmatch 'genealogyContext') {
throw "Missing genealogy context in $pageFile"
}
}
foreach ($method in @('markNotificationRead', 'markAllNotificationsRead')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing notification API method: $method"
}
}
foreach ($pageFile in @('pages/genealogy/applications.vue', 'pages/notification/index.vue')) {
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root $pageFile)
if ($page -match '1001') {
throw "Hard-coded genealogy ID remains in $pageFile"
}
}
$applications = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/applications.vue')
if ($applications -notmatch 'genealogyContext') {
throw 'Missing genealogy context in applications page'
}
$notifications = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/notification/index.vue')
foreach ($method in @('markNotificationRead', 'markAllNotificationsRead')) {
if ($notifications -notmatch [regex]::Escape($method)) {
throw "Notification page does not use $method"
}
}
$family = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/family/index.vue')
foreach ($token in @('genealogyContext', 'openSection', 'pages/content/list', 'pages/content/editor')) {
if ($family -notmatch [regex]::Escape($token)) {
throw "Missing family content flow: $token"
}
}
$profile = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/profile/index.vue')
foreach ($token in @('appApi', 'getProfile', 'profile.name')) {
if ($profile -notmatch [regex]::Escape($token)) {
throw "Missing dynamic profile behavior: $token"
}
}
Write-Output 'PASS core-flow session and auth contract'
+35
View File
@@ -0,0 +1,35 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
Add-Type -AssemblyName System.Drawing
$assets = @(
'static/assets/icons/common/location-v1.png',
'static/assets/icons/common/member-meta-v1.png',
'static/assets/icons/common/admin-v1.png',
'static/assets/icons/genealogy/seal-current-frame-v1.png',
'static/assets/icons/genealogy/seal-row-frame-v1.png',
'static/assets/icons/action/create-cloud-v1.png'
)
foreach ($asset in $assets) {
$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
)
$visiblePixels = 0
for ($y = 0; $y -lt $height; $y++) {
for ($x = 0; $x -lt $width; $x++) {
if ($bitmap.GetPixel($x, $y).A -gt 24) { $visiblePixels++ }
}
}
$bitmap.Dispose()
if (($corners | Where-Object { $_ -ne 0 }).Count -ne 0) { throw "$asset has opaque outer corners." }
if ($visiblePixels -lt 80) { throw "$asset has no usable visible artwork." }
Write-Output "PASS $asset transparent corners and visible artwork"
}
+107
View File
@@ -0,0 +1,107 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$requiredAssets = @(
'static/assets/icons/brand/jiapu-seal-logo.png',
'static/assets/icons/brand/jiapu-seal-logo-header.png',
'static/assets/icons/common/notice-v3.png',
'static/assets/icons/common/chevron-right-v2.png',
'static/assets/icons/common/location-v1.png',
'static/assets/icons/common/member-meta-v1.png',
'static/assets/icons/common/admin-v1.png',
'static/assets/icons/genealogy/tree-v2.png',
'static/assets/icons/genealogy/members-v2.png',
'static/assets/icons/genealogy/generation-poem-v2.png',
'static/assets/icons/genealogy/application-v2.png',
'static/assets/icons/genealogy/seal-current-frame-v1.png',
'static/assets/icons/genealogy/seal-row-frame-v1.png',
'static/assets/icons/action/add-v2.png',
'static/assets/icons/action/create-cloud-v1.png',
'static/assets/icons/tab/genealogy-v4.png',
'static/assets/icons/tab/genealogy-active-v4.png',
'static/assets/icons/tab/family-v4.png',
'static/assets/icons/tab/family-active-v4.png',
'static/assets/icons/tab/profile-v4.png',
'static/assets/icons/tab/profile-active-v4.png',
'static/assets/backgrounds/genealogy-current-slip-frame.png',
'static/assets/backgrounds/genealogy-list-slip-frame.png',
'static/assets/backgrounds/header-cinnabar-texture-v2.jpg',
'static/assets/backgrounds/paper-rice-texture-v2.jpg',
'static/assets/backgrounds/footer-mountain-bamboo.png',
'static/assets/backgrounds/genealogy-section-divider-v2.png'
)
foreach ($asset in $requiredAssets) {
if (-not (Test-Path -LiteralPath (Join-Path $root $asset))) {
throw "Missing G-01 visual asset: $asset"
}
}
$header = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/PageHeader.vue')
$tabbar = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/AppTabbar.vue')
if ($header -notmatch 'jiapu-seal-logo-header\.png') { throw 'Header does not use the approved tight brand PNG.' }
if ($header -notmatch 'jiapu-seal-logo-header\.png') { throw 'Header does not use the tight header Logo asset.' }
if ($header -notmatch 'notice-v3\.png') { throw 'Header does not use the approved notice PNG.' }
if ($header -notmatch 'unreadCount\s*>\s*0') { throw 'Header badge is not conditional on unread count.' }
if ($header -notmatch 'class="header-hall"') { throw 'Header does not isolate the hall line-art as a low-contrast background layer.' }
if ($header -match 'background-image: url') { throw 'Header still applies hall line-art directly to the red header surface.' }
if ($header -notmatch 'opacity:\s*\.16') { throw 'Header hall line-art is not reduced to the approved background contrast.' }
if ($header -notmatch 'header-cinnabar-texture-v2\.jpg') { throw 'Header does not use the approved cinnabar texture asset.' }
if ($header -notmatch 'height:\s*184rpx') { throw 'Header height does not match the approved compact red-head proportion.' }
if ($tabbar -match '/static/icons/tab-') { throw 'Tabbar still references rejected legacy tab assets.' }
if ($tabbar -notmatch 'safe-area-inset-bottom') { throw 'Tabbar has no Android safe-area padding.' }
if ($tabbar -notmatch 'font-size:\s*28rpx') { throw 'Tabbar label size is below the approved G-01 size.' }
if ($tabbar -notmatch 'width:\s*56rpx') { throw 'Tabbar icon size is below the approved G-01 size.' }
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/index.vue')
if ($page -match "from '@/utils/api\.js'") { throw 'G-01 visual phase must not call the remote API.' }
foreach ($state in @('isLoading', 'hasGenealogies', 'createdGenealogies', 'joinedGenealogies')) {
if ($page -notmatch $state) { throw "G-01 is missing presentation state: $state" }
}
foreach ($asset in @('tree-v2.png', 'members-v2.png', 'generation-poem-v2.png', 'application-v2.png', 'add-v2.png')) {
if ($page -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume $asset" }
}
if ($page -notmatch 'genealogy-current-slip-frame\.png') { throw 'G-01 does not use the approved current genealogy frame asset.' }
foreach ($token in @('current-meta-item', 'current-seal-frame', 'location-v1.png', 'member-meta-v1.png', 'admin-v1.png', 'create-cloud-v1.png')) {
if ($page -notmatch $token) { throw "G-01 current panel is missing $token" }
}
if ($page -notmatch 'width:\s*82rpx') { throw 'G-01 shortcut icons remain below the reference visual size.' }
foreach ($asset in @('paper-rice-texture-v2.jpg', 'footer-mountain-bamboo.png', 'genealogy-section-divider-v2.png')) {
if ($page -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume the approved background asset: $asset" }
}
if ($page -match 'footer-mountain-bamboo-v2\.png') { throw 'G-01 still uses the solid mountain footer asset instead of the translucent watercolor landscape.' }
if ($page -notmatch '(?s)\.page-footer-landscape\s*\{.*?opacity:\s*\.48') { throw 'G-01 footer landscape does not use the approved visible watercolor contrast.' }
if ($page -match 'background:\s*rgba\(255,\s*249,\s*238,\s*\.88\)') { throw 'G-01 current genealogy card retains a rectangular pale backing behind its transparent corners.' }
$card = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/GenealogyCard.vue')
if ($card -notmatch 'genealogy-list-slip-frame\.png') { throw 'G-01 list rows do not use the approved slip frame asset.' }
if ($card -match 'background:\s*rgba\(255,\s*249,\s*238,\s*\.(9|96)\)') { throw 'G-01 list cards retain a rectangular pale backing behind their transparent corners.' }
foreach ($token in @('seal-row-frame-v1.png', 'card-detail-row', 'card-updated')) {
if ($card -notmatch $token) { throw "G-01 list card is missing $token" }
}
if ($card -match 'background:\s*#b93b2e') { throw 'G-01 list seal is still a plain CSS red block.' }
foreach ($asset in @('location-v1.png', 'member-meta-v1.png', 'admin-v1.png', 'seal-current-frame-v1.png', 'seal-row-frame-v1.png', 'create-cloud-v1.png')) {
if ($page -notmatch [regex]::Escape($asset) -and $card -notmatch [regex]::Escape($asset)) {
throw "G-01 does not consume $asset"
}
}
$mock = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'data/mock.js')
$fixtures = @(
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bmFtZTogJ+axpOawj+WutuiwsSc=')),
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bG9jYXRpb246ICfmsrPljZfCt+a0m+mYsyc=')),
'memberCount: 158',
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bmFtZTogJ+axpOawj+Wul+iwsSc=')),
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bG9jYXRpb246ICflsbHkuJzCt+a1juWugSc=')),
'memberCount: 286'
)
foreach ($fixture in $fixtures) {
if (-not $mock.Contains($fixture)) { throw "G-01 visual fixture is missing $fixture" }
}
if ($page -notmatch 'current-info-divider') { throw 'G-01 current panel is missing the reference title-to-meta divider.' }
if ($card -match 'calendar-v1\.png') { throw 'G-01 list shows a calendar icon that does not exist in the reference.' }
if ($card -match 'genealogy\.surname') { throw 'G-01 list seal still shows a surname instead of the fixed 家谱 seal.' }
if ($card -notmatch '(?s)\.card-side\s*\{.*?flex-direction:\s*row') { throw 'G-01 list role and chevron are not aligned on one horizontal baseline.' }
Write-Output 'PASS G-01 visual contract'
+11
View File
@@ -0,0 +1,11 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
try {
Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json') | ConvertFrom-Json | Out-Null
} catch {
throw 'manifest.json must be valid JSON.'
}
Write-Output 'PASS manifest JSON contract'
+80
View File
@@ -0,0 +1,80 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$requiredFiles = @(
'App.vue',
'main.js',
'manifest.json',
'pages.json',
'uni.scss',
'styles/global.scss',
'data/mock.js',
'utils/config.js',
'utils/api.js',
'components/AppTabbar.vue',
'components/PageHeader.vue',
'pages/auth/login.vue',
'pages/genealogy/index.vue',
'pages/genealogy/create.vue',
'pages/genealogy/detail.vue',
'pages/genealogy/search.vue',
'pages/genealogy/applications.vue',
'pages/tree/index.vue',
'pages/member/detail.vue',
'pages/family/index.vue',
'pages/profile/index.vue',
'pages/notification/index.vue',
'static/icons/tab-genealogy.png',
'static/icons/tab-family.png',
'static/icons/tab-profile.png'
)
$missing = $requiredFiles | Where-Object { -not (Test-Path (Join-Path $root $_)) }
if ($missing) {
throw "Missing phase-one files: $($missing -join ', ')"
}
$pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json') | ConvertFrom-Json
$paths = @($pages.pages | ForEach-Object { $_.path })
$requiredRoutes = @(
'pages/auth/login',
'pages/genealogy/index',
'pages/genealogy/create',
'pages/genealogy/detail',
'pages/genealogy/search',
'pages/genealogy/applications',
'pages/tree/index',
'pages/member/detail',
'pages/family/index',
'pages/profile/index',
'pages/notification/index'
)
$missingRoutes = $requiredRoutes | Where-Object { $_ -notin $paths }
if ($missingRoutes) {
throw "Missing page routes: $($missingRoutes -join ', ')"
}
$tokens = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'uni.scss')
foreach ($token in @('$brand-red:', '$ink:', '$paper:', '$gold:')) {
if ($tokens -notmatch [regex]::Escape($token)) {
throw "Missing design token: $token"
}
}
Add-Type -AssemblyName System.Drawing
foreach ($icon in @('tab-genealogy.png', 'tab-family.png', 'tab-profile.png')) {
$bitmap = [System.Drawing.Bitmap]::new((Join-Path $root "static/icons/$icon"))
try {
if (-not [System.Drawing.Image]::IsAlphaPixelFormat($bitmap.PixelFormat)) {
throw "Icon must keep an alpha channel: $icon"
}
if ($bitmap.GetPixel(0, 0).A -ne 0) {
throw "Icon corner must be transparent: $icon"
}
} finally {
$bitmap.Dispose()
}
}
Write-Output 'PASS phase-one Uni-App contract'
+16
View File
@@ -0,0 +1,16 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$uniScss = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'uni.scss')
foreach ($token in @('$brand-red:', '$ink:', '$paper:', '$gold:')) {
if ($uniScss -notmatch [regex]::Escape($token)) {
throw "uni.scss must directly define $token"
}
}
if ($uniScss -match '@(?:import|use|forward)\s+["''](?:\./|\.\./)') {
throw 'uni.scss must not use relative Sass imports because it is injected into every page stylesheet.'
}
Write-Output 'PASS uni.scss injection contract'
+11
View File
@@ -0,0 +1,11 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$manifest = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json') | ConvertFrom-Json
$entryPath = Join-Path $root 'index.html'
if ($manifest.vueVersion -eq '3' -and -not (Test-Path $entryPath)) {
throw 'Vue 3 uni-app projects require index.html at the project root.'
}
Write-Output 'PASS Vue 3 entry contract'