fix(F006): industry page refactor (remove stats + hide sort col + add up/down btns + switch to categoryDeepPageList for status field)
This commit is contained in:
parent
660cf2acfc
commit
349f18082c
@ -1,19 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 行业/资讯分类 - 树形增删改完整版 (2026-08-18 从只读升级为可交付)
|
* F006 行业/资讯分类 - 树形增删改完整版 (2026-08-18, 2026-08-22 整改数据源 + 去统计卡 + 加排序按钮)
|
||||||
*
|
|
||||||
* 复用 CategoryGoodListView(商品分类) 树形 CRUD 范式 + 企业/用户列表新版 UI 标准
|
|
||||||
* props.type: 5=行业(企业认证所属行业) 3=资讯分类
|
|
||||||
*
|
*
|
||||||
* 后端端点 (ApiCategoryController /other/category):
|
* 后端端点 (ApiCategoryController /other/category):
|
||||||
* GET list?type= 平铺列表 (前端按 pid 组装树; categoryDeepPageList 返回平铺不组装, 勿用)
|
* GET categoryDeepPageList?type= 平铺 + 含 status + createDate 字段 (前端按 pid 组装树)
|
||||||
* POST save 新增 (body: name/type/level/pid/sort/status)
|
* POST save 新增 (body: name/type/level/pid/sort/status)
|
||||||
* POST update 修改 (body, 同 save)
|
* POST update 修改 (body, 同 save)
|
||||||
* GET delete?id= 删除 (递归删子级, 注意是 GET query!)
|
* GET delete?id= 删除 (递归删子级)
|
||||||
* POST enabled?id= 启用/禁用
|
* POST enabled?id= 启用/禁用
|
||||||
*
|
* GET moveCategoryNew?id=&type= 排序上(type=1)/下(type=2)移
|
||||||
* UI 标准: PageHeader+新增按钮 / el-table 树形 / 状态圆点胶囊(全局 --st-* 变量) /
|
|
||||||
* TableActions 操作列 / el-dialog 增改弹窗 / 无边框表格(全局兜底)
|
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive, computed } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
@ -21,6 +16,7 @@ import { Plus } from '@element-plus/icons-vue'
|
|||||||
import { httpGet, httpPost } from '@/utils/request'
|
import { httpGet, httpPost } from '@/utils/request'
|
||||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
||||||
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
import TableActions from '@/components/TableActions/TableActions.vue'
|
import TableActions from '@/components/TableActions/TableActions.vue'
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
@ -30,8 +26,10 @@ interface Category {
|
|||||||
pid: string
|
pid: string
|
||||||
sort: number
|
sort: number
|
||||||
status: number
|
status: number
|
||||||
createDate: string
|
createDate?: string
|
||||||
children?: Category[]
|
children?: Category[]
|
||||||
|
// 运行时挂载, 用于上移/下移时定位兄弟节点位置
|
||||||
|
_parentArray?: Category[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{ type?: number; title?: string }>()
|
const props = defineProps<{ type?: number; title?: string }>()
|
||||||
@ -41,12 +39,11 @@ const catType = computed(() => props.type ?? 1)
|
|||||||
|
|
||||||
const LIST = ref<Category[]>([])
|
const LIST = ref<Category[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const dataSource = ref<'api' | 'mock' | 'loading'>('loading')
|
||||||
|
const apiError = ref<string>('')
|
||||||
const search = reactive({ name: '' })
|
const search = reactive({ name: '' })
|
||||||
|
|
||||||
// 状态胶囊 (全局 --st-* 变量: 0=正常绿 / 1=禁用红)
|
// 状态胶囊 (全局 --st-* 变量: 0=正常绿 / 1=禁用红)
|
||||||
const LEVEL_MAP: Record<number, string> = { 1: '一级', 2: '二级', 3: '三级' }
|
|
||||||
function levelLabel(l: number): string { return LEVEL_MAP[l] || `Lv$l` }
|
|
||||||
|
|
||||||
const STATUS_META: Record<number, { label: string; color: string; bg: string }> = {
|
const STATUS_META: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
0: { label: '正常', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
0: { label: '正常', color: 'var(--st-ok-fg)', bg: 'var(--st-ok-bg)' },
|
||||||
1: { label: '禁用', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
1: { label: '禁用', color: 'var(--st-err-fg)', bg: 'var(--st-err-bg)' }
|
||||||
@ -55,56 +52,46 @@ function fmtDate(d: string | null | undefined): string {
|
|||||||
if (!d) return '--'
|
if (!d) return '--'
|
||||||
return String(d).slice(0, 19).replace('T', ' ')
|
return String(d).slice(0, 19).replace('T', ' ')
|
||||||
}
|
}
|
||||||
function statMeta(s: number) { return STATUS_META[s] || STATUS_META[0] }
|
function statMeta(s: number) { return STATUS_META[s] ?? STATUS_META[0] }
|
||||||
|
|
||||||
// 树形过滤 (按名称关键字, 保留命中节点及其父链)
|
// 树形过滤 (按名称关键字, 保留命中节点及其父链)
|
||||||
function filterTree(nodes: Category[], kw: string): Category[] {
|
function filterTree(nodes: Category[], kw: string): Category[] {
|
||||||
if (!kw) return nodes
|
if (!kw) return nodes
|
||||||
const out: Category[] = []
|
const out: Category[] = []
|
||||||
for (const n of nodes) {
|
for (const n of nodes) {
|
||||||
const matched = n.name.includes(kw)
|
const matched = (n.name ?? '').includes(kw)
|
||||||
const children = filterTree(n.children || [], kw)
|
const children = filterTree(n.children || [], kw)
|
||||||
if (matched || children.length) {
|
if (matched || children.length) out.push({ ...n, children })
|
||||||
out.push({ ...n, children })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
const displayList = computed(() => filterTree(LIST.value, search.name.trim()))
|
const displayList = computed(() => filterTree(LIST.value, search.name.trim()))
|
||||||
|
|
||||||
// 统计: 顶数 / 1级分类数 / 最深层级 / 已禁用数
|
// 找某节点在父级数组中的索引与同级数量, 用于排序上移/下移
|
||||||
const stats = computed(() => {
|
function siblingInfo(node: Category): { idx: number; total: number } | null {
|
||||||
const total = LIST.value.length
|
const list = node._parentArray
|
||||||
const roots = LIST.value.length
|
if (!list) return null
|
||||||
let maxDepth = 1
|
const idx = list.indexOf(node)
|
||||||
const flatten = (ns: Category[], d: number) => {
|
if (idx < 0) return null
|
||||||
for (const n of ns) { if (d > maxDepth) maxDepth = d; if (n.children?.length) flatten(n.children, d + 1) }
|
return { idx, total: list.length }
|
||||||
}
|
|
||||||
flatten(LIST.value, 1)
|
|
||||||
let disabled = 0
|
|
||||||
const count = (ns: Category[]) => {
|
|
||||||
for (const n of ns) { if (n.status === 1) disabled++; if (n.children?.length) count(n.children) }
|
|
||||||
}
|
|
||||||
count(LIST.value)
|
|
||||||
return [
|
|
||||||
{ label: '分类总数', value: total, icon: 'Files', color: 'var(--brand)' },
|
|
||||||
{ label: '一级分类', value: roots, icon: 'FolderOpened', color: 'var(--green)' },
|
|
||||||
{ label: '最大层级', value: 'Lv' + maxDepth, icon: 'Histogram', color: 'var(--amber)' },
|
|
||||||
{ label: '已禁用', value: disabled, icon: 'CircleClose', color: 'var(--red)' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
// 用 /list 平铺接口 + 前端按 pid 组装树 (categoryDeepPageList 返回平铺不组装, 会导致"二级无父级")
|
|
||||||
const r: any = await httpGet('/other/category/list', { type: catType.value })
|
|
||||||
const flat = r?.list || (Array.isArray(r) ? r : [])
|
|
||||||
LIST.value = buildTree(flat)
|
|
||||||
} catch { LIST.value = [] } finally { loading.value = false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 pid 组装树 (平铺 -> 层级), 排序 sort 升序, 孤立节点挂根
|
async function load() {
|
||||||
|
loading.value = true; dataSource.value = 'loading'
|
||||||
|
try {
|
||||||
|
// categoryDeepPageList 含 status + createDate 字段 (新分类表接口)
|
||||||
|
const data: any = await httpGet('/other/category/categoryDeepPageList', { type: catType.value, page: 1, limit: 9999 })
|
||||||
|
const flat: any[] = data?.list || data?.data?.list || []
|
||||||
|
LIST.value = buildTree(flat)
|
||||||
|
dataSource.value = 'api'; apiError.value = ''
|
||||||
|
} catch (e: any) {
|
||||||
|
dataSource.value = 'mock'
|
||||||
|
apiError.value = e?.msg || e?.message || '后端 API 暂未实装, 已加载前端示例数据'
|
||||||
|
LIST.value = []
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 pid 组装树, 并在每个节点挂上 _parentArray 指针供上下移判断使用
|
||||||
function buildTree(flat: any[]): Category[] {
|
function buildTree(flat: any[]): Category[] {
|
||||||
const map = new Map<string, any>()
|
const map = new Map<string, any>()
|
||||||
flat.forEach((n: any) => map.set(String(n.id), { ...n, children: [] }))
|
flat.forEach((n: any) => map.set(String(n.id), { ...n, children: [] }))
|
||||||
@ -116,28 +103,37 @@ function buildTree(flat: any[]): Category[] {
|
|||||||
if (parent) parent.children.push(node)
|
if (parent) parent.children.push(node)
|
||||||
else roots.push(node)
|
else roots.push(node)
|
||||||
}
|
}
|
||||||
const sortRec = (nodes: any[]) => {
|
// 给每个节点挂上 _parentArray 引用, 方便 siblingInfo 判断位置
|
||||||
nodes.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
roots.forEach(r => attachParentRef(r, roots))
|
||||||
nodes.forEach((n) => sortRec(n.children || []))
|
function attachParentRef(node: any, parentArr: any[]) {
|
||||||
|
node._parentArray = parentArr
|
||||||
|
if (node.children?.length) node.children.forEach((c: any) => attachParentRef(c, node.children))
|
||||||
|
}
|
||||||
|
const sortRec = (ns: any[]) => {
|
||||||
|
ns.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||||
|
ns.forEach((n) => sortRec(n.children || []))
|
||||||
}
|
}
|
||||||
sortRec(roots)
|
sortRec(roots)
|
||||||
return roots
|
return roots
|
||||||
}
|
}
|
||||||
function onSearch() { /* 客户端过滤即时生效 */ }
|
|
||||||
function onReset() { search.name = '' }
|
|
||||||
|
|
||||||
// ========== 新增/编辑 ==========
|
const searchFields = [
|
||||||
|
{ key: 'name', label: '名称', type: 'text' as const, placeholder: '按名称搜索' }
|
||||||
|
]
|
||||||
|
const filters = reactive({ name: '' })
|
||||||
|
function onSearch() { search.name = filters.name || '' }
|
||||||
|
function onReset() { filters.name = ''; search.name = '' }
|
||||||
|
|
||||||
|
// ============ 新增/编辑 ============
|
||||||
const editVisible = ref(false)
|
const editVisible = ref(false)
|
||||||
const editMode = ref<'add' | 'edit'>('add')
|
const editMode = ref<'add' | 'edit'>('add')
|
||||||
const editForm = reactive({ id: '', name: '', level: 1, pid: '0', sort: 0, status: 0 })
|
const editForm = reactive({ id: '', name: '', level: 1, pid: '0', sort: 0, status: 0 })
|
||||||
|
|
||||||
function onAdd(parent?: Category) {
|
function onAdd(parent?: Category) {
|
||||||
editMode.value = 'add'
|
editMode.value = 'add'
|
||||||
const lvl = parent ? Math.min(parent.level + 1, 5) : 1
|
|
||||||
editForm.id = ''
|
editForm.id = ''
|
||||||
editForm.pid = parent?.id || '0'
|
|
||||||
editForm.name = ''
|
editForm.name = ''
|
||||||
editForm.level = lvl
|
editForm.pid = parent?.id || '0'
|
||||||
|
editForm.level = Math.min((parent?.level ?? 0) + 1, 5)
|
||||||
editForm.sort = 0
|
editForm.sort = 0
|
||||||
editForm.status = 0
|
editForm.status = 0
|
||||||
editVisible.value = true
|
editVisible.value = true
|
||||||
@ -146,23 +142,19 @@ function onEdit(row: Category) {
|
|||||||
editMode.value = 'edit'
|
editMode.value = 'edit'
|
||||||
editForm.id = row.id
|
editForm.id = row.id
|
||||||
editForm.name = row.name
|
editForm.name = row.name
|
||||||
editForm.level = row.level || 1
|
editForm.pid = row.pid
|
||||||
editForm.pid = row.pid || '0'
|
editForm.level = row.level
|
||||||
editForm.sort = row.sort ?? 0
|
editForm.sort = row.sort
|
||||||
editForm.status = row.status ?? 0
|
editForm.status = row.status
|
||||||
editVisible.value = true
|
editVisible.value = true
|
||||||
}
|
}
|
||||||
async function onSubmitEdit() {
|
async function onSubmitEdit() {
|
||||||
if (!editForm.name.trim()) { ElMessage.warning('请输入名称'); return }
|
const name = editForm.name.trim()
|
||||||
const payload = {
|
if (!name) { ElMessage.warning('请填写名称'); return }
|
||||||
id: editForm.id || undefined,
|
if (name.length > 30) { ElMessage.warning('名称不能超过 30 个字符'); return }
|
||||||
name: editForm.name.trim(),
|
const payload = editMode.value === 'add'
|
||||||
type: catType.value,
|
? { type: catType.value, name, status: editForm.status, pid: editForm.pid, level: editForm.level }
|
||||||
level: editForm.level,
|
: { id: editForm.id, type: catType.value, name, status: editForm.status, pid: editForm.pid, level: editForm.level }
|
||||||
pid: editForm.pid,
|
|
||||||
sort: editForm.sort,
|
|
||||||
status: editForm.status
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await httpPost(editMode.value === 'add' ? '/other/category/save' : '/other/category/update', payload)
|
await httpPost(editMode.value === 'add' ? '/other/category/save' : '/other/category/update', payload)
|
||||||
ElMessage.success(editMode.value === 'add' ? '新增成功' : '修改成功')
|
ElMessage.success(editMode.value === 'add' ? '新增成功' : '修改成功')
|
||||||
@ -171,10 +163,15 @@ async function onSubmitEdit() {
|
|||||||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 启用/禁用 ==========
|
// ============ 启用/禁用 ============
|
||||||
async function toggleStatus(row: Category) {
|
async function toggleStatus(row: Category) {
|
||||||
const disabling = row.status === 0
|
const disabling = row.status === 0
|
||||||
try { await ElMessageBox.confirm(`${disabling ? '禁用' : '启用'}「${row.name}」?${disabling ? '禁用后该分类将不对外展示' : ''}`, '操作确认', { type: 'warning' }) } catch { return }
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`${disabling ? '禁用' : '启用'}「${row.name}」?${disabling ? '禁用后该分类将不对外展示' : ''}`,
|
||||||
|
'操作确认', { type: 'warning' }
|
||||||
|
)
|
||||||
|
} catch { return }
|
||||||
try {
|
try {
|
||||||
await httpPost(`/other/category/enabled?id=${row.id}`)
|
await httpPost(`/other/category/enabled?id=${row.id}`)
|
||||||
ElMessage.success(disabling ? '已禁用' : '已启用')
|
ElMessage.success(disabling ? '已禁用' : '已启用')
|
||||||
@ -182,7 +179,20 @@ async function toggleStatus(row: Category) {
|
|||||||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '操作失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 删除 ==========
|
// ============ 排序 (上移 type=1 / 下移 type=2) ============
|
||||||
|
async function moveRow(row: Category, direction: 'up' | 'down') {
|
||||||
|
const info = siblingInfo(row)
|
||||||
|
if (!info) { ElMessage.warning('无法移动'); return }
|
||||||
|
if (direction === 'up' && info.idx === 0) { ElMessage.warning('已是最顶部'); return }
|
||||||
|
if (direction === 'down' && info.idx === info.total - 1) { ElMessage.warning('已是最底部'); return }
|
||||||
|
try {
|
||||||
|
await httpGet('/other/category/moveCategoryNew', { id: row.id, type: direction === 'up' ? 1 : 2 })
|
||||||
|
ElMessage.success(direction === 'up' ? '已上移' : '已下移')
|
||||||
|
load()
|
||||||
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '排序失败') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 删除 (递归) ============
|
||||||
async function onDelete(row: Category) {
|
async function onDelete(row: Category) {
|
||||||
const hasChild = row.children?.length
|
const hasChild = row.children?.length
|
||||||
try {
|
try {
|
||||||
@ -192,104 +202,77 @@ async function onDelete(row: Category) {
|
|||||||
)
|
)
|
||||||
} catch { return }
|
} catch { return }
|
||||||
try {
|
try {
|
||||||
// 注意: 后端 delete 是 GET 接口 (商品分类页 POST body 是 bug, 此处按实测正确形态)
|
|
||||||
await httpGet('/other/category/delete', { id: row.id })
|
await httpGet('/other/category/delete', { id: row.id })
|
||||||
ElMessage.success('已删除')
|
ElMessage.success('已删除')
|
||||||
load()
|
load()
|
||||||
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '删除失败') }
|
} catch (e: any) { ElMessage.error(e?.msg || e?.message || '删除失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'name', label: '名称', minWidth: 200 },
|
||||||
|
{ label: '层级', width: 80, align: 'center' as const },
|
||||||
|
{ prop: 'status', label: '状态', width: 100, slot: 'status', align: 'center' as const },
|
||||||
|
{ prop: 'createDate', label: '创建时间', width: 170, slot: 'createTime' },
|
||||||
|
{ label: '操作', width: 280, slot: 'actions', fixed: 'right' as const, align: 'center' as const }
|
||||||
|
]
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<PageHeader :title="pageTitle" :sub="`共 ${LIST.length} 条分类`">
|
<PageHeader :title="pageTitle" :sub="`共 ${LIST.length} 条分类`" :data-source="dataSource" :api-error="apiError">
|
||||||
<el-button type="primary" @click="onAdd()">
|
<el-button type="primary" @click="onAdd()">
|
||||||
<el-icon style="margin-right: 4px; vertical-align: -2px"><Plus /></el-icon>
|
<el-icon style="margin-right:4px"><Plus /></el-icon>新增一级分类
|
||||||
新增一级分类
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<!-- 顶部统计卡 -->
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div v-for="(s, i) in stats" :key="i" class="stat-card">
|
|
||||||
<div class="stat-icon" :style="{ color: s.color, background: 'var(--brand-soft)' }">
|
|
||||||
<el-icon><component :is="s.icon" /></el-icon>
|
|
||||||
</div>
|
|
||||||
<div class="stat-info">
|
|
||||||
<div class="stat-label">{{ s.label }}</div>
|
|
||||||
<div class="stat-value">{{ s.value }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SearchBar
|
|
||||||
v-model:filters="search"
|
|
||||||
:fields="[{ key: 'name', label: '分类名称', type: 'text', placeholder: '按名称搜索' }]"
|
|
||||||
@search="onSearch"
|
|
||||||
@reset="onReset"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="rr-list-card">
|
<div class="rr-list-card">
|
||||||
<el-table
|
<div class="rr-toolbar">
|
||||||
|
<el-input v-model="filters.name" placeholder="按名称搜索" clearable style="width:280px" @keyup.enter="onSearch" />
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:columns="columns"
|
||||||
:data="displayList"
|
:data="displayList"
|
||||||
v-loading="loading"
|
:loading="loading"
|
||||||
style="width: 100%"
|
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:tree-props="{ children: 'children' }"
|
borderless
|
||||||
:default-expand-all="false"
|
|
||||||
:border="false"
|
|
||||||
empty-text="暂无分类"
|
empty-text="暂无分类"
|
||||||
|
:show-pagination="false"
|
||||||
|
:default-expand-all="false"
|
||||||
>
|
>
|
||||||
<el-table-column label="分类名称" min-width="260">
|
<template #status="{ row }">
|
||||||
<template #default="{ row }">
|
<span class="st-pill" :style="{ color: statMeta(row.status).color, background: statMeta(row.status).bg }">
|
||||||
<span class="cat-name" :style="{ marginLeft: (row.level - 1) * 24 + 'px', fontWeight: row.level === 1 ? '600' : '400' }">
|
<span class="st-dot"></span>{{ statMeta(row.status).label }}
|
||||||
{{ row.name }}
|
</span>
|
||||||
</span>
|
</template>
|
||||||
</template>
|
<template #createTime="{ row }">
|
||||||
</el-table-column>
|
{{ fmtDate(row.createDate) }}
|
||||||
<el-table-column label="层级" width="90" align="center">
|
</template>
|
||||||
<template #default="{ row }">{{ levelLabel(row.level) }}</template>
|
<template #actions="{ row }">
|
||||||
</el-table-column>
|
<TableActions
|
||||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
:row="row"
|
||||||
<el-table-column label="状态" width="100" align="center">
|
:actions="[
|
||||||
<template #default="{ row }">
|
{ label: '添加子级', type: 'primary', when: (r: any) => (r.level || 1) < 5, onClick: onAdd },
|
||||||
<span class="st-pill" :style="{ color: statMeta(row.status).color, background: statMeta(row.status).bg }">
|
{ label: '修改', type: 'primary', onClick: onEdit },
|
||||||
<span class="st-dot"></span>{{ statMeta(row.status).label }}
|
{ label: '上移', type: 'info', when: () => { const i = siblingInfo(row); return i ? i.idx > 0 : false }, onClick: () => moveRow(row, 'up') },
|
||||||
</span>
|
{ label: '下移', type: 'info', when: () => { const i = siblingInfo(row); return i ? i.idx < i.total - 1 : false }, onClick: () => moveRow(row, 'down') },
|
||||||
</template>
|
{ label: row.status === 0 ? '禁用' : '启用', type: row.status === 0 ? 'warning' : 'success', onClick: () => toggleStatus(row) },
|
||||||
</el-table-column>
|
{ label: '删除', type: 'danger', divided: true, onClick: () => onDelete(row) }
|
||||||
<el-table-column label="创建时间" width="170">
|
]"
|
||||||
<template #default="{ row }">{{ fmtDate(row.createDate) }}</template>
|
/>
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
</DataTable>
|
||||||
<template #default="{ row }">
|
|
||||||
<TableActions
|
|
||||||
:row="row"
|
|
||||||
:actions="[
|
|
||||||
{ label: '添加子级', type: 'primary', when: (r: any) => (r.level || 1) < 5, onClick: onAdd },
|
|
||||||
{ label: '编辑', type: 'primary', onClick: onEdit },
|
|
||||||
{ label: (r: any) => r.status === 0 ? '禁用' : '启用', type: (r: any) => r.status === 0 ? 'warning' : 'success', onClick: toggleStatus },
|
|
||||||
{ label: '删除', type: 'danger', divided: true, onClick: onDelete }
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/修改 弹窗 -->
|
||||||
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增分类' : '修改分类'" width="500px" :close-on-click-modal="false" destroy-on-close>
|
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增分类' : '修改分类'" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||||
<el-form label-width="100px" label-position="right">
|
<el-form label-width="100px" label-position="right">
|
||||||
<el-form-item label="分类名称" required>
|
<el-form-item label="名称" required>
|
||||||
<el-input v-model="editForm.name" placeholder="请输入分类名称" maxlength="30" show-word-limit clearable />
|
<el-input v-model="editForm.name" placeholder="请输入名称" maxlength="30" show-word-limit clearable />
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="级别" v-if="editMode === 'edit'">
|
|
||||||
<span class="level-tip">{{ levelLabel(editForm.level) }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="排序">
|
|
||||||
<el-input-number v-model="editForm.sort" :min="0" :max="999" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-radio-group v-model="editForm.status">
|
<el-radio-group v-model="editForm.status">
|
||||||
@ -307,35 +290,12 @@ onMounted(load)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
/* 顶部搜索工具栏 (label-less, 与新后台其它列表页一致) */
|
||||||
|
.rr-toolbar {
|
||||||
/* 顶部统计卡 */
|
padding: 14px 16px 0;
|
||||||
.stats-grid {
|
background: var(--bg-soft);
|
||||||
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
|
display: flex;
|
||||||
margin-bottom: 16px;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.stat-card {
|
</style>
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-lg); padding: 16px 18px;
|
|
||||||
display: flex; align-items: center; gap: 12px;
|
|
||||||
transition: all .15s;
|
|
||||||
&:hover { border-color: var(--border-strong); box-shadow: var(--shadow); }
|
|
||||||
}
|
|
||||||
.stat-icon {
|
|
||||||
width: 40px; height: 40px; border-radius: 10px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.stat-info { flex: 1; min-width: 0; }
|
|
||||||
.stat-label { font-size: 12px; color: var(--ink-3); margin-bottom: 4px; }
|
|
||||||
.stat-value { font-size: 22px; font-weight: 700; color: var(--ink); letter-spacing: -0.02em; }
|
|
||||||
|
|
||||||
.rr-list-card {
|
|
||||||
background: var(--surface); border-radius: 12px; overflow: hidden;
|
|
||||||
}
|
|
||||||
.cat-name { display: inline-block; font-size: 14px; color: var(--ink); }
|
|
||||||
.level-tip {
|
|
||||||
display: inline-block; font-size: 13px; color: var(--ink-2);
|
|
||||||
background: var(--bg, #f6f8fb); border-radius: 6px; padding: 4px 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user