接口开始5%

This commit is contained in:
rain
2026-07-22 17:31:33 +08:00
parent eced3d1e6e
commit 9b0ad62df4
426 changed files with 20111 additions and 28472 deletions
-65
View File
@@ -1,65 +0,0 @@
param(
[string]$ManifestPath = 'docs/design/assets/a01-vnext/source/a01-psd-manifest.json',
[string]$OutputPath = 'docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd'
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$rootFullPath = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
function Resolve-WorkspacePath {
param(
[string]$Path,
[bool]$MustExist
)
$candidate = if ([IO.Path]::IsPathRooted($Path)) { $Path } else { Join-Path $root $Path }
$fullPath = [IO.Path]::GetFullPath($candidate)
if (-not $fullPath.StartsWith($rootFullPath, [StringComparison]::OrdinalIgnoreCase)) {
throw "Path must remain inside the workspace: $Path"
}
if ($MustExist -and -not (Test-Path -LiteralPath $fullPath -PathType Leaf)) {
throw "Required file does not exist: $fullPath"
}
return $fullPath
}
function ConvertTo-JavaScriptString {
param([string]$Value)
$escaped = $Value.Replace('\', '\\').Replace("'", "\'").Replace("`r", '\r').Replace("`n", '\n')
return "'$escaped'"
}
$resolvedManifest = Resolve-WorkspacePath -Path $ManifestPath -MustExist $true
$resolvedOutput = Resolve-WorkspacePath -Path $OutputPath -MustExist $false
$outputName = [IO.Path]::GetFileName($resolvedOutput)
if ($outputName -notmatch '^A01-layered-source-v[0-9]+\.psd$') {
throw "PSD output must use a versioned A01 source name: $outputName"
}
if (Test-Path -LiteralPath $resolvedOutput) {
throw "Refusing to overwrite an existing PSD: $resolvedOutput"
}
if (-not (Get-Process -Name Photoshop -ErrorAction SilentlyContinue)) {
throw 'Adobe Photoshop must already be running.'
}
$outputDirectory = Split-Path -Parent $resolvedOutput
if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
}
$jsxPath = Resolve-WorkspacePath -Path 'scripts/photoshop/a01-build-layered-psd.jsx' -MustExist $true
$app = New-Object -ComObject 'Photoshop.Application.150'
$bootstrap = @"
var JIAPU_A01_ROOT = $(ConvertTo-JavaScriptString $rootFullPath.TrimEnd([IO.Path]::DirectorySeparatorChar));
var JIAPU_A01_MANIFEST = $(ConvertTo-JavaScriptString $resolvedManifest);
var JIAPU_A01_PSD = $(ConvertTo-JavaScriptString $resolvedOutput);
$.evalFile(new File($(ConvertTo-JavaScriptString $jsxPath)));
"@
$app.DoJavaScript($bootstrap, @(), 1)
if (-not (Test-Path -LiteralPath $resolvedOutput -PathType Leaf)) {
throw "Photoshop did not create the expected PSD: $resolvedOutput"
}
Write-Output "A01-LAYERED-PSD BUILD PASS: $resolvedOutput"
-553
View File
@@ -1,553 +0,0 @@
const fs = require('fs')
const path = require('path')
const [url, selector, output, width = '360', height = '800', action] = process.argv.slice(2)
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|invalid-login|verification-dialog|g06-results|g06-empty|g06-invite|g03-*|fresh-navigation]')
}
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
const connect = async () => {
const pages = await (await fetch(`http://127.0.0.1:${chromeDebuggingPort}/json/list`)).json()
const page = pages.find((item) => item.type === 'page' && item.url.startsWith('http://localhost:5173'))
if (!page) throw new Error('Chrome debugging has no localhost:5173 page')
const socket = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, { once: true })
socket.addEventListener('error', reject, { once: true })
})
let id = 0
const pending = new Map()
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data)
const request = pending.get(message.id)
if (!request) return
pending.delete(message.id)
if (message.error) request.reject(new Error(message.error.message))
else request.resolve(message.result)
})
const send = (method, params = {}) => new Promise((resolve, reject) => {
id += 1
pending.set(id, { resolve, reject })
socket.send(JSON.stringify({ id, method, params }))
})
return { socket, send }
}
const waitForLoadedImages = async (send) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: 'Array.from(document.images).every((image) => image.complete && image.naturalWidth > 0)',
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error('Page images did not finish loading before screenshot')
await sleep(100)
}
}
const prepareA01LoginState = async (send, action) => {
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', {
expression: `(() => {
const tabs = document.querySelectorAll('.login-tab')
if (tabs.length < 2) return false
tabs[${tabIndex}].click()
return true
})()`,
returnByValue: true
})
if (prepared.result?.value) break
if (attempt === 29) throw new Error(`A01 login tabs did not render for ${action}`)
await sleep(100)
}
for (let attempt = 0; attempt < 30; attempt += 1) {
const active = await send('Runtime.evaluate', {
expression: `document.querySelectorAll('.login-tab')[${tabIndex}]?.classList.contains('active') === true`,
returnByValue: true
})
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 prepareG01State = async (send, action) => {
if (action === 'g01-add-dialog') {
await send('Runtime.evaluate', { expression: "document.querySelector('.create-action')?.click()" })
await waitForSelector(send, '.add-dialog-layer', 'G01 add dialog did not render')
return
}
if (action !== 'g01-switcher') return
await send('Runtime.evaluate', { expression: "document.querySelector('.current-slip')?.click()" })
await waitForSelector(send, '.genealogy-switcher-layer', 'G01 switcher did not render')
}
const waitForRequestedUrl = async (send, url) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `location.href === ${JSON.stringify(url)}`,
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error(`Browser did not navigate to the requested URL: ${url}`)
await sleep(100)
}
}
const waitForFreshDocument = async (send, documentTimeOrigin) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: 'performance.timeOrigin',
returnByValue: true
})
if (result.result?.value !== documentTimeOrigin) return
if (attempt === 29) throw new Error('Page did not reload into a fresh document before screenshot')
await sleep(100)
}
}
const prepareG06State = async (send, action) => {
if (action !== 'g06-results' && action !== 'g06-empty' && action !== 'g06-error' && action !== 'g06-invite' && action !== 'g06-invite-invalid') return
if (action === 'g06-invite' || action === 'g06-invite-invalid') {
const switched = await send('Runtime.evaluate', {
expression: `(() => {
const tab = document.querySelectorAll('.mode-tab')[1]
if (!tab) return false
tab.click()
return true
})()`,
returnByValue: true
})
if (!switched.result?.value) throw new Error('Could not switch G06 to invite mode')
await sleep(100)
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.invite-input input')
const button = document.querySelector('.invite-controls .search-action')
if (!input || !button) return false
input.value = ${JSON.stringify(action === 'g06-invite' ? 'JP2026' : 'INVALID')}
input.dispatchEvent(new Event('input', { bubbles: true }))
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error('Could not prepare G06 invite code')
await sleep(100)
await send('Runtime.evaluate', { expression: "document.querySelector('.invite-controls .search-action').click()" })
} else {
const keyword = action === 'g06-results' ? '汤' : action === 'g06-error' ? '失败' : '不存在的家谱'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.search-input input')
if (!input) return false
input.value = ${JSON.stringify(keyword)}
input.dispatchEvent(new Event('input', { bubbles: true }))
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
await sleep(100)
await send('Runtime.evaluate', { expression: "document.querySelector('.search-action').click()" })
}
const stateSelector = action === 'g06-results' ? '.search-results .genealogy-card' : action === 'g06-empty' ? '.search-empty' : action === 'g06-error' ? '.search-error' : action === 'g06-invite-invalid' ? '.invite-invalid' : '.invite-result .genealogy-card'
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `Boolean(document.querySelector(${JSON.stringify(stateSelector)}))`,
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error(`Expected G06 state did not render: ${action}`)
await sleep(100)
}
}
const waitForPageLoad = async (socket, trigger) => new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.removeEventListener('message', onMessage)
reject(new Error('Page reload did not emit Page.loadEventFired'))
}, 4000)
const onMessage = (event) => {
const message = JSON.parse(event.data)
if (message.method !== 'Page.loadEventFired') return
clearTimeout(timeout)
socket.removeEventListener('message', onMessage)
resolve()
}
socket.addEventListener('message', onMessage)
Promise.resolve().then(trigger).catch((error) => {
clearTimeout(timeout)
socket.removeEventListener('message', onMessage)
reject(error)
})
})
const waitForSelector = async (send, selector, message) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `Boolean(document.querySelector(${JSON.stringify(selector)}))`,
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error(message)
await sleep(100)
}
}
const waitForText = async (send, selector, text, message) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `document.querySelector(${JSON.stringify(selector)})?.textContent.includes(${JSON.stringify(text)}) === true`,
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error(message)
await sleep(50)
}
}
const prepareG03State = async (send, action) => {
const actions = new Set([
'g03-create-validation',
'g03-duplicate-reminder',
'g03-create-submitting',
'g03-create-error',
'g03-ancestor-validation',
'g03-ancestor-submitting',
'g03-ancestor-error',
'g03-ancestor-success'
])
if (!actions.has(action)) return
if (action === 'g03-create-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.flow-primary-action').click()" })
await waitForSelector(send, '.field-error', 'G03 create validation did not render')
return
}
if (action.startsWith('g03-create') || action === 'g03-duplicate-reminder') {
const name = action === 'g03-create-error' ? '失败' : '汤氏家谱'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const inputs = document.querySelectorAll('.field-row input')
if (inputs.length < 4) return false
const values = ['汤', ${JSON.stringify(name)}, '', '河南·洛阳']
inputs.forEach((input, index) => {
input.value = values[index]
input.dispatchEvent(new Event('input', { bubbles: true }))
})
document.querySelector('.flow-primary-action').click()
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
await waitForSelector(send, '.duplicate-reminder', `G03 duplicate reminder did not render for ${action}`)
if (action === 'g03-duplicate-reminder') return
await send('Runtime.evaluate', { expression: "document.querySelector('.duplicate-reminder__confirm').click()" })
if (action === 'g03-create-submitting') {
await waitForText(send, '.flow-primary-action__copy', '正在创建', 'G03 create submitting state did not render')
return
}
await waitForSelector(send, '.flow-error', 'G03 create error state did not render')
return
}
if (action === 'g03-ancestor-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.flow-primary-action').click()" })
await waitForSelector(send, '.field-error', 'G03 ancestor validation did not render')
return
}
const personName = action === 'g03-ancestor-error' ? '失败' : '汤远'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.field-row input')
if (!input) return false
input.value = ${JSON.stringify(personName)}
input.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.flow-primary-action').click()
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
if (action === 'g03-ancestor-submitting') {
await waitForText(send, '.flow-primary-action__copy', '正在保存', 'G03 ancestor submitting state did not render')
} else if (action === 'g03-ancestor-error') {
await waitForSelector(send, '.flow-error', 'G03 ancestor error state did not render')
} else {
await waitForSelector(send, '.flow-success-layer', 'G03 ancestor success state did not render')
}
}
const prepareG08State = async (send, action) => {
if (!['g08-validation', 'g08-submitting', 'g08-error', 'g08-success'].includes(action)) return
if (action === 'g08-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.join-action').click()" })
await waitForSelector(send, '.join-field-error', 'G08 validation errors did not render')
return
}
const realName = action === 'g08-error' ? '失败' : '汤明'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const inputs = document.querySelectorAll('.join-field input')
if (inputs.length < 2) return false
const values = [${JSON.stringify(realName)}, '堂侄']
inputs.forEach((input, index) => {
input.value = values[index]
input.dispatchEvent(new Event('input', { bubbles: true }))
})
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
await sleep(50)
await send('Runtime.evaluate', { expression: "document.querySelector('.join-action').click()" })
if (action === 'g08-submitting') {
await waitForText(send, '.join-action', '正在', 'G08 submitting state did not render')
} else if (action === 'g08-error') {
await waitForSelector(send, '.join-state--error', 'G08 error state did not render')
} else {
await waitForSelector(send, '.join-state--success', 'G08 success state did not render')
}
}
const prepareG09State = async (send, action) => {
if (!['g09-withdraw-dialog', 'g09-withdrawn'].includes(action)) return
await send('Runtime.evaluate', { expression: "document.querySelector('.application-card__action')?.click()" })
await waitForSelector(send, '.app-dialog-layer', 'G09 withdrawal dialog did not render')
if (action === 'g09-withdrawn') {
await send('Runtime.evaluate', { expression: "document.querySelector('.app-dialog__actions .app-button:last-child')?.click()" })
await waitForText(send, '.application-card__status', '已撤回', 'G09 withdrawn state did not render')
}
}
const prepareG10State = async (send, action) => {
const actions = ['g10-help', 'g10-approve-dialog', 'g10-approved', 'g10-reject-dialog', 'g10-rejected']
if (!actions.includes(action)) return
if (action === 'g10-help') {
await send('Runtime.evaluate', { expression: "document.querySelector('.header-action')?.click()" })
await waitForText(send, '.app-dialog__title', '审核说明', 'G10 help dialog did not render')
return
}
const isApprove = action === 'g10-approve-dialog' || action === 'g10-approved'
const selector = isApprove ? '.review-action:last-child' : '.review-action:first-child'
await send('Runtime.evaluate', { expression: `document.querySelector(${JSON.stringify(selector)})?.click()` })
await waitForSelector(send, '.app-dialog-layer', 'G10 audit confirmation did not render')
if (action === 'g10-approved' || action === 'g10-rejected') {
await send('Runtime.evaluate', { expression: "document.querySelector('.app-dialog__actions .app-button:last-child')?.click()" })
await waitForText(send, '.application-card__status', isApprove ? '已通过' : '已拒绝', 'G10 audit result did not render')
}
}
const prepareG11State = async (send, action) => {
if (!['g11-validation', 'g11-saved'].includes(action)) return
if (action === 'g11-validation') {
await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.settings-field input')
if (!input) return false
input.value = ''
input.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.settings-action')?.click()
return true
})()`,
returnByValue: true
})
await waitForSelector(send, '.settings-field-error', 'G11 validation state did not render')
return
}
await send('Runtime.evaluate', { expression: "document.querySelector('.settings-action')?.click()" })
await waitForSelector(send, '.settings-state--success', 'G11 saved state did not render')
await waitForSelector(send, '.settings-feedback', 'G11 saved feedback did not render')
}
const prepareG12State = async (send, action) => {
if (!['g12-validation', 'g12-save-error', 'g12-saved'].includes(action)) return
const value = action === 'g12-validation' ? '' : action === 'g12-save-error' ? '失败' : '启宗敦本继世传芳'
await send('Runtime.evaluate', {
expression: `(() => {
const textarea = document.querySelector('.poem-field textarea')
if (!textarea) return false
textarea.value = ${JSON.stringify(value)}
textarea.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.poem-editor__actions .poem-action:last-child')?.click()
return true
})()`,
returnByValue: true
})
if (action === 'g12-saved') {
await waitForSelector(send, '.poem-state--list', 'G12 saved list did not render')
await waitForSelector(send, '.poem-feedback', 'G12 saved feedback did not render')
} else {
await waitForSelector(send, '.poem-field-error', 'G12 validation or save error did not render')
}
}
const prepareT03T06State = async (send, action) => {
if (action !== 't06-conflict-help') return
await send('Runtime.evaluate', { expression: "document.querySelector('.conflict-actions .member-form-action:last-child')?.click()" })
await waitForSelector(send, '.app-dialog-layer', 'T06 conflict help dialog did not render')
}
const prepareFSeriesState = async (send, action) => {
if (action === 'f02-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.publish-form .app-button')?.click()" })
await waitForSelector(send, '.app-toast', 'F02 empty-content toast did not render')
return
}
if (action !== 'module-preview-toast') return
await send('Runtime.evaluate', { expression: "(document.querySelector('.list-card') || document.querySelector('.settings-row'))?.click()" })
await waitForSelector(send, '.app-toast', 'ModulePage preview toast did not render')
}
const prepareNSeriesState = async (send, action) => {
if (action === 'n01-read-first') {
await send('Runtime.evaluate', { expression: "document.querySelector('.notice-card')?.click()" })
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: "document.querySelector('.notice-card')?.textContent.includes('未读提醒') === false",
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error('N01 first notice did not enter the read state')
await sleep(50)
}
return
}
if (action !== 'n01-mark-all') return
await send('Runtime.evaluate', { expression: "document.querySelector('.header-action')?.click()" })
await waitForSelector(send, '.app-toast', 'N01 mark-all-read toast did not render')
}
const capture = async () => {
const { socket, send } = await connect()
const viewportWidth = Number(width)
const viewportHeight = Number(height)
try {
await send('Page.enable')
await send('Runtime.enable')
await send('Emulation.setDeviceMetricsOverride', {
width: viewportWidth,
height: viewportHeight,
deviceScaleFactor: 1,
mobile: true,
screenWidth: viewportWidth,
screenHeight: viewportHeight
})
const timeOriginResult = await send('Runtime.evaluate', {
expression: 'performance.timeOrigin',
returnByValue: true
})
const documentTimeOrigin = timeOriginResult.result?.value
await send('Page.navigate', { url })
await waitForRequestedUrl(send, url)
if (action !== 'fresh-navigation') {
await waitForPageLoad(socket, () => send('Page.reload'))
await waitForFreshDocument(send, documentTimeOrigin)
}
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `Boolean(document.querySelector(${JSON.stringify(selector)}))`,
returnByValue: true
})
if (result.result?.value) break
if (attempt === 29) throw new Error(`Expected page selector did not render: ${selector}`)
await sleep(100)
}
await send('Runtime.evaluate', { expression: 'scrollTo(0, 0)' })
await prepareA01LoginState(send, action)
await prepareG01State(send, action)
await prepareG06State(send, action)
await prepareG03State(send, action)
await prepareG08State(send, action)
await prepareG09State(send, action)
await prepareG10State(send, action)
await prepareG11State(send, action)
await prepareG12State(send, action)
await prepareT03T06State(send, action)
await prepareFSeriesState(send, action)
await prepareNSeriesState(send, action)
await waitForLoadedImages(send)
await sleep(action?.endsWith('-submitting') ? 80 : renderSettleMilliseconds)
await send('Page.bringToFront')
const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: action !== 'from-view', captureBeyondViewport: false })
fs.mkdirSync(path.dirname(output), { recursive: true })
fs.writeFileSync(output, Buffer.from(screenshot.data, 'base64'))
process.stdout.write(`CAPTURED ${output}\n`)
} finally {
if (action !== 'keep-view') {
try { await send('Emulation.clearDeviceMetricsOverride') } catch (_) {}
}
socket.close()
}
}
capture().catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`)
process.exit(1)
})
@@ -1,43 +0,0 @@
param(
[Parameter(Mandatory = $true)][string]$Output,
[Parameter(Mandatory = $true)][string[]]$InputPaths
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
if ($InputPaths.Count -lt 2) { throw 'Contact sheet requires at least two images.' }
$images = @()
try {
foreach ($inputPath in $InputPaths) {
$resolved = (Resolve-Path -LiteralPath $inputPath).Path
$images += [System.Drawing.Bitmap]::FromFile($resolved)
}
$width = ($images | Measure-Object -Property Width -Sum).Sum
$height = ($images | Measure-Object -Property Height -Maximum).Maximum
$canvas = [System.Drawing.Bitmap]::new($width, $height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
try {
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
try {
$graphics.Clear([System.Drawing.Color]::FromArgb(247, 240, 228))
$offsetX = 0
foreach ($image in $images) {
$graphics.DrawImageUnscaled($image, $offsetX, 0)
$offsetX += $image.Width
}
} finally {
$graphics.Dispose()
}
$outputPath = [System.IO.Path]::GetFullPath($Output)
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($outputPath)) | Out-Null
$canvas.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)
Write-Output "CONTACT-SHEET $outputPath"
} finally {
$canvas.Dispose()
}
} finally {
foreach ($image in $images) { $image.Dispose() }
}
-45
View File
@@ -1,45 +0,0 @@
param(
[string]$ManifestPath = 'docs/design/assets/a01-vnext/source/a01-psd-manifest.json',
[string]$PsdPath = 'docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd'
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$rootFullPath = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
function Resolve-WorkspaceFile {
param([string]$Path)
$candidate = if ([IO.Path]::IsPathRooted($Path)) { $Path } else { Join-Path $root $Path }
$fullPath = [IO.Path]::GetFullPath($candidate)
if (-not $fullPath.StartsWith($rootFullPath, [StringComparison]::OrdinalIgnoreCase)) {
throw "Path must remain inside the workspace: $Path"
}
if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) {
throw "Required file does not exist: $fullPath"
}
return $fullPath
}
function ConvertTo-JavaScriptString {
param([string]$Value)
$escaped = $Value.Replace('\', '\\').Replace("'", "\'").Replace("`r", '\r').Replace("`n", '\n')
return "'$escaped'"
}
$resolvedManifest = Resolve-WorkspaceFile -Path $ManifestPath
$resolvedPsd = Resolve-WorkspaceFile -Path $PsdPath
$jsxPath = Resolve-WorkspaceFile -Path 'scripts/photoshop/a01-export-layered-assets.jsx'
if (-not (Get-Process -Name Photoshop -ErrorAction SilentlyContinue)) {
throw 'Adobe Photoshop must already be running.'
}
$app = New-Object -ComObject 'Photoshop.Application.150'
$bootstrap = @"
var JIAPU_A01_ROOT = $(ConvertTo-JavaScriptString $rootFullPath.TrimEnd([IO.Path]::DirectorySeparatorChar));
var JIAPU_A01_MANIFEST = $(ConvertTo-JavaScriptString $resolvedManifest);
var JIAPU_A01_PSD = $(ConvertTo-JavaScriptString $resolvedPsd);
$.evalFile(new File($(ConvertTo-JavaScriptString $jsxPath)));
"@
$app.DoJavaScript($bootstrap, @(), 1)
Write-Output 'A01-LAYERED-ASSETS EXPORT PASS'
@@ -1,63 +0,0 @@
#target photoshop
app.displayDialogs = DialogModes.NO;
var createdDocument = null;
var previousRulerUnits = app.preferences.rulerUnits;
function readJson(filePath) {
var file = File(filePath);
if (!file.exists) {
throw new Error('Manifest does not exist: ' + filePath);
}
file.encoding = 'UTF8';
if (!file.open('r')) {
throw new Error('Unable to open manifest: ' + filePath);
}
var content = file.read();
file.close();
return eval('(' + content + ')');
}
try {
app.preferences.rulerUnits = Units.PIXELS;
var manifest = readJson(JIAPU_A01_MANIFEST);
var psdFile = File(JIAPU_A01_PSD);
if (psdFile.exists) {
throw new Error('Refusing to overwrite existing PSD: ' + psdFile.fsName);
}
createdDocument = app.documents.add(
manifest.canvas.width,
manifest.canvas.height,
manifest.canvas.resolution,
'JIAPU_A01_SCRIPT_SOURCE',
NewDocumentMode.RGB,
DocumentFill.TRANSPARENT
);
for (var groupIndex = manifest.groups.length - 1; groupIndex >= 0; groupIndex--) {
var groupSpec = manifest.groups[groupIndex];
var group = createdDocument.layerSets.add();
group.name = groupSpec.name;
group.visible = groupSpec.visible;
}
if (createdDocument.artLayers.length > 0) {
createdDocument.artLayers[0].remove();
}
var saveOptions = new PhotoshopSaveOptions();
saveOptions.layers = true;
createdDocument.saveAs(psdFile, saveOptions, true, Extension.LOWERCASE);
createdDocument.close(SaveOptions.DONOTSAVECHANGES);
createdDocument = null;
} catch (error) {
if (createdDocument !== null) {
createdDocument.close(SaveOptions.DONOTSAVECHANGES);
}
throw error;
} finally {
app.preferences.rulerUnits = previousRulerUnits;
}
@@ -1,178 +0,0 @@
#target photoshop
app.displayDialogs = DialogModes.NO;
app.preferences.rulerUnits = Units.PIXELS;
var ROOT = new Folder(JIAPU_A01_ROOT).fsName;
var OUTPUT = new Folder(JIAPU_A01_OUTPUT_DIR);
var PREVIEW_VERSION = typeof JIAPU_A01_PREVIEW_VERSION === 'undefined' ? 'v2' : JIAPU_A01_PREVIEW_VERSION;
if (!OUTPUT.exists) {
OUTPUT.create();
}
function assetPath(relativePath) {
return ROOT + '/' + relativePath;
}
function color(hex) {
var value = hex.replace('#', '');
var result = new SolidColor();
result.rgb.red = parseInt(value.substring(0, 2), 16);
result.rgb.green = parseInt(value.substring(2, 4), 16);
result.rgb.blue = parseInt(value.substring(4, 6), 16);
return result;
}
function placeImage(document, sourcePath, layerName, x, y, width, height, group) {
var sourceDocument = app.open(File(sourcePath));
var layer = sourceDocument.activeLayer.duplicate(document, ElementPlacement.PLACEATBEGINNING);
sourceDocument.close(SaveOptions.DONOTSAVECHANGES);
app.activeDocument = document;
layer.name = layerName;
var bounds = layer.bounds;
var currentWidth = bounds[2].as('px') - bounds[0].as('px');
var currentHeight = bounds[3].as('px') - bounds[1].as('px');
layer.resize(width / currentWidth * 100, height / currentHeight * 100, AnchorPosition.TOPLEFT);
bounds = layer.bounds;
layer.translate(x - bounds[0].as('px'), y - bounds[1].as('px'));
if (group) {
layer.move(group, ElementPlacement.PLACEATBEGINNING);
}
return layer;
}
function addText(document, group, layerName, contents, x, y, size, hex, alignment) {
app.activeDocument = document;
var layer = document.artLayers.add();
layer.kind = LayerKind.TEXT;
layer.name = layerName;
layer.textItem.contents = contents;
layer.textItem.font = 'KaiTi';
layer.textItem.size = UnitValue(size, 'px');
layer.textItem.color = color(hex);
layer.textItem.position = [UnitValue(x, 'px'), UnitValue(y, 'px')];
layer.textItem.justification = alignment === 'left' ? Justification.LEFT : Justification.CENTER;
layer.textItem.antiAliasMethod = AntiAlias.SMOOTH;
layer.move(group, ElementPlacement.PLACEATBEGINNING);
return layer;
}
function addRect(document, group, layerName, x, y, width, height, hex) {
app.activeDocument = document;
var layer = document.artLayers.add();
layer.name = layerName;
document.selection.select([
[x, y],
[x + width, y],
[x + width, y + height],
[x, y + height]
]);
document.selection.fill(color(hex));
document.selection.deselect();
layer.move(group, ElementPlacement.PLACEATBEGINNING);
return layer;
}
function savePsd(document, filePath) {
var options = new PhotoshopSaveOptions();
options.layers = true;
document.saveAs(File(filePath), options, true, Extension.LOWERCASE);
}
function savePng(document, filePath) {
var options = new PNGSaveOptions();
options.interlaced = false;
document.saveAs(File(filePath), options, true, Extension.LOWERCASE);
}
function compose(stateName) {
var document = app.documents.add(1236, 2745, 72, 'A01-' + stateName, NewDocumentMode.RGB, DocumentFill.WHITE);
var common = document.layerSets.add();
common.name = '00-common';
var state = document.layerSets.add();
state.name = stateName === 'password' ? '10-password' : '10-sms';
addRect(document, common, 'page-background', 0, 0, 1236, 2745, '#f4eadc');
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-header-background-candidate-v3.png'), 'ancestral-header', 0, 0, 1236, 510, common);
// 设计预览使用完整母图,避免拆片尚未定稿时产生重叠接缝;运行时切片另行验收。
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-scroll-master-transparent-candidate-v4-1236x2745.png'), 'scroll-master', 29, 430, 1178, 2315, common);
placeImage(document, assetPath('static/assets/foundation/transparent/brand-seal.png'), 'brand-seal', 510, 105, 216, 259, common);
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-title-ornament-candidate-v2.png'), 'title-ornament', 430, 700, 376, 130, common);
addText(document, common, 'page-title', '\u767b\u5f55\u5bb6\u8c31', 618, 960, 126, '#721417', 'center');
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-divider-ornament-candidate-v2.png'), 'title-divider', 348, 995, 540, 90, common);
addText(document, common, 'password-tab', '\u5bc6\u7801\u767b\u5f55', 390, 1165, 70, stateName === 'password' ? '#b10f18' : '#aa8240', 'center');
addText(document, common, 'sms-tab', '\u9a8c\u8bc1\u7801\u767b\u5f55', 846, 1165, 70, stateName === 'sms' ? '#b10f18' : '#aa8240', 'center');
addRect(document, common, 'tab-rule', 180, 1210, 876, 3, '#d7b37a');
addRect(document, common, 'active-tab-rule', stateName === 'password' ? 300 : 756, 1200, 180, 12, '#c3151d');
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-icon-phone-candidate-v1.png'), 'phone-icon', 190, 1270, 100, 100, state);
addText(document, state, 'phone-placeholder', '\u624b\u673a\u53f7', 330, 1355, 60, '#8b8580', 'left');
addRect(document, state, 'phone-line', 180, 1440, 876, 3, '#d7b37a');
if (stateName === 'password') {
placeImage(document, assetPath('static/assets/modules/auth/transparent/a01-icon-lock-v1.png'), 'lock-icon', 196, 1518, 82, 98, state);
addText(document, state, 'password-placeholder', '\u5bc6\u7801', 330, 1590, 60, '#8b8580', 'left');
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-icon-eye-closed-pupil-candidate-v1.png'), 'eye-closed-pupil', 952, 1530, 84, 70, state);
addRect(document, state, 'password-line', 180, 1665, 876, 3, '#d7b37a');
addText(document, state, 'forgot-password', '\u5fd8\u8bb0\u5bc6\u7801', 1020, 1745, 48, '#b10f18', 'center');
} else {
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-icon-sms-three-dots-candidate-v1.png'), 'sms-three-dots', 190, 1510, 100, 100, state);
addText(document, state, 'code-placeholder', '\u9a8c\u8bc1\u7801', 330, 1590, 60, '#8b8580', 'left');
addText(document, state, 'get-code', '\u83b7\u53d6\u9a8c\u8bc1\u7801', 930, 1590, 48, '#b10f18', 'center');
addRect(document, state, 'code-line', 180, 1665, 876, 3, '#d7b37a');
}
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-primary-button-master-candidate-v2.png'), 'primary-button-skin', 180, 1790, 876, 195, common);
addText(document, common, 'login-label', '\u767b\u5f55', 618, 1925, 82, '#ffffff', 'center');
addRect(document, common, 'other-left-line', 190, 2072, 260, 3, '#d7b37a');
addRect(document, common, 'other-right-line', 786, 2072, 260, 3, '#d7b37a');
addText(document, common, 'other-methods', '\u5176\u4ed6\u767b\u5f55\u65b9\u5f0f', 618, 2100, 50, '#aa8240', 'center');
placeImage(document, assetPath('docs/design/assets/a01-vnext/candidates/a01-secondary-button-master-candidate-v2.png'), 'secondary-button-skin', 180, 2160, 876, 180, common);
placeImage(document, assetPath('static/assets/foundation/transparent/auth-wechat.png'), 'wechat-icon', 365, 2190, 105, 105, common);
addText(document, common, 'wechat-label', '\u5fae\u4fe1\u767b\u5f55', 690, 2290, 66, '#30241d', 'center');
addText(document, common, 'register-copy', '\u8fd8\u6ca1\u6709\u8d26\u53f7\uff1f \u6ce8\u518c\u8d26\u53f7', 618, 2425, 46, '#4c3b30', 'center');
placeImage(document, assetPath('static/assets/modules/auth/transparent/a02-agreement-unchecked.png'), 'agreement-unchecked', 180, 2460, 66, 66, common);
addText(document, common, 'agreement-copy', '\u6211\u5df2\u9605\u8bfb\u5e76\u540c\u610f\u300a\u7528\u6237\u534f\u8bae\u300b\u4e0e\u300a\u9690\u79c1\u653f\u7b56\u300b', 270, 2515, 36, '#4c3b30', 'left');
var baseName = stateName === 'password' ? 'A01-page-password-' + PREVIEW_VERSION : 'A01-page-sms-' + PREVIEW_VERSION;
savePsd(document, OUTPUT.fsName + '/' + baseName + '.psd');
savePng(document, OUTPUT.fsName + '/' + baseName + '-1236x2745.png');
document.close(SaveOptions.DONOTSAVECHANGES);
}
function makeContactSheet() {
var contact = null;
var step = 'create';
try {
contact = app.documents.add(2472, 2745, 72, 'A01-contact-sheet', NewDocumentMode.RGB, DocumentFill.WHITE);
step = 'place-password';
placeImage(contact, OUTPUT.fsName + '/A01-page-password-' + PREVIEW_VERSION + '-1236x2745.png', 'password', 0, 0, 1236, 2745, null);
step = 'place-sms';
placeImage(contact, OUTPUT.fsName + '/A01-page-sms-' + PREVIEW_VERSION + '-1236x2745.png', 'sms', 1236, 0, 1236, 2745, null);
step = 'resize';
contact.resizeImage(UnitValue(824, 'px'), UnitValue(915, 'px'), null, ResampleMethod.BICUBICSHARPER);
step = 'save';
savePng(contact, OUTPUT.fsName + '/A01-page-password-sms-contact-' + PREVIEW_VERSION + '-824x915.png');
contact.close(SaveOptions.DONOTSAVECHANGES);
contact = null;
} catch (error) {
if (contact) {
contact.close(SaveOptions.DONOTSAVECHANGES);
}
throw new Error('CONTACT_' + step + ': ' + error.message);
}
}
if (typeof JIAPU_A01_CONTACT_ONLY === 'undefined' || !JIAPU_A01_CONTACT_ONLY) {
compose('password');
compose('sms');
}
if (typeof JIAPU_A01_SKIP_CONTACT === 'undefined' || !JIAPU_A01_SKIP_CONTACT) {
makeContactSheet();
}
'A01_PAGE_PREVIEWS_OK';
@@ -1,38 +0,0 @@
#target photoshop
app.displayDialogs = DialogModes.NO;
var openedDocument = null;
function readJson(filePath) {
var file = File(filePath);
if (!file.exists) {
throw new Error('Manifest does not exist: ' + filePath);
}
file.encoding = 'UTF8';
if (!file.open('r')) {
throw new Error('Unable to open manifest: ' + filePath);
}
var content = file.read();
file.close();
return eval('(' + content + ')');
}
try {
var manifest = readJson(JIAPU_A01_MANIFEST);
var psdFile = File(JIAPU_A01_PSD);
if (!psdFile.exists) {
throw new Error('PSD does not exist: ' + psdFile.fsName);
}
openedDocument = app.open(psdFile);
if (manifest.exports.length !== 0) {
throw new Error('Layer export is not implemented until assets are approved.');
}
openedDocument.close(SaveOptions.DONOTSAVECHANGES);
openedDocument = null;
} catch (error) {
if (openedDocument !== null) {
openedDocument.close(SaveOptions.DONOTSAVECHANGES);
}
throw error;
}
@@ -1,20 +0,0 @@
#target photoshop
var runnerFile = new File($.fileName);
var root = runnerFile.parent.parent.parent.fsName;
var JIAPU_A01_ROOT = root;
var JIAPU_A01_OUTPUT_DIR = root + '/docs/design/assets/a01-vnext/review/page-v2';
var JIAPU_A01_PREVIEW_VERSION = 'v2';
var JIAPU_A01_SKIP_CONTACT = true;
try {
$.evalFile(new File(root + '/scripts/photoshop/a01-compose-page-preview.jsx'));
} catch (error) {
var log = new File(root + '/docs/design/assets/a01-vnext/review/page-v2/photoshop-error.log');
log.encoding = 'UTF8';
log.open('w');
log.write('number=' + error.number + '\nline=' + error.line + '\nmessage=' + error.message + '\nsource=' + error.source);
log.close();
throw error;
}