iye 086d92991e
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
统一 Airshelf 界面组件与图标
2026-05-27 12:29:41 +08:00

1356 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>团队 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 团队信息卡(深色 banner · 上标题行 + 下统计行)─── */
/* 顶部行:banner(左)+ 团队动态(右),与下方 team-grid 同列宽对齐 */
.team-top { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; margin-bottom: 24px; align-items: stretch; }
.team-banner {
background: var(--accent-black);
color: var(--accent-white);
padding: 22px 28px 24px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
}
/* ─── 团队动态卡(贴 banner 右边)─── */
.team-feed { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 16px 18px; display: flex; flex-direction: column; min-width: 0; }
.team-feed .h { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.team-feed .h h3 { font-size: 13.5px; font-weight: 600; margin: 0; }
.team-feed .h .ct { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
.team-feed .h .more { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--heat); text-decoration: none; cursor: pointer; }
.team-feed .feed-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; max-height: 240px; padding-right: 4px; scrollbar-width: thin; }
.team-feed .feed-list::-webkit-scrollbar { width: 4px; }
.team-feed .feed-list::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
.team-feed .feed-item { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 10px; align-items: start; }
.team-feed .feed-item .av { width: 24px; height: 24px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--accent-black); }
.team-feed .feed-item .txt { font-size: 12.5px; line-height: 1.45; color: var(--accent-black); min-width: 0; }
.team-feed .feed-item .txt .who { font-weight: 600; }
.team-feed .feed-item .txt .act { color: var(--black-alpha-56); margin: 0 3px; }
.team-feed .feed-item .txt .obj { color: var(--heat); }
.team-feed .feed-item .txt .obj-money { color: var(--accent-forest); font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
.team-feed .feed-item .ts { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }
/* 4 个装订线小十字(2 个 pseudo + 2 个 span)*/
.team-banner::before, .team-banner::after,
.team-banner > .corner-tr, .team-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;
}
.team-banner::before { top: -7px; left: -7px; }
.team-banner::after { bottom: -7px; right: -7px; }
.team-banner > .corner-tr { top: -7px; right: -7px; }
.team-banner > .corner-bl { bottom: -7px; left: -7px; }
/* 第 1 行:标题 + 主操作 */
.banner-head { display: flex; align-items: flex-start; gap: 20px; }
.banner-id { flex: 1; min-width: 0; }
.banner-id .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.banner-id .nm { font-size: 22px; font-weight: 700; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; }
.banner-id .nm .tag { font-size: 10.5px; font-family: var(--font-mono); padding: 2px 8px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); letter-spacing: .04em; font-weight: 500; }
.banner-id .meta { font-size: 12px; color: rgba(255,255,255,.5); margin-top: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.banner-actions { display: flex; gap: 8px; flex-shrink: 0; }
.banner-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }
.banner-actions .btn:hover { background: var(--background-base); }
.banner-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }
.banner-actions .btn-ghost:hover { background: rgba(255,255,255,.08); color: var(--accent-white); }
/* 分隔线 */
.banner-divider { height: 1px; background: rgba(255,255,255,.1); margin: 20px 0 18px; }
/* 第 2 行:4 列统计 */
.banner-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; }
.banner-stats .stat { min-width: 0; }
.banner-stats .stat .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.banner-stats .stat .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; }
/* color 必须显式写,否则会被 restraint.css 全局 .stat .v 的 color: var(--accent-black) 覆盖成黑字 */
.banner-stats .stat .v { color: var(--accent-white); }
.banner-stats .stat .v.warn { color: #FFB870; }
.banner-stats .stat .sub { font-size: 11px; color: rgba(255,255,255,.5); margin-top: 4px; font-family: var(--font-mono); letter-spacing: .02em; }
/* ─── 主体两栏 ─── */
.team-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; align-items: start; }
.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; display: flex; align-items: center; gap: 8px; }
.pane h3 .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); font-weight: 400; }
.pane h3 .spacer { margin-left: auto; }
/* ─── 成员表 ─── */
.members-table .av { width: 32px; height: 32px; border-radius: 50%; background: var(--background-lighter); display: inline-grid; place-items: center; font-weight: 600; font-size: 13px; color: var(--accent-black); border: 1px solid var(--border-faint); }
.members-table .who { display: flex; align-items: center; gap: 10px; }
.members-table .nm { font-weight: 500; font-size: 13.5px; line-height: 1.2; }
.members-table .em { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); }
.members-table .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; }
.members-table .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; }
.members-table .role-super { background: var(--heat-12); color: var(--heat); }
.members-table .role-super .dot { background: var(--heat); }
.members-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
.members-table .role-admin .dot { background: #1E40AF; }
.members-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
.members-table .role-member .dot { background: var(--black-alpha-56); }
.members-table .quota-cell { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 12px; }
.members-table .quota-cell .lbl { color: var(--black-alpha-48); }
.members-table .quota-cell .v { color: var(--accent-black); font-weight: 600; }
.members-table .used-bar { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; margin-top: 4px; }
.members-table .used-bar > span { display: block; height: 100%; background: var(--heat); }
.members-table .used-bar > span.ok { background: var(--accent-forest); }
.members-table .used-bar > span.warn { background: #B45309; }
.members-table .acts { display: flex; gap: 4px; justify-content: flex-end; }
.members-table .icon-btn-sm { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; color: var(--black-alpha-56); transition: all var(--t-base); }
.members-table .icon-btn-sm:hover { color: var(--heat); border-color: var(--heat-20); }
.members-table .icon-btn-sm svg { width: 14px; height: 14px; }
.members-table .icon-btn-sm.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }
.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); }
/* ─── 角色权限矩阵 ─── */
.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); }
.perm-table th { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; text-align: left; }
.perm-table th:not(:first-child), .perm-table td:not(:first-child) { text-align: center; }
.perm-table tbody td:first-child { color: var(--accent-black); }
.perm-table .yes { color: var(--accent-forest); font-weight: 600; }
.perm-table .no { color: var(--black-alpha-32); }
/* ─── 额度检查规则 ─── */
.quota-rules { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.8; }
.quota-rules .step { display: flex; gap: 10px; padding: 6px 0; align-items: flex-start; }
.quota-rules .num { width: 18px; height: 18px; 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; flex: 0 0 18px; margin-top: 1px; }
.quota-rules .v { color: var(--accent-black); font-weight: 500; }
.quota-rules .formula { font-family: var(--font-mono); font-size: 11px; color: var(--heat); background: var(--heat-12); padding: 1px 6px; }
/* ─── 邀请 modal ─── */
.invite-modal { width: min(480px, 92vw); }
.invite-modal .field { margin-bottom: 14px; }
.invite-modal label.field-label { display: block; font-size: 12px; color: var(--black-alpha-56); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.invite-modal label.field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.role-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.role-choice { padding: 12px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
.role-choice:hover { background: var(--background-lighter); }
.role-choice.selected { border-color: var(--heat); background: var(--heat-12); }
.role-choice .title { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.role-choice .desc { font-size: 11px; color: var(--black-alpha-56); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
.quota-input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.quota-input-row .input { font-variant-numeric: tabular-nums; }
/* ─── 重置密码 modal ─── */
.reset-pwd-modal { width: min(480px, 92vw); }
.reset-pwd-modal .reset-pwd-warn {
display: flex; align-items: flex-start; gap: 10px;
padding: 10px 12px;
background: var(--heat-12);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
margin-bottom: 14px;
font-size: 12.5px; color: var(--heat);
line-height: 1.5;
}
.reset-pwd-modal .reset-pwd-warn svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 2px; }
.reset-pwd-modal .btn-sm { height: 38px; padding: 0 12px; }
/* ─── 全部团队动态 modal ─── */
.feed-all-modal { width: min(640px, 92vw); max-width: min(640px, 92vw); position: relative; }
.feed-all-modal .md-x {
position: absolute; top: 14px; right: 16px;
width: 28px; height: 28px;
background: transparent; border: 0; border-radius: var(--r-sm);
color: var(--black-alpha-56); cursor: pointer;
display: grid; place-items: center;
}
.feed-all-modal .md-x:hover { background: var(--background-lighter); color: var(--accent-black); }
.feed-all-modal .md-x svg { width: 14px; height: 14px; }
.feed-all-filter {
display: flex; align-items: center; gap: 6px;
padding: 12px 20px;
border-bottom: 1px solid var(--border-faint);
background: var(--background-lighter);
}
.feed-all-filter .spacer { flex: 1; }
.feed-all-filter .fa-meta { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.feed-all-filter .fa-chip {
height: 26px; padding: 0 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
font-size: 11.5px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.feed-all-filter .fa-chip:hover { border-color: var(--heat-20); color: var(--heat); }
.feed-all-filter .fa-chip.selected { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }
.feed-all-body { max-height: 56vh; overflow-y: auto; padding: 8px 20px 20px; }
.feed-all-list { display: flex; flex-direction: column; gap: 10px; }
.feed-all-list .feed-item {
display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 12px;
align-items: start;
padding: 10px 0;
border-bottom: 1px solid var(--border-faint);
}
.feed-all-list .feed-item:last-child { border-bottom: 0; }
.feed-all-list .feed-item .av {
width: 28px; height: 28px;
background: var(--background-lighter); border: 1px solid var(--border-faint);
border-radius: 50%; display: grid; place-items: center;
font-size: 12px; font-weight: 600; color: var(--accent-black);
}
.feed-all-list .feed-item .txt { font-size: 13px; line-height: 1.5; color: var(--accent-black); }
.feed-all-list .feed-item .txt .who { font-weight: 600; }
.feed-all-list .feed-item .txt .act { color: var(--black-alpha-56); margin: 0 4px; }
.feed-all-list .feed-item .txt .obj { color: var(--heat); }
.feed-all-list .feed-item .ts { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; white-space: nowrap; padding-top: 2px; }
.feed-all-list .feed-empty { padding: 40px 0; text-align: center; font-family: var(--font-mono); font-size: 12px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── 月限额 modal ─── */
.limit-modal .limit-presets { display: flex; flex-wrap: wrap; gap: 6px; }
.limit-modal .limit-presets .lp {
height: 32px; padding: 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
font-size: 12px; font-family: var(--font-mono);
color: var(--accent-black);
cursor: pointer;
font-variant-numeric: tabular-nums;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.limit-modal .limit-presets .lp:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }
.limit-modal .limit-presets .lp.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }
.limit-modal .limit-info {
margin-top: 14px;
padding: 10px 12px;
background: var(--background-lighter);
border-radius: var(--r-md);
font-size: 11.5px;
display: flex; flex-direction: column; gap: 4px;
}
.limit-modal .limit-info .li-row { display: flex; align-items: baseline; gap: 8px; }
.limit-modal .limit-info .lk { font-family: var(--font-mono); color: var(--black-alpha-48); letter-spacing: .02em; }
.limit-modal .limit-info .lv { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }
.limit-modal .limit-info .lv.neg { color: var(--accent-crimson); }
.limit-modal label.field-label .lbl-note { color: var(--black-alpha-48); font-weight: 400; margin-left: 2px; font-family: var(--font-mono); }
.limit-modal input[type=number]::-webkit-outer-spin-button,
.limit-modal input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.limit-modal input[type=number] { -moz-appearance: textfield; }
/* ─── 创建账户 modal ─── */
.create-acct-modal label.field-label .lbl-note { color: var(--black-alpha-48); font-weight: 400; margin-left: 2px; font-family: var(--font-mono); }
.create-acct-modal .btn-sm { height: 38px; padding: 0 12px; }
.create-acct-modal .quota-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.create-acct-modal .quota-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.create-acct-modal .quota-cell .qk { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.create-acct-modal .quota-cell .input { font-variant-numeric: tabular-nums; height: 34px; padding: 0 10px; }
.create-acct-modal input[type=number]::-webkit-outer-spin-button,
.create-acct-modal input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.create-acct-modal input[type=number] { -moz-appearance: textfield; }
/* ─── 分享凭据 modal ─── */
.share-acct-modal { width: min(480px, 92vw); }
.share-acct-modal .cred-card {
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
overflow: hidden;
}
.share-acct-modal .cred-row {
display: grid;
grid-template-columns: 80px minmax(0, 1fr) 28px;
gap: 12px;
align-items: center;
padding: 12px 14px;
border-bottom: 1px solid var(--border-faint);
}
.share-acct-modal .cred-row:last-child { border-bottom: 0; }
.share-acct-modal .cred-row .ck { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
.share-acct-modal .cred-row .cv {
font-size: 13px; color: var(--accent-black); font-weight: 500;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.share-acct-modal .cred-row .cv.mono { font-family: var(--font-mono); letter-spacing: .04em; }
.share-acct-modal .cred-copy {
width: 28px; height: 28px;
background: transparent; border: 1px solid var(--border-faint);
border-radius: var(--r-sm); color: var(--black-alpha-56);
display: grid; place-items: center; cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.share-acct-modal .cred-copy:hover { border-color: var(--heat-20); color: var(--heat); }
.share-acct-modal .cred-copy svg { width: 13px; height: 13px; }
.share-acct-modal .cred-rules {
margin-top: 14px;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-56);
letter-spacing: .02em; line-height: 1.7;
}
.share-acct-modal .cred-rules .li { display: flex; gap: 8px; }
.share-acct-modal .cred-rules .li::before { content: '//'; color: var(--black-alpha-32); flex: 0 0 auto; }
/* ─── 编辑成员 modal ─── */
.edit-member-modal .ti .em-sep { color: var(--black-alpha-32); font-weight: 400; margin: 0 2px; }
.edit-member-modal .ti #edit-username { color: var(--accent-black); font-weight: 500; }
.edit-member-modal label.field-label .lbl-note { color: var(--black-alpha-48); font-weight: 400; margin-left: 2px; font-family: var(--font-mono); }
.edit-member-modal #edit-name-readonly { background: var(--background-lighter); color: var(--black-alpha-56); cursor: not-allowed; }
.edit-member-modal .readonly-text { font-size: 13px; color: var(--accent-black); padding: 8px 0; line-height: 1.4; }
.edit-member-modal .readonly-text .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 12px; font-weight: 500; background: var(--heat-12); color: var(--heat); }
.edit-member-modal .readonly-text .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }
.edit-member-modal input[type=number]::-webkit-outer-spin-button,
.edit-member-modal input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.edit-member-modal input[type=number] { -moz-appearance: textfield; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>团队管理</h1>
<div class="sub"><span class="mono">// 成员 · 角色 · 额度 · 共享资产库</span></div>
</div>
<div class="actions">
<button class="btn btn-primary" id="open-invite">
<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="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>
创建账户
</button>
</div>
</div>
<!-- 顶部行:团队 banner(左)+ 团队动态(右) -->
<div class="team-top">
<!-- 团队信息 banner -->
<div class="team-banner">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="banner-head">
<div class="banner-id">
<div class="lbl">[ TEAM ]</div>
<div class="nm">小李的店 <span class="tag">企业</span></div>
<div class="meta">// 团队 ID: T-2026-A8F2 · 创建于 2026-04-12 · 5 名成员</div>
</div>
<div class="banner-actions">
<button class="btn btn-sm" onclick="location.href='account.html'">
<svg width="13" height="13" 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="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
充值
</button>
<button class="btn btn-ghost btn-sm" id="open-limit">设置月限额</button>
</div>
</div>
<div class="banner-divider"></div>
<div class="banner-stats">
<div class="stat">
<div class="lbl">[ 充值余额 ]</div>
<div class="v">¥327.40</div>
<div class="sub">// 团队总池</div>
</div>
<div class="stat">
<div class="lbl">[ 月限额 ]</div>
<div class="v" id="stat-limit">¥3,000</div>
<div class="sub">// 自然月重置</div>
</div>
<div class="stat">
<div class="lbl">[ 当月已用 ]</div>
<div class="v" id="stat-used">¥162.60</div>
<div class="sub" id="stat-used-sub">// 占月限 5.4%</div>
</div>
<div class="stat">
<div class="lbl">[ 当月剩余 ]</div>
<div class="v warn" id="stat-left">¥2,837.40</div>
<div class="sub" id="stat-left-sub">// 还可生成约 280 个项目</div>
</div>
</div>
</div>
<!-- 团队动态(banner 右栏)-->
<div class="team-feed">
<div class="h">
<h3>团队动态</h3>
<span class="ct">// 最近 12 条</span>
<a class="more" id="open-feed-all" role="button" tabindex="0">全部 →</a>
</div>
<div class="feed-list">
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">张运营</span><span class="act">完成视频</span><span class="obj">补水面膜 · v3</span></div>
<div class="ts">10 分钟前</div>
</div>
</div>
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">王小姐</span><span class="act">上传到资产库</span><span class="obj">林夕 · 主播图</span></div>
<div class="ts">28 分钟前</div>
</div>
</div>
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">小李</span><span class="act">邀请新成员</span><span class="obj">林新人</span></div>
<div class="ts">2 小时前</div>
</div>
</div>
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">陈策划</span><span class="act">创建项目</span><span class="obj">蓝牙耳机 · 开箱测评</span></div>
<div class="ts">4 小时前</div>
</div>
</div>
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">张运营</span><span class="act">采用故事板</span><span class="obj">补水面膜 · 场 3 · v2</span></div>
<div class="ts">昨天 18:32</div>
</div>
</div>
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">小李</span><span class="act">团队充值</span><span class="obj-money">+¥500.00</span></div>
<div class="ts">昨天 11:02</div>
</div>
</div>
<div class="feed-item">
<div class="av"></div>
<div>
<div class="txt"><span class="who">王小姐</span><span class="act">删除资产</span><span class="obj">透真防晒 · 旧版主图</span></div>
<div class="ts">2 天前</div>
</div>
</div>
</div>
</div>
</div><!-- /.team-top -->
<div class="team-grid">
<!-- 左:成员表 -->
<div>
<div class="pane" style="padding: 0;">
<div style="display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border-faint);">
<h3 style="margin: 0;">成员列表 <span class="ct">// 5 人 · 1 超管 / 1 团管 / 3 成员</span></h3>
<span class="spacer"></span>
<input class="input" id="member-search" placeholder="搜索姓名 / 手机号" style="height: 32px; font-size: 12px; width: 220px;">
</div>
<table class="t members-table" style="border: 0; border-radius: 0;">
<thead>
<tr>
<th>成员</th>
<th>角色</th>
<th>每日额度</th>
<th>月度额度</th>
<th style="width: 140px;">当月已用</th>
<th style="text-align: right; width: 88px;">操作</th>
</tr>
</thead>
<tbody id="members-tbody">
<!-- JS 注入 -->
</tbody>
</table>
</div>
</div>
<!-- 右:权限矩阵 + 额度规则 -->
<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;">// PRD §10.2 权限矩阵节选</div>
<table class="perm-table">
<thead>
<tr><th>能力</th><th>超管</th><th>团管</th><th>成员</th></tr>
</thead>
<tbody>
<tr><td>邀请 / 移除成员</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>设置成员额度</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>团队充值</td><td class="yes"></td><td class="no"></td><td class="no"></td></tr>
<tr><td>设置月限额</td><td class="yes"></td><td class="no"></td><td class="no"></td></tr>
<tr><td>编辑别人项目</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>团队共享资产库管理</td><td class="yes"></td><td class="yes"></td><td class="no">仅自传</td></tr>
<tr><td>查看团队消费明细</td><td class="yes"></td><td class="yes"></td><td class="no">仅自己</td></tr>
<tr style="border-bottom: 0;"><td>创建项目 / 用 AI 流程</td><td class="yes"></td><td class="yes"></td><td class="yes"></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 设置月限额 modal -->
<div class="modal-bg" id="limit-bg" onclick="if(event.target===this)Shell.closeModal('limit-bg')">
<div class="modal invite-modal limit-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>// 自然月重置 · 仅超管可改</span></div>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">月限额 ¥ <span class="lbl-note">(-1 为不限)</span></label>
<input class="input" id="limit-input" type="number" inputmode="numeric" placeholder="例: 3000" style="font-variant-numeric: tabular-nums;">
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">快捷选择</label>
<div class="limit-presets">
<button type="button" class="lp" data-v="1000">¥1,000</button>
<button type="button" class="lp" data-v="3000">¥3,000</button>
<button type="button" class="lp" data-v="5000">¥5,000</button>
<button type="button" class="lp" data-v="10000">¥10,000</button>
<button type="button" class="lp" data-v="-1">不限</button>
</div>
</div>
<div class="limit-info">
<div class="li-row"><span class="lk">当月已用</span><span class="lv mono" id="limit-info-used">¥162.60</span></div>
<div class="li-row"><span class="lk">本次调整后剩余</span><span class="lv mono" id="limit-info-left">¥2,837.40</span></div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('limit-bg')">取消</button>
<button class="btn btn-primary" type="button" id="limit-save">
<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>
<!-- 重置密码 modal -->
<div class="modal-bg" id="reset-pwd-bg" onclick="if(event.target===this)Shell.closeModal('reset-pwd-bg')">
<div class="modal invite-modal reset-pwd-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="color: var(--heat); background: var(--heat-12);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>
</div>
<div class="ti">重置登录密码 <span class="em-sep" style="color: var(--black-alpha-32); margin: 0 2px;"></span> <span id="reset-pwd-name" style="color: var(--accent-black); font-weight: 500;"></span><span>// 该成员当前会话会被强制下线</span></div>
</div>
<div class="modal-b">
<div class="reset-pwd-warn">
<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 8v4M12 16h.01"/></svg>
<span>旧密码即刻失效,该成员需用新密码重新登录</span>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">新密码</label>
<div style="display:flex; gap:8px;">
<input class="input" id="reset-pwd-input" autocomplete="off" placeholder="≥ 8 位 · 含字母与数字" style="flex:1; font-family:var(--font-mono); letter-spacing:.04em;">
<button class="btn btn-sm" type="button" id="reset-pwd-gen" title="生成随机密码">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>
</button>
</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('reset-pwd-bg')">取消</button>
<button class="btn btn-primary" type="button" id="reset-pwd-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>
<!-- 全部团队动态 modal -->
<div class="modal-bg" id="feed-all-bg" onclick="if(event.target===this)Shell.closeModal('feed-all-bg')">
<div class="modal feed-all-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"><path d="M3 12h4l3-9 4 18 3-9h4"/></svg>
</div>
<div class="ti">团队动态<span id="feed-all-count">// 共 12 条</span></div>
<button class="md-x" type="button" aria-label="关闭" onclick="Shell.closeModal('feed-all-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>
<div class="feed-all-filter">
<button type="button" class="fa-chip selected" data-filter="all">全部</button>
<button type="button" class="fa-chip" data-filter="project">项目</button>
<button type="button" class="fa-chip" data-filter="asset">资产</button>
<button type="button" class="fa-chip" data-filter="team">团队</button>
<span class="spacer"></span>
<span class="fa-meta mono" id="feed-all-meta">// 共 12 条</span>
</div>
<div class="modal-b feed-all-body">
<div class="feed-all-list" id="feed-all-list"><!-- JS 注入 --></div>
</div>
</div>
</div>
<!-- 创建账户 modal -->
<div class="modal-bg" id="invite-bg" onclick="if(event.target===this)Shell.closeModal('invite-bg')">
<div class="modal invite-modal create-acct-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"><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>
<div class="modal-b">
<div class="field">
<label class="field-label">用户名 <span class="req">*</span></label>
<div style="display:flex; gap:8px;">
<input class="input" id="inv-username" autocomplete="off" placeholder="例: zhang.yunying" style="flex:1;">
<button class="btn btn-sm" type="button" id="inv-suggest-name" title="生成随机用户名">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>
</button>
</div>
</div>
<div class="field">
<label class="field-label">备注姓名(可选)</label>
<input class="input" id="inv-name" placeholder="例: 张某">
</div>
<div class="field">
<label class="field-label">登录密码 <span class="req">*</span></label>
<div style="display:flex; gap:8px;">
<input class="input" id="inv-password" autocomplete="off" placeholder="≥ 8 位 · 含字母与数字" style="flex:1; font-family:var(--font-mono); letter-spacing:.04em;">
<button class="btn btn-sm" type="button" id="inv-gen-pwd" title="生成随机密码">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>
</button>
</div>
</div>
<div class="field">
<label class="field-label">分配角色 <span class="req">*</span></label>
<div class="role-choices">
<div class="role-choice selected" data-role="member">
<div class="title">成员</div>
<div class="desc">// 创建项目 + 用资产</div>
</div>
<div class="role-choice" data-role="admin">
<div class="title">团管</div>
<div class="desc">// 管成员 + 改额度</div>
</div>
</div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">额度配置 <span class="lbl-note">(¥ · -1 为不限)</span></label>
<div class="quota-grid">
<label class="quota-cell">
<span class="qk">每日额度 ¥</span>
<input class="input" id="inv-daily" type="number" inputmode="numeric" value="100">
</label>
<label class="quota-cell">
<span class="qk">每月额度 ¥</span>
<input class="input" id="inv-monthly" type="number" inputmode="numeric" value="2000">
</label>
<label class="quota-cell">
<span class="qk">总额度 ¥</span>
<input class="input" id="inv-total" type="number" inputmode="numeric" value="-1">
</label>
</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('invite-bg')">取消</button>
<button class="btn btn-primary" type="button" id="inv-send">
<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="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>
创建账户
</button>
</div>
</div>
</div>
<!-- 账户创建成功 · 分享凭据弹窗 -->
<div class="modal-bg" id="invite-share-bg" onclick="if(event.target===this)Shell.closeModal('invite-share-bg')">
<div class="modal invite-modal share-acct-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="color: var(--accent-forest); background: rgba(34, 110, 51, .08);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
</div>
<div class="ti">账户已创建<span>// 把下方凭据分享给成员 · 凭据仅展示一次</span></div>
</div>
<div class="modal-b">
<div class="cred-card">
<div class="cred-row">
<span class="ck">登录地址</span>
<span class="cv mono" id="share-url">https://airshelf.com/login</span>
<button class="cred-copy" type="button" data-copy="share-url" title="复制">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
</div>
<div class="cred-row">
<span class="ck">用户名</span>
<span class="cv mono" id="share-username"></span>
<button class="cred-copy" type="button" data-copy="share-username" title="复制">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
</div>
<div class="cred-row">
<span class="ck">初始密码</span>
<span class="cv mono" id="share-password"></span>
<button class="cred-copy" type="button" data-copy="share-password" title="复制">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
</div>
</div>
<div class="cred-rules">
<div class="li">建议成员首次登录后立即修改密码</div>
<div class="li">凭据请通过加密渠道(企微 / 飞书私聊)分享,不要发到公共群</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" id="share-copy-all">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
一键复制全部
</button>
<button class="btn btn-primary" type="button" onclick="Shell.closeModal('invite-share-bg')">完成</button>
</div>
</div>
</div>
<!-- 编辑成员 modal -->
<div class="modal-bg" id="edit-member-bg" onclick="if(event.target===this)Shell.closeModal('edit-member-bg')">
<div class="modal invite-modal edit-member-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"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
</div>
<div class="ti" id="edit-title">编辑成员 <span class="em-sep"></span> <span id="edit-username"></span></div>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">用户名 <span class="lbl-note">(无权修改)</span></label>
<input class="input" id="edit-name-readonly" readonly tabindex="-1">
</div>
<div class="field">
<label class="field-label">角色 <span class="lbl-note" id="edit-role-note"></span></label>
<div class="readonly-text" id="edit-role-text" hidden></div>
<div class="role-choices" id="edit-role-choices" hidden>
<div class="role-choice" data-edit-role="member">
<div class="title">成员</div>
<div class="desc">// 创建项目 + 用资产</div>
</div>
<div class="role-choice" data-edit-role="admin">
<div class="title">团管</div>
<div class="desc">// 管成员 + 改额度</div>
</div>
</div>
</div>
<div class="field">
<label class="field-label">每日额度 ¥ <span class="lbl-note">(-1 为不限)</span></label>
<input class="input" id="edit-daily" type="number" inputmode="numeric" placeholder="例: 100">
</div>
<div class="field">
<label class="field-label">每月额度 ¥ <span class="lbl-note">(-1 为不限)</span></label>
<input class="input" id="edit-monthly" type="number" inputmode="numeric" placeholder="例: 2000">
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">总额度 ¥ <span class="lbl-note">(-1 为不限)</span></label>
<input class="input" id="edit-total" type="number" inputmode="numeric" placeholder="例: -1">
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('edit-member-bg')">取消</button>
<button class="btn btn-primary" type="button" id="edit-save">保存</button>
</div>
</div>
</div>
</div>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
Shell.render({
active: 'team',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '团队' }]
});
/* ─── 团队成员 mock + 渲染 ─── */
const ROLE_META = {
super: { cls: 'role-super', label: '超管' },
admin: { cls: 'role-admin', label: '团管' },
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, 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, ','); }
function usedClass(pct) { if (pct < 0.5) return 'ok'; if (pct < 0.85) return ''; return 'warn'; }
/* ─── 当前操作者角色 + PRD §10.2 权限矩阵 · 必须在 renderMembers 之前声明 ─── */
const CURRENT_ROLE = 'super'; // 'super' | 'admin' | 'member'
function canEditRole(target) {
// 只有超管可以改成员角色,创建者(初始超管)不可降级,其它超管暂不支持改
if (CURRENT_ROLE !== 'super') return false;
if (target.creator) return false;
return target.role !== 'super';
}
function canEditMember(target) {
if (target.creator) return false;
if (CURRENT_ROLE === 'super') return true;
if (CURRENT_ROLE === 'admin') return target.role === 'member';
return false;
}
function canResetPassword(target) {
if (target.creator) return false;
if (CURRENT_ROLE === 'super') return target.role !== 'super';
if (CURRENT_ROLE === 'admin') return target.role === 'member';
return false;
}
function canRemoveMember(target) {
return canEditMember(target);
}
function renderMembers(filter = '') {
const tb = document.getElementById('members-tbody');
const list = MEMBERS.filter(m => {
if (!filter) return true;
const q = filter.toLowerCase();
return m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q);
});
tb.innerHTML = list.map(m => {
const r = ROLE_META[m.role];
const pct = m.monthly > 0 ? m.used / m.monthly : 0;
return `
<tr data-id="${m.id}" class="${m.pending ? 'pending' : ''}${m.creator ? ' creator-row' : ''}">
<td>
<div class="who">
<div class="av">${m.av}</div>
<div>
<div class="nm">${m.name}${m.creator ? ' <span style="font-family:var(--font-mono);font-size:10px;color:var(--black-alpha-48);">· 创建者</span>' : ''}</div>
<div class="em">${m.email}</div>
</div>
</div>
</td>
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
<td><span class="quota-cell"><span class="v">${fmtMoney(m.daily)}</span></span></td>
<td><span class="quota-cell"><span class="v">${fmtMoney(m.monthly)}</span></span></td>
<td>
<div class="quota-cell"><span class="v">${fmtMoney(m.used)}</span> <span class="lbl">/ ${(pct * 100).toFixed(0)}%</span></div>
<div class="used-bar"><span class="${usedClass(pct)}" style="width: ${Math.min(100, pct * 100).toFixed(1)}%"></span></div>
</td>
<td>
<div class="acts">
${m.creator
? '<span style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-32);align-self:center;">不可编辑</span>'
: [
canEditMember(m) ? `<button class="icon-btn-sm" data-act="edit" data-id="${m.id}" title="编辑">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
</button>` : '',
canResetPassword(m) ? `<button class="icon-btn-sm" data-act="reset-pwd" data-id="${m.id}" title="重置密码">
<svg 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>
</button>` : '',
canRemoveMember(m) ? `<button class="icon-btn-sm danger" data-act="remove" data-id="${m.id}" title="移出">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>` : '',
].join('')
}
</div>
</td>
</tr>
`;
}).join('');
tb.querySelectorAll('[data-act="edit"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
openEdit(btn.dataset.id);
});
});
tb.querySelectorAll('[data-act="reset-pwd"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
openResetPwd(btn.dataset.id);
});
});
tb.querySelectorAll('[data-act="remove"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const m = MEMBERS.find(x => x.id === btn.dataset.id);
if (!m) return;
if (confirm('确定将「' + m.name + '」移出团队?')) {
const i = MEMBERS.findIndex(x => x.id === m.id);
MEMBERS.splice(i, 1);
Shell.toast('已移除成员', m.name);
renderMembers(document.getElementById('member-search').value);
}
});
});
}
renderMembers();
document.getElementById('member-search').addEventListener('input', e => {
renderMembers(e.target.value);
});
/* ─── 创建账户 modal ─── */
const $username = document.getElementById('inv-username');
const $name = document.getElementById('inv-name');
const $password = document.getElementById('inv-password');
const $daily = document.getElementById('inv-daily');
const $monthly = document.getElementById('inv-monthly');
const $total = document.getElementById('inv-total');
function genUsername() {
const prefixes = ['user', 'team', 'shop', 'creator', 'editor'];
const p = prefixes[Math.floor(Math.random() * prefixes.length)];
const n = Math.floor(1000 + Math.random() * 9000);
return `${p}.${n}`;
}
function genPassword() {
// 12 位 · 字母 + 数字混合 · 排除易混淆字符(0/O/1/l/I)
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
let s = '';
for (let i = 0; i < 12; i++) s += chars[Math.floor(Math.random() * chars.length)];
return s;
}
function parseQuota(v, fallback) {
if (v === '' || v == null) return fallback;
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
document.getElementById('open-invite').addEventListener('click', () => {
// 打开时填充建议用户名 + 默认密码,用户可改
if (!$username.value.trim()) $username.value = genUsername();
if (!$password.value.trim()) $password.value = genPassword();
Shell.openModal('invite-bg');
});
document.querySelectorAll('#invite-bg .role-choice').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#invite-bg .role-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
});
});
document.getElementById('inv-suggest-name').addEventListener('click', () => {
$username.value = genUsername();
$username.focus();
});
document.getElementById('inv-gen-pwd').addEventListener('click', () => {
$password.value = genPassword();
});
document.getElementById('inv-send').addEventListener('click', () => {
const username = ($username.value || '').trim();
const password = ($password.value || '').trim();
const name = ($name.value || '').trim() || username;
if (!username) { Shell.toast('请填用户名', '账户未创建'); $username.focus(); return; }
if (!/^[A-Za-z0-9._-]{3,32}$/.test(username)) {
Shell.toast('用户名格式不正确', '332 位 · 字母/数字/. _ -');
$username.focus(); return;
}
if (!password) { Shell.toast('请填密码', '账户未创建'); $password.focus(); return; }
if (password.length < 8 || !/[A-Za-z]/.test(password) || !/\d/.test(password)) {
Shell.toast('密码强度不够', '至少 8 位 · 含字母与数字');
$password.focus(); return;
}
const role = document.querySelector('#invite-bg .role-choice.selected')?.dataset.role || 'member';
const daily = parseQuota($daily.value, 100);
const monthly = parseQuota($monthly.value, 2000);
const totalQuota = parseQuota($total.value, -1);
MEMBERS.push({
id: 'u' + Date.now(),
av: name[0] || username[0]?.toUpperCase() || 'U',
name, username,
email: username + '@airshelf.local',
role,
daily, monthly, totalQuota,
used: 0, usedToday: 0,
lastActive: '尚未激活',
pending: true,
});
renderMembers();
Shell.closeModal('invite-bg');
Shell.toast('账户已创建', username + ' · 共享凭据已生成');
// 填充并打开分享凭据弹窗
document.getElementById('share-username').textContent = username;
document.getElementById('share-password').textContent = password;
Shell.openModal('invite-share-bg');
// 重置表单
$username.value = '';
$name.value = '';
$password.value = '';
});
/* ─── 分享凭据 · 复制 ─── */
async function copyText(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) { /* fallthrough to execCommand */ }
// 兜底:textarea + execCommand
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
return ok;
}
document.querySelectorAll('#invite-share-bg .cred-copy').forEach(btn => {
btn.addEventListener('click', async () => {
const target = btn.dataset.copy;
const el = document.getElementById(target);
if (!el) return;
const ok = await copyText(el.textContent.trim());
const lbl = target === 'share-url' ? '登录地址' : (target === 'share-username' ? '用户名' : '密码');
Shell.toast(ok ? '已复制' : '复制失败', ok ? lbl : '请手动选中复制');
});
});
document.getElementById('share-copy-all').addEventListener('click', async () => {
const url = document.getElementById('share-url').textContent.trim();
const u = document.getElementById('share-username').textContent.trim();
const p = document.getElementById('share-password').textContent.trim();
const text = `登录地址: ${url}\n用户名: ${u}\n初始密码: ${p}`;
const ok = await copyText(text);
Shell.toast(ok ? '已复制全部' : '复制失败', ok ? '可以粘贴到企微/飞书私聊' : '请手动复制');
});
/* ─── 编辑 modal ─── */
const ROLE_LABEL = { super: '超级管理员 · 创建者', admin: '副管理员', member: '成员' };
let editingId = null;
function openEdit(id) {
const m = MEMBERS.find(x => x.id === id);
if (!m) return;
editingId = id;
document.getElementById('edit-username').textContent = m.name;
document.getElementById('edit-name-readonly').value = m.name;
// 角色:可编辑 → 显示 role-choices,只读 → 显示 pill
const roleNote = document.getElementById('edit-role-note');
const roleText = document.getElementById('edit-role-text');
const roleChoices = document.getElementById('edit-role-choices');
if (canEditRole(m)) {
roleNote.textContent = '(可改)';
roleText.hidden = true;
roleChoices.hidden = false;
roleChoices.querySelectorAll('.role-choice').forEach(c => {
c.classList.toggle('selected', c.dataset.editRole === m.role);
});
} else {
roleNote.textContent = m.creator ? '(创建者不可改)' : (CURRENT_ROLE === 'super' ? '' : '(仅超管可改)');
roleText.hidden = false;
roleChoices.hidden = true;
const roleLabel = ROLE_LABEL[m.role] || '成员';
roleText.innerHTML = '<span class="role-pill"><span class="dot"></span>' + roleLabel + '</span>';
}
document.getElementById('edit-daily').value = m.daily != null ? m.daily : -1;
document.getElementById('edit-monthly').value = m.monthly != null ? m.monthly : -1;
document.getElementById('edit-total').value = m.totalQuota != null ? m.totalQuota : -1;
Shell.openModal('edit-member-bg');
}
// role chip 切换
document.querySelectorAll('#edit-role-choices .role-choice').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#edit-role-choices .role-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
});
});
/* ─── 重置密码 modal ─── */
let _resetPwdTarget = null;
function openResetPwd(id) {
const m = MEMBERS.find(x => x.id === id);
if (!m) return;
if (!canResetPassword(m)) {
Shell.toast('无权重置该账户密码', '当前角色权限不足');
return;
}
_resetPwdTarget = m;
document.getElementById('reset-pwd-name').textContent = m.name;
document.getElementById('reset-pwd-input').value = genPassword(); // 复用创建账户的生成器
Shell.openModal('reset-pwd-bg');
}
document.getElementById('reset-pwd-gen').addEventListener('click', () => {
document.getElementById('reset-pwd-input').value = genPassword();
});
document.getElementById('reset-pwd-confirm').addEventListener('click', () => {
if (!_resetPwdTarget) return;
const pwd = document.getElementById('reset-pwd-input').value.trim();
if (!pwd) { Shell.toast('请填写新密码', '或点刷新生成'); return; }
if (pwd.length < 8 || !/[A-Za-z]/.test(pwd) || !/\d/.test(pwd)) {
Shell.toast('密码强度不够', '至少 8 位 · 含字母与数字');
return;
}
const m = _resetPwdTarget;
Shell.closeModal('reset-pwd-bg');
// 复用「分享凭据」弹窗展示新密码,让管理员可一键复制给成员
document.getElementById('share-username').textContent = m.username || m.email || m.name;
document.getElementById('share-password').textContent = pwd;
// 标题语境从「账户已创建」临时改为「密码已重置」
const shareTi = document.querySelector('#invite-share-bg .ti');
if (shareTi) shareTi.innerHTML = '密码已重置<span>// 把新凭据分享给 ' + m.name + ' · 旧密码已失效</span>';
Shell.openModal('invite-share-bg');
Shell.toast('密码已重置', m.name + ' · 请把新密码同步给成员');
_resetPwdTarget = null;
});
// 分享凭据弹窗关闭后,把标题复位到「账户已创建」的默认文案,
// 这样下次「创建账户」流程打开时不会残留「密码已重置」的语境
(function () {
const bg = document.getElementById('invite-share-bg');
const DEFAULT_TI = '账户已创建<span>// 把下方凭据分享给成员 · 凭据仅展示一次</span>';
const obs = new MutationObserver(() => {
if (!bg.classList.contains('show')) {
const ti = bg.querySelector('.ti');
if (ti) ti.innerHTML = DEFAULT_TI;
}
});
obs.observe(bg, { attributes: true, attributeFilter: ['class'] });
})();
document.getElementById('edit-save').addEventListener('click', () => {
const m = MEMBERS.find(x => x.id === editingId);
if (!m) return;
const parseQuota = (v, fallback) => {
if (v === '' || v == null) return fallback;
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
};
// 如果允许改角色,读取新值
if (canEditRole(m)) {
const newRole = document.querySelector('#edit-role-choices .role-choice.selected')?.dataset.editRole;
if (newRole && newRole !== m.role) {
m.role = newRole;
}
}
m.daily = parseQuota(document.getElementById('edit-daily').value, m.daily);
m.monthly = parseQuota(document.getElementById('edit-monthly').value, m.monthly);
m.totalQuota = parseQuota(document.getElementById('edit-total').value, m.totalQuota != null ? m.totalQuota : -1);
Shell.closeModal('edit-member-bg');
Shell.toast('已保存', m.name + ' · 已更新');
renderMembers(document.getElementById('member-search').value);
});
/* ─── 设置月限额 modal ─── */
const TEAM_BUDGET = { limit: 3000, used: 162.60 };
function fmtMoneyComma(n) {
if (n < 0) return '不限';
return '¥' + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function refreshLimitInfo() {
const inputEl = document.getElementById('limit-input');
const raw = inputEl.value.trim();
const v = raw === '' ? TEAM_BUDGET.limit : Number(raw);
const left = v < 0 ? null : v - TEAM_BUDGET.used;
document.getElementById('limit-info-used').textContent = fmtMoneyComma(TEAM_BUDGET.used);
const leftEl = document.getElementById('limit-info-left');
if (left == null) {
leftEl.textContent = '不限';
leftEl.classList.remove('neg');
} else {
leftEl.textContent = fmtMoneyComma(left);
leftEl.classList.toggle('neg', left < 0);
}
document.querySelectorAll('#limit-bg .limit-presets .lp').forEach(b => {
b.classList.toggle('selected', Number(b.dataset.v) === v);
});
}
function applyBudgetToBanner() {
document.getElementById('stat-limit').textContent = TEAM_BUDGET.limit < 0 ? '不限' : fmtMoneyComma(TEAM_BUDGET.limit);
document.getElementById('stat-used').textContent = fmtMoneyComma(TEAM_BUDGET.used);
const left = TEAM_BUDGET.limit < 0 ? null : TEAM_BUDGET.limit - TEAM_BUDGET.used;
const leftEl = document.getElementById('stat-left');
if (left == null) {
leftEl.textContent = '不限';
leftEl.classList.remove('warn');
} else {
leftEl.textContent = fmtMoneyComma(left);
leftEl.classList.toggle('warn', left / TEAM_BUDGET.limit < 0.2);
}
// 占比 + 推算项目数
const pct = TEAM_BUDGET.limit > 0 ? (TEAM_BUDGET.used / TEAM_BUDGET.limit * 100).toFixed(1) : 0;
document.getElementById('stat-used-sub').textContent = TEAM_BUDGET.limit < 0 ? '// 不限' : `// 占月限 ${pct}%`;
document.getElementById('stat-left-sub').textContent = TEAM_BUDGET.limit < 0
? '// 月限额不限'
: (left != null ? `// 还可生成约 ${Math.max(0, Math.round(left / 10))} 个项目` : '');
}
document.getElementById('open-limit').addEventListener('click', () => {
document.getElementById('limit-input').value = TEAM_BUDGET.limit;
refreshLimitInfo();
Shell.openModal('limit-bg');
});
document.getElementById('limit-input').addEventListener('input', refreshLimitInfo);
document.querySelectorAll('#limit-bg .limit-presets .lp').forEach(b => {
b.addEventListener('click', () => {
document.getElementById('limit-input').value = b.dataset.v;
refreshLimitInfo();
});
});
document.getElementById('limit-save').addEventListener('click', () => {
const raw = document.getElementById('limit-input').value.trim();
if (raw === '') { Shell.toast('请填写月限额', '或选择「不限」'); return; }
const v = Number(raw);
if (!Number.isFinite(v)) { Shell.toast('数值不合法', '请输入数字'); return; }
if (v >= 0 && v < TEAM_BUDGET.used) {
if (!confirm(`新月限额 ${fmtMoneyComma(v)} 小于当月已用 ${fmtMoneyComma(TEAM_BUDGET.used)},仍要保存?`)) return;
}
TEAM_BUDGET.limit = v;
applyBudgetToBanner();
Shell.closeModal('limit-bg');
Shell.toast('月限额已更新', v < 0 ? '不限' : fmtMoneyComma(v));
});
/* ─── 团队动态 · 全部 → modal ─── */
const FEED_ALL = [
{ who: '张运营', av: '张', cat: 'project', act: '完成视频', obj: '补水面膜 · v3', ts: '10 分钟前' },
{ who: '王小姐', av: '王', cat: 'asset', act: '上传到资产库', obj: '林夕 · 主播图', ts: '28 分钟前' },
{ who: '小李', av: '李', cat: 'team', act: '邀请新成员', obj: '林新人', ts: '2 小时前' },
{ who: '陈策划', av: '陈', cat: 'project', act: '创建项目', obj: '蓝牙耳机 · 开箱测评', ts: '4 小时前' },
{ who: '张运营', av: '张', cat: 'project', act: '采用故事板', obj: '补水面膜 · 场 3 · v2', ts: '昨天 18:32' },
{ who: '小李', av: '李', cat: 'team', act: '充值', obj: '¥500.00', ts: '昨天 16:08' },
{ who: '陈策划', av: '陈', cat: 'asset', act: '加入资产库', obj: '阿强 · 健身男', ts: '昨天 14:12' },
{ who: '王小姐', av: '王', cat: 'project', act: '完成视频', obj: '瑜伽裤 · 通勤穿搭', ts: '昨天 11:38' },
{ who: '张运营', av: '张', cat: 'asset', act: '上传商品图', obj: '南卡 Lite Pro 蓝牙耳机', ts: '2 天前' },
{ who: '小李', av: '李', cat: 'team', act: '调整月限额', obj: '¥3,000', ts: '2 天前' },
{ who: '陈策划', av: '陈', cat: 'project', act: '取消项目', obj: '速食面 · 加班场景 · v1', ts: '2 天前' },
{ who: '王小姐', av: '王', cat: 'asset', act: '删除资产', obj: '透真防晒 · 旧版主图', ts: '2 天前' },
];
let _feedFilter = 'all';
function renderFeedAll() {
const list = _feedFilter === 'all' ? FEED_ALL : FEED_ALL.filter(f => f.cat === _feedFilter);
const root = document.getElementById('feed-all-list');
root.innerHTML = list.length === 0
? '<div class="feed-empty">// 没有匹配的动态</div>'
: list.map(f => `
<div class="feed-item">
<div class="av">${f.av}</div>
<div class="txt"><span class="who">${f.who}</span><span class="act">${f.act}</span><span class="obj">${f.obj}</span></div>
<div class="ts">${f.ts}</div>
</div>
`).join('');
document.getElementById('feed-all-meta').textContent = `// 共 ${list.length}`;
document.getElementById('feed-all-count').textContent = `// 共 ${list.length}`;
}
document.getElementById('open-feed-all').addEventListener('click', () => {
_feedFilter = 'all';
document.querySelectorAll('#feed-all-bg .fa-chip').forEach(c => c.classList.toggle('selected', c.dataset.filter === 'all'));
renderFeedAll();
Shell.openModal('feed-all-bg');
});
document.querySelectorAll('#feed-all-bg .fa-chip').forEach(chip => {
chip.addEventListener('click', () => {
_feedFilter = chip.dataset.filter;
document.querySelectorAll('#feed-all-bg .fa-chip').forEach(c => c.classList.toggle('selected', c === chip));
renderFeedAll();
});
});
/* ─── 成员行点击 → 打开编辑(创建者除外)─── */
document.getElementById('members-tbody').addEventListener('click', e => {
// 行内按钮已 stopPropagation,只剩单元格 / 头像 / 文本会冒泡上来
if (e.target.closest('button')) return;
const tr = e.target.closest('tr[data-id]');
if (!tr) return;
const m = MEMBERS.find(x => x.id === tr.dataset.id);
if (!m || m.creator) return;
openEdit(tr.dataset.id);
});
// 行 hover 提示可点击
const styleHint = document.createElement('style');
styleHint.textContent = `
#members-tbody tr[data-id]:not(.creator-row) { cursor: pointer; }
#members-tbody tr[data-id]:not(.creator-row):hover { background: var(--background-lighter); }
`;
document.head.appendChild(styleHint);
</script>
</body>
</html>