refactor: 全面迁移 vue-class-component 至 Vue 3 Composition API
- 将所有页面组件从 class 语法重写为 <script setup> 风格 - App.vue / Login.vue / Home.vue / Welcome.vue - api: Hitokoto.vue / HitokotoAdd.vue / Music.vue / PhotoWall.vue / SourceImage.vue - system: Article.vue / Statistics.vue / SystemConfig.vue / SystemConfigAdd.vue / SystemRole.vue / SystemUser.vue - 新增 src/utils/http.ts:独立 axios 实例,含请求/响应拦截器,替代 vue-axios 插件 - baselist.ts:abstract class BaseList<T> → useBaseList<T>() 组合式函数 - types.ts:VForm 类型改用 Element Plus 原生 FormInstance - main.ts:移除 vue-axios 及内联 axios 配置,路由守卫直接引用 store - 依赖清理:移除 vue-class-component、vue-axios
This commit is contained in:
parent
1ffd56efc2
commit
96b032d262
11539
package-lock.json
generated
Normal file
11539
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,8 +15,6 @@
|
||||
"moment": "^2.29.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-axios": "^3.3.7",
|
||||
"vue-class-component": "8.0.0-rc.1",
|
||||
"vue-router": "^4.0.11",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
|
||||
10
src/App.vue
10
src/App.vue
@ -6,16 +6,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
|
||||
|
||||
@Options({
|
||||
name: 'App'
|
||||
})
|
||||
export default class App extends Vue{
|
||||
locale = zhCn
|
||||
}
|
||||
const locale = zhCn
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import url('./static/common.less');
|
||||
|
||||
51
src/main.ts
51
src/main.ts
@ -3,58 +3,18 @@ import App from './App.vue'
|
||||
import { router, filterExclude } from './router'
|
||||
import store from './store'
|
||||
|
||||
import VueAxios from 'vue-axios'
|
||||
import axios from 'axios'
|
||||
// 配置默认axios参数
|
||||
const service = axios.create({
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/el-message.css'
|
||||
import 'element-plus/theme-chalk/el-message-box.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
// 添加请求拦截器
|
||||
service.interceptors.request.use(config => {
|
||||
// 在发送请求之前添加token到请求头
|
||||
const token = mountedApp.$store.state.loginInfo.token
|
||||
if (token !== null && config.headers) {
|
||||
config.headers.token = token
|
||||
}
|
||||
return config
|
||||
}, err => {
|
||||
// 请求错误的处理
|
||||
ElMessage.error('请求超时,请稍后再试')
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
service.interceptors.response.use(res=> {
|
||||
return res.data
|
||||
}, err => {
|
||||
if (err.response.status >= 500) {
|
||||
ElMessage.error('服务器内部错误')
|
||||
} else if (err.response.status >= 400) {
|
||||
if(typeof err.response.data.message === 'string') {
|
||||
ElMessage.warning(err.response.data.message)
|
||||
} else if (Array.isArray(err.response.data.message)) {
|
||||
let message = err.response.data.message.join('<br/>')
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
if (err.response.status === 403) {
|
||||
mountedApp.$router.push('/login')
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
// 全局路由导航前置守卫
|
||||
router.beforeEach(function (to, from, next) {
|
||||
if (to.meta?.title) {
|
||||
mountedApp.$store.commit('addTab', { title: to.meta.title, path: to.path, name: to.name })
|
||||
store.commit('addTab', { title: to.meta.title, path: to.path, name: to.name })
|
||||
}
|
||||
mountedApp.$store.state.activeTab = to.path
|
||||
if(filterExclude.indexOf(to.path) !== -1 || mountedApp.$store.state.loginInfo.token) {
|
||||
store.state.activeTab = to.path
|
||||
if(filterExclude.indexOf(to.path) !== -1 || store.state.loginInfo.token) {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
@ -66,8 +26,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
const mountedApp = app.use(router)
|
||||
app.use(router)
|
||||
.use(store)
|
||||
.use(VueAxios, service)
|
||||
.directive('loading', ElLoading.directive)
|
||||
.mount('#app')
|
||||
|
||||
@ -1,65 +1,44 @@
|
||||
import { Vue } from 'vue-class-component'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { Page } from './common.dto'
|
||||
import moment from 'moment'
|
||||
|
||||
export default abstract class BaseList<T extends Page> extends Vue {
|
||||
/**
|
||||
* 表格数据加载中
|
||||
*/
|
||||
loading: boolean = false
|
||||
/**
|
||||
* 表单提交中
|
||||
*/
|
||||
modalLoading: boolean = false
|
||||
/**
|
||||
* 数据总数
|
||||
*/
|
||||
total: number = 0
|
||||
abstract search: T
|
||||
/**
|
||||
* 加载数据的实现
|
||||
*/
|
||||
abstract loadData(): Promise<void>
|
||||
/**
|
||||
* 加载数据
|
||||
* @param resetPage 是否重置页码
|
||||
*/
|
||||
loadDataBase(resetPage: boolean = false): void {
|
||||
if(resetPage) {
|
||||
this.search.pageNum = 1
|
||||
export function useBaseList<T extends Page>(searchInit: T) {
|
||||
const loading = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const search = reactive(searchInit) as T
|
||||
|
||||
let _loadData: (() => Promise<void>) | null = null
|
||||
|
||||
function setLoadData(fn: () => Promise<void>) {
|
||||
_loadData = fn
|
||||
}
|
||||
|
||||
function loadDataBase(resetPage: boolean = false): void {
|
||||
if (resetPage) {
|
||||
search.pageNum = 1
|
||||
}
|
||||
this.loadData()
|
||||
}
|
||||
/**
|
||||
* 重置搜索项
|
||||
*/
|
||||
reset(): void {
|
||||
this.search.reset()
|
||||
this.loadData()
|
||||
}
|
||||
/**
|
||||
* 页码变更
|
||||
* @param pageNum 页码
|
||||
*/
|
||||
pageChange(pageNum: number): void {
|
||||
this.search.pageNum = pageNum
|
||||
this.loadData()
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 每页数据条数变更
|
||||
* @param pageSize 每页数据条数
|
||||
*/
|
||||
pageSizeChange(pageSize: number): void {
|
||||
this.search.limit = pageSize
|
||||
this.loadData()
|
||||
function reset(): void {
|
||||
search.reset()
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期时间格式化
|
||||
* @param dateStr 日期时间
|
||||
*/
|
||||
datetimeFormat(dateStr: string): string | null {
|
||||
function pageChange(pageNum: number): void {
|
||||
search.pageNum = pageNum
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
function pageSizeChange(pageSize: number): void {
|
||||
search.limit = pageSize
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
function datetimeFormat(dateStr: string): string | null {
|
||||
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
|
||||
return { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat }
|
||||
}
|
||||
@ -1,8 +1,3 @@
|
||||
import { Vue } from 'vue-class-component'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
export type VForm = Vue & {
|
||||
validate: (callback: (valid: boolean) => void) => void
|
||||
resetValidation: () => boolean
|
||||
reset: () => void
|
||||
clearValidate: () => void
|
||||
}
|
||||
export type VForm = FormInstance
|
||||
41
src/utils/http.ts
Normal file
41
src/utils/http.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import store from '@/store'
|
||||
import { router } from '@/router'
|
||||
|
||||
const http = axios.create({
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
http.interceptors.request.use(config => {
|
||||
const token = store.state.loginInfo.token
|
||||
if (token !== null && config.headers) {
|
||||
config.headers.token = token
|
||||
}
|
||||
return config
|
||||
}, err => {
|
||||
ElMessage.error('请求超时,请稍后再试')
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
http.interceptors.response.use(res => {
|
||||
return res.data
|
||||
}, err => {
|
||||
if (err.response.status >= 500) {
|
||||
ElMessage.error('服务器内部错误')
|
||||
} else if (err.response.status >= 400) {
|
||||
if (typeof err.response.data.message === 'string') {
|
||||
ElMessage.warning(err.response.data.message)
|
||||
} else if (Array.isArray(err.response.data.message)) {
|
||||
let message = err.response.data.message.join('<br/>')
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
if (err.response.status === 403) {
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
export default http
|
||||
@ -35,9 +35,9 @@
|
||||
</el-aside>
|
||||
<el-main class="layout-main">
|
||||
<div class="layout-tabs">
|
||||
<el-tabs type="card" class="nav-tabs" v-model="$store.state.activeTab" @tab-change="openMenu" @tab-remove="removeTab">
|
||||
<el-tabs type="card" class="nav-tabs" v-model="store.state.activeTab" @tab-change="openMenu" @tab-remove="removeTab">
|
||||
<el-tab-pane label="首页" name="/"></el-tab-pane>
|
||||
<el-tab-pane v-for="tab in $store.state.tabs" :key="tab.name" :label="tab.title" :name="tab.path" closable></el-tab-pane>
|
||||
<el-tab-pane v-for="tab in store.state.tabs" :key="tab.name" :label="tab.title" :name="tab.path" closable></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="layout-content">
|
||||
@ -53,72 +53,74 @@
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import menus from '../config/menu'
|
||||
import http from '@/utils/http'
|
||||
|
||||
@Options({
|
||||
name: 'Home'
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const version = process.env.VERSION
|
||||
const currentYear = new Date().getFullYear()
|
||||
const defaultActiveMenuKey = ref<string | null>(null)
|
||||
const openMenuNames = ref<string[]>([])
|
||||
|
||||
const realname = computed((): null | string => {
|
||||
return store.state.loginInfo.userInfo
|
||||
? store.state.loginInfo.userInfo.realname : null
|
||||
})
|
||||
export default class Home extends Vue{
|
||||
public version?: string = process.env.VERSION
|
||||
currentYear = new Date().getFullYear()
|
||||
// 菜单项
|
||||
menus = menus
|
||||
defaultActiveMenuKey: string | null = null
|
||||
openMenuNames: string[] = []
|
||||
get realname(): null | string { // 当前用户的显示名称
|
||||
return this.$store.state.loginInfo.userInfo
|
||||
? this.$store.state.loginInfo.userInfo.realname : null
|
||||
const keepViews = computed((): string[] => {
|
||||
return store.state.tabs
|
||||
.filter((item: any) => item.name)
|
||||
.map((item: any) => item.name)
|
||||
})
|
||||
|
||||
// created
|
||||
defaultActiveMenuKey.value = route.path
|
||||
if (defaultActiveMenuKey.value) {
|
||||
let result = /^\/(.*)\//.exec(defaultActiveMenuKey.value)
|
||||
if (result) {
|
||||
openMenuNames.value.push(result[1])
|
||||
}
|
||||
get keepViews(): string[] {
|
||||
return this.$store.state.tabs
|
||||
.filter(item => item.name)
|
||||
.map(item => item.name)
|
||||
}
|
||||
async created(): Promise<void> {
|
||||
this.defaultActiveMenuKey = this.$route.path
|
||||
if(this.defaultActiveMenuKey) {
|
||||
let result = /^\/(.*)\//.exec(this.defaultActiveMenuKey)
|
||||
if(result) {
|
||||
this.openMenuNames.push(result[1])
|
||||
}
|
||||
}
|
||||
if(!this.$store.state.loginInfo.token) {
|
||||
this.$router.push('/login')
|
||||
return
|
||||
}
|
||||
const data = await this.$http.post<{token: string}, any>('/api/v1/common/verifyToken', {token: this.$store.state.loginInfo.token})
|
||||
if(data.status) {
|
||||
// 如果是已过期的token 服务端会签发新的token
|
||||
this.$store.commit('login', {token: data.newToken || this.$store.state.loginInfo.token, userInfo: data.userInfo})
|
||||
}
|
||||
if (!store.state.loginInfo.token) {
|
||||
router.push('/login')
|
||||
} else {
|
||||
http.post<{token: string}, any>('/api/v1/common/verifyToken', {token: store.state.loginInfo.token}).then(data => {
|
||||
if (data.status) {
|
||||
store.commit('login', {token: data.newToken || store.state.loginInfo.token, userInfo: data.userInfo})
|
||||
} else {
|
||||
this.$router.push('/login')
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function dropdownMenuCommand(command: string): void {
|
||||
switch (command) {
|
||||
case 'home':
|
||||
router.push('/')
|
||||
break
|
||||
case 'changePassword':
|
||||
// TODO
|
||||
break
|
||||
case 'logout':
|
||||
store.commit('logout')
|
||||
router.push('/login')
|
||||
break
|
||||
}
|
||||
dropdownMenuCommand(command: string): void {
|
||||
switch(command) {
|
||||
case 'home': // 返回首页
|
||||
this.$router.push('/')
|
||||
break
|
||||
case 'changePassword': // 修改密码
|
||||
// TODO
|
||||
break
|
||||
case 'logout': //注销
|
||||
this.$store.commit('logout')
|
||||
this.$router.push('/login')
|
||||
break
|
||||
}
|
||||
}
|
||||
openMenu(path: string): void {
|
||||
this.$router.push(path)
|
||||
}
|
||||
removeTab(path: string): void {
|
||||
this.$store.commit('removeTab', path)
|
||||
if (this.$store.state.activeTab === path) { // 关闭的是当前打开的页签
|
||||
const { tabs } = this.$store.state
|
||||
this.openMenu(tabs[tabs.length - 1]?.path || '/')
|
||||
}
|
||||
}
|
||||
function openMenu(path: string): void {
|
||||
router.push(path)
|
||||
}
|
||||
function removeTab(path: string): void {
|
||||
store.commit('removeTab', path)
|
||||
if (store.state.activeTab === path) {
|
||||
const { tabs } = store.state
|
||||
openMenu(tabs[tabs.length - 1]?.path || '/')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -14,52 +14,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VForm } from "../types"
|
||||
import type { VForm } from '../types'
|
||||
import http from '@/utils/http'
|
||||
|
||||
@Options({
|
||||
name: 'Login'
|
||||
})
|
||||
export default class Login extends Vue {
|
||||
userInfo: UserInfo = {
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
ruleValidate = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
created(): void {
|
||||
this.$store.commit('logout')
|
||||
this.$store.commit('clearTabs')
|
||||
localStorage.clear()
|
||||
}
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
async login() {
|
||||
(this.$refs.loginForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
const data = await this.$http.post<UserInfo, any>('/api/v1/common/login', this.userInfo)
|
||||
if(data.token) {
|
||||
this.$store.commit('login', data)
|
||||
this.$router.push('/')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
type UserInfo = {
|
||||
username: string | null,
|
||||
password: string | null
|
||||
};
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const loginForm = ref<VForm>()
|
||||
|
||||
const userInfo: UserInfo = reactive({
|
||||
username: null,
|
||||
password: null
|
||||
})
|
||||
const ruleValidate = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
|
||||
// created
|
||||
store.commit('logout')
|
||||
store.commit('clearTabs')
|
||||
localStorage.clear()
|
||||
|
||||
async function login() {
|
||||
loginForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
const data = await http.post<UserInfo, any>('/api/v1/common/login', userInfo)
|
||||
if (data.token) {
|
||||
store.commit('login', data)
|
||||
router.push('/')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.login-wrapper {
|
||||
|
||||
@ -12,16 +12,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import menus from '../config/menu'
|
||||
|
||||
@Options({
|
||||
name: 'Welcome'
|
||||
})
|
||||
export default class Welcome extends Vue {
|
||||
menus = menus
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.nav-list {
|
||||
|
||||
@ -47,8 +47,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -66,73 +66,15 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import HitokotoAdd from './HitokotoAdd.vue'
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
import HitokotoModel from '@/model/api/hitokoto'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'Hitokoto',
|
||||
components: { HitokotoAdd }
|
||||
})
|
||||
export default class Hitokoto extends BaseList<HitokotoPage> {
|
||||
search = new HitokotoPage()
|
||||
typeList: {label: string, value: string}[] = []
|
||||
hitokotoData: HitokotoModel[] = []
|
||||
formData: {[propName:string]: string | null} = {}
|
||||
addModal: boolean = false
|
||||
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<HitokotoPage, any>('/api/v1/hitokoto/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.hitokotoData = data.data
|
||||
}
|
||||
async save() {
|
||||
((this.$refs.addForm as Vue).$refs.hitokotoForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<any, any>('/api/v1/hitokoto/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
// 清空表单
|
||||
this.formData = {}
|
||||
})
|
||||
}
|
||||
deleteAll() {
|
||||
if(!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<any, any>('/api/v1/hitokoto/delete', {params:{_ids: selectedData}})
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: HitokotoModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get<never, any>('/api/v1/common/config/hitokoto_type').then(data => {
|
||||
this.typeList = data
|
||||
})
|
||||
}
|
||||
findTypeText(value: string): string | null {
|
||||
const type = this.typeList.find(item => item.value === value)
|
||||
return type ? type.label : null
|
||||
}
|
||||
}
|
||||
import http from '@/utils/http'
|
||||
|
||||
class HitokotoPage extends Page {
|
||||
content?: string
|
||||
@ -145,4 +87,62 @@ class HitokotoPage extends Page {
|
||||
this.createdAt = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new HitokotoPage())
|
||||
|
||||
const typeList = ref<{label: string, value: string}[]>([])
|
||||
const hitokotoData = ref<HitokotoModel[]>([])
|
||||
const formData = reactive<{[propName: string]: string | null}>({})
|
||||
const addModal = ref(false)
|
||||
const addForm = ref<InstanceType<typeof HitokotoAdd>>()
|
||||
|
||||
let selectedData: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<HitokotoPage, any>('/api/v1/hitokoto/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
hitokotoData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
async function save() {
|
||||
addForm.value?.hitokotoForm?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<any, any>('/api/v1/hitokoto/save', formData)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
Object.keys(formData).forEach(key => delete formData[key])
|
||||
})
|
||||
}
|
||||
function deleteAll() {
|
||||
if (!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<any, any>('/api/v1/hitokoto/delete', {params: {_ids: selectedData}})
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: HitokotoModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function findTypeText(value: string): string | null {
|
||||
const type = typeList.value.find(item => item.value === value)
|
||||
return type ? type.label : null
|
||||
}
|
||||
|
||||
// created
|
||||
loadData()
|
||||
http.get<never, any>('/api/v1/common/config/hitokoto_type').then(data => {
|
||||
typeList.value = data
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -18,26 +18,25 @@
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemConfigAdd',
|
||||
props: {
|
||||
typeList: Array,
|
||||
formData: Object
|
||||
}
|
||||
})
|
||||
export default class HitokotoAdd extends Vue {
|
||||
typeList!: {label: string, value: string}[]
|
||||
formData!: {[propName:string]: string | null}
|
||||
ruleValidate = {
|
||||
hitokoto: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
defineProps<{
|
||||
typeList: {label: string, value: string}[]
|
||||
formData: {[propName: string]: string | null}
|
||||
}>()
|
||||
|
||||
const hitokotoForm = ref<VForm>()
|
||||
|
||||
const ruleValidate = {
|
||||
hitokoto: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
|
||||
defineExpose({ hitokotoForm })
|
||||
</script>
|
||||
@ -79,8 +79,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -118,7 +118,7 @@
|
||||
action="/api/v2/music/upload"
|
||||
name="file"
|
||||
accept=".mp3,.flac"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
:auto-upload="false"
|
||||
@ -148,207 +148,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { MsgResult, Page } from '@/model/common.dto'
|
||||
import { ElUpload, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElUploadInstance, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { MusicModel, MusicLibModel, MusicLyricModel, MusicPlayerItem } from '@/model/api/music'
|
||||
import APlayer from './aplayer/vue-aplayer.vue'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
let selectedIds: string[] = []
|
||||
@Options({
|
||||
name: 'Music',
|
||||
components: { ElUpload, APlayer }
|
||||
})
|
||||
export default class Music extends BaseList<MusicPage> {
|
||||
search = new MusicPage()
|
||||
currentRow: MusicModel | null = null
|
||||
libIdSelected: string | null = null
|
||||
exts: string[] = []
|
||||
musicLibs: MusicLibModel[] = []
|
||||
musicData: MusicModel[] = []
|
||||
uploadModal: boolean = false
|
||||
modifyLyricModal: boolean = false
|
||||
lyricRuleValidate = {
|
||||
cloud_id: [
|
||||
{ required: true, message: '请输入网易云ID', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' }
|
||||
],
|
||||
lyric: [
|
||||
{ required: true, message: '请输入歌词正文', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
prettyBytes = prettyBytes
|
||||
lyricFormData: MusicLyricModel = {}
|
||||
// 是否正在播放音乐
|
||||
musicPlaying: boolean = false
|
||||
musicList: MusicPlayerItem[] = []
|
||||
currentMusic?: MusicPlayerItem
|
||||
created() {
|
||||
this.$http.get<never, any>('/api/v1/music/listLibs').then(data => {
|
||||
this.musicLibs = data
|
||||
this.loadData()
|
||||
})
|
||||
this.$http.get<never, any>('/api/v1/music/listExts').then(data => {
|
||||
this.exts = data
|
||||
})
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<MusicPage, any>('/api/v1/music/list', {params: this.search})
|
||||
selectedIds = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.musicData = data.data
|
||||
}
|
||||
dataSelect(selection: MusicModel[]) {
|
||||
selectedIds = selection.map(item => item._id)
|
||||
}
|
||||
findMusicLib(value: string): string | null {
|
||||
const musicLib = this.musicLibs.find(item => item._id === value)
|
||||
return musicLib ? musicLib.name : null
|
||||
}
|
||||
// 根据当前搜索条件播放音乐
|
||||
async playMusic() {
|
||||
try {
|
||||
const data = await this.$http.get<any, any>('/api/v1/music/list/all', {params: selectedIds.length ? {ids: selectedIds} : this.search})
|
||||
this.musicList = data.map((item: MusicModel, index: number) => {
|
||||
const musicItem: MusicPlayerItem = {
|
||||
id: index,
|
||||
title: item.title || item.name,
|
||||
artist: item.artist,
|
||||
album: item.album,
|
||||
src: `/api/v2/common/music/load/${item._id}`,
|
||||
pic: `/api/v2/common/music/album/${item._id}`,
|
||||
}
|
||||
if(item.lyric_id) {
|
||||
musicItem.lrc = `${location.origin}/api/v2/common/music/lyric/${item.lyric_id}`
|
||||
}
|
||||
return musicItem
|
||||
})
|
||||
this.currentMusic = this.musicList[0]
|
||||
this.musicPlaying = true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error('获取播放列表失败')
|
||||
}
|
||||
}
|
||||
updateLib(row: MusicModel) {
|
||||
this.currentRow = { ...row }
|
||||
row.isEditing = true
|
||||
}
|
||||
download(row: MusicModel) {
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', `/api/v2/common/music/load/${row._id}`)
|
||||
link.setAttribute('download', row.name)
|
||||
link.setAttribute('target', '_blank')
|
||||
link.click()
|
||||
}
|
||||
remove(row: MusicModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} ?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v2/music/delete', {params: {id: row._id}})
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
async updateLyric(row: MusicModel) {
|
||||
this.currentRow = { ...row }
|
||||
this.modifyLyricModal = true
|
||||
if (row.lyric_id) {
|
||||
const data = (await this.$http.get<any, any>('/api/v1/music/lyric/get', {params: {lyricId: row.lyric_id}}))
|
||||
data.cloud_id = data.cloud_id ? data.cloud_id.toString() : null
|
||||
this.lyricFormData = data
|
||||
} else {
|
||||
this.lyricFormData = {}
|
||||
}
|
||||
}
|
||||
async saveLyric() {
|
||||
(this.$refs.lyricForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<MusicLyricModel, any>(`/api/v1/music/lyric/save?musicId=${this.currentRow ? this.currentRow._id : ''}`, this.lyricFormData)
|
||||
this.modalLoading = false
|
||||
this.modifyLyricModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
// 清空表单
|
||||
this.lyricFormData = {}
|
||||
})
|
||||
}
|
||||
async saveMusicLib(row: MusicModel) {
|
||||
if (!this.currentRow) return
|
||||
const data = await this.$http.post<{id: string, libId: string}, any>('/api/v2/music/updateLib', {id: this.currentRow._id, libId: this.currentRow.lib_id})
|
||||
ElMessage.success(data.message)
|
||||
row.lib_id = this.currentRow.lib_id
|
||||
row.isEditing = false
|
||||
}
|
||||
openUploadModal() {
|
||||
this.uploadModal = true
|
||||
this.libIdSelected = null
|
||||
}
|
||||
async uploadMusic() {
|
||||
if (!this.libIdSelected) {
|
||||
ElMessage.warning('请选择歌单')
|
||||
return
|
||||
}
|
||||
// 执行上传
|
||||
(this.$refs.musicUpload as typeof ElUpload).submit()
|
||||
}
|
||||
uploadSuccess(response: MsgResult) {
|
||||
if(response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
}
|
||||
uploadError(error: Error) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
uploadModalClosed() {
|
||||
(this.$refs.musicUpload as typeof ElUpload).clearFiles()
|
||||
}
|
||||
/**
|
||||
* 创建媒体信息
|
||||
*/
|
||||
musicPlay() {
|
||||
if(!('mediaSession' in window.navigator) || !this.currentMusic) return;
|
||||
const player = <any>this.$refs.player
|
||||
const currentId = this.currentMusic.id
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.currentMusic.title,
|
||||
artist: this.currentMusic.artist,
|
||||
album: this.currentMusic.album,
|
||||
artwork: [{src: location.origin + this.currentMusic.pic}]
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('play', () => { // 播放
|
||||
player.play()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('pause', () => { // 暂停
|
||||
player.pause()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => { // 上一首
|
||||
if (currentId === 0) { // 已经是第一首
|
||||
player.switch(this.musicList.length - 1)
|
||||
} else {
|
||||
player.switch(currentId - 1)
|
||||
}
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => { // 下一首
|
||||
if (currentId === this.musicList.length - 1) { // 已经是最后一首
|
||||
player.switch(0)
|
||||
} else {
|
||||
player.switch(currentId + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
import type { VForm } from '@/types'
|
||||
import http from '@/utils/http'
|
||||
|
||||
class MusicPage extends Page {
|
||||
name?: string
|
||||
@ -367,4 +177,190 @@ class MusicPage extends Page {
|
||||
this.lib_id = []
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new MusicPage())
|
||||
|
||||
const currentRow = ref<MusicModel | null>(null)
|
||||
const libIdSelected = ref<string | null>(null)
|
||||
const exts = ref<string[]>([])
|
||||
const musicLibs = ref<MusicLibModel[]>([])
|
||||
const musicData = ref<MusicModel[]>([])
|
||||
const uploadModal = ref(false)
|
||||
const modifyLyricModal = ref(false)
|
||||
const lyricFormData = ref<MusicLyricModel>({})
|
||||
const musicPlaying = ref(false)
|
||||
const musicList = ref<MusicPlayerItem[]>([])
|
||||
const currentMusic = ref<MusicPlayerItem>()
|
||||
const lyricForm = ref<VForm>()
|
||||
const musicUpload = ref<ElUploadInstance>()
|
||||
const player = ref<any>()
|
||||
|
||||
const lyricRuleValidate = {
|
||||
cloud_id: [
|
||||
{ required: true, message: '请输入网易云ID', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' }
|
||||
],
|
||||
lyric: [
|
||||
{ required: true, message: '请输入歌词正文', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
|
||||
let selectedIds: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<MusicPage, any>('/api/v1/music/list', {params: search})
|
||||
selectedIds = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
musicData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function dataSelect(selection: MusicModel[]) {
|
||||
selectedIds = selection.map(item => item._id)
|
||||
}
|
||||
function findMusicLib(value: string): string | null {
|
||||
const musicLib = musicLibs.value.find(item => item._id === value)
|
||||
return musicLib ? musicLib.name : null
|
||||
}
|
||||
async function playMusic() {
|
||||
try {
|
||||
const data = await http.get<any, any>('/api/v1/music/list/all', {params: selectedIds.length ? {ids: selectedIds} : search})
|
||||
musicList.value = data.map((item: MusicModel, index: number) => {
|
||||
const musicItem: MusicPlayerItem = {
|
||||
id: index,
|
||||
title: item.title || item.name,
|
||||
artist: item.artist,
|
||||
album: item.album,
|
||||
src: `/api/v2/common/music/load/${item._id}`,
|
||||
pic: `/api/v2/common/music/album/${item._id}`,
|
||||
}
|
||||
if (item.lyric_id) {
|
||||
musicItem.lrc = `${location.origin}/api/v2/common/music/lyric/${item.lyric_id}`
|
||||
}
|
||||
return musicItem
|
||||
})
|
||||
currentMusic.value = musicList.value[0]
|
||||
musicPlaying.value = true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error('获取播放列表失败')
|
||||
}
|
||||
}
|
||||
function updateLib(row: MusicModel) {
|
||||
currentRow.value = { ...row }
|
||||
row.isEditing = true
|
||||
}
|
||||
function download(row: MusicModel) {
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', `/api/v2/common/music/load/${row._id}`)
|
||||
link.setAttribute('download', row.name)
|
||||
link.setAttribute('target', '_blank')
|
||||
link.click()
|
||||
}
|
||||
function remove(row: MusicModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} ?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v2/music/delete', {params: {id: row._id}})
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
async function updateLyric(row: MusicModel) {
|
||||
currentRow.value = { ...row }
|
||||
modifyLyricModal.value = true
|
||||
if (row.lyric_id) {
|
||||
const data = (await http.get<any, any>('/api/v1/music/lyric/get', {params: {lyricId: row.lyric_id}}))
|
||||
data.cloud_id = data.cloud_id ? data.cloud_id.toString() : null
|
||||
lyricFormData.value = data
|
||||
} else {
|
||||
lyricFormData.value = {}
|
||||
}
|
||||
}
|
||||
async function saveLyric() {
|
||||
lyricForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<MusicLyricModel, any>(`/api/v1/music/lyric/save?musicId=${currentRow.value ? currentRow.value._id : ''}`, lyricFormData.value)
|
||||
modalLoading.value = false
|
||||
modifyLyricModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
lyricFormData.value = {}
|
||||
})
|
||||
}
|
||||
async function saveMusicLib(row: MusicModel) {
|
||||
if (!currentRow.value) return
|
||||
const data = await http.post<{id: string, libId: string}, any>('/api/v2/music/updateLib', {id: currentRow.value._id, libId: currentRow.value.lib_id})
|
||||
ElMessage.success(data.message)
|
||||
row.lib_id = currentRow.value.lib_id
|
||||
row.isEditing = false
|
||||
}
|
||||
function openUploadModal() {
|
||||
uploadModal.value = true
|
||||
libIdSelected.value = null
|
||||
}
|
||||
async function uploadMusic() {
|
||||
if (!libIdSelected.value) {
|
||||
ElMessage.warning('请选择歌单')
|
||||
return
|
||||
}
|
||||
musicUpload.value?.submit()
|
||||
}
|
||||
function uploadSuccess(response: MsgResult) {
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
}
|
||||
function uploadError(error: Error) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
function uploadModalClosed() {
|
||||
musicUpload.value?.clearFiles()
|
||||
}
|
||||
function musicPlay() {
|
||||
if (!('mediaSession' in window.navigator) || !currentMusic.value) return
|
||||
const currentId = currentMusic.value.id
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: currentMusic.value.title,
|
||||
artist: currentMusic.value.artist,
|
||||
album: currentMusic.value.album,
|
||||
artwork: [{src: location.origin + currentMusic.value.pic}]
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
player.value.play()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
player.value.pause()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
if (currentId === 0) {
|
||||
player.value.switch(musicList.value.length - 1)
|
||||
} else {
|
||||
player.value.switch(currentId - 1)
|
||||
}
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
if (currentId === musicList.value.length - 1) {
|
||||
player.value.switch(0)
|
||||
} else {
|
||||
player.value.switch(currentId + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// created
|
||||
http.get<never, any>('/api/v1/music/listLibs').then(data => {
|
||||
musicLibs.value = data
|
||||
loadData()
|
||||
})
|
||||
http.get<never, any>('/api/v1/music/listExts').then(data => {
|
||||
exts.value = data
|
||||
})
|
||||
</script>
|
||||
@ -34,7 +34,7 @@
|
||||
action="/api/v2/photoWall/upload"
|
||||
accept="image/jpeg,image/png"
|
||||
name="image"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
@ -67,8 +67,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -77,81 +77,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, h } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { MsgResult, Page } from '@/model/common.dto'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import PhotoWallModel from '@/model/api/photowall'
|
||||
import { h } from 'vue'
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'PhotoWall'
|
||||
})
|
||||
export default class PhotoWall extends BaseList<PhotoWallPage> {
|
||||
search = new PhotoWallPage()
|
||||
allowUploadExt = ['jpg','jpeg','png']
|
||||
photowallData = []
|
||||
isUploading: boolean = false
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<PhotoWallPage, any>('/api/v1/photowall/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.photowallData = data.data
|
||||
}
|
||||
deleteAll() {
|
||||
if(!selectedData || !selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await this.$http.delete('/api/v1/photowall/delete', {params:{_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: PhotoWallModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
beforeUpload(file: File): boolean {
|
||||
if(file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
this.isUploading = true
|
||||
return true
|
||||
}
|
||||
uploadSuccess(response: MsgResult) {
|
||||
if(response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
this.isUploading = false
|
||||
}
|
||||
uploadError(error: Error) {
|
||||
this.isUploading = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
async preview(row: PhotoWallModel) {
|
||||
const previewHeight = Math.floor(row.height * (500 / row.width))
|
||||
const pictureCdn = await this.$http.get('/api/v1/common/config/picture_cdn')
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px;height:${previewHeight}px;`, src: `${pictureCdn}/${row.name}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
import http from '@/utils/http'
|
||||
|
||||
class PhotoWallPage extends Page {
|
||||
name?: string
|
||||
@ -168,4 +101,73 @@ class PhotoWallPage extends Page {
|
||||
this.heightMax = 0
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange } = useBaseList(new PhotoWallPage())
|
||||
|
||||
const allowUploadExt = ['jpg', 'jpeg', 'png']
|
||||
const photowallData = ref([])
|
||||
const isUploading = ref(false)
|
||||
|
||||
let selectedData: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<PhotoWallPage, any>('/api/v1/photowall/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
photowallData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function deleteAll() {
|
||||
if (!selectedData || !selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await http.delete('/api/v1/photowall/delete', {params: {_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: PhotoWallModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function beforeUpload(file: File): boolean {
|
||||
if (file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
isUploading.value = true
|
||||
return true
|
||||
}
|
||||
function uploadSuccess(response: MsgResult) {
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
isUploading.value = false
|
||||
}
|
||||
function uploadError(error: Error) {
|
||||
isUploading.value = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
async function preview(row: PhotoWallModel) {
|
||||
const previewHeight = Math.floor(row.height * (500 / row.width))
|
||||
const pictureCdn = await http.get('/api/v1/common/config/picture_cdn')
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px;height:${previewHeight}px;`, src: `${pictureCdn}/${row.name}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// created
|
||||
loadData()
|
||||
</script>
|
||||
@ -9,7 +9,7 @@
|
||||
action="/api/source-image/upload"
|
||||
accept="image/jpeg,image/png,image/svg+xml,image/x-icon"
|
||||
name="image"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
@ -50,8 +50,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -74,105 +74,107 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { MsgResult, Page } from '@/model/common.dto'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { SourceImageModel } from '@/model/api/source-image'
|
||||
import { h } from 'vue'
|
||||
import http from '@/utils/http'
|
||||
|
||||
const store = useStore()
|
||||
const { loading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new Page())
|
||||
|
||||
const allowUploadExt = ['jpg', 'jpeg', 'png', 'svg', 'ico']
|
||||
const sourceImageData = ref<SourceImageModel[]>([])
|
||||
const curModifyLabels = ref<string[]>([])
|
||||
const labelList = ref<string[]>([])
|
||||
const curId = ref<string | null>(null)
|
||||
const modifyModal = ref(false)
|
||||
const isUploading = ref(false)
|
||||
|
||||
function renderFunc(h: Function, option: any) {
|
||||
return h('span', null, option.label)
|
||||
}
|
||||
const labels = computed(() => {
|
||||
return labelList.value.map(item => {
|
||||
return { key: item, label: item }
|
||||
})
|
||||
})
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'SourceImage',
|
||||
})
|
||||
export default class SourceImage extends BaseList<Page> {
|
||||
prettyBytes = prettyBytes
|
||||
search = new Page()
|
||||
allowUploadExt = ['jpg','jpeg','png','svg','ico']
|
||||
sourceImageData: SourceImageModel[] = []
|
||||
curModifyLabels: string[] = []
|
||||
labelList: string[] = []
|
||||
curId: string | null = null
|
||||
modifyModal: boolean = false
|
||||
isUploading: boolean = false
|
||||
renderFunc(h: Function, option: any) {
|
||||
return h('span', null, option.label)
|
||||
}
|
||||
get labels() {
|
||||
return this.labelList.map(item => {
|
||||
return { key: item, label: item }
|
||||
})
|
||||
}
|
||||
async loadData(): Promise<void> {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<Page, any>('/api/v1/source-image/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.sourceImageData = data.data
|
||||
}
|
||||
deleteAll(): void {
|
||||
if(!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await this.$http.delete('/api/v1/source-image/delete', {params:{_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: SourceImageModel[]): void {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
beforeUpload(file: File): boolean {
|
||||
if(file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
this.isUploading = true
|
||||
return true
|
||||
}
|
||||
uploadSuccess(response: MsgResult): void {
|
||||
if(response.status) {
|
||||
ElMessage.success(response.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
this.isUploading = false
|
||||
}
|
||||
uploadError(error: Error): void {
|
||||
this.isUploading = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
preview(row: SourceImageModel): void {
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px`, src: `/api/v1/common/randomBg?id=${row._id}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
modifyTags(item: SourceImageModel): void {
|
||||
this.curModifyLabels.length = 0
|
||||
if(item.label) {
|
||||
this.curModifyLabels.push(...item.label)
|
||||
}
|
||||
this.curId = item._id
|
||||
this.modifyModal = true
|
||||
}
|
||||
async tarnsferChange(newTargetKeys: string[], direction: 'right' | 'left', moveKeys: string[]) {
|
||||
await this.$http.post('/api/v1/source-image/updateLabel', {id: this.curId, labels: newTargetKeys})
|
||||
}
|
||||
created() {
|
||||
this.$http.get<never, any>('/api/v1/common/config/image_label').then(data => {
|
||||
this.labelList.push(...data)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
loading.value = true
|
||||
const data = await http.get<Page, any>('/api/v1/source-image/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
sourceImageData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function deleteAll(): void {
|
||||
if (!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await http.delete('/api/v1/source-image/delete', {params: {_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: SourceImageModel[]): void {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function beforeUpload(file: File): boolean {
|
||||
if (file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
isUploading.value = true
|
||||
return true
|
||||
}
|
||||
function uploadSuccess(response: MsgResult): void {
|
||||
if (response.status) {
|
||||
ElMessage.success(response.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
isUploading.value = false
|
||||
}
|
||||
function uploadError(error: Error): void {
|
||||
isUploading.value = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
function preview(row: SourceImageModel): void {
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px`, src: `/api/v1/common/randomBg?id=${row._id}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
function modifyTags(item: SourceImageModel): void {
|
||||
curModifyLabels.value.length = 0
|
||||
if (item.label) {
|
||||
curModifyLabels.value.push(...item.label)
|
||||
}
|
||||
curId.value = item._id
|
||||
modifyModal.value = true
|
||||
}
|
||||
async function tarnsferChange(newTargetKeys: string[], direction: 'right' | 'left', moveKeys: string[]) {
|
||||
await http.post('/api/v1/source-image/updateLabel', {id: curId.value, labels: newTargetKeys})
|
||||
}
|
||||
|
||||
// created
|
||||
http.get<never, any>('/api/v1/common/config/image_label').then(data => {
|
||||
labelList.value.push(...data)
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
@ -38,7 +38,7 @@
|
||||
action="/api/system/deployBlog"
|
||||
accept="application/zip"
|
||||
name="blogZip"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
@ -92,8 +92,8 @@
|
||||
</el-table>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -111,129 +111,16 @@
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import hyperdown from 'hyperdown'
|
||||
import { ArticleModel, TreeNodeData, TreeNodeSource } from '@/model/system/article'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Node from 'element-plus/lib/components/tree/src/model/node'
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page, MsgResult } from '@/model/common.dto'
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'Article'
|
||||
})
|
||||
export default class Article extends BaseList<ArticlePage> {
|
||||
search = new ArticlePage()
|
||||
articleData: ArticleModel[] = []
|
||||
tags: string[] = [] // 所有标签
|
||||
categories: string[] = [] // 所有分类
|
||||
markdownPreview :{ // markdown内容预览
|
||||
show: boolean,
|
||||
title: string | null,
|
||||
content: string | null
|
||||
} = {
|
||||
show: false,
|
||||
title: null,
|
||||
content: null
|
||||
}
|
||||
isUploading: boolean = false
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<ArticlePage, any>('/api/v1/article/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.articleData = data.data
|
||||
}
|
||||
splitWord() {
|
||||
if(!selectedData.length) {
|
||||
ElMessage.warning('请选择要执行分词的文章')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认对选中的${selectedData.length}篇文章执行分词处理?`, '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await this.$http.put<{_ids: string[]}, any>('/api/v1/article/splitWord', {_ids: selectedData})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
pullArticles() {
|
||||
ElMessageBox.confirm('确认拉取全部文章?', '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await this.$http.put<never, any>('/api/v1/article/pull')
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: ArticleModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
beforeUpload(file: File): boolean {
|
||||
this.isUploading = true
|
||||
return true
|
||||
}
|
||||
uploadSuccess(response: MsgResult) {
|
||||
if(response.status) {
|
||||
ElMessage.success(response.message)
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
this.isUploading = false
|
||||
}
|
||||
uploadError(error: Error) {
|
||||
this.isUploading = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
readonly treeProps = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
}
|
||||
async loadTreeData(node: Node, resolve: Function) {
|
||||
const childItems: TreeNodeSource[] = await this.$http.get('/api/v1/article/tree', {params:{deep: node.level, parent: node.data.name}})
|
||||
resolve(childItems.map((childItem): TreeNodeData => {
|
||||
const treeNode: TreeNodeData = {
|
||||
name: childItem._id,
|
||||
title: childItem.article_id ? childItem._id : `${childItem._id}(${childItem.cnt})`,
|
||||
id: childItem.article_id || childItem._id,
|
||||
isLeaf: !!childItem.article_id
|
||||
}
|
||||
return treeNode
|
||||
}))
|
||||
}
|
||||
/**
|
||||
* 树节点选中事件
|
||||
* @param selectNodes 当前已选中的节点(适用于带复选框的)
|
||||
* @param curNode 本次选中的节点
|
||||
*/
|
||||
async articlePreview(node: TreeNodeData) {
|
||||
if(!node.isLeaf) return
|
||||
// 加载文章markdown预览
|
||||
const mdText = await this.$http.get<never, any>('/api/v1/article/markdown', {params:{id: node.id}})
|
||||
this.markdownPreview.show = true
|
||||
const markdownHtml = new hyperdown().makeHtml(mdText)
|
||||
this.markdownPreview.content = markdownHtml.replace(/(?<=<pre><code[^>]*?>)[\s\S]*?(?=<\/code><\/pre>)/gi, content => {
|
||||
return content.replace(/_&/g, ' ').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||||
})
|
||||
this.markdownPreview.title = node.name
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get<never, any>('/api/v1/article/listCategories').then(data => {
|
||||
this.categories = data
|
||||
})
|
||||
this.$http.get<never, any>('/api/v1/article/listTags').then(data => {
|
||||
this.tags = data
|
||||
})
|
||||
}
|
||||
}
|
||||
import http from '@/utils/http'
|
||||
|
||||
class ArticlePage extends Page {
|
||||
title?: string
|
||||
@ -250,4 +137,115 @@ class ArticlePage extends Page {
|
||||
this.isSplited = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new ArticlePage())
|
||||
|
||||
const articleData = ref<ArticleModel[]>([])
|
||||
const tags = ref<string[]>([])
|
||||
const categories = ref<string[]>([])
|
||||
const markdownPreview = reactive<{
|
||||
show: boolean
|
||||
title: string | null
|
||||
content: string | null
|
||||
}>({
|
||||
show: false,
|
||||
title: null,
|
||||
content: null
|
||||
})
|
||||
const isUploading = ref(false)
|
||||
|
||||
let selectedData: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<ArticlePage, any>('/api/v1/article/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
articleData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function splitWord() {
|
||||
if (!selectedData.length) {
|
||||
ElMessage.warning('请选择要执行分词的文章')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认对选中的${selectedData.length}篇文章执行分词处理?`, '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await http.put<{_ids: string[]}, any>('/api/v1/article/splitWord', {_ids: selectedData})
|
||||
if (data.status) {
|
||||
ElMessage.success(data.message)
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
function pullArticles() {
|
||||
ElMessageBox.confirm('确认拉取全部文章?', '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await http.put<never, any>('/api/v1/article/pull')
|
||||
if (data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: ArticleModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function beforeUpload(file: File): boolean {
|
||||
isUploading.value = true
|
||||
return true
|
||||
}
|
||||
function uploadSuccess(response: MsgResult) {
|
||||
if (response.status) {
|
||||
ElMessage.success(response.message)
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
isUploading.value = false
|
||||
}
|
||||
function uploadError(error: Error) {
|
||||
isUploading.value = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
|
||||
const treeProps = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
}
|
||||
async function loadTreeData(node: Node, resolve: Function) {
|
||||
const childItems: TreeNodeSource[] = await http.get('/api/v1/article/tree', {params: {deep: node.level, parent: node.data.name}})
|
||||
resolve(childItems.map((childItem): TreeNodeData => {
|
||||
const treeNode: TreeNodeData = {
|
||||
name: childItem._id,
|
||||
title: childItem.article_id ? childItem._id : `${childItem._id}(${childItem.cnt})`,
|
||||
id: childItem.article_id || childItem._id,
|
||||
isLeaf: !!childItem.article_id
|
||||
}
|
||||
return treeNode
|
||||
}))
|
||||
}
|
||||
async function articlePreview(node: TreeNodeData) {
|
||||
if (!node.isLeaf) return
|
||||
const mdText = await http.get<never, any>('/api/v1/article/markdown', {params: {id: node.id}})
|
||||
markdownPreview.show = true
|
||||
const markdownHtml = new hyperdown().makeHtml(mdText)
|
||||
markdownPreview.content = markdownHtml.replace(/(?<=<pre><code[^>]*?>)[\s\S]*?(?=<\/code><\/pre>)/gi, (content: string) => {
|
||||
return content.replace(/_&/g, ' ').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||||
})
|
||||
markdownPreview.title = node.name
|
||||
}
|
||||
|
||||
// created
|
||||
loadData()
|
||||
http.get<never, any>('/api/v1/article/listCategories').then(data => {
|
||||
categories.value = data
|
||||
})
|
||||
http.get<never, any>('/api/v1/article/listTags').then(data => {
|
||||
tags.value = data
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -7,188 +7,189 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import http from '@/utils/http'
|
||||
|
||||
@Options({
|
||||
name: 'Statistics'
|
||||
})
|
||||
export default class Statistics extends Vue {
|
||||
categoriesChartLoading: boolean = false
|
||||
publishDatesChartLoading: boolean = false
|
||||
timelineWordsChartLoading: boolean = false
|
||||
async mounted() {
|
||||
this.categoriesChartLoading = true
|
||||
this.publishDatesChartLoading = true
|
||||
this.timelineWordsChartLoading = true
|
||||
const categoriesChart = ref<HTMLElement>()
|
||||
const publishDatesChart = ref<HTMLElement>()
|
||||
const timelineWordsChart = ref<HTMLElement>()
|
||||
|
||||
const articleData = await this.$http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params:{type:'normal'}})
|
||||
this.categoriesChartOption.legend.data = articleData.categories.map((item: any) => item._id)
|
||||
this.categoriesChartOption.series.data = articleData.categories.map((item: any) => {
|
||||
return {name: item._id, value: item.cnt}
|
||||
})
|
||||
this.publishDatesChartOption.xAxis.data = articleData.publishDates.map((item: any) => item._id)
|
||||
this.publishDatesChartOption.series.data = articleData.publishDates.map((item: any) => item.cnt)
|
||||
const categoriesChartLoading = ref(false)
|
||||
const publishDatesChartLoading = ref(false)
|
||||
const timelineWordsChartLoading = ref(false)
|
||||
|
||||
const categoriesChart = echarts.init(this.$refs.categoriesChart as HTMLElement)
|
||||
categoriesChart.setOption(this.categoriesChartOption)
|
||||
const publishDatesChart = echarts.init(this.$refs.publishDatesChart as HTMLElement)
|
||||
publishDatesChart.setOption(this.publishDatesChartOption)
|
||||
this.categoriesChartLoading = false
|
||||
this.publishDatesChartLoading = false
|
||||
|
||||
const timelineData = await this.$http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params:{type:'timelineWords'}})
|
||||
this.timelineWordsChartOption.timeline.data = timelineData.timelineWords.map((item: any) => item._id)
|
||||
timelineData.timelineWords.forEach((item: any) => {
|
||||
this.timelineWordsChartOption.options.push({
|
||||
title: { text: `${item._id}年发布的文章` },
|
||||
xAxis: { data: item.keys.map((keyItem: any) => keyItem.key) },
|
||||
series: { data: item.keys.map((keyItem: any) => keyItem.total) }
|
||||
})
|
||||
})
|
||||
const timelineWordsChart = echarts.init(this.$refs.timelineWordsChart as HTMLElement)
|
||||
timelineWordsChart.setOption(this.timelineWordsChartOption)
|
||||
this.timelineWordsChartLoading = false
|
||||
const categoriesChartOption: any = {
|
||||
title: {
|
||||
text: '文章分类',
|
||||
x: 'center',
|
||||
top: 10
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: "{a} <br/>{b} : {c} ({d}%)"
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 50,
|
||||
bottom: 20,
|
||||
data: [],
|
||||
},
|
||||
series: {
|
||||
name: '类别',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#FFF',
|
||||
borderWidth: 1
|
||||
},
|
||||
data: []
|
||||
}
|
||||
|
||||
categoriesChartOption = {
|
||||
title : {
|
||||
text: '文章分类',
|
||||
x: 'center',
|
||||
top: 10
|
||||
}
|
||||
const publishDatesChartOption: any = {
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '文章发布时间',
|
||||
top: 10
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
animation: false,
|
||||
label: {
|
||||
backgroundColor: '#ccc',
|
||||
borderColor: '#aaa',
|
||||
borderWidth: 1,
|
||||
shadowBlur: 0,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
color: '#222'
|
||||
}
|
||||
},
|
||||
tooltip : {
|
||||
trigger: 'item',
|
||||
formatter: "{a} <br/>{b} : {c} ({d}%)"
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 50,
|
||||
bottom: 20,
|
||||
data: [],
|
||||
},
|
||||
series: {
|
||||
name: '类别',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#FFF',
|
||||
borderWidth: 1
|
||||
},
|
||||
data: []
|
||||
},
|
||||
xAxis: {
|
||||
name: '发布时间',
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
name: '文章数量',
|
||||
type: 'value',
|
||||
max: function (value: {max: number}) {
|
||||
return value.max + 10
|
||||
}
|
||||
}
|
||||
publishDatesChartOption = {
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '文章发布时间',
|
||||
top: 10
|
||||
},
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
}, {
|
||||
start: 0,
|
||||
end: 10
|
||||
}],
|
||||
series: {
|
||||
name: '文章数量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
sampling: 'average',
|
||||
itemStyle: {
|
||||
color: 'rgb(255, 70, 131)'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
animation: false,
|
||||
label: {
|
||||
backgroundColor: '#ccc',
|
||||
borderColor: '#aaa',
|
||||
borderWidth: 1,
|
||||
shadowBlur: 0,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
color: '#222'
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 158, 68)'
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgb(255, 70, 131)'
|
||||
}
|
||||
},
|
||||
])
|
||||
},
|
||||
xAxis: {
|
||||
name: '发布时间',
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
name: '文章数量',
|
||||
type: 'value',
|
||||
max: function(value: {max: number}) {
|
||||
return value.max + 10
|
||||
}
|
||||
},
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
},{
|
||||
start: 0,
|
||||
end: 10
|
||||
}],
|
||||
series: {
|
||||
name: '文章数量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
sampling: 'average',
|
||||
itemStyle: {
|
||||
color: 'rgb(255, 70, 131)'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 158, 68)'
|
||||
},{
|
||||
offset: 1,
|
||||
color: 'rgb(255, 70, 131)'
|
||||
}
|
||||
])
|
||||
},
|
||||
data: []
|
||||
}
|
||||
data: []
|
||||
}
|
||||
timelineWordsChartOption: any = {
|
||||
options: [],
|
||||
timeline: {
|
||||
axisType: 'category',
|
||||
autoPlay: false,
|
||||
playInterval: 1000,
|
||||
data: [],
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
subtext: '数据来自文章分词结果'
|
||||
},
|
||||
calculable : true,
|
||||
grid: {
|
||||
top: 80,
|
||||
bottom: 80
|
||||
},
|
||||
xAxis: {
|
||||
name: '高频词汇',
|
||||
type: 'category',
|
||||
splitLine: {show: false}
|
||||
},
|
||||
yAxis: {
|
||||
name: '词汇出现次数',
|
||||
type: 'value'
|
||||
},
|
||||
tooltip : {
|
||||
trigger: 'axis',
|
||||
axisPointer : { // 坐标轴指示器,坐标轴触发有效
|
||||
type : 'shadow' // 默认为直线,可选为:'line' | 'shadow'
|
||||
}
|
||||
},
|
||||
series: {
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 5
|
||||
}
|
||||
}
|
||||
const timelineWordsChartOption: any = {
|
||||
options: [],
|
||||
timeline: {
|
||||
axisType: 'category',
|
||||
autoPlay: false,
|
||||
playInterval: 1000,
|
||||
data: [],
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
subtext: '数据来自文章分词结果'
|
||||
},
|
||||
calculable: true,
|
||||
grid: {
|
||||
top: 80,
|
||||
bottom: 80
|
||||
},
|
||||
xAxis: {
|
||||
name: '高频词汇',
|
||||
type: 'category',
|
||||
splitLine: {show: false}
|
||||
},
|
||||
yAxis: {
|
||||
name: '词汇出现次数',
|
||||
type: 'value'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
series: {
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
categoriesChartLoading.value = true
|
||||
publishDatesChartLoading.value = true
|
||||
timelineWordsChartLoading.value = true
|
||||
|
||||
const articleData = await http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params: {type: 'normal'}})
|
||||
categoriesChartOption.legend.data = articleData.categories.map((item: any) => item._id)
|
||||
categoriesChartOption.series.data = articleData.categories.map((item: any) => {
|
||||
return {name: item._id, value: item.cnt}
|
||||
})
|
||||
publishDatesChartOption.xAxis.data = articleData.publishDates.map((item: any) => item._id)
|
||||
publishDatesChartOption.series.data = articleData.publishDates.map((item: any) => item.cnt)
|
||||
|
||||
const categoriesChartInstance = echarts.init(categoriesChart.value as HTMLElement)
|
||||
categoriesChartInstance.setOption(categoriesChartOption)
|
||||
const publishDatesChartInstance = echarts.init(publishDatesChart.value as HTMLElement)
|
||||
publishDatesChartInstance.setOption(publishDatesChartOption)
|
||||
categoriesChartLoading.value = false
|
||||
publishDatesChartLoading.value = false
|
||||
|
||||
const timelineData = await http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params: {type: 'timelineWords'}})
|
||||
timelineWordsChartOption.timeline.data = timelineData.timelineWords.map((item: any) => item._id)
|
||||
timelineData.timelineWords.forEach((item: any) => {
|
||||
timelineWordsChartOption.options.push({
|
||||
title: {text: `${item._id}年发布的文章`},
|
||||
xAxis: {data: item.keys.map((keyItem: any) => keyItem.key)},
|
||||
series: {data: item.keys.map((keyItem: any) => keyItem.total)}
|
||||
})
|
||||
})
|
||||
const timelineWordsChartInstance = echarts.init(timelineWordsChart.value as HTMLElement)
|
||||
timelineWordsChartInstance.setOption(timelineWordsChartOption)
|
||||
timelineWordsChartLoading.value = false
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.echarts-container {
|
||||
|
||||
@ -55,95 +55,93 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import moment from 'moment'
|
||||
import http from '@/utils/http'
|
||||
import SystemConfigAdd from './SystemConfigAdd.vue'
|
||||
import { SystemConfigModel } from '@/model/system/system-config'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemConfig',
|
||||
components: { SystemConfigAdd }
|
||||
const modalLoading = ref(false)
|
||||
const loading = ref(false)
|
||||
const search = ref<{name?: string}>({})
|
||||
const systemConfigData = ref<SystemConfigModel[]>([])
|
||||
const addModal = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formData = ref<SystemConfigModel>({
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
is_public: false
|
||||
})
|
||||
export default class SystemConfig extends Vue {
|
||||
modalLoading: boolean = false
|
||||
loading: boolean = false
|
||||
search: {name?:string} = {}
|
||||
systemConfigData: SystemConfigModel[] = []
|
||||
addModal: boolean = false
|
||||
modalTitle: string = ''
|
||||
formData: SystemConfigModel = {
|
||||
|
||||
const addForm = ref<InstanceType<typeof SystemConfigAdd>>()
|
||||
|
||||
function reset() {
|
||||
search.value = {}
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
systemConfigData.value = await http.get('/api/v1/system/config/list', {params: search.value})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function add() {
|
||||
formData.value = {
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
is_public: false
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.search = {}
|
||||
this.loadData()
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
this.systemConfigData = await this.$http.get('/api/v1/system/config/list', {params:this.search})
|
||||
this.loading = false
|
||||
}
|
||||
add() {
|
||||
// 清空表单
|
||||
this.formData = {
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
is_public: false
|
||||
}
|
||||
this.modalTitle = '新增配置项'
|
||||
this.addModal = true
|
||||
}
|
||||
update(row: SystemConfigModel) {
|
||||
const formData = Object.assign({}, row)
|
||||
formData.value = JSON.stringify(formData.value, null, ' ')
|
||||
this.formData = formData
|
||||
this.modalTitle = '修改配置项'
|
||||
this.addModal = true
|
||||
}
|
||||
async save() {
|
||||
((this.$refs.addForm as Vue).$refs.configForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<SystemConfigModel, any>('/api/v1/system/config/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
remove(row: SystemConfigModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 配置项?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v1/system/config/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
clearValidate() {
|
||||
this.$nextTick(() => {
|
||||
((this.$refs.addForm as Vue).$refs.configForm as VForm).clearValidate()
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 日期时间格式化
|
||||
* @param dateStr 日期时间
|
||||
*/
|
||||
datetimeFormat(dateStr: string) {
|
||||
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
modalTitle.value = '新增配置项'
|
||||
addModal.value = true
|
||||
}
|
||||
|
||||
function update(row: SystemConfigModel) {
|
||||
const data = Object.assign({}, row)
|
||||
data.value = JSON.stringify(data.value, null, ' ')
|
||||
formData.value = data
|
||||
modalTitle.value = '修改配置项'
|
||||
addModal.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
addForm.value?.configForm?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<SystemConfigModel, any>('/api/v1/system/config/save', formData.value)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
function remove(row: SystemConfigModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 配置项?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/config/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function clearValidate() {
|
||||
nextTick(() => {
|
||||
addForm.value?.configForm?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
function datetimeFormat(dateStr: string) {
|
||||
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
@ -19,46 +19,45 @@
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import http from '@/utils/http'
|
||||
import { SystemConfigModel } from '@/model/system/system-config'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemConfigAdd',
|
||||
props: {
|
||||
formData: Object
|
||||
}
|
||||
})
|
||||
export default class SystenConfigAdd extends Vue {
|
||||
formData!: SystemConfigModel
|
||||
get ruleValidate() {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置项名称', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
this.$http.get<any, any>('/api/v1/system/config/exists', {params: {name: value, id: this.formData._id}}).then(data => {
|
||||
if(data.data.exists) {
|
||||
callback(new Error('配置项名称已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入配置项值', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
callback()
|
||||
} catch (e) {
|
||||
callback(new Error('值不符合JSON字符串格式'))
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
const props = defineProps<{
|
||||
formData: SystemConfigModel
|
||||
}>()
|
||||
|
||||
const configForm = ref<VForm>()
|
||||
|
||||
const ruleValidate = computed(() => ({
|
||||
name: [
|
||||
{ required: true, message: '请输入配置项名称', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
http.get<any, any>('/api/v1/system/config/exists', {params: {name: value, id: props.formData._id}}).then(data => {
|
||||
if(data.data.exists) {
|
||||
callback(new Error('配置项名称已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入配置项值', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
callback()
|
||||
} catch (e) {
|
||||
callback(new Error('值不符合JSON字符串格式'))
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
}))
|
||||
|
||||
defineExpose({ configForm })
|
||||
</script>
|
||||
@ -40,8 +40,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -91,122 +91,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
import http from '@/utils/http'
|
||||
import { SystemRoleModel } from '@/model/system/system-role'
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemRole',
|
||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption }
|
||||
})
|
||||
export default class SystemRole extends BaseList<SystemRolePage> {
|
||||
ruleValidate = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
systemRoleData: SystemRoleModel[] = []
|
||||
search = new SystemRolePage()
|
||||
addModal: boolean = false
|
||||
modalTitle: string | null = null
|
||||
uri: {
|
||||
include: string | null,
|
||||
exclude: string | null
|
||||
} = {
|
||||
include: null,
|
||||
exclude: null
|
||||
}
|
||||
formData: SystemRoleModel = {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<{params: SystemRolePage}, any>('/api/v1/system/role/list', {params:this.search})
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.systemRoleData = data.data
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
add() {
|
||||
// 清空表单
|
||||
this.uri.include = null
|
||||
this.uri.exclude = null
|
||||
this.formData = {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
}
|
||||
this.modalTitle = '新增角色'
|
||||
this.addModal = true
|
||||
this.clearValidate()
|
||||
}
|
||||
addUri(fieldName: 'include_uri' | 'exclude_uri', uri: string | null) {
|
||||
if(!uri) return
|
||||
if(this.formData[fieldName].indexOf(uri) === -1) {
|
||||
this.formData[fieldName].push(uri)
|
||||
}
|
||||
}
|
||||
removeUri(fieldName: 'include_uri' | 'exclude_uri', uri: string) {
|
||||
let index = this.formData[fieldName].indexOf(uri)
|
||||
if(index !== -1) {
|
||||
this.formData[fieldName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
update(row: SystemRoleModel) {
|
||||
this.uri.include = null
|
||||
this.uri.exclude = null
|
||||
this.formData._id = row._id
|
||||
this.formData.name = row.name
|
||||
this.formData.description = row.description
|
||||
this.formData.methods = row.methods
|
||||
this.formData.include_uri = row.include_uri
|
||||
this.formData.exclude_uri = row.exclude_uri
|
||||
this.modalTitle = '修改角色'
|
||||
this.addModal = true
|
||||
this.clearValidate()
|
||||
}
|
||||
remove(row: SystemRoleModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 角色?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v1/system/role/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
async save() {
|
||||
(this.$refs.roleForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<SystemRoleModel, any>('/api/v1/system/role/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
clearValidate() {
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.roleForm as VForm).clearValidate()
|
||||
})
|
||||
}
|
||||
}
|
||||
const store = useStore()
|
||||
|
||||
class SystemRolePage extends Page {
|
||||
name?: string
|
||||
@ -215,4 +110,118 @@ class SystemRolePage extends Page {
|
||||
this.name = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new SystemRolePage())
|
||||
|
||||
const ruleValidate = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
const systemRoleData = ref<SystemRoleModel[]>([])
|
||||
const addModal = ref(false)
|
||||
const modalTitle = ref<string | null>(null)
|
||||
const uri = reactive<{
|
||||
include: string | null,
|
||||
exclude: string | null
|
||||
}>({
|
||||
include: null,
|
||||
exclude: null
|
||||
})
|
||||
const formData = reactive<SystemRoleModel>({
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
})
|
||||
const roleForm = ref<VForm>()
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<{params: SystemRolePage}, any>('/api/v1/system/role/list', {params: search})
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
systemRoleData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function add() {
|
||||
uri.include = null
|
||||
uri.exclude = null
|
||||
Object.assign(formData, {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
})
|
||||
modalTitle.value = '新增角色'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
function addUri(fieldName: 'include_uri' | 'exclude_uri', uriValue: string | null) {
|
||||
if(!uriValue) return
|
||||
if(formData[fieldName].indexOf(uriValue) === -1) {
|
||||
formData[fieldName].push(uriValue)
|
||||
}
|
||||
}
|
||||
|
||||
function removeUri(fieldName: 'include_uri' | 'exclude_uri', uriValue: string) {
|
||||
let index = formData[fieldName].indexOf(uriValue)
|
||||
if(index !== -1) {
|
||||
formData[fieldName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function update(row: SystemRoleModel) {
|
||||
uri.include = null
|
||||
uri.exclude = null
|
||||
Object.assign(formData, {
|
||||
_id: row._id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
methods: row.methods,
|
||||
include_uri: row.include_uri,
|
||||
exclude_uri: row.exclude_uri
|
||||
})
|
||||
modalTitle.value = '修改角色'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
function remove(row: SystemRoleModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 角色?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/role/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
roleForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<SystemRoleModel, any>('/api/v1/system/role/save', formData)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
function clearValidate() {
|
||||
nextTick(() => {
|
||||
roleForm.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
@ -36,8 +36,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -70,115 +70,18 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
import http from '@/utils/http'
|
||||
import { SystemUserModel } from '@/model/system/system-user'
|
||||
import { SystemRoleModel } from '@/model/system/system-role'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemUser'
|
||||
})
|
||||
export default class SystemUser extends BaseList<SystemUserPage> {
|
||||
get ruleValidate() {
|
||||
return {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ validator: async (rule: object, value: string, callback: Function) => {
|
||||
const data = await this.$http.get<any, any>('/api/v1/system/user/exists', {params: {username: value, id: this.formData._id}})
|
||||
if(data.data.exists) {
|
||||
callback(new Error('用户名已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 16, message: '密码长度8~16位', trigger: 'blur' },
|
||||
{ pattern: /^(?![\d]+$)(?![a-zA-Z]+$)(?![-=+_.,]+$)[\da-zA-Z-=+_.,]{8,16}$/, message: '密码由字母、数字、特殊字符中的任意两种组成', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
}
|
||||
search = new SystemUserPage()
|
||||
systemUserData: SystemUserModel[] = []
|
||||
roles: SystemRoleModel[] = []
|
||||
addModal: boolean = false
|
||||
modalTitle: string | null = null
|
||||
formData: SystemUserModel = {
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<{params: SystemUserPage}, any>('/api/v1/system/user/list', {params:this.search})
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.systemUserData = data.data
|
||||
}
|
||||
add() {
|
||||
// 清空表单
|
||||
this.formData = {
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
}
|
||||
this.modalTitle = '新增用户'
|
||||
this.addModal = true
|
||||
this.clearValidate()
|
||||
}
|
||||
update(row: SystemUserModel) {
|
||||
this.formData._id = row._id
|
||||
this.formData.username = row.username
|
||||
this.formData.realname = row.realname
|
||||
this.formData.role_ids = row.role_ids
|
||||
this.modalTitle = '修改用户'
|
||||
this.addModal = true
|
||||
this.clearValidate()
|
||||
}
|
||||
async save() {
|
||||
(this.$refs.userForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<SystemUserModel, any>('/api/v1/system/user/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
remove(row: SystemUserModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.username} 用户?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v1/system/user/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get<never, any>('/api/v1/system/role/listAll').then(data => {
|
||||
this.roles = data
|
||||
})
|
||||
}
|
||||
clearValidate() {
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.userForm as VForm).clearValidate()
|
||||
})
|
||||
}
|
||||
}
|
||||
const store = useStore()
|
||||
|
||||
class SystemUserPage extends Page {
|
||||
username?: string
|
||||
@ -187,4 +90,108 @@ class SystemUserPage extends Page {
|
||||
this.username = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new SystemUserPage())
|
||||
|
||||
const systemUserData = ref<SystemUserModel[]>([])
|
||||
const roles = ref<SystemRoleModel[]>([])
|
||||
const addModal = ref(false)
|
||||
const modalTitle = ref<string | null>(null)
|
||||
const formData = reactive<SystemUserModel>({
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
})
|
||||
const userForm = ref<VForm>()
|
||||
|
||||
const ruleValidate = computed(() => ({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ validator: async (rule: object, value: string, callback: Function) => {
|
||||
const data = await http.get<any, any>('/api/v1/system/user/exists', {params: {username: value, id: formData._id}})
|
||||
if(data.data.exists) {
|
||||
callback(new Error('用户名已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 16, message: '密码长度8~16位', trigger: 'blur' },
|
||||
{ pattern: /^(?![\d]+$)(?![a-zA-Z]+$)(?![-=+_.,]+$)[\da-zA-Z-=+_.,]{8,16}$/, message: '密码由字母、数字、特殊字符中的任意两种组成', trigger: 'blur' }
|
||||
],
|
||||
}))
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<{params: SystemUserPage}, any>('/api/v1/system/user/list', {params: search})
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
systemUserData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function add() {
|
||||
Object.assign(formData, {
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
})
|
||||
modalTitle.value = '新增用户'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
function update(row: SystemUserModel) {
|
||||
Object.assign(formData, {
|
||||
_id: row._id,
|
||||
username: row.username,
|
||||
realname: row.realname,
|
||||
role_ids: row.role_ids
|
||||
})
|
||||
modalTitle.value = '修改用户'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
userForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<SystemUserModel, any>('/api/v1/system/user/save', formData)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
function remove(row: SystemUserModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.username} 用户?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/user/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function clearValidate() {
|
||||
nextTick(() => {
|
||||
userForm.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
loadData()
|
||||
http.get<never, any>('/api/v1/system/role/listAll').then(data => {
|
||||
roles.value = data
|
||||
})
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user