Skip to content

feat(forum): 按“专注画布”优化发布区布局、材料入口与配色 #184

Closed

Description

@lyyzka

背景与设计结论

作业 / 自由讨论发布区目前的文件上传、附加链接等入口较分散,白色区域与重复边框过多,削弱了正文、材料与辅助信息的层次。用户已从三版 HTML 原型中选定第三版「专注画布」,并确认两轮修改:

  1. 删除右上角的「发布内容」,在保持其他布局与交互不变的基础上优化配色。
  2. 删除品牌旁的「/ 课程讨论」,包含斜杠及其文案;继续保持其他部分不变。

本 issue 将以上已确认设计落实到现有学习论坛发布组件。用户可见名称使用「自由讨论」,与 #132 一致。

页面结构

采用连续的单栏编辑画布。自上而下的顺序为:

  1. 现有应用页头:保留品牌及原有导航结构,移除「发布内容」和「/ 课程讨论」两处冗余文字,清理其独立容器占位。
  2. 发布者信息行:账号头像、作者、实际可见范围,以及已有展开 / 收起入口。
  3. 发布类型:「自由讨论 / 正式作业」使用原型中的下划线选中态;正式作业入口沿用现有角色权限。
  4. 正文编辑区:作为主要视觉区域,保留现有输入、Markdown、预览、字数限制与快捷键能力。
  5. 格式工具栏:放在正文区域下方,以轻分隔线与正文衔接。
  6. 材料操作条:统一「添加材料」入口与「材料 N」汇总,使用浅鼠尾草绿背景区分正文和辅助操作。
  7. 标签与资料引用:保持较低视觉权重,按需展开。
  8. 底部操作区:左侧草稿状态,右侧主发布按钮。

除上述已确认的发布区调整外,页面其余结构、内容流及导航沿用现状。不要把原型预览的三方案切换控件放入产品页面。

材料入口与交互

  • 将文件、图片、参考链接归入同一个「添加材料」菜单;菜单项分别为「上传文件或图片」和「附加参考链接」。
  • 平时仅显示紧凑操作条、材料总数和文件 / 链接摘要;材料列表默认收起,按用户操作展开,避免为未使用功能常驻多块大面积上传区域。
  • 文件与链接使用统一的材料列表样式,保留类型图标、名称、必要状态及现有移除 / 查看能力;添加、移除后同步更新数量和摘要。
  • 正文工具栏中的「插入正文超链接」继续编辑正文;「附加参考链接」生成材料项,二者语义保持清晰。
  • 标签与作业 / 资料引用沿用现有数据来源和关联逻辑,按原型收纳,不增加新的业务字段。
  • 展开编辑区时,将增加的空间分配给正文;作者行和正文之间不产生大段空白。收起时同步收拢外围容器,文字及材料状态继续保留。

配色基准

使用第三版已确认的灰绿背景、白色编辑面与深绿色主操作。正文、辅助文本、材料区域与主按钮形成清晰层次。下列数值为最终原型基准,实施时映射至现有主题 token,作用范围限于该发布区。

用途 浅色主题 深色主题
页面背景 #EDF4F0 #14221B
编辑主表面 #FFFFFF #1E2D25
浮层 / 材料项表面 #FFFFFF #2A3D31
辅助区域底色 #EFF6F2 #25382D
浅色强调区域 #DEEEE4 #304C3B
正文文字 #243B2E #E7F0EA
次级文字 #5B7062 #ADBDAF
分隔线 / 边框 #D5E3DA #3C5243
主操作 / 选中态 #2E6B50 #98CCB0
主按钮文字 #FFFFFF #173222
阴影色 #1D493012 #00000038

保持原型第三版的字号、间距、圆角、控件排列与展开方式;两处文案删除不牵动其他元素布局。交互状态沿用同一套主题颜色,保证明暗主题的文字、图标、边框和焦点可辨识。

实现约束

  • 在现有 Memos 来源的内联发布组件及项目设计 token 上调整样式和材料入口组织,承接 feat(forum): 统一学习论坛命名,按 Memos 原生组件重做布局、内联发布与自动草稿 #132;保留当前正文编辑器、上传服务、标签 / 引用、发布接口及系统自动草稿管理。
  • 承接 fix(forum): 缩小顶部编辑输入区,统一使用账号已有真实头像 #163:顶部发布区保持轻量,长正文按现有机制控制高度;头像继续使用对应账号已有真实头像及统一缺省规则。
  • 原型中的 lyy、字母头像、示例正文、PDF、链接和材料数量仅用于评审;实际界面必须绑定真实账号、内容与材料状态。不要将原型中的模拟发布、内存草稿或简化 Markdown 解析替换进生产逻辑。
  • 保留正式作业现有字段、校验和角色权限。删除「/ 课程讨论」仅删除展示文案,不改变发布上下文、可见性、授权或数据模型。
  • 自动草稿、上传进行中 / 失败、发布中 / 失败 / 成功等状态应继续由真实业务流程驱动。

验收标准

  • 发布区采用已确认的第三版「专注画布」结构,正文 → 格式工具栏 → 材料条 → 标签与引用 → 底部操作的顺序和层级一致。
  • 页头不再显示「发布内容」或「/ 课程讨论」,无残留斜杠、空标签或多余占位;其他已确认的排版保持一致。
  • 配色与上表一致,正文和材料区域能够清楚区分,主操作突出,辅助文字清晰可读;明暗主题均可用。
  • 文件 / 图片与参考链接统一从「添加材料」进入;材料列表可收起 / 展开,数量和摘要随真实材料变化,无重复的大块上传区域。
  • 正文超链接与参考链接材料保持各自语义;上传、移除、预览、标签和资料引用按现有业务正常工作。
  • 展开 / 收起没有额外大段留白,长文本不会无限撑高顶部区域,已有输入与材料状态不丢失。
  • 自由讨论和正式作业既有权限、自动草稿恢复及发布失败 / 成功行为无回归,真实账号头像保持正确。
  • 桌面与窄屏无控件重叠或横向溢出,菜单与焦点可用;按仓库现行要求完成与本变更有关的验证,提供普通 / 展开状态、材料收起 / 展开及明暗主题的可复核截图。

设计参考与关联

最终第三版:HTML 参考源码(已删除两处顶部文案)

这是原型的 HTML 片段,包含作用域 CSS 与本地演示交互。原预览宿主提供全局 lucide 图标对象、data-tooltip 提示和 cursor-interaction 样式;独立预览时需提供相应运行环境。完整可直接打开的 HTML 已在需求会话中提供。源码中的示例数据与模拟发布不属于生产实现要求。

