fix: highlight active portal navigation
This commit is contained in:
@@ -82,21 +82,23 @@
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold" href="{% url 'portal-home' %}">cmhub</a>
|
||||
<div class="ms-auto d-flex gap-2 flex-wrap justify-content-end">
|
||||
{% with current_url=request.resolver_match.url_name %}
|
||||
{% if user.is_authenticated %}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-dashboard' %}">控制台</a>
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'portal-recharge' %}">充值</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-apikeys' %}">API Key</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-models' %}">可用模型</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-recharge-records' %}">充值记录</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-usage-records' %}">消费记录</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-dashboard' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-dashboard' %} aria-current="page"{% endif %} href="{% url 'portal-dashboard' %}">控制台</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-recharge' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-recharge' %} aria-current="page"{% endif %} href="{% url 'portal-recharge' %}">充值</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-apikeys' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-apikeys' %} aria-current="page"{% endif %} href="{% url 'portal-apikeys' %}">API Key</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-models' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-models' %} aria-current="page"{% endif %} href="{% url 'portal-models' %}">可用模型</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-recharge-records' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-recharge-records' %} aria-current="page"{% endif %} href="{% url 'portal-recharge-records' %}">充值记录</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-usage-records' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-usage-records' %} aria-current="page"{% endif %} href="{% url 'portal-usage-records' %}">消费记录</a>
|
||||
<form method="post" action="{% url 'portal-logout' %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">退出</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-login' %}">登录</a>
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'portal-signup' %}">注册</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-login' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-login' %} aria-current="page"{% endif %} href="{% url 'portal-login' %}">登录</a>
|
||||
<a class="btn btn-sm {% if current_url == 'portal-signup' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-signup' %} aria-current="page"{% endif %} href="{% url 'portal-signup' %}">注册</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -110,6 +110,20 @@ class PortalAccountFlowTests(TestCase):
|
||||
ai_model=ai_model,
|
||||
)
|
||||
|
||||
def assert_nav_link_active(self, response, *, href: str, label: str):
|
||||
html = response.content.decode("utf-8")
|
||||
self.assertIn(
|
||||
f'<a class="btn btn-sm btn-primary" aria-current="page" href="{href}">{label}</a>',
|
||||
html,
|
||||
)
|
||||
|
||||
def assert_nav_link_inactive(self, response, *, href: str, label: str):
|
||||
html = response.content.decode("utf-8")
|
||||
self.assertIn(
|
||||
f'<a class="btn btn-sm btn-outline-secondary" href="{href}">{label}</a>',
|
||||
html,
|
||||
)
|
||||
|
||||
def test_signup_creates_unverified_user_wallet_with_zero_points_and_no_ledger(self):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
email = f"signup-{suffix}@example.com"
|
||||
@@ -250,6 +264,31 @@ class PortalAccountFlowTests(TestCase):
|
||||
self.assertNotContains(response, "api_key_encrypted")
|
||||
self.assertNotContains(response, "extra_body")
|
||||
|
||||
def test_authenticated_nav_highlights_current_page_only(self):
|
||||
user = self.create_verified_user()
|
||||
self.client.force_login(user)
|
||||
cases = (
|
||||
("/dashboard", "/dashboard", "控制台"),
|
||||
("/recharge", "/recharge", "充值"),
|
||||
("/apikeys", "/apikeys", "API Key"),
|
||||
("/models", "/models", "可用模型"),
|
||||
("/records/recharge", "/records/recharge", "充值记录"),
|
||||
("/records/usage", "/records/usage", "消费记录"),
|
||||
)
|
||||
|
||||
for path, href, label in cases:
|
||||
with self.subTest(path=path):
|
||||
response = self.client.get(path)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assert_nav_link_active(response, href=href, label=label)
|
||||
if path != "/recharge":
|
||||
self.assert_nav_link_inactive(
|
||||
response,
|
||||
href="/recharge",
|
||||
label="充值",
|
||||
)
|
||||
|
||||
def test_create_api_key_shows_plaintext_once_and_stores_only_hash(self):
|
||||
user = self.create_verified_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
- 阶段:Phase 6 增强(MVP 后);Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成,Phase 5 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档已完成,Phase 6 T-601 可用别名发现已完成;当前暂无新的已拆 `TODO` 任务
|
||||
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-allauth 65.18.0 + PyMySQL 1.1.3 + cryptography 46.0.7 + requests 2.34.2 + django-admin;MySQL 8.4 已接入 settings,并支持 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT`;用户端已用 Django 模板 SSR + Bootstrap + allauth 落地注册登录;生产部署口径为 VPS / 宝塔 + Nginx + Gunicorn(gthread) + systemd;详见 `03-tech-stack.md` 与 `deployment.md`
|
||||
- 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/`;T-002 已创建 `apps/users|portal|billing|ai|api`;T-003 已把自定义 `User` 注册进 django-admin;T-004 已完成 email 唯一性、init 版本断言、app 顺序、`.env.example` 与 `pyproject.toml`;T-101 已新增 `apps/ai/providers/`(Provider 接口、注册表、chat/gemini/images/images_edits 适配器);T-102 已新增 `AiModel` / `ModelAlias`、Fernet 加密密钥存储、别名解析、admin 配置页、`import_ai_models` 导入命令;T-103 已新增 `AiConfigAuditLog` 审计表、admin 只读页面和后台保存/删除审计 hook;T-104/T-105 已完成录制 title/image smoke 与审核修补;T-201 已新增 `UserWallet` / `ApiKey`、`PointsLedger` / `CallRecord`、对应 admin 与迁移;T-202 已新增 `PricingRule` / `ExchangeRate`、`apps.billing.pricing` 计费计算函数、admin 配置页与迁移;T-203 已新增 `apps.billing.services`,实现并发安全预扣、成功确认与幂等失败退点;T-204 已新增 `billing.0003_pointsledger_unique_ledger_change_type_per_call`,用 MySQL 可落地的 `ref_call + change_type` 复合唯一约束兜底防重复 refund;T-301 已新增 `apps.api.authentication.ApiKeyAuthentication` 与 `ExternalApiView`;T-302 已新增生成接口编排、序列化器、图片本地存储和 `/api/v1/generate/title|image` 路由;T-303 已新增 `apps.billing.services.get_balance_snapshot()` 与 `/api/v1/balance` 余额查询接口;T-304 已新增 `RechargeOrder`、充值回调验签适配器、幂等入账服务、微信/支付宝回调路由与迁移 `billing.0004_rechargeorder_and_more`;T-305 已新增 `create_recharge_order()`、微信/支付宝扫码下单 mock/SDK 入口、`/api/v1/recharge/create` 与 `/api/v1/recharge/status`;T-306 已新增 `apps.api.throttles`、`apps.api.exceptions`、`REST_FRAMEWORK` 安全默认认证、生成/认证失败限流、`image_url` SSRF 防护与响应大小上限、充值单笔金额上限;T-501 已接入 allauth,新增 portal 路由、注册适配器、登录/注册/登出模板和最小 dashboard,注册成功创建 0 点钱包且不写赠点流水;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/dashboard` 为个人中心汇总,并新增 `/records/recharge` 充值记录与 `/records/usage` 消费记录,只读展示当前用户数据;T-504 已新增 `/recharge` 页面、`RechargeCreateForm`、充值导航入口和轮询脚本,页面创建 pending 订单、展示二维码票据、轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额;T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,并把充值记录 / 消费记录改为 Django `Paginator` 分页;T-401 已新增 `adjust_wallet_points()` 手工调点服务、钱包 admin 专用调点表单与模板,后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录,流水/订单/调用记录保持只读;T-402 已新增 `docs/mvp-acceptance.md`,按 P0 验收矩阵记录 MVP 完整验收结论、测试证据和已知限制;T-403 已新增 `docs/deployment.md` 与 `requirements-production.txt`,并在 settings 中补齐 `STATIC_ROOT`、`CSRF_TRUSTED_ORIGINS`、共享 `CACHES`、HTTPS cookie、proxy SSL 与 HSTS 环境变量支持;T-601 已新增 `apps.ai.catalog.get_public_model_catalog()`、`GET /api/v1/models` 与 portal `/models` 只读页面,只展示 active 可调用别名、能力、是否需要原图和点数单价,不解密 provider key,不暴露底层 SKU / URL / key / `extra_body`
|
||||
- 用户端导航:顶部导航 active 状态已修复,`portal/base.html` 基于 `request.resolver_match.url_name` 高亮当前页面入口,并用 `aria-current="page"` 标记;「充值」不再在非充值页固定深色高亮。
|
||||
- 测试:T-601 已验证:`.\init.ps1` 通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`py -3.12 -m py_compile apps\ai\catalog.py apps\api\views.py apps\api\urls.py apps\api\tests.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`git diff --check` 通过;`py -3.12 manage.py test apps.api.tests.ModelsCatalogApiTests apps.portal.tests.PortalAccountFlowTests.test_models_page_requires_session_login apps.portal.tests.PortalAccountFlowTests.test_models_page_lists_public_aliases_prices_and_unpriced_state --keepdb --noinput --verbosity 2` 首次运行时 3 条 API 测试已通过,portal `setUpClass` 阶段因远程 MySQL 连接 43.128.3.240 超时中断;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True`,单独重跑两条 portal 测试通过,2 tests OK。T-403 已验证:`./init.ps1` 开工前通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` 找到两个本地 static 文件;`py -3.12 manage.py check --deploy` 在当前开发 `.env` 下仅报预期安全配置警告,临时注入生产型安全环境变量(含 HSTS includeSubDomains/preload)后通过,0 issues;`py -3.12 manage.py collectstatic --dry-run --noinput` 通过,预期收集 169 个 static 文件;`py -3.12 manage.py createcachetable --dry-run cmhub_cache` 通过,输出 MySQL cache 表 DDL;`py -3.12 -m compileall config` 通过;尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` 时 25 条用例已通过,`GenerateApiTests` 14 条因远程 MySQL 连接重置/事务中断被记 ERROR,随后 `Test-NetConnection 43.128.3.240 -Port 3306` 端口可达,单独重跑 `py -3.12 manage.py test apps.api.tests.GenerateApiTests --noinput --keepdb --verbosity 2` 通过,14 tests OK。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本邮箱唯一性由 `user.email` 唯一约束承担。
|
||||
- 最新验证:`py -3.12 -m py_compile apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --keepdb --noinput --verbosity 2` 通过,覆盖 `/dashboard`、`/recharge`、`/apikeys`、`/models`、`/records/recharge`、`/records/usage` 的导航高亮。
|
||||
- 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`);真实 `ai_models.json` 不提交,需通过 `import_ai_models` 命令加密导入
|
||||
- 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh`
|
||||
- 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test`
|
||||
@@ -43,9 +45,9 @@
|
||||
|
||||
- 已完成:T-001 初始化 Django + DRF 项目骨架;T-002 建立 apps 目录、自定义 User 与配置;T-003 接通 django-admin 与最小测试;T-004 Phase 0 骨架审核修补;T-101 Provider 适配器层 + 移植 cmbot 调用;T-102 AiModel + ModelAlias 模型 + 别名解析;T-103 配置变更审计;T-104 跑通一次录制标题生成;T-105 Phase 1 AI 层审核修补;T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型;T-202 PricingRule / ExchangeRate 模型 + 计费计算;T-203 并发安全扣点 / 退点;T-204 Phase 2 计费核心审核加固;T-301 API Key 鉴权;T-302 生成标题 / 图片接口;T-303 余额查询接口;T-304 充值回调;T-305 扫码充值下单 + 轮询;T-306 Phase 3 对外 API 安全加固;T-501 注册 / 登录(allauth);T-502 API Key 自助管理页;T-503 个人中心 / 记录页;T-504 充值页(扫码 + 轮询到账);T-505 Phase 4 用户端审核优化;T-401 运营后台完善;T-402 完整验收 MVP;T-403 部署 / 运行文档;T-601 可用别名发现。
|
||||
- 正在进行:无。
|
||||
- 待开始:无已拆任务;后续从 Backlog 或生产配置待办拆新任务。
|
||||
- 待开始:无已拆任务;后续从 Backlog 或生产配置待办拆新任务。建议继续处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
|
||||
- 当前 blocker:远程 MySQL 连接当前不稳定或很慢,完整测试单次全绿可能需要稳定测试库;支付商户真实密钥/证书与生产 SDK 依赖仍待提供;真实 AI 上游 smoke 仍需配置密钥和模型数据后执行。
|
||||
- 下一个可领取任务:暂无已拆 `TODO`;建议先处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
|
||||
- 下一个可领取任务:暂无已拆 `TODO`;建议继续处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
|
||||
|
||||
## 当前可运行内容
|
||||
|
||||
|
||||
@@ -76,3 +76,10 @@ T-601 已落地 `/models`:登录用户可查看当前公开可调用别名、
|
||||
|
||||
- 对外只暴露 `/api/v1/*`;`/admin/` 仅限运营,生产环境建议限制来源或加额外保护。
|
||||
- 未授权访问 admin 跳转登录;API 未授权返回 401,不静默失败。
|
||||
- 用户端顶部导航的深色 / active 样式应表示“当前所在页面”,不能固定给某个入口。若保留“充值”作为常驻 CTA,应使用与当前页 active 明确区分的样式,避免用户误以为仍停留在充值页。
|
||||
|
||||
### 导航 active 状态
|
||||
|
||||
- 已修复:`apps/portal/templates/portal/base.html` 基于 `request.resolver_match.url_name` 判断当前页面,只给当前页面入口使用 `btn-primary` 与 `aria-current="page"`。
|
||||
- 非当前页面入口使用 `btn-outline-secondary`;`/records/recharge` 与 `/records/usage` 分别高亮对应记录入口。
|
||||
- 已补 portal 测试:登录后访问 `/models`、`/recharge`、`/apikeys`、`/records/recharge`、`/records/usage`、`/dashboard` 时,只有当前页面入口高亮;访问非充值页时「充值」不再固定高亮。
|
||||
|
||||
+15
@@ -999,6 +999,21 @@
|
||||
- 决策:模型目录接口是只读发现能力,不调用 `AiModel.to_resolved_model()`,不依赖 `AI_KEY_ENCRYPTION_KEY`,不解密或输出 provider key;未来接 `account_alias_permission` 时在同一 catalog 查询层按用户过滤。
|
||||
- 下一步:暂无已拆 `TODO`;建议先处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
|
||||
|
||||
## 2026-07-04 用户端顶部导航 active 状态修复
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:
|
||||
- `apps/portal/templates/portal/base.html`:顶部导航改为基于 `request.resolver_match.url_name` 判断当前页面;当前页面入口使用 `btn-primary` 与 `aria-current="page"`,非当前页面入口使用 `btn-outline-secondary`。修复访问「可用模型」/「API Key」/记录页时「充值」仍固定深色高亮的问题。
|
||||
- `apps/portal/tests.py`:新增导航 active 状态测试,覆盖 `/dashboard`、`/recharge`、`/apikeys`、`/models`、`/records/recharge`、`/records/usage`。
|
||||
- `docs/routes.md`:将导航缺陷口径更新为已修复,并记录 active 状态规则。
|
||||
- `docs/06-tasks.md` / `docs/current-state.md`:移除待修 Backlog 口径,更新当前状态与验证记录。
|
||||
- 验证:
|
||||
- `py -3.12 -m py_compile apps\portal\tests.py`:通过。
|
||||
- `py -3.12 manage.py check`:通过,0 issues。
|
||||
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --keepdb --noinput --verbosity 2`:通过,1 test OK;测试阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
|
||||
- 阻塞:无。
|
||||
- 下一步:暂无已拆 `TODO`;建议继续处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
|
||||
|
||||
## 2026-07-04 T-601 复核(Claude Code review,非任务)
|
||||
|
||||
- 状态:DONE(复核完成,结论:达标,质量高)
|
||||
|
||||
Reference in New Issue
Block a user