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

View File

@ -1,23 +1,28 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="robots" content="noindex,nofllow" />
<meta name="generator" content="Halo" />
<link rel="icon" href="<%= BASE_URL %>logo.png" />
<title>Halo Dashboard</title>
</head>
<body>
<noscript>
<strong
>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="robots" content="noindex,nofllow" />
<meta name="generator" content="Halo" />
<link rel="icon" href="<%= BASE_URL %>logo.png" />
<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>
<body>
<noscript>
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong>
</noscript>
<div id="app">
<div id="loader"></div>
</div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -12,6 +12,13 @@ adminApi.counts = () => {
})
}
adminApi.isInstalled = () => {
return service({
url: `${baseUrl}/is_installed`,
method: 'get'
})
}
adminApi.environments = () => {
return service({
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 = () => {
return service({
url: `${baseUrl}/halo-admin`,

View File

@ -6,7 +6,7 @@ const backupApi = {}
backupApi.importMarkdown = (formData, uploadProgress, cancelToken) => {
return service({
url: `${baseUrl}/import/markdowns`,
url: `${baseUrl}/import/markdown`,
timeout: 8640000, // 24 hours
data: formData, // form data
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.
* @param {String} target

View File

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

View File

@ -35,4 +35,11 @@ photoApi.delete = photoId => {
})
}
photoApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
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 = {
PUBLISHED: {
color: 'green',
@ -81,6 +88,11 @@ postApi.postStatus = {
color: 'red',
status: 'error',
text: '回收站'
},
INTIMATE: {
color: 'blue',
status: 'success',
text: '私密'
}
}
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 = {
PUBLISHED: {
color: 'green',

View File

@ -11,9 +11,16 @@ themeApi.listAll = () => {
})
}
themeApi.listFiles = () => {
themeApi.listFilesActivated = () => {
return service({
url: `${baseUrl}/files`,
url: `${baseUrl}/activation/files`,
method: 'get'
})
}
themeApi.listFiles = themeId => {
return service({
url: `${baseUrl}/${themeId}/files`,
method: 'get'
})
}
@ -41,7 +48,7 @@ themeApi.getActivatedTheme = () => {
themeApi.update = themeId => {
return service({
url: `${baseUrl}/${themeId}`,
url: `${baseUrl}/fetching/${themeId}`,
timeout: 60000,
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 => {
return service({
url: `${baseUrl}/fetching`,
@ -115,13 +133,34 @@ themeApi.getContent = path => {
})
}
themeApi.saveContent = (path, content) => {
themeApi.getContent = (themeId, path) => {
return service({
url: `${baseUrl}/files/content`,
url: `${baseUrl}/${themeId}/files/content`,
params: {
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'
})
}

View File

@ -1,6 +1,12 @@
<template>
<div class="footer">
<div class="copyright">
<div
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
<router-link :to="{ name:'About' }">
<a href="javascript:void(0);">Halo</a>
@ -19,13 +25,4 @@ export default {
</script>
<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>

View File

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

View File

@ -8,7 +8,6 @@
closable
@close="onClose"
:visible="visible"
:zIndex="9999"
>
<div class="setting-drawer-index-content">
<div :style="{ marginBottom: '24px' }">
@ -114,6 +113,77 @@
</div>
</div>
<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>
</a-drawer>
</div>
@ -122,7 +192,7 @@
<script>
import SettingItem from '@/components/SettingDrawer/SettingItem'
import config from '@/config/defaultSettings'
import { updateTheme, colorList } from '@/components/Tools/setting'
import { updateTheme, colorList } from './setting'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
@ -161,9 +231,10 @@ export default {
handleLayout(mode) {
this.baseConfig.layout = mode
this.$store.dispatch('ToggleLayoutMode', mode)
//
//
this.handleFixSiderbar(false)
if (mode === 'sidemenu') {
this.handleContentWidthChange('Fixed')
}
},
handleContentWidthChange(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
class="avatar"
size="small"
style="margin-right: 0.3rem;"
:src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'"
/>
</span>
@ -63,7 +64,6 @@
import HeaderComment from './HeaderComment'
import SettingDrawer from '@/components/SettingDrawer/SettingDrawer'
import { mapActions, mapGetters } from 'vuex'
import optionApi from '@/api/option'
export default {
name: 'UserMenu',
@ -73,19 +73,14 @@ export default {
},
data() {
return {
optionVisible: true,
options: [],
keys: ['blog_url']
optionVisible: true
}
},
mounted() {
this.optionVisible = this.$refs.drawer.visible
},
created() {
this.loadOptions()
},
computed: {
...mapGetters(['user'])
...mapGetters(['user', 'options'])
},
methods: {
...mapActions(['logout']),
@ -114,18 +109,7 @@ export default {
showOptionModal() {
this.optionVisible = this.$refs.drawer.visible
this.$refs.drawer.toggle()
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
}
}
}
</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>
<style>
/* you can make up upload button and sample style by using stylesheets */
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;

View File

@ -1,15 +1,17 @@
@import './index.less';
*{
* {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: #eee;
}
&::-webkit-scrollbar-thumb {
background-color: #1890ff;
cursor: pointer;
}
&::-webkit-scrollbar-track {
background-color: #eee;
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-item {
padding-bottom: 0 !important;
@ -694,22 +684,108 @@ body {
margin-bottom: 0;
p{
p {
margin-bottom: 0;
}
}
.post-thum {
.post-thumb,
.sheet-thumb {
.img {
width: 100%;
cursor: pointer;
border-radius: 4px;
}
.post-thum-remove {
margin-top: 16px;
}
.post-thumb-remove,
.sheet-thumb-remove {
margin-top: 16px;
}
.ant-calendar-picker {
width: 100% !important;
}
#editor {
.v-note-wrapper {
min-height: 580px;
}
}
.ant-calendar-picker{
width: 100%!important;
.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 FooterToolbar from '@/components/FooterToolbar'
import Upload from '@/components/Upload/Upload'
import FilePondUpload from '@/components/Upload/FilePondUpload'
const _components = {
Ellipsis,
FooterToolbar,
Upload
Upload,
FilePondUpload
}
const components = {}

View File

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

View File

@ -11,7 +11,8 @@ import {
DEFAULT_FIXED_SIDEMENU,
DEFAULT_CONTENT_WIDTH_TYPE,
USER,
API_URL
API_URL,
OPTIONS
} from '@/store/mutation-types'
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_USER', Vue.ls.get(USER))
store.commit('SET_API_URL', Vue.ls.get(API_URL))
store.commit('SET_OPTIONS', Vue.ls.get(OPTIONS))
// last step
}

View File

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

View File

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

View File

@ -1,14 +1,19 @@
import Vue from 'vue'
import router from './router'
import store from './store'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import {
setDocumentTitle,
domTitle
} from '@/utils/domUtil'
import NProgress from 'nprogress' // progress bar
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) => {
NProgress.start()
@ -16,12 +21,18 @@ router.beforeEach((to, from, next) => {
Vue.$log.debug('Token', store.getters.token)
if (store.getters.token) {
if (to.name === 'Login') {
next({ name: 'Dashboard' })
next({
name: 'Dashboard'
})
NProgress.done()
return
}
// TODO Get installation status
if (!store.getters.options) {
store.dispatch('loadOptions').then()
}
next()
NProgress.done()
return
@ -35,6 +46,11 @@ router.beforeEach((to, from, next) => {
return
}
next({ name: 'Login', query: { redirect: to.fullPath } })
next({
name: 'Login',
query: {
redirect: to.fullPath
}
})
NProgress.done()
})

View File

@ -4,16 +4,14 @@ const getters = {
color: state => state.app.color,
token: state => state.user.token,
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,
apiUrl: state => {
if (state.app.apiUrl) {
return state.app.apiUrl
}
return `${window.location.protocol}//${window.location.host}`
}
},
options: state => state.option.options
}
export default getters

View File

@ -4,6 +4,7 @@ import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
import permission from './modules/permission'
import option from './modules/option'
import getters from './getters'
Vue.use(Vuex)
@ -12,7 +13,8 @@ export default new Vuex.Store({
modules: {
app,
user,
permission
permission,
option
},
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
}
/**
* 单账户多角色时使用该方法可过滤角色不存在的菜单
*
* @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) {
const accessedRouters = routerMap.filter(route => {
if (hasPermission(roles.permissionList, route)) {

View File

@ -1,15 +1,14 @@
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 userApi from '@/api/user'
const user = {
state: {
token: null,
name: '',
avatar: '',
roles: [],
info: {},
user: {}
},
mutations: {
@ -17,18 +16,6 @@ const user = {
Vue.ls.set(ACCESS_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 => {
Vue.ls.remove(ACCESS_TOKEN)
state.token = null
@ -39,7 +26,9 @@ const user = {
}
},
actions: {
loadUser({ commit }) {
loadUser({
commit
}) {
return new Promise((resolve, reject) => {
userApi
.getProfile()
@ -52,7 +41,12 @@ const user = {
})
})
},
login({ commit }, { username, password }) {
login({
commit
}, {
username,
password
}) {
return new Promise((resolve, reject) => {
adminApi
.login(username, password)
@ -68,7 +62,9 @@ const user = {
})
})
},
logout({ commit }) {
logout({
commit
}) {
return new Promise(resolve => {
commit('CLEAR_TOKEN')
adminApi
@ -81,7 +77,9 @@ const user = {
})
})
},
refreshToken({ commit }, refreshToken) {
refreshToken({
commit
}, refreshToken) {
return new Promise((resolve, reject) => {
adminApi
.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 USER = 'USER'
export const API_URL = 'API_URL'
export const OPTIONS = 'OPTIONS'
export const CONTENT_WIDTH_TYPE = {
Fluid: 'Fluid',

View File

@ -8,7 +8,7 @@ import router from '@/router'
import { isObject } from './util'
const service = axios.create({
timeout: 5000,
timeout: 8000,
withCredentials: true
})
@ -147,7 +147,7 @@ service.interceptors.response.use(
message.error(data.message)
}
} else {
message.error('服务异常')
message.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
:span="24"
class="search-box"
style="padding-bottom: 12px;"
>
<a-card :bordered="false">
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
@ -73,7 +77,7 @@
</a-row>
</a-form>
</div>
<div class="table-operator">
<div class="table-operator" style="margin-bottom: 0;">
<a-button
type="primary"
icon="plus"
@ -84,7 +88,7 @@
</a-col>
<a-col :span="24">
<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"
:loading="listLoading"
>
@ -100,11 +104,14 @@
>
<div class="attach-thumb">
<span v-show="!handleJudgeMediaType(item)"></span>
<img :src="item.thumbPath" v-show="handleJudgeMediaType(item)">
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
</div>
<a-card-meta>
<a-card-meta style="padding: 0.8rem;">
<ellipsis
:length="isMobile()?36:16"
:length="isMobile()?12:16"
tooltip
slot="description"
>{{ item.name }}</ellipsis>
@ -130,21 +137,15 @@
v-model="uploadVisible"
:footer="null"
:afterClose="onUploadClose"
destroyOnClose
>
<upload
name="file"
multiple
<FilePondUpload
ref="upload"
:uploadHandler="uploadHandler"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择文件或将文件拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传</p>
</upload>
></FilePondUpload>
</a-modal>
<AttachmentDetailDrawer
v-model="drawerVisiable"
v-model="drawerVisible"
v-if="selectAttachment"
:attachment="selectAttachment"
:addToPhoto="true"
@ -154,8 +155,8 @@
</template>
<script>
import { PageView } from '@/layouts'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { PageView } from '@/layouts'
import AttachmentDetailDrawer from './components/AttachmentDetailDrawer'
import attachmentApi from '@/api/attachment'
@ -187,8 +188,8 @@ export default {
mediaType: null,
attachmentType: null
},
uploadHandler: attachmentApi.upload,
drawerVisiable: false
drawerVisible: false,
uploadHandler: attachmentApi.upload
}
},
computed: {
@ -203,6 +204,17 @@ export default {
this.loadAttachments()
this.loadMediaTypes()
},
destroyed: function() {
if (this.drawerVisible) {
this.drawerVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.drawerVisible) {
this.drawerVisible = false
}
next()
},
methods: {
loadAttachments() {
this.queryParam.page = this.pagination.page - 1
@ -222,7 +234,7 @@ export default {
},
handleShowDetailDrawer(attachment) {
this.selectAttachment = attachment
this.drawerVisiable = true
this.drawerVisible = true
},
handlePaginationChange(page, size) {
this.$log.debug(`Current: ${page}, PageSize: ${size}`)
@ -235,12 +247,15 @@ export default {
this.queryParam.mediaType = null
this.queryParam.attachmentType = null
this.loadAttachments()
this.loadMediaTypes()
},
handleQuery() {
this.queryParam.page = 0
this.pagination.page = 1
this.loadAttachments()
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadAttachments()
this.loadMediaTypes()
},
@ -264,47 +279,3 @@ export default {
}
}
</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="附件详情"
:width="isMobile()?'100%':'460'"
closable
:visible="visiable"
:visible="visible"
destroyOnClose
@close="onClose"
>
@ -19,13 +19,21 @@
>
<div class="attach-detail-img">
<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
class="video-player-box"
v-show="videoPreviewVisible"
ref="videoPlayer"
:options="playerOptions"
:playsinline="true">
:playsinline="true"
style="width: 100%;"
>
</video-player>
</div>
</a-skeleton>
@ -154,10 +162,10 @@
<script>
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 photoApi from '@/api/photo'
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
export default {
name: 'AttachmentDetailDrawer',
@ -182,10 +190,12 @@ export default {
controls: true,
loop: false,
playbackRates: [0.7, 1.0, 1.5, 2.0],
sources: [{
type: 'video/mp4',
src: 'https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm'
}],
sources: [
{
type: 'video/mp4',
src: 'https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm'
}
],
poster: '/static/images/author.jpg',
width: document.documentElement.clientWidth,
notSupportedMessage: '此视频暂无法播放,请稍后再试'
@ -193,7 +203,7 @@ export default {
}
},
model: {
prop: 'visiable',
prop: 'visible',
event: 'close'
},
props: {
@ -206,7 +216,7 @@ export default {
required: false,
default: false
},
visiable: {
visible: {
type: Boolean,
required: false,
default: true
@ -221,7 +231,7 @@ export default {
}
},
watch: {
visiable: function(newValue, oldValue) {
visible: function(newValue, oldValue) {
this.$log.debug('old value', oldValue)
this.$log.debug('new value', newValue)
if (newValue) {
@ -253,6 +263,13 @@ export default {
this.editable = !this.editable
},
doUpdateAttachment() {
if (!this.attachment.name) {
this.$notification['error']({
message: '提示',
description: '附件名称不能为空!'
})
return
}
attachmentApi.update(this.attachment.id, this.attachment).then(response => {
this.$log.debug('Updated attachment', response.data.data)
this.$message.success('附件修改成功!')
@ -339,12 +356,3 @@ export default {
}
}
</script>
<style scope>
.attach-detail-img img {
width: 100%;
}
.video-player-box {
width: 100%;
}
</style>

View File

@ -4,7 +4,7 @@
title="附件库"
:width="isMobile()?'100%':'460'"
closable
:visible="visiable"
:visible="visible"
destroyOnClose
@close="onClose"
>
@ -36,7 +36,11 @@
:key="index"
@click="handleShowDetailDrawer(item)"
>
<img :src="item.thumbPath">
<span v-show="!handleJudgeMediaType(item)"></span>
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
</div>
</a-col>
</a-skeleton>
@ -51,12 +55,12 @@
</div>
<AttachmentDetailDrawer
v-model="detailVisiable"
v-model="detailVisible"
v-if="selectedAttachment"
:attachment="selectedAttachment"
@delete="handleDelete"
/>
<a-divider class="divider-transparent"/>
<a-divider class="divider-transparent" />
<div class="bottom-control">
<a-button
@click="handleShowUploadModal"
@ -70,26 +74,20 @@
v-model="uploadVisible"
:footer="null"
:afterClose="onUploadClose"
destroyOnClose
>
<upload
name="file"
multiple
:uploadHandler="attachmentUploadHandler"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择文件或将文件拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传</p>
</upload>
<FilePondUpload
ref="upload"
:uploadHandler="uploadHandler"
></FilePondUpload>
</a-modal>
</div>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import attachmentApi from '@/api/attachment'
import AttachmentDetailDrawer from './AttachmentDetailDrawer'
import attachmentApi from '@/api/attachment'
export default {
name: 'AttachmentDrawer',
@ -98,11 +96,11 @@ export default {
AttachmentDetailDrawer
},
model: {
prop: 'visiable',
prop: 'visible',
event: 'close'
},
props: {
visiable: {
visible: {
type: Boolean,
required: false,
default: false
@ -111,7 +109,7 @@ export default {
data() {
return {
attachmentType: attachmentApi.type,
detailVisiable: false,
detailVisible: false,
attachmentDrawerVisible: false,
uploadVisible: false,
skeletonLoading: true,
@ -128,7 +126,7 @@ export default {
},
attachments: [],
selectedAttachment: {},
attachmentUploadHandler: attachmentApi.upload
uploadHandler: attachmentApi.upload
}
},
computed: {
@ -144,7 +142,7 @@ export default {
this.loadAttachments()
},
watch: {
visiable: function(newValue, oldValue) {
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
}
@ -163,7 +161,7 @@ export default {
handleShowDetailDrawer(attachment) {
this.selectedAttachment = attachment
this.$log.debug('Show detail of', attachment)
this.detailVisiable = true
this.detailVisible = true
},
loadAttachments(isSearch) {
this.queryParam.page = this.pagination.page - 1
@ -183,34 +181,33 @@ export default {
this.loadAttachments()
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadSkeleton()
this.loadAttachments()
},
handleDelete() {
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() {
this.$emit('close', false)
}
}
}
</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"
:width="isMobile()?'100%':drawerWidth"
closable
:visible="visiable"
:visible="visible"
destroyOnClose
@close="onClose"
>
@ -34,7 +34,11 @@
:key="index"
@click="handleSelectAttachment(item)"
>
<img :src="item.thumbPath">
<span v-show="!handleJudgeMediaType(item)"></span>
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
</div>
</a-col>
</a-skeleton>
@ -67,18 +71,12 @@
v-model="uploadVisible"
:footer="null"
:afterClose="onUploadClose"
destroyOnClose
>
<upload
name="file"
multiple
:uploadHandler="attachmentUploadHandler"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择文件或将文件拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传</p>
</upload>
<FilePondUpload
ref="upload"
:uploadHandler="uploadHandler"
></FilePondUpload>
</a-modal>
</div>
</template>
@ -91,11 +89,11 @@ export default {
name: 'AttachmentSelectDrawer',
mixins: [mixin, mixinDevice],
model: {
prop: 'visiable',
prop: 'visible',
event: 'close'
},
props: {
visiable: {
visible: {
type: Boolean,
required: false,
default: false
@ -126,7 +124,7 @@ export default {
sort: ''
},
attachments: [],
attachmentUploadHandler: attachmentApi.upload
uploadHandler: attachmentApi.upload
}
},
created() {
@ -134,7 +132,7 @@ export default {
this.loadAttachments()
},
watch: {
visiable: function(newValue, oldValue) {
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
}
@ -174,31 +172,30 @@ export default {
this.loadAttachments()
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadSkeleton()
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() {
this.$emit('close', false)
}
}
}
</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: {
PageView,
CommentTab
},
data() {
return {}
},
methods: {}
}
}
</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>
<div class="comment-tab-wrapper">
<a-card :bordered="false">
<a-card
:bordered="false"
:bodyStyle="{ padding: 0 }"
>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
@ -105,6 +108,22 @@
:loading="loading"
: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
class="comment-content-wrapper"
slot="content"
@ -116,8 +135,10 @@
slot="status"
slot-scope="statusProperty"
>
<a-badge :status="statusProperty.status" />
{{ statusProperty.text }}
<a-badge
:status="statusProperty.status"
:text="statusProperty.text"
/>
</span>
<a
v-if="type==='posts'"
@ -203,6 +224,13 @@
>
<a href="javascript:;">删除</a>
</a-popconfirm>
<!-- <a-divider type="vertical" />
<a
href="javascript:;"
@click="handleShowDetailDrawer(record)"
>详情</a> -->
</span>
</a-table>
<div class="page-wrapper">
@ -223,6 +251,7 @@
:title="'回复给:'+selectComment.author"
v-model="replyCommentVisible"
@close="onReplyClose"
destroyOnClose
>
<template slot="footer">
<a-button
@ -243,16 +272,24 @@
</a-form-item>
</a-form>
</a-modal>
<!-- <CommentDetail
v-model="commentDetailVisible"
v-if="selectComment"
:comment="selectComment"
:type="this.type"
/> -->
</div>
</template>
<script>
import commentApi from '@/api/comment'
import optionApi from '@/api/option'
import { mapGetters } from 'vuex'
import CommentDetail from './CommentDetail'
import marked from 'marked'
import commentApi from '@/api/comment'
const postColumns = [
{
title: '昵称',
dataIndex: 'author'
dataIndex: 'author',
scopedSlots: { customRender: 'author' }
},
{
title: '内容',
@ -263,29 +300,33 @@ const postColumns = [
title: '状态',
className: 'status',
dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' }
},
{
title: '评论文章',
dataIndex: 'post',
width: '200px',
scopedSlots: { customRender: 'post' }
},
{
title: '日期',
dataIndex: 'createTime',
width: '170px',
scopedSlots: { customRender: 'createTime' }
},
{
title: '操作',
dataIndex: 'action',
width: '150px',
width: '180px',
scopedSlots: { customRender: 'action' }
}
]
const sheetColumns = [
{
title: '昵称',
dataIndex: 'author'
dataIndex: 'author',
scopedSlots: { customRender: 'author' }
},
{
title: '内容',
@ -296,27 +337,33 @@ const sheetColumns = [
title: '状态',
className: 'status',
dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' }
},
{
title: '评论页面',
dataIndex: 'sheet',
width: '200px',
scopedSlots: { customRender: 'sheet' }
},
{
title: '日期',
dataIndex: 'createTime',
width: '150px',
scopedSlots: { customRender: 'createTime' }
},
{
title: '操作',
dataIndex: 'action',
width: '150px',
width: '180px',
scopedSlots: { customRender: 'action' }
}
]
export default {
name: 'CommentTab',
components: {
CommentDetail
},
props: {
type: {
type: String,
@ -350,13 +397,11 @@ export default {
replyComment: {},
loading: false,
commentStatus: commentApi.commentStatus,
options: [],
keys: ['blog_url']
commentDetailVisible: false
}
},
created() {
this.loadComments()
this.loadOptions()
},
computed: {
formattedComments() {
@ -365,7 +410,8 @@ export default {
comment.content = marked(comment.content, { sanitize: true })
return comment
})
}
},
...mapGetters(['options'])
},
methods: {
loadComments() {
@ -381,13 +427,9 @@ export default {
},
handleQuery() {
this.queryParam.page = 0
this.pagination.current = 1
this.loadComments()
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handleEditStatusClick(commentId, status) {
commentApi.updateStatus(this.type, commentId, status).then(response => {
this.$message.success('操作成功!')
@ -415,6 +457,13 @@ export default {
}
},
handleCreateClick() {
if (!this.replyComment.content) {
this.$notification['error']({
message: '提示',
description: '评论内容不能为空!'
})
return
}
commentApi.create(this.type, this.replyComment).then(response => {
this.$message.success('回复成功!')
this.replyComment = {}
@ -492,6 +541,10 @@ export default {
name: comment.author
}
}
},
handleShowDetailDrawer(comment) {
this.selectComment = comment
this.commentDetailVisible = true
}
}
}

View File

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

View File

@ -15,7 +15,6 @@
</div>
<div class="number">
<slot name="number">
<!-- <span>{{ typeof number === 'function' && number() || number }}</span> -->
<countTo
:startVal="startNumber"
:endVal="typeof number === 'function' && number() || number"
@ -64,37 +63,3 @@ export default {
}
}
</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"
target="_blank"
>{{ item.author }}</a> 发表在 <a
v-if="item.post.status=='PUBLISHED'"
:href="options.blog_url+'/archives/'+item.post.url"
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
slot="author"
@ -30,8 +43,16 @@
:href="item.authorUrl"
target="_blank"
>{{ item.author }}</a> 发表在 <a
v-if="item.sheet.status=='PUBLISHED'"
:href="options.blog_url+'/s/'+item.sheet.url"
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>
</template>
<!-- <template slot="actions">
@ -54,8 +75,10 @@
</template>
<script>
import { mapGetters } from 'vuex'
import commentApi from '@/api/comment'
import optionApi from '@/api/option'
import postApi from '@/api/post'
import sheetApi from '@/api/sheet'
import marked from 'marked'
export default {
@ -73,9 +96,7 @@ export default {
data() {
return {
comments: [],
loading: false,
options: [],
keys: ['blog_url']
loading: false
}
},
computed: {
@ -84,24 +105,29 @@ export default {
comment.content = marked(comment.content, { sanitize: true })
return comment
})
}
},
...mapGetters(['options'])
},
created() {
this.loadComments()
this.loadOptions()
},
methods: {
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
loadComments() {
this.loading = true
commentApi.latestComment(this.type, 5, 'PUBLISHED').then(response => {
this.comments = response.data.data
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>
<style scoped>
</style>

View File

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

View File

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

View File

@ -9,7 +9,7 @@
:xs="24"
:style="{'padding-bottom':'12px'}"
>
<a-card>
<a-card :bodyStyle="{ padding: '16px' }">
<a-form layout="vertical">
<a-form-item>
<codemirror
@ -35,8 +35,27 @@
:xs="24"
: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
v-if="files"
:files="files"
@listenToSelect="handleSelectFile"
/>
@ -65,25 +84,38 @@ export default {
lineNumbers: true,
line: true
},
files: [],
files: null,
file: {},
content: '',
activatedTheme: {}
themes: [],
selectedTheme: {}
}
},
created() {
this.loadFiles()
this.loadActivatedTheme()
this.loadFiles()
this.loadThemes()
},
methods: {
loadActivatedTheme() {
themeApi.getActivatedTheme().then(response => {
this.selectedTheme = response.data.data
})
},
loadFiles() {
themeApi.listFiles().then(response => {
themeApi.listFilesActivated().then(response => {
this.files = response.data.data
})
},
loadActivatedTheme() {
themeApi.getActivatedTheme().then(response => {
this.activatedTheme = response.data.data
loadThemes() {
themeApi.listAll().then(response => {
this.themes = response.data.data
})
},
onSelectTheme(themeId) {
this.files = null
themeApi.listFiles(themeId).then(response => {
this.files = response.data.data
})
},
handleSelectFile(file) {
@ -95,7 +127,12 @@ export default {
this.buttonDisabled = true
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({
title: '警告:请谨慎修改该配置文件',
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.file = file
this.buttonDisabled = false
})
},
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('保存成功!')
})
}

View File

@ -47,7 +47,7 @@
style="margin-right:3px"
/>
</div>
<div @click="handleEditClick(item)">
<div @click="handleShowThemeSetting(item)">
<a-icon
type="setting"
style="margin-right:3px"
@ -90,7 +90,10 @@
/>
</span>
</a-menu-item>
<a-menu-item :key="2">
<a-menu-item
:key="2"
v-if="item.repo"
>
<a-popconfirm
:title="'确定更新【' + item.name + '】主题?'"
@confirm="handleUpdateTheme(item.id)"
@ -98,11 +101,20 @@
cancelText="取消"
>
<a-icon
type="download"
type="cloud"
style="margin-right:3px"
/>
/>线
</a-popconfirm>
</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-dropdown>
</template>
@ -111,157 +123,13 @@
</a-list>
</a-col>
</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
v-if="themeConfiguration.length>0"
:style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}"
>
<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>
<ThemeSetting
:theme="selectedTheme"
v-if="themeSettingVisible"
@close="onThemeSettingsClose"
/>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
</a-drawer>
<div class="upload-button">
<a-dropdown
placement="topLeft"
@ -278,7 +146,7 @@
<a
rel="noopener noreferrer"
href="javascript:void(0);"
@click="()=>this.uploadVisible = true"
@click="()=>this.uploadThemeVisible = true"
>安装主题</a>
</a-menu-item>
<a-menu-item>
@ -293,9 +161,11 @@
</div>
<a-modal
title="安装主题"
v-model="uploadVisible"
v-model="uploadThemeVisible"
destroyOnClose
:footer="null"
:bodyStyle="{ padding: '0 24px 24px' }"
:afterClose="onThemeUploadClose"
>
<div class="custom-tab-wrapper">
<a-tabs>
@ -333,59 +203,61 @@
tab="本地上传"
key="2"
>
<upload
<FilePondUpload
ref="upload"
name="file"
multiple
accept="application/zip"
label="点击选择主题包或将主题包拖拽到此处<br>仅支持 ZIP 格式的文件"
:uploadHandler="uploadHandler"
@change="handleChange"
@success="handleUploadSuccess"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">点击选择主题或将主题拖拽到此处</p>
<p class="ant-upload-hint">支持单个或批量上传仅支持 ZIP 格式的文件</p>
</upload>
</FilePondUpload>
</a-tab-pane>
</a-tabs>
</div>
</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>
</template>
<script>
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import ThemeSetting from './components/ThemeSetting'
import themeApi from '@/api/theme'
export default {
components: {
AttachmentDrawer,
FooterToolBar
ThemeSetting
},
mixins: [mixin, mixinDevice],
data() {
return {
themeLoading: false,
optionLoading: true,
uploadVisible: false,
uploadThemeVisible: false,
uploadNewThemeVisible: false,
fetchButtonLoading: false,
wrapperCol: {
xl: { span: 12 },
lg: { span: 12 },
sm: { span: 24 },
xs: { span: 24 }
},
attachmentDrawerVisible: false,
themes: [],
visible: false,
themeConfiguration: [],
themeSettings: [],
themeProperty: null,
themeSettingVisible: false,
selectedTheme: {},
fetchingUrl: null,
uploadHandler: themeApi.upload
uploadHandler: themeApi.upload,
updateByUploadHandler: themeApi.updateByUpload,
prepareUpdateTheme: {}
}
},
computed: {
@ -400,13 +272,13 @@ export default {
this.loadThemes()
},
destroyed: function() {
if (this.visible) {
this.visible = false
if (this.themeSettingVisible) {
this.themeSettingVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.visible) {
this.visible = false
if (this.themeSettingVisible) {
this.themeSettingVisible = false
}
next()
},
@ -418,21 +290,7 @@ export default {
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) {
themeApi.active(themeId).then(response => {
this.$message.success('设置成功!')
@ -451,32 +309,15 @@ export default {
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() {
this.uploadVisible = false
if (this.uploadThemeVisible) {
this.uploadThemeVisible = false
}
if (this.uploadNewThemeVisible) {
this.uploadNewThemeVisible = false
}
this.loadThemes()
},
handleEllipsisClick(theme) {
this.$log.debug('Ellipsis clicked', theme)
},
handleEditClick(theme) {
this.settingDrawer(theme)
},
@ -484,12 +325,19 @@ export default {
this.activeTheme(theme.id)
},
handleFetching() {
if (!this.fetchingUrl) {
this.$notification['error']({
message: '提示',
description: '远程地址不能为空!'
})
return
}
this.fetchButtonLoading = true
themeApi
.fetching(this.fetchingUrl)
.then(response => {
this.$message.success('拉取成功!')
this.uploadVisible = false
this.uploadThemeVisible = false
this.loadThemes()
})
.finally(() => {
@ -501,12 +349,33 @@ export default {
this.loadThemes()
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>
<style lang="less" scoped>
<style lang="less">
@keyframes scaleDraw {
0% {
transform: scale(1);

View File

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

View File

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

View File

@ -12,187 +12,47 @@
</div>
<div id="editor">
<mavon-editor
<halo-editor
ref="md"
v-model="postToStage.originalContent"
:boxShadow="false"
:toolbars="toolbars"
:ishljs="true"
:autofocus="false"
@imgAdd="pictureUploadHandle"
@imgAdd="handleAttachmentUpload"
@keydown.ctrl.83.native="handleSaveDraft"
@keydown.meta.83.native="handleSaveDraft"
/>
</div>
</a-col>
</a-row>
<a-drawer
title="文章设置"
:width="isMobile()?'100%':'460'"
placement="right"
closable
@close="()=>this.postSettingVisible=false"
<PostSetting
:post="postToStage"
:tagIds="selectedTagIds"
:categoryIds="selectedCategoryIds"
:visible="postSettingVisible"
>
<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+'/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>
@close="onPostSettingsClose"
@onRefreshPost="onRefreshPostFromSetting"
@onRefreshTagIds="onRefreshTagIdsFromSetting"
@onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
/>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
<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
type="primary"
@click="()=>this.postSettingVisible = true"
@click="handleShowPostSetting"
style="margin-left: 8px;"
>发布</a-button>
<a-button
type="dashed"
@ -204,32 +64,26 @@
</template>
<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 { toolbars } from '@/core/const'
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 { mapGetters } from 'vuex'
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 {
components: {
TagSelect,
mavonEditor,
CategoryTree,
FooterToolBar,
AttachmentDrawer,
AttachmentSelectDrawer,
CategorySelectTree
},
mixins: [mixin, mixinDevice],
components: {
PostSetting,
haloEditor,
FooterToolBar,
AttachmentDrawer
},
data() {
return {
toolbars,
@ -240,41 +94,14 @@ export default {
},
attachmentDrawerVisible: false,
postSettingVisible: false,
thumDrawerVisible: false,
categoryForm: false,
categories: [],
selectedCategoryIds: [],
selectedTagIds: [],
postToStage: {},
categoryToCreate: {},
timer: null,
options: [],
keys: ['blog_url']
selectedTagIds: [],
selectedCategoryIds: []
}
},
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) {
// Get post id from query
const postId = to.query.postId
next(vm => {
if (postId) {
postApi.get(postId).then(response => {
@ -286,122 +113,108 @@ export default {
}
})
},
computed: {
pickerDefaultValue() {
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')
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: {
...mapGetters(['options'])
},
methods: {
loadCategories() {
categoryApi.listAll().then(response => {
this.categories = response.data.data
})
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
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
handleSaveDraft() {
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, autoSave).then(response => {
postApi.update(this.postToStage.id, this.postToStage, false).then(response => {
this.$log.debug('Updated post', response.data.data)
if (updateSuccess) {
updateSuccess()
}
this.$message.success('保存草稿成功!')
})
} else {
// 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)
if (createSuccess) {
createSuccess()
}
this.$message.success('保存草稿成功!')
this.postToStage = response.data.data
})
}
},
savePost() {
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) {
handleAttachmentUpload(pos, $file) {
var formdata = new FormData()
formdata.append('file', $file)
attachmentApi.upload(formdata).then((response) => {
attachmentApi.upload(formdata).then(response => {
var responseObject = response.data
if (responseObject.status === 200) {
var MavonEditor = this.$refs.md
MavonEditor.$img2Url(pos, encodeURI(responseObject.data.path))
this.$message.success('图片上传成功')
var HaloEditor = this.$refs.md
HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path))
this.$message.success('图片上传成功!')
} else {
this.$message.error('图片上传失败:' + responseObject.message)
}
})
},
onChange(value, dateString) {
this.postToStage.createTime = value.valueOf()
handleShowPostSetting() {
this.postSettingVisible = true
},
onOk(value) {
this.postToStage.createTime = value.valueOf()
handlePreview() {
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>
<style lang="less" scoped>
.v-note-wrapper {
z-index: 1000;
min-height: 580px;
}
</style>

View File

@ -1,6 +1,9 @@
<template>
<div class="page-header-index-wide">
<a-card :bordered="false">
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
@ -89,7 +92,7 @@
</a-menu-item>
<a-menu-item
key="2"
v-if="queryParam.status === 'PUBLISHED' || queryParam.status ==='DRAFT'"
v-if="queryParam.status === 'PUBLISHED' || queryParam.status ==='DRAFT' || queryParam.status === 'INTIMATE'"
>
<a
href="javascript:void(0);"
@ -128,33 +131,68 @@
:loading="postsLoading"
:pagination="false"
>
<!-- ellipsis内嵌a标签后文本会被置空
<ellipsis
:length="25"
tooltip
slot="postTitle"
slot-scope="text,record"
>
{{ text }}
</ellipsis> -->
<span
slot="postTitle"
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
v-if="record.status=='PUBLISHED'"
:href="options.blog_url+'/archives/'+record.url"
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>
</span>
<span
slot="status"
slot-scope="statusProperty"
>
<a-badge :status="statusProperty.status" />
{{ statusProperty.text }}
<a-badge
:status="statusProperty.status"
:text="statusProperty.text"
/>
</span>
<span
@ -165,6 +203,7 @@
v-for="(category,index) in categoriesOfPost"
:key="index"
color="blue"
style="margin-bottom: 8px"
>{{ category.name }}</a-tag>
</span>
@ -176,13 +215,45 @@
v-for="(tag, index) in tags"
:key="index"
color="green"
style="margin-bottom: 8px"
>{{ tag.name }}</a-tag>
</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
slot="createTime"
slot-scope="createTime"
>{{ createTime | timeAgo }}</span>
>
<a-tooltip placement="top">
<template slot="title">
{{ createTime | moment }}
</template>
{{ createTime | timeAgo }}
</a-tooltip>
</span>
<span
slot="action"
@ -191,7 +262,7 @@
<a
href="javascript:;"
@click="handleEditClick(post)"
v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT'"
v-if="post.status === 'PUBLISHED' || post.status === 'DRAFT' || post.status === 'INTIMATE'"
>编辑</a>
<a-popconfirm
:title="'你确定要发布【' + post.title + '】文章?'"
@ -210,7 +281,7 @@
@confirm="handleEditStatusClick(post.id,'RECYCLE')"
okText="确定"
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-popconfirm>
@ -229,7 +300,7 @@
<a
href="javascript:;"
@click="handlePostSettingsDrawer(post)"
@click="handleShowPostSettings(post)"
>设置</a>
</span>
</a-table>
@ -246,164 +317,32 @@
</div>
</a-card>
<a-drawer
title="文章设置"
:width="isMobile()?'100%':'460'"
placement="right"
closable
@close="onPostSettingsClose"
<PostSetting
:post="selectedPost"
:tagIds="selectedTagIds"
:categoryIds="selectedCategoryIds"
:needTitle="true"
:saveDraftButton="false"
:savePublishButton="false"
:saveButton="true"
:visible="postSettingVisible"
>
<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="文章标题:">
<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>
@close="onPostSettingsClose"
@onRefreshPost="onRefreshPostFromSetting"
@onRefreshTagIds="onRefreshTagIdsFromSetting"
@onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
/>
</div>
</template>
<script>
import categoryApi from '@/api/category'
import postApi from '@/api/post'
import optionApi from '@/api/option'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import PostSetting from './components/PostSetting'
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import TagSelect from './components/TagSelect'
import CategoryTree from './components/CategoryTree'
import { mapGetters } from 'vuex'
import categoryApi from '@/api/category'
import postApi from '@/api/post'
const columns = [
{
title: '标题',
@ -415,10 +354,11 @@ const columns = [
title: '状态',
className: 'status',
dataIndex: 'statusProperty',
width: '100px',
scopedSlots: { customRender: 'status' }
},
{
title: '分类目录',
title: '分类',
dataIndex: 'categories',
scopedSlots: { customRender: 'categories' }
},
@ -428,16 +368,21 @@ const columns = [
scopedSlots: { customRender: 'tags' }
},
{
title: '评论量',
dataIndex: 'commentCount'
title: '评论',
width: '70px',
dataIndex: 'commentCount',
scopedSlots: { customRender: 'commentCount' }
},
{
title: '访问量',
dataIndex: 'visits'
title: '访问',
width: '70px',
dataIndex: 'visits',
scopedSlots: { customRender: 'visits' }
},
{
title: '发布时间',
dataIndex: 'createTime',
width: '170px',
scopedSlots: { customRender: 'createTime' }
},
{
@ -451,7 +396,8 @@ export default {
components: {
AttachmentSelectDrawer,
TagSelect,
CategoryTree
CategoryTree,
PostSetting
},
mixins: [mixin, mixinDevice],
data() {
@ -478,14 +424,9 @@ export default {
posts: [],
postsLoading: false,
postSettingVisible: false,
thumDrawerVisible: false,
selectedPost: {},
selectedCategoryIds: [],
selectedTagIds: [],
categoryForm: false,
categoryToCreate: {},
options: [],
keys: ['blog_url']
selectedCategoryIds: []
}
},
computed: {
@ -494,12 +435,23 @@ export default {
post.statusProperty = this.postStatus[post.status]
return post
})
}
},
...mapGetters(['options'])
},
created() {
this.loadCategories()
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: {
loadPosts() {
@ -519,11 +471,6 @@ export default {
this.categories = response.data.data
})
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handleEditClick(post) {
this.$router.push({ name: 'PostEdit', query: { postId: post.id } })
},
@ -553,6 +500,7 @@ export default {
},
handleQuery() {
this.queryParam.page = 0
this.pagination.current = 1
this.loadPosts()
},
handleEditStatusClick(postId, status) {
@ -609,61 +557,34 @@ export default {
})
}
},
//
handlePostSettingsDrawer(post) {
this.postSettingVisible = true
handleShowPostSettings(post) {
postApi.get(post.id).then(response => {
const post = response.data.data
this.selectedPost = post
this.selectedTagIds = post.tagIds
this.selectedCategoryIds = post.categoryIds
this.selectedPost = response.data.data
this.selectedTagIds = this.selectedPost.tagIds
this.selectedCategoryIds = this.selectedPost.categoryIds
this.postSettingVisible = true
})
},
handleSelectPostThumb(data) {
this.selectedPost.thumbnail = encodeURI(data.path)
this.thumDrawerVisible = false
},
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 = {}
handlePreview(postId) {
postApi.preview(postId).then(response => {
window.open(response.data, '_blank')
})
},
//
onPostSettingsClose() {
this.postSettingVisible = false
this.selectedPost = {}
this.selectedTagIds = []
this.selectedCategoryIds = []
this.loadPosts()
},
onRefreshPostFromSetting(post) {
this.selectedPost = post
},
onRefreshTagIdsFromSetting(tagIds) {
this.selectedTagIds = tagIds
},
onRefreshCategoryIdsFromSetting(categoryIds) {
this.selectedCategoryIds = categoryIds
}
}
}
</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"
:style="{ 'padding-bottom': '12px' }"
>
<a-card :title="title">
<a-card :title="title" :bodyStyle="{ padding: '16px' }">
<a-form layout="horizontal">
<a-form-item
label="名称:"
@ -64,7 +64,7 @@
:xs="24"
:style="{ 'padding-bottom': '12px' }"
>
<a-card title="所有标签">
<a-card title="所有标签" :bodyStyle="{ padding: '16px' }">
<a-tooltip
placement="topLeft"
v-for="tag in tags"
@ -132,6 +132,13 @@ export default {
})
},
createOrUpdateTag() {
if (!this.tagToCreate.name) {
this.$notification['error']({
message: '提示',
description: '标签名称不能为空!'
})
return
}
if (this.tagToCreate.id) {
tagApi.update(this.tagToCreate.id, this.tagToCreate).then(response => {
this.$message.success('更新成功!')

View File

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

View File

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

View File

@ -11,122 +11,42 @@
/>
</div>
<div id="editor">
<mavon-editor
<halo-editor
ref="md"
v-model="sheetToStage.originalContent"
:boxShadow="false"
:toolbars="toolbars"
:ishljs="true"
:autofocus="false"
@imgAdd="pictureUploadHandle"
@imgAdd="handleAttachmentUpload"
@keydown.ctrl.83.native="handleSaveDraft"
@keydown.meta.83.native="handleSaveDraft"
/>
</div>
</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>
<SheetSetting
:sheet="sheetToStage"
:visible="sheetSettingVisible"
@close="onSheetSettingsClose"
@onRefreshSheet="onRefreshSheetFromSetting"
/>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
<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
type="primary"
@click="()=>this.sheetSettingVisible = true"
style="margin-left: 8px;"
@click="handleShowSheetSetting"
>发布</a-button>
<a-button
type="dashed"
@ -138,24 +58,23 @@
</template>
<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 { toolbars } from '@/core/const'
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 { mapGetters } from 'vuex'
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 {
components: {
mavonEditor,
haloEditor,
FooterToolBar,
AttachmentDrawer,
AttachmentSelectDrawer
SheetSetting
},
mixins: [mixin, mixinDevice],
data() {
@ -167,34 +86,10 @@ export default {
xs: { span: 24 }
},
attachmentDrawerVisible: false,
thumDrawerVisible: false,
sheetSettingVisible: false,
customTpls: [],
sheetToStage: {},
timer: null,
options: [],
keys: ['blog_url']
sheetToStage: {}
}
},
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) {
// Get sheetId id from query
const sheetId = to.query.sheetId
@ -208,116 +103,97 @@ export default {
}
})
},
computed: {
pickerDefaultValue() {
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')
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: {
...mapGetters(['options'])
},
methods: {
loadCustomTpls() {
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() {
handleSaveDraft() {
this.sheetToStage.status = 'DRAFT'
this.saveSheet()
},
handlerRemoveThumb() {
this.sheetToStage.thumbnail = null
},
createOrUpdateSheet(createSuccess, updateSuccess, autoSave) {
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, autoSave).then(response => {
sheetApi.update(this.sheetToStage.id, this.sheetToStage, false).then(response => {
this.$log.debug('Updated sheet', response.data.data)
if (updateSuccess) {
updateSuccess()
}
this.$message.success('保存草稿成功!')
})
} else {
sheetApi.create(this.sheetToStage, autoSave).then(response => {
sheetApi.create(this.sheetToStage, false).then(response => {
this.$log.debug('Created sheet', response.data.data)
if (createSuccess) {
createSuccess()
}
this.$message.success('保存草稿成功!')
this.sheetToStage = response.data.data
})
}
},
saveSheet() {
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) {
handleAttachmentUpload(pos, $file) {
var formdata = new FormData()
formdata.append('file', $file)
attachmentApi.upload(formdata).then(response => {
var responseObject = response.data
if (responseObject.status === 200) {
var MavonEditor = this.$refs.md
MavonEditor.$img2Url(pos, encodeURI(responseObject.data.path))
this.$message.success('图片上传成功')
var HaloEditor = this.$refs.md
HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path))
this.$message.success('图片上传成功!')
} else {
this.$message.error('图片上传失败:' + responseObject.message)
}
})
},
onChange(value, dateString) {
this.sheetToStage.createTime = value.valueOf()
handleShowSheetSetting() {
this.sheetSettingVisible = true
},
onOk(value) {
this.sheetToStage.createTime = value.valueOf()
handlePreview() {
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>
<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" />内置页面
</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
:columns="internalColumns"
:dataSource="internalSheets"
@ -95,33 +75,90 @@
:columns="customColumns"
:dataSource="formattedSheets"
:pagination="false"
:loading="sheetsLoading"
>
<span
slot="sheetTitle"
slot-scope="text,record"
class="sheet-title"
style="max-width: 150px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
>
<a
v-if="record.status=='PUBLISHED'"
:href="options.blog_url+'/s/'+record.url"
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"
:title="'点击预览【'+text+'】'"
>{{ text }}</a-tooltip>
</a>
<a
v-else
href="javascript:void(0);"
style="text-decoration: none;"
disabled
>
{{ text }}
</a>
</span>
<span
slot="status"
slot-scope="statusProperty"
>
<a-badge :status="statusProperty.status" />
{{ statusProperty.text }}
<a-badge
:status="statusProperty.status"
:text="statusProperty.text"
/>
</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
slot="createTime"
slot-scope="createTime"
>{{ createTime | timeAgo }}</span>
>
<a-tooltip placement="top">
<template slot="title">
{{ createTime | moment }}
</template>
{{ createTime | timeAgo }}
</a-tooltip>
</span>
<span
slot="action"
@ -172,6 +209,12 @@
>更多</a>
<a-menu slot="overlay">
<a-menu-item key="1">
<a
href="javascript:void(0);"
@click="handleShowSheetSettings(sheet)"
>设置</a>
</a-menu-item>
<a-menu-item key="2">
<a-popconfirm
:title="'你确定要添加【' + sheet.title + '】到菜单?'"
@confirm="handleSheetToMenu(sheet)"
@ -190,13 +233,23 @@
</div>
</a-col>
</a-row>
<SheetSetting
:sheet="selectedSheet"
:visible="sheetSettingVisible"
:needTitle="true"
@close="onSheetSettingsClose"
@onRefreshSheet="onRefreshSheetFromSetting"
/>
</div>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import SheetSetting from './components/SheetSetting'
import sheetApi from '@/api/sheet'
import optionApi from '@/api/option'
import menuApi from '@/api/menu'
const internalColumns = [
@ -234,11 +287,13 @@ const customColumns = [
},
{
title: '评论量',
dataIndex: 'commentCount'
dataIndex: 'commentCount',
scopedSlots: { customRender: 'commentCount' }
},
{
title: '访问量',
dataIndex: 'visits'
dataIndex: 'visits',
scopedSlots: { customRender: 'visits' }
},
{
title: '发布时间',
@ -253,16 +308,20 @@ const customColumns = [
]
export default {
mixins: [mixin, mixinDevice],
components: {
SheetSetting
},
data() {
return {
sheetsLoading: false,
sheetStatus: sheetApi.sheetStatus,
internalColumns,
customColumns,
selectedSheet: {},
sheetSettingVisible: false,
internalSheets: [],
sheets: [],
options: [],
menu: {},
keys: ['blog_url']
menu: {}
}
},
computed: {
@ -271,17 +330,30 @@ export default {
sheet.statusProperty = this.sheetStatus[sheet.status]
return sheet
})
}
},
...mapGetters(['options'])
},
created() {
this.loadSheets()
this.loadInternalSheets()
this.loadOptions()
},
destroyed: function() {
if (this.sheetSettingVisible) {
this.sheetSettingVisible = false
}
},
beforeRouteLeave(to, from, next) {
if (this.sheetSettingVisible) {
this.sheetSettingVisible = false
}
next()
},
methods: {
loadSheets() {
this.sheetsLoading = true
sheetApi.list().then(response => {
this.sheets = response.data.data.content
this.sheetsLoading = false
})
},
loadInternalSheets() {
@ -289,11 +361,6 @@ export default {
this.internalSheets = response.data.data
})
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
handleEditClick(sheet) {
this.$router.push({ name: 'SheetEdit', query: { sheetId: sheet.id } })
},
@ -316,19 +383,26 @@ export default {
this.$message.success('添加到菜单成功!')
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>
<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,36 +2,65 @@
<div class="page-header-index-wide">
<a-row>
<a-col :span="24">
<a-card :bordered="false">
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
<a-col :md="6" :sm="24">
<a-col
:md="6"
:sm="24"
>
<a-form-item label="关键词">
<a-input v-model="queryParam.keyword"/>
<a-input v-model="queryParam.keyword" />
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-col
:md="6"
:sm="24"
>
<a-form-item label="状态">
<a-select placeholder="请选择状态">
<a-select-option value="1">公开</a-select-option>
<a-select-option value="0">私密</a-select-option>
<a-select
placeholder="请选择状态"
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-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-col
:md="6"
:sm="24"
>
<span class="table-page-search-submitButtons">
<a-button type="primary" @click="loadJournals(true)"></a-button>
<a-button style="margin-left: 8px;" @click="resetParam"></a-button>
<a-button
type="primary"
@click="loadJournals(true)"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="resetParam"
>重置</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<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>
<a-divider/>
<a-divider />
<div style="margin-top:15px">
<a-list
itemLayout="vertical"
@ -39,7 +68,11 @@
:dataSource="journals"
: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
hoverable
@ -51,34 +84,63 @@
<img alt="example" :src="photo.thumbnail" slot="cover">
</a-card> -->
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancelPreview">
<!-- <a-modal
:visible="previewVisible"
:footer="null"
@cancel="handleCancelPreview"
>
<img
:alt="previewPhoto.name + previewPhoto.description"
style="width: 100%"
:src="previewPhoto.url"
>
</a-modal>
</a-modal> -->
<template slot="actions">
<span>
<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 }}
</a>
</span>
<span>
<a href="javascript:void(0);" @click="handleCommentShow(item)">
<a-icon type="message" style="margin-right: 8px"/>
<a
href="javascript:void(0);"
@click="handleCommentShow(item)"
>
<a-icon
type="message"
style="margin-right: 8px"
/>
{{ item.commentCount }}
</a>
</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>
From 微信
</span>-->
</template>
<template slot="extra">
<a href="javascript:void(0);" @click="handleEdit(item)"></a>
<a-divider type="vertical"/>
<a
href="javascript:void(0);"
@click="handleEdit(item)"
>编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="你确定要删除这条日志?"
@confirm="handleDelete(item.id)"
@ -91,7 +153,11 @@
<a-list-item-meta :description="item.content">
<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>
<div class="page-wrapper">
@ -115,16 +181,35 @@
<a-modal v-model="visible">
<template slot="title">
{{ title }}
<a-tooltip slot="action" title="只能输入250字">
<a-icon type="info-circle-o"/>
<a-tooltip
slot="action"
title="只能输入250字"
>
<a-icon type="info-circle-o" />
</a-tooltip>
</template>
<template slot="footer">
<a-button key="submit" type="primary" @click="createOrUpdateJournal"></a-button>
<a-button
key="submit"
type="primary"
@click="createOrUpdateJournal"
>发布</a-button>
</template>
<a-form layout="vertical">
<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 v-show="showMoreOptions">
<UploadPhoto
@ -154,11 +239,19 @@
v-model="selectCommentVisible"
>
<template slot="footer">
<a-button key="submit" type="primary" @click="handleReplyComment"></a-button>
<a-button
key="submit"
type="primary"
@click="handleReplyComment"
>回复</a-button>
</template>
<a-form layout="vertical">
<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>
</a-modal>
@ -168,20 +261,27 @@
title="评论列表"
:width="isMobile()?'100%':'460'"
closable
:visible="commentVisiable"
:visible="commentVisible"
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-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>
<span slot="datetime">{{ journal.createTime | moment }}</span>
</a-comment>
</a-col>
<a-divider/>
<a-divider />
<a-col :span="24">
<journal-comment-tree
v-for="(comment,index) in comments"
@ -199,29 +299,30 @@
<script>
import JournalCommentTree from './components/JournalCommentTree'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import journalApi from '@/api/journal'
import journalCommentApi from '@/api/journalComment'
import userApi from '@/api/user'
import UploadPhoto from '@/components/Upload/UploadPhoto.vue'
export default {
mixins: [mixin, mixinDevice],
components: { JournalCommentTree, UploadPhoto },
data() {
return {
plusPhotoVisible: true,
photoList: [], //
previewVisible: false,
journalType: journalApi.journalType,
// plusPhotoVisible: true,
// photoList: [], //
// previewVisible: false,
showMoreOptions: false,
previewPhoto: {
//
name: '',
description: '',
url: ''
},
// previewPhoto: {
// //
// name: '',
// description: '',
// url: ''
// },
title: '发表',
listLoading: false,
visible: false,
commentVisiable: false,
commentVisible: false,
selectCommentVisible: false,
pagination: {
page: 1,
@ -232,50 +333,49 @@ export default {
page: 0,
size: 10,
sort: null,
keyword: null
keyword: null,
type: null
},
journals: [],
comments: [],
journal: {
id: undefined,
content: '',
photos: []
},
journal: {},
isPublic: true,
journalPhotos: [], //
selectComment: null,
replyComment: {},
user: {}
replyComment: {}
}
},
created() {
this.loadJournals()
this.loadUser()
},
computed: {
...mapGetters(['user'])
},
methods: {
handleCancelPreview() {
this.previewVisible = false
},
handlerPhotoPreview(photo) {
//
this.previewVisible = true
this.previewPhoto = photo
},
handlerPhotoUploadSuccess(response, file) {
var callData = response.data.data
var photo = {
name: callData.name,
url: callData.path,
thumbnail: callData.thumbPath,
suffix: callData.suffix,
width: callData.width,
height: callData.height
}
this.journalPhotos.push(photo)
},
handleUploadPhotoWallClick() {
//
this.showMoreOptions = !this.showMoreOptions
},
// handleCancelPreview() {
// this.previewVisible = false
// },
// handlerPhotoPreview(photo) {
// //
// this.previewVisible = true
// this.previewPhoto = photo
// },
// handlerPhotoUploadSuccess(response, file) {
// var callData = response.data.data
// var photo = {
// name: callData.name,
// url: callData.path,
// thumbnail: callData.thumbPath,
// suffix: callData.suffix,
// width: callData.width,
// height: callData.height
// }
// this.journalPhotos.push(photo)
// },
// handleUploadPhotoWallClick() {
// //
// this.showMoreOptions = !this.showMoreOptions
// },
loadJournals(isSearch) {
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
@ -290,28 +390,23 @@ export default {
this.listLoading = false
})
},
loadUser() {
userApi.getProfile().then(response => {
this.user = response.data.data
})
},
handleNew() {
this.title = '新建'
this.visible = true
this.journal = {}
//
this.plusPhotoVisible = true
this.photoList = []
// this.plusPhotoVisible = true
// this.photoList = []
},
handleEdit(item) {
this.title = '编辑'
this.journal = item
this.isPublic = item.type !== 'INTIMATE'
this.visible = true
// ,
this.plusPhotoVisible = false
this.photoList = item.photos
// this.plusPhotoVisible = false
// this.photoList = item.photos
},
handleDelete(id) {
journalApi.delete(id).then(response => {
@ -323,7 +418,7 @@ export default {
this.journal = journal
journalApi.commentTree(this.journal.id).then(response => {
this.comments = response.data.data.content
this.commentVisiable = true
this.commentVisible = true
})
},
handleCommentReplyClick(comment) {
@ -349,18 +444,29 @@ export default {
},
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) {
journalApi.update(this.journal.id, this.journal).then(response => {
this.$message.success('更新成功!')
this.loadJournals()
this.isPublic = true
})
} else {
journalApi.create(this.journal).then(response => {
this.$message.success('发表成功!')
this.loadJournals()
this.photoList = []
// this.photoList = []
this.isPublic = true
})
}
this.visible = false
@ -373,21 +479,21 @@ export default {
},
resetParam() {
this.queryParam.keyword = null
this.queryParam.type = null
this.loadJournals()
}
}
}
</script>
<style scoped="scoped">
.more-options-btn {
/* .more-options-btn {
margin-left: 15px;
text-decoration: none;
}
/* 日志图片卡片样式 */
.photo-card {
width: 104px;
display: inline-block;
margin-right: 5px;
}
} */
</style>

View File

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

View File

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

View File

@ -2,10 +2,14 @@
<div class="page-header-index-wide">
<a-row>
<a-col :span="24">
<a-card :bordered="false">
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<a-card
:bordered="false"
class="environment-info"
:bodyStyle="{ padding: '16px' }"
>
<template slot="title">
环境信息
@ -39,7 +43,7 @@
</a-button>
</a-popconfirm>
<ul>
<ul style="margin: 0;padding: 0;list-style: none;">
<li>Server 版本{{ environments.version }}</li>
<li>Admin 版本{{ adminVersion }}</li>
<li>数据库{{ environments.database }}</li>
@ -50,16 +54,19 @@
<a
href="https://github.com/halo-dev"
target="_blank"
style="margin-right: 10px;"
>开源地址
<a-icon type="link" /></a>
<a
href="https://halo.run/guide"
target="_blank"
style="margin-right: 10px;"
>用户文档
<a-icon type="link" /></a>
<a
href="https://bbs.halo.run"
target="_blank"
style="margin-right: 10px;"
>在线社区
<a-icon type="link" /></a>
</a-card>
@ -67,6 +74,7 @@
<a-card
title="开发者"
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<a
:href="item.github"
@ -90,6 +98,7 @@
<a-card
title="时间轴"
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<a-timeline>
<a-timeline-item>...</a-timeline-item>
@ -107,6 +116,7 @@
<script>
import adminApi from '@/api/admin'
import axios from 'axios'
export default {
data() {
return {
@ -145,6 +155,10 @@ export default {
}
],
steps: [
{
date: '2019-09-11',
content: 'Halo v1.1.0 发布'
},
{
date: '2019-07-09',
content: 'Halo v1.0.3 发布'
@ -199,6 +213,7 @@ export default {
},
created() {
this.getEnvironments()
this.checkUpdate()
},
computed: {
updateText() {
@ -229,7 +244,8 @@ export default {
const text = `Server 版本:${this.environments.version}
Admin 版本${this.adminVersion}
数据库${this.environments.database}
运行模式${this.environments.mode}`
运行模式${this.environments.mode}
UA 信息${navigator.userAgent}`
this.$copyText(text)
.then(message => {
console.log('copy', message)
@ -239,25 +255,64 @@ Admin 版本:${this.adminVersion}
console.log('copy.err', err)
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>
<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>
<div>
<a-row
class="height-100"
type="flex"
justify="center"
align="middle"
style="height: 100vh;"
>
<a-col
:xl="8"
@ -16,7 +16,7 @@
<a-card
:bordered="false"
title="Halo 安装向导"
class="install-card"
style="box-shadow: 0px 10px 20px 0px rgba(236, 236, 236, 0.86);"
>
<a-steps :current="stepCurrent">
@ -91,7 +91,7 @@
<a-input
v-model="installation.password"
type="password"
placeholder="用户密码"
placeholder="用户密码8-100位"
v-decorator="[
'password',
{rules: [{ required: true, message: '请输入密码8-100位' }]}
@ -189,12 +189,14 @@
class="install-action"
type="flex"
justify="space-between"
style="margin-top: 1rem;"
>
<div>
<a-button
class="previus-button"
v-if="stepCurrent != 0"
@click="stepCurrent--"
style="margin-right: 1rem;"
>上一步</a-button>
<a-button
type="primary"
@ -218,7 +220,6 @@
<script>
import adminApi from '@/api/admin'
import optionApi from '@/api/option'
import recoveryApi from '@/api/recovery'
export default {
@ -244,8 +245,7 @@ export default {
migrationUploadName: 'file',
migrationData: null,
stepCurrent: 0,
bloggerForm: this.$form.createForm(this),
keys: ['is_installed']
bloggerForm: this.$form.createForm(this)
}
},
created() {
@ -254,8 +254,8 @@ export default {
},
methods: {
verifyIsInstall() {
optionApi.listAll(this.keys).then(response => {
if (response.data.data.is_installed) {
adminApi.isInstalled().then(response => {
if (response.data.data) {
this.$router.push({ name: 'Login' })
}
})
@ -292,7 +292,7 @@ export default {
this.$log.debug('Installation response', response)
this.$message.success('安装成功!')
setTimeout(() => {
this.$router.push({ name: 'Dashboard' })
this.$router.push({ name: 'Login' })
}, 300)
})
},
@ -322,20 +322,3 @@ export default {
}
}
</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"
:autosize="{ minRows: 5 }"
v-model="options.blog_footer_info"
placeholder="支持 HTML 格式的文本"
/>
</a-form-item>
<a-form-item>
@ -85,26 +86,21 @@
label="关键词: "
:wrapper-col="wrapperCol"
>
<a-tooltip
:trigger="['focus']"
placement="right"
title="多个关键词以英文逗号隔开"
>
<a-input v-model="options.seo_keywords" />
</a-tooltip>
<a-input
v-model="options.seo_keywords"
placeholder="多个关键词以英文状态下的逗号隔开"
/>
</a-form-item>
<a-form-item
label="博客描述:"
: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
label="百度推送 Token "
:wrapper-col="wrapperCol"
>
<a-input v-model="options.seo_baidu_token" />
</a-form-item> -->
<a-form-item>
<a-button
type="primary"
@ -118,6 +114,16 @@
<a-icon type="form" />文章设置
</span>
<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
label="首页显示条数:"
:wrapper-col="wrapperCol"
@ -196,6 +202,17 @@
>
<a-switch v-model="options.comment_api_enabled" />
</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
label="每页显示条数: "
:wrapper-col="wrapperCol"
@ -234,6 +251,30 @@
<a-icon type="picture" />附件设置
</span>
<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
label="存储位置:"
:wrapper-col="wrapperCol"
@ -250,20 +291,31 @@
</a-select>
</a-form-item>
<div
class="upyunForm"
v-show="upyunFormHidden"
class="smmsForm"
v-show="smmsFormVisible"
>
<a-form-item
label="域名"
label="Secret Token"
:wrapper-col="wrapperCol"
>
<a-tooltip
:trigger="['focus']"
placement="right"
title="需要加上 http:// 或者 https://"
>
<a-input v-model="options.oss_upyun_domain" />
</a-tooltip>
<a-input
v-model="options.smms_api_secret_token"
placeholder="需要到 sm.ms 官网注册后获取"
/>
</a-form-item>
</div>
<div
class="upyunForm"
v-show="upyunFormVisible"
>
<a-form-item
label="绑定域名:"
:wrapper-col="wrapperCol"
>
<a-input
v-model="options.oss_upyun_domain"
placeholder="需要加上 http:// 或者 https://"
/>
</a-form-item>
<a-form-item
label="空间名称:"
@ -292,17 +344,38 @@
>
<a-input v-model="options.oss_upyun_source" />
</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
label="缩略图处理策略:"
: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>
</div>
<div
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
label="区域:"
:wrapper-col="wrapperCol"
@ -316,18 +389,6 @@
<a-select-option value="as0">东南亚</a-select-option>
</a-select>
</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
label="Access Key"
:wrapper-col="wrapperCol"
@ -338,30 +399,60 @@
label="Secret Key"
: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
label="Bucket"
: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
label="缩略图处理策略:"
: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>
</div>
<div
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
label="Bucket"
: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
label="EndPoint地域节点"
@ -379,65 +470,119 @@
label="Access Secret"
: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
label="缩略图处理策略:"
: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>
</div>
<div
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
label="Bucket"
: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
label="EndPoint地域节点"
: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
label="Access Key"
: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
label="Access Secret"
label="Secret Key"
: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
label="缩略图处理策略:"
: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>
</div>
<div
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
label="Bucket"
: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
label="区域:"
: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">北京</a-select-option>
<a-select-option value="ap-shanghai">上海华东</a-select-option>
@ -450,19 +595,16 @@
label="Secret Id"
: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
label="Secret Key"
:wrapper-col="wrapperCol"
>
<a-input v-model="options.oss_tencentyun_access_secret" />
</a-form-item>
<a-form-item
label="缩略图处理策略:"
:wrapper-col="wrapperCol"
>
<a-input v-model="options.oss_tencentyun_style_rule" />
<a-input
type="password"
v-model="options.cos_tencentyun_secret_key"
/>
</a-form-item>
</div>
<a-form-item>
@ -518,13 +660,11 @@
label="邮箱密码:"
:wrapper-col="wrapperCol"
>
<a-tooltip
:trigger="['focus']"
placement="right"
title="部分邮箱可能是授权码"
>
<a-input v-model="options.email_password" />
</a-tooltip>
<a-input
v-model="options.email_password"
type="password"
placeholder="部分邮箱可能是授权码"
/>
</a-form-item>
<a-form-item
label="发件人:"
@ -580,7 +720,7 @@
</a-tab-pane>
<a-tab-pane key="api">
<span slot="tab">
<a-icon type="align-left" />API 设置
<a-icon type="thunderbolt" />API 设置
</span>
<a-form layout="vertical">
<a-form-item
@ -608,6 +748,15 @@
<a-icon type="align-left" />其他设置
</span>
<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
label="自定义 head"
:wrapper-col="wrapperCol"
@ -616,6 +765,7 @@
type="textarea"
:autosize="{ minRows: 5 }"
v-model="options.blog_custom_head"
placeholder="将放置于每个页面的<head></head>标签中"
/>
</a-form-item>
<a-form-item
@ -626,8 +776,20 @@
type="textarea"
:autosize="{ minRows: 5 }"
v-model="options.blog_statistics_code"
placeholder="第三方网站统计的代码Google Analytics、百度统计、CNZZ 等"
/>
</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-button
type="primary"
@ -655,10 +817,10 @@
</template>
<script>
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import { mapActions } from 'vuex'
import optionApi from '@/api/option'
import mailApi from '@/api/mail'
import attachmentApi from '@/api/attachment'
import { mapActions } from 'vuex'
export default {
components: {
@ -673,11 +835,12 @@ export default {
sm: { span: 12 },
xs: { span: 24 }
},
upyunFormHidden: false,
qnyunFormHidden: false,
aliyunFormHidden: false,
baiduyunFormHidden: false,
tencentyunFormHidden: false,
smmsFormVisible: false,
upyunFormVisible: false,
qnyunFormVisible: false,
aliyunFormVisible: false,
baiduyunFormVisible: false,
tencentyunFormVisible: false,
logoDrawerVisible: false,
faviconDrawerVisible: false,
options: [],
@ -685,18 +848,290 @@ export default {
}
},
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: {
...mapActions(['loadUser']),
loadOptions() {
...mapActions(['loadUser', 'loadOptions']),
loadFormOptions() {
optionApi.listAll().then(response => {
this.options = response.data.data
this.handleAttachChange(this.options['attachment_type'])
})
},
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 => {
this.loadFormOptions()
this.loadOptions()
this.loadUser()
this.$message.success('保存成功!')
@ -705,47 +1140,60 @@ export default {
handleAttachChange(e) {
switch (e) {
case 'LOCAL':
this.upyunFormVisible = false
this.qnyunFormVisible = false
this.aliyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
this.smmsFormVisible = false
break
case 'SMMS':
this.upyunFormHidden = false
this.qnyunFormHidden = false
this.aliyunFormHidden = false
this.baiduyunFormHidden = false
this.tencentyunFormHidden = false
this.smmsFormVisible = true
this.upyunFormVisible = false
this.qnyunFormVisible = false
this.aliyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break
case 'UPYUN':
this.upyunFormHidden = true
this.qnyunFormHidden = false
this.aliyunFormHidden = false
this.baiduyunFormHidden = false
this.tencentyunFormHidden = false
this.smmsFormVisible = false
this.upyunFormVisible = true
this.qnyunFormVisible = false
this.aliyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break
case 'QNYUN':
this.qnyunFormHidden = true
this.upyunFormHidden = false
this.aliyunFormHidden = false
this.baiduyunFormHidden = false
this.tencentyunFormHidden = false
this.smmsFormVisible = false
this.qnyunFormVisible = true
this.upyunFormVisible = false
this.aliyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break
case 'ALIYUN':
this.aliyunFormHidden = true
this.qnyunFormHidden = false
this.upyunFormHidden = false
this.baiduyunFormHidden = false
this.tencentyunFormHidden = false
this.smmsFormVisible = false
this.aliyunFormVisible = true
this.qnyunFormVisible = false
this.upyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = false
break
case 'BAIDUYUN':
this.aliyunFormHidden = false
this.qnyunFormHidden = false
this.upyunFormHidden = false
this.baiduyunFormHidden = true
this.tencentyunFormHidden = false
this.smmsFormVisible = false
this.aliyunFormVisible = false
this.qnyunFormVisible = false
this.upyunFormVisible = false
this.baiduyunFormVisible = true
this.tencentyunFormVisible = false
break
case 'TENCENTYUN':
this.aliyunFormHidden = false
this.qnyunFormHidden = false
this.upyunFormHidden = false
this.baiduyunFormHidden = false
this.tencentyunFormHidden = true
this.smmsFormVisible = false
this.aliyunFormVisible = false
this.qnyunFormVisible = false
this.upyunFormVisible = false
this.baiduyunFormVisible = false
this.tencentyunFormVisible = true
break
}
},
@ -754,6 +1202,27 @@ export default {
this.logoDrawerVisible = false
},
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 => {
this.$message.info(response.data.message)
})

View File

@ -27,20 +27,16 @@
title="Markdown 文章导入"
v-model="markdownUpload"
:footer="null"
destroyOnClose
:afterClose="onUploadClose"
>
<upload
name="files"
multiple
<FilePondUpload
ref="upload"
name="file"
accept="text/markdown"
label="拖拽或点击选择 Markdown 文件到此处"
:uploadHandler="uploadHandler"
@change="handleChange"
>
<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>
></FilePondUpload>
</a-modal>
</div>
</div>
@ -69,10 +65,10 @@ export default {
} else if (status === 'error') {
this.$message.error(`${info.file.name} 导入失败!`)
}
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
}
}
}
</script>
<style scoped>
</style>

View File

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

View File

@ -6,7 +6,10 @@
:md="24"
:style="{ 'padding-bottom': '12px' }"
>
<a-card :bordered="false">
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div class="profile-center-avatarHolder">
<a-tooltip
placement="right"
@ -149,8 +152,7 @@
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import userApi from '@/api/user'
import adminApi from '@/api/admin'
import optionApi from '@/api/option'
import { mapMutations } from 'vuex'
import { mapMutations, mapGetters } from 'vuex'
import MD5 from 'md5.js'
export default {
@ -168,20 +170,18 @@ export default {
newPassword: null,
confirmPassword: null
},
attachment: {},
options: [],
keys: ['blog_url']
attachment: {}
}
},
computed: {
passwordUpdateButtonDisabled() {
return !(this.passwordParam.oldPassword && this.passwordParam.newPassword)
}
},
...mapGetters(['options'])
},
created() {
this.loadUser()
this.getCounts()
this.loadOptions()
},
methods: {
...mapMutations({ setUser: 'SET_USER' }),
@ -191,11 +191,6 @@ export default {
this.profileLoading = false
})
},
loadOptions() {
optionApi.listAll(this.keys).then(response => {
this.options = response.data.data
})
},
getCounts() {
adminApi.counts().then(response => {
this.counts = response.data.data
@ -208,9 +203,35 @@ export default {
this.$message.error('确认密码和新密码不匹配!')
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() {
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 => {
this.user = response.data.data
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 webpack = require('webpack')
// const GenerateAssetPlugin = require('generate-asset-webpack-plugin')
function resolve(dir) {
return path.join(__dirname, dir)
}
// var createServerConfig = function(compilation) {
// const configJson = {
// apiUrl: 'http://localhost:8090'
// }
// return JSON.stringify(configJson)
// }
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
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: {
loaderOptions: {
less: {
modifyVars: {
/*
'primary-color': '#F5222D',
'link-color': '#F5222D',
'border-radius-base': '4px',
*/
},
javascriptEnabled: true
}
}