release 1.1.0

Co-authored-by: guqing <1484563614@qq.com>
pull/3445/head
Ryan Wang 2019-09-11 21:44:36 +08:00 committed by GitHub
commit 3ed70b20c5
75 changed files with 4603 additions and 2787 deletions

1260
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "halo-admin", "name": "halo-admin",
"version": "1.0.3", "version": "1.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -10,21 +10,23 @@
}, },
"dependencies": { "dependencies": {
"animate.css": "^3.7.0", "animate.css": "^3.7.0",
"ant-design-vue": "~1.3.9", "ant-design-vue": "^1.3.16",
"axios": "^0.18.0", "axios": "^0.18.0",
"enquire.js": "^2.1.6", "enquire.js": "^2.1.6",
"marked": "^0.6.2", "filepond": "^4.6.1",
"mavon-editor": "^2.7.4", "filepond-plugin-image-preview": "^4.4.0",
"halo-editor": "^2.7.6",
"marked": "^0.6.3",
"moment": "^2.24.0", "moment": "^2.24.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"viser-vue": "^2.4.6", "verte": "^0.0.12",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-clipboard2": "^0.3.0", "vue-clipboard2": "^0.3.0",
"vue-codemirror-lite": "^1.0.4", "vue-codemirror-lite": "^1.0.4",
"vue-count-to": "^1.0.13", "vue-count-to": "^1.0.13",
"vue-cropper": "0.4.9", "vue-filepond": "^5.1.3",
"vue-ls": "^3.2.1", "vue-ls": "^3.2.1",
"vue-router": "^3.0.6", "vue-router": "^3.1.2",
"vue-video-player": "^5.0.2", "vue-video-player": "^5.0.2",
"vuejs-logger": "^1.5.3", "vuejs-logger": "^1.5.3",
"vuex": "^3.1.1" "vuex": "^3.1.1"
@ -38,14 +40,14 @@
"@vue/eslint-config-standard": "^4.0.0", "@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.20", "@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.2",
"babel-jest": "^24.8.0", "babel-jest": "^24.9.0",
"babel-plugin-import": "^1.11.2", "babel-plugin-import": "^1.11.2",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"eslint-plugin-html": "^5.0.5", "eslint-plugin-html": "^5.0.5",
"eslint-plugin-vue": "^5.2.2", "eslint-plugin-vue": "^5.2.3",
"generate-asset-webpack-plugin": "^0.3.0", "generate-asset-webpack-plugin": "^0.3.0",
"less": "^3.9.0", "less": "^3.10.0",
"less-loader": "^5.0.0", "less-loader": "^5.0.0",
"vue-svg-icon-loader": "^2.1.1", "vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-cmn-Hans"> <html lang="zh-cmn-Hans">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
@ -8,16 +9,20 @@
<meta name="generator" content="Halo" /> <meta name="generator" content="Halo" />
<link rel="icon" href="<%= BASE_URL %>logo.png" /> <link rel="icon" href="<%= BASE_URL %>logo.png" />
<title>Halo Dashboard</title> <title>Halo Dashboard</title>
<style>
#loader{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto;border:solid 3px #e5e5e5;border-top-color:#333;border-radius:50%;width:30px;height:30px;animation:spin .6s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong <strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to
>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
continue.</strong
>
</noscript> </noscript>
<div id="app"></div> <div id="app">
<div id="loader"></div>
</div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View File

@ -12,6 +12,13 @@ adminApi.counts = () => {
}) })
} }
adminApi.isInstalled = () => {
return service({
url: `${baseUrl}/is_installed`,
method: 'get'
})
}
adminApi.environments = () => { adminApi.environments = () => {
return service({ return service({
url: `${baseUrl}/environments`, url: `${baseUrl}/environments`,
@ -52,6 +59,22 @@ adminApi.refreshToken = refreshToken => {
}) })
} }
adminApi.sendResetCode = param => {
return service({
url: `${baseUrl}/password/code`,
data: param,
method: 'post'
})
}
adminApi.resetPassword = param => {
return service({
url: `${baseUrl}/password/reset`,
data: param,
method: 'put'
})
}
adminApi.updateAdminAssets = () => { adminApi.updateAdminAssets = () => {
return service({ return service({
url: `${baseUrl}/halo-admin`, url: `${baseUrl}/halo-admin`,

View File

@ -6,7 +6,7 @@ const backupApi = {}
backupApi.importMarkdown = (formData, uploadProgress, cancelToken) => { backupApi.importMarkdown = (formData, uploadProgress, cancelToken) => {
return service({ return service({
url: `${baseUrl}/import/markdowns`, url: `${baseUrl}/import/markdown`,
timeout: 8640000, // 24 hours timeout: 8640000, // 24 hours
data: formData, // form data data: formData, // form data
onUploadProgress: uploadProgress, onUploadProgress: uploadProgress,

View File

@ -45,6 +45,14 @@ commentApi.create = (target, comment) => {
}) })
} }
commentApi.update = (target, commentId, comment) => {
return service({
url: `${baseUrl}/${target}/comments/${commentId}`,
data: comment,
method: 'put'
})
}
/** /**
* Creates a comment. * Creates a comment.
* @param {String} target * @param {String} target

View File

@ -42,4 +42,13 @@ journalApi.commentTree = journalId => {
}) })
} }
journalApi.journalType = {
PUBLIC: {
text: '公开'
},
INTIMATE: {
text: '私密'
}
}
export default journalApi export default journalApi

View File

@ -35,4 +35,11 @@ photoApi.delete = photoId => {
}) })
} }
photoApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
export default photoApi export default photoApi

View File

@ -66,6 +66,13 @@ postApi.delete = postId => {
}) })
} }
postApi.preview = postId => {
return service({
url: `${baseUrl}/preview/${postId}`,
method: 'get'
})
}
postApi.postStatus = { postApi.postStatus = {
PUBLISHED: { PUBLISHED: {
color: 'green', color: 'green',
@ -81,6 +88,11 @@ postApi.postStatus = {
color: 'red', color: 'red',
status: 'error', status: 'error',
text: '回收站' text: '回收站'
},
INTIMATE: {
color: 'blue',
status: 'success',
text: '私密'
} }
} }
export default postApi export default postApi

View File

@ -61,6 +61,13 @@ sheetApi.delete = sheetId => {
}) })
} }
sheetApi.preview = sheetId => {
return service({
url: `${baseUrl}/preview/${sheetId}`,
method: 'get'
})
}
sheetApi.sheetStatus = { sheetApi.sheetStatus = {
PUBLISHED: { PUBLISHED: {
color: 'green', color: 'green',

View File

@ -11,9 +11,16 @@ themeApi.listAll = () => {
}) })
} }
themeApi.listFiles = () => { themeApi.listFilesActivated = () => {
return service({ return service({
url: `${baseUrl}/files`, url: `${baseUrl}/activation/files`,
method: 'get'
})
}
themeApi.listFiles = themeId => {
return service({
url: `${baseUrl}/${themeId}/files`,
method: 'get' method: 'get'
}) })
} }
@ -41,7 +48,7 @@ themeApi.getActivatedTheme = () => {
themeApi.update = themeId => { themeApi.update = themeId => {
return service({ return service({
url: `${baseUrl}/${themeId}`, url: `${baseUrl}/fetching/${themeId}`,
timeout: 60000, timeout: 60000,
method: 'put' method: 'put'
}) })
@ -94,6 +101,17 @@ themeApi.upload = (formData, uploadProgress, cancelToken) => {
}) })
} }
themeApi.updateByUpload = (formData, uploadProgress, cancelToken, themeId) => {
return service({
url: `${baseUrl}/upload/${themeId}`,
timeout: 86400000, // 24 hours
data: formData, // form data
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'put'
})
}
themeApi.fetching = url => { themeApi.fetching = url => {
return service({ return service({
url: `${baseUrl}/fetching`, url: `${baseUrl}/fetching`,
@ -115,13 +133,34 @@ themeApi.getContent = path => {
}) })
} }
themeApi.saveContent = (path, content) => { themeApi.getContent = (themeId, path) => {
return service({ return service({
url: `${baseUrl}/files/content`, url: `${baseUrl}/${themeId}/files/content`,
params: { params: {
path: path path: path
}, },
data: content, method: 'get'
})
}
themeApi.saveContent = (path, content) => {
return service({
url: `${baseUrl}/files/content`,
data: {
path: path,
content: content
},
method: 'put'
})
}
themeApi.saveContent = (themeId, path, content) => {
return service({
url: `${baseUrl}/${themeId}/files/content`,
data: {
path: path,
content: content
},
method: 'put' method: 'put'
}) })
} }

View File

@ -1,6 +1,12 @@
<template> <template>
<div class="footer"> <div
<div class="copyright"> class="footer"
style="padding: 0 16px;margin: 48px 0 0;text-align: center;"
>
<div
class="copyright"
style="color: rgba(0, 0, 0, 0.45);font-size: 14px;"
>
Proudly power by Proudly power by
<router-link :to="{ name:'About' }"> <router-link :to="{ name:'About' }">
<a href="javascript:void(0);">Halo</a> <a href="javascript:void(0);">Halo</a>
@ -19,13 +25,4 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.footer {
padding: 0 16px;
margin: 48px 0 0;
text-align: center;
.copyright {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
</style> </style>

View File

@ -105,7 +105,7 @@ export default {
} }
}, },
mounted() { mounted() {
document.body.addEventListener('scroll', this.handleScroll, { passive: true }) document.addEventListener('scroll', this.handleScroll, { passive: true })
}, },
methods: { methods: {
handleScroll() { handleScroll() {

View File

@ -8,7 +8,6 @@
closable closable
@close="onClose" @close="onClose"
:visible="visible" :visible="visible"
:zIndex="9999"
> >
<div class="setting-drawer-index-content"> <div class="setting-drawer-index-content">
<div :style="{ marginBottom: '24px' }"> <div :style="{ marginBottom: '24px' }">
@ -114,6 +113,77 @@
</div> </div>
</div> </div>
<a-divider /> <a-divider />
<div :style="{ marginTop: '24px' }">
<a-list :split="false">
<a-list-item>
<a-tooltip slot="actions">
<template slot="title">
该设定仅 [顶部栏导航] 时有效
</template>
<a-select
size="small"
style="width: 80px;"
:defaultValue="contentWidth"
@change="handleContentWidthChange"
>
<a-select-option value="Fixed">固定</a-select-option>
<a-select-option
value="Fluid"
v-if="layoutMode != 'sidemenu'"
>流式</a-select-option>
</a-select>
</a-tooltip>
<a-list-item-meta>
<div slot="title">内容区域宽度</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch
slot="actions"
size="small"
:defaultChecked="fixedHeader"
@change="handleFixedHeader"
/>
<a-list-item-meta>
<div slot="title">固定 Header</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch
slot="actions"
size="small"
:disabled="!fixedHeader"
:defaultChecked="autoHideHeader"
@change="handleFixedHeaderHidden"
/>
<a-list-item-meta>
<a-tooltip
slot="title"
placement="left"
>
<template slot="title">固定 Header 时可配置</template>
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
</a-tooltip>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch
slot="actions"
size="small"
:disabled="(layoutMode === 'topmenu')"
:defaultChecked="fixSiderbar"
@change="handleFixSiderbar"
/>
<a-list-item-meta>
<div
slot="title"
:style="{ opacity: (layoutMode==='topmenu') ? '0.5' : '1' }"
>固定侧边菜单</div>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
<a-divider />
</div> </div>
</a-drawer> </a-drawer>
</div> </div>
@ -122,7 +192,7 @@
<script> <script>
import SettingItem from '@/components/SettingDrawer/SettingItem' import SettingItem from '@/components/SettingDrawer/SettingItem'
import config from '@/config/defaultSettings' import config from '@/config/defaultSettings'
import { updateTheme, colorList } from '@/components/Tools/setting' import { updateTheme, colorList } from './setting'
import { mixin, mixinDevice } from '@/utils/mixin' import { mixin, mixinDevice } from '@/utils/mixin'
export default { export default {
@ -161,9 +231,10 @@ export default {
handleLayout(mode) { handleLayout(mode) {
this.baseConfig.layout = mode this.baseConfig.layout = mode
this.$store.dispatch('ToggleLayoutMode', mode) this.$store.dispatch('ToggleLayoutMode', mode)
//
//
this.handleFixSiderbar(false) this.handleFixSiderbar(false)
if (mode === 'sidemenu') {
this.handleContentWidthChange('Fixed')
}
}, },
handleContentWidthChange(type) { handleContentWidthChange(type) {
this.baseConfig.contentWidth = type this.baseConfig.contentWidth = type

View File

@ -1,90 +0,0 @@
import { message } from 'ant-design-vue/es'
// import defaultSettings from '../defaultSettings';
let lessNodesAppended
const colorList = [
{
key: '薄暮', color: '#F5222D'
},
{
key: '火山', color: '#FA541C'
},
{
key: '日暮', color: '#FAAD14'
},
{
key: '明青', color: '#13C2C2'
},
{
key: '极光绿', color: '#52C41A'
},
{
key: '拂晓蓝(默认)', color: '#1890FF'
},
{
key: '极客蓝', color: '#2F54EB'
},
{
key: '酱紫', color: '#722ED1'
}
]
const updateTheme = primaryColor => {
// Don't compile less in production!
/* if (process.env.NODE_ENV === 'production') {
return;
} */
// Determine if the component is remounted
if (!primaryColor) {
return
}
const hideMessage = message.loading('正在编译主题!', 0)
function buildIt() {
if (!window.less) {
return
}
setTimeout(() => {
window.less
.modifyVars({
'@primary-color': primaryColor
})
.then(() => {
hideMessage()
})
.catch(() => {
message.error('Failed to update theme')
hideMessage()
})
}, 200)
}
if (!lessNodesAppended) {
// insert less.js and color.less
const lessStyleNode = document.createElement('link')
const lessConfigNode = document.createElement('script')
const lessScriptNode = document.createElement('script')
lessStyleNode.setAttribute('rel', 'stylesheet/less')
lessStyleNode.setAttribute('href', '/color.less')
lessConfigNode.innerHTML = `
window.less = {
async: true,
env: 'production',
javascriptEnabled: true
};
`
lessScriptNode.src = 'https://cdnjs.loli.net/ajax/libs/less.js/3.8.1/less.min.js'
lessScriptNode.async = true
lessScriptNode.onload = () => {
buildIt()
lessScriptNode.onload = null
}
document.body.appendChild(lessStyleNode)
document.body.appendChild(lessConfigNode)
document.body.appendChild(lessScriptNode)
lessNodesAppended = true
} else {
buildIt()
}
}
export { updateTheme, colorList }

View File

@ -30,6 +30,7 @@
<a-avatar <a-avatar
class="avatar" class="avatar"
size="small" size="small"
style="margin-right: 0.3rem;"
:src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'" :src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'"
/> />
</span> </span>
@ -63,7 +64,6 @@
import HeaderComment from './HeaderComment' import HeaderComment from './HeaderComment'
import SettingDrawer from '@/components/SettingDrawer/SettingDrawer' import SettingDrawer from '@/components/SettingDrawer/SettingDrawer'
import { mapActions, mapGetters } from 'vuex' import { mapActions, mapGetters } from 'vuex'
import optionApi from '@/api/option'
export default { export default {
name: 'UserMenu', name: 'UserMenu',
@ -73,19 +73,14 @@ export default {
}, },
data() { data() {
return { return {
optionVisible: true, optionVisible: true
options: [],
keys: ['blog_url']
} }
}, },
mounted() { mounted() {
this.optionVisible = this.$refs.drawer.visible this.optionVisible = this.$refs.drawer.visible
}, },
created() {
this.loadOptions()
},
computed: { computed: {
...mapGetters(['user']) ...mapGetters(['user', 'options'])
}, },
methods: { methods: {
...mapActions(['logout']), ...mapActions(['logout']),
@ -114,18 +109,7 @@ export default {
showOptionModal() { showOptionModal() {
this.optionVisible = this.$refs.drawer.visible this.optionVisible = this.$refs.drawer.visible
this.$refs.drawer.toggle() this.$refs.drawer.toggle()
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
} }
} }
} }
</script> </script>
<style lang="less" scoped>
.avatar {
margin-right: 0.3rem;
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div>
<file-pond
ref="pond"
:label-idle="label"
:name="name"
:allow-multiple="multiple"
:allowRevert="false"
:accepted-file-types="accept"
:maxParallelUploads="options.attachment_upload_max_parallel_uploads"
:allowImagePreview="options.attachment_upload_image_preview_enable"
:maxFiles="options.attachment_upload_max_files"
labelFileProcessing="上传中"
labelFileProcessingComplete="上传完成"
labelFileProcessingAborted="取消上传"
labelFileProcessingError="上传错误"
labelTapToCancel="点击取消"
labelTapToRetry="点击重试"
:files="fileList"
:server="server"
@init="handleFilePondInit"
>
</file-pond>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import axios from 'axios'
import vueFilePond from 'vue-filepond'
import 'filepond/dist/filepond.min.css'
// Plugins
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css'
// Create component and regist plugins
const FilePond = vueFilePond(FilePondPluginImagePreview)
export default {
name: 'FilePondUpload',
components: {
FilePond
},
props: {
name: {
type: String,
required: false,
default: 'file'
},
filed: {
type: String,
required: false,
default: ''
},
multiple: {
type: Boolean,
required: false,
default: true
},
accept: {
type: String,
required: false,
default: ''
},
label: {
type: String,
required: false,
default: '点击选择文件或将文件拖拽到此处'
},
uploadHandler: {
type: Function,
required: true
}
},
data: function() {
return {
server: {
process: (fieldName, file, metadata, load, error, progress, abort) => {
const formData = new FormData()
formData.append(fieldName, file, file.name)
const CancelToken = axios.CancelToken
const source = CancelToken.source()
this.uploadHandler(
formData,
progressEvent => {
if (progressEvent.total > 0) {
progress(progressEvent.lengthComputable, progressEvent.loaded, progressEvent.total)
}
},
source.token,
this.filed,
file
)
.then(response => {
load(response)
this.$log.debug('Uploaded successfully', response)
this.$emit('success', response, file)
})
.catch(failure => {
this.$log.debug('Failed to upload file', failure)
this.$emit('failure', failure, file)
error()
})
return {
abort: () => {
abort()
this.$log.debug('Upload operation aborted by the user')
source.cancel('Upload operation canceled by the user.')
}
}
}
},
fileList: []
}
},
computed: {
...mapGetters(['options'])
},
methods: {
handleFilePondInit() {
console.log('FilePond has initialized')
},
handleClearFileList() {
this.$refs.pond.removeFiles()
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -122,7 +122,6 @@ export default {
} }
</script> </script>
<style> <style>
/* you can make up upload button and sample style by using stylesheets */
.ant-upload-select-picture-card i { .ant-upload-select-picture-card i {
font-size: 32px; font-size: 32px;
color: #999; color: #999;

View File

@ -6,10 +6,12 @@
height: 6px; height: 6px;
background-color: #eee; background-color: #eee;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: #1890ff; background-color: #1890ff;
cursor: pointer; cursor: pointer;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background-color: #eee; background-color: #eee;
cursor: pointer; cursor: pointer;
@ -539,18 +541,6 @@ body {
} }
} }
.ant-card-wider-padding {
.ant-card-body {
padding: 16px !important;
}
}
.comment-tab-wrapper{
.ant-card-body {
padding: 0 !important;
}
}
.ant-form { .ant-form {
.ant-form-item { .ant-form-item {
padding-bottom: 0 !important; padding-bottom: 0 !important;
@ -699,17 +689,103 @@ body {
} }
} }
.post-thum { .post-thumb,
.sheet-thumb {
.img { .img {
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
} }
.post-thum-remove {
margin-top: 16px;
} }
.post-thumb-remove,
.sheet-thumb-remove {
margin-top: 16px;
} }
.ant-calendar-picker { .ant-calendar-picker {
width: 100% !important; width: 100% !important;
} }
#editor {
.v-note-wrapper {
min-height: 580px;
}
}
.attach-item {
width: 50%;
padding-bottom: 28%;
float: left;
}
.attach-thumb {
width: 100%;
padding-bottom: 56%;
}
.attach-item,
.attach-thumb {
margin: 0 auto;
position: relative;
overflow: hidden;
cursor: pointer;
img,
span {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
span {
display: flex;
font-size: 12px;
align-items: center;
justify-content: center;
color: #9b9ea0;
}
}
.analysis-card-container {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
line-height: 22px;
.analysis-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
}
.number {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 32px;
line-height: 38px;
height: 38px;
}
}
.ant-tree-child-tree {
li {
overflow: hidden;
}
}

View File

@ -4,11 +4,13 @@ import Vue from 'vue'
import Ellipsis from '@/components/Ellipsis' import Ellipsis from '@/components/Ellipsis'
import FooterToolbar from '@/components/FooterToolbar' import FooterToolbar from '@/components/FooterToolbar'
import Upload from '@/components/Upload/Upload' import Upload from '@/components/Upload/Upload'
import FilePondUpload from '@/components/Upload/FilePondUpload'
const _components = { const _components = {
Ellipsis, Ellipsis,
FooterToolbar, FooterToolbar,
Upload Upload,
FilePondUpload
} }
const components = {} const components = {}

View File

@ -1,5 +1,5 @@
// eslint-disable-next-line // eslint-disable-next-line
import { BasicLayout, RouteView, BlankLayout, PageView } from '@/layouts' import { BasicLayout, PageView } from '@/layouts'
export const asyncRouterMap = [ export const asyncRouterMap = [
{ {
@ -218,6 +218,12 @@ export const constantRouterMap = [
meta: { title: '安装向导' }, meta: { title: '安装向导' },
component: () => import('@/views/system/Installation') component: () => import('@/views/system/Installation')
}, },
{
path: '/password/reset',
name: 'ResetPassword',
meta: { title: '重置密码' },
component: () => import('@/views/user/ResetPassword')
},
{ {
path: '/404', path: '/404',
name: 'NotFound', name: 'NotFound',

View File

@ -11,7 +11,8 @@ import {
DEFAULT_FIXED_SIDEMENU, DEFAULT_FIXED_SIDEMENU,
DEFAULT_CONTENT_WIDTH_TYPE, DEFAULT_CONTENT_WIDTH_TYPE,
USER, USER,
API_URL API_URL,
OPTIONS
} from '@/store/mutation-types' } from '@/store/mutation-types'
import config from '@/config/defaultSettings' import config from '@/config/defaultSettings'
@ -27,6 +28,6 @@ export default function Initializer() {
store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN)) store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
store.commit('SET_USER', Vue.ls.get(USER)) store.commit('SET_USER', Vue.ls.get(USER))
store.commit('SET_API_URL', Vue.ls.get(API_URL)) store.commit('SET_API_URL', Vue.ls.get(API_URL))
store.commit('SET_OPTIONS', Vue.ls.get(OPTIONS))
// last step // last step
} }

View File

@ -4,20 +4,12 @@ import config from '@/config/defaultSettings'
// base library // base library
import '@/core/lazy_lib/components_use' import '@/core/lazy_lib/components_use'
import Viser from 'viser-vue'
import VueCropper from 'vue-cropper'
import 'ant-design-vue/dist/antd.less' import 'ant-design-vue/dist/antd.less'
import bootstrap from './bootstrap' import bootstrap from './bootstrap'
// ext library
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
VueClipboard.config.autoSetContainer = true
Vue.use(Viser)
Vue.use(VueStorage, config.storageOptions) Vue.use(VueStorage, config.storageOptions)
Vue.use(VueClipboard) Vue.use(VueClipboard)
Vue.use(VueCropper)
bootstrap() bootstrap()

View File

@ -4,18 +4,11 @@ import config from '@/config/defaultSettings'
// base library // base library
import Antd from 'ant-design-vue' import Antd from 'ant-design-vue'
import Viser from 'viser-vue'
import VueCropper from 'vue-cropper'
import 'ant-design-vue/dist/antd.less' import 'ant-design-vue/dist/antd.less'
// ext library
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
VueClipboard.config.autoSetContainer = true
Vue.use(Antd) Vue.use(Antd)
Vue.use(Viser)
Vue.use(VueStorage, config.storageOptions) Vue.use(VueStorage, config.storageOptions)
Vue.use(VueClipboard) Vue.use(VueClipboard)
Vue.use(VueCropper)

View File

@ -1,14 +1,19 @@
import Vue from 'vue' import Vue from 'vue'
import router from './router' import router from './router'
import store from './store' import store from './store'
import { setDocumentTitle, domTitle } from '@/utils/domUtil' import {
setDocumentTitle,
domTitle
} from '@/utils/domUtil'
import NProgress from 'nprogress' // progress bar import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style import 'nprogress/nprogress.css' // progress bar style
NProgress.configure({ showSpinner: false }) // NProgress Configuration NProgress.configure({
showSpinner: false
}) // NProgress Configuration
const whiteList = ['Login', 'Install', 'NotFound'] // no redirect whitelist const whiteList = ['Login', 'Install', 'NotFound', 'ResetPassword'] // no redirect whitelist
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start() NProgress.start()
@ -16,12 +21,18 @@ router.beforeEach((to, from, next) => {
Vue.$log.debug('Token', store.getters.token) Vue.$log.debug('Token', store.getters.token)
if (store.getters.token) { if (store.getters.token) {
if (to.name === 'Login') { if (to.name === 'Login') {
next({ name: 'Dashboard' }) next({
name: 'Dashboard'
})
NProgress.done() NProgress.done()
return return
} }
// TODO Get installation status // TODO Get installation status
if (!store.getters.options) {
store.dispatch('loadOptions').then()
}
next() next()
NProgress.done() NProgress.done()
return return
@ -35,6 +46,11 @@ router.beforeEach((to, from, next) => {
return return
} }
next({ name: 'Login', query: { redirect: to.fullPath } }) next({
name: 'Login',
query: {
redirect: to.fullPath
}
})
NProgress.done() NProgress.done()
}) })

View File

@ -4,16 +4,14 @@ const getters = {
color: state => state.app.color, color: state => state.app.color,
token: state => state.user.token, token: state => state.user.token,
user: state => state.user.user, user: state => state.user.user,
avatar: state => state.user.avatar,
nickname: state => state.user.name,
roles: state => state.user.roles,
addRouters: state => state.permission.addRouters, addRouters: state => state.permission.addRouters,
apiUrl: state => { apiUrl: state => {
if (state.app.apiUrl) { if (state.app.apiUrl) {
return state.app.apiUrl return state.app.apiUrl
} }
return `${window.location.protocol}//${window.location.host}` return `${window.location.protocol}//${window.location.host}`
} },
options: state => state.option.options
} }
export default getters export default getters

View File

@ -4,6 +4,7 @@ import Vuex from 'vuex'
import app from './modules/app' import app from './modules/app'
import user from './modules/user' import user from './modules/user'
import permission from './modules/permission' import permission from './modules/permission'
import option from './modules/option'
import getters from './getters' import getters from './getters'
Vue.use(Vuex) Vue.use(Vuex)
@ -12,7 +13,8 @@ export default new Vuex.Store({
modules: { modules: {
app, app,
user, user,
permission permission,
option
}, },
state: { state: {

View File

@ -0,0 +1,41 @@
import Vue from 'vue'
import {
OPTIONS
} from '@/store/mutation-types'
import optionApi from '@/api/option'
const keys = [
'blog_url',
'attachment_upload_image_preview_enable',
'attachment_upload_max_parallel_uploads',
'attachment_upload_max_files'
]
const option = {
state: {
options: []
},
mutations: {
SET_OPTIONS: (state, options) => {
Vue.ls.set(OPTIONS, options)
state.options = options
}
},
actions: {
loadOptions({
commit
}) {
return new Promise((resolve, reject) => {
optionApi
.listAll(keys)
.then(response => {
commit('SET_OPTIONS', response.data.data)
resolve(response)
})
.catch(error => {
reject(error)
})
})
}
}
}
export default option

View File

@ -21,22 +21,6 @@ function hasPermission(permission, route) {
return true return true
} }
/**
* 单账户多角色时使用该方法可过滤角色不存在的菜单
*
* @param roles
* @param route
* @returns {*}
*/
// eslint-disable-next-line
function hasRole(roles, route) {
if (route.meta && route.meta.roles) {
return route.meta.roles.includes(roles.id)
} else {
return true
}
}
function filterAsyncRouter(routerMap, roles) { function filterAsyncRouter(routerMap, roles) {
const accessedRouters = routerMap.filter(route => { const accessedRouters = routerMap.filter(route => {
if (hasPermission(roles.permissionList, route)) { if (hasPermission(roles.permissionList, route)) {

View File

@ -1,15 +1,14 @@
import Vue from 'vue' import Vue from 'vue'
import { ACCESS_TOKEN, USER } from '@/store/mutation-types' import {
ACCESS_TOKEN,
USER
} from '@/store/mutation-types'
import adminApi from '@/api/admin' import adminApi from '@/api/admin'
import userApi from '@/api/user' import userApi from '@/api/user'
const user = { const user = {
state: { state: {
token: null, token: null,
name: '',
avatar: '',
roles: [],
info: {},
user: {} user: {}
}, },
mutations: { mutations: {
@ -17,18 +16,6 @@ const user = {
Vue.ls.set(ACCESS_TOKEN, token) Vue.ls.set(ACCESS_TOKEN, token)
state.token = token state.token = token
}, },
SET_NAME: (state, { name }) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_INFO: (state, info) => {
state.info = info
},
CLEAR_TOKEN: state => { CLEAR_TOKEN: state => {
Vue.ls.remove(ACCESS_TOKEN) Vue.ls.remove(ACCESS_TOKEN)
state.token = null state.token = null
@ -39,7 +26,9 @@ const user = {
} }
}, },
actions: { actions: {
loadUser({ commit }) { loadUser({
commit
}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
userApi userApi
.getProfile() .getProfile()
@ -52,7 +41,12 @@ const user = {
}) })
}) })
}, },
login({ commit }, { username, password }) { login({
commit
}, {
username,
password
}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
adminApi adminApi
.login(username, password) .login(username, password)
@ -68,7 +62,9 @@ const user = {
}) })
}) })
}, },
logout({ commit }) { logout({
commit
}) {
return new Promise(resolve => { return new Promise(resolve => {
commit('CLEAR_TOKEN') commit('CLEAR_TOKEN')
adminApi adminApi
@ -81,7 +77,9 @@ const user = {
}) })
}) })
}, },
refreshToken({ commit }, refreshToken) { refreshToken({
commit
}, refreshToken) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
adminApi adminApi
.refreshToken(refreshToken) .refreshToken(refreshToken)

View File

@ -9,6 +9,7 @@ export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN'
export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE' export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
export const USER = 'USER' export const USER = 'USER'
export const API_URL = 'API_URL' export const API_URL = 'API_URL'
export const OPTIONS = 'OPTIONS'
export const CONTENT_WIDTH_TYPE = { export const CONTENT_WIDTH_TYPE = {
Fluid: 'Fluid', Fluid: 'Fluid',

View File

@ -8,7 +8,7 @@ import router from '@/router'
import { isObject } from './util' import { isObject } from './util'
const service = axios.create({ const service = axios.create({
timeout: 5000, timeout: 8000,
withCredentials: true withCredentials: true
}) })
@ -147,7 +147,7 @@ service.interceptors.response.use(
message.error(data.message) message.error(data.message)
} }
} else { } else {
message.error('服务异常') message.error('网络异常')
} }
return Promise.reject(error) return Promise.reject(error)

View File

@ -1,14 +0,0 @@
<template>
<div>
404 page
</div>
</template>
<script>
export default {
name: '404'
}
</script>
<style scoped>
</style>

View File

@ -1,9 +0,0 @@
<template>
<div> </div>
</template>
<script>
</script>
<style scoped>
</style>

View File

@ -8,8 +8,12 @@
<a-col <a-col
:span="24" :span="24"
class="search-box" class="search-box"
style="padding-bottom: 12px;"
>
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
> >
<a-card :bordered="false">
<div class="table-page-search-wrapper"> <div class="table-page-search-wrapper">
<a-form layout="inline"> <a-form layout="inline">
<a-row :gutter="48"> <a-row :gutter="48">
@ -73,7 +77,7 @@
</a-row> </a-row>
</a-form> </a-form>
</div> </div>
<div class="table-operator"> <div class="table-operator" style="margin-bottom: 0;">
<a-button <a-button
type="primary" type="primary"
icon="plus" icon="plus"
@ -84,7 +88,7 @@
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-list <a-list
:grid="{ gutter: 12, xs: 1, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }" :grid="{ gutter: 12, xs: 2, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }"
:dataSource="formattedDatas" :dataSource="formattedDatas"
:loading="listLoading" :loading="listLoading"
> >
@ -100,11 +104,14 @@
> >
<div class="attach-thumb"> <div class="attach-thumb">
<span v-show="!handleJudgeMediaType(item)"></span> <span v-show="!handleJudgeMediaType(item)"></span>
<img :src="item.thumbPath" v-show="handleJudgeMediaType(item)"> <img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
</div> </div>
<a-card-meta> <a-card-meta style="padding: 0.8rem;">
<ellipsis <ellipsis
:length="isMobile()?36:16" :length="isMobile()?12:16"
tooltip tooltip
slot="description" slot="description"
>{{ item.name }}</ellipsis> >{{ item.name }}</ellipsis>
@ -130,21 +137,15 @@
v-model="uploadVisible" v-model="uploadVisible"
:footer="null" :footer="null"
:afterClose="onUploadClose" :afterClose="onUploadClose"
destroyOnClose
> >
<upload <FilePondUpload
name="file" ref="upload"
multiple
:uploadHandler="uploadHandler" :uploadHandler="uploadHandler"
> ></FilePondUpload>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择文件或将文件拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传</p>
</upload>
</a-modal> </a-modal>
<AttachmentDetailDrawer <AttachmentDetailDrawer
v-model="drawerVisiable" v-model="drawerVisible"
v-if="selectAttachment" v-if="selectAttachment"
:attachment="selectAttachment" :attachment="selectAttachment"
:addToPhoto="true" :addToPhoto="true"
@ -154,8 +155,8 @@
</template> </template>
<script> <script>
import { PageView } from '@/layouts'
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import { PageView } from '@/layouts'
import AttachmentDetailDrawer from './components/AttachmentDetailDrawer' import AttachmentDetailDrawer from './components/AttachmentDetailDrawer'
import attachmentApi from '@/api/attachment' import attachmentApi from '@/api/attachment'
@ -187,8 +188,8 @@ export default {
mediaType: null, mediaType: null,
attachmentType: null attachmentType: null
}, },
uploadHandler: attachmentApi.upload, drawerVisible: false,
drawerVisiable: false uploadHandler: attachmentApi.upload
} }
}, },
computed: { computed: {
@ -203,6 +204,17 @@ export default {
this.loadAttachments() this.loadAttachments()
this.loadMediaTypes() this.loadMediaTypes()
}, },
destroyed: function() {
if (this.drawerVisible) {
this.drawerVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.drawerVisible) {
this.drawerVisible = false
}
next()
},
methods: { methods: {
loadAttachments() { loadAttachments() {
this.queryParam.page = this.pagination.page - 1 this.queryParam.page = this.pagination.page - 1
@ -222,7 +234,7 @@ export default {
}, },
handleShowDetailDrawer(attachment) { handleShowDetailDrawer(attachment) {
this.selectAttachment = attachment this.selectAttachment = attachment
this.drawerVisiable = true this.drawerVisible = true
}, },
handlePaginationChange(page, size) { handlePaginationChange(page, size) {
this.$log.debug(`Current: ${page}, PageSize: ${size}`) this.$log.debug(`Current: ${page}, PageSize: ${size}`)
@ -235,12 +247,15 @@ export default {
this.queryParam.mediaType = null this.queryParam.mediaType = null
this.queryParam.attachmentType = null this.queryParam.attachmentType = null
this.loadAttachments() this.loadAttachments()
this.loadMediaTypes()
}, },
handleQuery() { handleQuery() {
this.queryParam.page = 0 this.queryParam.page = 0
this.pagination.page = 1
this.loadAttachments() this.loadAttachments()
}, },
onUploadClose() { onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadAttachments() this.loadAttachments()
this.loadMediaTypes() this.loadMediaTypes()
}, },
@ -264,47 +279,3 @@ export default {
} }
} }
</script> </script>
<style lang="less" scoped>
.ant-divider-horizontal {
margin: 24px 0 12px 0;
}
.search-box {
padding-bottom: 12px;
}
.attach-thumb {
width: 100%;
margin: 0 auto;
position: relative;
padding-bottom: 56%;
overflow: hidden;
img, span{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
span {
display: flex;
font-size: 12px;
align-items: center;
justify-content: center;
color: #9b9ea0;
}
}
.ant-card-meta {
padding: 0.8rem;
}
.attach-detail-img img {
width: 100%;
}
.table-operator {
margin-bottom: 0;
}
</style>

View File

@ -3,7 +3,7 @@
title="附件详情" title="附件详情"
:width="isMobile()?'100%':'460'" :width="isMobile()?'100%':'460'"
closable closable
:visible="visiable" :visible="visible"
destroyOnClose destroyOnClose
@close="onClose" @close="onClose"
> >
@ -19,13 +19,21 @@
> >
<div class="attach-detail-img"> <div class="attach-detail-img">
<div v-show="nonsupportPreviewVisible"></div> <div v-show="nonsupportPreviewVisible"></div>
<img :src="attachment.path" v-show="photoPreviewVisible"> <a :href="attachment.path" target="_blank">
<img
:src="attachment.path"
v-show="photoPreviewVisible"
style="width: 100%;"
>
</a>
<video-player <video-player
class="video-player-box" class="video-player-box"
v-show="videoPreviewVisible" v-show="videoPreviewVisible"
ref="videoPlayer" ref="videoPlayer"
:options="playerOptions" :options="playerOptions"
:playsinline="true"> :playsinline="true"
style="width: 100%;"
>
</video-player> </video-player>
</div> </div>
</a-skeleton> </a-skeleton>
@ -154,10 +162,10 @@
<script> <script>
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import { videoPlayer } from 'vue-video-player'
import 'video.js/dist/video-js.css'
import attachmentApi from '@/api/attachment' import attachmentApi from '@/api/attachment'
import photoApi from '@/api/photo' import photoApi from '@/api/photo'
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
export default { export default {
name: 'AttachmentDetailDrawer', name: 'AttachmentDetailDrawer',
@ -182,10 +190,12 @@ export default {
controls: true, controls: true,
loop: false, loop: false,
playbackRates: [0.7, 1.0, 1.5, 2.0], playbackRates: [0.7, 1.0, 1.5, 2.0],
sources: [{ sources: [
{
type: 'video/mp4', type: 'video/mp4',
src: 'https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm' src: 'https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm'
}], }
],
poster: '/static/images/author.jpg', poster: '/static/images/author.jpg',
width: document.documentElement.clientWidth, width: document.documentElement.clientWidth,
notSupportedMessage: '此视频暂无法播放,请稍后再试' notSupportedMessage: '此视频暂无法播放,请稍后再试'
@ -193,7 +203,7 @@ export default {
} }
}, },
model: { model: {
prop: 'visiable', prop: 'visible',
event: 'close' event: 'close'
}, },
props: { props: {
@ -206,7 +216,7 @@ export default {
required: false, required: false,
default: false default: false
}, },
visiable: { visible: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true
@ -221,7 +231,7 @@ export default {
} }
}, },
watch: { watch: {
visiable: function(newValue, oldValue) { visible: function(newValue, oldValue) {
this.$log.debug('old value', oldValue) this.$log.debug('old value', oldValue)
this.$log.debug('new value', newValue) this.$log.debug('new value', newValue)
if (newValue) { if (newValue) {
@ -253,6 +263,13 @@ export default {
this.editable = !this.editable this.editable = !this.editable
}, },
doUpdateAttachment() { doUpdateAttachment() {
if (!this.attachment.name) {
this.$notification['error']({
message: '提示',
description: '附件名称不能为空!'
})
return
}
attachmentApi.update(this.attachment.id, this.attachment).then(response => { attachmentApi.update(this.attachment.id, this.attachment).then(response => {
this.$log.debug('Updated attachment', response.data.data) this.$log.debug('Updated attachment', response.data.data)
this.$message.success('附件修改成功!') this.$message.success('附件修改成功!')
@ -339,12 +356,3 @@ export default {
} }
} }
</script> </script>
<style scope>
.attach-detail-img img {
width: 100%;
}
.video-player-box {
width: 100%;
}
</style>

View File

@ -4,7 +4,7 @@
title="附件库" title="附件库"
:width="isMobile()?'100%':'460'" :width="isMobile()?'100%':'460'"
closable closable
:visible="visiable" :visible="visible"
destroyOnClose destroyOnClose
@close="onClose" @close="onClose"
> >
@ -36,7 +36,11 @@
:key="index" :key="index"
@click="handleShowDetailDrawer(item)" @click="handleShowDetailDrawer(item)"
> >
<img :src="item.thumbPath"> <span v-show="!handleJudgeMediaType(item)"></span>
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
</div> </div>
</a-col> </a-col>
</a-skeleton> </a-skeleton>
@ -51,7 +55,7 @@
</div> </div>
<AttachmentDetailDrawer <AttachmentDetailDrawer
v-model="detailVisiable" v-model="detailVisible"
v-if="selectedAttachment" v-if="selectedAttachment"
:attachment="selectedAttachment" :attachment="selectedAttachment"
@delete="handleDelete" @delete="handleDelete"
@ -70,26 +74,20 @@
v-model="uploadVisible" v-model="uploadVisible"
:footer="null" :footer="null"
:afterClose="onUploadClose" :afterClose="onUploadClose"
destroyOnClose
> >
<upload <FilePondUpload
name="file" ref="upload"
multiple :uploadHandler="uploadHandler"
:uploadHandler="attachmentUploadHandler" ></FilePondUpload>
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择文件或将文件拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传</p>
</upload>
</a-modal> </a-modal>
</div> </div>
</template> </template>
<script> <script>
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import attachmentApi from '@/api/attachment'
import AttachmentDetailDrawer from './AttachmentDetailDrawer' import AttachmentDetailDrawer from './AttachmentDetailDrawer'
import attachmentApi from '@/api/attachment'
export default { export default {
name: 'AttachmentDrawer', name: 'AttachmentDrawer',
@ -98,11 +96,11 @@ export default {
AttachmentDetailDrawer AttachmentDetailDrawer
}, },
model: { model: {
prop: 'visiable', prop: 'visible',
event: 'close' event: 'close'
}, },
props: { props: {
visiable: { visible: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
@ -111,7 +109,7 @@ export default {
data() { data() {
return { return {
attachmentType: attachmentApi.type, attachmentType: attachmentApi.type,
detailVisiable: false, detailVisible: false,
attachmentDrawerVisible: false, attachmentDrawerVisible: false,
uploadVisible: false, uploadVisible: false,
skeletonLoading: true, skeletonLoading: true,
@ -128,7 +126,7 @@ export default {
}, },
attachments: [], attachments: [],
selectedAttachment: {}, selectedAttachment: {},
attachmentUploadHandler: attachmentApi.upload uploadHandler: attachmentApi.upload
} }
}, },
computed: { computed: {
@ -144,7 +142,7 @@ export default {
this.loadAttachments() this.loadAttachments()
}, },
watch: { watch: {
visiable: function(newValue, oldValue) { visible: function(newValue, oldValue) {
if (newValue) { if (newValue) {
this.loadSkeleton() this.loadSkeleton()
} }
@ -163,7 +161,7 @@ export default {
handleShowDetailDrawer(attachment) { handleShowDetailDrawer(attachment) {
this.selectedAttachment = attachment this.selectedAttachment = attachment
this.$log.debug('Show detail of', attachment) this.$log.debug('Show detail of', attachment)
this.detailVisiable = true this.detailVisible = true
}, },
loadAttachments(isSearch) { loadAttachments(isSearch) {
this.queryParam.page = this.pagination.page - 1 this.queryParam.page = this.pagination.page - 1
@ -183,34 +181,33 @@ export default {
this.loadAttachments() this.loadAttachments()
}, },
onUploadClose() { onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadSkeleton() this.loadSkeleton()
this.loadAttachments() this.loadAttachments()
}, },
handleDelete() { handleDelete() {
this.loadAttachments() this.loadAttachments()
}, },
handleJudgeMediaType(attachment) {
var mediaType = attachment.mediaType
//
if (mediaType) {
var prefix = mediaType.split('/')[0]
if (prefix === 'image') {
//
return true
} else {
//
return false
}
}
// false
return false
},
onClose() { onClose() {
this.$emit('close', false) this.$emit('close', false)
} }
} }
} }
</script> </script>
<style lang="less" scope>
.attach-item {
width: 50%;
margin: 0 auto;
position: relative;
padding-bottom: 28%;
overflow: hidden;
float: left;
cursor: pointer;
img {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
}
</style>

View File

@ -4,7 +4,7 @@
:title="title" :title="title"
:width="isMobile()?'100%':drawerWidth" :width="isMobile()?'100%':drawerWidth"
closable closable
:visible="visiable" :visible="visible"
destroyOnClose destroyOnClose
@close="onClose" @close="onClose"
> >
@ -34,7 +34,11 @@
:key="index" :key="index"
@click="handleSelectAttachment(item)" @click="handleSelectAttachment(item)"
> >
<img :src="item.thumbPath"> <span v-show="!handleJudgeMediaType(item)"></span>
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
</div> </div>
</a-col> </a-col>
</a-skeleton> </a-skeleton>
@ -67,18 +71,12 @@
v-model="uploadVisible" v-model="uploadVisible"
:footer="null" :footer="null"
:afterClose="onUploadClose" :afterClose="onUploadClose"
destroyOnClose
> >
<upload <FilePondUpload
name="file" ref="upload"
multiple :uploadHandler="uploadHandler"
:uploadHandler="attachmentUploadHandler" ></FilePondUpload>
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择文件或将文件拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传</p>
</upload>
</a-modal> </a-modal>
</div> </div>
</template> </template>
@ -91,11 +89,11 @@ export default {
name: 'AttachmentSelectDrawer', name: 'AttachmentSelectDrawer',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
model: { model: {
prop: 'visiable', prop: 'visible',
event: 'close' event: 'close'
}, },
props: { props: {
visiable: { visible: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
@ -126,7 +124,7 @@ export default {
sort: '' sort: ''
}, },
attachments: [], attachments: [],
attachmentUploadHandler: attachmentApi.upload uploadHandler: attachmentApi.upload
} }
}, },
created() { created() {
@ -134,7 +132,7 @@ export default {
this.loadAttachments() this.loadAttachments()
}, },
watch: { watch: {
visiable: function(newValue, oldValue) { visible: function(newValue, oldValue) {
if (newValue) { if (newValue) {
this.loadSkeleton() this.loadSkeleton()
} }
@ -174,31 +172,30 @@ export default {
this.loadAttachments() this.loadAttachments()
}, },
onUploadClose() { onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadSkeleton() this.loadSkeleton()
this.loadAttachments() this.loadAttachments()
}, },
handleJudgeMediaType(attachment) {
var mediaType = attachment.mediaType
//
if (mediaType) {
var prefix = mediaType.split('/')[0]
if (prefix === 'image') {
//
return true
} else {
//
return false
}
}
// false
return false
},
onClose() { onClose() {
this.$emit('close', false) this.$emit('close', false)
} }
} }
} }
</script> </script>
<style lang="less" scope>
.attach-item {
width: 50%;
margin: 0 auto;
position: relative;
padding-bottom: 28%;
overflow: hidden;
float: left;
cursor: pointer;
img {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
}
</style>

View File

@ -26,10 +26,6 @@ export default {
components: { components: {
PageView, PageView,
CommentTab CommentTab
}, }
data() {
return {}
},
methods: {}
} }
</script> </script>

View File

@ -0,0 +1,203 @@
<template>
<a-drawer
title="评论详情"
:width="isMobile()?'100%':'460'"
closable
:visible="visible"
destroyOnClose
@close="onClose"
>
<a-row
type="flex"
align="middle"
>
<a-col :span="24">
<a-skeleton
active
:loading="detailLoading"
:paragraph="{rows: 8}"
>
<a-list itemLayout="horizontal">
<a-list-item>
<a-list-item-meta :description="comment.author">
<span slot="title">评论者昵称</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta :description="comment.email">
<span slot="title">评论者邮箱</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta :description="comment.ipAddress">
<span slot="title">评论者 IP</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<a
slot="description"
target="_blank"
:href="comment.authorUrl"
>{{ comment.authorUrl }}</a>
<span slot="title">评论者网址</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<span slot="description">
<a-badge :status="comment.statusProperty.status" :text="comment.statusProperty.text"/>
</span>
<span slot="title">评论状态</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<a
slot="description"
target="_blank"
:href="options.blog_url+'/archives/'+comment.post.url"
v-if="this.type=='posts'"
>{{ comment.post.title }}</a>
<a
slot="description"
target="_blank"
:href="options.blog_url+'/s/'+comment.sheet.url"
v-else-if="this.type=='sheets'"
>{{ comment.sheet.title }}</a>
<span
slot="title"
v-if="this.type=='posts'"
>评论文章</span>
<span
slot="title"
v-else-if="this.type=='sheets'"
>评论页面</span>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta>
<template
slot="description"
v-if="editable"
>
<a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="comment.content"
/>
</template>
<span
slot="description"
v-html="comment.content"
v-else
></span>
<span slot="title">评论内容</span>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-skeleton>
</a-col>
</a-row>
<a-divider class="divider-transparent" />
<div class="bottom-control">
<a-button
type="dashed"
style="marginRight: 8px"
@click="handleEditComment"
v-if="!editable"
>编辑</a-button>
<a-button
type="primary"
style="marginRight: 8px"
@click="handleUpdateComment"
v-if="editable"
>保存</a-button>
<a-popconfirm
title="你确定要将此评论者加入黑名单?"
okText="确定"
cancelText="取消"
>
<a-button type="danger">加入黑名单</a-button>
</a-popconfirm>
</div>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import commentApi from '@/api/comment'
export default {
name: 'CommentDetail',
mixins: [mixin, mixinDevice],
components: {},
data() {
return {
detailLoading: true,
editable: false,
commentStatus: commentApi.commentStatus,
options: [],
keys: ['blog_url']
}
},
model: {
prop: 'visible',
event: 'close'
},
props: {
comment: {
type: Object,
required: true
},
visible: {
type: Boolean,
required: false,
default: true
},
type: {
type: String,
required: false,
default: 'posts',
validator: function(value) {
return ['posts', 'sheets', 'journals'].indexOf(value) !== -1
}
}
},
created() {
this.loadSkeleton()
},
computed: {
...mapGetters(['options'])
},
watch: {
visible: function(newValue, oldValue) {
this.$log.debug('old value', oldValue)
this.$log.debug('new value', newValue)
if (newValue) {
this.loadSkeleton()
}
}
},
methods: {
loadSkeleton() {
this.detailLoading = true
setTimeout(() => {
this.detailLoading = false
}, 500)
},
handleEditComment() {
this.editable = true
},
handleUpdateComment() {
commentApi.update(this.type, this.comment.id, this.comment).then(response => {
this.$log.debug('Updated comment', response.data.data)
this.$message.success('评论修改成功!')
})
this.editable = false
},
onClose() {
this.$emit('close', false)
}
}
}
</script>

View File

@ -1,6 +1,9 @@
<template> <template>
<div class="comment-tab-wrapper"> <div class="comment-tab-wrapper">
<a-card :bordered="false"> <a-card
:bordered="false"
:bodyStyle="{ padding: 0 }"
>
<div class="table-page-search-wrapper"> <div class="table-page-search-wrapper">
<a-form layout="inline"> <a-form layout="inline">
<a-row :gutter="48"> <a-row :gutter="48">
@ -105,6 +108,22 @@
:loading="loading" :loading="loading"
:pagination="false" :pagination="false"
> >
<template
slot="author"
slot-scope="text,record"
>
<a-icon
type="user"
v-if="record.isAdmin"
style="margin-right: 3px;"
/>
<a
:href="record.authorUrl"
target="_blank"
v-if="record.authorUrl"
>{{ text }}</a>
<span v-else>{{ text }}</span>
</template>
<p <p
class="comment-content-wrapper" class="comment-content-wrapper"
slot="content" slot="content"
@ -116,8 +135,10 @@
slot="status" slot="status"
slot-scope="statusProperty" slot-scope="statusProperty"
> >
<a-badge :status="statusProperty.status" /> <a-badge
{{ statusProperty.text }} :status="statusProperty.status"
:text="statusProperty.text"
/>
</span> </span>
<a <a
v-if="type==='posts'" v-if="type==='posts'"
@ -203,6 +224,13 @@
> >
<a href="javascript:;">删除</a> <a href="javascript:;">删除</a>
</a-popconfirm> </a-popconfirm>
<!-- <a-divider type="vertical" />
<a
href="javascript:;"
@click="handleShowDetailDrawer(record)"
>详情</a> -->
</span> </span>
</a-table> </a-table>
<div class="page-wrapper"> <div class="page-wrapper">
@ -223,6 +251,7 @@
:title="'回复给:'+selectComment.author" :title="'回复给:'+selectComment.author"
v-model="replyCommentVisible" v-model="replyCommentVisible"
@close="onReplyClose" @close="onReplyClose"
destroyOnClose
> >
<template slot="footer"> <template slot="footer">
<a-button <a-button
@ -243,16 +272,24 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<!-- <CommentDetail
v-model="commentDetailVisible"
v-if="selectComment"
:comment="selectComment"
:type="this.type"
/> -->
</div> </div>
</template> </template>
<script> <script>
import commentApi from '@/api/comment' import { mapGetters } from 'vuex'
import optionApi from '@/api/option' import CommentDetail from './CommentDetail'
import marked from 'marked' import marked from 'marked'
import commentApi from '@/api/comment'
const postColumns = [ const postColumns = [
{ {
title: '昵称', title: '昵称',
dataIndex: 'author' dataIndex: 'author',
scopedSlots: { customRender: 'author' }
}, },
{ {
title: '内容', title: '内容',
@ -263,29 +300,33 @@ const postColumns = [
title: '状态', title: '状态',
className: 'status', className: 'status',
dataIndex: 'statusProperty', dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' } scopedSlots: { customRender: 'status' }
}, },
{ {
title: '评论文章', title: '评论文章',
dataIndex: 'post', dataIndex: 'post',
width: '200px',
scopedSlots: { customRender: 'post' } scopedSlots: { customRender: 'post' }
}, },
{ {
title: '日期', title: '日期',
dataIndex: 'createTime', dataIndex: 'createTime',
width: '170px',
scopedSlots: { customRender: 'createTime' } scopedSlots: { customRender: 'createTime' }
}, },
{ {
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
width: '150px', width: '180px',
scopedSlots: { customRender: 'action' } scopedSlots: { customRender: 'action' }
} }
] ]
const sheetColumns = [ const sheetColumns = [
{ {
title: '昵称', title: '昵称',
dataIndex: 'author' dataIndex: 'author',
scopedSlots: { customRender: 'author' }
}, },
{ {
title: '内容', title: '内容',
@ -296,27 +337,33 @@ const sheetColumns = [
title: '状态', title: '状态',
className: 'status', className: 'status',
dataIndex: 'statusProperty', dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' } scopedSlots: { customRender: 'status' }
}, },
{ {
title: '评论页面', title: '评论页面',
dataIndex: 'sheet', dataIndex: 'sheet',
width: '200px',
scopedSlots: { customRender: 'sheet' } scopedSlots: { customRender: 'sheet' }
}, },
{ {
title: '日期', title: '日期',
dataIndex: 'createTime', dataIndex: 'createTime',
width: '150px',
scopedSlots: { customRender: 'createTime' } scopedSlots: { customRender: 'createTime' }
}, },
{ {
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
width: '150px', width: '180px',
scopedSlots: { customRender: 'action' } scopedSlots: { customRender: 'action' }
} }
] ]
export default { export default {
name: 'CommentTab', name: 'CommentTab',
components: {
CommentDetail
},
props: { props: {
type: { type: {
type: String, type: String,
@ -350,13 +397,11 @@ export default {
replyComment: {}, replyComment: {},
loading: false, loading: false,
commentStatus: commentApi.commentStatus, commentStatus: commentApi.commentStatus,
options: [], commentDetailVisible: false
keys: ['blog_url']
} }
}, },
created() { created() {
this.loadComments() this.loadComments()
this.loadOptions()
}, },
computed: { computed: {
formattedComments() { formattedComments() {
@ -365,7 +410,8 @@ export default {
comment.content = marked(comment.content, { sanitize: true }) comment.content = marked(comment.content, { sanitize: true })
return comment return comment
}) })
} },
...mapGetters(['options'])
}, },
methods: { methods: {
loadComments() { loadComments() {
@ -381,13 +427,9 @@ export default {
}, },
handleQuery() { handleQuery() {
this.queryParam.page = 0 this.queryParam.page = 0
this.pagination.current = 1
this.loadComments() this.loadComments()
}, },
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handleEditStatusClick(commentId, status) { handleEditStatusClick(commentId, status) {
commentApi.updateStatus(this.type, commentId, status).then(response => { commentApi.updateStatus(this.type, commentId, status).then(response => {
this.$message.success('操作成功!') this.$message.success('操作成功!')
@ -415,6 +457,13 @@ export default {
} }
}, },
handleCreateClick() { handleCreateClick() {
if (!this.replyComment.content) {
this.$notification['error']({
message: '提示',
description: '评论内容不能为空!'
})
return
}
commentApi.create(this.type, this.replyComment).then(response => { commentApi.create(this.type, this.replyComment).then(response => {
this.$message.success('回复成功!') this.$message.success('回复成功!')
this.replyComment = {} this.replyComment = {}
@ -492,6 +541,10 @@ export default {
name: comment.author name: comment.author
} }
} }
},
handleShowDetailDrawer(comment) {
this.selectComment = comment
this.commentDetailVisible = true
} }
} }
} }

View File

@ -129,10 +129,30 @@
> >
<a-list-item-meta> <a-list-item-meta>
<a <a
v-if="item.status=='PUBLISHED'"
slot="title" slot="title"
:href="options.blog_url+'/archives/'+item.url" :href="options.blog_url+'/archives/'+item.url"
target="_blank" target="_blank"
>{{ item.title }}</a> >{{ item.title }}</a>
<a
v-else-if="item.status == 'INTIMATE'"
slot="title"
:href="options.blog_url+'/archives/'+item.url+'/password'"
target="_blank"
>{{ item.title }}</a>
<a
v-else-if="item.status=='DRAFT'"
slot="title"
href="javascript:void(0)"
@click="handlePostPreview(item.id)"
>{{ item.title }}</a>
<a
v-else
href="javascript:void(0);"
disabled
>
{{ text }}
</a>
</a-list-item-meta> </a-list-item-meta>
<div>{{ item.createTime | timeAgo }}</div> <div>{{ item.createTime | timeAgo }}</div>
</a-list-item> </a-list-item>
@ -271,9 +291,14 @@
title="操作日志" title="操作日志"
:width="isMobile()?'100%':'460'" :width="isMobile()?'100%':'460'"
closable closable
:visible="logDrawerVisiable" :visible="logDrawerVisible"
destroyOnClose destroyOnClose
@close="()=>this.logDrawerVisiable = false" @close="()=>this.logDrawerVisible = false"
>
<a-skeleton
active
:loading="logsLoading"
:paragraph="{rows: 18}"
> >
<a-row <a-row
type="flex" type="flex"
@ -306,6 +331,7 @@
</a-list> </a-list>
</a-col> </a-col>
</a-row> </a-row>
</a-skeleton>
<a-divider class="divider-transparent" /> <a-divider class="divider-transparent" />
<div class="bottom-control"> <div class="bottom-control">
<a-popconfirm <a-popconfirm
@ -322,18 +348,18 @@
</template> </template>
<script> <script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import { PageView } from '@/layouts' import { PageView } from '@/layouts'
import AnalysisCard from './components/AnalysisCard' import AnalysisCard from './components/AnalysisCard'
import RecentCommentTab from './components/RecentCommentTab' import RecentCommentTab from './components/RecentCommentTab'
import { mixin, mixinDevice } from '@/utils/mixin.js' import countTo from 'vue-count-to'
import optionApi from '@/api/option' import UploadPhoto from '../../components/Upload/UploadPhoto.vue'
import postApi from '@/api/post' import postApi from '@/api/post'
import logApi from '@/api/log' import logApi from '@/api/log'
import adminApi from '@/api/admin' import adminApi from '@/api/admin'
import journalApi from '@/api/journal' import journalApi from '@/api/journal'
import countTo from 'vue-count-to'
import UploadPhoto from '../../components/Upload/UploadPhoto.vue'
export default { export default {
name: 'Dashboard', name: 'Dashboard',
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
@ -347,14 +373,15 @@ export default {
data() { data() {
return { return {
photoList: [], photoList: [],
showMoreOptions: false, // showMoreOptions: false,
startVal: 0, startVal: 0,
logType: logApi.logType, logType: logApi.logType,
activityLoading: true, activityLoading: true,
writeLoading: true, writeLoading: true,
logLoading: true, logLoading: true,
logsLoading: true,
countsLoading: true, countsLoading: true,
logDrawerVisiable: false, logDrawerVisible: false,
postData: [], postData: [],
logData: [], logData: [],
countsData: {}, countsData: {},
@ -364,8 +391,6 @@ export default {
}, },
journalPhotos: [], // journalPhotos: [], //
logs: [], logs: [],
options: [],
keys: ['blog_url'],
logPagination: { logPagination: {
page: 1, page: 1,
size: 50, size: 50,
@ -378,11 +403,6 @@ export default {
this.getCounts() this.getCounts()
this.listLatestPosts() this.listLatestPosts()
this.listLatestLogs() this.listLatestLogs()
this.loadOptions()
// this.interval = setInterval(() => {
// this.getCounts()
// }, 5000)
}, },
computed: { computed: {
formattedPostData() { formattedPostData() {
@ -403,6 +423,12 @@ export default {
log.type = this.logType[log.type].text log.type = this.logType[log.type].text
return log return log
}) })
},
...mapGetters(['options'])
},
destroyed: function() {
if (this.logDrawerVisible) {
this.logDrawerVisible = false
} }
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
@ -418,26 +444,24 @@ export default {
this.interval = null this.interval = null
this.$log.debug('Cleared interval') this.$log.debug('Cleared interval')
} }
if (this.logDrawerVisible) {
this.logDrawerVisible = false
}
next() next()
}, },
methods: { methods: {
handlerPhotoUploadSuccess(response, file) { // handlerPhotoUploadSuccess(response, file) {
var callData = response.data.data // var callData = response.data.data
var photo = { // var photo = {
name: callData.name, // name: callData.name,
url: callData.path, // url: callData.path,
thumbnail: callData.thumbPath, // thumbnail: callData.thumbPath,
suffix: callData.suffix, // suffix: callData.suffix,
width: callData.width, // width: callData.width,
height: callData.height // height: callData.height
} // }
this.journalPhotos.push(photo) // this.journalPhotos.push(photo)
}, // },
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
listLatestPosts() { listLatestPosts() {
postApi.listLatest(5).then(response => { postApi.listLatest(5).then(response => {
this.postData = response.data.data this.postData = response.data.data
@ -462,24 +486,34 @@ export default {
}, },
handleCreateJournalClick() { handleCreateJournalClick() {
// //
this.journal.photos = this.journalPhotos // this.journal.photos = this.journalPhotos
if (!this.journal.content) {
this.$notification['error']({
message: '提示',
description: '内容不能为空!'
})
return
}
journalApi.create(this.journal).then(response => { journalApi.create(this.journal).then(response => {
this.$message.success('发表成功!') this.$message.success('发表成功!')
this.journal = {} this.journal = {}
this.photoList = [] // this.photoList = []
this.showMoreOptions = false // this.showMoreOptions = false
}) })
}, },
handleUploadPhotoWallClick() { // handleUploadPhotoWallClick() {
// // //
this.showMoreOptions = !this.showMoreOptions // this.showMoreOptions = !this.showMoreOptions
}, // },
handleShowLogDrawer() { handleShowLogDrawer() {
this.logDrawerVisiable = true this.logDrawerVisible = true
this.loadLogs() this.loadLogs()
}, },
loadLogs() { loadLogs() {
this.logsLoading = true
setTimeout(() => {
this.logsLoading = false
}, 500)
this.logPagination.page = this.logPagination.page - 1 this.logPagination.page = this.logPagination.page - 1
logApi.pageBy(this.logPagination).then(response => { logApi.pageBy(this.logPagination).then(response => {
this.logs = response.data.data.content this.logs = response.data.data.content
@ -493,6 +527,11 @@ export default {
this.listLatestLogs() this.listLatestLogs()
}) })
}, },
handlePostPreview(postId) {
postApi.preview(postId).then(response => {
window.open(response.data, '_blank')
})
},
onPaginationChange(page, pageSize) { onPaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`) this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.logPagination.page = page this.logPagination.page = page
@ -503,12 +542,12 @@ export default {
} }
</script> </script>
<style scoped="scoped"> <style lang="less" scoped>
.more-options-btn{ /* .more-options-btn {
margin-left: 15px; margin-left: 15px;
text-decoration: none; text-decoration: none;
} }
a { a {
text-decoration: none; text-decoration: none;
} } */
</style> </style>

View File

@ -15,7 +15,6 @@
</div> </div>
<div class="number"> <div class="number">
<slot name="number"> <slot name="number">
<!-- <span>{{ typeof number === 'function' && number() || number }}</span> -->
<countTo <countTo
:startVal="startNumber" :startVal="startNumber"
:endVal="typeof number === 'function' && number() || number" :endVal="typeof number === 'function' && number() || number"
@ -64,37 +63,3 @@ export default {
} }
} }
</script> </script>
<style lang="less" scoped>
.analysis-card-container {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
line-height: 22px;
.analysis-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
}
}
.number {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 32px;
line-height: 38px;
height: 38px;
}
</style>

View File

@ -18,9 +18,22 @@
:href="item.authorUrl" :href="item.authorUrl"
target="_blank" target="_blank"
>{{ item.author }}</a> 发表在 <a >{{ item.author }}</a> 发表在 <a
v-if="item.post.status=='PUBLISHED'"
:href="options.blog_url+'/archives/'+item.post.url" :href="options.blog_url+'/archives/'+item.post.url"
target="_blank" target="_blank"
>{{ item.post.title }}</a> >{{ item.post.title }}</a><a
v-else-if="item.post.status=='INTIMATE'"
:href="options.blog_url+'/archives/'+item.post.url+'/password'"
target="_blank"
>{{ item.post.title }}</a><a
v-else-if="item.post.status=='DRAFT'"
href="javascript:void(0)"
@click="handlePostPreview(item.post.id)"
>{{ item.post.title }}</a><a
v-else
href="javascript:void(0)"
>{{ item.post.title }}</a>
</template> </template>
<template <template
slot="author" slot="author"
@ -30,8 +43,16 @@
:href="item.authorUrl" :href="item.authorUrl"
target="_blank" target="_blank"
>{{ item.author }}</a> 发表在 <a >{{ item.author }}</a> 发表在 <a
v-if="item.sheet.status=='PUBLISHED'"
:href="options.blog_url+'/s/'+item.sheet.url" :href="options.blog_url+'/s/'+item.sheet.url"
target="_blank" target="_blank"
>{{ item.sheet.title }}</a><a
v-else-if="item.sheet.status=='DRAFT'"
href="javascript:void(0)"
@click="handleSheetPreview(item.sheet.id)"
>{{ item.sheet.title }}</a><a
v-else
href="javascript:void(0)"
>{{ item.sheet.title }}</a> >{{ item.sheet.title }}</a>
</template> </template>
<!-- <template slot="actions"> <!-- <template slot="actions">
@ -54,8 +75,10 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'
import commentApi from '@/api/comment' import commentApi from '@/api/comment'
import optionApi from '@/api/option' import postApi from '@/api/post'
import sheetApi from '@/api/sheet'
import marked from 'marked' import marked from 'marked'
export default { export default {
@ -73,9 +96,7 @@ export default {
data() { data() {
return { return {
comments: [], comments: [],
loading: false, loading: false
options: [],
keys: ['blog_url']
} }
}, },
computed: { computed: {
@ -84,24 +105,29 @@ export default {
comment.content = marked(comment.content, { sanitize: true }) comment.content = marked(comment.content, { sanitize: true })
return comment return comment
}) })
} },
...mapGetters(['options'])
}, },
created() { created() {
this.loadComments() this.loadComments()
this.loadOptions()
}, },
methods: { methods: {
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
loadComments() { loadComments() {
this.loading = true this.loading = true
commentApi.latestComment(this.type, 5, 'PUBLISHED').then(response => { commentApi.latestComment(this.type, 5, 'PUBLISHED').then(response => {
this.comments = response.data.data this.comments = response.data.data
this.loading = false this.loading = false
}) })
},
handlePostPreview(postId) {
postApi.preview(postId).then(response => {
window.open(response.data, '_blank')
})
},
handleSheetPreview(sheetId) {
sheetApi.preview(sheetId).then(response => {
window.open(response.data, '_blank')
})
} }
} }
} }

View File

@ -11,6 +11,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@ -11,6 +11,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@ -9,7 +9,10 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card :title="title"> <a-card
:title="title"
:bodyStyle="{ padding: '16px' }"
>
<a-form layout="horizontal"> <a-form layout="horizontal">
<a-form-item <a-form-item
label="名称:" label="名称:"
@ -42,6 +45,12 @@
> >
<a-input v-model="menuToCreate.icon" /> <a-input v-model="menuToCreate.icon" />
</a-form-item> </a-form-item>
<a-form-item
label="分组:"
:style="{ display: fieldExpand ? 'block' : 'none' }"
>
<a-input v-model="menuToCreate.team" />
</a-form-item>
<a-form-item <a-form-item
label="打开方式:" label="打开方式:"
:style="{ display: fieldExpand ? 'block' : 'none' }" :style="{ display: fieldExpand ? 'block' : 'none' }"
@ -90,7 +99,10 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card title="所有菜单"> <a-card
title="所有菜单"
:bodyStyle="{ padding: '16px' }"
>
<a-table <a-table
:columns="columns" :columns="columns"
:dataSource="menus" :dataSource="menus"
@ -141,6 +153,10 @@ const columns = [
title: '地址', title: '地址',
dataIndex: 'url' dataIndex: 'url'
}, },
{
title: '分组',
dataIndex: 'team'
},
{ {
title: '排序', title: '排序',
dataIndex: 'priority' dataIndex: 'priority'
@ -159,7 +175,9 @@ export default {
loading: false, loading: false,
columns, columns,
menus: [], menus: [],
menuToCreate: {}, menuToCreate: {
target: '_self'
},
fieldExpand: false fieldExpand: false
} }
}, },
@ -219,6 +237,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@ -9,7 +9,7 @@
:xs="24" :xs="24"
:style="{'padding-bottom':'12px'}" :style="{'padding-bottom':'12px'}"
> >
<a-card> <a-card :bodyStyle="{ padding: '16px' }">
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item> <a-form-item>
<codemirror <codemirror
@ -35,8 +35,27 @@
:xs="24" :xs="24"
:style="{'padding-bottom':'12px'}" :style="{'padding-bottom':'12px'}"
> >
<a-card :title="activatedTheme.name+' 主题'"> <a-card :bodyStyle="{ padding: '16px' }">
<template slot="title">
<a-select
style="width: 100%"
@change="onSelectTheme"
v-model="selectedTheme.id"
>
<a-select-option
v-for="(theme,index) in themes"
:key="index"
:value="theme.id"
>{{ theme.name }}
<a-icon
v-if="theme.activated"
type="check"
/>
</a-select-option>
</a-select>
</template>
<theme-file <theme-file
v-if="files"
:files="files" :files="files"
@listenToSelect="handleSelectFile" @listenToSelect="handleSelectFile"
/> />
@ -65,25 +84,38 @@ export default {
lineNumbers: true, lineNumbers: true,
line: true line: true
}, },
files: [], files: null,
file: {}, file: {},
content: '', content: '',
activatedTheme: {} themes: [],
selectedTheme: {}
} }
}, },
created() { created() {
this.loadFiles()
this.loadActivatedTheme() this.loadActivatedTheme()
this.loadFiles()
this.loadThemes()
}, },
methods: { methods: {
loadActivatedTheme() {
themeApi.getActivatedTheme().then(response => {
this.selectedTheme = response.data.data
})
},
loadFiles() { loadFiles() {
themeApi.listFiles().then(response => { themeApi.listFilesActivated().then(response => {
this.files = response.data.data this.files = response.data.data
}) })
}, },
loadActivatedTheme() { loadThemes() {
themeApi.getActivatedTheme().then(response => { themeApi.listAll().then(response => {
this.activatedTheme = response.data.data this.themes = response.data.data
})
},
onSelectTheme(themeId) {
this.files = null
themeApi.listFiles(themeId).then(response => {
this.files = response.data.data
}) })
}, },
handleSelectFile(file) { handleSelectFile(file) {
@ -95,7 +127,12 @@ export default {
this.buttonDisabled = true this.buttonDisabled = true
return return
} }
if (file.name === 'settings.yaml' || file.name === 'settings.yml' || file.name === 'theme.yaml' || file.name === 'theme.yml') { if (
file.name === 'settings.yaml' ||
file.name === 'settings.yml' ||
file.name === 'theme.yaml' ||
file.name === 'theme.yml'
) {
this.$confirm({ this.$confirm({
title: '警告:请谨慎修改该配置文件', title: '警告:请谨慎修改该配置文件',
content: '修改之后可能会产生不可预料的问题!', content: '修改之后可能会产生不可预料的问题!',
@ -106,14 +143,14 @@ export default {
} }
}) })
} }
themeApi.getContent(file.path).then(response => { themeApi.getContent(this.selectedTheme.id, file.path).then(response => {
this.content = response.data.data this.content = response.data.data
this.file = file this.file = file
this.buttonDisabled = false this.buttonDisabled = false
}) })
}, },
handlerSaveContent() { handlerSaveContent() {
themeApi.saveContent(this.file.path, this.content).then(response => { themeApi.saveContent(this.selectedTheme.id, this.file.path, this.content).then(response => {
this.$message.success('保存成功!') this.$message.success('保存成功!')
}) })
} }

View File

@ -47,7 +47,7 @@
style="margin-right:3px" style="margin-right:3px"
/> />
</div> </div>
<div @click="handleEditClick(item)"> <div @click="handleShowThemeSetting(item)">
<a-icon <a-icon
type="setting" type="setting"
style="margin-right:3px" style="margin-right:3px"
@ -90,7 +90,10 @@
/> />
</span> </span>
</a-menu-item> </a-menu-item>
<a-menu-item :key="2"> <a-menu-item
:key="2"
v-if="item.repo"
>
<a-popconfirm <a-popconfirm
:title="'确定更新【' + item.name + '】主题?'" :title="'确定更新【' + item.name + '】主题?'"
@confirm="handleUpdateTheme(item.id)" @confirm="handleUpdateTheme(item.id)"
@ -98,11 +101,20 @@
cancelText="取消" cancelText="取消"
> >
<a-icon <a-icon
type="download" type="cloud"
style="margin-right:3px" style="margin-right:3px"
/> />线
</a-popconfirm> </a-popconfirm>
</a-menu-item> </a-menu-item>
<a-menu-item
:key="3"
@click="handleShowUpdateNewThemeModal(item)"
>
<a-icon
type="file"
style="margin-right:3px"
/>
</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</template> </template>
@ -111,157 +123,13 @@
</a-list> </a-list>
</a-col> </a-col>
</a-row> </a-row>
<a-drawer
v-if="themeProperty"
:title="themeProperty.name + ' 主题设置'"
width="100%"
closable
@close="onClose"
:visible="visible"
destroyOnClose
>
<a-row
:gutter="12"
type="flex"
>
<a-col
:xl="12"
:lg="12"
:md="12"
:sm="24"
:xs="24"
>
<a-skeleton
active
:loading="optionLoading"
:paragraph="{rows: 10}"
>
<a-card :bordered="false">
<img
:alt="themeProperty.name"
:src="themeProperty.screenshots"
slot="cover"
>
<a-card-meta :description="themeProperty.description">
<template slot="title">
<a
:href="themeProperty.author.website"
target="_blank"
>{{ themeProperty.author.name }}</a>
</template>
<a-avatar
v-if="themeProperty.logo"
:src="themeProperty.logo"
size="large"
slot="avatar"
/>
<a-avatar
v-else
size="large"
slot="avatar"
>{{ themeProperty.author.name }}</a-avatar>
</a-card-meta>
</a-card>
</a-skeleton>
</a-col>
<a-col
:xl="12"
:lg="12"
:md="12"
:sm="24"
:xs="24"
style="padding-bottom: 50px;"
>
<a-skeleton
active
:loading="optionLoading"
:paragraph="{rows: 20}"
>
<div class="card-container">
<a-tabs
type="card"
defaultActiveKey="0"
v-if="themeConfiguration.length>0"
>
<a-tab-pane
v-for="(group, index) in themeConfiguration"
:key="index.toString()"
:tab="group.label"
>
<a-form layout="vertical">
<a-form-item
v-for="(item, index1) in group.items"
:label="item.label + ''"
:key="index1"
:wrapper-col="wrapperCol"
>
<a-input
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
v-if="item.type == 'TEXT'"
/>
<a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="themeSettings[item.name]"
:placeholder="item.placeholder"
v-else-if="item.type == 'TEXTAREA'"
/>
<a-radio-group
v-decorator="['radio-group']"
:defaultValue="item.defaultValue"
v-model="themeSettings[item.name]"
v-else-if="item.type == 'RADIO'"
>
<a-radio
v-for="(option, index2) in item.options"
:key="index2"
:value="option.value"
>{{ option.label }}</a-radio>
</a-radio-group>
<a-select
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
v-else-if="item.type == 'SELECT'"
>
<a-select-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
>{{ option.label }}</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
<a-alert
message="当前主题暂无设置选项"
banner
v-else
/>
</div>
</a-skeleton>
</a-col>
</a-row>
<footer-tool-bar <ThemeSetting
v-if="themeConfiguration.length>0" :theme="selectedTheme"
:style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}" v-if="themeSettingVisible"
> @close="onThemeSettingsClose"
<a-button />
type="primary"
@click="handleSaveSettings"
>保存</a-button>
<a-button
type="dashed"
@click="()=>this.attachmentDrawerVisible = true"
style="margin-left: 8px;"
>附件库</a-button>
</footer-tool-bar>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
</a-drawer>
<div class="upload-button"> <div class="upload-button">
<a-dropdown <a-dropdown
placement="topLeft" placement="topLeft"
@ -278,7 +146,7 @@
<a <a
rel="noopener noreferrer" rel="noopener noreferrer"
href="javascript:void(0);" href="javascript:void(0);"
@click="()=>this.uploadVisible = true" @click="()=>this.uploadThemeVisible = true"
>安装主题</a> >安装主题</a>
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
@ -293,9 +161,11 @@
</div> </div>
<a-modal <a-modal
title="安装主题" title="安装主题"
v-model="uploadVisible" v-model="uploadThemeVisible"
destroyOnClose
:footer="null" :footer="null"
:bodyStyle="{ padding: '0 24px 24px' }" :bodyStyle="{ padding: '0 24px 24px' }"
:afterClose="onThemeUploadClose"
> >
<div class="custom-tab-wrapper"> <div class="custom-tab-wrapper">
<a-tabs> <a-tabs>
@ -333,59 +203,61 @@
tab="本地上传" tab="本地上传"
key="2" key="2"
> >
<upload <FilePondUpload
ref="upload"
name="file" name="file"
multiple
accept="application/zip" accept="application/zip"
label="点击选择主题包或将主题包拖拽到此处<br>仅支持 ZIP 格式的文件"
:uploadHandler="uploadHandler" :uploadHandler="uploadHandler"
@change="handleChange"
@success="handleUploadSuccess" @success="handleUploadSuccess"
> >
<p class="ant-upload-drag-icon"> </FilePondUpload>
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择主题或将主题拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传仅支持 ZIP 格式的文件</p>
</upload>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
</a-modal> </a-modal>
<a-modal
title="更新主题"
v-model="uploadNewThemeVisible"
:footer="null"
destroyOnClose
:afterClose="onThemeUploadClose"
>
<FilePondUpload
ref="updateByupload"
name="file"
accept="application/zip"
label="点击选择主题更新包或将主题更新包拖拽到此处<br>仅支持 ZIP 格式的文件"
:uploadHandler="updateByUploadHandler"
:filed="prepareUpdateTheme.id"
:multiple="false"
@success="handleUploadSuccess"
>
</FilePondUpload>
</a-modal>
</div> </div>
</template> </template>
<script> <script>
import AttachmentDrawer from '../attachment/components/AttachmentDrawer' import ThemeSetting from './components/ThemeSetting'
import FooterToolBar from '@/components/FooterToolbar'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import themeApi from '@/api/theme' import themeApi from '@/api/theme'
export default { export default {
components: { components: {
AttachmentDrawer, ThemeSetting
FooterToolBar
}, },
mixins: [mixin, mixinDevice],
data() { data() {
return { return {
themeLoading: false, themeLoading: false,
optionLoading: true, uploadThemeVisible: false,
uploadVisible: false, uploadNewThemeVisible: false,
fetchButtonLoading: false, fetchButtonLoading: false,
wrapperCol: {
xl: { span: 12 },
lg: { span: 12 },
sm: { span: 24 },
xs: { span: 24 }
},
attachmentDrawerVisible: false,
themes: [], themes: [],
visible: false, themeSettingVisible: false,
themeConfiguration: [], selectedTheme: {},
themeSettings: [],
themeProperty: null,
fetchingUrl: null, fetchingUrl: null,
uploadHandler: themeApi.upload uploadHandler: themeApi.upload,
updateByUploadHandler: themeApi.updateByUpload,
prepareUpdateTheme: {}
} }
}, },
computed: { computed: {
@ -400,13 +272,13 @@ export default {
this.loadThemes() this.loadThemes()
}, },
destroyed: function() { destroyed: function() {
if (this.visible) { if (this.themeSettingVisible) {
this.visible = false this.themeSettingVisible = false
} }
}, },
beforeRouteLeave(to, from, next) { beforeRouteLeave(to, from, next) {
if (this.visible) { if (this.themeSettingVisible) {
this.visible = false this.themeSettingVisible = false
} }
next() next()
}, },
@ -418,21 +290,7 @@ export default {
this.themeLoading = false this.themeLoading = false
}) })
}, },
settingDrawer(theme) {
this.visible = true
this.optionLoading = true
this.themeProperty = theme
themeApi.fetchConfiguration(theme.id).then(response => {
this.themeConfiguration = response.data.data
themeApi.fetchSettings(theme.id).then(response => {
this.themeSettings = response.data.data
setTimeout(() => {
this.optionLoading = false
}, 300)
})
})
},
activeTheme(themeId) { activeTheme(themeId) {
themeApi.active(themeId).then(response => { themeApi.active(themeId).then(response => {
this.$message.success('设置成功!') this.$message.success('设置成功!')
@ -451,32 +309,15 @@ export default {
this.loadThemes() this.loadThemes()
}) })
}, },
handleSaveSettings() {
themeApi.saveSettings(this.themeProperty.id, this.themeSettings).then(response => {
this.$message.success('保存成功!')
})
},
onClose() {
this.visible = false
this.optionLoading = false
this.themeConfiguration = []
this.themeProperty = null
},
handleChange(info) {
const status = info.file.status
if (status === 'done') {
this.$message.success(`${info.file.name} 主题上传成功!`)
} else if (status === 'error') {
this.$message.error(`${info.file.name} 主题上传失败!`)
}
},
handleUploadSuccess() { handleUploadSuccess() {
this.uploadVisible = false if (this.uploadThemeVisible) {
this.uploadThemeVisible = false
}
if (this.uploadNewThemeVisible) {
this.uploadNewThemeVisible = false
}
this.loadThemes() this.loadThemes()
}, },
handleEllipsisClick(theme) {
this.$log.debug('Ellipsis clicked', theme)
},
handleEditClick(theme) { handleEditClick(theme) {
this.settingDrawer(theme) this.settingDrawer(theme)
}, },
@ -484,12 +325,19 @@ export default {
this.activeTheme(theme.id) this.activeTheme(theme.id)
}, },
handleFetching() { handleFetching() {
if (!this.fetchingUrl) {
this.$notification['error']({
message: '提示',
description: '远程地址不能为空!'
})
return
}
this.fetchButtonLoading = true this.fetchButtonLoading = true
themeApi themeApi
.fetching(this.fetchingUrl) .fetching(this.fetchingUrl)
.then(response => { .then(response => {
this.$message.success('拉取成功!') this.$message.success('拉取成功!')
this.uploadVisible = false this.uploadThemeVisible = false
this.loadThemes() this.loadThemes()
}) })
.finally(() => { .finally(() => {
@ -501,12 +349,33 @@ export default {
this.loadThemes() this.loadThemes()
this.$message.success('刷新成功!') this.$message.success('刷新成功!')
}) })
},
handleShowUpdateNewThemeModal(item) {
this.prepareUpdateTheme = item
this.uploadNewThemeVisible = true
},
handleShowThemeSetting(theme) {
this.selectedTheme = theme
this.themeSettingVisible = true
},
onThemeUploadClose() {
if (this.uploadThemeVisible) {
this.$refs.upload.handleClearFileList()
}
if (this.uploadNewThemeVisible) {
this.$refs.updateByupload.handleClearFileList()
}
this.loadThemes()
},
onThemeSettingsClose() {
this.themeSettingVisible = false
this.selectedTheme = {}
} }
} }
} }
</script> </script>
<style lang="less" scoped> <style lang="less">
@keyframes scaleDraw { @keyframes scaleDraw {
0% { 0% {
transform: scale(1); transform: scale(1);

View File

@ -55,6 +55,3 @@ export default {
} }
} }
</script> </script>
<style>
</style>

View File

@ -56,11 +56,3 @@ export default {
} }
} }
</script> </script>
<style lang="less" scoped>
.ant-tree-child-tree {
li {
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,345 @@
<template>
<a-drawer
:title="selectedTheme.name + ' 主题设置'"
width="100%"
placement="right"
closable
destroyOnClose
@close="onClose"
:visible="visible"
>
<a-row
:gutter="12"
type="flex"
>
<a-col
:xl="12"
:lg="12"
:md="12"
:sm="24"
:xs="24"
v-if="!viewMode"
>
<a-skeleton
active
:loading="settingLoading"
:paragraph="{rows: 10}"
>
<a-card :bordered="false">
<img
:alt="selectedTheme.name"
:src="selectedTheme.screenshots"
slot="cover"
>
<a-card-meta :description="selectedTheme.description">
<template slot="title">
<a
:href="selectedTheme.author.website"
target="_blank"
>{{ selectedTheme.author.name }}</a>
</template>
<a-avatar
v-if="selectedTheme.logo"
:src="selectedTheme.logo"
size="large"
slot="avatar"
/>
<a-avatar
v-else
size="large"
slot="avatar"
>{{ selectedTheme.author.name }}</a-avatar>
</a-card-meta>
</a-card>
</a-skeleton>
</a-col>
<a-col
:xl="formColValue"
:lg="formColValue"
:md="formColValue"
:sm="24"
:xs="24"
style="padding-bottom: 50px;"
>
<a-skeleton
active
:loading="settingLoading"
:paragraph="{rows: 20}"
>
<div class="card-container">
<a-tabs
type="card"
defaultActiveKey="0"
v-if="themeConfiguration.length>0"
>
<a-tab-pane
v-for="(group, index) in themeConfiguration"
:key="index.toString()"
:tab="group.label"
>
<a-form layout="vertical">
<a-form-item
v-for="(item, index1) in group.items"
:label="item.label + ''"
:key="index1"
:wrapper-col="wrapperCol"
>
<a-input
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
v-if="item.type == 'TEXT'"
/>
<a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="themeSettings[item.name]"
:placeholder="item.placeholder"
v-else-if="item.type == 'TEXTAREA'"
/>
<a-radio-group
v-decorator="['radio-group']"
:defaultValue="item.defaultValue"
v-model="themeSettings[item.name]"
v-else-if="item.type == 'RADIO'"
>
<a-radio
v-for="(option, index2) in item.options"
:key="index2"
:value="option.value"
>{{ option.label }}</a-radio>
</a-radio-group>
<a-select
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
v-else-if="item.type == 'SELECT'"
>
<a-select-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
>{{ option.label }}</a-select-option>
</a-select>
<verte
picker="square"
model="hex"
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
v-else-if="item.type == 'COLOR'"
style="display: inline-block;height: 24px;"
></verte>
<a-input
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
v-else-if="item.type == 'ATTACHMENT'"
>
<a
href="javascript:void(0);"
slot="addonAfter"
@click="handleShowSelectAttachment(item.name)"
>
<a-icon type="picture" />
</a>
</a-input>
<a-input
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
v-else
/>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
<a-alert
message="当前主题暂无设置选项"
banner
v-else
/>
</div>
</a-skeleton>
</a-col>
<a-col
:xl="20"
:lg="20"
:md="20"
:sm="24"
:xs="24"
v-if="viewMode"
style="padding-bottom: 50px;"
>
<a-card
:bordered="true"
:bodyStyle="{ padding: 0}"
>
<iframe
id="themeViewIframe"
title="主题预览"
frameborder="0"
scrolling="auto"
border="0"
:src="options.blog_url"
width="100%"
:height="clientHeight-165"
> </iframe>
</a-card>
</a-col>
</a-row>
<AttachmentSelectDrawer
v-model="attachmentDrawerVisible"
@listenToSelect="handleSelectAttachment"
title="选择附件"
/>
<footer-tool-bar
v-if="themeConfiguration.length>0"
:style="{ width : '100%'}"
>
<a-button
v-if="!this.isMobile() && theme.activated && viewMode"
type="primary"
@click="toggleViewMode"
style="marginRight: 8px"
ghost
>普通模式</a-button>
<a-button
v-else-if="!this.isMobile() && theme.activated && !viewMode"
type="dashed"
@click="toggleViewMode"
style="marginRight: 8px"
>预览模式</a-button>
<a-button
type="primary"
@click="handleSaveSettings"
>保存</a-button>
</footer-tool-bar>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import AttachmentSelectDrawer from '../../attachment/components/AttachmentSelectDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import Verte from 'verte'
import 'verte/dist/verte.css'
import themeApi from '@/api/theme'
export default {
name: 'ThemeSetting',
mixins: [mixin, mixinDevice],
components: {
AttachmentSelectDrawer,
FooterToolBar,
Verte
},
data() {
return {
attachmentDrawerVisible: false,
selectedTheme: this.theme,
themeConfiguration: [],
themeSettings: [],
settingLoading: true,
selectedField: '',
wrapperCol: {
xl: { span: 12 },
lg: { span: 12 },
sm: { span: 24 },
xs: { span: 24 }
},
viewMode: false,
formColValue: 12,
clientHeight: document.documentElement.clientHeight
}
},
model: {
prop: 'visible',
event: 'close'
},
props: {
theme: {
type: Object,
required: true
},
visible: {
type: Boolean,
required: false,
default: true
}
},
created() {
this.loadSkeleton()
this.initData()
},
watch: {
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
}
}
},
computed: {
...mapGetters(['options'])
},
methods: {
loadSkeleton() {
this.settingLoading = true
setTimeout(() => {
this.settingLoading = false
}, 500)
},
initData() {
this.settingLoading = true
themeApi.fetchConfiguration(this.selectedTheme.id).then(response => {
this.themeConfiguration = response.data.data
themeApi.fetchSettings(this.selectedTheme.id).then(response => {
this.themeSettings = response.data.data
setTimeout(() => {
this.settingLoading = false
}, 300)
})
})
},
handleSaveSettings() {
themeApi.saveSettings(this.selectedTheme.id, this.themeSettings).then(response => {
this.$message.success('保存成功!')
if (this.viewMode) {
document.getElementById('themeViewIframe').contentWindow.location.reload(true)
}
})
},
onClose() {
this.$emit('close', false)
},
handleShowSelectAttachment(field) {
this.selectedField = field
this.attachmentDrawerVisible = true
},
handleSelectAttachment(data) {
this.themeSettings[this.selectedField] = encodeURI(data.path)
this.attachmentDrawerVisible = false
},
toggleViewMode() {
this.viewMode = !this.viewMode
if (this.viewMode) {
this.formColValue = 4
this.wrapperCol = {
xl: { span: 24 },
lg: { span: 24 },
sm: { span: 24 },
xs: { span: 24 }
}
} else {
this.formColValue = 12
this.wrapperCol = {
xl: { span: 12 },
lg: { span: 12 },
sm: { span: 24 },
xs: { span: 24 }
}
}
}
}
}
</script>

View File

@ -9,7 +9,7 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card :title="title"> <a-card :title="title" :bodyStyle="{ padding: '16px' }">
<a-form layout="horizontal"> <a-form layout="horizontal">
<a-form-item <a-form-item
label="名称:" label="名称:"
@ -68,7 +68,7 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '1rem' }" :style="{ 'padding-bottom': '1rem' }"
> >
<a-card title="分类列表"> <a-card title="分类列表" :bodyStyle="{ padding: '16px' }">
<a-table <a-table
:columns="columns" :columns="columns"
:dataSource="categories" :dataSource="categories"
@ -206,6 +206,13 @@ export default {
}) })
}, },
createOrUpdateCategory() { createOrUpdateCategory() {
if (!this.categoryToCreate.name) {
this.$notification['error']({
message: '提示',
description: '分类名称不能为空!'
})
return
}
if (this.categoryToCreate.id) { if (this.categoryToCreate.id) {
categoryApi.update(this.categoryToCreate.id, this.categoryToCreate).then(response => { categoryApi.update(this.categoryToCreate.id, this.categoryToCreate).then(response => {
this.$message.success('更新成功!') this.$message.success('更新成功!')
@ -232,9 +239,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
.category-tree {
margin-top: 1rem;
}
</style>

View File

@ -12,187 +12,47 @@
</div> </div>
<div id="editor"> <div id="editor">
<mavon-editor <halo-editor
ref="md" ref="md"
v-model="postToStage.originalContent" v-model="postToStage.originalContent"
:boxShadow="false" :boxShadow="false"
:toolbars="toolbars" :toolbars="toolbars"
:ishljs="true" :ishljs="true"
:autofocus="false" :autofocus="false"
@imgAdd="pictureUploadHandle" @imgAdd="handleAttachmentUpload"
@keydown.ctrl.83.native="handleSaveDraft"
@keydown.meta.83.native="handleSaveDraft"
/> />
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
<a-drawer <PostSetting
title="文章设置" :post="postToStage"
:width="isMobile()?'100%':'460'" :tagIds="selectedTagIds"
placement="right" :categoryIds="selectedCategoryIds"
closable
@close="()=>this.postSettingVisible=false"
:visible="postSettingVisible" :visible="postSettingVisible"
> @close="onPostSettingsClose"
<div class="post-setting-drawer-content"> @onRefreshPost="onRefreshPostFromSetting"
<div :style="{ marginBottom: '16px' }"> @onRefreshTagIds="onRefreshTagIdsFromSetting"
<h3 class="post-setting-drawer-title">基本设置</h3> @onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item
label="文章路径:"
:help="options.blog_url+'/archives/' + (postToStage.url ? postToStage.url : '{auto_generate}')"
>
<a-input v-model="postToStage.url" />
</a-form-item>
<a-form-item
label="发表时间:"
>
<a-date-picker
showTime
:defaultValue="pickerDefaultValue"
format="YYYY-MM-DD HH:mm:ss"
placeholder="Select Publish Time"
@change="onChange"
@ok="onOk"
/> />
</a-form-item>
<a-form-item label="开启评论:">
<a-radio-group
v-model="postToStage.disallowComment"
:defaultValue="false"
>
<a-radio :value="false">开启</a-radio>
<a-radio :value="true">关闭</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">分类目录</h3>
<div class="post-setting-drawer-item">
<category-tree
v-model="selectedCategoryIds"
:categories="categories"
/>
<div>
<a-form layout="vertical">
<a-form-item v-if="categoryForm">
<category-select-tree
:categories="categories"
v-model="categoryToCreate.parentId"
/>
</a-form-item>
<a-form-item v-if="categoryForm">
<a-input
placeholder="分类名称"
v-model="categoryToCreate.name"
/>
</a-form-item>
<a-form-item v-if="categoryForm">
<a-input
placeholder="分类路径"
v-model="categoryToCreate.slugNames"
/>
</a-form-item>
<a-form-item>
<a-button
type="primary"
style="marginRight: 8px"
v-if="categoryForm"
@click="handlerCreateCategory"
>保存</a-button>
<a-button
type="dashed"
style="marginRight: 8px"
v-if="!categoryForm"
@click="toggleCategoryForm"
>新增</a-button>
<a-button
v-if="categoryForm"
@click="toggleCategoryForm"
>取消</a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">标签</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item>
<TagSelect v-model="selectedTagIds" />
</a-form-item>
</a-form>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">摘要</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item>
<a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="postToStage.summary"
placeholder="不填写则会自动生成"
/>
</a-form-item>
</a-form>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">缩略图</h3>
<div class="post-setting-drawer-item">
<div class="post-thum">
<img
class="img"
:src="postToStage.thumbnail || '//i.loli.net/2019/05/05/5ccf007c0a01d.png'"
@click="()=>this.thumDrawerVisible=true"
>
<a-button
class="post-thum-remove"
type="dashed"
@click="handlerRemoveThumb"
>移除</a-button>
</div>
</div>
</div>
<a-divider class="divider-transparent" />
</div>
<AttachmentSelectDrawer
v-model="thumDrawerVisible"
@listenToSelect="handleSelectPostThumb"
:drawerWidth="460"
/>
<div class="bottom-control">
<a-button
style="marginRight: 8px"
@click="handleDraftClick"
>保存草稿</a-button>
<a-button
@click="handlePublishClick"
type="primary"
>发布</a-button>
</div>
</a-drawer>
<AttachmentDrawer v-model="attachmentDrawerVisible" /> <AttachmentDrawer v-model="attachmentDrawerVisible" />
<footer-tool-bar :style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}"> <footer-tool-bar :style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}">
<a-button
type="danger"
@click="handleSaveDraft"
>保存草稿</a-button>
<a-button
@click="handlePreview"
style="margin-left: 8px;"
>预览</a-button>
<a-button <a-button
type="primary" type="primary"
@click="()=>this.postSettingVisible = true" @click="handleShowPostSetting"
style="margin-left: 8px;"
>发布</a-button> >发布</a-button>
<a-button <a-button
type="dashed" type="dashed"
@ -204,32 +64,26 @@
</template> </template>
<script> <script>
import CategoryTree from './components/CategoryTree'
import TagSelect from './components/TagSelect'
import { mavonEditor } from 'mavon-editor'
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import CategorySelectTree from './components/CategorySelectTree'
import FooterToolBar from '@/components/FooterToolbar'
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import { toolbars } from '@/core/const' import { mapGetters } from 'vuex'
import 'mavon-editor/dist/css/index.css'
import categoryApi from '@/api/category'
import postApi from '@/api/post'
import optionApi from '@/api/option'
import attachmentApi from '@/api/attachment'
import moment from 'moment' import moment from 'moment'
import PostSetting from './components/PostSetting'
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import { toolbars } from '@/core/const'
import { haloEditor } from 'halo-editor'
import 'halo-editor/dist/css/index.css'
import postApi from '@/api/post'
import attachmentApi from '@/api/attachment'
export default { export default {
components: {
TagSelect,
mavonEditor,
CategoryTree,
FooterToolBar,
AttachmentDrawer,
AttachmentSelectDrawer,
CategorySelectTree
},
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
components: {
PostSetting,
haloEditor,
FooterToolBar,
AttachmentDrawer
},
data() { data() {
return { return {
toolbars, toolbars,
@ -240,41 +94,14 @@ export default {
}, },
attachmentDrawerVisible: false, attachmentDrawerVisible: false,
postSettingVisible: false, postSettingVisible: false,
thumDrawerVisible: false,
categoryForm: false,
categories: [],
selectedCategoryIds: [],
selectedTagIds: [],
postToStage: {}, postToStage: {},
categoryToCreate: {}, selectedTagIds: [],
timer: null, selectedCategoryIds: []
options: [],
keys: ['blog_url']
} }
}, },
created() {
this.loadCategories()
this.loadOptions()
clearInterval(this.timer)
this.timer = null
this.autoSaveTimer()
},
destroyed: function() {
clearInterval(this.timer)
this.timer = null
},
beforeRouteLeave(to, from, next) {
if (this.timer !== null) {
clearInterval(this.timer)
}
// Auto save the post
this.autoSavePost()
next()
},
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
// Get post id from query // Get post id from query
const postId = to.query.postId const postId = to.query.postId
next(vm => { next(vm => {
if (postId) { if (postId) {
postApi.get(postId).then(response => { postApi.get(postId).then(response => {
@ -286,122 +113,108 @@ export default {
} }
}) })
}, },
destroyed: function() {
if (this.postSettingVisible) {
this.postSettingVisible = false
}
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.postSettingVisible) {
this.postSettingVisible = false
}
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
next()
},
computed: { computed: {
pickerDefaultValue() { ...mapGetters(['options'])
if (this.postToStage.createTime) {
var date = new Date(this.postToStage.createTime)
return moment(date, 'YYYY-MM-DD HH:mm:ss')
}
return moment(new Date(), 'YYYY-MM-DD HH:mm:ss')
}
}, },
methods: { methods: {
loadCategories() { handleSaveDraft() {
categoryApi.listAll().then(response => { this.postToStage.status = 'DRAFT'
this.categories = response.data.data if (!this.postToStage.title) {
}) this.postToStage.title = moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')
}, }
loadOptions() { if (!this.postToStage.originalContent) {
optionApi.listAll(this.keys).then(response => { this.postToStage.originalContent = '开始编辑...'
this.options = response.data.data }
})
},
createOrUpdatePost(createSuccess, updateSuccess, autoSave) {
// Set category ids
this.postToStage.categoryIds = this.selectedCategoryIds
// Set tag ids
this.postToStage.tagIds = this.selectedTagIds
if (this.postToStage.id) { if (this.postToStage.id) {
// Update the post // Update the post
postApi.update(this.postToStage.id, this.postToStage, autoSave).then(response => { postApi.update(this.postToStage.id, this.postToStage, false).then(response => {
this.$log.debug('Updated post', response.data.data) this.$log.debug('Updated post', response.data.data)
if (updateSuccess) { this.$message.success('保存草稿成功!')
updateSuccess()
}
}) })
} else { } else {
// Create the post // Create the post
postApi.create(this.postToStage, autoSave).then(response => { postApi.create(this.postToStage, false).then(response => {
this.$log.debug('Created post', response.data.data) this.$log.debug('Created post', response.data.data)
if (createSuccess) { this.$message.success('保存草稿成功!')
createSuccess()
}
this.postToStage = response.data.data this.postToStage = response.data.data
}) })
} }
}, },
savePost() { handleAttachmentUpload(pos, $file) {
this.createOrUpdatePost(
() => this.$message.success('文章创建成功'),
() => this.$message.success('文章更新成功'),
false
)
},
autoSavePost() {
if (this.postToStage.title != null && this.postToStage.originalContent != null) {
this.createOrUpdatePost(null, null, true)
}
},
toggleCategoryForm() {
this.categoryForm = !this.categoryForm
},
handlePublishClick() {
this.postToStage.status = 'PUBLISHED'
this.savePost()
},
handleDraftClick() {
this.postToStage.status = 'DRAFT'
this.savePost()
},
handlerRemoveThumb() {
this.postToStage.thumbnail = null
},
handlerCreateCategory() {
categoryApi.create(this.categoryToCreate).then(response => {
this.loadCategories()
this.categoryToCreate = {}
})
},
handleSelectPostThumb(data) {
this.postToStage.thumbnail = encodeURI(data.path)
this.thumDrawerVisible = false
},
autoSaveTimer() {
if (this.timer == null) {
this.timer = setInterval(() => {
this.autoSavePost()
}, 15000)
}
},
pictureUploadHandle(pos, $file) {
var formdata = new FormData() var formdata = new FormData()
formdata.append('file', $file) formdata.append('file', $file)
attachmentApi.upload(formdata).then((response) => { attachmentApi.upload(formdata).then(response => {
var responseObject = response.data var responseObject = response.data
if (responseObject.status === 200) { if (responseObject.status === 200) {
var MavonEditor = this.$refs.md var HaloEditor = this.$refs.md
MavonEditor.$img2Url(pos, encodeURI(responseObject.data.path)) HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path))
this.$message.success('图片上传成功') this.$message.success('图片上传成功!')
} else { } else {
this.$message.error('图片上传失败:' + responseObject.message) this.$message.error('图片上传失败:' + responseObject.message)
} }
}) })
}, },
onChange(value, dateString) { handleShowPostSetting() {
this.postToStage.createTime = value.valueOf() this.postSettingVisible = true
}, },
onOk(value) { handlePreview() {
this.postToStage.createTime = value.valueOf() this.postToStage.status = 'DRAFT'
if (!this.postToStage.title) {
this.postToStage.title = moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')
}
if (!this.postToStage.originalContent) {
this.postToStage.originalContent = '开始编辑...'
}
if (this.postToStage.id) {
// Update the post
postApi.update(this.postToStage.id, this.postToStage, false).then(response => {
this.$log.debug('Updated post', response.data.data)
postApi.preview(this.postToStage.id).then(response => {
window.open(response.data, '_blank')
})
})
} else {
// Create the post
postApi.create(this.postToStage, false).then(response => {
this.$log.debug('Created post', response.data.data)
this.postToStage = response.data.data
postApi.preview(this.postToStage.id).then(response => {
window.open(response.data, '_blank')
})
})
}
},
//
onPostSettingsClose() {
this.postSettingVisible = false
},
onRefreshPostFromSetting(post) {
this.postToStage = post
},
onRefreshTagIdsFromSetting(tagIds) {
this.selectedTagIds = tagIds
},
onRefreshCategoryIdsFromSetting(categoryIds) {
this.selectedCategoryIds = categoryIds
} }
} }
} }
</script> </script>
<style lang="less" scoped>
.v-note-wrapper {
z-index: 1000;
min-height: 580px;
}
</style>

View File

@ -1,6 +1,9 @@
<template> <template>
<div class="page-header-index-wide"> <div class="page-header-index-wide">
<a-card :bordered="false"> <a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="table-page-search-wrapper"> <div class="table-page-search-wrapper">
<a-form layout="inline"> <a-form layout="inline">
<a-row :gutter="48"> <a-row :gutter="48">
@ -89,7 +92,7 @@
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
key="2" key="2"
v-if="queryParam.status === 'PUBLISHED' || queryParam.status ==='DRAFT'" v-if="queryParam.status === 'PUBLISHED' || queryParam.status ==='DRAFT' || queryParam.status === 'INTIMATE'"
> >
<a <a
href="javascript:void(0);" href="javascript:void(0);"
@ -128,33 +131,68 @@
:loading="postsLoading" :loading="postsLoading"
:pagination="false" :pagination="false"
> >
<!-- ellipsis内嵌a标签后文本会被置空
<ellipsis
:length="25"
tooltip
slot="postTitle"
slot-scope="text,record"
>
{{ text }}
</ellipsis> -->
<span <span
slot="postTitle" slot="postTitle"
slot-scope="text,record" slot-scope="text,record"
class="post-title" style="max-width: 150px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
> >
<a-icon
type="pushpin"
v-if="record.topPriority!=0"
theme="twoTone"
twoToneColor="red"
style="margin-right: 3px;"
/>
<a <a
v-if="record.status=='PUBLISHED'"
:href="options.blog_url+'/archives/'+record.url" :href="options.blog_url+'/archives/'+record.url"
target="_blank" target="_blank"
style="text-decoration: none;"
> >
<a-tooltip placement="topLeft" :title="'点击预览 '+text">{{ text }}</a-tooltip> <a-tooltip
placement="top"
:title="'点击访问【'+text+'】'"
>{{ text }}</a-tooltip>
</a>
<a
v-else-if="record.status == 'INTIMATE'"
:href="options.blog_url+'/archives/'+record.url+'/password'"
target="_blank"
style="text-decoration: none;"
>
<a-tooltip
placement="top"
:title="'点击访问【'+text+'】'"
>{{ text }}</a-tooltip>
</a>
<a
v-else-if="record.status=='DRAFT'"
href="javascript:void(0)"
style="text-decoration: none;"
@click="handlePreview(record.id)"
>
<a-tooltip
placement="topLeft"
:title="'点击预览【'+text+'】'"
>{{ text }}</a-tooltip>
</a>
<a
v-else
href="javascript:void(0);"
style="text-decoration: none;"
disabled
>
{{ text }}
</a> </a>
</span> </span>
<span <span
slot="status" slot="status"
slot-scope="statusProperty" slot-scope="statusProperty"
> >
<a-badge :status="statusProperty.status" /> <a-badge
{{ statusProperty.text }} :status="statusProperty.status"
:text="statusProperty.text"
/>
</span> </span>
<span <span
@ -165,6 +203,7 @@
v-for="(category,index) in categoriesOfPost" v-for="(category,index) in categoriesOfPost"
:key="index" :key="index"
color="blue" color="blue"
style="margin-bottom: 8px"
>{{ category.name }}</a-tag> >{{ category.name }}</a-tag>
</span> </span>
@ -176,13 +215,45 @@
v-for="(tag, index) in tags" v-for="(tag, index) in tags"
:key="index" :key="index"
color="green" color="green"
style="margin-bottom: 8px"
>{{ tag.name }}</a-tag> >{{ tag.name }}</a-tag>
</span> </span>
<span
slot="commentCount"
slot-scope="commentCount"
>
<a-badge
:count="commentCount"
:numberStyle="{backgroundColor: '#f38181'} "
:showZero="true"
:overflowCount="999"
/>
</span>
<span
slot="visits"
slot-scope="visits"
>
<a-badge
:count="visits"
:numberStyle="{backgroundColor: '#00e0ff'} "
:showZero="true"
:overflowCount="9999"
/>
</span>
<span <span
slot="createTime" slot="createTime"
slot-scope="createTime" slot-scope="createTime"
>{{ createTime | timeAgo }}</span> >
<a-tooltip placement="top">
<template slot="title">
{{ createTime | moment }}
</template>
{{ createTime | timeAgo }}
</a-tooltip>
</span>
<span <span
slot="action" slot="action"
@ -191,7 +262,7 @@
<a <a
href="javascript:;" href="javascript:;"
@click="handleEditClick(post)" @click="handleEditClick(post)"
v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT'" v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT' || post.status === 'INTIMATE'"
>编辑</a> >编辑</a>
<a-popconfirm <a-popconfirm
:title="'你确定要发布【' + post.title + '】文章?'" :title="'你确定要发布【' + post.title + '】文章?'"
@ -210,7 +281,7 @@
@confirm="handleEditStatusClick(post.id,'RECYCLE')" @confirm="handleEditStatusClick(post.id,'RECYCLE')"
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT'" v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT' || post.status === 'INTIMATE'"
> >
<a href="javascript:;">回收站</a> <a href="javascript:;">回收站</a>
</a-popconfirm> </a-popconfirm>
@ -229,7 +300,7 @@
<a <a
href="javascript:;" href="javascript:;"
@click="handlePostSettingsDrawer(post)" @click="handleShowPostSettings(post)"
>设置</a> >设置</a>
</span> </span>
</a-table> </a-table>
@ -246,164 +317,32 @@
</div> </div>
</a-card> </a-card>
<a-drawer <PostSetting
title="文章设置" :post="selectedPost"
:width="isMobile()?'100%':'460'" :tagIds="selectedTagIds"
placement="right" :categoryIds="selectedCategoryIds"
closable :needTitle="true"
@close="onPostSettingsClose" :saveDraftButton="false"
:savePublishButton="false"
:saveButton="true"
:visible="postSettingVisible" :visible="postSettingVisible"
> @close="onPostSettingsClose"
<div class="post-setting-drawer-content"> @onRefreshPost="onRefreshPostFromSetting"
<div :style="{ marginBottom: '16px' }"> @onRefreshTagIds="onRefreshTagIdsFromSetting"
<h3 class="post-setting-drawer-title">基本设置</h3> @onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item label="文章标题:">
<a-input v-model="selectedPost.title" />
</a-form-item>
<a-form-item
label="文章路径:"
:help="options.blog_url+'/archives/' + (selectedPost.url ? selectedPost.url : '{auto_generate}')"
>
<a-input v-model="selectedPost.url" />
</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>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">分类目录</h3>
<div class="post-setting-drawer-item">
<category-tree
v-model="selectedCategoryIds"
:categories="categories"
/> />
<div>
<a-form layout="vertical">
<a-form-item v-if="categoryForm">
<category-select-tree
:categories="categories"
v-model="categoryToCreate.parentId"
/>
</a-form-item>
<a-form-item v-if="categoryForm">
<a-input
placeholder="分类名称"
v-model="categoryToCreate.name"
/>
</a-form-item>
<a-form-item v-if="categoryForm">
<a-input
placeholder="分类路径"
v-model="categoryToCreate.slugNames"
/>
</a-form-item>
<a-form-item>
<a-button
type="primary"
style="marginRight: 8px"
v-if="categoryForm"
@click="handlerCreateCategory"
>保存</a-button>
<a-button
type="dashed"
style="marginRight: 8px"
v-if="!categoryForm"
@click="toggleCategoryForm"
>新增</a-button>
<a-button
v-if="categoryForm"
@click="toggleCategoryForm"
>取消</a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">标签</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item>
<TagSelect v-model="selectedTagIds" />
</a-form-item>
</a-form>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">摘要</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<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 :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">缩略图</h3>
<div class="post-setting-drawer-item">
<div class="post-thum">
<img
class="img"
:src="selectedPost.thumbnail || '//i.loli.net/2019/05/05/5ccf007c0a01d.png'"
@click="()=>this.thumDrawerVisible=true"
>
<a-button
class="post-thum-remove"
type="dashed"
@click="handlerRemoveThumb"
>移除</a-button>
</div>
</div>
</div>
<a-divider class="divider-transparent" />
</div>
<AttachmentSelectDrawer
v-model="thumDrawerVisible"
@listenToSelect="handleSelectPostThumb"
:drawerWidth="460"
/>
<div class="bottom-control">
<a-button
@click="handleSavePostSettingsClick"
type="primary"
>保存</a-button>
</div>
</a-drawer>
</div> </div>
</template> </template>
<script> <script>
import categoryApi from '@/api/category'
import postApi from '@/api/post'
import optionApi from '@/api/option'
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import PostSetting from './components/PostSetting'
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer' import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import TagSelect from './components/TagSelect' import TagSelect from './components/TagSelect'
import CategoryTree from './components/CategoryTree' import CategoryTree from './components/CategoryTree'
import { mapGetters } from 'vuex'
import categoryApi from '@/api/category'
import postApi from '@/api/post'
const columns = [ const columns = [
{ {
title: '标题', title: '标题',
@ -415,10 +354,11 @@ const columns = [
title: '状态', title: '状态',
className: 'status', className: 'status',
dataIndex: 'statusProperty', dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' } scopedSlots: { customRender: 'status' }
}, },
{ {
title: '分类目录', title: '分类',
dataIndex: 'categories', dataIndex: 'categories',
scopedSlots: { customRender: 'categories' } scopedSlots: { customRender: 'categories' }
}, },
@ -428,16 +368,21 @@ const columns = [
scopedSlots: { customRender: 'tags' } scopedSlots: { customRender: 'tags' }
}, },
{ {
title: '评论量', title: '评论',
dataIndex: 'commentCount' width: '70px',
dataIndex: 'commentCount',
scopedSlots: { customRender: 'commentCount' }
}, },
{ {
title: '访问量', title: '访问',
dataIndex: 'visits' width: '70px',
dataIndex: 'visits',
scopedSlots: { customRender: 'visits' }
}, },
{ {
title: '发布时间', title: '发布时间',
dataIndex: 'createTime', dataIndex: 'createTime',
width: '170px',
scopedSlots: { customRender: 'createTime' } scopedSlots: { customRender: 'createTime' }
}, },
{ {
@ -451,7 +396,8 @@ export default {
components: { components: {
AttachmentSelectDrawer, AttachmentSelectDrawer,
TagSelect, TagSelect,
CategoryTree CategoryTree,
PostSetting
}, },
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
data() { data() {
@ -478,14 +424,9 @@ export default {
posts: [], posts: [],
postsLoading: false, postsLoading: false,
postSettingVisible: false, postSettingVisible: false,
thumDrawerVisible: false,
selectedPost: {}, selectedPost: {},
selectedCategoryIds: [],
selectedTagIds: [], selectedTagIds: [],
categoryForm: false, selectedCategoryIds: []
categoryToCreate: {},
options: [],
keys: ['blog_url']
} }
}, },
computed: { computed: {
@ -494,12 +435,23 @@ export default {
post.statusProperty = this.postStatus[post.status] post.statusProperty = this.postStatus[post.status]
return post return post
}) })
} },
...mapGetters(['options'])
}, },
created() { created() {
this.loadCategories()
this.loadPosts() this.loadPosts()
this.loadOptions() this.loadCategories()
},
destroyed: function() {
if (this.postSettingVisible) {
this.postSettingVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.postSettingVisible) {
this.postSettingVisible = false
}
next()
}, },
methods: { methods: {
loadPosts() { loadPosts() {
@ -519,11 +471,6 @@ export default {
this.categories = response.data.data this.categories = response.data.data
}) })
}, },
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handleEditClick(post) { handleEditClick(post) {
this.$router.push({ name: 'PostEdit', query: { postId: post.id } }) this.$router.push({ name: 'PostEdit', query: { postId: post.id } })
}, },
@ -553,6 +500,7 @@ export default {
}, },
handleQuery() { handleQuery() {
this.queryParam.page = 0 this.queryParam.page = 0
this.pagination.current = 1
this.loadPosts() this.loadPosts()
}, },
handleEditStatusClick(postId, status) { handleEditStatusClick(postId, status) {
@ -609,61 +557,34 @@ export default {
}) })
} }
}, },
// handleShowPostSettings(post) {
handlePostSettingsDrawer(post) {
this.postSettingVisible = true
postApi.get(post.id).then(response => { postApi.get(post.id).then(response => {
const post = response.data.data this.selectedPost = response.data.data
this.selectedPost = post this.selectedTagIds = this.selectedPost.tagIds
this.selectedTagIds = post.tagIds this.selectedCategoryIds = this.selectedPost.categoryIds
this.selectedCategoryIds = post.categoryIds this.postSettingVisible = true
}) })
}, },
handleSelectPostThumb(data) { handlePreview(postId) {
this.selectedPost.thumbnail = encodeURI(data.path) postApi.preview(postId).then(response => {
this.thumDrawerVisible = false window.open(response.data, '_blank')
},
handlerRemoveThumb() {
this.selectedPost.thumbnail = null
},
//
handleSavePostSettingsClick() {
this.selectedPost.categoryIds = this.selectedCategoryIds
this.selectedPost.tagIds = this.selectedTagIds
postApi.update(this.selectedPost.id, this.selectedPost, false).then(response => {
this.$log.debug('Updated post', response.data.data)
this.loadPosts()
this.$message.success('文章更新成功')
})
},
toggleCategoryForm() {
this.categoryForm = !this.categoryForm
},
handlerCreateCategory() {
categoryApi.create(this.categoryToCreate).then(response => {
this.loadCategories()
this.categoryToCreate = {}
}) })
}, },
// //
onPostSettingsClose() { onPostSettingsClose() {
this.postSettingVisible = false this.postSettingVisible = false
this.selectedPost = {} this.selectedPost = {}
this.selectedTagIds = [] this.loadPosts()
this.selectedCategoryIds = [] },
onRefreshPostFromSetting(post) {
this.selectedPost = post
},
onRefreshTagIdsFromSetting(tagIds) {
this.selectedTagIds = tagIds
},
onRefreshCategoryIdsFromSetting(categoryIds) {
this.selectedCategoryIds = categoryIds
} }
} }
} }
</script> </script>
<style scoped>
a {
text-decoration: none;
}
.post-title {
max-width: 150px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -9,7 +9,7 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card :title="title"> <a-card :title="title" :bodyStyle="{ padding: '16px' }">
<a-form layout="horizontal"> <a-form layout="horizontal">
<a-form-item <a-form-item
label="名称:" label="名称:"
@ -64,7 +64,7 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card title="所有标签"> <a-card title="所有标签" :bodyStyle="{ padding: '16px' }">
<a-tooltip <a-tooltip
placement="topLeft" placement="topLeft"
v-for="tag in tags" v-for="tag in tags"
@ -132,6 +132,13 @@ export default {
}) })
}, },
createOrUpdateTag() { createOrUpdateTag() {
if (!this.tagToCreate.name) {
this.$notification['error']({
message: '提示',
description: '标签名称不能为空!'
})
return
}
if (this.tagToCreate.id) { if (this.tagToCreate.id) {
tagApi.update(this.tagToCreate.id, this.tagToCreate).then(response => { tagApi.update(this.tagToCreate.id, this.tagToCreate).then(response => {
this.$message.success('更新成功!') this.$message.success('更新成功!')

View File

@ -58,6 +58,3 @@ export default {
} }
} }
</script> </script>
<style>
</style>

View File

@ -6,10 +6,6 @@
:checkedKeys="categoryIds" :checkedKeys="categoryIds"
@check="onCheck" @check="onCheck"
> >
<span
slot="title0010"
style="color: #1890ff"
>sss</span>
</a-tree> </a-tree>
</template> </template>
@ -56,6 +52,3 @@ export default {
} }
} }
</script> </script>
<style>
</style>

View File

@ -0,0 +1,435 @@
<template>
<a-drawer
title="文章设置"
:width="isMobile()?'100%':'460'"
placement="right"
closable
destroyOnClose
@close="onClose"
:visible="visible"
>
<a-skeleton
active
:loading="settingLoading"
:paragraph="{ rows: 24 }"
>
<div class="post-setting-drawer-content">
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">基本设置</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item
label="文章标题:"
v-if="needTitle"
>
<a-input v-model="selectedPost.title" />
</a-form-item>
<a-form-item
label="文章路径:"
:help="options.blog_url+'/archives/' + (selectedPost.url ? selectedPost.url : '{auto_generate}')"
>
<a-input v-model="selectedPost.url" />
</a-form-item>
<a-form-item label="访问密码:">
<a-input
v-model="selectedPost.password"
v-if="passwordVisible"
>
<a
href="javascript:void(0);"
slot="addonAfter"
@click="togglePasswordVisible"
>
<a-icon type="eye-invisible" />
</a>
</a-input>
<a-input
type="password"
v-model="selectedPost.password"
v-else
>
<a
href="javascript:void(0);"
slot="addonAfter"
@click="togglePasswordVisible"
>
<a-icon type="eye" />
</a>
</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>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">分类目录</h3>
<div class="post-setting-drawer-item">
<category-tree
v-model="selectedCategoryIds"
:categories="categories"
/>
<div>
<a-form layout="vertical">
<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.slugNames"
/>
</a-form-item>
<a-form-item>
<a-button
type="primary"
style="marginRight: 8px"
v-if="categoryFormVisible"
@click="handlerCreateCategory"
>保存</a-button>
<a-button
type="dashed"
style="marginRight: 8px"
v-if="!categoryFormVisible"
@click="toggleCategoryForm"
>新增</a-button>
<a-button
v-if="categoryFormVisible"
@click="toggleCategoryForm"
>取消</a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">标签</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item>
<TagSelect v-model="selectedTagIds" />
</a-form-item>
</a-form>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">摘要</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<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 :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">缩略图</h3>
<div class="post-setting-drawer-item">
<div class="post-thumb">
<img
class="img"
:src="selectedPost.thumbnail || '//i.loli.net/2019/05/05/5ccf007c0a01d.png'"
@click="()=>this.thumbDrawerVisible=true"
>
<a-button
class="post-thumb-remove"
type="dashed"
@click="handlerRemoveThumb"
>移除</a-button>
</div>
</div>
</div>
<a-divider class="divider-transparent" />
</div>
</a-skeleton>
<AttachmentSelectDrawer
v-model="thumbDrawerVisible"
@listenToSelect="handleSelectPostThumb"
:drawerWidth="460"
/>
<div class="bottom-control">
<a-button
style="marginRight: 8px"
@click="handleDraftClick"
v-if="saveDraftButton"
>保存草稿</a-button>
<a-button
@click="handlePublishClick"
type="primary"
v-if="savePublishButton"
>发布</a-button>
<a-button
@click="handlePublishClick"
type="primary"
v-if="saveButton"
>保存</a-button>
</div>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import moment from 'moment'
import CategoryTree from './CategoryTree'
import CategorySelectTree from './CategorySelectTree'
import TagSelect from './TagSelect'
import AttachmentSelectDrawer from '../../attachment/components/AttachmentSelectDrawer'
import { mapGetters } from 'vuex'
import categoryApi from '@/api/category'
import postApi from '@/api/post'
export default {
name: 'PostSetting',
mixins: [mixin, mixinDevice],
components: {
CategoryTree,
CategorySelectTree,
TagSelect,
AttachmentSelectDrawer
},
data() {
return {
thumbDrawerVisible: false,
categoryFormVisible: false,
settingLoading: true,
passwordVisible: false,
selectedPost: this.post,
selectedTagIds: this.tagIds,
selectedCategoryIds: this.categoryIds,
categories: [],
categoryToCreate: {}
}
},
props: {
post: {
type: Object,
required: true
},
tagIds: {
type: Array,
required: true
},
categoryIds: {
type: Array,
required: true
},
visible: {
type: Boolean,
required: false,
default: true
},
needTitle: {
type: Boolean,
required: false,
default: false
},
saveDraftButton: {
type: Boolean,
required: false,
default: true
},
savePublishButton: {
type: Boolean,
required: false,
default: true
},
saveButton: {
type: Boolean,
required: false,
default: false
}
},
created() {
this.loadSkeleton()
this.loadCategories()
},
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)
},
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
}
}
},
computed: {
pickerDefaultValue() {
if (this.selectedPost.createTime) {
var date = new Date(this.selectedPost.createTime)
return moment(date, 'YYYY-MM-DD HH:mm:ss')
}
return moment(new Date(), 'YYYY-MM-DD HH:mm:ss')
},
...mapGetters(['options'])
},
methods: {
loadSkeleton() {
this.settingLoading = true
setTimeout(() => {
this.settingLoading = false
}, 500)
},
loadCategories() {
categoryApi.listAll().then(response => {
this.categories = response.data.data
})
},
handleSelectPostThumb(data) {
this.selectedPost.thumbnail = encodeURI(data.path)
this.thumbDrawerVisible = false
},
handlerRemoveThumb() {
this.selectedPost.thumbnail = null
},
handlerCreateCategory() {
if (!this.categoryToCreate.name) {
this.$notification['error']({
message: '提示',
description: '分类名称不能为空!'
})
return
}
categoryApi.create(this.categoryToCreate).then(response => {
this.loadCategories()
this.categoryToCreate = {}
this.toggleCategoryForm()
})
},
toggleCategoryForm() {
this.categoryFormVisible = !this.categoryFormVisible
},
handleDraftClick() {
this.selectedPost.status = 'DRAFT'
this.savePost()
},
handlePublishClick() {
this.selectedPost.status = 'PUBLISHED'
this.savePost()
},
savePost() {
this.createOrUpdatePost(
() => this.$message.success('文章发布成功'),
() => this.$message.success('文章发布成功'),
false
)
},
createOrUpdatePost(createSuccess, updateSuccess, autoSave) {
if (!this.selectedPost.title) {
this.$notification['error']({
message: '提示',
description: '文章标题不能为空!'
})
return
}
if (!this.selectedPost.originalContent) {
this.$notification['error']({
message: '提示',
description: '文章内容不能为空!'
})
return
}
// Set category ids
this.selectedPost.categoryIds = this.selectedCategoryIds
// Set tag ids
this.selectedPost.tagIds = this.selectedTagIds
if (this.selectedPost.id) {
// Update the post
postApi.update(this.selectedPost.id, this.selectedPost, autoSave).then(response => {
this.$log.debug('Updated post', response.data.data)
if (updateSuccess) {
updateSuccess()
this.$router.push({ name: 'PostList' })
}
})
} else {
// Create the post
postApi.create(this.selectedPost, autoSave).then(response => {
this.$log.debug('Created post', response.data.data)
if (createSuccess) {
createSuccess()
this.$router.push({ name: 'PostList' })
}
this.selectedPost = response.data.data
})
}
},
togglePasswordVisible() {
this.passwordVisible = !this.passwordVisible
},
onClose() {
this.$emit('close', false)
this.passwordVisible = false
},
onPostDateChange(value, dateString) {
this.selectedPost.createTime = value.valueOf()
},
onPostDateOk(value) {
this.selectedPost.createTime = value.valueOf()
}
}
}
</script>

View File

@ -105,6 +105,3 @@ export default {
} }
} }
</script> </script>
<style>
</style>

View File

@ -11,122 +11,42 @@
/> />
</div> </div>
<div id="editor"> <div id="editor">
<mavon-editor <halo-editor
ref="md" ref="md"
v-model="sheetToStage.originalContent" v-model="sheetToStage.originalContent"
:boxShadow="false" :boxShadow="false"
:toolbars="toolbars" :toolbars="toolbars"
:ishljs="true" :ishljs="true"
:autofocus="false" :autofocus="false"
@imgAdd="pictureUploadHandle" @imgAdd="handleAttachmentUpload"
@keydown.ctrl.83.native="handleSaveDraft"
@keydown.meta.83.native="handleSaveDraft"
/> />
</div> </div>
</a-col> </a-col>
<a-col
:xl="24"
:lg="24"
:md="24"
:sm="24"
:xs="24"
>
<a-drawer
title="页面设置"
:width="isMobile()?'100%':'460'"
:closable="true"
@close="()=>this.sheetSettingVisible = false"
:visible="sheetSettingVisible"
>
<div class="post-setting-drawer-content">
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">基本设置</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item
label="页面路径:"
:help="options.blog_url+'/s/'+ (sheetToStage.url ? sheetToStage.url : '{auto_generate}')"
>
<a-input v-model="sheetToStage.url" />
</a-form-item>
<a-form-item label="发表时间:">
<a-date-picker
showTime
:defaultValue="pickerDefaultValue"
format="YYYY-MM-DD HH:mm:ss"
placeholder="Select Publish Time"
@change="onChange"
@ok="onOk"
/>
</a-form-item>
<a-form-item label="开启评论:">
<a-radio-group
v-model="sheetToStage.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-select v-model="sheetToStage.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 :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">缩略图</h3>
<div class="post-setting-drawer-item">
<div class="sheet-thum">
<img
class="img"
:src="sheetToStage.thumbnail || '//i.loli.net/2019/05/05/5ccf007c0a01d.png'"
@click="()=>this.thumDrawerVisible = true"
>
<a-button
class="sheet-thum-remove"
type="dashed"
@click="handlerRemoveThumb"
>移除</a-button>
</div>
</div>
</div>
<a-divider class="divider-transparent" />
</div>
<AttachmentSelectDrawer
v-model="thumDrawerVisible"
@listenToSelect="handleSelectSheetThumb"
:drawerWidth="460"
/>
<div class="bottom-control">
<a-button
style="marginRight: 8px"
@click="handleDraftClick"
>保存草稿</a-button>
<a-button
type="primary"
@click="handlePublishClick"
>发布</a-button>
</div>
</a-drawer>
</a-col>
</a-row> </a-row>
<SheetSetting
:sheet="sheetToStage"
:visible="sheetSettingVisible"
@close="onSheetSettingsClose"
@onRefreshSheet="onRefreshSheetFromSetting"
/>
<AttachmentDrawer v-model="attachmentDrawerVisible" /> <AttachmentDrawer v-model="attachmentDrawerVisible" />
<footer-tool-bar :style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}"> <footer-tool-bar :style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}">
<a-button
type="danger"
@click="handleSaveDraft"
>保存草稿</a-button>
<a-button
@click="handlePreview"
style="margin-left: 8px;"
>预览</a-button>
<a-button <a-button
type="primary" type="primary"
@click="()=>this.sheetSettingVisible = true" style="margin-left: 8px;"
@click="handleShowSheetSetting"
>发布</a-button> >发布</a-button>
<a-button <a-button
type="dashed" type="dashed"
@ -138,24 +58,23 @@
</template> </template>
<script> <script>
import { mavonEditor } from 'mavon-editor'
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import { toolbars } from '@/core/const' import { mapGetters } from 'vuex'
import 'mavon-editor/dist/css/index.css'
import sheetApi from '@/api/sheet'
import themeApi from '@/api/theme'
import optionApi from '@/api/option'
import attachmentApi from '@/api/attachment'
import moment from 'moment' import moment from 'moment'
import { toolbars } from '@/core/const'
import SheetSetting from './components/SheetSetting'
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import { haloEditor } from 'halo-editor'
import 'halo-editor/dist/css/index.css'
import sheetApi from '@/api/sheet'
import attachmentApi from '@/api/attachment'
export default { export default {
components: { components: {
mavonEditor, haloEditor,
FooterToolBar, FooterToolBar,
AttachmentDrawer, AttachmentDrawer,
AttachmentSelectDrawer SheetSetting
}, },
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
data() { data() {
@ -167,34 +86,10 @@ export default {
xs: { span: 24 } xs: { span: 24 }
}, },
attachmentDrawerVisible: false, attachmentDrawerVisible: false,
thumDrawerVisible: false,
sheetSettingVisible: false, sheetSettingVisible: false,
customTpls: [], sheetToStage: {}
sheetToStage: {},
timer: null,
options: [],
keys: ['blog_url']
} }
}, },
created() {
this.loadCustomTpls()
this.loadOptions()
clearInterval(this.timer)
this.timer = null
this.autoSaveTimer()
},
destroyed: function() {
clearInterval(this.timer)
this.timer = null
},
beforeRouteLeave(to, from, next) {
if (this.timer !== null) {
clearInterval(this.timer)
}
// Auto save the sheet
this.autoSaveSheet()
next()
},
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
// Get sheetId id from query // Get sheetId id from query
const sheetId = to.query.sheetId const sheetId = to.query.sheetId
@ -208,116 +103,97 @@ export default {
} }
}) })
}, },
destroyed: function() {
if (this.sheetSettingVisible) {
this.sheetSettingVisible = false
}
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.sheetSettingVisible) {
this.sheetSettingVisible = false
}
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
next()
},
computed: { computed: {
pickerDefaultValue() { ...mapGetters(['options'])
if (this.sheetToStage.createTime) {
var date = new Date(this.sheetToStage.createTime)
return moment(date, 'YYYY-MM-DD HH:mm:ss')
}
return moment(new Date(), 'YYYY-MM-DD HH:mm:ss')
}
}, },
methods: { methods: {
loadCustomTpls() { handleSaveDraft() {
themeApi.customTpls().then(response => {
this.customTpls = response.data.data
})
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handlePublishClick() {
this.sheetToStage.status = 'PUBLISHED'
this.saveSheet()
},
handleDraftClick() {
this.sheetToStage.status = 'DRAFT' this.sheetToStage.status = 'DRAFT'
this.saveSheet() if (!this.sheetToStage.title) {
}, this.sheetToStage.title = moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')
handlerRemoveThumb() {
this.sheetToStage.thumbnail = null
},
createOrUpdateSheet(createSuccess, updateSuccess, autoSave) {
if (this.sheetToStage.id) {
sheetApi.update(this.sheetToStage.id, this.sheetToStage, autoSave).then(response => {
this.$log.debug('Updated sheet', response.data.data)
if (updateSuccess) {
updateSuccess()
} }
if (!this.sheetToStage.originalContent) {
this.sheetToStage.originalContent = '开始编辑...'
}
if (this.sheetToStage.id) {
sheetApi.update(this.sheetToStage.id, this.sheetToStage, false).then(response => {
this.$log.debug('Updated sheet', response.data.data)
this.$message.success('保存草稿成功!')
}) })
} else { } else {
sheetApi.create(this.sheetToStage, autoSave).then(response => { sheetApi.create(this.sheetToStage, false).then(response => {
this.$log.debug('Created sheet', response.data.data) this.$log.debug('Created sheet', response.data.data)
if (createSuccess) { this.$message.success('保存草稿成功!')
createSuccess()
}
this.sheetToStage = response.data.data this.sheetToStage = response.data.data
}) })
} }
}, },
saveSheet() { handleAttachmentUpload(pos, $file) {
this.createOrUpdateSheet(
() => this.$message.success('页面创建成功!'),
() => this.$message.success('页面更新成功!'),
false
)
},
autoSaveSheet() {
if (this.sheetToStage.title != null && this.sheetToStage.originalContent != null) {
this.createOrUpdateSheet(null, null, true)
}
},
handleSelectSheetThumb(data) {
this.sheetToStage.thumbnail = encodeURI(data.path)
this.thumDrawerVisible = false
},
autoSaveTimer() {
if (this.timer == null) {
this.timer = setInterval(() => {
this.autoSaveSheet()
}, 15000)
}
},
pictureUploadHandle(pos, $file) {
var formdata = new FormData() var formdata = new FormData()
formdata.append('file', $file) formdata.append('file', $file)
attachmentApi.upload(formdata).then(response => { attachmentApi.upload(formdata).then(response => {
var responseObject = response.data var responseObject = response.data
if (responseObject.status === 200) { if (responseObject.status === 200) {
var MavonEditor = this.$refs.md var HaloEditor = this.$refs.md
MavonEditor.$img2Url(pos, encodeURI(responseObject.data.path)) HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path))
this.$message.success('图片上传成功') this.$message.success('图片上传成功!')
} else { } else {
this.$message.error('图片上传失败:' + responseObject.message) this.$message.error('图片上传失败:' + responseObject.message)
} }
}) })
}, },
onChange(value, dateString) { handleShowSheetSetting() {
this.sheetToStage.createTime = value.valueOf() this.sheetSettingVisible = true
}, },
onOk(value) { handlePreview() {
this.sheetToStage.createTime = value.valueOf() this.sheetToStage.status = 'DRAFT'
if (!this.sheetToStage.title) {
this.sheetToStage.title = moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')
}
if (!this.sheetToStage.originalContent) {
this.sheetToStage.originalContent = '开始编辑...'
}
if (this.sheetToStage.id) {
sheetApi.update(this.sheetToStage.id, this.sheetToStage, false).then(response => {
this.$log.debug('Updated sheet', response.data.data)
sheetApi.preview(this.sheetToStage.id).then(response => {
window.open(response.data, '_blank')
})
})
} else {
sheetApi.create(this.sheetToStage, false).then(response => {
this.$log.debug('Created sheet', response.data.data)
this.sheetToStage = response.data.data
sheetApi.preview(this.sheetToStage.id).then(response => {
window.open(response.data, '_blank')
})
})
}
},
onSheetSettingsClose() {
this.sheetSettingVisible = false
},
onRefreshSheetFromSetting(sheet) {
this.sheetToStage = sheet
} }
} }
} }
</script> </script>
<style lang="less" scoped>
.v-note-wrapper {
z-index: 1000;
min-height: 580px;
}
.sheet-thum {
.img {
width: 100%;
cursor: pointer;
border-radius: 4px;
}
.sheet-thum-remove {
margin-top: 16px;
}
}
</style>

View File

@ -9,26 +9,6 @@
<a-icon type="pushpin" />内置页面 <a-icon type="pushpin" />内置页面
</span> </span>
<!-- TODO 移动端展示 -->
<!-- <a-collapse
:bordered="false"
v-if="isMobile()"
>
<a-collapse-panel
v-for="(item,index) in internalSheets"
:key="index"
>
<a
href="javascript:void(0);"
slot="header"
> {{ item.name }} </a>
<div>
访问路径{{ item.url }}
操作{{ item.url }}
</div>
</a-collapse-panel>
</a-collapse> -->
<a-table <a-table
:columns="internalColumns" :columns="internalColumns"
:dataSource="internalSheets" :dataSource="internalSheets"
@ -95,33 +75,90 @@
:columns="customColumns" :columns="customColumns"
:dataSource="formattedSheets" :dataSource="formattedSheets"
:pagination="false" :pagination="false"
:loading="sheetsLoading"
> >
<span <span
slot="sheetTitle" slot="sheetTitle"
slot-scope="text,record" slot-scope="text,record"
class="sheet-title" style="max-width: 150px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
> >
<a <a
v-if="record.status=='PUBLISHED'"
:href="options.blog_url+'/s/'+record.url" :href="options.blog_url+'/s/'+record.url"
target="_blank" target="_blank"
style="text-decoration: none;"
>
<a-tooltip
placement="top"
:title="'点击访问【'+text+'】'"
>{{ text }}</a-tooltip>
</a>
<a
v-else-if="record.status=='DRAFT'"
href="javascript:void(0)"
style="text-decoration: none;"
@click="handlePreview(record.id)"
> >
<a-tooltip <a-tooltip
placement="topLeft" placement="topLeft"
:title="'点击预览 '+text" :title="'点击预览【'+text+'】'"
>{{ text }}</a-tooltip> >{{ text }}</a-tooltip>
</a> </a>
<a
v-else
href="javascript:void(0);"
style="text-decoration: none;"
disabled
>
{{ text }}
</a>
</span> </span>
<span <span
slot="status" slot="status"
slot-scope="statusProperty" slot-scope="statusProperty"
> >
<a-badge :status="statusProperty.status" /> <a-badge
{{ statusProperty.text }} :status="statusProperty.status"
:text="statusProperty.text"
/>
</span> </span>
<span
slot="commentCount"
slot-scope="commentCount"
>
<a-badge
:count="commentCount"
:numberStyle="{backgroundColor: '#f38181'} "
:showZero="true"
:overflowCount="999"
/>
</span>
<span
slot="visits"
slot-scope="visits"
>
<a-badge
:count="visits"
:numberStyle="{backgroundColor: '#00e0ff'} "
:showZero="true"
:overflowCount="9999"
/>
</span>
<span <span
slot="createTime" slot="createTime"
slot-scope="createTime" slot-scope="createTime"
>{{ createTime | timeAgo }}</span> >
<a-tooltip placement="top">
<template slot="title">
{{ createTime | moment }}
</template>
{{ createTime | timeAgo }}
</a-tooltip>
</span>
<span <span
slot="action" slot="action"
@ -172,6 +209,12 @@
>更多</a> >更多</a>
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item key="1"> <a-menu-item key="1">
<a
href="javascript:void(0);"
@click="handleShowSheetSettings(sheet)"
>设置</a>
</a-menu-item>
<a-menu-item key="2">
<a-popconfirm <a-popconfirm
:title="'你确定要添加【' + sheet.title + '】到菜单?'" :title="'你确定要添加【' + sheet.title + '】到菜单?'"
@confirm="handleSheetToMenu(sheet)" @confirm="handleSheetToMenu(sheet)"
@ -190,13 +233,23 @@
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
<SheetSetting
:sheet="selectedSheet"
:visible="sheetSettingVisible"
:needTitle="true"
@close="onSheetSettingsClose"
@onRefreshSheet="onRefreshSheetFromSetting"
/>
</div> </div>
</template> </template>
<script> <script>
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import SheetSetting from './components/SheetSetting'
import sheetApi from '@/api/sheet' import sheetApi from '@/api/sheet'
import optionApi from '@/api/option'
import menuApi from '@/api/menu' import menuApi from '@/api/menu'
const internalColumns = [ const internalColumns = [
@ -234,11 +287,13 @@ const customColumns = [
}, },
{ {
title: '评论量', title: '评论量',
dataIndex: 'commentCount' dataIndex: 'commentCount',
scopedSlots: { customRender: 'commentCount' }
}, },
{ {
title: '访问量', title: '访问量',
dataIndex: 'visits' dataIndex: 'visits',
scopedSlots: { customRender: 'visits' }
}, },
{ {
title: '发布时间', title: '发布时间',
@ -253,16 +308,20 @@ const customColumns = [
] ]
export default { export default {
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
components: {
SheetSetting
},
data() { data() {
return { return {
sheetsLoading: false,
sheetStatus: sheetApi.sheetStatus, sheetStatus: sheetApi.sheetStatus,
internalColumns, internalColumns,
customColumns, customColumns,
selectedSheet: {},
sheetSettingVisible: false,
internalSheets: [], internalSheets: [],
sheets: [], sheets: [],
options: [], menu: {}
menu: {},
keys: ['blog_url']
} }
}, },
computed: { computed: {
@ -271,17 +330,30 @@ export default {
sheet.statusProperty = this.sheetStatus[sheet.status] sheet.statusProperty = this.sheetStatus[sheet.status]
return sheet return sheet
}) })
} },
...mapGetters(['options'])
}, },
created() { created() {
this.loadSheets() this.loadSheets()
this.loadInternalSheets() this.loadInternalSheets()
this.loadOptions() },
destroyed: function() {
if (this.sheetSettingVisible) {
this.sheetSettingVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.sheetSettingVisible) {
this.sheetSettingVisible = false
}
next()
}, },
methods: { methods: {
loadSheets() { loadSheets() {
this.sheetsLoading = true
sheetApi.list().then(response => { sheetApi.list().then(response => {
this.sheets = response.data.data.content this.sheets = response.data.data.content
this.sheetsLoading = false
}) })
}, },
loadInternalSheets() { loadInternalSheets() {
@ -289,11 +361,6 @@ export default {
this.internalSheets = response.data.data this.internalSheets = response.data.data
}) })
}, },
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handleEditClick(sheet) { handleEditClick(sheet) {
this.$router.push({ name: 'SheetEdit', query: { sheetId: sheet.id } }) this.$router.push({ name: 'SheetEdit', query: { sheetId: sheet.id } })
}, },
@ -316,19 +383,26 @@ export default {
this.$message.success('添加到菜单成功!') this.$message.success('添加到菜单成功!')
this.menu = {} this.menu = {}
}) })
},
handleShowSheetSettings(sheet) {
sheetApi.get(sheet.id).then(response => {
this.selectedSheet = response.data.data
this.sheetSettingVisible = true
})
},
handlePreview(sheetId) {
sheetApi.preview(sheetId).then(response => {
window.open(response.data, '_blank')
})
},
onSheetSettingsClose() {
this.sheetSettingVisible = false
this.selectedSheet = {}
this.loadSheets()
},
onRefreshSheetFromSetting(sheet) {
this.selectedSheet = sheet
} }
} }
} }
</script> </script>
<style scoped>
a {
text-decoration: none;
}
.sheet-title {
max-width: 300px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,248 @@
<template>
<a-drawer
title="页面设置"
:width="isMobile()?'100%':'460'"
placement="right"
closable
destroyOnClose
@close="onClose"
:visible="visible"
>
<a-skeleton
active
:loading="settingLoading"
:paragraph="{ rows: 18 }"
>
<div class="post-setting-drawer-content">
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">基本设置</h3>
<div class="post-setting-drawer-item">
<a-form layout="vertical">
<a-form-item
label="页面标题:"
v-if="needTitle"
>
<a-input v-model="selectedSheet.title" />
</a-form-item>
<a-form-item
label="页面路径:"
:help="options.blog_url+'/s/'+ (selectedSheet.url ? selectedSheet.url : '{auto_generate}')"
>
<a-input v-model="selectedSheet.url" />
</a-form-item>
<a-form-item label="发表时间:">
<a-date-picker
showTime
:defaultValue="pickerDefaultValue"
format="YYYY-MM-DD HH:mm:ss"
placeholder="选择页面发表时间"
@change="onSheetDateChange"
@ok="onSheetDateOk"
/>
</a-form-item>
<a-form-item label="开启评论:">
<a-radio-group
v-model="selectedSheet.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-select v-model="selectedSheet.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 :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">缩略图</h3>
<div class="post-setting-drawer-item">
<div class="sheet-thumb">
<img
class="img"
:src="selectedSheet.thumbnail || '//i.loli.net/2019/05/05/5ccf007c0a01d.png'"
@click="()=>this.thumbDrawerVisible = true"
>
<a-button
class="sheet-thumb-remove"
type="dashed"
@click="handlerRemoveThumb"
>移除</a-button>
</div>
</div>
</div>
<a-divider class="divider-transparent" />
</div>
</a-skeleton>
<AttachmentSelectDrawer
v-model="thumbDrawerVisible"
@listenToSelect="handleSelectSheetThumb"
:drawerWidth="460"
/>
<div class="bottom-control">
<a-button
style="marginRight: 8px"
@click="handleDraftClick"
>保存草稿</a-button>
<a-button
type="primary"
@click="handlePublishClick"
>发布</a-button>
</div>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import moment from 'moment'
import AttachmentSelectDrawer from '../../attachment/components/AttachmentSelectDrawer'
import { mapGetters } from 'vuex'
import themeApi from '@/api/theme'
import sheetApi from '@/api/sheet'
export default {
name: 'SheetSetting',
mixins: [mixin, mixinDevice],
components: {
AttachmentSelectDrawer
},
data() {
return {
thumbDrawerVisible: false,
settingLoading: true,
selectedSheet: this.sheet,
customTpls: []
}
},
props: {
sheet: {
type: Object,
required: true
},
needTitle: {
type: Boolean,
required: false,
default: false
},
visible: {
type: Boolean,
required: false,
default: true
}
},
created() {
this.loadSkeleton()
this.loadCustomTpls()
},
watch: {
sheet(val) {
this.selectedSheet = val
},
selectedSheet(val) {
this.$emit('onRefreshSheet', val)
},
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
}
}
},
computed: {
pickerDefaultValue() {
if (this.selectedSheet.createTime) {
var date = new Date(this.selectedSheet.createTime)
return moment(date, 'YYYY-MM-DD HH:mm:ss')
}
return moment(new Date(), 'YYYY-MM-DD HH:mm:ss')
},
...mapGetters(['options'])
},
methods: {
loadSkeleton() {
this.settingLoading = true
setTimeout(() => {
this.settingLoading = false
}, 500)
},
loadCustomTpls() {
themeApi.customTpls().then(response => {
this.customTpls = response.data.data
})
},
handleSelectSheetThumb(data) {
this.selectedSheet.thumbnail = encodeURI(data.path)
this.thumbDrawerVisible = false
},
handlerRemoveThumb() {
this.selectedSheet.thumbnail = null
},
handlePublishClick() {
this.selectedSheet.status = 'PUBLISHED'
this.saveSheet()
},
handleDraftClick() {
this.selectedSheet.status = 'DRAFT'
this.saveSheet()
},
saveSheet() {
this.createOrUpdateSheet(
() => this.$message.success('页面发布成功!'),
() => this.$message.success('页面发布成功!'),
false
)
},
createOrUpdateSheet(createSuccess, updateSuccess, autoSave) {
if (!this.selectedSheet.title) {
this.$notification['error']({
message: '提示',
description: '页面标题不能为空!'
})
return
}
if (!this.selectedSheet.originalContent) {
this.$notification['error']({
message: '提示',
description: '页面内容不能为空!'
})
return
}
if (this.selectedSheet.id) {
sheetApi.update(this.selectedSheet.id, this.selectedSheet, autoSave).then(response => {
this.$log.debug('Updated sheet', response.data.data)
if (updateSuccess) {
updateSuccess()
}
})
} else {
sheetApi.create(this.selectedSheet, autoSave).then(response => {
this.$log.debug('Created sheet', response.data.data)
if (createSuccess) {
createSuccess()
}
this.selectedSheet = response.data.data
})
}
},
onClose() {
this.$emit('close', false)
},
onSheetDateChange(value, dateString) {
this.selectedSheet.createTime = value.valueOf()
},
onSheetDateOk(value) {
this.selectedSheet.createTime = value.valueOf()
}
}
}
</script>

View File

@ -2,34 +2,63 @@
<div class="page-header-index-wide"> <div class="page-header-index-wide">
<a-row> <a-row>
<a-col :span="24"> <a-col :span="24">
<a-card :bordered="false"> <a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="table-page-search-wrapper"> <div class="table-page-search-wrapper">
<a-form layout="inline"> <a-form layout="inline">
<a-row :gutter="48"> <a-row :gutter="48">
<a-col :md="6" :sm="24"> <a-col
:md="6"
:sm="24"
>
<a-form-item label="关键词"> <a-form-item label="关键词">
<a-input v-model="queryParam.keyword" /> <a-input v-model="queryParam.keyword" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col
:md="6"
:sm="24"
>
<a-form-item label="状态"> <a-form-item label="状态">
<a-select placeholder="请选择状态"> <a-select
<a-select-option value="1">公开</a-select-option> placeholder="请选择状态"
<a-select-option value="0">私密</a-select-option> v-model="queryParam.type"
@change="loadJournals(true)"
>
<a-select-option
v-for="type in Object.keys(journalType)"
:key="type"
:value="type"
>{{ journalType[type].text }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col
:md="6"
:sm="24"
>
<span class="table-page-search-submitButtons"> <span class="table-page-search-submitButtons">
<a-button type="primary" @click="loadJournals(true)"></a-button> <a-button
<a-button style="margin-left: 8px;" @click="resetParam"></a-button> type="primary"
@click="loadJournals(true)"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="resetParam"
>重置</a-button>
</span> </span>
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
</div> </div>
<div class="table-operator"> <div class="table-operator">
<a-button type="primary" icon="plus" @click="handleNew"></a-button> <a-button
type="primary"
icon="plus"
@click="handleNew"
>写日志</a-button>
</div> </div>
<a-divider /> <a-divider />
<div style="margin-top:15px"> <div style="margin-top:15px">
@ -39,7 +68,11 @@
:dataSource="journals" :dataSource="journals"
:loading="listLoading" :loading="listLoading"
> >
<a-list-item slot="renderItem" slot-scope="item, index" :key="index"> <a-list-item
slot="renderItem"
slot-scope="item, index"
:key="index"
>
<!-- 日志图片集合 --> <!-- 日志图片集合 -->
<!-- <a-card <!-- <a-card
hoverable hoverable
@ -51,33 +84,62 @@
<img alt="example" :src="photo.thumbnail" slot="cover"> <img alt="example" :src="photo.thumbnail" slot="cover">
</a-card> --> </a-card> -->
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancelPreview"> <!-- <a-modal
:visible="previewVisible"
:footer="null"
@cancel="handleCancelPreview"
>
<img <img
:alt="previewPhoto.name + previewPhoto.description" :alt="previewPhoto.name + previewPhoto.description"
style="width: 100%" style="width: 100%"
:src="previewPhoto.url" :src="previewPhoto.url"
> >
</a-modal> </a-modal> -->
<template slot="actions"> <template slot="actions">
<span> <span>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<a-icon type="like-o" style="margin-right: 8px"/> <a-icon
type="like-o"
style="margin-right: 8px"
/>
{{ item.likes }} {{ item.likes }}
</a> </a>
</span> </span>
<span> <span>
<a href="javascript:void(0);" @click="handleCommentShow(item)"> <a
<a-icon type="message" style="margin-right: 8px"/> href="javascript:void(0);"
@click="handleCommentShow(item)"
>
<a-icon
type="message"
style="margin-right: 8px"
/>
{{ item.commentCount }} {{ item.commentCount }}
</a> </a>
</span> </span>
<span v-if="item.type=='INTIMATE'">
<a
href="javascript:void(0);"
disabled
>
<a-icon type="lock" />
</a>
</span>
<span v-else>
<a href="javascript:void(0);">
<a-icon type="unlock" />
</a>
</span>
<!-- <span> <!-- <span>
From 微信 From 微信
</span>--> </span>-->
</template> </template>
<template slot="extra"> <template slot="extra">
<a href="javascript:void(0);" @click="handleEdit(item)"></a> <a
href="javascript:void(0);"
@click="handleEdit(item)"
>编辑</a>
<a-divider type="vertical" /> <a-divider type="vertical" />
<a-popconfirm <a-popconfirm
title="你确定要删除这条日志?" title="你确定要删除这条日志?"
@ -91,7 +153,11 @@
<a-list-item-meta :description="item.content"> <a-list-item-meta :description="item.content">
<span slot="title">{{ item.createTime | moment }}</span> <span slot="title">{{ item.createTime | moment }}</span>
<a-avatar slot="avatar" size="large" :src="user.avatar"/> <a-avatar
slot="avatar"
size="large"
:src="user.avatar"
/>
</a-list-item-meta> </a-list-item-meta>
</a-list-item> </a-list-item>
<div class="page-wrapper"> <div class="page-wrapper">
@ -115,16 +181,35 @@
<a-modal v-model="visible"> <a-modal v-model="visible">
<template slot="title"> <template slot="title">
{{ title }} {{ title }}
<a-tooltip slot="action" title="只能输入250字"> <a-tooltip
slot="action"
title="只能输入250字"
>
<a-icon type="info-circle-o" /> <a-icon type="info-circle-o" />
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="footer"> <template slot="footer">
<a-button key="submit" type="primary" @click="createOrUpdateJournal"></a-button> <a-button
key="submit"
type="primary"
@click="createOrUpdateJournal"
>发布</a-button>
</template> </template>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item> <a-form-item>
<a-input type="textarea" :autosize="{ minRows: 8 }" v-model="journal.content"/> <a-input
type="textarea"
:autosize="{ minRows: 8 }"
v-model="journal.content"
/>
</a-form-item>
<a-form-item>
<a-switch
checkedChildren="公开"
unCheckedChildren="私密"
v-model="isPublic"
defaultChecked
/>
</a-form-item> </a-form-item>
<!-- <a-form-item v-show="showMoreOptions"> <!-- <a-form-item v-show="showMoreOptions">
<UploadPhoto <UploadPhoto
@ -154,11 +239,19 @@
v-model="selectCommentVisible" v-model="selectCommentVisible"
> >
<template slot="footer"> <template slot="footer">
<a-button key="submit" type="primary" @click="handleReplyComment"></a-button> <a-button
key="submit"
type="primary"
@click="handleReplyComment"
>回复</a-button>
</template> </template>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item> <a-form-item>
<a-input type="textarea" :autosize="{ minRows: 8 }" v-model="replyComment.content"/> <a-input
type="textarea"
:autosize="{ minRows: 8 }"
v-model="replyComment.content"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@ -168,14 +261,21 @@
title="评论列表" title="评论列表"
:width="isMobile()?'100%':'460'" :width="isMobile()?'100%':'460'"
closable closable
:visible="commentVisiable" :visible="commentVisible"
destroyOnClose destroyOnClose
@close="()=>this.commentVisiable = false" @close="()=>this.commentVisible = false"
>
<a-row
type="flex"
align="middle"
> >
<a-row type="flex" align="middle">
<a-col :span="24"> <a-col :span="24">
<a-comment> <a-comment>
<a-avatar :src="user.avatar" :alt="user.nickname" slot="avatar"/> <a-avatar
:src="user.avatar"
:alt="user.nickname"
slot="avatar"
/>
<p slot="content">{{ journal.content }}</p> <p slot="content">{{ journal.content }}</p>
<span slot="datetime">{{ journal.createTime | moment }}</span> <span slot="datetime">{{ journal.createTime | moment }}</span>
@ -199,29 +299,30 @@
<script> <script>
import JournalCommentTree from './components/JournalCommentTree' import JournalCommentTree from './components/JournalCommentTree'
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import journalApi from '@/api/journal' import journalApi from '@/api/journal'
import journalCommentApi from '@/api/journalComment' import journalCommentApi from '@/api/journalComment'
import userApi from '@/api/user'
import UploadPhoto from '@/components/Upload/UploadPhoto.vue' import UploadPhoto from '@/components/Upload/UploadPhoto.vue'
export default { export default {
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
components: { JournalCommentTree, UploadPhoto }, components: { JournalCommentTree, UploadPhoto },
data() { data() {
return { return {
plusPhotoVisible: true, journalType: journalApi.journalType,
photoList: [], // // plusPhotoVisible: true,
previewVisible: false, // photoList: [], //
// previewVisible: false,
showMoreOptions: false, showMoreOptions: false,
previewPhoto: { // previewPhoto: {
// // //
name: '', // name: '',
description: '', // description: '',
url: '' // url: ''
}, // },
title: '发表', title: '发表',
listLoading: false, listLoading: false,
visible: false, visible: false,
commentVisiable: false, commentVisible: false,
selectCommentVisible: false, selectCommentVisible: false,
pagination: { pagination: {
page: 1, page: 1,
@ -232,50 +333,49 @@ export default {
page: 0, page: 0,
size: 10, size: 10,
sort: null, sort: null,
keyword: null keyword: null,
type: null
}, },
journals: [], journals: [],
comments: [], comments: [],
journal: { journal: {},
id: undefined, isPublic: true,
content: '',
photos: []
},
journalPhotos: [], // journalPhotos: [], //
selectComment: null, selectComment: null,
replyComment: {}, replyComment: {}
user: {}
} }
}, },
created() { created() {
this.loadJournals() this.loadJournals()
this.loadUser() },
computed: {
...mapGetters(['user'])
}, },
methods: { methods: {
handleCancelPreview() { // handleCancelPreview() {
this.previewVisible = false // this.previewVisible = false
}, // },
handlerPhotoPreview(photo) { // handlerPhotoPreview(photo) {
// // //
this.previewVisible = true // this.previewVisible = true
this.previewPhoto = photo // this.previewPhoto = photo
}, // },
handlerPhotoUploadSuccess(response, file) { // handlerPhotoUploadSuccess(response, file) {
var callData = response.data.data // var callData = response.data.data
var photo = { // var photo = {
name: callData.name, // name: callData.name,
url: callData.path, // url: callData.path,
thumbnail: callData.thumbPath, // thumbnail: callData.thumbPath,
suffix: callData.suffix, // suffix: callData.suffix,
width: callData.width, // width: callData.width,
height: callData.height // height: callData.height
} // }
this.journalPhotos.push(photo) // this.journalPhotos.push(photo)
}, // },
handleUploadPhotoWallClick() { // handleUploadPhotoWallClick() {
// // //
this.showMoreOptions = !this.showMoreOptions // this.showMoreOptions = !this.showMoreOptions
}, // },
loadJournals(isSearch) { loadJournals(isSearch) {
this.queryParam.page = this.pagination.page - 1 this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size this.queryParam.size = this.pagination.size
@ -290,28 +390,23 @@ export default {
this.listLoading = false this.listLoading = false
}) })
}, },
loadUser() {
userApi.getProfile().then(response => {
this.user = response.data.data
})
},
handleNew() { handleNew() {
this.title = '新建' this.title = '新建'
this.visible = true this.visible = true
this.journal = {} this.journal = {}
// //
this.plusPhotoVisible = true // this.plusPhotoVisible = true
this.photoList = [] // this.photoList = []
}, },
handleEdit(item) { handleEdit(item) {
this.title = '编辑' this.title = '编辑'
this.journal = item this.journal = item
this.isPublic = item.type !== 'INTIMATE'
this.visible = true this.visible = true
// , // ,
this.plusPhotoVisible = false // this.plusPhotoVisible = false
this.photoList = item.photos // this.photoList = item.photos
}, },
handleDelete(id) { handleDelete(id) {
journalApi.delete(id).then(response => { journalApi.delete(id).then(response => {
@ -323,7 +418,7 @@ export default {
this.journal = journal this.journal = journal
journalApi.commentTree(this.journal.id).then(response => { journalApi.commentTree(this.journal.id).then(response => {
this.comments = response.data.data.content this.comments = response.data.data.content
this.commentVisiable = true this.commentVisible = true
}) })
}, },
handleCommentReplyClick(comment) { handleCommentReplyClick(comment) {
@ -349,18 +444,29 @@ export default {
}, },
createOrUpdateJournal() { createOrUpdateJournal() {
// //
this.journal.photos = this.journalPhotos // this.journal.photos = this.journalPhotos
this.journal.type = this.isPublic ? 'PUBLIC' : 'INTIMATE'
if (!this.journal.content) {
this.$notification['error']({
message: '提示',
description: '发布内容不能为空!'
})
return
}
if (this.journal.id) { if (this.journal.id) {
journalApi.update(this.journal.id, this.journal).then(response => { journalApi.update(this.journal.id, this.journal).then(response => {
this.$message.success('更新成功!') this.$message.success('更新成功!')
this.loadJournals() this.loadJournals()
this.isPublic = true
}) })
} else { } else {
journalApi.create(this.journal).then(response => { journalApi.create(this.journal).then(response => {
this.$message.success('发表成功!') this.$message.success('发表成功!')
this.loadJournals() this.loadJournals()
this.photoList = [] // this.photoList = []
this.isPublic = true
}) })
} }
this.visible = false this.visible = false
@ -373,21 +479,21 @@ export default {
}, },
resetParam() { resetParam() {
this.queryParam.keyword = null this.queryParam.keyword = null
this.queryParam.type = null
this.loadJournals() this.loadJournals()
} }
} }
} }
</script> </script>
<style scoped="scoped"> <style scoped="scoped">
.more-options-btn { /* .more-options-btn {
margin-left: 15px; margin-left: 15px;
text-decoration: none; text-decoration: none;
} }
/* 日志图片卡片样式 */
.photo-card { .photo-card {
width: 104px; width: 104px;
display: inline-block; display: inline-block;
margin-right: 5px; margin-right: 5px;
} } */
</style> </style>

View File

@ -9,7 +9,7 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card :title="title"> <a-card :title="title" :bodyStyle="{ padding: '16px' }">
<a-form layout="horizontal"> <a-form layout="horizontal">
<a-form-item label="网站名称:"> <a-form-item label="网站名称:">
<a-input v-model="link.name" /> <a-input v-model="link.name" />
@ -65,7 +65,7 @@
:xs="24" :xs="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card title="所有友情链接"> <a-card title="所有友情链接" :bodyStyle="{ padding: '16px' }">
<a-table <a-table
:columns="columns" :columns="columns"
:dataSource="links" :dataSource="links"
@ -201,6 +201,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@ -9,7 +9,10 @@
:span="24" :span="24"
class="search-box" class="search-box"
> >
<a-card :bordered="false"> <a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="table-page-search-wrapper"> <div class="table-page-search-wrapper">
<a-form layout="inline"> <a-form layout="inline">
<a-row :gutter="48"> <a-row :gutter="48">
@ -26,9 +29,15 @@
:sm="24" :sm="24"
> >
<a-form-item label="分组"> <a-form-item label="分组">
<a-select> <a-select
<a-select-option value="11">11</a-select-option> v-model="queryParam.team"
<a-select-option value="22">22</a-select-option> @change="loadPhotos(true)"
>
<a-select-option
v-for="(item,index) in teams"
:key="index"
:value="item"
>{{ item }}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -61,7 +70,7 @@
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-list <a-list
:grid="{ gutter: 12, xs: 1, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }" :grid="{ gutter: 12, xs: 2, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }"
:dataSource="photos" :dataSource="photos"
:loading="listLoading" :loading="listLoading"
> >
@ -80,7 +89,7 @@
</div> </div>
<a-card-meta> <a-card-meta>
<ellipsis <ellipsis
:length="isMobile()?36:18" :length="isMobile()?12:16"
tooltip tooltip
slot="description" slot="description"
>{{ item.name }}</ellipsis> >{{ item.name }}</ellipsis>
@ -104,7 +113,7 @@
title="图片详情" title="图片详情"
:width="isMobile()?'100%':'460'" :width="isMobile()?'100%':'460'"
closable closable
:visible="drawerVisiable" :visible="drawerVisible"
destroyOnClose destroyOnClose
@close="onDrawerClose" @close="onDrawerClose"
> >
@ -272,8 +281,8 @@
</template> </template>
<script> <script>
import AttachmentSelectDrawer from '../../attachment/components/AttachmentSelectDrawer'
import { mixin, mixinDevice } from '@/utils/mixin.js' import { mixin, mixinDevice } from '@/utils/mixin.js'
import AttachmentSelectDrawer from '../../attachment/components/AttachmentSelectDrawer'
import photoApi from '@/api/photo' import photoApi from '@/api/photo'
export default { export default {
@ -283,12 +292,13 @@ export default {
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
data() { data() {
return { return {
drawerVisiable: false, drawerVisible: false,
drawerLoading: false, drawerLoading: false,
listLoading: true, listLoading: true,
thumDrawerVisible: false, thumDrawerVisible: false,
photo: {}, photo: {},
photos: [], photos: [],
teams: [],
editable: false, editable: false,
pagination: { pagination: {
page: 1, page: 1,
@ -299,12 +309,14 @@ export default {
page: 0, page: 0,
size: 18, size: 18,
sort: null, sort: null,
keyword: null keyword: null,
team: null
} }
} }
}, },
created() { created() {
this.loadPhotos() this.loadPhotos()
this.loadTeams()
}, },
methods: { methods: {
loadPhotos(isSearch) { loadPhotos(isSearch) {
@ -321,6 +333,11 @@ export default {
this.listLoading = false this.listLoading = false
}) })
}, },
loadTeams() {
photoApi.listTeams().then(response => {
this.teams = response.data.data
})
},
handleCreateOrUpdate() { handleCreateOrUpdate() {
if (this.photo.id) { if (this.photo.id) {
photoApi.update(this.photo.id, this.photo).then(response => { photoApi.update(this.photo.id, this.photo).then(response => {
@ -338,7 +355,7 @@ export default {
}, },
showDrawer(photo) { showDrawer(photo) {
this.photo = photo this.photo = photo
this.drawerVisiable = true this.drawerVisible = true
}, },
handlePaginationChange(page, size) { handlePaginationChange(page, size) {
this.$log.debug(`Current: ${page}, PageSize: ${size}`) this.$log.debug(`Current: ${page}, PageSize: ${size}`)
@ -348,7 +365,7 @@ export default {
}, },
handleAddClick() { handleAddClick() {
this.editable = true this.editable = true
this.drawerVisiable = true this.drawerVisible = true
}, },
handleEditClick() { handleEditClick() {
this.editable = true this.editable = true
@ -365,14 +382,17 @@ export default {
}, },
selectPhotoThumb(data) { selectPhotoThumb(data) {
this.photo.url = encodeURI(data.path) this.photo.url = encodeURI(data.path)
this.photo.thumbnail = encodeURI(data.thumbPath)
this.thumDrawerVisible = false this.thumDrawerVisible = false
}, },
resetParam() { resetParam() {
this.queryParam.keyword = null this.queryParam.keyword = null
this.queryParam.team = null
this.loadPhotos() this.loadPhotos()
this.loadTeams()
}, },
onDrawerClose() { onDrawerClose() {
this.drawerVisiable = false this.drawerVisible = false
this.photo = {} this.photo = {}
this.editable = false this.editable = false
} }

View File

@ -2,10 +2,14 @@
<div class="page-header-index-wide"> <div class="page-header-index-wide">
<a-row> <a-row>
<a-col :span="24"> <a-col :span="24">
<a-card :bordered="false"> <a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<a-card <a-card
:bordered="false" :bordered="false"
class="environment-info" class="environment-info"
:bodyStyle="{ padding: '16px' }"
> >
<template slot="title"> <template slot="title">
环境信息 环境信息
@ -39,7 +43,7 @@
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
<ul> <ul style="margin: 0;padding: 0;list-style: none;">
<li>Server 版本{{ environments.version }}</li> <li>Server 版本{{ environments.version }}</li>
<li>Admin 版本{{ adminVersion }}</li> <li>Admin 版本{{ adminVersion }}</li>
<li>数据库{{ environments.database }}</li> <li>数据库{{ environments.database }}</li>
@ -50,16 +54,19 @@
<a <a
href="https://github.com/halo-dev" href="https://github.com/halo-dev"
target="_blank" target="_blank"
style="margin-right: 10px;"
>开源地址 >开源地址
<a-icon type="link" /></a> <a-icon type="link" /></a>
<a <a
href="https://halo.run/guide" href="https://halo.run/guide"
target="_blank" target="_blank"
style="margin-right: 10px;"
>用户文档 >用户文档
<a-icon type="link" /></a> <a-icon type="link" /></a>
<a <a
href="https://bbs.halo.run" href="https://bbs.halo.run"
target="_blank" target="_blank"
style="margin-right: 10px;"
>在线社区 >在线社区
<a-icon type="link" /></a> <a-icon type="link" /></a>
</a-card> </a-card>
@ -67,6 +74,7 @@
<a-card <a-card
title="开发者" title="开发者"
:bordered="false" :bordered="false"
:bodyStyle="{ padding: '16px' }"
> >
<a <a
:href="item.github" :href="item.github"
@ -90,6 +98,7 @@
<a-card <a-card
title="时间轴" title="时间轴"
:bordered="false" :bordered="false"
:bodyStyle="{ padding: '16px' }"
> >
<a-timeline> <a-timeline>
<a-timeline-item>...</a-timeline-item> <a-timeline-item>...</a-timeline-item>
@ -107,6 +116,7 @@
<script> <script>
import adminApi from '@/api/admin' import adminApi from '@/api/admin'
import axios from 'axios'
export default { export default {
data() { data() {
return { return {
@ -145,6 +155,10 @@ export default {
} }
], ],
steps: [ steps: [
{
date: '2019-09-11',
content: 'Halo v1.1.0 发布'
},
{ {
date: '2019-07-09', date: '2019-07-09',
content: 'Halo v1.0.3 发布' content: 'Halo v1.0.3 发布'
@ -199,6 +213,7 @@ export default {
}, },
created() { created() {
this.getEnvironments() this.getEnvironments()
this.checkUpdate()
}, },
computed: { computed: {
updateText() { updateText() {
@ -229,7 +244,8 @@ export default {
const text = `Server 版本:${this.environments.version} const text = `Server 版本:${this.environments.version}
Admin 版本${this.adminVersion} Admin 版本${this.adminVersion}
数据库${this.environments.database} 数据库${this.environments.database}
运行模式${this.environments.mode}` 运行模式${this.environments.mode}
UA 信息${navigator.userAgent}`
this.$copyText(text) this.$copyText(text)
.then(message => { .then(message => {
console.log('copy', message) console.log('copy', message)
@ -239,25 +255,64 @@ Admin 版本:${this.adminVersion}
console.log('copy.err', err) console.log('copy.err', err)
this.$message.error('复制失败!') this.$message.error('复制失败!')
}) })
},
async checkUpdate() {
const _this = this
axios
.get('https://api.github.com/repos/halo-dev/halo/releases/latest')
.then(response => {
const data = response.data
if (data.draft || data.prerelease) {
return
}
const current = _this.calculateIntValue(_this.environments.version)
const latest = _this.calculateIntValue(data.name)
if (current >= latest) {
return
}
const title = '新版本提醒'
const content = '检测到新版本:' + data.name + ',点击下方按钮查看最新版本。'
const url = data.html_url
this.$notification.open({
message: title,
description: content,
icon: <a-icon type="smile" style="color: #108ee9" />,
btn: h => {
return h(
'a-button',
{
props: {
type: 'primary',
size: 'small'
},
on: {
click: () => window.open(url, '_blank')
}
},
'去看看'
)
}
})
})
.catch(function(error) {
console.error('Check update fail', error)
})
},
calculateIntValue(version) {
version = version.replace(/v/g, '')
const ss = version.split('.')
if (ss == null || ss.length !== 3) {
return -1
}
const major = parseInt(ss[0])
const minor = parseInt(ss[1])
const micro = parseInt(ss[2])
if (isNaN(major) || isNaN(minor) || isNaN(micro)) {
return -1
}
return major * 1000000 + minor * 1000 + micro
} }
} }
} }
</script> </script>
<style lang="less" scope>
ul {
margin: 0;
padding: 0;
list-style: none;
}
.environment-info {
ul {
margin: 0;
padding: 0;
list-style: none;
}
a {
margin-right: 10px;
}
}
</style>

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<a-row <a-row
class="height-100"
type="flex" type="flex"
justify="center" justify="center"
align="middle" align="middle"
style="height: 100vh;"
> >
<a-col <a-col
:xl="8" :xl="8"
@ -16,7 +16,7 @@
<a-card <a-card
:bordered="false" :bordered="false"
title="Halo 安装向导" title="Halo 安装向导"
class="install-card" style="box-shadow: 0px 10px 20px 0px rgba(236, 236, 236, 0.86);"
> >
<a-steps :current="stepCurrent"> <a-steps :current="stepCurrent">
@ -91,7 +91,7 @@
<a-input <a-input
v-model="installation.password" v-model="installation.password"
type="password" type="password"
placeholder="用户密码" placeholder="用户密码8-100位"
v-decorator="[ v-decorator="[
'password', 'password',
{rules: [{ required: true, message: '请输入密码8-100位' }]} {rules: [{ required: true, message: '请输入密码8-100位' }]}
@ -189,12 +189,14 @@
class="install-action" class="install-action"
type="flex" type="flex"
justify="space-between" justify="space-between"
style="margin-top: 1rem;"
> >
<div> <div>
<a-button <a-button
class="previus-button" class="previus-button"
v-if="stepCurrent != 0" v-if="stepCurrent != 0"
@click="stepCurrent--" @click="stepCurrent--"
style="margin-right: 1rem;"
>上一步</a-button> >上一步</a-button>
<a-button <a-button
type="primary" type="primary"
@ -218,7 +220,6 @@
<script> <script>
import adminApi from '@/api/admin' import adminApi from '@/api/admin'
import optionApi from '@/api/option'
import recoveryApi from '@/api/recovery' import recoveryApi from '@/api/recovery'
export default { export default {
@ -244,8 +245,7 @@ export default {
migrationUploadName: 'file', migrationUploadName: 'file',
migrationData: null, migrationData: null,
stepCurrent: 0, stepCurrent: 0,
bloggerForm: this.$form.createForm(this), bloggerForm: this.$form.createForm(this)
keys: ['is_installed']
} }
}, },
created() { created() {
@ -254,8 +254,8 @@ export default {
}, },
methods: { methods: {
verifyIsInstall() { verifyIsInstall() {
optionApi.listAll(this.keys).then(response => { adminApi.isInstalled().then(response => {
if (response.data.data.is_installed) { if (response.data.data) {
this.$router.push({ name: 'Login' }) this.$router.push({ name: 'Login' })
} }
}) })
@ -292,7 +292,7 @@ export default {
this.$log.debug('Installation response', response) this.$log.debug('Installation response', response)
this.$message.success('安装成功!') this.$message.success('安装成功!')
setTimeout(() => { setTimeout(() => {
this.$router.push({ name: 'Dashboard' }) this.$router.push({ name: 'Login' })
}, 300) }, 300)
}) })
}, },
@ -322,20 +322,3 @@ export default {
} }
} }
</script> </script>
<style lang="less" scoped>
.height-100 {
height: 100vh;
}
.install-action {
margin-top: 1rem;
}
.previus-button {
margin-right: 1rem;
}
.install-card {
box-shadow: 0px 10px 20px 0px rgba(236, 236, 236, 0.86);
}
</style>

View File

@ -60,6 +60,7 @@
type="textarea" type="textarea"
:autosize="{ minRows: 5 }" :autosize="{ minRows: 5 }"
v-model="options.blog_footer_info" v-model="options.blog_footer_info"
placeholder="支持 HTML 格式的文本"
/> />
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@ -85,26 +86,21 @@
label="关键词: " label="关键词: "
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-tooltip <a-input
:trigger="['focus']" v-model="options.seo_keywords"
placement="right" placeholder="多个关键词以英文状态下的逗号隔开"
title="多个关键词以英文逗号隔开" />
>
<a-input v-model="options.seo_keywords" />
</a-tooltip>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="博客描述:" label="博客描述:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.seo_description" /> <a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="options.seo_description"
/>
</a-form-item> </a-form-item>
<!-- <a-form-item
label="百度推送 Token "
:wrapper-col="wrapperCol"
>
<a-input v-model="options.seo_baidu_token" />
</a-form-item> -->
<a-form-item> <a-form-item>
<a-button <a-button
type="primary" type="primary"
@ -118,6 +114,16 @@
<a-icon type="form" />文章设置 <a-icon type="form" />文章设置
</span> </span>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item
label="首页文章排序:"
:wrapper-col="wrapperCol"
>
<a-select v-model="options.post_index_sort">
<a-select-option value="createTime">创建时间</a-select-option>
<a-select-option value="editTime">最后编辑时间</a-select-option>
<a-select-option value="visits">点击量</a-select-option>
</a-select>
</a-form-item>
<a-form-item <a-form-item
label="首页显示条数:" label="首页显示条数:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
@ -196,6 +202,17 @@
> >
<a-switch v-model="options.comment_api_enabled" /> <a-switch v-model="options.comment_api_enabled" />
</a-form-item> </a-form-item>
<a-form-item
label="评论模块 JS"
:wrapper-col="wrapperCol"
>
<a-input
type="textarea"
:autosize="{ minRows: 2 }"
v-model="options.comment_internal_plugin_js"
placeholder="该设置仅对内置的评论模块有效"
/>
</a-form-item>
<a-form-item <a-form-item
label="每页显示条数: " label="每页显示条数: "
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
@ -234,6 +251,30 @@
<a-icon type="picture" />附件设置 <a-icon type="picture" />附件设置
</span> </span>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item
label="上传图片时预览:"
:wrapper-col="wrapperCol"
>
<a-switch v-model="options.attachment_upload_image_preview_enable" />
</a-form-item>
<a-form-item
label="最大上传文件数:"
:wrapper-col="wrapperCol"
>
<a-input
type="number"
v-model="options.attachment_upload_max_files"
/>
</a-form-item>
<a-form-item
label="同时上传文件数:"
:wrapper-col="wrapperCol"
>
<a-input
type="number"
v-model="options.attachment_upload_max_parallel_uploads"
/>
</a-form-item>
<a-form-item <a-form-item
label="存储位置:" label="存储位置:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
@ -250,20 +291,31 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<div <div
class="upyunForm" class="smmsForm"
v-show="upyunFormHidden" v-show="smmsFormVisible"
> >
<a-form-item <a-form-item
label="域名" label="Secret Token"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-tooltip <a-input
:trigger="['focus']" v-model="options.smms_api_secret_token"
placement="right" placeholder="需要到 sm.ms 官网注册后获取"
title="需要加上 http:// 或者 https://" />
</a-form-item>
</div>
<div
class="upyunForm"
v-show="upyunFormVisible"
> >
<a-input v-model="options.oss_upyun_domain" /> <a-form-item
</a-tooltip> label="绑定域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_upyun_domain"
placeholder="需要加上 http:// 或者 https://"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="空间名称:" label="空间名称:"
@ -292,17 +344,38 @@
> >
<a-input v-model="options.oss_upyun_source" /> <a-input v-model="options.oss_upyun_source" />
</a-form-item> </a-form-item>
<a-form-item
label="图片处理策略:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_upyun_style_rule"
placeholder="间隔标识符+图片处理版本名称"
/>
</a-form-item>
<a-form-item <a-form-item
label="缩略图处理策略:" label="缩略图处理策略:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_upyun_style_rule" /> <a-input
v-model="options.oss_upyun_thumbnail_style_rule"
placeholder="间隔标识符+图片处理版本名称,一般为后台展示所用"
/>
</a-form-item> </a-form-item>
</div> </div>
<div <div
class="qnyunForm" class="qnyunForm"
v-show="qnyunFormHidden" v-show="qnyunFormVisible"
> >
<a-form-item
label="绑定域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_qiniu_domain"
placeholder="需要加上 http:// 或者 https://"
/>
</a-form-item>
<a-form-item <a-form-item
label="区域:" label="区域:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
@ -316,18 +389,6 @@
<a-select-option value="as0">东南亚</a-select-option> <a-select-option value="as0">东南亚</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item
label="域名:"
:wrapper-col="wrapperCol"
>
<a-tooltip
:trigger="['focus']"
placement="right"
title="需要加上 http:// 或者 https://"
>
<a-input v-model="options.oss_qiniu_domain" />
</a-tooltip>
</a-form-item>
<a-form-item <a-form-item
label="Access Key" label="Access Key"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
@ -338,30 +399,60 @@
label="Secret Key" label="Secret Key"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_qiniu_secret_key" /> <a-input
type="password"
v-model="options.oss_qiniu_secret_key"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="Bucket" label="Bucket"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_qiniu_bucket" /> <a-input
v-model="options.oss_qiniu_bucket"
placeholder="存储空间名称"
/>
</a-form-item>
<a-form-item
label="图片处理策略:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_qiniu_style_rule"
placeholder="样式分隔符+图片处理样式名称"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="缩略图处理策略:" label="缩略图处理策略:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_qiniu_style_rule" /> <a-input
v-model="options.oss_qiniu_thumbnail_style_rule"
placeholder="样式分隔符+图片处理样式名称,一般为后台展示所用"
/>
</a-form-item> </a-form-item>
</div> </div>
<div <div
class="aliyunForm" class="aliyunForm"
v-show="aliyunFormHidden" v-show="aliyunFormVisible"
> >
<a-form-item
label="绑定域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_aliyun_domain"
placeholder="如不填写,路径根域名将为 Bucket + EndPoint"
/>
</a-form-item>
<a-form-item <a-form-item
label="Bucket" label="Bucket"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_aliyun_bucket_name" /> <a-input
v-model="options.oss_aliyun_bucket_name"
placeholder="存储空间名称"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="EndPoint地域节点" label="EndPoint地域节点"
@ -379,65 +470,119 @@
label="Access Secret" label="Access Secret"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_aliyun_access_secret" /> <a-input
type="password"
v-model="options.oss_aliyun_access_secret"
/>
</a-form-item>
<a-form-item
label="图片处理策略:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_aliyun_style_rule"
placeholder="请到阿里云控制台的图片处理获取"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="缩略图处理策略:" label="缩略图处理策略:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_aliyun_style_rule" /> <a-input
v-model="options.oss_aliyun_thumbnail_style_rule"
placeholder="请到阿里云控制台的图片处理获取,一般为后台展示所用"
/>
</a-form-item> </a-form-item>
</div> </div>
<div <div
class="baiduyunForm" class="baiduyunForm"
v-show="baiduyunFormHidden" v-show="baiduyunFormVisible"
> >
<a-form-item
label="绑定域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.bos_baiduyun_domain"
placeholder="如不填写,路径根域名将为 Bucket + EndPoint"
/>
</a-form-item>
<a-form-item <a-form-item
label="Bucket" label="Bucket"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_baiduyun_bucket_name" /> <a-input
v-model="options.bos_baiduyun_bucket_name"
placeholder="存储空间名称"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="EndPoint地域节点" label="EndPoint地域节点"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_baiduyun_endpoint" /> <a-input v-model="options.bos_baiduyun_endpoint" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="Access Key" label="Access Key"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_baiduyun_access_key" /> <a-input v-model="options.bos_baiduyun_access_key" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="Access Secret" label="Secret Key"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_baiduyun_access_secret" /> <a-input
type="password"
v-model="options.bos_baiduyun_secret_key"
/>
</a-form-item>
<a-form-item
label="图片处理策略:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.bos_baiduyun_style_rule"
placeholder="请到百度云控制台的图片处理获取"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="缩略图处理策略:" label="缩略图处理策略:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_baiduyun_style_rule" /> <a-input
v-model="options.bos_baiduyun_thumbnail_style_rule"
placeholder="请到百度云控制台的图片处理获取,一般为后台展示所用"
/>
</a-form-item> </a-form-item>
</div> </div>
<div <div
class="tencentyunForm" class="tencentyunForm"
v-show="tencentyunFormHidden" v-show="tencentyunFormVisible"
> >
<a-form-item
label="绑定域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.cos_tencentyun_domain"
placeholder="如不填写,路径根域名将为 Bucket + 区域地址"
/>
</a-form-item>
<a-form-item <a-form-item
label="Bucket" label="Bucket"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_tencentyun_bucket_name" /> <a-input
v-model="options.cos_tencentyun_bucket_name"
placeholder="存储桶名称"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="区域:" label="区域:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-select v-model="options.oss_tencentyun_region"> <a-select v-model="options.cos_tencentyun_region">
<a-select-option value="ap-beijing-1">北京一区</a-select-option> <a-select-option value="ap-beijing-1">北京一区</a-select-option>
<a-select-option value="ap-beijing">北京</a-select-option> <a-select-option value="ap-beijing">北京</a-select-option>
<a-select-option value="ap-shanghai">上海华东</a-select-option> <a-select-option value="ap-shanghai">上海华东</a-select-option>
@ -450,19 +595,16 @@
label="Secret Id" label="Secret Id"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_tencentyun_access_key" /> <a-input v-model="options.cos_tencentyun_secret_id" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="Secret Key" label="Secret Key"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-input v-model="options.oss_tencentyun_access_secret" /> <a-input
</a-form-item> type="password"
<a-form-item v-model="options.cos_tencentyun_secret_key"
label="缩略图处理策略:" />
:wrapper-col="wrapperCol"
>
<a-input v-model="options.oss_tencentyun_style_rule" />
</a-form-item> </a-form-item>
</div> </div>
<a-form-item> <a-form-item>
@ -518,13 +660,11 @@
label="邮箱密码:" label="邮箱密码:"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
> >
<a-tooltip <a-input
:trigger="['focus']" v-model="options.email_password"
placement="right" type="password"
title="部分邮箱可能是授权码" placeholder="部分邮箱可能是授权码"
> />
<a-input v-model="options.email_password" />
</a-tooltip>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="发件人:" label="发件人:"
@ -580,7 +720,7 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="api"> <a-tab-pane key="api">
<span slot="tab"> <span slot="tab">
<a-icon type="align-left" />API 设置 <a-icon type="thunderbolt" />API 设置
</span> </span>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item <a-form-item
@ -608,6 +748,15 @@
<a-icon type="align-left" />其他设置 <a-icon type="align-left" />其他设置
</span> </span>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item
label="CDN 加速域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.blog_cdn_domain"
placeholder="请确保已经正确配置好了 CDN"
/>
</a-form-item>
<a-form-item <a-form-item
label="自定义 head" label="自定义 head"
:wrapper-col="wrapperCol" :wrapper-col="wrapperCol"
@ -616,6 +765,7 @@
type="textarea" type="textarea"
:autosize="{ minRows: 5 }" :autosize="{ minRows: 5 }"
v-model="options.blog_custom_head" v-model="options.blog_custom_head"
placeholder="将放置于每个页面的<head></head>标签中"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@ -626,8 +776,20 @@
type="textarea" type="textarea"
:autosize="{ minRows: 5 }" :autosize="{ minRows: 5 }"
v-model="options.blog_statistics_code" v-model="options.blog_statistics_code"
placeholder="第三方网站统计的代码Google Analytics、百度统计、CNZZ 等"
/> />
</a-form-item> </a-form-item>
<!-- <a-form-item
label="黑名单 IP"
:wrapper-col="wrapperCol"
>
<a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="options.blog_ip_blacklist"
placeholder="多个 IP 地址换行隔开"
/>
</a-form-item> -->
<a-form-item> <a-form-item>
<a-button <a-button
type="primary" type="primary"
@ -655,10 +817,10 @@
</template> </template>
<script> <script>
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer' import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import { mapActions } from 'vuex'
import optionApi from '@/api/option' import optionApi from '@/api/option'
import mailApi from '@/api/mail' import mailApi from '@/api/mail'
import attachmentApi from '@/api/attachment' import attachmentApi from '@/api/attachment'
import { mapActions } from 'vuex'
export default { export default {
components: { components: {
@ -673,11 +835,12 @@ export default {
sm: { span: 12 }, sm: { span: 12 },
xs: { span: 24 } xs: { span: 24 }
}, },
upyunFormHidden: false, smmsFormVisible: false,
qnyunFormHidden: false, upyunFormVisible: false,
aliyunFormHidden: false, qnyunFormVisible: false,
baiduyunFormHidden: false, aliyunFormVisible: false,
tencentyunFormHidden: false, baiduyunFormVisible: false,
tencentyunFormVisible: false,
logoDrawerVisible: false, logoDrawerVisible: false,
faviconDrawerVisible: false, faviconDrawerVisible: false,
options: [], options: [],
@ -685,18 +848,290 @@ export default {
} }
}, },
mounted() { mounted() {
this.loadOptions() this.loadFormOptions()
},
destroyed: function() {
if (this.faviconDrawerVisible) {
this.faviconDrawerVisible = false
}
if (this.logoDrawerVisible) {
this.logoDrawerVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.faviconDrawerVisible) {
this.faviconDrawerVisible = false
}
if (this.logoDrawerVisible) {
this.logoDrawerVisible = false
}
next()
}, },
methods: { methods: {
...mapActions(['loadUser']), ...mapActions(['loadUser', 'loadOptions']),
loadOptions() { loadFormOptions() {
optionApi.listAll().then(response => { optionApi.listAll().then(response => {
this.options = response.data.data this.options = response.data.data
this.handleAttachChange(this.options['attachment_type']) this.handleAttachChange(this.options['attachment_type'])
}) })
}, },
handleSaveOptions() { handleSaveOptions() {
if (!this.options.blog_title) {
this.$notification['error']({
message: '提示',
description: '博客标题不能为空!'
})
return
}
if (!this.options.blog_url) {
this.$notification['error']({
message: '提示',
description: '博客地址不能为空!'
})
return
}
//
if (this.options.comment_new_notice || this.options.comment_reply_notice) {
if (!this.options.email_enabled) {
this.$notification['error']({
message: '提示',
description: '新评论通知或回复通知需要打开和配置 SMTP 服务!'
})
return
}
}
//
switch (this.options.attachment_type) {
case 'SMMS':
if (!this.options.smms_api_secret_token) {
this.$notification['error']({
message: '提示',
description: 'Secret Token不能为空'
})
return
}
break
case 'UPYUN':
if (!this.options.oss_upyun_domain) {
this.$notification['error']({
message: '提示',
description: '域名不能为空!'
})
return
}
if (!this.options.oss_upyun_bucket) {
this.$notification['error']({
message: '提示',
description: '空间名称不能为空!'
})
return
}
if (!this.options.oss_upyun_operator) {
this.$notification['error']({
message: '提示',
description: '操作员名称不能为空!'
})
return
}
if (!this.options.oss_upyun_password) {
this.$notification['error']({
message: '提示',
description: '操作员密码不能为空!'
})
return
}
if (!this.options.oss_upyun_source) {
this.$notification['error']({
message: '提示',
description: '文件目录不能为空!'
})
return
}
break
case 'QNYUN':
if (!this.options.oss_qiniu_domain) {
this.$notification['error']({
message: '提示',
description: '域名不能为空!'
})
return
}
if (!this.options.oss_qiniu_access_key) {
this.$notification['error']({
message: '提示',
description: 'Access Key 不能为空!'
})
return
}
if (!this.options.oss_qiniu_secret_key) {
this.$notification['error']({
message: '提示',
description: 'Secret Key 不能为空!'
})
return
}
if (!this.options.oss_qiniu_bucket) {
this.$notification['error']({
message: '提示',
description: 'Bucket 不能为空!'
})
return
}
break
case 'ALIYUN':
if (!this.options.oss_aliyun_bucket_name) {
this.$notification['error']({
message: '提示',
description: 'Bucket 不能为空!'
})
return
}
if (!this.options.oss_aliyun_endpoint) {
this.$notification['error']({
message: '提示',
description: 'EndPoint地域节点 不能为空!'
})
return
}
if (!this.options.oss_aliyun_access_key) {
this.$notification['error']({
message: '提示',
description: 'Access Key 不能为空!'
})
return
}
if (!this.options.oss_aliyun_access_secret) {
this.$notification['error']({
message: '提示',
description: 'Access Secret 不能为空!'
})
return
}
break
case 'BAIDUYUN':
if (!this.options.bos_baiduyun_bucket_name) {
this.$notification['error']({
message: '提示',
description: 'Bucket 不能为空!'
})
return
}
if (!this.options.bos_baiduyun_endpoint) {
this.$notification['error']({
message: '提示',
description: 'EndPoint地域节点 不能为空!'
})
return
}
if (!this.options.bos_baiduyun_access_key) {
this.$notification['error']({
message: '提示',
description: 'Access Key 不能为空!'
})
return
}
if (!this.options.bos_baiduyun_secret_key) {
this.$notification['error']({
message: '提示',
description: 'Secret Key 不能为空!'
})
return
}
break
case 'TENCENTYUN':
if (!this.options.cos_tencentyun_bucket_name) {
this.$notification['error']({
message: '提示',
description: 'Bucket 不能为空!'
})
return
}
if (!this.options.cos_tencentyun_region) {
this.$notification['error']({
message: '提示',
description: '区域不能为空!'
})
return
}
if (!this.options.cos_tencentyun_secret_id) {
this.$notification['error']({
message: '提示',
description: 'Secret Id 不能为空!'
})
return
}
if (!this.options.cos_tencentyun_secret_key) {
this.$notification['error']({
message: '提示',
description: 'Secret Key 不能为空!'
})
return
}
break
}
// SMTP
if (this.options.email_enabled) {
if (!this.options.email_host) {
this.$notification['error']({
message: '提示',
description: 'SMTP 地址不能为空!'
})
return
}
if (!this.options.email_protocol) {
this.$notification['error']({
message: '提示',
description: '发送协议不能为空!'
})
return
}
if (!this.options.email_ssl_port) {
this.$notification['error']({
message: '提示',
description: 'SSL 端口不能为空!'
})
return
}
if (!this.options.email_username) {
this.$notification['error']({
message: '提示',
description: '邮箱账号不能为空!'
})
return
}
if (!this.options.email_password) {
this.$notification['error']({
message: '提示',
description: '邮箱密码不能为空!'
})
return
}
if (!this.options.email_from_name) {
this.$notification['error']({
message: '提示',
description: '发件人不能为空!'
})
return
}
}
// API
if (this.options.api_enabled) {
if (!this.options.api_access_key) {
this.$notification['error']({
message: '提示',
description: 'Access key 不能为空!'
})
return
}
}
optionApi.save(this.options).then(response => { optionApi.save(this.options).then(response => {
this.loadFormOptions()
this.loadOptions() this.loadOptions()
this.loadUser() this.loadUser()
this.$message.success('保存成功!') this.$message.success('保存成功!')
@ -705,47 +1140,60 @@ export default {
handleAttachChange(e) { handleAttachChange(e) {
switch (e) { switch (e) {
case 'LOCAL': case 'LOCAL':
this.upyunFormVisible = false
this.qnyunFormVisible = false
this.aliyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
this.smmsFormVisible = false
break
case 'SMMS': case 'SMMS':
this.upyunFormHidden = false this.smmsFormVisible = true
this.qnyunFormHidden = false this.upyunFormVisible = false
this.aliyunFormHidden = false this.qnyunFormVisible = false
this.baiduyunFormHidden = false this.aliyunFormVisible = false
this.tencentyunFormHidden = false this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break break
case 'UPYUN': case 'UPYUN':
this.upyunFormHidden = true this.smmsFormVisible = false
this.qnyunFormHidden = false this.upyunFormVisible = true
this.aliyunFormHidden = false this.qnyunFormVisible = false
this.baiduyunFormHidden = false this.aliyunFormVisible = false
this.tencentyunFormHidden = false this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break break
case 'QNYUN': case 'QNYUN':
this.qnyunFormHidden = true this.smmsFormVisible = false
this.upyunFormHidden = false this.qnyunFormVisible = true
this.aliyunFormHidden = false this.upyunFormVisible = false
this.baiduyunFormHidden = false this.aliyunFormVisible = false
this.tencentyunFormHidden = false this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break break
case 'ALIYUN': case 'ALIYUN':
this.aliyunFormHidden = true this.smmsFormVisible = false
this.qnyunFormHidden = false this.aliyunFormVisible = true
this.upyunFormHidden = false this.qnyunFormVisible = false
this.baiduyunFormHidden = false this.upyunFormVisible = false
this.tencentyunFormHidden = false this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break break
case 'BAIDUYUN': case 'BAIDUYUN':
this.aliyunFormHidden = false this.smmsFormVisible = false
this.qnyunFormHidden = false this.aliyunFormVisible = false
this.upyunFormHidden = false this.qnyunFormVisible = false
this.baiduyunFormHidden = true this.upyunFormVisible = false
this.tencentyunFormHidden = false this.baiduyunFormVisible = true
this.tencentyunFormVisible = false
break break
case 'TENCENTYUN': case 'TENCENTYUN':
this.aliyunFormHidden = false this.smmsFormVisible = false
this.qnyunFormHidden = false this.aliyunFormVisible = false
this.upyunFormHidden = false this.qnyunFormVisible = false
this.baiduyunFormHidden = false this.upyunFormVisible = false
this.tencentyunFormHidden = true this.baiduyunFormVisible = false
this.tencentyunFormVisible = true
break break
} }
}, },
@ -754,6 +1202,27 @@ export default {
this.logoDrawerVisible = false this.logoDrawerVisible = false
}, },
handleTestMailClick() { handleTestMailClick() {
if (!this.mailParam.to) {
this.$notification['error']({
message: '提示',
description: '收件人不能为空!'
})
return
}
if (!this.mailParam.subject) {
this.$notification['error']({
message: '提示',
description: '主题不能为空!'
})
return
}
if (!this.mailParam.content) {
this.$notification['error']({
message: '提示',
description: '内容不能为空!'
})
return
}
mailApi.testMail(this.mailParam).then(response => { mailApi.testMail(this.mailParam).then(response => {
this.$message.info(response.data.message) this.$message.info(response.data.message)
}) })

View File

@ -27,20 +27,16 @@
title="Markdown 文章导入" title="Markdown 文章导入"
v-model="markdownUpload" v-model="markdownUpload"
:footer="null" :footer="null"
destroyOnClose
:afterClose="onUploadClose"
> >
<upload <FilePondUpload
name="files" ref="upload"
multiple name="file"
accept="text/markdown" accept="text/markdown"
label="拖拽或点击选择 Markdown 文件到此处"
:uploadHandler="uploadHandler" :uploadHandler="uploadHandler"
@change="handleChange" ></FilePondUpload>
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">拖拽或点击选择 Markdown 文件到此处</p>
<p class="ant-upload-hint">支持多个文件同时上传</p>
</upload>
</a-modal> </a-modal>
</div> </div>
</div> </div>
@ -69,10 +65,10 @@ export default {
} else if (status === 'error') { } else if (status === 'error') {
this.$message.error(`${info.file.name} 导入失败!`) this.$message.error(`${info.file.name} 导入失败!`)
} }
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
} }
} }
} }
</script> </script>
<style scoped>
</style>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="container"> <div class="container-wrapper">
<div class="loginLogo animated fadeInUp"> <div class="halo-logo animated fadeInUp">
<span>Halo</span> <span>Halo</span>
</div> </div>
<div class="loginBody animated"> <div class="animated">
<a-form <a-form
layout="vertical" layout="vertical"
@keyup.enter.native="handleLogin" @keyup.enter.native="handleLogin"
@ -39,17 +39,27 @@
/> />
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-row> <a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>
<a-button <a-button
type="primary" type="primary"
:block="true" :block="true"
@click="handleLogin" @click="handleLogin"
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>登录</a-button> >登录</a-button>
</a-row> </a-form-item>
<a-row> <a-row>
<router-link :to="{ name:'ResetPassword' }">
<a
class="tip animated fadeInRight"
v-if="resetPasswordButton"
href="javascript:void(0);"
>
找回密码
</a>
</router-link>
<a <a
@click="handleApiModifyModalOpen" @click="handleApiModifyModalOpen"
class="tip animated fadeInUp" class="tip animated fadeInUp"
@ -61,7 +71,7 @@
<a-modal <a-modal
title="API 设置" title="API 设置"
:visible="apiModifyVisiable" :visible="apiModifyVisible"
@ok="handleApiModifyOk" @ok="handleApiModifyOk"
@cancel="handleApiModifyCancel" @cancel="handleApiModifyCancel"
> >
@ -90,16 +100,25 @@ export default {
return { return {
username: null, username: null,
password: null, password: null,
apiModifyVisiable: false, apiModifyVisible: false,
defaultApiBefore: window.location.protocol + '//', defaultApiBefore: window.location.protocol + '//',
apiUrl: window.location.host apiUrl: window.location.host,
resetPasswordButton: false
} }
}, },
computed: { computed: {
...mapGetters({ defaultApiUrl: 'apiUrl' }) ...mapGetters({ defaultApiUrl: 'apiUrl' })
}, },
created() {
const _this = this
document.addEventListener('keydown', function(e) {
if (e.keyCode === 72 && e.altKey && e.shiftKey) {
_this.toggleHidden()
}
})
},
methods: { methods: {
...mapActions(['login', 'loadUser']), ...mapActions(['login', 'loadUser', 'loadOptions']),
...mapMutations({ ...mapMutations({
setApiUrl: 'SET_API_URL', setApiUrl: 'SET_API_URL',
restoreApiUrl: 'RESTORE_API_URL' restoreApiUrl: 'RESTORE_API_URL'
@ -123,6 +142,7 @@ export default {
loginSuccess() { loginSuccess() {
// Cache the user info // Cache the user info
this.loadUser() this.loadUser()
this.loadOptions()
if (this.$route.query.redirect) { if (this.$route.query.redirect) {
this.$router.replace(this.$route.query.redirect) this.$router.replace(this.$route.query.redirect)
} else { } else {
@ -131,18 +151,21 @@ export default {
}, },
handleApiModifyModalOpen() { handleApiModifyModalOpen() {
this.apiUrl = this.defaultApiUrl this.apiUrl = this.defaultApiUrl
this.apiModifyVisiable = true this.apiModifyVisible = true
}, },
handleApiModifyOk() { handleApiModifyOk() {
this.setApiUrl(this.apiUrl) this.setApiUrl(this.apiUrl)
this.apiModifyVisiable = false this.apiModifyVisible = false
}, },
handleApiModifyCancel() { handleApiModifyCancel() {
this.apiModifyVisiable = false this.apiModifyVisible = false
}, },
handleApiUrlRestore() { handleApiUrlRestore() {
this.restoreApiUrl() this.restoreApiUrl()
this.apiUrl = this.defaultApiUrl this.apiUrl = this.defaultApiUrl
},
toggleHidden() {
this.resetPasswordButton = !this.resetPasswordButton
} }
} }
} }
@ -152,40 +175,41 @@ body {
height: 100%; height: 100%;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
.container {
background: #f7f7f7; .container-wrapper {
background: #ffffff;
position: absolute; position: absolute;
border-radius: 5px;
top: 45%; top: 45%;
left: 50%; left: 50%;
margin: -160px 0 0 -160px; margin: -160px 0 0 -160px;
width: 320px; width: 320px;
padding: 16px 32px 32px 32px; padding: 18px 28px 28px 28px;
box-shadow: -4px 7px 46px 2px rgba(0, 0, 0, 0.1); box-shadow: -4px 7px 46px 2px rgba(0, 0, 0, 0.1);
.tip {
cursor: pointer; .halo-logo {
margin-top: .5rem;
float: right;
}
}
.loginLogo {
margin-bottom: 20px; margin-bottom: 20px;
text-align: center; text-align: center;
} span {
.loginLogo span {
vertical-align: text-bottom; vertical-align: text-bottom;
font-size: 36px; font-size: 38px;
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
color: #1790fe; color: #1790fe;
background-image: -webkit-gradient( background-image: linear-gradient(-20deg, #6e45e2 0%, #88d3ce 100%);
linear,
37.219838% 34.532506%,
36.425669% 93.178216%,
from(#36c8f5),
to(#1790fe),
color-stop(0.37, #1790fe)
);
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text;
small {
margin-left: 5px;
font-size: 35%;
}
}
}
.tip {
cursor: pointer;
margin-left: 0.5rem;
float: right;
}
} }
</style> </style>

View File

@ -6,7 +6,10 @@
:md="24" :md="24"
:style="{ 'padding-bottom': '12px' }" :style="{ 'padding-bottom': '12px' }"
> >
<a-card :bordered="false"> <a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="profile-center-avatarHolder"> <div class="profile-center-avatarHolder">
<a-tooltip <a-tooltip
placement="right" placement="right"
@ -149,8 +152,7 @@
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer' import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import userApi from '@/api/user' import userApi from '@/api/user'
import adminApi from '@/api/admin' import adminApi from '@/api/admin'
import optionApi from '@/api/option' import { mapMutations, mapGetters } from 'vuex'
import { mapMutations } from 'vuex'
import MD5 from 'md5.js' import MD5 from 'md5.js'
export default { export default {
@ -168,20 +170,18 @@ export default {
newPassword: null, newPassword: null,
confirmPassword: null confirmPassword: null
}, },
attachment: {}, attachment: {}
options: [],
keys: ['blog_url']
} }
}, },
computed: { computed: {
passwordUpdateButtonDisabled() { passwordUpdateButtonDisabled() {
return !(this.passwordParam.oldPassword && this.passwordParam.newPassword) return !(this.passwordParam.oldPassword && this.passwordParam.newPassword)
} },
...mapGetters(['options'])
}, },
created() { created() {
this.loadUser() this.loadUser()
this.getCounts() this.getCounts()
this.loadOptions()
}, },
methods: { methods: {
...mapMutations({ setUser: 'SET_USER' }), ...mapMutations({ setUser: 'SET_USER' }),
@ -191,11 +191,6 @@ export default {
this.profileLoading = false this.profileLoading = false
}) })
}, },
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
getCounts() { getCounts() {
adminApi.counts().then(response => { adminApi.counts().then(response => {
this.counts = response.data.data this.counts = response.data.data
@ -208,9 +203,35 @@ export default {
this.$message.error('确认密码和新密码不匹配!') this.$message.error('确认密码和新密码不匹配!')
return return
} }
userApi.updatePassword(this.passwordParam.oldPassword, this.passwordParam.newPassword).then(response => {}) userApi.updatePassword(this.passwordParam.oldPassword, this.passwordParam.newPassword).then(response => {
this.$message.success('密码修改成功!')
this.passwordParam.oldPassword = null
this.passwordParam.newPassword = null
this.passwordParam.confirmPassword = null
})
}, },
handleUpdateProfile() { handleUpdateProfile() {
if (!this.user.username) {
this.$notification['error']({
message: '提示',
description: '用户名不能为空!'
})
return
}
if (!this.user.nickname) {
this.$notification['error']({
message: '提示',
description: '用户昵称不能为空!'
})
return
}
if (!this.user.email) {
this.$notification['error']({
message: '提示',
description: '邮箱不能为空!'
})
return
}
userApi.updateProfile(this.user).then(response => { userApi.updateProfile(this.user).then(response => {
this.user = response.data.data this.user = response.data.data
this.setUser(Object.assign({}, this.user)) this.setUser(Object.assign({}, this.user))

View File

@ -0,0 +1,248 @@
<template>
<div class="container-wrapper">
<div class="halo-logo animated fadeInUp">
<span>Halo<small>重置密码</small></span>
</div>
<div class="animated">
<a-form layout="vertical">
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
>
<a-input
placeholder="用户名"
v-model="resetParam.username"
>
<a-icon
slot="prefix"
type="user"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.2s'}"
>
<a-input
placeholder="邮箱"
v-model="resetParam.email"
>
<a-icon
slot="prefix"
type="mail"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>
<a-input
v-model="resetParam.code"
type="password"
placeholder="验证码"
>
<a-icon
slot="prefix"
type="safety-certificate"
style="color: rgba(0,0,0,.25)"
/>
<a
href="javascript:void(0);"
slot="addonAfter"
@click="handleSendCode"
>
获取
</a>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.4s'}"
>
<a-input
v-model="resetParam.password"
type="password"
placeholder="新密码"
>
<a-icon
slot="prefix"
type="lock"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.5s'}"
>
<a-input
v-model="resetParam.confirmPassword"
type="password"
placeholder="确认密码"
>
<a-icon
slot="prefix"
type="lock"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.6s'}"
>
<a-button
type="primary"
:block="true"
@click="handleResetPassword"
>重置密码</a-button>
</a-form-item>
<a-row>
<router-link :to="{ name:'Login' }">
<a
class="tip animated fadeInUp"
:style="{'animation-delay': '0.7s'}"
>
返回登陆
</a>
</router-link>
</a-row>
</a-form>
</div>
</div>
</template>
<script>
import adminApi from '@/api/admin'
export default {
data() {
return {
resetParam: {
username: '',
email: '',
code: '',
password: '',
confirmPassword: ''
}
}
},
methods: {
handleSendCode() {
if (!this.resetParam.username) {
this.$notification['error']({
message: '提示',
description: '用户名不能为空!'
})
return
}
if (!this.resetParam.email) {
this.$notification['error']({
message: '提示',
description: '邮箱不能为空!'
})
return
}
adminApi.sendResetCode(this.resetParam).then(response => {
this.$message.info('邮件发送成功,五分钟内有效')
})
},
handleResetPassword() {
if (!this.resetParam.username) {
this.$notification['error']({
message: '提示',
description: '用户名不能为空!'
})
return
}
if (!this.resetParam.email) {
this.$notification['error']({
message: '提示',
description: '邮箱不能为空!'
})
return
}
if (!this.resetParam.code) {
this.$notification['error']({
message: '提示',
description: '验证码不能为空!'
})
return
}
if (!this.resetParam.password) {
this.$notification['error']({
message: '提示',
description: '新密码不能为空!'
})
return
}
if (!this.resetParam.confirmPassword) {
this.$notification['error']({
message: '提示',
description: '确认密码不能为空!'
})
return
}
if (this.resetParam.confirmPassword !== this.resetParam.password) {
this.$notification['error']({
message: '提示',
description: '确认密码和新密码不匹配!'
})
return
}
adminApi.resetPassword(this.resetParam).then(response => {
this.$message.info('密码重置成功!')
this.$router.push({ name: 'Login' })
})
}
}
}
</script>
<style lang="less">
body {
height: 100%;
background-color: #f5f5f5;
}
.container-wrapper {
background: #ffffff;
position: absolute;
border-radius: 5px;
top: 45%;
left: 50%;
margin: -160px 0 0 -160px;
width: 320px;
padding: 18px 28px 28px 28px;
box-shadow: -4px 7px 46px 2px rgba(0, 0, 0, 0.1);
.halo-logo {
margin-bottom: 20px;
text-align: center;
span {
vertical-align: text-bottom;
font-size: 38px;
display: inline-block;
font-weight: 600;
color: #1790fe;
background-image: linear-gradient(-20deg, #6e45e2 0%, #88d3ce 100%);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
small {
margin-left: 5px;
font-size: 35%;
}
}
}
.tip {
cursor: pointer;
margin-left: 0.5rem;
float: right;
}
}
</style>

View File

@ -1,30 +1,14 @@
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
// const GenerateAssetPlugin = require('generate-asset-webpack-plugin')
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, dir) return path.join(__dirname, dir)
} }
// var createServerConfig = function(compilation) {
// const configJson = {
// apiUrl: 'http://localhost:8090'
// }
// return JSON.stringify(configJson)
// }
// vue.config.js // vue.config.js
module.exports = { module.exports = {
configureWebpack: { configureWebpack: {
plugins: [ plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
// new GenerateAssetPlugin({
// filename: 'config.json',
// fn: (compilation, cb) => {
// cb(null, createServerConfig(compilation))
// },
// extraFiles: []
// })
] ]
}, },
@ -58,13 +42,6 @@ module.exports = {
css: { css: {
loaderOptions: { loaderOptions: {
less: { less: {
modifyVars: {
/*
'primary-color': '#F5222D',
'link-color': '#F5222D',
'border-radius-base': '4px',
*/
},
javascriptEnabled: true javascriptEnabled: true
} }
} }