mirror of https://github.com/halo-dev/halo-admin
refactor: post category management (#435)
* refactor: post category management Signed-off-by: Ryan Wang <i@ryanc.cc> * refactor: post category management Signed-off-by: Ryan Wang <i@ryanc.cc> * refactor: tree data Signed-off-by: Ryan Wang <i@ryanc.cc> * refactor: tree data Signed-off-by: Ryan Wang <i@ryanc.cc>pull/442/head
parent
9704b00cc2
commit
36aa658790
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<page-view>
|
<page-view>
|
||||||
<a-row :gutter="12">
|
<a-row :gutter="12">
|
||||||
<a-col :lg="10" :md="10" :sm="24" :xl="10" :xs="24" class="pb-3">
|
<a-col :lg="8" :md="8" :sm="24" :xl="8" :xs="24" class="pb-3">
|
||||||
<a-card :bodyStyle="{ padding: '16px' }" :title="title">
|
<a-card :bodyStyle="{ padding: '16px' }" :title="title">
|
||||||
<a-form-model ref="categoryForm" :model="form.model" :rules="form.rules" layout="horizontal">
|
<a-form-model ref="categoryForm" :model="form.model" :rules="form.rules" layout="horizontal">
|
||||||
<a-form-model-item help="* 页面上所显示的名称" label="名称:" prop="name">
|
<a-form-model-item help="* 页面上所显示的名称" label="名称:" prop="name">
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
<a-input v-model="form.model.slug" />
|
<a-input v-model="form.model.slug" />
|
||||||
</a-form-model-item>
|
</a-form-model-item>
|
||||||
<a-form-model-item label="上级目录:" prop="parentId">
|
<a-form-model-item label="上级目录:" prop="parentId">
|
||||||
<category-select-tree v-model="form.model.parentId" :categories="table.data" />
|
<category-select-tree :categories="list.data" :category-id.sync="form.model.parentId" />
|
||||||
</a-form-model-item>
|
</a-form-model-item>
|
||||||
<a-form-model-item help="* 在分类页面可展示,需要主题支持" label="封面图:" prop="thumbnail">
|
<a-form-model-item help="* 在分类页面可展示,需要主题支持" label="封面图:" prop="thumbnail">
|
||||||
<AttachmentInput v-model="form.model.thumbnail" title="选择封面图" />
|
<AttachmentInput v-model="form.model.thumbnail" title="选择封面图" />
|
||||||
|
@ -51,102 +51,16 @@
|
||||||
</a-form-model>
|
</a-form-model>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :lg="14" :md="14" :sm="24" :xl="14" :xs="24" class="pb-3">
|
<a-col :lg="16" :md="16" :sm="24" :xl="16" :xs="24" class="pb-3">
|
||||||
<a-card :bodyStyle="{ padding: '16px' }" title="分类列表">
|
<a-card :bodyStyle="{ padding: '16px' }" title="分类列表">
|
||||||
<!-- Mobile -->
|
<a-spin :spinning="list.loading">
|
||||||
<a-list
|
<CategoryTreeNode
|
||||||
v-if="isMobile()"
|
v-model="list.treeData"
|
||||||
:dataSource="table.data"
|
@edit="handleEdit"
|
||||||
:loading="table.loading"
|
@reload="handleListCategories"
|
||||||
:pagination="false"
|
@select="handleSelect"
|
||||||
itemLayout="vertical"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<a-list-item :key="index" slot="renderItem" slot-scope="item, index">
|
|
||||||
<template slot="actions">
|
|
||||||
<span>
|
|
||||||
<a-icon type="form" />
|
|
||||||
{{ item.postCount }}
|
|
||||||
</span>
|
|
||||||
<a-dropdown :trigger="['click']" placement="topLeft">
|
|
||||||
<span>
|
|
||||||
<a-icon type="bars" />
|
|
||||||
</span>
|
|
||||||
<a-menu slot="overlay">
|
|
||||||
<a-menu-item @click="form.model = item">编辑</a-menu-item>
|
|
||||||
<a-menu-item>
|
|
||||||
<a-popconfirm
|
|
||||||
:title="'你确定要删除【' + item.name + '】分类?'"
|
|
||||||
cancelText="取消"
|
|
||||||
okText="确定"
|
|
||||||
@confirm="handleDeleteCategory(item.id)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</a-popconfirm>
|
|
||||||
</a-menu-item>
|
|
||||||
</a-menu>
|
|
||||||
</a-dropdown>
|
|
||||||
</template>
|
|
||||||
<a-list-item-meta>
|
|
||||||
<template slot="description">
|
|
||||||
{{ item.slug }}
|
|
||||||
</template>
|
|
||||||
<span
|
|
||||||
slot="title"
|
|
||||||
style="
|
|
||||||
max-width: 300px;
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ item.name }}{{ item.password ? '(加密)' : '' }}
|
|
||||||
</span></a-list-item-meta
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{ item.description }}
|
|
||||||
</span>
|
|
||||||
</a-list-item>
|
|
||||||
</a-list>
|
|
||||||
<!-- Desktop -->
|
|
||||||
<a-table
|
|
||||||
v-else
|
|
||||||
:columns="table.columns"
|
|
||||||
:dataSource="table.data"
|
|
||||||
:loading="table.loading"
|
|
||||||
:rowKey="record => record.id"
|
|
||||||
:scrollToFirstRowOnChange="true"
|
|
||||||
>
|
|
||||||
<span slot="name" slot-scope="text, record" class="cursor-pointer">
|
|
||||||
{{ record.name }}{{ record.password ? '(加密)' : '' }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
slot="postCount"
|
|
||||||
slot-scope="text, record"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="handleQueryCategoryPosts(record)"
|
|
||||||
>
|
|
||||||
<a-badge
|
|
||||||
:count="record.postCount"
|
|
||||||
:numberStyle="{ backgroundColor: '#00e0ff' }"
|
|
||||||
:overflowCount="9999"
|
|
||||||
:showZero="true"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</a-spin>
|
||||||
<span slot="action" slot-scope="text, record">
|
|
||||||
<a-button class="!p-0" type="link" @click="form.model = record"> 编辑 </a-button>
|
|
||||||
<a-divider type="vertical" />
|
|
||||||
<a-popconfirm
|
|
||||||
:title="'你确定要删除【' + record.name + '】分类?'"
|
|
||||||
cancelText="取消"
|
|
||||||
okText="确定"
|
|
||||||
@confirm="handleDeleteCategory(record.id)"
|
|
||||||
>
|
|
||||||
<a-button class="!p-0" type="link">删除</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</span>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
@ -154,43 +68,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// components
|
||||||
import { PageView } from '@/layouts'
|
import { PageView } from '@/layouts'
|
||||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
|
||||||
import CategorySelectTree from './components/CategorySelectTree'
|
import CategorySelectTree from './components/CategorySelectTree'
|
||||||
import apiClient from '@/utils/api-client'
|
import CategoryTreeNode from './components/CategoryTreeNode'
|
||||||
|
|
||||||
const columns = [
|
// libs
|
||||||
{
|
import apiClient from '@/utils/api-client'
|
||||||
title: '名称',
|
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||||
ellipsis: true,
|
|
||||||
dataIndex: 'name',
|
|
||||||
scopedSlots: { customRender: 'name' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '别名',
|
|
||||||
ellipsis: true,
|
|
||||||
dataIndex: 'slug'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '文章数',
|
|
||||||
dataIndex: 'postCount',
|
|
||||||
scopedSlots: { customRender: 'postCount' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
scopedSlots: { customRender: 'action' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { PageView, CategorySelectTree },
|
components: { PageView, CategorySelectTree, CategoryTreeNode },
|
||||||
mixins: [mixin, mixinDevice],
|
mixins: [mixin, mixinDevice],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
table: {
|
list: {
|
||||||
columns,
|
|
||||||
data: [],
|
data: [],
|
||||||
|
treeData: [],
|
||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
|
@ -224,27 +118,58 @@ export default {
|
||||||
this.handleListCategories()
|
this.handleListCategories()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleListCategories() {
|
async handleListCategories() {
|
||||||
this.table.loading = true
|
try {
|
||||||
apiClient.category
|
this.list.loading = true
|
||||||
.list({ sort: [], more: true })
|
|
||||||
.then(response => {
|
const { data } = await apiClient.category.list({})
|
||||||
this.table.data = response.data
|
this.list.data = data
|
||||||
})
|
this.list.treeData = this.convertDataToTree(data)
|
||||||
.finally(() => {
|
} catch (e) {
|
||||||
this.table.loading = false
|
this.$log.error('Failed to get categories', e)
|
||||||
})
|
} finally {
|
||||||
|
this.list.loading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleDeleteCategory(id) {
|
|
||||||
apiClient.category
|
convertDataToTree(categories) {
|
||||||
.delete(id)
|
const hashMap = {}
|
||||||
.then(() => {
|
const treeData = []
|
||||||
this.$message.success('删除成功!')
|
categories.forEach(category => (hashMap[category.id] = { ...category, children: [] }))
|
||||||
this.form.model = {}
|
categories.forEach(category => {
|
||||||
})
|
const current = hashMap[category.id]
|
||||||
.finally(() => {
|
const parent = hashMap[category.parentId]
|
||||||
this.handleListCategories()
|
|
||||||
|
if (current.password) {
|
||||||
|
current.hasPassword = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent && (parent.password || parent.hasPassword)) {
|
||||||
|
current.hasPassword = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.parentId) {
|
||||||
|
hashMap[category.parentId].children.push(current)
|
||||||
|
} else {
|
||||||
|
treeData.push(current)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
return treeData
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleEdit(category) {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.category.get(category.id)
|
||||||
|
this.form.model = data
|
||||||
|
} catch (e) {
|
||||||
|
this.$log.error('Failed to get category', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSelect(category) {
|
||||||
|
this.form.model = {
|
||||||
|
parentId: category.id
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -281,6 +206,7 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSavedCallback() {
|
handleSavedCallback() {
|
||||||
if (this.form.errored) {
|
if (this.form.errored) {
|
||||||
this.form.errored = false
|
this.form.errored = false
|
||||||
|
@ -290,6 +216,7 @@ export default {
|
||||||
_this.handleListCategories()
|
_this.handleListCategories()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleQueryCategoryPosts(category) {
|
handleQueryCategoryPosts(category) {
|
||||||
this.$router.push({ name: 'PostList', query: { categoryId: category.id } })
|
this.$router.push({ name: 'PostList', query: { categoryId: category.id } })
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
:allowClear="true"
|
:allowClear="true"
|
||||||
:treeData="categoryTreeData"
|
:treeData="categoryTreeData"
|
||||||
:treeDataSimpleMode="true"
|
:treeDataSimpleMode="true"
|
||||||
:value="categoryIdString"
|
v-model="categoryIdString"
|
||||||
placeholder="请选择上级目录,默认为顶级目录"
|
placeholder="请选择上级目录,默认为顶级目录"
|
||||||
treeDefaultExpandAll
|
treeDefaultExpandAll
|
||||||
@change="handleSelectionChange"
|
|
||||||
>
|
>
|
||||||
</a-tree-select>
|
</a-tree-select>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,14 +13,7 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'CategorySelectTree',
|
name: 'CategorySelectTree',
|
||||||
model: {
|
|
||||||
prop: 'categoryId',
|
|
||||||
event: 'change'
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
/**
|
|
||||||
* Category id.
|
|
||||||
*/
|
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -35,7 +27,14 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
categoryTreeData() {
|
categoryTreeData() {
|
||||||
return this.categories.map(category => {
|
return [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: '根目录',
|
||||||
|
value: '0',
|
||||||
|
pId: -1
|
||||||
|
},
|
||||||
|
...this.categories.map(category => {
|
||||||
return {
|
return {
|
||||||
id: category.id,
|
id: category.id,
|
||||||
title: category.name,
|
title: category.name,
|
||||||
|
@ -43,17 +42,15 @@ export default {
|
||||||
pId: category.parentId
|
pId: category.parentId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
]
|
||||||
},
|
},
|
||||||
categoryIdString() {
|
categoryIdString: {
|
||||||
|
get() {
|
||||||
return this.categoryId.toString()
|
return this.categoryId.toString()
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
set(value) {
|
||||||
handleSelectionChange(value, label, extra) {
|
this.$emit('update:categoryId', value ? parseInt(value) : 0)
|
||||||
this.$log.debug('value: ', value)
|
}
|
||||||
this.$log.debug('label: ', label)
|
|
||||||
this.$log.debug('extra: ', extra)
|
|
||||||
this.$emit('change', value ? parseInt(value) : 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<a-list item-layout="horizontal">
|
||||||
|
<draggable
|
||||||
|
:list="list"
|
||||||
|
:value="value"
|
||||||
|
class="item-container"
|
||||||
|
handle=".mover"
|
||||||
|
tag="div"
|
||||||
|
v-bind="{
|
||||||
|
animation: 300,
|
||||||
|
group: 'description',
|
||||||
|
ghostClass: 'ghost',
|
||||||
|
chosenClass: 'chosen',
|
||||||
|
dragClass: 'drag',
|
||||||
|
emptyInsertThreshold: 20
|
||||||
|
}"
|
||||||
|
@end="isDragging = false"
|
||||||
|
@input="emitter"
|
||||||
|
@start="isDragging = true"
|
||||||
|
>
|
||||||
|
<transition-group>
|
||||||
|
<div v-for="item in realValue" :key="item.id">
|
||||||
|
<a-list-item class="menu-item">
|
||||||
|
<a-list-item-meta>
|
||||||
|
<span slot="title" class="inline-block font-bold title">
|
||||||
|
<!-- <a-icon class="cursor-move mover mr-1" type="bars" />-->
|
||||||
|
{{ item.name }}{{ item.hasPassword ? '(加密)' : '' }}
|
||||||
|
</span>
|
||||||
|
<span slot="description" class="inline-block">
|
||||||
|
<a :href="item.fullPath" class="ant-anchor-link-title" target="_blank"> {{ item.fullPath }} </a>
|
||||||
|
</span>
|
||||||
|
</a-list-item-meta>
|
||||||
|
<template #actions>
|
||||||
|
<a-button class="!p-0" type="link" @click="handleSelect(item)">新增</a-button>
|
||||||
|
<a-button class="!p-0" type="link" @click="handleEdit(item)">编辑</a-button>
|
||||||
|
<a-button class="!p-0" type="link" @click="handleDelete(item)">删除</a-button>
|
||||||
|
</template>
|
||||||
|
</a-list-item>
|
||||||
|
<div class="a-list-nested" style="margin-left: 44px">
|
||||||
|
<CategoryTreeNode
|
||||||
|
:list="item.children"
|
||||||
|
@edit="handleEdit"
|
||||||
|
@reload="$emit('reload')"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</draggable>
|
||||||
|
</a-list>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
// components
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
import apiClient from '@/utils/api-client'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CategoryTreeNode',
|
||||||
|
components: {
|
||||||
|
draggable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false,
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
required: false,
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
realValue() {
|
||||||
|
return this.value ? this.value : this.list
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isDragging: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
emitter(value) {
|
||||||
|
this.$emit('input', value)
|
||||||
|
},
|
||||||
|
handleDelete(item) {
|
||||||
|
const _this = this
|
||||||
|
_this.$confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: `确定要删除名为${item.name}的分类?`,
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await apiClient.category.delete(item.id)
|
||||||
|
_this.$emit('reload')
|
||||||
|
} catch (e) {
|
||||||
|
_this.$log.error('Fail to delete category', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleEdit(item) {
|
||||||
|
this.$emit('edit', item)
|
||||||
|
},
|
||||||
|
handleSelect(item) {
|
||||||
|
this.$emit('select', item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.8;
|
||||||
|
@apply bg-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chosen {
|
||||||
|
opacity: 0.8;
|
||||||
|
@apply bg-gray-200;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag {
|
||||||
|
@apply bg-white;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .ant-list-item-action {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .menu-item:hover .ant-list-item-action {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue