release: 1.2.0. (#59)

release: 1.2.0.

Co-authored-by: Ryan Wang <i@ryanc.cc>
Co-authored-by: John Niang <johnniang@foxmail.com>
pull/64/head v1.2.0
Ryan Wang 2020-01-05 22:45:45 +08:00 committed by GitHub
commit 92e965aed1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 8562 additions and 8767 deletions

4
.npmignore Normal file
View File

@ -0,0 +1,4 @@
/node_modules/*
/.idea/*
/.git/*
/.github/*

View File

@ -22,3 +22,6 @@ branches:
only:
- master
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/
notifications:
webhooks: https://fathomless-fjord-24024.herokuapp.com/notify

11040
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
{
"name": "halo-admin",
"version": "1.1.2",
"private": true,
"version": "1.2.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
@ -9,25 +8,23 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"animate.css": "^3.7.0",
"ant-design-vue": "^1.4.1",
"axios": "^0.18.0",
"ant-design-vue": "^1.4.10",
"axios": "^0.19.0",
"enquire.js": "^2.1.6",
"filepond": "^4.7.2",
"filepond-plugin-image-preview": "^4.5.0",
"halo-editor": "^2.7.6",
"marked": "^0.6.3",
"filepond": "^4.9.2",
"filepond-plugin-image-preview": "^4.6.0",
"halo-editor": "^2.8.2",
"marked": "^0.8.0",
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"verte": "^0.0.12",
"vue": "^2.6.10",
"vue": "^2.6.11",
"vue-clipboard2": "^0.3.0",
"vue-codemirror-lite": "^1.0.4",
"vue-count-to": "^1.0.13",
"vue-filepond": "^5.1.3",
"vue-dplayer": "0.0.10",
"vue-filepond": "^6.0.0",
"vue-ls": "^3.2.1",
"vue-router": "^3.1.3",
"vue-video-player": "^5.0.2",
"vuejs-logger": "^1.5.3",
"vuex": "^3.1.1"
},
@ -35,7 +32,7 @@
"@babel/polyfill": "^7.4.4",
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-eslint": "^3.8.0",
"@vue/cli-plugin-unit-jest": "^3.8.0",
"@vue/cli-plugin-unit-jest": "^4.1.1",
"@vue/cli-service": "^3.8.0",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.20",
@ -46,11 +43,13 @@
"eslint": "^5.16.0",
"eslint-plugin-html": "^5.0.5",
"eslint-plugin-vue": "^5.2.3",
"flv.js": "^1.5.0",
"generate-asset-webpack-plugin": "^0.3.0",
"less": "^3.10.0",
"less-loader": "^5.0.0",
"vue-svg-component-runtime": "^1.0.1",
"vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.10"
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
@ -126,5 +125,16 @@
"> 1%",
"last 2 versions",
"not ie <= 10"
]
],
"description": "Halo admin client.",
"repository": {
"type": "git",
"url": "git+https://github.com/halo-dev/halo-admin.git"
},
"author": "halo-dev",
"license": "ISC",
"bugs": {
"url": "https://github.com/halo-dev/halo-admin/issues"
},
"homepage": "https://github.com/halo-dev/halo-admin#readme"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -6,12 +6,16 @@
<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" />
<meta name="generator" content="Halo 1.2.0" />
<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)}}
body {height: 100%;background-color: #f5f5f5;}#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>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
</head>
<body>
@ -22,6 +26,10 @@
<div id="app">
<div id="loader"></div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>

84
src/api/actuator.js Normal file
View File

@ -0,0 +1,84 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/actuator'
const actuatorApi = {}
actuatorApi.logfile = () => {
return service({
url: `${baseUrl}/logfile`,
method: 'get'
})
}
actuatorApi.env = () => {
return service({
url: `${baseUrl}/env`,
method: 'get'
})
}
actuatorApi.getSystemCpuCount = () => {
return service({
url: `${baseUrl}/metrics/system.cpu.count`,
method: 'get'
})
}
actuatorApi.getSystemCpuUsage = () => {
return service({
url: `${baseUrl}/metrics/system.cpu.usage`,
method: 'get'
})
}
actuatorApi.getProcessUptime = () => {
return service({
url: `${baseUrl}/metrics/process.uptime`,
method: 'get'
})
}
actuatorApi.getProcessStartTime = () => {
return service({
url: `${baseUrl}/metrics/process.start.time`,
method: 'get'
})
}
actuatorApi.getProcessCpuUsage = () => {
return service({
url: `${baseUrl}/metrics/process.cpu.usage`,
method: 'get'
})
}
actuatorApi.getJvmMemoryMax = () => {
return service({
url: `${baseUrl}/metrics/jvm.memory.max`,
method: 'get'
})
}
actuatorApi.getJvmMemoryCommitted = () => {
return service({
url: `${baseUrl}/metrics/jvm.memory.committed`,
method: 'get'
})
}
actuatorApi.getJvmMemoryUsed = () => {
return service({
url: `${baseUrl}/metrics/jvm.memory.used`,
method: 'get'
})
}
actuatorApi.getJvmGcPause = () => {
return service({
url: `${baseUrl}/metrics/jvm.gc.pause`,
method: 'get'
})
}
export default actuatorApi

View File

@ -7,8 +7,7 @@ const adminApi = {}
adminApi.counts = () => {
return service({
url: `${baseUrl}/counts`,
method: 'get',
mute: true
method: 'get'
})
}
@ -82,4 +81,49 @@ adminApi.updateAdminAssets = () => {
timeout: 600 * 1000
})
}
adminApi.getApplicationConfig = () => {
return service({
url: `${baseUrl}/spring/application.yaml`,
method: 'get'
})
}
adminApi.updateApplicationConfig = content => {
return service({
url: `${baseUrl}/spring/application.yaml`,
params: {
content: content
},
method: 'put'
})
}
adminApi.restartApplication = () => {
return service({
url: `${baseUrl}/spring/restart`,
method: 'post'
})
}
adminApi.getLogFiles = lines => {
return service({
url: `${baseUrl}/halo/logfile`,
params: {
lines: lines
},
method: 'get'
})
}
adminApi.downloadLogFiles = lines => {
return service({
url: `${baseUrl}/halo/logfile/download`,
params: {
lines: lines
},
method: 'get'
})
}
export default adminApi

View File

@ -27,6 +27,17 @@ attachmentApi.delete = attachmentId => {
})
}
attachmentApi.deleteInBatch = attachmentIds => {
return service({
url: `${baseUrl}`,
method: 'delete',
data: attachmentIds,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
attachmentApi.update = (attachmentId, attachment) => {
return service({
url: `${baseUrl}/${attachmentId}`,
@ -42,6 +53,13 @@ attachmentApi.getMediaTypes = () => {
})
}
attachmentApi.getTypes = () => {
return service({
url: `${baseUrl}/types`,
method: 'get'
})
}
attachmentApi.CancelToken = axios.CancelToken
attachmentApi.isCancel = axios.isCancel
@ -69,31 +87,31 @@ attachmentApi.uploads = (formDatas, uploadProgress, cancelToken) => {
attachmentApi.type = {
LOCAL: {
type: 'local',
type: 'LOCAL',
text: '本地'
},
SMMS: {
type: 'smms',
type: 'SMMS',
text: 'SM.MS'
},
UPYUN: {
type: 'upyun',
UPOSS: {
type: 'UPOSS',
text: '又拍云'
},
QNYUN: {
type: 'qnyun',
QINIUOSS: {
type: 'QINIUOSS',
text: '七牛云'
},
ALIYUN: {
type: 'aliyun',
ALIOSS: {
type: 'ALIOSS',
text: '阿里云'
},
BAIDUYUN: {
type: 'baiduyun',
BAIDUBOS: {
type: 'BAIDUBOS',
text: '百度云'
},
TENCENTYUN: {
type: 'tencentyun',
TENCENTCOS: {
type: 'TENCENTCOS',
text: '腾讯云'
}
}

View File

@ -15,4 +15,29 @@ backupApi.importMarkdown = (formData, uploadProgress, cancelToken) => {
})
}
backupApi.backupHalo = () => {
return service({
url: `${baseUrl}/halo`,
method: 'post',
timeout: 8640000 // 24 hours
})
}
backupApi.listHaloBackups = () => {
return service({
url: `${baseUrl}/halo`,
method: 'get'
})
}
backupApi.deleteHaloBackup = filename => {
return service({
url: `${baseUrl}/halo`,
params: {
filename: filename
},
method: 'delete'
})
}
export default backupApi

View File

@ -23,6 +23,14 @@ commentApi.queryComment = (target, params) => {
})
}
commentApi.commentTree = (target, id, params) => {
return service({
url: `${baseUrl}/${target}/comments/${id}/tree_view`,
params: params,
method: 'get'
})
}
commentApi.updateStatus = (target, commentId, status) => {
return service({
url: `${baseUrl}/${target}/comments/${commentId}/status/${status}`,
@ -30,6 +38,14 @@ commentApi.updateStatus = (target, commentId, status) => {
})
}
commentApi.updateStatusInBatch = (target, ids, status) => {
return service({
url: `${baseUrl}/${target}/comments/status/${status}`,
data: ids,
method: 'put'
})
}
commentApi.delete = (target, commentId) => {
return service({
url: `${baseUrl}/${target}/comments/${commentId}`,
@ -37,6 +53,14 @@ commentApi.delete = (target, commentId) => {
})
}
commentApi.deleteInBatch = (target, ids) => {
return service({
url: `${baseUrl}/${target}/comments`,
data: ids,
method: 'delete'
})
}
commentApi.create = (target, comment) => {
return service({
url: `${baseUrl}/${target}/comments`,
@ -94,16 +118,19 @@ commentApi.createComment = (comment, type) => {
commentApi.commentStatus = {
PUBLISHED: {
value: 'PUBLISHED',
color: 'green',
status: 'success',
text: '已发布'
},
AUDITING: {
value: 'AUDITING',
color: 'yellow',
status: 'warning',
text: '待审核'
},
RECYCLE: {
value: 'RECYCLE',
color: 'red',
status: 'error',
text: '回收站'

View File

@ -26,6 +26,16 @@ linkApi.get = linkId => {
})
}
linkApi.getByParse = url => {
return service({
url: `${baseUrl}/parse`,
params: {
url: url
},
method: 'get'
})
}
linkApi.update = (linkId, link) => {
return service({
url: `${baseUrl}/${linkId}`,
@ -41,4 +51,11 @@ linkApi.delete = linkId => {
})
}
linkApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
export default linkApi

View File

@ -48,4 +48,11 @@ menuApi.update = (menuId, menu) => {
})
}
menuApi.listTeams = () => {
return service({
url: `${baseUrl}/teams`,
method: 'get'
})
}
export default menuApi

View File

@ -14,6 +14,14 @@ optionApi.listAll = keys => {
})
}
optionApi.query = params => {
return service({
url: `${baseUrl}/list_view`,
params: params,
method: 'get'
})
}
optionApi.save = options => {
return service({
url: `${baseUrl}/map_view/saving`,
@ -22,4 +30,45 @@ optionApi.save = options => {
})
}
optionApi.create = option => {
return service({
url: baseUrl,
data: option,
method: 'post'
})
}
optionApi.delete = optionId => {
return service({
url: `${baseUrl}/${optionId}`,
method: 'delete'
})
}
optionApi.get = optionId => {
return service({
url: `${baseUrl}/${optionId}`,
method: 'get'
})
}
optionApi.update = (optionId, option) => {
return service({
url: `${baseUrl}/${optionId}`,
data: option,
method: 'put'
})
}
optionApi.type = {
INTERNAL: {
value: 'INTERNAL',
text: '系统'
},
CUSTOM: {
value: 'CUSTOM',
text: '自定义'
}
}
export default optionApi

View File

@ -34,7 +34,6 @@ postApi.create = (postToCreate, autoSave) => {
url: baseUrl,
method: 'post',
data: postToCreate,
mute: autoSave,
params: {
autoSave: autoSave
}
@ -52,6 +51,16 @@ postApi.update = (postId, postToUpdate, autoSave) => {
})
}
postApi.updateDraft = (postId, content) => {
return service({
url: `${baseUrl}/${postId}/status/draft/content`,
method: 'put',
data: {
content: content
}
})
}
postApi.updateStatus = (postId, status) => {
return service({
url: `${baseUrl}/${postId}/status/${status}`,
@ -59,6 +68,14 @@ postApi.updateStatus = (postId, status) => {
})
}
postApi.updateStatusInBatch = (ids, status) => {
return service({
url: `${baseUrl}/status/${status}`,
data: ids,
method: 'put'
})
}
postApi.delete = postId => {
return service({
url: `${baseUrl}/${postId}`,
@ -66,6 +83,14 @@ postApi.delete = postId => {
})
}
postApi.deleteInBatch = ids => {
return service({
url: `${baseUrl}`,
data: ids,
method: 'delete'
})
}
postApi.preview = postId => {
return service({
url: `${baseUrl}/preview/${postId}`,
@ -75,21 +100,25 @@ postApi.preview = postId => {
postApi.postStatus = {
PUBLISHED: {
value: 'PUBLISHED',
color: 'green',
status: 'success',
text: '已发布'
},
DRAFT: {
value: 'DRAFT',
color: 'yellow',
status: 'warning',
text: '草稿'
},
RECYCLE: {
value: 'RECYCLE',
color: 'red',
status: 'error',
text: '回收站'
},
INTIMATE: {
value: 'INTIMATE',
color: 'blue',
status: 'success',
text: '私密'

View File

@ -4,9 +4,10 @@ const baseUrl = '/api/admin/sheets'
const sheetApi = {}
sheetApi.list = () => {
sheetApi.list = params => {
return service({
url: baseUrl,
params: params,
method: 'get'
})
}

49
src/api/static.js Normal file
View File

@ -0,0 +1,49 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/statics'
const staticApi = {}
staticApi.list = () => {
return service({
url: baseUrl,
method: 'get'
})
}
staticApi.delete = path => {
return service({
url: baseUrl,
params: {
path: path
},
method: 'delete'
})
}
staticApi.createFolder = (basePath, folderName) => {
return service({
url: baseUrl,
params: {
basePath: basePath,
folderName: folderName
},
method: 'post'
})
}
staticApi.upload = (formData, uploadProgress, cancelToken, basePath) => {
return service({
url: `${baseUrl}/upload`,
timeout: 8640000,
data: formData,
params: {
basePath: basePath
},
onUploadProgress: uploadProgress,
cancelToken: cancelToken,
method: 'post'
})
}
export default staticApi

21
src/api/statistics.js Normal file
View File

@ -0,0 +1,21 @@
import service from '@/utils/service'
const baseUrl = '/api/admin/statistics'
const statisticsApi = {}
statisticsApi.statistics = () => {
return service({
url: `${baseUrl}`,
method: 'get'
})
}
statisticsApi.statisticsWithUser = () => {
return service({
url: `${baseUrl}/user`,
method: 'get'
})
}
export default statisticsApi

View File

@ -25,9 +25,16 @@ themeApi.listFiles = themeId => {
})
}
themeApi.customTpls = () => {
themeApi.customSheetTpls = () => {
return service({
url: `${baseUrl}/files/custom`,
url: `${baseUrl}/activation/template/custom/sheet`,
method: 'get'
})
}
themeApi.customPostTpls = () => {
return service({
url: `${baseUrl}/activation/template/custom/post`,
method: 'get'
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -24,7 +24,3 @@ export default {
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -9,7 +9,7 @@
title="待审核评论"
>
<template slot="content">
<a-spin :spinning="loadding">
<a-spin :spinning="loading">
<div class="custom-tab-wrapper">
<a-tabs>
<a-tab-pane
@ -99,7 +99,7 @@ export default {
name: 'HeaderComment',
data() {
return {
loadding: false,
loading: false,
visible: false,
postComments: [],
sheetComments: []
@ -111,13 +111,13 @@ export default {
computed: {
converttedPostComments() {
return this.postComments.map(comment => {
comment.content = marked(comment.content, { sanitize: true })
comment.content = marked(comment.content)
return comment
})
},
converttedSheetComments() {
return this.sheetComments.map(comment => {
comment.content = marked(comment.content, { sanitize: true })
comment.content = marked(comment.content)
return comment
})
}
@ -125,21 +125,21 @@ export default {
methods: {
fetchComment() {
if (!this.visible) {
this.loadding = true
this.loading = true
this.getComment()
} else {
this.loadding = false
this.loading = false
}
this.visible = !this.visible
},
getComment() {
commentApi.latestComment('posts', 5, 'AUDITING').then(response => {
this.postComments = response.data.data
this.loadding = false
this.loading = false
})
commentApi.latestComment('sheets', 5, 'AUDITING').then(response => {
this.sheetComments = response.data.data
this.loadding = false
this.loading = false
})
}
}

View File

@ -1,20 +1,54 @@
<template>
<div class="logo">
<router-link :to="{ name:'Dashboard' }">
<a
href="javascript:void(0);"
@click="onLogoClick()"
>
<h1 class="logo-title">Halo</h1>
<h1 class="logo-sub-title">Dashboard</h1>
</router-link>
<h1
class="logo-sub-title"
style="padding-left: 10px;"
>Dashboard</h1>
</a>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import optionApi from '@/api/option'
export default {
name: 'Logo'
name: 'Logo',
data() {
return {
clickCount: 0,
optionsToCreate: {
developer_mode: true
}
}
},
computed: {
...mapGetters(['options'])
},
methods: {
...mapActions(['loadOptions']),
onLogoClick() {
this.clickCount++
if (this.clickCount === 10) {
optionApi.save(this.optionsToCreate).then(response => {
this.loadOptions()
this.$message.success(`开发者选项已启用!`)
this.clickCount = 0
this.$router.push({ name: 'ToolList' })
})
} else if (this.clickCount >= 5) {
if (this.options.developer_mode) {
this.$message.info(`当前已启用开发者选项!`)
this.clickCount = 0
} else {
this.$message.info(`再点击 ${10 - this.clickCount} 次即可启用开发者选项!`)
}
}
}
}
}
</script>
<style scope>
.logo-sub-title{
padding-left: 10px;
}
</style>

View File

@ -30,7 +30,6 @@
<a-avatar
class="avatar"
size="small"
style="margin-right: 0.3rem;"
:src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'"
/>
</span>

View File

@ -1,138 +0,0 @@
<template>
<div class="clearfix">
<a-upload
:name="name"
:customRequest="handleUpload"
listType="picture-card"
:fileList="fileList"
@preview="handlePreview"
@change="handleChange"
>
<div v-if="fileList.length < 9 && plusPhotoVisible" id="plus-photo-uploadbox">
<a-icon type="plus"/>
<div class="ant-upload-text">Upload</div>
</div>
</a-upload>
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="previewImage">
</a-modal>
</div>
</template>
<script>
import axios from 'axios'
import attachmentApi from '@/api/attachment'
export default {
props: {
photoList: {
type: Array,
required: false,
default: function() {
return []
}
},
plusPhotoVisible: {
type: Boolean,
required: false,
default: true
}
},
data() {
return {
name: 'file',
previewVisible: false,
previewImage: '',
fileList: [],
uploadHandler: attachmentApi.upload
}
},
created() {
// watch
this.handlerEditPreviewPhoto(this.photoList)
},
watch: {
photoList(newValue, oldValue) {
this.handlerEditPreviewPhoto(newValue)
}
},
methods: {
handlerEditPreviewPhoto(data) {
//
this.fileList = []
//
if (data !== null && data !== undefined) {
for (var i = 0; i < data.length; i++) {
//
this.fileList.push({
uid: data[i].id,
name: data[i].name,
status: 'done',
url: data[i].thumbnail
})
}
}
},
handleCancel() {
this.previewVisible = false
},
handlePreview(file) {
this.previewImage = file.url || file.thumbUrl
this.previewVisible = true
},
handleChange({ fileList }) {
this.fileList = fileList
},
handleUpload(option) {
this.$log.debug('Uploading option', option)
const CancelToken = axios.CancelToken
const source = CancelToken.source()
const data = new FormData()
data.append(this.name, option.file)
this.uploadHandler(
data,
progressEvent => {
if (progressEvent.total > 0) {
progressEvent.percent = (progressEvent.loaded / progressEvent.total) * 100
}
this.$log.debug('Uploading percent: ', progressEvent.percent)
option.onProgress(progressEvent)
},
source.token,
option.file
)
.then(response => {
this.$log.debug('Uploaded successfully', response)
option.onSuccess(response, option.file)
this.$emit('success', response, option.file)
})
.catch(error => {
this.$log.debug('Failed to upload file', error)
option.onError(error, error.response)
this.$emit('failure', error, option.file)
})
return {
abort: () => {
this.$log.debug('Upload operation aborted by the user')
source.cancel('Upload operation canceled by the user.')
}
}
}
}
}
</script>
<style>
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
.ant-upload-list-picture-card {
/* 将浮动恢复为默认值,避免出现纵向换行情况 */
float: initial;
}
</style>

143
src/components/animate.less vendored Normal file
View File

