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 导师"维度精灵"人格化
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 生成统一线性图标集 → 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}`)
|
||||
Reference in New Issue
Block a user