mirror of https://github.com/halo-dev/halo-admin
Browse Source
* 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
Ryan Wang
3 years ago
committed by
GitHub
8 changed files with 875 additions and 855 deletions
@ -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> |
@ -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> |
Loading…
Reference in new issue