diff --git a/app.py b/app.py index e1d5cc8..913bc74 100644 --- a/app.py +++ b/app.py @@ -479,6 +479,7 @@ def create_app() -> Flask: if form.validate_on_submit(): g = ServiceGroup( name=form.name.data.strip(), + icon=(form.icon.data or "").strip(), sort_order=form.sort_order.data or 0, ) db.session.add(g) @@ -497,6 +498,7 @@ def create_app() -> Flask: form = GroupForm(obj=g) if form.validate_on_submit(): g.name = form.name.data.strip() + g.icon = (form.icon.data or "").strip() g.sort_order = form.sort_order.data or 0 db.session.commit() flash("分组已更新", "success") @@ -666,6 +668,17 @@ def _migrate_schema() -> None: ) ) print("[nav] 已为 services 表添加 embed_kind 列。", flush=True) + if "service_groups" in insp.get_table_names(): + gcols = {c["name"] for c in insp.get_columns("service_groups")} + if "icon" not in gcols: + with db.engine.begin() as conn: + conn.execute( + text( + "ALTER TABLE service_groups ADD COLUMN icon VARCHAR(16) " + "NOT NULL DEFAULT ''" + ) + ) + print("[nav] 已为 service_groups 表添加 icon 列。", flush=True) except Exception as exc: print(f"[nav] 数据库结构迁移跳过或失败: {exc}", flush=True) diff --git a/forms.py b/forms.py index c8b4b4b..df73492 100644 --- a/forms.py +++ b/forms.py @@ -11,6 +11,22 @@ class LoginForm(FlaskForm): class GroupForm(FlaskForm): name = StringField("分组名称", validators=[DataRequired(message="请输入分组名称")]) + icon = SelectField( + "图标", + choices=[ + ("", "自动(按分组名匹配)"), + ("order", "下单 / 订单"), + ("trade", "交易 / 行情"), + ("review", "复盘 / 历史"), + ("api", "API / 接口"), + ("gate", "Gate / 扫单"), + ("chart", "图表 / K线"), + ("server", "服务器 / 面板"), + ("folder", "文件夹(通用)"), + ], + default="", + validators=[Optional()], + ) sort_order = IntegerField( "排序(越小越靠前)", validators=[Optional(), NumberRange(min=-10**6, max=10**6)], diff --git a/models.py b/models.py index 1d8b6d5..ae66913 100644 --- a/models.py +++ b/models.py @@ -24,6 +24,7 @@ class ServiceGroup(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(120), nullable=False) + icon = db.Column(db.String(16), nullable=False, default="", server_default="") sort_order = db.Column(db.Integer, nullable=False, default=0) services = db.relationship( @@ -33,6 +34,26 @@ class ServiceGroup(db.Model): cascade="all, delete-orphan", ) + def resolve_icon_key(self) -> str: + key = (self.icon or "").strip().lower() + if key: + return key + name = self.name or "" + lower = name.lower() + if "下单" in name: + return "order" + if "交易" in name: + return "trade" + if "复盘" in name: + return "review" + if "api" in lower: + return "api" + if "gate" in lower or "扫单" in name: + return "gate" + if "k线" in lower or "k 线" in name or "chart" in lower: + return "chart" + return "folder" + class Service(db.Model): __tablename__ = "services" diff --git a/static/style.css b/static/style.css index 6a91a4f..9e1e9ec 100644 --- a/static/style.css +++ b/static/style.css @@ -170,37 +170,130 @@ a:hover { } .sidebar-section { - margin-bottom: 1rem; + margin-bottom: 0.35rem; + padding: 0 0.55rem; } -.sidebar-section h2 { - margin: 0 0 0.35rem; - padding: 0 1rem; - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); +.sidebar-group-title { + display: flex; + align-items: center; + gap: 0.55rem; + margin: 0.85rem 0 0.45rem; + padding: 0.35rem 0.5rem; + font-size: 0.78rem; font-weight: 600; + letter-spacing: 0.02em; + color: var(--text); + text-transform: none; +} + +.sidebar-group-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-group-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 8px; + flex-shrink: 0; +} + +.sidebar-group-icon svg { + width: 1rem; + height: 1rem; +} + +.sidebar-group-icon--order { + background: rgba(59, 130, 246, 0.16); + color: #60a5fa; +} + +.sidebar-group-icon--trade { + background: rgba(16, 185, 129, 0.16); + color: #34d399; +} + +.sidebar-group-icon--review { + background: rgba(168, 85, 247, 0.16); + color: #c084fc; +} + +.sidebar-group-icon--api { + background: rgba(245, 158, 11, 0.16); + color: #fbbf24; +} + +.sidebar-group-icon--gate { + background: rgba(236, 72, 153, 0.16); + color: #f472b6; +} + +.sidebar-group-icon--chart { + background: rgba(45, 212, 191, 0.16); + color: #2dd4bf; +} + +.sidebar-group-icon--server { + background: rgba(99, 102, 241, 0.16); + color: #818cf8; +} + +.sidebar-group-icon--folder { + background: rgba(148, 163, 184, 0.14); + color: #94a3b8; +} + +.sidebar-links { + display: flex; + flex-direction: column; + gap: 0.12rem; +} + +.sidebar-empty { + padding: 0.35rem 0.65rem 0.5rem; + font-size: 0.82rem; } .nav-link { - display: block; - padding: 0.45rem 1rem; + display: flex; + align-items: center; + margin: 0; + padding: 0.52rem 0.7rem 0.52rem 0.85rem; color: var(--text); cursor: pointer; - border-left: 3px solid transparent; - font-size: 0.9rem; + border-left: none; + border-radius: 8px; + font-size: 0.88rem; text-decoration: none; + transition: + background 0.15s ease, + color 0.15s ease, + box-shadow 0.15s ease; +} + +.nav-link-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .nav-link:hover { - background: rgba(61, 139, 253, 0.08); + background: rgba(61, 139, 253, 0.1); text-decoration: none; } .nav-link.active { - background: rgba(61, 139, 253, 0.15); - border-left-color: var(--accent); + background: rgba(61, 139, 253, 0.18); + box-shadow: inset 3px 0 0 var(--accent); text-decoration: none; } @@ -232,12 +325,25 @@ a:hover { } .dash-section-title { + display: flex; + align-items: center; + gap: 0.55rem; margin: 0 0 0.85rem; - font-size: 0.72rem; + font-size: 0.95rem; font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--muted); + letter-spacing: 0.02em; + color: var(--text); + text-transform: none; +} + +.dash-section-title .sidebar-group-icon { + width: 2rem; + height: 2rem; +} + +.dash-section-title .sidebar-group-icon svg { + width: 1.05rem; + height: 1.05rem; } .service-card-grid { diff --git a/templates/admin_group_form.html b/templates/admin_group_form.html index 34bea49..1d6bd1d 100644 --- a/templates/admin_group_form.html +++ b/templates/admin_group_form.html @@ -28,6 +28,14 @@
{{ form.name.errors[0] }}
{% endif %} +
+ {{ form.icon.label }} + {{ form.icon() }} +

留空则按分组名称自动匹配图标(如「交易」「Gate 扫单」)。

+ {% if form.icon.errors %} +
{{ form.icon.errors[0] }}
+ {% endif %} +
{{ form.sort_order.label }} {{ form.sort_order() }} diff --git a/templates/admin_groups.html b/templates/admin_groups.html index 48b93eb..4947615 100644 --- a/templates/admin_groups.html +++ b/templates/admin_groups.html @@ -30,6 +30,7 @@ ID + 图标 名称 排序 操作 @@ -39,6 +40,7 @@ {% for g in groups %} {{ g.id }} + {{ g.resolve_icon_key() }} {{ g.name }} {{ g.sort_order }} @@ -59,7 +61,7 @@ {% else %} - 暂无分组,点击「新建分组」 + 暂无分组,点击「新建分组」 {% endfor %} diff --git a/templates/index.html b/templates/index.html index 92872c6..1365968 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% from "macros/group_icon.html" import render as group_icon %} {% block title %}导航 · 本地导航{% endblock %} {% block body %}
@@ -35,7 +36,11 @@
{% for group, services in grouped %} {% else %}
@@ -74,7 +80,10 @@
{% for group, services in grouped %}
-

{{ group.name }}

+

+ {{ group_icon(group.resolve_icon_key()) }} + {{ group.name }} +

{% for svc in services %}