chore: 全量推送 · 累积页面改动 + Next.js 工程骨架 + v1/ 归档 + 文档
页面 (电商AI平台/) - account / team / settings / index / products / projects: 累积迭代 - restraint.css: 设计 token 补充 - login.html / register.html: 新增登录注册页 - _ARCHIVE.md: 归档说明 Next.js 工程骨架 - app/ + components/: 新一代 SPA 雏形 (page / layout / sidebar / topbar / GridBg / Icon) - package.json / package-lock.json / next.config.mjs / tsconfig.json / postcss.config.mjs / next-env.d.ts 历史归档 / 文档 - v1/: 原 V1 静态稿镜像 (含 mockup-A/B/C) - PRD.md / deployment-guide.md / _check.html - ui参考/ / screenshots/ 杂项 - .gitignore 调整 - 删除根 README.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# 待归档/清理的 HTML 文件
|
||||
|
||||
> 2026-05-21 · 设计稿对接前清理清单
|
||||
|
||||
下列文件是早期版本或独立实验页,**不在主流程上**,对接给团队前建议归档/删除。
|
||||
保留它们不会影响主流程渲染(没有 sidebar / 内部 link 指向),但会让 repo 文件目录显得混乱。
|
||||
|
||||
## ✅ 可以直接删除(无任何入口)
|
||||
|
||||
| 文件 | 原用途 | 现状 |
|
||||
|------|--------|------|
|
||||
| `product-create.legacy.html` | 旧版「新建商品」全屏页(3883 行 drawer) | 已被 `product-create.html`(stub 重定向)+ drawer 模式替代,无任何链接 |
|
||||
| `product-create-v2.html` | 备选「新建商品」实验版 | 仅 `product-studio.html` 单向关联,product-studio 本身已无入口 |
|
||||
| `studio.html` | 早期工作室页占位 | 已被 `pipeline.html` 替代,sidebar 已切换,无 link |
|
||||
| `studio-v2.html` | 早期工作室 V2 实验 | 同上 |
|
||||
| `product-studio.html` | 商品 + 工作室合并实验 | 主流程已拆为 `product-detail.html` + `pipeline.html` |
|
||||
|
||||
## ⚠️ 保留但要清理 sidebar 入口判断
|
||||
|
||||
| 文件 | 决定 |
|
||||
|------|------|
|
||||
| `product-create.html` | **保留** · stub 重定向,处理外部书签/直接访问 → 弹 drawer |
|
||||
| `product-create-upload.html` | **保留** · 商品图上传专用辅助页,通过 drawer 内部调用 |
|
||||
| `model-photo.html` / `platform-cover.html` | **保留** · 「AI 生成素材」二级页(从商品库 gen-choice 跳入) |
|
||||
| `asset-factory.html` | **保留** · sidebar 入口「图片生成」 |
|
||||
| `design-system.html` | **保留** · 设计系统参考,非用户路径 |
|
||||
|
||||
## 🚀 清理命令(对接前)
|
||||
|
||||
如果决定执行,可在 `电商AI平台/` 目录下:
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
Remove-Item product-create.legacy.html, product-create-v2.html, studio.html, studio-v2.html, product-studio.html
|
||||
```
|
||||
|
||||
清理后总文件数:**21 → 16**(含 design-system),核心 15 个。
|
||||
|
||||
## 📌 sidebar NAV 当前入口对照(assets/shell.js:10-43)
|
||||
|
||||
```
|
||||
工作台 → index.html ✓
|
||||
商品库 → products.html ✓
|
||||
视频项目 → projects.html ✓
|
||||
图片生成 → asset-factory.html ✓
|
||||
资产库 → library.html ✓
|
||||
团队 → team.html ✓
|
||||
消费 → account.html ✓
|
||||
设置 → settings.html ✓
|
||||
```
|
||||
|
||||
未在 sidebar 的"白名单"页面(从其他页面 link 进入):
|
||||
- login.html / register.html(auth flow · 顶级入口)
|
||||
- projects-new.html(项目向导 · 工作台「+新建项目」入口)
|
||||
- pipeline.html(流水线 · 项目列表「打开」入口)
|
||||
- product-detail.html(商品详情 · 商品库卡片入口)
|
||||
- product-create.html stub / product-create-upload.html(drawer 流程辅助)
|
||||
- model-photo.html / platform-cover.html(AI 素材生成入口)
|
||||
- design-system.html(设计参考 · 非用户路径)
|
||||
+555
-113
@@ -2,63 +2,186 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>账户 · 流·Studio</title>
|
||||
<title>消费 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<style>
|
||||
.acc-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; }
|
||||
/* ─── 顶部:左右布局(余额 banner + 快速充值)─── */
|
||||
.top-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }
|
||||
@media (max-width: 980px) { .top-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.balance-banner {
|
||||
background: var(--accent-black);
|
||||
color: var(--accent-white);
|
||||
padding: 28px 32px;
|
||||
margin-bottom: 24px;
|
||||
padding: 24px 28px;
|
||||
position: relative;
|
||||
border: 1px solid var(--accent-black);
|
||||
border-radius: var(--r-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
min-width: 0;
|
||||
}
|
||||
.balance-banner::before, .balance-banner::after,
|
||||
.balance-banner > .corner-tr, .balance-banner > .corner-bl {
|
||||
content: ''; position: absolute; width: 14px; height: 14px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain; pointer-events: none;
|
||||
}
|
||||
.balance-banner::before, .balance-banner::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
|
||||
.balance-banner::before { top: -7px; left: -7px; }
|
||||
.balance-banner::after { bottom: -7px; right: -7px; }
|
||||
.balance-banner .corner-tr, .balance-banner .corner-bl { position: absolute; color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 13px; }
|
||||
.balance-banner .corner-tr { top: -8px; right: -8px; }
|
||||
.balance-banner .corner-bl { bottom: -8px; left: -8px; }
|
||||
.balance-banner .lbl { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,.55); letter-spacing: .04em; }
|
||||
.balance-banner .v { font-size: 42px; font-weight: 700; letter-spacing: -.018em; margin-top: 8px; font-variant-numeric: tabular-nums; }
|
||||
.balance-banner .meta { font-size: 12.5px; color: rgba(255,255,255,.5); margin-top: 8px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.balance-banner .actions { display: flex; gap: 8px; margin-top: 18px; }
|
||||
.balance-banner .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }
|
||||
.balance-banner .btn:hover { background: var(--background-base); }
|
||||
.balance-banner .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }
|
||||
.balance-banner .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
|
||||
.balance-banner > .corner-tr { top: -7px; right: -7px; }
|
||||
.balance-banner > .corner-bl { bottom: -7px; left: -7px; }
|
||||
|
||||
.recharge-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 12px; }
|
||||
.recharge-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px; text-align: center; cursor: pointer; background: var(--surface); position: relative; }
|
||||
/* 主余额 · 突出展示 */
|
||||
.balance-hero { display: flex; flex-direction: column; gap: 4px; }
|
||||
.balance-hero .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.balance-hero .v { font-size: 38px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
|
||||
.balance-hero .meta { font-size: 11.5px; color: rgba(255,255,255,.5); font-family: var(--font-mono); letter-spacing: .02em; margin-top: 4px; }
|
||||
|
||||
/* 子统计 · 月限额 / 已用 */
|
||||
.balance-sub { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 14px 0 0; border-top: 1px solid rgba(255,255,255,.1); }
|
||||
.balance-sub .col { min-width: 0; }
|
||||
.balance-sub .lbl { font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,.5); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.balance-sub .v { font-size: 18px; font-weight: 700; letter-spacing: -.01em; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
||||
.balance-sub .meta { font-size: 10.5px; color: rgba(255,255,255,.42); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
|
||||
.balance-actions { display: flex; gap: 8px; margin-top: auto; }
|
||||
.balance-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); flex: 1; }
|
||||
.balance-actions .btn:hover { background: var(--background-base); }
|
||||
.balance-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); flex: 1; }
|
||||
.balance-actions .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
|
||||
|
||||
/* ─── 快速充值 pane(右栏)─── */
|
||||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
|
||||
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
|
||||
.pane .desc { font-size: 11.5px; color: var(--black-alpha-48); margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.topup-pane { display: flex; flex-direction: column; padding: 20px 22px; margin-bottom: 0; }
|
||||
|
||||
.recharge-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.recharge-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px 16px; text-align: center; cursor: pointer; background: var(--surface); position: relative; transition: background var(--t-base), border-color var(--t-base); }
|
||||
.recharge-card:hover { background: var(--background-lighter); }
|
||||
.recharge-card.selected { border-color: var(--heat); background: var(--heat-12); }
|
||||
.recharge-card.selected::before, .recharge-card.selected::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23FA5D19'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
|
||||
.recharge-card.selected::before { top: -7px; left: -7px; }
|
||||
.recharge-card.selected::after { bottom: -7px; right: -7px; }
|
||||
.recharge-card .amt { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.recharge-card .gift { font-size: 11px; color: var(--black-alpha-48); margin-top: 4px; font-family: var(--font-mono); }
|
||||
.recharge-card .amt { font-size: 19px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.recharge-card .gift { font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); }
|
||||
.recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; }
|
||||
.recharge-card .ribbon { position: absolute; top: -8px; right: 8px; font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--heat); color: var(--accent-white); letter-spacing: .04em; font-weight: 600; border-radius: var(--r-sm); }
|
||||
.pay-row { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 14px; }
|
||||
.pay-row .input { width: 100%; box-sizing: border-box; height: 38px; }
|
||||
.pay-btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.pay-btn-row .btn { width: 100%; }
|
||||
|
||||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
|
||||
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 14px; }
|
||||
.pane h3 + .desc { font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
/* ─── Tab strip ─── */
|
||||
.billing-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border-faint); margin: 26px 0 18px; padding: 0 2px; }
|
||||
.billing-tabs .tab { background: none; border: 0; padding: 10px 16px 11px; font-size: 13px; color: var(--black-alpha-56); font-family: inherit; cursor: pointer; border-bottom: 2px solid transparent; position: relative; top: 1px; letter-spacing: .01em; transition: color var(--t-base); }
|
||||
.billing-tabs .tab:hover { color: var(--accent-black); }
|
||||
.billing-tabs .tab.active { color: var(--accent-black); font-weight: 600; border-bottom-color: var(--heat); }
|
||||
.billing-tabs .tab .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-32); margin-left: 4px; letter-spacing: .02em; font-weight: 400; }
|
||||
.billing-tabs .tab.active .mono { color: var(--heat); }
|
||||
|
||||
.bills .neg { color: var(--accent-black); font-variant-numeric: tabular-nums; font-weight: 500; }
|
||||
.bills .pos { color: var(--accent-forest); font-variant-numeric: tabular-nums; font-weight: 500; }
|
||||
.bills .ref { color: var(--black-alpha-48); font-size: 11px; font-family: var(--font-mono); }
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.usage-line { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; }
|
||||
.usage-line .v { font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }
|
||||
.usage-bar { height: 4px; background: var(--background-lighter); border-radius: 2px; margin: 6px 0 12px; overflow: hidden; }
|
||||
.usage-bar > span { display: block; height: 100%; }
|
||||
/* ─── 总览 · 趋势 + 阶段分布 ─── */
|
||||
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: start; }
|
||||
|
||||
.rule-list { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7; }
|
||||
.rule-list strong { color: var(--accent-black); font-weight: 600; }
|
||||
.rule-list .mono { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; font-size: 11.5px; }
|
||||
.trend-pane { padding: 18px 20px 14px; }
|
||||
.trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }
|
||||
.trend-head h3 { margin-bottom: 0; }
|
||||
.trend-head .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.trend-head .spacer { flex: 1; }
|
||||
.trend-head .chip { font-family: var(--font-mono); font-size: 10.5px; padding: 3px 8px; border: 1px solid var(--border-faint); border-radius: var(--r-pill); color: var(--black-alpha-56); cursor: pointer; }
|
||||
.trend-head .chip.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
|
||||
|
||||
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; height: 170px; padding: 6px 4px 2px; position: relative; }
|
||||
.trend-chart .bars { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; height: 100%; }
|
||||
.trend-chart .bar { background: var(--background-lighter); border-radius: 2px 2px 0 0; position: relative; transition: background var(--t-base); cursor: pointer; }
|
||||
.trend-chart .bar > span { display: block; width: 100%; background: var(--heat); border-radius: 2px 2px 0 0; }
|
||||
.trend-chart .bar:hover > span { background: var(--accent-black); }
|
||||
.trend-chart .bar.peak > span { background: var(--accent-black); }
|
||||
.trend-chart .x-axis { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-32); text-align: center; letter-spacing: .02em; }
|
||||
.trend-foot { display: flex; gap: 14px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-faint); font-size: 12px; }
|
||||
.trend-foot .item { display: flex; align-items: baseline; gap: 6px; }
|
||||
.trend-foot .item .k { color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
|
||||
.trend-foot .item .v { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-black); }
|
||||
.trend-foot .item .v.warn { color: #B45309; }
|
||||
|
||||
/* ─── 阶段分布 ─── */
|
||||
.stage-pane .usage-line { display: flex; justify-content: space-between; padding: 4px 0 4px; font-size: 12.5px; }
|
||||
.stage-pane .usage-line .k { color: var(--accent-black); }
|
||||
.stage-pane .usage-line .v { font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }
|
||||
.stage-pane .usage-bar { height: 4px; background: var(--background-lighter); border-radius: 2px; margin: 4px 0 10px; overflow: hidden; }
|
||||
.stage-pane .usage-bar > span { display: block; height: 100%; transition: width .3s ease; }
|
||||
.stage-pane .total { display: flex; justify-content: space-between; padding-top: 10px; margin-top: 6px; border-top: 1px solid var(--border-faint); font-size: 13px; font-weight: 600; }
|
||||
.stage-pane .total .v { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ─── 扣费规则 + 四层预检 ─── */
|
||||
.rule-pane .rule-list { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7; }
|
||||
.rule-pane .rule-list strong { color: var(--accent-black); font-weight: 600; }
|
||||
.rule-pane .mono-acc { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; font-size: 11.5px; border-radius: var(--r-sm); }
|
||||
|
||||
.quota-rules { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-faint); }
|
||||
.quota-rules .qr-head { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 10px; }
|
||||
.quota-rules .step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; align-items: baseline; margin-bottom: 6px; font-size: 12.5px; color: var(--accent-black); }
|
||||
.quota-rules .step .num { width: 20px; height: 20px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; }
|
||||
.quota-rules .step .formula { font-family: var(--font-mono); font-size: 11.5px; color: var(--heat); background: var(--heat-12); padding: 0 4px; border-radius: var(--r-sm); }
|
||||
|
||||
/* ─── 表格通用 ─── */
|
||||
.billing-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
|
||||
.billing-table th, .billing-table td { padding: 11px 14px; text-align: left; font-size: 12.5px; border-bottom: 1px solid var(--border-faint); }
|
||||
.billing-table thead th { background: var(--background-lighter); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||||
.billing-table tbody tr:last-child td { border-bottom: 0; }
|
||||
.billing-table tbody tr:hover { background: var(--background-lighter); }
|
||||
.billing-table .ts { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.billing-table .neg { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-black); text-align: right; }
|
||||
.billing-table .pos { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-forest); text-align: right; }
|
||||
.billing-table .zero { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--black-alpha-32); text-align: right; }
|
||||
.billing-table .muted { color: var(--black-alpha-56); font-size: 11.5px; }
|
||||
.billing-table .ref { color: var(--black-alpha-48); font-size: 10.5px; font-family: var(--font-mono); }
|
||||
.billing-table .who { display: inline-flex; align-items: center; gap: 8px; }
|
||||
.billing-table .who .av { width: 24px; height: 24px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: inline-grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--accent-black); }
|
||||
.billing-table .role-pill { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: var(--r-pill); font-size: 10.5px; font-weight: 500; }
|
||||
.billing-table .role-pill .dot { width: 5px; height: 5px; border-radius: 50%; }
|
||||
.billing-table .role-super { background: var(--heat-12); color: var(--heat); }
|
||||
.billing-table .role-super .dot { background: var(--heat); }
|
||||
.billing-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
|
||||
.billing-table .role-admin .dot { background: #1E40AF; }
|
||||
.billing-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
|
||||
.billing-table .role-member .dot { background: var(--black-alpha-56); }
|
||||
.billing-table .status-tag { font-family: var(--font-mono); font-size: 10px; padding: 1px 6px; border-radius: var(--r-sm); letter-spacing: .04em; }
|
||||
.billing-table .status-tag.ok { background: rgba(66,195,102,.12); color: var(--accent-forest); }
|
||||
.billing-table .status-tag.wip { background: var(--heat-12); color: var(--heat); }
|
||||
.billing-table .status-tag.fail { background: rgba(235,52,36,.10); color: var(--accent-crimson); }
|
||||
.billing-table .progress-mini { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; margin-left: 8px; }
|
||||
.billing-table .progress-mini > span { display: block; height: 100%; background: var(--heat); }
|
||||
|
||||
/* ─── 流水筛选条 ─── */
|
||||
.filter-bar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
|
||||
.filter-bar select, .filter-bar input { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 6px 10px; font-size: 12.5px; font-family: inherit; color: var(--accent-black); }
|
||||
.filter-bar select { padding-right: 24px; }
|
||||
.filter-bar .spacer { flex: 1; }
|
||||
.filter-bar .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
|
||||
/* 充值 modal */
|
||||
.topup-modal { width: min(460px, 92vw); }
|
||||
.topup-modal .topup-qr { aspect-ratio: 1; max-width: 220px; margin: 4px auto 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; position: relative; overflow: hidden; }
|
||||
.topup-modal .topup-qr::before {
|
||||
content: ''; position: absolute; inset: 18px;
|
||||
background-image: linear-gradient(45deg, var(--accent-black) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--accent-black) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--accent-black) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--accent-black) 75%);
|
||||
background-size: 16px 16px;
|
||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
|
||||
opacity: .14;
|
||||
}
|
||||
.topup-modal .topup-qr .center { position: relative; z-index: 1; background: var(--surface); padding: 10px 12px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .02em; text-align: center; }
|
||||
.topup-modal .topup-info { text-align: center; font-size: 13px; color: var(--black-alpha-56); margin-bottom: 6px; }
|
||||
.topup-modal .topup-amt { text-align: center; font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--heat); margin-bottom: 6px; }
|
||||
.topup-modal .topup-note { text-align: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; margin-bottom: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -66,105 +189,424 @@
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>账户</h1>
|
||||
<div class="sub"><span class="mono">// 余额 · 充值 · 消费明细</span></div>
|
||||
<h1>消费</h1>
|
||||
<div class="sub"><span class="mono">// 余额 · 充值 · 4 维消费视图 + 账单流水</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="acc-grid">
|
||||
<div>
|
||||
<div class="balance-banner">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<div class="lbl">[ CURRENT BALANCE ]</div>
|
||||
<!-- 顶部:余额 banner(左)+ 快速充值(右)左右布局 -->
|
||||
<div class="top-grid">
|
||||
<!-- 左:余额 banner -->
|
||||
<div class="balance-banner">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<div class="balance-hero">
|
||||
<div class="lbl">团队余额</div>
|
||||
<div class="v">¥327.40</div>
|
||||
<div class="meta">// 本月已消费 ¥162.60 · 可使用约 32 个项目</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-lg" onclick="Shell.toast('充值', '/billing/topup')">充值</button>
|
||||
<button class="btn btn-ghost btn-lg" onclick="Shell.toast('提取明细', '/billing/export')">提取消费明细</button>
|
||||
<div class="meta">// 充值累加 · 不重置</div>
|
||||
</div>
|
||||
<div class="balance-sub">
|
||||
<div class="col">
|
||||
<div class="lbl">本月限额</div>
|
||||
<div class="v">¥3,000.00</div>
|
||||
<div class="meta">// 按自然月重置</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="lbl">当月已用</div>
|
||||
<div class="v">¥162.60</div>
|
||||
<div class="meta">// 占比 5.4% · 健康</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane">
|
||||
<h3>快速充值</h3>
|
||||
<div class="desc">// 充值后立刻到账,可开发票</div>
|
||||
<div class="recharge-row">
|
||||
<div class="recharge-card" onclick="Shell.toast('选择 ¥100')"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
|
||||
<div class="recharge-card selected"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
|
||||
<div class="recharge-card" onclick="Shell.toast('选择 ¥1000')"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
|
||||
<div class="recharge-card" onclick="Shell.toast('选择 ¥3000')"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; margin-top:14px;">
|
||||
<input class="input" placeholder="自定义金额(最低 ¥50)" style="flex:1;">
|
||||
<button class="btn btn-primary" onclick="Shell.toast('微信支付', '¥500 · WechatPay')">微信支付 ¥500</button>
|
||||
<button class="btn" onclick="Shell.toast('支付宝', '¥500 · Alipay')">支付宝</button>
|
||||
</div>
|
||||
<div class="balance-actions">
|
||||
<button class="btn btn-lg" onclick="openTopup()">充值</button>
|
||||
<button class="btn btn-ghost btn-lg" onclick="Shell.toast('提取明细', 'CSV · /billing/export')">导出账单</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:baseline; margin-bottom:12px;">
|
||||
<h2 style="font-size:15px; font-weight:600;">消费明细</h2>
|
||||
<span class="spacer"></span>
|
||||
<div class="hstack">
|
||||
<button class="chip" style="height:28px; font-size:12px;">近 30 天</button>
|
||||
<button class="chip" style="height:28px; font-size:12px;">导出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="t bills">
|
||||
<thead>
|
||||
<tr><th>时间</th><th>项目 / 类型</th><th>详情</th><th style="text-align:right;">金额</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.09 14:08</td><td>补水面膜 · v3</td><td class="muted">故事板 image-2 · 1 次</td><td style="text-align:right;" class="neg">-¥0.45</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.09 14:02</td><td>补水面膜 · v3</td><td class="muted">脚本 LLM · 2.4k tokens</td><td style="text-align:right;" class="neg">-¥0.04</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.09 13:38</td><td>补水面膜 · v3</td><td class="muted">基础资产 · 5 张图</td><td style="text-align:right;" class="neg">-¥1.05</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.08 18:21</td><td>透真防晒 · 通勤对比</td><td class="muted">视频片段 · 6 镜</td><td style="text-align:right;" class="neg">-¥1.20</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.08 11:02</td><td>充值</td><td class="muted">微信支付 · <span class="ref">TX2024050811021Z</span></td><td style="text-align:right;" class="pos">+¥500.00</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.07 20:14</td><td>蓝牙耳机 · 开箱</td><td class="muted">视频片段 · 5 镜(1 镜重跑不扣)</td><td style="text-align:right;" class="neg">-¥0.94</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.07 15:48</td><td>咖啡冻干 · 早八</td><td class="muted">故事板生成失败 · <span style="color:var(--black-alpha-48);">不扣费</span></td><td style="text-align:right;" class="muted-2">¥0.00</td></tr>
|
||||
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.06 10:30</td><td>瑜伽裤 · 通勤穿搭</td><td class="muted">项目导出 · 1 次</td><td style="text-align:right;" class="neg">-¥3.20</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="pane">
|
||||
<h3>本月消费分布</h3>
|
||||
<div class="usage-line"><span>视频片段(Seedance)</span><span class="v">¥98.40</span></div>
|
||||
<div class="usage-bar"><span style="width:60%; background:var(--heat);"></span></div>
|
||||
|
||||
<div class="usage-line"><span>故事板(image-2)</span><span class="v">¥36.00</span></div>
|
||||
<div class="usage-bar"><span style="width:22%; background:var(--accent-forest);"></span></div>
|
||||
|
||||
<div class="usage-line"><span>基础资产</span><span class="v">¥21.00</span></div>
|
||||
<div class="usage-bar"><span style="width:13%; background:var(--black-alpha-56);"></span></div>
|
||||
|
||||
<div class="usage-line"><span>脚本 LLM</span><span class="v">¥7.20</span></div>
|
||||
<div class="usage-bar"><span style="width:5%; background:var(--black-alpha-48);"></span></div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="usage-line" style="font-weight:600;"><span>合计</span><span class="v">¥162.60</span></div>
|
||||
<!-- 右:快速充值 -->
|
||||
<div class="pane topup-pane">
|
||||
<h3>快速充值</h3>
|
||||
<div class="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
|
||||
<div class="recharge-row">
|
||||
<div class="recharge-card" data-amt="100"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
|
||||
<div class="recharge-card selected" data-amt="500"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
|
||||
<div class="recharge-card" data-amt="1000"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
|
||||
<div class="recharge-card" data-amt="3000"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
|
||||
</div>
|
||||
<div class="pay-row">
|
||||
<input class="input" id="custom-amt" placeholder="自定义金额(最低 ¥50)">
|
||||
<div class="pay-btn-row">
|
||||
<button class="btn btn-primary" onclick="openTopup('wechat')">微信支付</button>
|
||||
<button class="btn" onclick="openTopup('alipay')">支付宝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane">
|
||||
<h3>扣费规则</h3>
|
||||
<div class="rule-list">
|
||||
<strong>① 失败不扣</strong>:模型超时、内容审核拦截、生成异常一律不扣费。<br>
|
||||
<strong>② 用户重跑不扣首次</strong>:第一次重跑保留原扣费,第二次起按次结算。<br>
|
||||
<strong>③ 仅在你点击 <span class="mono">[ 确认通过 ]</span> 时入账</strong>。<br>
|
||||
<strong>④ 导出不再扣费</strong>,所有 token 已在过程中结算。
|
||||
<!-- Tab strip -->
|
||||
<div class="billing-tabs" role="tablist">
|
||||
<button class="tab active" data-tab="overview" role="tab">总览 <span class="mono">// 当月趋势 + 阶段分布</span></button>
|
||||
<button class="tab" data-tab="by-project" role="tab">按项目 <span class="mono">// 8 个</span></button>
|
||||
<button class="tab" data-tab="by-member" role="tab">按成员 <span class="mono">// 5 人</span></button>
|
||||
<button class="tab" data-tab="bills" role="tab">账单流水 <span class="mono">// 30 天</span></button>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 1: 总览 ===== -->
|
||||
<div class="tab-panel active" id="panel-overview">
|
||||
<div class="overview-grid">
|
||||
<div class="pane trend-pane">
|
||||
<div class="trend-head">
|
||||
<h3>消费趋势</h3>
|
||||
<span class="sub">// 近 14 天 · 单位 ¥</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="chip active">日</button>
|
||||
<button class="chip">周</button>
|
||||
<button class="chip">月</button>
|
||||
</div>
|
||||
<div class="trend-chart">
|
||||
<div class="bars" id="trend-bars">
|
||||
<!-- JS 注入 14 根柱 -->
|
||||
</div>
|
||||
<div class="x-axis" id="trend-xaxis">
|
||||
<!-- JS 注入日期 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="trend-foot">
|
||||
<div class="item"><span class="k">14 天合计</span><span class="v" id="trend-sum">¥0.00</span></div>
|
||||
<div class="item"><span class="k">日均</span><span class="v" id="trend-avg">¥0.00</span></div>
|
||||
<div class="item"><span class="k">峰值</span><span class="v warn" id="trend-peak">¥0.00</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane">
|
||||
<h3>开发票</h3>
|
||||
<div style="font-size:12.5px; color:var(--black-alpha-56); margin-bottom:10px;">本月可开发票额度:<strong style="color:var(--accent-black);">¥162.60</strong></div>
|
||||
<button class="btn" style="width:100%;" onclick="Shell.toast('申请发票', '/billing/invoice')">申请发票</button>
|
||||
<div class="pane stage-pane">
|
||||
<h3>本月按阶段分布</h3>
|
||||
<div class="desc">// PRD §5.3.5 扣费规则 · 仅确认后扣</div>
|
||||
<div class="usage-line"><span class="k">视频片段(Seedance)</span><span class="v">¥98.40</span></div>
|
||||
<div class="usage-bar"><span style="width:60%; background:var(--heat);"></span></div>
|
||||
<div class="usage-line"><span class="k">故事板(image-2)</span><span class="v">¥36.00</span></div>
|
||||
<div class="usage-bar"><span style="width:22%; background:var(--accent-forest);"></span></div>
|
||||
<div class="usage-line"><span class="k">基础资产</span><span class="v">¥21.00</span></div>
|
||||
<div class="usage-bar"><span style="width:13%; background:var(--black-alpha-56);"></span></div>
|
||||
<div class="usage-line"><span class="k">脚本 LLM</span><span class="v">¥7.20</span></div>
|
||||
<div class="usage-bar"><span style="width:5%; background:var(--black-alpha-32);"></span></div>
|
||||
<div class="total"><span>合计</span><span class="v">¥162.60</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane rule-pane" style="margin-top: 16px;">
|
||||
<h3>扣费 + 四层额度预检规则</h3>
|
||||
<div class="desc">// PRD §5.3.5 + §10.3 · 对接团队请以此页为准</div>
|
||||
<div class="rule-list">
|
||||
<strong>① 失败不扣</strong>:模型超时 / 内容审核拦截 / 生成异常一律不扣费。<br>
|
||||
<strong>② 用户重跑不扣首次</strong>:第一次重跑保留原扣费,第二次起按次结算。<br>
|
||||
<strong>③ 仅在你点击 <span class="mono-acc">[ 确认通过 ]</span> 时入账</strong>。<br>
|
||||
<strong>④ 导出不再扣费</strong>,所有 token 已在过程中结算。
|
||||
</div>
|
||||
<div class="quota-rules">
|
||||
<div class="qr-head">// 任务确认前 · 四层额度预检(任一不通过即拦截)</div>
|
||||
<div class="step"><span class="num">1</span><span><strong>个人日剩余</strong> ≥ 任务预估 × <span class="formula">1.2</span></span></div>
|
||||
<div class="step"><span class="num">2</span><span><strong>个人月剩余</strong> ≥ 同上</span></div>
|
||||
<div class="step"><span class="num">3</span><span><strong>团队月剩余</strong> ≥ 同上</span></div>
|
||||
<div class="step"><span class="num">4</span><span><strong>团队总余额</strong> ≥ 同上</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 2: 按项目 ===== -->
|
||||
<div class="tab-panel" id="panel-by-project">
|
||||
<div class="filter-bar">
|
||||
<select><option>全部状态</option><option>进行中</option><option>已完成</option><option>已归档</option></select>
|
||||
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
|
||||
<span class="spacer"></span>
|
||||
<span class="ct">共 <b style="color:var(--accent-black);">8</b> 个项目 · 当月消耗 ¥162.60</span>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目</th>
|
||||
<th>商品</th>
|
||||
<th>所属成员</th>
|
||||
<th>当前阶段</th>
|
||||
<th>状态</th>
|
||||
<th style="text-align:right;">当月消耗</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="proj-body">
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 3: 按成员 ===== -->
|
||||
<div class="tab-panel" id="panel-by-member">
|
||||
<div class="filter-bar">
|
||||
<select><option>全部角色</option><option>超管</option><option>团管</option><option>成员</option></select>
|
||||
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
|
||||
<span class="spacer"></span>
|
||||
<span class="ct">共 <b style="color:var(--accent-black);">5</b> 人 · 当月合计 ¥319.00</span>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>成员</th>
|
||||
<th>角色</th>
|
||||
<th>已完成项目</th>
|
||||
<th>当月已用 / 月度额度</th>
|
||||
<th>最近活跃</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="member-body">
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 4: 账单流水 ===== -->
|
||||
<div class="tab-panel" id="panel-bills">
|
||||
<div class="filter-bar">
|
||||
<select><option>全部阶段</option><option>视频片段</option><option>故事板</option><option>基础资产</option><option>脚本 LLM</option><option>充值</option><option>导出</option></select>
|
||||
<select><option>全部成员</option><option>小李</option><option>张运营</option><option>王小姐</option><option>陈策划</option></select>
|
||||
<select><option>近 30 天</option><option>近 7 天</option><option>本月</option><option>全部</option></select>
|
||||
<span class="spacer"></span>
|
||||
<span class="ct">共 <b style="color:var(--accent-black);">28</b> 条 · <button class="chip" style="height:24px;font-size:11px;margin-left:4px;" onclick="Shell.toast('导出 CSV', '/billing/export?range=30d')">导出 CSV</button></span>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>项目 / 类型</th>
|
||||
<th>详情</th>
|
||||
<th>成员</th>
|
||||
<th>状态</th>
|
||||
<th style="text-align:right;">金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bills-body">
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 充值 modal -->
|
||||
<div class="modal-bg" id="topup-bg" onclick="if(event.target===this)Shell.closeModal('topup-bg')">
|
||||
<div class="modal topup-modal">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<div class="modal-h">
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
|
||||
</div>
|
||||
<div class="ti">扫码支付<span id="topup-channel-label">// 微信支付</span></div>
|
||||
</div>
|
||||
<div class="modal-b">
|
||||
<div class="topup-info">支付金额</div>
|
||||
<div class="topup-amt" id="topup-amt">¥500.00</div>
|
||||
<div class="topup-note" id="topup-bonus">// 含 ¥30 赠送 · 实到账 ¥530</div>
|
||||
<div class="topup-qr">
|
||||
<div class="center" id="topup-channel-name">微信扫码<br><span style="color:var(--black-alpha-32);">/topup/wx/TX...</span></div>
|
||||
</div>
|
||||
<div style="text-align:center; font-family:var(--font-mono); font-size:11px; color:var(--black-alpha-48); letter-spacing:.02em;">// 5 分钟内有效 · 到账后自动关闭</div>
|
||||
</div>
|
||||
<div class="modal-f">
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('topup-bg')">取消</button>
|
||||
<button class="btn btn-primary" type="button" onclick="topupDone()">已完成支付</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/shell.js?v=202605211643"></script>
|
||||
<script>Shell.render({ active: 'account', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '账户' }] });</script>
|
||||
<script>
|
||||
Shell.render({ active: 'account', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消费' }] });
|
||||
|
||||
/* ============================================================
|
||||
Mock 数据
|
||||
============================================================ */
|
||||
const TREND_DAYS = [
|
||||
{ d: '05.08', v: 12.40 }, { d: '05.09', v: 18.80 }, { d: '05.10', v: 6.20 }, { d: '05.11', v: 0 },
|
||||
{ d: '05.12', v: 4.50 }, { d: '05.13', v: 22.10 }, { d: '05.14', v: 14.60 }, { d: '05.15', v: 9.30 },
|
||||
{ d: '05.16', v: 28.40 }, { d: '05.17', v: 13.80 }, { d: '05.18', v: 8.20 }, { d: '05.19', v: 11.50 },
|
||||
{ d: '05.20', v: 19.40 }, { d: '05.21', v: 7.80 },
|
||||
];
|
||||
|
||||
const PROJECTS_BILL = [
|
||||
{ name: '补水面膜 · v3', product: '透真补水面膜', owner: '李', role: 'super', stage: 'Stage 3 故事板', stagePct: 60, status: 'wip', statusLabel: '进行中', amount: 48.20 },
|
||||
{ name: '透真防晒 · 通勤对比', product: '透真防晒', owner: '李', role: 'super', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 32.60 },
|
||||
{ name: '蓝牙耳机 · 开箱测评', product: 'Pro 4 蓝牙耳机', owner: '张', role: 'admin', stage: 'Stage 4 视频', stagePct: 80, status: 'wip', statusLabel: '进行中', amount: 28.40 },
|
||||
{ name: '速食面 · 加班场景', product: '熊猫速食面', owner: '陈', role: 'member', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 12.80 },
|
||||
{ name: '春日新品 · 立体口红', product: '凝彩立体口红', owner: '李', role: 'super', stage: 'Stage 2 资产', stagePct: 40, status: 'wip', statusLabel: '进行中', amount: 18.30 },
|
||||
{ name: '咖啡冻干 · 早八', product: '冷萃咖啡冻干', owner: '王', role: 'member', stage: 'Stage 3 故事板', stagePct: 60, status: 'fail', statusLabel: '失败 · 待重跑', amount: 0 },
|
||||
{ name: '瑜伽裤 · 通勤穿搭', product: '透气速干瑜伽裤', owner: '王', role: 'member', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 14.80 },
|
||||
{ name: '保温杯 · 户外随行', product: '316 保温杯', owner: '张', role: 'admin', stage: 'Stage 1 脚本', stagePct: 20, status: 'wip', statusLabel: '进行中', amount: 7.50 },
|
||||
];
|
||||
|
||||
const MEMBERS_BILL = [
|
||||
{ av: '李', name: '小李', role: 'super', projectsDone: 14, used: 162.60, monthly: 10000, lastActive: '15 分钟前' },
|
||||
{ av: '张', name: '张运营', role: 'admin', projectsDone: 8, used: 98.40, monthly: 6000, lastActive: '10 分钟前' },
|
||||
{ av: '王', name: '王小姐', role: 'member', projectsDone: 4, used: 45.20, monthly: 2000, lastActive: '28 分钟前' },
|
||||
{ av: '陈', name: '陈策划', role: 'member', projectsDone: 1, used: 12.80, monthly: 2000, lastActive: '4 小时前' },
|
||||
{ av: '林', name: '林新人', role: 'member', projectsDone: 0, used: 0.00, monthly: 2000, lastActive: '尚未激活' },
|
||||
];
|
||||
|
||||
const BILLS = [
|
||||
{ ts: '05.21 14:32', proj: '补水面膜 · v3', type: '视频片段', detail: 'Seedance · 场 1 · 1 镜', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.45 },
|
||||
{ ts: '05.21 14:08', proj: '补水面膜 · v3', type: '视频片段', detail: 'Seedance · 场 2 · 1 镜', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },
|
||||
{ ts: '05.20 21:42', proj: '蓝牙耳机 · 开箱测评', type: '视频片段', detail: 'Seedance · 场 3 · 6 镜', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -1.20 },
|
||||
{ ts: '05.20 18:21', proj: '透真防晒 · 通勤对比', type: '视频片段', detail: 'Seedance · 整段 · 6 镜', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -1.20 },
|
||||
{ ts: '05.20 16:00', proj: '蓝牙耳机 · 开箱测评', type: '故事板', detail: 'image-2 · 整张重跑 · 场 2', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },
|
||||
{ ts: '05.20 11:02', proj: '充值', type: '充值', detail: '微信支付 · TX2024052011021Z', who: '李', role: 'super', status: 'ok', statusLabel: '到账', amount: 500.00 },
|
||||
{ ts: '05.19 18:08', proj: '速食面 · 加班场景', type: '故事板', detail: 'image-2 · 场 1', who: '陈', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.30 },
|
||||
{ ts: '05.19 16:08', proj: '补水面膜 · v3', type: '故事板', detail: 'image-2 · 场 1', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.45 },
|
||||
{ ts: '05.19 14:02', proj: '补水面膜 · v3', type: '脚本 LLM', detail: '2.4k tokens · AI 全生', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.04 },
|
||||
{ ts: '05.19 13:38', proj: '补水面膜 · v3', type: '基础资产', detail: 'image-2 · 5 张', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -1.05 },
|
||||
{ ts: '05.19 11:18', proj: '补水面膜 · v3', type: '故事板', detail: 'image-2 · 场 2', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },
|
||||
{ ts: '05.19 11:12', proj: '速食面 · 加班场景', type: '基础资产', detail: 'image-2 · 2 张', who: '陈', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.42 },
|
||||
{ ts: '05.18 15:42', proj: '咖啡冻干 · 早八', type: '故事板', detail: 'image-2 · 场 3', who: '王', role: 'member', status: 'fail', statusLabel: '失败不扣', amount: 0 },
|
||||
{ ts: '05.18 09:42', proj: '蓝牙耳机 · 开箱测评', type: '基础资产', detail: 'image-2 · 4 张', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.84 },
|
||||
{ ts: '05.17 14:38', proj: '蓝牙耳机 · 开箱测评', type: '脚本 LLM', detail: '1.8k tokens · 自带粘贴', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.03 },
|
||||
{ ts: '05.17 10:30', proj: '瑜伽裤 · 通勤穿搭', type: '导出', detail: '1080×1920 · 9:16 · 38s', who: '王', role: 'member', status: 'ok', statusLabel: '免费', amount: 0 },
|
||||
{ ts: '05.17 10:08', proj: '瑜伽裤 · 通勤穿搭', type: '视频片段', detail: 'Seedance · 整段 · 5 镜', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -3.20 },
|
||||
{ ts: '05.16 19:38', proj: '透真防晒 · 通勤对比', type: '视频片段', detail: 'Seedance · 4 镜', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.80 },
|
||||
{ ts: '05.16 11:42', proj: '透真防晒 · 通勤对比', type: '故事板', detail: 'image-2 · 场 2', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.45 },
|
||||
{ ts: '05.15 16:08', proj: '透真防晒 · 通勤对比', type: '基础资产', detail: 'image-2 · 3 张', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.63 },
|
||||
];
|
||||
|
||||
const ROLE_META = {
|
||||
super: { label: '超管', cls: 'role-super' },
|
||||
admin: { label: '团管', cls: 'role-admin' },
|
||||
member: { label: '成员', cls: 'role-member' },
|
||||
};
|
||||
|
||||
function fmtMoney(n) { return '¥' + Math.abs(n).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
|
||||
function amtStr(n) {
|
||||
if (n === 0) return '¥0.00';
|
||||
return (n > 0 ? '+' : '-') + fmtMoney(n);
|
||||
}
|
||||
function amtCls(n) { return n > 0 ? 'pos' : (n === 0 ? 'zero' : 'neg'); }
|
||||
|
||||
/* ─── 趋势柱 ─── */
|
||||
function renderTrend() {
|
||||
const bars = document.getElementById('trend-bars');
|
||||
const xax = document.getElementById('trend-xaxis');
|
||||
const max = Math.max(...TREND_DAYS.map(d => d.v));
|
||||
bars.innerHTML = TREND_DAYS.map(d => {
|
||||
const h = max > 0 ? (d.v / max * 100) : 0;
|
||||
const isPeak = d.v === max;
|
||||
return `<div class="bar${isPeak ? ' peak' : ''}" title="${d.d} · ${fmtMoney(d.v)}"><span style="height: ${h.toFixed(1)}%"></span></div>`;
|
||||
}).join('');
|
||||
xax.innerHTML = TREND_DAYS.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
|
||||
const sum = TREND_DAYS.reduce((s, d) => s + d.v, 0);
|
||||
document.getElementById('trend-sum').textContent = fmtMoney(sum);
|
||||
document.getElementById('trend-avg').textContent = fmtMoney(sum / TREND_DAYS.length);
|
||||
document.getElementById('trend-peak').textContent = fmtMoney(max);
|
||||
}
|
||||
|
||||
/* ─── 按项目 表格 ─── */
|
||||
function renderProjects() {
|
||||
const tb = document.getElementById('proj-body');
|
||||
tb.innerHTML = PROJECTS_BILL.map(p => {
|
||||
const r = ROLE_META[p.role];
|
||||
return `
|
||||
<tr>
|
||||
<td><a href="pipeline.html?product=${encodeURIComponent(p.product)}" style="color:var(--accent-black);text-decoration:none;font-weight:500;">${p.name}</a></td>
|
||||
<td class="muted">${p.product}</td>
|
||||
<td><span class="who"><span class="av">${p.owner}</span><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></span></td>
|
||||
<td><span class="muted">${p.stage}</span><span class="progress-mini"><span style="width:${p.stagePct}%"></span></span></td>
|
||||
<td><span class="status-tag ${p.status}">${p.statusLabel}</span></td>
|
||||
<td class="${amtCls(-p.amount)}">${p.amount === 0 ? '¥0.00' : '-' + fmtMoney(p.amount)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ─── 按成员 表格 ─── */
|
||||
function renderMembers() {
|
||||
const tb = document.getElementById('member-body');
|
||||
tb.innerHTML = MEMBERS_BILL.map(m => {
|
||||
const r = ROLE_META[m.role];
|
||||
const pct = m.monthly > 0 ? (m.used / m.monthly * 100) : 0;
|
||||
return `
|
||||
<tr style="cursor:pointer;" onclick="location.href='team.html'">
|
||||
<td><span class="who"><span class="av">${m.av}</span><strong style="font-weight:500;">${m.name}</strong></span></td>
|
||||
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
|
||||
<td>${m.projectsDone}</td>
|
||||
<td>
|
||||
<strong style="font-variant-numeric:tabular-nums;font-weight:600;">${fmtMoney(m.used)}</strong>
|
||||
<span class="muted"> / ${fmtMoney(m.monthly)} · ${pct.toFixed(1)}%</span>
|
||||
<span class="progress-mini"><span style="width:${Math.min(100, pct)}%; background:${pct >= 85 ? '#B45309' : 'var(--heat)'};"></span></span>
|
||||
</td>
|
||||
<td><span class="ts">${m.lastActive}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ─── 账单流水 表格 ─── */
|
||||
function renderBills() {
|
||||
const tb = document.getElementById('bills-body');
|
||||
tb.innerHTML = BILLS.map(b => {
|
||||
const r = ROLE_META[b.role];
|
||||
return `
|
||||
<tr>
|
||||
<td class="ts">${b.ts}</td>
|
||||
<td><strong style="font-weight:500;">${b.proj}</strong><br><span class="muted">${b.type}</span></td>
|
||||
<td class="muted">${b.detail}</td>
|
||||
<td><span class="who"><span class="av">${b.who}</span></span></td>
|
||||
<td><span class="status-tag ${b.status}">${b.statusLabel}</span></td>
|
||||
<td class="${amtCls(b.amount)}">${b.amount === 0 ? '¥0.00' : (b.amount > 0 ? '+' + fmtMoney(b.amount) : '-' + fmtMoney(b.amount))}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ─── Tab 切换 ─── */
|
||||
document.querySelectorAll('.billing-tabs .tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.billing-tabs .tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── 快速充值卡选择 ─── */
|
||||
document.querySelectorAll('.recharge-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
document.querySelectorAll('.recharge-card').forEach(c => c.classList.remove('selected'));
|
||||
card.classList.add('selected');
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── 充值 modal ─── */
|
||||
function openTopup(channel) {
|
||||
const selected = document.querySelector('.recharge-card.selected');
|
||||
const customRaw = document.getElementById('custom-amt').value.trim();
|
||||
const custom = Number(customRaw);
|
||||
let amt = 500, bonus = 30;
|
||||
if (custom >= 50) { amt = custom; bonus = 0; }
|
||||
else if (selected) {
|
||||
amt = Number(selected.dataset.amt);
|
||||
const bonusEl = selected.querySelector('.gift.bonus');
|
||||
bonus = bonusEl ? Number(bonusEl.textContent.replace(/\D/g, '')) : 0;
|
||||
}
|
||||
document.getElementById('topup-amt').textContent = fmtMoney(amt);
|
||||
document.getElementById('topup-bonus').textContent = bonus > 0
|
||||
? `// 含 ¥${bonus} 赠送 · 实到账 ¥${(amt + bonus).toFixed(2)}`
|
||||
: '// 无赠送';
|
||||
const isAlipay = channel === 'alipay';
|
||||
document.getElementById('topup-channel-label').textContent = isAlipay ? '// 支付宝' : '// 微信支付';
|
||||
document.getElementById('topup-channel-name').innerHTML = (isAlipay ? '支付宝扫码' : '微信扫码') +
|
||||
'<br><span style="color:var(--black-alpha-32);">/topup/' + (isAlipay ? 'ali' : 'wx') + '/TX' + Date.now() + '</span>';
|
||||
Shell.openModal('topup-bg');
|
||||
}
|
||||
function topupDone() {
|
||||
Shell.closeModal('topup-bg');
|
||||
Shell.toast('充值成功', '余额已更新 · 可开发票');
|
||||
}
|
||||
|
||||
/* ─── 初始化 ─── */
|
||||
renderTrend();
|
||||
renderProjects();
|
||||
renderMembers();
|
||||
renderBills();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1440,3 +1440,32 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ─── 缺三视图 badge · 商品 / 人物 卡片 thumb 左上角提示 ─── */
|
||||
.tri-missing-badge {
|
||||
position: absolute;
|
||||
top: 8px; left: 8px;
|
||||
z-index: 2;
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 3px 8px 3px 6px;
|
||||
background: var(--heat);
|
||||
color: var(--accent-white);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: .03em;
|
||||
border-radius: var(--r-sm);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.12);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.tri-missing-badge::before {
|
||||
content: '!';
|
||||
width: 12px; height: 12px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
font-size: 9px; font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
|
||||
|
||||
+22
-22
@@ -82,51 +82,51 @@
|
||||
<a class="more" href="projects.html">[ ALL · 8 ] <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;"><path d="M5 12h14M12 5l7 7-7 7"/></svg></a>
|
||||
</div>
|
||||
<div class="card-hard">
|
||||
<a class="recent-row" href="pipeline.html#stage-3">
|
||||
<a class="recent-row" href="pipeline.html?product=%E9%80%8F%E7%9C%9F%E8%A1%A5%E6%B0%B4%E9%9D%A2%E8%86%9C#stage-3">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">补水面膜 · 痛点种草</div>
|
||||
<div class="sub">补水面膜 / AI 全生 / 6 镜</div>
|
||||
<div class="name">补水面膜 · 痛点种草 · v3</div>
|
||||
<div class="sub">透真补水面膜 / AI 全生 / 7 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>故事板 待确认</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-5">
|
||||
<a class="recent-row" href="pipeline.html?product=%E9%80%8F%E7%9C%9F%E9%98%B2%E6%99%92#stage-5">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">蓝牙耳机 · 开箱测评</div>
|
||||
<div class="sub">南卡 Lite Pro / 自带脚本 / 5 镜</div>
|
||||
<div class="name">透真防晒 · 通勤对比</div>
|
||||
<div class="sub">透真防晒 / AI 全生 / 6 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
<span class="btn btn-sm">打开</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-2">
|
||||
<a class="recent-row" href="pipeline.html?product=Pro%204%20%E8%93%9D%E7%89%99%E8%80%B3%E6%9C%BA#stage-4">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">速食牛肉面 · 一句话主题</div>
|
||||
<div class="sub">滋啦速食 / 一句话 / 4 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>资产生成中</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-4">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">防晒霜 · 对比展示</div>
|
||||
<div class="sub">透真防晒 / AI 全生 / 6 镜</div>
|
||||
<div class="name">蓝牙耳机 · 开箱测评</div>
|
||||
<div class="sub">Pro 4 蓝牙耳机 / 自带脚本 / 6 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>视频生成 4/6</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-3">
|
||||
<a class="recent-row" href="pipeline.html?product=%E5%87%9D%E5%BD%A9%E7%AB%8B%E4%BD%93%E5%8F%A3%E7%BA%A2#stage-2">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">咖啡冻干粉 · 剧情带货</div>
|
||||
<div class="sub">三顿半同款 / 一句话 / 5 镜</div>
|
||||
<div class="name">春日新品 · 立体口红</div>
|
||||
<div class="sub">凝彩立体口红 / 一句话 / 5 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>资产生成中</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html?product=%E5%86%B7%E8%90%83%E5%92%96%E5%95%A1%E5%86%BB%E5%B9%B2#stage-3">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">咖啡冻干 · 早八</div>
|
||||
<div class="sub">冷萃咖啡冻干 / 一句话 / 5 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
|
||||
<span class="pill err"><span class="dot"></span>故事板失败</span>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>登录 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background: var(--background-base); display: grid; place-items: center; padding: 32px 24px; }
|
||||
|
||||
.auth-wrap { width: 100%; max-width: 980px; display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; overflow: hidden; min-height: 560px; }
|
||||
|
||||
/* 4 装订线 */
|
||||
.auth-wrap::before, .auth-wrap::after,
|
||||
.auth-wrap > .corner-tr, .auth-wrap > .corner-bl {
|
||||
content: ''; position: absolute; width: 14px; height: 14px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain; pointer-events: none; z-index: 2;
|
||||
}
|
||||
.auth-wrap::before { top: -7px; left: -7px; }
|
||||
.auth-wrap::after { bottom: -7px; right: -7px; }
|
||||
.auth-wrap > .corner-tr { top: -7px; right: -7px; }
|
||||
.auth-wrap > .corner-bl { bottom: -7px; left: -7px; }
|
||||
|
||||
/* 左侧品牌区 · 深色 */
|
||||
.auth-brand { background: var(--accent-black); color: var(--accent-white); padding: 40px 44px; display: flex; flex-direction: column; position: relative; overflow: hidden; }
|
||||
.auth-brand .logo { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; letter-spacing: -.01em; }
|
||||
.auth-brand .logo .ic { width: 28px; height: 28px; background: var(--heat); border-radius: 6px; display: grid; place-items: center; }
|
||||
.auth-brand .logo .ic svg { width: 16px; height: 16px; color: var(--accent-white); }
|
||||
.auth-brand .tag { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.5); letter-spacing: .08em; text-transform: uppercase; margin-top: 4px; }
|
||||
|
||||
.auth-brand .hero { margin-top: auto; margin-bottom: auto; }
|
||||
.auth-brand .hero h1 { font-size: 30px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; margin: 0 0 14px; }
|
||||
.auth-brand .hero h1 .h { color: var(--heat); }
|
||||
.auth-brand .hero p { font-size: 13.5px; color: rgba(255,255,255,.62); line-height: 1.7; max-width: 320px; margin: 0; }
|
||||
|
||||
/* ASCII 装饰行 */
|
||||
.auth-brand .ascii { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.32); letter-spacing: .04em; line-height: 1.8; margin-top: 28px; }
|
||||
.auth-brand .ascii .ln .k { color: rgba(255,255,255,.5); }
|
||||
.auth-brand .ascii .ln .v { color: var(--heat); }
|
||||
|
||||
.auth-brand .foot { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.32); letter-spacing: .04em; margin-top: 22px; display: flex; gap: 14px; }
|
||||
.auth-brand .foot a { color: rgba(255,255,255,.5); text-decoration: none; }
|
||||
.auth-brand .foot a:hover { color: var(--heat); }
|
||||
|
||||
/* 右侧表单区 */
|
||||
.auth-form { padding: 44px 44px 36px; display: flex; flex-direction: column; }
|
||||
.auth-form .h-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
|
||||
.auth-form h2 { font-size: 22px; font-weight: 600; letter-spacing: -.012em; margin: 0; }
|
||||
.auth-form .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.auth-form .lead { font-size: 13px; color: var(--black-alpha-56); margin: 0 0 28px; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
|
||||
.field-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.field-label .req { color: var(--accent-crimson); }
|
||||
.field-input-wrap { position: relative; }
|
||||
.field-input-wrap .ic-l { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--black-alpha-32); width: 14px; height: 14px; pointer-events: none; }
|
||||
.field input { width: 100%; box-sizing: border-box; padding: 11px 12px 11px 36px; font-size: 13.5px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-family: inherit; color: var(--accent-black); transition: border-color var(--t-base), box-shadow var(--t-base); }
|
||||
.field input:focus { outline: none; border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }
|
||||
.field input::placeholder { color: var(--black-alpha-32); }
|
||||
.field .toggle-pwd { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--black-alpha-48); cursor: pointer; background: none; border: 0; padding: 0; display: grid; place-items: center; }
|
||||
.field .toggle-pwd:hover { color: var(--accent-black); }
|
||||
|
||||
.row-between { display: flex; align-items: center; justify-content: space-between; margin: 4px 0 22px; font-size: 12.5px; }
|
||||
.row-between label { display: inline-flex; align-items: center; gap: 6px; color: var(--black-alpha-56); cursor: pointer; user-select: none; }
|
||||
.row-between label input { width: 13px; height: 13px; accent-color: var(--heat); }
|
||||
.row-between a { color: var(--heat); text-decoration: none; font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .02em; }
|
||||
.row-between a:hover { text-decoration: underline; }
|
||||
|
||||
.btn-cta { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); padding: 12px 14px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: box-shadow var(--t-base), transform var(--t-base); display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.btn-cta:hover { box-shadow: 0 4px 14px rgba(250,93,25,.28); }
|
||||
.btn-cta:active { transform: translateY(1px); }
|
||||
.btn-cta svg { width: 15px; height: 15px; }
|
||||
|
||||
.divider { display: flex; align-items: center; gap: 10px; margin: 22px 0 18px; }
|
||||
.divider .line { flex: 1; height: 1px; background: var(--border-faint); }
|
||||
.divider .txt { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-32); letter-spacing: .08em; text-transform: uppercase; }
|
||||
|
||||
.sso-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.sso-btn { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px 12px; font-size: 12.5px; color: var(--accent-black); cursor: pointer; font-family: inherit; display: inline-flex; align-items: center; justify-content: center; gap: 8px; transition: border-color var(--t-base); }
|
||||
.sso-btn:hover { border-color: var(--black-alpha-32); }
|
||||
.sso-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
.switch-row { margin-top: auto; padding-top: 28px; text-align: center; font-size: 12.5px; color: var(--black-alpha-56); }
|
||||
.switch-row a { color: var(--heat); text-decoration: none; font-weight: 500; }
|
||||
.switch-row a:hover { text-decoration: underline; }
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 820px) {
|
||||
.auth-wrap { grid-template-columns: 1fr; min-height: 0; }
|
||||
.auth-brand { padding: 32px 28px; }
|
||||
.auth-brand .ascii { display: none; }
|
||||
.auth-form { padding: 32px 28px; }
|
||||
}
|
||||
|
||||
/* 顶部小返回链(可选) */
|
||||
.top-back { position: fixed; top: 20px; left: 24px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); text-decoration: none; letter-spacing: .04em; }
|
||||
.top-back:hover { color: var(--heat); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="top-back" href="index.html">← 返回工作台</a>
|
||||
|
||||
<div class="auth-wrap">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<!-- 左:品牌 -->
|
||||
<aside class="auth-brand">
|
||||
<div class="logo">
|
||||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 L19 7 V17 L12 22 L5 17 V7 Z"/><path d="M12 2 V22"/><path d="M5 7 L19 17"/></svg></span>
|
||||
<span>流·Studio</span>
|
||||
</div>
|
||||
<div class="tag">// SHORT-VIDEO COMMERCE PLATFORM</div>
|
||||
|
||||
<div class="hero">
|
||||
<h1>AI 全流程<br><span class="h">短剧化</span>带货生成</h1>
|
||||
<p>商品 → AI 脚本 → 故事板 → Seedance 视频片段 → 一键导出 9:16 ≤60s 成片。</p>
|
||||
</div>
|
||||
|
||||
<div class="ascii">
|
||||
<div class="ln"><span class="k">// step·1</span> 脚本生成 <span class="v">●●●●●</span></div>
|
||||
<div class="ln"><span class="k">// step·2</span> 基础资产 <span class="v">●●●●○</span></div>
|
||||
<div class="ln"><span class="k">// step·3</span> 故事板 <span class="v">●●●○○</span></div>
|
||||
<div class="ln"><span class="k">// step·4</span> 视频片段 <span class="v">●●○○○</span></div>
|
||||
<div class="ln"><span class="k">// step·5</span> 拼接导出 <span class="v">●○○○○</span></div>
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<a href="#">关于</a>
|
||||
<a href="#">定价</a>
|
||||
<a href="#">联系</a>
|
||||
<a href="#">隐私</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右:表单 -->
|
||||
<main class="auth-form">
|
||||
<div class="h-row">
|
||||
<h2>登录</h2>
|
||||
<span class="sub">// /auth/login</span>
|
||||
</div>
|
||||
<p class="lead">使用团队邀请邮箱登录,接受邀请后自动加入对应团队。</p>
|
||||
|
||||
<form id="login-form" autocomplete="off" onsubmit="event.preventDefault(); doLogin();">
|
||||
<div class="field">
|
||||
<label class="field-label" for="auth-email">邮箱 <span class="req">*</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="m22 6-10 7L2 6"/></svg>
|
||||
<input type="email" id="auth-email" placeholder="name@company.com" value="li@shop.com" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="auth-pwd">密码 <span class="req">*</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<input type="password" id="auth-pwd" placeholder="••••••••" value="demo-1234" required>
|
||||
<button type="button" class="toggle-pwd" aria-label="切换密码可见" onclick="togglePwd()">
|
||||
<svg id="pwd-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-between">
|
||||
<label><input type="checkbox" checked> 记住我 7 天</label>
|
||||
<a href="#" onclick="event.preventDefault();alert('演示稿 · 真实流程为重置密码邮件');">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
<button class="btn-cta" type="submit">
|
||||
登录
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="divider"><span class="line"></span><span class="txt">OR</span><span class="line"></span></div>
|
||||
|
||||
<div class="sso-row">
|
||||
<button type="button" class="sso-btn" onclick="alert('演示稿 · 微信扫码登录占位');">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 4C5 4 2 6.5 2 9.5c0 1.7 1 3.2 2.5 4.2L4 16l2.2-1.1c.7.2 1.5.3 2.3.3-.3-.5-.5-1.1-.5-1.7 0-2.8 2.9-5 6.5-5h.5C14.6 6 12 4 9 4zm-2 3.5c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zm4 0c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zM15 9c-3.3 0-6 2.2-6 5s2.7 5 6 5c.7 0 1.4-.1 2-.3L19 20l-.4-1.7c1.4-.9 2.4-2.3 2.4-3.8 0-2.8-2.7-5-6-5zm-2 2.5c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zm4 0c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1z"/></svg>
|
||||
微信扫码
|
||||
</button>
|
||||
<button type="button" class="sso-btn" onclick="alert('演示稿 · 飞书 SSO 占位');">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>
|
||||
飞书 SSO
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
还没账号? <a href="register.html">注册团队 →</a>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePwd() {
|
||||
const inp = document.getElementById('auth-pwd');
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
function doLogin() {
|
||||
const btn = document.querySelector('.btn-cta');
|
||||
btn.innerHTML = '<span style="font-family:var(--font-mono);font-size:12px;letter-spacing:.04em;">// 验证中...</span>';
|
||||
btn.disabled = true;
|
||||
setTimeout(() => { location.href = 'index.html'; }, 700);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+77
-95
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>商品库 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211800">
|
||||
<style>
|
||||
/* ─── 全局 viewport 高度链 (让右侧 toolbar/分页吸顶吸底) ─── */
|
||||
/* 整页滚动 · 头部 H1+actions sticky 固定 · 其他随页面滚 */
|
||||
@@ -1016,6 +1016,60 @@
|
||||
<script>
|
||||
Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });
|
||||
|
||||
// ============== 注入用户新建的商品(来自工作台 / 商品库 drawer,写入 localStorage)==============
|
||||
// 必须在 const cards / TOTAL / CAT_COUNT 之前执行,让后续逻辑把它们当普通商品处理
|
||||
(function injectExtraProducts() {
|
||||
let pending;
|
||||
try {
|
||||
pending = JSON.parse(localStorage.getItem('fs-extra-products') || '[]');
|
||||
} catch (e) { return; }
|
||||
if (!Array.isArray(pending) || !pending.length) return;
|
||||
const grid = document.getElementById('product-grid');
|
||||
if (!grid) return;
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
|
||||
// 按 createdAt 升序排,然后逐个 insertBefore firstChild → 最新的排最上
|
||||
pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
|
||||
pending.forEach(p => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'product-card';
|
||||
card.dataset.cat = p.cat || '美妆个护';
|
||||
card.dataset.name = p.name || '';
|
||||
card.dataset.tags = p.tags || '';
|
||||
card.dataset.added = '0';
|
||||
card.dataset.assets = String(p.assets || 0);
|
||||
card.dataset.videos = String(p.videos || 0);
|
||||
card.dataset.date = p.date || new Date(p.createdAt || Date.now()).toISOString().slice(0, 10);
|
||||
card.setAttribute('onclick', `location.href='product-detail.html?t='+Date.now()+'&product=${encodeURIComponent(p.name || '')}'`);
|
||||
card.dataset.triview = '0';
|
||||
card.innerHTML = `
|
||||
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder product-thumb">
|
||||
<span class="tri-missing-badge" title="该商品尚未生成商品三视图,进入详情可在工作流第一步补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="ph-frame">${esc(p.name)} · 新建</span>
|
||||
</div>
|
||||
<div class="product-body">
|
||||
<div class="product-name">${esc(p.name)}</div>
|
||||
<div class="product-cat">${esc(p.cat || '美妆个护')}</div>
|
||||
<div class="product-date">${esc(p.date || '')} 创建</div>
|
||||
</div>
|
||||
<div class="product-footer">
|
||||
<span class="stat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
|
||||
素材 <b>${p.assets || 0}</b>
|
||||
</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="stat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
|
||||
视频 <b>${p.videos || 0}</b>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
grid.insertBefore(card, grid.firstChild);
|
||||
});
|
||||
})();
|
||||
|
||||
// ============== Products: 商品分类多选 + 搜索 + 创建时间筛选 ==============
|
||||
// state.cats: Set<string>,空集 = 全部
|
||||
const PAGE_SIZES = [12, 24, 48, 96];
|
||||
@@ -1675,9 +1729,8 @@ blInput.addEventListener('keydown', e => {
|
||||
}
|
||||
});
|
||||
|
||||
// 创建商品 (真正插入 grid)
|
||||
// 创建商品 · 持久化到 localStorage + 立即跳详情(中间不闪商品库)
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// 防呆:每一步明确反馈,清晰看出在哪一步出问题
|
||||
const name = document.getElementById('pf-name').value.trim();
|
||||
const cat = document.getElementById('pf-cat').value;
|
||||
if (!name) {
|
||||
@@ -1689,110 +1742,39 @@ saveBtn.addEventListener('click', () => {
|
||||
Shell.toast('请上传商品主图', '至少 1 张 · 必填');
|
||||
return;
|
||||
}
|
||||
// 收集核心卖点:存在 .bl-item .bl-text 里(已确认的)+ .bl-add input(用户打了字但未回车)
|
||||
const confirmedBullets = [...document.querySelectorAll('#pf-bullets .bl-item .bl-text')]
|
||||
.map(el => el.textContent.trim()).filter(Boolean);
|
||||
const pendingInput = document.querySelector('#pf-bullets .bl-add .bl-input');
|
||||
const pendingText = (pendingInput?.value || '').trim();
|
||||
if (pendingText) confirmedBullets.push(pendingText); // 包容用户没按回车的情况
|
||||
if (pendingText) confirmedBullets.push(pendingText);
|
||||
if (confirmedBullets.length === 0) {
|
||||
Shell.toast('请填写核心卖点', '至少 1 条 · 回车确认');
|
||||
pendingInput?.focus();
|
||||
return;
|
||||
}
|
||||
const bullets = confirmedBullets;
|
||||
// 创建 card 并插入 grid 最前
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'product-card';
|
||||
card.dataset.cat = cat;
|
||||
card.dataset.name = name;
|
||||
card.dataset.tags = '';
|
||||
card.dataset.added = '0';
|
||||
card.dataset.assets = '0';
|
||||
card.dataset.videos = '0';
|
||||
card.dataset.date = today;
|
||||
card.setAttribute('onclick', "location.href='product-detail.html?t='+Date.now()");
|
||||
card.innerHTML = `
|
||||
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder product-thumb"><span class="ph-frame">${name} · 新建</span></div>
|
||||
<div class="product-body">
|
||||
<div class="product-name">${name}</div>
|
||||
<div class="product-cat">${cat}</div>
|
||||
<div class="product-date">${today} 创建</div>
|
||||
</div>
|
||||
<div class="product-footer">
|
||||
<span class="stat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
|
||||
素材 <b>0</b>
|
||||
</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="stat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
|
||||
视频 <b>0</b>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
const grid = document.getElementById('product-grid');
|
||||
grid.insertBefore(card, grid.firstChild);
|
||||
// 给新卡片注入交互:stat hover/click + del-btn 删除确认
|
||||
card.querySelectorAll('.product-footer .stat').forEach(stat => {
|
||||
const text = stat.textContent.trim();
|
||||
const m = text.match(/^(素材|视频)\s*(\d+)/);
|
||||
if (!m) return;
|
||||
const isAsset = m[1] === '素材';
|
||||
const svg = stat.querySelector('svg');
|
||||
stat.innerHTML = '';
|
||||
if (svg) stat.appendChild(svg);
|
||||
const dft = document.createElement('span');
|
||||
dft.className = 'stat-default';
|
||||
dft.innerHTML = `${m[1]} <b>${m[2]}</b>`;
|
||||
const hv = document.createElement('span');
|
||||
hv.className = 'stat-hover';
|
||||
hv.textContent = isAsset ? '+ 去生成素材' : '+ 去生成视频';
|
||||
stat.appendChild(dft); stat.appendChild(hv);
|
||||
stat.dataset.type = isAsset ? 'asset' : 'video';
|
||||
stat.title = isAsset ? '去生成 AI 素材' : '去生成视频项目';
|
||||
stat.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const n = card.dataset.name || '';
|
||||
if (isAsset) openGenAssetChoice(n);
|
||||
else location.href = 'projects-new.html?t=' + Date.now() + '&product=' + encodeURIComponent(n);
|
||||
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
|
||||
// 自动从 storage 读出并 prepend 到 grid
|
||||
try {
|
||||
const KEY = 'fs-extra-products';
|
||||
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
|
||||
list.push({
|
||||
id: 'pp-' + Date.now(),
|
||||
name, cat,
|
||||
tags: '',
|
||||
assets: 0,
|
||||
videos: 0,
|
||||
bullets,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
});
|
||||
const delBtn = card.querySelector('.card-del-btn');
|
||||
if (delBtn) {
|
||||
delBtn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
openDelConfirm([card]);
|
||||
});
|
||||
}
|
||||
// 同步 SKU 计数(标题 + 列表 meta + 侧栏 badge)
|
||||
const remaining = document.querySelectorAll('.product-card').length;
|
||||
const skuCount = document.getElementById('sku-count');
|
||||
if (skuCount) skuCount.textContent = remaining;
|
||||
const meta = document.querySelector('#result-meta .count');
|
||||
if (meta) meta.textContent = remaining;
|
||||
const sidebar = document.querySelector('aside.sidebar a[href="products.html"] .pill-mini');
|
||||
if (sidebar) sidebar.textContent = remaining;
|
||||
localStorage.setItem(KEY, JSON.stringify(list));
|
||||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||||
|
||||
Shell.toast('商品已创建 · 即将跳转详情', `+ ${name} · ${bullets.length} 条卖点`);
|
||||
closeNewProductDrawer();
|
||||
// 重置表单(为下次新建准备)
|
||||
document.getElementById('pf-name').value = '';
|
||||
document.getElementById('pf-cat').value = cat;
|
||||
if (typeof pfFiles !== 'undefined') pfFiles.length = 0;
|
||||
const pfGrid = document.getElementById('pf-grid');
|
||||
if (pfGrid) pfGrid.innerHTML = '';
|
||||
const blList = document.getElementById('pf-bullets');
|
||||
if (blList) blList.querySelectorAll('.bl-item').forEach(li => li.remove());
|
||||
const pendingI = document.querySelector('#pf-bullets .bl-add .bl-input');
|
||||
if (pendingI) pendingI.value = '';
|
||||
// 1200ms 后跳转(让 toast 看清),商品库新建商品后跳转到该商品的详情页
|
||||
setTimeout(() => {
|
||||
location.href = 'product-detail.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
|
||||
}, 1200);
|
||||
// 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底消除"闪商品库"
|
||||
// 浏览器导航开始后,整页会被新页面替换,drawer 自然消失
|
||||
Shell.toast('商品已创建 · 跳转详情', `+ ${name} · ${bullets.length} 条卖点`);
|
||||
location.href = 'product-detail.html?t=' + Date.now() + '&product=' + encodeURIComponent(name) + '&id=new';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -558,7 +558,9 @@ Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.h
|
||||
// 按 createdAt 升序 → 倒序 insert,最新的排最上
|
||||
pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
|
||||
pending.forEach(p => {
|
||||
const href = `pipeline.html#stage-${p.stage || 1}`;
|
||||
// 链接带 ?product= 让 pipeline 显示正确商品名(贯穿 5 stage)
|
||||
const productQ = p.product ? `?product=${encodeURIComponent(p.product)}` : '';
|
||||
const href = `pipeline.html${productQ}#stage-${p.stage || 1}`;
|
||||
const status = p.status || 'wip';
|
||||
const shots = p.shots || 5;
|
||||
const durLabel = p.durationLabel || '0-15s';
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>注册团队 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; background: var(--background-base); display: grid; place-items: center; padding: 32px 24px; }
|
||||
|
||||
.auth-wrap { width: 100%; max-width: 980px; display: grid; grid-template-columns: minmax(0, 1fr) 460px; gap: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; overflow: hidden; min-height: 620px; }
|
||||
|
||||
/* 4 装订线 */
|
||||
.auth-wrap::before, .auth-wrap::after,
|
||||
.auth-wrap > .corner-tr, .auth-wrap > .corner-bl {
|
||||
content: ''; position: absolute; width: 14px; height: 14px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain; pointer-events: none; z-index: 2;
|
||||
}
|
||||
.auth-wrap::before { top: -7px; left: -7px; }
|
||||
.auth-wrap::after { bottom: -7px; right: -7px; }
|
||||
.auth-wrap > .corner-tr { top: -7px; right: -7px; }
|
||||
.auth-wrap > .corner-bl { bottom: -7px; left: -7px; }
|
||||
|
||||
/* 左:品牌 + 价值点 */
|
||||
.auth-brand { background: var(--accent-black); color: var(--accent-white); padding: 40px 44px; display: flex; flex-direction: column; position: relative; overflow: hidden; }
|
||||
.auth-brand .logo { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; letter-spacing: -.01em; }
|
||||
.auth-brand .logo .ic { width: 28px; height: 28px; background: var(--heat); border-radius: 6px; display: grid; place-items: center; }
|
||||
.auth-brand .logo .ic svg { width: 16px; height: 16px; color: var(--accent-white); }
|
||||
.auth-brand .tag { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.5); letter-spacing: .08em; text-transform: uppercase; margin-top: 4px; }
|
||||
|
||||
.auth-brand .hero { margin-top: 28px; }
|
||||
.auth-brand .hero h1 { font-size: 28px; font-weight: 700; letter-spacing: -.02em; line-height: 1.25; margin: 0 0 14px; }
|
||||
.auth-brand .hero h1 .h { color: var(--heat); }
|
||||
.auth-brand .hero p { font-size: 13.5px; color: rgba(255,255,255,.62); line-height: 1.7; max-width: 320px; margin: 0; }
|
||||
|
||||
/* 价值点列表 */
|
||||
.val-list { margin: 30px 0 0; display: flex; flex-direction: column; gap: 14px; }
|
||||
.val-item { display: grid; grid-template-columns: 24px minmax(0,1fr); gap: 12px; align-items: start; }
|
||||
.val-item .ic-v { width: 22px; height: 22px; border: 1px solid rgba(255,255,255,.18); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--heat); }
|
||||
.val-item .ic-v svg { width: 12px; height: 12px; }
|
||||
.val-item .txt-v { font-size: 12.5px; color: rgba(255,255,255,.78); line-height: 1.5; }
|
||||
.val-item .txt-v b { color: var(--accent-white); font-weight: 600; }
|
||||
|
||||
.auth-brand .foot { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.32); letter-spacing: .04em; margin-top: auto; padding-top: 24px; display: flex; gap: 14px; }
|
||||
.auth-brand .foot a { color: rgba(255,255,255,.5); text-decoration: none; }
|
||||
.auth-brand .foot a:hover { color: var(--heat); }
|
||||
|
||||
/* 右:表单 */
|
||||
.auth-form { padding: 40px 44px 32px; display: flex; flex-direction: column; }
|
||||
.auth-form .h-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
|
||||
.auth-form h2 { font-size: 22px; font-weight: 600; letter-spacing: -.012em; margin: 0; }
|
||||
.auth-form .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.auth-form .lead { font-size: 13px; color: var(--black-alpha-56); margin: 0 0 22px; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||
.field-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.field-label .req { color: var(--accent-crimson); }
|
||||
.field-label .hint { margin-left: auto; font-size: 10.5px; color: var(--black-alpha-32); }
|
||||
.field-input-wrap { position: relative; }
|
||||
.field-input-wrap .ic-l { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--black-alpha-32); width: 14px; height: 14px; pointer-events: none; }
|
||||
.field input { width: 100%; box-sizing: border-box; padding: 11px 12px 11px 36px; font-size: 13.5px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-family: inherit; color: var(--accent-black); transition: border-color var(--t-base), box-shadow var(--t-base); }
|
||||
.field input:focus { outline: none; border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }
|
||||
.field input::placeholder { color: var(--black-alpha-32); }
|
||||
.field .toggle-pwd { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--black-alpha-48); cursor: pointer; background: none; border: 0; padding: 0; display: grid; place-items: center; }
|
||||
.field .toggle-pwd:hover { color: var(--accent-black); }
|
||||
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
|
||||
.field-row .field { margin-bottom: 0; }
|
||||
|
||||
.agree { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--black-alpha-56); line-height: 1.6; margin: 8px 0 18px; cursor: pointer; user-select: none; }
|
||||
.agree input { margin-top: 3px; width: 13px; height: 13px; accent-color: var(--heat); flex-shrink: 0; }
|
||||
.agree a { color: var(--heat); text-decoration: none; }
|
||||
.agree a:hover { text-decoration: underline; }
|
||||
|
||||
.btn-cta { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); padding: 12px 14px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: box-shadow var(--t-base), transform var(--t-base); display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.btn-cta:hover { box-shadow: 0 4px 14px rgba(250,93,25,.28); }
|
||||
.btn-cta:active { transform: translateY(1px); }
|
||||
.btn-cta:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
|
||||
.btn-cta svg { width: 15px; height: 15px; }
|
||||
|
||||
.switch-row { margin-top: 22px; text-align: center; font-size: 12.5px; color: var(--black-alpha-56); }
|
||||
.switch-row a { color: var(--heat); text-decoration: none; font-weight: 500; }
|
||||
.switch-row a:hover { text-decoration: underline; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.auth-wrap { grid-template-columns: 1fr; min-height: 0; }
|
||||
.auth-brand { padding: 32px 28px; }
|
||||
.auth-brand .val-list { display: none; }
|
||||
.auth-form { padding: 32px 28px; }
|
||||
}
|
||||
|
||||
.top-back { position: fixed; top: 20px; left: 24px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); text-decoration: none; letter-spacing: .04em; }
|
||||
.top-back:hover { color: var(--heat); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="top-back" href="login.html">← 返回登录</a>
|
||||
|
||||
<div class="auth-wrap">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<aside class="auth-brand">
|
||||
<div class="logo">
|
||||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 L19 7 V17 L12 22 L5 17 V7 Z"/><path d="M12 2 V22"/><path d="M5 7 L19 17"/></svg></span>
|
||||
<span>流·Studio</span>
|
||||
</div>
|
||||
<div class="tag">// SHORT-VIDEO COMMERCE PLATFORM</div>
|
||||
|
||||
<div class="hero">
|
||||
<h1>开通团队,<br>开始 <span class="h">AI 带货</span> 第一条短剧</h1>
|
||||
<p>个人 / 企业团队 1-2 小时出成片,失败不扣费,确认后扣。</p>
|
||||
</div>
|
||||
|
||||
<div class="val-list">
|
||||
<div class="val-item">
|
||||
<span class="ic-v"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></span>
|
||||
<span class="txt-v"><b>5 阶段流水线</b> · 脚本 → 资产 → 故事板 → 视频片段 → 拼接导出</span>
|
||||
</div>
|
||||
<div class="val-item">
|
||||
<span class="ic-v"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></span>
|
||||
<span class="txt-v"><b>失败不扣费</b> · 任务仅在用户通过时扣费,失败 / 超时 / 重跑一律不扣</span>
|
||||
</div>
|
||||
<div class="val-item">
|
||||
<span class="ic-v"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></span>
|
||||
<span class="txt-v"><b>团队 + 角色 + 四层额度</b> · 超管 / 团管 / 成员,日 / 月 + 团队 / 总四层防超支</span>
|
||||
</div>
|
||||
<div class="val-item">
|
||||
<span class="ic-v"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></span>
|
||||
<span class="txt-v"><b>跨项目资产库</b> · 主播 / 场景 / 商品图沉淀复用,不重复生成</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<a href="#">关于</a>
|
||||
<a href="#">定价</a>
|
||||
<a href="#">联系</a>
|
||||
<a href="#">隐私</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="auth-form">
|
||||
<div class="h-row">
|
||||
<h2>注册团队</h2>
|
||||
<span class="sub">// /auth/register</span>
|
||||
</div>
|
||||
<p class="lead">填写团队信息开通账户,默认成为团队超管。</p>
|
||||
|
||||
<form id="register-form" autocomplete="off" onsubmit="event.preventDefault(); doRegister();">
|
||||
<div class="field">
|
||||
<label class="field-label" for="reg-team">团队名 <span class="req">*</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg>
|
||||
<input type="text" id="reg-team" placeholder="例: 小李的店 / XX 文化传媒" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="reg-email">超管邮箱 <span class="req">*</span><span class="hint">用于成员邀请 + 找回密码</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="m22 6-10 7L2 6"/></svg>
|
||||
<input type="email" id="reg-email" placeholder="name@company.com" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field-label" for="reg-pwd">密码 <span class="req">*</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<input type="password" id="reg-pwd" placeholder="至少 8 位" required>
|
||||
<button type="button" class="toggle-pwd" onclick="togglePwd('reg-pwd')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="reg-pwd2">确认密码 <span class="req">*</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<input type="password" id="reg-pwd2" placeholder="再输一次" required>
|
||||
<button type="button" class="toggle-pwd" onclick="togglePwd('reg-pwd2')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="reg-invite">邀请码 <span class="hint">可选 · 团队邀请才需要</span></label>
|
||||
<div class="field-input-wrap">
|
||||
<svg class="ic-l" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
|
||||
<input type="text" id="reg-invite" placeholder="例: TEAM-XXXX-XXXX">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="agree">
|
||||
<input type="checkbox" id="reg-agree" checked>
|
||||
<span>我已阅读并同意 <a href="#" onclick="event.preventDefault()">用户协议</a> 与 <a href="#" onclick="event.preventDefault()">隐私政策</a>,知悉「失败不扣费 · 确认后扣」的扣费规则。</span>
|
||||
</label>
|
||||
|
||||
<button class="btn-cta" type="submit" id="reg-submit">
|
||||
创建团队 · 开始使用
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="switch-row">
|
||||
已有账号? <a href="login.html">登录 →</a>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePwd(id) {
|
||||
const inp = document.getElementById(id);
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
function doRegister() {
|
||||
const team = document.getElementById('reg-team').value.trim();
|
||||
const email = document.getElementById('reg-email').value.trim();
|
||||
const p1 = document.getElementById('reg-pwd').value;
|
||||
const p2 = document.getElementById('reg-pwd2').value;
|
||||
const agree = document.getElementById('reg-agree').checked;
|
||||
if (!team || !email) return alert('请补全团队名 + 邮箱');
|
||||
if (p1.length < 8) return alert('密码至少 8 位');
|
||||
if (p1 !== p2) return alert('两次密码不一致');
|
||||
if (!agree) return alert('请同意用户协议');
|
||||
const btn = document.getElementById('reg-submit');
|
||||
btn.innerHTML = '<span style="font-family:var(--font-mono);font-size:12px;letter-spacing:.04em;">// 创建团队中...</span>';
|
||||
btn.disabled = true;
|
||||
setTimeout(() => { location.href = 'index.html'; }, 900);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+345
-74
@@ -11,10 +11,16 @@
|
||||
|
||||
.settings-nav { position: sticky; top: 16px; }
|
||||
.settings-nav .nav-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; padding: 0 12px 8px; }
|
||||
.settings-nav a { display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: background var(--t-base), border-color var(--t-base); }
|
||||
.settings-nav a { display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: background var(--t-base), border-color var(--t-base); position: relative; }
|
||||
.settings-nav a:hover { background: var(--background-lighter); }
|
||||
.settings-nav a:focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }
|
||||
.settings-nav a.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
|
||||
.settings-nav a svg { width: 16px; height: 16px; stroke-width: 1.5; }
|
||||
.settings-nav a .nav-badge { margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); padding: 1px 6px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-pill); letter-spacing: .02em; line-height: 14px; }
|
||||
.settings-nav a.active .nav-badge { color: var(--heat); background: var(--accent-white); border-color: var(--heat-20); }
|
||||
.settings-nav a .nav-dot { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 6px; height: 6px; border-radius: 50%; background: var(--heat); display: none; }
|
||||
.settings-nav a.has-changes .nav-dot { display: block; }
|
||||
.settings-nav a.active .nav-dot { right: -4px; }
|
||||
|
||||
/* ─── pane ─── */
|
||||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 24px; margin-bottom: 16px; }
|
||||
@@ -73,6 +79,23 @@
|
||||
.device-row .meta { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
|
||||
.device-row .tag-cur { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--accent-forest); color: var(--accent-white); border-radius: var(--r-sm); margin-left: 8px; letter-spacing: .04em; font-weight: 600; }
|
||||
.device-row .spacer { margin-left: auto; }
|
||||
|
||||
/* ─── 头像上传 modal · V2.1 Restraint ─── */
|
||||
.av-up-modal { width: min(440px, 92vw); max-width: min(440px, 92vw); }
|
||||
|
||||
/* 预览区:左圆头像 + 右 mono 元数据 · 装订线点划线分隔 */
|
||||
.av-up-preview-row { display: flex; align-items: center; gap: 14px; padding-bottom: 14px; margin-bottom: 14px; position: relative; }
|
||||
.av-up-preview-row::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: repeating-linear-gradient(to right, var(--border-faint) 0, var(--border-faint) 4px, transparent 4px, transparent 8px); }
|
||||
.av-up-preview { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 22px; font-weight: 600; color: var(--accent-black); overflow: hidden; flex: 0 0 64px; }
|
||||
.av-up-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.av-up-preview-meta { min-width: 0; }
|
||||
.av-up-preview-meta .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); margin-bottom: 3px; letter-spacing: .01em; }
|
||||
.av-up-preview-meta .d { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; line-height: 1.55; }
|
||||
|
||||
/* 规则文本 · 纯 mono, 装订线分隔 */
|
||||
.av-up-rules { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--border-faint); font-size: 11px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; line-height: 1.7; }
|
||||
.av-up-rules .li { display: flex; gap: 8px; }
|
||||
.av-up-rules .li::before { content: '//'; color: var(--black-alpha-32); flex: 0 0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -94,33 +117,40 @@
|
||||
|
||||
<div class="settings-grid">
|
||||
<!-- 左侧 nav -->
|
||||
<aside class="settings-nav">
|
||||
<aside class="settings-nav" role="tablist" aria-label="设置分区">
|
||||
<div class="nav-h">个人</div>
|
||||
<a href="#sec-profile" class="active" data-jump="sec-profile">
|
||||
<a href="#sec-profile" class="active" data-jump="sec-profile" role="tab" aria-controls="sec-profile">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
|
||||
个人信息
|
||||
<span>个人信息</span>
|
||||
<span class="nav-dot" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a href="#sec-security" data-jump="sec-security">
|
||||
<a href="#sec-security" data-jump="sec-security" role="tab" aria-controls="sec-security">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
安全
|
||||
<span>安全</span>
|
||||
<span class="nav-badge" data-count-source="sec-security">3</span>
|
||||
<span class="nav-dot" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a href="#sec-notify" data-jump="sec-notify">
|
||||
<a href="#sec-notify" data-jump="sec-notify" role="tab" aria-controls="sec-notify">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
||||
通知
|
||||
<span>通知</span>
|
||||
<span class="nav-badge" data-count-source="sec-notify">5</span>
|
||||
<span class="nav-dot" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="nav-h" style="margin-top: 16px;">偏好</div>
|
||||
<a href="#sec-pref" data-jump="sec-pref">
|
||||
<a href="#sec-pref" data-jump="sec-pref" role="tab" aria-controls="sec-pref">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg>
|
||||
创作默认
|
||||
<span>创作默认</span>
|
||||
<span class="nav-dot" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a href="#sec-display" data-jump="sec-display">
|
||||
<a href="#sec-display" data-jump="sec-display" role="tab" aria-controls="sec-display">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
|
||||
显示
|
||||
<span>显示</span>
|
||||
<span class="nav-dot" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="nav-h" style="margin-top: 16px;">账号</div>
|
||||
<a href="#sec-danger" data-jump="sec-danger" style="color: var(--accent-crimson);">
|
||||
<a href="#sec-danger" data-jump="sec-danger" role="tab" aria-controls="sec-danger" style="color: var(--accent-crimson);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
|
||||
危险操作
|
||||
<span>危险操作</span>
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
@@ -135,10 +165,10 @@
|
||||
<div class="lbl">头像</div>
|
||||
<div class="val">
|
||||
<div class="avatar-edit">
|
||||
<div class="av-big">李</div>
|
||||
<div class="av-big" id="prof-avatar-preview">李</div>
|
||||
<div class="av-actions">
|
||||
<button class="btn btn-sm" onclick="Shell.toast('上传头像', '占位 · 选择本地图片')">上传新头像</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('恢复默认头像', '已重置')">恢复默认</button>
|
||||
<button class="btn btn-sm" type="button" onclick="Shell.openModal('avatar-up-bg')">上传新头像</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" id="prof-avatar-reset">恢复默认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,6 +446,51 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ─── 上传头像 modal ─── -->
|
||||
<div class="modal-bg" id="avatar-up-bg" onclick="if(event.target===this)Shell.closeModal('avatar-up-bg')">
|
||||
<div class="modal av-up-modal">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<div class="modal-h">
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
|
||||
</div>
|
||||
<div class="ti">上传头像<span>// 用于个人主页、评论与团队展示</span></div>
|
||||
</div>
|
||||
<div class="modal-b np-body">
|
||||
|
||||
<div class="av-up-preview-row">
|
||||
<div class="av-up-preview" id="av-up-preview">李</div>
|
||||
<div class="av-up-preview-meta">
|
||||
<div class="t" id="av-up-preview-name">当前头像 · 默认</div>
|
||||
<div class="d" id="av-up-preview-info">// 系统生成 · 取姓氏首字</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-zone" id="av-up-zone" tabindex="0" role="button" aria-label="点击或拖入图片上传">
|
||||
<span class="uz-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||
</span>
|
||||
<div><strong>点击选择</strong> · 或拖入图片</div>
|
||||
<span class="uz-hint">JPG / PNG / WebP · ≤ 2 MB · 推荐 256 × 256</span>
|
||||
<input type="file" id="av-up-file" accept="image/jpeg,image/png,image/webp" hidden>
|
||||
</div>
|
||||
|
||||
<div class="av-up-rules">
|
||||
<div class="li">最大 2 MB · 长宽比建议 1:1 · 系统会自动裁切为圆形</div>
|
||||
<div class="li">不要上传含他人肖像的图片,违规可能导致账号封停</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-f">
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('avatar-up-bg')">取消</button>
|
||||
<button class="btn btn-primary" type="button" id="av-up-confirm">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
确认使用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/shell.js?v=202605211643"></script>
|
||||
<script>
|
||||
@@ -424,50 +499,60 @@ Shell.render({
|
||||
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]
|
||||
});
|
||||
|
||||
/* ─── 侧边 nav 高亮 + 滚动联动 ─── */
|
||||
const sections = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
|
||||
/* ─── 配置 ─── */
|
||||
const SECTIONS = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
|
||||
const navLinks = document.querySelectorAll('.settings-nav a[data-jump]');
|
||||
|
||||
/* ─── 1. 点击 nav → 只显示对应 section,其余隐藏 + 同步 URL hash ─── */
|
||||
function showSection(id) {
|
||||
if (!SECTIONS.includes(id)) id = SECTIONS[0];
|
||||
SECTIONS.forEach(sid => {
|
||||
const el = document.getElementById(sid);
|
||||
if (el) el.style.display = (sid === id) ? '' : 'none';
|
||||
});
|
||||
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
|
||||
if (location.hash.slice(1) !== id) {
|
||||
history.replaceState(null, '', '#' + id);
|
||||
}
|
||||
// 切换面板后回到顶端,避免长面板留下的滚动位置错乱
|
||||
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
}
|
||||
|
||||
navLinks.forEach(a => {
|
||||
a.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const id = a.dataset.jump;
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
|
||||
const offset = 16;
|
||||
if (contentEl) {
|
||||
contentEl.scrollTo({ top: el.offsetTop - contentEl.offsetTop - offset, behavior: 'smooth' });
|
||||
} else {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
showSection(a.dataset.jump);
|
||||
a.focus();
|
||||
});
|
||||
a.addEventListener('keydown', e => {
|
||||
const idx = [...navLinks].indexOf(a);
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); navLinks[(idx + 1) % navLinks.length].focus(); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); navLinks[(idx - 1 + navLinks.length) % navLinks.length].focus(); }
|
||||
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); a.click(); }
|
||||
});
|
||||
});
|
||||
|
||||
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(en => {
|
||||
if (en.isIntersecting) {
|
||||
const id = en.target.id;
|
||||
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
|
||||
}
|
||||
});
|
||||
}, { root: contentEl || null, rootMargin: '-20% 0px -60% 0px', threshold: 0 });
|
||||
sections.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) observer.observe(el);
|
||||
/* ─── 2. 初始加载:读 URL hash 显示对应 section(无 hash 时默认 sec-profile) ─── */
|
||||
(function initSection() {
|
||||
const hash = location.hash.slice(1);
|
||||
showSection(SECTIONS.includes(hash) ? hash : 'sec-profile');
|
||||
})();
|
||||
|
||||
/* hash 外部变化(如浏览器前进后退)也跟着切 */
|
||||
window.addEventListener('hashchange', () => {
|
||||
const hash = location.hash.slice(1);
|
||||
if (SECTIONS.includes(hash)) showSection(hash);
|
||||
});
|
||||
|
||||
/* ─── 偏好 chip 选择 ─── */
|
||||
function bindChoice(containerId, label) {
|
||||
/* ─── 4. 偏好 chip 选择 ─── */
|
||||
function bindChoice(containerId) {
|
||||
const ct = document.getElementById(containerId);
|
||||
if (!ct) return;
|
||||
ct.querySelectorAll('.pref-choice').forEach(c => {
|
||||
c.addEventListener('click', () => {
|
||||
ct.querySelectorAll('.pref-choice').forEach(x => x.classList.remove('selected'));
|
||||
c.classList.add('selected');
|
||||
markDirty();
|
||||
markDirty(c);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -478,46 +563,232 @@ document.querySelectorAll('#pref-duration .dur-chip').forEach(c => {
|
||||
c.addEventListener('click', () => {
|
||||
document.querySelectorAll('#pref-duration .dur-chip').forEach(x => x.classList.remove('selected'));
|
||||
c.classList.add('selected');
|
||||
markDirty();
|
||||
markDirty(c);
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── dirty state ─── */
|
||||
let dirty = false;
|
||||
function markDirty() {
|
||||
if (dirty) return;
|
||||
dirty = true;
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const cancelBtn = document.getElementById('save-cancel');
|
||||
[saveBtn, cancelBtn].forEach(b => {
|
||||
b.disabled = false;
|
||||
b.style.opacity = '';
|
||||
b.style.cursor = '';
|
||||
});
|
||||
/* ─── 5. dirty state · 追踪改了哪几节 + save btn 显示变更条数 ─── */
|
||||
const dirtyFields = new Set(); // 改过的字段 id 集合
|
||||
const dirtySections = new Set(); // 涉及的 section id 集合
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const cancelBtn = document.getElementById('save-cancel');
|
||||
const saveBtnDefaultLabel = saveBtn.innerHTML;
|
||||
|
||||
function sectionOf(el) {
|
||||
const sec = el.closest('section[id]');
|
||||
return sec ? sec.id : null;
|
||||
}
|
||||
|
||||
function syncSaveBtn() {
|
||||
const n = dirtyFields.size;
|
||||
if (n === 0) {
|
||||
saveBtn.disabled = true;
|
||||
cancelBtn.disabled = true;
|
||||
saveBtn.style.opacity = '.5';
|
||||
saveBtn.style.cursor = 'not-allowed';
|
||||
cancelBtn.style.opacity = '.5';
|
||||
cancelBtn.style.cursor = 'not-allowed';
|
||||
saveBtn.innerHTML = saveBtnDefaultLabel;
|
||||
} else {
|
||||
saveBtn.disabled = false;
|
||||
cancelBtn.disabled = false;
|
||||
saveBtn.style.opacity = '';
|
||||
saveBtn.style.cursor = '';
|
||||
cancelBtn.style.opacity = '';
|
||||
cancelBtn.style.cursor = '';
|
||||
saveBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg> 保存所有变更 <span style="opacity:.75;font-family:var(--font-mono);font-size:11px;margin-left:4px;">· ${n} 项</span>`;
|
||||
}
|
||||
// nav 上加 dirty dot
|
||||
navLinks.forEach(a => a.classList.toggle('has-changes', dirtySections.has(a.dataset.jump)));
|
||||
}
|
||||
|
||||
function markDirty(el) {
|
||||
const id = el.id || (el.dataset && el.dataset.v) || ('anon-' + Math.random().toString(36).slice(2, 8));
|
||||
if (dirtyFields.has(id)) return;
|
||||
dirtyFields.add(id);
|
||||
const sec = sectionOf(el);
|
||||
if (sec) dirtySections.add(sec);
|
||||
syncSaveBtn();
|
||||
}
|
||||
function clearDirty() {
|
||||
dirty = false;
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const cancelBtn = document.getElementById('save-cancel');
|
||||
[saveBtn, cancelBtn].forEach(b => {
|
||||
b.disabled = true;
|
||||
b.style.opacity = '.5';
|
||||
b.style.cursor = 'not-allowed';
|
||||
});
|
||||
dirtyFields.clear();
|
||||
dirtySections.clear();
|
||||
syncSaveBtn();
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-track], input[type="checkbox"], select').forEach(el => {
|
||||
el.addEventListener('change', markDirty);
|
||||
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', markDirty);
|
||||
el.addEventListener('change', () => markDirty(el));
|
||||
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', () => markDirty(el));
|
||||
});
|
||||
|
||||
document.getElementById('save-btn').addEventListener('click', () => {
|
||||
if (!dirty) return;
|
||||
Shell.toast('设置已保存', '所有变更已生效');
|
||||
/* ─── 6. 表单校验:必填 + 邮箱格式 ─── */
|
||||
const VALIDATORS = {
|
||||
'prof-name': v => v.trim().length === 0 ? '显示名称不能为空' : (v.length > 20 ? '显示名称最多 20 字' : null),
|
||||
'prof-email': v => !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? '邮箱格式不正确' : null,
|
||||
'prof-phone': v => !/^\d{3}\*{4}\d{4}$|^1\d{10}$/.test(v.trim()) ? '手机号格式不正确' : null,
|
||||
};
|
||||
function validate() {
|
||||
const errors = [];
|
||||
Object.entries(VALIDATORS).forEach(([id, fn]) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const err = fn(el.value);
|
||||
el.classList.toggle('invalid', !!err);
|
||||
if (err) errors.push(`${id}: ${err}`);
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
/* ─── 7. 保存 / 取消 ─── */
|
||||
saveBtn.addEventListener('click', () => {
|
||||
if (dirtyFields.size === 0) return;
|
||||
const errors = validate();
|
||||
if (errors.length) {
|
||||
Shell.toast('校验未通过', errors[0].split(': ')[1] + ' · 共 ' + errors.length + ' 项');
|
||||
return;
|
||||
}
|
||||
Shell.toast('设置已保存', `${dirtyFields.size} 项变更已生效 · 涉及 ${dirtySections.size} 个分区`);
|
||||
clearDirty();
|
||||
});
|
||||
document.getElementById('save-cancel').addEventListener('click', () => {
|
||||
if (!dirty) return;
|
||||
if (confirm('放弃未保存的变更?')) location.reload();
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
if (dirtyFields.size === 0) return;
|
||||
if (confirm(`放弃 ${dirtyFields.size} 项未保存的变更?`)) location.reload();
|
||||
});
|
||||
|
||||
/* ─── 8. 离开页面前提醒(有未保存变更时)─── */
|
||||
window.addEventListener('beforeunload', e => {
|
||||
if (dirtyFields.size > 0) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
/* ─── 9. invalid input 视觉反馈样式 ─── */
|
||||
const _invalidStyle = document.createElement('style');
|
||||
_invalidStyle.textContent = `
|
||||
.input.invalid { border-color: var(--accent-crimson); box-shadow: 0 0 0 3px rgba(235,52,36,.12); }
|
||||
.input.invalid:focus { border-color: var(--accent-crimson); box-shadow: 0 0 0 3px rgba(235,52,36,.18); }
|
||||
`;
|
||||
document.head.appendChild(_invalidStyle);
|
||||
|
||||
/* ─── 10. nav badge 计数自动同步(从 section 内 switch 开启数 / device 数等) ─── */
|
||||
function syncBadges() {
|
||||
const secNotify = document.getElementById('sec-notify');
|
||||
if (secNotify) {
|
||||
const onCount = secNotify.querySelectorAll('input[type="checkbox"]:checked').length;
|
||||
const totalCount = secNotify.querySelectorAll('input[type="checkbox"]').length;
|
||||
const badge = document.querySelector('.settings-nav a[data-jump="sec-notify"] .nav-badge');
|
||||
if (badge) badge.textContent = `${onCount}/${totalCount}`;
|
||||
}
|
||||
const secSecurity = document.getElementById('sec-security');
|
||||
if (secSecurity) {
|
||||
const devCount = secSecurity.querySelectorAll('.device-row').length;
|
||||
const badge = document.querySelector('.settings-nav a[data-jump="sec-security"] .nav-badge');
|
||||
if (badge) badge.textContent = `${devCount} 设备`;
|
||||
}
|
||||
}
|
||||
syncBadges();
|
||||
document.querySelectorAll('#sec-notify input[type="checkbox"]').forEach(cb => cb.addEventListener('change', syncBadges));
|
||||
|
||||
/* ─── 11. 头像上传 modal ─── */
|
||||
const DEFAULT_AVATAR = { kind: 'default', label: '李', name: '默认', info: '// 系统生成 · 取姓氏首字' };
|
||||
let _draftAvatar = { ...DEFAULT_AVATAR };
|
||||
let _currentAvatar = { ...DEFAULT_AVATAR };
|
||||
|
||||
const avPreview = document.getElementById('av-up-preview');
|
||||
const avPreviewName = document.getElementById('av-up-preview-name');
|
||||
const avPreviewInfo = document.getElementById('av-up-preview-info');
|
||||
const avProfPreview = document.getElementById('prof-avatar-preview');
|
||||
const avZone = document.getElementById('av-up-zone');
|
||||
const avFile = document.getElementById('av-up-file');
|
||||
const avConfirm = document.getElementById('av-up-confirm');
|
||||
const avResetBtn = document.getElementById('prof-avatar-reset');
|
||||
|
||||
function paintPreview() {
|
||||
if (_draftAvatar.kind === 'image') {
|
||||
avPreview.innerHTML = `<img src="${_draftAvatar.src}" alt="头像预览">`;
|
||||
} else {
|
||||
avPreview.innerHTML = '';
|
||||
avPreview.textContent = _draftAvatar.label || '李';
|
||||
}
|
||||
avPreviewName.textContent = `预览 · ${_draftAvatar.name}`;
|
||||
avPreviewInfo.textContent = _draftAvatar.info;
|
||||
}
|
||||
|
||||
function applyAvatarTo(el, avatar) {
|
||||
if (avatar.kind === 'image') {
|
||||
el.innerHTML = `<img src="${avatar.src}" alt="头像" style="width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;">`;
|
||||
} else {
|
||||
el.innerHTML = '';
|
||||
el.textContent = avatar.label || '李';
|
||||
}
|
||||
}
|
||||
|
||||
avZone.addEventListener('click', () => avFile.click());
|
||||
avZone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); avFile.click(); } });
|
||||
avZone.addEventListener('dragover', e => { e.preventDefault(); avZone.classList.add('dragover'); });
|
||||
avZone.addEventListener('dragleave', () => avZone.classList.remove('dragover'));
|
||||
avZone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
avZone.classList.remove('dragover');
|
||||
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
||||
if (f) handleAvatarFile(f);
|
||||
});
|
||||
avFile.addEventListener('change', () => { if (avFile.files[0]) handleAvatarFile(avFile.files[0]); });
|
||||
|
||||
function handleAvatarFile(f) {
|
||||
if (!/^image\/(jpeg|png|webp)$/.test(f.type)) {
|
||||
Shell.toast('格式不支持', '仅支持 JPG / PNG / WebP');
|
||||
return;
|
||||
}
|
||||
if (f.size > 2 * 1024 * 1024) {
|
||||
Shell.toast('文件过大', `${(f.size / 1024 / 1024).toFixed(2)} MB · 限 2 MB`);
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(f);
|
||||
_draftAvatar = { kind: 'image', src: url, name: f.name, info: `// ${(f.size / 1024).toFixed(0)} KB · ${f.type.split('/')[1].toUpperCase()}` };
|
||||
paintPreview();
|
||||
}
|
||||
|
||||
avConfirm.addEventListener('click', () => {
|
||||
_currentAvatar = { ..._draftAvatar };
|
||||
applyAvatarTo(avProfPreview, _currentAvatar);
|
||||
Shell.closeModal('avatar-up-bg');
|
||||
Shell.toast('头像已更新', _currentAvatar.name);
|
||||
markDirty(avProfPreview);
|
||||
});
|
||||
|
||||
avResetBtn.addEventListener('click', () => {
|
||||
_currentAvatar = { ...DEFAULT_AVATAR };
|
||||
applyAvatarTo(avProfPreview, _currentAvatar);
|
||||
_draftAvatar = { ...DEFAULT_AVATAR };
|
||||
paintPreview();
|
||||
Shell.toast('已恢复默认头像', _currentAvatar.name);
|
||||
markDirty(avProfPreview);
|
||||
});
|
||||
|
||||
// 打开 modal 时同步当前到 draft + preview
|
||||
document.querySelector('.av-actions button.btn-sm').addEventListener('click', () => {
|
||||
_draftAvatar = { ..._currentAvatar };
|
||||
paintPreview();
|
||||
});
|
||||
|
||||
paintPreview();
|
||||
|
||||
/* ─── 12. 实时邮箱 / 名称 input 校验反馈(blur 时显示错) ─── */
|
||||
['prof-name', 'prof-email', 'prof-phone'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !VALIDATORS[id]) return;
|
||||
el.addEventListener('blur', () => {
|
||||
const err = VALIDATORS[id](el.value);
|
||||
el.classList.toggle('invalid', !!err);
|
||||
});
|
||||
el.addEventListener('input', () => {
|
||||
if (el.classList.contains('invalid')) {
|
||||
const err = VALIDATORS[id](el.value);
|
||||
if (!err) el.classList.remove('invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+515
-33
@@ -110,6 +110,125 @@
|
||||
.members-table tr.pending td { opacity: .65; }
|
||||
.members-table tr.pending .nm::after { content: '· 待激活'; font-size: 11px; color: var(--black-alpha-48); margin-left: 6px; font-weight: 400; font-family: var(--font-mono); }
|
||||
|
||||
/* ─── 成员详情 modal ─── */
|
||||
.member-detail-modal { width: min(760px, 92vw); max-width: min(760px, 92vw); }
|
||||
/* 给 modal-h 留出右侧 X 的位置 */
|
||||
.member-detail-modal .modal-h { padding-right: 56px; }
|
||||
/* 模态右上角关闭 X */
|
||||
.md-x { position: absolute; top: 14px; right: 16px; width: 28px; height: 28px; border-radius: var(--r-sm); border: 1px solid var(--border-faint); background: var(--surface); color: var(--black-alpha-56); display: grid; place-items: center; cursor: pointer; transition: all var(--t-base); z-index: 2; }
|
||||
.md-x:hover { color: var(--accent-black); border-color: var(--black-alpha-32); }
|
||||
.md-x svg { width: 13px; height: 13px; }
|
||||
|
||||
.md-header { display: flex; align-items: center; gap: 14px; padding: 4px 0 16px; border-bottom: 1px solid var(--border-faint); margin-bottom: 18px; }
|
||||
.md-header .av-big { width: 52px; height: 52px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 20px; font-weight: 600; color: var(--accent-black); flex-shrink: 0; }
|
||||
.md-header .who-main { min-width: 0; flex: 1; }
|
||||
.md-header .nm-big { font-size: 16px; font-weight: 600; color: var(--accent-black); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.md-header .em-big { font-size: 12px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 4px; letter-spacing: .02em; }
|
||||
.md-header .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; }
|
||||
.md-header .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||
.md-header .role-pill.role-super { background: var(--heat-12); color: var(--heat); }
|
||||
.md-header .role-pill.role-super .dot { background: var(--heat); }
|
||||
.md-header .role-pill.role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
|
||||
.md-header .role-pill.role-admin .dot { background: #1E40AF; }
|
||||
.md-header .role-pill.role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
|
||||
.md-header .role-pill.role-member .dot { background: var(--black-alpha-56); }
|
||||
.md-header .tag-pending { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: rgba(180,83,9,.12); color: #B45309; border-radius: var(--r-sm); letter-spacing: .04em; }
|
||||
/* 创建者:mono 小 tag,不再用 "·" 字符串拼接 */
|
||||
.md-header .tag-creator { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: var(--r-sm); letter-spacing: .04em; font-weight: 500; }
|
||||
|
||||
.md-section { margin-bottom: 18px; }
|
||||
.md-section:last-child { margin-bottom: 0; }
|
||||
.md-section-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; }
|
||||
.md-section-h .hint { margin-left: 6px; font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-32); font-weight: 400; letter-spacing: .02em; text-transform: none; }
|
||||
|
||||
/* 角色权限速览 */
|
||||
.md-perm-list { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; }
|
||||
.md-perm-list .pi { display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; color: var(--accent-black); line-height: 1.4; }
|
||||
.md-perm-list .pi .ck { flex: 0 0 12px; color: var(--accent-forest); font-family: var(--font-mono); font-size: 11px; font-weight: 600; }
|
||||
.md-perm-list .pi.deny .ck { color: var(--black-alpha-32); }
|
||||
.md-perm-list .pi.deny { color: var(--black-alpha-48); }
|
||||
.md-perm-list .pi .lb { flex: 1; min-width: 0; }
|
||||
.md-perm-list .pi .lb em { font-style: normal; color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 11px; margin-left: 4px; }
|
||||
|
||||
/* 额度日消耗子行 */
|
||||
.md-daily-row { display: flex; gap: 16px; align-items: center; margin-top: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-56); letter-spacing: .02em; }
|
||||
.md-daily-row .k { color: var(--black-alpha-48); }
|
||||
.md-daily-row .v { color: var(--accent-black); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.md-daily-row .v.warn { color: #B45309; }
|
||||
.md-daily-row .sep { color: var(--black-alpha-32); }
|
||||
|
||||
.md-quota-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
|
||||
.md-quota-box { background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; }
|
||||
.md-quota-box .lbl { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.md-quota-box .v { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--accent-black); margin-top: 4px; }
|
||||
.md-quota-box .v.warn { color: #B45309; }
|
||||
.md-progress { margin-top: 10px; }
|
||||
.md-progress .label { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--black-alpha-56); font-family: var(--font-mono); margin-bottom: 4px; }
|
||||
.md-progress .bar { height: 6px; background: var(--background-lighter); border-radius: 3px; overflow: hidden; border: 1px solid var(--border-faint); }
|
||||
.md-progress .bar > span { display: block; height: 100%; background: var(--heat); }
|
||||
.md-progress .bar > span.ok { background: var(--accent-forest); }
|
||||
.md-progress .bar > span.warn { background: #B45309; }
|
||||
|
||||
.md-stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.md-stat { background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px 12px; text-align: center; }
|
||||
.md-stat .v { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--accent-black); }
|
||||
.md-stat .lbl { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
|
||||
|
||||
.md-activity-list { display: flex; flex-direction: column; gap: 8px; max-height: 180px; overflow-y: auto; padding-right: 4px; }
|
||||
.md-activity-list::-webkit-scrollbar { width: 4px; }
|
||||
.md-activity-list::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
|
||||
.md-activity-item { display: grid; grid-template-columns: 70px 1fr; gap: 10px; align-items: baseline; font-size: 12.5px; line-height: 1.4; }
|
||||
.md-activity-item .ts { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.md-activity-item .act { color: var(--black-alpha-56); }
|
||||
.md-activity-item .obj { color: var(--heat); }
|
||||
|
||||
/* ─── 消费明细 section ─── */
|
||||
.md-section-h .right { margin-left: auto; font-family: var(--font-mono); font-size: 11.5px; color: var(--accent-black); font-weight: 600; letter-spacing: 0; text-transform: none; font-variant-numeric: tabular-nums; }
|
||||
.md-section-h { display: flex; align-items: baseline; }
|
||||
|
||||
.md-stage-list { display: flex; flex-direction: column; gap: 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px 16px; }
|
||||
.md-stage-row { display: grid; grid-template-columns: 12px 1fr; column-gap: 10px; row-gap: 5px; align-items: center; }
|
||||
.md-stage-row .swatch { width: 8px; height: 8px; border-radius: 2px; align-self: center; }
|
||||
.md-stage-row .swatch.s-video { background: var(--heat); }
|
||||
.md-stage-row .swatch.s-storyboard { background: var(--accent-forest); }
|
||||
.md-stage-row .swatch.s-asset { background: var(--black-alpha-56); }
|
||||
.md-stage-row .swatch.s-script { background: var(--black-alpha-32); }
|
||||
.md-stage-row .line { display: flex; align-items: baseline; font-size: 12.5px; min-width: 0; }
|
||||
.md-stage-row .nm { color: var(--accent-black); }
|
||||
.md-stage-row .pct { margin-left: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); font-variant-numeric: tabular-nums; }
|
||||
.md-stage-row .amt { margin-left: auto; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-black); font-size: 12.5px; }
|
||||
.md-stage-row .bar { grid-column: 2; height: 4px; background: var(--surface); border-radius: 2px; overflow: hidden; }
|
||||
.md-stage-row .bar > span { display: block; height: 100%; transition: width .3s ease; }
|
||||
.md-stage-row .bar > span.s-video { background: var(--heat); }
|
||||
.md-stage-row .bar > span.s-storyboard { background: var(--accent-forest); }
|
||||
.md-stage-row .bar > span.s-asset { background: var(--black-alpha-56); }
|
||||
.md-stage-row .bar > span.s-script { background: var(--black-alpha-32); }
|
||||
|
||||
/* 流水明细切换:按钮化 */
|
||||
.md-tx-bar { display: flex; align-items: center; margin-top: 14px; padding: 8px 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
|
||||
.md-tx-bar .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-56); letter-spacing: .02em; }
|
||||
.md-tx-bar .ct b { color: var(--accent-black); font-weight: 600; }
|
||||
.md-tx-toggle { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 11px; color: var(--heat); cursor: pointer; margin-left: auto; user-select: none; padding: 4px 10px; border-radius: var(--r-sm); transition: background var(--t-base); }
|
||||
.md-tx-toggle:hover { background: var(--heat-12); }
|
||||
.md-tx-toggle svg { width: 11px; height: 11px; transition: transform var(--t-base); }
|
||||
.md-tx-toggle.expanded svg { transform: rotate(180deg); }
|
||||
|
||||
.md-tx-table { display: none; flex-direction: column; max-height: 220px; overflow-y: auto; margin-top: 10px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); }
|
||||
.md-tx-table.show { display: flex; }
|
||||
.md-tx-table::-webkit-scrollbar { width: 4px; }
|
||||
.md-tx-table::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
|
||||
.md-tx-row { display: grid; grid-template-columns: 96px 1fr 110px 78px; gap: 10px; padding: 9px 12px; font-size: 12px; border-bottom: 1px solid var(--border-faint); align-items: center; }
|
||||
.md-tx-row:last-child { border-bottom: 0; }
|
||||
.md-tx-row.head { background: var(--black-alpha-3); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; padding-top: 8px; padding-bottom: 8px; }
|
||||
.md-tx-row .ts { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
|
||||
.md-tx-row .proj { color: var(--accent-black); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.md-tx-row .type { color: var(--black-alpha-56); font-size: 11.5px; }
|
||||
.md-tx-row .amt { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-black); }
|
||||
.md-tx-row .amt.pos { color: var(--accent-forest); }
|
||||
.md-tx-row .amt.zero { color: var(--black-alpha-32); font-weight: 500; }
|
||||
|
||||
.md-meta { font-size: 12px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; padding-top: 14px; margin-top: 14px; border-top: 1px solid var(--border-faint); }
|
||||
|
||||
/* ─── 角色权限矩阵 ─── */
|
||||
.perm-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.perm-table th, .perm-table td { padding: 8px 4px; border-bottom: 1px solid var(--border-faint); }
|
||||
@@ -320,26 +439,6 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pane">
|
||||
<h3>额度预检规则</h3>
|
||||
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// 任一不通过即拦截</div>
|
||||
<div class="quota-rules">
|
||||
<div class="step"><span class="num">1</span><span><span class="v">个人日剩余</span> ≥ 任务预估 × <span class="formula">1.2</span></span></div>
|
||||
<div class="step"><span class="num">2</span><span><span class="v">个人月剩余</span> ≥ 同上</span></div>
|
||||
<div class="step"><span class="num">3</span><span><span class="v">团队月剩余</span> ≥ 同上</span></div>
|
||||
<div class="step"><span class="num">4</span><span><span class="v">团队总余额</span> ≥ 同上</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane" style="background: var(--heat-12); border-color: var(--heat-20);">
|
||||
<h3 style="color: var(--heat);">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||
失败不扣费
|
||||
</h3>
|
||||
<div style="font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7;">
|
||||
所有生成任务<strong style="color: var(--accent-black);">仅在用户 <span class="mono" style="background: var(--surface); padding: 1px 5px; font-family: var(--font-mono); color: var(--heat); font-size: 11.5px;">[ 通过 ]</span> 时才扣费</strong>。失败 / 超时 / 重跑(旧版本作废)一律不扣。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -351,12 +450,12 @@
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
|
||||
</div>
|
||||
<div class="ti">邀请成员<span>// 短信邀请 · 接受后自动加入团队</span></div>
|
||||
<div class="ti">邀请成员<span>// 邮件邀请 · 接受后自动加入团队</span></div>
|
||||
</div>
|
||||
<div class="modal-b">
|
||||
<div class="field">
|
||||
<label class="field-label">手机号 <span class="req">*</span></label>
|
||||
<input class="input" id="inv-phone" placeholder="例: 138 0013 8000">
|
||||
<label class="field-label">邮箱 <span class="req">*</span></label>
|
||||
<input class="input" id="inv-email" type="email" autocomplete="off" placeholder="例: name@company.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">备注姓名(可选)</label>
|
||||
@@ -433,6 +532,107 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成员详情 modal · 点击成员行打开 -->
|
||||
<div class="modal-bg" id="member-detail-bg" onclick="if(event.target===this)Shell.closeModal('member-detail-bg')">
|
||||
<div class="modal member-detail-modal">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<button class="md-x" type="button" aria-label="关闭" onclick="Shell.closeModal('member-detail-bg')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<div class="modal-h">
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
|
||||
</div>
|
||||
<div class="ti">成员详情<span id="md-sub">// 角色权限 / 额度 / 消费明细 / 团队贡献 / 最近活动</span></div>
|
||||
</div>
|
||||
<div class="modal-b">
|
||||
<div class="md-header">
|
||||
<div class="av-big" id="md-av">—</div>
|
||||
<div class="who-main">
|
||||
<div class="nm-big">
|
||||
<span id="md-name">—</span>
|
||||
<span class="role-pill" id="md-role"><span class="dot"></span><span id="md-role-label">—</span></span>
|
||||
<span class="tag-creator" id="md-creator" style="display:none">创建者</span>
|
||||
<span class="tag-pending" id="md-pending" style="display:none">待激活</span>
|
||||
</div>
|
||||
<div class="em-big" id="md-email">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-section">
|
||||
<div class="md-section-h">// 角色权限 · <span id="md-perm-role">—</span> 可以<span class="hint">PRD §10.2 权限矩阵</span></div>
|
||||
<div class="md-perm-list" id="md-perm-list">
|
||||
<!-- JS 注入 4 条 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-section">
|
||||
<div class="md-section-h">// 额度使用<span class="hint">PRD §7.2 四层额度</span></div>
|
||||
<div class="md-quota-grid">
|
||||
<div class="md-quota-box"><div class="lbl">每日额度</div><div class="v" id="md-daily">—</div></div>
|
||||
<div class="md-quota-box"><div class="lbl">月度额度</div><div class="v" id="md-monthly">—</div></div>
|
||||
<div class="md-quota-box"><div class="lbl">当月已用</div><div class="v" id="md-used">—</div></div>
|
||||
</div>
|
||||
<div class="md-daily-row">
|
||||
<span><span class="k">今日已用</span> <span class="v" id="md-used-today">¥0.00</span></span>
|
||||
<span class="sep">·</span>
|
||||
<span><span class="k">今日剩余</span> <span class="v" id="md-left-today">¥0.00</span></span>
|
||||
</div>
|
||||
<div class="md-progress">
|
||||
<div class="label"><span>当月消耗</span><span id="md-pct">0%</span></div>
|
||||
<div class="bar"><span id="md-bar" style="width: 0%"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-section" id="md-cost-section">
|
||||
<div class="md-section-h">// 消费明细 · 当月按阶段拆分<span class="hint">仅超管 / 团管可见</span><span class="right" id="md-cost-total">¥0.00</span></div>
|
||||
<div class="md-stage-list" id="md-stage-list">
|
||||
<!-- JS 注入 -->
|
||||
</div>
|
||||
<div class="md-tx-bar">
|
||||
<span class="ct">// 流水明细 · 共 <b id="md-tx-count">0</b> 条</span>
|
||||
<span class="md-tx-toggle" id="md-tx-toggle" role="button" tabindex="0">
|
||||
<span class="md-tx-label">展开</span>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-tx-table" id="md-tx-table">
|
||||
<div class="md-tx-row head">
|
||||
<span>时间</span>
|
||||
<span>项目</span>
|
||||
<span>类型</span>
|
||||
<span style="text-align:right">金额</span>
|
||||
</div>
|
||||
<!-- JS 注入 rows -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-section">
|
||||
<div class="md-section-h">// 团队贡献</div>
|
||||
<div class="md-stats-row">
|
||||
<div class="md-stat"><div class="v" id="md-projects-active">0</div><div class="lbl">进行中项目</div></div>
|
||||
<div class="md-stat"><div class="v" id="md-projects-done">0</div><div class="lbl">已完成项目</div></div>
|
||||
<div class="md-stat"><div class="v" id="md-assets">0</div><div class="lbl">上传共享素材</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-section">
|
||||
<div class="md-section-h">// 最近活动<span class="hint" id="md-last-active">· 最近活跃 —</span></div>
|
||||
<div class="md-activity-list" id="md-activity">
|
||||
<!-- JS 注入 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-meta" id="md-meta">// —</div>
|
||||
</div>
|
||||
<div class="modal-f">
|
||||
<button class="btn" type="button" style="margin-right: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" id="md-remove" data-creator-hide>移出团队</button>
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('member-detail-bg')">关闭</button>
|
||||
<button class="btn btn-primary" type="button" id="md-edit" data-creator-hide>编辑成员</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/shell.js?v=202605211643"></script>
|
||||
<script>
|
||||
@@ -448,12 +648,94 @@ const ROLE_META = {
|
||||
member: { cls: 'role-member', label: '成员' },
|
||||
};
|
||||
|
||||
/* PRD §10.2 权限矩阵 · 4 条精简到弹窗速览(grant + deny 各 2 条) */
|
||||
const ROLE_PERMS = {
|
||||
super: [
|
||||
{ ok: true, txt: '团队设置 · 月限额 / 名称' },
|
||||
{ ok: true, txt: '充值 · 划拨团队总额度' },
|
||||
{ ok: true, txt: '任命团管 · 调整任意成员额度' },
|
||||
{ ok: true, txt: '查看团队全部消费明细 + 财务' },
|
||||
],
|
||||
admin: [
|
||||
{ ok: true, txt: '邀请 / 移除成员 + 设置额度' },
|
||||
{ ok: true, txt: '管理团队共享资产库' },
|
||||
{ ok: true, txt: '查看团队全部消费明细' },
|
||||
{ ok: false, txt: '团队设置 / 充值 · 仅超管' },
|
||||
],
|
||||
member: [
|
||||
{ ok: true, txt: '创建项目 + 流水线操作' },
|
||||
{ ok: true, txt: '上传 / 引用团队共享资产' },
|
||||
{ ok: false, txt: '邀请 / 移除成员 · 仅超管 / 团管' },
|
||||
{ ok: false, txt: '消费明细 · 仅本人可见' },
|
||||
],
|
||||
};
|
||||
|
||||
const MEMBERS = [
|
||||
{ id: 'u1', av: '李', name: '小李', email: 'li@shop.com', role: 'super', daily: 500, monthly: 10000, used: 162.60, pending: false, creator: true },
|
||||
{ id: 'u2', av: '张', name: '张运营', email: 'zhang@shop.com', role: 'admin', daily: 300, monthly: 6000, used: 98.40, pending: false },
|
||||
{ id: 'u3', av: '王', name: '王小姐', email: 'wang@shop.com', role: 'member', daily: 100, monthly: 2000, used: 45.20, pending: false },
|
||||
{ id: 'u4', av: '陈', name: '陈策划', email: 'chen@shop.com', role: 'member', daily: 100, monthly: 2000, used: 12.80, pending: false },
|
||||
{ id: 'u5', av: '林', name: '林新人', email: '186****1102', role: 'member', daily: 100, monthly: 2000, used: 0, pending: true },
|
||||
{ id: 'u1', av: '李', name: '小李', email: 'li@shop.com', role: 'super', daily: 500, monthly: 10000, used: 162.60, usedToday: 0.45, lastActive: '15 分钟前', pending: false, creator: true,
|
||||
joinDate: '2026-04-12', inviter: '—', projectsActive: 2, projectsDone: 14, assetsUploaded: 32,
|
||||
activity: [
|
||||
{ ts: '昨天 11:02', act: '团队充值', obj: '+¥500.00' },
|
||||
{ ts: '2 天前', act: '邀请新成员', obj: '林新人' },
|
||||
{ ts: '3 天前', act: '创建项目', obj: '春日新品 · 立体口红' },
|
||||
],
|
||||
byStage: { video: 98.40, storyboard: 36.00, asset: 21.00, script: 7.20 },
|
||||
transactions: [
|
||||
{ ts: '05.21 14:32', proj: '补水面膜 · v3', type: '视频片段 · 1 镜', amount: -0.45 },
|
||||
{ ts: '05.20 18:21', proj: '透真防晒 · 通勤对比', type: '视频片段 · 6 镜', amount: -1.20 },
|
||||
{ ts: '05.20 11:02', proj: '充值', type: '微信支付', amount: 500.00 },
|
||||
{ ts: '05.19 16:08', proj: '补水面膜 · v3', type: '故事板 image-2', amount: -0.45 },
|
||||
{ ts: '05.19 14:02', proj: '补水面膜 · v3', type: '脚本 LLM · 2.4k', amount: -0.04 },
|
||||
{ ts: '05.19 13:38', proj: '补水面膜 · v3', type: '基础资产 · 5 张', amount: -1.05 },
|
||||
{ ts: '05.18 15:42', proj: '咖啡冻干 · 早八', type: '故事板生成失败', amount: 0 },
|
||||
{ ts: '05.17 10:30', proj: '瑜伽裤 · 通勤穿搭', type: '项目导出', amount: -3.20 },
|
||||
] },
|
||||
{ id: 'u2', av: '张', name: '张运营', email: 'zhang@shop.com', role: 'admin', daily: 300, monthly: 6000, used: 98.40, usedToday: 0.45, lastActive: '10 分钟前', pending: false,
|
||||
joinDate: '2026-04-18', inviter: '小李', projectsActive: 3, projectsDone: 8, assetsUploaded: 18,
|
||||
activity: [
|
||||
{ ts: '10 分钟前', act: '完成视频', obj: '补水面膜 · v3' },
|
||||
{ ts: '昨天 18:32', act: '采用故事板', obj: '场 3 · v2' },
|
||||
{ ts: '2 天前', act: '创建项目', obj: '蓝牙耳机 · 开箱测评' },
|
||||
],
|
||||
byStage: { video: 60.00, storyboard: 22.00, asset: 12.00, script: 4.40 },
|
||||
transactions: [
|
||||
{ ts: '05.21 14:08', proj: '补水面膜 · v3', type: '视频片段 · 1 镜', amount: -0.45 },
|
||||
{ ts: '05.20 21:42', proj: '蓝牙耳机 · 开箱测评', type: '视频片段 · 6 镜', amount: -1.20 },
|
||||
{ ts: '05.20 16:00', proj: '蓝牙耳机 · 开箱测评', type: '故事板 image-2', amount: -0.45 },
|
||||
{ ts: '05.19 11:18', proj: '补水面膜 · v3', type: '故事板 image-2', amount: -0.45 },
|
||||
{ ts: '05.18 09:42', proj: '蓝牙耳机 · 开箱测评', type: '基础资产 · 4 张', amount: -0.84 },
|
||||
{ ts: '05.17 14:38', proj: '蓝牙耳机 · 开箱测评', type: '脚本 LLM · 1.8k', amount: -0.03 },
|
||||
] },
|
||||
{ id: 'u3', av: '王', name: '王小姐', email: 'wang@shop.com', role: 'member', daily: 100, monthly: 2000, used: 45.20, usedToday: 0, lastActive: '28 分钟前', pending: false,
|
||||
joinDate: '2026-04-22', inviter: '小李', projectsActive: 1, projectsDone: 4, assetsUploaded: 12,
|
||||
activity: [
|
||||
{ ts: '28 分钟前', act: '上传到资产库', obj: '林夕 · 主播图' },
|
||||
{ ts: '2 天前', act: '删除资产', obj: '透真防晒 · 旧版主图' },
|
||||
{ ts: '5 天前', act: '完成视频', obj: '透真防晒 · 通勤对比' },
|
||||
],
|
||||
byStage: { video: 28.00, storyboard: 10.00, asset: 5.00, script: 2.20 },
|
||||
transactions: [
|
||||
{ ts: '05.16 19:38', proj: '透真防晒 · 通勤对比', type: '视频片段 · 4 镜', amount: -0.80 },
|
||||
{ ts: '05.16 11:42', proj: '透真防晒 · 通勤对比', type: '故事板 image-2', amount: -0.45 },
|
||||
{ ts: '05.15 16:08', proj: '透真防晒 · 通勤对比', type: '基础资产 · 3 张', amount: -0.63 },
|
||||
{ ts: '05.15 09:30', proj: '透真防晒 · 通勤对比', type: '脚本 LLM · 1.5k', amount: -0.02 },
|
||||
] },
|
||||
{ id: 'u4', av: '陈', name: '陈策划', email: 'chen@shop.com', role: 'member', daily: 100, monthly: 2000, used: 12.80, usedToday: 0, lastActive: '4 小时前', pending: false,
|
||||
joinDate: '2026-05-02', inviter: '张运营', projectsActive: 1, projectsDone: 1, assetsUploaded: 5,
|
||||
activity: [
|
||||
{ ts: '4 小时前', act: '创建项目', obj: '蓝牙耳机 · 开箱测评' },
|
||||
{ ts: '昨天', act: '完成视频', obj: '速食面 · 加班场景' },
|
||||
],
|
||||
byStage: { video: 8.00, storyboard: 3.00, asset: 1.20, script: 0.60 },
|
||||
transactions: [
|
||||
{ ts: '05.20 14:32', proj: '速食面 · 加班场景', type: '视频片段 · 3 镜', amount: -0.60 },
|
||||
{ ts: '05.19 18:08', proj: '速食面 · 加班场景', type: '故事板 image-2', amount: -0.30 },
|
||||
{ ts: '05.19 11:12', proj: '速食面 · 加班场景', type: '基础资产 · 2 张', amount: -0.42 },
|
||||
] },
|
||||
{ id: 'u5', av: '林', name: '林新人', email: '186****1102', role: 'member', daily: 100, monthly: 2000, used: 0, usedToday: 0, lastActive: '尚未激活', pending: true,
|
||||
joinDate: '2026-05-19', inviter: '小李', projectsActive: 0, projectsDone: 0, assetsUploaded: 0,
|
||||
activity: [],
|
||||
byStage: { video: 0, storyboard: 0, asset: 0, script: 0 },
|
||||
transactions: [] },
|
||||
];
|
||||
|
||||
function fmtMoney(n) { return '¥' + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
|
||||
@@ -522,6 +804,11 @@ function renderMembers(filter = '') {
|
||||
}
|
||||
});
|
||||
});
|
||||
// 行点击 → 成员详情 modal(按钮区已 stopPropagation,不会冲突)
|
||||
tb.querySelectorAll('tr[data-id]').forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', () => openMemberDetail(row.dataset.id));
|
||||
});
|
||||
}
|
||||
renderMembers();
|
||||
|
||||
@@ -540,8 +827,11 @@ document.querySelectorAll('#invite-bg .role-choice').forEach(c => {
|
||||
});
|
||||
});
|
||||
document.getElementById('inv-send').addEventListener('click', () => {
|
||||
const phone = document.getElementById('inv-phone').value.trim();
|
||||
if (!phone) { Shell.toast('请填手机号', '邀请失败'); return; }
|
||||
const email = document.getElementById('inv-email').value.trim();
|
||||
if (!email) { Shell.toast('请填邮箱', '邀请失败'); return; }
|
||||
// 简单邮箱格式校验:必须含 @ 和域名后缀
|
||||
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
if (!emailOk) { Shell.toast('邮箱格式不正确', '请检查后重试'); return; }
|
||||
const role = document.querySelector('#invite-bg .role-choice.selected')?.dataset.role || 'member';
|
||||
const name = document.getElementById('inv-name').value.trim() || '待激活成员';
|
||||
const daily = Number(document.getElementById('inv-daily').value) || 100;
|
||||
@@ -549,13 +839,13 @@ document.getElementById('inv-send').addEventListener('click', () => {
|
||||
MEMBERS.push({
|
||||
id: 'u' + Date.now(),
|
||||
av: name[0] || '新',
|
||||
name, email: phone, role,
|
||||
name, email, role,
|
||||
daily, monthly, used: 0, pending: true,
|
||||
});
|
||||
renderMembers();
|
||||
Shell.closeModal('invite-bg');
|
||||
Shell.toast('邀请已发送', phone);
|
||||
document.getElementById('inv-phone').value = '';
|
||||
Shell.toast('邀请邮件已发送', email);
|
||||
document.getElementById('inv-email').value = '';
|
||||
document.getElementById('inv-name').value = '';
|
||||
});
|
||||
|
||||
@@ -600,6 +890,198 @@ document.getElementById('edit-remove').addEventListener('click', () => {
|
||||
renderMembers(document.getElementById('member-search').value);
|
||||
}
|
||||
});
|
||||
|
||||
/* ─── 成员详情 modal · 点击行打开 ─── */
|
||||
let detailId = null;
|
||||
function openMemberDetail(id) {
|
||||
const m = MEMBERS.find(x => x.id === id);
|
||||
if (!m) return;
|
||||
detailId = id;
|
||||
const r = ROLE_META[m.role];
|
||||
const pct = m.monthly > 0 ? m.used / m.monthly : 0;
|
||||
|
||||
document.getElementById('md-av').textContent = m.av;
|
||||
document.getElementById('md-name').textContent = m.name;
|
||||
document.getElementById('md-email').textContent = m.email;
|
||||
const rolePill = document.getElementById('md-role');
|
||||
rolePill.className = 'role-pill ' + r.cls;
|
||||
document.getElementById('md-role-label').textContent = r.label;
|
||||
document.getElementById('md-creator').style.display = m.creator ? '' : 'none';
|
||||
document.getElementById('md-pending').style.display = m.pending ? '' : 'none';
|
||||
|
||||
// 角色权限速览(PRD §10.2 权限矩阵)
|
||||
document.getElementById('md-perm-role').textContent = r.label;
|
||||
const permList = document.getElementById('md-perm-list');
|
||||
const perms = ROLE_PERMS[m.role] || [];
|
||||
permList.innerHTML = perms.map(p => `
|
||||
<div class="pi${p.ok ? '' : ' deny'}">
|
||||
<span class="ck">${p.ok ? '✓' : '—'}</span>
|
||||
<span class="lb">${p.txt}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('md-daily').textContent = fmtMoney(m.daily);
|
||||
document.getElementById('md-monthly').textContent = fmtMoney(m.monthly);
|
||||
const usedEl = document.getElementById('md-used');
|
||||
usedEl.textContent = fmtMoney(m.used);
|
||||
usedEl.classList.toggle('warn', pct >= 0.85);
|
||||
document.getElementById('md-pct').textContent = (pct * 100).toFixed(1) + '%';
|
||||
const bar = document.getElementById('md-bar');
|
||||
bar.style.width = Math.min(100, pct * 100).toFixed(1) + '%';
|
||||
bar.className = usedClass(pct);
|
||||
|
||||
// 今日已用 / 剩余
|
||||
const usedToday = m.usedToday || 0;
|
||||
const leftToday = Math.max(0, m.daily - usedToday);
|
||||
document.getElementById('md-used-today').textContent = fmtMoney(usedToday);
|
||||
const leftTodayEl = document.getElementById('md-left-today');
|
||||
leftTodayEl.textContent = fmtMoney(leftToday);
|
||||
leftTodayEl.classList.toggle('warn', m.daily > 0 && leftToday / m.daily < 0.2);
|
||||
|
||||
// 最近活跃
|
||||
document.getElementById('md-last-active').textContent = '· 最近活跃 ' + (m.lastActive || '—');
|
||||
|
||||
document.getElementById('md-projects-active').textContent = m.projectsActive ?? 0;
|
||||
document.getElementById('md-projects-done').textContent = m.projectsDone ?? 0;
|
||||
document.getElementById('md-assets').textContent = m.assetsUploaded ?? 0;
|
||||
|
||||
// ─── 消费明细:按阶段柱状条 ───
|
||||
const stageList = document.getElementById('md-stage-list');
|
||||
const bs = m.byStage || { video: 0, storyboard: 0, asset: 0, script: 0 };
|
||||
const totalCost = bs.video + bs.storyboard + bs.asset + bs.script;
|
||||
const STAGES = [
|
||||
{ key: 'video', label: '视频片段(Seedance)', cls: 's-video' },
|
||||
{ key: 'storyboard', label: '故事板(image-2)', cls: 's-storyboard' },
|
||||
{ key: 'asset', label: '基础资产', cls: 's-asset' },
|
||||
{ key: 'script', label: '脚本 LLM', cls: 's-script' },
|
||||
];
|
||||
if (totalCost === 0) {
|
||||
stageList.innerHTML = '<div style="font-size:12px;color:var(--black-alpha-48);font-family:var(--font-mono);text-align:center;padding:8px 0;">// 本月暂无消费</div>';
|
||||
} else {
|
||||
stageList.innerHTML = STAGES.map(s => {
|
||||
const v = bs[s.key] || 0;
|
||||
const pct = totalCost > 0 ? (v / totalCost * 100) : 0;
|
||||
return `
|
||||
<div class="md-stage-row">
|
||||
<span class="swatch ${s.cls}"></span>
|
||||
<div class="line">
|
||||
<span class="nm">${s.label}</span>
|
||||
<span class="pct">${pct.toFixed(0)}%</span>
|
||||
<span class="amt">${fmtMoney(v)}</span>
|
||||
</div>
|
||||
<div class="bar"><span class="${s.cls}" style="width: ${pct.toFixed(1)}%"></span></div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
document.getElementById('md-cost-total').textContent = fmtMoney(totalCost);
|
||||
|
||||
// ─── 流水明细表 ───
|
||||
const txTable = document.getElementById('md-tx-table');
|
||||
const txToggle = document.getElementById('md-tx-toggle');
|
||||
const txs = m.transactions || [];
|
||||
document.getElementById('md-tx-count').textContent = txs.length;
|
||||
// 保留 head 行,清除旧的明细行后重新注入
|
||||
txTable.querySelectorAll('.md-tx-row:not(.head)').forEach(el => el.remove());
|
||||
if (txs.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'md-tx-row';
|
||||
empty.style.gridTemplateColumns = '1fr';
|
||||
empty.style.color = 'var(--black-alpha-48)';
|
||||
empty.style.fontFamily = 'var(--font-mono)';
|
||||
empty.style.textAlign = 'center';
|
||||
empty.style.padding = '14px';
|
||||
empty.textContent = '// 暂无消费记录';
|
||||
txTable.appendChild(empty);
|
||||
} else {
|
||||
txs.forEach(t => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'md-tx-row';
|
||||
const amtCls = t.amount > 0 ? 'pos' : (t.amount === 0 ? 'zero' : '');
|
||||
const amtStr = t.amount === 0 ? '¥0.00' : (t.amount > 0 ? '+' + fmtMoney(t.amount) : '-' + fmtMoney(Math.abs(t.amount)));
|
||||
row.innerHTML = `
|
||||
<span class="ts">${t.ts}</span>
|
||||
<span class="proj" title="${t.proj}">${t.proj}</span>
|
||||
<span class="type">${t.type}</span>
|
||||
<span class="amt ${amtCls}">${amtStr}</span>
|
||||
`;
|
||||
txTable.appendChild(row);
|
||||
});
|
||||
}
|
||||
// 重置展开状态
|
||||
txTable.classList.remove('show');
|
||||
txToggle.classList.remove('expanded');
|
||||
const txLabel = txToggle.querySelector('.md-tx-label');
|
||||
txLabel.textContent = '展开';
|
||||
txToggle.onclick = () => {
|
||||
const opening = !txTable.classList.contains('show');
|
||||
txTable.classList.toggle('show', opening);
|
||||
txToggle.classList.toggle('expanded', opening);
|
||||
txLabel.textContent = opening ? '收起' : '展开';
|
||||
};
|
||||
txToggle.onkeydown = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); txToggle.click(); }
|
||||
};
|
||||
|
||||
const list = document.getElementById('md-activity');
|
||||
if (!m.activity || m.activity.length === 0) {
|
||||
list.innerHTML = '<div style="font-size:12px;color:var(--black-alpha-48);font-family:var(--font-mono);text-align:center;padding:16px 0;">// 暂无活动记录</div>';
|
||||
} else {
|
||||
list.innerHTML = m.activity.map(a =>
|
||||
'<div class="md-activity-item"><span class="ts">' + a.ts + '</span><span><span class="act">' + a.act + '</span> <span class="obj">' + a.obj + '</span></span></div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
document.getElementById('md-meta').textContent =
|
||||
'// 加入团队 ' + (m.joinDate || '—') +
|
||||
(m.inviter && m.inviter !== '—' ? ' · 由 ' + m.inviter + ' 邀请' : ' · 团队创建者');
|
||||
|
||||
// footer 操作权限分支:
|
||||
// - 创建者(超管):隐藏编辑/移除(协议保护,PRD 没有明确路径降级团队创建者)
|
||||
// - 待激活:编辑成员 → 重发邀请,移出团队 → 取消邀请
|
||||
// - 普通成员/团管:标准 编辑成员 + 移出团队
|
||||
const editBtn = document.getElementById('md-edit');
|
||||
const removeBtn = document.getElementById('md-remove');
|
||||
if (m.creator) {
|
||||
editBtn.style.display = 'none';
|
||||
removeBtn.style.display = 'none';
|
||||
} else if (m.pending) {
|
||||
editBtn.style.display = '';
|
||||
removeBtn.style.display = '';
|
||||
editBtn.textContent = '重发邀请';
|
||||
removeBtn.textContent = '取消邀请';
|
||||
} else {
|
||||
editBtn.style.display = '';
|
||||
removeBtn.style.display = '';
|
||||
editBtn.textContent = '编辑成员';
|
||||
removeBtn.textContent = '移出团队';
|
||||
}
|
||||
|
||||
Shell.openModal('member-detail-bg');
|
||||
}
|
||||
document.getElementById('md-edit').addEventListener('click', () => {
|
||||
if (!detailId) return;
|
||||
const m = MEMBERS.find(x => x.id === detailId);
|
||||
if (!m) return;
|
||||
if (m.pending) {
|
||||
Shell.toast('邀请邮件已重发', m.email);
|
||||
return;
|
||||
}
|
||||
Shell.closeModal('member-detail-bg');
|
||||
openEdit(detailId);
|
||||
});
|
||||
document.getElementById('md-remove').addEventListener('click', () => {
|
||||
const m = MEMBERS.find(x => x.id === detailId);
|
||||
if (!m) return;
|
||||
const verb = m.pending ? '取消「' + m.name + '」的邀请' : '将「' + m.name + '」移出团队';
|
||||
if (confirm('确定' + verb + '?')) {
|
||||
const i = MEMBERS.findIndex(x => x.id === m.id);
|
||||
MEMBERS.splice(i, 1);
|
||||
Shell.closeModal('member-detail-bg');
|
||||
Shell.toast(m.pending ? '邀请已取消' : '已移除', m.name);
|
||||
renderMembers(document.getElementById('member-search').value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user