1356 lines
72 KiB
HTML
1356 lines
72 KiB
HTML
<!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: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-muted); border-radius: var(--r-md); overflow: hidden; font-size: 12.5px; }
|
||
.perm-table th, .perm-table td { padding: 8px 10px; border-bottom: 0; }
|
||
.perm-table th { border-bottom: 1px solid var(--border-muted); background: var(--background-lighter); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; 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('用户名格式不正确', '3–32 位 · 字母/数字/. _ -');
|
||
$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>
|