oh-my-muse/design-docs/prototypes/产品-02C-用户工作区与作品工作台原型.html
2026-05-22 23:18:03 +08:00

1803 lines
69 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>产品-02C 用户工作区与作品工作台原型</title>
<style>
:root {
--bg: #f3f5f4;
--paper: #fffdf9;
--surface: #ffffff;
--surface-soft: #f8faf9;
--surface-strong: #eef3f1;
--ink: #17221f;
--muted: #50615c;
--weak: #7a8985;
--line: #d9e1df;
--line-strong: #bdcbc7;
--accent: #236f61;
--accent-deep: #174f46;
--accent-soft: #e6f3f0;
--blue: #315f89;
--blue-soft: #e8f0f7;
--gold: #80601c;
--gold-soft: #fff5dc;
--danger: #a83d35;
--danger-soft: #fff0ee;
--ok: #28744f;
--ok-soft: #e8f5ed;
--shadow: 0 14px 34px rgba(23, 34, 31, 0.07);
--radius: 8px;
--sidebar: 246px;
color-scheme: light;
}
* {
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--bg);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.45;
}
body {
margin: 0;
}
button,
input,
select,
textarea {
font: inherit;
}
button,
select {
cursor: pointer;
}
.app {
min-height: 100vh;
display: grid;
grid-template-columns: var(--sidebar) minmax(0, 1fr);
grid-template-rows: 58px minmax(0, 1fr);
}
.topbar {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 16px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.96);
position: sticky;
top: 0;
z-index: 30;
}
.brand,
.top-actions,
.row,
.badge-row,
.mini-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 7px;
background: var(--accent);
color: #fff;
font-weight: 900;
letter-spacing: 0;
}
.brand h1 {
margin: 0;
font-size: 15px;
line-height: 1.1;
letter-spacing: 0;
}
.brand p {
margin: 2px 0 0;
color: var(--muted);
font-size: 11px;
line-height: 1.1;
}
.avatar {
width: 29px;
height: 29px;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: 50%;
background: var(--surface-soft);
font-size: 12px;
font-weight: 800;
}
.sidebar {
grid-column: 1;
grid-row: 2;
min-height: 0;
overflow: auto;
border-right: 1px solid var(--line);
background: var(--surface);
}
.nav-section {
padding: 12px;
border-bottom: 1px solid var(--line);
}
.nav-title {
margin: 0 0 8px;
color: var(--weak);
font-size: 11px;
font-weight: 850;
letter-spacing: 0;
}
.nav-button {
width: 100%;
min-height: 36px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
margin: 0 0 4px;
padding: 7px 8px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--ink);
text-align: left;
}
.nav-button:hover {
border-color: var(--line);
background: var(--surface-soft);
}
.nav-button.active {
border-color: #a5d5cb;
background: var(--accent-soft);
color: var(--accent-deep);
}
.nav-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 750;
}
.nav-status {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--line-strong);
}
.nav-button.active .nav-status {
background: var(--accent);
}
.main {
grid-column: 2;
grid-row: 2;
min-width: 0;
min-height: 0;
overflow: auto;
padding: 16px;
}
.screen {
max-width: 1640px;
margin: 0 auto;
animation: screen-in 140ms ease-out;
}
@keyframes screen-in {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.page-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 14px;
margin-bottom: 12px;
}
.kicker {
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.page-title {
margin: 3px 0 5px;
font-size: 25px;
line-height: 1.18;
letter-spacing: 0;
}
.page-desc {
margin: 0;
color: var(--muted);
font-size: 12px;
}
.button {
min-height: 31px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 5px 10px;
border: 1px solid var(--line-strong);
border-radius: 7px;
background: var(--surface);
color: var(--ink);
font-size: 12px;
font-weight: 750;
white-space: nowrap;
}
.button:hover {
border-color: #91aaa4;
background: #f8fbfa;
}
.button.primary {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
.button.primary:hover {
background: var(--accent-deep);
}
.button.soft {
border-color: #afd8d0;
background: var(--accent-soft);
color: var(--accent-deep);
}
.button.warn {
border-color: #e5c77d;
background: var(--gold-soft);
color: var(--gold);
}
.button.danger {
border-color: #eeb9b3;
background: var(--danger-soft);
color: var(--danger);
}
.button.ghost {
border-color: transparent;
background: transparent;
color: var(--muted);
}
.button:disabled,
.nav-button:disabled {
cursor: not-allowed;
opacity: 0.54;
}
.button:focus-visible,
.nav-button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 3px solid rgba(35, 111, 97, 0.18);
outline-offset: 2px;
}
.badge {
display: inline-flex;
align-items: center;
max-width: 100%;
min-height: 22px;
padding: 2px 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface);
color: var(--muted);
font-size: 11px;
font-weight: 750;
word-break: break-word;
}
.badge.ok {
border-color: #b9dfca;
background: var(--ok-soft);
color: var(--ok);
}
.badge.warn {
border-color: #e6cc89;
background: var(--gold-soft);
color: var(--gold);
}
.badge.danger {
border-color: #ecb4ae;
background: var(--danger-soft);
color: var(--danger);
}
.badge.blue {
border-color: #bfd4e6;
background: var(--blue-soft);
color: var(--blue);
}
.panel,
.filter-bar,
.metric,
.work-card,
.workspace-context,
.editor,
.chapter-pane,
.side-panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow);
}
.filter-bar {
margin-bottom: 12px;
padding: 10px;
}
.filter-grid {
display: grid;
grid-template-columns: minmax(220px, 1.6fr) repeat(4, minmax(122px, 1fr)) auto;
gap: 8px;
align-items: end;
}
.control label {
display: block;
margin-bottom: 4px;
color: var(--muted);
font-size: 11px;
font-weight: 850;
}
.control input,
.control select,
.control textarea {
width: 100%;
min-height: 31px;
padding: 6px 8px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface-soft);
color: var(--ink);
font-size: 12px;
}
.control textarea {
min-height: 86px;
resize: vertical;
}
.hint {
margin: 4px 0 0;
color: var(--weak);
font-size: 11px;
}
.grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.main-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(310px, 390px);
gap: 12px;
align-items: start;
}
.panel {
min-width: 0;
overflow: hidden;
}
.panel-head {
min-height: 42px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 11px;
border-bottom: 1px solid var(--line);
background: var(--surface-soft);
}
.panel-title {
margin: 0;
font-size: 13px;
font-weight: 850;
letter-spacing: 0;
}
.panel-body {
padding: 11px;
}
.metric {
min-height: 84px;
padding: 10px;
}
.metric-label {
margin: 0;
color: var(--muted);
font-size: 11px;
font-weight: 850;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.metric-value {
margin: 5px 0 4px;
font-size: 22px;
line-height: 1.1;
font-weight: 900;
letter-spacing: 0;
}
.metric-foot {
color: var(--weak);
font-size: 11px;
}
.works-grid {
display: grid;
grid-template-columns: repeat(3, minmax(260px, 1fr));
gap: 12px;
}
.work-card {
display: grid;
gap: 10px;
padding: 12px;
min-height: 176px;
}
.work-card-head {
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
gap: 10px;
align-items: center;
}
.cover {
width: 42px;
height: 56px;
border-radius: 6px;
display: grid;
place-items: center;
background: linear-gradient(145deg, #295e55, #dfebdf);
color: #fff;
font-size: 12px;
font-weight: 900;
}
.work-title {
margin: 0;
font-size: 15px;
line-height: 1.25;
letter-spacing: 0;
}
.work-meta {
margin: 3px 0 0;
color: var(--muted);
font-size: 11px;
}
.stat-line {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 8px;
border-radius: 7px;
background: var(--surface-soft);
}
.stat-line strong {
display: block;
font-size: 14px;
line-height: 1.1;
}
.stat-line span {
color: var(--weak);
font-size: 10px;
}
.workspace-context {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
margin-bottom: 12px;
padding: 10px 12px;
background: var(--paper);
}
.workspace-context h2 {
margin: 0;
font-size: 17px;
line-height: 1.2;
letter-spacing: 0;
}
.workspace-context p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
}
.workspace-tabs {
display: flex;
align-items: center;
gap: 6px;
overflow: auto;
margin-bottom: 12px;
padding-bottom: 2px;
}
.workspace-tabs .button.active,
.workspace-secondary .button.active {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
.workspace-secondary {
display: flex;
align-items: center;
gap: 6px;
overflow: auto;
margin: -6px 0 12px;
padding-bottom: 2px;
}
.writing-grid {
display: grid;
grid-template-columns: 242px minmax(420px, 1fr) 360px;
gap: 12px;
align-items: stretch;
min-height: calc(100vh - 178px);
}
.chapter-pane,
.side-panel,
.editor {
min-width: 0;
overflow: hidden;
}
.chapter-list,
.record-list,
.compact-list {
display: grid;
gap: 7px;
}
.chapter-item,
.list-item {
padding: 8px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface);
}
.chapter-item.active {
border-color: #a5d5cb;
background: var(--accent-soft);
}
.chapter-item h4,
.list-item h4 {
margin: 0 0 4px;
font-size: 12px;
line-height: 1.2;
}
.chapter-item p,
.list-item p {
margin: 0;
color: var(--muted);
font-size: 11px;
}
.editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 9px 11px;
border-bottom: 1px solid var(--line);
background: var(--surface-soft);
}
.manuscript {
min-height: 515px;
padding: 24px 30px;
background: var(--paper);
color: #1a211f;
font-family: "Songti SC", "STSong", "SimSun", serif;
font-size: 18px;
line-height: 1.9;
outline: none;
overflow: auto;
}
.manuscript p {
margin: 0 0 14px;
}
.selected-text {
background: #dcece8;
border-bottom: 2px solid #72b7ab;
}
.side-panel .panel-body {
display: grid;
gap: 10px;
}
.suggestion-box {
padding: 10px;
border: 1px solid #cdded9;
border-radius: 8px;
background: #fbfefc;
}
.suggestion-text {
margin: 8px 0;
color: #202926;
font-size: 13px;
line-height: 1.65;
}
.score-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 7px;
}
.score {
padding: 8px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface-soft);
}
.score strong {
display: block;
font-size: 18px;
line-height: 1;
}
.score span {
color: var(--muted);
font-size: 10px;
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
font-size: 12px;
}
th,
td {
padding: 9px 8px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
background: var(--surface-soft);
font-size: 11px;
font-weight: 850;
white-space: nowrap;
}
td {
color: var(--ink);
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
.form-grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.choice-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.choice {
padding: 11px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}
.choice.active {
border-color: #9fd1c8;
background: var(--accent-soft);
}
.choice h3 {
margin: 0 0 6px;
font-size: 14px;
}
.choice p {
margin: 0;
color: var(--muted);
font-size: 12px;
}
.progress {
height: 9px;
overflow: hidden;
border-radius: 999px;
background: var(--surface-strong);
}
.progress > span {
display: block;
height: 100%;
border-radius: inherit;
background: var(--accent);
}
.timeline {
display: grid;
gap: 10px;
}
.timeline-item {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
gap: 9px;
}
.timeline-dot {
width: 10px;
height: 10px;
margin-top: 5px;
border-radius: 50%;
background: var(--line-strong);
}
.timeline-dot.ok {
background: var(--ok);
}
.timeline-dot.warn {
background: var(--gold);
}
.timeline-item h4 {
margin: 0 0 2px;
font-size: 12px;
}
.timeline-item p {
margin: 0;
color: var(--muted);
font-size: 11px;
}
.diff {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.diff-box {
min-height: 150px;
padding: 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface-soft);
font-size: 12px;
line-height: 1.65;
}
.danger-zone {
border-color: #edb8b2;
background: #fff8f7;
}
.empty-state {
min-height: 200px;
display: grid;
place-items: center;
padding: 24px;
border: 1px dashed var(--line-strong);
border-radius: 8px;
background: var(--surface-soft);
color: var(--muted);
text-align: center;
}
.empty-state h3 {
margin: 0 0 6px;
color: var(--ink);
font-size: 15px;
}
.empty-state p {
margin: 0 0 12px;
font-size: 12px;
}
@media (max-width: 1220px) {
.app {
grid-template-columns: 208px minmax(0, 1fr);
}
.writing-grid {
grid-template-columns: 210px minmax(360px, 1fr);
}
.writing-grid .side-panel {
grid-column: 1 / -1;
}
.works-grid,
.grid-3,
.choice-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.filter-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 820px) {
.app {
display: block;
}
.topbar {
position: static;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--line);
max-height: 240px;
}
.main {
padding: 12px;
}
.page-head,
.workspace-context,
.main-grid,
.grid-2,
.grid-3,
.grid-4,
.filter-grid,
.form-grid,
.form-grid.three,
.choice-grid,
.diff,
.writing-grid,
.works-grid {
grid-template-columns: 1fr;
}
.manuscript {
min-height: 420px;
padding: 18px;
font-size: 16px;
}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="brand-mark">M</div>
<div>
<h1>Muse 用户工作区</h1>
<p>我的作品 / 作品工作台 / 写作台</p>
</div>
</div>
<div class="top-actions">
<button class="button soft" data-go="work-create-import-entry">新建作品</button>
<button class="button" data-go="work-import-parse">导入旧稿</button>
<div class="avatar">林</div>
</div>
</header>
<aside class="sidebar" id="nav"></aside>
<main class="main" id="app" tabindex="-1"></main>
</div>
<script>
const pages = [
{ id: "user-my-works", name: "我的作品", group: "用户工作区", desc: "找到、创建、导入和继续写作作品" },
{ id: "work-create-import-entry", name: "新建/导入作品", group: "用户工作区", desc: "创建空作品、使用模板或上传旧稿" },
{ id: "work-workspace-shell", name: "作品工作台壳", group: "当前作品", desc: "单作品导航、状态摘要和返回规则" },
{ id: "work-writing-desk", name: "写作台", group: "当前作品", desc: "编辑正文、触发 AI 生成并处理候选" },
{ id: "work-suggestion-panel", name: "AI 候选决策面板", group: "当前作品", desc: "接受、修改后合并或丢弃候选" },
{ id: "work-planning-desk", name: "作品规划台", group: "当前作品", desc: "维护方向、大纲、世界、角色和文风" },
{ id: "work-knowledge-consistency", name: "知识与一致性", group: "当前作品", desc: "管理作品知识、草稿和一致性问题" },
{ id: "work-knowledge-draft-detail", name: "知识草稿详情", group: "当前作品", desc: "查看来源、差异、风险并做确认" },
{ id: "work-agent-association", name: "作品智能体关联", group: "当前作品设置", desc: "查看组合并替换开放槽位" },
{ id: "work-knowledge-source", name: "作品知识来源", group: "当前作品设置", desc: "绑定、解绑、降权和关闭生成使用" },
{ id: "work-import-parse", name: "导入解析", group: "当前作品流程", desc: "上传旧稿并查看解析进度" },
{ id: "work-full-parse-review", name: "全书解析审阅", group: "当前作品流程", desc: "按章节确认、驳回或重试解析结果" },
{ id: "work-export-delivery", name: "导出交付", group: "当前作品流程", desc: "选择范围和格式并下载交付物" },
{ id: "work-records-usage", name: "记录与用量", group: "当前作品", desc: "查看单作品记录、任务和 Token 使用" },
{ id: "work-settings", name: "作品设置", group: "当前作品设置", desc: "管理基础信息、归档和删除" }
];
const works = [
{ title: "雾港异闻录", type: "都市奇幻", words: "128,430", chapters: 42, edited: "今天 16:22", status: "连载中", drafts: 7, risk: "2 个来源异常", badge: "待处理" },
{ title: "第九封来信", type: "悬疑", words: "62,810", chapters: 21, edited: "昨天 23:41", status: "草稿", drafts: 3, risk: "无阻断风险", badge: "继续写" },
{ title: "群星旧约", type: "科幻", words: "211,020", chapters: 66, edited: "05-20 11:08", status: "修订", drafts: 18, risk: "最近解析部分失败", badge: "审阅" },
{ title: "南桥旧梦", type: "年代", words: "31,560", chapters: 12, edited: "05-18 09:15", status: "归档", drafts: 0, risk: "只读归档", badge: "查看" }
];
const chapters = [
{ title: "第 1 章 雾港来客", words: "3,420", state: "已保存" },
{ title: "第 2 章 码头上的黑伞", words: "3,880", state: "待确认 2" },
{ title: "第 3 章 旧钟楼", words: "2,760", state: "当前章节" },
{ title: "第 4 章 失踪的账本", words: "1,240", state: "草稿" }
];
const workspaceTabs = [
["work-writing-desk", "写作"],
["work-planning-desk", "规划"],
["work-knowledge-consistency", "知识与一致性"]
];
const workspaceSecondaryLinks = [
["work-agent-association", "智能体设置"],
["work-knowledge-source", "知识来源"],
["work-import-parse", "导入任务"],
["work-export-delivery", "导出任务"],
["work-records-usage", "记录"],
["work-settings", "设置"]
];
function go(id) {
location.hash = id;
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;"
}[char]));
}
function badge(text, tone = "") {
return `<span class="badge ${tone}">${escapeHtml(text)}</span>`;
}
function pageHead(page, actions = "") {
return `
<div class="page-head">
<div>
<div class="kicker">${escapeHtml(page.group)}</div>
<h2 class="page-title">${escapeHtml(page.name)}</h2>
<p class="page-desc">${escapeHtml(page.desc)}</p>
</div>
<div class="row">${actions}</div>
</div>
`;
}
function panel(title, body, actions = "") {
return `
<section class="panel">
<div class="panel-head">
<h3 class="panel-title">${title}</h3>
<div class="mini-actions">${actions}</div>
</div>
<div class="panel-body">${body}</div>
</section>
`;
}
function workspaceFrame(activeId, content, right = "") {
const tabHtml = workspaceTabs.map(([id, name]) => `
<button class="button ${id === activeId ? "active" : ""}" data-go="${id}">${name}</button>
`).join("");
const secondaryHtml = workspaceSecondaryLinks.map(([id, name]) => `
<button class="button ghost ${id === activeId ? "active" : ""}" data-go="${id}">${name}</button>
`).join("");
const context = `
<section class="workspace-context">
<div>
<h2>雾港异闻录</h2>
<p>当前章节:第 3 章 旧钟楼 · 最近保存:16:18 · 待确认知识 7 · 来源异常 2</p>
</div>
<div class="row">
${badge("正常", "ok")}
${badge("有待确认", "warn")}
<button class="button" data-go="user-my-works">返回我的作品</button>
</div>
</section>
<nav class="workspace-tabs">${tabHtml}</nav>
<nav class="workspace-secondary">${secondaryHtml}</nav>
`;
if (!right) return context + content;
return context + `<div class="main-grid"><div>${content}</div><div>${right}</div></div>`;
}
function renderNav(activeId) {
const groups = pages.reduce((acc, page) => {
acc[page.group] = acc[page.group] || [];
acc[page.group].push(page);
return acc;
}, {});
document.getElementById("nav").innerHTML = Object.entries(groups).map(([group, items]) => `
<section class="nav-section">
<h2 class="nav-title">${escapeHtml(group)}</h2>
${items.map((page) => `
<button class="nav-button ${page.id === activeId ? "active" : ""}" data-go="${page.id}">
<span class="nav-name">${escapeHtml(page.name)}</span>
<span class="nav-status"></span>
</button>
`).join("")}
</section>
`).join("");
}
function renderMyWorks(page) {
const cards = works.map((work, index) => `
<article class="work-card">
<div class="work-card-head">
<div class="cover">${index + 1}</div>
<div>
<h3 class="work-title">${escapeHtml(work.title)}</h3>
<p class="work-meta">${escapeHtml(work.type)} · ${escapeHtml(work.status)} · 最近编辑 ${escapeHtml(work.edited)}</p>
</div>
</div>
<div class="stat-line">
<div><strong>${work.words}</strong><span>字数</span></div>
<div><strong>${work.chapters}</strong><span>章节</span></div>
<div><strong>${work.drafts}</strong><span>待确认</span></div>
</div>
<div class="badge-row">
${badge(work.badge, work.badge === "继续写" ? "ok" : work.badge === "待处理" ? "warn" : "blue")}
${badge(work.risk, work.risk.includes("异常") || work.risk.includes("失败") ? "warn" : "")}
</div>
<div class="row">
<button class="button primary" data-go="work-writing-desk">继续写作</button>
<button class="button" data-go="work-export-delivery">导出</button>
<button class="button ghost" data-go="work-settings">设置</button>
</div>
</article>
`).join("");
return `
<div class="screen">
${pageHead(page, `<button class="button primary" data-go="work-create-import-entry">新建作品</button><button class="button" data-go="work-import-parse">导入作品</button>`)}
<section class="filter-bar">
<div class="filter-grid">
<div class="control"><label>作品名</label><input value="雾港" /></div>
<div class="control"><label>类型</label><select><option>全部类型</option><option>都市奇幻</option><option>悬疑</option></select></div>
<div class="control"><label>状态</label><select><option>全部状态</option><option>连载中</option><option>草稿</option><option>归档</option></select></div>
<div class="control"><label>风险</label><select><option>全部风险</option><option>有待确认</option><option>来源异常</option></select></div>
<div class="control"><label>排序</label><select><option>最近编辑优先</option><option>字数最多</option><option>章节最多</option></select></div>
<button class="button">筛选</button>
</div>
</section>
<div class="grid-4" style="margin-bottom:12px">
<div class="metric"><p class="metric-label">全部作品</p><div class="metric-value">4</div><div class="metric-foot">当前账户可访问</div></div>
<div class="metric"><p class="metric-label">待确认知识</p><div class="metric-value">28</div><div class="metric-foot">来自 3 部作品</div></div>
<div class="metric"><p class="metric-label">最近失败任务</p><div class="metric-value">2</div><div class="metric-foot">可进入对应作品处理</div></div>
<div class="metric"><p class="metric-label">本周写作</p><div class="metric-value">18,240</div><div class="metric-foot">新增字数</div></div>
</div>
<section class="works-grid">${cards}</section>
</div>
`;
}
function renderCreateImport(page) {
return `
<div class="screen">
${pageHead(page, `<button class="button" data-go="user-my-works">取消</button><button class="button primary" data-go="work-writing-desk">创建并进入写作台</button>`)}
<div class="grid-2">
${panel("创建方式", `
<div class="choice-grid">
<div class="choice active"><h3>空白作品</h3><p>创建默认章节,立即进入写作台。</p></div>
<div class="choice"><h3>使用模板</h3><p>从可用模板带入题材、结构和默认章节。</p></div>
<div class="choice"><h3>导入旧稿</h3><p>上传旧文稿,稍后按章节审阅解析结果。</p></div>
</div>
`)}
${panel("文件上传", `
<div class="empty-state">
<div>
<h3>拖入旧稿或选择文件</h3>
<p>支持 txt、docx、md;上传后进入导入解析。</p>
<button class="button">选择文件</button>
</div>
</div>
`)}
</div>
<div style="height:12px"></div>
${panel("基础信息", `
<div class="form-grid three">
<div class="control"><label>作品标题</label><input value="雾港异闻录" /></div>
<div class="control"><label>类型</label><select><option>都市奇幻</option><option>悬疑</option><option>科幻</option></select></div>
<div class="control"><label>题材</label><input value="港口、旧神、家族秘密" /></div>
<div class="control"><label>目标字数</label><input value="350000" /></div>
<div class="control"><label>模板</label><select><option>长篇连载默认模板</option><option>三幕式章节模板</option></select></div>
<div class="control"><label>创作状态</label><select><option>草稿</option><option>连载中</option></select></div>
<div class="control" style="grid-column:1 / -1"><label>简介</label><textarea>一名档案修复师回到雾港,发现父亲留下的旧钟楼记录与多年前失踪案有关。</textarea></div>
</div>
`, `<button class="button">保存为稍后填写</button>`)}
</div>
`;
}
function renderWorkspaceShell(page) {
const body = `
<div class="grid-4" style="margin-bottom:12px">
<div class="metric"><p class="metric-label">当前章节</p><div class="metric-value">第 3 章</div><div class="metric-foot">旧钟楼 · 2,760 字</div></div>
<div class="metric"><p class="metric-label">保存状态</p><div class="metric-value">16:18</div><div class="metric-foot">最近保存成功</div></div>
<div class="metric"><p class="metric-label">待确认知识</p><div class="metric-value">7</div><div class="metric-foot">人物 3 · 事件 2 · 地点 2</div></div>
<div class="metric"><p class="metric-label">来源异常</p><div class="metric-value">2</div><div class="metric-foot">可在知识来源处理</div></div>
</div>
<div class="grid-3">
${panel("主工作面", `<div class="compact-list">
<div class="list-item"><h4>写作台</h4><p>编辑正文、保存文本块、处理候选。</p></div>
<div class="list-item"><h4>作品规划台</h4><p>作品方向、章节大纲、世界设定、角色关系、文风检查。</p></div>
<div class="list-item"><h4>知识与一致性</h4><p>正式知识、待确认草稿、冲突问题和来源追溯。</p></div>
</div>`)}
${panel("设置型入口", `<div class="compact-list">
<div class="list-item"><h4>作品智能体关联</h4><p>查看组合,替换开放槽位,回退默认。</p></div>
<div class="list-item"><h4>作品知识来源</h4><p>绑定、解绑、降权和关闭生成使用。</p></div>
<div class="list-item"><h4>作品设置</h4><p>基础信息、归档、恢复和删除。</p></div>
</div>`)}
${panel("流程入口", `<div class="compact-list">
<div class="list-item"><h4>导入解析</h4><p>上传旧稿,查看解析进度。</p></div>
<div class="list-item"><h4>导出交付</h4><p>选择范围和格式,下载交付物。</p></div>
<div class="list-item"><h4>记录与用量</h4><p>生成记录、任务记录、失败原因和 Token。</p></div>
</div>`)}
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, body)}</div>`;
}
function renderWritingDesk(page) {
const chapterItems = chapters.map((chapter, index) => `
<button class="chapter-item ${index === 2 ? "active" : ""}" style="text-align:left">
<h4>${escapeHtml(chapter.title)}</h4>
<p>${chapter.words} 字 · ${chapter.state}</p>
</button>
`).join("");
const content = `
<div class="writing-grid">
<section class="chapter-pane">
<div class="panel-head">
<h3 class="panel-title">章节</h3>
<button class="button">新建</button>
</div>
<div class="panel-body">
<div class="control"><label>章节搜索</label><input value="旧钟楼" /></div>
<div style="height:8px"></div>
<div class="chapter-list">${chapterItems}</div>
</div>
</section>
<section class="editor">
<div class="editor-toolbar">
<div>
<strong>第 3 章 旧钟楼</strong>
<div class="hint">章节目标:让林澈第一次发现父亲失踪与港务账本有关。</div>
</div>
<div class="row">
${badge("未保存改动", "warn")}
<button class="button primary">保存正文</button>
</div>
</div>
<div class="manuscript" contenteditable="true" aria-label="正文编辑器">
<p>旧钟楼的门比林澈记忆里更沉。铜环上浮着一层潮湿的绿锈,像某种没有完全闭合的眼睛。</p>
<p>他把钥匙插进去时,远处港口传来三声短促的汽笛。那声音穿过雨幕,落在空旷的街面上,竟和父亲录音里的背景音一模一样。</p>
<p><span class="selected-text">账本夹在钟摆后方的暗格里,封面没有署名,只用蓝墨水写着一串日期。</span>林澈盯着最末一行,忽然意识到那不是账目日期,而是失踪名单。</p>
<p>楼下有人推门。风灌进来,灯泡轻轻晃了一下。</p>
</div>
</section>
<aside class="side-panel">
<div class="panel-head">
<h3 class="panel-title">AI 候选与状态</h3>
<button class="button" data-go="work-suggestion-panel">展开</button>
</div>
<div class="panel-body">
<div class="row">
<button class="button primary">续写</button>
<button class="button">改写</button>
<button class="button">扩写</button>
<button class="button">润色</button>
<button class="button">纠错</button>
<button class="button">检查</button>
</div>
<div class="suggestion-box">
<div class="badge-row">${badge("候选可处理", "ok")}${badge("有 2 条知识草稿", "warn")}</div>
<p class="suggestion-text">“别回头。”楼梯间传来的声音压得很低,像是怕惊醒钟楼深处某个沉睡的东西。林澈的手指停在账本边缘,纸页上的日期被雨声一点点浸湿。</p>
<div class="row">
<button class="button primary">原样接受</button>
<button class="button">修改后合并</button>
<button class="button danger">丢弃</button>
</div>
</div>
<div class="score-grid">
<div class="score"><strong>86</strong><span>设定一致</span></div>
<div class="score"><strong>91</strong><span>角色声音</span></div>
<div class="score"><strong>78</strong><span>场景结构</span></div>
</div>
<div class="compact-list">
<div class="list-item"><h4>当前智能体</h4><p>悬疑续写助手 · 来源:用户替换 · 授权正常</p></div>
<div class="list-item"><h4>知识来源</h4><p>雾港世界设定、港务资料库 · 1 个来源版本变化</p></div>
<div class="list-item"><h4>版本历史</h4><p>16:18 保存 · 15:42 保存 · 14:05 候选合并</p></div>
</div>
</div>
</aside>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderSuggestionPanel(page) {
const content = `
<div class="main-grid">
${panel("候选文本", `
<div class="suggestion-box">
<div class="badge-row">${badge("来源需重验", "warn")}${badge("知识草稿提取中", "warn")}${badge("直接接受已禁用", "danger")}</div>
<p class="suggestion-text">“别回头。”楼梯间传来的声音压得很低,像是怕惊醒钟楼深处某个沉睡的东西。林澈的手指停在账本边缘,纸页上的日期被雨声一点点浸湿。他知道,只要把账本合上,父亲留下的谜题就还能维持原样;可如果他继续读下去,雾港就再也不是记忆里的雾港。</p>
</div>
<div style="height:10px"></div>
<div class="row">
<button class="button primary" disabled>原样接受</button>
<button class="button">修改后合并</button>
<button class="button danger">丢弃当前建议</button>
<button class="button primary">重新生成</button>
<button class="button">复制非阻断文本</button>
</div>
`)}
<div>
${panel("创作健康度", `
<div class="score-grid">
<div class="score"><strong>86</strong><span>设定一致</span></div>
<div class="score"><strong>91</strong><span>角色声音</span></div>
<div class="score"><strong>78</strong><span>场景结构</span></div>
</div>
<div style="height:8px"></div>
<div class="compact-list">
<div class="list-item"><h4>改进建议</h4><p>候选后半段可以增加外部动作,避免停留在内心判断。</p></div>
<div class="list-item"><h4>关键风险</h4><p>账本来源版本变化,不能原样接受;需要重新生成或手动处理。</p></div>
</div>
`)}
<div style="height:12px"></div>
${panel("来源与知识状态", `
<div class="compact-list">
<div class="list-item"><h4>关联智能体</h4><p>悬疑续写助手 · 用户替换 · 授权正常</p></div>
<div class="list-item"><h4>知识来源</h4><p>雾港世界设定 v4 可生成;港务资料库 v2 来源已变化。</p></div>
<div class="list-item"><h4>知识草稿</h4><p>2 条提取中;接受正文不会自动确认知识。</p></div>
</div>
`, `<button class="button" data-go="work-knowledge-draft-detail">查看草稿</button>`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderPlanningDesk(page) {
const outlineRows = [
["第 1 章", "林澈回到雾港", "建立悬念", "已保存"],
["第 2 章", "黑伞人出现", "引出追踪", "已保存"],
["第 3 章", "旧钟楼账本", "揭示父亲线索", "候选待确认"],
["第 4 章", "账本失窃", "制造转折", "草稿"]
].map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}<td><button class="button">查看</button></td></tr>`).join("");
const content = `
<div class="grid-4" style="margin-bottom:12px">
<div class="metric"><p class="metric-label">作品方向</p><div class="metric-value">悬疑</div><div class="metric-foot">都市奇幻基调</div></div>
<div class="metric"><p class="metric-label">章节大纲</p><div class="metric-value">42</div><div class="metric-foot">已规划章节</div></div>
<div class="metric"><p class="metric-label">角色关系</p><div class="metric-value">18</div><div class="metric-foot">主要关系</div></div>
<div class="metric"><p class="metric-label">文风检查</p><div class="metric-value">82</div><div class="metric-foot">节奏略慢</div></div>
</div>
<div class="main-grid">
<div>
${panel("作品设定", `
<div class="form-grid">
<div class="control"><label>题材</label><input value="港口、旧神、家族秘密" /></div>
<div class="control"><label>基调</label><input value="潮湿、克制、悬疑递进" /></div>
<div class="control"><label>禁区</label><input value="不使用全知解释谜底" /></div>
<div class="control"><label>结局方向</label><input value="林澈主动继承守望者身份" /></div>
</div>
`, `<button class="button primary">保存规划项</button><button class="button">生成候选</button>`)}
<div style="height:12px"></div>
${panel("章节大纲", `<div class="table-wrap"><table><thead><tr><th>章节</th><th>摘要</th><th>章节目标</th><th>状态</th><th>操作</th></tr></thead><tbody>${outlineRows}</tbody></table></div>`)}
</div>
<div>
${panel("角色与世界", `
<div class="compact-list">
<div class="list-item"><h4>林澈</h4><p>档案修复师;动机是查清父亲失踪。</p></div>
<div class="list-item"><h4>顾南枝</h4><p>港务局档案员;知道旧账本来源。</p></div>
<div class="list-item"><h4>旧钟楼</h4><p>雾港最早的潮汐观测点;隐藏账本暗格。</p></div>
</div>
`)}
<div style="height:12px"></div>
${panel("文风检查", `
<div class="score-grid">
<div class="score"><strong>88</strong><span>作者声音</span></div>
<div class="score"><strong>76</strong><span>句式变化</span></div>
<div class="score"><strong>81</strong><span>张力节奏</span></div>
</div>
<div style="height:8px"></div>
<button class="button">发起文风检查</button>
`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderKnowledgeConsistency(page) {
const knowledgeRows = [
["人物", "林澈", "父亲失踪后回到雾港", "第 1 章", "已确认"],
["地点", "旧钟楼", "潮汐观测点,钟摆后有暗格", "第 3 章", "待修正"],
["事件", "港务账本失踪", "与失踪名单有关", "第 3 章", "草稿待确认"],
["关系", "林澈 / 顾南枝", "互相试探的协作关系", "第 2 章", "已确认"]
].map((row) => `<tr>${row.map((cell, index) => `<td>${index === 4 ? badge(cell, cell.includes("待") ? "warn" : cell.includes("修正") ? "danger" : "ok") : escapeHtml(cell)}</td>`).join("")}<td><button class="button">查看来源</button></td></tr>`).join("");
const content = `
<div class="main-grid">
<div>
${panel("知识列表", `
<div class="filter-bar">
<div class="filter-grid" style="grid-template-columns: minmax(180px,1.5fr) repeat(3,minmax(110px,1fr)) auto">
<div class="control"><label>搜索</label><input value="旧钟楼" /></div>
<div class="control"><label>知识类型</label><select><option>全部类型</option><option>人物</option><option>地点</option><option>事件</option></select></div>
<div class="control"><label>风险</label><select><option>全部风险</option><option>有冲突</option><option>来源失效</option></select></div>
<div class="control"><label>状态</label><select><option>全部状态</option><option>待确认</option><option>已确认</option></select></div>
<button class="button">筛选</button>
</div>
</div>
<div class="table-wrap"><table><thead><tr><th>类型</th><th>对象</th><th>内容摘要</th><th>来源</th><th>状态</th><th>操作</th></tr></thead><tbody>${knowledgeRows}</tbody></table></div>
`)}
</div>
<div>
${panel("待确认草稿", `
<div class="compact-list">
<div class="list-item"><h4>旧钟楼暗格</h4><p>来源:第 3 章 · 新增地点属性 · 可确认</p><div class="row" style="margin-top:8px"><button class="button primary">确认</button><button class="button">忽略</button><button class="button" data-go="work-knowledge-draft-detail">详情</button></div></div>
<div class="list-item"><h4>账本日期</h4><p>来源版本变化,需要重新提取。</p><div class="row" style="margin-top:8px"><button class="button">重新提取</button><button class="button">忽略</button></div></div>
</div>
`, `<button class="button">发起一致性检查</button>`)}
<div style="height:12px"></div>
${panel("一致性问题", `
<div class="compact-list">
<div class="list-item"><h4>旧钟楼位置不一致</h4><p>第 1 章写在东港,第 3 章写在北港。建议定位正文后修正。</p></div>
<div class="list-item"><h4>顾南枝知情程度冲突</h4><p>第 2 章“不知道账本”,第 3 章候选暗示她见过账本。</p></div>
</div>
`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderKnowledgeDraftDetail(page) {
const content = `
<div class="main-grid">
<div>
${panel("草稿摘要", `
<div class="badge-row">${badge("存在冲突", "warn")}${badge("来源:第 3 章 旧钟楼", "blue")}${badge("直接确认已禁用", "danger")}</div>
<div style="height:10px"></div>
<div class="diff">
<div class="diff-box"><strong>当前正式知识</strong><br />旧钟楼是雾港早期建筑,曾用于潮汐观测。位置:东港旧街。</div>
<div class="diff-box"><strong>草稿变更</strong><br />旧钟楼钟摆后存在暗格,保存港务账本。位置需要从东港旧街修正为北港斜坡街。</div>
</div>
<div style="height:10px"></div>
<div class="row">
<button class="button primary" disabled>确认</button>
<button class="button primary">修改后确认</button>
<button class="button">忽略</button>
<button class="button">重新提取</button>
</div>
`)}
</div>
<div>
${panel("来源快照", `
<div class="suggestion-box">
<p class="suggestion-text">账本夹在钟摆后方的暗格里,封面没有署名,只用蓝墨水写着一串日期。</p>
</div>
<div class="compact-list" style="margin-top:10px">
<div class="list-item"><h4>来源状态</h4><p>当前章节版本匹配,可定位正文。</p></div>
<div class="list-item"><h4>风险说明</h4><p>位置字段与已有知识冲突,不能直接确认;需要修改后确认或忽略。</p></div>
<div class="list-item"><h4>相关章节</h4><p>第 1 章 雾港来客;第 3 章 旧钟楼。</p></div>
</div>
`, `<button class="button">定位来源</button>`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderAgentAssociation(page) {
const rows = [
["续写风格", "悬疑续写助手", "用户替换", "授权正常", "失败时回退默认", "替换"],
["角色声音建议", "角色对白检查", "系统默认", "可用", "失败时显示原因", "替换"],
["输出合规检查", "平台安全检查", "不可替换", "可用", "阻断高风险输出", "不可替换"],
["上下文授权过滤", "来源授权过滤", "不可替换", "可用", "防止越权来源进入生成", "不可替换"],
["一致性叙事检查", "长篇一致性检查", "系统默认", "可用", "失败时保留候选", "试用"]
].map((row) => `
<tr>
<td><strong>${row[0]}</strong></td>
<td>${row[1]}</td>
<td>${badge(row[2], row[2].includes("用户") ? "blue" : row[2].includes("不可") ? "warn" : "")}</td>
<td>${badge(row[3], row[3].includes("正常") || row[3] === "可用" ? "ok" : "warn")}</td>
<td>${row[4]}</td>
<td><button class="button" ${row[5] === "不可替换" ? "disabled" : ""}>${row[5]}</button></td>
</tr>
`).join("");
const content = `
${panel("当前作品智能体组合", `
<div class="grid-3">
<div class="metric"><p class="metric-label">开放槽位</p><div class="metric-value">3</div><div class="metric-foot">可替换子智能体</div></div>
<div class="metric"><p class="metric-label">已替换</p><div class="metric-value">1</div><div class="metric-foot">续写风格使用用户智能体</div></div>
<div class="metric"><p class="metric-label">不可替换</p><div class="metric-value">2</div><div class="metric-foot">安全和授权边界</div></div>
</div>
`)}
<div style="height:12px"></div>
${panel("槽位列表", `<div class="table-wrap"><table><thead><tr><th>槽位用途</th><th>当前智能体</th><th>来源</th><th>授权</th><th>失败回退</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table></div>`, `<button class="button">查看授权</button><button class="button">回退默认</button>`)}
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderKnowledgeSource(page) {
const rows = [
["雾港世界设定", "用户知识库", "v4", "可检索、可生成", "本周 34 次", "正常"],
["港务资料库", "市场知识库", "v2", "仅检索", "本周 8 次", "版本变化"],
["民国港口法规摘录", "全局知识库", "v1", "只读参考", "本周 3 次", "授权正常"],
["旧报纸剪辑", "市场知识库", "v3", "已关闭生成", "本周 0 次", "来源下架"]
].map((row) => `
<tr>
<td><strong>${row[0]}</strong></td>
<td>${row[1]}</td>
<td>${row[2]}</td>
<td>${row[3]}</td>
<td>${row[4]}</td>
<td>${badge(row[5], row[5] === "正常" || row[5] === "授权正常" ? "ok" : "warn")}</td>
<td><div class="row"><button class="button">查看命中</button><button class="button">降权</button><button class="button danger">解绑</button></div></td>
</tr>
`).join("");
const content = `
<div class="main-grid">
${panel("当前绑定来源", `<div class="table-wrap"><table><thead><tr><th>知识库</th><th>来源类型</th><th>版本</th><th>用途</th><th>最近命中</th><th>状态</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table></div>`, `<button class="button primary">绑定知识库</button>`)}
<div>
${panel("可绑定来源", `
<div class="compact-list">
<div class="list-item"><h4>人物职业资料包</h4><p>用户知识库 · 可检索 · 处理完成</p><button class="button" style="margin-top:8px">绑定</button></div>
<div class="list-item"><h4>悬疑节奏参考库</h4><p>市场知识库 · 已安装 · 可生成</p><button class="button" style="margin-top:8px">绑定</button></div>
</div>
`)}
<div style="height:12px"></div>
${panel("用途说明", `
<div class="compact-list">
<div class="list-item"><h4>可检索</h4><p>写作时可被查到并摘要展示。</p></div>
<div class="list-item"><h4>可生成</h4><p>允许进入本次生成上下文。</p></div>
<div class="list-item"><h4>关闭生成</h4><p>只做参考,不进入后续生成。</p></div>
</div>
`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderImportParse(page) {
const content = `
<div class="main-grid">
<div>
${panel("导入文件", `
<div class="empty-state" style="min-height:150px">
<div>
<h3>雾港旧稿-前四十章.docx</h3>
<p>4.8 MB · 上传于 16:03 · 当前解析中</p>
<div class="progress"><span style="width:68%"></span></div>
<p class="hint">已识别 28 / 42 章,失败 2 章。</p>
</div>
</div>
<div style="height:10px"></div>
<div class="row"><button class="button">重新上传</button><button class="button">重试失败</button><button class="button danger">取消解析</button><button class="button" data-go="work-full-parse-review">进入审阅</button></div>
`)}
<div style="height:12px"></div>
${panel("章节识别摘要", `
<div class="grid-4">
<div class="metric"><p class="metric-label">识别章节</p><div class="metric-value">28</div><div class="metric-foot">已完成</div></div>
<div class="metric"><p class="metric-label">待处理</p><div class="metric-value">12</div><div class="metric-foot">排队中</div></div>
<div class="metric"><p class="metric-label">失败章节</p><div class="metric-value">2</div><div class="metric-foot">可重试</div></div>
<div class="metric"><p class="metric-label">待审阅</p><div class="metric-value">18</div><div class="metric-foot">章节结果</div></div>
</div>
`)}
</div>
<div>
${panel("解析进度", `
<div class="timeline">
<div class="timeline-item"><span class="timeline-dot ok"></span><div><h4>文件上传完成</h4><p>16:03 · 文件格式可用。</p></div></div>
<div class="timeline-item"><span class="timeline-dot ok"></span><div><h4>章节识别完成</h4><p>识别到 42 个章节标题。</p></div></div>
<div class="timeline-item"><span class="timeline-dot warn"></span><div><h4>内容解析进行中</h4><p>第 29 章正在处理,可稍后查看。</p></div></div>
<div class="timeline-item"><span class="timeline-dot"></span><div><h4>等待章节审阅</h4><p>解析完成后进入全书解析审阅。</p></div></div>
</div>
`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderFullParseReview(page) {
const rows = [
["第 1 章 雾港来客", "已解析", "人物 3 · 地点 2", "低", "可确认"],
["第 2 章 码头上的黑伞", "已解析", "人物 2 · 事件 1", "中", "可确认"],
["第 3 章 旧钟楼", "待确认", "地点 1 · 事件 2", "高", "需处理"],
["第 18 章 潮汐表", "失败", "无", "未知", "可重试"]
].map((row) => {
const canConfirm = row[4] === "可确认";
return `
<tr>
<td><strong>${row[0]}</strong></td>
<td>${badge(row[1], row[1] === "失败" ? "danger" : row[1] === "待确认" ? "warn" : "ok")}</td>
<td>${row[2]}</td>
<td>${badge(row[3], row[3] === "高" ? "danger" : row[3] === "中" ? "warn" : "")}</td>
<td>${row[4]}</td>
<td><div class="row"><button class="button" ${canConfirm ? "" : "disabled"}>${canConfirm ? "确认" : "确认禁用"}</button><button class="button">驳回</button><button class="button">重试</button></div></td>
</tr>
`;
}).join("");
const content = `
${panel("章节审阅表", `<div class="table-wrap"><table><thead><tr><th>章节</th><th>解析状态</th><th>识别结果</th><th>风险</th><th>可确认性</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table></div>`, `<button class="button primary">确认 2 个可确认章节</button>`)}
<div style="height:12px"></div>
<div class="grid-3">
${panel("章节详情", `<div class="suggestion-box"><p class="suggestion-text">第 3 章识别到“旧钟楼位置”与已有知识冲突,确认按钮已禁用。确认其他可确认章节只会产生待确认知识草稿,不会直接写入作品知识。</p></div>`)}
${panel("风险分布", `<div class="score-grid"><div class="score"><strong>12</strong><span>低风险</span></div><div class="score"><strong>4</strong><span>中风险</span></div><div class="score"><strong>1</strong><span>高风险</span></div></div>`)}
${panel("处理结果", `<div class="compact-list"><div class="list-item"><h4>已确认章节</h4><p>确认后进入正文和章节结果历史。</p></div><div class="list-item"><h4>知识草稿</h4><p>仍需在知识与一致性中确认。</p></div></div>`)}
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderExportDelivery(page) {
const content = `
<div class="main-grid">
<div>
${panel("导出配置", `
<div class="form-grid three">
<div class="control"><label>导出类型</label><select><option>正文</option><option>设定</option><option>作品知识</option></select></div>
<div class="control"><label>范围</label><select><option>全部章节</option><option>当前章节</option><option>已选章节</option></select></div>
<div class="control"><label>格式</label><select><option>docx</option><option>txt</option><option>md</option><option>zip</option></select></div>
<div class="control"><label>预计字数</label><input value="128430" /></div>
<div class="control"><label>预计大小</label><input value="2.4 MB" /></div>
<div class="control"><label>授权限制</label><input value="旧报纸剪辑不可导出" /></div>
</div>
<div style="height:10px"></div>
<div class="row"><button class="button primary">创建导出任务</button><button class="button">查看最近导出任务</button></div>
`)}
</div>
<div>
${panel("授权限制提示", `
<div class="compact-list">
<div class="list-item"><h4>市场知识库限制</h4><p>旧报纸剪辑来源已下架,不能包含在导出范围。</p></div>
<div class="list-item"><h4>第三方来源</h4><p>港务资料库仅允许内部参考,导出时只保留作品正文。</p></div>
</div>
`)}
</div>
</div>
<div style="height:12px"></div>
${panel("导出任务", `
<div class="table-wrap"><table><thead><tr><th>任务</th><th>范围</th><th>格式</th><th>状态</th><th>下载有效期</th><th>操作</th></tr></thead><tbody>
<tr><td>正文导出 16:12</td><td>全部章节</td><td>docx</td><td>${badge("完成", "ok")}</td><td>23 小时</td><td><button class="button primary">下载</button></td></tr>
<tr><td>作品知识导出 14:30</td><td>人物、地点、事件</td><td>zip</td><td>${badge("失败", "danger")}</td><td>-</td><td><button class="button">重试</button></td></tr>
<tr><td>设定导出 13:05</td><td>规划项</td><td>md</td><td>${badge("准备中", "warn")}</td><td>-</td><td><button class="button" disabled>等待</button></td></tr>
</tbody></table></div>
`)}
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderRecordsUsage(page) {
const rows = [
["16:20", "续写", "第 3 章", "悬疑续写助手", "候选可处理", "3,820", "已结算"],
["16:05", "一致性检查", "第 1-3 章", "长篇一致性检查", "有问题", "1,440", "已结算"],
["15:42", "保存正文", "第 3 章", "-", "成功", "0", "无消耗"],
["14:58", "规划候选", "第 4 章", "章节规划助手", "失败", "0", "已返还"]
].map((row) => `
<tr>
<td>${row[0]}</td><td>${row[1]}</td><td>${row[2]}</td><td>${row[3]}</td>
<td>${badge(row[4], row[4] === "失败" ? "danger" : row[4].includes("问题") ? "warn" : "ok")}</td>
<td>${row[5]}</td><td>${row[6]}</td>
<td><div class="row"><button class="button">详情</button><button class="button">进入章节</button></div></td>
</tr>
`).join("");
const content = `
<div class="grid-4" style="margin-bottom:12px">
<div class="metric"><p class="metric-label">今日 Token</p><div class="metric-value">8,912</div><div class="metric-foot">当前作品</div></div>
<div class="metric"><p class="metric-label">生成记录</p><div class="metric-value">16</div><div class="metric-foot">近 7 天</div></div>
<div class="metric"><p class="metric-label">失败任务</p><div class="metric-value">2</div><div class="metric-foot">可回到工作面重试</div></div>
<div class="metric"><p class="metric-label">待结算</p><div class="metric-value">1</div><div class="metric-foot">等待网关返回</div></div>
</div>
${panel("记录筛选", `
<div class="filter-grid" style="grid-template-columns: repeat(5,minmax(120px,1fr)) auto">
<div class="control"><label>时间</label><select><option>今天</option><option>近 7 天</option><option>近 30 天</option></select></div>
<div class="control"><label>动作</label><select><option>全部动作</option><option>续写</option><option>检查</option><option>规划</option></select></div>
<div class="control"><label>状态</label><select><option>全部状态</option><option>成功</option><option>失败</option><option>待结算</option></select></div>
<div class="control"><label>智能体</label><select><option>全部智能体</option><option>悬疑续写助手</option></select></div>
<div class="control"><label>来源</label><select><option>全部来源</option><option>雾港世界设定</option></select></div>
<button class="button">筛选</button>
</div>
`)}
<div style="height:12px"></div>
${panel("生成与任务记录", `<div class="table-wrap"><table><thead><tr><th>时间</th><th>动作</th><th>章节</th><th>智能体摘要</th><th>状态</th><th>Token</th><th>结算</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table></div>`, `<button class="button" data-go="work-writing-desk">回到写作台</button>`)}
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
function renderSettings(page) {
const content = `
<div class="main-grid">
<div>
${panel("基础信息", `
<div class="form-grid">
<div class="control"><label>标题</label><input value="雾港异闻录" /></div>
<div class="control"><label>类型</label><select><option>都市奇幻</option><option>悬疑</option></select></div>
<div class="control"><label>目标字数</label><input value="350000" /></div>
<div class="control"><label>创作状态</label><select><option>连载中</option><option>草稿</option><option>归档</option></select></div>
<div class="control" style="grid-column:1 / -1"><label>简介</label><textarea>一名档案修复师回到雾港,发现父亲留下的旧钟楼记录与多年前失踪案有关。</textarea></div>
</div>
<div style="height:10px"></div>
<button class="button primary">保存基础信息</button>
`)}
<div style="height:12px"></div>
${panel("创作偏好摘要", `
<div class="compact-list">
<div class="list-item"><h4>默认写作方向</h4><p>悬疑递进、克制描写、少解释。</p></div>
<div class="list-item"><h4>当前来源状态</h4><p>2 个来源需要处理;不会自动写入作品事实。</p></div>
<div class="list-item"><h4>最近导出</h4><p>今天 12:00 生成正文导出任务,可在导出交付页查看。</p></div>
</div>
`)}
</div>
<div>
${panel("作品状态", `
<div class="compact-list">
<div class="list-item"><h4>当前状态</h4><p>正常 · 可编辑 · 可导出。</p></div>
<div class="list-item"><h4>归档</h4><p>归档后默认列表隐藏,可从设置恢复。</p><button class="button warn" style="margin-top:8px">归档作品</button></div>
<div class="list-item"><h4>恢复</h4><p>归档作品可恢复到正常状态。</p><button class="button" style="margin-top:8px">恢复作品</button></div>
</div>
`)}
<div style="height:12px"></div>
${panel("危险操作区", `
<div class="danger-zone list-item">
<h4>删除作品</h4>
<p>删除前需要预览正文、知识、绑定、市场发布和导出建议。当前存在 1 个导出任务和 2 个来源异常,删除受限。</p>
<div class="row" style="margin-top:8px"><button class="button">预览删除影响</button><button class="button danger" disabled>删除作品</button></div>
</div>
`)}
</div>
</div>
`;
return `<div class="screen">${pageHead(page)}${workspaceFrame(page.id, content)}</div>`;
}
const renderers = {
"user-my-works": renderMyWorks,
"work-create-import-entry": renderCreateImport,
"work-workspace-shell": renderWorkspaceShell,
"work-writing-desk": renderWritingDesk,
"work-suggestion-panel": renderSuggestionPanel,
"work-planning-desk": renderPlanningDesk,
"work-knowledge-consistency": renderKnowledgeConsistency,
"work-knowledge-draft-detail": renderKnowledgeDraftDetail,
"work-agent-association": renderAgentAssociation,
"work-knowledge-source": renderKnowledgeSource,
"work-import-parse": renderImportParse,
"work-full-parse-review": renderFullParseReview,
"work-export-delivery": renderExportDelivery,
"work-records-usage": renderRecordsUsage,
"work-settings": renderSettings
};
function render() {
const requested = location.hash.replace("#", "");
const page = pages.find((item) => item.id === requested) || pages[0];
if (page.id !== requested) {
history.replaceState(null, "", `#${page.id}`);
}
renderNav(page.id);
document.getElementById("app").innerHTML = renderers[page.id](page);
document.getElementById("app").focus({ preventScroll: true });
}
document.addEventListener("click", (event) => {
const target = event.target.closest("[data-go]");
if (!target) return;
go(target.dataset.go);
});
window.addEventListener("hashchange", render);
render();
</script>
</body>
</html>