- 主题切换:菜单改为 fixed 定位并弹出在父菜单右侧;FIXED_THEMES 扩为 9 主题并统一校验; 子菜单点击无响应修复(合并 IIFE 作用域使 applyTheme 对点击处理器可见);删除死代码。 - 卡片图标:新增 GET /site/icon/<domain>?letter=X 接口,首次请求由服务端拉取 favicon 并缓存到 data/icon/(运行时自动建目录,已加 .gitignore),之后直接读本地不再重复拉取; 拉取失败或内网/不可达网址自动生成「站点名称首字母」SVG 头像兜底,杜绝空白图标。 - 模型 Nav.to_dict 新增 domain/scheme/letter/color 计算字段,供卡片与图标接口复用。
105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
"""
|
|
前台公开导航模型。
|
|
|
|
Nav 用于「公开访问的导航聚合页」,与 admin_power 表完全解耦:
|
|
- admin_power 控制后台管理菜单,需要登录
|
|
- public_nav 控制前台公开导航,无需登录
|
|
|
|
后续若要做前台用户(收藏、提交链接),只需加独立 User 模型,
|
|
不需要碰 RBAC。
|
|
"""
|
|
import datetime
|
|
from applications.extensions import db
|
|
|
|
|
|
class Nav(db.Model):
|
|
__tablename__ = 'public_nav'
|
|
|
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='导航ID')
|
|
|
|
# 分类名(如"开发工具"/"AI"/"设计资源"),用于首页聚合展示
|
|
category = db.Column(db.String(50), nullable=False, comment='分类', index=True)
|
|
|
|
# 单条导航的标题
|
|
title = db.Column(db.String(100), nullable=False, comment='标题')
|
|
|
|
# 链接 URL(站内或外链)
|
|
url = db.Column(db.String(255), nullable=False, comment='链接')
|
|
|
|
# 简短描述(鼠标悬停时可见)
|
|
description = db.Column(db.String(255), default='', comment='简介')
|
|
|
|
# layui-icon 的 class 名,例如 "layui-icon-website"
|
|
icon = db.Column(db.String(100), default='layui-icon-link', comment='图标')
|
|
|
|
# 同分类下的显示顺序
|
|
sort = db.Column(db.Integer, default=0, comment='排序')
|
|
|
|
# 1=启用,0=禁用(禁用后前台不再展示,后台列表仍可见)
|
|
enable = db.Column(db.Integer, default=1, comment='状态(1启用,0关闭)')
|
|
|
|
# 1=外链(新窗口打开),0=站内(当前窗口)
|
|
is_external = db.Column(db.Integer, default=1, comment='是否外链')
|
|
|
|
# 创建者(后台管理员用户名,便于追溯),非空约束放宽以便匿名维护
|
|
create_by = db.Column(db.String(50), default='admin', comment='创建者')
|
|
|
|
create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
|
|
update_at = db.Column(
|
|
db.DateTime,
|
|
default=datetime.datetime.now,
|
|
onupdate=datetime.datetime.now,
|
|
comment='更新时间',
|
|
)
|
|
|
|
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,
|
|
'title': self.title,
|
|
'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,
|
|
'create_by': self.create_by,
|
|
'create_at': self.create_at.strftime('%Y-%m-%d %H:%M:%S') if self.create_at else '',
|
|
} |