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

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>