Compare commits

..

4 Commits

Author SHA1 Message Date
2c8789b8cb
导航tab美化 2026-03-19 23:30:31 +08:00
8a2ba4e2bf
样式优化 2026-03-19 23:24:30 +08:00
cf34013c53
登录页/欢迎页美化 2026-03-19 23:11:03 +08:00
96b032d262
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
2026-03-19 23:05:59 +08:00
24 changed files with 14328 additions and 2342 deletions

1
components.d.ts vendored
View File

@ -22,6 +22,7 @@ declare module '@vue/runtime-core' {
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']

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 {
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) {
this.search.pageNum = 1
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

@ -3,13 +3,21 @@ html,body,#app,.layout {
padding: 0;
height: 100%;
}
/* ===== 顶部导航栏 ===== */
.layout-header {
color: #fff;
background-color: #515a6e;
background: linear-gradient(90deg, #1d2b45 0%, #2c3e60 100%);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
display: flex;
align-items: center;
position: relative;
div.main-title {
display: inline-block;
font-size: 22px;
font-size: 20px;
font-weight: var(--el-font-weight-primary);
letter-spacing: 1px;
line-height: 60px;
}
.nav-btns-right {
@ -20,45 +28,165 @@ html,body,#app,.layout {
transform: translateY(-50%);
}
}
/* ===== 侧边栏 ===== */
.el-aside {
background: var(--el-fill-color-light);
border-right: 1px solid var(--el-border-color-light);
.el-menu {
border-right: none;
background: transparent;
}
.el-menu-item {
border-radius: var(--el-border-radius-base);
margin: 2px 8px;
width: calc(100% - 16px);
&.is-active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: var(--el-font-weight-primary);
}
&:hover {
background-color: var(--el-fill-color);
}
}
.el-sub-menu__title:hover {
background-color: var(--el-fill-color);
}
}
/* ===== 主内容区 ===== */
.layout-main {
padding: 10px !important;
padding: 0 !important;
display: flex !important;
flex-direction: column;
background: var(--el-fill-color);
> .layout-tabs {
margin: 15px 15px 0 15px;
margin: 12px 16px 0;
}
> .layout-content {
margin: 0 15px 15px 15px;
margin: 12px 16px;
overflow: auto;
background: #fff;
border: 1px solid #e1e1e1;
padding: 10px;
background: var(--el-bg-color);
border-radius: 10px;
border: 1px solid var(--el-border-color-lighter);
padding: 20px;
flex-grow: 1;
flex-basis: 0;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
> .layout-copy {
text-align: center;
padding: 10px 0;
color: #9ea7b4;
padding: 8px 0 12px;
color: var(--el-text-color-placeholder);
font-size: var(--el-font-size-extra-small);
}
}
/* ===== 表格美化 ===== */
.table-container {
position: relative;
.el-table {
border-radius: var(--el-border-radius-base);
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
&::before {
display: none;
}
th.el-table__cell {
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
font-weight: var(--el-font-weight-primary);
font-size: var(--el-font-size-small);
border-bottom: 1px solid var(--el-border-color-lighter);
}
td.el-table__cell {
border-bottom: 1px solid var(--el-fill-color);
color: var(--el-text-color-primary);
font-size: var(--el-font-size-small);
}
.el-table__row:hover > td {
background: var(--el-color-primary-light-9) !important;
}
.el-table__row--striped > td {
background: var(--el-fill-color-lighter);
}
.el-button.is-link {
font-size: 16px;
padding: 4px 6px;
}
}
}
/* ===== 操作栏 ===== */
.btn-container {
margin-bottom: 10px;
margin-bottom: 14px;
display: flex;
align-items: center;
.search-btn {
float: right;
margin-left: auto;
display: flex;
gap: 8px;
}
}
/* ===== 分页 ===== */
.page-container {
padding: 10px;
text-align: center;
padding: 16px 0 4px;
display: flex;
justify-content: center;
}
/* ===== 表单弹窗 ===== */
.el-dialog {
border-radius: 12px;
overflow: hidden;
.el-dialog__header {
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
padding: 16px 20px;
margin: 0;
.el-dialog__title {
font-size: var(--el-font-size-base);
font-weight: var(--el-font-weight-primary);
color: var(--el-text-color-primary);
}
}
.el-dialog__body {
padding: 24px 24px 8px;
}
.el-dialog__footer {
border-top: 1px solid var(--el-border-color-lighter);
padding: 12px 20px;
background: var(--el-fill-color-lighter);
}
}
/* ===== 搜索表单 ===== */
.el-form--inline {
margin-bottom: 14px;
.el-form-item {
margin-bottom: 0;
}
}
.search-panel {
margin-bottom: 14px;
}
/* ===== 其他工具类 ===== */
.carsouel-img {
position: relative;
left: 50%;transform:
translateX(-50%);
left: 50%;
transform: translateX(-50%);
height: 500px;
width: auto;
}
@ -74,7 +202,67 @@ html,body,#app,.layout {
overflow: auto;
}
}
.el-tabs--card.nav-tabs > .el-tabs__header {
.el-tabs--card.nav-tabs {
> .el-tabs__header {
margin-bottom: 0;
border-bottom: none;
background: var(--el-bg-color);
padding: 0 4px;
border-radius: 8px 8px 0 0;
.el-tabs__nav-wrap {
margin-bottom: 0;
}
.el-tabs__nav {
border: none;
}
.el-tabs__item {
height: 36px;
line-height: 36px;
padding: 0 16px;
font-size: var(--el-font-size-small);
color: var(--el-text-color-secondary);
border: none;
border-radius: 6px 6px 0 0;
margin-right: 2px;
transition: color 0.2s, background 0.2s;
position: relative;
&:hover {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
&.is-active {
color: var(--el-color-primary);
font-weight: var(--el-font-weight-primary);
background: var(--el-fill-color);
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--el-color-primary);
border-radius: 2px 2px 0 0;
}
}
.is-icon-close {
font-size: 11px;
margin-left: 4px;
border-radius: 50%;
transition: background 0.2s;
&:hover {
background: var(--el-color-danger-light-7);
color: var(--el-color-danger);
}
}
}
}
}

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
}
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)
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) {
this.openMenuNames.push(result[1])
openMenuNames.value.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 {
this.$router.push('/login')
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 {
router.push('/login')
}
})
}
dropdownMenuCommand(command: string): void {
function dropdownMenuCommand(command: string): void {
switch (command) {
case 'home': //
this.$router.push('/')
case 'home':
router.push('/')
break
case 'changePassword': //
case 'changePassword':
// TODO
break
case 'logout': //
this.$store.commit('logout')
this.$router.push('/login')
case 'logout':
store.commit('logout')
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

@ -1,33 +1,67 @@
<template>
<div class="login-wrapper">
<h2 class="title">博客管理后台</h2>
<el-form ref="loginForm" :model="userInfo" :rules="ruleValidate" :label-width="80">
<div class="login-page">
<div class="login-card">
<div class="card-left">
<div class="brand">
<div class="brand-icon"></div>
<h1>Blog Admin</h1>
<p>简洁 · 高效 · 专注写作</p>
</div>
</div>
<div class="card-right">
<h2 class="form-title">欢迎回来</h2>
<p class="form-subtitle">请登录你的管理账号</p>
<el-form ref="loginForm" :model="userInfo" :rules="ruleValidate" label-position="top">
<el-form-item label="用户名" prop="username">
<el-input v-model="userInfo.username" @on-enter="login"/>
<el-input
v-model="userInfo.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
@keyup.enter="login"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="userInfo.password" type="password" @on-enter="login" />
<el-input
v-model="userInfo.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
size="large"
show-password
@keyup.enter="login"
/>
</el-form-item>
<el-button class="login-btn" type="primary" size="large" :loading="loading" @click="login">
</el-button>
</el-form>
<div class="login-btn">
<el-button type="primary" @click="login">登录</el-button>
</div>
</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 = {
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
}
ruleValidate = {
})
const ruleValidate = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
@ -35,46 +69,164 @@ export default class Login extends Vue {
{ required: true, message: '请输入密码', trigger: 'blur' }
],
}
created(): void {
this.$store.commit('logout')
this.$store.commit('clearTabs')
const loading = ref(false)
// created
store.commit('logout')
store.commit('clearTabs')
localStorage.clear()
}
/**
* 登录
*/
async login() {
(this.$refs.loginForm as VForm).validate(async (valid: boolean) => {
async function login() {
loginForm.value?.validate(async (valid: boolean) => {
if (!valid) return
const data = await this.$http.post<UserInfo, any>('/api/v1/common/login', this.userInfo)
loading.value = true
const data = await http.post<UserInfo, any>('/api/v1/common/login', userInfo).finally(() => {
loading.value = false
})
if (data.token) {
this.$store.commit('login', data)
this.$router.push('/')
store.commit('login', data)
router.push('/')
} else {
ElMessage.error(data.message)
}
})
}
}
type UserInfo = {
username: string | null,
password: string | null
};
</script>
<style lang="less" scoped>
.login-wrapper {
width: 400px;
vertical-align: middle;
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 40%, #0f3460 100%);
position: relative;
overflow: hidden;
&::before,
&::after {
content: '';
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
.title {
text-align: center;
margin-bottom: 30px;
border-radius: 50%;
filter: blur(80px);
opacity: 0.15;
}
.login-btn {
&::before {
width: 500px;
height: 500px;
background: #409eff;
top: -100px;
left: -100px;
}
&::after {
width: 400px;
height: 400px;
background: #67c23a;
bottom: -100px;
right: -100px;
}
}
.login-card {
display: flex;
width: 820px;
min-height: 480px;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
position: relative;
z-index: 1;
}
.card-left {
flex: 1;
background: linear-gradient(145deg, #409eff, #0068d4);
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
top: -80px;
right: -80px;
}
&::after {
content: '';
position: absolute;
width: 200px;
height: 200px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
bottom: -60px;
left: -60px;
}
.brand {
text-align: center;
color: #fff;
position: relative;
z-index: 1;
.brand-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.9;
}
h1 {
font-size: 28px;
font-weight: 700;
margin: 0 0 12px;
letter-spacing: 2px;
}
p {
font-size: 14px;
opacity: 0.75;
letter-spacing: 3px;
margin: 0;
}
}
}
.card-right {
flex: 1.1;
background: #fff;
padding: 52px 48px;
display: flex;
flex-direction: column;
justify-content: center;
.form-title {
font-size: 26px;
font-weight: 700;
color: #1d2129;
margin: 0 0 8px;
}
.form-subtitle {
font-size: 14px;
color: #86909c;
margin: 0 0 36px;
}
:deep(.el-form-item__label) {
font-weight: 600;
color: #4e5969;
}
.login-btn {
width: 100%;
margin-top: 8px;
height: 46px;
font-size: 16px;
letter-spacing: 4px;
border-radius: 8px;
}
}
</style>

View File

@ -1,45 +1,119 @@
<template>
<div>
<div class="nav-list">
<el-row v-for="menu in menus" :key="menu.name">
<el-col :span="3" class="nav-title">{{ menu.title }}</el-col>
<el-col :span="21">
<router-link :to="submenu.path" v-for="submenu in menu.child" :key="submenu.path" class="nav-item">
{{submenu.title}}
<div class="welcome-page">
<div class="welcome-header">
<h2>欢迎回来 👋</h2>
<p>选择下方快捷入口开始管理你的博客</p>
</div>
<div class="menu-sections">
<div class="menu-section" v-for="menu in menus" :key="menu.name">
<div class="section-title">
<el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.title }}</span>
</div>
<div class="section-items">
<router-link
:to="submenu.path"
v-for="submenu in menu.child"
:key="submenu.path"
class="menu-item"
>
<span class="item-label">{{ submenu.title }}</span>
<el-icon class="item-arrow"><ArrowRight /></el-icon>
</router-link>
</el-col>
</el-row>
</div>
</div>
</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 {
.nav-title {
font-size: 16px;
line-height: 66px;
text-align: right;
padding-right: 20px;
.welcome-page {
padding: 32px;
max-width: 900px;
}
.nav-item {
display: inline-block;
width: 200px;
font-size: 16px;
margin: 10px;
border: 1px solid #ccc;
padding: 10px;
text-decoration: none;
.welcome-header {
margin-bottom: 32px;
h2 {
font-size: 26px;
font-weight: 700;
color: #1d2129;
margin: 0 0 8px;
}
p {
font-size: 14px;
color: #86909c;
margin: 0;
}
}
.menu-sections {
display: flex;
flex-direction: column;
gap: 24px;
}
.menu-section {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #4e5969;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f5f5f5;
.el-icon {
font-size: 17px;
color: var(--el-color-primary);
}
}
.section-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-radius: 8px;
background: #f7f8fa;
text-decoration: none;
color: #1d2129;
font-size: 14px;
transition: all 0.2s;
border: 1px solid transparent;
.item-arrow {
font-size: 13px;
color: #c9cdd4;
transition: transform 0.2s, color 0.2s;
}
&:hover {
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-5);
color: var(--el-color-primary);
.item-arrow {
color: var(--el-color-primary);
transform: translateX(3px);
}
}
}
</style>

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,20 +18,18 @@
</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 = {
defineProps<{
typeList: {label: string, value: string}[]
formData: {[propName: string]: string | null}
}>()
const hitokotoForm = ref<VForm>()
const ruleValidate = {
hitokoto: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
@ -39,5 +37,6 @@ export default class HitokotoAdd extends Vue {
{ 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 { UploadInstance, 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 } = 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<UploadInstance>()
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,81 +74,84 @@
</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'
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) {
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)
}
get labels() {
return this.labelList.map(item => {
const labels = computed(() => {
return labelList.value.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})
})
let selectedData: string[] = []
async function loadData(): Promise<void> {
loading.value = true
const data = await http.get<Page, any>('/api/v1/source-image/list', {params: search})
selectedData = []
this.loading = false
this.total = data.total
this.sourceImageData = data.data
loading.value = false
total.value = data.total
sourceImageData.value = data.data
}
deleteAll(): void {
setLoadData(loadData)
function 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}})
await http.delete('/api/v1/source-image/delete', {params: {_ids: selectedData}})
ElMessage.success('删除成功')
this.loadData()
loadData()
}).catch(() => {})
}
dataSelect(selection: SourceImageModel[]): void {
function dataSelect(selection: SourceImageModel[]): void {
selectedData = selection.map(item => item._id)
}
beforeUpload(file: File): boolean {
function beforeUpload(file: File): boolean {
if (file.size > 10 << 20) {
ElMessage.warning('文件大小超过10MB')
return false
}
this.isUploading = true
isUploading.value = true
return true
}
uploadSuccess(response: MsgResult): void {
function uploadSuccess(response: MsgResult): void {
if (response.status) {
ElMessage.success(response.message)
this.loadData()
loadData()
} else {
ElMessage.warning(response.message)
}
this.isUploading = false
isUploading.value = false
}
uploadError(error: Error): void {
this.isUploading = false
function uploadError(error: Error): void {
isUploading.value = false
ElMessage.error(error.message)
}
preview(row: SourceImageModel): void {
function preview(row: SourceImageModel): void {
ElMessageBox({
title: '图片预览',
message: h('img', { style: `width:500px`, src: `/api/v1/common/randomBg?id=${row._id}` }, ''),
@ -157,22 +160,21 @@ export default class SourceImage extends BaseList<Page> {
customStyle: {width: '530px', maxWidth: 'unset'}
}).catch(() => {})
}
modifyTags(item: SourceImageModel): void {
this.curModifyLabels.length = 0
function modifyTags(item: SourceImageModel): void {
curModifyLabels.value.length = 0
if (item.label) {
this.curModifyLabels.push(...item.label)
curModifyLabels.value.push(...item.label)
}
this.curId = item._id
this.modifyModal = true
curId.value = item._id
modifyModal.value = true
}
async tarnsferChange(newTargetKeys: string[], direction: 'right' | 'left', moveKeys: string[]) {
await this.$http.post('/api/v1/source-image/updateLabel', {id: this.curId, labels: newTargetKeys})
async function tarnsferChange(newTargetKeys: string[], direction: 'right' | 'left', moveKeys: string[]) {
await http.post('/api/v1/source-image/updateLabel', {id: curId.value, labels: newTargetKeys})
}
created() {
this.$http.get<never, any>('/api/v1/common/config/image_label').then(data => {
this.labelList.push(...data)
this.loadData()
// 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"
@ -57,6 +57,7 @@
<el-tree :props="treeProps" :load="loadTreeData" lazy highlight-current @node-click="articlePreview" />
</el-col>
<el-col :span="20">
<div class="table-container">
<el-table :data="articleData" v-loading="loading" stripe @selection-change="dataSelect">
<el-table-column type="selection" width="55" />
<el-table-column prop="title" label="标题" />
@ -92,14 +93,15 @@
</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"
@current-change="pageChange">
</el-pagination>
</div>
</div>
</el-col>
</el-row>
<el-drawer
@ -111,129 +113,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 +139,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,52 +7,20 @@
</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
}
categoriesChartOption = {
const categoriesChartOption: any = {
title: {
text: '文章分类',
x: 'center',
@ -83,7 +51,7 @@ export default class Statistics extends Vue {
data: []
}
}
publishDatesChartOption = {
const publishDatesChartOption: any = {
title: {
left: 'center',
text: '文章发布时间',
@ -149,7 +117,7 @@ export default class Statistics extends Vue {
data: []
}
}
timelineWordsChartOption: any = {
const timelineWordsChartOption: any = {
options: [],
timeline: {
axisType: 'category',
@ -177,8 +145,8 @@ export default class Statistics extends Vue {
},
tooltip: {
trigger: 'axis',
axisPointer : { //
type : 'shadow' // 线'line' | 'shadow'
axisPointer: {
type: 'shadow'
}
},
series: {
@ -188,7 +156,40 @@ export default class Statistics extends Vue {
}
}
}
}
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 }
})
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 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
})
const addForm = ref<InstanceType<typeof SystemConfigAdd>>()
function reset() {
search.value = {}
loadData()
}
reset() {
this.search = {}
this.loadData()
async function loadData() {
loading.value = true
systemConfigData.value = await http.get('/api/v1/system/config/list', {params: search.value})
loading.value = false
}
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 = {
function add() {
formData.value = {
name: '',
value: '',
description: '',
is_public: false
}
this.modalTitle = '新增配置项'
this.addModal = true
modalTitle.value = '新增配置项'
addModal.value = true
}
update(row: SystemConfigModel) {
const formData = Object.assign({}, row)
formData.value = JSON.stringify(formData.value, null, ' ')
this.formData = formData
this.modalTitle = '修改配置项'
this.addModal = 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 save() {
((this.$refs.addForm as Vue).$refs.configForm as VForm).validate(async (valid: boolean) => {
async function save() {
addForm.value?.configForm?.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
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)
this.loadData()
loadData()
})
}
remove(row: SystemConfigModel) {
function 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}})
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)
this.loadData()
loadData()
} else {
ElMessage.warning(data.message)
}
}).catch(() => {})
}
created() {
this.loadData()
}
clearValidate() {
this.$nextTick(() => {
((this.$refs.addForm as Vue).$refs.configForm as VForm).clearValidate()
function clearValidate() {
nextTick(() => {
addForm.value?.configForm?.clearValidate()
})
}
/**
* 日期时间格式化
* @param dateStr 日期时间
*/
datetimeFormat(dateStr: string) {
function datetimeFormat(dateStr: string) {
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
}
}
loadData()
</script>

View File

@ -19,24 +19,23 @@
</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 {
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) => {
this.$http.get<any, any>('/api/v1/system/config/exists', {params: {name: value, id: this.formData._id}}).then(data => {
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 {
@ -58,7 +57,7 @@ export default class SystenConfigAdd extends Vue {
}, 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,25 +70,47 @@
</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'
const store = useStore()
class SystemUserPage extends Page {
username?: string
reset() {
super.reset()
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: []
})
export default class SystemUser extends BaseList<SystemUserPage> {
get ruleValidate() {
return {
const userForm = ref<VForm>()
const ruleValidate = computed(() => ({
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}})
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 {
@ -102,89 +124,74 @@ export default class SystemUser extends BaseList<SystemUserPage> {
{ 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
}
}
search = new SystemUserPage()
systemUserData: SystemUserModel[] = []
roles: SystemRoleModel[] = []
addModal: boolean = false
modalTitle: string | null = null
formData: SystemUserModel = {
setLoadData(loadData)
function add() {
Object.assign(formData, {
_id: null,
username: null,
password: null,
realname: null,
role_ids: []
})
modalTitle.value = '新增用户'
addModal.value = true
clearValidate()
}
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
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()
}
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) => {
async function save() {
userForm.value?.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
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)
this.loadData()
loadData()
})
}
remove(row: SystemUserModel) {
function 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}})
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)
this.loadData()
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
function clearValidate() {
nextTick(() => {
userForm.value?.clearValidate()
})
}
clearValidate() {
this.$nextTick(() => {
(this.$refs.userForm as VForm).clearValidate()
})
}
}
class SystemUserPage extends Page {
username?: string
reset() {
super.reset()
this.username = undefined
}
}
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