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>
|
||||
<page-view>
|
||||
<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-form-model ref="categoryForm" :model="form.model" :rules="form.rules" layout="horizontal">
|
||||
<a-form-model-item help="* 页面上所显示的名称" label="名称:" prop="name">
|
||||
|
@ -11,7 +11,7 @@
|
|||
<a-input v-model="form.model.slug" />
|
||||
</a-form-model-item>
|
||||
<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 help="* 在分类页面可展示,需要主题支持" label="封面图:" prop="thumbnail">
|
||||
<AttachmentInput v-model="form.model.thumbnail" title="选择封面图" />
|
||||
|
@ -51,102 +51,16 @@
|
|||
</a-form-model>
|
||||
</a-card>
|
||||
</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="分类列表">
|
||||
<!-- Mobile -->
|
||||
<a-list
|
||||
v-if="isMobile()"
|
||||
:dataSource="table.data"
|
||||
:loading="table.loading"
|
||||
:pagination="false"
|
||||
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"
|
||||
<a-spin :spinning="list.loading">
|
||||
<CategoryTreeNode
|
||||
v-model="list.treeData"
|
||||
@edit="handleEdit"
|
||||
@reload="handleListCategories"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</span>
|
||||
<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-spin>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -154,43 +68,23 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
// components
|
||||
import { PageView } from '@/layouts'
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
import CategorySelectTree from './components/CategorySelectTree'
|
||||
import apiClient from '@/utils/api-client'
|
||||
import CategoryTreeNode from './components/CategoryTreeNode'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
ellipsis: true,
|
||||
dataIndex: 'name',
|
||||
scopedSlots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: '别名',
|
||||
ellipsis: true,
|
||||
dataIndex: 'slug'
|
||||
},
|
||||
{
|
||||
title: '文章数',
|
||||
dataIndex: 'postCount',
|
||||
scopedSlots: { customRender: 'postCount' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
// libs
|
||||
import apiClient from '@/utils/api-client'
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
|
||||
export default {
|
||||
components: { PageView, CategorySelectTree },
|
||||
components: { PageView, CategorySelectTree, CategoryTreeNode },
|
||||
mixins: [mixin, mixinDevice],
|
||||
data() {
|
||||
return {
|
||||
table: {
|
||||
columns,
|
||||
list: {
|
||||
data: [],
|
||||
treeData: [],
|
||||
loading: false
|
||||
},
|
||||
form: {
|
||||
|
@ -224,27 +118,58 @@ export default {
|
|||
this.handleListCategories()
|
||||
},
|
||||
methods: {
|
||||
handleListCategories() {
|
||||
this.table.loading = true
|
||||
apiClient.category
|
||||
.list({ sort: [], more: true })
|
||||
.then(response => {
|
||||
this.table.data = response.data
|
||||
})
|
||||
.finally(() => {
|
||||
this.table.loading = false
|
||||
})
|
||||
async handleListCategories() {
|
||||
try {
|
||||
this.list.loading = true
|
||||
|
||||
const { data } = await apiClient.category.list({})
|
||||
this.list.data = data
|
||||
this.list.treeData = this.convertDataToTree(data)
|
||||
} catch (e) {
|
||||
this.$log.error('Failed to get categories', e)
|
||||
} finally {
|
||||
this.list.loading = false
|
||||
}
|
||||
},
|
||||
handleDeleteCategory(id) {
|
||||
apiClient.category
|
||||
.delete(id)
|
||||
.then(() => {
|
||||
this.$message.success('删除成功!')
|
||||
this.form.model = {}
|
||||
})
|
||||
.finally(() => {
|
||||
this.handleListCategories()
|
||||
|
||||
convertDataToTree(categories) {
|
||||
const hashMap = {}
|
||||
const treeData = []
|
||||
categories.forEach(category => (hashMap[category.id] = { ...category, children: [] }))
|
||||
categories.forEach(category => {
|
||||
const current = hashMap[category.id]
|
||||
const parent = hashMap[category.parentId]
|
||||
|
||||
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() {
|
||||
if (this.form.errored) {
|
||||
this.form.errored = false
|
||||
|
@ -290,6 +216,7 @@ export default {
|
|||
_this.handleListCategories()
|
||||
}
|
||||
},
|
||||
|
||||
handleQueryCategoryPosts(category) {
|
||||
this.$router.push({ name: 'PostList', query: { categoryId: category.id } })
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
:allowClear="true"
|
||||
:treeData="categoryTreeData"
|
||||
:treeDataSimpleMode="true"
|
||||
:value="categoryIdString"
|
||||
v-model="categoryIdString"
|
||||
placeholder="请选择上级目录,默认为顶级目录"
|
||||
treeDefaultExpandAll
|
||||
@change="handleSelectionChange"
|
||||
>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
|
@ -14,14 +13,7 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'CategorySelectTree',
|
||||
model: {
|
||||
prop: 'categoryId',
|
||||
event: 'change'
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* Category id.
|
||||
*/
|
||||
categoryId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
@ -35,7 +27,14 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
categoryTreeData() {
|
||||
return this.categories.map(category => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
title: '根目录',
|
||||
value: '0',
|
||||
pId: -1
|
||||
},
|
||||
...this.categories.map(category => {
|
||||
return {
|
||||
id: category.id,
|
||||
title: category.name,
|
||||
|
@ -43,17 +42,15 @@ export default {
|
|||
pId: category.parentId
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
categoryIdString() {
|
||||
categoryIdString: {
|
||||
get() {
|
||||
return this.categoryId.toString()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelectionChange(value, label, extra) {
|
||||
this.$log.debug('value: ', value)
|
||||
this.$log.debug('label: ', label)
|
||||
this.$log.debug('extra: ', extra)
|
||||
this.$emit('change', value ? parseInt(value) : 0)
|
||||
set(value) {
|
||||
this.$emit('update:categoryId', 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