743 lines
27 KiB
HTML
743 lines
27 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>
|
|
.msg-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.msg-page .page-head {
|
|
margin-bottom: 0;
|
|
}
|
|
.msg-head-actions {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.msg-workbench {
|
|
display: grid;
|
|
grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
|
|
min-height: 640px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
overflow: hidden;
|
|
}
|
|
.msg-panel {
|
|
min-width: 0;
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 0;
|
|
overflow: hidden;
|
|
}
|
|
.msg-inbox,
|
|
.msg-detail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
.msg-inbox { border-right: 1px solid var(--border-faint); }
|
|
.msg-panel-h {
|
|
min-height: 58px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
}
|
|
.msg-panel-h .ti {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--accent-black);
|
|
}
|
|
.msg-panel-h .mono {
|
|
margin-left: auto;
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
color: var(--black-alpha-48);
|
|
letter-spacing: .04em;
|
|
}
|
|
.msg-filters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
}
|
|
.msg-filter {
|
|
height: 30px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 0 10px;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-pill);
|
|
background: var(--surface);
|
|
color: var(--black-alpha-56);
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.msg-filter:hover {
|
|
border-color: var(--black-alpha-24);
|
|
color: var(--accent-black);
|
|
background: var(--black-alpha-4);
|
|
}
|
|
.msg-filter.active {
|
|
border-color: var(--heat-20);
|
|
background: var(--heat-12);
|
|
color: var(--heat);
|
|
font-weight: 600;
|
|
}
|
|
.msg-filter .ct {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
letter-spacing: .02em;
|
|
}
|
|
.msg-search {
|
|
position: relative;
|
|
padding: 0 14px 12px;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
}
|
|
.msg-search svg {
|
|
position: absolute;
|
|
left: 26px;
|
|
top: 10px;
|
|
width: 13px;
|
|
height: 13px;
|
|
color: var(--black-alpha-48);
|
|
pointer-events: none;
|
|
}
|
|
.msg-search input {
|
|
width: 100%;
|
|
height: 34px;
|
|
padding: 0 12px 0 32px;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
background: var(--background-lighter);
|
|
color: var(--accent-black);
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.msg-search input:focus {
|
|
background: var(--surface);
|
|
border-color: var(--heat-40);
|
|
box-shadow: inset 0 0 0 1px var(--heat-40);
|
|
}
|
|
.msg-list {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
}
|
|
.msg-item {
|
|
position: relative;
|
|
width: 100%;
|
|
display: grid;
|
|
grid-template-columns: 30px minmax(0, 1fr);
|
|
gap: 10px;
|
|
padding: 14px 16px;
|
|
border: 0;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
background: transparent;
|
|
font-family: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.msg-item:hover { background: var(--black-alpha-4); }
|
|
.msg-item.active { background: var(--heat-12); }
|
|
.msg-item.active::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: var(--heat);
|
|
}
|
|
.msg-item.read .msg-item-title { color: var(--black-alpha-56); font-weight: 500; }
|
|
.msg-type-ic {
|
|
width: 30px;
|
|
height: 30px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-sm);
|
|
background: var(--background-lighter);
|
|
color: var(--black-alpha-72);
|
|
}
|
|
.msg-type-ic svg { width: 14px; height: 14px; }
|
|
.msg-type-ic.task { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
|
|
.msg-type-ic.team { background: var(--black-alpha-4); color: var(--accent-black); }
|
|
.msg-type-ic.billing { background: var(--honey-bg); border-color: var(--honey-bd); color: var(--accent-honey); }
|
|
.msg-type-ic.system { background: var(--black-alpha-7); color: var(--black-alpha-72); }
|
|
.msg-item-main { min-width: 0; }
|
|
.msg-item-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.msg-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: var(--r-pill);
|
|
background: var(--heat);
|
|
flex-shrink: 0;
|
|
}
|
|
.msg-item.read .msg-dot { display: none; }
|
|
.msg-item-title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--accent-black);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.msg-time {
|
|
flex-shrink: 0;
|
|
color: var(--black-alpha-48);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: .02em;
|
|
}
|
|
.msg-brief {
|
|
margin-top: 4px;
|
|
color: var(--black-alpha-56);
|
|
font-size: 12px;
|
|
line-height: 1.55;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.msg-item-foot {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
.msg-priority {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
height: 20px;
|
|
padding: 0 7px;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-sm);
|
|
background: var(--background-lighter);
|
|
color: var(--black-alpha-56);
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
letter-spacing: .02em;
|
|
}
|
|
.msg-priority.ok { background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest); }
|
|
.msg-priority.warn { background: var(--honey-bg); border-color: var(--honey-bd); color: var(--accent-honey); }
|
|
.msg-priority.err { background: var(--crimson-bg); border-color: var(--crimson-bd); color: var(--accent-crimson); }
|
|
.msg-priority.info { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
|
|
.msg-empty {
|
|
min-height: 320px;
|
|
display: grid;
|
|
place-items: center;
|
|
gap: 8px;
|
|
padding: 24px;
|
|
color: var(--black-alpha-48);
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
.msg-empty svg { width: 24px; height: 24px; color: var(--black-alpha-40); }
|
|
.msg-detail-empty {
|
|
flex: 1;
|
|
min-height: 520px;
|
|
display: grid;
|
|
place-items: center;
|
|
gap: 8px;
|
|
color: var(--black-alpha-48);
|
|
text-align: center;
|
|
}
|
|
.msg-detail-empty .ic {
|
|
width: 46px;
|
|
height: 46px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
background: var(--background-lighter);
|
|
}
|
|
.msg-detail-empty svg { width: 21px; height: 21px; }
|
|
.msg-detail-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: 22px 24px 24px;
|
|
}
|
|
.msg-detail-top {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding-bottom: 18px;
|
|
border-bottom: 1px solid var(--border-faint);
|
|
}
|
|
.msg-detail-title {
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.msg-detail-title h2 {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
line-height: 1.35;
|
|
font-weight: 600;
|
|
letter-spacing: -.012em;
|
|
color: var(--accent-black);
|
|
}
|
|
.msg-detail-title .meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
color: var(--black-alpha-48);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: .04em;
|
|
}
|
|
.msg-body-text {
|
|
margin: 18px 0 0;
|
|
color: var(--accent-black);
|
|
font-size: 14px;
|
|
line-height: 1.75;
|
|
}
|
|
.msg-props {
|
|
display: grid;
|
|
grid-template-columns: 110px 1fr;
|
|
gap: 10px 16px;
|
|
margin-top: 18px;
|
|
padding: 14px 16px;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
background: var(--background-lighter);
|
|
}
|
|
.msg-props .k {
|
|
color: var(--black-alpha-48);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: .04em;
|
|
}
|
|
.msg-props .v {
|
|
min-width: 0;
|
|
color: var(--accent-black);
|
|
font-size: 13px;
|
|
}
|
|
.msg-props .v a { color: var(--heat); }
|
|
.msg-timeline {
|
|
margin-top: 18px;
|
|
padding: 16px;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
}
|
|
.msg-timeline-h {
|
|
margin-bottom: 12px;
|
|
color: var(--accent-black);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.msg-step {
|
|
display: grid;
|
|
grid-template-columns: 68px 1fr;
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
border-top: 1px solid var(--border-faint);
|
|
}
|
|
.msg-step:first-of-type { border-top: 0; padding-top: 0; }
|
|
.msg-step .t {
|
|
color: var(--black-alpha-48);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: .02em;
|
|
}
|
|
.msg-step .d {
|
|
color: var(--black-alpha-72);
|
|
font-size: 12.5px;
|
|
line-height: 1.55;
|
|
}
|
|
.msg-log {
|
|
margin-top: 14px;
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--border-faint);
|
|
border-radius: var(--r-md);
|
|
background: var(--accent-black);
|
|
color: var(--accent-white);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
line-height: 1.7;
|
|
letter-spacing: .02em;
|
|
white-space: pre-wrap;
|
|
}
|
|
.msg-detail-f {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 14px 16px;
|
|
border-top: 1px solid var(--border-faint);
|
|
background: var(--background-lighter);
|
|
}
|
|
.msg-detail-f .spacer { flex: 1; }
|
|
.msg-foot-note {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--black-alpha-48);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: .04em;
|
|
}
|
|
.msg-foot-note a { color: var(--heat); }
|
|
@media (max-width: 1280px) {
|
|
.msg-workbench { grid-template-columns: minmax(300px, 340px) minmax(0, 1fr); }
|
|
}
|
|
@media (max-width: 860px) {
|
|
.msg-workbench { grid-template-columns: 1fr; }
|
|
.msg-inbox { border-right: 0; border-bottom: 1px solid var(--border-faint); }
|
|
.msg-list { max-height: 360px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="page">
|
|
<div class="msg-page">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>消息中心</h1>
|
|
<div class="sub"><span class="mono" id="msg-head-sub">// INBOX</span> 任务提醒 · 团队协作 · 计费与系统公告</div>
|
|
</div>
|
|
<div class="msg-head-actions">
|
|
<button class="btn" type="button" id="msg-mark-all">全部标已读</button>
|
|
<button class="btn" type="button" id="msg-settings">通知设置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="msg-workbench">
|
|
<section class="msg-panel msg-inbox">
|
|
<div class="msg-panel-h">
|
|
<span class="ti">收件箱</span>
|
|
<span class="mono" id="msg-list-count">// 0 条</span>
|
|
</div>
|
|
<div class="msg-filters" id="msg-filters"></div>
|
|
<div class="msg-search">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
<input id="msg-search" type="text" placeholder="搜索项目、来源、内容">
|
|
</div>
|
|
<div class="msg-list" id="msg-list"></div>
|
|
</section>
|
|
|
|
<section class="msg-panel msg-detail" id="msg-detail"></section>
|
|
</div>
|
|
|
|
<div class="msg-foot-note">
|
|
<span>// 消息保留 90 天 · 高风险任务会同时进入工作台队列</span>
|
|
<a href="settings.html#sec-notify">管理通知策略 →</a>
|
|
</div>
|
|
</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: '消息中心' }] });
|
|
|
|
(function () {
|
|
const NOW = new Date('2026-05-28T10:30:00');
|
|
const $ = sel => document.querySelector(sel);
|
|
const icon = name => window.IconKit ? window.IconKit.svg(name) : '';
|
|
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
|
const mins = n => new Date(NOW.getTime() - n * 60000);
|
|
const zhType = { all: '全部', unread: '未读', task: '任务', team: '团队', billing: '计费', system: '系统' };
|
|
const typeIcon = { task: 'clapperboard', team: 'users', billing: 'creditCard', system: 'info' };
|
|
const priLabel = { ok: '已完成', warn: '需关注', err: '风险', info: '更新' };
|
|
|
|
function fmtTime(d) {
|
|
const diff = Math.round((NOW - d) / 60000);
|
|
if (diff < 60) return diff + 'm';
|
|
if (diff < 1440) return Math.floor(diff / 60) + 'h';
|
|
return Math.floor(diff / 1440) + 'd';
|
|
}
|
|
function fmtFull(d) {
|
|
const z = n => String(n).padStart(2, '0');
|
|
return d.getFullYear() + '-' + z(d.getMonth() + 1) + '-' + z(d.getDate()) + ' ' + z(d.getHours()) + ':' + z(d.getMinutes());
|
|
}
|
|
|
|
const messages = [
|
|
{
|
|
id: 'task-video-ready', type: 'task', priority: 'ok', unread: true,
|
|
title: '补水面膜 · 痛点种草 v3 成片已完成',
|
|
brief: '7 镜 · 40 秒 · ¥18.40 已结算,可确认进入投放阶段。',
|
|
body: 'Seedance 视频生成全部完成,本次输出 3 条 9:16 成片。系统检测到镜头 3 的停留率预测最高,建议优先作为首版投放素材。',
|
|
source: '视频生成队列', project: '补水面膜', stage: 'Stage 5 · 拼接导出', owner: '李', cost: '¥18.40',
|
|
time: mins(12), href: 'pipeline.html?product=' + encodeURIComponent('补水面膜'),
|
|
log: 'render_id=vid-20260528-1030\nstatus=ok\nclips=7\nratio=9:16\ncharge=18.40',
|
|
timeline: [['10:06', '脚本与镜头配置锁定'], ['10:14', '3 条视频完成生成'], ['10:26', '字幕/BGM 合成完成'], ['10:30', '成片已入库,等待确认投放']],
|
|
actions: [{ id: 'goto', label: '进入项目', primary: true }, { id: 'preview', label: '查看成片' }, { id: 'rerun', label: '重跑视频' }],
|
|
},
|
|
{
|
|
id: 'task-script-failed', type: 'task', priority: 'err', unread: true,
|
|
title: '脚本生成失败 · 618 大促',
|
|
brief: 'Prompt 超过服务限制,本次失败未扣费,可一键重试。',
|
|
body: '脚本助手收到的商品卖点和参考文案过长,超过当前脚本模型单次上下文限制。系统已保存失败日志,并建议先压缩参考内容再重跑。',
|
|
source: '脚本助手', project: '618 大促', stage: 'Stage 1 · 脚本', owner: '张', cost: '¥0.00',
|
|
time: mins(64), href: 'pipeline.html?product=' + encodeURIComponent('618 大促'),
|
|
log: 'request_id=script-413\nerror=context_limit\nprompt_tokens=8124\nmax_tokens=8000\ncharged=false',
|
|
timeline: [['09:12', '提交脚本生成'], ['09:13', '模型返回 context_limit'], ['09:13', '费用回滚完成']],
|
|
actions: [{ id: 'rerun', label: '重试生成', primary: true }, { id: 'log', label: '查看日志' }],
|
|
},
|
|
{
|
|
id: 'task-asset-done', type: 'task', priority: 'ok', unread: true,
|
|
title: '4 张模特上身图已加入资产库',
|
|
brief: '祛痘精华 · 通勤白领 · 3:4,可直接进入视频项目。',
|
|
body: '模特上身图通过基础审核,已同步到资产库「未分类」。建议把其中 2 张加入祛痘精华项目作为 Stage 3 镜头参考。',
|
|
source: '图片生成', project: '祛痘精华', stage: '图片生成 · 模特上身图', owner: '陈', cost: '¥3.20',
|
|
time: mins(140), href: 'library.html',
|
|
timeline: [['08:05', '收到 2 张参考图'], ['08:08', '生成 4 张候选'], ['08:11', '通过筛选并写入资产库']],
|
|
actions: [{ id: 'goto', label: '查看素材', primary: true }, { id: 'attach', label: '加入项目' }],
|
|
},
|
|
{
|
|
id: 'team-edit-lock', type: 'team', priority: 'info', unread: true,
|
|
title: '@刘 正在编辑「补水面膜」项目',
|
|
brief: '你暂时以只读方式查看该项目,避免两人同时改动覆盖。',
|
|
body: '系统检测到同项目正在被团队成员编辑。为避免脚本、镜头或资产配置被互相覆盖,后进入者会进入只读查看状态。编辑结束后会自动恢复操作权限。',
|
|
source: '协作锁定', project: '补水面膜', stage: 'Stage 3 · 镜头', owner: '刘', cost: '-',
|
|
time: mins(210), href: 'pipeline.html?product=' + encodeURIComponent('补水面膜') + '#stage-3',
|
|
timeline: [['07:00', '刘进入项目编辑'], ['07:01', '系统为后进入成员开启只读状态']],
|
|
actions: [{ id: 'goto', label: '查看项目', primary: true }, { id: 'ack', label: '我已了解' }],
|
|
},
|
|
{
|
|
id: 'team-member', type: 'team', priority: 'info', unread: false,
|
|
title: '@王芳 已加入团队',
|
|
brief: '角色为运营,月限额 ¥800,可读写团队资产。',
|
|
body: '新成员已完成邮箱验证。她可以查看并编辑团队项目,但不能调整计费、安全与团队角色设置。',
|
|
source: '@李(超管)', project: '团队', stage: '成员管理', owner: '王', cost: '-',
|
|
time: mins(320), href: 'team.html',
|
|
timeline: [['05:02', '邀请邮件送达'], ['05:18', '成员完成验证'], ['05:19', '角色权限生效']],
|
|
actions: [{ id: 'goto', label: '查看团队', primary: true }, { id: 'settings', label: '调整权限' }],
|
|
},
|
|
{
|
|
id: 'billing-low', type: 'billing', priority: 'warn', unread: false,
|
|
title: '团队余额低于预警线',
|
|
brief: '当前余额 ¥87.20,按近 7 天速度预计可支撑 4 个视频项目。',
|
|
body: '余额预警阈值为 ¥100。若今天继续批量生成视频,建议先充值或临时降低成员月限额。',
|
|
source: '计费中心', project: '消费', stage: '余额监控', owner: '系统', cost: '¥87.20',
|
|
time: mins(460), href: 'account.html',
|
|
timeline: [['02:50', '余额低于 ¥100'], ['02:51', '已发送站内通知']],
|
|
actions: [{ id: 'goto', label: '前往充值', primary: true }, { id: 'settings', label: '预警设置' }],
|
|
},
|
|
{
|
|
id: 'system-maintenance', type: 'system', priority: 'info', unread: false,
|
|
title: '视频生成服务今晚例行维护',
|
|
brief: '23:00 - 01:00 期间提交任务会自动排队。',
|
|
body: '维护期间已进入生成中的任务不受影响。新提交的视频生成、三视图生成会进入队列,维护结束后按提交时间继续处理。',
|
|
source: 'Airshelf 运维', project: '系统', stage: '服务公告', owner: '系统', cost: '-',
|
|
time: mins(720), href: '',
|
|
timeline: [['昨天', '发布维护窗口'], ['今晚', '队列自动延后处理']],
|
|
actions: [{ id: 'ack', label: '我已了解', primary: true }],
|
|
},
|
|
{
|
|
id: 'system-review', type: 'system', priority: 'ok', unread: false,
|
|
title: '祛痘精华首版素材通过平台审核',
|
|
brief: '4 张图 / 2 条视频审核通过,可进入投放。',
|
|
body: '素材合规审核耗时 1 小时 12 分钟。系统未发现夸大功效、违禁词或画面遮挡问题。',
|
|
source: '审核中台', project: '祛痘精华', stage: 'Stage 5 · 投放', owner: '系统', cost: '-',
|
|
time: mins(1120), href: 'pipeline.html?product=' + encodeURIComponent('祛痘精华'),
|
|
timeline: [['昨天 16:40', '提交平台审核'], ['昨天 17:52', '审核通过'], ['昨天 17:53', '写入项目状态']],
|
|
actions: [{ id: 'goto', label: '进入投放', primary: true }],
|
|
},
|
|
];
|
|
|
|
const state = { tab: 'all', q: '', selectedId: messages[0].id, showLogId: null };
|
|
|
|
function counts() {
|
|
return {
|
|
unread: messages.filter(m => m.unread).length,
|
|
task: messages.filter(m => m.type === 'task').length,
|
|
team: messages.filter(m => m.type === 'team').length,
|
|
billing: messages.filter(m => m.type === 'billing').length,
|
|
system: messages.filter(m => m.type === 'system').length,
|
|
risk: messages.filter(m => m.priority === 'err' || m.priority === 'warn').length,
|
|
};
|
|
}
|
|
function visibleMessages() {
|
|
const q = state.q.toLowerCase();
|
|
return messages.filter(m => {
|
|
if (state.tab === 'unread' && !m.unread) return false;
|
|
if (!['all', 'unread'].includes(state.tab) && m.type !== state.tab) return false;
|
|
if (q && ![m.title, m.brief, m.body, m.source, m.project, m.stage].join(' ').toLowerCase().includes(q)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function renderSummary() {
|
|
const c = counts();
|
|
$('#msg-head-sub').textContent = '// ' + c.unread + ' 条未读 · ' + messages.length + ' 条总计';
|
|
}
|
|
|
|
function renderFilters() {
|
|
const c = counts();
|
|
const filters = [
|
|
['all', '全部', messages.length],
|
|
['unread', '未读', c.unread],
|
|
['task', '任务', c.task],
|
|
['team', '团队', c.team],
|
|
['billing', '计费', c.billing],
|
|
['system', '系统', c.system],
|
|
];
|
|
$('#msg-filters').innerHTML = filters.map(([id, label, ct]) => `
|
|
<button class="msg-filter${state.tab === id ? ' active' : ''}" type="button" data-tab="${id}">
|
|
${label}<span class="ct">${ct}</span>
|
|
</button>
|
|
`).join('');
|
|
$('#msg-filters').querySelectorAll('[data-tab]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.tab = btn.dataset.tab;
|
|
render();
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderList() {
|
|
const list = visibleMessages();
|
|
$('#msg-list-count').textContent = '// 显示 ' + list.length + ' 条';
|
|
if (!list.length) {
|
|
$('#msg-list').innerHTML = `<div class="msg-empty">${icon('search')}<span>没有符合条件的消息</span></div>`;
|
|
return;
|
|
}
|
|
$('#msg-list').innerHTML = list.map(m => `
|
|
<button class="msg-item${state.selectedId === m.id ? ' active' : ''}${m.unread ? '' : ' read'}" type="button" data-id="${esc(m.id)}">
|
|
<span class="msg-type-ic ${esc(m.type)}">${icon(typeIcon[m.type] || 'info')}</span>
|
|
<span class="msg-item-main">
|
|
<span class="msg-item-row">
|
|
<span class="msg-dot"></span>
|
|
<span class="msg-item-title">${esc(m.title)}</span>
|
|
<span class="msg-time">${fmtTime(m.time)}</span>
|
|
</span>
|
|
<span class="msg-brief">${esc(m.brief)}</span>
|
|
<span class="msg-item-foot">
|
|
<span class="msg-priority ${esc(m.priority)}">${esc(priLabel[m.priority] || '更新')}</span>
|
|
<span class="msg-priority">${esc(m.project)}</span>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
`).join('');
|
|
$('#msg-list').querySelectorAll('[data-id]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.selectedId = btn.dataset.id;
|
|
const msg = messages.find(m => m.id === state.selectedId);
|
|
if (msg) msg.unread = false;
|
|
render();
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderDetail() {
|
|
const m = messages.find(x => x.id === state.selectedId) || visibleMessages()[0] || messages[0];
|
|
if (!m) {
|
|
$('#msg-detail').innerHTML = `<div class="msg-detail-empty"><div class="ic">${icon('bell')}</div><div>暂无消息</div></div>`;
|
|
return;
|
|
}
|
|
state.selectedId = m.id;
|
|
const props = [
|
|
['来源', m.source],
|
|
['类别', zhType[m.type]],
|
|
['项目', m.project],
|
|
['阶段', m.stage],
|
|
['负责人', m.owner],
|
|
['费用', m.cost],
|
|
['时间', fmtFull(m.time)],
|
|
['关联资源', m.href ? `<a href="${esc(m.href)}">${esc(m.project)} →</a>` : '无'],
|
|
].map(([k, v]) => `<span class="k">${k}</span><span class="v">${v}</span>`).join('');
|
|
const actions = m.actions.map(a => `<button class="btn${a.primary ? ' btn-primary' : ''}" type="button" data-action="${esc(a.id)}">${esc(a.label)}</button>`).join('');
|
|
$('#msg-detail').innerHTML = `
|
|
<div class="msg-detail-body">
|
|
<div class="msg-detail-top">
|
|
<span class="msg-type-ic ${esc(m.type)}">${icon(typeIcon[m.type] || 'info')}</span>
|
|
<div class="msg-detail-title">
|
|
<h2>${esc(m.title)}</h2>
|
|
<div class="meta"><span>${esc(m.source)}</span><span>// ${esc(zhType[m.type])}</span><span>${fmtFull(m.time)}</span></div>
|
|
</div>
|
|
<span class="msg-priority ${esc(m.priority)}">${esc(priLabel[m.priority] || '更新')}</span>
|
|
</div>
|
|
<p class="msg-body-text">${esc(m.body)}</p>
|
|
<div class="msg-props">${props}</div>
|
|
<div class="msg-timeline">
|
|
<div class="msg-timeline-h">处理记录</div>
|
|
${m.timeline.map(([t, d]) => `<div class="msg-step"><span class="t">${esc(t)}</span><span class="d">${esc(d)}</span></div>`).join('')}
|
|
</div>
|
|
${state.showLogId === m.id && m.log ? `<pre class="msg-log">${esc(m.log)}</pre>` : ''}
|
|
</div>
|
|
<div class="msg-detail-f">
|
|
<button class="btn btn-ghost" type="button" data-action="archive">归档</button>
|
|
<button class="btn btn-ghost" type="button" data-action="mute">静音同类</button>
|
|
<span class="spacer"></span>
|
|
${actions}
|
|
</div>
|
|
`;
|
|
$('#msg-detail').querySelectorAll('[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', () => runAction(m, btn.dataset.action));
|
|
});
|
|
}
|
|
|
|
function runAction(m, act) {
|
|
if (!m) return;
|
|
if (act === 'goto') {
|
|
if (m.href) location.href = m.href;
|
|
else location.href = 'settings.html#sec-notify';
|
|
return;
|
|
}
|
|
if (act === 'preview') { Shell.toast('打开视频预览', m.project + ' · 成片'); return; }
|
|
if (act === 'rerun') {
|
|
m.priority = 'info';
|
|
m.brief = '任务已重新排队,完成后会再次提醒。';
|
|
Shell.toast('已重新排队', m.project + ' · ' + m.stage);
|
|
render();
|
|
return;
|
|
}
|
|
if (act === 'log') { state.showLogId = state.showLogId === m.id ? null : m.id; renderDetail(); return; }
|
|
if (act === 'attach') { location.href = 'projects-new.html'; return; }
|
|
if (act === 'settings') { location.href = 'settings.html#sec-notify'; return; }
|
|
if (act === 'ack') { m.unread = false; Shell.toast('已确认', m.title); render(); return; }
|
|
if (act === 'archive') {
|
|
const i = messages.findIndex(x => x.id === m.id);
|
|
if (i >= 0) messages.splice(i, 1);
|
|
state.selectedId = messages[0]?.id || null;
|
|
Shell.toast('已归档', m.title);
|
|
render();
|
|
return;
|
|
}
|
|
if (act === 'mute') { Shell.toast('已静音同类', zhType[m.type] + ' 类提醒可在设置恢复'); return; }
|
|
}
|
|
|
|
function render() {
|
|
renderSummary();
|
|
renderFilters();
|
|
renderList();
|
|
renderDetail();
|
|
}
|
|
|
|
$('#msg-search').addEventListener('input', e => {
|
|
state.q = e.target.value.trim();
|
|
renderList();
|
|
});
|
|
$('#msg-mark-all').addEventListener('click', () => {
|
|
messages.forEach(m => { m.unread = false; });
|
|
Shell.toast('已全部标为已读', messages.length + ' 条');
|
|
render();
|
|
});
|
|
$('#msg-settings').addEventListener('click', () => { location.href = 'settings.html#sec-notify'; });
|
|
|
|
render();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|