halo-admin/src/views/post/PostList.vue

797 lines
25 KiB
Vue

<template>
<page-view>
<a-card :bodyStyle="{ padding: '16px' }" :bordered="false">
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
<a-col :md="6" :sm="24">
<a-form-item label="关键词:">
<a-input v-model="list.params.keyword" @keyup.enter="handleQuery()" />
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-form-item label="文章状态:">
<a-select v-model="list.params.status" allowClear placeholder="请选择文章状态" @change="handleQuery()">
<a-select-option v-for="status in Object.keys(postStatus)" :key="status" :value="status">
{{ postStatus[status].text }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-form-item label="分类目录:">
<a-select
v-model="list.params.categoryId"
:loading="categories.loading"
allowClear
placeholder="请选择分类"
@change="handleQuery()"
>
<a-select-option v-for="category in categories.data" :key="category.id">
{{ category.name }}({{ category.postCount }})
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<span class="table-page-search-submitButtons">
<a-space>
<a-button type="primary" @click="handleQuery()"></a-button>
<a-button @click="handleResetParam()"></a-button>
</a-space>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div class="table-operator">
<router-link :to="{ name: 'PostWrite' }">
<a-button icon="plus" type="primary">写文章</a-button>
</router-link>
<a-dropdown v-show="list.params.status != null && list.params.status !== '' && !isMobile()">
<a-menu slot="overlay">
<a-menu-item v-if="['DRAFT', 'RECYCLE'].includes(list.params.status)" key="1">
<a href="javascript:void(0);" @click="handleEditStatusMore(postStatus.PUBLISHED.value)">
<span>发布</span>
</a>
</a-menu-item>
<a-menu-item v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(list.params.status)" key="2">
<a href="javascript:void(0);" @click="handleEditStatusMore(postStatus.RECYCLE.value)">
<span>移到回收站</span>
</a>
</a-menu-item>
<a-menu-item v-if="['RECYCLE', 'PUBLISHED', 'INTIMATE'].includes(list.params.status)" key="3">
<a href="javascript:void(0);" @click="handleEditStatusMore(postStatus.DRAFT.value)">
<span>草稿</span>
</a>
</a-menu-item>
<a-menu-item v-if="['RECYCLE', 'DRAFT'].includes(list.params.status)" key="4">
<a href="javascript:void(0);" @click="handleDeleteMore">
<span>永久删除</span>
</a>
</a-menu-item>
</a-menu>
<a-button class="ml-2">
批量操作
<a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<div class="mt-4">
<!-- Mobile -->
<a-list
v-if="isMobile()"
:dataSource="formattedPosts"
:loading="list.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="eye" />
{{ item.visits }}
</span>
<span @click="handleShowPostComments(item)">
<a-icon type="message" />
{{ item.commentCount }}
</span>
<a-dropdown :trigger="['click']" placement="topLeft">
<span>
<a-icon type="bars" />
</span>
<a-menu slot="overlay">
<a-menu-item v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(item.status)">
<a href="javascript:void(0);" @click="handleEditClick(item)">编辑</a>
</a-menu-item>
<a-menu-item v-else-if="item.status === 'RECYCLE'">
<a-popconfirm
:title="'你确定要发布【' + item.title + '】文章?'"
cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick(item.id, 'PUBLISHED')"
>
<a href="javascript:void(0);">还原</a>
</a-popconfirm>
</a-menu-item>
<a-menu-item v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(item.status)">
<a-popconfirm
:title="'你确定要将【' + item.title + '】文章移到回收站?'"
cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick(item.id, 'RECYCLE')"
>
<a href="javascript:void(0);">回收站</a>
</a-popconfirm>
</a-menu-item>
<a-menu-item v-else-if="item.status === 'RECYCLE'">
<a-popconfirm
:title="'你确定要永久删除【' + item.title + '】文章?'"
cancelText="取消"
okText="确定"
@confirm="handleDeleteClick(item.id)"
>
<a href="javascript:void(0);">删除</a>
</a-popconfirm>
</a-menu-item>
<a-menu-item>
<a href="javascript:void(0);" rel="noopener noreferrer" @click="handleShowPostSettings(item)">
设置
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="extra">
<span>
<a-badge :status="item.statusProperty.status" :text="item.statusProperty.text" />
</span>
</template>
<a-list-item-meta>
<template slot="description">
{{ item.createTime | moment }}
</template>
<span
slot="title"
style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
>
<a-icon
v-if="item.topPriority !== 0"
style="margin-right: 3px;"
theme="twoTone"
twoToneColor="red"
type="pushpin"
/>
<a
v-if="['PUBLISHED', 'INTIMATE'].includes(item.status)"
:href="item.fullPath"
class="no-underline"
target="_blank"
>
<a-tooltip :title="'点击访问【' + item.title + '】'" placement="top">{{ item.title }}</a-tooltip>
</a>
<a
v-else-if="item.status === 'DRAFT'"
class="no-underline"
href="javascript:void(0)"
@click="handlePreview(item.id)"
>
<a-tooltip :title="'点击预览【' + item.title + '】'" placement="topLeft">{{ item.title }}</a-tooltip>
</a>
<a v-else class="no-underline" disabled href="javascript:void(0);">
{{ item.title }}
</a>
</span>
</a-list-item-meta>
<span> {{ item.summary }}... </span>
<br />
<br />
<a-tag
v-for="(category, categoryIndex) in item.categories"
:key="'category_' + categoryIndex"
color="blue"
style="margin-bottom: 8px"
@click="handleSelectCategory(category)"
>{{ category.name }}
</a-tag>
<br />
<a-tag
v-for="(tag, tagIndex) in item.tags"
:key="'tag_' + tagIndex"
:color="tag.color"
style="margin-bottom: 8px"
>{{ tag.name }}
</a-tag>
</a-list-item>
</a-list>
<!-- Desktop -->
<a-table
v-else
:columns="columns"
:dataSource="formattedPosts"
:loading="list.loading"
:pagination="false"
:rowKey="post => post.id"
:rowSelection="{
selectedRowKeys: selectedRowKeys,
onChange: onSelectionChange,
getCheckboxProps: getCheckboxProps
}"
:scrollToFirstRowOnChange="true"
>
<span slot="postTitle" slot-scope="text, record">
<a-icon
v-if="record.topPriority !== 0"
style="margin-right: 3px;"
theme="twoTone"
twoToneColor="red"
type="pushpin"
/>
<a
v-if="['PUBLISHED', 'INTIMATE'].includes(record.status)"
:href="record.fullPath"
class="no-underline"
target="_blank"
>
<a-tooltip :title="'点击访问【' + text + '】'" placement="top">{{ text }}</a-tooltip>
</a>
<a
v-else-if="record.status === 'DRAFT'"
class="no-underline"
href="javascript:void(0)"
@click="handlePreview(record.id)"
>
<a-tooltip :title="'点击预览【' + text + '】'" placement="topLeft">{{ text }}</a-tooltip>
</a>
<a v-else class="no-underline" disabled href="javascript:void(0);">
{{ text }}
</a>
</span>
<span slot="status" slot-scope="statusProperty">
<a-badge :status="statusProperty.status" :text="statusProperty.text" />
</span>
<span slot="categories" slot-scope="categoriesOfPost">
<a-tag
v-for="(category, index) in categoriesOfPost"
:key="index"
color="blue"
style="margin-bottom: 8px;cursor:pointer"
@click="handleSelectCategory(category)"
>
{{ category.name }}
</a-tag>
</span>
<span slot="tags" slot-scope="tags">
<a-tag v-for="(tag, index) in tags" :key="index" :color="tag.color" style="margin-bottom: 8px">
{{ tag.name }}
</a-tag>
</span>
<span
slot="commentCount"
slot-scope="text, record"
style="cursor: pointer;"
@click="handleShowPostComments(record)"
>
<a-badge
:count="record.commentCount"
:numberStyle="{ backgroundColor: '#f38181' }"
:overflowCount="999"
:showZero="true"
/>
</span>
<span slot="visits" slot-scope="visits">
<a-badge
:count="visits"
:numberStyle="{ backgroundColor: '#00e0ff' }"
:overflowCount="9999"
:showZero="true"
/>
</span>
<span slot="createTime" slot-scope="createTime">
<a-tooltip placement="top">
<template slot="title">
{{ createTime | moment }}
</template>
{{ createTime | timeAgo }}
</a-tooltip>
</span>
<span slot="action" slot-scope="text, post">
<a
v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(post.status)"
href="javascript:void(0);"
@click="handleEditClick(post)"
>编辑</a
>
<a-popconfirm
v-else-if="post.status === 'RECYCLE'"
:title="'你确定要发布【' + post.title + '】文章?'"
cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick(post.id, 'PUBLISHED')"
>
<a href="javascript:void(0);">还原</a>
</a-popconfirm>
<a-divider type="vertical" />
<a-popconfirm
v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(post.status)"
:title="'你确定要将【' + post.title + '】文章移到回收站?'"
cancelText="取消"
okText="确定"
@confirm="handleEditStatusClick(post.id, 'RECYCLE')"
>
<a href="javascript:void(0);">回收站</a>
</a-popconfirm>
<a-popconfirm
v-else-if="post.status === 'RECYCLE'"
:title="'你确定要永久删除【' + post.title + '】文章?'"
cancelText="取消"
okText="确定"
@confirm="handleDeleteClick(post.id)"
>
<a href="javascript:void(0);">删除</a>
</a-popconfirm>
<a-divider type="vertical" />
<a href="javascript:void(0);" @click="handleShowPostSettings(post)">设置</a>
</span>
</a-table>
<div class="page-wrapper">
<a-pagination
:current="pagination.page"
:defaultPageSize="pagination.size"
:pageSizeOptions="['10', '20', '50', '100']"
:total="pagination.total"
class="pagination"
showLessItems
showSizeChanger
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
/>
</div>
</div>
</a-card>
<PostSettingModal
:loading="postSettingLoading"
:post="selectedPost"
:savedCallback="onPostSavedCallback"
:visible.sync="postSettingVisible"
@onClose="selectedPost = {}"
>
<template #extraFooter>
<a-button :disabled="selectPreviousButtonDisabled" @click="handleSelectPrevious">
上一篇
</a-button>
<a-button :disabled="selectNextButtonDisabled" @click="handleSelectNext">
</a-button>
</template>
</PostSettingModal>
<TargetCommentDrawer
:id="selectedPost.id"
:description="selectedPost.summary"
:target="`posts`"
:title="selectedPost.title"
:visible="postCommentVisible"
@close="onPostCommentsClose"
/>
</page-view>
</template>
<script>
import { mixin, mixinDevice } from '@/mixins/mixin.js'
import { PageView } from '@/layouts'
import PostSettingModal from './components/PostSettingModal.vue'
import TargetCommentDrawer from '../comment/components/TargetCommentDrawer'
import apiClient from '@/utils/api-client'
const columns = [
{
title: '标题',
dataIndex: 'title',
width: '150px',
ellipsis: true,
scopedSlots: { customRender: 'postTitle' }
},
{
title: '状态',
className: 'status',
dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' }
},
{
title: '分类',
dataIndex: 'categories',
scopedSlots: { customRender: 'categories' }
},
{
title: '标签',
dataIndex: 'tags',
scopedSlots: { customRender: 'tags' }
},
{
title: '评论',
width: '70px',
dataIndex: 'commentCount',
scopedSlots: { customRender: 'commentCount' }
},
{
title: '访问',
width: '70px',
dataIndex: 'visits',
scopedSlots: { customRender: 'visits' }
},
{
title: '发布时间',
dataIndex: 'createTime',
width: '170px',
scopedSlots: { customRender: 'createTime' }
},
{
title: '操作',
width: '180px',
scopedSlots: { customRender: 'action' }
}
]
const postStatus = {
PUBLISHED: {
value: 'PUBLISHED',
color: 'green',
status: 'success',
text: '已发布'
},
DRAFT: {
value: 'DRAFT',
color: 'yellow',
status: 'warning',
text: '草稿'
},
RECYCLE: {
value: 'RECYCLE',
color: 'red',
status: 'error',
text: '回收站'
},
INTIMATE: {
value: 'INTIMATE',
color: 'blue',
status: 'success',
text: '私密'
}
}
export default {
name: 'PostList',
components: {
PageView,
PostSettingModal,
TargetCommentDrawer
},
mixins: [mixin, mixinDevice],
data() {
return {
postStatus,
columns,
list: {
data: [],
loading: false,
total: 0,
hasPrevious: false,
hasNext: false,
params: {
page: 0,
size: 10,
keyword: undefined,
categoryId: undefined,
status: undefined
}
},
categories: {
data: [],
loading: false
},
selectedRowKeys: [],
postSettingVisible: false,
postSettingLoading: false,
postCommentVisible: false,
selectedPost: {}
}
},
computed: {
formattedPosts() {
return this.list.data.map(post => {
post.statusProperty = this.postStatus[post.status]
return post
})
},
pagination() {
return {
page: this.list.params.page + 1,
size: this.list.params.size,
total: this.list.total
}
},
selectPreviousButtonDisabled() {
const index = this.list.data.findIndex(post => post.id === this.selectedPost.id)
return index === 0 && !this.list.hasPrevious
},
selectNextButtonDisabled() {
const index = this.list.data.findIndex(post => post.id === this.selectedPost.id)
return index === this.list.data.length - 1 && !this.list.hasNext
}
},
beforeMount() {
this.handleListCategories()
},
beforeRouteEnter(to, from, next) {
next(vm => {
if (to.query.page) {
vm.list.params.page = Number(to.query.page)
}
if (to.query.size) {
vm.list.params.size = Number(to.query.size)
}
vm.list.params.sort = to.query.sort
vm.list.params.keyword = to.query.keyword
vm.list.params.categoryId = to.query.categoryId
vm.list.params.status = to.query.status
vm.handleListPosts()
})
},
watch: {
'list.params': {
deep: true,
handler: function(newVal) {
if (newVal) {
const params = JSON.parse(JSON.stringify(this.list.params))
const path = this.$router.history.current.path
this.$router.push({ path, query: params }).catch(err => err)
}
}
}
},
methods: {
/**
* Fetch post data
*/
async handleListPosts(enableLoading = true) {
try {
if (enableLoading) {
this.list.loading = true
}
const response = await apiClient.post.list(this.list.params)
this.list.data = response.data.content
this.list.total = response.data.total
this.list.hasPrevious = response.data.hasPrevious
this.list.hasNext = response.data.hasNext
} catch (error) {
this.$log.error(error)
} finally {
this.list.loading = false
}
},
/**
* Fetch categories data
*/
async handleListCategories() {
try {
this.categories.loading = true
const response = await apiClient.category.list({ sort: [], more: true })
this.categories.data = response.data
} catch (error) {
this.$log.error(error)
} finally {
this.categories.loading = false
}
},
handleEditClick(post) {
this.$router.push({ name: 'PostEdit', query: { postId: post.id } })
},
onSelectionChange(selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys
this.$log.debug(`SelectedRowKeys: ${selectedRowKeys}`)
},
getCheckboxProps(post) {
return {
props: {
disabled: this.list.params.status == null || this.list.params.status === '',
name: post.title
}
}
},
/**
* Handle page change
*/
handlePageChange(page = 1) {
this.list.params.page = page - 1
this.handleListPosts()
},
/**
* Handle page size change
*/
handlePageSizeChange(current, size) {
this.$log.debug(`Current: ${current}, PageSize: ${size}`)
this.list.params.page = 0
this.list.params.size = size
this.handleListPosts()
},
/**
* Reset query params
*/
handleResetParam() {
this.list.params.keyword = undefined
this.list.params.categoryId = undefined
this.list.params.status = undefined
this.handleClearRowKeys()
this.handlePageChange(1)
this.handleListCategories()
},
handleQuery() {
this.handleClearRowKeys()
this.handlePageChange(1)
},
handleSelectCategory(category) {
this.list.params.categoryId = category.id
this.handleQuery()
},
handleEditStatusClick(postId, status) {
apiClient.post
.updateStatusById(postId, status)
.then(() => {
this.$message.success('操作成功!')
})
.finally(() => {
this.handleListPosts()
})
},
handleDeleteClick(postId) {
apiClient.post
.delete(postId)
.then(() => {
this.$message.success('删除成功!')
})
.finally(() => {
this.handleListPosts()
})
},
handleEditStatusMore(status) {
if (this.selectedRowKeys.length <= 0) {
this.$message.info('请至少选择一项!')
return
}
apiClient.post
.updateStatusInBatch(this.selectedRowKeys, status)
.then(() => {
this.$log.debug(`postId: ${this.selectedRowKeys}, status: ${status}`)
this.selectedRowKeys = []
})
.finally(() => {
this.handleListPosts()
})
},
handleDeleteMore() {
if (this.selectedRowKeys.length <= 0) {
this.$message.info('请至少选择一项!')
return
}
apiClient.post
.deleteInBatch(this.selectedRowKeys)
.then(() => {
this.$log.debug(`delete: ${this.selectedRowKeys}`)
this.selectedRowKeys = []
})
.finally(() => {
this.handleListPosts()
})
},
handleShowPostSettings(post) {
this.postSettingVisible = true
this.postSettingLoading = true
apiClient.post
.get(post.id)
.then(response => {
this.selectedPost = response.data
})
.finally(() => {
this.postSettingLoading = false
})
},
handleShowPostComments(post) {
apiClient.post.get(post.id).then(response => {
this.selectedPost = response.data
this.postCommentVisible = true
})
},
handlePreview(postId) {
apiClient.post.getPreviewLinkById(postId).then(response => {
window.open(response.data, '_blank')
})
},
handleClearRowKeys() {
this.selectedRowKeys = []
},
onPostSavedCallback() {
this.handleListPosts(false)
},
onPostCommentsClose() {
this.postCommentVisible = false
this.selectedPost = {}
setTimeout(() => {
this.handleListPosts(false)
}, 500)
},
/**
* Select previous post
*/
async handleSelectPrevious() {
const index = this.list.data.findIndex(post => post.id === this.selectedPost.id)
if (index > 0) {
this.postSettingLoading = true
const response = await apiClient.post.get(this.list.data[index - 1].id)
this.selectedPost = response.data
this.postSettingLoading = false
return
}
if (index === 0 && this.list.hasPrevious) {
this.list.params.page--
await this.handleListPosts()
this.postSettingLoading = true
const response = await apiClient.post.get(this.list.data[this.list.data.length - 1].id)
this.selectedPost = response.data
this.postSettingLoading = false
}
},
/**
* Select next post
*/
async handleSelectNext() {
const index = this.list.data.findIndex(post => post.id === this.selectedPost.id)
if (index < this.list.data.length - 1) {
this.postSettingLoading = true
const response = await apiClient.post.get(this.list.data[index + 1].id)
this.selectedPost = response.data
this.postSettingLoading = false
return
}
if (index === this.list.data.length - 1 && this.list.hasNext) {
this.list.params.page++
await this.handleListPosts()
this.postSettingLoading = true
const response = await apiClient.post.get(this.list.data[0].id)
this.selectedPost = response.data
this.postSettingLoading = false
}
}
}
}
</script>