- 新建 components/category/CategoryContentSummary.vue(194):列表型栏目右侧不再挂文章表格, 改「共 N 篇 + 最近 5 行 + 查看全部」概览;计数口径=直属+子孙(与门户 collectListDescendantIds 同语义)、全状态计入,一次 page 请求同时拿 total 与 5 行(零新接口、零后端改动) - CategoryManage.vue 列表分支换组件 + 头注更新;编辑弹窗「首页展示形态」提示改为 「展示形态由当前皮肤包的主题化实现决定,未实现的形态回落通用骨架渲染」(v2 §七 验收第 8 条点名项) - ArticleListPanel.vue +7:setup 读 route.query.categoryId 作为初始筛选(深链回显) - types/cms.ts 1 行:ArticlePageQuery.categoryIds 放宽为 number|string(逗号串),手册未点名、已在 audit-D §六 披露为偏差 - categoryWorkbench.spec.ts 15→17 例:T-D-01 按裁决改写旧断言(概览而非表格,并机器证明无操作列)、 T-D-02 深链回显、T-D-03 查看全部跳转;夹具上提 + beforeEach 复位,默认值与旧硬编码行为逐字一致 - 未动 router/routes.ts 的 meta.subOf(护栏 siteRouter.spec 8 / siteLayer.spec 16 绿) - 遗留立案:R-43 fixedCategory/extraHeight 无生产调用方(按纪律未删)/ R-44 概览含子孙而深链只筛直属 需产品定口径 / R-45 菜单名来自平台模块表 / R-46 ?categoryId 仅读一次 - 前端 166/166 绿 + build:base exit=0;S10 复跑同绿
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import Utils from "@/utils/index";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
@@ -23,6 +23,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useCmsStore();
|
||||
|
||||
const rows = ref<ArticlePageItem[]>([]);
|
||||
@@ -32,7 +33,9 @@ const pageNum = ref(1);
|
||||
const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
|
||||
|
||||
const search = reactive({
|
||||
categoryId: undefined as number | undefined,
|
||||
// P-D1:栏目页概览「查看全部 →」以 /article?categoryId={id} 深链进入,读 query 预筛选
|
||||
// (query 是字符串,转数字才能对上 Option 的 value 并回显下拉选中项)
|
||||
categoryId: Number((route.query?.categoryId as any)) || undefined,
|
||||
status: undefined as number | undefined,
|
||||
keyword: "",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Tag } from "view-ui-plus";
|
||||
import { articleApi } from "@/api/module/cms";
|
||||
import { useCmsStore } from "@/store";
|
||||
import { ArticlePageItem } from "@/types/cms";
|
||||
import { adminPathOf } from "@/utils/sitePath";
|
||||
|
||||
/**
|
||||
* 栏目内容概览(P-D1,v2 §B.3):/category 右侧列表型栏目的降级面板。
|
||||
* 定位=只读概览;文章的发布/下架/置顶/删除只在 /article(唯一操作台)做。
|
||||
*
|
||||
* 统计口径(手册 P-D1 定稿,勿自行改):
|
||||
* - 范围 = 选中栏目的**直属 + 全部子孙栏目**。后端 ArticlePageQuery.categoryIds 是不递归的 IN 查询,
|
||||
* 故子孙 id 由前端从栏目树展开(与门户 PortalPageRenderer.collectListDescendantIds 同语义)。
|
||||
* - 状态 = **全状态计数**(草稿/已发布/已下架全部计入,不带 status 过滤;系统内无"回收站"概念)。
|
||||
* - 一次 page 请求同时拿到总数(total)与最近 5 条(list,后端按置顶降序 + 创建时间降序)。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
categoryId: number;
|
||||
categoryName?: string;
|
||||
/** 全量栏目树(CategoryManage 的 treeData,含 W2 虚拟根;按 id 定位,虚拟根 id=0 不会命中真实栏目) */
|
||||
tree?: any[];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const store = useCmsStore();
|
||||
|
||||
const total = ref(0);
|
||||
const rows = ref<ArticlePageItem[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const statusDict = [
|
||||
{ value: 0, label: "草稿", color: "default" },
|
||||
{ value: 1, label: "已发布", color: "success" },
|
||||
{ value: 2, label: "已下架", color: "warning" },
|
||||
];
|
||||
const statusText = (s: number) => statusDict.find(i => i.value === s)?.label || "未知";
|
||||
const statusColor = (s: number) => statusDict.find(i => i.value === s)?.color || "default";
|
||||
|
||||
function findNode(list: any[], id: number): any {
|
||||
for (const n of list || []) {
|
||||
if (Number(n.id) === Number(id)) return n;
|
||||
const hit = findNode(n.children || [], id);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectIds(node: any, acc: number[]): number[] {
|
||||
acc.push(Number(node.id));
|
||||
(node.children || []).forEach((c: any) => collectIds(c, acc));
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** 统计范围 id:树未就绪(或未命中)时退化为仅直属栏目 */
|
||||
const scopeIds = computed<number[]>(() => {
|
||||
const node = findNode(props.tree || [], props.categoryId);
|
||||
return node ? collectIds(node, []) : [Number(props.categoryId)];
|
||||
});
|
||||
const scopeKey = computed(() => scopeIds.value.join(","));
|
||||
|
||||
async function run() {
|
||||
if (!store.siteId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const page = await articleApi
|
||||
.page({
|
||||
siteId: store.siteId,
|
||||
categoryIds: scopeKey.value,
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
})
|
||||
.send();
|
||||
total.value = page?.total || 0;
|
||||
rows.value = (page?.list || []).slice(0, 5);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳 /article 深链预筛选(子栏目行按自身栏目过滤,父栏目"查看全部"按父栏目过滤) */
|
||||
function goArticles(categoryId?: number) {
|
||||
const code = store.schoolCode;
|
||||
if (!code) return;
|
||||
const id = categoryId ?? props.categoryId;
|
||||
router.push(adminPathOf(code, store.siteSegCode, "article") + `?categoryId=${id}`);
|
||||
}
|
||||
|
||||
// 树稍后到达(子孙变化)与切换栏目都重取;站点切换由 CategoryManage 清选中后重建本组件
|
||||
watch(scopeKey, run);
|
||||
onMounted(run);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="category-content-summary">
|
||||
<div class="ccs-stat fix">
|
||||
<span class="ccs-total">共 <b>{{ total }}</b> 篇</span>
|
||||
<span class="ccs-scope">
|
||||
{{ categoryName || "本栏目" }}及子栏目({{ scopeIds.length }} 个栏目)· 草稿/已发布/已下架全部计入
|
||||
</span>
|
||||
<Button class="fr" size="small" @click="goArticles()">查看全部 →</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="ccs-empty">加载中…</div>
|
||||
<ul v-else-if="rows.length" class="ccs-list">
|
||||
<li v-for="row in rows" :key="row.id" class="ccs-row fix">
|
||||
<a class="ccs-title operating-link" :title="row.title" @click="goArticles(row.categoryId)">{{ row.title }}</a>
|
||||
<Tag class="ccs-tag" :color="statusColor(row.status)">{{ statusText(row.status) }}</Tag>
|
||||
<span class="ccs-time">{{ row.publishTime || row.createTime || "-" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="ccs-empty">
|
||||
<p>本栏目及子栏目下暂无文章</p>
|
||||
<p class="ccs-empty-tip">可在上方「新建文章」直接发布到本栏目,或到「文章列表」页检索全站内容。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.category-content-summary {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 4px 16px 16px;
|
||||
}
|
||||
|
||||
.ccs-stat {
|
||||
padding: 14px 0 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.ccs-total {
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
|
||||
b {
|
||||
font-size: 20px;
|
||||
color: #2d8cf0;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ccs-scope {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
}
|
||||
|
||||
.ccs-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ccs-row {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ccs-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ccs-tag {
|
||||
flex: none;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.ccs-time {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
}
|
||||
|
||||
.ccs-empty {
|
||||
padding: 32px 0;
|
||||
text-align: center;
|
||||
color: #808695;
|
||||
}
|
||||
|
||||
.ccs-empty-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user