From b5702147b8ce6227e12c4b06bfc881dceda8e121 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 4 Aug 2026 19:20:02 -0600 Subject: [PATCH] =?UTF-8?q?feat(achievement):=20=E6=88=90=E5=B0=B1?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=BB=8E=20emoji=20=E6=8D=A2=E6=88=90=20icon?= =?UTF-8?q?ify=20=E5=9B=BE=E6=A0=87=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 机房的老浏览器缺 emoji 字体,成就图标会渲染成方块。改存 iconify 图标名(noto 集,Noto Color Emoji 的 SVG 版),前端渲染成 SVG, 和浏览器字体无关,视觉上与原 emoji 基本一致。 隐藏成就的遮罩图标一并改成 noto:red-question-mark。 迁移 0002 带 77 条 emoji → noto 映射,覆盖首批成就用到的全部图标; 表里没有的保持原值,前端按纯文本兜底,之后在后台手工补。 Co-Authored-By: Claude Opus 5 --- .../migrations/0002_emoji_icon_to_iconify.py | 123 ++++++++++++++++++ achievement/models.py | 2 + achievement/serializers.py | 4 +- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 achievement/migrations/0002_emoji_icon_to_iconify.py diff --git a/achievement/migrations/0002_emoji_icon_to_iconify.py b/achievement/migrations/0002_emoji_icon_to_iconify.py new file mode 100644 index 0000000..f27a29c --- /dev/null +++ b/achievement/migrations/0002_emoji_icon_to_iconify.py @@ -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)] diff --git a/achievement/models.py b/achievement/models.py index e81a812..7e4f965 100644 --- a/achievement/models.py +++ b/achievement/models.py @@ -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="是否隐藏") diff --git a/achievement/serializers.py b/achievement/serializers.py index c1ac518..257c79c 100644 --- a/achievement/serializers.py +++ b/achievement/serializers.py @@ -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