feat(public): 主题切换修复 + 卡片图标服务端本地缓存
- 主题切换:菜单改为 fixed 定位并弹出在父菜单右侧;FIXED_THEMES 扩为 9 主题并统一校验; 子菜单点击无响应修复(合并 IIFE 作用域使 applyTheme 对点击处理器可见);删除死代码。 - 卡片图标:新增 GET /site/icon/<domain>?letter=X 接口,首次请求由服务端拉取 favicon 并缓存到 data/icon/(运行时自动建目录,已加 .gitignore),之后直接读本地不再重复拉取; 拉取失败或内网/不可达网址自动生成「站点名称首字母」SVG 头像兜底,杜绝空白图标。 - 模型 Nav.to_dict 新增 domain/scheme/letter/color 计算字段,供卡片与图标接口复用。
This commit is contained in:
@@ -54,6 +54,38 @@ class Nav(db.Model):
|
||||
|
||||
def to_dict(self):
|
||||
"""方便模板直接读取"""
|
||||
url = self.url or ''
|
||||
# 提取域名 / 协议,用于加载 favicon 与生成确定性配色
|
||||
domain = ''
|
||||
scheme = 'https'
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url if '://' in url else 'http://' + url)
|
||||
domain = parsed.netloc or parsed.path.split('/')[0]
|
||||
if parsed.scheme:
|
||||
scheme = parsed.scheme
|
||||
except Exception:
|
||||
domain = ''
|
||||
|
||||
# 取标题首个可见字符作为「无 favicon 时的兜底字母头像」
|
||||
letter = ''
|
||||
for ch in (self.title or ''):
|
||||
if ch.strip():
|
||||
letter = ch.upper()
|
||||
break
|
||||
if not letter:
|
||||
letter = (domain[:1] or '·').upper()
|
||||
|
||||
# 按域名哈希得到稳定的渐变配色(同域名永远同色,跨域名尽量区分)
|
||||
hue = 0
|
||||
if domain:
|
||||
s = 0
|
||||
for ch in domain:
|
||||
s = (s * 31 + ord(ch)) & 0xffffffff
|
||||
hue = s % 360
|
||||
color = 'linear-gradient(135deg, hsl(%d,62%%,56%%) 0%%, hsl(%d,72%%,46%%) 100%%)' % (
|
||||
hue, (hue + 28) % 360)
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'category': self.category,
|
||||
@@ -61,6 +93,10 @@ class Nav(db.Model):
|
||||
'url': self.url,
|
||||
'description': self.description or '',
|
||||
'icon': self.icon or 'layui-icon-link',
|
||||
'letter': letter,
|
||||
'color': color,
|
||||
'domain': domain,
|
||||
'scheme': scheme,
|
||||
'sort': self.sort or 0,
|
||||
'enable': self.enable,
|
||||
'is_external': self.is_external,
|
||||
|
||||
Reference in New Issue
Block a user