feat: 小菜记账 v1.0 - 完整功能实现
核心功能: - 记账CRUD(支出/收入/分类/备注/日期) - 统计分析(概览/分类占比/每日趋势) - 预算管理(按月设置/进度条/超支提醒) - 数据导出CSV 安全与认证: - HMAC-SHA256签名token认证 - 用户数据隔离 - 输入验证与错误处理 - CORS配置 前端优化: - 骨架屏加载 - 账单按日期分组 - 预算页面重构(快捷预设+Numpad) - SvgIcon组件(H5+微信双端适配) - 下拉刷新 后端优化: - 共享日期工具函数 - 数据库连接池优化 - 健康检查端点 - 优雅关闭处理 技术栈: - 前端:Uni-app (Vue 3 + Pinia) - 后端:Node.js + Express + MySQL
This commit is contained in:
238
scripts/gen-icons.js
Normal file
238
scripts/gen-icons.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 生成 Tab Bar 图标 PNG 文件 (81x81)
|
||||
* 使用 Node.js 内置模块,无需安装额外依赖
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const zlib = require('zlib')
|
||||
|
||||
const SIZE = 81
|
||||
const OUT_DIR = path.join(__dirname, '../client/src/static/icons')
|
||||
|
||||
// 颜色
|
||||
const INACTIVE = [191, 179, 179] // #BFB3B3
|
||||
const ACTIVE = [255, 140, 105] // #FF8C69
|
||||
const TRANSPARENT = [0, 0, 0, 0]
|
||||
|
||||
function createPNG(pixels) {
|
||||
// PNG signature
|
||||
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
||||
|
||||
// IHDR chunk
|
||||
const ihdr = Buffer.alloc(13)
|
||||
ihdr.writeUInt32BE(SIZE, 0)
|
||||
ihdr.writeUInt32BE(SIZE, 4)
|
||||
ihdr[8] = 8 // bit depth
|
||||
ihdr[9] = 6 // color type: RGBA
|
||||
ihdr[10] = 0 // compression
|
||||
ihdr[11] = 0 // filter
|
||||
ihdr[12] = 0 // interlace
|
||||
|
||||
// Raw image data: filter byte (0) + RGBA pixels per row
|
||||
const rawData = Buffer.alloc(SIZE * (1 + SIZE * 4))
|
||||
for (let y = 0; y < SIZE; y++) {
|
||||
const rowOffset = y * (1 + SIZE * 4)
|
||||
rawData[rowOffset] = 0 // no filter
|
||||
for (let x = 0; x < SIZE; x++) {
|
||||
const srcIdx = (y * SIZE + x) * 4
|
||||
const dstIdx = rowOffset + 1 + x * 4
|
||||
rawData[dstIdx] = pixels[srcIdx]
|
||||
rawData[dstIdx + 1] = pixels[srcIdx + 1]
|
||||
rawData[dstIdx + 2] = pixels[srcIdx + 2]
|
||||
rawData[dstIdx + 3] = pixels[srcIdx + 3]
|
||||
}
|
||||
}
|
||||
|
||||
const compressed = zlib.deflateSync(rawData)
|
||||
|
||||
function makeChunk(type, data) {
|
||||
const typeBuffer = Buffer.from(type, 'ascii')
|
||||
const length = Buffer.alloc(4)
|
||||
length.writeUInt32BE(data.length, 0)
|
||||
const combined = Buffer.concat([typeBuffer, data])
|
||||
const crc = crc32(combined)
|
||||
return Buffer.concat([length, combined, crc])
|
||||
}
|
||||
|
||||
const ihdrChunk = makeChunk('IHDR', ihdr)
|
||||
const idatChunk = makeChunk('IDAT', compressed)
|
||||
const iendChunk = makeChunk('IEND', Buffer.alloc(0))
|
||||
|
||||
return Buffer.concat([signature, ihdrChunk, idatChunk, iendChunk])
|
||||
}
|
||||
|
||||
// CRC32 table
|
||||
const crcTable = new Uint32Array(256)
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)
|
||||
}
|
||||
crcTable[n] = c
|
||||
}
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xFFFFFFFF
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
crc = crcTable[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8)
|
||||
}
|
||||
const result = Buffer.alloc(4)
|
||||
result.writeUInt32BE((crc ^ 0xFFFFFFFF) >>> 0, 0)
|
||||
return result
|
||||
}
|
||||
|
||||
function makePixels(drawFn, color) {
|
||||
const pixels = new Uint8Array(SIZE * SIZE * 4)
|
||||
// Fill transparent
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
pixels[i + 3] = 0
|
||||
}
|
||||
drawFn(pixels, color)
|
||||
return pixels
|
||||
}
|
||||
|
||||
function setPixel(pixels, x, y, r, g, b, a = 255) {
|
||||
if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) return
|
||||
const idx = (y * SIZE + x) * 4
|
||||
// Alpha blending
|
||||
const srcA = a / 255
|
||||
const dstA = pixels[idx + 3] / 255
|
||||
const outA = srcA + dstA * (1 - srcA)
|
||||
if (outA > 0) {
|
||||
pixels[idx] = Math.round((r * srcA + pixels[idx] * dstA * (1 - srcA)) / outA)
|
||||
pixels[idx + 1] = Math.round((g * srcA + pixels[idx + 1] * dstA * (1 - srcA)) / outA)
|
||||
pixels[idx + 2] = Math.round((b * srcA + pixels[idx + 2] * dstA * (1 - srcA)) / outA)
|
||||
pixels[idx + 3] = Math.round(outA * 255)
|
||||
}
|
||||
}
|
||||
|
||||
function drawLine(pixels, x0, y0, x1, y1, r, g, b, thickness = 2) {
|
||||
const dx = Math.abs(x1 - x0)
|
||||
const dy = Math.abs(y1 - y0)
|
||||
const sx = x0 < x1 ? 1 : -1
|
||||
const sy = y0 < y1 ? 1 : -1
|
||||
let err = dx - dy
|
||||
while (true) {
|
||||
for (let tx = -thickness; tx <= thickness; tx++) {
|
||||
for (let ty = -thickness; ty <= thickness; ty++) {
|
||||
if (tx * tx + ty * ty <= thickness * thickness) {
|
||||
setPixel(pixels, x0 + tx, y0 + ty, r, g, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (x0 === x1 && y0 === y1) break
|
||||
const e2 = 2 * err
|
||||
if (e2 > -dy) { err -= dy; x0 += sx }
|
||||
if (e2 < dx) { err += dx; y0 += sy }
|
||||
}
|
||||
}
|
||||
|
||||
function drawCircle(pixels, cx, cy, radius, r, g, b, filled = true) {
|
||||
for (let y = -radius; y <= radius; y++) {
|
||||
for (let x = -radius; x <= radius; x++) {
|
||||
const dist = Math.sqrt(x * x + y * y)
|
||||
if (filled ? dist <= radius : (dist >= radius - 1.5 && dist <= radius + 0.5)) {
|
||||
setPixel(pixels, cx + x, cy + y, r, g, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawRect(pixels, x, y, w, h, r, g, b) {
|
||||
for (let dy = 0; dy < h; dy++) {
|
||||
for (let dx = 0; dx < w; dx++) {
|
||||
setPixel(pixels, x + dx, y + dy, r, g, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Icon drawing functions ===
|
||||
|
||||
function drawHome(pixels, color) {
|
||||
const [r, g, b] = color
|
||||
const cx = 40, cy = 40
|
||||
// House body (rectangle)
|
||||
drawRect(pixels, 24, 38, 34, 26, r, g, b)
|
||||
// Roof (triangle)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
drawLine(pixels, cx, 18, cx - 20 + i * 2, 38, r, g, b, 1)
|
||||
}
|
||||
// Door
|
||||
drawRect(pixels, 35, 50, 12, 14, 255, 248, 240)
|
||||
}
|
||||
|
||||
function drawChart(pixels, color) {
|
||||
const [r, g, b] = color
|
||||
// Three bars
|
||||
drawRect(pixels, 18, 45, 12, 20, r, g, b)
|
||||
drawRect(pixels, 35, 30, 12, 35, r, g, b)
|
||||
drawRect(pixels, 52, 20, 12, 45, r, g, b)
|
||||
// Base line
|
||||
drawRect(pixels, 14, 65, 54, 2, r, g, b)
|
||||
}
|
||||
|
||||
function drawBill(pixels, color) {
|
||||
const [r, g, b] = color
|
||||
// Document shape
|
||||
drawRect(pixels, 22, 14, 38, 54, r, g, b)
|
||||
// Inner white area
|
||||
drawRect(pixels, 28, 22, 26, 38, 255, 248, 240)
|
||||
// Lines on document
|
||||
drawRect(pixels, 31, 30, 20, 2, r, g, b)
|
||||
drawRect(pixels, 31, 38, 16, 2, r, g, b)
|
||||
drawRect(pixels, 31, 46, 20, 2, r, g, b)
|
||||
drawRect(pixels, 31, 54, 12, 2, r, g, b)
|
||||
// Corner fold
|
||||
drawLine(pixels, 52, 14, 60, 22, r, g, b, 1)
|
||||
}
|
||||
|
||||
function drawUser(pixels, color) {
|
||||
const [r, g, b] = color
|
||||
// Head (circle)
|
||||
drawCircle(pixels, 40, 28, 12, r, g, b)
|
||||
// Body (half circle / arc)
|
||||
for (let x = -24; x <= 24; x++) {
|
||||
const y = Math.round(Math.sqrt(Math.max(0, 24 * 24 - x * x)))
|
||||
for (let dy = 0; dy < 3; dy++) {
|
||||
setPixel(pixels, 40 + x, 58 - y + dy, r, g, b)
|
||||
}
|
||||
}
|
||||
// Fill body
|
||||
for (let y = 48; y <= 66; y++) {
|
||||
for (let x = 16; x <= 64; x++) {
|
||||
const dx = x - 40
|
||||
const dy = y - 58
|
||||
if (dy * dy + dx * dx <= 26 * 26) {
|
||||
setPixel(pixels, x, y, r, g, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all icons
|
||||
const icons = [
|
||||
{ name: 'home', draw: drawHome },
|
||||
{ name: 'chart', draw: drawChart },
|
||||
{ name: 'bill', draw: drawBill },
|
||||
{ name: 'user', draw: drawUser },
|
||||
]
|
||||
|
||||
if (!fs.existsSync(OUT_DIR)) {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
for (const icon of icons) {
|
||||
// Inactive
|
||||
const inactivePixels = makePixels(icon.draw, INACTIVE)
|
||||
const inactivePNG = createPNG(inactivePixels)
|
||||
fs.writeFileSync(path.join(OUT_DIR, `${icon.name}.png`), inactivePNG)
|
||||
|
||||
// Active
|
||||
const activePixels = makePixels(icon.draw, ACTIVE)
|
||||
const activePNG = createPNG(activePixels)
|
||||
fs.writeFileSync(path.join(OUT_DIR, `${icon.name}-active.png`), activePNG)
|
||||
|
||||
console.log(`Generated: ${icon.name}.png, ${icon.name}-active.png`)
|
||||
}
|
||||
|
||||
console.log('All icons generated successfully!')
|
||||
Reference in New Issue
Block a user