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:
灌糖包子 2026-03-19 23:05:59 +08:00
parent 1ffd56efc2
commit 96b032d262
Signed by: sookie
GPG Key ID: 0599BECB75C1E68D
22 changed files with 13830 additions and 2268 deletions

11539
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
},

View File

@ -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');

View File

@ -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')

View File

@ -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 }
}

View File

@ -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
View 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

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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>

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

1967
yarn.lock

File diff suppressed because it is too large Load Diff