@ -0,0 +1,143 @@
@charset "UTF-8";
/*!
* animate.css -https://daneden.github.io/animate.css/
* Version - 3.7.2
* Licensed under the MIT license - https://opensource.org/licenses/MIT
*
* Copyright (c) 2019 Daniel Eden
*/
@-webkit-keyframes fadeInRight {
from {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.fadeInRight {
-webkit-animation-name: fadeInRight;
animation-name: fadeInRight;
}
@-webkit-keyframes fadeInUp {
from {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.fadeInUp {
-webkit-animation-name: fadeInUp;
animation-name: fadeInUp;
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.delay-1s {
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.animated.delay-2s {
-webkit-animation-delay: 2s;
animation-delay: 2s;
}
.animated.delay-3s {
-webkit-animation-delay: 3s;
animation-delay: 3s;
}
.animated.delay-4s {
-webkit-animation-delay: 4s;
animation-delay: 4s;
}
.animated.delay-5s {
-webkit-animation-delay: 5s;
animation-delay: 5s;
}
.animated.fast {
-webkit-animation-duration: 800ms;
animation-duration: 800ms;
}
.animated.faster {
-webkit-animation-duration: 500ms;
animation-duration: 500ms;
}
.animated.slow {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.animated.slower {
-webkit-animation-duration: 3s;
animation-duration: 3s;
}
@media (print), (prefers-reduced-motion: reduce) {
.animated {
-webkit-animation-duration: 1ms !important;
animation-duration: 1ms !important;
-webkit-transition-duration: 1ms !important;
transition-duration: 1ms !important;
-webkit-animation-iteration-count: 1 !important;
animation-iteration-count: 1 !important;
}
}

View File

@ -1,4 +1,5 @@
@import './index.less';
@import './style.less';
* {
&::-webkit-scrollbar {
@ -112,7 +113,23 @@ body {
&.content-width-Fluid {
.header-index-wide {
max-width: unset;
margin-left: 24px;
.header-index-left {
flex: 1 1 1000px;
.logo {
margin-left: 25px;
}
.ant-menu.ant-menu-horizontal {
max-width: calc(100vw - 190px - 238px - 25px);
flex: 1 1 calc(100vw - 190px - 238px - 25px);
}
}
.header-index-right {
margin-right: 25px;
}
}
.page-header-index-wide {
@ -143,7 +160,7 @@ body {
.header {
height: 64px;
padding: 0 12px 0 0;
padding: 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
position: relative;
@ -158,7 +175,7 @@ body {
.action {
cursor: pointer;
padding: 0 12px;
padding: 0 18px;
display: inline-block;
transition: all .3s;
height: 100%;
@ -255,7 +272,7 @@ body {
}
.ant-menu.ant-menu-horizontal {
flex: 1 1;
flex: 1 1 auto;
white-space: normal;
}
}
@ -276,16 +293,19 @@ body {
height: 64px;
.ant-menu.ant-menu-horizontal {
max-width: 835px;
flex: 0 1 835px;
border: none;
height: 64px;
line-height: 64px;
}
.header-index-left {
flex: 1 1;
flex: 0 1 1000px;
display: flex;
.logo.top-nav-header {
flex: 0 0 165px;
width: 165px;
height: 64px;
position: relative;
@ -314,8 +334,20 @@ body {
.header-index-right {
flex: 0 0 auto;
align-self: flex-end;
height: 64px;
overflow: hidden;
.content-box {
float: right;
.action {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
@ -380,11 +412,10 @@ body {
.sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
position: relative;
z-index: 10;
height: auto;
z-index: @ant-global-sider-zindex;
min-height: 100vh;
.ant-layout-sider-children {
padding-top: 64px;
overflow-y: hidden;
&:hover {
@ -398,18 +429,13 @@ body {
}
.logo {
position: absolute;
position: relative;
text-align: center;
top: 0;
left: 0;
width: 100%;
height: 64px;
line-height: 64px;
-webkit-transition: all .3s;
transition: all .3s;
background: #002140;
overflow: hidden;
z-index: 9;
line-height: 64px;
background: #002140;
transition: all .3s;
img,
svg,
@ -477,11 +503,6 @@ body {
}
// 数据列表 样式
.table-alert {
margin-bottom: 16px;
}
.table-page-search-wrapper {
.ant-form-inline {
@ -518,6 +539,10 @@ body {
}
.ant-table-thead>tr>th {
background: #fff !important;
}
.content {
.table-operator {
@ -592,18 +617,27 @@ body {
}
.ant-comment-inner {
padding: 0 !important;
.ant-comment-content {
.ant-comment-content-detail {
p {
margin-top: 1rem;
margin-bottom: 0;
img {
width: 100%;
}
}
}
}
}
.ant-comment-avatar {
img {
width: 40px !important;
height: 40px !important;
}
}
.bottom-control {
position: absolute;
bottom: 0px;
@ -719,13 +753,15 @@ body {
float: left;
}
.attach-thumb {
.attach-thumb,
.photo-thumb {
width: 100%;
padding-bottom: 56%;
}
.attach-item,
.attach-thumb {
.attach-thumb,
.photo-thumb {
margin: 0 auto;
position: relative;
overflow: hidden;
@ -789,3 +825,90 @@ body {
overflow: hidden;
}
}
.exception {
min-height: 500px;
height: 80%;
align-items: center;
text-align: center;
margin-top: 150px;
.img {
display: inline-block;
padding-right: 52px;
zoom: 1;
img {
height: 360px;
max-width: 430px;
}
}
.content {
display: inline-block;
flex: auto;
h1 {
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc {
color: rgba(0, 0, 0, 0.45);
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
}
}
.mobile {
.exception {
margin-top: 30px;
.img {
padding-right: unset;
img {
height: 40%;
max-width: 80%;
}
}
}
}
.vue-codemirror-wrap {
.CodeMirror {
height: 560px;
}
.CodeMirror-gutters {
border-right: 1px solid #fff3f3;
background-color: #ffffff;
}
}
.select-attachment-checkbox {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 10;
.ant-checkbox {
margin-left: 4px;
}
}
.journal-list-content,
.comment-drawer-content {
img {
width: 100%;
}
}

View File

@ -7,3 +7,5 @@ html [type='button'] {
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;
@ant-global-sider-zindex : 106;
@ant-global-header-zindex : 105;

40
src/components/style.less Normal file
View File

@ -0,0 +1,40 @@
@import './animate.less';
.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;
}
}

View File

@ -14,7 +14,7 @@ export const asyncRouterMap = [
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/Dashboard'),
meta: { title: '仪表盘', icon: 'dashboard', hiddenHeaderContent: false }
meta: { title: '仪表盘', icon: 'dashboard', hiddenHeaderContent: false, keepAlive: false }
},
// posts
@ -35,7 +35,7 @@ export const asyncRouterMap = [
path: '/posts/write',
name: 'PostEdit',
component: () => import('@/views/post/PostEdit'),
meta: { title: '写文章', hiddenHeaderContent: false }
meta: { title: '写文章', hiddenHeaderContent: false, keepAlive: false }
},
{
path: '/categories',
@ -70,7 +70,7 @@ export const asyncRouterMap = [
path: '/sheets/write',
name: 'SheetEdit',
component: () => import('@/views/sheet/SheetEdit'),
meta: { title: '新建页面', hiddenHeaderContent: false }
meta: { title: '新建页面', hiddenHeaderContent: false, keepAlive: false }
},
{
path: '/sheets/links',
@ -166,18 +166,19 @@ export const asyncRouterMap = [
redirect: '/system/options',
meta: { title: '系统', icon: 'setting' },
children: [
{
path: '/system/developer/options',
name: 'DeveloperOptions',
hidden: true,
component: () => import('@/views/system/developer/DeveloperOptions'),
meta: { title: '开发者选项', hiddenHeaderContent: false }
},
{
path: '/system/options',
name: 'OptionForm',
component: () => import('@/views/system/OptionForm'),
meta: { title: '博客设置', hiddenHeaderContent: false }
},
// {
// path: '/system/backup',
// name: 'BackupList',
// component: () => import('@/views/system/BackupList'),
// meta: { title: '博客备份', hiddenHeaderContent: false }
// },
{
path: '/system/tools',
name: 'ToolList',
@ -201,10 +202,6 @@ export const asyncRouterMap = [
}
]
/**
* 基础路由
* @type { *[] }
*/
export const constantRouterMap = [
{
path: '/login',
@ -227,6 +224,6 @@ export const constantRouterMap = [
{
path: '/404',
name: 'NotFound',
component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404')
component: () => import('@/views/exception/404')
}
]

View File

@ -9,14 +9,12 @@ import './core/lazy_use'
import './permission'
import '@/utils/filter' // global filter
import './components'
import animated from 'animate.css'
import { version } from '../package.json'
Vue.config.productionTip = false
Vue.prototype.VERSION = version
Vue.use(router)
Vue.use(animated)
new Vue({
router,

View File

@ -6,17 +6,9 @@ import {
domTitle
} from '@/utils/domUtil'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
const whiteList = ['Login', 'Install', 'NotFound', 'ResetPassword'] // no redirect whitelist
router.beforeEach((to, from, next) => {
NProgress.start()
to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
Vue.$log.debug('Token', store.getters.token)
if (store.getters.token) {
@ -24,7 +16,6 @@ router.beforeEach((to, from, next) => {
next({
name: 'Dashboard'
})
NProgress.done()
return
}
// TODO Get installation status
@ -34,7 +25,6 @@ router.beforeEach((to, from, next) => {
}
next()
NProgress.done()
return
}
@ -42,7 +32,6 @@ router.beforeEach((to, from, next) => {
// Check whitelist
if (whiteList.includes(to.name)) {
next()
NProgress.done()
return
}
@ -52,5 +41,4 @@ router.beforeEach((to, from, next) => {
redirect: to.fullPath
}
})
NProgress.done()
})

View File

@ -5,6 +5,7 @@ import {
import optionApi from '@/api/option'
const keys = [
'blog_url',
'developer_mode',
'attachment_upload_image_preview_enable',
'attachment_upload_max_parallel_uploads',
'attachment_upload_max_files'

View File

@ -36,3 +36,12 @@ Vue.filter('fileSizeFormat', function(value) {
size = size.toFixed(2)
return size + ' ' + unitArr[index]
})
Vue.filter('dayTime', function(value) {
var days = Math.floor(value / 86400)
var hours = Math.floor((value % 86400) / 3600)
var minutes = Math.floor(((value % 86400) % 3600) / 60)
var seconds = Math.floor(((value % 86400) % 3600) % 60)
var duration = days + 'd ' + hours + 'h ' + minutes + 'm ' + seconds + 's'
return duration
})

View File

@ -1,6 +1,4 @@
import axios from 'axios'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import Vue from 'vue'
import { message, notification } from 'ant-design-vue'
import store from '@/store'
@ -8,7 +6,7 @@ import router from '@/router'
import { isObject } from './util'
const service = axios.create({
timeout: 8000,
timeout: 10000,
withCredentials: true
})
@ -63,27 +61,20 @@ function getFieldValidationError(data) {
service.interceptors.request.use(
config => {
config.baseURL = store.getters.apiUrl
if (!config.mute) {
NProgress.start()
}
// TODO set token
setTokenToHeader(config)
return config
},
error => {
NProgress.remove()
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
NProgress.done()
return response
},
error => {
NProgress.done()
if (axios.isCancel(error)) {
Vue.$log.debug('Cancelled uploading by user.')
return Promise.reject(error)

View File

@ -10,20 +10,6 @@ export function triggerWindowResizeEvent() {
window.dispatchEvent(event)
}
/**
* Remove loading animate
* @param id parent element id or class
* @param timeout
*/
export function removeLoadingAnimate(id = '', timeout = 1500) {
if (id === '') {
return
}
setTimeout(() => {
document.body.removeChild(document.getElementById(id))
}, timeout)
}
export function timeAgo(time) {
var currentTime = new Date().getTime()
var between = currentTime - time

View File

@ -7,7 +7,6 @@
>
<a-col
:span="24"
class="search-box"
style="padding-bottom: 12px;"
>
<a-card
@ -22,7 +21,10 @@
:sm="24"
>
<a-form-item label="关键词">
<a-input v-model="queryParam.keyword" />
<a-input
v-model="queryParam.keyword"
@keyup.enter="handleQuery()"
/>
</a-form-item>
</a-col>
<a-col
@ -32,13 +34,15 @@
<a-form-item label="存储位置">
<a-select
v-model="queryParam.attachmentType"
@change="handleQuery"
@change="handleQuery()"
>
<a-select-option
v-for="item in Object.keys(attachmentType)"
v-for="item in types"
:key="item"
:value="item"
>{{ attachmentType[item].text }}</a-select-option>
>{{
attachmentType[item].text
}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
@ -49,13 +53,15 @@
<a-form-item label="文件类型">
<a-select
v-model="queryParam.mediaType"
@change="handleQuery"
@change="handleQuery()"
>
<a-select-option
v-for="(item,index) in mediaTypes"
v-for="(item, index) in mediaTypes"
:key="index"
:value="item"
>{{ item }}</a-select-option>
>{{
item
}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
@ -66,23 +72,48 @@
<span class="table-page-search-submitButtons">
<a-button
type="primary"
@click="handleQuery"
@click="handleQuery()"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="handleResetParam"
@click="handleResetParam()"
>重置</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div class="table-operator" style="margin-bottom: 0;">
<div
class="table-operator"
style="margin-bottom: 0;"
>
<a-button
type="primary"
icon="plus"
@click="()=>this.uploadVisible = true"
icon="cloud-upload"
@click="() => (uploadVisible = true)"
>上传</a-button>
<a-button
icon="select"
v-show="!supportMultipleSelection"
@click="handleMultipleSelection"
>
批量操作
</a-button>
<a-button
type="danger"
icon="delete"
v-show="supportMultipleSelection"
@click="handleDeleteAttachmentInBatch"
>
删除
</a-button>
<a-button
icon="close"
v-show="supportMultipleSelection"
@click="handleCancelMultipleSelection"
>
取消
</a-button>
</div>
</a-card>
</a-col>
@ -107,15 +138,23 @@
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
>
loading="lazy"
/>
</div>
<a-card-meta style="padding: 0.8rem;">
<ellipsis
:length="isMobile()?12:16"
:length="isMobile() ? 12 : 16"
tooltip
slot="description"
>{{ item.name }}</ellipsis>
</a-card-meta>
<a-checkbox
class="select-attachment-checkbox"
:style="getCheckStatus(item.id) ? selectedAttachmentStyle : ''"
:checked="getCheckStatus(item.id)"
@click="handleAttachmentSelectionChanged($event, item)"
v-show="supportMultipleSelection"
></a-checkbox>
</a-card>
</a-list-item>
</a-list>
@ -124,9 +163,10 @@
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['18', '36', '54','72','90','108']"
:pageSizeOptions="['18', '36', '54', '72', '90', '108']"
showSizeChanger
@change="handlePaginationChange"
@showSizeChange="handlePaginationChange"
@ -149,7 +189,7 @@
v-if="selectAttachment"
:attachment="selectAttachment"
:addToPhoto="true"
@delete="()=>this.loadAttachments()"
@delete="() => this.loadAttachments()"
/>
</page-view>
</template>
@ -159,6 +199,7 @@ import { mixin, mixinDevice } from '@/utils/mixin.js'
import { PageView } from '@/layouts'
import AttachmentDetailDrawer from './components/AttachmentDetailDrawer'
import attachmentApi from '@/api/attachment'
import { mapGetters } from 'vuex'
export default {
components: {
@ -171,9 +212,13 @@ export default {
attachmentType: attachmentApi.type,
listLoading: true,
uploadVisible: false,
supportMultipleSelection: false,
selectedAttachmentCheckbox: {},
batchSelectedAttachments: [],
selectAttachment: {},
attachments: [],
mediaTypes: [],
types: [],
editable: false,
pagination: {
page: 1,
@ -198,11 +243,17 @@ export default {
attachment.typeProperty = this.attachmentType[attachment.type]
return attachment
})
},
selectedAttachmentStyle() {
return {
border: `2px solid ${this.color()}`
}
}
},
created() {
this.loadAttachments()
this.loadMediaTypes()
this.loadTypes()
},
destroyed: function() {
if (this.drawerVisible) {
@ -216,6 +267,7 @@ export default {
next()
},
methods: {
...mapGetters(['color']),
loadAttachments() {
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
@ -232,9 +284,18 @@ export default {
this.mediaTypes = response.data.data
})
},
loadTypes() {
attachmentApi.getTypes().then(response => {
this.types = response.data.data
})
},
handleShowDetailDrawer(attachment) {
this.selectAttachment = attachment
this.drawerVisible = true
if (this.supportMultipleSelection) {
this.drawerVisible = false
} else {
this.drawerVisible = true
}
},
handlePaginationChange(page, size) {
this.$log.debug(`Current: ${page}, PageSize: ${size}`)
@ -246,18 +307,18 @@ export default {
this.queryParam.keyword = null
this.queryParam.mediaType = null
this.queryParam.attachmentType = null
this.loadAttachments()
this.handlePaginationChange(1, this.pagination.size)
this.loadMediaTypes()
this.loadTypes()
},
handleQuery() {
this.queryParam.page = 0
this.pagination.page = 1
this.loadAttachments()
this.handlePaginationChange(1, this.pagination.size)
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadAttachments()
this.handlePaginationChange(1, this.pagination.size)
this.loadMediaTypes()
this.loadTypes()
},
handleJudgeMediaType(attachment) {
var mediaType = attachment.mediaType
@ -275,6 +336,56 @@ export default {
}
// false
return false
},
getCheckStatus(key) {
return this.selectedAttachmentCheckbox[key] || false
},
handleMultipleSelection() {
this.supportMultipleSelection = true
//
this.drawerVisible = false
this.attachments.forEach(item => {
this.$set(this.selectedAttachmentCheckbox, item.id, false)
})
},
handleCancelMultipleSelection() {
this.supportMultipleSelection = false
this.drawerVisible = false
this.batchSelectedAttachments = []
for (var key in this.selectedCheckbox) {
this.$set(this.selectedAttachmentCheckbox, key, false)
}
},
handleAttachmentSelectionChanged(e, item) {
var isChecked = e.target.checked || false
if (isChecked) {
this.$set(this.selectedAttachmentCheckbox, item.id, true)
this.batchSelectedAttachments.push(item.id)
} else {
this.$set(this.selectedAttachmentCheckbox, item.id, false)
// idid
var index = this.batchSelectedAttachments.indexOf(item.id)
this.batchSelectedAttachments.splice(index, 1)
}
},
handleDeleteAttachmentInBatch() {
var that = this
if (this.batchSelectedAttachments.length <= 0) {
this.$message.success('你还未选择任何附件,请至少选择一个!')
return
}
this.$confirm({
title: '确定要批量删除选中的附件吗?',
content: '一旦删除不可恢复,请谨慎操作',
onOk() {
attachmentApi.deleteInBatch(that.batchSelectedAttachments).then(res => {
that.handleCancelMultipleSelection()
that.loadAttachments()
that.$message.success('删除成功')
})
},
onCancel() {}
})
}
}
}

View File

@ -19,22 +19,25 @@
>
<div class="attach-detail-img">
<div v-show="nonsupportPreviewVisible"></div>
<a :href="attachment.path" target="_blank">
<a
:href="attachment.path"
target="_blank"
>
<img
:src="attachment.path"
v-show="photoPreviewVisible"
style="width: 100%;"
loading="lazy"
>
</a>
<video-player
class="video-player-box"
<d-player
ref="player"
:options="videoOptions"
v-show="videoPreviewVisible"
ref="videoPlayer"
:options="playerOptions"
:playsinline="true"
class="video-player-box"
style="width: 100%;"
>
</video-player>
</d-player>
</div>
</a-skeleton>
</a-col>
@ -162,16 +165,18 @@
<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 'vue-dplayer/dist/vue-dplayer.css'
import VueDPlayer from 'vue-dplayer'
import flvjs from 'flv.js'
window.flvjs = flvjs
export default {
name: 'AttachmentDetailDrawer',
mixins: [mixin, mixinDevice],
components: {
videoPlayer
'd-player': VueDPlayer
},
data() {
return {
@ -181,24 +186,13 @@ export default {
photoPreviewVisible: false,
videoPreviewVisible: false,
nonsupportPreviewVisible: false,
playerOptions: {
// videojs options
muted: true,
language: 'zh-CN',
aspectRatio: '16:9',
fluid: true,
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'
}
],
poster: '/static/images/author.jpg',
width: document.documentElement.clientWidth,
notSupportedMessage: '此视频暂无法播放,请稍后再试'
player: {},
videoOptions: {
lang: 'zh-cn',
video: {
url: '',
type: 'auto'
}
}
}
},
@ -222,13 +216,8 @@ export default {
default: true
}
},
created() {
this.loadSkeleton()
},
computed: {
player() {
return this.$refs.videoPlayer.player
}
mounted() {
this.player = this.$refs.player
},
watch: {
visible: function(newValue, oldValue) {
@ -302,8 +291,8 @@ export default {
},
handleAddToPhoto() {
this.photo['name'] = this.attachment.name
this.photo['thumbnail'] = this.attachment.thumbPath
this.photo['url'] = this.attachment.path
this.photo['thumbnail'] = encodeURI(this.attachment.thumbPath)
this.photo['url'] = encodeURI(this.attachment.path)
this.photo['takeTime'] = new Date().getTime()
photoApi.create(this.photo).then(response => {
this.$message.success('添加成功!')
@ -320,39 +309,29 @@ export default {
var prefix = mediaType.split('/')[0]
if (prefix === 'video' || prefix === 'flv') {
this.videoPreviewVisible = true
this.photoPreviewVisible = false
this.nonsupportPreviewVisible = false
//
this.handlePreviewVisible(false, true, false)
//
var lastIndex = attachment.path.lastIndexOf('?')
var path = attachment.path.substring(0, lastIndex)
//
this.$set(this.playerOptions.sources, 0, {
type: mediaType,
src: attachment.path
})
console.log(this.playerOptions.sources)
this.$set(this.videoOptions.video, 'url', path)
this.$log.debug('video url', path)
} else if (prefix === 'image') {
this.photoPreviewVisible = true
this.videoPreviewVisible = false
this.nonsupportPreviewVisible = false
this.handlePreviewVisible(true, false, false)
} else {
this.nonsupportPreviewVisible = true
this.videoPreviewVisible = false
this.photoPreviewVisible = false
this.handlePreviewVisible(false, false, true)
}
}
},
handlePreviewVisible(photo, video, nonsupport) {
// 使vue,
this.$set(this, 'photoPreviewVisible', photo)
this.$set(this, 'videoPreviewVisible', video)
this.$set(this, 'nonsupportPreviewVisible', nonsupport)
}
// handleDownLoadPhoto(attachment) {
// var path = attachment.path
// var index = path.lastIndexOf('/')
// var filename = path.substr(index+1, path.length)
// // chrome/firefox
// var aTag = document.createElement('a')
// aTag.download = filename
// aTag.href = path//URL.createObjectURL(blob)
// aTag.target = '_blank'
// aTag.click()
// URL.revokeObjectURL(aTag.href)
// }
}
}
</script>

View File

@ -15,7 +15,7 @@
<a-input-search
placeholder="搜索附件"
v-model="queryParam.keyword"
@search="loadAttachments(true)"
@search="handleQuery()"
enterButton
/>
</a-row>
@ -30,7 +30,7 @@
:paragraph="{ rows: 18 }"
>
<a-col :span="24">
<a-empty v-if="formattedDatas.length==0"/>
<a-empty v-if="formattedDatas.length==0" />
<div
v-else
class="attach-item"
@ -42,6 +42,7 @@
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
loading="lazy"
>
</div>
</a-col>
@ -50,8 +51,9 @@
<a-divider />
<div class="page-wrapper">
<a-pagination
:defaultPageSize="pagination.size"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
@change="handlePaginationChange"
></a-pagination>
</div>
@ -122,7 +124,7 @@ export default {
},
queryParam: {
page: 0,
size: 18,
size: 12,
sort: null,
keyword: null
},
@ -139,14 +141,11 @@ export default {
})
}
},
created() {
this.loadSkeleton()
this.loadAttachments()
},
watch: {
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
this.loadAttachments()
}
}
},
@ -165,18 +164,18 @@ export default {
this.$log.debug('Show detail of', attachment)
this.detailVisible = true
},
loadAttachments(isSearch) {
loadAttachments() {
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
if (isSearch) {
this.queryParam.page = 0
}
attachmentApi.query(this.queryParam).then(response => {
this.attachments = response.data.data.content
this.pagination.total = response.data.data.total
})
},
handleQuery() {
this.handlePaginationChange(1, this.pagination.size)
},
handlePaginationChange(page, pageSize) {
this.pagination.page = page
this.pagination.size = pageSize
@ -185,7 +184,7 @@ export default {
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadSkeleton()
this.loadAttachments()
this.handlePaginationChange(1, this.pagination.size)
},
handleDelete() {
this.loadAttachments()

View File

@ -14,6 +14,8 @@
>
<a-input-search
placeholder="搜索附件"
v-model="queryParam.keyword"
@search="handleQuery()"
enterButton
/>
</a-row>
@ -28,7 +30,7 @@
:paragraph="{ rows: 18 }"
>
<a-col :span="24">
<a-empty v-if="attachments.length==0"/>
<a-empty v-if="attachments.length==0" />
<div
v-else
class="attach-item"
@ -40,6 +42,7 @@
<img
:src="item.thumbPath"
v-show="handleJudgeMediaType(item)"
loading="lazy"
>
</div>
</a-col>
@ -48,8 +51,9 @@
<a-divider />
<div class="page-wrapper">
<a-pagination
:defaultPageSize="pagination.size"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
@change="handlePaginationChange"
></a-pagination>
</div>
@ -125,18 +129,21 @@ export default {
size: 12,
sort: ''
},
queryParam: {
page: 0,
size: 12,
sort: null,
keyword: null
},
attachments: [],
uploadHandler: attachmentApi.upload
}
},
created() {
this.loadSkeleton()
this.loadAttachments()
},
watch: {
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
this.loadAttachments()
}
}
},
@ -151,13 +158,17 @@ export default {
this.uploadVisible = true
},
loadAttachments() {
const pagination = Object.assign({}, this.pagination)
pagination.page--
attachmentApi.query(pagination).then(response => {
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
attachmentApi.query(this.queryParam).then(response => {
this.attachments = response.data.data.content
this.pagination.total = response.data.data.total
})
},
handleQuery() {
this.handlePaginationChange(1, this.pagination.size)
},
handleSelectAttachment(item) {
this.$emit('listenToSelect', item)
},
@ -169,14 +180,10 @@ export default {
this.pagination.size = pageSize
this.loadAttachments()
},
handleAttachmentUploadSuccess() {
this.$message.success('上传成功!')
this.loadAttachments()
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.loadSkeleton()
this.loadAttachments()
this.handlePaginationChange(1, this.pagination.size)
},
handleJudgeMediaType(attachment) {
var mediaType = attachment.mediaType

View File

@ -163,9 +163,6 @@ export default {
}
}
},
created() {
this.loadSkeleton()
},
computed: {
...mapGetters(['options'])
},

View File

@ -12,7 +12,10 @@
:sm="24"
>
<a-form-item label="关键词">
<a-input v-model="queryParam.keyword" />
<a-input
v-model="queryParam.keyword"
@keyup.enter="handleQuery()"
/>
</a-form-item>
</a-col>
<a-col
@ -23,7 +26,7 @@
<a-select
v-model="queryParam.status"
placeholder="请选择评论状态"
@change="handleQuery"
@change="handleQuery()"
>
<a-select-option
v-for="status in Object.keys(commentStatus)"
@ -41,11 +44,11 @@
<span class="table-page-search-submitButtons">
<a-button
type="primary"
@click="handleQuery"
@click="handleQuery()"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="handleResetParam"
@click="handleResetParam()"
>重置</a-button>
</span>
</a-col>
@ -62,7 +65,7 @@
>
<a
href="javascript:void(0);"
@click="handlePublishMore"
@click="handleEditStatusMore(commentStatus.PUBLISHED.value)"
>
通过
</a>
@ -73,7 +76,7 @@
>
<a
href="javascript:void(0);"
@click="handleRecycleMore"
@click="handleEditStatusMore(commentStatus.RECYCLE.value)"
>
移到回收站
</a>
@ -234,6 +237,7 @@
v-else
:rowKey="comment => comment.id"
:rowSelection="{
selectedRowKeys: selectedRowKeys,
onChange: onSelectionChange,
getCheckboxProps: getCheckboxProps
}"
@ -377,7 +381,9 @@
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']"
showSizeChanger
@showSizeChange="handlePaginationChange"
@ -388,8 +394,8 @@
</a-card>
<a-modal
v-if="selectComment"
:title="'回复给:'+selectComment.author"
v-if="selectedComment"
:title="'回复给:'+selectedComment.author"
v-model="replyCommentVisible"
@close="onReplyClose"
destroyOnClose
@ -415,8 +421,8 @@
</a-modal>
<!-- <CommentDetail
v-model="commentDetailVisible"
v-if="selectComment"
:comment="selectComment"
v-if="selectedComment"
:comment="selectedComment"
:type="this.type"
/> -->
</div>
@ -431,6 +437,7 @@ const postColumns = [
{
title: '昵称',
dataIndex: 'author',
width: '150px',
scopedSlots: { customRender: 'author' }
},
{
@ -468,6 +475,7 @@ const sheetColumns = [
{
title: '昵称',
dataIndex: 'author',
width: '150px',
scopedSlots: { customRender: 'author' }
},
{
@ -522,8 +530,8 @@ export default {
columns: this.type === 'posts' ? postColumns : sheetColumns,
replyCommentVisible: false,
pagination: {
current: 1,
pageSize: 10,
page: 1,
size: 10,
sort: null
},
queryParam: {
@ -536,7 +544,7 @@ export default {
selectedRowKeys: [],
selectedRows: [],
comments: [],
selectComment: {},
selectedComment: {},
replyComment: {},
loading: false,
commentStatus: commentApi.commentStatus,
@ -550,7 +558,7 @@ export default {
formattedComments() {
return this.comments.map(comment => {
comment.statusProperty = this.commentStatus[comment.status]
comment.content = marked(comment.content, { sanitize: true })
comment.content = marked(comment.content)
return comment
})
},
@ -559,8 +567,8 @@ export default {
methods: {
loadComments() {
this.loading = true
this.queryParam.page = this.pagination.current - 1
this.queryParam.size = this.pagination.pageSize
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
commentApi.queryComment(this.type, this.queryParam).then(response => {
this.comments = response.data.data.content
@ -569,9 +577,8 @@ export default {
})
},
handleQuery() {
this.queryParam.page = 0
this.pagination.current = 1
this.loadComments()
this.handleClearRowKeys()
this.handlePaginationChange(1, this.pagination.size)
},
handleEditStatusClick(commentId, status) {
commentApi.updateStatus(this.type, commentId, status).then(response => {
@ -590,7 +597,7 @@ export default {
this.handleEditStatusClick(comment.id, 'PUBLISHED')
},
handleReplyClick(comment) {
this.selectComment = comment
this.selectedComment = comment
this.replyCommentVisible = true
this.replyComment.parentId = comment.id
if (this.type === 'posts') {
@ -610,67 +617,51 @@ export default {
commentApi.create(this.type, this.replyComment).then(response => {
this.$message.success('回复成功!')
this.replyComment = {}
this.selectComment = {}
this.selectedComment = {}
this.replyCommentVisible = false
this.loadComments()
})
},
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.pagination.current = page
this.pagination.pageSize = pageSize
this.pagination.page = page
this.pagination.size = pageSize
this.loadComments()
},
handleResetParam() {
this.queryParam.keyword = null
this.queryParam.status = null
this.loadComments()
this.handleClearRowKeys()
this.handlePaginationChange(1, this.pagination.size)
},
handlePublishMore() {
handleEditStatusMore(status) {
if (this.selectedRowKeys.length <= 0) {
this.$message.success('请至少选择一项!')
return
}
for (let index = 0; index < this.selectedRowKeys.length; index++) {
const element = this.selectedRowKeys[index]
commentApi.updateStatus(this.type, element, 'PUBLISHED').then(response => {
this.$log.debug(`commentId: ${element}, status: PUBLISHED`)
this.selectedRowKeys = []
this.loadComments()
})
}
},
handleRecycleMore() {
if (this.selectedRowKeys.length <= 0) {
this.$message.success('请至少选择一项!')
return
}
for (let index = 0; index < this.selectedRowKeys.length; index++) {
const element = this.selectedRowKeys[index]
commentApi.updateStatus(this.type, element, 'RECYCLE').then(response => {
this.$log.debug(`commentId: ${element}, status: RECYCLE`)
this.selectedRowKeys = []
this.loadComments()
})
}
commentApi.updateStatusInBatch(this.type, this.selectedRowKeys, status).then(response => {
this.$log.debug(`commentIds: ${this.selectedRowKeys}, status: ${status}`)
this.selectedRowKeys = []
this.loadComments()
})
},
handleDeleteMore() {
if (this.selectedRowKeys.length <= 0) {
this.$message.success('请至少选择一项!')
return
}
for (let index = 0; index < this.selectedRowKeys.length; index++) {
const element = this.selectedRowKeys[index]
commentApi.delete(this.type, element).then(response => {
this.$log.debug(`delete: ${element}`)
this.selectedRowKeys = []
this.loadComments()
})
}
commentApi.deleteInBatch(this.type, this.selectedRowKeys).then(response => {
this.$log.debug(`delete: ${this.selectedRowKeys}`)
this.selectedRowKeys = []
this.loadComments()
})
},
handleClearRowKeys() {
this.selectedRowKeys = []
},
onReplyClose() {
this.replyComment = {}
this.selectComment = {}
this.selectedComment = {}
this.replyCommentVisible = false
},
onSelectionChange(selectedRowKeys) {
@ -680,13 +671,13 @@ export default {
getCheckboxProps(comment) {
return {
props: {
disabled: comment.status === 'RECYCLE',
disabled: this.queryParam.status == null || this.queryParam.status === '',
name: comment.author
}
}
},
handleShowDetailDrawer(comment) {
this.selectComment = comment
this.selectedComment = comment
this.commentDetailVisible = true
}
}

View File

@ -0,0 +1,250 @@
<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-list itemLayout="horizontal">
<a-list-item>
<a-list-item-meta>
<template slot="description">
<p
v-html="description"
class="comment-drawer-content"
></p>
</template>
<h3 slot="title">{{ title }}</h3>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-col>
<a-divider />
<a-col :span="24">
<a-empty v-if="comments.length == 0" />
<TargetCommentTree
v-else
v-for="(comment, index) in comments"
:key="index"
:comment="comment"
@reply="handleCommentReply"
@delete="handleCommentDelete"
@editStatus="handleEditStatusClick"
/>
</a-col>
</a-row>
<a-divider />
<div class="page-wrapper">
<a-pagination
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
@change="handlePaginationChange"
></a-pagination>
</div>
<a-divider class="divider-transparent" />
<div class="bottom-control">
<a-button
type="primary"
@click="handleComment"
>评论</a-button>
</div>
<a-modal
v-if="selectedComment"
:title="'回复给:' + selectedComment.author"
v-model="replyCommentVisible"
@close="onReplyClose"
destroyOnClose
>
<template slot="footer">
<a-button
key="submit"
type="primary"
@click="handleCreateClick"
>
回复
</a-button>
</template>
<a-form layout="vertical">
<a-form-item>
<a-input
type="textarea"
:autosize="{ minRows: 8 }"
v-model="replyComment.content"
/>
</a-form-item>
</a-form>
</a-modal>
<a-modal
title="评论"
v-model="commentVisible"
@close="onCommentClose"
destroyOnClose
>
<template slot="footer">
<a-button
key="submit"
type="primary"
@click="handleCreateClick"
>
回复
</a-button>
</template>
<a-form layout="vertical">
<a-form-item>
<a-input
type="textarea"
:autosize="{ minRows: 8 }"
v-model="replyComment.content"
/>
</a-form-item>
</a-form>
</a-modal>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import TargetCommentTree from './TargetCommentTree'
import commentApi from '@/api/comment'
export default {
name: 'TargetCommentDrawer',
mixins: [mixin, mixinDevice],
components: { TargetCommentTree },
data() {
return {
comments: [],
selectedComment: {},
replyComment: {},
replyCommentVisible: false,
commentVisible: false,
pagination: {
page: 1,
size: 10,
sort: ''
},
queryParam: {
page: 0,
size: 10,
sort: null,
keyword: null
}
}
},
props: {
visible: {
type: Boolean,
required: false,
default: false
},
title: {
type: String,
required: false,
default: ''
},
description: {
type: String,
required: false,
default: ''
},
target: {
type: String,
required: false,
default: ''
},
id: {
type: Number,
required: false,
default: 0
}
},
watch: {
visible(newValue, oldValue) {
this.$log.debug('old value', oldValue)
this.$log.debug('new value', newValue)
if (newValue) {
this.loadComments()
}
}
},
methods: {
loadComments() {
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
commentApi.commentTree(this.target, this.id, this.queryParam).then(response => {
this.comments = response.data.data.content
this.pagination.total = response.data.data.total
})
},
handlePaginationChange(page, pageSize) {
this.pagination.page = page
this.pagination.size = pageSize
this.loadComments()
},
handleCommentReply(comment) {
this.selectedComment = comment
this.replyCommentVisible = true
this.replyComment.parentId = comment.id
this.replyComment.postId = this.id
},
handleComment() {
this.replyComment.postId = this.id
this.commentVisible = true
},
handleCreateClick() {
if (!this.replyComment.content) {
this.$notification['error']({
message: '提示',
description: '评论内容不能为空!'
})
return
}
commentApi.create(this.target, this.replyComment).then(response => {
this.$message.success('回复成功!')
this.replyComment = {}
this.selectedComment = {}
this.replyCommentVisible = false
this.commentVisible = false
this.loadComments()
})
},
handleEditStatusClick(comment, status) {
commentApi.updateStatus(this.target, comment.id, status).then(response => {
this.$message.success('操作成功!')
this.loadComments()
})
},
handleCommentDelete(comment) {
commentApi.delete(this.target, comment.id).then(response => {
this.$message.success('删除成功!')
this.loadComments()
})
},
onReplyClose() {
this.replyComment = {}
this.selectedComment = {}
this.replyCommentVisible = false
},
onCommentClose() {
this.replyComment = {}
this.commentVisible = false
},
onClose() {
this.comments = []
this.pagination = {
page: 1,
size: 10,
sort: ''
}
this.$emit('close', false)
}
}
}
</script>

View File

@ -0,0 +1,129 @@
<template>
<div>
<a-comment>
<template slot="actions">
<a-dropdown
:trigger="['click']"
v-if="comment.status === 'AUDITING'"
>
<span href="javascript:void(0);">通过</span>
<a-menu slot="overlay">
<a-menu-item key="1">
<span
href="javascript:void(0);"
@click="handleEditStatusClick('PUBLISHED')"
>通过</span>
</a-menu-item>
<a-menu-item key="2">
<span href="javascript:void(0);">通过并回复</span>
</a-menu-item>
</a-menu>
</a-dropdown>
<span
v-else-if="comment.status === 'PUBLISHED'"
@click="handleReplyClick"
>回复</span>
<a-popconfirm
v-else-if="comment.status === 'RECYCLE'"
:title="'你确定要还原该评论?'"
@confirm="handleEditStatusClick('PUBLISHED')"
okText="确定"
cancelText="取消"
>
<span>还原</span>
</a-popconfirm>
<a-popconfirm
v-if="comment.status === 'PUBLISHED' || comment.status === 'AUDITING'"
:title="'你确定要将该评论移到回收站?'"
@confirm="handleEditStatusClick('RECYCLE')"
okText="确定"
cancelText="取消"
>
<span>回收站</span>
</a-popconfirm>
<a-popconfirm
:title="'你确定要永久删除该评论?'"
@confirm="handleDeleteClick"
okText="确定"
cancelText="取消"
>
<span>删除</span>
</a-popconfirm>
</template>
<a
slot="author"
:href="comment.authorUrl"
target="_blank"
>
<a-icon
type="user"
v-if="comment.isAdmin"
style="margin-right: 3px;"
/>
{{ comment.author }}
</a>
<a-avatar
size="large"
slot="avatar"
:src="avatar"
:alt="comment.author"
/>
<p
slot="content"
v-html="content"
></p>
<a-tooltip slot="datetime">
<span slot="title">{{ comment.createTime | moment }}</span>
<span>{{ comment.createTime | timeAgo }}</span>
</a-tooltip>
<template v-if="comment.children">
<TargetCommentTree
v-for="(child, index) in comment.children"
:key="index"
:comment="child"
v-on="$listeners"
v-bind="$attrs"
@reply="handleReplyClick"
@delete="handleDeleteClick"
@editStatus="handleEditStatusClick"
/>
</template>
</a-comment>
</div>
</template>
<script>
import marked from 'marked'
export default {
name: 'TargetCommentTree',
props: {
comment: {
type: Object,
required: false,
default: null
}
},
computed: {
avatar() {
return `//cn.gravatar.com/avatar/${this.comment.gravatarMd5}/?s=256&d=mp`
},
content() {
return marked(this.comment.content)
}
},
methods: {
handleReplyClick() {
this.$emit('reply', this.comment)
},
handleEditStatusClick(status) {
this.$emit('editStatus', this.comment, status)
},
handleDeleteClick() {
this.$emit('delete', this.comment)
}
}
}
</script>

View File

@ -12,7 +12,7 @@
<analysis-card
:loading="countsLoading"
title="文章"
:number="countsData.postCount"
:number="statisticsData.postCount"
>
<router-link
:to="{ name:'PostList' }"
@ -33,7 +33,7 @@
<analysis-card
:loading="countsLoading"
title="评论"
:number="countsData.commentCount"
:number="statisticsData.commentCount"
>
<router-link
:to="{ name:'Comments' }"
@ -51,27 +51,20 @@
:xs="12"
:style="{ marginBottom: '12px' }"
>
<!-- <analysis-card :loading="countsLoading" title="总访问" :number="countsData.visitCount">
<a-tooltip slot="action">
<template slot="title">文章总访问共 {{ countsData.visitCount }} </template>-->
<analysis-card
:loading="countsLoading"
title="总访问"
:number="countsData.visitCount"
:number="statisticsData.visitCount"
>
<a-tooltip slot="action">
<template slot="title">
文章总访问共
<countTo
:startVal="0"
:endVal="countsData.visitCount"
:endVal="statisticsData.visitCount"
:duration="3000"
></countTo>
</template>
<!-- <countTo :startVal="0" :endVal="countsData.visitCount" :duration="3000"></countTo> -->
<!-- <template>
<countTo :startVal="0" :endVal="countsData.visitCount" :duration="3000"></countTo>
</template>-->
<a href="javascript:void(0);">
<a-icon type="info-circle-o" />
</a>
@ -89,10 +82,10 @@
<analysis-card
:loading="countsLoading"
title="建立天数"
:number="countsData.establishDays"
:number="statisticsData.establishDays"
>
<a-tooltip slot="action">
<template slot="title">博客建立于 {{ countsData.birthday | moment }}</template>
<template slot="title">博客建立于 {{ statisticsData.birthday | moment }}</template>
<a href="javascript:void(0);">
<a-icon type="info-circle-o" />
</a>
@ -129,17 +122,11 @@
>
<a-list-item-meta>
<a
v-if="item.status=='PUBLISHED'"
v-if="item.status=='PUBLISHED' || item.status == 'INTIMATE'"
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"
@ -216,30 +203,15 @@
<a-input
type="textarea"
:autosize="{ minRows: 8 }"
v-model="journal.content"
v-model="journal.sourceContent"
placeholder="写点什么吧..."
/>
</a-form-item>
<!-- 日志图片上传 -->
<!-- <a-form-item v-show="showMoreOptions">
<UploadPhoto
@success="handlerPhotoUploadSuccess"
:photoList="photoList"
></UploadPhoto>
</a-form-item> -->
<a-form-item>
<a-button
type="primary"
@click="handleCreateJournalClick"
>保存</a-button>
<!-- <a
href="javascript:;"
class="more-options-btn"
type="default"
@click="handleUploadPhotoWallClick"
>更多选项<a-icon type="down" /></a> -->
</a-form-item>
</a-form>
</a-card>
@ -295,16 +267,16 @@
destroyOnClose
@close="()=>this.logDrawerVisible = false"
>
<a-skeleton
active
:loading="logsLoading"
:paragraph="{rows: 18}"
<a-row
type="flex"
align="middle"
>
<a-row
type="flex"
align="middle"
>
<a-col :span="24">
<a-col :span="24">
<a-skeleton
active
:loading="logsLoading"
:paragraph="{rows: 18}"
>
<a-list :dataSource="formattedLogsDatas">
<a-list-item
slot="renderItem"
@ -316,22 +288,23 @@
</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>
</a-skeleton>
</a-skeleton>
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="logPagination.page"
:total="logPagination.total"
:defaultPageSize="logPagination.size"
:pageSizeOptions="['50', '100','150','200']"
showSizeChanger
@showSizeChange="handlePaginationChange"
@change="handlePaginationChange"
/>
</div>
</a-col>
</a-row>
<a-divider class="divider-transparent" />
<div class="bottom-control">
<a-popconfirm
@ -354,11 +327,10 @@ import { PageView } from '@/layouts'
import AnalysisCard from './components/AnalysisCard'
import RecentCommentTab from './components/RecentCommentTab'
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 statisticsApi from '@/api/statistics'
import journalApi from '@/api/journal'
export default {
name: 'Dashboard',
@ -367,13 +339,10 @@ export default {
PageView,
AnalysisCard,
RecentCommentTab,
countTo,
UploadPhoto
countTo
},
data() {
return {
photoList: [],
// showMoreOptions: false,
startVal: 0,
logType: logApi.logType,
activityLoading: true,
@ -384,23 +353,27 @@ export default {
logDrawerVisible: false,
postData: [],
logData: [],
countsData: {},
statisticsData: {},
journal: {
content: '',
photos: []
},
journalPhotos: [], //
logs: [],
logPagination: {
page: 1,
size: 50,
sort: null
},
logQueryParam: {
page: 0,
size: 50,
sort: null
},
interval: null
}
},
created() {
this.getCounts()
this.getStatistics()
this.listLatestPosts()
this.listLatestLogs()
},
@ -434,7 +407,7 @@ export default {
beforeRouteEnter(to, from, next) {
next(vm => {
vm.interval = setInterval(() => {
vm.getCounts()
vm.getStatistics()
}, 5000)
})
},
@ -450,18 +423,6 @@ export default {
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)
// },
listLatestPosts() {
postApi.listLatest(5).then(response => {
this.postData = response.data.data
@ -475,9 +436,9 @@ export default {
this.writeLoading = false
})
},
getCounts() {
adminApi.counts().then(response => {
this.countsData = response.data.data
getStatistics() {
statisticsApi.statistics().then(response => {
this.statisticsData = response.data.data
this.countsLoading = false
})
},
@ -485,9 +446,7 @@ export default {
this.$router.push({ name: 'PostEdit', query: { postId: post.id } })
},
handleCreateJournalClick() {
//
// this.journal.photos = this.journalPhotos
if (!this.journal.content) {
if (!this.journal.sourceContent) {
this.$notification['error']({
message: '提示',
description: '内容不能为空!'
@ -497,14 +456,8 @@ export default {
journalApi.create(this.journal).then(response => {
this.$message.success('发表成功!')
this.journal = {}
// this.photoList = []
// this.showMoreOptions = false
})
},
// handleUploadPhotoWallClick() {
// //
// this.showMoreOptions = !this.showMoreOptions
// },
handleShowLogDrawer() {
this.logDrawerVisible = true
this.loadLogs()
@ -514,8 +467,10 @@ export default {
setTimeout(() => {
this.logsLoading = false
}, 500)
this.logPagination.page = this.logPagination.page - 1
logApi.pageBy(this.logPagination).then(response => {
this.logQueryParam.page = this.logPagination.page - 1
this.logQueryParam.size = this.logPagination.size
this.logQueryParam.sort = this.logPagination.sort
logApi.pageBy(this.logQueryParam).then(response => {
this.logs = response.data.data.content
this.logPagination.total = response.data.data.total
})
@ -532,7 +487,7 @@ export default {
window.open(response.data, '_blank')
})
},
onPaginationChange(page, pageSize) {
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.logPagination.page = page
this.logPagination.size = pageSize
@ -541,13 +496,3 @@ export default {
}
}
</script>
<style lang="less" scoped>
/* .more-options-btn {
margin-left: 15px;
text-decoration: none;
}
a {
text-decoration: none;
} */
</style>

View File

@ -1,7 +1,7 @@
<template>
<a-card
:loading="loading"
:body-style="{ padding: '18px 24px 18px' }"
:body-style="{ padding: '24px' }"
:bordered="false"
>
<div class="analysis-card-container">

View File

@ -18,13 +18,9 @@
:href="item.authorUrl"
target="_blank"
>{{ item.author }}</a> 发表在 <a
v-if="item.post.status=='PUBLISHED'"
v-if="item.post.status=='PUBLISHED' || item.post.status=='INTIMATE'"
:href="options.blog_url+'/archives/'+item.post.url"
target="_blank"
>{{ 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)"
@ -102,7 +98,7 @@ export default {
computed: {
formmatedCommentData() {
return this.comments.map(comment => {
comment.content = marked(comment.content, { sanitize: true })
comment.content = marked(comment.content)
return comment
})
},

View File

@ -39,53 +39,3 @@ export default {
}
}
</script>
<style lang="less" scoped>
.exception {
min-height: 500px;
height: 80%;
align-items: center;
text-align: center;
margin-top: 150px;
.img {
display: inline-block;
padding-right: 52px;
zoom: 1;
img {
height: 360px;
max-width: 430px;
}
}
.content {
display: inline-block;
flex: auto;
h1 {
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc {
color: rgba(0, 0, 0, 0.45);
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
}
}
.mobile {
.exception {
margin-top: 30px;
.img {
padding-right: unset;
img {
height: 40%;
max-width: 80%;
}
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col
:xl="10"
@ -49,7 +49,11 @@
label="分组:"
:style="{ display: fieldExpand ? 'block' : 'none' }"
>
<a-input v-model="menuToCreate.team" />
<a-auto-complete
:dataSource="teams"
v-model="menuToCreate.team"
allowClear
/>
</a-form-item>
<a-form-item
label="打开方式:"
@ -243,7 +247,8 @@ export default {
menuToCreate: {
target: '_self'
},
fieldExpand: false
fieldExpand: false,
teams: []
}
},
computed: {
@ -256,6 +261,7 @@ export default {
},
created() {
this.loadMenus()
this.loadTeams()
},
methods: {
loadMenus() {
@ -265,6 +271,11 @@ export default {
this.loading = false
})
},
loadTeams() {
menuApi.listTeams().then(response => {
this.teams = response.data.data
})
},
handleSaveClick() {
this.createOrUpdateMenu()
},
@ -280,18 +291,35 @@ export default {
menuApi.delete(id).then(response => {
this.$message.success('删除成功!')
this.loadMenus()
this.loadTeams()
})
},
createOrUpdateMenu() {
if (!this.menuToCreate.name) {
this.$notification['error']({
message: '提示',
description: '菜单名称不能为空!'
})
return
}
if (!this.menuToCreate.url) {
this.$notification['error']({
message: '提示',
description: '菜单地址不能为空!'
})
return
}
if (this.menuToCreate.id) {
menuApi.update(this.menuToCreate.id, this.menuToCreate).then(response => {
this.$message.success('更新成功!')
this.loadMenus()
this.loadTeams()
})
} else {
menuApi.create(this.menuToCreate).then(response => {
this.$message.success('保存成功!')
this.loadMenus()
this.loadTeams()
})
}
this.handleAddMenu()

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col
:xl="18"
@ -14,7 +14,7 @@
<a-form-item>
<codemirror
v-model="content"
:options="options"
:options="codemirrorOptions"
></codemirror>
</a-form-item>
<a-form-item>
@ -78,7 +78,7 @@ export default {
data() {
return {
buttonDisabled: true,
options: {
codemirrorOptions: {
tabSize: 4,
mode: 'text/html',
lineNumbers: true,
@ -157,13 +157,3 @@ export default {
}
}
</script>
<style lang="less">
.CodeMirror {
height: 560px;
}
.CodeMirror-gutters {
border-right: 1px solid #fff3f3;
background-color: #ffffff;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row
:gutter="12"
type="flex"
@ -24,7 +24,8 @@
<div class="theme-thumb">
<img
:alt="item.name"
:src="item.screenshots"
:src="item.screenshots || '/images/placeholder.jpg'"
loading="lazy"
>
</div>
<template
@ -298,10 +299,16 @@ export default {
})
},
handleUpdateTheme(themeId) {
themeApi.update(themeId).then(response => {
this.$message.success('更新成功!')
this.loadThemes()
})
const hide = this.$message.loading('更新中...', 0)
themeApi
.update(themeId)
.then(response => {
this.$message.success('更新成功!')
this.loadThemes()
})
.finally(() => {
hide()
})
},
handleDeleteTheme(key) {
themeApi.delete(key).then(response => {

View File

@ -77,13 +77,20 @@
:key="index.toString()"
:tab="group.label"
>
<a-form layout="vertical">
<a-form
layout="vertical"
:wrapperCol="wrapperCol"
>
<a-form-item
v-for="(item, index1) in group.items"
:label="item.label + ''"
:key="index1"
:wrapper-col="wrapperCol"
>
<p
v-if="item.description && item.description!=''"
slot="help"
v-html="item.description"
></p>
<a-input
v-model="themeSettings[item.name]"
:defaultValue="item.defaultValue"

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col
:xl="10"
@ -300,13 +300,11 @@ export default {
categoryApi.update(this.categoryToCreate.id, this.categoryToCreate).then(response => {
this.$message.success('更新成功!')
this.loadCategories()
this.categoryToCreate = {}
})
} else {
categoryApi.create(this.categoryToCreate).then(response => {
this.$message.success('保存成功!')
this.loadCategories()
this.categoryToCreate = {}
})
}
this.handleAddCategory()

View File

@ -1,11 +1,10 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col :span="24">
<div style="margin-bottom: 16px">
<a-input
v-model="postToStage.title"
v-decorator="['title', { rules: [{ required: true, message: '请输入文章标题' }] }]"
size="large"
placeholder="请输入文章标题"
/>
@ -20,22 +19,24 @@
:ishljs="true"
:autofocus="false"
@imgAdd="handleAttachmentUpload"
@keydown.ctrl.83.native="handleSaveDraft"
@keydown.meta.83.native="handleSaveDraft"
@save="handleSaveDraft(true)"
/>
</div>
</a-col>
</a-row>
<PostSetting
<PostSettingDrawer
:post="postToStage"
:tagIds="selectedTagIds"
:categoryIds="selectedCategoryIds"
:postMetas="selectedPostMetas"
:visible="postSettingVisible"
@close="onPostSettingsClose"
@onRefreshPost="onRefreshPostFromSetting"
@onRefreshTagIds="onRefreshTagIdsFromSetting"
@onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
@onRefreshPostMetas="onRefreshPostMetasFromSetting"
@onSaved="onSaved"
/>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
@ -43,11 +44,13 @@
<footer-tool-bar :style="{ width: isSideMenu() && isDesktop() ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%'}">
<a-button
type="danger"
@click="handleSaveDraft"
@click="handleSaveDraft(false)"
:disabled="saving"
>保存草稿</a-button>
<a-button
@click="handlePreview"
style="margin-left: 8px;"
:disabled="saving"
>预览</a-button>
<a-button
type="primary"
@ -67,7 +70,7 @@
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import moment from 'moment'
import PostSetting from './components/PostSetting'
import PostSettingDrawer from './components/PostSettingDrawer'
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import { toolbars } from '@/core/const'
@ -79,7 +82,7 @@ import attachmentApi from '@/api/attachment'
export default {
mixins: [mixin, mixinDevice],
components: {
PostSetting,
PostSettingDrawer,
haloEditor,
FooterToolBar,
AttachmentDrawer
@ -87,16 +90,15 @@ export default {
data() {
return {
toolbars,
wrapperCol: {
xl: { span: 24 },
sm: { span: 24 },
xs: { span: 24 }
},
attachmentDrawerVisible: false,
postSettingVisible: false,
postToStage: {},
selectedTagIds: [],
selectedCategoryIds: []
selectedCategoryIds: [],
selectedPostMetas: [],
isSaved: false,
contentChanges: 0,
saving: false
}
},
beforeRouteEnter(to, from, next) {
@ -109,6 +111,7 @@ export default {
vm.postToStage = post
vm.selectedTagIds = post.tagIds
vm.selectedCategoryIds = post.categoryIds
vm.selectedPostMetas = post.postMetas
})
}
})
@ -120,6 +123,9 @@ export default {
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
if (window.onbeforeunload) {
window.onbeforeunload = null
}
},
beforeRouteLeave(to, from, next) {
if (this.postSettingVisible) {
@ -128,32 +134,75 @@ export default {
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
next()
if (this.contentChanges <= 1) {
next()
} else if (this.isSaved) {
next()
} else {
this.$confirm({
title: '当前页面数据未保存,确定要离开吗?',
content: h => <div style="color:red;">如果离开当面页面你的数据很可能会丢失</div>,
onOk() {
next()
},
onCancel() {
next(false)
}
})
}
},
mounted() {
window.onbeforeunload = function(e) {
e = e || window.event
if (e) {
e.returnValue = '当前页面数据未保存,确定要离开吗?'
}
return '当前页面数据未保存,确定要离开吗?'
}
},
watch: {
temporaryContent: function(newValue, oldValue) {
if (newValue) {
this.contentChanges++
}
}
},
computed: {
temporaryContent() {
return this.postToStage.originalContent
},
...mapGetters(['options'])
},
methods: {
handleSaveDraft() {
handleSaveDraft(draftOnly = false) {
this.$log.debug('Draft only: ' + draftOnly)
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 = '开始编辑...'
}
this.saving = true
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)
this.$message.success('保存草稿成功!')
})
if (draftOnly) {
postApi.updateDraft(this.postToStage.id, this.postToStage.originalContent).then(response => {
this.$message.success('保存草稿成功!')
this.saving = false
})
} else {
postApi.update(this.postToStage.id, this.postToStage, false).then(response => {
this.$log.debug('Updated post', response.data.data)
this.$message.success('保存草稿成功!')
this.saving = false
})
}
} else {
// Create the post
postApi.create(this.postToStage, false).then(response => {
this.$log.debug('Created post', response.data.data)
this.$message.success('保存草稿成功!')
this.postToStage = response.data.data
this.saving = false
})
}
},
@ -180,15 +229,14 @@ export default {
if (!this.postToStage.title) {
this.postToStage.title = moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')
}
if (!this.postToStage.originalContent) {
this.postToStage.originalContent = '开始编辑...'
}
this.saving = true
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')
this.saving = false
})
})
} else {
@ -198,6 +246,7 @@ export default {
this.postToStage = response.data.data
postApi.preview(this.postToStage.id).then(response => {
window.open(response.data, '_blank')
this.saving = false
})
})
}
@ -214,6 +263,12 @@ export default {
},
onRefreshCategoryIdsFromSetting(categoryIds) {
this.selectedCategoryIds = categoryIds
},
onRefreshPostMetasFromSetting(postMetas) {
this.selectedPostMetas = postMetas
},
onSaved(isSaved) {
this.isSaved = isSaved
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
@ -12,7 +12,10 @@
:sm="24"
>
<a-form-item label="关键词">
<a-input v-model="queryParam.keyword" />
<a-input
v-model="queryParam.keyword"
@keyup.enter="handleQuery()"
/>
</a-form-item>
</a-col>
<a-col
@ -23,7 +26,7 @@
<a-select
v-model="queryParam.status"
placeholder="请选择文章状态"
@change="handleQuery"
@change="handleQuery()"
>
<a-select-option
v-for="status in Object.keys(postStatus)"
@ -41,7 +44,7 @@
<a-select
v-model="queryParam.categoryId"
placeholder="请选择分类"
@change="handleQuery"
@change="handleQuery()"
>
<a-select-option
v-for="category in categories"
@ -58,11 +61,11 @@
<span class="table-page-search-submitButtons">
<a-button
type="primary"
@click="handleQuery"
@click="handleQuery()"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="handleResetParam"
@click="handleResetParam()"
>重置</a-button>
</span>
</a-col>
@ -81,11 +84,11 @@
<a-menu slot="overlay">
<a-menu-item
key="1"
v-if="queryParam.status === 'DRAFT'"
v-if="queryParam.status === 'DRAFT' || queryParam.status === 'RECYCLE'"
>
<a
href="javascript:void(0);"
@click="handlePublishMore"
@click="handleEditStatusMore(postStatus.PUBLISHED.value)"
>
<span>发布</span>
</a>
@ -96,14 +99,25 @@
>
<a
href="javascript:void(0);"
@click="handleRecycleMore"
@click="handleEditStatusMore(postStatus.RECYCLE.value)"
>
<span>移到回收站</span>
</a>
</a-menu-item>
<a-menu-item
key="3"
v-if="queryParam.status === 'RECYCLE'"
v-if="queryParam.status === 'RECYCLE' || queryParam.status === 'PUBLISHED' || queryParam.status === 'INTIMATE'"
>
<a
href="javascript:void(0);"
@click="handleEditStatusMore(postStatus.DRAFT.value)"
>
<span>草稿</span>
</a>
</a-menu-item>
<a-menu-item
key="4"
v-if="queryParam.status === 'RECYCLE' || queryParam.status === 'DRAFT'"
>
<a
href="javascript:void(0);"
@ -140,7 +154,7 @@
<a-icon type="eye" />
{{ item.visits }}
</span>
<span>
<span @click="handleShowPostComments(item)">
<a-icon type="message" />
{{ item.commentCount }}
</span>
@ -222,7 +236,7 @@
style="margin-right: 3px;"
/>
<a
v-if="item.status=='PUBLISHED'"
v-if="item.status=='PUBLISHED' || item.status == 'INTIMATE'"
:href="options.blog_url+'/archives/'+item.url"
target="_blank"
style="text-decoration: none;"
@ -232,17 +246,6 @@
:title="'点击访问【'+item.title+'】'"
>{{ item.title }}</a-tooltip>
</a>
<a
v-else-if="item.status == 'INTIMATE'"
:href="options.blog_url+'/archives/'+item.url+'/password'"
target="_blank"
style="text-decoration: none;"
>
<a-tooltip
placement="top"
:title="'点击访问【'+item.title+'】'"
>{{ item.title }}</a-tooltip>
</a>
<a
v-else-if="item.status=='DRAFT'"
href="javascript:void(0)"
@ -292,6 +295,7 @@
v-else
:rowKey="post => post.id"
:rowSelection="{
selectedRowKeys: selectedRowKeys,
onChange: onSelectionChange,
getCheckboxProps: getCheckboxProps
}"
@ -313,7 +317,7 @@
style="margin-right: 3px;"
/>
<a
v-if="record.status=='PUBLISHED'"
v-if="record.status=='PUBLISHED' || record.status == 'INTIMATE'"
:href="options.blog_url+'/archives/'+record.url"
target="_blank"
style="text-decoration: none;"
@ -323,17 +327,6 @@
: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)"
@ -390,10 +383,12 @@
<span
slot="commentCount"
slot-scope="commentCount"
slot-scope="text,record"
@click="handleShowPostComments(record)"
style="cursor: pointer;"
>
<a-badge
:count="commentCount"
:count="record.commentCount"
:numberStyle="{backgroundColor: '#f38181'} "
:showZero="true"
:overflowCount="999"
@ -476,7 +471,9 @@
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']"
showSizeChanger
@showSizeChange="handlePaginationChange"
@ -486,10 +483,11 @@
</div>
</a-card>
<PostSetting
<PostSettingDrawer
:post="selectedPost"
:tagIds="selectedTagIds"
:categoryIds="selectedCategoryIds"
:postMetas="selectedPostMetas"
:needTitle="true"
:saveDraftButton="false"
:savePublishButton="false"
@ -499,13 +497,24 @@
@onRefreshPost="onRefreshPostFromSetting"
@onRefreshTagIds="onRefreshTagIdsFromSetting"
@onRefreshCategoryIds="onRefreshCategoryIdsFromSetting"
@onRefreshPostMetas="onRefreshPostMetasFromSetting"
/>
<TargetCommentDrawer
:visible="postCommentVisible"
:title="selectedPost.title"
:description="selectedPost.summary"
:target="`posts`"
:id="selectedPost.id"
@close="onPostCommentsClose"
/>
</div>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import PostSetting from './components/PostSetting'
import PostSettingDrawer from './components/PostSettingDrawer'
import TargetCommentDrawer from '../comment/components/TargetCommentDrawer'
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import TagSelect from './components/TagSelect'
import CategoryTree from './components/CategoryTree'
@ -566,15 +575,16 @@ export default {
AttachmentSelectDrawer,
TagSelect,
CategoryTree,
PostSetting
PostSettingDrawer,
TargetCommentDrawer
},
mixins: [mixin, mixinDevice],
data() {
return {
postStatus: postApi.postStatus,
pagination: {
current: 1,
pageSize: 10,
page: 1,
size: 10,
sort: null
},
queryParam: {
@ -588,11 +598,17 @@ export default {
//
columns,
selectedRowKeys: [],
selectedRows: [],
categories: [],
selectedPostMetas: [
{
key: '',
value: ''
}
],
posts: [],
postsLoading: false,
postSettingVisible: false,
postCommentVisible: false,
selectedPost: {},
selectedTagIds: [],
selectedCategoryIds: []
@ -626,8 +642,8 @@ export default {
loadPosts() {
this.postsLoading = true
// Set from pagination
this.queryParam.page = this.pagination.current - 1
this.queryParam.size = this.pagination.pageSize
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
postApi.query(this.queryParam).then(response => {
this.posts = response.data.data.content
@ -650,27 +666,27 @@ export default {
getCheckboxProps(post) {
return {
props: {
disabled: post.status === 'RECYCLE',
disabled: this.queryParam.status == null || this.queryParam.status === '',
name: post.title
}
}
},
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.pagination.current = page
this.pagination.pageSize = pageSize
this.pagination.page = page
this.pagination.size = pageSize
this.loadPosts()
},
handleResetParam() {
this.queryParam.keyword = null
this.queryParam.categoryId = null
this.queryParam.status = null
this.loadPosts()
this.handleClearRowKeys()
this.handlePaginationChange(1, this.pagination.size)
},
handleQuery() {
this.queryParam.page = 0
this.pagination.current = 1
this.loadPosts()
this.handleClearRowKeys()
this.handlePaginationChange(1, this.pagination.size)
},
handleEditStatusClick(postId, status) {
postApi.updateStatus(postId, status).then(response => {
@ -684,61 +700,51 @@ export default {
this.loadPosts()
})
},
handlePublishMore() {
handleEditStatusMore(status) {
if (this.selectedRowKeys.length <= 0) {
this.$message.success('请至少选择一项!')
return
}
for (let index = 0; index < this.selectedRowKeys.length; index++) {
const element = this.selectedRowKeys[index]
postApi.updateStatus(element, 'PUBLISHED').then(response => {
this.$log.debug(`postId: ${element}, status: PUBLISHED`)
this.selectedRowKeys = []
this.loadPosts()
})
}
},
handleRecycleMore() {
if (this.selectedRowKeys.length <= 0) {
this.$message.success('请至少选择一项!')
return
}
for (let index = 0; index < this.selectedRowKeys.length; index++) {
const element = this.selectedRowKeys[index]
postApi.updateStatus(element, 'RECYCLE').then(response => {
this.$log.debug(`postId: ${element}, status: RECYCLE`)
this.selectedRowKeys = []
this.loadPosts()
})
}
postApi.updateStatusInBatch(this.selectedRowKeys, status).then(response => {
this.$log.debug(`postId: ${this.selectedRowKeys}, status: ${status}`)
this.selectedRowKeys = []
this.loadPosts()
})
},
handleDeleteMore() {
if (this.selectedRowKeys.length <= 0) {
this.$message.success('请至少选择一项!')
return
}
for (let index = 0; index < this.selectedRowKeys.length; index++) {
const element = this.selectedRowKeys[index]
postApi.delete(element).then(response => {
this.$log.debug(`delete: ${element}`)
this.selectedRowKeys = []
this.loadPosts()
})
}
postApi.deleteInBatch(this.selectedRowKeys).then(response => {
this.$log.debug(`delete: ${this.selectedRowKeys}`)
this.selectedRowKeys = []
this.loadPosts()
})
},
handleShowPostSettings(post) {
postApi.get(post.id).then(response => {
this.selectedPost = response.data.data
this.selectedTagIds = this.selectedPost.tagIds
this.selectedCategoryIds = this.selectedPost.categoryIds
this.selectedPostMetas = this.selectedPost.postMetas
this.postSettingVisible = true
})
},
handleShowPostComments(post) {
postApi.get(post.id).then(response => {
this.selectedPost = response.data.data
this.postCommentVisible = true
})
},
handlePreview(postId) {
postApi.preview(postId).then(response => {
window.open(response.data, '_blank')
})
},
handleClearRowKeys() {
this.selectedRowKeys = []
},
//
onPostSettingsClose() {
this.postSettingVisible = false
@ -747,6 +753,13 @@ export default {
this.loadPosts()
}, 500)
},
onPostCommentsClose() {
this.postCommentVisible = false
this.selectedPost = {}
setTimeout(() => {
this.loadPosts()
}, 500)
},
onRefreshPostFromSetting(post) {
this.selectedPost = post
},
@ -755,6 +768,9 @@ export default {
},
onRefreshCategoryIdsFromSetting(categoryIds) {
this.selectedCategoryIds = categoryIds
},
onRefreshPostMetasFromSetting(postMetas) {
this.selectedPostMetas = postMetas
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col
:xl="10"

View File

@ -31,7 +31,10 @@
<a-input v-model="selectedPost.url" />
</a-form-item>
<a-form-item label="访问密码:">
<a-input-password v-model="selectedPost.password" />
<a-input-password
v-model="selectedPost.password"
autocomplete="new-password"
/>
</a-form-item>
<a-form-item label="发表时间:">
@ -62,6 +65,19 @@
<a-radio :value="0"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="自定义模板:">
<a-select v-model="selectedPost.template">
<a-select-option
key=""
value=""
></a-select-option>
<a-select-option
v-for="tpl in customTpls"
:key="tpl"
:value="tpl"
>{{ tpl }}</a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
</div>
@ -70,50 +86,50 @@
<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>
<a-form layout="vertical">
<a-form-item>
<category-tree
v-model="selectedCategoryIds"
:categories="categories"
/>
</a-form-item>
<a-form-item v-if="categoryFormVisible">
<category-select-tree
:categories="categories"
v-model="categoryToCreate.parentId"
/>
</a-form-item>
<a-form-item v-if="categoryFormVisible">
<a-input
placeholder="分类名称"
v-model="categoryToCreate.name"
/>
</a-form-item>
<a-form-item v-if="categoryFormVisible">
<a-input
placeholder="分类路径"
v-model="categoryToCreate.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>
<a-divider />
@ -153,9 +169,19 @@
<div class="post-thumb">
<img
class="img"
:src="selectedPost.thumbnail || '/images/placeholder.png'"
:src="selectedPost.thumbnail || '/images/placeholder.jpg'"
@click="()=>this.thumbDrawerVisible=true"
>
<a-form layout="vertial">
<a-form-item>
<a-input
v-model="selectedPost.thumbnail"
placeholder="点击缩略图选择图片,或者输入外部链接"
></a-input>
</a-form-item>
</a-form>
<a-button
class="post-thumb-remove"
type="dashed"
@ -164,6 +190,41 @@
</div>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">元数据</h3>
<a-form layout="vertical">
<a-form-item
v-for="(postMeta, index) in selectedPostMetas"
:key="index"
:prop="'postMetas.' + index + '.value'"
>
<a-row :gutter="5">
<a-col :span="12">
<a-input v-model="postMeta.key"><i slot="addonBefore">K</i></a-input>
</a-col>
<a-col :span="12">
<a-input v-model="postMeta.value">
<i slot="addonBefore">V</i>
<a
href="javascript:void(0);"
slot="addonAfter"
@click.prevent="handleRemovePostMeta(postMeta)"
>
<a-icon type="close" />
</a>
</a-input>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button
type="dashed"
@click="handleInsertPostMeta"
>新增</a-button>
</a-form-item>
</a-form>
</div>
<a-divider class="divider-transparent" />
</div>
</a-skeleton>
@ -177,16 +238,19 @@
style="marginRight: 8px"
@click="handleDraftClick"
v-if="saveDraftButton"
:disabled="saving"
>保存草稿</a-button>
<a-button
@click="handlePublishClick"
type="primary"
v-if="savePublishButton"
:disabled="saving"
>发布</a-button>
<a-button
@click="handlePublishClick"
type="primary"
v-if="saveButton"
:disabled="saving"
>保存</a-button>
</div>
</a-drawer>
@ -201,8 +265,9 @@ import AttachmentSelectDrawer from '../../attachment/components/AttachmentSelect
import { mapGetters } from 'vuex'
import categoryApi from '@/api/category'
import postApi from '@/api/post'
import themeApi from '@/api/theme'
export default {
name: 'PostSetting',
name: 'PostSettingDrawer',
mixins: [mixin, mixinDevice],
components: {
CategoryTree,
@ -219,7 +284,9 @@ export default {
selectedTagIds: this.tagIds,
selectedCategoryIds: this.categoryIds,
categories: [],
categoryToCreate: {}
categoryToCreate: {},
customTpls: [],
saving: false
}
},
props: {
@ -235,6 +302,10 @@ export default {
type: Array,
required: true
},
postMetas: {
type: Array,
required: true
},
visible: {
type: Boolean,
required: false,
@ -261,10 +332,6 @@ export default {
default: false
}
},
created() {
this.loadSkeleton()
this.loadCategories()
},
watch: {
post(val) {
this.selectedPost = val
@ -284,13 +351,24 @@ export default {
selectedCategoryIds(val) {
this.$emit('onRefreshCategoryIds', val)
},
selectedPostMetas(val) {
this.$emit('onRefreshPostMetas', val)
},
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
this.loadCategories()
this.loadPresetMetasField()
this.loadCustomTpls()
}
}
},
computed: {
selectedPostMetas() {
// selectedPostMetasdata
// ,使postMetas
return this.postMetas
},
pickerDefaultValue() {
if (this.selectedPost.createTime) {
var date = new Date(this.selectedPost.createTime)
@ -312,6 +390,26 @@ export default {
this.categories = response.data.data
})
},
loadPresetMetasField() {
if (this.postMetas.length <= 0) {
themeApi.getActivatedTheme().then(response => {
const fields = response.data.data.postMetaField
if (fields && fields.length > 0) {
for (let i = 0, len = fields.length; i < len; i++) {
this.selectedPostMetas.push({
value: '',
key: fields[i]
})
}
}
})
}
},
loadCustomTpls() {
themeApi.customPostTpls().then(response => {
this.customTpls = response.data.data
})
},
handleSelectPostThumb(data) {
this.selectedPost.thumbnail = encodeURI(data.path)
this.thumbDrawerVisible = false
@ -359,24 +457,21 @@ export default {
})
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
// Set post metas
this.selectedPost.postMetas = this.selectedPostMetas
this.saving = true
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.saving = false
this.$emit('onSaved', true)
this.$router.push({ name: 'PostList' })
}
})
@ -386,6 +481,8 @@ export default {
this.$log.debug('Created post', response.data.data)
if (createSuccess) {
createSuccess()
this.saving = false
this.$emit('onSaved', true)
this.$router.push({ name: 'PostList' })
}
this.selectedPost = response.data.data
@ -400,6 +497,18 @@ export default {
},
onPostDateOk(value) {
this.selectedPost.createTime = value.valueOf()
},
handleRemovePostMeta(item) {
var index = this.selectedPostMetas.indexOf(item)
if (index !== -1) {
this.selectedPostMetas.splice(index, 1)
}
},
handleInsertPostMeta() {
this.selectedPostMetas.push({
value: '',
key: ''
})
}
}
}

View File

@ -1,11 +1,10 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col :span="24">
<div style="margin-bottom: 16px">
<a-input
v-model="sheetToStage.title"
v-decorator="['title', { rules: [{ required: true, message: '请输入页面标题' }] }]"
size="large"
placeholder="请输入页面标题"
/>
@ -19,18 +18,20 @@
:ishljs="true"
:autofocus="false"
@imgAdd="handleAttachmentUpload"
@keydown.ctrl.83.native="handleSaveDraft"
@keydown.meta.83.native="handleSaveDraft"
@save="handleSaveDraft"
/>
</div>
</a-col>
</a-row>
<SheetSetting
<SheetSettingDrawer
:sheet="sheetToStage"
:sheetMetas="selectedSheetMetas"
:visible="sheetSettingVisible"
@close="onSheetSettingsClose"
@onRefreshSheet="onRefreshSheetFromSetting"
@onRefreshSheetMetas="onRefreshSheetMetasFromSetting"
@onSaved="onSaved"
/>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
@ -38,10 +39,12 @@
<a-button
type="danger"
@click="handleSaveDraft"
:disabled="saving"
>保存草稿</a-button>
<a-button
@click="handlePreview"
style="margin-left: 8px;"
:disabled="saving"
>预览</a-button>
<a-button
type="primary"
@ -62,7 +65,7 @@ import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import moment from 'moment'
import { toolbars } from '@/core/const'
import SheetSetting from './components/SheetSetting'
import SheetSettingDrawer from './components/SheetSettingDrawer'
import AttachmentDrawer from '../attachment/components/AttachmentDrawer'
import FooterToolBar from '@/components/FooterToolbar'
import { haloEditor } from 'halo-editor'
@ -74,20 +77,19 @@ export default {
haloEditor,
FooterToolBar,
AttachmentDrawer,
SheetSetting
SheetSettingDrawer
},
mixins: [mixin, mixinDevice],
data() {
return {
toolbars,
wrapperCol: {
xl: { span: 24 },
sm: { span: 24 },
xs: { span: 24 }
},
attachmentDrawerVisible: false,
sheetSettingVisible: false,
sheetToStage: {}
sheetToStage: {},
selectedSheetMetas: [],
isSaved: false,
contentChanges: 0,
saving: false
}
},
beforeRouteEnter(to, from, next) {
@ -99,6 +101,7 @@ export default {
sheetApi.get(sheetId).then(response => {
const sheet = response.data.data
vm.sheetToStage = sheet
vm.selectedSheetMetas = sheet.sheetMetas
})
}
})
@ -110,6 +113,9 @@ export default {
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
if (window.onbeforeunload) {
window.onbeforeunload = null
}
},
beforeRouteLeave(to, from, next) {
if (this.sheetSettingVisible) {
@ -118,30 +124,64 @@ export default {
if (this.attachmentDrawerVisible) {
this.attachmentDrawerVisible = false
}
next()
if (this.contentChanges <= 1) {
next()
} else if (this.isSaved) {
next()
} else {
this.$confirm({
title: '当前页面数据未保存,确定要离开吗?',
content: h => <div style="color:red;">如果离开当面页面你的数据很可能会丢失</div>,
onOk() {
next()
},
onCancel() {
next(false)
}
})
}
},
mounted() {
window.onbeforeunload = function(e) {
e = e || window.event
if (e) {
e.returnValue = '当前页面数据未保存,确定要离开吗?'
}
return '当前页面数据未保存,确定要离开吗?'
}
},
watch: {
temporaryContent: function(newValue, oldValue) {
if (newValue) {
this.contentChanges++
}
}
},
computed: {
temporaryContent() {
return this.sheetToStage.originalContent
},
...mapGetters(['options'])
},
methods: {
handleSaveDraft() {
this.sheetToStage.status = 'DRAFT'
this.saving = true
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)
this.$message.success('保存草稿成功!')
this.saving = false
})
} else {
sheetApi.create(this.sheetToStage, false).then(response => {
this.$log.debug('Created sheet', response.data.data)
this.$message.success('保存草稿成功!')
this.sheetToStage = response.data.data
this.saving = false
})
}
},
@ -168,14 +208,13 @@ export default {
if (!this.sheetToStage.title) {
this.sheetToStage.title = moment(new Date()).format('YYYY-MM-DD-HH-mm-ss')
}
if (!this.sheetToStage.originalContent) {
this.sheetToStage.originalContent = '开始编辑...'
}
this.saving = true
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')
this.saving = false
})
})
} else {
@ -184,6 +223,7 @@ export default {
this.sheetToStage = response.data.data
sheetApi.preview(this.sheetToStage.id).then(response => {
window.open(response.data, '_blank')
this.saving = false
})
})
}
@ -193,6 +233,12 @@ export default {
},
onRefreshSheetFromSetting(sheet) {
this.sheetToStage = sheet
},
onRefreshSheetMetasFromSetting(sheetMetas) {
this.selectedSheetMetas = sheetMetas
},
onSaved(isSaved) {
this.isSaved = isSaved
}
}
}

View File

@ -1,12 +1,12 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row>
<a-col :span="24">
<div class="card-container">
<a-tabs type="card">
<a-tab-pane key="internal">
<span slot="tab">
<a-icon type="pushpin" />内置页面
<a-icon type="paper-clip" />内置页面
</span>
<!-- Mobile -->
@ -161,7 +161,7 @@
<a-icon type="eye" />
{{ item.visits }}
</span>
<span>
<span @click="handleShowSheetComments(item)">
<a-icon type="message" />
{{ item.commentCount }}
</span>
@ -181,7 +181,7 @@
</a-menu-item>
<a-menu-item v-else-if="item.status === 'RECYCLE'">
<a-popconfirm
:title="'你确定要发布【' + item.title + '】文章'"
:title="'你确定要发布【' + item.title + '】页面'"
@confirm="handleEditStatusClick(item.id,'PUBLISHED')"
okText="确定"
cancelText="取消"
@ -191,7 +191,7 @@
</a-menu-item>
<a-menu-item v-if="item.status === 'PUBLISHED' || item.status === 'DRAFT'">
<a-popconfirm
:title="'你确定要将【' + item.title + '】文章移到回收站?'"
:title="'你确定要将【' + item.title + '】页面移到回收站?'"
@confirm="handleEditStatusClick(item.id,'RECYCLE')"
okText="确定"
cancelText="取消"
@ -201,7 +201,7 @@
</a-menu-item>
<a-menu-item v-else-if="item.status === 'RECYCLE'">
<a-popconfirm
:title="'你确定要永久删除【' + item.title + '】文章'"
:title="'你确定要永久删除【' + item.title + '】页面'"
@confirm="handleDeleteClick(item.id)"
okText="确定"
cancelText="取消"
@ -245,13 +245,6 @@
slot="title"
style="max-width: 300px;display: block;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"
>
<a-icon
type="pushpin"
v-if="item.topPriority!=0"
theme="twoTone"
twoToneColor="red"
style="margin-right: 3px;"
/>
<a
v-if="item.status=='PUBLISHED'"
:href="options.blog_url+'/archives/'+item.url"
@ -263,17 +256,6 @@
:title="'点击访问【'+item.title+'】'"
>{{ item.title }}</a-tooltip>
</a>
<a
v-else-if="item.status == 'INTIMATE'"
:href="options.blog_url+'/archives/'+item.url+'/password'"
target="_blank"
style="text-decoration: none;"
>
<a-tooltip
placement="top"
:title="'点击访问【'+item.title+'】'"
>{{ item.title }}</a-tooltip>
</a>
<a
v-else-if="item.status=='DRAFT'"
href="javascript:void(0)"
@ -358,10 +340,12 @@
<span
slot="commentCount"
slot-scope="commentCount"
slot-scope="text,record"
@click="handleShowSheetComments(record)"
style="cursor: pointer;"
>
<a-badge
:count="commentCount"
:count="record.commentCount"
:numberStyle="{backgroundColor: '#f38181'} "
:showZero="true"
:overflowCount="999"
@ -460,27 +444,50 @@
</a-dropdown>
</span>
</a-table>
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']"
showSizeChanger
@showSizeChange="handlePaginationChange"
@change="handlePaginationChange"
/>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-col>
</a-row>
<SheetSetting
<SheetSettingDrawer
:sheet="selectedSheet"
:sheetMetas="selectedSheetMetas"
:visible="sheetSettingVisible"
:needTitle="true"
@close="onSheetSettingsClose"
@onRefreshSheet="onRefreshSheetFromSetting"
@onRefreshSheetMetas="onRefreshSheetMetasFromSetting"
/>
<TargetCommentDrawer
:visible="sheetCommentVisible"
:title="selectedSheet.title"
:description="selectedSheet.summary"
:target="`sheets`"
:id="selectedSheet.id"
@close="onSheetCommentsClose"
/>
</div>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import SheetSetting from './components/SheetSetting'
import SheetSettingDrawer from './components/SheetSettingDrawer'
import TargetCommentDrawer from '../comment/components/TargetCommentDrawer'
import sheetApi from '@/api/sheet'
import menuApi from '@/api/menu'
@ -541,16 +548,32 @@ const customColumns = [
export default {
mixins: [mixin, mixinDevice],
components: {
SheetSetting
SheetSettingDrawer,
TargetCommentDrawer
},
data() {
return {
pagination: {
page: 1,
size: 10,
sort: null
},
queryParam: {
page: 0,
size: 10,
sort: null,
keyword: null,
categoryId: null,
status: null
},
sheetsLoading: false,
sheetStatus: sheetApi.sheetStatus,
internalColumns,
customColumns,
selectedSheet: {},
selectedSheetMetas: [],
sheetSettingVisible: false,
sheetCommentVisible: false,
internalSheets: [],
sheets: [],
menu: {}
@ -583,8 +606,12 @@ export default {
methods: {
loadSheets() {
this.sheetsLoading = true
sheetApi.list().then(response => {
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
sheetApi.list(this.queryParam).then(response => {
this.sheets = response.data.data.content
this.pagination.total = response.data.data.total
this.sheetsLoading = false
})
},
@ -619,14 +646,27 @@ export default {
handleShowSheetSettings(sheet) {
sheetApi.get(sheet.id).then(response => {
this.selectedSheet = response.data.data
this.selectedSheetMetas = this.selectedSheet.sheetMetas
this.sheetSettingVisible = true
})
},
handleShowSheetComments(sheet) {
sheetApi.get(sheet.id).then(response => {
this.selectedSheet = response.data.data
this.sheetCommentVisible = true
})
},
handlePreview(sheetId) {
sheetApi.preview(sheetId).then(response => {
window.open(response.data, '_blank')
})
},
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.pagination.page = page
this.pagination.size = pageSize
this.loadSheets()
},
onSheetSettingsClose() {
this.sheetSettingVisible = false
this.selectedSheet = {}
@ -634,8 +674,18 @@ export default {
this.loadSheets()
}, 500)
},
onSheetCommentsClose() {
this.sheetCommentVisible = false
this.selectedSheet = {}
setTimeout(() => {
this.loadSheets()
}, 500)
},
onRefreshSheetFromSetting(sheet) {
this.selectedSheet = sheet
},
onRefreshSheetMetasFromSetting(sheetMetas) {
this.selectedSheetMetas = sheetMetas
}
}
}

View File

@ -67,15 +67,42 @@
</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="selectedSheet.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="sheet-thumb">
<img
class="img"
:src="selectedSheet.thumbnail || '/images/placeholder.png'"
:src="selectedSheet.thumbnail || '/images/placeholder.jpg'"
@click="()=>this.thumbDrawerVisible = true"
>
<a-form layout="vertial">
<a-form-item>
<a-input
v-model="selectedSheet.thumbnail"
placeholder="点击缩略图选择图片,或者输入外部链接"
></a-input>
</a-form-item>
</a-form>
<a-button
class="sheet-thumb-remove"
type="dashed"
@ -84,6 +111,41 @@
</div>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '16px' }">
<h3 class="post-setting-drawer-title">元数据</h3>
<a-form layout="vertical">
<a-form-item
v-for="(sheetMeta, index) in selectedSheetMetas"
:key="index"
:prop="'sheetMeta.' + index + '.value'"
>
<a-row :gutter="5">
<a-col :span="12">
<a-input v-model="sheetMeta.key"><i slot="addonBefore">K</i></a-input>
</a-col>
<a-col :span="12">
<a-input v-model="sheetMeta.value">
<i slot="addonBefore">V</i>
<a
href="javascript:void(0);"
slot="addonAfter"
@click.prevent="handleRemoveSheetMeta(sheetMeta)"
>
<a-icon type="close" />
</a>
</a-input>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button
type="dashed"
@click="handleInsertSheetMeta()"
>新增</a-button>
</a-form-item>
</a-form>
</div>
<a-divider class="divider-transparent" />
</div>
</a-skeleton>
@ -96,10 +158,12 @@
<a-button
style="marginRight: 8px"
@click="handleDraftClick"
:disabled="saving"
>保存草稿</a-button>
<a-button
type="primary"
@click="handlePublishClick"
:disabled="saving"
>发布</a-button>
</div>
</a-drawer>
@ -112,7 +176,7 @@ import { mapGetters } from 'vuex'
import themeApi from '@/api/theme'
import sheetApi from '@/api/sheet'
export default {
name: 'SheetSetting',
name: 'SheetSettingDrawer',
mixins: [mixin, mixinDevice],
components: {
AttachmentSelectDrawer
@ -122,7 +186,8 @@ export default {
thumbDrawerVisible: false,
settingLoading: true,
selectedSheet: this.sheet,
customTpls: []
customTpls: [],
saving: false
}
},
props: {
@ -130,6 +195,10 @@ export default {
type: Object,
required: true
},
sheetMetas: {
type: Array,
required: true
},
needTitle: {
type: Boolean,
required: false,
@ -152,13 +221,20 @@ export default {
selectedSheet(val) {
this.$emit('onRefreshSheet', val)
},
selectedSheetMetas(val) {
this.$emit('onRefreshSheetMetas', val)
},
visible: function(newValue, oldValue) {
if (newValue) {
this.loadSkeleton()
this.loadPresetMetasField()
}
}
},
computed: {
selectedSheetMetas() {
return this.sheetMetas
},
pickerDefaultValue() {
if (this.selectedSheet.createTime) {
var date = new Date(this.selectedSheet.createTime)
@ -175,8 +251,23 @@ export default {
this.settingLoading = false
}, 500)
},
loadPresetMetasField() {
if (this.sheetMetas.length <= 0) {
themeApi.getActivatedTheme().then(response => {
const fields = response.data.data.sheetMetaField
if (fields && fields.length > 0) {
for (let i = 0, len = fields.length; i < len; i++) {
this.selectedSheetMetas.push({
value: '',
key: fields[i]
})
}
}
})
}
},
loadCustomTpls() {
themeApi.customTpls().then(response => {
themeApi.customSheetTpls().then(response => {
this.customTpls = response.data.data
})
},
@ -210,18 +301,16 @@ export default {
})
return
}
if (!this.selectedSheet.originalContent) {
this.$notification['error']({
message: '提示',
description: '页面内容不能为空!'
})
return
}
this.selectedSheet.sheetMetas = this.selectedSheetMetas
this.saving = true
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()
this.saving = false
this.$emit('onSaved', true)
this.$router.push({ name: 'SheetList' })
}
})
} else {
@ -229,6 +318,9 @@ export default {
this.$log.debug('Created sheet', response.data.data)
if (createSuccess) {
createSuccess()
this.saving = false
this.$emit('onSaved', true)
this.$router.push({ name: 'SheetList' })
}
this.selectedSheet = response.data.data
})
@ -242,6 +334,18 @@ export default {
},
onSheetDateOk(value) {
this.selectedSheet.createTime = value.valueOf()
},
handleRemoveSheetMeta(item) {
var index = this.selectedSheetMetas.indexOf(item)
if (index !== -1) {
this.selectedSheetMetas.splice(index, 1)
}
},
handleInsertSheetMeta() {
this.selectedSheetMetas.push({
value: '',
key: ''
})
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row>
<a-col :span="24">
<a-card
@ -14,7 +14,10 @@
:sm="24"
>
<a-form-item label="关键词">
<a-input v-model="queryParam.keyword" />
<a-input
v-model="queryParam.keyword"
@keyup.enter="handleQuery()"
/>
</a-form-item>
</a-col>
<a-col
@ -25,7 +28,7 @@
<a-select
placeholder="请选择状态"
v-model="queryParam.type"
@change="loadJournals(true)"
@change="handleQuery()"
>
<a-select-option
v-for="type in Object.keys(journalType)"
@ -42,11 +45,11 @@
<span class="table-page-search-submitButtons">
<a-button
type="primary"
@click="loadJournals(true)"
@click="handleQuery()"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="resetParam"
@click="resetParam()"
>重置</a-button>
</span>
</a-col>
@ -62,7 +65,9 @@
</div>
<a-divider />
<div style="margin-top:15px">
<a-empty v-if="journals.length==0" />
<a-list
v-else
itemLayout="vertical"
:pagination="false"
:dataSource="journals"
@ -73,46 +78,19 @@
slot-scope="item, index"
:key="index"
>
<!-- 日志图片集合 -->
<!-- <a-card
hoverable
v-for="(photo, photoIndex) in item.photos"
:key="photoIndex"
class="photo-card"
@click="handlerPhotoPreview(photo)"
>
<img alt="example" :src="photo.thumbnail" slot="cover">
</a-card> -->
<!-- <a-modal
:visible="previewVisible"
:footer="null"
@cancel="handleCancelPreview"
>
<img
:alt="previewPhoto.name + previewPhoto.description"
style="width: 100%"
:src="previewPhoto.url"
>
</a-modal> -->
<template slot="actions">
<span>
<a href="javascript:void(0);">
<a-icon
type="like-o"
/>
<a-icon type="like-o" />
{{ item.likes }}
</a>
</span>
<span>
<a
href="javascript:void(0);"
@click="handleCommentShow(item)"
@click="handleShowJournalComments(item)"
>
<a-icon
type="message"
/>
<a-icon type="message" />
{{ item.commentCount }}
</a>
</span>
@ -129,9 +107,6 @@
<a-icon type="unlock" />
</a>
</span>
<!-- <span>
From 微信
</span>-->
</template>
<template slot="extra">
<a
@ -149,7 +124,10 @@
</a-popconfirm>
</template>
<a-list-item-meta :description="item.content">
<a-list-item-meta>
<template slot="description">
<p v-html="item.content" class="journal-list-content"></p>
</template>
<span slot="title">{{ item.createTime | moment }}</span>
<a-avatar
slot="avatar"
@ -161,12 +139,13 @@
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']"
showSizeChanger
@showSizeChange="onPaginationChange"
@change="onPaginationChange"
@showSizeChange="handlePaginationChange"
@change="handlePaginationChange"
/>
</div>
</a-list>
@ -187,6 +166,10 @@
</a-tooltip>
</template>
<template slot="footer">
<a-button
type="dashed"
@click="()=>this.attachmentDrawerVisible = true"
>附件库</a-button>
<a-button
key="submit"
type="primary"
@ -198,7 +181,7 @@
<a-input
type="textarea"
:autosize="{ minRows: 8 }"
v-model="journal.content"
v-model="journal.sourceContent"
/>
</a-form-item>
<a-form-item>
@ -209,119 +192,39 @@
defaultChecked
/>
</a-form-item>
<!-- <a-form-item v-show="showMoreOptions">
<UploadPhoto
@success="handlerPhotoUploadSuccess"
:photoList="photoList"
:plusPhotoVisible="plusPhotoVisible"
></UploadPhoto>
</a-form-item>
<a-form-item>
<a
href="javascript:;"
class="more-options-btn"
type="default"
@click="handleUploadPhotoWallClick"
>
更多选项
<a-icon type="down"/>
</a>
</a-form-item> -->
</a-form>
</a-modal>
<!-- 评论回复弹窗 -->
<a-modal
v-if="selectComment"
:title="'回复给:'+selectComment.author"
v-model="selectCommentVisible"
>
<template slot="footer">
<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-form-item>
</a-form>
</a-modal>
<TargetCommentDrawer
:visible="journalCommentVisible"
:description="journal.content"
:target="`journals`"
:id="journal.id"
@close="onJournalCommentsClose"
/>
<!-- 评论列表抽屉 -->
<a-drawer
title="评论列表"
:width="isMobile()?'100%':'460'"
closable
:visible="commentVisible"
destroyOnClose
@close="()=>this.commentVisible = false"
>
<a-row
type="flex"
align="middle"
>
<a-col :span="24">
<a-comment>
<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-col :span="24">
<journal-comment-tree
v-for="(comment,index) in comments"
:key="index"
:comment="comment"
@reply="handleCommentReplyClick"
@delete="handleCommentDelete"
/>
</a-col>
</a-row>
</a-drawer>
<AttachmentDrawer v-model="attachmentDrawerVisible" />
</div>
</template>
<script>
import JournalCommentTree from './components/JournalCommentTree'
import TargetCommentDrawer from '../../comment/components/TargetCommentDrawer'
import AttachmentDrawer from '../../attachment/components/AttachmentDrawer'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { mapGetters } from 'vuex'
import journalApi from '@/api/journal'
import journalCommentApi from '@/api/journalComment'
import UploadPhoto from '@/components/Upload/UploadPhoto.vue'
export default {
mixins: [mixin, mixinDevice],
components: { JournalCommentTree, UploadPhoto },
components: { TargetCommentDrawer, AttachmentDrawer },
data() {
return {
journalType: journalApi.journalType,
// plusPhotoVisible: true,
// photoList: [], //
// previewVisible: false,
showMoreOptions: false,
// previewPhoto: {
// //
// name: '',
// description: '',
// url: ''
// },
title: '发表',
listLoading: false,
visible: false,
commentVisible: false,
selectCommentVisible: false,
journalCommentVisible: false,
attachmentDrawerVisible: false,
pagination: {
page: 1,
size: 10,
@ -338,8 +241,6 @@ export default {
comments: [],
journal: {},
isPublic: true,
journalPhotos: [], //
selectComment: null,
replyComment: {}
}
},
@ -350,61 +251,30 @@ export default {
...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
// },
loadJournals(isSearch) {
loadJournals() {
this.listLoading = true
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
if (isSearch) {
this.queryParam.page = 0
}
this.listLoading = true
journalApi.query(this.queryParam).then(response => {
this.journals = response.data.data.content
this.pagination.total = response.data.data.total
this.listLoading = false
})
},
handleQuery() {
this.handlePaginationChange(1, this.pagination.size)
},
handleNew() {
this.title = '新建'
this.visible = true
this.journal = {}
//
// 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
},
handleDelete(id) {
journalApi.delete(id).then(response => {
@ -412,27 +282,9 @@ export default {
this.loadJournals()
})
},
handleCommentShow(journal) {
handleShowJournalComments(journal) {
this.journal = journal
journalApi.commentTree(this.journal.id).then(response => {
this.comments = response.data.data.content
this.commentVisible = true
})
},
handleCommentReplyClick(comment) {
this.selectComment = comment
this.selectCommentVisible = true
this.replyComment.parentId = comment.id
this.replyComment.postId = this.journal.id
},
handleReplyComment() {
journalCommentApi.create(this.replyComment).then(response => {
this.$message.success('回复成功!')
this.replyComment = {}
this.selectComment = {}
this.selectCommentVisible = false
this.handleCommentShow(this.journal)
})
this.journalCommentVisible = true
},
handleCommentDelete(comment) {
journalCommentApi.delete(comment.id).then(response => {
@ -441,11 +293,9 @@ export default {
})
},
createOrUpdateJournal() {
//
// this.journal.photos = this.journalPhotos
this.journal.type = this.isPublic ? 'PUBLIC' : 'INTIMATE'
if (!this.journal.content) {
if (!this.journal.sourceContent) {
this.$notification['error']({
message: '提示',
description: '发布内容不能为空!'
@ -463,35 +313,26 @@ export default {
journalApi.create(this.journal).then(response => {
this.$message.success('发表成功!')
this.loadJournals()
// this.photoList = []
this.isPublic = true
})
}
this.visible = false
},
onPaginationChange(page, pageSize) {
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.pagination.page = page
this.pagination.size = pageSize
this.loadJournals()
},
onJournalCommentsClose() {
this.journal = {}
this.journalCommentVisible = false
},
resetParam() {
this.queryParam.keyword = null
this.queryParam.type = null
this.loadJournals()
this.handlePaginationChange(1, this.pagination.size)
}
}
}
</script>
<style scoped="scoped">
/* .more-options-btn {
margin-left: 15px;
text-decoration: none;
}
.photo-card {
width: 104px;
display: inline-block;
margin-right: 5px;
} */
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row :gutter="12">
<a-col
:xl="10"
@ -21,16 +21,25 @@
label="网站地址:"
help="* 需要加上 http://"
>
<a-input v-model="link.url" />
<a-input v-model="link.url">
<!-- <a
href="javascript:void(0);"
slot="addonAfter"
@click="handleParseUrl"
>
<a-icon type="sync" />
</a> -->
</a-input>
</a-form-item>
<a-form-item label="Logo">
<a-input v-model="link.logo" />
</a-form-item>
<a-form-item
label="分组:"
help="* 非必填"
>
<a-input v-model="link.team" />
<a-form-item label="分组:">
<a-auto-complete
:dataSource="teams"
v-model="link.team"
allowClear
/>
</a-form-item>
<a-form-item label="排序编号:">
<a-input
@ -226,7 +235,8 @@ export default {
loading: false,
columns,
links: [],
link: {}
link: {},
teams: []
}
},
computed: {
@ -239,6 +249,7 @@ export default {
},
created() {
this.loadLinks()
this.loadTeams()
},
methods: {
loadLinks() {
@ -248,6 +259,11 @@ export default {
this.loading = false
})
},
loadTeams() {
linkApi.listTeams().then(response => {
this.teams = response.data.data
})
},
handleSaveClick() {
this.createOrUpdateLink()
},
@ -265,18 +281,40 @@ export default {
linkApi.delete(id).then(response => {
this.$message.success('删除成功!')
this.loadLinks()
this.loadTeams()
})
},
handleParseUrl() {
linkApi.getByParse(this.link.url).then(response => {
this.link = response.data.data
})
},
createOrUpdateLink() {
if (!this.link.name) {
this.$notification['error']({
message: '提示',
description: '网站名称不能为空!'
})
return
}
if (!this.link.url) {
this.$notification['error']({
message: '提示',
description: '网站地址不能为空!'
})
return
}
if (this.link.id) {
linkApi.update(this.link.id, this.link).then(response => {
this.$message.success('更新成功!')
this.loadLinks()
this.loadTeams()
})
} else {
linkApi.create(this.link).then(response => {
this.$message.success('保存成功!')
this.loadLinks()
this.loadTeams()
})
}
this.handleAddLink()

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row
:gutter="12"
type="flex"
@ -7,7 +7,7 @@
>
<a-col
:span="24"
class="search-box"
style="padding-bottom: 12px;"
>
<a-card
:bordered="false"
@ -31,7 +31,7 @@
<a-form-item label="分组">
<a-select
v-model="queryParam.team"
@change="loadPhotos(true)"
@change="handleQuery()"
>
<a-select-option
v-for="(item,index) in teams"
@ -48,18 +48,18 @@
<span class="table-page-search-submitButtons">
<a-button
type="primary"
@click="loadPhotos(true)"
@click="handleQuery()"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="resetParam"
@click="resetParam()"
>重置</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div class="table-operator">
<div class="table-operator" style="margin-bottom: 0;">
<a-button
type="primary"
icon="plus"
@ -85,9 +85,9 @@
@click="showDrawer(item)"
>
<div class="photo-thumb">
<img :src="item.thumbnail">
<img :src="item.thumbnail" loading="lazy">
</div>
<a-card-meta>
<a-card-meta style="padding: 0.8rem;">
<ellipsis
:length="isMobile()?12:16"
tooltip
@ -101,6 +101,7 @@
</a-row>
<div class="page-wrapper">
<a-pagination
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['18', '36', '54','72','90','108']"
@ -129,13 +130,15 @@
>
<div class="photo-detail-img">
<img
:src="photo.url || '/images/placeholder.png'"
:src="photo.url || '/images/placeholder.jpg'"
@click="showThumbDrawer"
style="width: 100%;"
>
</div>
</a-skeleton>
</a-col>
<a-divider />
<a-divider style="margin: 24px 0 12px 0;"/>
<a-col :span="24">
<a-skeleton
active
@ -216,7 +219,12 @@
slot="description"
v-if="editable"
>
<a-input v-model="photo.team" />
<a-auto-complete
:dataSource="teams"
v-model="photo.team"
allowClear
style="width:100%"
/>
</template>
<span
slot="description"
@ -319,20 +327,20 @@ export default {
this.loadTeams()
},
methods: {
loadPhotos(isSearch) {
loadPhotos() {
this.listLoading = true
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
if (isSearch) {
this.queryParam.page = 0
}
this.listLoading = true
photoApi.query(this.queryParam).then(response => {
this.photos = response.data.data.content
this.pagination.total = response.data.data.total
this.listLoading = false
})
},
handleQuery() {
this.handlePaginationChange(1, this.pagination.size)
},
loadTeams() {
photoApi.listTeams().then(response => {
this.teams = response.data.data
@ -343,11 +351,13 @@ export default {
photoApi.update(this.photo.id, this.photo).then(response => {
this.$message.success('照片更新成功!')
this.loadPhotos()
this.loadTeams()
})
} else {
photoApi.create(this.photo).then(response => {
this.$message.success('照片添加成功!')
this.loadPhotos()
this.loadTeams()
this.photo = response.data.data
})
}
@ -375,6 +385,7 @@ export default {
this.$message.success('删除成功!')
this.onDrawerClose()
this.loadPhotos()
this.loadTeams()
})
},
showThumbDrawer() {
@ -388,7 +399,7 @@ export default {
resetParam() {
this.queryParam.keyword = null
this.queryParam.team = null
this.loadPhotos()
this.handlePaginationChange(1, this.pagination.size)
this.loadTeams()
},
onDrawerClose() {
@ -399,40 +410,3 @@ export default {
}
}
</script>
<style lang="less" scoped>
.ant-divider-horizontal {
margin: 24px 0 12px 0;
}
.search-box {
padding-bottom: 12px;
}
.photo-thumb {
width: 100%;
margin: 0 auto;
position: relative;
padding-bottom: 56%;
overflow: hidden;
img {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
}
.ant-card-meta {
padding: 0.8rem;
}
.photo-detail-img img {
width: 100%;
}
.table-operator {
margin-bottom: 0;
}
</style>

View File

@ -1,69 +0,0 @@
<template>
<div>
<a-comment>
<span
slot="actions"
@click="handleReplyClick"
>回复</span>
<a-popconfirm
:title="'你确定要永久删除该评论?'"
@confirm="handleDeleteClick"
okText="确定"
cancelText="取消"
slot="actions"
>
<span>删除</span>
</a-popconfirm>
<a slot="author"> {{ comment.author }} </a>
<a-avatar
slot="avatar"
:src="avatar"
:alt="comment.author"
/>
<p slot="content">{{ comment.content }}</p>
<template v-if="comment.children">
<journal-comment-tree
v-for="(child, index) in comment.children"
:key="index"
:comment="child"
@reply="handleSubReply"
@delete="handleSubDelete"
/>
</template>
</a-comment>
</div>
</template>
<script>
export default {
name: 'JournalCommentTree',
props: {
comment: {
type: Object,
required: false,
default: null
}
},
computed: {
avatar() {
return `//cn.gravatar.com/avatar/${this.comment.gravatarMd5}/?s=256&d=mp`
}
},
methods: {
handleReplyClick() {
this.$emit('reply', this.comment)
},
handleSubReply(comment) {
this.$emit('reply', comment)
},
handleDeleteClick() {
this.$emit('delete', this.comment)
},
handleSubDelete(comment) {
this.$emit('delete', comment)
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="page-header-index-wide">
<div>
<a-row>
<a-col :span="24">
<a-card

View File

@ -1,186 +0,0 @@
<template>
<div class="page-header-index-wide">
<div class="card-container">
<a-tabs type="card">
<a-tab-pane key="1">
<span slot="tab">
<a-icon type="folder" />资源文件备份
</span>
<a-table
:columns="columns"
:dataSource="ResourcesData"
>
<span
slot="action"
slot-scope="text, record"
>
<a
href="javascript:;"
@click="downResources('ResourcesData',record.id)"
>下载</a>
<a-divider type="vertical" />
<a
href="javascript:;"
@click="sendResources('ResourcesData',record.id)"
>发送到邮箱</a>
<a-divider type="vertical" />
<a
href="javascript:;"
@click="deleteResources('ResourcesData',record.id)"
>删除</a>
</span>
</a-table>
<a-button
type="primary"
@click="backupData('ResourcesData')"
>备份</a-button>
</a-tab-pane>
<a-tab-pane key="2">
<span slot="tab">
<a-icon type="database" />数据库备份
</span>
<a-table
:columns="columns"
:dataSource="DataBaseData"
>
<span
slot="action"
slot-scope="text, record"
>
<a
href="javascript:;"
@click="downResources('DataBaseData',record.id)"
>下载</a>
<a-divider type="vertical" />
<a
href="javascript:;"
@click="sendResources('DataBaseData',record.id)"
>发送到邮箱</a>
<a-divider type="vertical" />
<a
href="javascript:;"
@click="deleteResources('DataBaseData',record.id)"
>删除</a>
</span>
</a-table>
<a-button
type="primary"
@click="backupData('DataBaseData')"
>备份</a-button>
</a-tab-pane>
<a-tab-pane key="3">
<span slot="tab">
<a-icon type="read" />文章备份
</span>
<a-table
:columns="columns"
:dataSource="FileData"
>
<span
slot="action"
slot-scope="text, record"
>
<a
href="javascript:;"
@click="downResources('FileData',record.id)"
>下载</a>
<a-divider type="vertical" />
<a
href="javascript:;"
@click="sendResources('FileData',record.id)"
>发送到邮箱</a>
<a-divider type="vertical" />
<a
href="javascript:;"
@click="deleteResources('FileData',record.id)"
>删除</a>
</span>
</a-table>
<a-button
type="primary"
@click="backupData('FileData')"
>备份</a-button>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<script>
export default {
components: {},
data() {
return {
num: 0,
columns: [
{
title: '文件名称',
dataIndex: 'name'
},
{
title: '日期',
dataIndex: 'date'
},
{
title: '文件大小',
dataIndex: 'size'
},
{
title: '文件类型',
dataIndex: 'type'
},
{
title: '操作',
scopedSlots: { customRender: 'action' }
}
],
ResourcesData: [],
DataBaseData: [],
FileData: []
}
},
created() {},
methods: {
//
downResources(type, id) {
if (type === 'ResourcesData') {
alert('资源文件下载' + id)
} else if (type === 'DataBaseData') {
alert('数据库文件下载' + id)
} else {
alert('文件下载' + id)
}
},
//
sendResources(type, id) {
if (type === 'ResourcesData') {
alert('资源文件发送到邮箱' + id)
} else if (type === 'DataBaseData') {
alert('数据库文件发送到邮箱' + id)
} else {
alert('文件发送到邮箱' + id)
}
},
//
deleteResources(type, id) {
if (type === 'ResourcesData') {
alert('资源文件删除' + id)
} else if (type === 'DataBaseData') {
alert('数据库文件删除' + id)
} else {
alert('文件删除' + id)
}
},
//
backupData(type) {
if (type === 'ResourcesData') {
alert('资源文件备份')
} else if (type === 'DataBaseData') {
alert('数据库文件备份')
} else {
alert('文件备份')
}
}
}
}
</script>

View File

@ -225,22 +225,6 @@ import recoveryApi from '@/api/recovery'
export default {
data() {
return {
formItemLayout: {
labelCol: {
xs: { span: 24 },
sm: { span: 5 },
lg: { span: 4 },
xl: { span: 4 },
xxl: { span: 3 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 19 },
lg: { span: 20 },
xl: { span: 20 },
xxl: { span: 21 }
}
},
installation: {},
migrationUploadName: 'file',
migrationData: null,

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,69 @@
<template>
<div class="page-header-index-wide">
<div>
<div class="card-content">
<a-row :gutter="12">
<a-col
v-if="options.developer_mode"
:xl="6"
:lg="6"
:md="12"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div slot="title">
<a-icon type="experiment" /> 开发者选项
</div>
<p>点击进入开发者选项页面</p>
<a-button
type="primary"
style="float:right"
@click="handleToDeveloperOptions()"
>进入</a-button>
</a-card>
</a-col>
<a-col
:xl="6"
:lg="6"
:md="12"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
title="Markdown 文章导入"
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div slot="title">
<a-icon type="hdd" /> 博客备份
</div>
<p>支持备份全站数据</p>
<a-button
type="primary"
style="float:right"
@click="()=>this.backupDrawerVisible = true"
>备份</a-button>
</a-card>
</a-col>
<a-col
:xl="6"
:lg="6"
:md="12"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
:bordered="false"
:bodyStyle="{ padding: '16px' }"
>
<div slot="title">
<a-icon type="file-markdown" /> Markdown 文章导入
</div>
<p>支持 Hexo/Jekyll 文章导入并解析元数据</p>
<a-button
type="primary"
@ -38,19 +88,28 @@
:uploadHandler="uploadHandler"
></FilePondUpload>
</a-modal>
<BackupDrawer v-model="backupDrawerVisible"></BackupDrawer>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import backupApi from '@/api/backup'
import BackupDrawer from './components/BackupDrawer'
export default {
components: { BackupDrawer },
data() {
return {
backupDrawerVisible: false,
markdownUpload: false,
uploadHandler: backupApi.importMarkdown
}
},
computed: {
...mapGetters(['options'])
},
methods: {
handleImportMarkdown() {
this.markdownUpload = true
@ -66,6 +125,9 @@ export default {
this.$message.error(`${info.file.name} 导入失败!`)
}
},
handleToDeveloperOptions() {
this.$router.push({ name: 'DeveloperOptions' })
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
}

View File

@ -0,0 +1,194 @@
<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-card
:bordered="false"
:bodyStyle="{ padding: '0' }"
>
<a-list
itemLayout="horizontal"
:dataSource="backupTips"
>
<a-list-item
slot="renderItem"
slot-scope="backupTip"
>
<a-list-item-meta :description="backupTip.description">
<h4 slot="title">{{ backupTip.title }}</h4>
</a-list-item-meta>
<a-alert
slot="extra"
v-if="backupTip.alert"
:message="backupTip.alert.message"
:type="backupTip.alert.type"
banner
/>
</a-list-item>
</a-list>
<a-divider>历史备份</a-divider>
<a-list
itemLayout="vertical"
size="small"
:dataSource="backups"
>
<a-list-item
slot="renderItem"
slot-scope="backup"
>
<a-button
slot="extra"
type="link"
style="color: red"
icon="delete"
:loading="deleting"
@click="handleBackupDeleteClick(backup.filename)"
>删除</a-button>
<a-list-item-meta>
<a
slot="title"
:href="backup.downloadUrl"
>
<a-icon
type="schedule"
style="color: #52c41a"
/>
{{ backup.filename }}
</a>
<p slot="description">{{ backup.updateTime | timeAgo }}/{{ backup.fileSize | fileSizeFormat }}</p>
</a-list-item-meta>
</a-list-item>
<div
v-if="loading"
class="loading-container"
style="position: absolute;bottom: 40px; width: 100%;text-align: center;"
>
<a-spin />
</div>
</a-list>
</a-card>
</a-col>
</a-row>
<a-divider class="divider-transparent" />
<div class="bottom-control">
<a-button
type="primary"
icon="download"
style="marginRight: 8px"
:loading="backuping"
@click="handleBackupClick"
>备份</a-button>
<a-button
type="dashed"
icon="reload"
:loading="loading"
@click="handleBAckupRefreshClick"
>刷新</a-button>
</div>
</a-drawer>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import backupApi from '@/api/backup'
export default {
name: 'BackupDrawer',
mixins: [mixin, mixinDevice],
data() {
return {
backuping: false,
loading: false,
deleting: false,
backups: [],
backupTips: [
{
title: '博客备份',
description:
'将会压缩 Halo 的工作目录到临时文件中,并提供下载链接。如果附件太多的话,可能会十分耗时,请耐心等待!',
alert: {
type: 'warning',
message: '注意:备份后生成的压缩文件存储在临时文件中,重启服务器会造成备份文件的丢失,所以请尽快下载!'
}
},
{ title: '备份查询', description: '查询近期的备份,按照备份时间递减排序。' },
{ title: '备份删除', description: '删除已经备份的内容。' },
{
title: '版本要求',
alert: {
type: 'warning',
message: '注意:要求 Halo server 版本大于 v1.1.1!你可以在 【系统 | 关于】 里面找到系统的版本信息。'
}
}
]
}
},
model: {
prop: 'visible',
event: 'close'
},
props: {
visible: {
type: Boolean,
required: false,
default: true
}
},
watch: {
visible: function(newValue, oldValue) {
if (newValue) {
this.getBackups()
}
}
},
methods: {
getBackups() {
this.loading = true
backupApi
.listHaloBackups()
.then(response => {
this.backups = response.data.data
})
.finally(() => (this.loading = false))
},
handleBackupClick() {
this.backuping = true
backupApi
.backupHalo()
.then(response => {
this.$notification.success({ message: '备份成功!' })
this.getBackups()
})
.finally(() => {
this.backuping = false
})
},
handleBackupDeleteClick(filename) {
this.deleting = true
backupApi
.deleteHaloBackup(filename)
.then(response => {
this.$notification.success({ message: '删除成功!' })
this.getBackups()
})
.finally(() => (this.deleting = false))
},
handleBAckupRefreshClick() {
this.getBackups()
},
onClose() {
this.$emit('close', false)
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<div>
<a-row>
<a-col :span="24">
<div
class="card-container"
v-if="options.developer_mode"
>
<a-tabs type="card">
<a-tab-pane key="environment">
<span slot="tab">
<a-icon type="safety" />运行环境
</span>
<Environment />
</a-tab-pane>
<a-tab-pane key="runtimeLogs">
<span slot="tab">
<a-icon type="code" />实时日志
</span>
<RuntimeLogs />
</a-tab-pane>
<a-tab-pane key="optionsList">
<span slot="tab">
<a-icon type="table" />系统变量
</span>
<OptionsList />
</a-tab-pane>
<a-tab-pane key="applicationConfig">
<span slot="tab">
<a-icon type="file-protect" />配置文件
</span>
<ApplicationConfig />
</a-tab-pane>
<a-tab-pane key="staticStorage">
<span slot="tab">
<a-icon type="cloud" />静态存储
</span>
<StaticStorage />
</a-tab-pane>
<a-tab-pane key="settings">
<span slot="tab">
<a-icon type="setting" />设置
</span>
<SettingsForm />
</a-tab-pane>
</a-tabs>
</div>
<a-alert
v-else
message="提示"
description="当前没有启用开发者选项,请启用之后再访问该页面!"
type="error"
showIcon
/>
</a-col>
</a-row>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Environment from './tabs/Environment'
import RuntimeLogs from './tabs/RuntimeLogs'
import SettingsForm from './tabs/SettingsForm'
import OptionsList from './tabs/OptionsList'
import ApplicationConfig from './tabs/ApplicationConfig'
import StaticStorage from './tabs/StaticStorage'
export default {
components: {
Environment,
RuntimeLogs,
SettingsForm,
OptionsList,
ApplicationConfig,
StaticStorage
},
computed: {
...mapGetters(['options'])
}
}
</script>

View File

@ -0,0 +1,90 @@
<template>
<div>
<a-alert
message="注意:配置文件严格要求代码格式,上下文必须对齐,属性与值之间必须以英文冒号和空格隔开。如格式有误,将无法启动。"
banner
closable
/>
<a-form layout="vertical">
<a-form-item>
<a-skeleton
active
:loading="loading"
:paragraph="{rows: 12}"
>
<codemirror
v-model="content"
:options="codemirrorOptions"
></codemirror>
</a-skeleton>
</a-form-item>
<a-form-item>
<a-popconfirm
:title="'修改配置文件之后需重启才能生效,是否继续?'"
okText="确定"
cancelText="取消"
@confirm="handleUpdateConfig()"
>
<a-button
type="primary"
style="margin-right: 8px;"
>保存</a-button>
</a-popconfirm>
<a-popconfirm
:title="'你确定要重启吗?'"
okText="确定"
cancelText="取消"
@confirm="handleRestartApplication()"
>
<a-button type="danger">重启</a-button>
</a-popconfirm>
</a-form-item>
</a-form>
</div>
</template>
<script>
import { codemirror } from 'vue-codemirror-lite'
import 'codemirror/mode/yaml/yaml.js'
import adminApi from '@/api/admin'
export default {
name: 'ApplicationConfig',
components: {
codemirror
},
data() {
return {
codemirrorOptions: {
tabSize: 4,
mode: 'text/x-yaml',
lineNumbers: true,
line: true
},
content: '',
loading: true
}
},
created() {
this.loadConfig()
},
methods: {
loadConfig() {
this.loading = true
adminApi.getApplicationConfig().then(response => {
this.content = response.data.data
this.loading = false
})
},
handleUpdateConfig() {
adminApi.updateApplicationConfig(this.content).then(response => {
this.$message.success(`配置保存成功!`)
this.loadConfig()
})
},
handleRestartApplication() {
adminApi.restartApplication().then(response => {
this.$message.success(`重启中...`)
})
}
}
}
</script>

View File

@ -0,0 +1,292 @@
<template>
<div>
<a-row :gutter="12">
<a-col
:xl="12"
:lg="12"
:md="24"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
title="服务器"
:bordered="false"
hoverable
:bodyStyle="{ padding: 0 }"
>
<table style="width:100%">
<tbody class="ant-table-tbody">
<tr>
<td>系统</td>
<td>{{ systemProperties['os.name'].value }} {{ systemProperties['os.version'].value }}</td>
</tr>
<tr>
<td>平台</td>
<td>{{ systemProperties['os.arch'].value }}</td>
</tr>
<tr>
<td>语言</td>
<td>{{ systemProperties['user.language'].value }}</td>
</tr>
<tr>
<td>时区</td>
<td>{{ systemProperties['user.timezone'].value }}</td>
</tr>
<tr>
<td>当前用户</td>
<td>{{ systemProperties['user.name'].value }}</td>
</tr>
<tr>
<td>用户目录</td>
<td>{{ systemProperties['user.home'].value }}</td>
</tr>
</tbody>
</table>
</a-card>
<a-divider dashed />
</a-col>
<a-col
:xl="12"
:lg="12"
:md="24"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
title="使用情况"
:bordered="false"
hoverable
:bodyStyle="{ padding: 0 }"
>
<table style="width:100%">
<tbody class="ant-table-tbody">
<tr>
<td>CPU 数量</td>
<td>{{ system.cpu.count }} </td>
</tr>
<tr>
<td>CPU 使用率</td>
<td>{{ system.cpu.usage }} %</td>
</tr>
<tr>
<td>JVM 最大可用内存</td>
<td>{{ jvm.memory.max | fileSizeFormat }}</td>
</tr>
<tr>
<td>JVM 可用内存</td>
<td>{{ jvm.memory.committed | fileSizeFormat }}</td>
</tr>
<tr>
<td>JVM 已用内存</td>
<td>{{ jvm.memory.used | fileSizeFormat }}</td>
</tr>
<tr>
<td>GC 次数</td>
<td>{{ jvm.gc.pause.count }} </td>
</tr>
</tbody>
</table>
</a-card>
<a-divider dashed />
</a-col>
<a-col
:xl="24"
:lg="24"
:md="24"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
title="环境"
:bordered="false"
hoverable
:bodyStyle="{ padding: 0 }"
>
<table style="width:100%">
<tbody class="ant-table-tbody">
<tr>
<td>Java 名称</td>
<td>{{ systemProperties['java.vm.name'].value }}</td>
</tr>
<tr>
<td>Java 版本</td>
<td>{{ systemProperties['java.version'].value }}</td>
</tr>
<tr>
<td>Java Home</td>
<td>
<ellipsis
:length="isMobile() ? 50 : 256"
tooltip
>
{{ systemProperties['java.home'].value }}
</ellipsis>
</td>
</tr>
</tbody>
</table>
</a-card>
<a-divider dashed />
</a-col>
<a-col
:xl="24"
:lg="24"
:md="24"
:sm="24"
:xs="24"
:style="{ marginBottom: '12px' }"
>
<a-card
title="应用"
:bordered="false"
hoverable
:bodyStyle="{ padding: 0 }"
>
<table style="width:100%">
<tbody class="ant-table-tbody">
<tr>
<td>端口</td>
<td>{{ propertiesSourcesMap['server.ports']['local.server.port'].value }}</td>
</tr>
<tr>
<td>PID</td>
<td>{{ systemProperties['PID'].value }}</td>
</tr>
<tr>
<td>启动时间</td>
<td>{{ system.process.startTime | moment }}</td>
</tr>
<tr>
<td>已启动时间</td>
<td>{{ system.process.uptime | dayTime }} </td>
</tr>
<tr>
<td>启动目录</td>
<td>
<ellipsis
:length="isMobile() ? 50 : 256"
tooltip
>
{{ systemProperties['user.dir'].value }}
</ellipsis>
</td>
</tr>
<tr>
<td>日志目录</td>
<td>
<ellipsis
:length="isMobile() ? 50 : 256"
tooltip
>
{{ systemProperties['LOG_FILE'].value }}
</ellipsis>
</td>
</tr>
</tbody>
</table>
</a-card>
</a-col>
</a-row>
<div style="position: fixed;bottom: 30px;right: 30px;">
<a-button
type="primary"
shape="circle"
icon="sync"
size="large"
@click="handleRefresh"
></a-button>
</div>
</div>
</template>
<script>
import { mixin, mixinDevice } from '@/utils/mixin.js'
import actuatorApi from '@/api/actuator'
export default {
name: 'Environment',
mixins: [mixin, mixinDevice],
data() {
return {
propertiesSourcesMap: {},
systemProperties: [],
interval: null,
system: {
cpu: {
count: 0,
usage: 0
},
process: {
cpuUsage: 0,
uptime: 0,
startTime: 0
}
},
jvm: {
memory: {
max: 0,
committed: 0,
used: 0
},
gc: {
pause: {
count: 0
}
}
}
}
},
created() {
this.loadEnv()
this.loadSystemInfo()
this.loadJvmInfo()
},
methods: {
loadEnv() {
actuatorApi.env().then(response => {
const propertiesSources = response.data.propertySources
propertiesSources.forEach(item => {
this.propertiesSourcesMap[item.name] = item.properties
})
this.systemProperties = this.propertiesSourcesMap['systemProperties']
})
},
loadSystemInfo() {
actuatorApi.getSystemCpuCount().then(response => {
this.system.cpu.count = response.data.measurements[0].value
})
actuatorApi.getSystemCpuUsage().then(response => {
this.system.cpu.usage = Number(response.data.measurements[0].value * 100).toFixed(2)
})
actuatorApi.getProcessUptime().then(response => {
this.system.process.uptime = response.data.measurements[0].value
})
actuatorApi.getProcessStartTime().then(response => {
this.system.process.startTime = response.data.measurements[0].value * 1000
})
actuatorApi.getProcessCpuUsage().then(response => {
this.system.process.cpuUsage = response.data.measurements[0].value
})
},
loadJvmInfo() {
actuatorApi.getJvmMemoryMax().then(response => {
this.jvm.memory.max = response.data.measurements[0].value
})
actuatorApi.getJvmMemoryCommitted().then(response => {
this.jvm.memory.committed = response.data.measurements[0].value
})
actuatorApi.getJvmMemoryUsed().then(response => {
this.jvm.memory.used = response.data.measurements[0].value
})
actuatorApi.getJvmGcPause().then(response => {
this.jvm.gc.pause.count = response.data.measurements[0].value
})
},
handleRefresh() {
this.loadSystemInfo()
this.loadJvmInfo()
}
}
}
</script>

View File

@ -0,0 +1,338 @@
<template>
<div class="option-tab-wrapper">
<a-card
:bordered="false"
:bodyStyle="{ padding: 0 }"
>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
<a-col
:md="6"
:sm="24"
>
<a-form-item label="关键词">
<a-input
v-model="queryParam.keyword"
@keyup.enter="handleQuery()"
/>
</a-form-item>
</a-col>
<a-col
:md="6"
:sm="24"
>
<a-form-item label="类型">
<a-select
v-model="queryParam.type"
placeholder="请选择类型"
@change="handleQuery()"
>
<a-select-option
v-for="item in Object.keys(optionType)"
:key="item"
:value="item"
>{{ optionType[item].text }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col
:md="12"
:sm="24"
>
<span class="table-page-search-submitButtons">
<a-button
type="primary"
@click="handleQuery()"
>查询</a-button>
<a-button
style="margin-left: 8px;"
@click="handleResetParam()"
>重置</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div class="table-operator">
<a-button
type="primary"
icon="plus"
@click="()=>this.formVisible=true"
>新增</a-button>
</div>
<div style="margin-top:15px">
<a-table
:rowKey="option => option.id"
:columns="columns"
:dataSource="formattedDatas"
:loading="loading"
:pagination="false"
>
<ellipsis
:length="50"
tooltip
slot="key"
slot-scope="key"
>
{{ key }}
</ellipsis>
<ellipsis
:length="50"
tooltip
slot="value"
slot-scope="value"
>
{{ value }}
</ellipsis>
<span
slot="type"
slot-scope="typeProperty"
>
{{ typeProperty.text }}
</span>
<span
slot="createTime"
slot-scope="createTime"
>
<a-tooltip placement="top">
<template slot="title">
{{ createTime | moment }}
</template>
{{ createTime | timeAgo }}
</a-tooltip>
</span>
<span
slot="updateTime"
slot-scope="updateTime"
>
<a-tooltip placement="top">
<template slot="title">
{{ updateTime | moment }}
</template>
{{ updateTime | timeAgo }}
</a-tooltip>
</span>
<span
slot="action"
slot-scope="text, record"
>
<a
href="javascript:void(0);"
@click="handleEditOption(record)"
>编辑</a>
<a-divider type="vertical" />
<a-popconfirm
:title="'你确定要永久删除该变量?'"
okText="确定"
cancelText="取消"
@confirm="handleDeleteOption(record.id)"
>
<a href="javascript:;">删除</a>
</a-popconfirm>
</span>
</a-table>
<div class="page-wrapper">
<a-pagination
class="pagination"
:current="pagination.page"
:total="pagination.total"
:defaultPageSize="pagination.size"
:pageSizeOptions="['1', '2', '5', '10', '20', '50', '100']"
showSizeChanger
@showSizeChange="handlePaginationChange"
@change="handlePaginationChange"
/>
</div>
</div>
</a-card>
<a-modal
v-model="formVisible"
:title="formTitle"
:afterClose="onFormClose"
>
<template slot="footer">
<a-button
key="submit"
type="primary"
@click="createOrUpdateOption()"
>保存</a-button>
</template>
<a-alert
v-if="optionToStage.type === optionType.INTERNAL.value"
message="注意:在不知道系统变量的具体用途时,请不要随意修改!"
banner
closable
/>
<a-form layout="vertical">
<a-form-item label="Key">
<a-input v-model="optionToStage.key" />
</a-form-item>
<a-form-item label="Value">
<a-input
type="textarea"
:autosize="{ minRows: 5 }"
v-model="optionToStage.value"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
import optionApi from '@/api/option'
import { mapActions } from 'vuex'
const columns = [
{
title: 'Key',
dataIndex: 'key',
scopedSlots: { customRender: 'key' }
},
{
title: 'Value',
dataIndex: 'value',
scopedSlots: { customRender: 'value' }
},
{
title: '类型',
dataIndex: 'typeProperty',
width: '100px',
scopedSlots: { customRender: 'type' }
},
{
title: '创建时间',
dataIndex: 'createTime',
width: '200px',
scopedSlots: { customRender: 'createTime' }
},
{
title: '更新时间',
dataIndex: 'updateTime',
width: '200px',
scopedSlots: { customRender: 'updateTime' }
},
{
title: '操作',
dataIndex: 'action',
width: '120px',
scopedSlots: { customRender: 'action' }
}
]
export default {
name: 'OptionsList',
data() {
return {
optionType: optionApi.type,
columns: columns,
formVisible: false,
pagination: {
page: 1,
size: 10,
sort: null
},
queryParam: {
page: 0,
size: 10,
sort: null,
keyword: null,
status: null
},
optionToStage: {},
loading: false,
options: []
}
},
computed: {
formattedDatas() {
return this.options.map(option => {
option.typeProperty = this.optionType[option.type]
return option
})
},
formTitle() {
return this.optionToStage.id ? '编辑' : '新增'
}
},
created() {
this.loadOptionsList()
},
methods: {
...mapActions(['loadOptions']),
loadOptionsList() {
this.loading = true
this.queryParam.page = this.pagination.page - 1
this.queryParam.size = this.pagination.size
this.queryParam.sort = this.pagination.sort
optionApi.query(this.queryParam).then(response => {
this.options = response.data.data.content
this.pagination.total = response.data.data.total
this.loading = false
})
},
handleQuery() {
this.handlePaginationChange(1, this.pagination.size)
},
handleDeleteOption(id) {
optionApi.delete(id).then(response => {
this.$message.success('删除成功!')
this.loadOptionsList()
this.loadOptions()
})
},
handleEditOption(option) {
this.optionToStage = option
this.formVisible = true
},
handlePaginationChange(page, pageSize) {
this.$log.debug(`Current: ${page}, PageSize: ${pageSize}`)
this.pagination.page = page
this.pagination.size = pageSize
this.loadOptionsList()
},
handleResetParam() {
this.queryParam.keyword = null
this.queryParam.type = null
this.handlePaginationChange(1, this.pagination.size)
},
onFormClose() {
this.formVisible = false
this.optionToStage = {}
},
createOrUpdateOption() {
if (!this.optionToStage.key) {
this.$notification['error']({
message: '提示',
description: 'Key 不能为空!'
})
return
}
if (!this.optionToStage.value) {
this.$notification['error']({
message: '提示',
description: 'Value 不能为空!'
})
return
}
if (this.optionToStage.id) {
optionApi.update(this.optionToStage.id, this.optionToStage).then(response => {
this.$message.success('更新成功!')
this.loadOptionsList()
this.loadOptions()
this.optionToStage = {}
this.formVisible = false
})
} else {
this.optionToStage.type = this.optionType.CUSTOM.value
optionApi.create(this.optionToStage).then(response => {
this.$message.success('保存成功!')
this.loadOptionsList()
this.loadOptions()
this.optionToStage = {}
this.formVisible = false
})
}
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<a-form layout="vertical">
<a-form-item>
<a-skeleton
active
:loading="loading"
:paragraph="{rows: 12}"
>
<codemirror
v-model="logContent"
:options="codemirrorOptions"
></codemirror>
</a-skeleton>
</a-form-item>
<a-form-item>
<a-select
defaultValue="200"
style="margin-right: 8px;width: 100px"
@change="handleLinesChange"
>
<a-select-option value="200">200 </a-select-option>
<a-select-option value="500">500 </a-select-option>
<a-select-option value="800">800 </a-select-option>
<a-select-option value="1000">1000 </a-select-option>
</a-select>
<a-button
type="primary"
style="margin-right: 8px;"
@click="()=>this.loadLogs()"
>刷新</a-button>
<a-button
type="dashed"
@click="handleDownloadLogFile()"
>下载</a-button>
</a-form-item>
</a-form>
</template>
<script>
import { codemirror } from 'vue-codemirror-lite'
import 'codemirror/mode/shell/shell.js'
import adminApi from '@/api/admin'
import moment from 'moment'
export default {
name: 'RuntimeLogs',
components: {
codemirror
},
data() {
return {
codemirrorOptions: {
tabSize: 4,
mode: 'shell',
lineNumbers: true,
line: true
},
logContent: '',
loading: true,
logLines: 200
}
},
created() {
this.loadLogs()
},
methods: {
loadLogs() {
this.loading = true
adminApi.getLogFiles(this.logLines).then(response => {
this.logContent = response.data.data
this.loading = false
})
},
handleDownloadLogFile() {
const hide = this.$message.loading('下载中...', 0)
adminApi
.getLogFiles(this.logLines)
.then(response => {
var blob = new Blob([response.data.data])
var downloadElement = document.createElement('a')
var href = window.URL.createObjectURL(blob)
downloadElement.href = href
downloadElement.download = 'halo-log-' + moment(new Date(), 'YYYY-MM-DD-HH-mm-ss') + '.log'
document.body.appendChild(downloadElement)
downloadElement.click()
document.body.removeChild(downloadElement)
window.URL.revokeObjectURL(href)
this.$message.success('下载成功!')
})
.catch(() => {
this.$message.error('下载失败!')
})
.finally(() => {
hide()
})
},
handleLinesChange(value) {
this.logLines = value
}
}
}
</script>

View File

@ -0,0 +1,46 @@
<template>
<a-form layout="vertical">
<a-form-item label="开发者选项:">
<a-switch v-model="options.developer_mode" />
</a-form-item>
<a-form-item>
<a-button
type="primary"
@click="handleSaveOptions"
>保存</a-button>
</a-form-item>
</a-form>
</template>
<script>
import { mapActions } from 'vuex'
import optionApi from '@/api/option'
export default {
name: 'SettingsForm',
data() {
return {
options: []
}
},
created() {
this.loadFormOptions()
},
methods: {
...mapActions(['loadOptions']),
loadFormOptions() {
optionApi.listAll().then(response => {
this.options = response.data.data
})
},
handleSaveOptions() {
optionApi.save(this.options).then(response => {
this.loadFormOptions()
this.loadOptions()
this.$message.success('保存成功!')
if (!this.options.developer_mode) {
this.$router.push({ name: 'ToolList' })
}
})
}
}
}
</script>

View File

@ -0,0 +1,230 @@
<template>
<div class="option-tab-wrapper">
<a-card
:bordered="false"
:bodyStyle="{ padding: 0 }"
>
<div class="table-operator">
<a-button
type="primary"
icon="cloud-upload"
@click="() => (uploadVisible = true)"
>上传</a-button>
<a-button
icon="plus"
@click="() => (createFolderModal = true)"
>
新建文件夹
</a-button>
<a-button
icon="sync"
@click="loadStaticList"
:loading="loading"
:disabled="loading"
>
刷新
</a-button>
</div>
<div style="margin-top:15px">
<a-table
:rowKey="record => record.name"
:columns="columns"
:dataSource="sortedStatics"
:pagination="false"
size="middle"
:loading="loading"
>
<span
slot="name"
slot-scope="name"
>
<ellipsis
length="64"
tooltip
>
{{ name }}
</ellipsis>
</span>
<span
slot="createTime"
slot-scope="createTime"
>
{{ createTime | moment }}
</span>
<span
slot="action"
slot-scope="text, record"
>
<a
href="javascript:;"
v-if="!record.isFile"
@click="handleUpload(record)"
>上传</a>
<a
:href="options.blog_url+record.relativePath"
target="_blank"
v-else
>访问</a>
<a-divider type="vertical" />
<a-dropdown :trigger="['click']">
<a
href="javascript:void(0);"
class="ant-dropdown-link"
>更多</a>
<a-menu slot="overlay">
<a-menu-item
key="1"
v-if="!record.isFile"
>
<a
href="javascript:;"
@click="handleShowCreateFolderModal(record)"
>创建文件夹</a>
</a-menu-item>
<a-menu-item key="2">
<a-popconfirm
:title="record.isFile?'你确定要删除该文件?':'你确定要删除该文件夹?'"
okText="确定"
cancelText="取消"
@confirm="handleDelete(record.relativePath)"
>
<a href="javascript:;">删除</a>
</a-popconfirm>
</a-menu-item>
</a-menu>
</a-dropdown>
</span>
</a-table>
</div>
</a-card>
<a-modal
title="上传文件"
v-model="uploadVisible"
:footer="null"
:afterClose="onUploadClose"
destroyOnClose
>
<FilePondUpload
ref="upload"
name="file"
:uploadHandler="uploadHandler"
:filed="selectedFile.relativePath"
></FilePondUpload>
</a-modal>
<a-modal
v-model="createFolderModal"
:afterClose="onCreateFolderClose"
title="创建文件夹"
>
<template slot="footer">
<a-button
key="submit"
type="primary"
@click="handleCreateFolder()"
>创建</a-button>
</template>
<a-form layout="vertical">
<a-form-item label="文件夹名:">
<a-input
v-model="createFolderName"
@keyup.enter="handleCreateFolder"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import staticApi from '@/api/static'
const columns = [
{
title: '文件名',
dataIndex: 'name',
scopedSlots: { customRender: 'name' }
},
{
title: '文件类型',
dataIndex: 'mimeType',
scopedSlots: { customRender: 'mimeType' }
},
{
title: '上传时间',
dataIndex: 'createTime',
width: '200px',
scopedSlots: { customRender: 'createTime' }
},
{
title: '操作',
dataIndex: 'action',
width: '120px',
scopedSlots: { customRender: 'action' }
}
]
export default {
name: 'StaticStorage',
data() {
return {
columns: columns,
statics: [],
loading: false,
uploadHandler: staticApi.upload,
uploadVisible: false,
selectedFile: {},
createFolderModal: false,
createFolderName: ''
}
},
created() {
this.loadStaticList()
},
computed: {
...mapGetters(['options']),
sortedStatics() {
const data = this.statics.slice(0)
return data.sort(function(a, b) {
return b.createTime - a.createTime
})
}
},
methods: {
loadStaticList() {
this.loading = true
staticApi.list().then(response => {
this.statics = response.data.data
this.loading = false
})
},
handleDelete(path) {
staticApi.delete(path).then(response => {
this.$message.success(`删除成功!`)
this.loadStaticList()
})
},
handleUpload(file) {
this.selectedFile = file
this.uploadVisible = true
},
handleShowCreateFolderModal(file) {
this.selectedFile = file
this.createFolderModal = true
},
handleCreateFolder() {
staticApi.createFolder(this.selectedFile.relativePath, this.createFolderName).then(response => {
this.$message.success(`创建文件夹成功!`)
this.createFolderModal = false
this.loadStaticList()
})
},
onCreateFolderClose() {
this.selectedFile = {}
this.createFolderName = ''
},
onUploadClose() {
this.$refs.upload.handleClearFileList()
this.selectedFile = {}
this.loadStaticList()
}
}
}
</script>

View File

@ -1,9 +1,12 @@
<template>
<div class="container-wrapper">
<div class="halo-logo animated fadeInUp">
<span>Halo</span>
<span>Halo<small v-if="apiModifyVisible">API </small></span>
</div>
<div class="animated">
<div
v-show="!apiModifyVisible"
class="login-form animated"
>
<a-form
layout="vertical"
@keyup.enter.native="handleLogin"
@ -61,32 +64,65 @@
</a>
</router-link>
<a
@click="handleApiModifyModalOpen"
@click="toggleShowApiForm"
class="tip animated fadeInUp"
:style="{'animation-delay': '0.4s'}"
>
API 设置
<a-icon type="setting" />
</a>
</a-row>
<a-modal
title="API 设置"
:visible="apiModifyVisible"
@ok="handleApiModifyOk"
@cancel="handleApiModifyCancel"
</a-form>
</div>
<div
v-show="apiModifyVisible"
class="api-form animated"
>
<a-form layout="vertical">
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
extra="* 如果 Admin 不是独立部署,请不要更改此 API"
>
<a-form>
<a-form-item extra="如果 halo admin 不是独立部署,请不要更改此 API">
<a-input v-model="apiUrl"></a-input>
</a-form-item>
<a-input
placeholder="API 地址"
v-model="apiUrl"
>
<a-icon
slot="prefix"
type="api"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.2s'}"
>
<a-button
:block="true"
@click="handleApiUrlRestore"
>恢复默认</a-button>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>
<a-button
type="primary"
:block="true"
@click="handleApiModifyOk"
>保存设置</a-button>
</a-form-item>
<a-form-item>
<a-button @click="handleApiUrlRestore">
恢复默认
</a-button>
</a-form-item>
</a-form>
</a-modal>
<a-row>
<a
@click="toggleShowApiForm"
class="tip animated fadeInUp"
:style="{'animation-delay': '0.4s'}"
>
<a-icon type="rollback" />
</a>
</a-row>
</a-form>
</div>
</div>
@ -149,67 +185,23 @@ export default {
this.$router.replace({ name: 'Dashboard' })
}
},
handleApiModifyModalOpen() {
this.apiUrl = this.defaultApiUrl
this.apiModifyVisible = true
},
handleApiModifyOk() {
this.setApiUrl(this.apiUrl)
this.apiModifyVisible = false
},
handleApiModifyCancel() {
this.apiModifyVisible = false
},
handleApiUrlRestore() {
this.restoreApiUrl()
this.apiUrl = this.defaultApiUrl
},
toggleShowApiForm() {
this.apiModifyVisible = !this.apiModifyVisible
if (this.apiModifyVisible) {
this.apiUrl = this.defaultApiUrl
}
},
toggleHidden() {
this.resetPasswordButton = !this.resetPasswordButton
}
}
}
</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,5 +1,5 @@
<template>
<div class="page-header-index-wide page-header-wrapper-grid-content-main">
<div>
<a-row :gutter="12">
<a-col
:lg="10"
@ -40,20 +40,21 @@
<a-icon type="mail" />{{ user.email }}
</p>
<p>
<a-icon type="calendar" />{{ counts.establishDays || 0 }}
<a-icon type="calendar" />{{ statistics.establishDays || 0 }}
</p>
</div>
<a-divider />
<div class="general-profile">
<a-list
:loading="countsLoading"
:loading="statisticsLoading"
itemLayout="horizontal"
>
<a-list-item>累计发表了 {{ counts.postCount || 0 }} 篇文章</a-list-item>
<a-list-item>累计创建了 {{ counts.linkCount || 0 }} 个标签</a-list-item>
<a-list-item>累计获得了 {{ counts.commentCount || 0 }} 条评论</a-list-item>
<a-list-item>累计添加了 {{ counts.linkCount || 0 }} 个友链</a-list-item>
<a-list-item>文章总访问 {{ counts.visitCount || 0 }} </a-list-item>
<a-list-item>累计发表了 {{ statistics.postCount || 0 }} 篇文章</a-list-item>
<a-list-item>累计创建了 {{ statistics.categoryCount || 0 }} 个分类</a-list-item>
<a-list-item>累计创建了 {{ statistics.tagCount || 0 }} 个标签</a-list-item>
<a-list-item>累计获得了 {{ statistics.commentCount || 0 }} 条评论</a-list-item>
<a-list-item>累计添加了 {{ statistics.linkCount || 0 }} 个友链</a-list-item>
<a-list-item>文章总访问 {{ statistics.visitCount || 0 }} </a-list-item>
<a-list-item></a-list-item>
</a-list>
</div>
@ -106,13 +107,13 @@
</span>
<a-form layout="vertical">
<a-form-item label="原密码:">
<a-input-password v-model="passwordParam.oldPassword"/>
<a-input-password v-model="passwordParam.oldPassword" />
</a-form-item>
<a-form-item label="新密码:">
<a-input-password v-model="passwordParam.newPassword"/>
<a-input-password v-model="passwordParam.newPassword" />
</a-form-item>
<a-form-item label="确认密码:">
<a-input-password v-model="passwordParam.confirmPassword"/>
<a-input-password v-model="passwordParam.confirmPassword" />
</a-form-item>
<a-form-item>
<a-button
@ -142,7 +143,7 @@
<script>
import AttachmentSelectDrawer from '../attachment/components/AttachmentSelectDrawer'
import userApi from '@/api/user'
import adminApi from '@/api/admin'
import statisticsApi from '@/api/statistics'
import { mapMutations, mapGetters } from 'vuex'
import MD5 from 'md5.js'
@ -152,10 +153,10 @@ export default {
},
data() {
return {
countsLoading: true,
statisticsLoading: true,
attachmentDrawerVisible: false,
user: {},
counts: {},
statistics: {},
passwordParam: {
oldPassword: null,
newPassword: null,
@ -171,21 +172,15 @@ export default {
...mapGetters(['options'])
},
created() {
this.loadUser()
this.getCounts()
this.getStatistics()
},
methods: {
...mapMutations({ setUser: 'SET_USER' }),
loadUser() {
userApi.getProfile().then(response => {
this.user = response.data.data
this.profileLoading = false
})
},
getCounts() {
adminApi.counts().then(response => {
this.counts = response.data.data
this.countsLoading = false
getStatistics() {
statisticsApi.statisticsWithUser().then(response => {
this.user = response.data.data.user
this.statistics = response.data.data
this.statisticsLoading = false
})
},
handleUpdatePassword() {
@ -242,54 +237,47 @@ export default {
</script>
<style lang="less" scoped>
.page-header-wrapper-grid-content-main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
.profile-center-avatarHolder {
text-align: center;
margin-bottom: 24px;
.profile-center-avatarHolder {
text-align: center;
margin-bottom: 24px;
& > .avatar {
margin: 0 auto;
width: 104px;
height: 104px;
margin-bottom: 20px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
& > .avatar {
margin: 0 auto;
width: 104px;
height: 104px;
margin-bottom: 20px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
.username {
color: rgba(0, 0, 0, 0.85);
font-size: 20px;
line-height: 28px;
font-weight: 500;
margin-bottom: 4px;
img {
height: 100%;
width: 100%;
}
}
.profile-center-detail {
p {
margin-bottom: 8px;
padding-left: 26px;
position: relative;
}
.username {
color: rgba(0, 0, 0, 0.85);
font-size: 20px;
line-height: 28px;
font-weight: 500;
margin-bottom: 4px;
}
}
i {
position: absolute;
height: 14px;
width: 14px;
left: 0;
top: 4px;
}
.profile-center-detail {
p {
margin-bottom: 8px;
padding-left: 26px;
position: relative;
}
i {
position: absolute;
height: 14px;
width: 14px;
left: 0;
top: 4px;
}
}
</style>

View File

@ -67,6 +67,7 @@
v-model="resetParam.password"
type="password"
placeholder="新密码"
autocomplete="new-password"
>
<a-icon
slot="prefix"
@ -83,6 +84,7 @@
v-model="resetParam.confirmPassword"
type="password"
placeholder="确认密码"
autocomplete="new-password"
>
<a-icon
slot="prefix"
@ -148,9 +150,15 @@ export default {
})
return
}
adminApi.sendResetCode(this.resetParam).then(response => {
this.$message.info('邮件发送成功,五分钟内有效')
})
const hide = this.$message.loading('发送中...', 0)
adminApi
.sendResetCode(this.resetParam)
.then(response => {
this.$message.info('邮件发送成功,五分钟内有效')
})
.finally(() => {
hide()
})
},
handleResetPassword() {
if (!this.resetParam.username) {
@ -203,46 +211,3 @@ export default {
}
}
</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

@ -4,12 +4,35 @@ const webpack = require('webpack')
function resolve(dir) {
return path.join(__dirname, dir)
}
const isProd = process.env.NODE_ENV === 'production'
const assetsCDN = {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
marked: 'marked'
},
css: [
],
js: [
'//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
'//cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js',
'//cdn.jsdelivr.net/npm/vuex@3.1.1/dist/vuex.min.js',
'//cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js',
'//cdn.jsdelivr.net/npm/marked@0.8.0/marked.min.js'
]
}
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
],
externals: isProd ? assetsCDN.externals : {}
},
chainWebpack: (config) => {
@ -37,6 +60,12 @@ module.exports = {
.options({
name: 'assets/[name].[hash:8].[ext]'
})
if (isProd) {
config.plugin('html').tap(args => {
args[0].cdn = assetsCDN
return args
})
}
},
css: {