align(商品类目): el-tree 节点补来源(内置/自定义)/排序/备注展示、行内编辑删除(有子禁用)、按需懒加载子级+加载更多(已加载x/y)、搜索表格补来源列、按钮名/占位对齐(对齐 admin.js renderProductCategoryRows/loadProductCategoryChildren)
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/** 商品类目:对齐 admin panel-product-categories —— 树(关键字搜索态独立分页/加载更多)、导出、删除(有子禁用)、新增/编辑弹窗语义。 */
|
/** 商品类目:对齐 admin panel-product-categories —— 树节点带 来源/排序/备注 + 行内编辑删除(有子禁用)、
|
||||||
|
* 按需懒加载子级(「加载更多 已加载 x/y」)、搜索态独立分页/加载更多、导出、新增/编辑弹窗语义。 */
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { fetchProductCategoryTree, searchProductCategories } from './product-category-api.ts'
|
import { fetchProductCategoryChildren, searchProductCategories } from './product-category-api.ts'
|
||||||
import { createProductCategory, deleteProductCategory, updateProductCategory } from './product-category-editor-api.ts'
|
import { createProductCategory, deleteProductCategory, updateProductCategory } from './product-category-editor-api.ts'
|
||||||
import { PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE, type ProductCategoryNode } from './product-category-model.ts'
|
import {
|
||||||
|
appendCategoryChildren,
|
||||||
|
isCategoryLoadMoreNode,
|
||||||
|
makeCategoryLoadMoreNode,
|
||||||
|
PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE,
|
||||||
|
productCategoryNodeMeta,
|
||||||
|
productCategorySourceLabel,
|
||||||
|
type ProductCategoryNode,
|
||||||
|
} from './product-category-model.ts'
|
||||||
import { categoryDraftError, createProductCategoryDraft, toCategorySaveRequest, type ProductCategoryDraft } from './product-category-editor-model.ts'
|
import { categoryDraftError, createProductCategoryDraft, toCategorySaveRequest, type ProductCategoryDraft } from './product-category-editor-model.ts'
|
||||||
import { buildProductCategoryExportUrl } from './product-category-export.ts'
|
import { buildProductCategoryExportUrl } from './product-category-export.ts'
|
||||||
|
|
||||||
@@ -24,12 +33,22 @@ const editingId = ref<number | null>(null)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const draft = reactive<ProductCategoryDraft>(createProductCategoryDraft())
|
const draft = reactive<ProductCategoryDraft>(createProductCategoryDraft())
|
||||||
|
|
||||||
|
/** 每父级(根=null)的子级分页状态:page/total/hasMore/loading,支撑「加载更多 已加载 x/y」。 */
|
||||||
|
const childrenState = new Map<string, { page: number; total: number; hasMore: boolean; loading: boolean }>()
|
||||||
|
const ROOT_KEY = '__root__'
|
||||||
|
|
||||||
|
function stateKeyOf(parentId: number | null): string {
|
||||||
|
return parentId == null ? ROOT_KEY : String(parentId)
|
||||||
|
}
|
||||||
|
|
||||||
function flatten(nodes: ProductCategoryNode[]): ProductCategoryNode[] {
|
function flatten(nodes: ProductCategoryNode[]): ProductCategoryNode[] {
|
||||||
const out: ProductCategoryNode[] = []
|
const out: ProductCategoryNode[] = []
|
||||||
for (const node of nodes || []) {
|
for (const node of nodes || []) {
|
||||||
|
if (!isCategoryLoadMoreNode(node)) {
|
||||||
out.push(node)
|
out.push(node)
|
||||||
out.push(...flatten(node.children))
|
out.push(...flatten(node.children))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +56,16 @@ const allNodes = computed(() => flatten(tree.value))
|
|||||||
const searching = computed(() => (keyword.value || '').trim().length > 0)
|
const searching = computed(() => (keyword.value || '').trim().length > 0)
|
||||||
const dialogTitle = computed(() => (editingId.value == null ? '新增商品类目' : '编辑商品类目'))
|
const dialogTitle = computed(() => (editingId.value == null ? '新增商品类目' : '编辑商品类目'))
|
||||||
|
|
||||||
|
/** 拉取根级第一页(对齐 admin.js loadProductCategoryChildren(null) 懒加载根)。 */
|
||||||
async function loadTree(): Promise<void> {
|
async function loadTree(): Promise<void> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
tree.value = (await fetchProductCategoryTree('')).tree
|
const result = await fetchProductCategoryChildren(null, 1, PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE)
|
||||||
|
const state = { page: result.page, total: result.total, hasMore: result.hasMore, loading: false }
|
||||||
|
childrenState.set(ROOT_KEY, state)
|
||||||
|
tree.value = result.hasMore
|
||||||
|
? [...result.items, makeCategoryLoadMoreNode(null, result.items.length, result.total)]
|
||||||
|
: result.items
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '类目加载失败')
|
ElMessage.error(error instanceof Error ? error.message : '类目加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -48,6 +73,62 @@ async function loadTree(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 节点展开时按需加载子级第一页(childCount>0 且未加载过才拉取)。 */
|
||||||
|
async function onNodeExpand(node: ProductCategoryNode): Promise<void> {
|
||||||
|
if (isCategoryLoadMoreNode(node) || node.childCount <= 0 || node.children.length > 0) return
|
||||||
|
await loadChildrenPage(node, 1, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载某父级一页子级(append=false 重置;append=true 追加并重建「加载更多」节点)。 */
|
||||||
|
async function loadChildrenPage(parent: ProductCategoryNode | null, page: number, append: boolean): Promise<void> {
|
||||||
|
const key = stateKeyOf(parent?.id ?? null)
|
||||||
|
const state = childrenState.get(key)
|
||||||
|
if (state?.loading) return
|
||||||
|
childrenState.set(key, { page: state?.page ?? 0, total: state?.total ?? 0, hasMore: state?.hasMore ?? false, loading: true })
|
||||||
|
try {
|
||||||
|
const result = await fetchProductCategoryChildren(parent?.id ?? null, page, PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE)
|
||||||
|
const loaded = append ? (state?.page ?? 0) + result.items.length : result.items.length
|
||||||
|
const nextState = { page: result.page, total: result.total, hasMore: result.hasMore, loading: false }
|
||||||
|
childrenState.set(key, nextState)
|
||||||
|
if (parent) {
|
||||||
|
const current = parent.children || []
|
||||||
|
parent.children = append
|
||||||
|
? appendCategoryChildren(current, result.items, parent.id, loaded, result.total, result.hasMore)
|
||||||
|
: (result.hasMore
|
||||||
|
? [...result.items, makeCategoryLoadMoreNode(parent.id, result.items.length, result.total)]
|
||||||
|
: result.items)
|
||||||
|
} else {
|
||||||
|
const current = tree.value
|
||||||
|
tree.value = append
|
||||||
|
? appendCategoryChildren(current, result.items, null, loaded, result.total, result.hasMore)
|
||||||
|
: (result.hasMore
|
||||||
|
? [...result.items, makeCategoryLoadMoreNode(null, result.items.length, result.total)]
|
||||||
|
: result.items)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const fallback = childrenState.get(key) ?? { page: 0, total: 0, hasMore: false, loading: false }
|
||||||
|
fallback.loading = false
|
||||||
|
childrenState.set(key, fallback)
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '子类目加载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击「加载更多」合成节点:翻下一页追加。 */
|
||||||
|
async function loadMoreChildren(more: ProductCategoryNode): Promise<void> {
|
||||||
|
if (!isCategoryLoadMoreNode(more)) return
|
||||||
|
const parentId = more.parentId
|
||||||
|
const parent = parentId == null ? null : allNodes.value.find((node) => node.id === parentId) ?? null
|
||||||
|
if (parentId != null && !parent) return
|
||||||
|
const state = childrenState.get(stateKeyOf(parentId))
|
||||||
|
if (state?.loading) return
|
||||||
|
await loadChildrenPage(parent, (state?.page ?? 0) + 1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「加载更多」按钮的加载态:取其父级分页状态的 loading。 */
|
||||||
|
function moreLoading(more: ProductCategoryNode): boolean {
|
||||||
|
return isCategoryLoadMoreNode(more) && (childrenState.get(stateKeyOf(more.parentId))?.loading ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
async function runSearch(page: number): Promise<void> {
|
async function runSearch(page: number): Promise<void> {
|
||||||
searchLoading.value = true
|
searchLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -99,12 +180,16 @@ function openEdit(node: ProductCategoryNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onNodeClick(node: ProductCategoryNode) {
|
function onNodeClick(node: ProductCategoryNode) {
|
||||||
|
if (isCategoryLoadMoreNode(node)) {
|
||||||
|
void loadMoreChildren(node)
|
||||||
|
return
|
||||||
|
}
|
||||||
selected.value = node
|
selected.value = node
|
||||||
}
|
}
|
||||||
|
|
||||||
function canDelete(node: ProductCategoryNode | null): boolean {
|
function canDelete(node: ProductCategoryNode | null): boolean {
|
||||||
if (!node) return false
|
if (!node || isCategoryLoadMoreNode(node)) return false
|
||||||
return !(node.childCount > 0 || (Array.isArray(node.children) && node.children.length > 0))
|
return !(node.childCount > 0 || (Array.isArray(node.children) && node.children.some((child) => !isCategoryLoadMoreNode(child))))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeCategory(node: ProductCategoryNode): Promise<void> {
|
async function removeCategory(node: ProductCategoryNode): Promise<void> {
|
||||||
@@ -176,14 +261,14 @@ onMounted(loadTree)
|
|||||||
<p>维护商品类目树与类目名称/说明;有子类目节点不可删除。</p>
|
<p>维护商品类目树与类目名称/说明;有子类目节点不可删除。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<el-button type="primary" @click="openCreate(null)">新增根类目</el-button>
|
<el-button type="primary" @click="openCreate(null)">新增类目</el-button>
|
||||||
<el-button @click="exportCategories">导出</el-button>
|
<el-button @click="exportCategories">导出</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card shadow="never" style="margin-bottom: 14px">
|
<el-card shadow="never" style="margin-bottom: 14px">
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<el-input v-model="keyword" placeholder="搜索类目名称(保留父路径)" clearable style="width: 280px" @keyup.enter="apply" />
|
<el-input v-model="keyword" placeholder="搜索类目名称、编码或备注" clearable style="width: 280px" @keyup.enter="apply" />
|
||||||
<el-button type="primary" @click="apply">搜索</el-button>
|
<el-button type="primary" @click="apply">搜索</el-button>
|
||||||
<el-button @click="reset">清空</el-button>
|
<el-button @click="reset">清空</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,16 +280,42 @@ onMounted(loadTree)
|
|||||||
:data="tree"
|
:data="tree"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
:props="{ label: 'name', children: 'children' }"
|
:props="{ label: 'name', children: 'children' }"
|
||||||
default-expand-all
|
:expand-on-click-node="false"
|
||||||
highlight-current
|
highlight-current
|
||||||
@node-click="onNodeClick"
|
@node-click="onNodeClick"
|
||||||
/>
|
@node-expand="onNodeExpand"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<template v-if="isCategoryLoadMoreNode(data)">
|
||||||
|
<span class="node-more" @click.stop="loadMoreChildren(data)">
|
||||||
|
<el-button link size="small" :loading="moreLoading(data)">加载更多</el-button>
|
||||||
|
<span class="dim">已加载 {{ data.loadedCount }} / {{ data.totalCount }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="node-row">
|
||||||
|
<span class="node-main">
|
||||||
|
<span class="node-name">{{ data.name }}</span>
|
||||||
|
<el-tag size="small" effect="plain" :type="data.isBuiltin ? 'info' : 'success'">{{ productCategorySourceLabel(data) }}</el-tag>
|
||||||
|
<span class="node-meta">{{ productCategoryNodeMeta(data) }}</span>
|
||||||
|
<span v-if="data.description" class="node-desc" :title="data.description">{{ data.description }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="node-ops" @click.stop>
|
||||||
|
<el-button link size="small" type="primary" @click="openEdit(data as ProductCategoryNode)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
:disabled="!canDelete(data as ProductCategoryNode)"
|
||||||
|
@click="removeCategory(data as ProductCategoryNode)"
|
||||||
|
>删除</el-button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
<div class="tree-actions">
|
<div class="tree-actions">
|
||||||
<span class="dim">{{ selected ? `已选:${selected.path || selected.name}` : '未选中节点' }}</span>
|
<span class="dim">{{ selected ? `已选:${selected.path || selected.name}` : '未选中节点' }}</span>
|
||||||
<span class="btn-group">
|
<span class="btn-group">
|
||||||
<el-button size="small" :disabled="!selected" @click="openCreate(selected)">新增子类目</el-button>
|
<el-button size="small" :disabled="!selected" @click="openCreate(selected)">新增子类目</el-button>
|
||||||
<el-button size="small" type="primary" :disabled="!selected" @click="openEdit(selected as ProductCategoryNode)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" :disabled="!canDelete(selected)" @click="removeCategory(selected as ProductCategoryNode)">删除</el-button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -222,6 +333,13 @@ onMounted(loadTree)
|
|||||||
<el-table-column label="排序" width="90">
|
<el-table-column label="排序" width="90">
|
||||||
<template #default="{ row }">{{ (row as ProductCategoryNode).sortOrder ?? '—' }}</template>
|
<template #default="{ row }">{{ (row as ProductCategoryNode).sortOrder ?? '—' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="来源" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" effect="plain" :type="(row as ProductCategoryNode).isBuiltin ? 'info' : 'success'">
|
||||||
|
{{ productCategorySourceLabel(row as ProductCategoryNode) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="说明" min-width="180">
|
<el-table-column label="说明" min-width="180">
|
||||||
<template #default="{ row }">{{ (row as ProductCategoryNode).description || '—' }}</template>
|
<template #default="{ row }">{{ (row as ProductCategoryNode).description || '—' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
parseProductCategoryList,
|
parseProductCategoryList,
|
||||||
parseProductCategorySearchPage,
|
parseProductCategorySearchPage,
|
||||||
type ProductCategoryListResult,
|
type ProductCategoryListResult,
|
||||||
|
type ProductCategoryNode,
|
||||||
type ProductCategorySearchPage,
|
type ProductCategorySearchPage,
|
||||||
} from './product-category-model'
|
} from './product-category-model'
|
||||||
|
|
||||||
@@ -30,3 +31,24 @@ export async function searchProductCategories(
|
|||||||
const { data } = await http.get<unknown>(`${PRODUCT_CATEGORY_ENDPOINT}/search`, { params })
|
const { data } = await http.get<unknown>(`${PRODUCT_CATEGORY_ENDPOINT}/search`, { params })
|
||||||
return parseProductCategorySearchPage(data)
|
return parseProductCategorySearchPage(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 子级类目分页页(对齐 admin.js loadProductCategoryChildren 的 /children 端点语义)。 */
|
||||||
|
export interface ProductCategoryChildrenPage {
|
||||||
|
items: ProductCategoryNode[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 懒加载某父级的子类目页(parentId 为空表示根级),解析 page/total/hasMore。 */
|
||||||
|
export async function fetchProductCategoryChildren(
|
||||||
|
parentId: number | null,
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
): Promise<ProductCategoryChildrenPage> {
|
||||||
|
const params: Record<string, string | number> = { page, page_size: pageSize }
|
||||||
|
if (parentId != null) params.parent_id = parentId
|
||||||
|
const { data } = await http.get<unknown>(`${PRODUCT_CATEGORY_ENDPOINT}/children`, { params })
|
||||||
|
const parsed = parseProductCategorySearchPage(data)
|
||||||
|
return { items: parsed.items, total: parsed.total, page: parsed.page, hasMore: parsed.hasMore }
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,3 +107,63 @@ export function parseProductCategorySearchPage(payload: unknown): ProductCategor
|
|||||||
const hasMore = typeof record?.hasMore === 'boolean' ? record.hasMore : page * pageSize < total
|
const hasMore = typeof record?.hasMore === 'boolean' ? record.hasMore : page * pageSize < total
|
||||||
return { items, total, page, pageSize, hasMore }
|
return { items, total, page, pageSize, hasMore }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 来源展示标签(对齐 admin.js:6004 内置/自定义 tag)。 */
|
||||||
|
export function productCategorySourceLabel(node: Pick<ProductCategoryNode, 'isBuiltin'> | null | undefined): string {
|
||||||
|
return node?.isBuiltin ? '内置' : '自定义'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 节点元信息文案:来源 + 排序(对齐参考 来源列/排序列)。 */
|
||||||
|
export function productCategoryNodeMeta(node: Pick<ProductCategoryNode, 'isBuiltin' | 'sortOrder'>): string {
|
||||||
|
return `${productCategorySourceLabel(node)} · 排序 ${node.sortOrder ?? 0}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 合成"加载更多"节点:挂在同级 children 末尾,点击翻下一页(对齐 admin.js 的 load-more 行)。 */
|
||||||
|
export interface CategoryLoadMoreNode extends ProductCategoryNode {
|
||||||
|
isLoadMore: true
|
||||||
|
loadedCount: number
|
||||||
|
totalCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCategoryLoadMoreNode(node: ProductCategoryNode | null | undefined): node is CategoryLoadMoreNode {
|
||||||
|
return !!node && (node as Partial<CategoryLoadMoreNode>).isLoadMore === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCategoryLoadMoreNode(
|
||||||
|
parentId: number | null,
|
||||||
|
loadedCount: number,
|
||||||
|
totalCount: number,
|
||||||
|
): CategoryLoadMoreNode {
|
||||||
|
const syntheticId = parentId ? -parentId : -1
|
||||||
|
return {
|
||||||
|
id: syntheticId,
|
||||||
|
parentId,
|
||||||
|
name: '',
|
||||||
|
categoryKey: '',
|
||||||
|
sortOrder: null,
|
||||||
|
description: '',
|
||||||
|
isBuiltin: false,
|
||||||
|
childCount: 0,
|
||||||
|
level: null,
|
||||||
|
path: '',
|
||||||
|
children: [],
|
||||||
|
isLoadMore: true,
|
||||||
|
loadedCount,
|
||||||
|
totalCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把新一页并入同级 children:去掉旧"加载更多"节点,追加新条目;仍有下一页时补新"加载更多"节点。 */
|
||||||
|
export function appendCategoryChildren(
|
||||||
|
current: ProductCategoryNode[],
|
||||||
|
nextItems: ProductCategoryNode[],
|
||||||
|
parentId: number | null,
|
||||||
|
loadedCount: number,
|
||||||
|
totalCount: number,
|
||||||
|
hasMore: boolean,
|
||||||
|
): ProductCategoryNode[] {
|
||||||
|
const base = current.filter((node) => !isCategoryLoadMoreNode(node))
|
||||||
|
const merged = [...base, ...nextItems]
|
||||||
|
if (hasMore) merged.push(makeCategoryLoadMoreNode(parentId, loadedCount, totalCount))
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readSource } from './helpers.ts'
|
||||||
|
import {
|
||||||
|
appendCategoryChildren,
|
||||||
|
isCategoryLoadMoreNode,
|
||||||
|
makeCategoryLoadMoreNode,
|
||||||
|
productCategoryNodeMeta,
|
||||||
|
productCategorySourceLabel,
|
||||||
|
type ProductCategoryNode,
|
||||||
|
} from '../src/pages/asin/product-category-model.ts'
|
||||||
|
|
||||||
|
/** 对齐 admin.js:5998-6012/6077-6120:树节点带来源/排序/备注、按需懒加载 + 加载更多(已加载 x/y)。 */
|
||||||
|
|
||||||
|
function node(id: number, overrides: Partial<ProductCategoryNode> = {}): ProductCategoryNode {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
parentId: null,
|
||||||
|
name: `类目${id}`,
|
||||||
|
categoryKey: `k${id}`,
|
||||||
|
sortOrder: id,
|
||||||
|
description: '',
|
||||||
|
isBuiltin: false,
|
||||||
|
childCount: 0,
|
||||||
|
level: 0,
|
||||||
|
path: `类目${id}`,
|
||||||
|
children: [],
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('align_category_source_label', () => {
|
||||||
|
assert.equal(productCategorySourceLabel({ isBuiltin: true } as ProductCategoryNode), '内置')
|
||||||
|
assert.equal(productCategorySourceLabel({ isBuiltin: false } as ProductCategoryNode), '自定义')
|
||||||
|
assert.equal(productCategorySourceLabel(null), '自定义')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_category_node_meta_contains_source_and_sort', () => {
|
||||||
|
assert.equal(productCategoryNodeMeta({ isBuiltin: true, sortOrder: 3 } as ProductCategoryNode), '内置 · 排序 3')
|
||||||
|
assert.equal(productCategoryNodeMeta({ isBuiltin: false, sortOrder: null } as ProductCategoryNode), '自定义 · 排序 0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_category_load_more_node_factory', () => {
|
||||||
|
const more = makeCategoryLoadMoreNode(5, 20, 45)
|
||||||
|
assert.equal(isCategoryLoadMoreNode(more), true)
|
||||||
|
assert.equal(more.parentId, 5)
|
||||||
|
assert.equal(more.loadedCount, 20)
|
||||||
|
assert.equal(more.totalCount, 45)
|
||||||
|
assert.equal(isCategoryLoadMoreNode(node(1)), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_category_append_children_keeps_more_until_done', () => {
|
||||||
|
const current = [node(1), makeCategoryLoadMoreNode(null, 20, 45)]
|
||||||
|
const next = appendCategoryChildren(current, [node(2), node(3)], null, 40, 45, true)
|
||||||
|
assert.deepEqual(next.filter((n) => !isCategoryLoadMoreNode(n)).map((n) => n.id), [1, 2, 3])
|
||||||
|
const tail = next[next.length - 1]
|
||||||
|
assert.equal(isCategoryLoadMoreNode(tail), true)
|
||||||
|
assert.equal((tail as ReturnType<typeof makeCategoryLoadMoreNode>).loadedCount, 40)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_category_append_children_drops_more_when_complete', () => {
|
||||||
|
const current = [node(1), makeCategoryLoadMoreNode(9, 20, 22)]
|
||||||
|
const next = appendCategoryChildren(current, [node(2), node(3)], 9, 22, 22, false)
|
||||||
|
assert.deepEqual(next.map((n) => n.id), [1, 2, 3])
|
||||||
|
assert.equal(next.some((n) => isCategoryLoadMoreNode(n)), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_category_children_api_wiring', () => {
|
||||||
|
const api = readSource('src/pages/asin/product-category-api.ts')
|
||||||
|
assert.match(api, /children/, '懒加载走 children 端点')
|
||||||
|
assert.match(api, /parent_id|parentId/, '带父级参数')
|
||||||
|
assert.match(api, /page_size|pageSize/, '分页参数')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_category_page_tree_and_search_wiring', () => {
|
||||||
|
const page = readSource('src/pages/asin/ProductCategoryPage.vue')
|
||||||
|
assert.doesNotMatch(page, /default-expand-all/, '树不再全量展开,改按需懒加载')
|
||||||
|
assert.match(page, /加载更多/, '节点带加载更多')
|
||||||
|
assert.match(page, /已加载/, '显示已加载 x/y')
|
||||||
|
assert.match(page, /productCategorySourceLabel/, '节点展示来源')
|
||||||
|
assert.match(page, /排序/, '节点展示排序')
|
||||||
|
assert.doesNotMatch(page, /新增根类目/, '按钮名对齐参考“新增类目”')
|
||||||
|
assert.match(page, /新增类目/, '顶部新增类目按钮')
|
||||||
|
assert.match(page, /搜索类目名称、编码或备注/, '搜索占位对齐参考')
|
||||||
|
assert.match(page, /来源/, '搜索表格补来源列')
|
||||||
|
assert.match(page, /childCount/, '删除按钮按有子禁用')
|
||||||
|
})
|
||||||
@@ -28,7 +28,8 @@ test('test_task_256_category_normal_repeated_operation_is_idempotent', () => {
|
|||||||
|
|
||||||
test('test_task_256_category_boundary_empty_input', () => {
|
test('test_task_256_category_boundary_empty_input', () => {
|
||||||
const page = readSource('src/pages/asin/ProductCategoryPage.vue')
|
const page = readSource('src/pages/asin/ProductCategoryPage.vue')
|
||||||
assert.match(page, /新增根类目/, '需保留新增根类目')
|
// 对齐 admin.html:顶部按钮名「新增类目」;选中节点后仍可新增子类目。
|
||||||
|
assert.match(page, /新增类目/, '顶部新增类目入口')
|
||||||
assert.match(page, /新增子类目/, '需保留新增子类目')
|
assert.match(page, /新增子类目/, '需保留新增子类目')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user