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

View File

@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

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;
@ -788,4 +824,91 @@ body {
li {
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: {