mirror of https://github.com/halo-dev/halo-admin
refactor: post setting (#376)
* refactor: post setting modal * refactor: pagination * refactor: post setting modal * fix: TagSelect * feat: add draft button * feat: add meta editor * style: remove PostSettingDrawer.vuepull/381/head
parent
2939cbbfbd
commit
760fffd605
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-form>
|
||||
<a-form-item v-for="(meta, index) in presetMetas" :key="index">
|
||||
<a-row :gutter="5">
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.key" :disabled="true"><i slot="addonBefore">K</i></a-input>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.value">
|
||||
<i slot="addonBefore">V</i>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form>
|
||||
<a-form-item v-for="(meta, index) in customMetas" :key="index">
|
||||
<a-row :gutter="5">
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.key"><i slot="addonBefore">K</i></a-input>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.value">
|
||||
<i slot="addonBefore">V</i>
|
||||
<a slot="addonAfter" href="javascript:void(0);" @click.prevent="handleRemove(index)">
|
||||
<a-icon type="close" />
|
||||
</a>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="dashed" @click="handleAdd">新增</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import themeApi from '@/api/theme'
|
||||
|
||||
export default {
|
||||
name: 'MetaEditor',
|
||||
props: {
|
||||
target: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
validator: function(value) {
|
||||
return ['post', 'sheet'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
targetId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
metas: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
presetFields: [],
|
||||
presetMetas: [],
|
||||
customMetas: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
presetMetas: {
|
||||
handler() {
|
||||
this.handleChange()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
customMetas: {
|
||||
handler() {
|
||||
this.handleChange()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
targetId() {
|
||||
this.handleGenerateMetas()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleListPresetMetasField()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Fetch preset metas fields
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleListPresetMetasField() {
|
||||
try {
|
||||
const response = await themeApi.getActivatedTheme()
|
||||
this.presetFields = response.data.data[`${this.target}MetaField`] || []
|
||||
|
||||
this.handleGenerateMetas()
|
||||
} catch (e) {
|
||||
this.$log.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate preset and custom metas
|
||||
*/
|
||||
handleGenerateMetas() {
|
||||
this.presetMetas = this.presetFields.map(field => {
|
||||
const meta = this.metas.find(meta => meta.key === field)
|
||||
return meta ? { key: field, value: meta.value } : { key: field, value: '' }
|
||||
})
|
||||
|
||||
this.customMetas = this.metas
|
||||
.filter(meta => this.presetFields.indexOf(meta.key) === -1)
|
||||
.map(meta => {
|
||||
return {
|
||||
key: meta.key,
|
||||
value: meta.value
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new custom meta
|
||||
*/
|
||||
handleAdd() {
|
||||
this.customMetas.push({
|
||||
key: '',
|
||||
value: ''
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove custom meta
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
handleRemove(index) {
|
||||
this.customMetas.splice(index, 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle change
|
||||
*/
|
||||
handleChange() {
|
||||
this.$emit('update:metas', this.presetMetas.concat(this.customMetas))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -7,14 +7,6 @@ import { timeAgo } from '@/utils/datetime'
|
|||
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
Vue.filter('NumberFormat', function(value) {
|
||||
if (!value) {
|
||||
return '0'
|
||||
}
|
||||
// 将整数部分逢三一断
|
||||
return value.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
||||
})
|
||||
|
||||
Vue.filter('moment', function(dataStr, pattern = 'YYYY-MM-DD HH:mm') {
|
||||
return dayjs(dataStr).format(pattern)
|
||||
})
|
||||
|
|
|
@ -32,18 +32,11 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<PostSettingDrawer
|
||||
:categoryIds="selectedCategoryIds"
|
||||
:metas="selectedMetas"
|
||||
<PostSettingModal
|
||||
:post="postToStage"
|
||||
:tagIds="selectedTagIds"
|
||||
:visible="postSettingVisible"
|
||||
@close="postSettingVisible = false"
|
||||
@onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
|
||||
@onRefreshPost="onRefreshPostFromSetting"
|
||||
@onRefreshPostMetas="onRefreshPostMetasFromSetting"
|
||||
@onRefreshTagIds="onRefreshTagIdsFromSetting"
|
||||
@onSaved="handleRestoreSavedStatus"
|
||||
:savedCallback="onPostSavedCallback"
|
||||
:visible.sync="postSettingVisible"
|
||||
@onUpdate="onUpdateFromSetting"
|
||||
/>
|
||||
|
||||
<AttachmentDrawer v-model="attachmentDrawerVisible" />
|
||||
|
@ -54,7 +47,7 @@
|
|||
import { mixin, mixinDevice, mixinPostEdit } from '@/mixins/mixin.js'
|
||||
import { datetimeFormat } from '@/utils/datetime'
|
||||
|
||||
import PostSettingDrawer from './components/PostSettingDrawer'
|
||||
import PostSettingModal from './components/PostSettingModal'
|
||||
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
|
||||
import MarkdownEditor from '@/components/Editor/MarkdownEditor'
|
||||
import { PageView } from '@/layouts'
|
||||
|
@ -64,7 +57,7 @@ import postApi from '@/api/post'
|
|||
export default {
|
||||
mixins: [mixin, mixinDevice, mixinPostEdit],
|
||||
components: {
|
||||
PostSettingDrawer,
|
||||
PostSettingModal,
|
||||
AttachmentDrawer,
|
||||
MarkdownEditor,
|
||||
PageView
|
||||
|
@ -74,9 +67,6 @@ export default {
|
|||
attachmentDrawerVisible: false,
|
||||
postSettingVisible: false,
|
||||
postToStage: {},
|
||||
selectedTagIds: [],
|
||||
selectedCategoryIds: [],
|
||||
selectedMetas: [],
|
||||
contentChanges: 0,
|
||||
draftSaving: false,
|
||||
previewSaving: false,
|
||||
|
@ -89,19 +79,12 @@ export default {
|
|||
next(vm => {
|
||||
if (postId) {
|
||||
postApi.get(postId).then(response => {
|
||||
const post = response.data.data
|
||||
vm.postToStage = post
|
||||
vm.selectedTagIds = post.tagIds
|
||||
vm.selectedCategoryIds = post.categoryIds
|
||||
vm.selectedMetas = post.metas
|
||||
vm.postToStage = response.data.data
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
destroyed: function() {
|
||||
if (this.postSettingVisible) {
|
||||
this.postSettingVisible = false
|
||||
}
|
||||
destroyed() {
|
||||
if (this.attachmentDrawerVisible) {
|
||||
this.attachmentDrawerVisible = false
|
||||
}
|
||||
|
@ -110,9 +93,6 @@ export default {
|
|||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.postSettingVisible) {
|
||||
this.postSettingVisible = false
|
||||
}
|
||||
if (this.attachmentDrawerVisible) {
|
||||
this.attachmentDrawerVisible = false
|
||||
}
|
||||
|
@ -247,17 +227,12 @@ export default {
|
|||
this.contentChanges++
|
||||
this.postToStage.originalContent = val
|
||||
},
|
||||
onRefreshPostFromSetting(post) {
|
||||
onPostSavedCallback() {
|
||||
this.contentChanges = 0
|
||||
this.$router.push({ name: 'PostList' })
|
||||
},
|
||||
onUpdateFromSetting(post) {
|
||||
this.postToStage = post
|
||||
},
|
||||
onRefreshTagIdsFromSetting(tagIds) {
|
||||
this.selectedTagIds = tagIds
|
||||
},
|
||||
onRefreshCategoryIdsFromSetting(categoryIds) {
|
||||
this.selectedCategoryIds = categoryIds
|
||||
},
|
||||
onRefreshPostMetasFromSetting(metas) {
|
||||
this.selectedMetas = metas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
<template>
|
||||
<page-view>
|
||||
<a-card :bordered="false" :bodyStyle="{ padding: '16px' }">
|
||||
<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="queryParam.keyword" @keyup.enter="handleQuery()" />
|
||||
<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="queryParam.status" placeholder="请选择文章状态" @change="handleQuery()" allowClear>
|
||||
<a-select-option v-for="status in Object.keys(postStatus)" :key="status" :value="status">{{
|
||||
postStatus[status].text
|
||||
}}</a-select-option>
|
||||
<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="queryParam.categoryId"
|
||||
v-model="list.params.categoryId"
|
||||
:loading="categories.loading"
|
||||
allowClear
|
||||
placeholder="请选择分类"
|
||||
@change="handleQuery()"
|
||||
:loading="categoriesLoading"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option v-for="category in categories" :key="category.id"
|
||||
>{{ category.name }}({{ category.postCount }})</a-select-option
|
||||
>
|
||||
<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>
|
||||
|
@ -48,36 +48,26 @@
|
|||
|
||||
<div class="table-operator">
|
||||
<router-link :to="{ name: 'PostWrite' }">
|
||||
<a-button type="primary" icon="plus">写文章</a-button>
|
||||
<a-button icon="plus" type="primary">写文章</a-button>
|
||||
</router-link>
|
||||
<a-dropdown v-show="queryParam.status != null && queryParam.status !== '' && !isMobile()">
|
||||
<a-dropdown v-show="list.params.status != null && list.params.status !== '' && !isMobile()">
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item key="1" v-if="queryParam.status === 'DRAFT' || queryParam.status === 'RECYCLE'">
|
||||
<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
|
||||
key="2"
|
||||
v-if="
|
||||
queryParam.status === 'PUBLISHED' || queryParam.status === 'DRAFT' || queryParam.status === 'INTIMATE'
|
||||
"
|
||||
>
|
||||
<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
|
||||
key="3"
|
||||
v-if="
|
||||
queryParam.status === 'RECYCLE' || queryParam.status === 'PUBLISHED' || queryParam.status === 'INTIMATE'
|
||||
"
|
||||
>
|
||||
<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 key="4" v-if="queryParam.status === 'RECYCLE' || queryParam.status === 'DRAFT'">
|
||||
<a-menu-item v-if="['RECYCLE', 'DRAFT'].includes(list.params.status)" key="4">
|
||||
<a href="javascript:void(0);" @click="handleDeleteMore">
|
||||
<span>永久删除</span>
|
||||
</a>
|
||||
|
@ -93,13 +83,13 @@
|
|||
<!-- Mobile -->
|
||||
<a-list
|
||||
v-if="isMobile()"
|
||||
:dataSource="formattedPosts"
|
||||
:loading="list.loading"
|
||||
:pagination="false"
|
||||
itemLayout="vertical"
|
||||
size="large"
|
||||
:pagination="false"
|
||||
:dataSource="formattedPosts"
|
||||
:loading="postsLoading"
|
||||
>
|
||||
<a-list-item slot="renderItem" slot-scope="item, index" :key="index">
|
||||
<a-list-item :key="index" slot="renderItem" slot-scope="item, index">
|
||||
<template slot="actions">
|
||||
<span>
|
||||
<a-icon type="eye" />
|
||||
|
@ -109,34 +99,30 @@
|
|||
<a-icon type="message" />
|
||||
{{ item.commentCount }}
|
||||
</span>
|
||||
<a-dropdown placement="topLeft" :trigger="['click']">
|
||||
<a-dropdown :trigger="['click']" placement="topLeft">
|
||||
<span>
|
||||
<a-icon type="bars" />
|
||||
</span>
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item
|
||||
v-if="item.status === 'PUBLISHED' || item.status === 'DRAFT' || item.status === 'INTIMATE'"
|
||||
>
|
||||
<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 + '】文章?'"
|
||||
@confirm="handleEditStatusClick(item.id, 'PUBLISHED')"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleEditStatusClick(item.id, 'PUBLISHED')"
|
||||
>
|
||||
<a href="javascript:void(0);">还原</a>
|
||||
</a-popconfirm>
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="item.status === 'PUBLISHED' || item.status === 'DRAFT' || item.status === 'INTIMATE'"
|
||||
>
|
||||
<a-menu-item v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(item.status)">
|
||||
<a-popconfirm
|
||||
:title="'你确定要将【' + item.title + '】文章移到回收站?'"
|
||||
@confirm="handleEditStatusClick(item.id, 'RECYCLE')"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleEditStatusClick(item.id, 'RECYCLE')"
|
||||
>
|
||||
<a href="javascript:void(0);">回收站</a>
|
||||
</a-popconfirm>
|
||||
|
@ -144,17 +130,17 @@
|
|||
<a-menu-item v-else-if="item.status === 'RECYCLE'">
|
||||
<a-popconfirm
|
||||
:title="'你确定要永久删除【' + item.title + '】文章?'"
|
||||
@confirm="handleDeleteClick(item.id)"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleDeleteClick(item.id)"
|
||||
>
|
||||
<a href="javascript:void(0);">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a rel="noopener noreferrer" href="javascript:void(0);" @click="handleShowPostSettings(item)"
|
||||
>设置</a
|
||||
>
|
||||
<a href="javascript:void(0);" rel="noopener noreferrer" @click="handleShowPostSettings(item)">
|
||||
设置
|
||||
</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
|
@ -173,29 +159,29 @@
|
|||
style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
|
||||
>
|
||||
<a-icon
|
||||
type="pushpin"
|
||||
v-if="item.topPriority !== 0"
|
||||
style="margin-right: 3px;"
|
||||
theme="twoTone"
|
||||
twoToneColor="red"
|
||||
style="margin-right: 3px;"
|
||||
type="pushpin"
|
||||
/>
|
||||
<a
|
||||
v-if="['PUBLISHED', 'INTIMATE'].includes(item.status)"
|
||||
:href="item.fullPath"
|
||||
target="_blank"
|
||||
class="no-underline"
|
||||
target="_blank"
|
||||
>
|
||||
<a-tooltip placement="top" :title="'点击访问【' + item.title + '】'">{{ item.title }}</a-tooltip>
|
||||
<a-tooltip :title="'点击访问【' + item.title + '】'" placement="top">{{ item.title }}</a-tooltip>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="item.status === 'DRAFT'"
|
||||
href="javascript:void(0)"
|
||||
class="no-underline"
|
||||
href="javascript:void(0)"
|
||||
@click="handlePreview(item.id)"
|
||||
>
|
||||
<a-tooltip placement="topLeft" :title="'点击预览【' + item.title + '】'">{{ item.title }}</a-tooltip>
|
||||
<a-tooltip :title="'点击预览【' + item.title + '】'" placement="topLeft">{{ item.title }}</a-tooltip>
|
||||
</a>
|
||||
<a v-else href="javascript:void(0);" class="no-underline" disabled>
|
||||
<a v-else class="no-underline" disabled href="javascript:void(0);">
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</span>
|
||||
|
@ -207,61 +193,61 @@
|
|||
v-for="(category, categoryIndex) in item.categories"
|
||||
:key="'category_' + categoryIndex"
|
||||
color="blue"
|
||||
@click="handleSelectCategory(category)"
|
||||
style="margin-bottom: 8px"
|
||||
>{{ category.name }}</a-tag
|
||||
>
|
||||
@click="handleSelectCategory(category)"
|
||||
>{{ category.name }}
|
||||
</a-tag>
|
||||
<br />
|
||||
<a-tag
|
||||
v-for="(tag, tagIndex) in item.tags"
|
||||
:key="'tag_' + tagIndex"
|
||||
color="green"
|
||||
style="margin-bottom: 8px"
|
||||
>{{ tag.name }}</a-tag
|
||||
>
|
||||
>{{ 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
|
||||
}"
|
||||
:columns="columns"
|
||||
:dataSource="formattedPosts"
|
||||
:loading="postsLoading"
|
||||
:pagination="false"
|
||||
:scrollToFirstRowOnChange="true"
|
||||
>
|
||||
<span slot="postTitle" slot-scope="text, record">
|
||||
<a-icon
|
||||
type="pushpin"
|
||||
v-if="record.topPriority !== 0"
|
||||
style="margin-right: 3px;"
|
||||
theme="twoTone"
|
||||
twoToneColor="red"
|
||||
style="margin-right: 3px;"
|
||||
type="pushpin"
|
||||
/>
|
||||
<a
|
||||
v-if="['PUBLISHED', 'INTIMATE'].includes(record.status)"
|
||||
:href="record.fullPath"
|
||||
target="_blank"
|
||||
class="no-underline"
|
||||
target="_blank"
|
||||
>
|
||||
<a-tooltip placement="top" :title="'点击访问【' + text + '】'">{{ text }}</a-tooltip>
|
||||
<a-tooltip :title="'点击访问【' + text + '】'" placement="top">{{ text }}</a-tooltip>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="record.status === 'DRAFT'"
|
||||
href="javascript:void(0)"
|
||||
class="no-underline"
|
||||
href="javascript:void(0)"
|
||||
@click="handlePreview(record.id)"
|
||||
>
|
||||
<a-tooltip placement="topLeft" :title="'点击预览【' + text + '】'">{{ text }}</a-tooltip>
|
||||
<a-tooltip :title="'点击预览【' + text + '】'" placement="topLeft">{{ text }}</a-tooltip>
|
||||
</a>
|
||||
<a v-else href="javascript:void(0);" class="no-underline" disabled>
|
||||
<a v-else class="no-underline" disabled href="javascript:void(0);">
|
||||
{{ text }}
|
||||
</a>
|
||||
</span>
|
||||
|
@ -274,8 +260,8 @@
|
|||
v-for="(category, index) in categoriesOfPost"
|
||||
:key="index"
|
||||
color="blue"
|
||||
@click="handleSelectCategory(category)"
|
||||
style="margin-bottom: 8px;cursor:pointer"
|
||||
@click="handleSelectCategory(category)"
|
||||
>{{ category.name }}</a-tag
|
||||
>
|
||||
</span>
|
||||
|
@ -289,14 +275,14 @@
|
|||
<span
|
||||
slot="commentCount"
|
||||
slot-scope="text, record"
|
||||
@click="handleShowPostComments(record)"
|
||||
style="cursor: pointer;"
|
||||
@click="handleShowPostComments(record)"
|
||||
>
|
||||
<a-badge
|
||||
:count="record.commentCount"
|
||||
:numberStyle="{ backgroundColor: '#f38181' }"
|
||||
:showZero="true"
|
||||
:overflowCount="999"
|
||||
:showZero="true"
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
@ -304,8 +290,8 @@
|
|||
<a-badge
|
||||
:count="visits"
|
||||
:numberStyle="{ backgroundColor: '#00e0ff' }"
|
||||
:showZero="true"
|
||||
:overflowCount="9999"
|
||||
:showZero="true"
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
@ -320,17 +306,17 @@
|
|||
|
||||
<span slot="action" slot-scope="text, post">
|
||||
<a
|
||||
v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(post.status)"
|
||||
href="javascript:void(0);"
|
||||
@click="handleEditClick(post)"
|
||||
v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT' || post.status === 'INTIMATE'"
|
||||
>编辑</a
|
||||
>
|
||||
<a-popconfirm
|
||||
:title="'你确定要发布【' + post.title + '】文章?'"
|
||||
@confirm="handleEditStatusClick(post.id, 'PUBLISHED')"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
v-else-if="post.status === 'RECYCLE'"
|
||||
:title="'你确定要发布【' + post.title + '】文章?'"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleEditStatusClick(post.id, 'PUBLISHED')"
|
||||
>
|
||||
<a href="javascript:void(0);">还原</a>
|
||||
</a-popconfirm>
|
||||
|
@ -338,21 +324,21 @@
|
|||
<a-divider type="vertical" />
|
||||
|
||||
<a-popconfirm
|
||||
v-if="['PUBLISHED', 'DRAFT', 'INTIMATE'].includes(post.status)"
|
||||
:title="'你确定要将【' + post.title + '】文章移到回收站?'"
|
||||
@confirm="handleEditStatusClick(post.id, 'RECYCLE')"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT' || post.status === 'INTIMATE'"
|
||||
okText="确定"
|
||||
@confirm="handleEditStatusClick(post.id, 'RECYCLE')"
|
||||
>
|
||||
<a href="javascript:void(0);">回收站</a>
|
||||
</a-popconfirm>
|
||||
|
||||
<a-popconfirm
|
||||
:title="'你确定要永久删除【' + post.title + '】文章?'"
|
||||
@confirm="handleDeleteClick(post.id)"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
v-else-if="post.status === 'RECYCLE'"
|
||||
:title="'你确定要永久删除【' + post.title + '】文章?'"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleDeleteClick(post.id)"
|
||||
>
|
||||
<a href="javascript:void(0);">删除</a>
|
||||
</a-popconfirm>
|
||||
|
@ -364,42 +350,43 @@
|
|||
</a-table>
|
||||
<div class="page-wrapper">
|
||||
<a-pagination
|
||||
v-if="posts && posts.length > 0"
|
||||
class="pagination"
|
||||
:current="pagination.page"
|
||||
:total="pagination.total"
|
||||
:defaultPageSize="pagination.size"
|
||||
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']"
|
||||
showSizeChanger
|
||||
@showSizeChange="handlePaginationChange"
|
||||
@change="handlePaginationChange"
|
||||
:pageSizeOptions="['10', '20', '50', '100']"
|
||||
:total="pagination.total"
|
||||
class="pagination"
|
||||
showLessItems
|
||||
showSizeChanger
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<PostSettingDrawer
|
||||
<PostSettingModal
|
||||
:loading="postSettingLoading"
|
||||
:post="selectedPost"
|
||||
:tagIds="selectedTagIds"
|
||||
:categoryIds="selectedCategoryIds"
|
||||
:metas="selectedMetas"
|
||||
:needTitle="true"
|
||||
:saveDraftButton="false"
|
||||
:visible="postSettingVisible"
|
||||
@close="onPostSettingsClose"
|
||||
@onRefreshPost="onRefreshPostFromSetting"
|
||||
@onRefreshTagIds="onRefreshTagIdsFromSetting"
|
||||
@onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
|
||||
@onRefreshPostMetas="onRefreshPostMetasFromSetting"
|
||||
/>
|
||||
: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
|
||||
:visible="postCommentVisible"
|
||||
:title="selectedPost.title"
|
||||
:id="selectedPost.id"
|
||||
:description="selectedPost.summary"
|
||||
:target="`posts`"
|
||||
:id="selectedPost.id"
|
||||
:title="selectedPost.title"
|
||||
:visible="postCommentVisible"
|
||||
@close="onPostCommentsClose"
|
||||
/>
|
||||
</page-view>
|
||||
|
@ -408,7 +395,7 @@
|
|||
<script>
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
import { PageView } from '@/layouts'
|
||||
import PostSettingDrawer from './components/PostSettingDrawer'
|
||||
import PostSettingModal from './components/PostSettingModal.vue'
|
||||
import TargetCommentDrawer from '../comment/components/TargetCommentDrawer'
|
||||
import categoryApi from '@/api/category'
|
||||
import postApi from '@/api/post'
|
||||
|
@ -466,92 +453,90 @@ export default {
|
|||
name: 'PostList',
|
||||
components: {
|
||||
PageView,
|
||||
PostSettingDrawer,
|
||||
PostSettingModal,
|
||||
TargetCommentDrawer
|
||||
},
|
||||
mixins: [mixin, mixinDevice],
|
||||
data() {
|
||||
return {
|
||||
postStatus: postApi.postStatus,
|
||||
pagination: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: null,
|
||||
total: 1
|
||||
},
|
||||
queryParam: {
|
||||
page: 0,
|
||||
size: 10,
|
||||
sort: null,
|
||||
keyword: null,
|
||||
categoryId: null,
|
||||
status: null
|
||||
},
|
||||
// 表头
|
||||
columns,
|
||||
selectedRowKeys: [],
|
||||
categories: [],
|
||||
selectedMetas: [
|
||||
{
|
||||
key: '',
|
||||
value: ''
|
||||
list: {
|
||||
data: [],
|
||||
loading: false,
|
||||
total: 0,
|
||||
hasPrevious: false,
|
||||
hasNext: false,
|
||||
params: {
|
||||
page: 0,
|
||||
size: 10,
|
||||
keyword: null,
|
||||
categoryId: null,
|
||||
status: null
|
||||
}
|
||||
],
|
||||
posts: [],
|
||||
postsLoading: false,
|
||||
categoriesLoading: false,
|
||||
},
|
||||
|
||||
categories: {
|
||||
data: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
selectedRowKeys: [],
|
||||
postSettingVisible: false,
|
||||
postSettingLoading: false,
|
||||
postCommentVisible: false,
|
||||
selectedPost: {},
|
||||
selectedTagIds: [],
|
||||
selectedCategoryIds: []
|
||||
selectedPost: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formattedPosts() {
|
||||
return this.posts.map(post => {
|
||||
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()
|
||||
},
|
||||
destroyed: function() {
|
||||
if (this.postSettingVisible) {
|
||||
this.postSettingVisible = false
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
next(vm => {
|
||||
if (to.query.page) {
|
||||
vm.pagination.page = Number(to.query.page) + 1
|
||||
vm.list.params.page = Number(to.query.page)
|
||||
}
|
||||
if (to.query.size) {
|
||||
vm.pagination.size = Number(to.query.size)
|
||||
vm.list.params.size = Number(to.query.size)
|
||||
}
|
||||
|
||||
vm.queryParam.sort = to.query.sort
|
||||
vm.queryParam.keyword = to.query.keyword
|
||||
vm.queryParam.categoryId = to.query.categoryId
|
||||
vm.queryParam.status = to.query.status
|
||||
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()
|
||||
})
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.postSettingVisible) {
|
||||
this.postSettingVisible = false
|
||||
}
|
||||
next()
|
||||
},
|
||||
watch: {
|
||||
queryParam: {
|
||||
'list.params': {
|
||||
deep: true,
|
||||
handler: function(newVal) {
|
||||
if (newVal) {
|
||||
const params = JSON.parse(JSON.stringify(this.queryParam))
|
||||
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)
|
||||
}
|
||||
|
@ -559,36 +544,42 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
handleListPosts(enableLoading = true) {
|
||||
if (enableLoading) {
|
||||
this.postsLoading = true
|
||||
/**
|
||||
* Fetch post data
|
||||
*/
|
||||
async handleListPosts(enableLoading = true) {
|
||||
try {
|
||||
if (enableLoading) {
|
||||
this.list.loading = true
|
||||
}
|
||||
const response = await postApi.query(this.list.params)
|
||||
|
||||
this.list.data = response.data.data.content
|
||||
this.list.total = response.data.data.total
|
||||
this.list.hasPrevious = response.data.data.hasPrevious
|
||||
this.list.hasNext = response.data.data.hasNext
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.list.loading = false
|
||||
}
|
||||
// Set from pagination
|
||||
this.queryParam.page = this.pagination.page - 1
|
||||
this.queryParam.size = this.pagination.size
|
||||
this.queryParam.sort = this.pagination.sort
|
||||
postApi
|
||||
.query(this.queryParam)
|
||||
.then(response => {
|
||||
this.posts = response.data.data.content
|
||||
this.pagination.total = response.data.data.total
|
||||
})
|
||||
.finally(() => {
|
||||
this.postsLoading = false
|
||||
})
|
||||
},
|
||||
handleListCategories() {
|
||||
this.categoriesLoading = true
|
||||
categoryApi
|
||||
.listAll(true)
|
||||
.then(response => {
|
||||
this.categories = response.data.data
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.categoriesLoading = false
|
||||
}, 200)
|
||||
})
|
||||
|
||||
/**
|
||||
* Fetch categories data
|
||||
*/
|
||||
async handleListCategories() {
|
||||
try {
|
||||
this.categories.loading = true
|
||||
|
||||
const response = await categoryApi.listAll(true)
|
||||
|
||||
this.categories.data = response.data.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.categories.loading = false
|
||||
}
|
||||
},
|
||||
handleEditClick(post) {
|
||||
this.$router.push({ name: 'PostEdit', query: { postId: post.id } })
|
||||
|
@ -600,31 +591,47 @@ export default {
|
|||
getCheckboxProps(post) {
|
||||
return {
|
||||
props: {
|
||||
disabled: this.queryParam.status == null || this.queryParam.status === '',
|
||||
disabled: this.list.params.status == null || this.list.params.status === '',
|
||||
name: post.title
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePaginationChange(page, pageSize) {
|
||||
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
|
||||
this.pagination.page = page
|
||||
this.pagination.size = pageSize
|
||||
|
||||
/**
|
||||
* 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.queryParam.keyword = null
|
||||
this.queryParam.categoryId = null
|
||||
this.queryParam.status = null
|
||||
this.list.params.keyword = null
|
||||
this.list.params.categoryId = null
|
||||
this.list.params.status = null
|
||||
this.handleClearRowKeys()
|
||||
this.handlePaginationChange(1, this.pagination.size)
|
||||
this.handlePageChange(1)
|
||||
this.handleListCategories()
|
||||
},
|
||||
handleQuery() {
|
||||
this.handleClearRowKeys()
|
||||
this.handlePaginationChange(1, this.pagination.size)
|
||||
this.handlePageChange(1)
|
||||
},
|
||||
handleSelectCategory(category) {
|
||||
this.queryParam.categoryId = category.id
|
||||
this.list.params.categoryId = category.id
|
||||
this.handleQuery()
|
||||
},
|
||||
handleEditStatusClick(postId, status) {
|
||||
|
@ -678,13 +685,16 @@ export default {
|
|||
})
|
||||
},
|
||||
handleShowPostSettings(post) {
|
||||
postApi.get(post.id).then(response => {
|
||||
this.selectedPost = response.data.data
|
||||
this.selectedTagIds = this.selectedPost.tagIds
|
||||
this.selectedCategoryIds = this.selectedPost.categoryIds
|
||||
this.selectedMetas = this.selectedPost.metas
|
||||
this.postSettingVisible = true
|
||||
})
|
||||
this.postSettingVisible = true
|
||||
this.postSettingLoading = true
|
||||
postApi
|
||||
.get(post.id)
|
||||
.then(response => {
|
||||
this.selectedPost = response.data.data
|
||||
})
|
||||
.finally(() => {
|
||||
this.postSettingLoading = false
|
||||
})
|
||||
},
|
||||
handleShowPostComments(post) {
|
||||
postApi.get(post.id).then(response => {
|
||||
|
@ -700,13 +710,8 @@ export default {
|
|||
handleClearRowKeys() {
|
||||
this.selectedRowKeys = []
|
||||
},
|
||||
// 关闭文章设置抽屉
|
||||
onPostSettingsClose() {
|
||||
this.postSettingVisible = false
|
||||
this.selectedPost = {}
|
||||
setTimeout(() => {
|
||||
this.handleListPosts(false)
|
||||
}, 500)
|
||||
onPostSavedCallback() {
|
||||
this.handleListPosts(false)
|
||||
},
|
||||
onPostCommentsClose() {
|
||||
this.postCommentVisible = false
|
||||
|
@ -715,17 +720,50 @@ export default {
|
|||
this.handleListPosts(false)
|
||||
}, 500)
|
||||
},
|
||||
onRefreshPostFromSetting(post) {
|
||||
this.selectedPost = post
|
||||
|
||||
/**
|
||||
* 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 postApi.get(this.list.data[index - 1].id)
|
||||
this.selectedPost = response.data.data
|
||||
this.postSettingLoading = false
|
||||
return
|
||||
}
|
||||
if (index === 0 && this.list.hasPrevious) {
|
||||
this.list.params.page--
|
||||
await this.handleListPosts()
|
||||
this.postSettingLoading = true
|
||||
const response = await postApi.get(this.list.data[this.list.data.length - 1].id)
|
||||
this.selectedPost = response.data.data
|
||||
this.postSettingLoading = false
|
||||
}
|
||||
},
|
||||
onRefreshTagIdsFromSetting(tagIds) {
|
||||
this.selectedTagIds = tagIds
|
||||
},
|
||||
onRefreshCategoryIdsFromSetting(categoryIds) {
|
||||
this.selectedCategoryIds = categoryIds
|
||||
},
|
||||
onRefreshPostMetasFromSetting(metas) {
|
||||
this.selectedMetas = metas
|
||||
|
||||
/**
|
||||
* 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 postApi.get(this.list.data[index + 1].id)
|
||||
this.selectedPost = response.data.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 postApi.get(this.list.data[0].id)
|
||||
this.selectedPost = response.data.data
|
||||
this.postSettingLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<a-tree checkable :treeData="categoryTree" defaultExpandAll checkStrictly :checkedKeys="categoryIds" @check="onCheck">
|
||||
<a-tree
|
||||
checkable
|
||||
:treeData="categoryTree"
|
||||
defaultExpandAll
|
||||
checkStrictly
|
||||
showLine
|
||||
:checkedKeys="categoryIds"
|
||||
@check="onCheck"
|
||||
>
|
||||
</a-tree>
|
||||
</template>
|
||||
|
||||
|
@ -17,19 +25,42 @@ export default {
|
|||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
categories: {
|
||||
data: [],
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
categoryTree() {
|
||||
return categoryApi.concreteTree(this.categories)
|
||||
if (!this.categories.data.length) {
|
||||
return []
|
||||
}
|
||||
return categoryApi.concreteTree(this.categories.data)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleListCategories()
|
||||
},
|
||||
methods: {
|
||||
async handleListCategories() {
|
||||
try {
|
||||
this.categories.loading = true
|
||||
|
||||
const { data } = await categoryApi.listAll()
|
||||
|
||||
this.categories.data = data.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.categories.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
onCheck(checkedKeys, e) {
|
||||
this.$log.debug('Chekced keys', checkedKeys)
|
||||
this.$log.debug('e', e)
|
||||
|
|
|
@ -1,567 +0,0 @@
|
|||
<template>
|
||||
<a-drawer
|
||||
title="文章设置"
|
||||
:width="isMobile() ? '100%' : '480'"
|
||||
placement="right"
|
||||
closable
|
||||
destroyOnClose
|
||||
@close="onClose"
|
||||
:visible="visible"
|
||||
:afterVisibleChange="handleAfterVisibleChanged"
|
||||
>
|
||||
<div class="post-setting-drawer-content">
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">基本设置</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<a-form>
|
||||
<a-form-item label="文章标题:" v-if="needTitle">
|
||||
<a-input v-model="selectedPost.title" />
|
||||
</a-form-item>
|
||||
<a-form-item label="文章别名:" :help="fullPath">
|
||||
<a-input v-model="selectedPost.slug">
|
||||
<template #addonAfter>
|
||||
<a-popconfirm
|
||||
title="是否确定根据标题重新生成别名?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
placement="left"
|
||||
@confirm="handleSetPinyinSlug"
|
||||
>
|
||||
<a-icon class="cursor-pointer" type="sync" />
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="发表时间:">
|
||||
<a-date-picker
|
||||
showTime
|
||||
:defaultValue="pickerDefaultValue"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择文章发表时间"
|
||||
@change="onPostDateChange"
|
||||
@ok="onPostDateOk"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="开启评论:">
|
||||
<a-radio-group v-model="selectedPost.disallowComment" :defaultValue="false">
|
||||
<a-radio :value="false">开启</a-radio>
|
||||
<a-radio :value="true">关闭</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否置顶:">
|
||||
<a-radio-group v-model="selectedPost.topPriority" :defaultValue="0">
|
||||
<a-radio :value="1">是</a-radio>
|
||||
<a-radio :value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="自定义模板:" v-if="customTpls.length > 0">
|
||||
<a-select v-model="selectedPost.template">
|
||||
<a-select-option key="" value="">无</a-select-option>
|
||||
<a-select-option v-for="tpl in customTpls" :key="tpl" :value="tpl">{{ tpl }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">分类目录</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<a-form>
|
||||
<a-form-item>
|
||||
<category-tree v-model="selectedCategoryIds" :categories="categories" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="categoryFormVisible">
|
||||
<category-select-tree :categories="categories" v-model="categoryToCreate.parentId" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="categoryFormVisible">
|
||||
<a-input placeholder="分类名称" v-model="categoryToCreate.name" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="categoryFormVisible">
|
||||
<a-input placeholder="分类路径" v-model="categoryToCreate.slug" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" v-if="categoryFormVisible" @click="handlerCreateCategory">保存</a-button>
|
||||
<a-button type="dashed" v-if="!categoryFormVisible" @click="categoryFormVisible = true">新增</a-button>
|
||||
<a-button v-if="categoryFormVisible" @click="categoryFormVisible = false">取消</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">标签</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<a-form>
|
||||
<a-form-item>
|
||||
<TagSelect v-model="selectedTagIds" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">摘要</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<a-form>
|
||||
<a-form-item>
|
||||
<a-input
|
||||
type="textarea"
|
||||
:autoSize="{ minRows: 5 }"
|
||||
v-model="selectedPost.summary"
|
||||
placeholder="如不填写,会从文章中自动截取"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">封面图</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<div class="post-thumb">
|
||||
<a-space direction="vertical">
|
||||
<img
|
||||
class="img"
|
||||
:src="selectedPost.thumbnail || '/images/placeholder.jpg'"
|
||||
@click="thumbDrawerVisible = true"
|
||||
/>
|
||||
<a-input v-model="selectedPost.thumbnail" placeholder="点击封面图选择图片,或者输入外部链接"></a-input>
|
||||
<a-button type="dashed" @click="selectedPost.thumbnail = null">移除</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider class="divider-transparent" />
|
||||
</div>
|
||||
<AttachmentSelectDrawer v-model="thumbDrawerVisible" @listenToSelect="handleSelectPostThumb" :drawerWidth="480" />
|
||||
|
||||
<a-drawer
|
||||
title="高级设置"
|
||||
:width="isMobile() ? '100%' : '480'"
|
||||
placement="right"
|
||||
closable
|
||||
destroyOnClose
|
||||
@close="advancedVisible = false"
|
||||
:visible="advancedVisible"
|
||||
>
|
||||
<div class="post-setting-drawer-content">
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">加密设置</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<a-form>
|
||||
<a-form-item label="访问密码:">
|
||||
<a-input-password v-model="selectedPost.password" autocomplete="new-password" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">SEO 设置</h3>
|
||||
<div class="post-setting-drawer-item">
|
||||
<a-form>
|
||||
<a-form-item label="自定义关键词:">
|
||||
<a-input
|
||||
v-model="selectedPost.metaKeywords"
|
||||
placeholder="多个关键词以英文逗号隔开,如不填写,将自动使用标签作为关键词"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="自定义描述:">
|
||||
<a-input
|
||||
type="textarea"
|
||||
:autoSize="{ minRows: 5 }"
|
||||
v-model="selectedPost.metaDescription"
|
||||
placeholder="如不填写,会从文章中自动截取"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="mb-4">
|
||||
<h3 class="post-setting-drawer-title">元数据</h3>
|
||||
<a-form>
|
||||
<a-form-item v-for="(meta, index) in selectedMetas" :key="index" :prop="'metas.' + index + '.value'">
|
||||
<a-row :gutter="5">
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.key"><i slot="addonBefore">K</i></a-input>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.value">
|
||||
<i slot="addonBefore">V</i>
|
||||
<a href="javascript:void(0);" slot="addonAfter" @click.prevent="handleRemovePostMeta(meta)">
|
||||
<a-icon type="close" />
|
||||
</a>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="dashed" @click="handleInsertPostMeta">新增</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<a-divider class="divider-transparent" />
|
||||
</div>
|
||||
<div class="bottom-control">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="advancedVisible = false">返回</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-drawer>
|
||||
|
||||
<div class="bottom-control">
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="advancedVisible = true">高级</a-button>
|
||||
<ReactiveButton
|
||||
type="danger"
|
||||
v-if="saveDraftButton"
|
||||
@click="handleDraftClick"
|
||||
@callback="handleSavedCallback"
|
||||
:loading="draftSaving"
|
||||
:errored="draftSaveErrored"
|
||||
text="保存草稿"
|
||||
loadedText="保存成功"
|
||||
erroredText="保存失败"
|
||||
></ReactiveButton>
|
||||
<ReactiveButton
|
||||
@click="handlePublishClick()"
|
||||
@callback="handleSavedCallback"
|
||||
:loading="saving"
|
||||
:errored="saveErrored"
|
||||
:text="`${selectedPost.id ? '保存' : '发布'}`"
|
||||
:loadedText="`${selectedPost.id ? '保存' : '发布'}成功`"
|
||||
:erroredText="`${selectedPost.id ? '保存' : '发布'}失败`"
|
||||
></ReactiveButton>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script>
|
||||
// components
|
||||
import CategoryTree from './CategoryTree'
|
||||
import CategorySelectTree from './CategorySelectTree'
|
||||
import TagSelect from './TagSelect'
|
||||
|
||||
// libs
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
import { datetimeFormat } from '@/utils/datetime'
|
||||
import pinyin from 'tiny-pinyin'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
// apis
|
||||
import categoryApi from '@/api/category'
|
||||
import postApi from '@/api/post'
|
||||
import themeApi from '@/api/theme'
|
||||
|
||||
export default {
|
||||
name: 'PostSettingDrawer',
|
||||
mixins: [mixin, mixinDevice],
|
||||
components: {
|
||||
CategoryTree,
|
||||
CategorySelectTree,
|
||||
TagSelect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
thumbDrawerVisible: false,
|
||||
categoryFormVisible: false,
|
||||
advancedVisible: false,
|
||||
selectedPost: this.post,
|
||||
selectedTagIds: this.tagIds,
|
||||
selectedCategoryIds: this.categoryIds,
|
||||
categories: [],
|
||||
categoryToCreate: {},
|
||||
customTpls: [],
|
||||
saving: false,
|
||||
saveErrored: false,
|
||||
draftSaving: false,
|
||||
draftSaveErrored: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
post: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
tagIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
categoryIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
metas: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
needTitle: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
saveDraftButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
post(val) {
|
||||
this.selectedPost = val
|
||||
},
|
||||
selectedPost(val) {
|
||||
this.$emit('onRefreshPost', val)
|
||||
},
|
||||
tagIds(val) {
|
||||
this.selectedTagIds = val
|
||||
},
|
||||
selectedTagIds(val) {
|
||||
this.$emit('onRefreshTagIds', val)
|
||||
},
|
||||
categoryIds(val) {
|
||||
this.selectedCategoryIds = val
|
||||
},
|
||||
selectedCategoryIds(val) {
|
||||
this.$emit('onRefreshCategoryIds', val)
|
||||
},
|
||||
selectedMetas(val) {
|
||||
this.$emit('onRefreshPostMetas', val)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['options']),
|
||||
selectedMetas() {
|
||||
return this.metas
|
||||
},
|
||||
pickerDefaultValue() {
|
||||
if (this.selectedPost.createTime) {
|
||||
const date = new Date(this.selectedPost.createTime)
|
||||
return datetimeFormat(date, 'YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
return datetimeFormat(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
fullPath() {
|
||||
const permalinkType = this.options.post_permalink_type
|
||||
const blogUrl = this.options.blog_url
|
||||
const archivesPrefix = this.options.archives_prefix
|
||||
const pathSuffix = this.options.path_suffix ? this.options.path_suffix : ''
|
||||
switch (permalinkType) {
|
||||
case 'DEFAULT':
|
||||
return `${blogUrl}/${archivesPrefix}/${
|
||||
this.selectedPost.slug ? this.selectedPost.slug : '{slug}'
|
||||
}${pathSuffix}`
|
||||
case 'YEAR':
|
||||
return `${blogUrl}${datetimeFormat(
|
||||
this.selectedPost.createTime ? this.selectedPost.createTime : new Date(),
|
||||
'/YYYY/'
|
||||
)}${this.selectedPost.slug ? this.selectedPost.slug : '{slug}'}${pathSuffix}`
|
||||
case 'DATE':
|
||||
return `${blogUrl}${datetimeFormat(
|
||||
this.selectedPost.createTime ? this.selectedPost.createTime : new Date(),
|
||||
'/YYYY/MM/'
|
||||
)}${this.selectedPost.slug ? this.selectedPost.slug : '{slug}'}${pathSuffix}`
|
||||
case 'DAY':
|
||||
return `${blogUrl}${datetimeFormat(
|
||||
this.selectedPost.createTime ? this.selectedPost.createTime : new Date(),
|
||||
'/YYYY/MM/DD/'
|
||||
)}${this.selectedPost.slug ? this.selectedPost.slug : '{slug}'}${pathSuffix}`
|
||||
case 'ID':
|
||||
return `${blogUrl}/?p=${this.selectedPost.id ? this.selectedPost.id : '{id}'}`
|
||||
case 'ID_SLUG':
|
||||
return `${blogUrl}/${archivesPrefix}/${this.selectedPost.id ? this.selectedPost.id : '{id}'}${pathSuffix}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAfterVisibleChanged(visible) {
|
||||
if (visible) {
|
||||
this.handleListCategories()
|
||||
this.handleListPresetMetasField()
|
||||
this.handleListCustomTpls()
|
||||
if (!this.selectedPost.slug && !this.selectedPost.id) {
|
||||
this.handleSetPinyinSlug()
|
||||
}
|
||||
}
|
||||
},
|
||||
handleListCategories() {
|
||||
categoryApi.listAll().then(response => {
|
||||
this.categories = response.data.data
|
||||
})
|
||||
},
|
||||
handleListPresetMetasField() {
|
||||
if (this.metas.length <= 0) {
|
||||
themeApi.getActivatedTheme().then(response => {
|
||||
const fields = response.data.data.postMetaField
|
||||
if (fields && fields.length > 0) {
|
||||
for (let i = 0, len = fields.length; i < len; i++) {
|
||||
this.selectedMetas.push({
|
||||
value: '',
|
||||
key: fields[i]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
handleListCustomTpls() {
|
||||
themeApi.customPostTpls().then(response => {
|
||||
this.customTpls = response.data.data
|
||||
})
|
||||
},
|
||||
handleSelectPostThumb(data) {
|
||||
this.selectedPost.thumbnail = encodeURI(data.path)
|
||||
this.thumbDrawerVisible = false
|
||||
},
|
||||
handlerCreateCategory() {
|
||||
if (!this.categoryToCreate.name) {
|
||||
this.$notification['error']({
|
||||
message: '提示',
|
||||
description: '分类名称不能为空!'
|
||||
})
|
||||
return
|
||||
}
|
||||
categoryApi
|
||||
.create(this.categoryToCreate)
|
||||
.then(() => {
|
||||
this.categoryToCreate = {}
|
||||
this.categoryFormVisible = false
|
||||
})
|
||||
.finally(() => {
|
||||
this.handleListCategories()
|
||||
})
|
||||
},
|
||||
handleDraftClick() {
|
||||
this.selectedPost.status = 'DRAFT'
|
||||
this.createOrUpdatePost()
|
||||
},
|
||||
handlePublishClick() {
|
||||
this.selectedPost.status = 'PUBLISHED'
|
||||
this.createOrUpdatePost()
|
||||
},
|
||||
createOrUpdatePost() {
|
||||
if (!this.selectedPost.title) {
|
||||
this.$notification['error']({
|
||||
message: '提示',
|
||||
description: '文章标题不能为空!'
|
||||
})
|
||||
return
|
||||
}
|
||||
// Set category ids
|
||||
this.selectedPost.categoryIds = this.selectedCategoryIds
|
||||
// Set tag ids
|
||||
this.selectedPost.tagIds = this.selectedTagIds
|
||||
// Set post metas
|
||||
this.selectedPost.metas = this.selectedMetas
|
||||
if (this.selectedPost.status === 'DRAFT') {
|
||||
this.draftSaving = true
|
||||
} else {
|
||||
this.saving = true
|
||||
}
|
||||
if (this.selectedPost.id) {
|
||||
// Update the post
|
||||
postApi
|
||||
.update(this.selectedPost.id, this.selectedPost, false)
|
||||
.catch(() => {
|
||||
if (this.selectedPost.status === 'DRAFT') {
|
||||
this.draftSaveErrored = true
|
||||
} else {
|
||||
this.saveErrored = true
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.saving = false
|
||||
this.draftSaving = false
|
||||
}, 400)
|
||||
})
|
||||
} else {
|
||||
// Create the post
|
||||
postApi
|
||||
.create(this.selectedPost, false)
|
||||
.catch(() => {
|
||||
if (this.selectedPost.status === 'DRAFT') {
|
||||
this.draftSaveErrored = true
|
||||
} else {
|
||||
this.saveErrored = true
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
this.selectedPost = response.data.data
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.saving = false
|
||||
this.draftSaving = false
|
||||
}, 400)
|
||||
})
|
||||
}
|
||||
},
|
||||
handleSavedCallback() {
|
||||
if (this.draftSaveErrored || this.saveErrored) {
|
||||
this.draftSaveErrored = false
|
||||
this.saveErrored = false
|
||||
} else {
|
||||
this.$emit('onSaved', true)
|
||||
this.$router.push({ name: 'PostList' })
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('close', false)
|
||||
},
|
||||
onPostDateChange(value) {
|
||||
this.selectedPost.createTime = value.valueOf()
|
||||
},
|
||||
onPostDateOk(value) {
|
||||
this.selectedPost.createTime = value.valueOf()
|
||||
},
|
||||
handleRemovePostMeta(item) {
|
||||
const index = this.selectedMetas.indexOf(item)
|
||||
if (index !== -1) {
|
||||
this.selectedMetas.splice(index, 1)
|
||||
}
|
||||
},
|
||||
handleInsertPostMeta() {
|
||||
this.selectedMetas.push({
|
||||
value: '',
|
||||
key: ''
|
||||
})
|
||||
},
|
||||
handleSetPinyinSlug() {
|
||||
if (this.selectedPost.title) {
|
||||
if (pinyin.isSupported()) {
|
||||
let result = ''
|
||||
const tokens = pinyin.parse(this.selectedPost.title.replace(/\s+/g, '').toLowerCase())
|
||||
let lastToken
|
||||
tokens.forEach(token => {
|
||||
if (token.type === 2) {
|
||||
const target = token.target ? token.target.toLowerCase() : ''
|
||||
result += result && !/\n|\s/.test(lastToken.target) ? '-' + target : target
|
||||
} else {
|
||||
result += (lastToken && lastToken.type === 2 ? '-' : '') + token.target
|
||||
}
|
||||
lastToken = token
|
||||
})
|
||||
this.$set(this.selectedPost, 'slug', result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,394 @@
|
|||
<template>
|
||||
<a-modal
|
||||
v-model="modalVisible"
|
||||
:afterClose="onClosed"
|
||||
:bodyStyle="{ padding: 0 }"
|
||||
:maskClosable="false"
|
||||
:width="680"
|
||||
destroyOnClose
|
||||
>
|
||||
<template #title>
|
||||
{{ modalTitle }}
|
||||
<a-icon v-if="loading" type="loading" />
|
||||
</template>
|
||||
|
||||
<div class="card-container">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="normal" tab="常规">
|
||||
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" labelAlign="left">
|
||||
<a-form-item label="文章标题">
|
||||
<a-input v-model="form.model.title" />
|
||||
</a-form-item>
|
||||
<a-form-item :help="fullPath" label="文章别名">
|
||||
<a-input v-model="form.model.slug">
|
||||
<template #addonAfter>
|
||||
<a-popconfirm
|
||||
cancel-text="取消"
|
||||
ok-text="确定"
|
||||
placement="left"
|
||||
title="是否确定根据标题重新生成别名?"
|
||||
@confirm="handleGenerateSlug"
|
||||
>
|
||||
<a-icon class="cursor-pointer" type="sync" />
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="分类目录">
|
||||
<category-tree v-model="form.model.categoryIds" />
|
||||
</a-form-item>
|
||||
<a-form-item label="标签">
|
||||
<TagSelect v-model="form.model.tagIds" />
|
||||
</a-form-item>
|
||||
<a-form-item label="摘要">
|
||||
<a-input
|
||||
v-model="form.model.summary"
|
||||
:autoSize="{ minRows: 5 }"
|
||||
placeholder="如不填写,会从文章中自动截取"
|
||||
type="textarea"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="advanced" tab="高级">
|
||||
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" labelAlign="left">
|
||||
<a-form-item label="禁止评论">
|
||||
<a-switch v-model="form.model.disallowComment" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否置顶">
|
||||
<a-switch v-model="topPriority" />
|
||||
</a-form-item>
|
||||
<a-form-item label="发表时间:">
|
||||
<a-date-picker
|
||||
:defaultValue="createTimeDefaultValue"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择文章发表时间"
|
||||
showTime
|
||||
@change="onCreateTimeSelect"
|
||||
@ok="onCreateTimeSelect"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="自定义模板:">
|
||||
<a-select v-model="form.model.template">
|
||||
<a-select-option key="" value="">无</a-select-option>
|
||||
<a-select-option v-for="template in templates" :key="template" :value="template">
|
||||
{{ template }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="访问密码:">
|
||||
<a-input-password v-model="form.model.password" autocomplete="new-password" />
|
||||
</a-form-item>
|
||||
<a-form-item label="封面图:">
|
||||
<div class="post-thumb">
|
||||
<a-space direction="vertical">
|
||||
<img
|
||||
:src="form.model.thumbnail || '/images/placeholder.jpg'"
|
||||
alt="Post cover thumbnail"
|
||||
class="img"
|
||||
@click="attachmentSelectVisible = true"
|
||||
/>
|
||||
<a-input v-model="form.model.thumbnail" placeholder="点击封面图选择图片,或者输入外部链接"></a-input>
|
||||
<a-button type="dashed" @click="form.model.thumbnail = null">移除</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="seo" tab="SEO">
|
||||
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" labelAlign="left">
|
||||
<a-form-item label="自定义关键词">
|
||||
<a-input
|
||||
v-model="form.model.metaKeywords"
|
||||
:autoSize="{ minRows: 5 }"
|
||||
placeholder="多个关键词以英文逗号隔开,如不填写,将自动使用标签作为关键词"
|
||||
type="textarea"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="自定义描述">
|
||||
<a-input
|
||||
v-model="form.model.metaDescription"
|
||||
:autoSize="{ minRows: 5 }"
|
||||
placeholder="如不填写,会从文章中自动截取"
|
||||
type="textarea"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="meta" tab="元数据">
|
||||
<MetaEditor :metas.sync="form.model.metas" :targetId="form.model.id" target="post" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<template slot="footer">
|
||||
<slot name="extraFooter" />
|
||||
<a-button :disabled="loading" @click="modalVisible = false">
|
||||
关闭
|
||||
</a-button>
|
||||
<ReactiveButton
|
||||
v-if="!form.model.id"
|
||||
:errored="form.draftSaveErrored"
|
||||
:loading="form.draftSaving"
|
||||
erroredText="保存失败"
|
||||
loadedText="保存成功"
|
||||
text="保存草稿"
|
||||
type="danger"
|
||||
@callback="handleSavedCallback"
|
||||
@click="handleCreateOrUpdate('DRAFT')"
|
||||
></ReactiveButton>
|
||||
<ReactiveButton
|
||||
:errored="form.saveErrored"
|
||||
:erroredText="`${form.model.id ? '保存' : '发布'}失败`"
|
||||
:loadedText="`${form.model.id ? '保存' : '发布'}成功`"
|
||||
:loading="form.saving"
|
||||
:text="`${form.model.id ? '保存' : '发布'}`"
|
||||
@callback="handleSavedCallback"
|
||||
@click="handleCreateOrUpdate()"
|
||||
></ReactiveButton>
|
||||
</template>
|
||||
<AttachmentSelectDrawer
|
||||
v-model="attachmentSelectVisible"
|
||||
:drawerWidth="480"
|
||||
@listenToSelect="handleSelectPostThumb"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
// components
|
||||
import CategoryTree from './CategoryTree'
|
||||
import TagSelect from './TagSelect'
|
||||
import MetaEditor from '@/components/Post/MetaEditor'
|
||||
|
||||
// libs
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
import { datetimeFormat } from '@/utils/datetime'
|
||||
import pinyin from 'tiny-pinyin'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
// apis
|
||||
import postApi from '@/api/post'
|
||||
import themeApi from '@/api/theme'
|
||||
|
||||
export default {
|
||||
name: 'PostSettingModal',
|
||||
mixins: [mixin, mixinDevice],
|
||||
components: {
|
||||
CategoryTree,
|
||||
TagSelect,
|
||||
MetaEditor
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
post: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
savedCallback: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
model: {},
|
||||
saving: false,
|
||||
saveErrored: false,
|
||||
draftSaving: false,
|
||||
draftSaveErrored: false
|
||||
},
|
||||
|
||||
templates: [],
|
||||
|
||||
attachmentSelectVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['options']),
|
||||
modalVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
modalTitle() {
|
||||
return this.form.model.id ? '文章设置' : '文章发布'
|
||||
},
|
||||
createTimeDefaultValue() {
|
||||
if (this.form.model.createTime) {
|
||||
const date = new Date(this.form.model.createTime)
|
||||
return datetimeFormat(date, 'YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
return datetimeFormat(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
topPriority: {
|
||||
get() {
|
||||
return this.form.model.topPriority !== 0
|
||||
},
|
||||
set(value) {
|
||||
this.form.model.topPriority = value ? 1 : 0
|
||||
}
|
||||
},
|
||||
fullPath() {
|
||||
const permalinkType = this.options.post_permalink_type
|
||||
const blogUrl = this.options.blog_url
|
||||
const archivesPrefix = this.options.archives_prefix
|
||||
const pathSuffix = this.options.path_suffix || ''
|
||||
const slug = this.form.model.slug || '{slug}'
|
||||
const createTime = this.form.model.createTime || new Date()
|
||||
const id = this.form.model.id || '{id}'
|
||||
switch (permalinkType) {
|
||||
case 'DEFAULT':
|
||||
return `${blogUrl}/${archivesPrefix}/${slug}${pathSuffix}`
|
||||
case 'YEAR':
|
||||
return `${blogUrl}${datetimeFormat(createTime, '/YYYY/')}${slug}${pathSuffix}`
|
||||
case 'DATE':
|
||||
return `${blogUrl}${datetimeFormat(createTime, '/YYYY/MM/')}${slug}${pathSuffix}`
|
||||
case 'DAY':
|
||||
return `${blogUrl}${datetimeFormat(createTime, '/YYYY/MM/DD/')}${slug}${pathSuffix}`
|
||||
case 'ID':
|
||||
return `${blogUrl}/?p=${id}`
|
||||
case 'ID_SLUG':
|
||||
return `${blogUrl}/${archivesPrefix}/${id}${pathSuffix}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modalVisible(value) {
|
||||
if (value) {
|
||||
this.form.model = Object.assign({}, this.post)
|
||||
|
||||
if (!this.form.model.slug && !this.form.model.id) {
|
||||
this.handleGenerateSlug()
|
||||
}
|
||||
}
|
||||
},
|
||||
post: {
|
||||
deep: true,
|
||||
handler(value) {
|
||||
this.form.model = Object.assign({}, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleListCustomTemplates()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Creates or updates a post
|
||||
*/
|
||||
async handleCreateOrUpdate(preStatus = 'PUBLISHED') {
|
||||
if (!this.form.model.title) {
|
||||
this.$notification['error']({
|
||||
message: '提示',
|
||||
description: '文章标题不能为空!'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.form.model.status = preStatus
|
||||
const { id, status } = this.form.model
|
||||
try {
|
||||
this.form[status === 'PUBLISHED' ? 'saving' : 'draftSaving'] = true
|
||||
|
||||
if (id) {
|
||||
await postApi.update(id, this.form.model, false)
|
||||
} else {
|
||||
await postApi.create(this.form.model, false)
|
||||
}
|
||||
} catch (error) {
|
||||
this.form[status === 'PUBLISHED' ? 'saveErrored' : 'draftSaveErrored'] = true
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.form.saving = false
|
||||
this.form.draftSaving = false
|
||||
}, 400)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle saved callback event
|
||||
*/
|
||||
handleSavedCallback() {
|
||||
if (this.form.saveErrored || this.form.draftSaveErrored) {
|
||||
this.form.saveErrored = false
|
||||
this.form.draftSaveErrored = false
|
||||
} else {
|
||||
this.savedCallback && this.savedCallback()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle list custom templates
|
||||
*/
|
||||
async handleListCustomTemplates() {
|
||||
try {
|
||||
const response = await themeApi.customPostTpls()
|
||||
|
||||
this.templates = response.data.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle create time selected event
|
||||
*/
|
||||
onCreateTimeSelect(value) {
|
||||
this.form.model.createTime = value.valueOf()
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate slug
|
||||
*/
|
||||
handleGenerateSlug() {
|
||||
if (this.form.model.title) {
|
||||
if (pinyin.isSupported()) {
|
||||
let result = ''
|
||||
const tokens = pinyin.parse(this.form.model.title.replace(/\s+/g, '').toLowerCase())
|
||||
let lastToken
|
||||
tokens.forEach(token => {
|
||||
if (token.type === 2) {
|
||||
const target = token.target ? token.target.toLowerCase() : ''
|
||||
result += result && !/\n|\s/.test(lastToken.target) ? '-' + target : target
|
||||
} else {
|
||||
result += (lastToken && lastToken.type === 2 ? '-' : '') + token.target
|
||||
}
|
||||
lastToken = token
|
||||
})
|
||||
this.$set(this.form.model, 'slug', result)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select post thumbnail
|
||||
* @param data
|
||||
*/
|
||||
handleSelectPostThumb(data) {
|
||||
this.form.model.thumbnail = encodeURI(data.path)
|
||||
this.attachmentSelectVisible = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle modal close event
|
||||
*/
|
||||
onClosed() {
|
||||
this.$emit('onClose')
|
||||
this.$emit('onUpdate', this.form.model)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,16 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-select
|
||||
v-model="selectedTagNames"
|
||||
class="w-full"
|
||||
allowClear
|
||||
mode="tags"
|
||||
placeholder="选择或输入标签"
|
||||
@change="handleChange"
|
||||
>
|
||||
<a-select-option v-for="tag in tags" :key="tag.id" :value="tag.name">{{ tag.name }}</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<a-select
|
||||
v-model="selectedTagNames"
|
||||
:token-separators="[',', '|']"
|
||||
allowClear
|
||||
class="w-full"
|
||||
mode="tags"
|
||||
placeholder="选择或输入标签"
|
||||
@change="handleChange"
|
||||
>
|
||||
<a-select-option v-for="tag in tags" :key="tag.id" :value="tag.name">{{ tag.name }}</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -44,6 +43,15 @@ export default {
|
|||
if (newValue) {
|
||||
this.selectedTagNames = this.tagIds.map(tagId => this.tagIdMap[tagId].name)
|
||||
}
|
||||
},
|
||||
tagIds: {
|
||||
handler(newValue) {
|
||||
if (!this.tags.length) {
|
||||
return
|
||||
}
|
||||
this.selectedTagNames = newValue.map(tagId => this.tagIdMap[tagId].name)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -72,14 +80,12 @@ export default {
|
|||
})
|
||||
},
|
||||
handleChange() {
|
||||
this.$log.debug('Changed')
|
||||
const tagNamesToCreate = this.selectedTagNames.filter(tagName => !this.tagNameMap[tagName])
|
||||
|
||||
this.$log.debug('Tag names to create', tagNamesToCreate)
|
||||
|
||||
if (tagNamesToCreate === []) {
|
||||
if (!tagNamesToCreate.length) {
|
||||
const tagIds = this.selectedTagNames.map(tagName => this.tagNameMap[tagName].id)
|
||||
// If empty
|
||||
this.$emit('change', tagIds)
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue