mirror of https://github.com/halo-dev/halo-admin
chore: clean code for next major version
Signed-off-by: Ryan Wang <i@ryanc.cc>pull/478/head
parent
0f5875168d
commit
1eac16acfd
|
@ -1,3 +0,0 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
|
@ -1,3 +0,0 @@
|
|||
NODE_ENV=development
|
||||
PUBLIC_PATH=/
|
||||
VUE_APP_API_URL=http://localhost:8090
|
26
.eslintrc.js
26
.eslintrc.js
|
@ -1,26 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: ['plugin:vue/essential', 'eslint:recommended', 'plugin:prettier/recommended'],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/no-mutating-props': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
printWidth: 120,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'none',
|
||||
arrowParens: 'avoid'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
_
|
|
@ -1,6 +0,0 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
|
||||
git add .
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset']
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"include": ["./src/**/*"]
|
||||
}
|
71
package.json
71
package.json
|
@ -1,71 +0,0 @@
|
|||
{
|
||||
"name": "halo-admin",
|
||||
"version": "1.5.0-alpha.1",
|
||||
"author": "halo-dev",
|
||||
"description": "Halo admin client.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/halo-dev/halo-admin.git"
|
||||
},
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/halo-dev/halo-admin/issues"
|
||||
},
|
||||
"homepage": "https://github.com/halo-dev/halo-admin#readme",
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build --no-module",
|
||||
"lint": "vue-cli-service lint",
|
||||
"test:unit": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/basic-setup": "^0.19.1",
|
||||
"@codemirror/lang-html": "^0.19.4",
|
||||
"@codemirror/lang-java": "^0.19.1",
|
||||
"@halo-dev/admin-api": "^1.0.0-alpha.50",
|
||||
"@halo-dev/editor": "^3.0.0-alpha.2",
|
||||
"ant-design-vue": "^1.7.8",
|
||||
"dayjs": "^1.10.7",
|
||||
"enquire.js": "^2.1.6",
|
||||
"filepond": "^4.30.3",
|
||||
"filepond-plugin-file-validate-type": "^1.2.6",
|
||||
"filepond-plugin-image-preview": "^4.6.10",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"marked": "^4.0.12",
|
||||
"md5.js": "^1.3.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
"verte": "^0.0.12",
|
||||
"vue": "^2.6.14",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"vue-contextmenujs": "^1.3.13",
|
||||
"vue-count-to": "^1.0.13",
|
||||
"vue-filepond": "^6.0.3",
|
||||
"vue-ls": "^3.2.2",
|
||||
"vue-router": "^3.5.3",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-logger": "^1.10.2",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/eslint-parser": "^7.17.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.1",
|
||||
"@vue/cli-plugin-eslint": "~5.0.1",
|
||||
"@vue/cli-plugin-router": "~5.0.1",
|
||||
"@vue/cli-plugin-vuex": "~5.0.1",
|
||||
"@vue/cli-service": "~5.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.4.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.4.1",
|
||||
"husky": "^6.0.0",
|
||||
"less": "^3.13.1",
|
||||
"less-loader": "^5.0.0",
|
||||
"lint-staged": "^11.2.6",
|
||||
"prettier": "^2.5.1",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
}
|
8810
pnpm-lock.yaml
8810
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: [require('autoprefixer'), require('tailwindcss')]
|
||||
}
|
7683
public/color.less
7683
public/color.less
File diff suppressed because it is too large
Load Diff
|
@ -1 +0,0 @@
|
|||
<svg height="45" viewBox="0 0 52 45" width="52" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="122.5%" width="118.8%" x="-9.4%" y="-6.2%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><rect id="b" height="40" rx="4" width="48"/><filter id="c" height="110%" width="108.3%" x="-4.2%" y="-2.5%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/><feColorMatrix in="shadowBlurOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/></filter><mask id="d" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd" filter="url(#a)" transform="translate(2 1)"><use fill="#000" filter="url(#c)" xlink:href="#b"/><use fill="#f0f2f5" fill-rule="evenodd" xlink:href="#b"/><path d="m-1 0h49v10h-49z" fill="#fff" mask="url(#d)"/><path d="m0 0h16v44h-16z" fill="#303648" mask="url(#d)"/></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg height="45" viewBox="0 0 52 45" width="52" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="122.5%" width="118.8%" x="-9.4%" y="-6.2%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><rect id="b" height="40" rx="4" width="48"/><filter id="c" height="110%" width="108.3%" x="-4.2%" y="-2.5%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/><feColorMatrix in="shadowBlurOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/></filter><mask id="d" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd" filter="url(#a)" transform="translate(2 1)"><use fill="#000" filter="url(#c)" xlink:href="#b"/><use fill="#f0f2f5" fill-rule="evenodd" xlink:href="#b"/><g fill="#fff"><path d="m0 0h16v44h-16z" mask="url(#d)"/><path d="m-1 0h49v10h-49z" mask="url(#d)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg height="45" viewBox="0 0 52 45" width="52" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="122.5%" width="118.8%" x="-9.4%" y="-6.2%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><rect id="b" height="40" rx="4" width="48"/><filter id="c" height="110%" width="108.3%" x="-4.2%" y="-2.5%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/><feColorMatrix in="shadowBlurOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/></filter><mask id="d" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd" filter="url(#a)" transform="translate(2 1)"><use fill="#000" filter="url(#c)" xlink:href="#b"/><use fill="#f0f2f5" fill-rule="evenodd" xlink:href="#b"/><path d="m-1 0h49v10h-49z" fill="#fff" mask="url(#d)"/><path d="m0 0h16v44h-16z" fill="#303648" mask="url(#d)"/></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg height="45" viewBox="0 0 52 45" width="52" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="122.5%" width="118.8%" x="-9.4%" y="-6.2%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><rect id="b" height="40" rx="4" width="48"/><filter id="c" height="110%" width="108.3%" x="-4.2%" y="-2.5%"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/><feColorMatrix in="shadowBlurOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/></filter><mask id="d" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd" filter="url(#a)" transform="translate(2 1)"><use fill="#000" filter="url(#c)" xlink:href="#b"/><use fill="#f0f2f5" fill-rule="evenodd" xlink:href="#b"/><path d="m-1 0h49v10h-49z" fill="#303648" mask="url(#d)"/></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,29 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-cmn-Hans">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="generator" content="Halo <%= htmlWebpackPlugin.options.version %>" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>Halo</title>
|
||||
<style>
|
||||
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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but halo admin client doesn't work properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div id="loader"></div>
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
40
src/App.vue
40
src/App.vue
|
@ -1,40 +0,0 @@
|
|||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<div id="app" class="h-full">
|
||||
<router-view />
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
import { DEVICE_TYPE, deviceEnquire } from '@/utils/device'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
locale: zhCN
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const { $store } = this
|
||||
deviceEnquire(deviceType => {
|
||||
switch (deviceType) {
|
||||
case DEVICE_TYPE.DESKTOP:
|
||||
$store.commit('TOGGLE_DEVICE', 'desktop')
|
||||
$store.dispatch('setSidebar', true)
|
||||
break
|
||||
case DEVICE_TYPE.TABLET:
|
||||
$store.commit('TOGGLE_DEVICE', 'tablet')
|
||||
$store.dispatch('setSidebar', false)
|
||||
break
|
||||
case DEVICE_TYPE.MOBILE:
|
||||
default:
|
||||
$store.commit('TOGGLE_DEVICE', 'mobile')
|
||||
$store.dispatch('setSidebar', true)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,233 +0,0 @@
|
|||
<template>
|
||||
<a-modal v-model="modalVisible" :width="isMobile() ? '100%' : '50%'" title="附件详情">
|
||||
<a-row :gutter="24" type="flex">
|
||||
<a-col :lg="9" :md="24" :sm="24" :xl="9" :xs="24">
|
||||
<div class="attach-detail-img pb-3">
|
||||
<a v-if="isImage" :href="attachment.path" target="_blank">
|
||||
<img :src="attachment.path" class="w-full" loading="lazy" />
|
||||
</a>
|
||||
<div v-else>此文件不支持预览</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :lg="15" :md="24" :sm="24" :xl="15" :xs="24">
|
||||
<a-list itemLayout="horizontal">
|
||||
<a-list-item style="padding-top: 0">
|
||||
<a-list-item-meta>
|
||||
<template v-if="editable" slot="description">
|
||||
<a-input ref="nameInput" v-model="attachment.name" @blur="handleUpdateName" />
|
||||
</template>
|
||||
<template v-else slot="description">{{ attachment.name }}</template>
|
||||
<span slot="title">
|
||||
附件名:
|
||||
<a-button class="!p-0" type="link" @click="handleEditName">
|
||||
<a-icon type="edit" />
|
||||
</a-button>
|
||||
</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta :description="attachment.mediaType">
|
||||
<span slot="title">附件类型:</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta :description="attachment.type | typeText">
|
||||
<span slot="title">存储位置:</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template slot="description">
|
||||
{{ attachment.size | fileSizeFormat }}
|
||||
</template>
|
||||
<span slot="title">附件大小:</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="isImage">
|
||||
<a-list-item-meta :description="attachment.height + 'x' + attachment.width">
|
||||
<span slot="title">图片尺寸:</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template slot="description">
|
||||
{{ attachment.createTime | moment }}
|
||||
</template>
|
||||
<span slot="title">上传日期:</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #description>
|
||||
<a :href="attachment.path" target="_blank">{{ attachment.path }}</a>
|
||||
</template>
|
||||
<span slot="title">
|
||||
普通链接:
|
||||
<a-button class="!p-0" type="link" @click="handleCopyLink(`${encodeURI(attachment.path)}`)">
|
||||
<a-icon type="copy" />
|
||||
</a-button>
|
||||
</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="isImage">
|
||||
<a-list-item-meta>
|
||||
<span slot="description">![{{ attachment.name }}]({{ attachment.path }})</span>
|
||||
<span slot="title">
|
||||
Markdown 格式:
|
||||
<a-button
|
||||
class="!p-0"
|
||||
type="link"
|
||||
@click="handleCopyLink(`![${attachment.name}](${encodeURI(attachment.path)})`)"
|
||||
>
|
||||
<a-icon type="copy" />
|
||||
</a-button>
|
||||
</span>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<template #footer>
|
||||
<slot name="extraFooter" />
|
||||
<a-popconfirm cancelText="取消" okText="确定" title="你确定要删除该附件?" @confirm="handleDelete">
|
||||
<ReactiveButton
|
||||
:errored="deleteErrored"
|
||||
:loading="deleting"
|
||||
erroredText="删除失败"
|
||||
icon="delete"
|
||||
loadedText="删除成功"
|
||||
text="删除"
|
||||
type="danger"
|
||||
@callback="handleDeletedCallback"
|
||||
></ReactiveButton>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
import apiClient from '@/utils/api-client'
|
||||
import { attachmentTypes } from '@/core/constant'
|
||||
|
||||
export default {
|
||||
name: 'AttachmentDetailModal',
|
||||
mixins: [mixin, mixinDevice],
|
||||
filters: {
|
||||
typeText(type) {
|
||||
return type ? attachmentTypes[type].text : ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
attachment: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editable: false,
|
||||
deleting: false,
|
||||
deleteErrored: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
isImage() {
|
||||
if (!this.attachment || !this.attachment.mediaType) {
|
||||
return false
|
||||
}
|
||||
return this.attachment.mediaType.startsWith('image')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Deletes the attachment
|
||||
*/
|
||||
async handleDelete() {
|
||||
try {
|
||||
this.deleting = true
|
||||
|
||||
await apiClient.attachment.delete(this.attachment.id)
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
this.deleteErrored = true
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.deleting = false
|
||||
}, 400)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the deletion callback event
|
||||
*/
|
||||
handleDeletedCallback() {
|
||||
this.$emit('delete', this.attachment)
|
||||
this.deleteErrored = false
|
||||
this.modalVisible = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the edit name input
|
||||
*/
|
||||
handleEditName() {
|
||||
this.editable = !this.editable
|
||||
if (this.editable) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.nameInput.focus()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the attachment name
|
||||
*/
|
||||
async handleUpdateName() {
|
||||
if (!this.attachment.name) {
|
||||
this.$notification['error']({
|
||||
message: '提示',
|
||||
description: '附件名称不能为空!'
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
// TODO 修改 sdk 的方法为 updateName
|
||||
await apiClient.attachment.update(this.attachment.id, this.attachment.name)
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.editable = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the copy link event
|
||||
* @param {String} link
|
||||
*/
|
||||
handleCopyLink(link) {
|
||||
this.$copyText(link)
|
||||
.then(message => {
|
||||
this.$log.debug('copy', message)
|
||||
this.$message.success('复制成功!')
|
||||
})
|
||||
.catch(err => {
|
||||
this.$log.debug('copy.err', err)
|
||||
this.$message.error('复制失败!')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,459 +0,0 @@
|
|||
<template>
|
||||
<a-modal v-model="modalVisible" :afterClose="onAfterClose" :title="title" :width="1024" destroyOnClose>
|
||||
<div class="table-page-search-wrapper">
|
||||
<a-form layout="inline">
|
||||
<a-row :gutter="24">
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="关键词:">
|
||||
<a-input v-model="list.params.keyword" @keyup.enter="handleSearch()" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="存储位置:">
|
||||
<a-select
|
||||
v-model="list.params.attachmentType"
|
||||
:loading="types.loading"
|
||||
allowClear
|
||||
@change="handleSearch()"
|
||||
>
|
||||
<a-select-option v-for="item in types.data" :key="item" :value="item">
|
||||
{{ item | typeText }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="文件类型:">
|
||||
<a-select
|
||||
v-model="list.params.mediaType"
|
||||
:loading="mediaTypes.loading"
|
||||
allowClear
|
||||
@change="handleSearch()"
|
||||
>
|
||||
<a-select-option v-for="(item, index) in mediaTypes.data" :key="index" :value="item">
|
||||
{{ item }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<span class="table-page-search-submitButtons">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch()">查询</a-button>
|
||||
<a-button @click="handleResetParam(), handleListAttachments()">重置</a-button>
|
||||
</a-space>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<div class="mb-0 table-operator">
|
||||
<a-button icon="cloud-upload" type="primary" @click="upload.visible = true">上传</a-button>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-list
|
||||
:dataSource="list.data"
|
||||
:grid="{ gutter: 6, xs: 2, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }"
|
||||
:loading="list.loading"
|
||||
class="attachments-group"
|
||||
>
|
||||
<template #renderItem="item, index">
|
||||
<a-list-item
|
||||
@mouseenter="$set(item, 'hover', true)"
|
||||
@mouseleave="$set(item, 'hover', false)"
|
||||
:key="index"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<div :class="`${isItemSelect(item) ? 'border-blue-600' : 'border-slate-200'}`" class="border border-solid">
|
||||
<div class="attach-thumb attachments-group-item">
|
||||
<span v-if="!isImage(item)" class="attachments-group-item-type">{{ item.suffix }}</span>
|
||||
<span
|
||||
v-else
|
||||
:style="`background-image:url(${encodeURI(item.thumbPath)})`"
|
||||
class="attachments-group-item-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<a-card-meta class="p-2 cursor-pointer">
|
||||
<template #description>
|
||||
<a-tooltip :title="item.name">
|
||||
<div class="truncate">{{ item.name }}</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
<a-icon
|
||||
v-show="isItemSelect(item) && !item.hover"
|
||||
type="check-circle"
|
||||
theme="twoTone"
|
||||
class="absolute top-1 right-2 font-bold cursor-pointer transition-all"
|
||||
:style="{ fontSize: '18px', color: 'rgb(37 99 235)' }"
|
||||
/>
|
||||
<a-icon
|
||||
v-show="item.hover"
|
||||
type="profile"
|
||||
theme="twoTone"
|
||||
class="absolute top-1 right-2 font-bold cursor-pointer transition-all"
|
||||
@click.stop="handleOpenDetail(item)"
|
||||
:style="{ fontSize: '18px' }"
|
||||
/>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<a-popover placement="right" title="预览" trigger="click">
|
||||
<template slot="content">
|
||||
<a-tabs v-if="list.selected.length" default-active-key="markdown" tab-position="left">
|
||||
<a-tab-pane key="markdown" tab="Markdown">
|
||||
<div class="text-slate-400" v-html="markdownSyntaxList.join('<br />')"></div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="html" force-render tab="HTML">
|
||||
<div class="text-slate-400">
|
||||
<span v-for="(item, index) in htmlSyntaxList" :key="index" class="text-slate-400">
|
||||
{{ item }}<br />
|
||||
</span>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div v-else class="text-slate-400">未选择附件</div>
|
||||
</template>
|
||||
<a-tooltip placement="top" title="点击预览">
|
||||
<div class="self-center text-slate-400 select-none cursor-pointer hover:text-blue-400 transition-all">
|
||||
已选择 {{ list.selected.length }} 项
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</a-popover>
|
||||
|
||||
<div class="page-wrapper flex justify-end self-center">
|
||||
<a-pagination
|
||||
:current="pagination.page"
|
||||
:defaultPageSize="pagination.size"
|
||||
:pageSizeOptions="['12', '18', '24', '30', '36', '42']"
|
||||
:total="pagination.total"
|
||||
class="pagination !mt-0"
|
||||
showLessItems
|
||||
showSizeChanger
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<a-button @click="modalVisible = false">取消</a-button>
|
||||
<a-button type="primary" :disabled="!list.selected.length" @click="handleConfirm">确定</a-button>
|
||||
</template>
|
||||
|
||||
<AttachmentUploadModal :visible.sync="upload.visible" @close="handleSearch" />
|
||||
|
||||
<AttachmentDetailModal :attachment="list.current" :visible.sync="detailVisible" @delete="handleListAttachments()">
|
||||
<template #extraFooter>
|
||||
<a-button :disabled="selectPreviousButtonDisabled" @click="handleSelectPrevious">上一项</a-button>
|
||||
<a-button :disabled="selectNextButtonDisabled" @click="handleSelectNext">下一项</a-button>
|
||||
<a-button @click="handleItemClick(list.current)" type="primary">
|
||||
{{ list.selected.findIndex(item => item.id === list.current.id) > -1 ? '取消选择' : '选择' }}
|
||||
</a-button>
|
||||
</template>
|
||||
</AttachmentDetailModal>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
import apiClient from '@/utils/api-client'
|
||||
import { attachmentTypes } from '@/core/constant'
|
||||
|
||||
export default {
|
||||
name: 'AttachmentSelectModal',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '选择附件'
|
||||
},
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
list: {
|
||||
data: [],
|
||||
total: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
loading: false,
|
||||
params: {
|
||||
page: 0,
|
||||
size: 12,
|
||||
keyword: undefined,
|
||||
mediaType: undefined,
|
||||
attachmentType: undefined
|
||||
},
|
||||
selected: [],
|
||||
current: {}
|
||||
},
|
||||
|
||||
mediaTypes: {
|
||||
data: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
types: {
|
||||
data: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
upload: {
|
||||
visible: false
|
||||
},
|
||||
|
||||
detailVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
pagination() {
|
||||
return {
|
||||
page: this.list.params.page + 1,
|
||||
size: this.list.params.size,
|
||||
total: this.list.total
|
||||
}
|
||||
},
|
||||
selectPreviousButtonDisabled() {
|
||||
const index = this.list.data.findIndex(attachment => attachment.id === this.list.current.id)
|
||||
return index === 0 && !this.list.hasPrevious
|
||||
},
|
||||
selectNextButtonDisabled() {
|
||||
const index = this.list.data.findIndex(attachment => attachment.id === this.list.current.id)
|
||||
return index === this.list.data.length - 1 && !this.list.hasNext
|
||||
},
|
||||
isImage() {
|
||||
return function (attachment) {
|
||||
if (!attachment || !attachment.mediaType) {
|
||||
return false
|
||||
}
|
||||
return attachment.mediaType.startsWith('image')
|
||||
}
|
||||
},
|
||||
isItemSelect() {
|
||||
return function (attachment) {
|
||||
return this.list.selected.findIndex(item => item.id === attachment.id) > -1
|
||||
}
|
||||
},
|
||||
markdownSyntaxList() {
|
||||
if (!this.list.selected.length) {
|
||||
return []
|
||||
}
|
||||
return this.list.selected.map(item => {
|
||||
return `![${item.name}](${encodeURI(item.path)})`
|
||||
})
|
||||
},
|
||||
htmlSyntaxList() {
|
||||
if (!this.list.selected.length) {
|
||||
return []
|
||||
}
|
||||
return this.list.selected.map(item => {
|
||||
return `<img src="${encodeURI(item.path)}" alt="${item.name}">`
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modalVisible(value) {
|
||||
if (value) {
|
||||
this.handleListAttachments()
|
||||
this.handleListMediaTypes()
|
||||
this.handleListTypes()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* List attachments
|
||||
*/
|
||||
async handleListAttachments() {
|
||||
try {
|
||||
this.list.loading = true
|
||||
|
||||
const response = await apiClient.attachment.list(this.list.params)
|
||||
|
||||
this.list.data = response.data.content
|
||||
this.list.total = response.data.total
|
||||
this.list.hasNext = response.data.hasNext
|
||||
this.list.hasPrevious = response.data.hasPrevious
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.list.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List attachment media types
|
||||
*/
|
||||
async handleListMediaTypes() {
|
||||
try {
|
||||
this.mediaTypes.loading = true
|
||||
|
||||
const response = await apiClient.attachment.listMediaTypes()
|
||||
|
||||
this.mediaTypes.data = response.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.mediaTypes.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List attachment upload types
|
||||
*/
|
||||
async handleListTypes() {
|
||||
try {
|
||||
this.types.loading = true
|
||||
|
||||
const response = await apiClient.attachment.listTypes()
|
||||
|
||||
this.types.data = response.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.types.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle page change
|
||||
*/
|
||||
handlePageChange(page = 1) {
|
||||
this.list.params.page = page - 1
|
||||
this.handleListAttachments()
|
||||
},
|
||||
|
||||
/**
|
||||
* Search attachments
|
||||
*/
|
||||
handleSearch() {
|
||||
this.handlePageChange(1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset search params
|
||||
*/
|
||||
handleResetParam() {
|
||||
this.list.params = {
|
||||
page: 0,
|
||||
size: 12,
|
||||
keyword: undefined,
|
||||
mediaType: undefined,
|
||||
attachmentType: undefined
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle page size change
|
||||
*/
|
||||
handlePageSizeChange(current, size) {
|
||||
this.$log.debug(`Current: ${current}, PageSize: ${size}`)
|
||||
this.list.params.page = 0
|
||||
this.list.params.size = size
|
||||
this.handleListAttachments()
|
||||
},
|
||||
|
||||
handleItemClick(attachment) {
|
||||
// single select
|
||||
if (!this.multiSelect) {
|
||||
this.$emit('confirm', {
|
||||
raw: [attachment],
|
||||
markdown: [`![${attachment.name}](${encodeURI(attachment.path)})`],
|
||||
html: [`<img src="${encodeURI(attachment.path)}" alt="${attachment.name}">`]
|
||||
})
|
||||
this.modalVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
const isSelect = this.list.selected.findIndex(item => item.id === attachment.id) > -1
|
||||
isSelect ? this.handleUnselect(attachment) : this.handleSelect(attachment)
|
||||
},
|
||||
|
||||
handleSelect(attachment) {
|
||||
this.list.selected = [...this.list.selected, attachment]
|
||||
},
|
||||
|
||||
handleUnselect(attachment) {
|
||||
this.list.selected = this.list.selected.filter(item => item.id !== attachment.id)
|
||||
},
|
||||
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', {
|
||||
raw: this.list.selected,
|
||||
markdown: this.markdownSyntaxList,
|
||||
html: this.htmlSyntaxList
|
||||
})
|
||||
this.modalVisible = false
|
||||
},
|
||||
|
||||
handleOpenDetail(attachment) {
|
||||
this.list.current = attachment
|
||||
this.detailVisible = true
|
||||
},
|
||||
|
||||
/**
|
||||
* Select previous attachment
|
||||
*/
|
||||
async handleSelectPrevious() {
|
||||
const index = this.list.data.findIndex(item => item.id === this.list.current.id)
|
||||
if (index > 0) {
|
||||
this.list.current = this.list.data[index - 1]
|
||||
return
|
||||
}
|
||||
if (index === 0 && this.list.hasPrevious) {
|
||||
this.list.params.page--
|
||||
await this.handleListAttachments()
|
||||
|
||||
this.list.current = this.list.data[this.list.data.length - 1]
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select next attachment
|
||||
*/
|
||||
async handleSelectNext() {
|
||||
const index = this.list.data.findIndex(item => item.id === this.list.current.id)
|
||||
if (index < this.list.data.length - 1) {
|
||||
this.list.current = this.list.data[index + 1]
|
||||
return
|
||||
}
|
||||
if (index === this.list.data.length - 1 && this.list.hasNext) {
|
||||
this.list.params.page++
|
||||
await this.handleListAttachments()
|
||||
|
||||
this.list.current = this.list.data[0]
|
||||
}
|
||||
},
|
||||
|
||||
onAfterClose() {
|
||||
this.handleResetParam()
|
||||
this.list.selected = []
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
typeText(type) {
|
||||
return attachmentTypes[type].text
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,39 +0,0 @@
|
|||
<template>
|
||||
<a-modal v-model="modalVisible" :afterClose="onClose" :footer="null" destroyOnClose title="上传附件">
|
||||
<FilePondUpload ref="upload" :uploadHandler="uploadHandler"></FilePondUpload>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'AttachmentUploadModal',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploadHandler: (file, options) => apiClient.attachment.upload(file, options)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$refs.upload.handleClearFileList()
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,100 +0,0 @@
|
|||
<template>
|
||||
<a-button
|
||||
:block="block"
|
||||
:icon="computedIcon"
|
||||
:loading="loading"
|
||||
:size="size"
|
||||
:type="computedType"
|
||||
@click="handleClick"
|
||||
>{{ computedText }}
|
||||
</a-button>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ReactiveButton',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
errored: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
loadedText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
erroredText: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
hasError: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
loading(value) {
|
||||
if (!value) {
|
||||
this.loaded = true
|
||||
if (this.errored) {
|
||||
this.hasError = true
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.loaded = false
|
||||
this.hasError = false
|
||||
this.$emit('callback')
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedType() {
|
||||
if (this.loaded) {
|
||||
return this.hasError ? 'danger' : this.type
|
||||
}
|
||||
return this.type
|
||||
},
|
||||
computedIcon() {
|
||||
if (this.loaded) {
|
||||
return this.hasError ? 'close-circle' : 'check-circle'
|
||||
}
|
||||
return this.icon
|
||||
},
|
||||
computedText() {
|
||||
if (this.loaded) {
|
||||
return this.hasError ? this.erroredText : this.loadedText
|
||||
}
|
||||
return this.text
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,77 +0,0 @@
|
|||
<template>
|
||||
<div ref="codemirrorRef"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { basicSetup } from '@codemirror/basic-setup'
|
||||
import { EditorView, keymap } from '@codemirror/view'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { indentWithTab } from '@codemirror/commands'
|
||||
|
||||
export default {
|
||||
name: 'Codemirror',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'update:value'
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
extensions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '500px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
codemirrorState: null,
|
||||
codemirrorView: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handleInitCodemirror()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.codemirrorView) {
|
||||
this.codemirrorView.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInitCodemirror() {
|
||||
if (this.codemirrorView) {
|
||||
this.codemirrorView.destroy()
|
||||
}
|
||||
|
||||
const codemirrorRef = this.$refs.codemirrorRef
|
||||
|
||||
const onUpdateExtension = EditorView.updateListener.of(vu => {
|
||||
if (vu.docChanged) {
|
||||
const doc = vu.state.doc
|
||||
this.$emit('update:value', doc.toString())
|
||||
}
|
||||
})
|
||||
|
||||
const defaultTheme = EditorView.theme({
|
||||
'&': {
|
||||
height: this.height
|
||||
}
|
||||
})
|
||||
|
||||
this.codemirrorState = EditorState.create({
|
||||
doc: this.value,
|
||||
extensions: [basicSetup, onUpdateExtension, keymap.of([indentWithTab]), defaultTheme, ...this.extensions]
|
||||
})
|
||||
|
||||
this.codemirrorView = new EditorView({
|
||||
state: this.codemirrorState,
|
||||
parent: codemirrorRef
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,79 +0,0 @@
|
|||
<template>
|
||||
<a-list :dataSource="comments" :loading="loading" item-layout="vertical">
|
||||
<template #renderItem="item, index">
|
||||
<a-list-item :key="index" class="!p-0">
|
||||
<a-comment :avatar="item.avatar">
|
||||
<template #author>
|
||||
<a v-if="item.authorUrl" :href="item.authorUrl" class="!text-gray-800 hover:!text-blue-500" target="_blank">
|
||||
{{ item.author }}
|
||||
</a>
|
||||
<span v-else class="!text-gray-500">{{ item.author }}</span>
|
||||
发表在
|
||||
<span class="hover:!text-blue-500 cursor-pointer" @click="handleOpenTarget(item)">
|
||||
《{{ targetTitle(item) }}》
|
||||
</span>
|
||||
</template>
|
||||
<template #content>
|
||||
<p v-html="$options.filters.markdownRender(item.content)" />
|
||||
</template>
|
||||
<template #datetime>
|
||||
<a-tooltip :title="item.createTime | moment">
|
||||
<span>{{ item.createTime | timeAgo }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-comment>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</template>
|
||||
<script>
|
||||
import { datetimeFormat } from '@/utils/datetime'
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'CommentListView',
|
||||
props: {
|
||||
comments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
targetTitle() {
|
||||
return function (comment) {
|
||||
if (comment.post) {
|
||||
return comment.post.title
|
||||
}
|
||||
if (comment.sheet) {
|
||||
return comment.sheet.title
|
||||
}
|
||||
if (comment.journal) {
|
||||
return datetimeFormat(comment.journal.createTime)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleOpenTarget(comment) {
|
||||
const { post, sheet } = comment
|
||||
if (post || sheet) {
|
||||
const { status, fullPath, id } = post || sheet
|
||||
if (['PUBLISHED', 'INTIMATE'].includes(status)) {
|
||||
window.open(fullPath, '_blank')
|
||||
return
|
||||
}
|
||||
if (status === 'DRAFT') {
|
||||
const target = post ? 'post' : 'sheet'
|
||||
const link = await apiClient[target].getPreviewLinkById(id)
|
||||
window.open(link, '_blank')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,119 +0,0 @@
|
|||
<template>
|
||||
<a-modal v-model="modalVisible" destroyOnClose title="评论回复" @close="onClose">
|
||||
<template #footer>
|
||||
<ReactiveButton
|
||||
:errored="submitErrored"
|
||||
:loading="submitting"
|
||||
erroredText="回复失败"
|
||||
loadedText="回复成功"
|
||||
text="回复"
|
||||
type="primary"
|
||||
@callback="handleSubmitCallback"
|
||||
@click="handleSubmit"
|
||||
></ReactiveButton>
|
||||
</template>
|
||||
<a-form-model ref="replyCommentForm" :model="model" :rules="rules" layout="vertical">
|
||||
<a-form-model-item prop="content">
|
||||
<a-input ref="contentInput" v-model="model.content" :autoSize="{ minRows: 8 }" type="textarea" />
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'CommentReplyModal',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
comment: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
targetId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => {
|
||||
return ['post', 'sheet', 'journal'].indexOf(value) !== -1
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model: {},
|
||||
submitting: false,
|
||||
submitErrored: false,
|
||||
rules: {
|
||||
content: [{ required: true, message: '* 内容不能为空', trigger: ['change'] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modalVisible(value) {
|
||||
if (value) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.contentInput.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
const _this = this
|
||||
_this.$refs.replyCommentForm.validate(async valid => {
|
||||
if (valid) {
|
||||
try {
|
||||
_this.submitting = true
|
||||
|
||||
_this.model.postId = _this.targetId
|
||||
|
||||
if (_this.comment) {
|
||||
_this.model.parentId = _this.comment.id
|
||||
}
|
||||
|
||||
await apiClient.comment.create(`${_this.target}s`, _this.model)
|
||||
} catch (e) {
|
||||
_this.submitErrored = true
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
_this.submitting = false
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmitCallback() {
|
||||
if (this.submitErrored) {
|
||||
this.submitErrored = false
|
||||
} else {
|
||||
this.model = {}
|
||||
this.modalVisible = false
|
||||
this.$emit('succeed')
|
||||
}
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.model = {}
|
||||
this.modalVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,157 +0,0 @@
|
|||
<template>
|
||||
<a-modal v-model="modalVisible" :afterClose="onClose" :title="title" :width="1024" destroyOnClose>
|
||||
<a-spin :spinning="list.loading">
|
||||
<TargetCommentTreeNode
|
||||
v-for="(comment, index) in list.data"
|
||||
:key="index"
|
||||
:comment="comment"
|
||||
:target="target"
|
||||
:target-id="targetId"
|
||||
@reload="handleGetComments"
|
||||
/>
|
||||
</a-spin>
|
||||
|
||||
<a-empty v-if="!list.loading && !list.data.length" />
|
||||
|
||||
<div class="page-wrapper">
|
||||
<a-pagination
|
||||
:current="pagination.page"
|
||||
:defaultPageSize="pagination.size"
|
||||
:pageSizeOptions="['10', '20', '50', '100']"
|
||||
:total="pagination.total"
|
||||
class="pagination"
|
||||
showLessItems
|
||||
showSizeChanger
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<slot name="extraFooter" />
|
||||
<a-button type="primary" @click="replyModalVisible = true">创建评论</a-button>
|
||||
<a-button @click="modalVisible = false">关闭</a-button>
|
||||
</template>
|
||||
|
||||
<CommentReplyModal
|
||||
:target="target"
|
||||
:target-id="targetId"
|
||||
:visible.sync="replyModalVisible"
|
||||
@succeed="handleGetComments"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
// components
|
||||
import TargetCommentTreeNode from './TargetCommentTreeNode'
|
||||
import CommentReplyModal from './CommentReplyModal'
|
||||
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'TargetCommentListModal',
|
||||
components: {
|
||||
TargetCommentTreeNode,
|
||||
CommentReplyModal
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '评论'
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => {
|
||||
return ['post', 'sheet', 'journal'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
targetId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
list: {
|
||||
data: [],
|
||||
loading: false,
|
||||
params: {
|
||||
page: 0,
|
||||
size: 10
|
||||
},
|
||||
total: 0
|
||||
},
|
||||
replyModalVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:visible', value)
|
||||
}
|
||||
},
|
||||
pagination() {
|
||||
return {
|
||||
page: this.list.params.page + 1,
|
||||
size: this.list.params.size,
|
||||
total: this.list.total
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modalVisible(value) {
|
||||
if (value) {
|
||||
this.handleGetComments()
|
||||
}
|
||||
},
|
||||
targetId() {
|
||||
this.handleGetComments()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleGetComments() {
|
||||
try {
|
||||
this.list.loading = true
|
||||
|
||||
const response = await apiClient.comment.listAsTreeView(`${this.target}s`, this.targetId, this.list.params)
|
||||
this.list.data = response.data.content
|
||||
this.list.total = response.data.total
|
||||
} catch (e) {
|
||||
this.$log.error('Failed to get target comments', e)
|
||||
} finally {
|
||||
this.list.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle page change
|
||||
*/
|
||||
handlePageChange(page = 1) {
|
||||
this.list.params.page = page - 1
|
||||
this.handleGetComments()
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle page size change
|
||||
*/
|
||||
handlePageSizeChange(current, size) {
|
||||
this.list.params.page = 0
|
||||
this.list.params.size = size
|
||||
this.handleGetComments()
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,146 +0,0 @@
|
|||
<template>
|
||||
<a-comment>
|
||||
<template #author>
|
||||
<a :href="comment.authorUrl" target="_blank">
|
||||
<a-icon v-if="comment.isAdmin" style="margin-right: 3px" type="user" />
|
||||
{{ comment.author }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template #avatar>
|
||||
<a-avatar :alt="comment.author" :src="comment.avatar" size="large" />
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<p v-html="$options.filters.markdownRender(comment.content)"></p>
|
||||
</template>
|
||||
|
||||
<template #datetime>
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<span>{{ comment.createTime | moment }}</span>
|
||||
</template>
|
||||
<span>{{ comment.createTime | timeAgo }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<a-dropdown v-if="comment.status === 'AUDITING'" :trigger="['click']">
|
||||
<span>通过</span>
|
||||
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="handleChangeStatus('PUBLISHED')"> 通过</a-menu-item>
|
||||
<a-menu-item key="2" @click="handlePublishAndReply"> 通过并回复</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<span v-else-if="comment.status === 'PUBLISHED'" @click="replyModalVisible = true">回复</span>
|
||||
|
||||
<a-popconfirm
|
||||
v-else-if="comment.status === 'RECYCLE'"
|
||||
:title="'你确定要还原该评论?'"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleChangeStatus('PUBLISHED')"
|
||||
>
|
||||
还原
|
||||
</a-popconfirm>
|
||||
|
||||
<a-popconfirm
|
||||
v-if="comment.status === 'PUBLISHED' || comment.status === 'AUDITING'"
|
||||
:title="'你确定要将该评论移到回收站?'"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@confirm="handleChangeStatus('RECYCLE')"
|
||||
>
|
||||
回收站
|
||||
</a-popconfirm>
|
||||
|
||||
<a-popconfirm :title="'你确定要永久删除该评论?'" cancelText="取消" okText="确定" @confirm="handleDelete">
|
||||
删除
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
|
||||
<template v-if="comment.children">
|
||||
<TargetCommentTreeNode
|
||||
v-for="(child, index) in comment.children"
|
||||
:key="index"
|
||||
:comment="child"
|
||||
:target="target"
|
||||
:target-id="targetId"
|
||||
@reload="$emit('reload')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CommentReplyModal
|
||||
:comment="comment"
|
||||
:target="target"
|
||||
:target-id="targetId"
|
||||
:visible.sync="replyModalVisible"
|
||||
@succeed="$emit('reload')"
|
||||
/>
|
||||
</a-comment>
|
||||
</template>
|
||||
<script>
|
||||
import apiClient from '@/utils/api-client'
|
||||
import CommentReplyModal from './CommentReplyModal'
|
||||
|
||||
export default {
|
||||
name: 'TargetCommentTreeNode',
|
||||
components: {
|
||||
CommentReplyModal
|
||||
},
|
||||
props: {
|
||||
target: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => {
|
||||
return ['post', 'sheet', 'journal'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
targetId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
comment: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
replyModalVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleChangeStatus(status) {
|
||||
try {
|
||||
await apiClient.comment.updateStatusById(`${this.target}s`, this.comment.id, status)
|
||||
} catch (e) {
|
||||
this.$log.error('Failed to change comment status', e)
|
||||
} finally {
|
||||
this.$emit('reload')
|
||||
}
|
||||
},
|
||||
|
||||
async handlePublishAndReply() {
|
||||
await this.handleChangeStatus('PUBLISHED')
|
||||
this.replyModalVisible = true
|
||||
},
|
||||
|
||||
async handleDelete() {
|
||||
try {
|
||||
await apiClient.comment.delete(`${this.target}s`, this.comment.id)
|
||||
} catch (e) {
|
||||
this.$log.error('Failed to delete comment', e)
|
||||
} finally {
|
||||
this.$emit('reload')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,85 +0,0 @@
|
|||
<template>
|
||||
<div class="h-full">
|
||||
<halo-editor
|
||||
ref="editor"
|
||||
v-model="originalContentData"
|
||||
:boxShadow="false"
|
||||
:toolbars="markdownEditorToolbars"
|
||||
:uploadRequest="handleAttachmentUpload"
|
||||
autofocus
|
||||
@change="handleChange"
|
||||
@openImagePicker="attachmentSelectVisible = true"
|
||||
@save="handleSave"
|
||||
/>
|
||||
|
||||
<AttachmentSelectModal :visible.sync="attachmentSelectVisible" @confirm="handleSelectAttachment" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import haloEditor from '@halo-dev/editor'
|
||||
import '@halo-dev/editor/dist/lib/style.css'
|
||||
import apiClient from '@/utils/api-client'
|
||||
import { markdownEditorToolbars } from '@/core/constant'
|
||||
|
||||
export default {
|
||||
name: 'MarkdownEditor',
|
||||
components: {
|
||||
haloEditor: haloEditor.editor
|
||||
},
|
||||
props: {
|
||||
originalContent: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
markdownEditorToolbars,
|
||||
attachmentSelectVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
originalContentData: {
|
||||
get() {
|
||||
return this.originalContent
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:originalContent', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAttachmentUpload(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hideLoading = this.$message.loading('上传中...', 0)
|
||||
apiClient.attachment
|
||||
.upload(file)
|
||||
.then(response => {
|
||||
const attachment = response.data
|
||||
resolve({
|
||||
name: attachment.name,
|
||||
path: encodeURI(attachment.path)
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.$log.error('upload image error: ', e)
|
||||
reject(e)
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleSelectAttachment({ markdown }) {
|
||||
this.$refs.editor.insetAtCursor(markdown.join('\n'))
|
||||
},
|
||||
handleSave() {
|
||||
this.$emit('save')
|
||||
},
|
||||
handleChange({ originalContent, renderContent }) {
|
||||
this.$emit('change', { originalContent, renderContent })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-input v-model="originalContent" :rows="16" type="textarea" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'RichTextEditor',
|
||||
props: {
|
||||
originalContent: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
originalContentData: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
originalContent(val) {
|
||||
this.originalContentData = val
|
||||
},
|
||||
originalContentData(val) {
|
||||
this.$emit('onContentChange', val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,75 +0,0 @@
|
|||
<script>
|
||||
import Tooltip from 'ant-design-vue/es/tooltip'
|
||||
|
||||
const getStrFullLength = (str = '') =>
|
||||
str.split('').reduce((pre, cur) => {
|
||||
const charCode = cur.charCodeAt(0)
|
||||
if (charCode >= 0 && charCode <= 128) {
|
||||
return pre + 1
|
||||
}
|
||||
return pre + 2
|
||||
}, 0)
|
||||
|
||||
const cutStrByFullLength = (str = '', maxLength) => {
|
||||
let showLength = 0
|
||||
return str.split('').reduce((pre, cur) => {
|
||||
const charCode = cur.charCodeAt(0)
|
||||
if (charCode >= 0 && charCode <= 128) {
|
||||
showLength += 1
|
||||
} else {
|
||||
showLength += 2
|
||||
}
|
||||
if (showLength <= maxLength) {
|
||||
return pre + cur
|
||||
}
|
||||
return pre
|
||||
}, '')
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Ellipsis',
|
||||
components: {
|
||||
Tooltip
|
||||
},
|
||||
props: {
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'ant-pro-ellipsis'
|
||||
},
|
||||
tooltip: {
|
||||
type: Boolean
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lines: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
fullWidthRecognition: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStrDom(str, fullLength) {
|
||||
return <span>{cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '')}</span>
|
||||
},
|
||||
getTooltip(fullStr, fullLength) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<template slot="title">{fullStr}</template>
|
||||
{this.getStrDom(fullStr, fullLength)}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { tooltip, length } = this.$props
|
||||
const str = this.$slots.default.map(vNode => vNode.text).join('')
|
||||
const fullLength = getStrFullLength(str)
|
||||
return tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,3 +0,0 @@
|
|||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export default Ellipsis
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<div :class="prefixCls">
|
||||
<div class="float-left">
|
||||
<slot name="extra">{{ extra }}</slot>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FooterToolBar',
|
||||
props: {
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'ant-pro-footer-toolbar'
|
||||
},
|
||||
extra: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,4 +0,0 @@
|
|||
import FooterToolBar from './FooterToolBar'
|
||||
import './index.less'
|
||||
|
||||
export default FooterToolBar
|
|
@ -1,23 +0,0 @@
|
|||
@import '../../styles/index.less';
|
||||
|
||||
@footer-toolbar-prefix-cls: ~'@{ant-pro-prefix}-footer-toolbar';
|
||||
|
||||
.@{footer-toolbar-prefix-cls} {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
|
||||
background: #fff;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 0 24px;
|
||||
z-index: 1000;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
<template>
|
||||
<div class="footer text-center" style="padding: 0 16px; margin: 48px 0 0">
|
||||
<div class="copyright" style="color: rgba(0, 0, 0, 0.45); font-size: 14px">
|
||||
Proudly power by
|
||||
<router-link :to="{ name: 'About' }"> Halo </router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GlobalFooter'
|
||||
}
|
||||
</script>
|
|
@ -1,3 +0,0 @@
|
|||
import GlobalFooter from './GlobalFooter'
|
||||
|
||||
export default GlobalFooter
|
|
@ -1,136 +0,0 @@
|
|||
<template>
|
||||
<transition name="showHeader">
|
||||
<div v-if="visible" class="header-animat">
|
||||
<a-layout-header
|
||||
v-if="visible"
|
||||
:class="[
|
||||
fixedHeader && 'ant-header-fixedHeader',
|
||||
sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed'
|
||||
]"
|
||||
style="padding: 0"
|
||||
>
|
||||
<div v-if="mode === 'sidemenu'" class="header">
|
||||
<a-icon
|
||||
v-if="device === 'mobile'"
|
||||
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
|
||||
class="trigger"
|
||||
@click="toggle"
|
||||
/>
|
||||
<a-icon v-else :type="collapsed ? 'menu-unfold' : 'menu-fold'" class="trigger" @click="toggle" />
|
||||
<user-menu></user-menu>
|
||||
</div>
|
||||
<div v-else :class="['top-nav-header-index', theme]">
|
||||
<div class="header-index-wide">
|
||||
<div class="header-index-left">
|
||||
<logo v-if="device !== 'mobile'" class="top-nav-header" />
|
||||
<s-menu v-if="device !== 'mobile'" :menu="menus" :theme="theme" mode="horizontal" />
|
||||
<a-icon v-else :type="collapsed ? 'menu-fold' : 'menu-unfold'" class="trigger" @click="toggle" />
|
||||
</div>
|
||||
<user-menu class="header-index-right"></user-menu>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserMenu from '../Tools/UserMenu'
|
||||
import SMenu from '../Menu/'
|
||||
import Logo from '../Tools/Logo'
|
||||
import { mixin } from '@/mixins/mixin'
|
||||
|
||||
export default {
|
||||
name: 'GlobalHeader',
|
||||
components: {
|
||||
UserMenu,
|
||||
SMenu,
|
||||
Logo
|
||||
},
|
||||
mixins: [mixin],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
// sidemenu, topmenu
|
||||
default: 'sidemenu'
|
||||
},
|
||||
menus: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'dark'
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
device: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'desktop'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: true,
|
||||
oldScrollTop: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('scroll', this.handleScroll, { passive: true })
|
||||
},
|
||||
methods: {
|
||||
handleScroll() {
|
||||
if (!this.autoHideHeader) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop
|
||||
if (!this.ticking) {
|
||||
this.ticking = true
|
||||
requestAnimationFrame(() => {
|
||||
if (this.oldScrollTop > scrollTop) {
|
||||
this.visible = true
|
||||
} else if (scrollTop > 300 && this.visible) {
|
||||
this.visible = false
|
||||
} else if (scrollTop < 300 && !this.visible) {
|
||||
this.visible = true
|
||||
}
|
||||
this.oldScrollTop = scrollTop
|
||||
this.ticking = false
|
||||
})
|
||||
}
|
||||
},
|
||||
toggle() {
|
||||
this.$emit('toggle')
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.removeEventListener('scroll', this.handleScroll, true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.header-animat {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.showHeader-enter-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.showHeader-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.showHeader-enter,
|
||||
.showHeader-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,3 +0,0 @@
|
|||
import GlobalHeader from './GlobalHeader'
|
||||
|
||||
export default GlobalHeader
|
|
@ -1,54 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-input :defaultValue="defaultValue" :placeholder="placeholder" :value="value" @change="onInputChange">
|
||||
<template #addonAfter>
|
||||
<a-button class="!p-0 !h-auto" type="link" @click="attachmentModalVisible = true">
|
||||
<a-icon type="picture" />
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
<AttachmentSelectModal
|
||||
:multiSelect="false"
|
||||
:visible.sync="attachmentModalVisible"
|
||||
@confirm="handleSelectAttachment"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'AttachmentInput',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
defaultValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '选择附件'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attachmentModalVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInputChange(e) {
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
handleSelectAttachment({ raw }) {
|
||||
if (raw.length) {
|
||||
this.$emit('input', encodeURI(raw[0].path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,123 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-form-model
|
||||
ref="loginForm"
|
||||
:model="form.model"
|
||||
:rules="form.rules"
|
||||
layout="vertical"
|
||||
@keyup.enter.native="form.needAuthCode ? handleLogin() : handleLoginClick()"
|
||||
>
|
||||
<a-form-model-item v-if="!form.needAuthCode" prop="username">
|
||||
<a-input v-model="form.model.username" placeholder="用户名/邮箱">
|
||||
<a-icon slot="prefix" style="color: rgba(0, 0, 0, 0.25)" type="user" />
|
||||
</a-input>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item v-if="!form.needAuthCode" prop="password">
|
||||
<a-input v-model="form.model.password" placeholder="密码" type="password">
|
||||
<a-icon slot="prefix" style="color: rgba(0, 0, 0, 0.25)" type="lock" />
|
||||
</a-input>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item v-if="form.needAuthCode" prop="authcode">
|
||||
<a-input v-model="form.model.authcode" :maxLength="6" placeholder="两步验证码">
|
||||
<a-icon slot="prefix" style="color: rgba(0, 0, 0, 0.25)" type="safety-certificate" />
|
||||
</a-input>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item>
|
||||
<a-button
|
||||
:block="true"
|
||||
:loading="form.logging"
|
||||
type="primary"
|
||||
@click="form.needAuthCode ? handleLogin() : handleLoginClick()"
|
||||
>
|
||||
{{ buttonName }}
|
||||
</a-button>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'LoginForm',
|
||||
data() {
|
||||
const mfaValidate = (rule, value, callback) => {
|
||||
if (!value && this.form.needAuthCode) {
|
||||
callback(new Error('* 请输入两步验证码'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
form: {
|
||||
model: {
|
||||
authcode: null,
|
||||
password: null,
|
||||
username: null
|
||||
},
|
||||
rules: {
|
||||
username: [{ required: true, message: '* 用户名/邮箱不能为空', trigger: ['change'] }],
|
||||
password: [{ required: true, message: '* 密码不能为空', trigger: ['change'] }],
|
||||
authcode: [{ validator: mfaValidate, trigger: ['change'] }]
|
||||
},
|
||||
needAuthCode: false,
|
||||
logging: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
buttonName() {
|
||||
return this.form.needAuthCode ? '验证' : '登录'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login', 'refreshUserCache', 'refreshOptionsCache']),
|
||||
handleLoginClick() {
|
||||
const _this = this
|
||||
_this.$refs.loginForm.validate(valid => {
|
||||
if (valid) {
|
||||
_this.form.logging = true
|
||||
apiClient
|
||||
.needMFACode({
|
||||
username: _this.form.model.username,
|
||||
password: _this.form.model.password
|
||||
})
|
||||
.then(response => {
|
||||
const data = response.data
|
||||
if (data && data.needMFACode) {
|
||||
_this.form.needAuthCode = true
|
||||
_this.form.model.authcode = null
|
||||
} else {
|
||||
_this.handleLogin()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
_this.form.logging = false
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleLogin() {
|
||||
const _this = this
|
||||
_this.$refs.loginForm.validate(valid => {
|
||||
if (valid) {
|
||||
_this.form.logging = true
|
||||
_this
|
||||
.login(_this.form.model)
|
||||
.then(() => {
|
||||
_this.$emit('success')
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
_this.form.logging = false
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,38 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-modal
|
||||
v-model="loginModal"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
:width="320"
|
||||
title="重新登录"
|
||||
@cancel="handleCancelLogin"
|
||||
>
|
||||
<LoginForm @success="onLoginSucceed" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import LoginForm from './LoginForm'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'LoginModal',
|
||||
components: {
|
||||
LoginForm
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['loginModal'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['ToggleLoginModal']),
|
||||
onLoginSucceed() {
|
||||
this.$emit('success')
|
||||
},
|
||||
handleCancelLogin() {
|
||||
this.ToggleLoginModal(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
|
@ -1,62 +0,0 @@
|
|||
<template>
|
||||
<a-layout-sider
|
||||
v-model="collapsed"
|
||||
:class="['sider', isDesktop() ? null : 'shadow', theme, fixedSidebar ? 'ant-fixed-sidemenu' : null]"
|
||||
:collapsible="collapsible"
|
||||
:trigger="null"
|
||||
width="256px"
|
||||
>
|
||||
<logo />
|
||||
<s-menu
|
||||
:collapsed="collapsed"
|
||||
:menu="menus"
|
||||
:mode="mode"
|
||||
:theme="theme"
|
||||
style="padding: 16px 0px"
|
||||
@select="onSelect"
|
||||
></s-menu>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logo from '@/components/Tools/Logo'
|
||||
import SMenu from './index'
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin'
|
||||
|
||||
export default {
|
||||
name: 'SideMenu',
|
||||
components: { Logo, SMenu },
|
||||
mixins: [mixin, mixinDevice],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'inline'
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'dark'
|
||||
},
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
menus: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelect(obj) {
|
||||
this.$emit('menuSelect', obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,3 +0,0 @@
|
|||
import SMenu from './menu'
|
||||
|
||||
export default SMenu
|
|
@ -1,165 +0,0 @@
|
|||
import Menu from 'ant-design-vue/es/menu'
|
||||
import Icon from 'ant-design-vue/es/icon'
|
||||
|
||||
export default {
|
||||
name: 'SMenu',
|
||||
props: {
|
||||
menu: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'dark'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'inline'
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openKeys: [],
|
||||
selectedKeys: [],
|
||||
cachedOpenKeys: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rootSubmenuKeys: vm => {
|
||||
const keys = []
|
||||
vm.menu.forEach(item => keys.push(item.path))
|
||||
return keys
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateMenu()
|
||||
},
|
||||
watch: {
|
||||
collapsed(val) {
|
||||
if (val) {
|
||||
this.cachedOpenKeys = this.openKeys.concat()
|
||||
this.openKeys = []
|
||||
} else {
|
||||
this.openKeys = this.cachedOpenKeys
|
||||
}
|
||||
},
|
||||
$route: function () {
|
||||
this.updateMenu()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// select menu item
|
||||
onOpenChange(openKeys) {
|
||||
// 在水平模式下时执行,并且不再执行后续
|
||||
if (this.mode === 'horizontal') {
|
||||
this.openKeys = openKeys
|
||||
return
|
||||
}
|
||||
// 非水平模式时
|
||||
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
|
||||
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
|
||||
this.openKeys = openKeys
|
||||
} else {
|
||||
this.openKeys = latestOpenKey ? [latestOpenKey] : []
|
||||
}
|
||||
},
|
||||
onSelect({ item, key, selectedKeys }) {
|
||||
this.selectedKeys = selectedKeys
|
||||
this.$emit('select', { item, key, selectedKeys })
|
||||
},
|
||||
updateMenu() {
|
||||
const routes = this.$route.matched.concat()
|
||||
|
||||
if (routes.length >= 4 && this.$route.meta.hidden) {
|
||||
routes.pop()
|
||||
this.selectedKeys = [routes[2].path]
|
||||
} else {
|
||||
this.selectedKeys = [routes.pop().path]
|
||||
}
|
||||
|
||||
const openKeys = []
|
||||
if (this.mode === 'inline') {
|
||||
routes.forEach(item => {
|
||||
openKeys.push(item.path)
|
||||
})
|
||||
}
|
||||
|
||||
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
|
||||
},
|
||||
|
||||
// render
|
||||
renderItem(menu) {
|
||||
if (!menu.hidden) {
|
||||
return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
|
||||
}
|
||||
return null
|
||||
},
|
||||
renderMenuItem(menu) {
|
||||
const target = menu.meta.target || null
|
||||
const CustomTag = (target && 'a') || 'router-link'
|
||||
const props = { to: { name: menu.name } }
|
||||
const attrs = { href: menu.path, target: menu.meta.target }
|
||||
return (
|
||||
<Menu.Item {...{ key: menu.path }}>
|
||||
<CustomTag {...{ props, attrs }}>
|
||||
{this.renderIcon(menu.meta.icon)}
|
||||
<span>{menu.meta.title}</span>
|
||||
</CustomTag>
|
||||
</Menu.Item>
|
||||
)
|
||||
},
|
||||
renderSubMenu(menu) {
|
||||
const itemArr = []
|
||||
if (!menu.hideChildrenInMenu) {
|
||||
menu.children.forEach(item => itemArr.push(this.renderItem(item)))
|
||||
}
|
||||
return (
|
||||
<Menu.SubMenu {...{ key: menu.path }}>
|
||||
<span slot="title">
|
||||
{this.renderIcon(menu.meta.icon)}
|
||||
<span>{menu.meta.title}</span>
|
||||
</span>
|
||||
{itemArr}
|
||||
</Menu.SubMenu>
|
||||
)
|
||||
},
|
||||
renderIcon(icon) {
|
||||
if (icon === 'none' || icon === undefined) {
|
||||
return null
|
||||
}
|
||||
const props = {}
|
||||
typeof icon === 'object' ? (props.component = icon) : (props.type = icon)
|
||||
return <Icon {...{ props }} />
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const dynamicProps = {
|
||||
props: {
|
||||
mode: this.mode,
|
||||
theme: this.theme,
|
||||
openKeys: this.openKeys,
|
||||
selectedKeys: this.selectedKeys
|
||||
},
|
||||
on: {
|
||||
openChange: this.onOpenChange,
|
||||
select: this.onSelect
|
||||
}
|
||||
}
|
||||
|
||||
const menuTree = this.menu.map(item => {
|
||||
if (item.hidden) {
|
||||
return null
|
||||
}
|
||||
return this.renderItem(item)
|
||||
})
|
||||
|
||||
return <Menu {...dynamicProps}>{menuTree}</Menu>
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
import Menu from 'ant-design-vue/es/menu'
|
||||
import Icon from 'ant-design-vue/es/icon'
|
||||
|
||||
const { Item, SubMenu } = Menu
|
||||
|
||||
export default {
|
||||
name: 'SMenu',
|
||||
props: {
|
||||
menu: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'dark'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'inline'
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openKeys: [],
|
||||
selectedKeys: [],
|
||||
cachedOpenKeys: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rootSubmenuKeys: vm => {
|
||||
const keys = []
|
||||
vm.menu.forEach(item => keys.push(item.path))
|
||||
return keys
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateMenu()
|
||||
},
|
||||
watch: {
|
||||
collapsed(val) {
|
||||
if (val) {
|
||||
this.cachedOpenKeys = this.openKeys.concat()
|
||||
this.openKeys = []
|
||||
} else {
|
||||
this.openKeys = this.cachedOpenKeys
|
||||
}
|
||||
},
|
||||
$route: function () {
|
||||
this.updateMenu()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderIcon: function (h, icon) {
|
||||
if (icon === 'none' || icon === undefined) {
|
||||
return null
|
||||
}
|
||||
const props = {}
|
||||
typeof icon === 'object' ? (props.component = icon) : (props.type = icon)
|
||||
return h(Icon, { props: { ...props } })
|
||||
},
|
||||
renderMenuItem: function (h, menu, pIndex, index) {
|
||||
const target = menu.meta.target || null
|
||||
return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
|
||||
h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
|
||||
this.renderIcon(h, menu.meta.icon),
|
||||
h('span', [menu.meta.title])
|
||||
])
|
||||
])
|
||||
},
|
||||
renderSubMenu: function (h, menu, pIndex, index) {
|
||||
const this2_ = this
|
||||
const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
|
||||
const itemArr = []
|
||||
const pIndex_ = pIndex + '_' + index
|
||||
this.$log.debug('menu', menu)
|
||||
if (!menu.hideChildrenInMenu) {
|
||||
menu.children.forEach(function (item, i) {
|
||||
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
|
||||
})
|
||||
}
|
||||
return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
|
||||
},
|
||||
renderItem: function (h, menu, pIndex, index) {
|
||||
if (!menu.hidden) {
|
||||
return menu.children && !menu.hideChildrenInMenu
|
||||
? this.renderSubMenu(h, menu, pIndex, index)
|
||||
: this.renderMenuItem(h, menu, pIndex, index)
|
||||
}
|
||||
},
|
||||
renderMenu: function (h, menuTree) {
|
||||
const this2_ = this
|
||||
const menuArr = []
|
||||
menuTree.forEach(function (menu, i) {
|
||||
if (!menu.hidden) {
|
||||
menuArr.push(this2_.renderItem(h, menu, '0', i))
|
||||
}
|
||||
})
|
||||
return menuArr
|
||||
},
|
||||
onOpenChange(openKeys) {
|
||||
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
|
||||
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
|
||||
this.openKeys = openKeys
|
||||
} else {
|
||||
this.openKeys = latestOpenKey ? [latestOpenKey] : []
|
||||
}
|
||||
},
|
||||
updateMenu() {
|
||||
const routes = this.$route.matched.concat()
|
||||
|
||||
if (routes.length >= 4 && this.$route.meta.hidden) {
|
||||
routes.pop()
|
||||
this.selectedKeys = [routes[2].path]
|
||||
} else {
|
||||
this.selectedKeys = [routes.pop().path]
|
||||
}
|
||||
|
||||
const openKeys = []
|
||||
if (this.mode === 'inline') {
|
||||
routes.forEach(item => {
|
||||
openKeys.push(item.path)
|
||||
})
|
||||
}
|
||||
|
||||
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
return h(
|
||||
Menu,
|
||||
{
|
||||
props: {
|
||||
theme: this.$props.theme,
|
||||
mode: this.$props.mode,
|
||||
openKeys: this.openKeys,
|
||||
selectedKeys: this.selectedKeys
|
||||
},
|
||||
on: {
|
||||
openChange: this.onOpenChange,
|
||||
select: obj => {
|
||||
this.selectedKeys = obj.selectedKeys
|
||||
this.$emit('select', obj)
|
||||
}
|
||||
}
|
||||
},
|
||||
this.renderMenu(h, this.menu)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-form>
|
||||
<a-form-item v-for="(meta, index) in presetMetas" :key="index">
|
||||
<a-row :gutter="5">
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.key" :disabled="true">
|
||||
<template #addonBefore>
|
||||
<i>K</i>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.value">
|
||||
<template #addonBefore>
|
||||
<i>V</i>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form>
|
||||
<a-form-item v-for="(meta, index) in customMetas" :key="index">
|
||||
<a-row :gutter="5">
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.key">
|
||||
<template #addonBefore>
|
||||
<i>K</i>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-input v-model="meta.value">
|
||||
<template #addonBefore>
|
||||
<i>V</i>
|
||||
</template>
|
||||
<template #addonAfter>
|
||||
<a-button class="!p-0 !h-auto" type="link" @click.prevent="handleRemove(index)">
|
||||
<a-icon type="close" />
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="dashed" @click="handleAdd">新增</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'MetaEditor',
|
||||
props: {
|
||||
target: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
validator: function (value) {
|
||||
return ['post', 'sheet'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
targetId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
metas: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
presetFields: [],
|
||||
presetMetas: [],
|
||||
customMetas: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
presetMetas: {
|
||||
handler() {
|
||||
this.handleChange()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
customMetas: {
|
||||
handler() {
|
||||
this.handleChange()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
targetId() {
|
||||
this.handleGenerateMetas()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleListPresetMetasField()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Fetch preset metas fields
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleListPresetMetasField() {
|
||||
try {
|
||||
const response = await apiClient.theme.getActivatedTheme()
|
||||
this.presetFields = response.data[`${this.target}MetaField`] || []
|
||||
|
||||
this.handleGenerateMetas()
|
||||
} catch (e) {
|
||||
this.$log.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate preset and custom metas
|
||||
*/
|
||||
handleGenerateMetas() {
|
||||
this.presetMetas = this.presetFields.map(field => {
|
||||
const meta = this.metas.find(meta => meta.key === field)
|
||||
return meta ? { key: field, value: meta.value } : { key: field, value: '' }
|
||||
})
|
||||
|
||||
this.customMetas = this.metas
|
||||
.filter(meta => this.presetFields.indexOf(meta.key) === -1)
|
||||
.map(meta => {
|
||||
return {
|
||||
key: meta.key,
|
||||
value: meta.value
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new custom meta
|
||||
*/
|
||||
handleAdd() {
|
||||
this.customMetas.push({
|
||||
key: '',
|
||||
value: ''
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove custom meta
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
handleRemove(index) {
|
||||
this.customMetas.splice(index, 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle change
|
||||
*/
|
||||
handleChange() {
|
||||
this.$emit('update:metas', this.presetMetas.concat(this.customMetas))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<a-tag :color="tag.color" :style="{ color: labelColor }">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<script>
|
||||
import { isHex, isLight } from '@/utils/colorUtil'
|
||||
|
||||
export default {
|
||||
name: 'PostTag',
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelColor() {
|
||||
const { color } = this.tag || {}
|
||||
if (!color) return 'inherit'
|
||||
if (!isHex(color)) {
|
||||
return 'inherit'
|
||||
}
|
||||
return !isLight(color) ? '#fff' : 'inherit'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,283 +0,0 @@
|
|||
<template>
|
||||
<div ref="settingDrawer" class="setting-drawer">
|
||||
<a-drawer :visible="layoutSetting" closable width="300" @close="onClose">
|
||||
<div class="setting-drawer-index-content">
|
||||
<div class="mb-6">
|
||||
<h3 class="setting-drawer-index-title">整体风格设置</h3>
|
||||
<div class="setting-drawer-index-blockChecbox">
|
||||
<a-tooltip>
|
||||
<template #title>暗色菜单风格</template>
|
||||
<div class="setting-drawer-index-item" @click="handleSetMenuTheme('dark')">
|
||||
<img alt="dark" src="/images/dark.svg" />
|
||||
<div v-if="navTheme === 'dark'" class="setting-drawer-index-selectIcon">
|
||||
<a-icon type="check" />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>亮色菜单风格</template>
|
||||
<div class="setting-drawer-index-item" @click="handleSetMenuTheme('light')">
|
||||
<img alt="light" src="/images/dark.svg" />
|
||||
<div v-if="navTheme !== 'dark'" class="setting-drawer-index-selectIcon">
|
||||
<a-icon type="check" />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="mb-6">
|
||||
<h3 class="setting-drawer-index-title">主题色</h3>
|
||||
<div class="h-5">
|
||||
<a-tooltip v-for="(item, index) in colorList" :key="index" class="setting-drawer-theme-color-colorBlock">
|
||||
<template #title>{{ item.key }}</template>
|
||||
<a-tag :color="item.color" @click="handleChangeColor(item.color)">
|
||||
<a-icon v-if="item.color === primaryColor" type="check"></a-icon>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="mb-6">
|
||||
<h3 class="setting-drawer-index-title">导航模式</h3>
|
||||
|
||||
<div class="setting-drawer-index-blockChecbox">
|
||||
<div class="setting-drawer-index-item" @click="handleSetLayout('sidemenu')">
|
||||
<img alt="sidemenu" src="/images/sidemenu.svg" />
|
||||
<div v-if="layoutMode === 'sidemenu'" class="setting-drawer-index-selectIcon">
|
||||
<a-icon type="check" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-drawer-index-item" @click="handleSetLayout('topmenu')">
|
||||
<img alt="topmenu" src="/images/topmenu.svg" />
|
||||
<div v-if="layoutMode !== 'sidemenu'" class="setting-drawer-index-selectIcon">
|
||||
<a-icon type="check" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="mt-6">
|
||||
<a-list :split="false">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-tooltip>
|
||||
<template #title> 该设定仅 [顶部栏导航] 时有效</template>
|
||||
<a-select
|
||||
:disabled="layoutMode !== 'topmenu'"
|
||||
:value="contentWidth"
|
||||
size="small"
|
||||
style="width: 80px"
|
||||
@change="handleContentWidthChange"
|
||||
>
|
||||
<a-select-option value="Fixed">固定</a-select-option>
|
||||
<a-select-option v-if="layoutMode !== 'sidemenu'" value="Fluid">流式</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div>内容区域宽度</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-switch :checked="fixedHeader" size="small" @change="handleSetFixedHeader" />
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div>固定 Header</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-switch
|
||||
:checked="autoHideHeader"
|
||||
:disabled="!fixedHeader"
|
||||
size="small"
|
||||
@change="handleSetAutoHideHeader"
|
||||
/>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<a-tooltip placement="left">
|
||||
<template #title>固定 Header 时可配置</template>
|
||||
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-switch
|
||||
:checked="fixedSidebar"
|
||||
:disabled="layoutMode === 'topmenu'"
|
||||
size="small"
|
||||
@change="handleSetFixedSidebar"
|
||||
/>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div :style="{ opacity: layoutMode === 'topmenu' ? '0.5' : '1' }">固定侧边菜单</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
<a-divider />
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/config/defaultSettings'
|
||||
import { colorList, updateTheme } from './setting'
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
mixins: [mixin, mixinDevice],
|
||||
data() {
|
||||
return {
|
||||
colorList,
|
||||
baseConfig: Object.assign({}, config)
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
// 当主题色不是默认色时,才进行主题编译
|
||||
if (this.primaryColor !== config.primaryColor) {
|
||||
updateTheme(this.primaryColor)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['layoutSetting'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSidebar', 'ToggleLayoutSetting']),
|
||||
onClose() {
|
||||
this.ToggleLayoutSetting(false)
|
||||
},
|
||||
handleSetMenuTheme(theme) {
|
||||
this.baseConfig.navTheme = theme
|
||||
this.$store.dispatch('ToggleTheme', theme)
|
||||
},
|
||||
handleSetLayout(mode) {
|
||||
this.baseConfig.layout = mode
|
||||
this.$store.dispatch('ToggleLayoutMode', mode)
|
||||
if (mode === 'sidemenu') {
|
||||
this.handleContentWidthChange('Fixed')
|
||||
this.handleSetFixedSidebar(true)
|
||||
} else {
|
||||
this.handleSetFixedHeader(true)
|
||||
this.handleSetFixedSidebar(false)
|
||||
}
|
||||
},
|
||||
handleContentWidthChange(type) {
|
||||
this.baseConfig.contentWidth = type
|
||||
this.$store.dispatch('ToggleContentWidth', type)
|
||||
},
|
||||
handleChangeColor(color) {
|
||||
this.baseConfig.primaryColor = color
|
||||
if (this.primaryColor !== color) {
|
||||
this.$store.dispatch('ToggleColor', color)
|
||||
updateTheme(color)
|
||||
}
|
||||
},
|
||||
handleSetFixedHeader(fixed) {
|
||||
this.baseConfig.fixedHeader = fixed
|
||||
this.$store.dispatch('ToggleFixedHeader', fixed)
|
||||
|
||||
if (!fixed) {
|
||||
this.handleSetAutoHideHeader(false)
|
||||
}
|
||||
},
|
||||
handleSetAutoHideHeader(autoHidden) {
|
||||
this.baseConfig.autoHideHeader = autoHidden
|
||||
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
|
||||
},
|
||||
handleSetFixedSidebar(fixed) {
|
||||
this.baseConfig.fixedSidebar = fixed
|
||||
this.$store.dispatch('ToggleFixedSidebar', fixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-drawer-index-content {
|
||||
.setting-drawer-index-blockChecbox {
|
||||
display: flex;
|
||||
|
||||
.setting-drawer-index-item {
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.setting-drawer-index-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
height: 100%;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-drawer-theme-color-colorBlock {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-drawer-index-handle {
|
||||
position: absolute;
|
||||
top: 240px;
|
||||
background: #1890ff;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
right: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
z-index: 1001;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
i {
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||
<template>
|
||||
<div class="setting-drawer-index-item">
|
||||
<h3 class="setting-drawer-index-title">{{ title }}</h3>
|
||||
<slot></slot>
|
||||
<a-divider v-if="divider" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingItem',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-drawer-index-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.setting-drawer-index-title {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,3 +0,0 @@
|
|||
import SettingDrawer from './SettingDrawer'
|
||||
|
||||
export default SettingDrawer
|
|
@ -1,100 +0,0 @@
|
|||
import { message } from 'ant-design-vue/es'
|
||||
// import defaultSettings from '../defaultSettings';
|
||||
|
||||
let lessNodesAppended
|
||||
|
||||
const colorList = [
|
||||
{
|
||||
key: '红色',
|
||||
color: '#F5222D'
|
||||
},
|
||||
{
|
||||
key: '浅红色',
|
||||
color: '#FA541C'
|
||||
},
|
||||
{
|
||||
key: '日暮',
|
||||
color: '#FAAD14'
|
||||
},
|
||||
{
|
||||
key: '青色',
|
||||
color: '#13C2C2'
|
||||
},
|
||||
{
|
||||
key: '绿色',
|
||||
color: '#52C41A'
|
||||
},
|
||||
{
|
||||
key: '默认',
|
||||
color: '#1890FF'
|
||||
},
|
||||
{
|
||||
key: '蓝色',
|
||||
color: '#2F54EB'
|
||||
},
|
||||
{
|
||||
key: '紫色',
|
||||
color: '#722ED1'
|
||||
}
|
||||
]
|
||||
|
||||
const updateTheme = primaryColor => {
|
||||
// Don't compile less in production!
|
||||
/* if (process.env.NODE_ENV === 'production') {
|
||||
return;
|
||||
} */
|
||||
// Determine if the component is remounted
|
||||
if (!primaryColor) {
|
||||
return
|
||||
}
|
||||
const hideMessage = message.loading('正在编译主题!', 0)
|
||||
|
||||
function buildIt() {
|
||||
if (!window.less) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.less
|
||||
.modifyVars({
|
||||
'@primary-color': primaryColor
|
||||
})
|
||||
.then(() => {
|
||||
hideMessage()
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('Failed to update theme')
|
||||
hideMessage()
|
||||
})
|
||||
}, 200)
|
||||
}
|
||||
|
||||
if (!lessNodesAppended) {
|
||||
// insert less.js and color.less
|
||||
const lessStyleNode = document.createElement('link')
|
||||
const lessConfigNode = document.createElement('script')
|
||||
const lessScriptNode = document.createElement('script')
|
||||
lessStyleNode.setAttribute('rel', 'stylesheet/less')
|
||||
lessStyleNode.setAttribute('href', '/color.less')
|
||||
lessConfigNode.innerHTML = `
|
||||
window.less = {
|
||||
async: true,
|
||||
env: 'production',
|
||||
javascriptEnabled: true
|
||||
};
|
||||
`
|
||||
lessScriptNode.src = 'https://unpkg.com/less@3.8.1/dist/less.min.js'
|
||||
lessScriptNode.async = true
|
||||
lessScriptNode.onload = () => {
|
||||
buildIt()
|
||||
lessScriptNode.onload = null
|
||||
}
|
||||
document.body.appendChild(lessStyleNode)
|
||||
document.body.appendChild(lessConfigNode)
|
||||
document.body.appendChild(lessScriptNode)
|
||||
lessNodesAppended = true
|
||||
} else {
|
||||
buildIt()
|
||||
}
|
||||
}
|
||||
|
||||
export { updateTheme, colorList }
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<div :class="center && 'center'" class="head-info">
|
||||
<span>{{ title }}</span>
|
||||
<p>{{ content }}</p>
|
||||
<em v-if="bordered" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HeadInfo',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.head-info {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
padding: 0 32px 0 0;
|
||||
min-width: 125px;
|
||||
|
||||
&.center {
|
||||
text-align: center;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
em {
|
||||
background-color: #e8e8e8;
|
||||
position: absolute;
|
||||
height: 56px;
|
||||
width: 1px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,93 +0,0 @@
|
|||
<template>
|
||||
<a-popover
|
||||
:arrowPointAtCenter="true"
|
||||
:autoAdjustOverflow="true"
|
||||
:overlayStyle="{ width: '400px', top: '50px' }"
|
||||
overlayClassName="header-comment-popover"
|
||||
placement="bottomRight"
|
||||
title="待审核评论"
|
||||
trigger="click"
|
||||
>
|
||||
<template #content>
|
||||
<div class="custom-tab-wrapper">
|
||||
<a-tabs v-model="activeKey" :animated="{ inkBar: true, tabPane: false }" @change="handleListAuditingComments">
|
||||
<a-tab-pane v-for="target in targets" :key="target.key" :tab="target.label">
|
||||
<CommentListView :comments="comments[target.dataKey]" :loading="comments.loading" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<span class="inline-block transition-all">
|
||||
<a-badge v-if="comments.post.length || comments.sheet.length || comments.journal.length" dot>
|
||||
<a-icon type="bell" />
|
||||
</a-badge>
|
||||
<a-badge v-else>
|
||||
<a-icon type="bell" />
|
||||
</a-badge>
|
||||
</span>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
const targets = [
|
||||
{
|
||||
key: 'posts',
|
||||
dataKey: 'post',
|
||||
label: '文章'
|
||||
},
|
||||
{
|
||||
key: 'sheets',
|
||||
dataKey: 'sheet',
|
||||
label: '页面'
|
||||
},
|
||||
{
|
||||
key: 'journals',
|
||||
dataKey: 'journal',
|
||||
label: '日志'
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'HeaderComment',
|
||||
data() {
|
||||
return {
|
||||
targets: targets,
|
||||
activeKey: 'posts',
|
||||
comments: {
|
||||
post: [],
|
||||
sheet: [],
|
||||
journal: [],
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleListAuditingComments()
|
||||
},
|
||||
methods: {
|
||||
async handleListAuditingComments() {
|
||||
try {
|
||||
this.comments.loading = true
|
||||
|
||||
const params = { status: 'AUDITING', size: 20 }
|
||||
|
||||
const responses = await Promise.all(
|
||||
targets.map(target => {
|
||||
return apiClient.comment.list(target.key, params)
|
||||
})
|
||||
)
|
||||
|
||||
this.comments.post = responses[0].data.content
|
||||
this.comments.sheet = responses[1].data.content
|
||||
this.comments.journal = responses[2].data.content
|
||||
} catch (e) {
|
||||
this.$log.error('Failed to get auditing comments', e)
|
||||
} finally {
|
||||
this.comments.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,7 +0,0 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
|
@ -1,54 +0,0 @@
|
|||
<template>
|
||||
<div class="logo">
|
||||
<img
|
||||
alt="Halo Logo"
|
||||
class="select-none cursor-pointer hover:brightness-125 transition-all"
|
||||
src="/images/logo.svg"
|
||||
@click="onLogoClick()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
export default {
|
||||
name: 'Logo',
|
||||
data() {
|
||||
return {
|
||||
clickCount: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['options'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['refreshOptionsCache']),
|
||||
async onLogoClick() {
|
||||
this.clickCount++
|
||||
if (this.clickCount === 10) {
|
||||
try {
|
||||
await apiClient.option.saveMapView({ developer_mode: true })
|
||||
|
||||
await this.refreshOptionsCache()
|
||||
this.$message.success(`开发者选项已启用!`)
|
||||
this.clickCount = 0
|
||||
this.$router.push({ name: 'ToolList' }).catch(() => {})
|
||||
} catch (e) {
|
||||
this.$log.error(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (this.clickCount >= 5) {
|
||||
if (this.options.developer_mode) {
|
||||
this.$message.info(`当前已启用开发者选项!`)
|
||||
this.clickCount = 0
|
||||
} else {
|
||||
this.$message.info(`再点击 ${10 - this.clickCount} 次即可启用开发者选项!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,79 +0,0 @@
|
|||
<template>
|
||||
<div class="user-wrapper">
|
||||
<a :href="options.blog_url" target="_blank">
|
||||
<a-tooltip placement="bottom" title="点击跳转到首页">
|
||||
<span class="action">
|
||||
<a-icon type="link" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</a>
|
||||
<a href="javascript:void(0)" @click="handleShowLayoutSetting">
|
||||
<a-tooltip placement="bottom" title="后台布局设置">
|
||||
<span class="action">
|
||||
<a-icon type="setting" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</a>
|
||||
<header-comment class="action" />
|
||||
<a-dropdown>
|
||||
<span v-if="user" class="action ant-dropdown-link user-dropdown-menu">
|
||||
<a-avatar :src="user.avatar || '//cn.gravatar.com/avatar/?s=256&d=mm'" class="avatar" size="small" />
|
||||
</span>
|
||||
<a-menu slot="overlay" class="user-dropdown-menu-wrapper">
|
||||
<a-menu-item key="0">
|
||||
<router-link :to="{ name: 'Profile' }">
|
||||
<a-icon type="user" />
|
||||
<span>个人资料</span>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="1">
|
||||
<a href="javascript:void(0);" @click="handleLogout">
|
||||
<a-icon type="logout" />
|
||||
<span>退出登录</span>
|
||||
</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderComment from './HeaderComment'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'UserMenu',
|
||||
components: {
|
||||
HeaderComment
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user', 'options'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['logout', 'ToggleLayoutSetting']),
|
||||
handleLogout() {
|
||||
const _this = this
|
||||
|
||||
this.$confirm({
|
||||
title: '提示',
|
||||
content: '确定要注销登录吗 ?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await _this.logout()
|
||||
window.location.reload()
|
||||
} catch (e) {
|
||||
_this.$message.error({
|
||||
title: '错误',
|
||||
description: e.message
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleShowLayoutSetting() {
|
||||
this.ToggleLayoutSetting(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,150 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<file-pond
|
||||
ref="pond"
|
||||
:accepted-file-types="accepts"
|
||||
:allow-multiple="multiple"
|
||||
:allowImagePreview="allowImagePreview"
|
||||
:allowRevert="false"
|
||||
:files="fileList"
|
||||
:label-idle="label"
|
||||
:maxFiles="maxFiles"
|
||||
:maxParallelUploads="maxParallelUploads"
|
||||
:name="name"
|
||||
:server="server"
|
||||
fileValidateTypeLabelExpectedTypes="请选择 {lastType} 格式的文件"
|
||||
labelFileProcessing="上传中"
|
||||
labelFileProcessingAborted="取消上传"
|
||||
labelFileProcessingComplete="上传完成"
|
||||
labelFileProcessingError="上传错误"
|
||||
labelFileTypeNotAllowed="不支持当前文件格式"
|
||||
labelTapToCancel="点击取消"
|
||||
labelTapToRetry="点击重试"
|
||||
@init="handleFilePondInit"
|
||||
>
|
||||
</file-pond>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { Axios } from '@halo-dev/admin-api'
|
||||
|
||||
import vueFilePond from 'vue-filepond'
|
||||
import 'filepond/dist/filepond.min.css'
|
||||
|
||||
// Plugins
|
||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
|
||||
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css'
|
||||
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
|
||||
|
||||
// Create component and register plugins
|
||||
const FilePond = vueFilePond(FilePondPluginImagePreview, FilePondPluginFileValidateType)
|
||||
export default {
|
||||
name: 'FilePondUpload',
|
||||
components: {
|
||||
FilePond
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'file'
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
accepts: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return null
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '点击选择文件或将文件拖拽到此处'
|
||||
},
|
||||
uploadHandler: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['options']),
|
||||
maxParallelUploads() {
|
||||
if (this.options) {
|
||||
return this.options.attachment_upload_max_parallel_uploads
|
||||
}
|
||||
return 1
|
||||
},
|
||||
allowImagePreview() {
|
||||
if (this.options) {
|
||||
return this.options.attachment_upload_image_preview_enable
|
||||
}
|
||||
return false
|
||||
},
|
||||
maxFiles() {
|
||||
if (this.options) {
|
||||
return this.options.attachment_upload_max_files
|
||||
}
|
||||
return 1
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
server: {
|
||||
process: (fieldName, file, metadata, load, error, progress, abort) => {
|
||||
const CancelToken = Axios.CancelToken
|
||||
const source = CancelToken.source()
|
||||
this.uploadHandler(
|
||||
file,
|
||||
{
|
||||
onUploadProgress: progressEvent => {
|
||||
if (progressEvent.total > 0) {
|
||||
progress(progressEvent.lengthComputable, progressEvent.loaded, progressEvent.total)
|
||||
}
|
||||
},
|
||||
cancelToken: source.token
|
||||
},
|
||||
this.field
|
||||
)
|
||||
.then(response => {
|
||||
load(response)
|
||||
this.$log.debug('Uploaded successfully', response)
|
||||
this.$emit('success', response, file)
|
||||
})
|
||||
.catch(failure => {
|
||||
this.$log.debug('Failed to upload file', failure)
|
||||
this.$emit('failure', failure, file)
|
||||
error()
|
||||
})
|
||||
return {
|
||||
abort: () => {
|
||||
abort()
|
||||
this.$log.debug('Upload operation aborted by the user')
|
||||
source.cancel('Upload operation canceled by the user.')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
fileList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFilePondInit() {
|
||||
this.$log.debug('FilePond has initialized')
|
||||
},
|
||||
handleClearFileList() {
|
||||
this.$refs.pond.removeFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,33 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
import Ellipsis from '@/components/Ellipsis'
|
||||
import FooterToolbar from '@/components/FooterToolbar'
|
||||
import FilePondUpload from '@/components/Upload/FilePondUpload'
|
||||
import AttachmentUploadModal from './Attachment/AttachmentUploadModal'
|
||||
import AttachmentSelectModal from './Attachment/AttachmentSelectModal'
|
||||
import AttachmentDetailModal from './Attachment/AttachmentDetailModal'
|
||||
import ReactiveButton from './Button/ReactiveButton'
|
||||
import PostTag from './Post/PostTag'
|
||||
import AttachmentInput from './Input/AttachmentInput'
|
||||
import CommentListView from './Comment/CommentListView'
|
||||
|
||||
const _components = {
|
||||
Ellipsis,
|
||||
FooterToolbar,
|
||||
FilePondUpload,
|
||||
AttachmentUploadModal,
|
||||
AttachmentSelectModal,
|
||||
AttachmentDetailModal,
|
||||
ReactiveButton,
|
||||
PostTag,
|
||||
AttachmentInput,
|
||||
CommentListView
|
||||
}
|
||||
|
||||
const components = {}
|
||||
|
||||
Object.keys(_components).forEach(key => {
|
||||
components[key] = Vue.component(key, _components[key])
|
||||
})
|
||||
|
||||
export default components
|
|
@ -1,14 +0,0 @@
|
|||
export default {
|
||||
primaryColor: '#1890FF',
|
||||
navTheme: 'dark',
|
||||
layout: 'sidemenu',
|
||||
contentWidth: 'Fixed',
|
||||
fixedHeader: false,
|
||||
fixedSidebar: true,
|
||||
autoHideHeader: false,
|
||||
storageOptions: {
|
||||
namespace: 'halo__',
|
||||
name: 'ls',
|
||||
storage: 'local'
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
// eslint-disable-next-line
|
||||
import { BasicLayout, BlankLayout, PageView } from '@/layouts'
|
||||
|
||||
export const asyncRouterMap = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: BasicLayout,
|
||||
meta: { title: '首页' },
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
// dashboard
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/Dashboard'),
|
||||
meta: { title: '仪表盘', icon: 'dashboard', hiddenHeaderContent: false, keepAlive: false }
|
||||
},
|
||||
|
||||
// posts
|
||||
{
|
||||
path: '/posts',
|
||||
name: 'Posts',
|
||||
component: BlankLayout,
|
||||
redirect: '/posts/list',
|
||||
meta: { title: '文章', icon: 'form' },
|
||||
children: [
|
||||
{
|
||||
path: '/posts/list',
|
||||
name: 'PostList',
|
||||
component: () => import('@/views/post/PostList'),
|
||||
meta: { title: '所有文章', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/posts/write',
|
||||
name: 'PostWrite',
|
||||
component: () => import('@/views/post/PostEdit'),
|
||||
meta: { title: '写文章', hiddenHeaderContent: false, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/posts/edit',
|
||||
name: 'PostEdit',
|
||||
hidden: true,
|
||||
component: () => import('@/views/post/PostEdit'),
|
||||
meta: { title: '编辑文章', hiddenHeaderContent: false, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/categories',
|
||||
name: 'CategoryList',
|
||||
component: () => import('@/views/post/CategoryList'),
|
||||
meta: { title: '分类目录', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/tags',
|
||||
name: 'TagList',
|
||||
component: () => import('@/views/post/TagList'),
|
||||
meta: { title: '标签', hiddenHeaderContent: false }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// sheets
|
||||
{
|
||||
path: '/sheets',
|
||||
name: 'Sheets',
|
||||
component: BlankLayout,
|
||||
redirect: '/sheets/list',
|
||||
meta: { title: '页面', icon: 'read' },
|
||||
children: [
|
||||
{
|
||||
path: '/sheets/list',
|
||||
name: 'SheetList',
|
||||
component: () => import('@/views/sheet/SheetList'),
|
||||
meta: { title: '所有页面', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/sheets/write',
|
||||
name: 'SheetWrite',
|
||||
component: () => import('@/views/sheet/SheetEdit'),
|
||||
meta: { title: '新建页面', hiddenHeaderContent: false, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/sheets/edit',
|
||||
name: 'SheetEdit',
|
||||
hidden: true,
|
||||
component: () => import('@/views/sheet/SheetEdit'),
|
||||
meta: { title: '编辑页面', hiddenHeaderContent: false, keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/sheets/links',
|
||||
name: 'LinkList',
|
||||
hidden: true,
|
||||
component: () => import('@/views/sheet/independent/LinkList'),
|
||||
meta: { title: '友情链接', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/sheets/photos',
|
||||
name: 'PhotoList',
|
||||
hidden: true,
|
||||
component: () => import('@/views/sheet/independent/PhotoList'),
|
||||
meta: { title: '图库', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/sheets/journals',
|
||||
name: 'JournalList',
|
||||
hidden: true,
|
||||
component: () => import('@/views/sheet/independent/JournalList'),
|
||||
meta: { title: '日志', hiddenHeaderContent: false }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// attachments
|
||||
{
|
||||
path: '/attachments',
|
||||
name: 'Attachments',
|
||||
component: () => import('@/views/attachment/AttachmentList'),
|
||||
meta: { title: '附件', icon: 'picture', hiddenHeaderContent: false }
|
||||
},
|
||||
|
||||
// comments
|
||||
{
|
||||
path: '/comments',
|
||||
name: 'Comments',
|
||||
component: () => import('@/views/comment/CommentList'),
|
||||
meta: { title: '评论', icon: 'message', hiddenHeaderContent: false }
|
||||
},
|
||||
|
||||
// interface
|
||||
{
|
||||
path: '/interface',
|
||||
name: 'Interface',
|
||||
component: BlankLayout,
|
||||
redirect: '/interface/themes',
|
||||
meta: { title: '外观', icon: 'skin' },
|
||||
children: [
|
||||
{
|
||||
path: '/interface/themes',
|
||||
name: 'ThemeList',
|
||||
component: () => import('@/views/interface/ThemeList'),
|
||||
meta: { title: '主题', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/interface/themes/setting',
|
||||
name: 'ThemeSetting',
|
||||
component: () => import('@/views/interface/ThemeSetting'),
|
||||
meta: { title: '主题设置', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/interface/themes/edit',
|
||||
name: 'ThemeEdit',
|
||||
component: () => import('@/views/interface/ThemeEdit'),
|
||||
meta: { title: '主题编辑', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/interface/menus',
|
||||
name: 'MenuList',
|
||||
component: () => import('@/views/interface/MenuList'),
|
||||
meta: { title: '菜单设置', hiddenHeaderContent: false }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// user
|
||||
{
|
||||
path: '/user',
|
||||
name: 'User',
|
||||
component: PageView,
|
||||
redirect: '/user/profile',
|
||||
meta: { title: '用户', icon: 'user' },
|
||||
children: [
|
||||
{
|
||||
path: '/user/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('@/views/user/Profile'),
|
||||
meta: { title: '个人资料', hiddenHeaderContent: false }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// system
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
component: BlankLayout,
|
||||
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: 'SystemOptions',
|
||||
component: () => import('@/views/system/SystemOptions'),
|
||||
meta: { title: '博客设置', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/system/tools',
|
||||
name: 'ToolList',
|
||||
component: () => import('@/views/system/ToolList'),
|
||||
meta: { title: '小工具', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/system/actionlogs',
|
||||
name: 'SystemActionLogs',
|
||||
hidden: true,
|
||||
component: () => import('@/views/system/ActionLogs'),
|
||||
meta: { title: '操作日志', hiddenHeaderContent: false }
|
||||
},
|
||||
{
|
||||
path: '/system/about',
|
||||
name: 'About',
|
||||
component: () => import('@/views/system/About'),
|
||||
meta: { title: '关于', hiddenHeaderContent: false }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
redirect: '/404',
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
|
||||
export const constantRouterMap = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
meta: { title: '登录' },
|
||||
component: () => import('@/views/user/Login')
|
||||
},
|
||||
{
|
||||
path: '/install',
|
||||
name: 'Install',
|
||||
meta: { title: '安装向导' },
|
||||
component: () => import('@/views/system/Installation')
|
||||
},
|
||||
{
|
||||
path: '/password/reset',
|
||||
name: 'ResetPassword',
|
||||
meta: { title: '重置密码' },
|
||||
component: () => import('@/views/user/ResetPassword')
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/exception/404')
|
||||
}
|
||||
]
|
|
@ -1,31 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import store from '@/store/'
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
DEFAULT_COLOR,
|
||||
DEFAULT_CONTENT_WIDTH_TYPE,
|
||||
DEFAULT_FIXED_HEADER,
|
||||
DEFAULT_FIXED_HEADER_HIDDEN,
|
||||
DEFAULT_FIXED_SIDEBAR,
|
||||
DEFAULT_LAYOUT_MODE,
|
||||
DEFAULT_THEME,
|
||||
OPTIONS,
|
||||
SIDEBAR_TYPE,
|
||||
USER
|
||||
} from '@/store/mutation-types'
|
||||
import config from '@/config/defaultSettings'
|
||||
|
||||
export default function Initializer() {
|
||||
store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
|
||||
store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
|
||||
store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
|
||||
store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
|
||||
store.commit('TOGGLE_FIXED_SIDEBAR', Vue.ls.get(DEFAULT_FIXED_SIDEBAR, config.fixedSidebar))
|
||||
store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
|
||||
store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
|
||||
store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
|
||||
store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
|
||||
store.commit('SET_USER', Vue.ls.get(USER))
|
||||
store.commit('SET_OPTIONS', Vue.ls.get(OPTIONS))
|
||||
// last step
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
export const markdownEditorToolbars = {
|
||||
bold: true,
|
||||
italic: true,
|
||||
header: true,
|
||||
underline: true,
|
||||
strikethrough: true,
|
||||
superscript: true,
|
||||
subscript: true,
|
||||
quote: true,
|
||||
ol: true,
|
||||
ul: true,
|
||||
link: true,
|
||||
imagelink: true,
|
||||
code: true,
|
||||
table: true,
|
||||
undo: true,
|
||||
redo: true,
|
||||
save: true,
|
||||
navigation: true,
|
||||
subfield: true,
|
||||
fullscreen: true,
|
||||
readmodel: true,
|
||||
htmlcode: true,
|
||||
preview: true
|
||||
}
|
||||
|
||||
export const actionLogTypes = {
|
||||
BLOG_INITIALIZED: {
|
||||
value: 0,
|
||||
text: '博客初始化'
|
||||
},
|
||||
POST_PUBLISHED: {
|
||||
value: 5,
|
||||
text: '文章发布'
|
||||
},
|
||||
POST_EDITED: {
|
||||
value: 15,
|
||||
text: '文章修改'
|
||||
},
|
||||
POST_DELETED: {
|
||||
value: 20,
|
||||
text: '文章删除'
|
||||
},
|
||||
LOGGED_IN: {
|
||||
value: 25,
|
||||
text: '用户登录'
|
||||
},
|
||||
LOGGED_OUT: {
|
||||
value: 30,
|
||||
text: '注销登录'
|
||||
},
|
||||
LOGIN_FAILED: {
|
||||
value: 35,
|
||||
text: '登录失败'
|
||||
},
|
||||
PASSWORD_UPDATED: {
|
||||
value: 40,
|
||||
text: '修改密码'
|
||||
},
|
||||
PROFILE_UPDATED: {
|
||||
value: 45,
|
||||
text: '资料修改'
|
||||
},
|
||||
SHEET_PUBLISHED: {
|
||||
value: 50,
|
||||
text: '页面发布'
|
||||
},
|
||||
SHEET_EDITED: {
|
||||
value: 55,
|
||||
text: '页面修改'
|
||||
},
|
||||
SHEET_DELETED: {
|
||||
value: 60,
|
||||
text: '页面删除'
|
||||
},
|
||||
MFA_UPDATED: {
|
||||
value: 65,
|
||||
text: '两步验证'
|
||||
},
|
||||
LOGGED_PRE_CHECK: {
|
||||
value: 70,
|
||||
text: '登录验证'
|
||||
}
|
||||
}
|
||||
|
||||
export const attachmentTypes = {
|
||||
LOCAL: {
|
||||
type: 'LOCAL',
|
||||
text: '本地'
|
||||
},
|
||||
SMMS: {
|
||||
type: 'SMMS',
|
||||
text: 'SM.MS'
|
||||
},
|
||||
UPOSS: {
|
||||
type: 'UPOSS',
|
||||
text: '又拍云'
|
||||
},
|
||||
QINIUOSS: {
|
||||
type: 'QINIUOSS',
|
||||
text: '七牛云'
|
||||
},
|
||||
ALIOSS: {
|
||||
type: 'ALIOSS',
|
||||
text: '阿里云'
|
||||
},
|
||||
BAIDUBOS: {
|
||||
type: 'BAIDUBOS',
|
||||
text: '百度云'
|
||||
},
|
||||
TENCENTCOS: {
|
||||
type: 'TENCENTCOS',
|
||||
text: '腾讯云'
|
||||
},
|
||||
HUAWEIOBS: {
|
||||
type: 'HUAWEIOBS',
|
||||
text: '华为云'
|
||||
},
|
||||
MINIO: {
|
||||
type: 'MINIO',
|
||||
text: 'MinIO'
|
||||
}
|
||||
}
|
||||
|
||||
export const postStatuses = {
|
||||
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: '私密'
|
||||
}
|
||||
}
|
||||
|
||||
export const sheetStatuses = {
|
||||
PUBLISHED: {
|
||||
color: 'green',
|
||||
status: 'success',
|
||||
text: '已发布'
|
||||
},
|
||||
DRAFT: {
|
||||
color: 'yellow',
|
||||
status: 'warning',
|
||||
text: '草稿'
|
||||
},
|
||||
RECYCLE: {
|
||||
color: 'red',
|
||||
status: 'error',
|
||||
text: '回收站'
|
||||
}
|
||||
}
|
||||
|
||||
export const commentStatuses = {
|
||||
PUBLISHED: {
|
||||
value: 'PUBLISHED',
|
||||
color: 'green',
|
||||
status: 'success',
|
||||
text: '已发布'
|
||||
},
|
||||
AUDITING: {
|
||||
value: 'AUDITING',
|
||||
color: 'yellow',
|
||||
status: 'warning',
|
||||
text: '待审核'
|
||||
},
|
||||
RECYCLE: {
|
||||
value: 'RECYCLE',
|
||||
color: 'red',
|
||||
status: 'error',
|
||||
text: '回收站'
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import {
|
||||
Affix,
|
||||
Alert,
|
||||
Anchor,
|
||||
AutoComplete,
|
||||
Avatar,
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
Collapse,
|
||||
Comment,
|
||||
ConfigProvider,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Form,
|
||||
FormModel,
|
||||
Icon,
|
||||
Input,
|
||||
InputNumber,
|
||||
Layout,
|
||||
List,
|
||||
LocaleProvider,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
notification,
|
||||
PageHeader,
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Progress,
|
||||
Radio,
|
||||
Result,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space,
|
||||
Spin,
|
||||
Steps,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Timeline,
|
||||
TimePicker,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
Descriptions
|
||||
} from 'ant-design-vue'
|
||||
|
||||
Vue.use(Affix)
|
||||
Vue.use(Anchor)
|
||||
Vue.use(AutoComplete)
|
||||
Vue.use(Alert)
|
||||
Vue.use(Avatar)
|
||||
Vue.use(Badge)
|
||||
Vue.use(Breadcrumb)
|
||||
Vue.use(Button)
|
||||
Vue.use(Card)
|
||||
Vue.use(Collapse)
|
||||
Vue.use(Checkbox)
|
||||
Vue.use(Col)
|
||||
Vue.use(DatePicker)
|
||||
Vue.use(Divider)
|
||||
Vue.use(Drawer)
|
||||
Vue.use(Dropdown)
|
||||
Vue.use(Form)
|
||||
Vue.use(FormModel)
|
||||
Vue.use(Icon)
|
||||
Vue.use(Input)
|
||||
Vue.use(InputNumber)
|
||||
Vue.use(Layout)
|
||||
Vue.use(List)
|
||||
Vue.use(LocaleProvider)
|
||||
Vue.use(Menu)
|
||||
Vue.use(Modal)
|
||||
Vue.use(PageHeader)
|
||||
Vue.use(Pagination)
|
||||
Vue.use(Popconfirm)
|
||||
Vue.use(Popover)
|
||||
Vue.use(Progress)
|
||||
Vue.use(Radio)
|
||||
Vue.use(Row)
|
||||
Vue.use(Select)
|
||||
Vue.use(Spin)
|
||||
Vue.use(Switch)
|
||||
Vue.use(Table)
|
||||
Vue.use(Tree)
|
||||
Vue.use(TreeSelect)
|
||||
Vue.use(Tabs)
|
||||
Vue.use(Tag)
|
||||
Vue.use(TimePicker)
|
||||
Vue.use(Tooltip)
|
||||
Vue.use(Skeleton)
|
||||
Vue.use(Comment)
|
||||
Vue.use(ConfigProvider)
|
||||
Vue.use(Timeline)
|
||||
Vue.use(Steps)
|
||||
Vue.use(Empty)
|
||||
Vue.use(Result)
|
||||
Vue.use(Space)
|
||||
Vue.use(Descriptions)
|
||||
|
||||
// message config
|
||||
message.config({
|
||||
maxCount: 1
|
||||
})
|
||||
|
||||
Vue.prototype.$message = message
|
||||
Vue.prototype.$notification = notification
|
||||
Vue.prototype.$info = Modal.info
|
||||
Vue.prototype.$success = Modal.success
|
||||
Vue.prototype.$error = Modal.error
|
||||
Vue.prototype.$warning = Modal.warning
|
||||
Vue.prototype.$confirm = Modal.confirm
|
|
@ -1,15 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import VueStorage from 'vue-ls'
|
||||
import config from '@/config/defaultSettings'
|
||||
|
||||
// base library
|
||||
import '@/core/lazy_lib/components_use'
|
||||
import 'ant-design-vue/dist/antd.less'
|
||||
import bootstrap from './bootstrap'
|
||||
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
|
||||
Vue.use(VueStorage, config.storageOptions)
|
||||
Vue.use(VueClipboard)
|
||||
|
||||
bootstrap()
|
|
@ -1,14 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import VueStorage from 'vue-ls'
|
||||
import config from '@/config/defaultSettings'
|
||||
|
||||
// base library
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/antd.less'
|
||||
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
|
||||
Vue.use(Antd)
|
||||
|
||||
Vue.use(VueStorage, config.storageOptions)
|
||||
Vue.use(VueClipboard)
|
|
@ -1,52 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
import { timeAgo } from '@/utils/datetime'
|
||||
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
import { marked } from 'marked'
|
||||
|
||||
Vue.filter('moment', function (dataStr, pattern = 'YYYY-MM-DD HH:mm') {
|
||||
return dayjs(dataStr).format(pattern)
|
||||
})
|
||||
|
||||
Vue.filter('moment_post_date', function (dataStr, pattern = '/YYYY/MM/') {
|
||||
return dayjs(dataStr).format(pattern)
|
||||
})
|
||||
|
||||
Vue.filter('moment_post_year', function (dataStr, pattern = '/YYYY/') {
|
||||
return dayjs(dataStr).format(pattern)
|
||||
})
|
||||
|
||||
Vue.filter('moment_post_day', function (dataStr, pattern = '/YYYY/MM/DD/') {
|
||||
return dayjs(dataStr).format(pattern)
|
||||
})
|
||||
|
||||
Vue.filter('timeAgo', timeAgo)
|
||||
|
||||
Vue.filter('fileSizeFormat', function (value) {
|
||||
if (!value) {
|
||||
return '0 Bytes'
|
||||
}
|
||||
const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const srcsize = parseFloat(value)
|
||||
let index = Math.floor(Math.log(srcsize) / Math.log(1024))
|
||||
let size = srcsize / Math.pow(1024, index)
|
||||
size = size.toFixed(2)
|
||||
return size + ' ' + unitArr[index]
|
||||
})
|
||||
|
||||
Vue.filter('dayTime', function (value) {
|
||||
const days = Math.floor(value / 86400)
|
||||
const hours = Math.floor((value % 86400) / 3600)
|
||||
const minutes = Math.floor(((value % 86400) % 3600) / 60)
|
||||
const seconds = Math.floor(((value % 86400) % 3600) % 60)
|
||||
return days + 'd ' + hours + 'h ' + minutes + 'm ' + seconds + 's'
|
||||
})
|
||||
|
||||
Vue.filter('markdownRender', function (value) {
|
||||
return marked.parse(value)
|
||||
})
|
|
@ -1,174 +0,0 @@
|
|||
<template>
|
||||
<a-layout :class="['layout', device]">
|
||||
<!-- SideMenu -->
|
||||
<a-drawer
|
||||
v-if="isMobile()"
|
||||
:closable="false"
|
||||
:visible="collapsed"
|
||||
:wrapClassName="`drawer-sider ${navTheme}`"
|
||||
placement="left"
|
||||
@close="drawerClose"
|
||||
>
|
||||
<side-menu
|
||||
:collapsed="false"
|
||||
:collapsible="true"
|
||||
:menus="menus"
|
||||
:theme="navTheme"
|
||||
mode="inline"
|
||||
@menuSelect="menuSelect"
|
||||
></side-menu>
|
||||
</a-drawer>
|
||||
|
||||
<side-menu
|
||||
v-else-if="isSideMenu()"
|
||||
:collapsed="collapsed"
|
||||
:collapsible="true"
|
||||
:menus="menus"
|
||||
:theme="navTheme"
|
||||
mode="inline"
|
||||
></side-menu>
|
||||
|
||||
<a-layout
|
||||
:class="[layoutMode, `content-width-${contentWidth}`]"
|
||||
:style="{ paddingLeft: contentPaddingLeft, minHeight: '100vh' }"
|
||||
>
|
||||
<!-- layout header -->
|
||||
<global-header
|
||||
:collapsed="collapsed"
|
||||
:device="device"
|
||||
:menus="menus"
|
||||
:mode="layoutMode"
|
||||
:theme="navTheme"
|
||||
@toggle="toggle"
|
||||
/>
|
||||
|
||||
<!-- layout content -->
|
||||
<a-layout-content :style="{ height: '100%', margin: '24px 24px 0', paddingTop: fixedHeader ? '64px' : '0' }">
|
||||
<transition name="page-transition">
|
||||
<route-view />
|
||||
</transition>
|
||||
</a-layout-content>
|
||||
|
||||
<!-- layout footer -->
|
||||
<a-layout-footer>
|
||||
<global-footer />
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
|
||||
<setting-drawer ref="drawer"></setting-drawer>
|
||||
|
||||
<LoginModal @success="onLoginSucceed" />
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { triggerWindowResizeEvent } from '@/utils/util'
|
||||
import { mapActions } from 'vuex'
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin'
|
||||
import config from '@/config/defaultSettings'
|
||||
import { asyncRouterMap } from '@/config/router.config.js'
|
||||
|
||||
import RouteView from './RouteView'
|
||||
import SideMenu from '@/components/Menu/SideMenu'
|
||||
import GlobalHeader from '@/components/GlobalHeader'
|
||||
import GlobalFooter from '@/components/GlobalFooter'
|
||||
import SettingDrawer from '@/components/SettingDrawer/SettingDrawer'
|
||||
import LoginModal from '@/components/Login/LoginModal'
|
||||
|
||||
export default {
|
||||
name: 'BasicLayout',
|
||||
mixins: [mixin, mixinDevice],
|
||||
components: {
|
||||
RouteView,
|
||||
SideMenu,
|
||||
GlobalHeader,
|
||||
GlobalFooter,
|
||||
SettingDrawer,
|
||||
LoginModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
production: config.production,
|
||||
collapsed: false,
|
||||
menus: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
contentPaddingLeft() {
|
||||
if (!this.fixedSidebar || this.isMobile()) {
|
||||
return '0'
|
||||
}
|
||||
if (this.sidebarOpened) {
|
||||
return '256px'
|
||||
}
|
||||
return '80px'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sidebarOpened(val) {
|
||||
this.collapsed = !val
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.menus = asyncRouterMap.find(item => item.path === '/').children
|
||||
this.collapsed = !this.sidebarOpened
|
||||
},
|
||||
mounted() {
|
||||
const userAgent = navigator.userAgent
|
||||
if (userAgent.indexOf('Edge') > -1) {
|
||||
this.$nextTick(() => {
|
||||
this.collapsed = !this.collapsed
|
||||
setTimeout(() => {
|
||||
this.collapsed = !this.collapsed
|
||||
}, 16)
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSidebar', 'ToggleLoginModal']),
|
||||
toggle() {
|
||||
this.collapsed = !this.collapsed
|
||||
this.setSidebar(!this.collapsed)
|
||||
triggerWindowResizeEvent()
|
||||
},
|
||||
paddingCalc() {
|
||||
let left = ''
|
||||
if (this.sidebarOpened) {
|
||||
left = this.isDesktop() ? '256px' : '80px'
|
||||
} else {
|
||||
left = (this.isMobile() && '0') || (this.fixedSidebar && '80px') || '0'
|
||||
}
|
||||
return left
|
||||
},
|
||||
menuSelect() {
|
||||
if (!this.isDesktop()) {
|
||||
this.collapsed = false
|
||||
}
|
||||
},
|
||||
drawerClose() {
|
||||
this.collapsed = false
|
||||
},
|
||||
onLoginSucceed() {
|
||||
this.ToggleLoginModal(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import url('../styles/global.less');
|
||||
|
||||
.page-transition-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page-transition-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page-transition-enter .page-transition-container,
|
||||
.page-transition-leave-active .page-transition-container {
|
||||
-webkit-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
|
@ -1,11 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BlankLayout'
|
||||
}
|
||||
</script>
|
|
@ -1,126 +0,0 @@
|
|||
<template>
|
||||
<div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null">
|
||||
<a-affix v-if="affix">
|
||||
<div v-if="!$route.meta.hiddenHeaderContent" class="page-header">
|
||||
<div class="page-header-index-wide">
|
||||
<a-page-header :breadcrumb="{ props: { routes: breadList } }" :sub-title="subTitle" :title="title">
|
||||
<slot slot="extra" name="extra"></slot>
|
||||
<slot slot="footer" name="footer"></slot>
|
||||
<slot name="content" />
|
||||
</a-page-header>
|
||||
</div>
|
||||
</div>
|
||||
</a-affix>
|
||||
<div v-if="!$route.meta.hiddenHeaderContent && !affix" class="page-header">
|
||||
<div class="page-header-index-wide">
|
||||
<a-page-header :breadcrumb="{ props: { routes: breadList } }" :sub-title="subTitle" :title="title">
|
||||
<slot slot="extra" name="extra"></slot>
|
||||
<slot slot="footer" name="footer"></slot>
|
||||
<slot name="content" />
|
||||
</a-page-header>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="page-header-index-wide">
|
||||
<slot>
|
||||
<router-view ref="content" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageView',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
affix: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
breadList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getBreadcrumb()
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getBreadcrumb()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getBreadcrumb() {
|
||||
this.breadList = []
|
||||
this.$route.matched.forEach(item => {
|
||||
item.breadcrumbName = item.meta.title
|
||||
this.breadList.push(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-header {
|
||||
background: #fff;
|
||||
padding: 0 24px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
.ant-page-header {
|
||||
padding: 16px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile .page-header,
|
||||
.tablet .page-header {
|
||||
padding: 0 !important;
|
||||
|
||||
.ant-page-header {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 24px 24px 0;
|
||||
|
||||
.link {
|
||||
margin-top: 16px;
|
||||
|
||||
&:not(:empty) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-right: 32px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
display: inline-block;
|
||||
|
||||
i {
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,31 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'RouteView',
|
||||
props: {
|
||||
keepAlive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
const {
|
||||
$route: { meta }
|
||||
} = this
|
||||
const inKeep = (
|
||||
<keep-alive>
|
||||
<router-view />
|
||||
</keep-alive>
|
||||
)
|
||||
const notKeep = <router-view />
|
||||
// 应当全部组件皆缓存,否则会导致切换页面后页面还原成原始状态
|
||||
// 若确实不需要,可改为 return meta.keepAlive ? inKeep : notKeep
|
||||
if (meta.keepAlive === false) {
|
||||
return notKeep
|
||||
}
|
||||
return this.keepAlive || meta.keepAlive ? inKeep : notKeep
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,6 +0,0 @@
|
|||
import BlankLayout from './BlankLayout'
|
||||
import BasicLayout from './BasicLayout'
|
||||
import RouteView from './RouteView'
|
||||
import PageView from './PageView'
|
||||
|
||||
export { BasicLayout, BlankLayout, RouteView, PageView }
|
|
@ -1,15 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import VueLogger from 'vuejs-logger'
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const options = {
|
||||
isEnabled: true,
|
||||
logLevel: isProduction ? 'error' : 'debug',
|
||||
stringifyArguments: false,
|
||||
showLogLevel: true,
|
||||
showMethodName: true,
|
||||
separator: '|',
|
||||
showConsoleColors: true
|
||||
}
|
||||
|
||||
Vue.use(VueLogger, options)
|
25
src/main.js
25
src/main.js
|
@ -1,25 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import Contextmenu from 'vue-contextmenujs'
|
||||
import store from './store/'
|
||||
import './logger'
|
||||
|
||||
import '@/styles/tailwind.css'
|
||||
import './core/lazy_use'
|
||||
import '@/router/guard/'
|
||||
import '@/filters/filter' // global filter
|
||||
import './components'
|
||||
import pkg from '../package.json'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.prototype.VERSION = pkg.version
|
||||
|
||||
Vue.use(router)
|
||||
Vue.use(Contextmenu)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
|
@ -1,91 +0,0 @@
|
|||
// import Vue from 'vue'
|
||||
import { DEVICE_TYPE } from '@/utils/device'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
// const mixinsComputed = Vue.config.optionMergeStrategies.computed
|
||||
// const mixinsMethods = Vue.config.optionMergeStrategies.methods
|
||||
|
||||
const mixin = {
|
||||
computed: {
|
||||
...mapState({
|
||||
layoutMode: state => state.app.layout,
|
||||
navTheme: state => state.app.theme,
|
||||
primaryColor: state => state.app.color,
|
||||
fixedHeader: state => state.app.fixedHeader,
|
||||
fixedSidebar: state => state.app.fixedSidebar,
|
||||
contentWidth: state => state.app.contentWidth,
|
||||
autoHideHeader: state => state.app.autoHideHeader,
|
||||
sidebarOpened: state => state.app.sidebar
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
isTopMenu() {
|
||||
return this.layoutMode === 'topmenu'
|
||||
},
|
||||
isSideMenu() {
|
||||
return !this.isTopMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mixinDevice = {
|
||||
computed: {
|
||||
...mapState({
|
||||
device: state => state.app.device
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
isMobile() {
|
||||
return this.device === DEVICE_TYPE.MOBILE
|
||||
},
|
||||
isDesktop() {
|
||||
return this.device === DEVICE_TYPE.DESKTOP
|
||||
},
|
||||
isTablet() {
|
||||
return this.device === DEVICE_TYPE.TABLET
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mixinPostEdit = {
|
||||
data() {
|
||||
return {
|
||||
viewMetas: {
|
||||
pageHeaderHeight: 0,
|
||||
pageFooterHeight: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
editorHeight() {
|
||||
const toolbarHeight = 64
|
||||
const contentMarginTop = 24
|
||||
const titleInputHeight = 40
|
||||
return `calc(100vh - ${
|
||||
toolbarHeight +
|
||||
contentMarginTop +
|
||||
titleInputHeight +
|
||||
this.viewMetas.pageHeaderHeight +
|
||||
this.viewMetas.pageFooterHeight +
|
||||
10
|
||||
}px - 1rem)`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handleGetViewMetas()
|
||||
},
|
||||
methods: {
|
||||
handleGetViewMetas() {
|
||||
const pageHeaderView = document.getElementsByClassName('page-header')
|
||||
if (pageHeaderView && pageHeaderView.length > 0) {
|
||||
this.viewMetas.pageHeaderHeight = pageHeaderView[0].clientHeight
|
||||
}
|
||||
const pageFooterView = document.getElementsByClassName('ant-layout-footer')
|
||||
if (pageFooterView && pageFooterView.length > 0) {
|
||||
this.viewMetas.pageFooterHeight = pageFooterView[0].clientHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { mixin, mixinDevice, mixinPostEdit }
|
|
@ -1 +0,0 @@
|
|||
import './permissionGuard'
|
|
@ -1,70 +0,0 @@
|
|||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import NProgress from 'nprogress'
|
||||
import { domTitle, setDocumentTitle } from '@/utils/domUtil'
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
NProgress.configure({ showSpinner: false, speed: 500 })
|
||||
|
||||
const whiteList = ['Login', 'Install', 'NotFound', 'ResetPassword'] // no redirect whitelist
|
||||
|
||||
let progressTimer = null
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
onProgressTimerDone()
|
||||
progressTimer = setTimeout(() => {
|
||||
NProgress.start()
|
||||
}, 250)
|
||||
to.meta && typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`)
|
||||
if (store.getters.token) {
|
||||
if (to.name === 'Install') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
const response = await apiClient.isInstalled()
|
||||
if (!response.data) {
|
||||
next({
|
||||
name: 'Install'
|
||||
})
|
||||
onProgressTimerDone()
|
||||
return
|
||||
}
|
||||
if (to.name === 'Login') {
|
||||
next({
|
||||
name: 'Dashboard'
|
||||
})
|
||||
onProgressTimerDone()
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.getters.options) {
|
||||
store.dispatch('refreshOptionsCache').then()
|
||||
}
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// Check whitelist
|
||||
if (whiteList.includes(to.name)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
next({
|
||||
name: 'Login',
|
||||
query: {
|
||||
redirect: to.fullPath
|
||||
}
|
||||
})
|
||||
onProgressTimerDone()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
onProgressTimerDone()
|
||||
})
|
||||
|
||||
function onProgressTimerDone() {
|
||||
if (progressTimer && progressTimer !== 0) {
|
||||
clearTimeout(progressTimer)
|
||||
progressTimer = null
|
||||
NProgress.done()
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
export default new Router({
|
||||
mode: 'hash',
|
||||
base: process.env.BASE_URL,
|
||||
scrollBehavior: () => ({
|
||||
y: 0
|
||||
}),
|
||||
routes: constantRouterMap.concat(asyncRouterMap)
|
||||
})
|
|
@ -1,12 +0,0 @@
|
|||
const getters = {
|
||||
device: state => state.app.device,
|
||||
theme: state => state.app.theme,
|
||||
color: state => state.app.color,
|
||||
layoutSetting: state => state.app.layoutSetting,
|
||||
loginModal: state => state.app.loginModal,
|
||||
token: state => state.user.token,
|
||||
user: state => state.user.user,
|
||||
options: state => state.option.options
|
||||
}
|
||||
|
||||
export default getters
|
|
@ -1,21 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import app from './modules/app'
|
||||
import user from './modules/user'
|
||||
import option from './modules/option'
|
||||
import getters from './getters'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
app,
|
||||
user,
|
||||
option
|
||||
},
|
||||
state: {},
|
||||
mutations: {},
|
||||
actions: {},
|
||||
getters
|
||||
})
|
|
@ -1,110 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import {
|
||||
DEFAULT_COLOR,
|
||||
DEFAULT_CONTENT_WIDTH_TYPE,
|
||||
DEFAULT_FIXED_HEADER,
|
||||
DEFAULT_FIXED_HEADER_HIDDEN,
|
||||
DEFAULT_FIXED_SIDEBAR,
|
||||
DEFAULT_LAYOUT_MODE,
|
||||
DEFAULT_THEME,
|
||||
LAYOUT_SETTING,
|
||||
SIDEBAR_TYPE
|
||||
} from '@/store/mutation-types'
|
||||
|
||||
const app = {
|
||||
state: {
|
||||
sidebar: true,
|
||||
device: 'desktop',
|
||||
theme: '',
|
||||
layout: '',
|
||||
contentWidth: '',
|
||||
fixedHeader: false,
|
||||
fixedSidebar: false,
|
||||
autoHideHeader: false,
|
||||
color: null,
|
||||
layoutSetting: false,
|
||||
loginModal: false
|
||||
},
|
||||
mutations: {
|
||||
SET_SIDEBAR_TYPE: (state, type) => {
|
||||
state.sidebar = type
|
||||
Vue.ls.set(SIDEBAR_TYPE, type)
|
||||
},
|
||||
CLOSE_SIDEBAR: state => {
|
||||
Vue.ls.set(SIDEBAR_TYPE, true)
|
||||
state.sidebar = false
|
||||
},
|
||||
TOGGLE_DEVICE: (state, device) => {
|
||||
state.device = device
|
||||
},
|
||||
TOGGLE_THEME: (state, theme) => {
|
||||
Vue.ls.set(DEFAULT_THEME, theme)
|
||||
state.theme = theme
|
||||
},
|
||||
TOGGLE_LAYOUT_MODE: (state, layout) => {
|
||||
Vue.ls.set(DEFAULT_LAYOUT_MODE, layout)
|
||||
state.layout = layout
|
||||
},
|
||||
TOGGLE_FIXED_HEADER: (state, fixed) => {
|
||||
Vue.ls.set(DEFAULT_FIXED_HEADER, fixed)
|
||||
state.fixedHeader = fixed
|
||||
},
|
||||
TOGGLE_FIXED_SIDEBAR: (state, fixed) => {
|
||||
Vue.ls.set(DEFAULT_FIXED_SIDEBAR, fixed)
|
||||
state.fixedSidebar = fixed
|
||||
},
|
||||
TOGGLE_FIXED_HEADER_HIDDEN: (state, show) => {
|
||||
Vue.ls.set(DEFAULT_FIXED_HEADER_HIDDEN, show)
|
||||
state.autoHideHeader = show
|
||||
},
|
||||
TOGGLE_CONTENT_WIDTH: (state, type) => {
|
||||
Vue.ls.set(DEFAULT_CONTENT_WIDTH_TYPE, type)
|
||||
state.contentWidth = type
|
||||
},
|
||||
TOGGLE_COLOR: (state, color) => {
|
||||
Vue.ls.set(DEFAULT_COLOR, color)
|
||||
state.color = color
|
||||
},
|
||||
TOGGLE_LAYOUT_SETTING: (state, show) => {
|
||||
Vue.ls.set(LAYOUT_SETTING, show)
|
||||
state.layoutSetting = show
|
||||
},
|
||||
TOGGLE_LOGIN_MODAL: (state, show) => {
|
||||
state.loginModal = show
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setSidebar({ commit }, type) {
|
||||
commit('SET_SIDEBAR_TYPE', type)
|
||||
},
|
||||
ToggleTheme({ commit }, theme) {
|
||||
commit('TOGGLE_THEME', theme)
|
||||
},
|
||||
ToggleLayoutMode({ commit }, mode) {
|
||||
commit('TOGGLE_LAYOUT_MODE', mode)
|
||||
},
|
||||
ToggleFixedHeader({ commit }, fixedHeader) {
|
||||
commit('TOGGLE_FIXED_HEADER', fixedHeader)
|
||||
},
|
||||
ToggleFixedSidebar({ commit }, fixedSidebar) {
|
||||
commit('TOGGLE_FIXED_SIDEBAR', fixedSidebar)
|
||||
},
|
||||
ToggleFixedHeaderHidden({ commit }, show) {
|
||||
commit('TOGGLE_FIXED_HEADER_HIDDEN', show)
|
||||
},
|
||||
ToggleContentWidth({ commit }, type) {
|
||||
commit('TOGGLE_CONTENT_WIDTH', type)
|
||||
},
|
||||
ToggleColor({ commit }, color) {
|
||||
commit('TOGGLE_COLOR', color)
|
||||
},
|
||||
ToggleLayoutSetting({ commit }, show) {
|
||||
commit('TOGGLE_LAYOUT_SETTING', show)
|
||||
},
|
||||
ToggleLoginModal({ commit }, show) {
|
||||
commit('TOGGLE_LOGIN_MODAL', show)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default app
|
|
@ -1,46 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { OPTIONS } from '@/store/mutation-types'
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
const keys = [
|
||||
'blog_url',
|
||||
'developer_mode',
|
||||
'attachment_upload_image_preview_enable',
|
||||
'attachment_upload_max_parallel_uploads',
|
||||
'attachment_upload_max_files',
|
||||
'sheet_prefix',
|
||||
'post_permalink_type',
|
||||
'sheet_permalink_type',
|
||||
'archives_prefix',
|
||||
'path_suffix',
|
||||
'default_editor',
|
||||
'default_menu_team'
|
||||
]
|
||||
const option = {
|
||||
state: {
|
||||
options: []
|
||||
},
|
||||
mutations: {
|
||||
SET_OPTIONS: (state, options) => {
|
||||
Vue.ls.set(OPTIONS, options)
|
||||
state.options = options
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
refreshOptionsCache({ commit }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient.option
|
||||
.listAsMapViewByKeys(keys)
|
||||
.then(response => {
|
||||
commit('SET_OPTIONS', response.data)
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default option
|
|
@ -1,106 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { ACCESS_TOKEN, USER } from '@/store/mutation-types'
|
||||
import apiClient from '@/utils/api-client'
|
||||
|
||||
const user = {
|
||||
state: {
|
||||
token: null,
|
||||
user: {}
|
||||
},
|
||||
mutations: {
|
||||
SET_TOKEN: (state, token) => {
|
||||
Vue.ls.set(ACCESS_TOKEN, token, token ? token.expired_in * 1000 : null)
|
||||
state.token = token
|
||||
},
|
||||
CLEAR_TOKEN: state => {
|
||||
Vue.ls.remove(ACCESS_TOKEN)
|
||||
state.token = null
|
||||
},
|
||||
SET_USER: (state, user) => {
|
||||
Vue.ls.set(USER, user)
|
||||
state.user = user
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
installCleanToken({ commit }, installData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient.installation
|
||||
.install(installData)
|
||||
.then(response => {
|
||||
commit('CLEAR_TOKEN')
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
refreshUserCache({ commit }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient.user
|
||||
.getProfile()
|
||||
.then(response => {
|
||||
commit('SET_USER', response.data)
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
login({ commit }, { username, password, authcode }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.login({ username, password, authcode })
|
||||
.then(response => {
|
||||
const token = response.data
|
||||
Vue.$log.debug('Got token', token)
|
||||
commit('SET_TOKEN', token)
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
logout({ commit }) {
|
||||
return new Promise(resolve => {
|
||||
apiClient
|
||||
.logout()
|
||||
.then(() => {
|
||||
commit('CLEAR_TOKEN')
|
||||
commit('SET_USER', {})
|
||||
resolve()
|
||||
})
|
||||
.catch(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
refreshToken({ commit }, refreshToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.refreshToken(refreshToken)
|
||||
.then(response => {
|
||||
const token = response.data
|
||||
Vue.$log.debug('Got token', token)
|
||||
commit('SET_TOKEN', token)
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch(error => {
|
||||
const data = error.data
|
||||
Vue.$log.debug('Refresh error data', data)
|
||||
if (data && data.status === 400 && data.data === refreshToken) {
|
||||
// The refresh token expired
|
||||
commit('CLEAR_TOKEN')
|
||||
}
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default user
|
|
@ -1,17 +0,0 @@
|
|||
export const ACCESS_TOKEN = 'Access-Token'
|
||||
export const SIDEBAR_TYPE = 'SIDEBAR_TYPE'
|
||||
export const DEFAULT_THEME = 'DEFAULT_THEME'
|
||||
export const DEFAULT_LAYOUT_MODE = 'DEFAULT_LAYOUT_MODE'
|
||||
export const DEFAULT_COLOR = 'DEFAULT_COLOR'
|
||||
export const DEFAULT_FIXED_HEADER = 'DEFAULT_FIXED_HEADER'
|
||||
export const DEFAULT_FIXED_SIDEBAR = 'DEFAULT_FIXED_SIDEBAR'
|
||||
export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN'
|
||||
export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
|
||||
export const USER = 'USER'
|
||||
export const OPTIONS = 'OPTIONS'
|
||||
export const LAYOUT_SETTING = 'LAYOUT_SETTING'
|
||||
|
||||
export const CONTENT_WIDTH_TYPE = {
|
||||
Fluid: 'Fluid',
|
||||
Fixed: 'Fixed'
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
|
@ -1,948 +0,0 @@
|
|||
@import './index.less';
|
||||
@import './style.less';
|
||||
|
||||
body {
|
||||
overflow-y: overlay;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.layout.ant-layout {
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.mobile,
|
||||
&.tablet {
|
||||
.ant-layout-content {
|
||||
.content {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ant-table-wrapper
|
||||
* 覆盖的表格手机模式样式,如果想修改在手机上表格最低宽度,可以在这里改动
|
||||
*/
|
||||
|
||||
.ant-table-wrapper {
|
||||
.ant-table-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ant-table-body {
|
||||
min-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
.topmenu {
|
||||
/* 必须为 topmenu 才能启用流式布局 */
|
||||
|
||||
&.content-width-Fluid {
|
||||
.header-index-wide {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.sidemenu {
|
||||
.ant-header-fixedHeader {
|
||||
&.ant-header-side-opened,
|
||||
&.ant-header-side-closed {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-layout-has-sider {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
font-size: 20px;
|
||||
line-height: 64px;
|
||||
padding: 0 24px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
|
||||
.topmenu {
|
||||
.ant-header-fixedHeader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
transition: width 0.2s;
|
||||
|
||||
&.ant-header-side-opened {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.ant-header-side-closed {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 必须为 topmenu 才能启用流式布局 */
|
||||
|
||||
&.content-width-Fluid {
|
||||
.header-index-wide {
|
||||
max-width: unset;
|
||||
|
||||
.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 {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidemenu {
|
||||
.ant-header-fixedHeader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
transition: width 0.2s;
|
||||
|
||||
&.ant-header-side-opened {
|
||||
width: calc(100% - 256px);
|
||||
}
|
||||
|
||||
&.ant-header-side-closed {
|
||||
width: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header,
|
||||
.top-nav-header-index {
|
||||
.user-wrapper {
|
||||
float: right;
|
||||
height: 100%;
|
||||
|
||||
.action {
|
||||
cursor: pointer;
|
||||
padding: 0 18px;
|
||||
display: inline-block;
|
||||
transition: all 0.3s;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin: 20px 0 20px 0;
|
||||
color: #1890ff;
|
||||
background: hsla(0, 0%, 100%, 0.85);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
.user-wrapper {
|
||||
.action {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile,
|
||||
&.tablet {
|
||||
.top-nav-header-index {
|
||||
.header-index-wide {
|
||||
.header-index-left {
|
||||
.trigger {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.logo.top-nav-header {
|
||||
flex: 0;
|
||||
text-align: center;
|
||||
line-height: 58px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
.header-index-wide {
|
||||
.header-index-left {
|
||||
.trigger {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tablet {
|
||||
// overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
|
||||
.top-nav-header-index {
|
||||
.header-index-wide {
|
||||
.ant-menu.ant-menu-horizontal {
|
||||
flex: 1 1 auto;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-nav-header-index {
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
position: relative;
|
||||
transition: background 0.3s, width 0.2s;
|
||||
|
||||
.header-index-wide {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
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: 0 1 1000px;
|
||||
display: flex;
|
||||
|
||||
.logo.top-nav-header {
|
||||
flex: 0 0 115px;
|
||||
width: 115px;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
line-height: 64px;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
img,
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区
|
||||
.layout-content {
|
||||
margin: 24px 24px 0px;
|
||||
height: 100%;
|
||||
height: 64px;
|
||||
padding: 0 12px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.topmenu {
|
||||
.page-header-index-wide {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
// drawer-sider 自定义
|
||||
.ant-drawer.drawer-sider {
|
||||
.sider {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
.ant-drawer-content {
|
||||
background-color: rgb(0, 21, 41);
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
box-shadow: none;
|
||||
|
||||
.ant-drawer-content {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单样式
|
||||
.sider {
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
position: relative;
|
||||
z-index: @ant-global-sider-zindex;
|
||||
min-height: 100vh;
|
||||
|
||||
.ant-layout-sider-children {
|
||||
overflow-y: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-fixed-sidemenu {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
height: 64px;
|
||||
overflow: hidden;
|
||||
line-height: 64px;
|
||||
background: #002140;
|
||||
transition: all 0.3s;
|
||||
|
||||
img,
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: #fff;
|
||||
box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
|
||||
|
||||
.logo {
|
||||
background: #fff;
|
||||
box-shadow: 1px 1px 0px 0px #e8e8e8;
|
||||
}
|
||||
|
||||
.ant-menu-light {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 外置的样式控制
|
||||
.user-dropdown-menu {
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown-menu-wrapper.ant-dropdown-menu {
|
||||
padding: 4px 0;
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item > .anticon:first-child,
|
||||
.ant-dropdown-menu-item > a > .anticon:first-child,
|
||||
.ant-dropdown-menu-submenu-title > .anticon:first-child .ant-dropdown-menu-submenu-title > a > .anticon:first-child {
|
||||
min-width: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-page-search-wrapper {
|
||||
.ant-form-inline {
|
||||
.ant-form-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
margin-right: 0;
|
||||
|
||||
.ant-form-item-control-wrapper {
|
||||
flex: 1 1;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .ant-form-item-label {
|
||||
line-height: 32px;
|
||||
padding-right: 8px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ant-form-item-control {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
.table-operator {
|
||||
margin-bottom: 18px;
|
||||
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
.ant-card-head {
|
||||
padding: 0 16px !important;
|
||||
|
||||
.ant-card-head-wrapper {
|
||||
.ant-card-head-title {
|
||||
padding: 12px 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form {
|
||||
.ant-form-item {
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
background: #f5f5f5;
|
||||
|
||||
& > .ant-tabs-card {
|
||||
& > .ant-tabs-content {
|
||||
margin-top: -16px;
|
||||
|
||||
& > .ant-tabs-tabpane {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& > .ant-tabs-bar {
|
||||
border-color: #fff;
|
||||
|
||||
.ant-tabs-tab {
|
||||
border: none !important;
|
||||
margin-right: 0 !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
border-color: #fff;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-comment {
|
||||
.ant-comment-actions {
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-comment-inner {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-anchor-link-title {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.bottom-control {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
border-top: 1px solid rgb(232, 232, 232);
|
||||
padding: 10px 16px;
|
||||
text-align: right;
|
||||
left: 0px;
|
||||
background: rgb(255, 255, 255);
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
|
||||
.ant-pagination-options-size-changer.ant-select {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.divider-transparent {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.custom-tab-wrapper {
|
||||
.ant-tabs-nav {
|
||||
.ant-tabs-tab {
|
||||
margin: 0 24px 0 0;
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content-wrapper {
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
margin-bottom: 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.post-thumb,
|
||||
.sheet-thumb {
|
||||
.img {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-calendar-picker {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#editor {
|
||||
.v-note-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.attach-item {
|
||||
width: 50%;
|
||||
padding-bottom: 28%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.attach-thumb,
|
||||
.photo-thumb {
|
||||
width: 100%;
|
||||
padding-bottom: 56%;
|
||||
}
|
||||
|
||||
.attach-item,
|
||||
.attach-thumb,
|
||||
.photo-thumb {
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
span {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9b9ea0;
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-card-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.meta {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
|
||||
.analysis-card-action {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
color: #000;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
font-size: 32px;
|
||||
line-height: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tree-child-tree {
|
||||
li {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.exception {
|
||||
min-height: 500px;
|
||||
height: 80%;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 150px;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.exception {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
.ant-list-item-main,
|
||||
.ant-list-item-meta-content,
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.journal-list-content,
|
||||
.comment-drawer-content {
|
||||
img {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
line-height: initial !important;
|
||||
}
|
||||
|
||||
.theme-screenshot {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
padding-bottom: 56%;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-fixed .ant-card-head {
|
||||
position: fixed;
|
||||
background: white;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
// 附件图片样式
|
||||
.attachments-group, .photos-group {
|
||||
&-item {
|
||||
padding: 0;
|
||||
height: 130px;
|
||||
|
||||
&-img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.attachments-group &-type {
|
||||
font-size: 38px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-affix {
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
.header-comment-popover {
|
||||
.ant-popover-content {
|
||||
.ant-popover-inner-content {
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: #29d;
|
||||
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
|
||||
opacity: 1;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: #29d;
|
||||
border-left-color: #29d;
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@-webkit-keyframes nprogress-spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* scroll bar style */
|
||||
*::-webkit-scrollbar-track-piece {
|
||||
background-color: #f8f8f8;
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #ddd;
|
||||
background-clip: padding-box;
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
@import '~ant-design-vue/lib/style/index';
|
||||
|
||||
button,
|
||||
html [type='button'] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
// 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;
|
|
@ -1,35 +0,0 @@
|
|||
@import './animate.less';
|
||||
|
||||
.container-wrapper {
|
||||
background: #ffffff;
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
margin: -160px 0 0 -160px;
|
||||
width: 320px;
|
||||
padding: 22px 28px 28px 28px;
|
||||
box-shadow: rgba(0, 0, 0, 0.08) 0 4px 12px;
|
||||
.tip {
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.halo-logo {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 38px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
color: #1790fe;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 88px;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -1,118 +0,0 @@
|
|||
import { AdminApiClient, Axios, HaloRestAPIClient } from '@halo-dev/admin-api'
|
||||
import store from '@/store'
|
||||
import { message, notification } from 'ant-design-vue'
|
||||
import { isObject } from './util'
|
||||
|
||||
const storedApiUrl = localStorage.getItem('apiUrl')
|
||||
|
||||
const apiUrl = storedApiUrl ? storedApiUrl : process.env.VUE_APP_API_URL
|
||||
|
||||
const haloRestApiClient = new HaloRestAPIClient({
|
||||
baseUrl: apiUrl
|
||||
})
|
||||
|
||||
const apiClient = new AdminApiClient(haloRestApiClient)
|
||||
|
||||
haloRestApiClient.interceptors.request.use(
|
||||
config => {
|
||||
const token = store.getters.token
|
||||
if (token && token.access_token) {
|
||||
config.headers['Admin-Authorization'] = token.access_token
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
let isRefreshingToken = false
|
||||
let pendingRequests = []
|
||||
|
||||
haloRestApiClient.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
async error => {
|
||||
if (Axios.isCancel(error)) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (/Network Error/.test(error.message)) {
|
||||
message.error('网络错误,请检查网络连接')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
const token = store.getters.token
|
||||
const originalRequest = error.config
|
||||
|
||||
const response = error.response
|
||||
|
||||
const data = response ? response.data : null
|
||||
|
||||
if (data) {
|
||||
if (data.status === 400) {
|
||||
const params = data.data
|
||||
|
||||
if (isObject(params)) {
|
||||
const paramMessages = Object.keys(params || {}).map(key => params[key])
|
||||
notification.error({
|
||||
message: data.message,
|
||||
description: h => {
|
||||
const errorNodes = paramMessages.map(errorDetail => {
|
||||
return h('a-alert', {
|
||||
props: {
|
||||
message: errorDetail,
|
||||
banner: true,
|
||||
showIcon: false,
|
||||
type: 'error'
|
||||
}
|
||||
})
|
||||
})
|
||||
return h('div', errorNodes)
|
||||
},
|
||||
duration: 10
|
||||
})
|
||||
} else {
|
||||
message.error(data.message)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
if (data.status === 401) {
|
||||
if (!isRefreshingToken) {
|
||||
isRefreshingToken = true
|
||||
try {
|
||||
await store.dispatch('refreshToken', token.refresh_token)
|
||||
|
||||
pendingRequests.forEach(callback => callback())
|
||||
pendingRequests = []
|
||||
|
||||
return Axios(originalRequest)
|
||||
} catch (e) {
|
||||
message.warning('当前登录状态已失效,请重新登录')
|
||||
await store.dispatch('ToggleLoginModal', true)
|
||||
return Promise.reject(e)
|
||||
} finally {
|
||||
isRefreshingToken = false
|
||||
}
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
pendingRequests.push(() => {
|
||||
resolve(Axios(originalRequest))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
message.error(data.message || '服务器错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
message.error('网络异常')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default apiClient
|
||||
|
||||
export { haloRestApiClient }
|
|
@ -1,47 +0,0 @@
|
|||
const isLight = color => {
|
||||
if (!isHex(color)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the format of the color, HEX or RGB?
|
||||
let r, g, b, hsp
|
||||
if (color.match(/^rgb/)) {
|
||||
// If HEX --> store the red, green, blue values in separate variables
|
||||
color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/)
|
||||
|
||||
r = color[1]
|
||||
g = color[2]
|
||||
b = color[3]
|
||||
} else {
|
||||
// If RGB --> Convert it to HEX: http://gist.github.com/983661
|
||||
color = +('0x' + color.slice(1).replace(color.length < 5 && /./g, '$&$&'))
|
||||
|
||||
r = color >> 16
|
||||
g = (color >> 8) & 255
|
||||
b = color & 255
|
||||
}
|
||||
|
||||
// HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
|
||||
hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b))
|
||||
|
||||
// Using the HSP value, determine whether the color is light or dark
|
||||
return hsp > 127.5
|
||||
}
|
||||
|
||||
const randomHex = () => {
|
||||
return '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
const hexRegExp = /(^#[0-9A-F])/i
|
||||
|
||||
const isHex = color => {
|
||||
return hexRegExp.test(color)
|
||||
}
|
||||
|
||||
const isRgb = color => {
|
||||
return /^rgb/.test(color)
|
||||
}
|
||||
const isHsl = color => {
|
||||
return /^hsl/.test(color)
|
||||
}
|
||||
|
||||
export { isLight, randomHex, isHex, hexRegExp, isRgb, isHsl }
|
|
@ -1,37 +0,0 @@
|
|||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
function datetimeFormat(datetime = new Date(), pattern = 'YYYY-MM-DD HH:mm') {
|
||||
return dayjs(datetime).format(pattern)
|
||||
}
|
||||
|
||||
function timeAgo(datetime) {
|
||||
const currentTime = new Date().getTime()
|
||||
const between = currentTime - datetime
|
||||
const days = Math.floor(between / (24 * 3600 * 1000))
|
||||
if (days === 0) {
|
||||
const leave1 = between % (24 * 3600 * 1000)
|
||||
const hours = Math.floor(leave1 / (3600 * 1000))
|
||||
if (hours === 0) {
|
||||
const leave2 = leave1 % (3600 * 1000)
|
||||
const minutes = Math.floor(leave2 / (60 * 1000))
|
||||
if (minutes === 0) {
|
||||
const leave3 = leave2 % (60 * 1000)
|
||||
const seconds = Math.round(leave3 / 1000)
|
||||
return seconds + ' 秒前'
|
||||
}
|
||||
return minutes + ' 分钟前'
|
||||
}
|
||||
return hours + ' 小时前'
|
||||
}
|
||||
if (days < 0) return '刚刚'
|
||||
if (days < 5) {
|
||||
return days + ' 天前'
|
||||
} else {
|
||||
return dayjs(datetime).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
}
|
||||
|
||||
export { datetimeFormat, timeAgo }
|
|
@ -1,33 +0,0 @@
|
|||
import enquireJs from 'enquire.js'
|
||||
|
||||
export const DEVICE_TYPE = {
|
||||
DESKTOP: 'desktop',
|
||||
TABLET: 'tablet',
|
||||
MOBILE: 'mobile'
|
||||
}
|
||||
|
||||
export const deviceEnquire = function (callback) {
|
||||
const matchDesktop = {
|
||||
match: () => {
|
||||
callback && callback(DEVICE_TYPE.DESKTOP)
|
||||
}
|
||||
}
|
||||
|
||||
const matchTablet = {
|
||||
match: () => {
|
||||
callback && callback(DEVICE_TYPE.TABLET)
|
||||
}
|
||||
}
|
||||
|
||||
const matchMobile = {
|
||||
match: () => {
|
||||
callback && callback(DEVICE_TYPE.MOBILE)
|
||||
}
|
||||
}
|
||||
|
||||
// screen and (max-width: 1087.99px)
|
||||
enquireJs
|
||||
.register('screen and (max-width: 576px)', matchMobile)
|
||||
.register('screen and (min-width: 576px) and (max-width: 1199px)', matchTablet)
|
||||
.register('screen and (min-width: 1200px)', matchDesktop)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
export const setDocumentTitle = function (title) {
|
||||
document.title = title
|
||||
const ua = navigator.userAgent
|
||||
// eslint-disable-next-line
|
||||
const regex = /\bMicroMessenger\/([\d\.]+)/
|
||||
if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) {
|
||||
const i = document.createElement('iframe')
|
||||
i.src = '/favicon.ico'
|
||||
i.style.display = 'none'
|
||||
i.onload = function () {
|
||||
setTimeout(function () {
|
||||
i.remove()
|
||||
}, 9)
|
||||
}
|
||||
document.body.appendChild(i)
|
||||
}
|
||||
}
|
||||
|
||||
export const domTitle = 'Halo'
|
|
@ -1,25 +0,0 @@
|
|||
export function triggerWindowResizeEvent() {
|
||||
const event = document.createEvent('HTMLEvents')
|
||||
event.initEvent('resize', true, true)
|
||||
event.eventType = 'message'
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
export function isObject(value) {
|
||||
return value && typeof value === 'object' && value.constructor === Object
|
||||
}
|
||||
|
||||
export function deepClone(source) {
|
||||
if (!source && typeof source !== 'object') {
|
||||
throw new Error('error arguments')
|
||||
}
|
||||
const targetObj = source.constructor === Array ? [] : {}
|
||||
Object.keys(source).forEach(keys => {
|
||||
if (source[keys] && typeof source[keys] === 'object') {
|
||||
targetObj[keys] = deepClone(source[keys])
|
||||
} else {
|
||||
targetObj[keys] = source[keys]
|
||||
}
|
||||
})
|
||||
return targetObj
|
||||
}
|
|
@ -1,497 +0,0 @@
|
|||
<template>
|
||||
<page-view>
|
||||
<a-row :gutter="12" align="middle" type="flex">
|
||||
<a-col :span="24" class="pb-3">
|
||||
<a-card :bodyStyle="{ padding: '16px' }" :bordered="false">
|
||||
<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="list.params.keyword" @keyup.enter="handleQuery()" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="存储位置:">
|
||||
<a-select
|
||||
v-model="list.params.attachmentType"
|
||||
:loading="types.loading"
|
||||
allowClear
|
||||
@change="handleQuery()"
|
||||
>
|
||||
<a-select-option v-for="item in types.data" :key="item" :value="item">
|
||||
{{ item | typeText }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="文件类型:">
|
||||
<a-select
|
||||
v-model="list.params.mediaType"
|
||||
:loading="mediaTypes.loading"
|
||||
allowClear
|
||||
@change="handleQuery()"
|
||||
>
|
||||
<a-select-option v-for="(item, index) in mediaTypes.data" :key="index" :value="item"
|
||||
>{{ item }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<span class="table-page-search-submitButtons">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleQuery()">查询</a-button>
|
||||
<a-button @click="handleResetParam()">重置</a-button>
|
||||
</a-space>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="mb-0 table-operator">
|
||||
<a-button icon="cloud-upload" type="primary" @click="upload.visible = true">上传</a-button>
|
||||
<a-button v-show="list.selected.length" icon="check-circle" type="primary" @click="handleSelectAll">
|
||||
全选
|
||||
</a-button>
|
||||
<a-button v-show="list.selected.length" icon="delete" type="danger" @click="handleDeleteAttachmentInBatch">
|
||||
删除
|
||||
</a-button>
|
||||
<a-button v-show="list.selected.length" icon="close" @click="list.selected = []"> 取消</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-list
|
||||
:dataSource="list.data"
|
||||
:grid="{ gutter: 6, xs: 2, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }"
|
||||
:loading="list.loading"
|
||||
class="attachments-group"
|
||||
>
|
||||
<template #renderItem="item, index">
|
||||
<a-list-item
|
||||
@mouseenter="$set(item, 'hover', true)"
|
||||
@mouseleave="$set(item, 'hover', false)"
|
||||
:key="index"
|
||||
@click="handleItemClick(item)"
|
||||
@contextmenu.prevent="handleContextMenu($event, item)"
|
||||
>
|
||||
<div
|
||||
:class="`${isItemSelect(item) ? 'border-blue-600' : 'border-slate-200'}`"
|
||||
class="border border-solid"
|
||||
>
|
||||
<div class="attach-thumb attachments-group-item">
|
||||
<span v-if="!isImage(item)" class="attachments-group-item-type">{{ item.suffix }}</span>
|
||||
<span
|
||||
v-else
|
||||
:style="`background-image:url(${encodeURI(item.thumbPath)})`"
|
||||
class="attachments-group-item-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<a-card-meta class="p-2 cursor-pointer">
|
||||
<template #description>
|
||||
<a-tooltip :title="item.name">
|
||||
<div class="truncate">{{ item.name }}</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
<a-icon
|
||||
v-show="!isItemSelect(item) && item.hover"
|
||||
type="plus-circle"
|
||||
theme="twoTone"
|
||||
class="absolute top-1 right-2 font-bold cursor-pointer transition-all"
|
||||
:style="{ fontSize: '18px', color: 'rgb(37 99 235)' }"
|
||||
@click.stop="handleSelect(item)"
|
||||
/>
|
||||
<a-icon
|
||||
v-show="isItemSelect(item)"
|
||||
type="check-circle"
|
||||
theme="twoTone"
|
||||
class="absolute top-1 right-2 font-bold cursor-pointer transition-all"
|
||||
:style="{ fontSize: '18px', color: 'rgb(37 99 235)' }"
|
||||
/>
|
||||
<a-icon
|
||||
v-show="item.hover && list.selected.length > 0"
|
||||
type="profile"
|
||||
theme="twoTone"
|
||||
class="absolute top-1 left-2 font-bold cursor-pointer transition-all"
|
||||
@click.stop="handleOpenDetail(item)"
|
||||
:style="{ fontSize: '18px' }"
|
||||
/>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<a-pagination
|
||||
:current="pagination.page"
|
||||
:defaultPageSize="pagination.size"
|
||||
:pageSizeOptions="['18', '36', '54', '72', '90', '108']"
|
||||
:total="pagination.total"
|
||||
class="pagination"
|
||||
showLessItems
|
||||
showSizeChanger
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AttachmentUploadModal :visible.sync="upload.visible" @close="onUploadClose" />
|
||||
|
||||
<AttachmentDetailModal
|
||||
:addToPhoto="true"
|
||||
:attachment="list.current"
|
||||
:visible.sync="detailVisible"
|
||||
@delete="handleListAttachments()"
|
||||
>
|
||||
<template #extraFooter>
|
||||
<a-button :disabled="selectPreviousButtonDisabled" @click="handleSelectPrevious">上一项</a-button>
|
||||
<a-button :disabled="selectNextButtonDisabled" @click="handleSelectNext">下一项</a-button>
|
||||
</template>
|
||||
</AttachmentDetailModal>
|
||||
</page-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin, mixinDevice } from '@/mixins/mixin.js'
|
||||
import { PageView } from '@/layouts'
|
||||
import apiClient from '@/utils/api-client'
|
||||
import { attachmentTypes } from '@/core/constant'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageView
|
||||
},
|
||||
mixins: [mixin, mixinDevice],
|
||||
filters: {
|
||||
typeText(type) {
|
||||
return attachmentTypes[type].text
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
list: {
|
||||
data: [],
|
||||
loading: false,
|
||||
total: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
params: {
|
||||
page: 0,
|
||||
size: 18,
|
||||
keyword: undefined,
|
||||
mediaType: undefined,
|
||||
attachmentType: undefined
|
||||
},
|
||||
selected: [],
|
||||
current: {}
|
||||
},
|
||||
|
||||
mediaTypes: {
|
||||
data: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
types: {
|
||||
data: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
upload: {
|
||||
visible: false
|
||||
},
|
||||
|
||||
detailVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isImage() {
|
||||
return function (attachment) {
|
||||
if (!attachment || !attachment.mediaType) {
|
||||
return false
|
||||
}
|
||||
return attachment.mediaType.startsWith('image')
|
||||
}
|
||||
},
|
||||
isItemSelect() {
|
||||
return function (attachment) {
|
||||
return this.list.selected.findIndex(item => item.id === attachment.id) > -1
|
||||
}
|
||||
},
|
||||
pagination() {
|
||||
return {
|
||||
page: this.list.params.page + 1,
|
||||
size: this.list.params.size,
|
||||
total: this.list.total
|
||||
}
|
||||
},
|
||||
selectPreviousButtonDisabled() {
|
||||
const index = this.list.data.findIndex(attachment => attachment.id === this.list.current.id)
|
||||
return index === 0 && !this.list.hasPrevious
|
||||
},
|
||||
selectNextButtonDisabled() {
|
||||
const index = this.list.data.findIndex(attachment => attachment.id === this.list.current.id)
|
||||
return index === this.list.data.length - 1 && !this.list.hasNext
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleListAttachments()
|
||||
this.handleListMediaTypes()
|
||||
this.handleListTypes()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* List attachments
|
||||
*/
|
||||
async handleListAttachments() {
|
||||
try {
|
||||
this.list.loading = true
|
||||
|
||||
const response = await apiClient.attachment.list(this.list.params)
|
||||
|
||||
this.list.data = response.data.content
|
||||
this.list.total = response.data.total
|
||||
this.list.hasNext = response.data.hasNext
|
||||
this.list.hasPrevious = response.data.hasPrevious
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.list.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List attachment media types
|
||||
*/
|
||||
async handleListMediaTypes() {
|
||||
try {
|
||||
this.mediaTypes.loading = true
|
||||
|
||||
const response = await apiClient.attachment.listMediaTypes()
|
||||
|
||||
this.mediaTypes.data = response.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.mediaTypes.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List attachment upload types
|
||||
*/
|
||||
async handleListTypes() {
|
||||
try {
|
||||
this.types.loading = true
|
||||
|
||||
const response = await apiClient.attachment.listTypes()
|
||||
|
||||
this.types.data = response.data
|
||||
} catch (error) {
|
||||
this.$log.error(error)
|
||||
} finally {
|
||||
this.types.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle open attachment detail modal event
|
||||
*/
|
||||
handleOpenDetail(attachment) {
|
||||
this.list.current = attachment
|
||||
this.detailVisible = true
|
||||
},
|
||||
|
||||
handleItemClick(attachment) {
|
||||
if (this.list.selected.length <= 0) {
|
||||
this.handleOpenDetail(attachment)
|
||||
return
|
||||
}
|
||||
this.isItemSelect(attachment) ? this.handleUnselect(attachment) : this.handleSelect(attachment)
|
||||
},
|
||||
|
||||
handleSelect(attachment) {
|
||||
this.list.selected = [...this.list.selected, attachment]
|
||||
},
|
||||
|
||||
handleUnselect(attachment) {
|
||||
this.list.selected = this.list.selected.filter(item => item.id !== attachment.id)
|
||||
},
|
||||
|
||||
handleSelectAll() {
|
||||
this.list.selected = this.list.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Show context menu
|
||||
*/
|
||||
handleContextMenu(event, item) {
|
||||
this.$contextmenu({
|
||||
items: [
|
||||
{
|
||||
label: `复制${this.isImage(item) ? '图片' : '文件'}链接`,
|
||||
onClick: () => {
|
||||
const text = `${encodeURI(item.path)}`
|
||||
this.$copyText(text)
|
||||
.then(message => {
|
||||
this.$log.debug('copy', message)
|
||||
this.$message.success('复制成功!')
|
||||
})
|
||||
.catch(err => {
|
||||
this.$log.debug('copy.err', err)
|
||||
this.$message.error('复制失败!')
|
||||
})
|
||||
},
|
||||
divided: true
|
||||
},
|
||||
{
|
||||
disabled: !this.isImage(item),
|
||||
label: '复制 Markdown 格式链接',
|
||||
onClick: () => {
|
||||
const text = `![${item.name}](${encodeURI(item.path)})`
|
||||
this.$copyText(text)
|
||||
.then(message => {
|
||||
this.$log.debug('copy', message)
|
||||
this.$message.success('复制成功!')
|
||||
})
|
||||
.catch(err => {
|
||||
this.$log.debug('copy.err', err)
|
||||
this.$message.error('复制失败!')
|
||||
})
|
||||
},
|
||||
divided: true
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
onClick: () => {
|
||||
this.$confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该附件?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
await apiClient.attachment.delete(item.id)
|
||||
await this.handleListAttachments()
|
||||
this.handleUnselect(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
event,
|
||||
minWidth: 210
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle page change
|
||||
*/
|
||||
handlePageChange(page = 1) {
|
||||
this.list.params.page = page - 1
|
||||
this.handleListAttachments()
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle page size change
|
||||
*/
|
||||
handlePageSizeChange(current, size) {
|
||||
this.$log.debug(`Current: ${current}, PageSize: ${size}`)
|
||||
this.list.params.page = 0
|
||||
this.list.params.size = size
|
||||
this.handleListAttachments()
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset query params
|
||||
*/
|
||||
handleResetParam() {
|
||||
this.list.params.keyword = undefined
|
||||
this.list.params.mediaType = undefined
|
||||
this.list.params.attachmentType = undefined
|
||||
this.handlePageChange()
|
||||
this.handleListMediaTypes()
|
||||
this.handleListTypes()
|
||||
},
|
||||
|
||||
/**
|
||||
* Search attachments
|
||||
*/
|
||||
handleQuery() {
|
||||
this.handlePageChange()
|
||||
},
|
||||
|
||||
onUploadClose() {
|
||||
this.handlePageChange()
|
||||
this.handleListMediaTypes()
|
||||
this.handleListTypes()
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes selected attachments
|
||||
*/
|
||||
handleDeleteAttachmentInBatch() {
|
||||
const _this = this
|
||||
if (this.list.selected.length <= 0) {
|
||||
this.$message.warn('你还未选择任何附件,请至少选择一个!')
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确定要批量删除选中的附件吗?',
|
||||
content: '一旦删除不可恢复,请谨慎操作',
|
||||
async onOk() {
|
||||
try {
|
||||
const attachmentIds = _this.list.selected.map(attachment => attachment.id)
|
||||
await apiClient.attachment.deleteInBatch(attachmentIds)
|
||||
_this.list.selected = []
|
||||
_this.$message.success('删除成功')
|
||||
} catch (e) {
|
||||
_this.$log.error('Failed to delete selected attachments', e)
|
||||
} finally {
|
||||
await _this.handleListAttachments()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Select previous attachment
|
||||
*/
|
||||
async handleSelectPrevious() {
|
||||
const index = this.list.data.findIndex(item => item.id === this.list.current.id)
|
||||
if (index > 0) {
|
||||
this.list.current = this.list.data[index - 1]
|
||||
return
|
||||
}
|
||||
if (index === 0 && this.list.hasPrevious) {
|
||||
this.list.params.page--
|
||||
await this.handleListAttachments()
|
||||
|
||||
this.list.current = this.list.data[this.list.data.length - 1]
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select next attachment
|
||||
*/
|
||||
async handleSelectNext() {
|
||||
const index = this.list.data.findIndex(item => item.id === this.list.current.id)
|
||||
if (index < this.list.data.length - 1) {
|
||||
this.list.current = this.list.data[index + 1]
|
||||
return
|
||||
}
|
||||
if (index === this.list.data.length - 1 && this.list.hasNext) {
|
||||
this.list.params.page++
|
||||
await this.handleListAttachments()
|
||||
|
||||
this.list.current = this.list.data[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue