feat: 管理端板块管理与旧版数据导入,补充部署运营文档
- 新增管理端板块管理页面与后端接口(admin_board) - 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具 - 聊天用户卡片、板块图标等 UI 组件与界面优化 - 补充 about/公告/1Panel 部署等文档 - gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { ArrowLeft, Copy, ExternalLink, Loader2 } from "lucide-react";
|
||||
import { ArrowLeft, Copy, ExternalLink, Loader2, Megaphone } from "lucide-react";
|
||||
import {
|
||||
apiAdminCreateAnnouncement,
|
||||
apiAdminUpdateAnnouncement,
|
||||
@@ -246,34 +246,68 @@ export default function AnnouncementComposeClient({
|
||||
|
||||
return (
|
||||
<div className="admin-doc-page admin-doc-editor">
|
||||
<header className="admin-doc-head">
|
||||
<div className="min-w-0">
|
||||
<Link href={backHref} className="admin-doc-back">
|
||||
<ArrowLeft size={14} aria-hidden /> 返回列表
|
||||
</Link>
|
||||
<div className="admin-doc-head-title">
|
||||
<header className="admin-doc-sticky">
|
||||
<div className="admin-doc-sticky-inner">
|
||||
<div className="admin-doc-sticky-title">
|
||||
<Link href={backHref} className="admin-doc-back" aria-label="返回公告列表">
|
||||
<ArrowLeft size={15} aria-hidden />
|
||||
<span className="admin-doc-back-text">返回列表</span>
|
||||
</Link>
|
||||
<span className="admin-doc-sticky-icon" aria-hidden>
|
||||
<Megaphone size={15} />
|
||||
</span>
|
||||
<h1>{isEdit ? "编辑公告" : "新建公告"}</h1>
|
||||
<AdminStatusChip tone={savedPublished ? "ok" : savedPublished === false ? "warn" : "accent"}>
|
||||
{savedPublished ? "已发布" : savedPublished === false ? "草稿" : "尚未保存"}
|
||||
</AdminStatusChip>
|
||||
</div>
|
||||
<div className="admin-doc-sticky-actions">
|
||||
{canViewOnline ? (
|
||||
<a
|
||||
href={publicPath}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-line btn-sm"
|
||||
title="打开已发布内容。未保存的修改不会出现在这里。"
|
||||
>
|
||||
<ExternalLink size={14} aria-hidden /> 线上页面
|
||||
</a>
|
||||
) : null}
|
||||
<AdminSegmented<PublishKey>
|
||||
role="group"
|
||||
ariaLabel="保存后状态"
|
||||
size="sm"
|
||||
value={form.published ? "published" : "draft"}
|
||||
onChange={(key) => patchForm({ published: key === "published" })}
|
||||
options={[
|
||||
{ key: "draft", label: "草稿" },
|
||||
{ key: "published", label: "发布" },
|
||||
]}
|
||||
/>
|
||||
<button type="button" className="btn btn-line btn-sm" disabled={!dirty || saving} onClick={discard}>
|
||||
撤销修改
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={!dirty || saving}
|
||||
onClick={requestSave}
|
||||
aria-busy={saving || undefined}
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" aria-hidden /> : null}
|
||||
{saving ? "保存中…" : saveLabel}
|
||||
</button>
|
||||
<p className={`admin-doc-status${errors.form ? " is-error" : ""}`} aria-live="polite">
|
||||
{statusText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{canViewOnline ? (
|
||||
<a
|
||||
href={publicPath}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-line"
|
||||
title="打开已发布内容。未保存的修改不会出现在这里。"
|
||||
>
|
||||
<ExternalLink size={14} aria-hidden /> 查看线上页面
|
||||
</a>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<div className="admin-doc-split">
|
||||
<div className="admin-doc-main">
|
||||
<AdminField
|
||||
className="admin-doc-title"
|
||||
id="anno-title"
|
||||
label="标题"
|
||||
maxLength={200}
|
||||
@@ -328,7 +362,12 @@ export default function AnnouncementComposeClient({
|
||||
/>
|
||||
|
||||
<div className="admin-doc-field">
|
||||
<p className="admin-doc-label">标签颜色</p>
|
||||
<p className="admin-doc-label">
|
||||
标签颜色
|
||||
<span className="admin-doc-label-value">
|
||||
{ANNOUNCEMENT_COLORS.find((c) => c.value === form.tag_color)?.label}
|
||||
</span>
|
||||
</p>
|
||||
<div className="admin-doc-colors" role="radiogroup" aria-label="标签颜色">
|
||||
{ANNOUNCEMENT_COLORS.map((c) => (
|
||||
<button
|
||||
@@ -343,7 +382,12 @@ export default function AnnouncementComposeClient({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="admin-doc-hint">只影响标签颜色,不改正文。</p>
|
||||
{/* 与前台同一套 j13-anno-tag 标记,所见即所得 */}
|
||||
<div className="admin-doc-tagpreview" data-note="前台胶囊效果">
|
||||
<span className={`j13-anno-tag is-${form.tag_color}`}>
|
||||
{form.tag.trim() || "公告"}
|
||||
</span>
|
||||
</div>
|
||||
{errors.tag_color ? (
|
||||
<p className="admin-settings-row-error" role="alert">
|
||||
{errors.tag_color}
|
||||
@@ -366,65 +410,25 @@ export default function AnnouncementComposeClient({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isEdit && saved ? (
|
||||
<div className="admin-doc-field">
|
||||
<p className="admin-doc-label">访问地址</p>
|
||||
<p className="admin-doc-hint">
|
||||
{publicPath}
|
||||
{origin ? ` · ${origin.replace(/\/$/, "")}${publicPath}` : ""}
|
||||
</p>
|
||||
<p className="admin-doc-hint">地址按公告编号生成,不能改成自定义路径。</p>
|
||||
<button type="button" className="btn btn-line admin-doc-copy" onClick={() => void copyAddress()}>
|
||||
<Copy size={14} aria-hidden /> 复制地址
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="admin-doc-hint">保存后才会有公开地址 /announcement/编号。草稿没有公开入口。</p>
|
||||
)}
|
||||
|
||||
<div className="admin-doc-field">
|
||||
<p className="admin-doc-label">保存后状态</p>
|
||||
<AdminSegmented<PublishKey>
|
||||
role="group"
|
||||
ariaLabel="保存后状态"
|
||||
size="sm"
|
||||
value={form.published ? "published" : "draft"}
|
||||
onChange={(key) => patchForm({ published: key === "published" })}
|
||||
options={[
|
||||
{ key: "draft", label: "草稿" },
|
||||
{ key: "published", label: "已发布" },
|
||||
]}
|
||||
/>
|
||||
<p className="admin-doc-hint">点选只改变这次要保存的状态,不会立刻发布或下线。</p>
|
||||
<p className="admin-doc-label">访问地址</p>
|
||||
{isEdit && saved ? (
|
||||
<>
|
||||
<p className="admin-doc-hint">
|
||||
{publicPath}
|
||||
{origin ? ` · ${origin.replace(/\/$/, "")}${publicPath}` : ""}
|
||||
</p>
|
||||
<p className="admin-doc-hint">地址按公告编号生成,不能改成自定义路径。</p>
|
||||
<button type="button" className="btn btn-line admin-doc-copy" onClick={() => void copyAddress()}>
|
||||
<Copy size={14} aria-hidden /> 复制地址
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p className="admin-doc-hint">保存后按公告编号生成 /announcement/编号;草稿没有公开入口。</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="admin-doc-actions" aria-busy={saving || undefined}>
|
||||
<div className="admin-doc-actions-btns">
|
||||
<button type="button" className="btn btn-line" disabled={!dirty || saving} onClick={discard}>
|
||||
撤销修改
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!dirty || saving}
|
||||
onClick={requestSave}
|
||||
>
|
||||
{saving ? <Loader2 size={15} className="animate-spin" aria-hidden /> : null}
|
||||
{saving ? "保存中…" : saveLabel}
|
||||
</button>
|
||||
<p className={`admin-doc-status${errors.form ? " is-error" : ""}`} aria-live="polite">
|
||||
{statusText}
|
||||
</p>
|
||||
</div>
|
||||
<p className="admin-doc-hint">一次保存会写入标题、正文和公告设置。Ctrl/⌘ + Enter 与这个按钮相同。</p>
|
||||
{canViewOnline ? (
|
||||
<p className="admin-doc-hint">查看线上页面打开的是已发布内容,未保存的修改不会出现在那里。</p>
|
||||
) : (
|
||||
<p className="admin-doc-hint">草稿没有公开地址。要看正文,用编辑器里的预览。</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user