## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
生成文章编辑器「添加内容」面板的图标。
|
|
|
|
原实现引用 qnycdn.mymoyu.top 上的图标,该域名不在本仓库管理范围内,
|
|
且项目里其余同类地址早已因签名过期而 403。这里改为本地生成,
|
|
产物落在 static/editor-icons/ 下,任何环境都能直接渲染。
|
|
|
|
用法: python tools/gen-editor-icons.py
|
|
"""
|
|
|
|
import os
|
|
import math
|
|
from PIL import Image, ImageDraw
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
OUT_DIR = os.path.join(ROOT, "static", "editor-icons")
|
|
|
|
SIZE = 96 # 逻辑尺寸
|
|
SCALE = 3 # 超采样倍数,边缘更干净
|
|
S = SIZE * SCALE
|
|
|
|
INK = (58, 50, 44, 255) # 主体线条
|
|
ACCENT = (193, 28, 31, 255) # 军旅红点缀
|
|
MUTED = (176, 168, 160, 255)
|
|
|
|
|
|
def blank():
|
|
return Image.new("RGBA", (S, S), (0, 0, 0, 0))
|
|
|
|
|
|
def stroke(width):
|
|
return max(1, int(width * SCALE))
|
|
|
|
|
|
def draw_text_icon():
|
|
"""文字:两行横线,长度不同"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(7)
|
|
d.rounded_rectangle([14 * SCALE, 20 * SCALE, 82 * SCALE, 76 * SCALE],
|
|
radius=8 * SCALE, outline=INK, width=w)
|
|
d.line([26 * SCALE, 38 * SCALE, 70 * SCALE, 38 * SCALE], fill=INK, width=w)
|
|
d.line([26 * SCALE, 54 * SCALE, 56 * SCALE, 54 * SCALE], fill=ACCENT, width=w)
|
|
return img
|
|
|
|
|
|
def draw_image_icon():
|
|
"""图片:方框 + 山峰 + 太阳"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(7)
|
|
d.rounded_rectangle([12 * SCALE, 20 * SCALE, 84 * SCALE, 76 * SCALE],
|
|
radius=8 * SCALE, outline=INK, width=w)
|
|
d.ellipse([26 * SCALE, 32 * SCALE, 40 * SCALE, 46 * SCALE], fill=ACCENT)
|
|
d.polygon([(20 * SCALE, 70 * SCALE), (44 * SCALE, 46 * SCALE), (64 * SCALE, 70 * SCALE)],
|
|
fill=INK)
|
|
d.polygon([(52 * SCALE, 70 * SCALE), (66 * SCALE, 56 * SCALE), (82 * SCALE, 70 * SCALE)],
|
|
fill=MUTED)
|
|
return img
|
|
|
|
|
|
def draw_video_icon():
|
|
"""视频:圆角矩形 + 播放三角"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(7)
|
|
d.rounded_rectangle([10 * SCALE, 22 * SCALE, 86 * SCALE, 74 * SCALE],
|
|
radius=10 * SCALE, outline=INK, width=w)
|
|
d.polygon([(40 * SCALE, 36 * SCALE), (40 * SCALE, 60 * SCALE), (62 * SCALE, 48 * SCALE)],
|
|
fill=ACCENT)
|
|
return img
|
|
|
|
|
|
def draw_vote_icon():
|
|
"""投票:三条横条,第一条已勾选"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(7)
|
|
rows = [26, 48, 70]
|
|
for i, y in enumerate(rows):
|
|
color = ACCENT if i == 0 else INK
|
|
d.rounded_rectangle([14 * SCALE, (y - 9) * SCALE, 34 * SCALE, (y + 9) * SCALE],
|
|
radius=4 * SCALE, outline=color, width=w)
|
|
if i == 0:
|
|
d.line([18 * SCALE, y * SCALE, 22 * SCALE, (y + 5) * SCALE], fill=ACCENT, width=w)
|
|
d.line([22 * SCALE, (y + 5) * SCALE, 30 * SCALE, (y - 6) * SCALE], fill=ACCENT, width=w)
|
|
d.line([44 * SCALE, y * SCALE, 82 * SCALE, y * SCALE], fill=color, width=w)
|
|
return img
|
|
|
|
|
|
def draw_title_icon():
|
|
"""小标题:短横线 + 下方长横线"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(9)
|
|
d.line([16 * SCALE, 36 * SCALE, 56 * SCALE, 36 * SCALE], fill=ACCENT, width=w)
|
|
d.line([16 * SCALE, 58 * SCALE, 80 * SCALE, 58 * SCALE], fill=INK, width=stroke(6))
|
|
return img
|
|
|
|
|
|
def draw_not_image_icon():
|
|
"""无图占位:方框 + 斜杠"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(6)
|
|
d.rounded_rectangle([12 * SCALE, 20 * SCALE, 84 * SCALE, 76 * SCALE],
|
|
radius=8 * SCALE, outline=MUTED, width=w)
|
|
d.line([18 * SCALE, 28 * SCALE, 78 * SCALE, 68 * SCALE], fill=MUTED, width=w)
|
|
return img
|
|
|
|
|
|
def draw_stop_icon():
|
|
"""视频遮罩上的播放按钮"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
d.ellipse([8 * SCALE, 8 * SCALE, 88 * SCALE, 88 * SCALE], fill=(0, 0, 0, 110))
|
|
d.ellipse([8 * SCALE, 8 * SCALE, 88 * SCALE, 88 * SCALE],
|
|
outline=(255, 255, 255, 230), width=stroke(5))
|
|
d.polygon([(40 * SCALE, 32 * SCALE), (40 * SCALE, 64 * SCALE), (68 * SCALE, 48 * SCALE)],
|
|
fill=(255, 255, 255, 240))
|
|
return img
|
|
|
|
|
|
def draw_is_vote_icon():
|
|
"""投票空态插画"""
|
|
img = blank()
|
|
d = ImageDraw.Draw(img)
|
|
w = stroke(6)
|
|
d.rounded_rectangle([20 * SCALE, 18 * SCALE, 76 * SCALE, 80 * SCALE],
|
|
radius=8 * SCALE, outline=MUTED, width=w)
|
|
for i, y in enumerate([34, 50, 66]):
|
|
d.ellipse([30 * SCALE, (y - 5) * SCALE, 40 * SCALE, (y + 5) * SCALE],
|
|
outline=ACCENT if i == 0 else MUTED, width=w)
|
|
d.line([48 * SCALE, y * SCALE, 68 * SCALE, y * SCALE], fill=MUTED, width=w)
|
|
return img
|
|
|
|
|
|
BUILDERS = {
|
|
"text": draw_text_icon,
|
|
"image": draw_image_icon,
|
|
"video": draw_video_icon,
|
|
"vote": draw_vote_icon,
|
|
"title": draw_title_icon,
|
|
"not_image": draw_not_image_icon,
|
|
"stop": draw_stop_icon,
|
|
"is_vote": draw_is_vote_icon,
|
|
}
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
for name, fn in BUILDERS.items():
|
|
img = fn().resize((SIZE, SIZE), Image.LANCZOS)
|
|
path = os.path.join(OUT_DIR, f"{name}.png")
|
|
img.save(path, "PNG", optimize=True)
|
|
print(f" {name + '.png':<18} {os.path.getsize(path)/1024:5.1f} KB")
|
|
print(f"共 {len(BUILDERS)} 个图标 → {OUT_DIR}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|