All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
772 lines
34 KiB
HTML
772 lines
34 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>
|
|
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
#page { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
|
|
|
/* ─── 页头 ─── */
|
|
.page-head { margin-bottom: 18px; }
|
|
.page-head h1 .ct { font-family: var(--font-mono); font-size: 12px; color: var(--black-alpha-48); margin-left: 8px; font-weight: 400; letter-spacing: .04em; }
|
|
|
|
/* ─── 顶部工具栏:tabs + 搜索 + 操作 ─── */
|
|
.ms-toolbar {
|
|
display: flex; align-items: center; gap: 14px;
|
|
padding: 10px 14px;
|
|
background: var(--surface); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
margin-bottom: 14px; flex-wrap: wrap;
|
|
}
|
|
.ms-tabs { display: inline-flex; gap: 4px; }
|
|
.ms-tab {
|
|
height: 30px; padding: 0 14px;
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: transparent; border: 1px solid transparent;
|
|
border-radius: var(--r-pill);
|
|
font-family: inherit; font-size: 12.5px; color: var(--black-alpha-72);
|
|
cursor: pointer;
|
|
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
|
}
|
|
.ms-tab:hover { color: var(--accent-black); background: var(--background-lighter); }
|
|
.ms-tab.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
|
|
.ms-tab .ct {
|
|
font-family: var(--font-mono); font-size: 10.5px;
|
|
background: var(--black-alpha-7); color: var(--black-alpha-72);
|
|
padding: 1px 6px; border-radius: var(--r-pill);
|
|
min-width: 18px; text-align: center;
|
|
}
|
|
.ms-tab.active .ct { background: var(--heat); color: var(--accent-white); }
|
|
|
|
.ms-search {
|
|
flex: 1; min-width: 200px; max-width: 320px;
|
|
position: relative; display: inline-flex; align-items: center;
|
|
}
|
|
.ms-search svg {
|
|
position: absolute; left: 10px; width: 13px; height: 13px;
|
|
color: var(--black-alpha-40); pointer-events: none;
|
|
}
|
|
.ms-search input {
|
|
width: 100%; height: 32px; padding: 0 12px 0 30px;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-sm); font-family: inherit; font-size: 12.5px;
|
|
color: var(--accent-black); outline: none;
|
|
}
|
|
.ms-search input:focus { border-color: var(--heat-40); background: var(--surface); }
|
|
|
|
.ms-toggle {
|
|
height: 30px; padding: 0 12px;
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-pill);
|
|
font-family: inherit; font-size: 12px; color: var(--black-alpha-72);
|
|
cursor: pointer; transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
|
}
|
|
.ms-toggle:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
|
|
.ms-toggle.on { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); font-weight: 600; }
|
|
.ms-toggle .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
|
|
.ms-actions { margin-left: auto; display: inline-flex; gap: 8px; }
|
|
.ms-actions .btn { height: 30px; padding: 0 12px; font-size: 12px; }
|
|
|
|
/* ─── 主体:左侧列表 + 右侧详情 ─── */
|
|
.ms-body {
|
|
flex: 1; min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(340px, 420px) 1fr;
|
|
gap: 14px;
|
|
}
|
|
@media (max-width: 980px) { .ms-body { grid-template-columns: 1fr; } }
|
|
|
|
/* 左:列表卡片 */
|
|
.ms-list-pane {
|
|
background: var(--surface); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
display: flex; flex-direction: column;
|
|
overflow: hidden; min-height: 0;
|
|
}
|
|
.ms-list-h {
|
|
padding: 12px 16px; border-bottom: 1px solid var(--border-faint);
|
|
display: flex; align-items: center; gap: 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
.ms-list-h .mono { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
|
.ms-list-h .group-toggle {
|
|
margin-left: auto;
|
|
background: transparent; border: 0; cursor: pointer;
|
|
font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56);
|
|
letter-spacing: .04em; padding: 2px 6px; border-radius: var(--r-sm);
|
|
}
|
|
.ms-list-h .group-toggle:hover { background: var(--background-lighter); color: var(--accent-black); }
|
|
|
|
.ms-list { flex: 1; min-height: 0; overflow-y: auto; }
|
|
.ms-group-h {
|
|
padding: 8px 16px 6px;
|
|
font-family: var(--font-mono); font-size: 10.5px;
|
|
color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase;
|
|
background: var(--background-base);
|
|
position: sticky; top: 0; z-index: 1;
|
|
}
|
|
.ms-item {
|
|
position: relative;
|
|
display: grid; grid-template-columns: 28px 1fr; gap: 10px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
cursor: pointer;
|
|
transition: background var(--t-base);
|
|
}
|
|
.ms-item:hover { background: var(--background-lighter); }
|
|
.ms-item.active { background: var(--heat-12); }
|
|
.ms-item.active::before {
|
|
content: ''; position: absolute; left: 0; top: 0; bottom: 0;
|
|
width: 3px; background: var(--heat);
|
|
}
|
|
.ms-item.read .ms-item-title { color: var(--black-alpha-56); font-weight: 400; }
|
|
.ms-item.read .ms-item-icon { opacity: .55; }
|
|
|
|
.ms-item-icon {
|
|
width: 28px; height: 28px;
|
|
display: grid; place-items: center;
|
|
border-radius: var(--r-sm);
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
color: var(--black-alpha-72);
|
|
flex-shrink: 0;
|
|
}
|
|
.ms-item-icon svg { width: 14px; height: 14px; }
|
|
.ms-item-icon.task { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
|
|
.ms-item-icon.team { background: rgba(76,134,255,.10); border-color: rgba(76,134,255,.24); color: #3a6dd1; }
|
|
.ms-item-icon.system { background: var(--black-alpha-7); border-color: var(--border-faint); color: var(--black-alpha-72); }
|
|
.ms-item-icon.billing { background: rgba(241,176,29,.12); border-color: rgba(241,176,29,.28); color: #b5860d; }
|
|
.ms-item-icon.success { background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest); }
|
|
.ms-item-icon.error { background: var(--crimson-bg); border-color: var(--crimson-bd); color: var(--accent-crimson); }
|
|
|
|
.ms-item-main { min-width: 0; }
|
|
.ms-item-row1 { display: flex; align-items: center; gap: 8px; }
|
|
.ms-item-title { font-size: 13px; font-weight: 600; color: var(--accent-black); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.ms-item-ts { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; flex-shrink: 0; }
|
|
.ms-item-unread {
|
|
width: 7px; height: 7px; border-radius: 50%;
|
|
background: var(--heat); flex-shrink: 0;
|
|
}
|
|
.ms-item.read .ms-item-unread { display: none; }
|
|
.ms-item-body {
|
|
font-size: 12px; color: var(--black-alpha-56);
|
|
line-height: 1.5; margin-top: 3px;
|
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.ms-item-tag {
|
|
display: inline-block; margin-top: 6px;
|
|
padding: 1px 6px;
|
|
font-family: var(--font-mono); font-size: 10px; letter-spacing: .02em;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-sm); color: var(--black-alpha-56);
|
|
}
|
|
.ms-item-tag.crit { background: var(--crimson-bg); border-color: var(--crimson-bd); color: var(--accent-crimson); }
|
|
.ms-item-tag.warn { background: rgba(241,176,29,.14); border-color: rgba(241,176,29,.32); color: #b5860d; }
|
|
.ms-item-tag.ok { background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest); }
|
|
.ms-item-tag.info { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
|
|
|
|
.ms-list-empty {
|
|
padding: 60px 24px;
|
|
display: flex; flex-direction: column; align-items: center; gap: 10px;
|
|
color: var(--black-alpha-48); font-size: 13px;
|
|
}
|
|
.ms-list-empty .ic {
|
|
width: 44px; height: 44px;
|
|
display: grid; place-items: center;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: 50%; color: var(--black-alpha-40);
|
|
}
|
|
.ms-list-empty .mono { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; }
|
|
|
|
/* 右:详情 pane */
|
|
.ms-detail-pane {
|
|
background: var(--surface); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
display: flex; flex-direction: column;
|
|
overflow: hidden; min-height: 0;
|
|
}
|
|
.ms-detail-empty {
|
|
flex: 1; display: grid; place-items: center;
|
|
color: var(--black-alpha-40); font-size: 13px;
|
|
flex-direction: column; gap: 8px;
|
|
}
|
|
.ms-detail-empty .ic {
|
|
width: 48px; height: 48px;
|
|
display: grid; place-items: center;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md); color: var(--black-alpha-40);
|
|
}
|
|
.ms-detail-empty .ic svg { width: 22px; height: 22px; }
|
|
.ms-detail-empty .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
|
|
|
.ms-detail-h {
|
|
padding: 18px 24px 14px;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
flex-shrink: 0;
|
|
}
|
|
.ms-detail-h .row1 { display: flex; align-items: center; gap: 10px; }
|
|
.ms-detail-h .kind-tag {
|
|
font-family: var(--font-mono); font-size: 10.5px;
|
|
padding: 2px 8px; border-radius: var(--r-sm);
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase;
|
|
}
|
|
.ms-detail-h .kind-tag.task { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
|
|
.ms-detail-h .kind-tag.team { background: rgba(76,134,255,.10); color: #3a6dd1; border-color: rgba(76,134,255,.24); }
|
|
.ms-detail-h .kind-tag.system { background: var(--black-alpha-7); color: var(--black-alpha-72); }
|
|
.ms-detail-h .kind-tag.billing { background: rgba(241,176,29,.12); color: #b5860d; border-color: rgba(241,176,29,.28); }
|
|
.ms-detail-h .x {
|
|
margin-left: auto; 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;
|
|
}
|
|
.ms-detail-h .x:hover { background: var(--background-lighter); color: var(--accent-black); }
|
|
.ms-detail-h .x svg { width: 14px; height: 14px; }
|
|
.ms-detail-h h2 { font-size: 18px; font-weight: 600; letter-spacing: -.012em; color: var(--accent-black); margin: 12px 0 6px; }
|
|
.ms-detail-h .meta { display: flex; align-items: center; gap: 14px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
|
.ms-detail-h .meta .from { color: var(--accent-black); font-weight: 600; }
|
|
.ms-detail-h .meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--black-alpha-24); }
|
|
|
|
.ms-detail-b {
|
|
flex: 1; min-height: 0; overflow-y: auto;
|
|
padding: 18px 24px 20px;
|
|
}
|
|
.ms-detail-b p { font-size: 13.5px; line-height: 1.7; color: var(--accent-black); margin: 0 0 12px; }
|
|
.ms-detail-b .quote {
|
|
border-left: 3px solid var(--heat);
|
|
padding: 8px 14px;
|
|
background: var(--heat-12);
|
|
border-radius: 0 var(--r-sm) var(--r-sm) 0;
|
|
font-size: 13px; line-height: 1.65; color: var(--accent-black);
|
|
margin: 12px 0;
|
|
}
|
|
|
|
/* 详情属性表 */
|
|
.ms-props {
|
|
display: grid; grid-template-columns: 110px 1fr;
|
|
row-gap: 10px; column-gap: 16px;
|
|
margin: 14px 0 6px;
|
|
padding: 12px 16px;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-sm);
|
|
}
|
|
.ms-props .k {
|
|
font-family: var(--font-mono); font-size: 11px;
|
|
color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase;
|
|
}
|
|
.ms-props .v { font-size: 12.5px; color: var(--accent-black); word-break: break-all; }
|
|
.ms-props .v.mono { font-family: var(--font-mono); }
|
|
.ms-props .v a { color: var(--heat); }
|
|
.ms-props .v a:hover { text-decoration: underline; }
|
|
|
|
.ms-detail-f {
|
|
padding: 12px 20px;
|
|
border-top: 1px solid var(--border-faint);
|
|
background: var(--background-lighter);
|
|
display: flex; align-items: center; gap: 8px;
|
|
flex-shrink: 0; flex-wrap: wrap;
|
|
}
|
|
.ms-detail-f .spacer { flex: 1; }
|
|
.ms-detail-f .btn { height: 32px; padding: 0 14px; font-size: 12.5px; }
|
|
|
|
/* 设置入口卡(底部) */
|
|
.ms-foot-hint {
|
|
margin-top: 14px;
|
|
padding: 10px 14px;
|
|
background: var(--background-lighter); border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
display: flex; align-items: center; gap: 10px;
|
|
font-size: 12px; color: var(--black-alpha-56);
|
|
}
|
|
.ms-foot-hint svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
|
|
.ms-foot-hint a { color: var(--heat); margin-left: auto; font-weight: 600; }
|
|
.ms-foot-hint a:hover { text-decoration: underline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="page">
|
|
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>消息中心<span class="ct" id="ms-head-ct">// 0 条未读 · 0 条总计</span></h1>
|
|
<div class="sub"><span class="mono">// 任务 · 团队 · 系统 · 计费 四类事件流</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 顶部工具栏 -->
|
|
<div class="ms-toolbar">
|
|
<div class="ms-tabs" id="ms-tabs">
|
|
<button class="ms-tab active" type="button" data-tab="all">全部 <span class="ct" data-ct="all">0</span></button>
|
|
<button class="ms-tab" type="button" data-tab="task">任务 <span class="ct" data-ct="task">0</span></button>
|
|
<button class="ms-tab" type="button" data-tab="team">团队 <span class="ct" data-ct="team">0</span></button>
|
|
<button class="ms-tab" type="button" data-tab="system">系统 <span class="ct" data-ct="system">0</span></button>
|
|
<button class="ms-tab" type="button" data-tab="billing">计费 <span class="ct" data-ct="billing">0</span></button>
|
|
</div>
|
|
<button class="ms-toggle" type="button" id="ms-only-unread"><span class="dot"></span>仅看未读</button>
|
|
<div class="ms-search">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
<input type="text" id="ms-search-input" placeholder="搜索消息内容 / 来源 / 项目…">
|
|
</div>
|
|
<div class="ms-actions">
|
|
<button class="btn" type="button" id="ms-mark-all">全部标已读</button>
|
|
<button class="btn btn-ghost" type="button" id="ms-settings">通知设置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 列表 + 详情 -->
|
|
<div class="ms-body">
|
|
<!-- 左 · 列表 -->
|
|
<div class="ms-list-pane">
|
|
<div class="ms-list-h">
|
|
<span class="mono" id="ms-list-ct">// 显示 0 条</span>
|
|
<button class="group-toggle" type="button" id="ms-group-toggle">按时间分组 ▾</button>
|
|
</div>
|
|
<div class="ms-list" id="ms-list"></div>
|
|
</div>
|
|
|
|
<!-- 右 · 详情 -->
|
|
<div class="ms-detail-pane" id="ms-detail-pane">
|
|
<div class="ms-detail-empty" id="ms-detail-empty">
|
|
<div class="ic">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
|
</div>
|
|
<div>从左侧选择一条消息查看详情</div>
|
|
<div class="mono">// 点击列表项 · 自动标为已读</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ms-foot-hint">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
|
消息默认保留 90 天,任务类消息可在「通知设置」中按类别静音或转发到团队邮箱。
|
|
<a href="settings.html">前往设置 →</a>
|
|
</div>
|
|
|
|
</div>
|
|
<script src="assets/icons.js?v=2026052608"></script>
|
|
<script src="assets/shell.js?v=2026052607"></script>
|
|
<script>
|
|
Shell.render({ active: '', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消息中心' }] });
|
|
|
|
/* ─── Mock 数据 · 12 条,贴合 PRD 与项目当前内容 ─── */
|
|
const NOW = new Date('2026-05-25T15:18:00');
|
|
function _ago(min) {
|
|
const d = new Date(NOW.getTime() - min * 60000);
|
|
return d;
|
|
}
|
|
function _fmt(d) {
|
|
const today = new Date(NOW.getFullYear(), NOW.getMonth(), NOW.getDate());
|
|
const dDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
const diff = (today - dDay) / (24 * 3600 * 1000);
|
|
const hh = String(d.getHours()).padStart(2, '0'), mm = String(d.getMinutes()).padStart(2, '0');
|
|
if (diff === 0) return '今天 ' + hh + ':' + mm;
|
|
if (diff === 1) return '昨天 ' + hh + ':' + mm;
|
|
if (diff < 7) return diff + ' 天前';
|
|
return (d.getMonth() + 1) + '.' + d.getDate() + ' ' + hh + ':' + mm;
|
|
}
|
|
function _fmtFull(d) {
|
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
|
+ ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
}
|
|
|
|
const icon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';
|
|
const ICONS = {
|
|
taskOk: icon('check'),
|
|
taskFail: icon('x'),
|
|
taskRun: icon('clock'),
|
|
team: icon('users'),
|
|
comment: icon('messageCircle'),
|
|
system: icon('info'),
|
|
billing: icon('wallet'),
|
|
shield: icon('shieldCheck'),
|
|
pipeline: icon('film'),
|
|
};
|
|
|
|
const MESSAGES = [
|
|
// ─ 任务类(5) ─
|
|
{
|
|
id: 'm-001', type: 'task', icon: 'taskOk', severity: 'ok',
|
|
title: '视频已完成 · 补水面膜 · 痛点种草 v3',
|
|
body: '7 镜 · 40 秒成片渲染完成,余额已扣除 ¥18.40。点 [✓ 通过] 进入投放阶段,或 [↻ 重跑] 调整脚本。',
|
|
ts: _ago(8), from: '系统', tag: '通过', tagKind: 'ok',
|
|
project: '补水面膜', stage: 'Stage 5 · 投放',
|
|
target: { type: 'project', href: 'pipeline.html?product=' + encodeURIComponent('补水面膜'), name: '补水面膜 · 痛点种草' },
|
|
actions: [
|
|
{ label: '进入项目', primary: true, action: 'goto' },
|
|
{ label: '查看成片', action: 'preview' },
|
|
{ label: '再次生成', action: 'rerun' },
|
|
],
|
|
unread: true,
|
|
},
|
|
{
|
|
id: 'm-002', type: 'task', icon: 'taskOk', severity: 'ok',
|
|
title: '4 张模特上身图已生成 · 祛痘精华',
|
|
body: '阿楠 · 通勤白领 · 室内自然光 · 3:4。预估 ¥3.20,可在「资产库 / 我的上传」直接调用或加入新项目。',
|
|
ts: _ago(42), from: '系统', tag: '通过', tagKind: 'ok',
|
|
project: '祛痘精华', stage: '图片生成 · 模特上身图',
|
|
target: { type: 'asset-factory', href: 'asset-factory.html', name: '图片生成 · 模特上身图' },
|
|
actions: [
|
|
{ label: '查看素材', primary: true, action: 'goto' },
|
|
{ label: '加入项目', action: 'attach' },
|
|
],
|
|
unread: true,
|
|
},
|
|
{
|
|
id: 'm-003', type: 'task', icon: 'taskOk', severity: 'ok',
|
|
title: '演员「林夕」三视图已就绪',
|
|
body: '正/侧/背 三视图生成完成,共 ¥0.30。该演员现已具备多角度一致性,可放心进入视频生成。',
|
|
ts: _ago(95), from: '系统', tag: '资产更新', tagKind: 'info',
|
|
project: '资产库', stage: '演员库 · 我的上传',
|
|
target: { type: 'library', href: 'library.html#person', name: '资产库 / 人物 / 林夕' },
|
|
actions: [
|
|
{ label: '查看演员', primary: true, action: 'goto' },
|
|
{ label: '在项目中使用', action: 'attach' },
|
|
],
|
|
unread: true,
|
|
},
|
|
{
|
|
id: 'm-004', type: 'task', icon: 'taskFail', severity: 'err',
|
|
title: '脚本生成失败 · 618 大促',
|
|
body: '上下文超长(prompt > 8k tokens),AI 脚本服务返回 413。系统已自动截断 30% 内容并重试,本次失败不扣费。',
|
|
ts: _ago(180), from: '系统', tag: '失败 · 免费重试', tagKind: 'crit',
|
|
project: '618 大促', stage: 'Stage 1 · 脚本',
|
|
target: { type: 'project', href: 'pipeline.html?product=' + encodeURIComponent('618 大促'), name: '618 大促 · Stage 1' },
|
|
actions: [
|
|
{ label: '重试生成', primary: true, action: 'rerun' },
|
|
{ label: '查看错误日志', action: 'log' },
|
|
],
|
|
unread: true,
|
|
},
|
|
{
|
|
id: 'm-005', type: 'task', icon: 'taskRun', severity: 'warn',
|
|
title: '图片优化任务已超时,自动重跑',
|
|
body: '原任务 12:45 提交,> 10 分钟未完成。已按 PRD §10.3 自动重跑,如再次失败将退还本次预估额度。',
|
|
ts: _ago(220), from: '系统', tag: '超时 · 已重跑', tagKind: 'warn',
|
|
project: '资产库', stage: '图片优化 · 队列',
|
|
target: { type: 'queue', href: 'index.html', name: '工作台 · 任务队列' },
|
|
actions: [
|
|
{ label: '查看队列', primary: true, action: 'goto' },
|
|
],
|
|
unread: true,
|
|
},
|
|
|
|
// ─ 团队类(4) ─
|
|
{
|
|
id: 'm-006', type: 'team', icon: 'team',
|
|
title: '@王芳 已加入团队 · 角色 运营',
|
|
body: '由超管 李 邀请。月限额 ¥800 · 资产可读可写。如需调整权限请前往团队设置。',
|
|
ts: _ago(265), from: '@李(超管)', tag: '团队', tagKind: 'info',
|
|
project: '团队', stage: '成员管理',
|
|
target: { type: 'team', href: 'team.html', name: '团队 / 成员' },
|
|
actions: [
|
|
{ label: '查看团队', primary: true, action: 'goto' },
|
|
{ label: '调整权限', action: 'edit' },
|
|
],
|
|
unread: true,
|
|
},
|
|
{
|
|
id: 'm-007', type: 'team', icon: 'team',
|
|
title: '@张磊 由「运营」升级为「主管」',
|
|
body: '权限范围:可管理本人 + 下属团员的项目、资产、额度。变更生效于今天 13:22。',
|
|
ts: _ago(310), from: '@李(超管)', tag: '权限变更', tagKind: 'info',
|
|
project: '团队', stage: '权限管理',
|
|
target: { type: 'team', href: 'team.html', name: '团队 / 角色' },
|
|
actions: [
|
|
{ label: '查看团队', primary: true, action: 'goto' },
|
|
],
|
|
unread: false,
|
|
},
|
|
{
|
|
id: 'm-008', type: 'team', icon: 'team',
|
|
title: '@李 创建了模特「小七 · 学生女」',
|
|
body: '团队资产已同步 · 你可立即在「资产库 / 人物」中调用,免重复生成。',
|
|
ts: _ago(420), from: '@李', tag: '资产共用', tagKind: 'ok',
|
|
project: '资产库', stage: '人物 · 我的上传',
|
|
target: { type: 'library', href: 'library.html#person', name: '资产库 / 人物 / 小七' },
|
|
actions: [
|
|
{ label: '查看资产', primary: true, action: 'goto' },
|
|
],
|
|
unread: false,
|
|
},
|
|
{
|
|
id: 'm-009', type: 'team', icon: 'comment',
|
|
title: '@刘 在「补水面膜」镜头 3 留下了评论',
|
|
body: '"开头节奏可以更紧凑,前 2 秒建议直接进痛点。可以让 AI 把镜头 1 缩到 3 秒吗?"',
|
|
ts: _ago(540), from: '@刘(运营)', tag: '评论', tagKind: 'info',
|
|
project: '补水面膜', stage: 'Stage 3 · 镜头',
|
|
target: { type: 'project', href: 'pipeline.html?product=' + encodeURIComponent('补水面膜') + '#sh3', name: '补水面膜 / 镜头 3' },
|
|
actions: [
|
|
{ label: '查看镜头', primary: true, action: 'goto' },
|
|
{ label: '回复', action: 'reply' },
|
|
],
|
|
unread: true,
|
|
},
|
|
|
|
// ─ 系统/计费类(3) ─
|
|
{
|
|
id: 'm-010', type: 'billing', icon: 'billing', severity: 'warn',
|
|
title: '团队余额低于 ¥100',
|
|
body: '当前余额 ¥87.20,按本周消耗速度估算可支撑 4-5 个视频项目。建议尽早充值以免任务中断。',
|
|
ts: _ago(660), from: '系统', tag: '余额预警', tagKind: 'warn',
|
|
project: '计费', stage: '余额监控',
|
|
target: { type: 'account', href: 'account.html', name: '消费 / 充值' },
|
|
actions: [
|
|
{ label: '前往充值', primary: true, action: 'goto' },
|
|
{ label: '设置预警阈值', action: 'edit' },
|
|
],
|
|
unread: false,
|
|
},
|
|
{
|
|
id: 'm-011', type: 'system', icon: 'system',
|
|
title: '5/25 23:00 - 5/26 01:00 · 视频生成服务例行维护',
|
|
body: '本次维护涉及视频生成 + 三视图生成两项服务,期间提交的任务会自动延迟到维护结束后开始处理,已生成任务不受影响。',
|
|
ts: _ago(720), from: 'Airshelf 运维', tag: '公告', tagKind: 'info',
|
|
project: '系统', stage: '维护公告',
|
|
target: null,
|
|
actions: [
|
|
{ label: '我已了解', primary: true, action: 'ack' },
|
|
],
|
|
unread: false,
|
|
},
|
|
{
|
|
id: 'm-012', type: 'system', icon: 'shield',
|
|
title: '「祛痘精华」首版投放素材已通过平台审核',
|
|
body: '审核耗时 1h 12min,通过 4 张图 / 2 条视频,可直接进入「投放」阶段。',
|
|
ts: _ago(1380), from: '审核中台', tag: '审核通过', tagKind: 'ok',
|
|
project: '祛痘精华', stage: 'Stage 5 · 投放',
|
|
target: { type: 'project', href: 'pipeline.html?product=' + encodeURIComponent('祛痘精华'), name: '祛痘精华 · 投放' },
|
|
actions: [
|
|
{ label: '进入投放', primary: true, action: 'goto' },
|
|
],
|
|
unread: false,
|
|
},
|
|
];
|
|
|
|
/* ─── 状态 + 渲染 ─── */
|
|
const state = { tab: 'all', onlyUnread: false, q: '', selectedId: null };
|
|
|
|
function _filter() {
|
|
return MESSAGES.filter(m => {
|
|
if (state.tab !== 'all' && m.type !== state.tab) return false;
|
|
if (state.onlyUnread && !m.unread) return false;
|
|
if (state.q) {
|
|
const q = state.q.toLowerCase();
|
|
const hay = (m.title + ' ' + m.body + ' ' + (m.project || '') + ' ' + (m.from || '')).toLowerCase();
|
|
if (!hay.includes(q)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function _groupByTs(items) {
|
|
const groups = { '今天': [], '昨天': [], '更早': [] };
|
|
const today = new Date(NOW.getFullYear(), NOW.getMonth(), NOW.getDate());
|
|
items.forEach(m => {
|
|
const dDay = new Date(m.ts.getFullYear(), m.ts.getMonth(), m.ts.getDate());
|
|
const diff = (today - dDay) / (24 * 3600 * 1000);
|
|
if (diff === 0) groups['今天'].push(m);
|
|
else if (diff === 1) groups['昨天'].push(m);
|
|
else groups['更早'].push(m);
|
|
});
|
|
return groups;
|
|
}
|
|
|
|
function _renderTabs() {
|
|
const counts = { all: 0, task: 0, team: 0, system: 0, billing: 0 };
|
|
MESSAGES.forEach(m => {
|
|
if (m.unread) {
|
|
counts.all++;
|
|
if (counts[m.type] !== undefined) counts[m.type]++;
|
|
}
|
|
});
|
|
// 显示总数(不只是未读) — 与 tabs 自身含义一致
|
|
const tot = { all: MESSAGES.length, task: 0, team: 0, system: 0, billing: 0 };
|
|
MESSAGES.forEach(m => { if (tot[m.type] !== undefined) tot[m.type]++; });
|
|
document.querySelectorAll('#ms-tabs .ms-tab').forEach(btn => {
|
|
const k = btn.dataset.tab;
|
|
const ct = btn.querySelector('[data-ct]');
|
|
ct.textContent = tot[k];
|
|
btn.classList.toggle('active', k === state.tab);
|
|
});
|
|
const totalUnread = counts.all;
|
|
document.getElementById('ms-head-ct').textContent = '// ' + totalUnread + ' 条未读 · ' + MESSAGES.length + ' 条总计';
|
|
}
|
|
|
|
function _renderList() {
|
|
const list = _filter();
|
|
const listEl = document.getElementById('ms-list');
|
|
document.getElementById('ms-list-ct').textContent = '// 显示 ' + list.length + ' 条';
|
|
if (!list.length) {
|
|
listEl.innerHTML = `
|
|
<div class="ms-list-empty">
|
|
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.1V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg></div>
|
|
<div>没有符合条件的消息</div>
|
|
<div class="mono">// 试试切换分类 / 关闭「仅看未读」</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
const groups = _groupByTs(list);
|
|
let html = '';
|
|
['今天', '昨天', '更早'].forEach(g => {
|
|
if (!groups[g].length) return;
|
|
html += `<div class="ms-group-h">${g} · ${groups[g].length}</div>`;
|
|
html += groups[g].map(m => _itemHtml(m)).join('');
|
|
});
|
|
listEl.innerHTML = html;
|
|
listEl.querySelectorAll('.ms-item').forEach(el => {
|
|
el.addEventListener('click', () => _select(el.dataset.id));
|
|
});
|
|
}
|
|
|
|
function _itemHtml(m) {
|
|
const iconClass = m.severity === 'err' ? 'error' : (m.severity === 'ok' ? 'success' : m.type);
|
|
return `
|
|
<div class="ms-item ${m.unread ? '' : 'read'} ${state.selectedId === m.id ? 'active' : ''}" data-id="${m.id}">
|
|
<div class="ms-item-icon ${iconClass}">${ICONS[m.icon] || ICONS.system}</div>
|
|
<div class="ms-item-main">
|
|
<div class="ms-item-row1">
|
|
<span class="ms-item-unread"></span>
|
|
<span class="ms-item-title">${m.title}</span>
|
|
<span class="ms-item-ts">${_fmt(m.ts)}</span>
|
|
</div>
|
|
<div class="ms-item-body">${m.body}</div>
|
|
${m.tag ? `<span class="ms-item-tag ${m.tagKind || ''}">${m.tag}</span>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function _select(id) {
|
|
const m = MESSAGES.find(x => x.id === id);
|
|
if (!m) return;
|
|
state.selectedId = id;
|
|
if (m.unread) {
|
|
m.unread = false;
|
|
_renderTabs();
|
|
_renderList();
|
|
} else {
|
|
document.querySelectorAll('#ms-list .ms-item').forEach(el => {
|
|
el.classList.toggle('active', el.dataset.id === id);
|
|
});
|
|
}
|
|
_renderDetail(m);
|
|
}
|
|
|
|
function _renderDetail(m) {
|
|
const pane = document.getElementById('ms-detail-pane');
|
|
const propsHtml = [
|
|
['来源', m.from || '系统'],
|
|
['类别', { task: '任务', team: '团队', system: '系统', billing: '计费' }[m.type] || '系统'],
|
|
['项目', m.project || '-'],
|
|
['阶段', m.stage || '-'],
|
|
['时间', _fmtFull(m.ts)],
|
|
...(m.target ? [['关联资源', `<a href="${m.target.href}">${m.target.name} →</a>`]] : []),
|
|
].map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join('');
|
|
|
|
const actionsHtml = (m.actions || []).map(a => {
|
|
const cls = a.primary ? 'btn btn-primary' : 'btn';
|
|
return `<button class="${cls}" type="button" data-act="${a.action}">${a.label}</button>`;
|
|
}).join('');
|
|
|
|
pane.innerHTML = `
|
|
<div class="ms-detail-h">
|
|
<div class="row1">
|
|
<span class="kind-tag ${m.type}">${ { task: '任务', team: '团队', system: '系统', billing: '计费' }[m.type] || '系统' }</span>
|
|
${m.tag ? `<span class="ms-item-tag ${m.tagKind || ''}" style="margin: 0;">${m.tag}</span>` : ''}
|
|
<button class="x" type="button" id="ms-detail-close" title="关闭"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
|
|
</div>
|
|
<h2>${m.title}</h2>
|
|
<div class="meta">
|
|
<span class="from">${m.from || '系统'}</span>
|
|
<span class="dot"></span>
|
|
<span>${_fmtFull(m.ts)}</span>
|
|
</div>
|
|
</div>
|
|
<div class="ms-detail-b">
|
|
<p>${m.body}</p>
|
|
${m.quote ? `<div class="quote">${m.quote}</div>` : ''}
|
|
<div class="ms-props">${propsHtml}</div>
|
|
</div>
|
|
<div class="ms-detail-f">
|
|
<button class="btn btn-ghost" type="button" id="ms-detail-del">删除</button>
|
|
<button class="btn btn-ghost" type="button" id="ms-detail-mute">不再接收同类</button>
|
|
<span class="spacer"></span>
|
|
${actionsHtml}
|
|
</div>`;
|
|
document.getElementById('ms-detail-close').addEventListener('click', () => {
|
|
state.selectedId = null;
|
|
pane.innerHTML = `
|
|
<div class="ms-detail-empty">
|
|
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg></div>
|
|
<div>从左侧选择一条消息查看详情</div>
|
|
<div class="mono">// 点击列表项 · 自动标为已读</div>
|
|
</div>`;
|
|
_renderList();
|
|
});
|
|
document.getElementById('ms-detail-del').addEventListener('click', () => {
|
|
const i = MESSAGES.findIndex(x => x.id === m.id);
|
|
if (i >= 0) MESSAGES.splice(i, 1);
|
|
state.selectedId = null;
|
|
Shell.toast('已删除', m.title);
|
|
_renderTabs(); _renderList();
|
|
document.getElementById('ms-detail-close').click();
|
|
});
|
|
document.getElementById('ms-detail-mute').addEventListener('click', () => {
|
|
Shell.toast('已静音同类消息', `不再接收「${ { task: '任务', team: '团队', system: '系统', billing: '计费' }[m.type] }」类提醒 · 可在通知设置中恢复`);
|
|
});
|
|
pane.querySelectorAll('.ms-detail-f .btn-primary, .ms-detail-f [data-act]').forEach(btn => {
|
|
btn.addEventListener('click', e => {
|
|
const act = btn.dataset.act;
|
|
if (act === 'goto' && m.target?.href) { location.href = m.target.href; return; }
|
|
if (act === 'rerun') { Shell.toast('已发起重试', m.project + ' · 任务已重新排队'); return; }
|
|
if (act === 'preview') { Shell.toast('打开预览', m.project + ' · 视频成片'); return; }
|
|
if (act === 'attach') { Shell.toast('已加入项目', '请到工作台选择目标项目'); return; }
|
|
if (act === 'log') { Shell.toast('错误日志', 'prompt=tokens 8124 / max=8000 · type=413'); return; }
|
|
if (act === 'reply') { Shell.toast('打开评论', '可在镜头详情页直接回复'); return; }
|
|
if (act === 'ack') { Shell.toast('已确认'); return; }
|
|
if (act === 'edit') { Shell.toast('打开设置', '权限 / 阈值 · 可在设置中调整'); return; }
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ─── 事件绑定 ─── */
|
|
document.querySelectorAll('#ms-tabs .ms-tab').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.tab = btn.dataset.tab;
|
|
_renderTabs(); _renderList();
|
|
});
|
|
});
|
|
const ouBtn = document.getElementById('ms-only-unread');
|
|
ouBtn.addEventListener('click', () => {
|
|
state.onlyUnread = !state.onlyUnread;
|
|
ouBtn.classList.toggle('on', state.onlyUnread);
|
|
_renderList();
|
|
});
|
|
document.getElementById('ms-search-input').addEventListener('input', e => {
|
|
state.q = e.target.value.trim();
|
|
_renderList();
|
|
});
|
|
document.getElementById('ms-mark-all').addEventListener('click', () => {
|
|
MESSAGES.forEach(m => { m.unread = false; });
|
|
Shell.toast('已全部标为已读', MESSAGES.length + ' 条');
|
|
_renderTabs(); _renderList();
|
|
});
|
|
document.getElementById('ms-settings').addEventListener('click', () => {
|
|
location.href = 'settings.html';
|
|
});
|
|
document.getElementById('ms-group-toggle').addEventListener('click', () => {
|
|
Shell.toast('排序', '当前按时间分组 · 后续支持按项目分组');
|
|
});
|
|
|
|
/* 首次渲染 */
|
|
_renderTabs();
|
|
_renderList();
|
|
</script>
|
|
</body>
|
|
</html>
|