160 lines
7.7 KiB
JavaScript
160 lines
7.7 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import sharp from 'sharp'
|
|
|
|
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
|
|
const workspace = path.resolve(scriptDirectory, '..', '..')
|
|
const manifestPath = path.join(workspace, 'design-pipeline', 'manifests', 'a01.json')
|
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
const generatedDirectory = path.join(workspace, 'design-pipeline', 'generated', 'a01')
|
|
|
|
const resolveWorkspacePath = (relativePath) => {
|
|
const absolutePath = path.resolve(workspace, relativePath)
|
|
const relative = path.relative(workspace, absolutePath)
|
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
throw new Error(`路径越出工作区:${relativePath}`)
|
|
}
|
|
return absolutePath
|
|
}
|
|
|
|
const sha256 = (buffer) => createHash('sha256').update(buffer).digest('hex')
|
|
|
|
const writeVersionedFile = async (outputPath, buffer) => {
|
|
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
try {
|
|
const current = await readFile(outputPath)
|
|
if (sha256(current) !== sha256(buffer)) {
|
|
throw new Error(`拒绝覆盖内容不同的版本化输出:${path.relative(workspace, outputPath)}`)
|
|
}
|
|
return
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') throw error
|
|
}
|
|
await writeFile(outputPath, buffer)
|
|
}
|
|
|
|
const runtimeAssets = new Map()
|
|
const reportAssets = []
|
|
|
|
for (const asset of manifest.assets) {
|
|
const sourcePath = resolveWorkspacePath(asset.source)
|
|
const outputPath = resolveWorkspacePath(asset.output)
|
|
await stat(sourcePath)
|
|
const pipeline = sharp(sourcePath).resize(asset.width, asset.height, { fit: 'fill' })
|
|
if (!asset.alpha) pipeline.flatten({ background: '#f4eadc' })
|
|
const buffer = await pipeline.png({ compressionLevel: 9, adaptiveFiltering: true, palette: false }).toBuffer()
|
|
await writeVersionedFile(outputPath, buffer)
|
|
runtimeAssets.set(asset.id, buffer)
|
|
const metadata = await sharp(buffer).metadata()
|
|
reportAssets.push({
|
|
id: asset.id,
|
|
output: asset.output,
|
|
width: metadata.width,
|
|
height: metadata.height,
|
|
bytes: buffer.length,
|
|
sha256: sha256(buffer)
|
|
})
|
|
}
|
|
|
|
const loadShared = async (key) => readFile(resolveWorkspacePath(manifest.shared[key]))
|
|
const shared = {
|
|
brandSeal: await loadShared('brandSeal'),
|
|
lock: await loadShared('lock'),
|
|
wechat: await loadShared('wechat'),
|
|
agreementUnchecked: await loadShared('agreementUnchecked')
|
|
}
|
|
|
|
const svgLayer = (mode) => Buffer.from(`
|
|
<svg width="412" height="915" xmlns="http://www.w3.org/2000/svg">
|
|
<style>
|
|
.k { font-family: KaiTi, STKaiti, serif; }
|
|
.red { fill: #9f1717; }
|
|
.gold { fill: #a97936; }
|
|
.body { fill: #51443b; }
|
|
.muted { fill: #9a8e84; }
|
|
</style>
|
|
<text x="206" y="321" text-anchor="middle" class="k red" font-size="36">登录家谱</text>
|
|
<text x="130" y="389" text-anchor="middle" class="k ${mode === 'password' ? 'red' : 'gold'}" font-size="20">密码登录</text>
|
|
<text x="282" y="389" text-anchor="middle" class="k ${mode === 'sms' ? 'red' : 'gold'}" font-size="20">验证码登录</text>
|
|
<line x1="61" y1="404" x2="351" y2="404" stroke="#d1a965" stroke-width="1"/>
|
|
<line x1="${mode === 'password' ? 99 : 251}" y1="401" x2="${mode === 'password' ? 161 : 313}" y2="401" stroke="#bd151b" stroke-width="4"/>
|
|
<text x="112" y="449" class="k muted" font-size="18">手机号</text>
|
|
<line x1="61" y1="481" x2="351" y2="481" stroke="#d1a965" stroke-width="1"/>
|
|
<text x="112" y="559" class="k muted" font-size="18">${mode === 'password' ? '密码' : '验证码'}</text>
|
|
${mode === 'password'
|
|
? '<text x="349" y="590" text-anchor="end" class="k red" font-size="15">忘记密码</text>'
|
|
: '<text x="349" y="559" text-anchor="end" class="k red" font-size="15">获取验证码</text>'}
|
|
<line x1="61" y1="576" x2="351" y2="576" stroke="#d1a965" stroke-width="1"/>
|
|
<text x="206" y="646" text-anchor="middle" class="k" fill="#fffaf2" font-size="26">登录</text>
|
|
<line x1="62" y1="692" x2="151" y2="692" stroke="#d1a965"/>
|
|
<line x1="261" y1="692" x2="350" y2="692" stroke="#d1a965"/>
|
|
<text x="206" y="699" text-anchor="middle" class="k gold" font-size="15">其他登录方式</text>
|
|
<text x="219" y="761" text-anchor="middle" class="k body" font-size="20">微信登录</text>
|
|
<text x="206" y="812" text-anchor="middle" class="k body" font-size="14">还没有账号? <tspan class="red">注册账号</tspan></text>
|
|
<text x="93" y="851" class="k body" font-size="11">我已阅读并同意《用户协议》与《隐私政策》</text>
|
|
</svg>`)
|
|
|
|
const sized = async (input, width, height) => sharp(input).resize(width, height, { fit: 'fill' }).png().toBuffer()
|
|
|
|
const buildBasePreview = async (mode) => {
|
|
const stateIcon = mode === 'password' ? shared.lock : runtimeAssets.get('smsThreeDots')
|
|
const rightIcon = mode === 'password' ? runtimeAssets.get('eyeClosedPupil') : null
|
|
const layers = [
|
|
{ input: await sized(runtimeAssets.get('header'), 412, 170), left: 0, top: 0 },
|
|
{ input: await sized(runtimeAssets.get('scroll'), 392, 772), left: 10, top: 143 },
|
|
{ input: await sized(shared.brandSeal, 72, 86), left: 170, top: 35 },
|
|
{ input: await sized(runtimeAssets.get('titleOrnament'), 126, 44), left: 143, top: 235 },
|
|
{ input: await sized(runtimeAssets.get('divider'), 180, 30), left: 116, top: 330 },
|
|
{ input: await sized(runtimeAssets.get('phone'), 40, 40), left: 68, top: 420 },
|
|
{ input: await sized(stateIcon, 40, 40), left: 68, top: 522 },
|
|
{ input: await sized(runtimeAssets.get('primaryButton'), 292, 65), left: 60, top: 608 },
|
|
{ input: await sized(runtimeAssets.get('secondaryButton'), 292, 60), left: 60, top: 718 },
|
|
{ input: await sized(shared.wechat, 36, 36), left: 126, top: 730 },
|
|
{ input: await sized(shared.agreementUnchecked, 24, 24), left: 62, top: 831 },
|
|
{ input: svgLayer(mode), left: 0, top: 0 }
|
|
]
|
|
if (rightIcon) layers.splice(7, 0, { input: await sized(rightIcon, 42, 35), left: 312, top: 525 })
|
|
return sharp({ create: { width: 412, height: 915, channels: 4, background: '#f4eadc' } })
|
|
.composite(layers)
|
|
.png({ compressionLevel: 9, adaptiveFiltering: true })
|
|
.toBuffer()
|
|
}
|
|
|
|
await mkdir(generatedDirectory, { recursive: true })
|
|
const passwordPreview = await buildBasePreview('password')
|
|
const smsPreview = await buildBasePreview('sms')
|
|
|
|
const writeGenerated = async (relativePath, buffer) => {
|
|
const outputPath = resolveWorkspacePath(relativePath)
|
|
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
await writeFile(outputPath, buffer)
|
|
}
|
|
|
|
await writeGenerated('design-pipeline/generated/a01/A01-password-412x915.png', passwordPreview)
|
|
await writeGenerated('design-pipeline/generated/a01/A01-sms-412x915.png', smsPreview)
|
|
const contact = await sharp({ create: { width: 824, height: 915, channels: 4, background: '#ffffff' } })
|
|
.composite([{ input: passwordPreview, left: 0, top: 0 }, { input: smsPreview, left: 412, top: 0 }])
|
|
.png({ compressionLevel: 9, adaptiveFiltering: true })
|
|
.toBuffer()
|
|
await writeGenerated('design-pipeline/generated/a01/A01-password-sms-contact-824x915.png', contact)
|
|
|
|
for (const preview of manifest.previews.filter((item) => item.width !== 412 && item.width !== 824)) {
|
|
const compact = await sharp(passwordPreview)
|
|
.resize(preview.width, preview.height, { fit: 'fill' })
|
|
.png({ compressionLevel: 9, adaptiveFiltering: true })
|
|
.toBuffer()
|
|
await writeGenerated(preview.output, compact)
|
|
}
|
|
|
|
const buildReport = {
|
|
schemaVersion: 1,
|
|
manifest: path.relative(workspace, manifestPath).replaceAll('\\', '/'),
|
|
assets: reportAssets,
|
|
generatedAt: new Date().toISOString()
|
|
}
|
|
await writeGenerated('design-pipeline/generated/a01/build-report.json', Buffer.from(`${JSON.stringify(buildReport, null, 2)}\n`))
|
|
|
|
console.log('A01-CODE-PIPELINE BUILD PASS')
|