Files
ai-dimension/scripts/gen-icons.mjs
T
txai-dev 2a111d21c0 feat: v1.3.0 差异化重构——星图点亮体系
P0 修复:
- 详情页正文渲染(content 分段),71 条知识不再空白
- 问答页去登录墙:游客每日 3 次(IP 限流),登录后每日 5 次
- 修正问答配额为真正的每日重置语义

点亮闭环:
- KnowledgeLit 模型 + 服务端判题(POST /:id/quiz,答案不下发)
- /api/knowledge/stats 星域统计 + 点亮进度 + streak + litIds
- AI 批量生成 71 条读后检测题(scripts/generate-quiz.js)

设计系统 2.0:
- 70 个统一线性图标替换全部 emoji 图标
- 主题 token mixin 化,修复 H5 暗色级联 bug,MP 跟随系统(darkmode)
- 首页星域导航(进度环)、我的页学习者档案、星域列表页重写
- 硬编码色清理,误植 map 页移除

服务端加固:
- trust proxy 真实 IP、错误信息脱敏、ask 独立限流、无效 ObjectId 404
- seo.js ESM→CJS(修复容器启动)、AI 导师"维度精灵"人格化
2026-09-12 23:39:05 +08:00

213 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 生成统一线性图标集 → client/src/static/icons/
* 用法: NODE_PATH=<ai-art>/server/node_modules node scripts/gen-icons.mjs
* 规格: 48x48 (显示 22-24px @2x), 2.5px 线宽, 圆角端点
*/
import { createRequire } from 'node:module'
import { mkdirSync, writeFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const CANVAS_PATH = process.env.CANVAS_PATH ||
'/home/dev2-ubuntu/workspace/ai-art/server/node_modules/@napi-rs/canvas/index.js'
const { createCanvas } = createRequire(import.meta.url)(CANVAS_PATH)
const __dirname = dirname(fileURLToPath(import.meta.url))
const OUT = join(__dirname, '../client/src/static/icons')
mkdirSync(OUT, { recursive: true })
const BRAND = '#5b5ff8'
const GRAY = '#8e93a8'
const WHITE = '#ffffff'
const GREEN = '#22c55e'
const S = 96
const LW = 2.6
const SCALE = 2
async function make(draw, color = BRAND) {
const c = createCanvas(S, S)
const ctx = c.getContext('2d')
ctx.scale(SCALE, SCALE)
ctx.strokeStyle = color
ctx.fillStyle = color
ctx.lineWidth = LW
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
draw(ctx)
return await c.encode('png')
}
// —— 图标绘制函数(几何线条,风格统一)——
const shapes = {
// 铃铛
notice: (g) => {
g.beginPath(); g.arc(24, 18, 9, Math.PI, 0); g.stroke()
g.beginPath(); g.moveTo(15, 18); g.lineTo(13, 30); g.lineTo(35, 30); g.lineTo(33, 18); g.stroke()
g.beginPath(); g.arc(24, 34, 3, Math.PI, 0, true); g.stroke()
},
// 用户
user: (g) => {
g.beginPath(); g.arc(24, 17, 7, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.arc(24, 40, 12, Math.PI * 1.15, Math.PI * 1.85); g.stroke()
},
// 搜索
search: (g) => {
g.beginPath(); g.arc(21, 21, 10, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.moveTo(29, 29); g.lineTo(38, 38); g.stroke()
},
// 收藏星(空心/实心两版)
star: (g, fill) => {
const pts = []
for (let i = 0; i < 10; i++) {
const r = i % 2 === 0 ? 14 : 6
const a = -Math.PI / 2 + (i * Math.PI) / 5
pts.push([24 + r * Math.cos(a), 24 + r * Math.sin(a)])
}
g.beginPath(); pts.forEach(([x, y], i) => (i ? g.lineTo(x, y) : g.moveTo(x, y))); g.closePath()
if (fill) { g.save(); g.lineWidth = LW / 2; g.fill(); g.restore() }
g.stroke()
},
// 火焰(streak)
flame: (g) => {
g.beginPath()
g.moveTo(24, 10)
g.bezierCurveTo(30, 17, 34, 22, 34, 29)
g.arc(24, 29, 10, 0, Math.PI, false)
g.bezierCurveTo(14, 22, 18, 17, 24, 10)
g.stroke()
g.beginPath(); g.arc(24, 31, 4, 0, Math.PI * 2); g.stroke()
},
// 星球(品牌)
planet: (g) => {
g.beginPath(); g.arc(22, 22, 11, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.ellipse(24, 24, 17, 6.5, -0.45, 0, Math.PI * 2); g.stroke()
},
// 对话气泡
chat: (g) => {
g.beginPath()
g.moveTo(10, 14); g.lineTo(38, 14); g.quadraticCurveTo(41, 14, 41, 17)
g.lineTo(41, 29); g.quadraticCurveTo(41, 32, 38, 32); g.lineTo(22, 32)
g.lineTo(14, 39); g.lineTo(14, 32); g.quadraticCurveTo(11, 32, 11, 29)
g.lineTo(11, 17); g.quadraticCurveTo(11, 14, 14, 14); g.closePath(); g.stroke()
},
// 分享
share: (g) => {
g.beginPath(); g.arc(34, 12, 5, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.arc(34, 36, 5, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.arc(14, 24, 5, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.moveTo(19.5, 21.5); g.lineTo(28.5, 14.5); g.stroke()
g.beginPath(); g.moveTo(19.5, 26.5); g.lineTo(28.5, 33.5); g.stroke()
},
// 设置齿轮
settings: (g) => {
g.beginPath(); g.arc(24, 24, 6.5, 0, Math.PI * 2); g.stroke()
for (let i = 0; i < 8; i++) {
const a = (i * Math.PI) / 4
g.beginPath()
g.moveTo(24 + 10.5 * Math.cos(a), 24 + 10.5 * Math.sin(a))
g.lineTo(24 + 14.5 * Math.cos(a), 24 + 14.5 * Math.sin(a))
g.stroke()
}
},
// 反馈(对话+点)
feedback: (g) => {
g.beginPath()
g.moveTo(10, 13); g.lineTo(38, 13); g.quadraticCurveTo(41, 13, 41, 16)
g.lineTo(41, 28); g.quadraticCurveTo(41, 31, 38, 31); g.lineTo(20, 31)
g.lineTo(13, 38); g.lineTo(13, 31); g.quadraticCurveTo(10, 31, 10, 28)
g.lineTo(10, 16); g.quadraticCurveTo(10, 13, 13, 13); g.closePath(); g.stroke()
g.beginPath(); g.arc(17, 22, 1.6, 0, Math.PI * 2); g.fill()
g.beginPath(); g.arc(25, 22, 1.6, 0, Math.PI * 2); g.fill()
g.beginPath(); g.arc(33, 22, 1.6, 0, Math.PI * 2); g.fill()
},
// 奖杯
trophy: (g) => {
g.beginPath(); g.moveTo(16, 11); g.lineTo(32, 11); g.lineTo(31, 24); g.quadraticCurveTo(30, 30, 24, 30); g.quadraticCurveTo(18, 30, 17, 24); g.closePath(); g.stroke()
g.beginPath(); g.moveTo(24, 30); g.lineTo(24, 36); g.stroke()
g.beginPath(); g.moveTo(17, 38); g.lineTo(31, 38); g.stroke()
g.beginPath(); g.moveTo(16, 14); g.lineTo(10, 16); g.quadraticCurveTo(10, 25, 16, 26); g.stroke()
g.beginPath(); g.moveTo(32, 14); g.lineTo(38, 16); g.quadraticCurveTo(38, 25, 32, 26); g.stroke()
},
// 火箭(趋势)
rocket: (g) => {
g.beginPath()
g.moveTo(24, 8)
g.bezierCurveTo(31, 13, 33, 20, 33, 27)
g.lineTo(24, 33); g.lineTo(15, 27)
g.bezierCurveTo(15, 20, 17, 13, 24, 8)
g.closePath(); g.stroke()
g.beginPath(); g.arc(24, 20, 3.5, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.moveTo(15, 27); g.lineTo(10, 36); g.lineTo(17, 33); g.stroke()
g.beginPath(); g.moveTo(33, 27); g.lineTo(38, 36); g.lineTo(31, 33); g.stroke()
},
// 书本(知识)
book: (g) => {
g.beginPath(); g.moveTo(24, 13); g.quadraticCurveTo(17, 9, 9, 11); g.lineTo(9, 36); g.quadraticCurveTo(17, 34, 24, 38); g.quadraticCurveTo(31, 34, 39, 36); g.lineTo(39, 11); g.quadraticCurveTo(31, 9, 24, 13); g.closePath(); g.stroke()
g.beginPath(); g.moveTo(24, 13); g.lineTo(24, 38); g.stroke()
},
// 灯泡(提示/AI)
bulb: (g) => {
g.beginPath(); g.arc(24, 19, 9.5, Math.PI * 0.95, Math.PI * 0.05, false); g.stroke()
g.beginPath(); g.moveTo(19, 27); g.lineTo(19, 33); g.lineTo(29, 33); g.lineTo(29, 27); g.stroke()
g.beginPath(); g.moveTo(20, 37); g.lineTo(28, 37); g.stroke()
},
// 勾
check: (g) => {
g.beginPath(); g.moveTo(13, 25); g.lineTo(21, 33); g.lineTo(35, 16); g.stroke()
},
// 锁
lock: (g) => {
g.beginPath(); g.moveTo(14, 23); g.lineTo(14, 37); g.lineTo(34, 37); g.lineTo(34, 23); g.closePath(); g.stroke()
g.beginPath(); g.arc(24, 23, 6.5, Math.PI, 0); g.stroke()
},
// 返回箭头
back: (g) => {
g.beginPath(); g.moveTo(27, 12); g.lineTo(18, 24); g.lineTo(27, 36); g.stroke()
},
// 右箭头
arrow: (g) => {
g.beginPath(); g.moveTo(20, 12); g.lineTo(30, 24); g.lineTo(20, 36); g.stroke()
},
// 发送
send: (g) => {
g.beginPath(); g.moveTo(10, 24); g.lineTo(38, 12); g.lineTo(30, 36); g.lineTo(24, 27); g.closePath(); g.stroke()
g.beginPath(); g.moveTo(24, 27); g.lineTo(38, 12); g.stroke()
},
// 礼物
gift: (g) => {
g.beginPath(); g.moveTo(10, 18); g.lineTo(38, 18); g.lineTo(38, 38); g.lineTo(10, 38); g.closePath(); g.stroke()
g.beginPath(); g.moveTo(24, 18); g.lineTo(24, 38); g.stroke()
g.beginPath(); g.moveTo(10, 24); g.lineTo(38, 24); g.stroke()
g.beginPath(); g.arc(19.5, 13, 4.5, Math.PI * 0.6, Math.PI * 1.7); g.stroke()
g.beginPath(); g.arc(28.5, 13, 4.5, Math.PI * 1.3, Math.PI * 0.4); g.stroke()
},
// 时钟(继续探索)
clock: (g) => {
g.beginPath(); g.arc(24, 24, 12, 0, Math.PI * 2); g.stroke()
g.beginPath(); g.moveTo(24, 16); g.lineTo(24, 24); g.lineTo(30, 28); g.stroke()
},
// 闪烁(点亮/正确反馈)
sparkle: (g) => {
g.beginPath()
g.moveTo(24, 8); g.quadraticCurveTo(26, 20, 38, 24); g.quadraticCurveTo(26, 28, 24, 40); g.quadraticCurveTo(22, 28, 10, 24); g.quadraticCurveTo(22, 20, 24, 8)
g.closePath(); g.stroke()
g.beginPath(); g.arc(37, 11, 1.8, 0, Math.PI * 2); g.fill()
g.beginPath(); g.arc(11, 35, 1.4, 0, Math.PI * 2); g.fill()
},
}
const jobs = []
for (const [name, draw] of Object.entries(shapes)) {
jobs.push([name, await make(draw, BRAND)])
jobs.push([`${name}-gray`, await make(draw, GRAY)])
jobs.push([`${name}-white`, await make(draw, WHITE)])
}
// 特例:实心星(品牌色 + 灰)
jobs.push(['star-fill', await make((g) => shapes.star(g, true), BRAND)])
jobs.push(['star-fill-gray', await make((g) => shapes.star(g, true), GRAY)])
jobs.push(['star-fill-white', await make((g) => shapes.star(g, true), WHITE)])
jobs.push(['check-green', await make(shapes.check, GREEN)])
for (const [name, buf] of jobs) writeFileSync(join(OUT, `${name}.png`), buf)
console.log(`生成 ${jobs.length} 个图标 → ${OUT}`)