feat(achievement): 成就图标从 emoji 换成 iconify 图标名
机房的老浏览器缺 emoji 字体,成就图标会渲染成方块。改存 iconify 图标名(noto 集,Noto Color Emoji 的 SVG 版),前端渲染成 SVG, 和浏览器字体无关,视觉上与原 emoji 基本一致。 隐藏成就的遮罩图标一并改成 noto:red-question-mark。 迁移 0002 带 77 条 emoji → noto 映射,覆盖首批成就用到的全部图标; 表里没有的保持原值,前端按纯文本兜底,之后在后台手工补。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
123
achievement/migrations/0002_emoji_icon_to_iconify.py
Normal file
123
achievement/migrations/0002_emoji_icon_to_iconify.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""把 Achievement.icon 从 emoji 字符换成 iconify 图标名。
|
||||
|
||||
机房里的 Chrome 91 之类的老浏览器缺 emoji 字体,emoji 会渲染成方块。
|
||||
iconify 出来的是 SVG,跟浏览器字体无关。
|
||||
|
||||
映射到 noto(Noto Color Emoji 的 SVG 版),视觉上和原 emoji 基本一致。
|
||||
表里没有的 emoji 保持原值不动,前端会按纯文本兜底渲染,之后在管理后台手工补。
|
||||
"""
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
# 带 ️(variation selector-16)的 emoji 后台可能存成带或不带两种形态,
|
||||
# 迁移时统一去掉再查表,所以这里的 key 一律不带 ️
|
||||
EMOJI_TO_ICONIFY = {
|
||||
# 计划里首批成就用到的
|
||||
"🌱": "noto:seedling",
|
||||
"📗": "noto:green-book",
|
||||
"📚": "noto:books",
|
||||
"📅": "noto:calendar",
|
||||
"🗓": "noto:spiral-calendar",
|
||||
"🌐": "noto:globe-with-meridians",
|
||||
"🎖": "noto:military-medal",
|
||||
"🎯": "noto:direct-hit",
|
||||
"🏹": "noto:bow-and-arrow",
|
||||
"🦉": "noto:owl",
|
||||
"🌙": "noto:crescent-moon",
|
||||
"💥": "noto:collision",
|
||||
"🔥": "noto:fire",
|
||||
"⚡": "noto:high-voltage",
|
||||
"✂": "noto:scissors",
|
||||
"📜": "noto:scroll",
|
||||
"💎": "noto:gem-stone",
|
||||
"🏆": "noto:trophy",
|
||||
"❓": "noto:red-question-mark",
|
||||
# 其余常用图标,后台可能已经用上了
|
||||
"❔": "noto:white-question-mark",
|
||||
"📖": "noto:open-book",
|
||||
"🥇": "noto:1st-place-medal",
|
||||
"🥈": "noto:2nd-place-medal",
|
||||
"🥉": "noto:3rd-place-medal",
|
||||
"🏅": "noto:sports-medal",
|
||||
"⭐": "noto:star",
|
||||
"🌟": "noto:glowing-star",
|
||||
"✨": "noto:sparkles",
|
||||
"🎉": "noto:party-popper",
|
||||
"🎊": "noto:confetti-ball",
|
||||
"🚀": "noto:rocket",
|
||||
"💡": "noto:light-bulb",
|
||||
"🐛": "noto:bug",
|
||||
"👑": "noto:crown",
|
||||
"🥷": "noto:ninja",
|
||||
"🧠": "noto:brain",
|
||||
"💪": "noto:flexed-biceps",
|
||||
"🎓": "noto:graduation-cap",
|
||||
"📈": "noto:chart-increasing",
|
||||
"🎁": "noto:wrapped-gift",
|
||||
"🔮": "noto:crystal-ball",
|
||||
"🦄": "noto:unicorn",
|
||||
"🐢": "noto:turtle",
|
||||
"☕": "noto:hot-beverage",
|
||||
"🌈": "noto:rainbow",
|
||||
"⏰": "noto:alarm-clock",
|
||||
"📝": "noto:memo",
|
||||
"✅": "noto:check-mark-button",
|
||||
"🔒": "noto:locked",
|
||||
"🎮": "noto:video-game",
|
||||
"🧩": "noto:puzzle-piece",
|
||||
"🔧": "noto:wrench",
|
||||
"🐍": "noto:snake",
|
||||
"💻": "noto:laptop",
|
||||
"⌨": "noto:keyboard",
|
||||
"☀": "noto:sun",
|
||||
"😴": "noto:sleeping-face",
|
||||
"🤖": "noto:robot",
|
||||
"🔍": "noto:magnifying-glass-tilted-left",
|
||||
"🎨": "noto:artist-palette",
|
||||
"⚔": "noto:crossed-swords",
|
||||
"🛡": "noto:shield",
|
||||
"🏰": "noto:castle",
|
||||
"🗿": "noto:moai",
|
||||
"🌊": "noto:water-wave",
|
||||
"🍀": "noto:four-leaf-clover",
|
||||
"🔁": "noto:repeat-button",
|
||||
"🎪": "noto:circus-tent",
|
||||
"🦅": "noto:eagle",
|
||||
"🐉": "noto:dragon",
|
||||
"💫": "noto:dizzy",
|
||||
"🧭": "noto:compass",
|
||||
"🪄": "noto:magic-wand",
|
||||
"🔔": "noto:bell",
|
||||
"📌": "noto:pushpin",
|
||||
"🏁": "noto:chequered-flag",
|
||||
"🎢": "noto:roller-coaster",
|
||||
}
|
||||
|
||||
ICONIFY_TO_EMOJI = {v: k for k, v in EMOJI_TO_ICONIFY.items()}
|
||||
|
||||
|
||||
def _convert(apps, schema_editor, table):
|
||||
Achievement = apps.get_model("achievement", "Achievement")
|
||||
updated = []
|
||||
for a in Achievement.objects.all():
|
||||
key = (a.icon or "").replace("️", "").strip()
|
||||
new = table.get(key)
|
||||
if new and new != a.icon:
|
||||
a.icon = new
|
||||
updated.append(a)
|
||||
if updated:
|
||||
Achievement.objects.bulk_update(updated, ["icon"])
|
||||
|
||||
|
||||
def emoji_to_iconify(apps, schema_editor):
|
||||
_convert(apps, schema_editor, EMOJI_TO_ICONIFY)
|
||||
|
||||
|
||||
def iconify_to_emoji(apps, schema_editor):
|
||||
_convert(apps, schema_editor, ICONIFY_TO_EMOJI)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("achievement", "0001_initial")]
|
||||
|
||||
operations = [migrations.RunPython(emoji_to_iconify, iconify_to_emoji)]
|
||||
@@ -19,6 +19,8 @@ class Operator(models.TextChoices):
|
||||
class Achievement(models.Model):
|
||||
name = models.TextField(verbose_name="成就名称")
|
||||
description = models.TextField(verbose_name="成就描述")
|
||||
# iconify 图标名(如 noto:owl),不是 emoji 字符:
|
||||
# 机房的老浏览器缺 emoji 字体会渲染成方块,前端统一渲染成 SVG
|
||||
icon = models.TextField(verbose_name="图标")
|
||||
rarity = models.TextField(default=Rarity.BRONZE, choices=Rarity.choices, verbose_name="稀有度")
|
||||
hidden = models.BooleanField(default=False, db_default=False, verbose_name="是否隐藏")
|
||||
|
||||
@@ -63,7 +63,9 @@ class AchievementSerializer(serializers.ModelSerializer):
|
||||
return "达成条件保密" if self._masked(obj) else obj.description
|
||||
|
||||
def get_icon(self, obj):
|
||||
return "❓" if self._masked(obj) else obj.icon
|
||||
# icon 存的是 iconify 图标名,不是 emoji 字符:机房里的老浏览器缺 emoji
|
||||
# 字体会渲染成方块,前端统一按 iconify 渲染成 SVG
|
||||
return "noto:red-question-mark" if self._masked(obj) else obj.icon
|
||||
|
||||
def get_metric(self, obj):
|
||||
return None if self._masked(obj) else obj.metric
|
||||
|
||||
Reference in New Issue
Block a user