<style>
  #lingxi-publish-focus-color .lx-shell {
    --lx-canvas: light-dark(#edf3ef, #15211b);
    --lx-surface: light-dark(#ffffff, #202b25);
    --lx-raised: light-dark(#ffffff, #29372f);
    --lx-soft: light-dark(#f4f7f5, #25332b);
    --lx-tint: light-dark(#e8f2eb, #293e30);
    --lx-ink: light-dark(#24372b, #e5eee8);
    --lx-subtle: light-dark(#65746a, #a3b5a9);
    --lx-line: light-dark(#dce5df, #3b4b40);
    --lx-green: light-dark(#35764c, #94c9a3);
    --lx-on-green: light-dark(#ffffff, #142b1a);
    --lx-danger: light-dark(#aa3838, #ffb4b4);
    --lx-shadow: light-dark(#1e402016, #00000030);
    color: var(--lx-ink);
    background: var(--lx-canvas);
    font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
    border: 1px solid var(--lx-line);
    border-radius: 18px;
    text-align: left;
    min-width: 0;
    overflow-wrap: anywhere;
  }
  #lingxi-publish-focus-color .lx-shell * { box-sizing: border-box; }
  #lingxi-publish-focus-color .lx-shell [hidden] { display: none !important; }
  #lingxi-publish-focus-color :where(.lx-shell button, .lx-shell input, .lx-shell select, .lx-shell textarea) { font: inherit; }
  #lingxi-publish-focus-color :where(.lx-shell button) { color: inherit; }
  #lingxi-publish-focus-color .lx-shell button:disabled { cursor: not-allowed; opacity: .45; }
  #lingxi-publish-focus-color .lx-shell button svg,
  #lingxi-publish-focus-color .lx-shell i[data-lucide] { width: 16px; height: 16px; flex: 0 0 16px; }
  #lingxi-publish-focus-color .lx-shell input::placeholder,
  #lingxi-publish-focus-color .lx-shell textarea::placeholder { color: var(--lx-subtle); opacity: .9; }
  #lingxi-publish-focus-color .lx-shell button { -webkit-tap-highlight-color: transparent; }
  #lingxi-publish-focus-color .lx-appbar { height: 54px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 24px; border-bottom: 1px solid var(--lx-line); }
  #lingxi-publish-focus-color .lx-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: .03em; }
  #lingxi-publish-focus-color .lx-brandmark { display: grid; place-items: center; width: 27px; height: 27px; background: var(--lx-green); color: var(--lx-on-green); border-radius: 8px; }
  #lingxi-publish-focus-color .lx-app-context { color: var(--lx-subtle); font-size: 12px; font-weight: 400; letter-spacing: 0; }
  #lingxi-publish-focus-color .lx-stage { min-height: 722px; padding: 24px; }
  #lingxi-publish-focus-color .lx-collapsed-shell .lx-stage { min-height: 0; }
  #lingxi-publish-focus-color .lx-composer { background: var(--lx-surface); border: 1px solid var(--lx-line); border-radius: 14px; box-shadow: 0 8px 24px var(--lx-shadow); position: relative; min-width: 0; }
  #lingxi-publish-focus-color .lx-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 22px 12px; }
  #lingxi-publish-focus-color .lx-author { display: flex; align-items: center; gap: 10px; min-width: 0; }
  #lingxi-publish-focus-color .lx-avatar { display: grid; place-items: center; width: 32px; height: 32px; flex: 0 0 32px; border-radius: 50%; background: var(--lx-tint); color: var(--lx-green); font-size: 14px; font-weight: 600; }
  #lingxi-publish-focus-color .lx-author-name { display: block; font-size: 14px; font-weight: 600; }
  #lingxi-publish-focus-color .lx-meta { display: inline-flex; align-items: center; gap: 5px; color: var(--lx-subtle); font-size: 12px; }
  #lingxi-publish-focus-color .lx-head-actions { display: flex; align-items: center; gap: 3px; }
  #lingxi-publish-focus-color .lx-icon { padding: 0; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: transparent; }
  #lingxi-publish-focus-color .lx-icon:hover { background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-btn { min-height: 34px; padding: 6px 11px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; border: 1px solid var(--lx-line); border-radius: 8px; background: var(--lx-surface); white-space: nowrap; font-size: 13px; }
  #lingxi-publish-focus-color .lx-btn:hover { background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-btn-quiet { border-color: transparent; background: transparent; color: var(--lx-subtle); }
  #lingxi-publish-focus-color .lx-btn-primary { background: var(--lx-green); color: var(--lx-on-green); border-color: var(--lx-green); min-height: 38px; padding: 8px 19px; font-weight: 500; }
  #lingxi-publish-focus-color .lx-btn-primary:hover { background: var(--lx-green); filter: brightness(.95); }
  #lingxi-publish-focus-color .lx-btn-tint { color: var(--lx-green); background: var(--lx-tint); border-color: transparent; }
  #lingxi-publish-focus-color .lx-modes { display: inline-flex; align-items: center; gap: 3px; padding: 3px; border-radius: 9px; background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-mode { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 12px; border: 0; border-radius: 6px; background: transparent; color: var(--lx-subtle); font-size: 13px; white-space: nowrap; }
  #lingxi-publish-focus-color .lx-mode[aria-pressed="true"] { background: var(--lx-surface); color: var(--lx-green); box-shadow: 0 1px 4px var(--lx-shadow); font-weight: 600; }
  #lingxi-publish-focus-color .lx-mode-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 22px 12px; }
  #lingxi-publish-focus-color .lx-editor { min-width: 0; }
  #lingxi-publish-focus-color .lx-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 5px 14px; border-top: 1px solid var(--lx-line); border-bottom: 1px solid var(--lx-line); background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-formatting { display: flex; align-items: center; flex-wrap: wrap; gap: 1px; }
  #lingxi-publish-focus-color .lx-divider { width: 1px; height: 16px; margin: 0 6px; background: var(--lx-line); }
  #lingxi-publish-focus-color .lx-write { display: block; width: 100%; min-height: 168px; padding: 19px 23px 10px; border: none; border-radius: 0; background: transparent; color: var(--lx-ink); font-size: 15px; line-height: 1.9; resize: vertical; }
  #lingxi-publish-focus-color .lx-write:focus { outline-offset: -3px; }
  #lingxi-publish-focus-color .lx-write-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 23px 10px; color: var(--lx-subtle); font-size: 11px; }
  #lingxi-publish-focus-color .lx-counter { font-variant-numeric: tabular-nums; white-space: nowrap; }
  #lingxi-publish-focus-color .lx-live-preview { min-height: 168px; padding: 19px 23px 10px; font-size: 15px; line-height: 1.9; overflow-wrap: anywhere; }
  #lingxi-publish-focus-color .lx-live-preview img { display: block; max-width: 100%; max-height: 320px; object-fit: contain; border-radius: 7px; margin: 8px 0; }
  #lingxi-publish-focus-color .lx-live-preview p { margin: 0 0 13px; }
  #lingxi-publish-focus-color .lx-live-preview h3 { margin: 0 0 12px; font-size: 19px; line-height: 1.5; }
  #lingxi-publish-focus-color .lx-live-preview ul,
  #lingxi-publish-focus-color .lx-live-preview ol { margin: 0 0 12px; padding-left: 24px; }
  #lingxi-publish-focus-color .lx-live-preview code { background: var(--lx-soft); border-radius: 4px; padding: 2px 4px; font-size: 13px; }
  #lingxi-publish-focus-color .lx-live-preview a { color: var(--lx-green); text-decoration: underline; }
  #lingxi-publish-focus-color .lx-materials { padding: 12px 22px; border-top: 1px solid var(--lx-line); background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-materials-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
  #lingxi-publish-focus-color .lx-compact .lx-no-materials .lx-materials-head { margin-bottom: 0; }
  #lingxi-publish-focus-color .lx-section-label { font-size: 12px; font-weight: 600; letter-spacing: .03em; display: inline-flex; align-items: center; gap: 6px; }
  #lingxi-publish-focus-color .lx-count-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 5px; background: var(--lx-tint); color: var(--lx-green); font-size: 11px; }
  #lingxi-publish-focus-color .lx-add-wrap { position: relative; }
  #lingxi-publish-focus-color .lx-add-menu { position: absolute; top: 41px; left: 0; z-index: 6; width: 218px; padding: 6px; border: 1px solid var(--lx-line); border-radius: 10px; background: var(--lx-raised); box-shadow: 0 12px 34px var(--lx-shadow); }
  #lingxi-publish-focus-color .lx-add-menu button { display: flex; align-items: center; gap: 9px; padding: 10px; width: 100%; background: transparent; border: none; border-radius: 6px; text-align: left; font-size: 13px; }
  #lingxi-publish-focus-color .lx-add-menu button:hover { background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-files { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
  #lingxi-publish-focus-color .lx-file { display: flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; padding: 5px 7px 5px 9px; background: var(--lx-surface); border: 1px solid var(--lx-line); border-radius: 8px; }
  #lingxi-publish-focus-color .lx-file-kind { display: grid; place-items: center; flex: 0 0 29px; width: 29px; height: 29px; background: var(--lx-tint); color: var(--lx-green); border-radius: 6px; }
  #lingxi-publish-focus-color .lx-file-text { min-width: 0; }
  #lingxi-publish-focus-color .lx-file-name { display: block; font-size: 12px; font-weight: 500; overflow-wrap: anywhere; }
  #lingxi-publish-focus-color .lx-file-size { display: block; font-size: 11px; color: var(--lx-subtle); }
  #lingxi-publish-focus-color .lx-file .lx-icon { width: 25px; height: 27px; flex: 0 0 25px; color: var(--lx-subtle); }
  #lingxi-publish-focus-color .lx-empty { margin: 0; color: var(--lx-subtle); font-size: 12px; padding: 5px 0; }
  #lingxi-publish-focus-color .lx-extras { padding: 8px 22px; border-top: 1px solid var(--lx-line); }
  #lingxi-publish-focus-color .lx-extra-summary { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
  #lingxi-publish-focus-color .lx-extra-trigger { display: inline-flex; align-items: center; gap: 5px; color: var(--lx-subtle); background: transparent; border: 0; padding: 5px 0; font-size: 12px; }
  #lingxi-publish-focus-color .lx-tags { display: flex; flex-wrap: wrap; gap: 5px; }
  #lingxi-publish-focus-color .lx-tag { display: inline-flex; align-items: center; gap: 4px; border: 0; background: var(--lx-tint); color: var(--lx-green); border-radius: 5px; padding: 3px 6px; font-size: 11px; }
  #lingxi-publish-focus-color .lx-tag svg { width: 12px !important; height: 12px !important; }
  #lingxi-publish-focus-color .lx-extra-panel { margin: 12px 0 2px; padding: 13px; border: 1px solid var(--lx-line); border-radius: 9px; background: var(--lx-soft); display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
  #lingxi-publish-focus-color .lx-label { display: block; margin-bottom: 6px; font-size: 12px; color: var(--lx-ink); font-weight: 500; }
  #lingxi-publish-focus-color .lx-input { width: 100%; min-width: 0; height: 35px; padding: 6px 9px; border: 1px solid var(--lx-line); border-radius: 7px; background: var(--lx-surface); color: var(--lx-ink); font-size: 13px; }
  #lingxi-publish-focus-color .lx-inline-field { display: flex; gap: 6px; }
  #lingxi-publish-focus-color .lx-inline-field .lx-btn { flex: 0 0 auto; }
  #lingxi-publish-focus-color .lx-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 22px; border-top: 1px solid var(--lx-line); background: var(--lx-surface); border-radius: 0 0 14px 14px; }
  #lingxi-publish-focus-color .lx-saved { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--lx-subtle); }
  #lingxi-publish-focus-color .lx-saved svg { color: var(--lx-green); width: 14px; height: 14px; }
  #lingxi-publish-focus-color .lx-foot-actions { display: flex; align-items: center; gap: 8px; }
  #lingxi-publish-focus-color .lx-collapsed { padding: 4px 22px 19px; font-size: 13px; color: var(--lx-subtle); }
  #lingxi-publish-focus-color .lx-status { padding: 0 22px 12px; font-size: 12px; color: var(--lx-green); }
  #lingxi-publish-focus-color .lx-status:empty { display: none; }
  #lingxi-publish-focus-color .lx-is-expanded .lx-write,
  #lingxi-publish-focus-color .lx-is-expanded .lx-live-preview { min-height: 330px; }
  #lingxi-publish-focus-color .lx-drop-active { box-shadow: inset 0 0 0 2px var(--lx-green); background: var(--lx-tint); }
  #lingxi-publish-focus-color .lx-inline-dialog { margin: 8px 22px 14px; padding: 15px; background: var(--lx-tint); border: 1px solid var(--lx-line); border-radius: 10px; }
  #lingxi-publish-focus-color .lx-dialog-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 9px; font-weight: 500; font-size: 13px; }
  #lingxi-publish-focus-color .lx-dialog-fields { display: grid; grid-template-columns: 1fr 1.6fr; gap: 10px; }
  #lingxi-publish-focus-color .lx-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
  #lingxi-publish-focus-color .lx-error { color: var(--lx-danger); font-size: 12px; margin: 8px 0 0; }
  #lingxi-publish-focus-color .lx-error:empty { display: none; }

  #lingxi-publish-focus-color .lx-split { --lx-canvas: light-dark(#eef2f1, #16211d); --lx-soft: light-dark(#f1f6f3, #25332b); }
  #lingxi-publish-focus-color .lx-split .lx-head { padding-bottom: 18px; border-bottom: 1px solid var(--lx-line); }
  #lingxi-publish-focus-color .lx-split-grid { display: grid; grid-template-columns: minmax(0, 1fr) 242px; }
  #lingxi-publish-focus-color .lx-split .lx-writing-column { padding: 19px 0 0; min-width: 0; }
  #lingxi-publish-focus-color .lx-split .lx-mode-row { padding-bottom: 18px; }
  #lingxi-publish-focus-color .lx-split .lx-toolbar { border-top: 0; background: var(--lx-surface); padding-left: 16px; padding-right: 16px; }
  #lingxi-publish-focus-color .lx-split .lx-write,
  #lingxi-publish-focus-color .lx-split .lx-live-preview { min-height: 244px; }
  #lingxi-publish-focus-color .lx-split .lx-materials { padding: 18px 15px; border-top: 0; border-left: 1px solid var(--lx-line); }
  #lingxi-publish-focus-color .lx-split .lx-materials-head { margin-bottom: 16px; }
  #lingxi-publish-focus-color .lx-material-buttons { display: flex; align-items: center; gap: 7px; margin-bottom: 14px; }
  #lingxi-publish-focus-color .lx-material-buttons .lx-btn { flex: 1; }
  #lingxi-publish-focus-color .lx-split .lx-files { flex-direction: column; }
  #lingxi-publish-focus-color .lx-split .lx-file { width: 100%; padding: 9px 6px 9px 8px; }
  #lingxi-publish-focus-color .lx-split .lx-file-text { flex: 1; }
  #lingxi-publish-focus-color .lx-split .lx-extras { margin-top: 20px; padding: 14px 0 0; }
  #lingxi-publish-focus-color .lx-split .lx-extra-summary { align-items: flex-start; gap: 8px; flex-direction: column; }
  #lingxi-publish-focus-color .lx-split .lx-extra-panel { grid-template-columns: 1fr; padding: 10px; }
  #lingxi-publish-focus-color .lx-split .lx-inline-dialog { margin-top: 14px; }
  #lingxi-publish-focus-color .lx-split .lx-foot { background: var(--lx-soft); }
  #lingxi-publish-focus-color .lx-split .lx-is-expanded .lx-write,
  #lingxi-publish-focus-color .lx-split .lx-is-expanded .lx-live-preview { min-height: 350px; }

  #lingxi-publish-focus-color .lx-focus {
    --lx-canvas: light-dark(#edf4f0, #14221b);
    --lx-surface: light-dark(#ffffff, #1e2d25);
    --lx-raised: light-dark(#ffffff, #2a3d31);
    --lx-soft: light-dark(#eff6f2, #25382d);
    --lx-tint: light-dark(#deeee4, #304c3b);
    --lx-ink: light-dark(#243b2e, #e7f0ea);
    --lx-subtle: light-dark(#5b7062, #adbdaf);
    --lx-line: light-dark(#d5e3da, #3c5243);
    --lx-green: light-dark(#2e6b50, #98ccb0);
    --lx-on-green: light-dark(#ffffff, #173222);
    --lx-shadow: light-dark(#1d493012, #00000038);
  }
  #lingxi-publish-focus-color .lx-focus .lx-composer { border: 0; border-radius: 9px; box-shadow: 0 5px 22px var(--lx-shadow); }
  #lingxi-publish-focus-color .lx-focus .lx-head { padding: 19px 26px 15px; }
  #lingxi-publish-focus-color .lx-focus .lx-mode-row { padding: 0 26px; }
  #lingxi-publish-focus-color .lx-focus .lx-modes { padding: 0; gap: 22px; background: transparent; border-radius: 0; }
  #lingxi-publish-focus-color .lx-focus .lx-mode { padding: 10px 1px; border-radius: 0; border-bottom: 2px solid transparent; background: transparent; }
  #lingxi-publish-focus-color .lx-focus .lx-mode[aria-pressed="true"] { background: transparent; box-shadow: none; border-bottom-color: var(--lx-green); }
  #lingxi-publish-focus-color .lx-focus .lx-write,
  #lingxi-publish-focus-color .lx-focus .lx-live-preview { min-height: 225px; padding: 25px 27px 12px; }
  #lingxi-publish-focus-color .lx-focus .lx-write-meta { padding-left: 27px; padding-right: 27px; }
  #lingxi-publish-focus-color .lx-focus .lx-toolbar { margin: 0 26px; padding: 9px 0; background: transparent; border-top: 1px solid var(--lx-line); border-bottom: 0; }
  #lingxi-publish-focus-color .lx-focus-bar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 20px; background: var(--lx-soft); border-top: 1px solid var(--lx-line); }
  #lingxi-publish-focus-color .lx-focus-left { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
  #lingxi-publish-focus-color .lx-focus .lx-materials { border-top: 0; background: var(--lx-soft); padding-top: 0; }
  #lingxi-publish-focus-color .lx-focus .lx-files { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  #lingxi-publish-focus-color .lx-focus .lx-file-text { flex: 1; }
  #lingxi-publish-focus-color .lx-focus .lx-extras { padding: 0; border: 0; }
  #lingxi-publish-focus-color .lx-focus .lx-extra-panel { margin: 12px 22px; }
  #lingxi-publish-focus-color .lx-focus .lx-foot { border-radius: 0 0 9px 9px; }
  #lingxi-publish-focus-color .lx-focus .lx-is-expanded .lx-write,
  #lingxi-publish-focus-color .lx-focus .lx-is-expanded .lx-live-preview { min-height: 370px; }
  #lingxi-publish-focus-color .lx-focus .lx-add-menu { top: auto; bottom: 40px; }
  #lingxi-publish-focus-color .lx-tray-summary { color: var(--lx-subtle); font-size: 12px; }

  @media (max-width: 800px) {
    #lingxi-publish-focus-color .lx-stage { padding: 20px 17px; min-height: 730px; }
    #lingxi-publish-focus-color .lx-split-grid { grid-template-columns: minmax(0, 1fr) 210px; }
    #lingxi-publish-focus-color .lx-split .lx-materials { padding-left: 12px; padding-right: 12px; }
    #lingxi-publish-focus-color .lx-split .lx-head { padding-left: 18px; padding-right: 18px; }
    #lingxi-publish-focus-color .lx-split .lx-toolbar { flex-wrap: wrap; }
    #lingxi-publish-focus-color .lx-split .lx-toolbar .lx-preview-btn { margin-left: auto; }
    #lingxi-publish-focus-color .lx-shortcut { display: none; }
  }
  @media (max-width: 620px) {
    #lingxi-publish-focus-color .lx-appbar { padding: 0 15px; height: 49px; }
    #lingxi-publish-focus-color .lx-app-context { font-size: 11px; }
    #lingxi-publish-focus-color .lx-stage { padding: 14px 10px; min-height: 950px; }
    #lingxi-publish-focus-color .lx-head { padding: 16px 15px 14px; }
    #lingxi-publish-focus-color .lx-mode-row { padding: 0 15px 14px; }
    #lingxi-publish-focus-color .lx-toolbar { padding: 7px 9px; flex-wrap: wrap; }
    #lingxi-publish-focus-color .lx-formatting { gap: 0; }
    #lingxi-publish-focus-color .lx-divider { margin-left: 3px; margin-right: 3px; }
    #lingxi-publish-focus-color .lx-toolbar .lx-preview-btn { margin-left: auto; }
    #lingxi-publish-focus-color .lx-write,
    #lingxi-publish-focus-color .lx-live-preview { padding: 18px 16px 10px; min-height: 224px; font-size: 16px; }
    #lingxi-publish-focus-color .lx-write-meta { padding: 6px 16px 12px; }
    #lingxi-publish-focus-color .lx-materials { padding: 13px 15px; }
    #lingxi-publish-focus-color .lx-materials-head { flex-wrap: wrap; }
    #lingxi-publish-focus-color .lx-materials-hint { display: none; }
    #lingxi-publish-focus-color .lx-extras { padding: 10px 15px; }
    #lingxi-publish-focus-color .lx-file { width: 100%; }
    #lingxi-publish-focus-color .lx-file-text { flex: 1; }
    #lingxi-publish-focus-color .lx-foot { padding: 13px 15px; gap: 8px; flex-wrap: wrap; }
    #lingxi-publish-focus-color .lx-foot-actions { margin-left: auto; }
    #lingxi-publish-focus-color .lx-saved { font-size: 11px; }
    #lingxi-publish-focus-color .lx-extra-panel,
    #lingxi-publish-focus-color .lx-dialog-fields { grid-template-columns: 1fr; }
    #lingxi-publish-focus-color .lx-inline-dialog { margin-left: 15px; margin-right: 15px; }
    #lingxi-publish-focus-color .lx-input { font-size: 16px; min-height: 39px; }
    #lingxi-publish-focus-color .lx-split-grid { grid-template-columns: 1fr; }
    #lingxi-publish-focus-color .lx-split .lx-materials { border-left: 0; border-top: 1px solid var(--lx-line); padding: 15px; }
    #lingxi-publish-focus-color .lx-split .lx-files { flex-direction: row; }
    #lingxi-publish-focus-color .lx-split .lx-materials-head { margin-bottom: 10px; }
    #lingxi-publish-focus-color .lx-split .lx-extras { margin-top: 12px; }
    #lingxi-publish-focus-color .lx-split .lx-write,
    #lingxi-publish-focus-color .lx-split .lx-live-preview { min-height: 220px; }
    #lingxi-publish-focus-color .lx-split .lx-extra-summary { flex-direction: row; align-items: center; }
    #lingxi-publish-focus-color .lx-focus .lx-head { padding: 16px 16px 13px; }
    #lingxi-publish-focus-color .lx-focus .lx-mode-row { padding: 0 16px; }
    #lingxi-publish-focus-color .lx-focus .lx-write,
    #lingxi-publish-focus-color .lx-focus .lx-live-preview { padding: 19px 17px 10px; min-height: 260px; }
    #lingxi-publish-focus-color .lx-focus .lx-toolbar { margin: 0 16px; }
    #lingxi-publish-focus-color .lx-focus-bar { padding: 11px 13px; }
    #lingxi-publish-focus-color .lx-focus .lx-files { grid-template-columns: 1fr; }
    #lingxi-publish-focus-color .lx-focus .lx-extra-panel { margin: 12px 15px; }
    #lingxi-publish-focus-color .lx-tray-summary { display: none; }
  }
  @media (pointer: coarse) {
    #lingxi-publish-focus-color .lx-icon { min-width: 38px; min-height: 40px; }
    #lingxi-publish-focus-color .lx-btn,
    #lingxi-publish-focus-color .lx-mode,
    #lingxi-publish-focus-color .lx-extra-trigger { min-height: 44px; }
    #lingxi-publish-focus-color .lx-input { min-height: 44px; font-size: 16px; }
    #lingxi-publish-focus-color .lx-file .lx-icon { min-width: 36px; min-height: 40px; }
  }
</style>

<div id="lingxi-publish-focus-color" aria-label="灵犀智学专注画布配色优化">
  <section data-variant="专注画布" aria-label="专注画布方案">
    <div class="lx-shell lx-focus" data-layout="focus">
      <header class="lx-appbar"><div class="lx-brand"><span class="lx-brandmark"><i data-lucide="sprout" aria-hidden="true"></i></span>灵犀智学</div></header>
      <div class="lx-stage"><article class="lx-composer" aria-label="专注式发布编辑器"></article></div>
    </div>
  </section>
</div>

<script>
(() => {
  const root = document.getElementById('lingxi-publish-focus-color');
  if (!root) return;
  const sampleText = '关于子网划分,我想和大家讨论一个问题:\n\n把一个 /24 的网段划分成 4 个等大的子网时,为什么掩码会变成 /26?我整理了一份练习,欢迎分享你的理解。';
  const icon = name => '<i data-lucide="' + name + '" aria-hidden="true"></i>';
  const safe = value => String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  const icons = () => { if (globalThis.lucide) globalThis.lucide.createIcons({ attrs: { width: 16, height: 16 } }); };
  const button = (action, name, label, cls = 'lx-btn') => '<button type="button" class="' + cls + ' cursor-interaction" data-action="' + action + '">' + icon(name) + '<span>' + label + '</span></button>';
  const iconButton = (action, name, label) => '<button type="button" class="lx-icon cursor-interaction" data-action="' + action + '" aria-label="' + label + '" data-tooltip="' + label + '">' + icon(name) + '</button>';
  const formatButton = (format, name, label) => '<button type="button" class="lx-icon cursor-interaction" data-format="' + format + '" aria-label="' + label + '" data-tooltip="' + label + '">' + icon(name) + '</button>';

  root.querySelectorAll('.lx-shell').forEach((shell, index) => {
    const layout = shell.dataset.layout;
    const id = 'lx-' + layout;
    const app = shell.querySelector('.lx-composer');
    const state = {
      mode: 'discussion', preview: false, expanded: false, collapsed: false, trayOpen: false,
      files: [
        { id: id + '-sample-pdf', kind: 'file', name: '子网划分练习.pdf', meta: 'PDF · 428 KB · 示例' },
        { id: id + '-sample-link', kind: 'link', name: '课堂参考链接', meta: 'example.com · 示例', url: 'https://example.com/subnet' }
      ], tags: ['子网划分'], refs: [], inlineImages: [], linkTarget: 'material', linkSelection: [0, 0], sequence: 0
    };
    const author = '<header class="lx-head"><div class="lx-author"><span class="lx-avatar" aria-hidden="true">L</span><div><span class="lx-author-name">lyy</span><span class="lx-meta">' + icon('users-round') + '本课程成员可见</span></div></div><div class="lx-head-actions">' + iconButton('expand','maximize-2','展开编辑区') + iconButton('collapse','chevron-up','收起编辑器') + '</div></header>';
    const modes = '<div class="lx-mode-row"><div class="lx-modes" role="group" aria-label="发布类型"><button type="button" class="lx-mode cursor-interaction" data-mode="discussion" aria-pressed="true">' + icon('messages-square') + '自由讨论</button><button type="button" class="lx-mode cursor-interaction" data-mode="assignment" aria-pressed="false">' + icon('notebook-pen') + '正式作业</button></div></div>';
    const toolbar = '<div class="lx-toolbar"><div class="lx-formatting" role="group" aria-label="正文格式">' + formatButton('heading','type','标题格式') + '<span class="lx-divider" aria-hidden="true"></span>' + formatButton('bold','bold','加粗') + formatButton('italic','italic','斜体') + formatButton('strike','strikethrough','删除线') + '<span class="lx-divider" aria-hidden="true"></span>' + formatButton('bullet','list','无序列表') + formatButton('ordered','list-ordered','有序列表') + formatButton('task','list-checks','任务列表') + formatButton('code','code','行内代码') + iconButton('body-link','link','插入正文超链接') + '</div>' + button('preview','eye','预览','lx-btn lx-btn-quiet lx-preview-btn') + '</div>';
    const writing = '<div class="lx-editor" data-dropzone><textarea class="lx-write" aria-label="发布正文" maxlength="20000" placeholder="写下你的想法,支持 Markdown…">' + safe(sampleText) + '</textarea><div class="lx-live-preview" aria-label="正文预览" hidden></div><div class="lx-write-meta"><span class="lx-shortcut">Markdown · ⌘ / Ctrl + Enter 发布</span><span class="lx-counter"><span data-count>0</span> / 20,000</span></div></div>';
    const addMenu = '<div class="lx-add-wrap">' + button('add-menu','plus','添加材料','lx-btn lx-btn-tint') + '<div class="lx-add-menu" aria-label="添加材料" hidden><button type="button" class="cursor-interaction" data-action="file">' + icon('paperclip') + '上传文件或图片</button><button type="button" class="cursor-interaction" data-action="material-link">' + icon('link') + '附加参考链接</button></div></div>';
    const files = '<div class="lx-files" aria-label="已添加材料"></div>';
    const extrasTrigger = '<div class="lx-extra-summary"><button type="button" class="lx-extra-trigger cursor-interaction" data-action="extras" aria-expanded="false" aria-controls="' + id + '-extras">' + icon('tag') + '标签与资料引用' + icon('chevron-down') + '</button><div class="lx-tags" aria-label="已选标签及引用"></div></div>';
    const extrasPanel = '<div id="' + id + '-extras" class="lx-extra-panel" hidden><div><label class="lx-label" for="' + id + '-tag-input">添加标签</label><div class="lx-inline-field"><input class="lx-input" id="' + id + '-tag-input" data-tag-input maxlength="20" placeholder="如:网络层">' + button('add-tag','plus','添加') + '</div></div><div><label class="lx-label" for="' + id + '-ref-input">引用作业或资料</label><div class="lx-inline-field"><select class="lx-input" id="' + id + '-ref-input" data-ref-input><option value="">选择资料(示例)</option>第四章:网络层讲义</option><option>第三次作业:IP 地址与子网</option></select>' + button('add-ref','plus','引用') + '</div></div></div>';
    const extras = '<div class="lx-extras">' + extrasTrigger + extrasPanel + '</div>';
    const compactMaterials = '<section class="lx-materials" aria-label="附件材料"><div class="lx-materials-head">' + addMenu + '<span class="lx-meta lx-materials-hint">' + icon('paperclip') + '文件、图片、链接,统一收纳</span></div>' + files + '</section>';
    const sideMaterials = '<aside class="lx-materials" aria-label="发布材料侧栏"><div class="lx-materials-head"><span class="lx-section-label">' + icon('folder-open') + '附加材料 <span class="lx-count-badge" data-material-count>2</span></span></div><div class="lx-material-buttons">' + button('file','upload','文件') + button('material-link','link','链接') + '</div>' + files + extras + '</aside>';
    const focusMaterials = '<div class="lx-focus-bar"><div class="lx-focus-left">' + addMenu + '<button type="button" class="lx-btn lx-btn-quiet cursor-interaction" data-action="tray" aria-expanded="false" aria-controls="' + id + '-tray">' + icon('paperclip') + '<span data-tray-label>材料 2</span>' + icon('chevron-down') + '</button><span class="lx-tray-summary">1 个文件 · 1 个链接</span></div></div><section class="lx-materials" id="' + id + '-tray" aria-label="材料托盘" hidden>' + files + '</section><div class="lx-extras" style="padding: 10px 22px;">' + extrasTrigger + '</div>' + extrasPanel;
    const linkDialog = '<form class="lx-inline-dialog" data-link-dialog hidden><div class="lx-dialog-head"><span data-link-heading>附加参考链接</span>' + iconButton('close-link','x','关闭链接输入') + '</div><div class="lx-dialog-fields"><div><label class="lx-label" for="' + id + '-link-name">链接名称 <span class="lx-meta">选填</span></label><input class="lx-input" id="' + id + '-link-name" data-link-name maxlength="100" placeholder="例如:课堂参考资料"></div><div><label class="lx-label" for="' + id + '-link-url">链接地址</label><input class="lx-input" id="' + id + '-link-url" data-link-url type="url" placeholder="https://" required></div></div><p class="lx-error" data-link-error role="alert"></p><div class="lx-dialog-actions">' + button('close-link','x','取消','lx-btn lx-btn-quiet') + '<button type="submit" class="lx-btn lx-btn-primary cursor-interaction" data-link-submit>添加链接</button></div></form>';
    const foot = '<footer class="lx-foot"><span class="lx-saved">' + icon('check') + '<span data-save-status>草稿已暂存</span></span><div class="lx-foot-actions"><span class="lx-meta lx-shortcut">Esc 离开编辑器</span>' + button('publish','arrow-up-right','发布讨论','lx-btn lx-btn-primary lx-publish') + '</div></footer>';
    const compact = modes + toolbar + writing + compactMaterials + extras;
    const split = '<div class="lx-split-grid"><div class="lx-writing-column">' + modes + toolbar + writing + '</div>' + sideMaterials + '</div>';
    const focus = modes + writing + toolbar + focusMaterials;
    app.innerHTML = author + '<div class="lx-compose-content">' + (layout === 'split' ? split : layout === 'focus' ? focus : compact) + linkDialog + '<div class="lx-status" role="status" aria-live="polite"></div>' + foot + '</div><div class="lx-collapsed" hidden>草稿已收起,点击右上角继续编辑。</div><input type="file" data-file-input multiple hidden aria-label="选择附件文件">';
    const textArea = app.querySelector('textarea');
    const preview = app.querySelector('.lx-live-preview');
    const status = app.querySelector('.lx-status');
    const linkForm = app.querySelector('[data-link-dialog]');
    const fileInput = app.querySelector('[data-file-input]');
    const setStatus = text => { status.textContent = text; };

    function inlineMarkdown(text) {
      return safe(text).replace(/`([^`]+)`/g, '<code>$1</code>')
        .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
        .replace(/\*([^*]+)\*/g, '<em>$1</em>')
        .replace(/~~([^~]+)~~/g, '<del>$1</del>')
        .replace(/!\[([^\]]*)\]\(local-image:([\w-]+)\)/g, (_, alt, key) => {
          const local = state.inlineImages.find(item => item.key === key);
          return local ? '<img src="' + safe(local.url) + '" alt="' + alt + '">' : alt;
        })
        .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
    }
    function renderMarkdown(text) {
      if (!text.trim()) return '<p class="lx-empty">正文预览会显示在这里。</p>';
      return text.split(/\n\n+/).map(block => {
        if (/^#{1,6}\s/.test(block)) return '<h3>' + inlineMarkdown(block.replace(/^#{1,6}\s/, '')) + '</h3>';
        const lines = block.split('\n');
        if (lines.every(line => /^- \[[ x]\] /.test(line))) return '<ul>' + lines.map(line => '<li>' + (line.startsWith('- [x]') ? '已完成:' : '待完成:') + inlineMarkdown(line.slice(6)) + '</li>').join('') + '</ul>';
        if (lines.every(line => /^- /.test(line))) return '<ul>' + lines.map(line => '<li>' + inlineMarkdown(line.slice(2)) + '</li>').join('') + '</ul>';
        if (lines.every(line => /^\d+\. /.test(line))) return '<ol>' + lines.map(line => '<li>' + inlineMarkdown(line.replace(/^\d+\. /, '')) + '</li>').join('') + '</ol>';
        return '<p>' + lines.map(inlineMarkdown).join('<br>') + '</p>';
      }).join('');
    }
    function updateText() {
      app.querySelector('[data-count]').textContent = textArea.value.length.toLocaleString('en-US');
      preview.innerHTML = renderMarkdown(textArea.value);
      app.querySelector('[data-save-status]').textContent = '草稿已暂存';
      app.querySelector('.lx-publish').disabled = !textArea.value.trim() && state.files.length === 0;
    }
    function drawFiles() {
      const list = app.querySelector('.lx-files');
      list.hidden = layout === 'compact' && state.files.length === 0;
      app.querySelector('.lx-materials').classList.toggle('lx-no-materials', state.files.length === 0);
      if (!state.files.length) {
        list.innerHTML = '<p class="lx-empty">还没有材料,可添加文件或参考链接。</p>';
      } else {
        list.innerHTML = state.files.map(file => '<div class="lx-file"><span class="lx-file-kind">' + icon(file.kind === 'link' ? 'link' : 'file-text') + '</span><div class="lx-file-text"><span class="lx-file-name">' + safe(file.name) + '</span><span class="lx-file-size">' + safe(file.meta) + '</span></div><button type="button" class="lx-icon cursor-interaction" data-remove-file="' + safe(file.id) + '" aria-label="移除' + safe(file.name) + '" data-tooltip="移除材料">' + icon('x') + '</button></div>').join('');
      }
      app.querySelectorAll('[data-material-count]').forEach(el => { el.textContent = state.files.length; });
      const trayLabel = app.querySelector('[data-tray-label]');
      if (trayLabel) trayLabel.textContent = '材料 ' + state.files.length;
      const traySummary = app.querySelector('.lx-tray-summary');
      if (traySummary) traySummary.textContent = state.files.length ? state.files.filter(file => file.kind === 'file').length + ' 个文件 · ' + state.files.filter(file => file.kind === 'link').length + ' 个链接' : '还没有附加材料';
      updateText(); icons();
    }
    function drawTags() {
      const tags = app.querySelector('.lx-tags');
      tags.innerHTML = state.tags.map((tag, n) => '<button type="button" class="lx-tag cursor-interaction" data-remove-tag="' + n + '" aria-label="移除标签' + safe(tag) + '"># ' + safe(tag) + icon('x') + '</button>').join('') + state.refs.map((ref, n) => '<button type="button" class="lx-tag cursor-interaction" data-remove-ref="' + n + '" aria-label="移除引用' + safe(ref) + '">' + icon('book-open') + safe(ref) + icon('x') + '</button>').join('');
      icons();
    }
    function closeMenus() {
      app.querySelectorAll('.lx-add-menu').forEach(el => { el.hidden = true; });
      app.querySelectorAll('[data-action="add-menu"]').forEach(el => { el.setAttribute('aria-expanded', 'false'); });
    }
    function openLink(target) {
      state.linkTarget = target;
      state.linkSelection = [textArea.selectionStart, textArea.selectionEnd];
      textArea.readOnly = target === 'body';
      closeMenus();
      linkForm.hidden = false;
      app.querySelector('[data-link-heading]').textContent = target === 'body' ? '插入正文超链接' : '附加参考链接';
      app.querySelector('[data-link-submit]').textContent = target === 'body' ? '插入正文' : '添加到材料';
      app.querySelector('[data-link-error]').textContent = '';
      app.querySelector('[data-link-name]').value = target === 'body' ? textArea.value.slice(textArea.selectionStart, textArea.selectionEnd) : '';
      app.querySelector('[data-link-url]').value = '';
      app.querySelector('[data-link-url]').focus();
    }
    function setTray(open) {
      if (layout !== 'focus') return;
      state.trayOpen = open;
      app.querySelector('#' + id + '-tray').hidden = !open;
      const trigger = app.querySelector('[data-action="tray"]');
      trigger.setAttribute('aria-expanded', String(open));
    }
    function acceptFiles(incoming, placeImagesInBody = false) {
      const batch = Array.from(incoming || []);
      if (!batch.length) return;
      let insertedImages = 0;
      let addedMaterials = 0;
      for (const file of batch) {
        if (placeImagesInBody && file.type.startsWith('image/') && !textArea.readOnly) {
          const key = id + '-image-' + (++state.sequence);
          const token = '\n\n![' + file.name.replace(/[\[\]]/g, '') + '](local-image:' + key + ')\n';
          if (textArea.value.length + token.length <= 20000) {
            state.inlineImages.push({ key, url: URL.createObjectURL(file) });
            textArea.setRangeText(token, textArea.selectionStart, textArea.selectionEnd, 'end');
            insertedImages += 1;
            continue;
          }
        }
        const extension = file.name.includes('.') ? file.name.split('.').pop().toUpperCase() : 'FILE';
        const size = file.size >= 1048576 ? (file.size / 1048576).toFixed(1) + ' MB' : Math.max(1, Math.round(file.size / 1024)) + ' KB';
        state.files.push({ id: id + '-local-' + (++state.sequence), kind: 'file', name: file.name, meta: extension + ' · ' + size + ' · 本地预览' });
        addedMaterials += 1;
      }
      drawFiles();
      if (addedMaterials) setTray(true);
      setStatus(insertedImages ? '已将 ' + insertedImages + ' 张图片插入正文,可点击预览查看。' + (addedMaterials ? '另添加 ' + addedMaterials + ' 份材料。' : '') : '已添加 ' + addedMaterials + ' 个文件,可继续编辑或移除。');
    }
    function insertFormat(format) {
      if (textArea.readOnly) return;
      if (state.preview) togglePreview();
      const start = textArea.selectionStart, end = textArea.selectionEnd;
      const selected = textArea.value.slice(start, end) || '文字';
      const forms = { bold: '**' + selected + '**', italic: '*' + selected + '*', strike: '~~' + selected + '~~', heading: '## ' + selected, bullet: '- ' + selected, ordered: '1. ' + selected, task: '- [ ] ' + selected, code: '`' + selected + '`' };
      const replacement = forms[format];
      if (!replacement || textArea.value.length - (end - start) + replacement.length > 20000) return;
      textArea.setRangeText(replacement, start, end, 'select'); textArea.focus(); updateText();
    }
    function togglePreview() {
      state.preview = !state.preview;
      textArea.hidden = state.preview; preview.hidden = !state.preview;
      const trigger = app.querySelector('[data-action="preview"]');
      trigger.innerHTML = icon(state.preview ? 'pencil' : 'eye') + '<span>' + (state.preview ? '继续编辑' : '预览') + '</span>';
      trigger.setAttribute('aria-pressed', String(state.preview));
      updateText(); icons();
    }
    function publish() {
      if (!textArea.value.trim() && !state.files.length) { setStatus('请先填写正文或添加材料。'); textArea.focus(); return; }
      setStatus('发布演示完成:' + (state.mode === 'assignment' ? '正式作业' : '自由讨论') + ' · ' + state.files.length + ' 份材料。内容未提交到课程。');
    }
    app.addEventListener('click', event => {
      const control = event.target.closest('button');
      if (!control || !app.contains(control)) return;
      if (control.dataset.mode) {
        state.mode = control.dataset.mode;
        app.querySelectorAll('[data-mode]').forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.mode === state.mode)));
        app.querySelector('.lx-publish span').textContent = state.mode === 'assignment' ? '发布作业' : '发布讨论';
        textArea.placeholder = state.mode === 'assignment' ? '写下作业内容与要求,支持 Markdown…' : '写下你的想法,支持 Markdown…';
        setStatus(''); return;
      }
      if (control.dataset.format) { insertFormat(control.dataset.format); return; }
      if (control.dataset.removeFile) { state.files = state.files.filter(file => file.id !== control.dataset.removeFile); drawFiles(); setStatus('材料已移除。'); return; }
      if (control.dataset.removeTag !== undefined) { state.tags.splice(Number(control.dataset.removeTag), 1); drawTags(); return; }
      if (control.dataset.removeRef !== undefined) { state.refs.splice(Number(control.dataset.removeRef), 1); drawTags(); return; }
      switch (control.dataset.action) {
        case 'add-menu': {
          const menu = control.parentElement.querySelector('.lx-add-menu');
          const open = menu.hidden;
          closeMenus(); menu.hidden = !open; control.setAttribute('aria-expanded', String(open)); break;
        }
        case 'file': closeMenus(); fileInput.click(); break;
        case 'material-link': openLink('material'); break;
        case 'body-link': openLink('body'); break;
        case 'close-link': linkForm.hidden = true; textArea.readOnly = false; break;
        case 'preview': togglePreview(); break;
        case 'tray': setTray(!state.trayOpen); break;
        case 'extras': {
          const panel = app.querySelector('#' + id + '-extras');
          panel.hidden = !panel.hidden; control.setAttribute('aria-expanded', String(!panel.hidden)); break;
        }
        case 'add-tag': {
          const input = app.querySelector('[data-tag-input]');
          const value = input.value.trim().replace(/^#/, '');
          if (value && !state.tags.includes(value)) state.tags.push(value);
          input.value = ''; drawTags(); break;
        }
        case 'add-ref': {
          const input = app.querySelector('[data-ref-input]');
          if (input.value && !state.refs.includes(input.value)) state.refs.push(input.value);
          input.value = ''; drawTags(); break;
        }
        case 'expand': {
          state.expanded = !state.expanded;
          app.classList.toggle('lx-is-expanded', state.expanded);
          control.innerHTML = icon(state.expanded ? 'minimize-2' : 'maximize-2');
          control.setAttribute('aria-label', state.expanded ? '恢复紧凑编辑区' : '展开编辑区');
          control.dataset.tooltip = state.expanded ? '恢复紧凑编辑区' : '展开编辑区';
          control.setAttribute('aria-pressed', String(state.expanded)); icons(); break;
        }
        case 'collapse': {
          state.collapsed = !state.collapsed;
          shell.classList.toggle('lx-collapsed-shell', state.collapsed);
          app.querySelector('.lx-compose-content').hidden = state.collapsed;
          app.querySelector('.lx-collapsed').hidden = !state.collapsed;
          control.innerHTML = icon(state.collapsed ? 'chevron-down' : 'chevron-up');
          control.setAttribute('aria-label', state.collapsed ? '展开编辑器' : '收起编辑器');
          control.dataset.tooltip = state.collapsed ? '展开编辑器' : '收起编辑器';
          control.setAttribute('aria-expanded', String(!state.collapsed)); icons(); break;
        }
        case 'publish': publish(); break;
      }
    });
    textArea.addEventListener('input', () => { updateText(); setStatus(''); });
    textArea.addEventListener('paste', event => {
      if (event.clipboardData && event.clipboardData.files.length) { event.preventDefault(); acceptFiles(event.clipboardData.files, true); }
    });
    fileInput.addEventListener('change', () => { acceptFiles(fileInput.files); fileInput.value = ''; });
    const dropzone = app.querySelector('[data-dropzone]');
    dropzone.addEventListener('dragover', event => { if (event.dataTransfer && Array.from(event.dataTransfer.types).includes('Files')) { event.preventDefault(); dropzone.classList.add('lx-drop-active'); } });
    dropzone.addEventListener('dragleave', event => { if (!dropzone.contains(event.relatedTarget)) dropzone.classList.remove('lx-drop-active'); });
    dropzone.addEventListener('drop', event => { event.preventDefault(); dropzone.classList.remove('lx-drop-active'); acceptFiles(event.dataTransfer.files, true); });
    app.addEventListener('keydown', event => {
      if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); publish(); }
      if (event.key === 'Escape') {
        closeMenus();
        if (!linkForm.hidden) { linkForm.hidden = true; textArea.readOnly = false; }
        else if (event.target === textArea) textArea.blur();
      }
      if (event.target.matches('[data-tag-input]') && event.key === 'Enter') { event.preventDefault(); app.querySelector('[data-action="add-tag"]').click(); }
    });
    linkForm.addEventListener('submit', event => {
      event.preventDefault();
      const raw = app.querySelector('[data-link-url]').value.trim();
      let url;
      try { url = new URL(raw); if (!['http:', 'https:'].includes(url.protocol)) throw new Error('scheme'); }
      catch { app.querySelector('[data-link-error]').textContent = '请输入以 http:// 或 https:// 开头的有效链接。'; return; }
      const name = app.querySelector('[data-link-name]').value.trim() || url.hostname;
      if (state.linkTarget === 'body') {
        const replacement = '[' + name.replace(/[\[\]]/g, '') + '](' + url.href.replace(/\)/g, '%29') + ')';
        const [start, end] = state.linkSelection;
        if (textArea.value.length - (end - start) + replacement.length > 20000) { app.querySelector('[data-link-error]').textContent = '正文已达到 20,000 字上限。'; return; }
        textArea.setRangeText(replacement, start, end, 'end');
        if (state.preview) togglePreview();
        updateText(); setStatus('链接已插入正文。');
      } else {
        state.files.push({ id: id + '-link-' + (++state.sequence), kind: 'link', name, meta: url.hostname, url: url.href });
        drawFiles(); setTray(true); setStatus('链接已添加到材料。');
      }
      linkForm.hidden = true;
      textArea.readOnly = false;
    });
    window.addEventListener('pagehide', () => { state.inlineImages.forEach(item => URL.revokeObjectURL(item.url)); });
    document.addEventListener('click', event => { if (!event.target.closest('.lx-add-wrap')) closeMenus(); });
    app.querySelectorAll('[data-action="add-menu"]').forEach(el => { el.setAttribute('aria-expanded', 'false'); });
    drawFiles(); drawTags(); updateText();
  });
  icons();
})();
</script>

Activity

  1. added a commit that references this issue on Oct 19, 2026
  2. lyyzka commented on Oct 19, 2026

    @lyyzka
    ContributorAuthor

    阶段进度:PR188 已审查并合入 main(26d485dd),Web/Admin CI 成功;原作者官方浏览器3/3、头像回归1/1均零跳过,截图与命令保留在PR。当前仅完成开发合流,生产尚未部署;issue 保持开放,统一发布后回填实际版本与验收。

  3. lyyzka commented on Oct 19, 2026

    @lyyzka
    ContributorAuthor

    本轮已通过 发布 PR194 正常上线;发布工作流 成功,Komodo 已返回 Complete / success=true。生产实际镜像、Web/API 与 Worker readiness、版本及迁移均已核验,源码为 b76c4cc0,正式 LingxiOS 3.5.3。

    关闭依据:PR188 已落实专注画布、统一材料入口、紧凑展开/收起及明暗主题,保留真实上传、草稿与发布流程。正式包浏览器专项 3/3、零跳过,覆盖材料/正文链接、上传与发布失败后的状态保留、草稿恢复,以及桌面和窄屏明暗外观。另有论坛范围收敛集成 10/10 和浏览器 1/1。

    论坛浏览器报告与截图可复核;功能证据来自隔离环境,生产版本及 readiness 已确认。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions