AI 接入 V1.0
UI更新
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成 App 的双色图标集(SVG)。
|
||||
|
||||
为什么要有这个脚本:
|
||||
图标是「设计系统」的一部分,不是一堆散文件。把形状和配色集中写在这里,
|
||||
改动(换圆角、调粗细、换某个颜色的深浅)只需要改一个地方再重跑,
|
||||
不用手工去改 30 个 SVG。
|
||||
|
||||
设计规范:
|
||||
· 画布 24×24,内容控制在 3~21 之间留出视觉边距
|
||||
· 线条粗细统一 2.0,端点和连接处全部圆角(这是「可爱」的关键)
|
||||
· 双色:浅色填充 + 深色描边(类似 Phosphor Duotone 的做法)
|
||||
· 配色以品牌焦糖色 #C8956E 为锚,压低饱和度,保证整体暖调统一
|
||||
|
||||
产物:
|
||||
assets/icons/<name>.svg 提交进仓库,由 flutter_svg 渲染
|
||||
<后台>/static/admin/icons/<name>.svg 同一套图标给网页后台用
|
||||
<后台>/static/admin/icons.js 图标名清单 + emoji 兼容映射
|
||||
tools/_icons_preview.html 预览页(浏览器打开检查,不提交)
|
||||
|
||||
用法:
|
||||
python tools/make_icons.py # 只生成 App 的图标
|
||||
python tools/make_icons.py --preview # 额外生成预览页
|
||||
python tools/make_icons.py --sync-web # 同时同步给网页后台(改了图标就带上)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
OUT_DIR = ROOT / "assets" / "icons"
|
||||
PREVIEW = ROOT / "tools" / "_icons_preview.html"
|
||||
|
||||
# 网页后台也要用同一套图标(否则分类图标从 emoji 换成图标名之后,
|
||||
# 后台会把 "food" 当文字显示出来)。这里是后台的静态资源目录。
|
||||
WEB_DIR = ROOT.parent / "AI-accounting-soft" / "src" / "main" / "resources" / "static" / "admin"
|
||||
|
||||
STROKE = 2.0
|
||||
|
||||
# ---------------------------------------------------------------- 形状原语
|
||||
# 所有图标都由这几个原语拼出来 —— 这是「风格统一」的机制保证:
|
||||
# 圆角半径、线宽、端点全是同一套参数,不会出现某个图标特别突兀。
|
||||
|
||||
|
||||
def rr(x: float, y: float, w: float, h: float, r: float) -> str:
|
||||
"""圆角矩形"""
|
||||
return (
|
||||
f"M{x + r} {y}h{w - 2 * r}a{r} {r} 0 0 1 {r} {r}v{h - 2 * r}"
|
||||
f"a{r} {r} 0 0 1 -{r} {r}h-{w - 2 * r}a{r} {r} 0 0 1 -{r} -{r}"
|
||||
f"v-{h - 2 * r}a{r} {r} 0 0 1 {r} -{r}z"
|
||||
)
|
||||
|
||||
|
||||
def circle(cx: float, cy: float, r: float) -> str:
|
||||
return f"M{cx - r} {cy}a{r} {r} 0 1 0 {2 * r} 0a{r} {r} 0 1 0 -{2 * r} 0z"
|
||||
|
||||
|
||||
def bowl() -> str:
|
||||
"""半圆碗(上沿平、下半圆)"""
|
||||
return "M4 11h16a8 8 0 0 1-16 0z"
|
||||
|
||||
|
||||
def sparkle() -> str:
|
||||
"""四角星光 —— 用曲线收腰,直线版会像菱形"""
|
||||
return (
|
||||
"M12 2.5C12.6 7.8 16.2 11.4 21.5 12 16.2 12.6 12.6 16.2 12 21.5"
|
||||
"5.8 16.2 9.4 12.6 4.2 12 9.4 11.4 12.6 7.8 12 2.5z"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 配色
|
||||
# (描边色, 填充色)。以焦糖 #C8956E 为锚,整体暖调、低饱和。
|
||||
C_BRAND = "#C8956E"
|
||||
C_BRAND_L = "#F6E8DB"
|
||||
C_TEXT = "#8D6E63" # UI 图标默认色(次级文字色)
|
||||
C_TEXT_L = "#F1E7E0"
|
||||
|
||||
PALETTE = {
|
||||
"food": ("#E2914B", "#FBE7D0"), # 橙
|
||||
"transport": ("#5B8DB8", "#DCE8F2"), # 灰蓝
|
||||
"shopping": ("#D97C93", "#F7E2E8"), # 玫红
|
||||
"entertainment": ("#9B7BC4", "#EBE3F5"), # 紫
|
||||
"medical": ("#E0725C", "#FAE2DD"), # 珊瑚红
|
||||
"education": ("#6B8E4E", "#E4EDDA"), # 橄榄绿
|
||||
"housing": ("#B5794F", "#F0E2D6"), # 棕
|
||||
"utilities": ("#D9A93C", "#F8ECD2"), # 琥珀
|
||||
"phone": ("#4FA0B0", "#DBEDF1"), # 青
|
||||
"other": ("#9A8C80", "#EBE5E0"), # 灰棕
|
||||
"salary": (C_BRAND, C_BRAND_L), # 品牌焦糖
|
||||
"bonus": ("#D99C3C", "#F9EDD5"), # 金
|
||||
"invest": ("#4FA98A", "#DBEFE8"), # 青绿
|
||||
"parttime": ("#8A7BB8", "#E8E3F4"), # 蓝紫
|
||||
"bill": (C_BRAND, C_BRAND_L),
|
||||
"note": (C_BRAND, C_BRAND_L),
|
||||
"assistant": ("#D9A93C", "#F8ECD2"),
|
||||
}
|
||||
|
||||
# name -> (palette_key, [(path, 是否填充), ...])
|
||||
ICONS: dict[str, tuple[str, list[tuple[str, bool]]]] = {
|
||||
|
||||
# ============================ 记账分类 ============================
|
||||
"food": ("food", [
|
||||
(bowl(), True),
|
||||
("M9.2 8.2c1-1.4-.9-2.6.1-4", False),
|
||||
("M13.6 8.2c1-1.4-.9-2.6.1-4", False),
|
||||
]),
|
||||
"transport": ("transport", [
|
||||
(rr(7, 4.8, 10, 5.4, 2.6), True), # 车厢
|
||||
(rr(3.2, 9.4, 17.6, 7.6, 3), True), # 车身
|
||||
(circle(8, 17.2, 1.9), True), # 轮
|
||||
(circle(16, 17.2, 1.9), True),
|
||||
]),
|
||||
"shopping": ("shopping", [
|
||||
(rr(4.8, 8.6, 14.4, 11.8, 3), True), # 袋身
|
||||
("M9 8.6V7a3 3 0 0 1 6 0v1.6", False), # 提手
|
||||
]),
|
||||
"entertainment": ("entertainment", [
|
||||
(rr(3.2, 9.6, 17.6, 10.4, 2.5), True), # 板身
|
||||
("M3.2 9.6h17.6", False),
|
||||
("M7 6.2l-1.6 3.4M12 6.2l-1.6 3.4M17 6.2l-1.6 3.4", False), # 斜纹
|
||||
]),
|
||||
"medical": ("medical", [
|
||||
(rr(3.6, 3.6, 16.8, 16.8, 5), True),
|
||||
("M12 8.4v7.2M8.4 12h7.2", False), # 十字
|
||||
]),
|
||||
"education": ("education", [
|
||||
("M12 4.4l9 4.6-9 4.6-9-4.6 9-4.6z", True), # 帽檐
|
||||
("M20.6 9.2v5.4", False), # 流苏
|
||||
(circle(20.6, 16.2, 1.3), True),
|
||||
]),
|
||||
"housing": ("housing", [
|
||||
("M3.8 11.4L12 4.6l8.2 6.8V19a2.2 2.2 0 0 1-2.2 2.2H6a2.2 2.2 0 0 1-2.2-2.2z", True),
|
||||
(rr(9.8, 14, 4.4, 7.2, 1.6), False), # 门
|
||||
]),
|
||||
"utilities": ("utilities", [
|
||||
("M12 2.8a6.4 6.4 0 0 0-3.7 11.6c.8.6 1.3 1.4 1.3 2.3v.6h4.8v-.6"
|
||||
"c0-.9.5-1.7 1.3-2.3A6.4 6.4 0 0 0 12 2.8z", True),
|
||||
("M9.8 20.2h4.4M10.8 22.2h2.4", False), # 灯座
|
||||
]),
|
||||
"phone": ("phone", [
|
||||
(rr(6.4, 2.8, 11.2, 18.4, 3), True),
|
||||
("M10.6 5.6h2.8", False),
|
||||
("M11 18.4h2", False),
|
||||
]),
|
||||
"other": ("other", [
|
||||
(rr(3.4, 6.8, 17.2, 13.4, 2.6), True),
|
||||
("M3.4 11.4h17.2", False),
|
||||
("M12 6.8v4.6", False),
|
||||
]),
|
||||
"salary": ("salary", [
|
||||
(rr(2.4, 6.4, 19.2, 11.2, 2.6), True),
|
||||
(circle(12, 12, 3.1), False),
|
||||
]),
|
||||
"bonus": ("bonus", [
|
||||
(rr(3.4, 10, 17.2, 10.2, 2.2), True), # 盒身
|
||||
(rr(2.4, 6.2, 19.2, 4, 1.6), True), # 盒盖
|
||||
("M12 6.2v14", False), # 丝带
|
||||
("M12 6.2C10 3.4 6.6 4.2 7.6 6.6", False), # 蝴蝶结
|
||||
("M12 6.2c2-2.8 5.4-2 4.4.4", False),
|
||||
]),
|
||||
"invest": ("invest", [
|
||||
(rr(3.4, 14.4, 3.2, 6, 1.4), True), # 柱状
|
||||
(rr(10.4, 11.4, 3.2, 9, 1.4), True),
|
||||
(rr(17.4, 8.4, 3.2, 12, 1.4), True),
|
||||
("M4 11.6l5-4 3.4 2.8L19.6 5", False), # 趋势线
|
||||
("M16 5h3.6v3.4", False), # 箭头
|
||||
]),
|
||||
"parttime": ("parttime", [
|
||||
(rr(3, 8.4, 18, 12.2, 2.6), True),
|
||||
("M9 8.4V7a1.6 1.6 0 0 1 1.6-1.6h2.8A1.6 1.6 0 0 1 15 7v1.4", False),
|
||||
("M3 13.2h18", False),
|
||||
]),
|
||||
|
||||
# ============================ 模块图标 ============================
|
||||
"bill": ("bill", [
|
||||
(rr(2.8, 6.6, 18.4, 12.4, 3), True), # 钱包
|
||||
("M2.8 11.4h18.4", False),
|
||||
(circle(16.4, 15, 1.5), True), # 扣子
|
||||
]),
|
||||
"note": ("note", [
|
||||
(rr(4.4, 3.4, 15.2, 17.2, 2.6), True), # 本子
|
||||
("M8.6 3.4v17.2", False), # 书脊
|
||||
("M11.8 9h5M11.8 12.6h5", False), # 文字线
|
||||
]),
|
||||
"assistant": ("assistant", [
|
||||
(sparkle(), True),
|
||||
("M18.4 15.6l.9 2.3 2.3.9-2.3.9-.9 2.3-.9-2.3-2.3-.9 2.3-.9.9-2.3z", True),
|
||||
]),
|
||||
|
||||
# ============================ 界面图标 ============================
|
||||
"expense": (None, [ # 支出:托盘 + 向上箭头
|
||||
("M4 13.6v3.4a2.6 2.6 0 0 0 2.6 2.6h10.8a2.6 2.6 0 0 0 2.6-2.6v-3.4"
|
||||
"H15.4a3.4 3.4 0 0 1-6.8 0H4z", True),
|
||||
("M12 14.6V4.4M7.8 8.6L12 4.4l4.2 4.2", False),
|
||||
]),
|
||||
"income": (None, [ # 收入:托盘 + 向下箭头
|
||||
("M4 13.6v3.4a2.6 2.6 0 0 0 2.6 2.6h10.8a2.6 2.6 0 0 0 2.6-2.6v-3.4"
|
||||
"H15.4a3.4 3.4 0 0 1-6.8 0H4z", True),
|
||||
("M12 4.4v10.2M7.8 10.4L12 14.6l4.2-4.2", False),
|
||||
]),
|
||||
"camera": (None, [
|
||||
(rr(3, 6.8, 18, 13.4, 3), True),
|
||||
(circle(12, 13.4, 3.8), False),
|
||||
("M9 6.8l1.3-2.4h3.4L15 6.8", False),
|
||||
]),
|
||||
"search": (None, [
|
||||
(circle(10.4, 10.4, 6.4), True),
|
||||
("M15.2 15.2L20.6 20.6", False),
|
||||
]),
|
||||
"user": (None, [
|
||||
(circle(12, 8.4, 3.9), True),
|
||||
("M4.8 20.4c0-4 3.2-6.6 7.2-6.6s7.2 2.6 7.2 6.6z", True),
|
||||
]),
|
||||
"lock": (None, [
|
||||
(rr(4.4, 10.4, 15.2, 10.2, 2.6), True),
|
||||
("M8.4 10.4V7.8a3.6 3.6 0 0 1 7.2 0v2.6", False),
|
||||
(circle(12, 15.4, 1.2), True),
|
||||
]),
|
||||
"eye": (None, [
|
||||
("M2.6 12C5.2 7.6 8.6 5.6 12 5.6s6.8 2 9.4 6.4C18.8 16.4 15.4 18.4 12 18.4"
|
||||
"S5.2 16.4 2.6 12z", True),
|
||||
(circle(12, 12, 2.9), False),
|
||||
]),
|
||||
"image": (None, [
|
||||
(rr(3, 4.8, 18, 14.4, 3), True),
|
||||
("M4.4 16.6l4.6-5 3.4 3.8 3.2-3.4 4.6 4.8", False),
|
||||
(circle(8.6, 9.4, 1.5), True),
|
||||
]),
|
||||
"file": (None, [
|
||||
("M6 3h7.4L19 8.6V19a2.2 2.2 0 0 1-2.2 2.2H6A2.2 2.2 0 0 1 3.8 19V5.2"
|
||||
"A2.2 2.2 0 0 1 6 3z", True),
|
||||
("M13.4 3v5.6H19", False),
|
||||
("M7.6 13h8.8M7.6 16.6h5.4", False),
|
||||
]),
|
||||
"folder": (None, [
|
||||
("M3.4 7.4a2.2 2.2 0 0 1 2.2-2.2h3.6a2.2 2.2 0 0 1 1.8.9l.9 1.3h8.5"
|
||||
"a2.2 2.2 0 0 1 2.2 2.2v8.6a2.2 2.2 0 0 1-2.2 2.2H5.6a2.2 2.2 0 0 1-2.2-2.2z", True),
|
||||
]),
|
||||
"folder-open": (None, [
|
||||
("M3.4 17.6V7.4a2.2 2.2 0 0 1 2.2-2.2h3.6a2.2 2.2 0 0 1 1.8.9l.9 1.3h6.5"
|
||||
"a2.2 2.2 0 0 1 2.2 2.2v1.2", False),
|
||||
("M3.4 18.4l2.3-7h15.1l-2.3 7a2.2 2.2 0 0 1-2.1 1.6H5.5a2.2 2.2 0 0 1-2.1-1.6z", True),
|
||||
]),
|
||||
"check": (None, [
|
||||
(circle(12, 12, 8.6), True),
|
||||
("M8 12.3l2.9 2.9 5.2-6", False),
|
||||
]),
|
||||
"pin": (None, [
|
||||
("M8.8 3.2h6.4l-1.1 5.4 3 2.9v1.6H6.9v-1.6l3-2.9z", True),
|
||||
("M12 13.1v7.6", False),
|
||||
]),
|
||||
"target": (None, [
|
||||
(circle(12, 12, 8.6), True),
|
||||
(circle(12, 12, 4.6), False),
|
||||
(circle(12, 12, 1.5), True),
|
||||
]),
|
||||
"inbox": (None, [
|
||||
("M5.6 4.6h12.8l2.6 8.2v4.4a2.2 2.2 0 0 1-2.2 2.2H5.2A2.2 2.2 0 0 1 3 17.2v-4.4z", True),
|
||||
("M3 12.8h4.6l1.4 2.6h6l1.4-2.6H21", False),
|
||||
]),
|
||||
"error": (None, [
|
||||
(circle(12, 12, 8.6), True),
|
||||
("M12 7.6v5.2", False),
|
||||
(circle(12, 16.4, 1.15), True),
|
||||
]),
|
||||
}
|
||||
|
||||
FALLBACK = "other"
|
||||
|
||||
# 和 cute_icon.dart 里的 cuteEmojiAliases 保持一致 ——
|
||||
# 这份只用于「迁移还没跑」的过渡期,以及用户自己填 emoji 的情况
|
||||
EMOJI_ALIASES = {
|
||||
"🍔": "food", "🍜": "food", "🍚": "food", "🍕": "food", "☕": "food",
|
||||
"🚗": "transport", "🚌": "transport", "🚇": "transport", "✈": "transport",
|
||||
"🛍": "shopping", "🛒": "shopping", "👕": "shopping",
|
||||
"🎬": "entertainment", "🎮": "entertainment", "🎵": "entertainment",
|
||||
"🏥": "medical", "💊": "medical", "💉": "medical",
|
||||
"📚": "education", "🎓": "education", "📖": "education", "✏": "education",
|
||||
"🏠": "housing", "🏡": "housing", "🛏": "housing",
|
||||
"💡": "utilities", "💧": "utilities", "⚡": "utilities", "🔌": "utilities",
|
||||
"📱": "phone", "☎": "phone", "💻": "phone",
|
||||
"💰": "salary", "💵": "salary", "🏦": "salary", "💴": "salary",
|
||||
"🎁": "bonus", "🧧": "bonus",
|
||||
"📈": "invest", "📊": "invest", "💹": "invest", "🪙": "invest",
|
||||
"💼": "parttime", "🔧": "parttime",
|
||||
"📦": "other", "📌": "other", "🏷": "other", "❓": "other",
|
||||
}
|
||||
|
||||
|
||||
def render(name: str, key: str | None, paths: list[tuple[str, bool]]) -> str:
|
||||
"""把形状列表拼成 SVG。key=None 表示界面图标,用默认的次级文字色"""
|
||||
if key is None:
|
||||
stroke, fill = C_TEXT, C_TEXT_L
|
||||
else:
|
||||
stroke, fill = PALETTE[key]
|
||||
|
||||
body = "\n".join(
|
||||
f' <path d="{d}" fill="{fill if filled else "none"}"/>'
|
||||
for d, filled in paths
|
||||
)
|
||||
return (
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" '
|
||||
f'width="24" height="24" fill="none" stroke="{stroke}" '
|
||||
f'stroke-width="{STROKE}" stroke-linecap="round" stroke-linejoin="round">\n'
|
||||
f'{body}\n</svg>\n'
|
||||
)
|
||||
|
||||
|
||||
def build() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for name, (key, paths) in ICONS.items():
|
||||
(OUT_DIR / f"{name}.svg").write_text(render(name, key, paths), encoding="utf-8")
|
||||
print(f"-> {OUT_DIR} ({len(ICONS)} 个图标)")
|
||||
|
||||
|
||||
def preview() -> None:
|
||||
"""生成预览页 —— SVG 好不好看必须用眼睛验,光看代码看不出来"""
|
||||
groups = [("记账分类", list(PALETTE.keys())[:14]),
|
||||
("模块 / 界面", [n for n in ICONS if n not in list(PALETTE.keys())[:14]])]
|
||||
html = ["""<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<title>图标预览</title>
|
||||
<style>
|
||||
body{font-family:-apple-system,"Microsoft YaHei",sans-serif;background:#FAF8F5;color:#2B2B2B;padding:28px}
|
||||
h2{font-size:15px;margin:26px 0 12px;color:#5D4037}
|
||||
.row{display:flex;flex-wrap:wrap;gap:14px}
|
||||
.cell{width:104px;background:#fff;border:1px solid #EAE3DC;border-radius:12px;
|
||||
padding:14px 8px;text-align:center}
|
||||
.cell img{width:44px;height:44px;display:block;margin:0 auto 8px}
|
||||
.cell .n{font-size:11.5px;color:#8D7B70;word-break:break-all}
|
||||
.big img{width:72px;height:72px}
|
||||
</style></head><body>
|
||||
<h1 style="font-size:19px;margin:0">强宝小助手 · 图标集</h1>
|
||||
<p style="color:#8D7B70;font-size:13px;margin:6px 0 0">上:44px 实际显示尺寸 下:72px 放大看细节</p>"""]
|
||||
for title, names in groups:
|
||||
html.append(f"<h2>{title}</h2><div class='row'>")
|
||||
for n in names:
|
||||
if n not in ICONS:
|
||||
continue
|
||||
html.append(
|
||||
f"<div class='cell'><img src='../assets/icons/{n}.svg'>"
|
||||
f"<span class='n'>{n}</span></div>")
|
||||
html.append("</div>")
|
||||
html.append("<h2>放大检查(72px)</h2><div class='row big'>")
|
||||
for n in ICONS:
|
||||
html.append(f"<div class='cell'><img src='../assets/icons/{n}.svg'>"
|
||||
f"<span class='n'>{n}</span></div>")
|
||||
html.append("</div></body></html>")
|
||||
PREVIEW.write_text("\n".join(html), encoding="utf-8")
|
||||
print("->", PREVIEW)
|
||||
|
||||
|
||||
def sync_web() -> None:
|
||||
"""把图标同步给网页后台。
|
||||
|
||||
后台也要显示分类图标,而分类图标已经从 emoji 换成了图标名 ——
|
||||
不同步的话后台会把 "food" 当文字显示出来。
|
||||
顺手生成一份 icons.js,里面有图标名清单和 emoji 兼容映射,
|
||||
免得在两个项目里各维护一份(那种重复迟早会不同步)。
|
||||
"""
|
||||
if not WEB_DIR.parent.exists():
|
||||
print(f" (跳过) 找不到网页后台目录: {WEB_DIR}")
|
||||
return
|
||||
|
||||
dst = WEB_DIR / "icons"
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
for f in dst.glob("*.svg"):
|
||||
f.unlink()
|
||||
for name in ICONS:
|
||||
(dst / f"{name}.svg").write_text(
|
||||
render(name, *ICONS[name]), encoding="utf-8")
|
||||
|
||||
names = ",\n ".join(f"'{n}'" for n in ICONS)
|
||||
aliases = ",\n ".join(f"'{k}': '{v}'" for k, v in sorted(EMOJI_ALIASES.items()))
|
||||
(WEB_DIR / "icons.js").write_text(
|
||||
"/* 由 tools/make_icons.py 生成,不要手工编辑 —— 改图标请改脚本再重跑 */\n"
|
||||
"window.CUTE_ICONS = {\n"
|
||||
f" fallback: '{FALLBACK}',\n names: [\n {names},\n ],\n"
|
||||
f" aliases: {{\n {aliases},\n }},\n"
|
||||
"};\n", encoding="utf-8")
|
||||
print(f"-> {dst} ({len(ICONS)} 个图标) + icons.js")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--preview", action="store_true", help="额外生成预览页")
|
||||
ap.add_argument("--sync-web", action="store_true", help="同时同步给网页后台")
|
||||
args = ap.parse_args()
|
||||
build()
|
||||
if args.preview:
|
||||
preview()
|
||||
if args.sync_web:
|
||||
sync_web()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user