feat(B5): 栏目工作台-树+类型化内容面板,ArticleList 组件化薄壳
- 新建 ArticleListPanel(props categoryId/fixedCategory):无props=全局检索行为不变(薄壳红线); 固定栏目模式隐藏栏目下拉,watch categoryId 联动 reload(拖拽排序不触发,红线) - CategoryManage 右侧:列表型=本栏目文章面板+新建文章(query.categoryId 预选)+预览; 单页型=摘要+编辑内容+预览;外链型=URL+打开链接;空态引导;工具栏增树折叠(窄屏) - ArticleEdit 支持 query.categoryId 新建态预选栏目 - 后端零改动(红线);vitest 32/32 绿(新增 categoryWorkbench.spec 7 例,手写挂载+vi.mock 桩)
This commit is contained in:
@@ -5,13 +5,18 @@ import MyConfirm from "@/components/MyConfirm";
|
||||
import CategoryTreePanel from "@/components/CategoryTreePanel.vue";
|
||||
import CategorySortModal from "@/components/CategorySortModal.vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import ArticleListPanel from "@/components/article/ArticleListPanel.vue";
|
||||
import { categoryApi } from "@/api/module/cms";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { CategoryTreeNode } from "@/types/cms";
|
||||
|
||||
/* UserBase group/index 同款交互:图标工具栏(新增/编辑/子组排序/删除/搜索)+ ztree */
|
||||
const store = useCmsStore();
|
||||
|
||||
/** 树折叠(窄屏/专注内容时收起左树,右侧面板占满;不触碰 ztree 内部交互) */
|
||||
const treeCollapsed = ref(false);
|
||||
|
||||
const refTreePanel = useTemplateRef("refTreePanel");
|
||||
const refFormData = useTemplateRef("refFormData");
|
||||
const refNodeNameInput = useTemplateRef("refNodeNameInput");
|
||||
@@ -195,10 +200,38 @@ async function save() {
|
||||
function onSortSaved() {
|
||||
refTreePanel.value?.refreshTree(curTreeNode.value?.id);
|
||||
}
|
||||
|
||||
/* ===== B5 栏目工作台:右侧类型化内容面板 ===== */
|
||||
|
||||
/** 单页摘要:去 HTML 标签截断展示 */
|
||||
function summaryText(node: any): string {
|
||||
const text = String(node?.content || "").replace(/<[^>]+>/g, "").trim();
|
||||
return text.length > 200 ? text.slice(0, 200) + "…" : text || "(暂无内容,点击「编辑内容」补充)";
|
||||
}
|
||||
|
||||
/** 新建文章:带 categoryId 预选跳编辑页(ArticleEdit onMounted 回填) */
|
||||
function goNewArticle(categoryId: number) {
|
||||
window.location.hash = `#/sch/${store.schoolCode}/article/edit?categoryId=${categoryId}`;
|
||||
}
|
||||
|
||||
/** 门户预览:新开 /portal/{siteCode}/c/{id} */
|
||||
function openPreview(node: any) {
|
||||
const code = store.currentSite?.code || store.schoolCode;
|
||||
if (!code) {
|
||||
Message.warning("当前无站点上下文,无法预览");
|
||||
return;
|
||||
}
|
||||
window.open(`${config.serviceHost}/portal/${code}/c/${node.id}`, "_blank");
|
||||
}
|
||||
|
||||
/** 外链栏目直达 */
|
||||
function openLink(url: string) {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-wrapper category-page">
|
||||
<div class="tree-wrapper category-page" :class="{ 'tree-collapsed': treeCollapsed }">
|
||||
<!-- 左侧栏目树(userbase rule-tree 风格) -->
|
||||
<div class="tree-container category-tree-box">
|
||||
<ul class="opera-ul">
|
||||
@@ -227,6 +260,11 @@ function onSortSaved() {
|
||||
<Icon type="md-search" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip content="收起/展开栏目树" placement="top" transfer>
|
||||
<li @click="treeCollapsed = !treeCollapsed">
|
||||
<Icon :type="treeCollapsed ? 'ios-arrow-forward' : 'ios-arrow-back'" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
|
||||
<CategoryTreePanel
|
||||
@@ -240,18 +278,71 @@ function onSortSaved() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:选中栏目详情 -->
|
||||
<div class="main-container">
|
||||
<div class="info-panel" v-if="selected">
|
||||
<h3>{{ selected.name }}</h3>
|
||||
<p>类型:{{ typeDict.find(t => t.value === selected!.type)?.label }}</p>
|
||||
<p v-if="selected.type === 3">外链:{{ selected.linkUrl }}</p>
|
||||
<p v-if="selected.type === 2">单页内容:见门户预览</p>
|
||||
<p>排序号:{{ selected.sortNo }} · 状态:{{ selected.status === 1 ? "显示" : "隐藏" }}</p>
|
||||
<p v-if="selected.seoTitle">SEO 标题:{{ selected.seoTitle }}</p>
|
||||
<p v-if="selected.seoDescription">SEO 描述:{{ selected.seoDescription }}</p>
|
||||
</div>
|
||||
<div v-else class="empty-tip">请选择左侧栏目,或使用顶部图标新增子栏目</div>
|
||||
<!-- 右侧:类型化内容面板(B5 栏目工作台) -->
|
||||
<div class="main-container workbench-main">
|
||||
<template v-if="selected">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h3>{{ selected.name }}</h3>
|
||||
<Tag :color="selected.type === 1 ? 'primary' : selected.type === 2 ? 'success' : 'warning'">
|
||||
{{ typeDict.find(t => t.value === selected!.type)?.label }}
|
||||
</Tag>
|
||||
<span class="panel-meta">排序 {{ selected.sortNo }} · {{ selected.status === 1 ? "显示" : "隐藏" }}</span>
|
||||
</div>
|
||||
<div class="panel-tools">
|
||||
<Button v-if="selected.type === 1" type="primary" @click="goNewArticle(selected.id)">
|
||||
<Icon type="ios-add" />
|
||||
新建文章
|
||||
</Button>
|
||||
<Button v-if="selected.type !== 3" @click="openPreview(selected)">
|
||||
<Icon type="ios-eye" />
|
||||
预览
|
||||
</Button>
|
||||
<Button @click="showEditNode">
|
||||
<Icon type="ios-create" />
|
||||
编辑栏目
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表型:本栏目文章管理(联动仅经 categoryId watch,拖拽排序不触发) -->
|
||||
<ArticleListPanel
|
||||
v-if="selected.type === 1"
|
||||
class="panel-body"
|
||||
:category-id="selected.id"
|
||||
fixed-category
|
||||
/>
|
||||
|
||||
<!-- 单页型:内容摘要 + 编辑入口 -->
|
||||
<div v-else-if="selected.type === 2" class="single-summary">
|
||||
<p class="summary-text">{{ summaryText(selected) }}</p>
|
||||
<div class="summary-actions">
|
||||
<Button type="primary" @click="showEditNode">
|
||||
<Icon type="ios-create" />
|
||||
编辑内容
|
||||
</Button>
|
||||
<Button @click="openPreview(selected)">
|
||||
<Icon type="ios-eye" />
|
||||
预览单页
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="selected.seoTitle" class="seo-line">SEO 标题:{{ selected.seoTitle }}</p>
|
||||
<p v-if="selected.seoDescription" class="seo-line">SEO 描述:{{ selected.seoDescription }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 外链型:URL 展示 + 直达 -->
|
||||
<div v-else class="link-info">
|
||||
<p class="link-url-label">跳转地址</p>
|
||||
<p class="link-url">{{ selected.linkUrl }}</p>
|
||||
<div class="summary-actions">
|
||||
<Button type="primary" @click="openLink(selected.linkUrl)">
|
||||
<Icon type="ios-open-outline" />
|
||||
打开链接
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="empty-tip">选择左侧栏目查看内容</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
@@ -359,6 +450,17 @@ function onSortSaved() {
|
||||
}
|
||||
}
|
||||
|
||||
/* 树折叠态:隐藏左树,右侧面板占满(覆盖全局 .tree-wrapper .main-container 的 264px 边距) */
|
||||
.category-page.tree-collapsed {
|
||||
.tree-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
@@ -371,6 +473,98 @@ function onSortSaved() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== B5 栏目工作台右侧面板 ===== */
|
||||
.workbench-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
max-width: 320px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel-meta {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-tools {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.single-summary,
|
||||
.link-info {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin-top: 10px;
|
||||
|
||||
.summary-text {
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.seo-line {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-info {
|
||||
.link-url-label {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
color: #2d8cf0;
|
||||
word-break: break-all;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user