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:
iye
2026-05-21 21:16:46 +08:00
co-authored by Claude Opus 4.7
parent 8a783ca36f
commit f420af2069
82 changed files with 15367 additions and 418 deletions
+515 -33
View File
@@ -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>