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:
+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