init: 初始系统模板
This commit is contained in:
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
# 🎨 editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
17
.env.development
Normal file
17
.env.development
Normal file
@@ -0,0 +1,17 @@
|
||||
# 历史路径-哈希带井号标识
|
||||
VITE_HISTORY_HASH = false
|
||||
|
||||
# 历史路径-前缀URL如:/h5
|
||||
VITE_HISTORY_BASE_URL = /
|
||||
|
||||
# 应用名称
|
||||
VITE_APP_NAME = Mask管理系统
|
||||
|
||||
# 应用标识
|
||||
VITE_APP_CODE = maskAntd
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION = '0.2.1'
|
||||
|
||||
# 接口基础URL地址-不带/后缀
|
||||
VITE_API_BASE_URL = /dev-api
|
||||
19
.env.production
Normal file
19
.env.production
Normal file
@@ -0,0 +1,19 @@
|
||||
# 历史路径-哈希带井号标识
|
||||
VITE_HISTORY_HASH = true
|
||||
|
||||
# 历史路径-前缀URL如:/h5
|
||||
VITE_HISTORY_BASE_URL = /mask-antd
|
||||
|
||||
# 应用名称
|
||||
VITE_APP_NAME = Mask管理系统
|
||||
|
||||
# 应用标识
|
||||
VITE_APP_CODE = maskAntd
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION = '0.2.1'
|
||||
|
||||
# 接口基础URL地址-不带/后缀
|
||||
# VITE_API_BASE_URL = https://mock.apifox.cn/m1/1551143-0-default
|
||||
VITE_API_BASE_URL = http://124.223.91.248:8102/prod-api
|
||||
# VITE_API_BASE_URL = http://192.168.56.1:6275
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
**/*.log
|
||||
|
||||
tests/**/coverage/
|
||||
tests/e2e/reports
|
||||
selenium-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
18
README.md
Normal file
18
README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 基于 Ant-Design-Vue + Vue3 的管理系统
|
||||
|
||||
[](https://gitee.com/TsMask/mask_antd_vue3/stargazers)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 简介
|
||||
|
||||
该项目选择 [RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3) 进行功能适配。
|
||||
|
||||
- 系统布局使用 [@ant-design-vue/pro-layout](https://github.com/vueComponent/pro-components)
|
||||
- 图标来源 [@ant-design/icons-vue](https://ant.design/components/icon)
|
||||
- 菜单图标使用自定义iconfont `font_8d5l8fzk5b87iudi.js`图标文件
|
||||
|
||||
> 有任何问题或者建议,可以在 [_Issues_](https://gitee.com/TsMask/mask_api_midwayjs/issues) 或通过QQ群:[_57242844_](https://jq.qq.com/?_wv=1027&k=z6Y4YQcB) 提出想法。
|
||||
> 如果觉得项目对您有帮助,可以来个Star ⭐
|
||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title> </title>
|
||||
<link rel="preload" href="/loading.js" as="script">
|
||||
<script async src="/loading.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "ems_frontend_vue3",
|
||||
"type": "module",
|
||||
"description": "核心网管理系统",
|
||||
"author": "TsMask",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design-vue/pro-layout": "^3.2.4",
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"dayjs": "^1.11.8",
|
||||
"echarts": "^5.4.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"js-base64": "^3.7.5",
|
||||
"js-cookie": "^3.0.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.4",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"less": "^4.1.3",
|
||||
"typescript": "^5.1.3",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue-i18n": "^9.3.0-beta.27",
|
||||
"vue-tsc": "^1.8.0"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1
public/font_8d5l8fzk5b87iudi.js
Normal file
1
public/font_8d5l8fzk5b87iudi.js
Normal file
File diff suppressed because one or more lines are too long
205
public/loading.js
Normal file
205
public/loading.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* loading 占位
|
||||
* 解决首次加载时白屏的问题
|
||||
*/
|
||||
(function () {
|
||||
const _app = document.querySelector('#app');
|
||||
if (_app && _app.innerHTML === '') {
|
||||
const styleStr = `
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#app {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
.loading-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.loading-sub-title {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
color: #888;
|
||||
}
|
||||
.page-loading-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 26px;
|
||||
}
|
||||
.ant-spin {
|
||||
position: absolute;
|
||||
display: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s
|
||||
cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: -webkit-transform 0.3s
|
||||
cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
-webkit-font-feature-settings: "tnum";
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
.ant-spin-spinning {
|
||||
position: static;
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
}
|
||||
.ant-spin-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.ant-spin-dot-item {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 100%;
|
||||
-webkit-transform: scale(0.75);
|
||||
-ms-transform: scale(0.75);
|
||||
transform: scale(0.75);
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
-ms-transform-origin: 50% 50%;
|
||||
transform-origin: 50% 50%;
|
||||
opacity: 0.3;
|
||||
-webkit-animation: antspinmove 1s infinite linear alternate;
|
||||
animation: antSpinMove 1s infinite linear alternate;
|
||||
}
|
||||
.ant-spin-dot-item:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.ant-spin-dot-item:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.ant-spin-dot-item:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
.ant-spin-dot-item:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
.ant-spin-dot-spin {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-animation: antrotate 1.2s infinite linear;
|
||||
animation: antRotate 1.2s infinite linear;
|
||||
}
|
||||
.ant-spin-lg .ant-spin-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
}
|
||||
.ant-spin-lg .ant-spin-dot i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.ant-spin-blur {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
@keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
</style>`;
|
||||
|
||||
let loadInfo = {
|
||||
title: '正在加载资源',
|
||||
titleSub: '初次加载资源可能需要较多时间',
|
||||
msg: '请耐心等待',
|
||||
};
|
||||
document.title = "管理系统";
|
||||
|
||||
// 判断选择语言
|
||||
const lang = localStorage.getItem('cache:local:i18n') || 'zh_CN';
|
||||
if (lang === 'en_US') {
|
||||
loadInfo = {
|
||||
title: 'Loading Resources',
|
||||
titleSub: 'Loading resources for the first time may take a lot of time',
|
||||
msg: 'Please be patient',
|
||||
};
|
||||
document.title = "Managerial System";
|
||||
}
|
||||
|
||||
const divStr = `
|
||||
<div style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 362px;
|
||||
">
|
||||
<div class="page-loading-warp">
|
||||
<div class="ant-spin ant-spin-lg ant-spin-spinning">
|
||||
<span class="ant-spin-dot ant-spin-dot-spin">
|
||||
<i class="ant-spin-dot-item"></i>
|
||||
<i class="ant-spin-dot-item"></i>
|
||||
<i class="ant-spin-dot-item"></i>
|
||||
<i class="ant-spin-dot-item"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-title">
|
||||
${loadInfo.title}
|
||||
</div>
|
||||
<div class="loading-sub-title">
|
||||
${loadInfo.titleSub} </br> ${loadInfo.msg}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
_app.innerHTML = styleStr + divStr;
|
||||
}
|
||||
})();
|
||||
80
src/App.vue
Normal file
80
src/App.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { ConfigProvider } from 'ant-design-vue/lib';
|
||||
import { usePrimaryColor } from '@/hooks/useTheme';
|
||||
import zhCN from 'ant-design-vue/lib/locale/zh_CN';
|
||||
import enUS from 'ant-design-vue/lib/locale/en_US';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { ref, watch } from 'vue';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
const { currentLocale } = useI18n();
|
||||
|
||||
dayjs.locale('zh-cn'); // 默认中文
|
||||
usePrimaryColor(); // 载入用户自定义主题色
|
||||
|
||||
let locale = ref(zhCN); // 国际化初始中文
|
||||
|
||||
// 国际化切换语言
|
||||
function fnChangeLocale(v: string) {
|
||||
switch (v) {
|
||||
case 'zh_CN':
|
||||
locale.value = zhCN;
|
||||
dayjs.locale(zhCN.locale);
|
||||
break;
|
||||
case 'en_US':
|
||||
locale.value = enUS;
|
||||
dayjs.locale(enUS.locale);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载多语言并进行监听
|
||||
fnChangeLocale(currentLocale.value);
|
||||
watch(currentLocale, val => {
|
||||
fnChangeLocale(val);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="locale">
|
||||
<RouterView />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
body .ant-pro-basicLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.ant-pro-sider {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition-duration: 0.5s;
|
||||
transition-property: height, opacity, transform;
|
||||
transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-left-enter,
|
||||
.slide-right-leave-active {
|
||||
opacity: 0;
|
||||
transform: translate(2em, 0);
|
||||
}
|
||||
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter {
|
||||
opacity: 0;
|
||||
transform: translate(-2em, 0);
|
||||
}
|
||||
</style>
|
||||
59
src/api/login.ts
Normal file
59
src/api/login.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
// 登录方法
|
||||
export function login(data: Record<string, string>) {
|
||||
return request({
|
||||
url: '/login',
|
||||
method: 'post',
|
||||
data: data,
|
||||
whithToken: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册方法
|
||||
* @param data 注册对象
|
||||
* @returns object
|
||||
*/
|
||||
export function register(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/register',
|
||||
method: 'post',
|
||||
data: data,
|
||||
whithToken: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详细信息
|
||||
* @returns object
|
||||
*/
|
||||
export function getInfo() {
|
||||
return request({
|
||||
url: '/getInfo',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出方法
|
||||
* @returns object
|
||||
*/
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/logout',
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns object
|
||||
*/
|
||||
export function getCaptchaImage() {
|
||||
return request({
|
||||
url: '/captchaImage',
|
||||
method: 'get',
|
||||
whithToken: false,
|
||||
});
|
||||
}
|
||||
86
src/api/monitor/cache.ts
Normal file
86
src/api/monitor/cache.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询缓存详细
|
||||
* @returns object
|
||||
*/
|
||||
export function getCache() {
|
||||
return request({
|
||||
url: '/monitor/cache',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存名称列表
|
||||
* @returns object
|
||||
*/
|
||||
export function listCacheName() {
|
||||
return request({
|
||||
url: '/monitor/cache/getNames',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存名称下键名列表
|
||||
* @param cacheName 缓存名称列表中得到的缓存名称
|
||||
* @returns object
|
||||
*/
|
||||
export function listCacheKey(cacheName: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/getKeys/${cacheName}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询缓存内容
|
||||
* @param cacheName 键名列表中得到的缓存名称
|
||||
* @param cacheKey 键名列表中得到的缓存键名
|
||||
* @returns object
|
||||
*/
|
||||
export function getCacheValue(cacheName: string, cacheKey: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/getValue/${cacheName}/${cacheKey}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存名称下键名列表
|
||||
* @param cacheName 缓存名称列表中得到的缓存名称
|
||||
* @returns object
|
||||
*/
|
||||
export function clearCacheName(cacheName: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/clearCacheName/${cacheName}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存键名
|
||||
* @param cacheName 键名列表中得到的缓存名称
|
||||
* @param cacheKey 键名列表中得到的缓存键名
|
||||
* @returns object
|
||||
*/
|
||||
export function clearCacheKey(cacheName: string, cacheKey: string) {
|
||||
return request({
|
||||
url: `/monitor/cache/clearCacheKey/${cacheName}/${cacheKey}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全清理缓存名称
|
||||
*
|
||||
* 指定可清理的缓存key
|
||||
* @returns object
|
||||
*/
|
||||
export function clearCacheSafe() {
|
||||
return request({
|
||||
url: '/monitor/cache/clearCacheSafe',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
121
src/api/monitor/job.ts
Normal file
121
src/api/monitor/job.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 定时任务调度列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportJob(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定时任务调度列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listJob(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定时任务调度详细
|
||||
* @param jobId 任务ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getJob(jobId: string | number) {
|
||||
return request({
|
||||
url: `/monitor/job/${jobId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增定时任务调度
|
||||
* @param data 任务对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addJob(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改定时任务调度
|
||||
* @param data 任务对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateJob(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/job',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定时任务调度
|
||||
* @param jobId 任务ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delJob(jobId: string | number) {
|
||||
return request({
|
||||
url: `/monitor/job/${jobId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务状态修改
|
||||
* @param jobId 任务ID
|
||||
* @param status 变更状态值
|
||||
* @returns
|
||||
*/
|
||||
export function changeJobStatus(
|
||||
jobId: string | number,
|
||||
status: string | number
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/job/changeStatus',
|
||||
method: 'put',
|
||||
data: {
|
||||
jobId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务立即执行一次
|
||||
* @param jobId 任务ID
|
||||
* @returns object
|
||||
*/
|
||||
export function runJob(jobId: string) {
|
||||
return request({
|
||||
url: `/monitor/job/run/${jobId}`,
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置刷新队列
|
||||
* @returns object
|
||||
*/
|
||||
export function resetQueueJob() {
|
||||
return request({
|
||||
url: '/monitor/job/resetQueueJob',
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
53
src/api/monitor/jobLog.ts
Normal file
53
src/api/monitor/jobLog.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 定时任务调度日志列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportJobLog(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/jobLog/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询调度日志列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listJobLog(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/jobLog/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除调度日志
|
||||
* @param jobLogId 任务日志Id
|
||||
* @returns object
|
||||
*/
|
||||
export function delJobLog(jobLogId: string) {
|
||||
return request({
|
||||
url: `/monitor/jobLog/${jobLogId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空调度日志
|
||||
* @returns object
|
||||
*/
|
||||
export function cleanJobLog() {
|
||||
return request({
|
||||
url: '/monitor/jobLog/clean',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
67
src/api/monitor/logininfor.ts
Normal file
67
src/api/monitor/logininfor.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 登录日志列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportLogininfor(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询登录日志列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listLogininfor(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除登录日志
|
||||
* @param infoId 登录日志Id
|
||||
* @returns object
|
||||
*/
|
||||
export function delLogininfor(infoId: string) {
|
||||
return request({
|
||||
url: `/monitor/logininfor/${infoId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空登录日志
|
||||
* @returns object
|
||||
*/
|
||||
export function cleanLogininfor() {
|
||||
return request({
|
||||
url: '/monitor/logininfor/clean',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁用户登录状态
|
||||
* @param userName 登录账号
|
||||
* @returns object
|
||||
*/
|
||||
export function unlockLogininfor(userName: string) {
|
||||
return request({
|
||||
url: `/monitor/logininfor/unlock/${userName}`,
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
26
src/api/monitor/online.ts
Normal file
26
src/api/monitor/online.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询在线用户列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listOnline(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/monitor/online/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 强退用户
|
||||
* @param tokenId 授权标识
|
||||
* @returns object
|
||||
*/
|
||||
export function forceLogout(tokenId: string) {
|
||||
return request({
|
||||
url: `/monitor/online/${tokenId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
55
src/api/monitor/operlog.ts
Normal file
55
src/api/monitor/operlog.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 操作日志列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportOperlog(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/operlog/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询操作日志列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listOperlog(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/monitor/operlog/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
* @param operId 操作日志ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delOperlog(operId: string) {
|
||||
return request({
|
||||
url: `/monitor/operlog/${operId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空操作日志
|
||||
* @returns object
|
||||
*/
|
||||
export function cleanOperlog() {
|
||||
return request({
|
||||
url: '/monitor/operlog/clean',
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
9
src/api/monitor/server.ts
Normal file
9
src/api/monitor/server.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**获取服务信息 */
|
||||
export function getServer() {
|
||||
return request({
|
||||
url: '/monitor/server',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
56
src/api/profile.ts
Normal file
56
src/api/profile.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询用户个人信息
|
||||
* @returns object
|
||||
*/
|
||||
export function getUserProfile() {
|
||||
return request({
|
||||
url: '/system/user/profile',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户个人信息
|
||||
* @param data 用户对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateUserProfile(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user/profile',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户密码重置
|
||||
* @param userId 用户ID
|
||||
* @param status 变更状态值
|
||||
* @returns object
|
||||
*/
|
||||
export function updateUserPwd(oldPassword: string, newPassword: string) {
|
||||
return request({
|
||||
url: '/system/user/profile/updatePwd',
|
||||
method: 'put',
|
||||
data: {
|
||||
oldPassword,
|
||||
newPassword,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户头像上传
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function uploadAvatar(data: FormData) {
|
||||
return request({
|
||||
url: '/system/user/profile/avatar',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
12
src/api/router.ts
Normal file
12
src/api/router.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 获取路由
|
||||
* @returns object
|
||||
*/
|
||||
export const getRouters = () => {
|
||||
return request({
|
||||
url: '/getRouters',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
103
src/api/system/config.ts
Normal file
103
src/api/system/config.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 参数配置列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportConfig(
|
||||
query: Record<string, any>
|
||||
) {
|
||||
return request({
|
||||
url: '/system/config/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数配置列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listConfig(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/config/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数详细
|
||||
* @param configId 参数配置ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getConfig(configId: string | number) {
|
||||
return request({
|
||||
url: `/system/config/${configId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数键名查询参数值
|
||||
* @param configKey 参数键名
|
||||
* @returns object
|
||||
*/
|
||||
export function getConfigKey(configKey: string) {
|
||||
return request({
|
||||
url: `/system/config/configKey/${configKey}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增参数配置
|
||||
* @param data 参数配置对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addConfig(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/config',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改参数配置
|
||||
* @param data 参数配置对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateConfig(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/config',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除参数配置
|
||||
* @param configId 参数配置ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delConfig(configId: string | number) {
|
||||
return request({
|
||||
url: `/system/config/${configId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新参数缓存
|
||||
* @returns object
|
||||
*/
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: '/system/config/refreshCache',
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
99
src/api/system/dept.ts
Normal file
99
src/api/system/dept.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询部门列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listDept(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dept/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门列表(排除节点)
|
||||
* @param deptId 部门ID
|
||||
* @returns object
|
||||
*/
|
||||
export function listDeptExcludeChild(deptId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/list/exclude/${deptId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门详细
|
||||
* @param deptId 部门ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getDept(deptId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/${deptId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增部门
|
||||
* @param data 部门对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addDept(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dept',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改部门
|
||||
* @param data 部门对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateDept(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dept',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param deptId 部门ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delDept(deptId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/${deptId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门下拉树结构
|
||||
* @returns object
|
||||
*/
|
||||
export function deptTreeSelect() {
|
||||
return request({
|
||||
url: '/system/dept/treeSelect',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门树结构列表(指定角色)
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function roleDeptTreeSelect(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/dept/roleDeptTreeSelect/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
90
src/api/system/dict/data.ts
Normal file
90
src/api/system/dict/data.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 字典数据列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportData(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典数据列表
|
||||
* @param query 查询值
|
||||
* @returns
|
||||
*/
|
||||
export function listData(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典数据详细
|
||||
* @param dictCode 字典代码值
|
||||
* @returns object
|
||||
*/
|
||||
export function getData(dictCode: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/data/${dictCode}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增字典数据
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addData(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改字典数据
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateData(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/data',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典数据
|
||||
* @param dictCode 字典代码值
|
||||
* @returns object
|
||||
*/
|
||||
export function delData(dictCode: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/data/${dictCode}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据列表(指定字典类型)
|
||||
* @param dictType 字典类型
|
||||
* @returns object
|
||||
*/
|
||||
export function getDictDataType(dictType: string) {
|
||||
return request({
|
||||
url: `/system/dict/data/type/${dictType}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
102
src/api/system/dict/type.ts
Normal file
102
src/api/system/dict/type.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 字典类型列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportType(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典类型列表
|
||||
* @param query 查询值
|
||||
* @returns
|
||||
*/
|
||||
export function listType(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典类型详细
|
||||
* @param dictId 字典编号
|
||||
* @returns object
|
||||
*/
|
||||
export function getType(dictId: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/type/${dictId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增字典类型
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addType(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改字典类型
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateType(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/dict/type',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典类型
|
||||
* @param dictCode 字典代码值
|
||||
* @returns object
|
||||
*/
|
||||
export function delType(dictId: string | number) {
|
||||
return request({
|
||||
url: `/system/dict/type/${dictId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新字典缓存
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: '/system/dict/type/refreshCache',
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典选择框列表
|
||||
* @param data 字典数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function getDictOptionselect() {
|
||||
return request({
|
||||
url: '/system/dict/type/getDictOptionselect',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
87
src/api/system/menu.ts
Normal file
87
src/api/system/menu.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询菜单列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listMenu(query?: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/menu/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询菜单详细
|
||||
* @param menuId 菜单ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getMenu(menuId: string | number) {
|
||||
return request({
|
||||
url: `/system/menu/${menuId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询菜单下拉树结构
|
||||
* @returns object
|
||||
*/
|
||||
export function menuTreeSelect() {
|
||||
return request({
|
||||
url: '/system/menu/treeSelect',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色ID查询菜单下拉树结构
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function roleMenuTreeSelect(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/menu/roleMenuTreeSelect/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增菜单
|
||||
* @param data 菜单对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addMenu(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/menu',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改菜单
|
||||
* @param data 菜单对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateMenu(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/menu',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* @param menuId 菜单ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delMenu(menuId: string | number) {
|
||||
return request({
|
||||
url: `/system/menu/${menuId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
64
src/api/system/notice.ts
Normal file
64
src/api/system/notice.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 查询公告列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listNotice(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/notice/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询公告详细
|
||||
* @param menuId 公告ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getNotice(noticeId: string | number) {
|
||||
return request({
|
||||
url: `/system/notice/${noticeId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增公告
|
||||
* @param data 公告对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addNotice(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/notice',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改公告
|
||||
* @param data 公告对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateNotice(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/notice',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除公告
|
||||
* @param noticeId 公告ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delNotice(noticeId: string | number) {
|
||||
return request({
|
||||
url: `/system/notice/${noticeId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
78
src/api/system/post.ts
Normal file
78
src/api/system/post.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 岗位列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportPost(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询岗位列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listPost(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询岗位详细
|
||||
* @param postId 岗位ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getPost(postId: string | number) {
|
||||
return request({
|
||||
url: `/system/post/${postId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增岗位
|
||||
* @param data 岗位对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addPost(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改岗位
|
||||
* @param data 岗位对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updatePost(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/post',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除岗位
|
||||
* @param postId 岗位ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delPost(postId: string | number) {
|
||||
return request({
|
||||
url: `/system/post/${postId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
134
src/api/system/role.ts
Normal file
134
src/api/system/role.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 角色列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportRole(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listRole(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色详细
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function getRole(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/role/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增角色
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addRole(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改角色
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateRole(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param roleId 角色ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delRole(roleId: string | number) {
|
||||
return request({
|
||||
url: `/system/role/${roleId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色状态修改
|
||||
* @param roleId 角色ID
|
||||
* @param status 角色状态
|
||||
* @returns object
|
||||
*/
|
||||
export function changeRoleStatus(roleId: string, status: string | number) {
|
||||
return request({
|
||||
url: '/system/role/changeStatus',
|
||||
method: 'put',
|
||||
data: {
|
||||
roleId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改角色数据权限
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function dataScope(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/dataScope',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色分配用户列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function authUserAllocatedList(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/authUser/allocatedList',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色分配选择授权
|
||||
* @param data 角色对象
|
||||
* @returns object
|
||||
*/
|
||||
export function authUserChecked(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/role/authUser/checked',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
141
src/api/system/user.ts
Normal file
141
src/api/system/user.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
|
||||
/**
|
||||
* 导入用户模板数据
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function importData(data: FormData) {
|
||||
return request({
|
||||
url: '/system/user/importData',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用户模板下载
|
||||
* @returns bolb
|
||||
*/
|
||||
export function importTemplate() {
|
||||
return request({
|
||||
url: '/system/user/importTemplate',
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户列表导出
|
||||
* @param query 查询参数
|
||||
* @returns bolb
|
||||
*/
|
||||
export function exportUser(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户列表
|
||||
* @param query 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export function listUser(query: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户详细
|
||||
* @param userId 用户ID,新增0
|
||||
* @returns object
|
||||
*/
|
||||
export function getUser(userId: string | number = '0') {
|
||||
return request({
|
||||
url: `/system/user/${userId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
* @param data 用户对象
|
||||
* @returns object
|
||||
*/
|
||||
export function addUser(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user',
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
* @param data 用户对象
|
||||
* @returns object
|
||||
*/
|
||||
export function updateUser(data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/system/user',
|
||||
method: 'put',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param userId 用户ID
|
||||
* @returns object
|
||||
*/
|
||||
export function delUser(userId: string | number) {
|
||||
return request({
|
||||
url: `/system/user/${userId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户密码重置
|
||||
* @param userId 用户ID
|
||||
* @param password 密码
|
||||
* @returns object
|
||||
*/
|
||||
export function resetUserPwd(userId: string | number, password: string) {
|
||||
return request({
|
||||
url: '/system/user/resetPwd',
|
||||
method: 'put',
|
||||
data: {
|
||||
userId,
|
||||
password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户状态修改
|
||||
* @param userId 用户ID
|
||||
* @param status 变更状态值
|
||||
* @returns object
|
||||
*/
|
||||
export function changeUserStatus(
|
||||
userId: string | number,
|
||||
status: string | number
|
||||
) {
|
||||
return request({
|
||||
url: '/system/user/changeStatus',
|
||||
method: 'put',
|
||||
data: {
|
||||
userId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
195
src/api/tool/file.ts
Normal file
195
src/api/tool/file.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { request } from '@/plugins/http-fetch';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png
|
||||
* @param range 断点续传标识,填入字符串 `bytes=${startByte}-${endByte}`
|
||||
* @returns object
|
||||
*/
|
||||
export async function downloadFile(filePath: string, range?: string) {
|
||||
return request({
|
||||
url: `/file/download/${encode(filePath)}`,
|
||||
method: 'get',
|
||||
headers: range ? { range } : {},
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件切片
|
||||
* @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png
|
||||
* @param chunkSize 数据块大小MB,默认1MB
|
||||
* @returns bolb
|
||||
*/
|
||||
export async function downloadFileChunk(
|
||||
filePath: string,
|
||||
chunkSize: number = 1
|
||||
): Promise<Blob> {
|
||||
chunkSize = chunkSize * 1024 * 1024;
|
||||
let start = 0; // 文件块的起始字节
|
||||
let end = chunkSize - 1; // 文件块的结束字节
|
||||
let totalSize = 0; // 文件总大小
|
||||
let downloadedSize = 0; // 已下载的文件大小
|
||||
let filePart: Blob[] = []; // 文件数据块内容
|
||||
|
||||
// 发送带有 Range 请求头的 HTTP 请求
|
||||
async function sendRequest() {
|
||||
const range = `bytes=${start}-${end}`;
|
||||
const res = await downloadFile(filePath, range);
|
||||
if (res.code === 200 && res.status === 206) {
|
||||
// 总大小
|
||||
const contentRange = res.headers.get('content-range') || '0/0';
|
||||
totalSize = parseInt(contentRange.split('/')[1]);
|
||||
// 已下载大小
|
||||
const contentLength = res.headers.get('content-length') || '0';
|
||||
const chunkSize = parseInt(contentLength);
|
||||
// 下一段数据块区间
|
||||
start += chunkSize;
|
||||
end = Math.min(start + chunkSize - 1, totalSize - 1);
|
||||
// 记录下载结果
|
||||
filePart.push(res.data);
|
||||
downloadedSize += chunkSize;
|
||||
// 小于总大小继续下载后续数据
|
||||
if (downloadedSize < totalSize) {
|
||||
await sendRequest();
|
||||
}
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
await sendRequest();
|
||||
return new Blob(filePart, { type: 'application/octet-stream' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function uploadFile(data: FormData) {
|
||||
return request({
|
||||
url: '/file/upload',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传切片文件
|
||||
* @param file 文件对象
|
||||
* @param chunkSize 数据块大小MB,默认1MB
|
||||
* @param subPath 归属子路径, 默认default
|
||||
* @returns
|
||||
*/
|
||||
export async function uploadFileChunk(
|
||||
fileData: File,
|
||||
chunkSize: number = 1,
|
||||
subPath: string = 'default'
|
||||
) {
|
||||
const { name, size } = fileData;
|
||||
const chunkSizeInBytes = chunkSize * 1024 * 1024;
|
||||
// 文件标识使用唯一编码 MD5(文件名+文件大小)
|
||||
const fileIdentifier = `${name}-${size}`;
|
||||
// 文件切分为多少份进行上传
|
||||
const chunksCount = Math.ceil(size / chunkSizeInBytes);
|
||||
// 切块的数据数据用于上传
|
||||
const fileChunks: { index: number; chunk: Blob }[] = [];
|
||||
|
||||
for (let i = 0; i < chunksCount; i++) {
|
||||
const start = i * chunkSizeInBytes;
|
||||
const end = Math.min(start + chunkSizeInBytes, size);
|
||||
fileChunks.push({
|
||||
index: i,
|
||||
chunk: fileData.slice(start, end),
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否已上传部分数据块
|
||||
const resCheck = await chunkCheck(fileIdentifier, name);
|
||||
if (resCheck.code !== 200) {
|
||||
return resCheck;
|
||||
}
|
||||
|
||||
let uploadedSize = 0;
|
||||
let uploadProgress = 0;
|
||||
|
||||
for (const { index, chunk } of fileChunks) {
|
||||
const chunksIndex = `${index}`;
|
||||
// 跳过已上传块
|
||||
if (resCheck.data.includes(chunksIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 上传数据块
|
||||
const formData = new FormData();
|
||||
formData.append('file', chunk, name);
|
||||
formData.append('index', chunksIndex);
|
||||
formData.append('identifier', fileIdentifier);
|
||||
|
||||
const resUpload = await chunkUpload(formData);
|
||||
if (resUpload.code === 200) {
|
||||
uploadedSize += chunk.size;
|
||||
uploadProgress = (uploadedSize / size) * 100;
|
||||
console.log(`上传进度:${uploadProgress}%`);
|
||||
} else {
|
||||
// 上传失败处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传数据完整后合并数据块
|
||||
if (uploadedSize === size) {
|
||||
return await chunkMerge(fileIdentifier, name, subPath);
|
||||
}
|
||||
return { code: 500, msg: '上传出错,请重试' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片文件检查
|
||||
* @param identifier 文件标识
|
||||
* @param fileName 原文件名称
|
||||
* @returns object
|
||||
*/
|
||||
export function chunkCheck(identifier: string, fileName: string) {
|
||||
return request({
|
||||
url: '/file/chunkCheck',
|
||||
method: 'post',
|
||||
data: { identifier, fileName },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片文件合并
|
||||
* @param identifier 文件标识
|
||||
* @param fileName 原文件名称
|
||||
* @param subPath 文件归属
|
||||
* @returns object
|
||||
*/
|
||||
export function chunkMerge(
|
||||
identifier: string,
|
||||
fileName: string,
|
||||
subPath: string = 'default'
|
||||
) {
|
||||
return request({
|
||||
url: '/file/chunkMerge',
|
||||
method: 'post',
|
||||
data: { identifier, fileName, subPath },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片文件上传
|
||||
* @param data 表单数据对象
|
||||
* @returns object
|
||||
*/
|
||||
export function chunkUpload(data: FormData) {
|
||||
return request({
|
||||
url: '/file/chunkUpload',
|
||||
method: 'post',
|
||||
data,
|
||||
dataType: 'form-data',
|
||||
});
|
||||
}
|
||||
69
src/assets/background.svg
Normal file
69
src/assets/background.svg
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Ant-Design-Pro</title>
|
||||
<desc>mask-and-vue3 By TsMask</desc>
|
||||
<defs></defs>
|
||||
<g id="Ant-Design-Pro" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="mask-and-vue3" transform="translate(-79.000000, -82.000000)">
|
||||
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
|
||||
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
|
||||
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
|
||||
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
|
||||
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
|
||||
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
|
||||
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
|
||||
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
|
||||
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||
</g>
|
||||
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
|
||||
</g>
|
||||
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
|
||||
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
|
||||
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
|
||||
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||
</g>
|
||||
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
|
||||
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||
</g>
|
||||
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
|
||||
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/donate.jpg
Normal file
BIN
src/assets/donate.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 482 KiB |
BIN
src/assets/images/default_avatar.png
Normal file
BIN
src/assets/images/default_avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
130
src/assets/js/icon_font_8d5l8fzk5b87iudi.ts
Normal file
130
src/assets/js/icon_font_8d5l8fzk5b87iudi.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 字体图标文件-静态资源文件路径
|
||||
*/
|
||||
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
|
||||
export const scriptUrl = `${
|
||||
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
|
||||
? ''
|
||||
: baseUrl.indexOf('/') === -1
|
||||
? '/' + baseUrl
|
||||
: baseUrl
|
||||
}/font_8d5l8fzk5b87iudi.js`;
|
||||
|
||||
/**
|
||||
* 读取 font_8d5l8fzk5b87iudi.js 文件内svg图标名称
|
||||
*
|
||||
* JSON.stringify(txt.match(/icon-(\S*)"/gi).map(i=>i.slice(0,-1)))
|
||||
*/
|
||||
export const iconFonts = [
|
||||
'#',
|
||||
'icon-alibaba',
|
||||
'icon-alimama',
|
||||
'icon-aliyun',
|
||||
'icon-anzhuo',
|
||||
'icon-biaoqing',
|
||||
'icon-chexiao',
|
||||
'icon-chexiao2',
|
||||
'icon-daimayingyong',
|
||||
'icon-daishenhe',
|
||||
'icon-dashang',
|
||||
'icon-dianzan',
|
||||
'icon-dianzan1',
|
||||
'icon-facebook',
|
||||
'icon-fangda',
|
||||
'icon-fangda2',
|
||||
'icon-fanhui',
|
||||
'icon-fanhui1',
|
||||
'icon-fankui1',
|
||||
'icon-fenxiang',
|
||||
'icon-fuzhichenggong',
|
||||
'icon-fuzhidaima',
|
||||
'icon-fuzhidaima1',
|
||||
'icon-gengduo',
|
||||
'icon-gerenzhanghu',
|
||||
'icon-github',
|
||||
'icon-gonggao',
|
||||
'icon-gonggaodayi',
|
||||
'icon-gongnengjieshao',
|
||||
'icon-gouwuche',
|
||||
'icon-gouwuche2',
|
||||
'icon-guanbi',
|
||||
'icon-huidingbu',
|
||||
'icon-huifu',
|
||||
'icon-huizhiguize',
|
||||
'icon-iconfont1',
|
||||
'icon-ios',
|
||||
'icon-jieshi',
|
||||
'icon-jinggao',
|
||||
'icon-lishi',
|
||||
'icon-morentouxiang',
|
||||
'icon-paixu',
|
||||
'icon-pcduan',
|
||||
'icon-piliang',
|
||||
'icon-qingchu',
|
||||
'icon-qq',
|
||||
'icon-qunzhu',
|
||||
'icon-right',
|
||||
'icon-saoyisao',
|
||||
'icon-shanchu',
|
||||
'icon-shang',
|
||||
'icon-shang1',
|
||||
'icon-shang2',
|
||||
'icon-shangchuan',
|
||||
'icon-shenhejujue',
|
||||
'icon-shenhetongguo',
|
||||
'icon-shijian',
|
||||
'icon-shuoming',
|
||||
'icon-souren',
|
||||
'icon-sousuo',
|
||||
'icon-soutubiao',
|
||||
'icon-suofang',
|
||||
'icon-suoxiao',
|
||||
'icon-suoxiao2',
|
||||
'icon-taobaowang',
|
||||
'icon-tengxunwang',
|
||||
'icon-tianjiachengyuan',
|
||||
'icon-tianmao',
|
||||
'icon-tubiaohuizhi',
|
||||
'icon-tubiaoku',
|
||||
'icon-tuichu',
|
||||
'icon-twitter',
|
||||
'icon-weibo',
|
||||
'icon-weibo1',
|
||||
'icon-weibo2',
|
||||
'icon-weijiaru',
|
||||
'icon-weitijiao',
|
||||
'icon-weixin',
|
||||
'icon-wenjian',
|
||||
'icon-wocanyu',
|
||||
'icon-wofaqi',
|
||||
'icon-xia',
|
||||
'icon-xia2',
|
||||
'icon-xiangmu',
|
||||
'icon-xiangmuchengyuan',
|
||||
'icon-xiangxia',
|
||||
'icon-xiangxia1',
|
||||
'icon-xiangxia2',
|
||||
'icon-xiangyou',
|
||||
'icon-xiaomi',
|
||||
'icon-xiazai',
|
||||
'icon-xinjiantubiaoku',
|
||||
'icon-yingwen',
|
||||
'icon-you',
|
||||
'icon-you1',
|
||||
'icon-you2',
|
||||
'icon-youxiang',
|
||||
'icon-youxuan',
|
||||
'icon-youxuan2',
|
||||
'icon-yuzhanghao',
|
||||
'icon-yuzhanghao1',
|
||||
'icon-zhifubao',
|
||||
'icon-zhizuoliucheng',
|
||||
'icon-zhongguodianxin',
|
||||
'icon-zhuanrang',
|
||||
'icon-zhubajie',
|
||||
'icon-zuo',
|
||||
'icon-zuo1',
|
||||
'icon-zuo2',
|
||||
'icon-zuoxuan',
|
||||
'icon-zuoxuan2',
|
||||
];
|
||||
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
141
src/components/CronModal/components/Day.vue
Normal file
141
src/components/CronModal/components/Day.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 31 }, (_, i) => {
|
||||
let idx = i + 1;
|
||||
return {
|
||||
label: `${idx}`,
|
||||
value: `${idx}`,
|
||||
};
|
||||
});
|
||||
|
||||
/**数据 */
|
||||
let data = reactive({
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 2,
|
||||
incrementStart: 1,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定 */
|
||||
specific: ['1'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一天</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
天执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
日开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="1"
|
||||
:max="31"
|
||||
placeholder="1-31"
|
||||
></a-input-number>
|
||||
日
|
||||
</a-radio>
|
||||
<a-radio value="4">指定日(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定日(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
<a-radio value="5">本月最后一天</a-radio>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
137
src/components/CronModal/components/Hour.vue
Normal file
137
src/components/CronModal/components/Hour.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 24 }, (_, i) => ({
|
||||
label: `${i}`.padStart(2, '0'),
|
||||
value: `${i}`,
|
||||
}));
|
||||
|
||||
/**数据 */
|
||||
let data = reactive({
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 2,
|
||||
incrementStart: 0,
|
||||
/**周期 */
|
||||
rangeStart: 0,
|
||||
rangeEnd: 2,
|
||||
/**指定 */
|
||||
specific: ['0'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一小时</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="0-23"
|
||||
></a-input-number>
|
||||
小时执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="0-23"
|
||||
></a-input-number>
|
||||
时开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="1-23"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="0"
|
||||
:max="23"
|
||||
placeholder="0-23"
|
||||
></a-input-number>
|
||||
小时
|
||||
</a-radio>
|
||||
<a-radio value="4">指定小时(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定小时(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
139
src/components/CronModal/components/Minute.vue
Normal file
139
src/components/CronModal/components/Minute.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 60 }, (_, i) => {
|
||||
return {
|
||||
label: `${i}`.padStart(2, '0'),
|
||||
value: `${i}`,
|
||||
};
|
||||
});
|
||||
|
||||
/**数据 */
|
||||
let data = reactive({
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 1,
|
||||
incrementStart: 0,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定 */
|
||||
specific: ['0'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一分钟</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
分钟执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
分钟开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
分钟
|
||||
</a-radio>
|
||||
<a-radio value="4">指定分钟(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定分钟(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
141
src/components/CronModal/components/Month.vue
Normal file
141
src/components/CronModal/components/Month.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 12 }, (_, i) => {
|
||||
let idx = i + 1;
|
||||
return {
|
||||
label: `${idx}`,
|
||||
value: `${idx}`,
|
||||
};
|
||||
});
|
||||
|
||||
/**构建数据 */
|
||||
let data = reactive({
|
||||
/**类型 */
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 1,
|
||||
incrementStart: 1,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定秒 */
|
||||
specific: ['1'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一月</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
月执行,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
月开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="1"
|
||||
:max="12"
|
||||
placeholder="1-12"
|
||||
></a-input-number>
|
||||
月之间的每个月
|
||||
</a-radio>
|
||||
<a-radio value="4">指定月(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定月(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
138
src/components/CronModal/components/Second.vue
Normal file
138
src/components/CronModal/components/Second.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onBeforeMount } from 'vue';
|
||||
const emit = defineEmits(['update:value']);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '*',
|
||||
},
|
||||
});
|
||||
|
||||
/**指定列表初始数据 */
|
||||
const optionsSpecific = Array.from({ length: 60 }, (_, i) => ({
|
||||
label: `${i}`.padStart(2, '0'),
|
||||
value: `${i}`,
|
||||
}));
|
||||
|
||||
/**构建数据 */
|
||||
let data = reactive({
|
||||
/**类型 */
|
||||
type: '1',
|
||||
/**间隔 */
|
||||
increment: 2,
|
||||
incrementStart: 0,
|
||||
/**周期 */
|
||||
rangeStart: 1,
|
||||
rangeEnd: 2,
|
||||
/**指定秒 */
|
||||
specific: ['0'],
|
||||
});
|
||||
|
||||
/**监听数据,将数值格式化 */
|
||||
watch(data, () => {
|
||||
let reultValue = '*';
|
||||
let val = data.type;
|
||||
// 每一
|
||||
if (val === '1') {
|
||||
reultValue = '*';
|
||||
}
|
||||
// 间隔
|
||||
if (val === '2') {
|
||||
let start = data.incrementStart;
|
||||
let increment = data.increment;
|
||||
reultValue = `${start ?? 0}/${increment ?? 0}`;
|
||||
}
|
||||
// 周期
|
||||
if (val === '3') {
|
||||
let start = data.rangeStart;
|
||||
let end = data.rangeEnd;
|
||||
reultValue = `${start ?? 0}-${end ?? 0}`;
|
||||
}
|
||||
// 指定
|
||||
if (val === '4') {
|
||||
reultValue = data.specific.sort((a, b) => +a - +b).join(',');
|
||||
}
|
||||
emit('update:value', reultValue);
|
||||
});
|
||||
|
||||
/**挂载前初始属性 */
|
||||
onBeforeMount(() => {
|
||||
const val = props.value;
|
||||
if (val === '*') {
|
||||
data.type = '1';
|
||||
}
|
||||
if (val.includes('/')) {
|
||||
const arr = val.split('/');
|
||||
data.incrementStart = Number(arr[0]);
|
||||
data.increment = Number(arr[1]);
|
||||
data.type = '2';
|
||||
}
|
||||
if (val.includes('-')) {
|
||||
const arr = val.split('-');
|
||||
data.rangeStart = Number(arr[0]);
|
||||
data.rangeEnd = Number(arr[1]);
|
||||
data.type = '3';
|
||||
}
|
||||
if (val.includes(',')) {
|
||||
data.specific = val.split(',').sort((a, b) => +a - +b);
|
||||
data.type = '4';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-radio-group size="small" v-model:value="data.type">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-radio value="1">每一秒钟</a-radio>
|
||||
<a-radio value="2">
|
||||
每隔
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.increment"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
秒执行一次,从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.incrementStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
秒开始
|
||||
</a-radio>
|
||||
<a-radio value="3">
|
||||
周期从
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeStart"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
到
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-model:value="data.rangeEnd"
|
||||
:min="0"
|
||||
:max="59"
|
||||
placeholder="0-59"
|
||||
></a-input-number>
|
||||
秒
|
||||
</a-radio>
|
||||
<a-radio value="4">指定秒数(可多选)</a-radio>
|
||||
<a-select
|
||||
v-model:value="data.specific"
|
||||
size="small"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="指定秒数(可多选)"
|
||||
:options="optionsSpecific"
|
||||
></a-select>
|
||||
</a-space>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
112
src/components/CronModal/index.vue
Normal file
112
src/components/CronModal/index.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-modal
|
||||
title="Cron表达式生成"
|
||||
:visible="props.visible"
|
||||
:body-style="{ padding: '0 24px' }"
|
||||
:destroy-on-close="true"
|
||||
@cancel="fnCronModal(false)"
|
||||
@ok="fnCronModal(true)"
|
||||
>
|
||||
<a-tabs tab-position="top" type="line">
|
||||
<a-tab-pane key="1" tab="秒">
|
||||
<CronSecond v-model:value="cronValue.second"></CronSecond>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="分钟">
|
||||
<CronMinute v-model:value="cronValue.minute"></CronMinute>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="小时">
|
||||
<CronHour v-model:value="cronValue.hour"></CronHour>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="日">
|
||||
<CronDay v-model:value="cronValue.day"></CronDay>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab="月">
|
||||
<CronMonth v-model:value="cronValue.month"></CronMonth>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-input
|
||||
class="reultBox"
|
||||
addon-before="表达式预览:"
|
||||
v-model:value="cronStr"
|
||||
disabled
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import CronSecond from './components/Second.vue';
|
||||
import CronMinute from './components/Minute.vue';
|
||||
import CronHour from './components/Hour.vue';
|
||||
import CronDay from './components/Day.vue';
|
||||
import CronMonth from './components/Month.vue';
|
||||
import { reactive, computed, watch } from 'vue';
|
||||
|
||||
const emit = defineEmits(['cancel', 'ok', 'update:visible']);
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
cron: {
|
||||
type: String,
|
||||
default: '* * * * * ?',
|
||||
},
|
||||
});
|
||||
|
||||
/**cron属性值 */
|
||||
let cronValue = reactive({
|
||||
second: '*',
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
month: '*',
|
||||
week: '?',
|
||||
});
|
||||
|
||||
/**组合cron结果 */
|
||||
const cronStr = computed(() => {
|
||||
let time = `${cronValue.second} ${cronValue.minute} ${cronValue.hour}`;
|
||||
let date = `${cronValue.day} ${cronValue.month} ${cronValue.week}`;
|
||||
return `${time} ${date}`;
|
||||
});
|
||||
|
||||
/**监听是否显示,初始cron属性 */
|
||||
watch(
|
||||
() => props.visible,
|
||||
val => {
|
||||
if (!val) return;
|
||||
const arr = props.cron.split(' ');
|
||||
// 6 位以上是合法表达式
|
||||
if (arr.length >= 6) {
|
||||
Object.assign(cronValue, {
|
||||
second: arr[0],
|
||||
minute: arr[1],
|
||||
hour: arr[2],
|
||||
day: arr[3],
|
||||
month: arr[4],
|
||||
week: arr[5],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 窗口事件
|
||||
* @param val modal触发事件
|
||||
*/
|
||||
function fnCronModal(val: boolean) {
|
||||
emit('update:visible', false);
|
||||
if (val) {
|
||||
emit('ok', cronStr.value);
|
||||
} else {
|
||||
emit('cancel');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.reultBox {
|
||||
margin-top: 48px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
48
src/components/DictTag/index.vue
Normal file
48
src/components/DictTag/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
/**数据 */
|
||||
options: {
|
||||
type: Array,
|
||||
},
|
||||
/**当前的值对应数据中的项字段 */
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
/**当前的值 */
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
/**遍历找到对应值数据项 */
|
||||
const item = computed(() => {
|
||||
if (Array.isArray(props.options) && props.options.length > 0) {
|
||||
const option = (props.options as any[]).find(
|
||||
item => `${item[props.valueField]}` === `${props.value}`
|
||||
);
|
||||
return option;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="item">
|
||||
<a-tag
|
||||
v-if="item.elTagType"
|
||||
:class="item.elTagClass"
|
||||
:color="item.elTagType"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-tag>
|
||||
<span v-else :class="item.elTagClass">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else></span>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
30
src/components/IconFont/index.vue
Normal file
30
src/components/IconFont/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
|
||||
import { createFromIconfontCN } from '@ant-design/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: '#',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
});
|
||||
|
||||
/**字体图标加载为组件 */
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: scriptUrl,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconFont
|
||||
v-if="type != '#'"
|
||||
:type="props.type"
|
||||
:style="{ fontSize: size + 'px' }"
|
||||
></IconFont>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
40
src/components/LinkiFrame/index.vue
Normal file
40
src/components/LinkiFrame/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const height = ref<string>(document.documentElement.clientHeight - 94.5 + 'px');
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
let iframe = reactive({
|
||||
id: 'link',
|
||||
src: props.src,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.name) {
|
||||
iframe.id = route.name.toString();
|
||||
}
|
||||
window.onresize = () => {
|
||||
height.value = document.documentElement.clientHeight - 94.5 + 'px;';
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="'height:' + height">
|
||||
<iframe
|
||||
:id="iframe.id"
|
||||
:src="iframe.src"
|
||||
frameborder="no"
|
||||
style="width: 100%; height: 100%"
|
||||
scrolling="auto"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
5
src/constants/admin-constants.ts
Normal file
5
src/constants/admin-constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**管理员-系统指定角色KEY */
|
||||
export const ADMIN_ROLE_KEY = 'admin';
|
||||
|
||||
/**管理员-系统指定权限 */
|
||||
export const ADMIN_PERMISSION = '*:*:*';
|
||||
5
src/constants/app-constants.ts
Normal file
5
src/constants/app-constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**应用-请求头-系统标识 */
|
||||
export const APP_REQUEST_HEADER_CODE = 'X-App-Code';
|
||||
|
||||
/**应用-请求头-系统版本 */
|
||||
export const APP_REQUEST_HEADER_VERSION = 'X-App-Version';
|
||||
11
src/constants/cache-keys-constants.ts
Normal file
11
src/constants/cache-keys-constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**会话缓存-网络请求 */
|
||||
export const CACHE_SESSION_FATCH = 'cache:session:fatch';
|
||||
|
||||
/**本地缓存-布局设置 */
|
||||
export const CACHE_LOCAL_PROCONFIG = 'cache:local:proConfig';
|
||||
|
||||
/**本地缓存-主题色 */
|
||||
export const CACHE_LOCAL_PRIMARY_COLOR = 'cache:local:primaryColor';
|
||||
|
||||
/**本地缓存-多语言 */
|
||||
export const CACHE_LOCAL_I18N = 'cache:local:i18n';
|
||||
20
src/constants/menu-constants.ts
Normal file
20
src/constants/menu-constants.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**组件布局类型-基础布局组件标识 */
|
||||
export const MENU_COMPONENT_LAYOUT_BASIC = 'BasicLayout';
|
||||
|
||||
/**组件布局类型-空白布局组件标识 */
|
||||
export const MENU_COMPONENT_LAYOUT_BLANK = 'BlankLayout';
|
||||
|
||||
/**组件布局类型-内链接布局组件标识 */
|
||||
export const MENU_COMPONENT_LAYOUT_LINK = 'LinkLayout';
|
||||
|
||||
/**菜单类型-目录 */
|
||||
export const MENU_TYPE_DIR = 'D';
|
||||
|
||||
/**菜单类型-菜单 */
|
||||
export const MENU_TYPE_MENU = 'M';
|
||||
|
||||
/**菜单类型-按钮 */
|
||||
export const MENU_TYPE_BUTTON = 'B';
|
||||
|
||||
/**菜单内嵌地址标识-带/前缀 */
|
||||
export const MENU_PATH_INLINE = '/inline';
|
||||
11
src/constants/token-constants.ts
Normal file
11
src/constants/token-constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**令牌-数据响应字段 */
|
||||
export const TOKEN_RESPONSE_FIELD = 'access_token';
|
||||
|
||||
/**令牌-请求头标识前缀 */
|
||||
export const TOKEN_KEY_PREFIX = 'Bearer ';
|
||||
|
||||
/**令牌-请求头标识 */
|
||||
export const TOKEN_KEY = 'Authorization';
|
||||
|
||||
/**令牌-存放Cookie标识 */
|
||||
export const TOKEN_COOKIE = 'AuthAntdv';
|
||||
10
src/directive/index.ts
Normal file
10
src/directive/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { App } from 'vue';
|
||||
import permsDirective from './perms-directive';
|
||||
import rolesDirective from './roles-directive';
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.directive('perms', permsDirective);
|
||||
app.directive('roles', rolesDirective);
|
||||
},
|
||||
};
|
||||
38
src/directive/perms-directive.ts
Normal file
38
src/directive/perms-directive.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { hasPermissions, matchPermissions } from '@/plugins/auth-user';
|
||||
import { DirectiveBinding } from 'vue';
|
||||
|
||||
/**
|
||||
* perms-权限标识
|
||||
*
|
||||
* 指令值:字符串数组
|
||||
*
|
||||
* 指令的参数:has/math,默认has
|
||||
*
|
||||
* v-perms="['monitor:server:query']"
|
||||
* 等同
|
||||
* v-perms:has="['monitor:server:query']"
|
||||
*
|
||||
* v-perms:math="['style:user:query', 'style:user:edit']"
|
||||
*
|
||||
* @param el 指令绑定到的元素
|
||||
* @param binding 一个对象
|
||||
*/
|
||||
export default function (el: any, binding: DirectiveBinding<any>) {
|
||||
const value = binding.value;
|
||||
let arg = binding.arg;
|
||||
let ok: boolean = false;
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// 匹配
|
||||
if (arg === 'math') {
|
||||
ok = matchPermissions(value);
|
||||
}
|
||||
// 含有
|
||||
if (!arg || arg === 'has') {
|
||||
ok = hasPermissions(value);
|
||||
}
|
||||
}
|
||||
// 没有权限就移除节点
|
||||
if (!ok) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
38
src/directive/roles-directive.ts
Normal file
38
src/directive/roles-directive.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { hasRoles, matchRoles } from '@/plugins/auth-user';
|
||||
import { DirectiveBinding } from 'vue';
|
||||
|
||||
/**
|
||||
* roles-权限标识
|
||||
*
|
||||
* 指令值:字符串数组
|
||||
*
|
||||
* 指令的参数:has/math,默认has
|
||||
*
|
||||
* v-roles="['admin']"
|
||||
* 等同
|
||||
* v-roles:has="['admin']"
|
||||
*
|
||||
* v-roles:math="['common', 'user']"
|
||||
*
|
||||
* @param el 指令绑定到的元素
|
||||
* @param binding 一个对象
|
||||
*/
|
||||
export default function (el: any, binding: DirectiveBinding<any>) {
|
||||
const value = binding.value;
|
||||
let arg = binding.arg;
|
||||
let ok: boolean = false;
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// 匹配
|
||||
if (arg === 'math') {
|
||||
ok = matchRoles(value);
|
||||
}
|
||||
// 含有
|
||||
if (!arg || arg === 'has') {
|
||||
ok = hasRoles(value);
|
||||
}
|
||||
}
|
||||
// 没有权限就移除节点
|
||||
if (!ok) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
26
src/hooks/useI18n.ts
Normal file
26
src/hooks/useI18n.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { localSet } from '@/utils/cache-local-utils';
|
||||
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
|
||||
|
||||
export default function useLocale() {
|
||||
//实例化i18n
|
||||
const i18n = useI18n();
|
||||
|
||||
// 获取当前语言设置
|
||||
const currentLocale = computed(() => {
|
||||
return i18n.locale.value;
|
||||
});
|
||||
|
||||
// 切换语言
|
||||
const changeLocale = (value: string) => {
|
||||
i18n.locale.value = value;
|
||||
localSet(CACHE_LOCAL_I18N, value);
|
||||
};
|
||||
|
||||
return {
|
||||
currentLocale,
|
||||
changeLocale,
|
||||
t: i18n.t,
|
||||
};
|
||||
}
|
||||
16
src/hooks/useLoading.ts
Normal file
16
src/hooks/useLoading.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ref, inject, provide } from 'vue';
|
||||
|
||||
const INJECT_LOADING_KEY = Symbol('loading_store');
|
||||
|
||||
export const createLoading = (v = false) => {
|
||||
const loading = ref<boolean>(v);
|
||||
const change = (bool: boolean) => {
|
||||
loading.value = bool;
|
||||
};
|
||||
provide(INJECT_LOADING_KEY, loading);
|
||||
return [loading, change];
|
||||
};
|
||||
|
||||
export const useLoading = () => {
|
||||
return inject(INJECT_LOADING_KEY);
|
||||
};
|
||||
58
src/hooks/useTheme.ts
Normal file
58
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { ConfigProvider } from 'ant-design-vue/lib';
|
||||
import { CACHE_LOCAL_PRIMARY_COLOR } from '@/constants/cache-keys-constants';
|
||||
import { localGet, localSet } from '@/utils/cache-local-utils';
|
||||
|
||||
/**
|
||||
* 初始主题色
|
||||
*/
|
||||
export const usePrimaryColor = () => {
|
||||
onBeforeMount(() => {
|
||||
changePrimaryColor(getLocalColor());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 改变主题色
|
||||
* @param color 颜色
|
||||
*/
|
||||
export function changePrimaryColor(color?: string) {
|
||||
if (!color) {
|
||||
color = getRandomColor();
|
||||
}
|
||||
ConfigProvider.config({
|
||||
theme: {
|
||||
primaryColor: color,
|
||||
},
|
||||
});
|
||||
localSet(CACHE_LOCAL_PRIMARY_COLOR, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题色-本地缓存
|
||||
* @returns 颜色
|
||||
*/
|
||||
export function getLocalColor() {
|
||||
return localGet(CACHE_LOCAL_PRIMARY_COLOR) || '#1890ff';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机颜色范围
|
||||
* @returns 颜色
|
||||
*/
|
||||
function getRandomColor(): string {
|
||||
const colors: string[] = [
|
||||
'#f5222d',
|
||||
'#fa541c',
|
||||
'#fa8c16',
|
||||
'#a0d911',
|
||||
'#13c2c2',
|
||||
'#1890ff',
|
||||
'#722ed1',
|
||||
'#eb2f96',
|
||||
'#faad14',
|
||||
'#52c41a',
|
||||
];
|
||||
const i = Math.floor(Math.random() * 10);
|
||||
return colors[i];
|
||||
}
|
||||
16
src/i18n/index.ts
Normal file
16
src/i18n/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { localGet } from '@/utils/cache-local-utils';
|
||||
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
|
||||
import zhCN from './locales/zh-CN';
|
||||
import enUS from './locales/en-US';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API 的方式创建 i18n 实例
|
||||
locale: localGet(CACHE_LOCAL_I18N) || 'zh_CN', // 默认显示语言
|
||||
messages: {
|
||||
zh_CN: zhCN,
|
||||
en_US: enUS,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
62
src/i18n/locales/en-US.ts
Normal file
62
src/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export default {
|
||||
// 语言
|
||||
i18n: 'English',
|
||||
hello: 'Hello',
|
||||
|
||||
// 通用
|
||||
common: {
|
||||
title: 'Mask Antd Vue3',
|
||||
desc: 'Management system based on ant design vue+vue3',
|
||||
loading: 'Please wait...',
|
||||
tipTitle: 'Prompt',
|
||||
},
|
||||
|
||||
// 全局页脚
|
||||
globalFooter: {
|
||||
help: 'Help',
|
||||
privacy: 'Privacy',
|
||||
term: 'Term',
|
||||
},
|
||||
|
||||
// 校验
|
||||
valid: {
|
||||
userNameReg:
|
||||
'The account cannot start with a number and can contain uppercase and lowercase letters, numbers, and no less than 5 digits.',
|
||||
userNamePlease: 'Please enter the correct login account',
|
||||
userNameHit: 'Login account',
|
||||
passwordReg:
|
||||
'The password should contain at least uppercase and lowercase letters, numbers, special symbols, and no less than 6 digits.',
|
||||
passwordPlease: 'Please enter the correct login password',
|
||||
passwordHit: 'Login password',
|
||||
passwordConfirmHit: 'Confirm login password',
|
||||
phoneReg: 'Incorrect phone number',
|
||||
phonePlease: 'Please enter the correct phone number',
|
||||
phoneHit: 'Mobile number',
|
||||
codePlease: 'Please enter the correct verification code',
|
||||
codeHit: 'Verification code',
|
||||
codeText: 'Obtain verification code',
|
||||
codeSmsSend: 'Successfully sent, please pay attention to checking the SMS',
|
||||
},
|
||||
|
||||
// 页面
|
||||
views: {
|
||||
login: {
|
||||
tabPane1: 'Account password login',
|
||||
tabPane2: 'Login with phone number',
|
||||
registerBtn: 'Register an account',
|
||||
loginBtn: 'Login',
|
||||
loginSuccess: 'Login successful',
|
||||
loginMethod: 'Other login methods:',
|
||||
loginMethodWX: 'WeChat Scan Login',
|
||||
loginMethodQQ: 'QQ Scan Code Login',
|
||||
},
|
||||
register: {
|
||||
registerBtn: 'Register',
|
||||
loginBtn: 'Log in with an existing account',
|
||||
passwordErr: 'Please enter the correct confirmation password',
|
||||
passwordConfirmErr: 'The two passwords entered do not match',
|
||||
tipContent: 'Congratulations, {username} account registration succeeded!',
|
||||
tipBtn: 'Go to login',
|
||||
},
|
||||
},
|
||||
};
|
||||
60
src/i18n/locales/zh-CN.ts
Normal file
60
src/i18n/locales/zh-CN.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export default {
|
||||
// 语言
|
||||
i18n: '中文',
|
||||
hello: '你好',
|
||||
|
||||
// 通用
|
||||
common: {
|
||||
title: 'Mask Antd Vue3',
|
||||
desc: '基于 ant-design-vue + vue3 的管理系统',
|
||||
loading: '请稍等...',
|
||||
tipTitle: '提示',
|
||||
},
|
||||
|
||||
// 全局页脚
|
||||
globalFooter: {
|
||||
help: '帮助',
|
||||
privacy: '隐私',
|
||||
term: '条款',
|
||||
},
|
||||
|
||||
// 校验
|
||||
valid: {
|
||||
userNameReg: '账号不能以数字开头,可包含大写小写字母,数字,且不少于5位',
|
||||
userNamePlease: '请输入正确登录账号',
|
||||
userNameHit: '登录账号',
|
||||
passwordReg: '密码至少包含大小写字母、数字、特殊符号,且不少于6位',
|
||||
passwordPlease: '请输入正确登录密码',
|
||||
passwordHit: '登录密码',
|
||||
passwordConfirmHit: '确认登录密码',
|
||||
phoneReg: '手机号码不正确',
|
||||
phonePlease: '请输入正确的手机号码',
|
||||
phoneHit: '手机号码',
|
||||
codePlease: '请输入正确的验证码',
|
||||
codeHit: '验证码',
|
||||
codeText: '获取验证码',
|
||||
codeSmsSend: '发送成功,请注意查看短信',
|
||||
},
|
||||
|
||||
// 页面
|
||||
views: {
|
||||
login: {
|
||||
tabPane1: '账号密码登录',
|
||||
tabPane2: '手机号登录',
|
||||
registerBtn: '注册账号',
|
||||
loginBtn: '登录',
|
||||
loginSuccess: '登录成功',
|
||||
loginMethod: '其他登录方式:',
|
||||
loginMethodWX: '微信扫一扫登录',
|
||||
loginMethodQQ: 'QQ扫码登录',
|
||||
},
|
||||
register: {
|
||||
registerBtn: '注册',
|
||||
loginBtn: '使用已有账号登录',
|
||||
passwordErr: '请正确输入确认密码',
|
||||
passwordConfirmErr: '两次输入的密码不一致',
|
||||
tipContent: '恭喜您,{username} 账号注册成功!',
|
||||
tipBtn: '前往登录',
|
||||
},
|
||||
},
|
||||
};
|
||||
212
src/layouts/BasicLayout.vue
Normal file
212
src/layouts/BasicLayout.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ProLayout,
|
||||
GlobalFooter,
|
||||
WaterMark,
|
||||
getMenuData,
|
||||
clearMenuItem,
|
||||
} from '@ant-design-vue/pro-layout';
|
||||
import RightContent from './components/RightContent.vue';
|
||||
import Tabs from './components/Tabs.vue';
|
||||
import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import useLayoutStore from '@/store/modules/layout';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useRouterStore from '@/store/modules/router';
|
||||
import useTabsStore from '@/store/modules/tabs';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
|
||||
const { proConfig, waterMarkContent } = useLayoutStore();
|
||||
const { appName } = useAppStore();
|
||||
const routerStore = useRouterStore();
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
|
||||
/**菜单面板 */
|
||||
let layoutState = reactive({
|
||||
collapsed: false, // 是否展开菜单面板
|
||||
openKeys: ['/'], // 打开菜单key
|
||||
selectedKeys: ['/index'], // 选中高亮菜单key
|
||||
});
|
||||
|
||||
/**监听路由变化改变菜单面板选项 */
|
||||
watch(
|
||||
router.currentRoute,
|
||||
v => {
|
||||
const matched = v.matched.concat();
|
||||
layoutState.openKeys = matched
|
||||
.filter(r => r.path !== v.path)
|
||||
.map(r => r.path);
|
||||
layoutState.selectedKeys = matched
|
||||
.filter(r => r.name !== 'Root')
|
||||
.map(r => r.path);
|
||||
// 路由地址含有内嵌地址标识又是隐藏菜单需要处理打开高亮菜单栏
|
||||
if (v.path.includes(MENU_PATH_INLINE) && v.meta.hideInMenu) {
|
||||
const idx = v.path.lastIndexOf(MENU_PATH_INLINE);
|
||||
layoutState.openKeys.splice(-1);
|
||||
layoutState.selectedKeys[matched.length - 1] = v.path.slice(0, idx);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 动态路由添加到菜单面板
|
||||
const rootRoute = router.getRoutes().find(r => r.name === 'Root');
|
||||
if (rootRoute) {
|
||||
const children = routerStore.setRootRouterData(rootRoute.children);
|
||||
const buildRouterData = routerStore.buildRouterData;
|
||||
if (buildRouterData.length > 0) {
|
||||
rootRoute.children = children.concat(buildRouterData);
|
||||
} else {
|
||||
rootRoute.children = children;
|
||||
}
|
||||
}
|
||||
|
||||
const { menuData } = getMenuData(clearMenuItem(router.getRoutes()));
|
||||
|
||||
/**面包屑数据对象,排除根节点和首页不显示 */
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = router.currentRoute.value.matched.concat();
|
||||
// 菜单中隐藏子节点不显示面包屑
|
||||
if (matched.length == 2) {
|
||||
const hideChildInMenu = matched[0].meta?.hideChildInMenu || false;
|
||||
if (hideChildInMenu) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return matched
|
||||
.filter(r => !['Root', 'Index'].includes(r.name as string))
|
||||
.map(item => {
|
||||
return {
|
||||
path: item.path,
|
||||
breadcrumbName: item.meta.title || '-',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 给页面组件设置路由名称
|
||||
*
|
||||
* 路由名称设为缓存key
|
||||
* @param component 页面组件
|
||||
* @param name 路由名称
|
||||
*/
|
||||
function fnComponentSetName(component: any, to: any) {
|
||||
if (component && component.type) {
|
||||
// 通过路由取最后匹配的,确认是缓存的才处理
|
||||
const matched = to.matched.concat();
|
||||
const lastRoute = matched[matched.length - 1];
|
||||
if (!lastRoute.meta.cache) return component;
|
||||
const routeName = lastRoute.name;
|
||||
const routeDef = lastRoute.components.default;
|
||||
// 有命名但不是跳转的路由文件
|
||||
const __name = component.type.__name;
|
||||
if (__name && __name !== routeName) {
|
||||
routeDef.name = routeName;
|
||||
routeDef.__name = routeName;
|
||||
Reflect.set(component, 'type', routeDef);
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
// 清空导航栏标签
|
||||
tabsStore.clear();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WaterMark :content="waterMarkContent" :z-index="100">
|
||||
<ProLayout
|
||||
v-model:collapsed="layoutState.collapsed"
|
||||
v-model:selectedKeys="layoutState.selectedKeys"
|
||||
v-model:openKeys="layoutState.openKeys"
|
||||
:menu-data="menuData"
|
||||
:breadcrumb="{ routes: breadcrumb }"
|
||||
disable-content-margin
|
||||
v-bind="proConfig"
|
||||
:iconfont-url="scriptUrl"
|
||||
>
|
||||
<!--插槽-菜单头-->
|
||||
<template #menuHeaderRender>
|
||||
<RouterLink :to="{ name: 'Index' }" :replace="true">
|
||||
<img class="logo" src="@/assets/logo.png" />
|
||||
<h1>{{ appName }}</h1>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!--插槽-顶部左侧,只对side布局有效-->
|
||||
<template #headerContentRender></template>
|
||||
|
||||
<!--插槽-顶部右侧-->
|
||||
<template #rightContentRender>
|
||||
<RightContent />
|
||||
</template>
|
||||
|
||||
<!--插槽-导航标签项-->
|
||||
<template #tabRender="{ width, fixedHeader, headerRender }">
|
||||
<Tabs
|
||||
:width="width"
|
||||
:fixed-header="fixedHeader"
|
||||
:header-render="headerRender"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!--插槽-页面路由导航面包屑-->
|
||||
<template #breadcrumbRender="{ route, params, routes }">
|
||||
<span v-if="routes.indexOf(route) === routes.length - 1">
|
||||
{{ route.breadcrumbName }}
|
||||
</span>
|
||||
<RouterLink v-else :to="{ path: route.path, params }">
|
||||
{{ route.breadcrumbName }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!--内容页面视图-->
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<transition name="slide-left" mode="out-in">
|
||||
<KeepAlive :include="tabsStore.getCaches">
|
||||
<component
|
||||
:is="fnComponentSetName(Component, route)"
|
||||
:key="route.path"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
</RouterView>
|
||||
|
||||
<!--插槽-内容底部-->
|
||||
<template #footerRender>
|
||||
<GlobalFooter
|
||||
:links="[
|
||||
{
|
||||
blankTarget: true,
|
||||
title: '开发手册',
|
||||
href: 'https://juejin.cn/column/7188761626017792056',
|
||||
},
|
||||
{
|
||||
blankTarget: true,
|
||||
title: '开源仓库',
|
||||
href: 'https://gitee.com/TsMask/',
|
||||
},
|
||||
{
|
||||
blankTarget: true,
|
||||
title: '接口文档',
|
||||
href: 'https://mask-api-midwayjs.apifox.cn/',
|
||||
},
|
||||
]"
|
||||
copyright="Copyright © 2023 Gitee For TsMask"
|
||||
>
|
||||
</GlobalFooter>
|
||||
</template>
|
||||
</ProLayout>
|
||||
</WaterMark>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logo {
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
border-radius: 6.66px;
|
||||
}
|
||||
</style>
|
||||
7
src/layouts/BlankLayout.vue
Normal file
7
src/layouts/BlankLayout.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
// 承载目录下级菜单页面,需要声明才会生成name
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
47
src/layouts/LinkLayout.vue
Normal file
47
src/layouts/LinkLayout.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { isValid, decode } from 'js-base64';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { validHttp } from '@/utils/regular-utils';
|
||||
const route = useRoute();
|
||||
const height = ref<string>(document.documentElement.clientHeight - 94.5 + 'px');
|
||||
|
||||
let iframe = reactive({
|
||||
id: 'link',
|
||||
src: '',
|
||||
});
|
||||
|
||||
// 设置Frame窗口名称并设置链接地址
|
||||
if (route.name) {
|
||||
const name = route.name.toString();
|
||||
const pathArr = route.matched.concat().map(i => i.path);
|
||||
const pathLen = pathArr.length;
|
||||
let path = pathArr[pathLen - 1].replace(pathArr[pathLen - 2], '');
|
||||
path = path.substring(1);
|
||||
if (isValid(path)) {
|
||||
const url = decode(path);
|
||||
if (validHttp(url)) {
|
||||
iframe.src = url;
|
||||
} else {
|
||||
let endS = name.substring(4, 5).endsWith('s');
|
||||
iframe.src = `${endS ? 'https://' : 'http://'}${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
iframe.id = name;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="'height:' + height">
|
||||
<iframe
|
||||
:id="iframe.id"
|
||||
:src="iframe.src"
|
||||
frameborder="no"
|
||||
style="width: 100%; height: 100%"
|
||||
scrolling="auto"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
155
src/layouts/components/RightContent.vue
Normal file
155
src/layouts/components/RightContent.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
const { t, changeLocale } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
/**头像展开项点击 */
|
||||
function fnClick({ key }: MenuInfo) {
|
||||
switch (key) {
|
||||
case 'settings':
|
||||
router.push({ name: 'Settings' });
|
||||
break;
|
||||
case 'profile':
|
||||
router.push({ name: 'Profile' });
|
||||
break;
|
||||
case 'logout':
|
||||
userStore.fnLogOut().finally(() => router.push({ name: 'Login' }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**改变多语言 */
|
||||
function fnChangeLocale(e: any) {
|
||||
changeLocale(e.key);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-space :size="12" align="center">
|
||||
<a-popover
|
||||
overlayClassName="head-notice"
|
||||
placement="bottomRight"
|
||||
:trigger="['click']"
|
||||
>
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
<a-badge :count="123" :overflow-count="99">
|
||||
<BellOutlined :style="{ fontSize: '20px' }" />
|
||||
</a-badge>
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content :style="{ padding: 0 }">
|
||||
<a-tabs centered :tabBarStyle="{ width: '336px' }">
|
||||
<a-tab-pane key="1" tab="通知(41)">
|
||||
Content of Tab 通知
|
||||
<a-button type="link" block>查看更多</a-button>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="消息(231)">
|
||||
Content of Tab 消息
|
||||
<a-button type="link" block>查看更多</a-button>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="待办(1)">
|
||||
Content of Tab 待办
|
||||
<a-button type="link" block>查看更多</a-button>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
</a-popover>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>开源仓库</template>
|
||||
<a-button type="text" href="https://gitee.com/TsMask" target="_blank">
|
||||
<template #icon>
|
||||
<GithubOutlined :style="{ fontSize: '20px' }" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template #title>文档手册</template>
|
||||
<a-button
|
||||
type="text"
|
||||
href="https://juejin.cn/column/7188761626017792056"
|
||||
target="_blank"
|
||||
>
|
||||
<template #icon>
|
||||
<QuestionCircleOutlined :style="{ fontSize: '20px' }" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-dropdown :trigger="['click', 'hover']">
|
||||
<a-button size="small" type="default">
|
||||
{{ t('i18n') }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="fnChangeLocale">
|
||||
<a-menu-item key="zh_CN">中文</a-menu-item>
|
||||
<a-menu-item key="en_US">English</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-dropdown placement="bottomRight" :trigger="['click', 'hover']">
|
||||
<div class="user">
|
||||
<a-avatar
|
||||
shape="circle"
|
||||
size="default"
|
||||
:src="userStore.getAvatar"
|
||||
:alt="userStore.userName"
|
||||
></a-avatar>
|
||||
<span class="nick">
|
||||
{{ userStore.nickName }}
|
||||
</span>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu @click="fnClick">
|
||||
<a-menu-item key="profile">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
<span>个人中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
<span>个人设置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">
|
||||
<template #icon>
|
||||
<LogoutOutlined />
|
||||
</template>
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.nick {
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
font-size: 16px;
|
||||
max-width: 164px;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
187
src/layouts/components/Tabs.vue
Normal file
187
src/layouts/components/Tabs.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script lang="ts" setup>
|
||||
import IconFont from '@/components/IconFont/index.vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useTabsStore from '@/store/modules/tabs';
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
|
||||
defineProps({
|
||||
/**标签栏宽度 */
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
/**是否固定顶部栏 */
|
||||
fixedHeader: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**是否有顶部栏 */
|
||||
headerRender: {
|
||||
type: [Object, Function, Boolean],
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
/**导航标签项列表长度 */
|
||||
let tabLen = computed(() => tabsStore.getTabs.length);
|
||||
|
||||
/**
|
||||
* 标签更多菜单项
|
||||
* @param key 菜单key
|
||||
*/
|
||||
function fnTabMenu(key: string | number) {
|
||||
const route = router.currentRoute.value;
|
||||
// 刷新当前
|
||||
if (key === 'reload') {
|
||||
const name = (route.name && route.name.toString()) || '';
|
||||
tabsStore.cacheDelete(name);
|
||||
router.replace({
|
||||
path: `/redirect${route.path}`,
|
||||
query: route.query,
|
||||
});
|
||||
}
|
||||
// 关闭当前
|
||||
if (key === 'current') {
|
||||
const to = tabsStore.tabClose(route.path);
|
||||
if (!to) return;
|
||||
// 避免重复跳转
|
||||
if (route.path === to.path) {
|
||||
tabsStore.tabOpen(route);
|
||||
} else {
|
||||
router.push(to);
|
||||
}
|
||||
}
|
||||
// 关闭其他
|
||||
if (key === 'other') {
|
||||
tabsStore.clear();
|
||||
tabsStore.tabOpen(route);
|
||||
}
|
||||
// 关闭全部
|
||||
if (key === 'all') {
|
||||
tabsStore.clear();
|
||||
// 已经是首页的避免重复跳转,默认返回首页
|
||||
if (route.path === '/index') {
|
||||
tabsStore.tabOpen(route);
|
||||
} else {
|
||||
router.push({ name: 'Index' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航标签点击
|
||||
* @param path 标签的路由路径
|
||||
*/
|
||||
function fnTabClick(path: string) {
|
||||
const to = tabsStore.tabGoto(path);
|
||||
if (!to) return;
|
||||
router.push(to);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航标签关闭
|
||||
* @param path 标签的路由路径
|
||||
*/
|
||||
function fnTabClose(path: string) {
|
||||
const to = tabsStore.tabClose(path);
|
||||
if (!to) return;
|
||||
router.push(to);
|
||||
}
|
||||
|
||||
/**监听当前路由添加到导航标签列表 */
|
||||
watch(router.currentRoute, v => tabsStore.tabOpen(v), { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<header class="ant-layout-header tabs-header" v-if="fixedHeader"></header>
|
||||
<a-tabs
|
||||
class="tabs"
|
||||
:class="{ 'tabs-fixed': fixedHeader }"
|
||||
:style="{ width: width, top: headerRender === false ? 0 : undefined }"
|
||||
hide-add
|
||||
tab-position="top"
|
||||
type="editable-card"
|
||||
:tab-bar-gutter="8"
|
||||
:tab-bar-style="{ margin: '0', height: '28px', lineHeight: '28px' }"
|
||||
v-model:activeKey="tabsStore.activePath"
|
||||
@tab-click="path => fnTabClick(path as string)"
|
||||
@edit="path => fnTabClose(path as string)"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabsStore.getTabs"
|
||||
:key="tab.path"
|
||||
:closable="tabLen > 1"
|
||||
>
|
||||
<template #tab>
|
||||
<span>
|
||||
<IconFont :type="tab.icon" style="margin: 0"></IconFont>
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
|
||||
<template #rightExtra>
|
||||
<a-space :size="8" align="end">
|
||||
<a-tooltip placement="topRight">
|
||||
<template #title>刷新当前</template>
|
||||
<a-button
|
||||
type="ghost"
|
||||
shape="circle"
|
||||
size="small"
|
||||
@click="fnTabMenu('reload')"
|
||||
>
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="topRight">
|
||||
<template #title>更多选项</template>
|
||||
<a-dropdown :trigger="['click', 'hover']" placement="bottomRight">
|
||||
<a-button type="ghost" shape="circle" size="small">
|
||||
<template #icon><DownOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => fnTabMenu(key)">
|
||||
<a-menu-item key="current">关闭当前</a-menu-item>
|
||||
<a-menu-item key="other">关闭其他 </a-menu-item>
|
||||
<a-menu-item key="all">关闭全部</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tabs-header {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
z-index: 16;
|
||||
margin: 0px;
|
||||
padding: 4px 16px;
|
||||
width: calc(100% - 208px);
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px #0015291f;
|
||||
transition: background 0.3s, width 0.2s;
|
||||
position: relative;
|
||||
|
||||
&.tabs-fixed {
|
||||
right: 0px;
|
||||
top: 48px;
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs :deep(.ant-tabs-nav:before) {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
16
src/main.ts
Normal file
16
src/main.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
import store from './store';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import directive from './directive';
|
||||
import i18n from './i18n';
|
||||
import '@ant-design-vue/pro-layout/dist/style.css';
|
||||
import 'ant-design-vue/dist/antd.variable.min.css';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
app.use(directive);
|
||||
app.use(i18n);
|
||||
|
||||
app.mount('#app');
|
||||
17
src/plugins/auth-token.ts
Normal file
17
src/plugins/auth-token.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE } from '@/constants/token-constants';
|
||||
|
||||
/**获取cookis中Token字符串 */
|
||||
export function getToken(): string {
|
||||
return Cookies.get(TOKEN_COOKIE) || '';
|
||||
}
|
||||
|
||||
/**设置cookis中Token字符串 */
|
||||
export function setToken(token: string): void {
|
||||
Cookies.set(TOKEN_COOKIE, token);
|
||||
}
|
||||
|
||||
/**移除cookis中Token字符串 */
|
||||
export function removeToken(): void {
|
||||
Cookies.remove(TOKEN_COOKIE);
|
||||
}
|
||||
54
src/plugins/auth-user.ts
Normal file
54
src/plugins/auth-user.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ADMIN_PERMISSION, ADMIN_ROLE_KEY } from '@/constants/admin-constants';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
|
||||
/**
|
||||
* 只需含有其中权限
|
||||
* @param role 权限字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function hasPermissions(permissions: string[]): boolean {
|
||||
if (!permissions || permissions.length === 0) return false;
|
||||
const userPermissions = useUserStore().permissions;
|
||||
if (!userPermissions || userPermissions.length === 0) return false;
|
||||
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
|
||||
return permissions.some(p => userPermissions.some(up => up === p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时匹配其中权限
|
||||
* @param role 权限字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function matchPermissions(permissions: string[]): boolean {
|
||||
if (!permissions || permissions.length === 0) return false;
|
||||
const userPermissions = useUserStore().permissions;
|
||||
if (!userPermissions || userPermissions.length === 0) return false;
|
||||
if (userPermissions.includes(ADMIN_PERMISSION)) return true;
|
||||
return permissions.every(p => userPermissions.some(up => up === p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 只需含有其中角色
|
||||
* @param role 角色字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function hasRoles(roles: string[]): boolean {
|
||||
if (!roles || roles.length === 0) return false;
|
||||
const userRoles = useUserStore().roles;
|
||||
if (!userRoles || userRoles.length === 0) return false;
|
||||
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
|
||||
return roles.some(r => userRoles.some(ur => ur === r));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时匹配其中角色
|
||||
* @param role 角色字符数组
|
||||
* @returns true | false
|
||||
*/
|
||||
export function matchRoles(roles: string[]): boolean {
|
||||
if (!roles || roles.length === 0) return false;
|
||||
const userRoles = useUserStore().roles;
|
||||
if (!userRoles || userRoles.length === 0) return false;
|
||||
if (userRoles.includes(ADMIN_ROLE_KEY)) return true;
|
||||
return roles.every(r => userRoles.some(ur => ur === r));
|
||||
}
|
||||
270
src/plugins/http-fetch.ts
Normal file
270
src/plugins/http-fetch.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { getToken, removeToken } from '@/plugins/auth-token';
|
||||
import { sessionGetJSON, sessionSetJSON } from '@/utils/cache-session-utils';
|
||||
import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants';
|
||||
import { CACHE_SESSION_FATCH } from '@/constants/cache-keys-constants';
|
||||
import {
|
||||
APP_REQUEST_HEADER_CODE,
|
||||
APP_REQUEST_HEADER_VERSION,
|
||||
} from '@/constants/app-constants';
|
||||
|
||||
/**响应结果类型 */
|
||||
export type ResultType = {
|
||||
/**响应码 */
|
||||
code: number | 200 | 500;
|
||||
/**信息 */
|
||||
msg: string;
|
||||
/**数据 */
|
||||
data?: any;
|
||||
/**未知属性 */
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**防止重复提交类型 */
|
||||
type RepeatSubmitType = {
|
||||
/**请求地址 */
|
||||
url: string;
|
||||
/**请求数据 */
|
||||
data: string;
|
||||
/**请求时间 */
|
||||
time: number;
|
||||
};
|
||||
|
||||
/**请求参数类型 */
|
||||
type OptionsType = {
|
||||
/**请求地址 */
|
||||
url: string;
|
||||
/**请求方法 */
|
||||
method: 'get' | 'post' | 'put' | 'delete';
|
||||
/**请求头 */
|
||||
headers?: HeadersInit;
|
||||
/**地址栏参数 */
|
||||
params?: Record<string, string | number | boolean | undefined>;
|
||||
/**发送数据 */
|
||||
data?: Record<string, any> | FormData | object;
|
||||
/**请求数据类型 */
|
||||
dataType?: 'form-data' | 'json';
|
||||
/**响应数据类型 */
|
||||
responseType?: 'text' | 'json' | 'blob' | 'arrayBuffer';
|
||||
/**请求缓存策略 */
|
||||
cache?: RequestCache;
|
||||
/**请求的凭证,如 omit、same-origin、include */
|
||||
credentials?: RequestCredentials;
|
||||
/**请求体 */
|
||||
body?: BodyInit;
|
||||
/**防止数据重复提交 */
|
||||
repeatSubmit?: boolean;
|
||||
/**携带授权Token请求头 */
|
||||
whithToken?: boolean;
|
||||
};
|
||||
|
||||
/**全局配置类型 */
|
||||
type ConfigType = {
|
||||
/**请求的根域名地址-不带/后缀 */
|
||||
baseUrl: string;
|
||||
/**超时时间,毫秒 */
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
/**默认配置 */
|
||||
const FATCH_CONFIG: ConfigType = {
|
||||
baseUrl: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10 * 1000,
|
||||
};
|
||||
|
||||
/**默认请求参数 */
|
||||
const FATCH_OPTIONS: OptionsType = {
|
||||
url: '',
|
||||
method: 'get',
|
||||
headers: {
|
||||
[APP_REQUEST_HEADER_CODE]: import.meta.env.VITE_APP_CODE,
|
||||
[APP_REQUEST_HEADER_VERSION]: import.meta.env.VITE_APP_VERSION,
|
||||
// 使用mock.apifox.cn时开启
|
||||
// apifoxToken: '8zCzh3vipdEwd1ukv9lQEuTekdWIH7xN',
|
||||
},
|
||||
dataType: 'json',
|
||||
responseType: 'json',
|
||||
cache: 'no-cache',
|
||||
credentials: undefined,
|
||||
repeatSubmit: true,
|
||||
whithToken: true,
|
||||
};
|
||||
|
||||
/**请求前的拦截 */
|
||||
function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
|
||||
options.headers = Object.assign({}, options.headers);
|
||||
//console.log('请求前的拦截', options);
|
||||
|
||||
// 给发送数据类型设置请求头
|
||||
if (options.dataType === 'json') {
|
||||
Reflect.set(
|
||||
options.headers,
|
||||
'content-type',
|
||||
'application/json;charset=utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
// 是否需要设置 token
|
||||
const token = getToken();
|
||||
if (options.whithToken && token) {
|
||||
Reflect.set(options.headers, TOKEN_KEY, TOKEN_KEY_PREFIX + token);
|
||||
}
|
||||
// 是否需要防止数据重复提交
|
||||
if (
|
||||
options.repeatSubmit &&
|
||||
options.dataType === 'json' &&
|
||||
['post', 'put'].includes(options.method)
|
||||
) {
|
||||
const requestObj: RepeatSubmitType = {
|
||||
url: options.url,
|
||||
data: JSON.stringify(options.data),
|
||||
time: Date.now(),
|
||||
};
|
||||
const sessionObj: RepeatSubmitType = sessionGetJSON(CACHE_SESSION_FATCH);
|
||||
if (sessionObj) {
|
||||
const { url, data, time } = sessionObj;
|
||||
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
|
||||
if (
|
||||
requestObj.url === url &&
|
||||
requestObj.data === data &&
|
||||
requestObj.time - time < interval
|
||||
) {
|
||||
const message = '数据正在处理,请勿重复提交';
|
||||
console.warn(`[${url}]: ${message}`);
|
||||
return Promise.reject(message);
|
||||
} else {
|
||||
sessionSetJSON(CACHE_SESSION_FATCH, requestObj);
|
||||
}
|
||||
} else {
|
||||
sessionSetJSON(CACHE_SESSION_FATCH, requestObj);
|
||||
}
|
||||
}
|
||||
|
||||
// get请求拼接地址栏参数
|
||||
if (options.method === 'get' && options.params) {
|
||||
let paramStr = '';
|
||||
const params = options.params;
|
||||
for (const key in params) {
|
||||
const value = params[key];
|
||||
// 空字符或未定义的值不作为参数发送
|
||||
if (value === '' || value === undefined) continue;
|
||||
paramStr += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
if (paramStr && paramStr.startsWith('&')) {
|
||||
options.url = `${options.url}?${paramStr.substring(1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 非get参数提交
|
||||
if (options.data instanceof FormData) {
|
||||
options.body = options.data;
|
||||
} else {
|
||||
options.body = JSON.stringify(options.data);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**请求后的拦截 */
|
||||
function interceptorResponse(res: ResultType): ResultType | Promise<any> {
|
||||
//console.log('请求后的拦截', res);
|
||||
// 登录失效时,移除授权令牌并重新刷新页面
|
||||
if (res.code === 401) {
|
||||
removeToken();
|
||||
window.location.reload();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求http
|
||||
*
|
||||
* @param options 请求参数
|
||||
*
|
||||
* responseType改变响应结果类型
|
||||
* @returns 返回 Promise<ResultType>
|
||||
*/
|
||||
export async function request(options: OptionsType): Promise<ResultType> {
|
||||
// 请求超时控制请求终止
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort(); // 终止请求
|
||||
}, FATCH_CONFIG.timeout);
|
||||
|
||||
options = Object.assign({ signal }, FATCH_OPTIONS, options);
|
||||
|
||||
// 检查请求拦截
|
||||
const beforeReq = beforeRequest(options);
|
||||
if (beforeReq instanceof Promise) {
|
||||
return await beforeReq;
|
||||
}
|
||||
options = beforeReq;
|
||||
|
||||
// 判断用户传递的URL是否http或/开头
|
||||
if (!options.url.startsWith('http')) {
|
||||
const uri = options.url.startsWith('/') ? options.url : `/${options.url}`;
|
||||
options.url = FATCH_CONFIG.baseUrl + uri;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(options.url, options);
|
||||
// console.log('请求结果:', res);
|
||||
if (res.status === 500) {
|
||||
return {
|
||||
code: 500,
|
||||
msg: '服务器连接出错!',
|
||||
};
|
||||
}
|
||||
|
||||
// 根据响应数据类型返回
|
||||
switch (options.responseType) {
|
||||
case 'text': // 文本数据
|
||||
const str = await res.text();
|
||||
return {
|
||||
code: 200,
|
||||
msg: str,
|
||||
};
|
||||
case 'json': // json格式数据
|
||||
const result = await res.json();
|
||||
// 请求后的拦截
|
||||
const beforeRes = interceptorResponse(result);
|
||||
if (beforeRes instanceof Promise) {
|
||||
return await beforeRes;
|
||||
}
|
||||
return result;
|
||||
case 'blob': // 二进制数据则直接返回
|
||||
case 'arrayBuffer':
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.startsWith('application/json')) {
|
||||
const result = await res.json();
|
||||
return result as ResultType;
|
||||
}
|
||||
const data =
|
||||
options.responseType === 'blob'
|
||||
? await res.blob()
|
||||
: await res.arrayBuffer();
|
||||
return {
|
||||
code: 200,
|
||||
msg: '成功',
|
||||
data: data,
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
code: 500,
|
||||
msg: '未知响应数据类型',
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 请求被终止时
|
||||
if (error.name === 'AbortError') {
|
||||
return {
|
||||
code: 500,
|
||||
msg: '网络连接超时!',
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId); // 请求成功,清除超时计时器
|
||||
}
|
||||
}
|
||||
289
src/router/index.ts
Normal file
289
src/router/index.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
RouteRecordRaw,
|
||||
} from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import BasicLayout from '../layouts/BasicLayout.vue';
|
||||
import BlankLayout from '../layouts/BlankLayout.vue';
|
||||
import LinkLayout from '../layouts/LinkLayout.vue';
|
||||
import { encode } from 'js-base64';
|
||||
import { getToken } from '@/plugins/auth-token';
|
||||
import { validHttp } from '@/utils/regular-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useRouterStore from '@/store/modules/router';
|
||||
|
||||
// NProgress Configuration
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
||||
// import { MetaRecord, MenuDataItem } from '@ant-design-vue/pro-layout';
|
||||
// mate数据类型 MetaRecord
|
||||
// 根据/路径构建菜单列表,列表项类型 MenuDataItem
|
||||
// https://github.com/vueComponent/pro-components/blob/a19279f3a28190bf11e8c36f316c92dbd3387a6d/packages/pro-layout/src/typings.ts#L16
|
||||
// 菜单图标来源 https://ant.design/components/icon 自定义iconfont
|
||||
|
||||
/**公共路由 */
|
||||
const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Root',
|
||||
meta: { title: '根节点' },
|
||||
component: BasicLayout,
|
||||
redirect: '/index',
|
||||
children: [
|
||||
{
|
||||
path: '/index',
|
||||
name: 'Index',
|
||||
meta: { title: '首页', icon: 'icon-pcduan', cache: true },
|
||||
component: () => import('@/views/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dome1',
|
||||
name: 'Dome1',
|
||||
meta: { title: '示例一', icon: 'icon-ios' },
|
||||
component: () => import('@/views/dome/dome1.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dome2',
|
||||
name: 'Dome2',
|
||||
meta: { title: '示例二', icon: 'icon-anzhuo' },
|
||||
component: () => import('@/views/dome/dome2.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dome3',
|
||||
name: 'Dome3',
|
||||
meta: { title: '示例三', icon: 'icon-qunzhu' },
|
||||
component: () => import('@/views/dome/dome3.vue'),
|
||||
},
|
||||
{
|
||||
path: '/domes',
|
||||
name: 'Domes',
|
||||
meta: {
|
||||
title: '示例目录',
|
||||
icon: 'icon-zhizuoliucheng',
|
||||
},
|
||||
component: BlankLayout,
|
||||
redirect: () => ({ name: 'PageInfo' }),
|
||||
children: [
|
||||
{
|
||||
path: 'page-info',
|
||||
name: 'PageInfo',
|
||||
meta: { title: '页面信息', icon: 'icon-huifu' },
|
||||
component: () => import('../views/domes/page-info.vue'),
|
||||
},
|
||||
{
|
||||
path: 'page-typography',
|
||||
name: 'PageTypography',
|
||||
meta: { title: '文本信息', icon: 'icon-huizhiguize' },
|
||||
component: () => import('../views/domes/page-typography.vue'),
|
||||
},
|
||||
{
|
||||
path: 'dynamic-match/:id(\\d+)',
|
||||
name: 'DynamicMatch',
|
||||
// 路由 path 默认参数再 meta.params 里
|
||||
meta: { title: '动态参数页面', params: { id: 1 }, cache: true },
|
||||
component: () => import('../views/domes/dynamic-match.vue'),
|
||||
},
|
||||
{
|
||||
path: 'disabled',
|
||||
name: 'Disabled',
|
||||
meta: { title: '禁止点击', disabled: true },
|
||||
component: () => {},
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/TsMask',
|
||||
name: 'BlankGithubTsMask',
|
||||
meta: {
|
||||
title: 'TsMask-打开新窗',
|
||||
icon: 'icon-github',
|
||||
target: '_blank',
|
||||
},
|
||||
component: () => {},
|
||||
},
|
||||
{
|
||||
path: encode('https://www.antdv.com/components/comment-cn'),
|
||||
name: 'HttpsAntDesignVue',
|
||||
meta: {
|
||||
title: 'Antdv-内嵌窗口',
|
||||
icon: 'icon-morentouxiang',
|
||||
target: null,
|
||||
},
|
||||
component: LinkLayout,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/',
|
||||
name: 'BlankGithub',
|
||||
meta: {
|
||||
title: 'Github-打开新窗',
|
||||
icon: 'icon-github',
|
||||
target: '_blank',
|
||||
},
|
||||
component: () => {},
|
||||
},
|
||||
{
|
||||
path: 'https://www.antdv.com/components/comment-cn?sdf=12321&id=12&sdnf',
|
||||
name: 'SelfAnt Design Vue',
|
||||
meta: {
|
||||
title: 'Antdv-当前窗口',
|
||||
icon: 'icon-morentouxiang',
|
||||
target: '_self',
|
||||
},
|
||||
component: LinkLayout,
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'Account',
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
},
|
||||
component: BlankLayout,
|
||||
redirect: '/account/profile',
|
||||
children: [
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'Profile',
|
||||
meta: { title: '个人信息', cache: true },
|
||||
component: () => import('@/views/account/profile.vue'),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
meta: { title: '个人设置', cache: true },
|
||||
component: () => import('@/views/account/settings.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
meta: { title: '登录' },
|
||||
component: () => import('@/views/login.vue'),
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
meta: { title: '注册' },
|
||||
component: () => import('@/views/register.vue'),
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'NotPermission',
|
||||
meta: { title: '没有访问权限' },
|
||||
component: () => import('@/views/error/403.vue'),
|
||||
},
|
||||
{
|
||||
path: '/redirect',
|
||||
name: 'Redirect',
|
||||
meta: { title: '重定向' },
|
||||
component: BasicLayout,
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path(.*)',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
meta: { title: '找不到匹配页面' },
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
// 根据.env配置获取是否带井号和基础路径
|
||||
const hasHash = import.meta.env.VITE_HISTORY_HASH;
|
||||
const bashUrl = import.meta.env.VITE_HISTORY_BASE_URL;
|
||||
|
||||
/**全局路由 */
|
||||
const router = createRouter({
|
||||
history:
|
||||
hasHash === 'true'
|
||||
? createWebHashHistory(bashUrl)
|
||||
: createWebHistory(bashUrl),
|
||||
routes: constantRoutes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
} else {
|
||||
return { top: 0 };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**全局路由-后置守卫 */
|
||||
router.afterEach((to, from, failure) => {
|
||||
NProgress.done();
|
||||
// 设置标题
|
||||
if (to.meta?.title) {
|
||||
useAppStore().setTitle(to.meta.title);
|
||||
}
|
||||
});
|
||||
|
||||
/**无Token可访问页面地址白名单 */
|
||||
const WHITE_LIST: string[] = ['/login', '/auth-redirect', '/bind', '/register'];
|
||||
|
||||
/**全局路由-前置守卫 */
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start();
|
||||
const token = getToken();
|
||||
|
||||
// 没有token
|
||||
if (!token) {
|
||||
if (WHITE_LIST.includes(to.path)) {
|
||||
// 在免登录白名单,直接进入
|
||||
next();
|
||||
} else {
|
||||
// 否则全部重定向到登录页
|
||||
next(`/login?redirect=${to.fullPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 有Token
|
||||
if (token) {
|
||||
// 防止重复访问登录页面
|
||||
if (to.path === '/login') {
|
||||
next({ name: 'Index' });
|
||||
} else {
|
||||
// 判断当前用户是否有角色信息
|
||||
const user = useUserStore();
|
||||
if (user.roles && user.roles.length === 0) {
|
||||
// 获取用户信息
|
||||
user
|
||||
.fnGetInfo()
|
||||
.then(() => {
|
||||
return useRouterStore().generateRoutes();
|
||||
})
|
||||
.then(accessRoutes => {
|
||||
// 根据后台配置生成可访问的路由表
|
||||
if (accessRoutes && accessRoutes.length !== 0) {
|
||||
for (const route of accessRoutes) {
|
||||
// 动态添加可访问路由表,http开头会异常
|
||||
if (!validHttp(route.path)) {
|
||||
router.addRoute(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 刷新替换原先路由,确保addRoutes已完成
|
||||
next({ ...to, replace: true });
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(`[${to.path}]: ${e.message}`);
|
||||
user.fnLogOut().finally(() => {
|
||||
next({ name: 'Login' });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
5
src/store/index.ts
Normal file
5
src/store/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
export default store;
|
||||
31
src/store/modules/app.ts
Normal file
31
src/store/modules/app.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**应用参数类型 */
|
||||
type AppStore = {
|
||||
/**应用名称 */
|
||||
appName: string;
|
||||
/**应用标识 */
|
||||
appCode: string;
|
||||
/**应用版本 */
|
||||
appVersion: string;
|
||||
};
|
||||
|
||||
const useAppStore = defineStore('app', {
|
||||
state: (): AppStore => ({
|
||||
appName: import.meta.env.VITE_APP_NAME,
|
||||
appCode: import.meta.env.VITE_APP_CODE,
|
||||
appVersion: import.meta.env.VITE_APP_VERSION,
|
||||
}),
|
||||
actions: {
|
||||
/**设置网页标题 */
|
||||
setTitle(title?: string) {
|
||||
if (title) {
|
||||
document.title = `${title} - ${this.appName}`;
|
||||
} else {
|
||||
document.title = this.appName;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useAppStore;
|
||||
63
src/store/modules/dict.ts
Normal file
63
src/store/modules/dict.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { getDictDataType } from '@/api/system/dict/data';
|
||||
|
||||
/**字典参数类型 */
|
||||
type DictStore = {
|
||||
/**字典数据 */
|
||||
dicts: Map<string, DictType[]>;
|
||||
};
|
||||
|
||||
const useDictStore = defineStore('dict', {
|
||||
state: (): DictStore => ({
|
||||
dicts: new Map(),
|
||||
}),
|
||||
actions: {
|
||||
/**清空字典 */
|
||||
clearDict() {
|
||||
this.dicts.clear();
|
||||
},
|
||||
/**删除字典 */
|
||||
removeDict(key: string) {
|
||||
if (!key) return;
|
||||
return this.dicts.delete(key);
|
||||
},
|
||||
/**
|
||||
* 处理字典数据对象用于回显标签
|
||||
* @param data 字典数据项
|
||||
* @returns
|
||||
*/
|
||||
parseDataDict(data: Record<string, any>) {
|
||||
return [
|
||||
{
|
||||
label: data.dictLabel,
|
||||
value: data.dictValue,
|
||||
elTagType: data.tagType,
|
||||
elTagClass: data.tagClass,
|
||||
},
|
||||
];
|
||||
},
|
||||
/**获取字典 */
|
||||
async getDict(key: string) {
|
||||
if (!key) return [];
|
||||
let disct = this.dicts.get(key);
|
||||
if (disct === undefined || disct.length === 0) {
|
||||
const res = await getDictDataType(key);
|
||||
if (res.code === 200 && Array.isArray(res.data)) {
|
||||
const dictData: DictType[] = res.data.map(d => ({
|
||||
label: d.dictLabel,
|
||||
value: d.dictValue,
|
||||
elTagType: d.tagType,
|
||||
elTagClass: d.tagClass,
|
||||
}));
|
||||
this.dicts.set(key, dictData);
|
||||
disct = dictData;
|
||||
} else {
|
||||
disct = [];
|
||||
}
|
||||
}
|
||||
return disct;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useDictStore;
|
||||
91
src/store/modules/layout.ts
Normal file
91
src/store/modules/layout.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { CACHE_LOCAL_PROCONFIG } from '@/constants/cache-keys-constants';
|
||||
import { localGetJSON, localSetJSON } from '@/utils/cache-local-utils';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**布局参数类型 */
|
||||
type LayoutStore = {
|
||||
/**布局设置抽屉显示 */
|
||||
visible: boolean;
|
||||
/**布局配置 */
|
||||
proConfig: {
|
||||
/**导航布局 */
|
||||
layout: 'side' | 'top' | 'mix';
|
||||
/**导航菜单主题色 */
|
||||
navTheme: 'dark' | 'light';
|
||||
/**顶部导航主题,仅导航布局为mix时生效 */
|
||||
headerTheme: 'dark' | 'light';
|
||||
/**固定顶部栏 */
|
||||
fixedHeader: boolean;
|
||||
/**固定菜单栏 */
|
||||
fixSiderbar: boolean;
|
||||
/**自动分割菜单 */
|
||||
splitMenus: boolean;
|
||||
/**内容区域-顶栏 */
|
||||
headerRender: any | boolean | undefined;
|
||||
/**内容区域-页脚 */
|
||||
footerRender: any | boolean | undefined;
|
||||
/**内容区域-菜单头 */
|
||||
menuHeaderRender: any | boolean | undefined;
|
||||
/**内容区域-导航标签项 */
|
||||
tabRender: any | boolean | undefined;
|
||||
};
|
||||
/**水印内容 */
|
||||
waterMarkContent: string;
|
||||
};
|
||||
|
||||
/**判断是否关闭内容区域 */
|
||||
const proRender = (render: any) => (render === false ? false : undefined);
|
||||
|
||||
/**本地缓存-布局配置设置 */
|
||||
const proConfigLocal: LayoutStore['proConfig'] = localGetJSON(
|
||||
CACHE_LOCAL_PROCONFIG
|
||||
) || {
|
||||
layout: 'side',
|
||||
headerTheme: 'light',
|
||||
navTheme: 'light',
|
||||
fixSiderbar: true,
|
||||
fixedHeader: true,
|
||||
splitMenus: true,
|
||||
};
|
||||
|
||||
const useLayoutStore = defineStore('layout', {
|
||||
state: (): LayoutStore => ({
|
||||
visible: false,
|
||||
proConfig: {
|
||||
layout: proConfigLocal.layout,
|
||||
navTheme: proConfigLocal.navTheme,
|
||||
headerTheme: proConfigLocal.headerTheme,
|
||||
fixedHeader: Boolean(proConfigLocal.fixedHeader),
|
||||
fixSiderbar: Boolean(proConfigLocal.fixSiderbar),
|
||||
splitMenus: Boolean(proConfigLocal.splitMenus),
|
||||
headerRender: proRender(proConfigLocal.headerRender),
|
||||
footerRender: proRender(proConfigLocal.footerRender),
|
||||
menuHeaderRender: proRender(proConfigLocal.menuHeaderRender),
|
||||
tabRender: proRender(proConfigLocal.tabRender),
|
||||
},
|
||||
waterMarkContent: import.meta.env.VITE_APP_NAME,
|
||||
}),
|
||||
actions: {
|
||||
/**改变显示状态 */
|
||||
changeVisibleLayoutSetting() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
/**修改水印文字 */
|
||||
changeWaterMark(text: string) {
|
||||
this.waterMarkContent = text;
|
||||
},
|
||||
/**修改布局设置 */
|
||||
changeConf(key: string, value: boolean | string | number | undefined) {
|
||||
if (Reflect.has(this.proConfig, key)) {
|
||||
// 同时修改mix混合菜单的导航主题
|
||||
if (key === 'navTheme') {
|
||||
Reflect.set(this.proConfig, 'headerTheme', value);
|
||||
}
|
||||
Reflect.set(this.proConfig, key, value);
|
||||
localSetJSON(CACHE_LOCAL_PROCONFIG, this.proConfig);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useLayoutStore;
|
||||
152
src/store/modules/router.ts
Normal file
152
src/store/modules/router.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
RouteComponent,
|
||||
RouteLocationRaw,
|
||||
RouteMeta,
|
||||
RouteRecordRaw,
|
||||
} from 'vue-router';
|
||||
import { getRouters } from '@/api/router';
|
||||
import BasicLayout from '@/layouts/BasicLayout.vue';
|
||||
import BlankLayout from '@/layouts/BlankLayout.vue';
|
||||
import LinkLayout from '@/layouts/LinkLayout.vue';
|
||||
import {
|
||||
MENU_COMPONENT_LAYOUT_BASIC,
|
||||
MENU_COMPONENT_LAYOUT_BLANK,
|
||||
MENU_COMPONENT_LAYOUT_LINK,
|
||||
} from '@/constants/menu-constants';
|
||||
|
||||
/**路由构建参数类型 */
|
||||
type RouterStore = {
|
||||
/**初始的根路由数据 */
|
||||
rootRouterData: RouteRecordRaw[];
|
||||
/**动态路由数据 */
|
||||
buildRouterData: RouteRecordRaw[];
|
||||
};
|
||||
|
||||
const useRouterStore = defineStore('router', {
|
||||
state: (): RouterStore => ({
|
||||
rootRouterData: [],
|
||||
buildRouterData: [],
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
* 记录初始根节点菜单数据
|
||||
* @param data 初始数据
|
||||
* @returns 初始数据
|
||||
*/
|
||||
setRootRouterData(data: RouteRecordRaw[]) {
|
||||
if (this.rootRouterData.length <= 0) {
|
||||
this.rootRouterData = data;
|
||||
}
|
||||
return this.rootRouterData;
|
||||
},
|
||||
/**
|
||||
* 动态路由列表数据生成
|
||||
* @returns 生成的路由菜单
|
||||
*/
|
||||
async generateRoutes() {
|
||||
const res = await getRouters();
|
||||
if (res.code === 200 && Array.isArray(res.data)) {
|
||||
const buildRoutes = buildRouters(res.data.concat());
|
||||
this.buildRouterData = buildRoutes;
|
||||
return buildRoutes;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**异步路由类型 */
|
||||
type RecordRaws = {
|
||||
path: string;
|
||||
name: string;
|
||||
meta: RouteMeta;
|
||||
redirect: RouteLocationRaw;
|
||||
component: string;
|
||||
children: RecordRaws[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建动态路由
|
||||
*
|
||||
* 遍历后台配置的路由菜单,转换为组件路由菜单
|
||||
*
|
||||
* @param recordRaws 异步路由列表
|
||||
* @returns 可添加的路由列表
|
||||
*/
|
||||
function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] {
|
||||
const routers: RouteRecordRaw[] = [];
|
||||
for (const item of recordRaws) {
|
||||
// 路由页面组件
|
||||
let component: RouteComponent = {};
|
||||
if (item.component) {
|
||||
const comp = item.component;
|
||||
if (comp === MENU_COMPONENT_LAYOUT_BASIC) {
|
||||
component = BasicLayout;
|
||||
} else if (comp === MENU_COMPONENT_LAYOUT_BLANK) {
|
||||
component = BlankLayout;
|
||||
} else if (comp === MENU_COMPONENT_LAYOUT_LINK) {
|
||||
component = LinkLayout;
|
||||
} else {
|
||||
// 指定页面视图,一般用于显示子菜单
|
||||
component = findView(comp);
|
||||
}
|
||||
}
|
||||
|
||||
// 有子菜单进行递归
|
||||
let children: RouteRecordRaw[] = [];
|
||||
if (item.children && item.children.length > 0) {
|
||||
children = buildRouters(item.children);
|
||||
}
|
||||
|
||||
// 对元数据特殊参数进行处理
|
||||
let metaIcon = (item.meta?.icon as string) || '';
|
||||
if (!metaIcon.startsWith('icon-')) {
|
||||
metaIcon = '';
|
||||
}
|
||||
item.meta = Object.assign(item.meta, {
|
||||
icon: metaIcon,
|
||||
});
|
||||
|
||||
// 构建路由
|
||||
const router: RouteRecordRaw = {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
meta: item.meta,
|
||||
redirect: item.redirect,
|
||||
component: component,
|
||||
children: children,
|
||||
};
|
||||
routers.push(router);
|
||||
}
|
||||
return routers;
|
||||
}
|
||||
|
||||
/**匹配views里面所有的.vue或.tsx文件 */
|
||||
const views = import.meta.glob('./../../views/**/*.{vue,tsx}');
|
||||
|
||||
/**
|
||||
* 查找页面模块
|
||||
*
|
||||
* 查找 `/views/system/menu/index.vue` 或 `/views/system/menu/index.tsx`
|
||||
*
|
||||
* 参数值为 `system/menu/index`
|
||||
*
|
||||
* @param dirName 组件路径
|
||||
* @returns 路由懒加载函数
|
||||
*/
|
||||
function findView(dirName: string) {
|
||||
for (const dir in views) {
|
||||
let viewDirName = '';
|
||||
const component = dir.match(/views\/(.+)\.(vue|tsx)/);
|
||||
if (component && component.length === 3) {
|
||||
viewDirName = component[1];
|
||||
}
|
||||
if (viewDirName === dirName) {
|
||||
return () => views[dir]();
|
||||
}
|
||||
}
|
||||
return () => import('@/views/error/404.vue');
|
||||
}
|
||||
|
||||
export default useRouterStore;
|
||||
189
src/store/modules/tabs.ts
Normal file
189
src/store/modules/tabs.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import type { LocationQuery, RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
/**导航标签栏类型 */
|
||||
type TabsStore = {
|
||||
/**标签列表 */
|
||||
tabs: TabType[];
|
||||
/**激活标签项 */
|
||||
activePath: string;
|
||||
/**缓存页面路由名称 */
|
||||
caches: Set<string>;
|
||||
};
|
||||
|
||||
/**标签信息类型 */
|
||||
type TabType = {
|
||||
path: string;
|
||||
query: LocationQuery;
|
||||
name: string;
|
||||
title: string;
|
||||
icon?: any;
|
||||
cache?: boolean;
|
||||
};
|
||||
|
||||
const useTabsStore = defineStore('tabs', {
|
||||
state: (): TabsStore => ({
|
||||
tabs: [],
|
||||
activePath: '',
|
||||
caches: new Set(),
|
||||
}),
|
||||
getters: {
|
||||
/**获取导航标签栏列表 */
|
||||
getTabs(state) {
|
||||
return state.tabs;
|
||||
},
|
||||
/**获取缓存页面名 */
|
||||
getCaches(state) {
|
||||
return [...state.caches];
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**清空标签项和缓存项列表 */
|
||||
clear() {
|
||||
this.tabs = [];
|
||||
this.caches.clear();
|
||||
},
|
||||
/**
|
||||
* 删除标签项
|
||||
* @param path 当期标签路由地址
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
remove(path: string) {
|
||||
if (!path) return false;
|
||||
const tabIndex = this.tabs.findIndex(tab => tab.path === path);
|
||||
if (tabIndex === -1) return false;
|
||||
// 同名称标签只剩一个时,才移除缓存
|
||||
const name = this.tabs[tabIndex].name;
|
||||
const tabs = this.tabs.filter(tab => tab.name === name);
|
||||
if (tabs.length <= 1) {
|
||||
this.cacheDelete(name);
|
||||
}
|
||||
this.tabs.splice(tabIndex, 1);
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* 添加标签项
|
||||
* @param tab 标签信息对象
|
||||
* @param index 插入指定位置,默认加到最后
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
add(tab: TabType, index?: number) {
|
||||
const { path, query, name, title, icon, cache } = tab;
|
||||
// 是否缓存
|
||||
if (cache) {
|
||||
this.cacheAdd(name);
|
||||
}
|
||||
// 获取没有才添加
|
||||
let tabIndex = this.tabs.findIndex(tab => tab.path === path);
|
||||
if (tabIndex >= 0) return false;
|
||||
const idx = index ? index : this.tabs.length;
|
||||
this.tabs.splice(idx, 0, { path, query, name, title, icon });
|
||||
return true;
|
||||
},
|
||||
/**添加缓存项
|
||||
* @param name 路由名称
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
cacheAdd(name: string) {
|
||||
if (!name) return;
|
||||
const has = this.caches.has(name);
|
||||
if (has) return;
|
||||
this.caches.add(name);
|
||||
},
|
||||
/**
|
||||
* 删除缓存项
|
||||
* @param name 路由名称
|
||||
* @returns 布尔 true/false
|
||||
*/
|
||||
cacheDelete(name: string) {
|
||||
if (!name) return false;
|
||||
const has = this.caches.has(name);
|
||||
if (!has) return false;
|
||||
return this.caches.delete(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开标签
|
||||
*
|
||||
* 动态参数会开新标签,这是考虑多信息查看才没用同一个标签打开。
|
||||
* @param raw 跳转的路由信息
|
||||
* @returns 无
|
||||
*/
|
||||
tabOpen(raw: RouteLocationNormalizedLoaded) {
|
||||
// 刷新是重定向不记录
|
||||
if (raw.path.startsWith('/redirect')) return;
|
||||
// 标签缓存使用路由名称
|
||||
const name = (raw.name && raw.name.toString()) || '-';
|
||||
// 新增到当期标签后面打开,获取当期标签下标
|
||||
const tabIndex = this.tabs.findIndex(tab => tab.path === this.activePath);
|
||||
this.add(
|
||||
{
|
||||
path: raw.path,
|
||||
query: raw.query,
|
||||
name: name,
|
||||
title: raw.meta.title || '-',
|
||||
icon: raw.meta.icon || '#',
|
||||
cache: Boolean(raw.meta.cache),
|
||||
},
|
||||
tabIndex + 1
|
||||
);
|
||||
// 设置激活项
|
||||
this.activePath = raw.path;
|
||||
},
|
||||
/**
|
||||
* 关闭标签
|
||||
* @param path 当期标签路由地址
|
||||
* @returns 新跳转push路由参数
|
||||
*/
|
||||
tabClose(path: string) {
|
||||
if (!path) return null;
|
||||
// 获取当前项和最后项下标
|
||||
const tabIndex = this.tabs.findIndex(tab => tab.path === path);
|
||||
if (tabIndex === -1) return null;
|
||||
const lastIndex = this.tabs.length - 1;
|
||||
let to = null;
|
||||
// 只有一项默认跳首页
|
||||
if (lastIndex === 0) {
|
||||
to = {
|
||||
path: '/index',
|
||||
query: {},
|
||||
};
|
||||
}
|
||||
// 关闭当期标签,操作第一项跳后一项
|
||||
else if (path === this.activePath && tabIndex === 0) {
|
||||
const tab = this.tabs[tabIndex + 1];
|
||||
to = {
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
};
|
||||
}
|
||||
// 关闭当期标签,默认跳前一项
|
||||
else if (path === this.activePath && tabIndex <= lastIndex) {
|
||||
const tab = this.tabs[tabIndex - 1];
|
||||
to = {
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
};
|
||||
}
|
||||
// 移除标签
|
||||
this.remove(path);
|
||||
return to;
|
||||
},
|
||||
/**
|
||||
* 跳转标签
|
||||
* @param path 当期标签路由地址
|
||||
* @returns 新跳转push路由参数
|
||||
*/
|
||||
tabGoto(path: string) {
|
||||
if (!path) return null;
|
||||
const tab = this.tabs.find(tab => tab.path === path);
|
||||
if (!tab) return null;
|
||||
return {
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useTabsStore;
|
||||
171
src/store/modules/user.ts
Normal file
171
src/store/modules/user.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import defaultAvatar from '@/assets/images/default_avatar.png';
|
||||
import useLayoutStore from './layout';
|
||||
import { login, logout, getInfo } from '@/api/login';
|
||||
import { getToken, setToken, removeToken } from '@/plugins/auth-token';
|
||||
import { defineStore } from 'pinia';
|
||||
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
|
||||
import { validHttp } from '@/utils/regular-utils';
|
||||
|
||||
/**用户信息类型 */
|
||||
type UserInfo = {
|
||||
/**授权凭证 */
|
||||
token: string;
|
||||
/**登录账号 */
|
||||
userName: string;
|
||||
/**用户角色 字符串数组 */
|
||||
roles: string[];
|
||||
/**用户权限 字符串数组 */
|
||||
permissions: string[];
|
||||
/**用户头像 */
|
||||
avatar: string;
|
||||
/**用户昵称 */
|
||||
nickName: string;
|
||||
/**用户手机号 */
|
||||
phonenumber: string;
|
||||
/**用户邮箱 */
|
||||
email: string;
|
||||
/**用户性别 */
|
||||
sex: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式解析头像地址
|
||||
* @param avatar 头像路径
|
||||
* @returns url地址
|
||||
*/
|
||||
function parseAvatar(avatar: string): string {
|
||||
if (!avatar) {
|
||||
return defaultAvatar;
|
||||
}
|
||||
if (validHttp(avatar)) {
|
||||
return avatar;
|
||||
}
|
||||
const baseApi = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${baseApi}${avatar}`;
|
||||
}
|
||||
|
||||
const useUserStore = defineStore('user', {
|
||||
state: (): UserInfo => ({
|
||||
token: getToken(),
|
||||
userName: '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
avatar: '',
|
||||
nickName: '',
|
||||
phonenumber: '',
|
||||
email: '',
|
||||
sex: undefined,
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
* 获取正确头像地址
|
||||
* @param state 内部属性不用传入
|
||||
* @returns 头像地址url
|
||||
*/
|
||||
getAvatar(state) {
|
||||
return parseAvatar(state.avatar);
|
||||
},
|
||||
/**
|
||||
* 获取基础信息属性
|
||||
* @param state 内部属性不用传入
|
||||
* @returns 基础信息
|
||||
*/
|
||||
getBaseInfo(state) {
|
||||
return {
|
||||
nickName: state.nickName,
|
||||
phonenumber: state.phonenumber,
|
||||
email: state.email,
|
||||
sex: state.sex,
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* 更新基础信息属性
|
||||
* @param data 变更信息
|
||||
*/
|
||||
setBaseInfo(data: Record<string, any>) {
|
||||
this.nickName = data.nickName;
|
||||
this.phonenumber = data.phonenumber;
|
||||
this.email = data.email;
|
||||
this.sex = data.sex;
|
||||
},
|
||||
/**
|
||||
* 更新头像
|
||||
* @param avatar 上传后的地址
|
||||
*/
|
||||
setAvatar(avatar: string) {
|
||||
this.avatar = avatar;
|
||||
},
|
||||
/**
|
||||
* 获取正确头像地址
|
||||
* @param avatar
|
||||
*/
|
||||
fnAvatar(avatar: string) {
|
||||
return parseAvatar(avatar);
|
||||
},
|
||||
// 登录
|
||||
async fnLogin(loginBody: Record<string, string>) {
|
||||
const res = await login(loginBody);
|
||||
if (res.code === 200 && res.data) {
|
||||
const token = res.data[TOKEN_RESPONSE_FIELD];
|
||||
setToken(token);
|
||||
this.token = token;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
// 获取用户信息
|
||||
async fnGetInfo() {
|
||||
const res = await getInfo();
|
||||
if (res.code === 200 && res.data) {
|
||||
const { user, roles, permissions } = res.data;
|
||||
// 登录账号
|
||||
this.userName = user.userName;
|
||||
// 用户头像
|
||||
this.avatar = user.avatar;
|
||||
// 基础信息
|
||||
this.nickName = user.nickName;
|
||||
this.phonenumber = user.phonenumber;
|
||||
this.email = user.email;
|
||||
this.sex = user.sex;
|
||||
|
||||
// 验证返回的roles是否是一个非空数组
|
||||
if (Array.isArray(roles) && roles.length > 0) {
|
||||
this.roles = roles;
|
||||
this.permissions = permissions;
|
||||
} else {
|
||||
this.roles = ['ROLE_DEFAULT'];
|
||||
this.permissions = [];
|
||||
}
|
||||
|
||||
// 水印文字信息=用户昵称 手机号
|
||||
let waterMarkContent = this.nickName;
|
||||
if (this.phonenumber) {
|
||||
waterMarkContent = `${this.nickName} ${this.phonenumber}`;
|
||||
}
|
||||
useLayoutStore().changeWaterMark(waterMarkContent);
|
||||
}
|
||||
// 网络错误时退出登录状态
|
||||
if (res.code === 500) {
|
||||
removeToken();
|
||||
window.location.reload();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
// 退出系统
|
||||
async fnLogOut() {
|
||||
try {
|
||||
await logout();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.token = '';
|
||||
this.roles = [];
|
||||
this.permissions = [];
|
||||
removeToken();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useUserStore;
|
||||
54
src/typings/components.d.ts
vendored
Normal file
54
src/typings/components.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AAvatar: typeof import('ant-design-vue/lib')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/lib')['Badge']
|
||||
AButton: typeof import('ant-design-vue/lib')['Button']
|
||||
ACol: typeof import('ant-design-vue/lib')['Col']
|
||||
ADropdown: typeof import('ant-design-vue/lib')['Dropdown']
|
||||
AForm: typeof import('ant-design-vue/lib')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/lib')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/lib')['Image']
|
||||
AInput: typeof import('ant-design-vue/lib')['Input']
|
||||
AInputPassword: typeof import('ant-design-vue/lib')['InputPassword']
|
||||
AMenu: typeof import('ant-design-vue/lib')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/lib')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/lib')['MenuItem']
|
||||
APopover: typeof import('ant-design-vue/lib')['Popover']
|
||||
ARow: typeof import('ant-design-vue/lib')['Row']
|
||||
ASpace: typeof import('ant-design-vue/lib')['Space']
|
||||
ATabPane: typeof import('ant-design-vue/lib')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/lib')['Tabs']
|
||||
ATooltip: typeof import('ant-design-vue/lib')['Tooltip']
|
||||
BellOutlined: typeof import('@ant-design/icons-vue')['BellOutlined']
|
||||
CronModal: typeof import('./../components/CronModal/index.vue')['default']
|
||||
Day: typeof import('./../components/CronModal/components/Day.vue')['default']
|
||||
DictTag: typeof import('./../components/DictTag/index.vue')['default']
|
||||
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
|
||||
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
||||
Hour: typeof import('./../components/CronModal/components/Hour.vue')['default']
|
||||
IconFont: typeof import('./../components/IconFont/index.vue')['default']
|
||||
LinkiFrame: typeof import('./../components/LinkiFrame/index.vue')['default']
|
||||
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
|
||||
LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined']
|
||||
Minute: typeof import('./../components/CronModal/components/Minute.vue')['default']
|
||||
MobileOutlined: typeof import('@ant-design/icons-vue')['MobileOutlined']
|
||||
Month: typeof import('./../components/CronModal/components/Month.vue')['default']
|
||||
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
||||
QuestionCircleOutlined: typeof import('@ant-design/icons-vue')['QuestionCircleOutlined']
|
||||
ReloadOutlined: typeof import('@ant-design/icons-vue')['ReloadOutlined']
|
||||
RobotOutlined: typeof import('@ant-design/icons-vue')['RobotOutlined']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Second: typeof import('./../components/CronModal/components/Second.vue')['default']
|
||||
SettingOutlined: typeof import('@ant-design/icons-vue')['SettingOutlined']
|
||||
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
|
||||
}
|
||||
}
|
||||
7
src/typings/dict.d.ts
vendored
Normal file
7
src/typings/dict.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/**字段类型 */
|
||||
type DictType = {
|
||||
label: string;
|
||||
value: string;
|
||||
elTagType: string;
|
||||
elTagClass: string;
|
||||
};
|
||||
13
src/typings/router.d.ts
vendored
Normal file
13
src/typings/router.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'vue-router';
|
||||
import { MetaRecord, MenuDataItem } from '@ant-design-vue/pro-layout';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta extends MetaRecord {
|
||||
/**请求授权 */
|
||||
requiresAuth?: boolean;
|
||||
/**权限 */
|
||||
permissions?: string[];
|
||||
/**角色 */
|
||||
roles?: string[];
|
||||
}
|
||||
}
|
||||
8
src/typings/vite-env.d.ts
vendored
Normal file
8
src/typings/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
40
src/utils/cache-local-utils.ts
Normal file
40
src/utils/cache-local-utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**长期级缓存设置 */
|
||||
export function localSet(key: string, value: string) {
|
||||
if (!localStorage || key == null || value == null) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
/**长期级缓存获取 */
|
||||
export function localGet(key: string) {
|
||||
if (!localStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
/**长期级缓存移除 */
|
||||
export function localRemove(key: string) {
|
||||
if (!localStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**长期级缓存设置JSON */
|
||||
export function localSetJSON(key: string, jsonValue: object) {
|
||||
if (key == null || jsonValue == null) {
|
||||
return null;
|
||||
}
|
||||
localSet(key, JSON.stringify(jsonValue));
|
||||
}
|
||||
|
||||
/**长期级缓存获取JSON */
|
||||
export function localGetJSON(key: string) {
|
||||
const value = localGet(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
40
src/utils/cache-session-utils.ts
Normal file
40
src/utils/cache-session-utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**会话级缓存设置 */
|
||||
export function sessionSet(key: string, value: string) {
|
||||
if (!sessionStorage || key == null || value == null) {
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
/**会话级缓存获取 */
|
||||
export function sessionGet(key: string) {
|
||||
if (!sessionStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return sessionStorage.getItem(key);
|
||||
}
|
||||
|
||||
/**会话级缓存移除 */
|
||||
export function sessionRemove(key: string) {
|
||||
if (!sessionStorage || key == null) {
|
||||
return null;
|
||||
}
|
||||
return sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**会话级缓存设置JSON */
|
||||
export function sessionSetJSON(key: string, jsonValue: object) {
|
||||
if (key == null || jsonValue == null) {
|
||||
return null;
|
||||
}
|
||||
sessionSet(key, JSON.stringify(jsonValue));
|
||||
}
|
||||
|
||||
/**会话级缓存获取JSON */
|
||||
export function sessionGetJSON(key: string) {
|
||||
const value = sessionGet(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
72
src/utils/date-utils.ts
Normal file
72
src/utils/date-utils.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// 依赖来源 https://github.com/iamkun/dayjs
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 导入本地化语言并设为默认使用
|
||||
import('dayjs/locale/zh-cn');
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
/**年 列如:2022 */
|
||||
export const YYYY = 'YYYY';
|
||||
|
||||
/**年-月 列如:2022-12 */
|
||||
export const YYYY_MM = 'YYYY-MM';
|
||||
|
||||
/**年-月-日 列如:2022-12-30 */
|
||||
export const YYYY_MM_DD = 'YYYY-MM-DD';
|
||||
|
||||
/**年月日时分秒 列如:20221230010159 */
|
||||
export const YYYYMMDDHHMMSS = 'YYYYMMDDHHmmss';
|
||||
|
||||
/**年-月-日 时:分:秒 列如:2022-12-30 01:01:59 */
|
||||
export const YYYY_MM_DD_HH_MM_SS = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
/**
|
||||
* 格式时间字符串
|
||||
* @param dateStr 时间字符串
|
||||
* @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
* @returns Date对象
|
||||
*/
|
||||
export function parseStrToDate(
|
||||
dateStr: string,
|
||||
formatStr: string = YYYY_MM_DD_HH_MM_SS
|
||||
): Date {
|
||||
return dayjs(dateStr, formatStr).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式时间
|
||||
* @param date 可转的Date对象
|
||||
* @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss
|
||||
* @returns 时间格式字符串
|
||||
*/
|
||||
export function parseDateToStr(
|
||||
date: string | number | Date,
|
||||
formatStr: string = YYYY_MM_DD_HH_MM_SS
|
||||
): string {
|
||||
return dayjs(date).format(formatStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式时间成日期路径
|
||||
*
|
||||
* 年/月 列如:2022/12
|
||||
* @returns 时间格式字符串 YYYY/MM
|
||||
*/
|
||||
export function parseDatePath(date: number | Date = Date.now()): string {
|
||||
return dayjs(date).format('YYYY/MM');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两次时间差
|
||||
* @param endDate 结束时间
|
||||
* @param startDate 开始时间
|
||||
* @returns 单位秒
|
||||
*/
|
||||
export function diffSeconds(
|
||||
endDate: number | Date,
|
||||
startDate: number | Date
|
||||
): number {
|
||||
const value = Math.ceil(dayjs(endDate).diff(startDate, 'seconds'));
|
||||
if (Number.isNaN(value)) return 0;
|
||||
return value;
|
||||
}
|
||||
188
src/utils/parse-tree-utils.ts
Normal file
188
src/utils/parse-tree-utils.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 解析数据层级转树结构
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldParentId 读取节点父节点字段 默认 'parentId'
|
||||
* @param fieldChildren 设置子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseDataToTree(
|
||||
data: Record<string, any>[],
|
||||
fieldId: string = 'id',
|
||||
fieldParentId: string = 'parentId',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点分组
|
||||
let map: Map<string, Record<string, any>[]> = new Map();
|
||||
// 节点id
|
||||
let treeIds: string[] = [];
|
||||
// 树节点
|
||||
let tree: Record<string, any>[] = [];
|
||||
|
||||
for (const item of data) {
|
||||
let parentId = item[fieldParentId];
|
||||
// 分组
|
||||
let mapItem = map.get(parentId) ?? [];
|
||||
mapItem.push(item);
|
||||
map.set(parentId, mapItem);
|
||||
// 记录节点id
|
||||
treeIds.push(item[fieldId]);
|
||||
}
|
||||
|
||||
for (const [key, value] of map) {
|
||||
// 选择不是节点id的作为树节点
|
||||
if (!treeIds.includes(key)) {
|
||||
tree.push(...value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const iterator of tree) {
|
||||
componet(iterator);
|
||||
}
|
||||
|
||||
/**闭包递归函数 */
|
||||
function componet(iterator: Record<string, any>) {
|
||||
let id = iterator[fieldId];
|
||||
let item = map.get(id);
|
||||
if (item) {
|
||||
iterator[fieldChildren] = item;
|
||||
}
|
||||
if (iterator[fieldChildren]) {
|
||||
for (let i of iterator[fieldChildren]) {
|
||||
componet(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据层级转树结构-排除节点
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param excludeField 排除节点字段 默认 'type'
|
||||
* @param excludeValue 排除节点值 默认 '0'
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldParentId 读取节点父节点字段 默认 'parentId'
|
||||
* @param fieldChildren 设置子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseDataToTreeExclude(
|
||||
data: Record<string, any>[],
|
||||
excludeField = 'type',
|
||||
excludeValue = '0',
|
||||
fieldId: string = 'id',
|
||||
fieldParentId: string = 'parentId',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点分组
|
||||
let map: Map<string, Record<string, any>[]> = new Map();
|
||||
// 节点id
|
||||
let treeIds: string[] = [];
|
||||
// 树节点
|
||||
let tree: Record<string, any>[] = [];
|
||||
|
||||
for (const item of data) {
|
||||
// 排除值跳过
|
||||
let exclude = item[excludeField];
|
||||
if (exclude && exclude === excludeValue) {
|
||||
continue;
|
||||
}
|
||||
let parentId = item[fieldParentId];
|
||||
// 分组
|
||||
let mapItem = map.get(parentId) ?? [];
|
||||
mapItem.push(item);
|
||||
map.set(parentId, mapItem);
|
||||
// 记录节点id
|
||||
treeIds.push(item[fieldId]);
|
||||
}
|
||||
|
||||
for (const [key, value] of map) {
|
||||
// 选择不是节点id的作为树节点
|
||||
if (!treeIds.includes(key)) {
|
||||
tree.push(...value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const iterator of tree) {
|
||||
componet(iterator);
|
||||
}
|
||||
|
||||
/**闭包递归函数 */
|
||||
function componet(iterator: Record<string, any>) {
|
||||
let id = iterator[fieldId];
|
||||
let item = map.get(id);
|
||||
if (item) {
|
||||
iterator[fieldChildren] = item;
|
||||
}
|
||||
if (iterator[fieldChildren]) {
|
||||
for (let i of iterator[fieldChildren]) {
|
||||
componet(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析树结构数据转出一维id数组
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldChildren 读取子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseTreeKeys(
|
||||
data: Record<string, any>[],
|
||||
fieldId: string = 'id',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点id
|
||||
let treeIds: string[] | number[] = [];
|
||||
componet(data);
|
||||
/**闭包递归函数 */
|
||||
function componet(data: Record<string, any>[]) {
|
||||
if (data.length <= 0) return;
|
||||
for (const iterator of data) {
|
||||
let id = iterator[fieldId];
|
||||
if (id) {
|
||||
treeIds.push(id as never);
|
||||
}
|
||||
if (Array.isArray(iterator[fieldChildren])) {
|
||||
componet(iterator[fieldChildren]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析树结构数据转出含子节点的一维id数组
|
||||
*
|
||||
* @param data 数组数据
|
||||
* @param fieldId 读取节点字段 默认 'id'
|
||||
* @param fieldChildren 读取子节点字段 默认 'children'
|
||||
* @returns 层级数组
|
||||
*/
|
||||
export function parseTreeNodeKeys(
|
||||
data: Record<string, any>[],
|
||||
fieldId: string = 'id',
|
||||
fieldChildren: string = 'children'
|
||||
) {
|
||||
// 节点id
|
||||
let treeIds: string[] | number[] = [];
|
||||
componet(data);
|
||||
/**闭包递归函数 */
|
||||
function componet(data: Record<string, any>[]) {
|
||||
if (data.length <= 0) return;
|
||||
for (const iterator of data) {
|
||||
let nodes = iterator[fieldChildren];
|
||||
if (Array.isArray(nodes) && nodes.length > 0) {
|
||||
treeIds.push(iterator[fieldId] as never);
|
||||
componet(iterator[fieldChildren]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return treeIds;
|
||||
}
|
||||
67
src/utils/regular-utils.ts
Normal file
67
src/utils/regular-utils.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 有效账号格式
|
||||
*
|
||||
* 账号不能以数字开头,可包含大写小写字母,数字,且不少于5位
|
||||
*/
|
||||
export const regExpUserName = /^[a-zA-Z][a-z0-9A-Z]{5,}$/;
|
||||
|
||||
/**
|
||||
* 有效密码格式
|
||||
*
|
||||
* 密码至少包含大小写字母、数字、特殊符号,且不少于6位
|
||||
*/
|
||||
export const regExpPasswd =
|
||||
/^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$/;
|
||||
|
||||
/**
|
||||
* 有效手机号格式
|
||||
*/
|
||||
export const regExpMobile = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/;
|
||||
|
||||
/**
|
||||
* 有效邮箱格式
|
||||
*/
|
||||
export const regExpEmail =
|
||||
/^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/;
|
||||
|
||||
/**
|
||||
* 有效用户昵称格式
|
||||
*
|
||||
* 用户昵称只能包含字母、数字、中文和下划线,且不少于2位
|
||||
*/
|
||||
export const regExpNick = /^[\w\u4e00-\u9fa5-]{2,}$/;
|
||||
|
||||
/**
|
||||
* 是否为http(s)://开头
|
||||
*/
|
||||
export const regExpHttp = /^http(s)?:\/\/+/;
|
||||
|
||||
/**
|
||||
* 判断是否为http(s)://开头
|
||||
* @param link 网络链接
|
||||
* @returns true | false
|
||||
*/
|
||||
export function validHttp(link: string): boolean {
|
||||
if (!link) return false;
|
||||
return regExpHttp.test(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效手机号格式
|
||||
* @param mobile 手机号字符串
|
||||
* @returns true | false
|
||||
*/
|
||||
export function validMobile(mobile: string): boolean {
|
||||
if (!mobile) return false;
|
||||
return regExpMobile.test(mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效邮箱格式
|
||||
* @param email 邮箱字符串
|
||||
* @returns true | false
|
||||
*/
|
||||
export function validEmail(email: string): boolean {
|
||||
if (!email) return false;
|
||||
return regExpEmail.test(email);
|
||||
}
|
||||
248
src/views/account/components/base-info.vue
Normal file
248
src/views/account/components/base-info.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<script lang="ts" setup>
|
||||
import { Modal, message } from 'ant-design-vue/lib';
|
||||
import { FileType } from 'ant-design-vue/lib/upload/interface';
|
||||
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
import { onMounted, reactive, ref, toRaw } from 'vue';
|
||||
import { updateUserProfile, uploadAvatar } from '@/api/profile';
|
||||
import { regExpEmail, regExpMobile, regExpNick } from '@/utils/regular-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
const uerStore = useUserStore();
|
||||
const { getDict } = useDictStore();
|
||||
|
||||
/**用户性别字典 */
|
||||
let sysUserSex = ref<DictType[]>([
|
||||
{ label: '未知', value: '0', elTagType: '', elTagClass: '' },
|
||||
{ label: '男', value: '1', elTagType: '', elTagClass: '' },
|
||||
{ label: '女', value: '2', elTagType: '', elTagClass: '' },
|
||||
]);
|
||||
|
||||
/**表单数据状态 */
|
||||
let stateForm = reactive({
|
||||
/**表单属性 */
|
||||
form: {
|
||||
nickName: '',
|
||||
email: '',
|
||||
phonenumber: '',
|
||||
sex: undefined,
|
||||
},
|
||||
/**表单提交点击状态 */
|
||||
formClick: false,
|
||||
});
|
||||
|
||||
/**表单数据状态初始化 */
|
||||
function fnInitstateForm() {
|
||||
stateForm.form = Object.assign(stateForm.form, uerStore.getBaseInfo);
|
||||
stateForm.formClick = false;
|
||||
}
|
||||
|
||||
/**表单验证通过 */
|
||||
function fnFinish() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认要提交修改用户基本信息吗?`,
|
||||
onOk() {
|
||||
stateForm.formClick = true;
|
||||
// 发送请求
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
const form = toRaw(stateForm.form);
|
||||
updateUserProfile(form).then(res => {
|
||||
hide();
|
||||
stateForm.formClick = false;
|
||||
if (res.code === 200) {
|
||||
Modal.success({
|
||||
title: '提示',
|
||||
content: `用户基本信息修改成功!`,
|
||||
okText: '我知道了',
|
||||
onOk() {
|
||||
uerStore.setBaseInfo(form);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**上传状态 */
|
||||
let upState = ref<boolean>(false);
|
||||
|
||||
/**上传前检查或转换压缩 */
|
||||
function fnBeforeUpload(file: FileType) {
|
||||
if (upState.value) return false;
|
||||
const isJpgOrPng = ['image/jpeg', 'image/png'].includes(file.type);
|
||||
if (!isJpgOrPng) {
|
||||
message.error('只支持上传图片格式(jpg、png)', 3);
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('图片文件大小必须小于 2MB', 3);
|
||||
}
|
||||
return isJpgOrPng && isLt2M;
|
||||
}
|
||||
|
||||
/**上传变更 */
|
||||
function fnUpload(up: UploadRequestOption) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认要上传/变更用户头像吗?`,
|
||||
onOk() {
|
||||
// 发送请求
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
upState.value = true;
|
||||
let formData = new FormData();
|
||||
formData.append('file', up.file);
|
||||
uploadAvatar(formData).then(res => {
|
||||
upState.value = false;
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
message.success('头像上传/变更成功', 3);
|
||||
uerStore.setAvatar(res.data);
|
||||
} else {
|
||||
message.error(res.msg, 3);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
getDict('sys_user_sex').then(res => {
|
||||
if (res.length > 0) {
|
||||
sysUserSex.value = res;
|
||||
}
|
||||
});
|
||||
// 初始表单值
|
||||
fnInitstateForm();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form
|
||||
:model="stateForm.form"
|
||||
name="stateForm"
|
||||
layout="vertical"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
@finish="fnFinish"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="12" :md="12" :xs="24" style="margin-bottom: 30px">
|
||||
<a-form-item
|
||||
label="用户昵称"
|
||||
name="nickName"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpNick,
|
||||
message: '昵称只能包含字母、数字、中文和下划线,且不少于2位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="stateForm.form.nickName"
|
||||
allow-clear
|
||||
:maxlength="26"
|
||||
placeholder="请输入用户昵称"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="手机号码"
|
||||
name="phonenumber"
|
||||
:rules="[
|
||||
{
|
||||
required: false,
|
||||
pattern: regExpMobile,
|
||||
message: '请输入正确手机号码',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="stateForm.form.phonenumber"
|
||||
allow-clear
|
||||
:maxlength="11"
|
||||
placeholder="请输入手机号码"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="电子邮箱"
|
||||
name="email"
|
||||
:rules="[
|
||||
{
|
||||
required: false,
|
||||
pattern: regExpEmail,
|
||||
message: '请输入正确电子邮箱',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="stateForm.form.email"
|
||||
allow-clear
|
||||
:maxlength="40"
|
||||
placeholder="请输入电子邮箱"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="用户性别"
|
||||
name="sex"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择用户性别',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="stateForm.form.sex"
|
||||
placeholder="用户性别"
|
||||
:options="sysUserSex"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
block
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:loading="stateForm.formClick"
|
||||
>
|
||||
确认修改
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
@click="fnInitstateForm"
|
||||
:disabled="stateForm.formClick"
|
||||
>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24">
|
||||
<a-space direction="vertical" :size="16">
|
||||
<a-image :width="128" :height="128" :src="uerStore.getAvatar" />
|
||||
<span>请选择等比大小图片作为头像,如200x200、400x400</span>
|
||||
<a-upload
|
||||
name="file"
|
||||
list-type="picture"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
:before-upload="fnBeforeUpload"
|
||||
:custom-request="fnUpload"
|
||||
>
|
||||
<a-button type="default" :loading="upState">
|
||||
上传/变更图片
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
158
src/views/account/components/reset-passwd.vue
Normal file
158
src/views/account/components/reset-passwd.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script lang="ts" setup>
|
||||
import { Modal, message } from 'ant-design-vue/lib';
|
||||
import { reactive } from 'vue';
|
||||
import { updateUserPwd } from '@/api/profile';
|
||||
import { regExpPasswd } from '@/utils/regular-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
const { userName, fnLogOut } = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
let state = reactive({
|
||||
/**表单属性 */
|
||||
form: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
/**表单提交点击状态 */
|
||||
formClick: false,
|
||||
});
|
||||
|
||||
/**表单验证确认密码是否一致 */
|
||||
function fnEqualToPassword(
|
||||
rule: Record<string, any>,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (!value) {
|
||||
return Promise.reject('请输入确认新密码');
|
||||
}
|
||||
if (state.form.oldPassword === value) {
|
||||
return Promise.reject('与旧密码一致,请重新输入新密码');
|
||||
}
|
||||
if (state.form.newPassword === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject('两次输入的新密码不一致');
|
||||
}
|
||||
|
||||
/**表单验证通过 */
|
||||
function fnFinish() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认要提交修改密码吗?`,
|
||||
onOk() {
|
||||
state.formClick = true;
|
||||
// 发送请求
|
||||
const hide = message.loading('请稍等...', 0);
|
||||
updateUserPwd(state.form.oldPassword, state.form.confirmPassword)
|
||||
.then(res => {
|
||||
hide();
|
||||
if (res.code === 200) {
|
||||
Modal.success({
|
||||
title: '提示',
|
||||
content: `恭喜您,${userName} 账号密码修改成功!`,
|
||||
okText: '重新登录',
|
||||
onOk() {
|
||||
fnLogOut().finally(() => router.push({ name: 'Login' }));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
state.formClick = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form
|
||||
:model="state.form"
|
||||
name="stateForm"
|
||||
layout="vertical"
|
||||
:wrapper-col="{ span: 9 }"
|
||||
@finish="fnFinish"
|
||||
>
|
||||
<a-form-item
|
||||
label="旧密码"
|
||||
name="oldPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
min: 6,
|
||||
max: 26,
|
||||
message: '旧密码不能为空,且不少于6位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.oldPassword"
|
||||
placeholder="请输入旧密码"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
pattern: regExpPasswd,
|
||||
message: '密码至少包含大小写字母、数字、特殊符号,且不少于6位',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.newPassword"
|
||||
placeholder="请输入新密码"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="确认新密码"
|
||||
name="confirmPassword"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
validator: fnEqualToPassword,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="state.form.confirmPassword"
|
||||
placeholder="请确认新密码"
|
||||
:maxlength="26"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="prefix-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ span: 3 }">
|
||||
<a-button
|
||||
block
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:loading="state.formClick"
|
||||
>
|
||||
提交修改
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
165
src/views/account/components/style-layout.vue
Normal file
165
src/views/account/components/style-layout.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { getLocalColor, changePrimaryColor } from '@/hooks/useTheme';
|
||||
import useLayoutStore from '@/store/modules/layout';
|
||||
const { proConfig, changeConf } = useLayoutStore();
|
||||
|
||||
let color = ref<string>(getLocalColor());
|
||||
|
||||
/**改变主题色 */
|
||||
function fnColorChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.nodeName === 'INPUT') {
|
||||
changePrimaryColor(target.value ?? '#1890ff');
|
||||
} else {
|
||||
changePrimaryColor();
|
||||
}
|
||||
color.value = getLocalColor();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-divider orientation="left">布局属性</a-divider>
|
||||
<a-list item-layout="vertical" size="large" row-key="title">
|
||||
<a-list-item>
|
||||
整体布局
|
||||
<template #actions> 导航模式模块设置 </template>
|
||||
<template #extra>
|
||||
<a-radio-group
|
||||
style="margin-bottom: 12px"
|
||||
:value="proConfig.layout"
|
||||
@change="(e:any) => changeConf('layout', e.target.value)"
|
||||
>
|
||||
<a-radio value="side">左侧菜单布局</a-radio>
|
||||
<a-radio value="top">顶部菜单布局</a-radio>
|
||||
<a-radio value="mix">混合菜单布局</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
风格配色
|
||||
<template #actions> 整体风格配色设置 </template>
|
||||
<template #extra>
|
||||
<a-space :size="16" align="end" direction="horizontal">
|
||||
<a-button type="primary" size="small" @click="fnColorChange">
|
||||
<BgColorsOutlined /> 随机
|
||||
</a-button>
|
||||
<input type="color" :value="color" @input="fnColorChange" />
|
||||
</a-space>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
深色菜单
|
||||
<template #actions> 只能改变导航模式的菜单 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.navTheme === 'dark'"
|
||||
@change="
|
||||
(checked:any) => changeConf('navTheme', checked ? 'dark' : 'light')
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
固定顶部导航栏
|
||||
<template #actions> 顶部导航栏是否固定,不随滚动条移动 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.fixedHeader"
|
||||
@change="(checked:any) => changeConf('fixedHeader', checked)"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
固定左侧菜单
|
||||
<template #actions> 左侧菜单是否固定,仅左侧菜单布局时有效 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.fixSiderbar"
|
||||
@change="(checked:any) => changeConf('fixSiderbar', checked)"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
自动分割菜单
|
||||
<template #actions>
|
||||
顶部有多级菜单时显示左侧菜单,仅混合菜单布局时有效
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
:checked="proConfig.splitMenus"
|
||||
@change="(checked:any) => changeConf('splitMenus', checked)"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<a-divider orientation="left">内容区域</a-divider>
|
||||
<a-list item-layout="vertical" size="large" row-key="title">
|
||||
<a-list-item>
|
||||
顶栏
|
||||
<template #actions> 是否显示顶部导航栏 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.headerRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('headerRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
页脚
|
||||
<template #actions> 是否显示底部导航栏 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.footerRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('footerRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
菜单头
|
||||
<template #actions> 是否显示左侧菜单栏顶部LOGO区域 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.menuHeaderRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('menuHeaderRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
导航标签项
|
||||
<template #actions> 是否显示顶部Tab导航标签项 </template>
|
||||
<template #extra>
|
||||
<a-switch
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:checked="proConfig.tabRender === undefined"
|
||||
@change="
|
||||
(checked:any) => changeConf('tabRender', checked === true && undefined)
|
||||
"
|
||||
></a-switch>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
198
src/views/account/profile.vue
Normal file
198
src/views/account/profile.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="ts" setup>
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
import { getUserProfile } from '@/api/profile';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
|
||||
/**加载状态 */
|
||||
let loading = ref<boolean>(true);
|
||||
|
||||
/**Tab标签激活 */
|
||||
let activeKey = ref<string>('list');
|
||||
|
||||
/**个人信息数据状态 */
|
||||
let state = reactive<{
|
||||
user: Record<string, any>;
|
||||
postGroup: string[];
|
||||
roleGroup: string[];
|
||||
}>({
|
||||
user: {},
|
||||
postGroup: [],
|
||||
roleGroup: [],
|
||||
});
|
||||
|
||||
/**列表数据 */
|
||||
let listData = ref([
|
||||
{
|
||||
id: 'Vue',
|
||||
title: 'Vue.js - 渐进式 JavaScript 框架 | Vue.js',
|
||||
description:
|
||||
'基于标准 HTML、CSS 和 JavaScript 构建,提供容易上手的 API 和一流的文档。 性能出色 经过编译器优化、完全响应式的渲染系统,几乎不需要手动优化。',
|
||||
},
|
||||
{
|
||||
id: 'Vue Router',
|
||||
title: 'Vue Router | Vue.js 的官方路由',
|
||||
description:
|
||||
'为Vue.js 提供富有表现力、可配置的、方便的路由,用直观且强大的语法来定义静态或动态路由。',
|
||||
},
|
||||
{
|
||||
id: 'Pinia',
|
||||
title: 'Pinia | The intuitive store for Vue.js',
|
||||
description:
|
||||
'Pinia hooks into Vue devtools to give you an enhanced development experience in both Vue 2 and Vue 3. ',
|
||||
},
|
||||
{
|
||||
id: 'Vite',
|
||||
title: 'Vite | 下一代的前端工具链',
|
||||
description:
|
||||
'Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验',
|
||||
},
|
||||
]);
|
||||
|
||||
/**查询用户个人信息 */
|
||||
function fnGetProfile() {
|
||||
getUserProfile().then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const { user, roleGroup, postGroup } = res.data;
|
||||
state.user = user;
|
||||
state.roleGroup = roleGroup;
|
||||
state.postGroup = postGroup;
|
||||
// 头像解析
|
||||
state.user.avatar = useUserStore().fnAvatar(user.avatar);
|
||||
loading.value = false;
|
||||
} else {
|
||||
message.error(res.msg, 3);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取信息
|
||||
fnGetProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer :loading="loading">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="6" :md="6" :xs="24">
|
||||
<a-card :body-style="{ padding: '0px' }" style="margin-bottom: 16px">
|
||||
<template #title>
|
||||
<div class="info-top">
|
||||
<div class="info-top-no">No:{{ state.user.userId }}</div>
|
||||
<a-avatar
|
||||
shape="circle"
|
||||
:size="96"
|
||||
:src="state.user.avatar"
|
||||
:alt="state.user.userName"
|
||||
></a-avatar>
|
||||
<div class="info-top-nickname" :title="state.user.nickName">
|
||||
{{ state.user.nickName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-descriptions
|
||||
size="small"
|
||||
layout="vertical"
|
||||
:bordered="true"
|
||||
:column="1"
|
||||
>
|
||||
<a-descriptions-item label="手机号码">
|
||||
{{ state.user.phonenumber || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户邮箱">
|
||||
{{ state.user.email || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属部门">
|
||||
{{ state.user.dept?.deptName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="拥有岗位">
|
||||
<span v-if="state.postGroup.length === 0">-</span>
|
||||
<a-tag v-else v-for="v in state.postGroup" :key="v">
|
||||
{{ v }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="拥有角色">
|
||||
<span v-if="state.roleGroup.length === 0">-</span>
|
||||
<a-tag v-else v-for="v in state.roleGroup" :key="v">
|
||||
{{ v }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="登录地址">
|
||||
{{ state.user.loginIp || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="登录时间">
|
||||
<span v-if="+state.user.loginDate > 0">
|
||||
{{ parseDateToStr(+state.user.loginDate) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :lg="18" :md="18" :xs="24">
|
||||
<a-card>
|
||||
<a-tabs
|
||||
tab-position="top"
|
||||
:destroy-inactive-tab-pane="true"
|
||||
v-model:activeKey="activeKey"
|
||||
>
|
||||
<a-tab-pane key="list" tab="列表">
|
||||
<a-list
|
||||
item-layout="horizontal"
|
||||
:data-source="listData"
|
||||
row-key="id"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ item.description }}
|
||||
</template>
|
||||
<template #avatar>
|
||||
<a-avatar>{{ item.id }}</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="empty" tab="空状态">
|
||||
<a-empty>
|
||||
<template #description> 暂无数据,尝试刷新看看 </template>
|
||||
<a-button type="primary">刷新</a-button>
|
||||
</a-empty>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.info-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
&-no {
|
||||
align-self: flex-start;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
&-nickname {
|
||||
margin-top: 16px;
|
||||
font-size: 24px;
|
||||
align-self: flex-start;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
30
src/views/account/settings.vue
Normal file
30
src/views/account/settings.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import BaseInfo from './components/base-info.vue';
|
||||
import ResetPasswd from './components/reset-passwd.vue';
|
||||
import StyleLayout from './components/style-layout.vue';
|
||||
|
||||
/**Tab标签激活 */
|
||||
let activeKey = ref<string>('base-info');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card>
|
||||
<a-tabs tab-position="left" v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="base-info" tab="基础信息">
|
||||
<BaseInfo></BaseInfo>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="reset-passwd" tab="重置密码">
|
||||
<ResetPasswd></ResetPasswd>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="style-layout" tab="个性化">
|
||||
<StyleLayout></StyleLayout>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
27
src/views/dome/dome1.vue
Normal file
27
src/views/dome/dome1.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-result
|
||||
status="404"
|
||||
:style="{
|
||||
height: '100%',
|
||||
background: '#fff',
|
||||
}"
|
||||
title="Hello World"
|
||||
sub-title="Sorry, you are not authorized to access this page."
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleClick">Back Home</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
|
||||
const handleClick = () => {
|
||||
console.log('info');
|
||||
message.info('BackHome button clicked!');
|
||||
};
|
||||
</script>
|
||||
61
src/views/dome/dome2.vue
Normal file
61
src/views/dome/dome2.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<a-layout class="layout">
|
||||
<a-layout-header>
|
||||
<div class="logo" />
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
:style="{ lineHeight: '64px' }"
|
||||
>
|
||||
<a-menu-item key="1">nav 1</a-menu-item>
|
||||
<a-menu-item key="2">nav 2</a-menu-item>
|
||||
<a-menu-item key="3">nav 3</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-header>
|
||||
<a-layout-content style="padding: 0 50px">
|
||||
<a-breadcrumb style="margin: 16px 0">
|
||||
<a-breadcrumb-item>Home</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>List</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>App</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<div :style="{ background: '#fff', padding: '24px', minHeight: '280px' }">
|
||||
Content {{ selectedKeys }}
|
||||
</div>
|
||||
</a-layout-content>
|
||||
<a-layout-footer style="text-align: center">
|
||||
Ant Design ©2018 Created by Ant UED
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const selectedKeys = ref<string[]>(['2']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.site-layout-content {
|
||||
min-height: 280px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#components-layout-demo-top .logo {
|
||||
float: left;
|
||||
width: 120px;
|
||||
height: 31px;
|
||||
margin: 16px 24px 16px 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ant-row-rtl #components-layout-demo-top .logo {
|
||||
float: right;
|
||||
margin: 16px 0 16px 24px;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .site-layout-content {
|
||||
background: #141414;
|
||||
}
|
||||
</style>
|
||||
30
src/views/dome/dome3.vue
Normal file
30
src/views/dome/dome3.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<PageContainer title="Version" sub-title="show current project dependencies">
|
||||
<template #content>
|
||||
<strong>Content Area</strong>
|
||||
</template>
|
||||
<template #extra>
|
||||
<strong>Extra Area</strong>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<strong>ExtraContent Area</strong>
|
||||
</template>
|
||||
<template #tags>
|
||||
<a-tag>Tag1</a-tag>
|
||||
<a-tag color="pink">Tag2</a-tag>
|
||||
</template>
|
||||
<a-card title="Info">
|
||||
<p v-for="i in list" :key="i">
|
||||
text block...
|
||||
<a-tag>{{ i }}</a-tag>
|
||||
</p>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
|
||||
const list = ref<number>(50);
|
||||
</script>
|
||||
87
src/views/domes/dynamic-match.vue
Normal file
87
src/views/domes/dynamic-match.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<PageContainer :title="`${route.meta.title} ${route.params.id}`">
|
||||
<template #content>
|
||||
<a-descriptions size="small" :column="2">
|
||||
<a-descriptions-item label="创建人">张三</a-descriptions-item>
|
||||
<a-descriptions-item label="联系方式">
|
||||
<a>421421</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">2017-01-10</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">2017-10-10</a-descriptions-item>
|
||||
<a-descriptions-item label="备注">
|
||||
中国浙江省杭州市西湖区古翠路
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button key="3">操作</a-button>
|
||||
<a-button key="2">操作</a-button>
|
||||
<a-button key="1" type="primary">主操作</a-button>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<a-space>
|
||||
<a-statistic title="Feedback" :value="1128">
|
||||
<template #prefix>
|
||||
<LikeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<a-statistic title="Unmerged" :value="93" suffix="/ 100" />
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 主内容区 -->
|
||||
<div style="height: 300px">
|
||||
<p>路由参数联动 分页器 组件</p>
|
||||
<a-space>
|
||||
<a-button type="dashed" @click="prev">跳转上一页</a-button>
|
||||
<a-button type="dashed" @click="next">跳转下一页</a-button>
|
||||
</a-space>
|
||||
<a-divider />
|
||||
<a-pagination
|
||||
:current="currentId"
|
||||
:total="total"
|
||||
show-less-items
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentId = computed(() => {
|
||||
let id = route.params && (route.params.id as string);
|
||||
return Number.parseInt(id as string, 10) || 1;
|
||||
});
|
||||
const total = computed(() => {
|
||||
const v = currentId.value * 20;
|
||||
if (v >= Number.MAX_SAFE_INTEGER) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
|
||||
const next = () => {
|
||||
router.push({
|
||||
name: 'DynamicMatch',
|
||||
params: { id: currentId.value + 1 },
|
||||
});
|
||||
};
|
||||
const prev = () => {
|
||||
router.push({
|
||||
name: 'DynamicMatch',
|
||||
params: { id: currentId.value > 1 ? currentId.value - 1 : 1 },
|
||||
});
|
||||
};
|
||||
const handlePageChange = (currentPage: number) => {
|
||||
router.push({
|
||||
name: 'DynamicMatch',
|
||||
params: { id: currentPage },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
58
src/views/domes/page-info.vue
Normal file
58
src/views/domes/page-info.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<PageContainer :title="title">
|
||||
<template #content>
|
||||
<a-descriptions size="small" :column="2">
|
||||
<a-descriptions-item label="创建人">张三</a-descriptions-item>
|
||||
<a-descriptions-item label="联系方式">
|
||||
<a>421421</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">2017-01-10</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">2017-10-10</a-descriptions-item>
|
||||
<a-descriptions-item label="备注">
|
||||
中国浙江省杭州市西湖区古翠路
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button key="3">操作</a-button>
|
||||
<a-button key="2">操作</a-button>
|
||||
<a-button key="1" type="primary">主操作</a-button>
|
||||
</template>
|
||||
<template #extraContent>
|
||||
<a-space>
|
||||
<a-statistic title="Feedback" :value="1128">
|
||||
<template #prefix>
|
||||
<LikeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<a-statistic title="Unmerged" :value="93" suffix="/ 100" />
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 主内容区 -->
|
||||
<div style="height: 120vh">
|
||||
<a-result
|
||||
status="404"
|
||||
:style="{
|
||||
height: '100%',
|
||||
background: '#fff',
|
||||
}"
|
||||
title="Hello World"
|
||||
sub-title="Sorry, you are not authorized to access this page."
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary">Back Home</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PageContainer } from '@ant-design-vue/pro-layout';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
/**路由标题 */
|
||||
let title = ref<string>(route.meta.title ?? '标题');
|
||||
</script>
|
||||
102
src/views/domes/page-typography.vue
Normal file
102
src/views/domes/page-typography.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<a-typography>
|
||||
<a-typography-title>Introduction</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
In the process of internal desktop applications development, many
|
||||
different design specs and implementations would be involved, which
|
||||
might cause designers and developers difficulties and duplication and
|
||||
reduce the efficiency of development.
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
After massive project practice and summaries, Ant Design, a design
|
||||
language for background applications, is refined by Ant UED Team, which
|
||||
aims to
|
||||
<a-typography-text strong>
|
||||
uniform the user interface specs for internal background projects,
|
||||
lower the unnecessary cost of design differences and implementation
|
||||
and liberate the resources of design and front-end development.
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-title :level="2"
|
||||
>Guidelines and Resources</a-typography-title
|
||||
>
|
||||
<a-typography-paragraph>
|
||||
We supply a series of design principles, practical patterns and high
|
||||
quality design resources (
|
||||
<a-typography-text code>Sketch</a-typography-text>
|
||||
and
|
||||
<a-typography-text code>Axure</a-typography-text>
|
||||
), to help people create their product prototypes beautifully and
|
||||
efficiently.
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<a-typography-link href="/docs/resources"
|
||||
>Resource Download</a-typography-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
Press
|
||||
<a-typography-text keyboard>Esc</a-typography-text>
|
||||
to exit...
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-typography-title>介绍</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
随着商业化的趋势,越来越多的企业级产品对更好的用户体验有了进一步的要求。带着这样的一个终极目标,我们(蚂蚁金服体验技术部)经过大量的项目实践和总结,逐步打磨出一个服务于企业级产品的设计体系
|
||||
Ant Design。基于
|
||||
<a-typography-text mark>『确定』和『自然』</a-typography-text>
|
||||
的设计价值观,通过模块化的解决方案,降低冗余的生产成本,让设计者专注于
|
||||
<a-typography-text strong>更好的用户体验</a-typography-text>
|
||||
。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-title :level="2">设计资源</a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
我们提供完善的设计原则、最佳实践和设计资源文件(
|
||||
<a-typography-text code>Sketch</a-typography-text>
|
||||
和
|
||||
<a-typography-text code>Axure</a-typography-text>
|
||||
),来帮助业务快速设计出高质量的产品原型。
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<a-typography-link href="/docs/resources-cn"
|
||||
>设计资源</a-typography-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
<blockquote>{{ blockContent }}</blockquote>
|
||||
<pre>{{ blockContent }}</pre>
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-typography-paragraph>
|
||||
按
|
||||
<a-typography-text keyboard>Esc</a-typography-text>
|
||||
键退出阅读……
|
||||
</a-typography-paragraph>
|
||||
</a-typography>
|
||||
</a-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const blockContent = ref<string>(`AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、不限可能的数据可视化最佳实践。得益于丰富的业务场景和用户需求挑战,AntV 经历多年积累与不断打磨,已支撑整个阿里集团内外 20000+ 业务系统,通过了日均千万级 UV 产品的严苛考验。
|
||||
我们正在基础图表,图分析,图编辑,地理空间可视化,智能可视化等各个可视化的领域耕耘,欢迎同路人一起前行。` );
|
||||
|
||||
</script>
|
||||
21
src/views/error/403.vue
Normal file
21
src/views/error/403.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-result
|
||||
status="403"
|
||||
title="没有访问权限"
|
||||
sub-title="请不要进行非法操作,您可以返回主页面或返回"
|
||||
>
|
||||
<template #extra>
|
||||
<RouterLink :to="{ name: 'Index' }" :replace="true">
|
||||
<a-button type="primary"> 返回首页 </a-button>
|
||||
</RouterLink>
|
||||
<a-button type="default" @click="() => router.back()"> 返回 </a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
26
src/views/error/404.vue
Normal file
26
src/views/error/404.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<a-result
|
||||
status="404"
|
||||
title="找不到匹配页面"
|
||||
sub-title="对不起,您正在寻找的页面不存在。"
|
||||
>
|
||||
<template #extra>
|
||||
<RouterLink :to="{ name: 'Index' }" :replace="true">
|
||||
<a-button type="primary"> 返回首页 </a-button>
|
||||
</RouterLink>
|
||||
</template>
|
||||
<a-typography>
|
||||
<a-typography-title> 找不到网页? </a-typography-title>
|
||||
<a-typography-paragraph>
|
||||
1. 尝试检查URL的错误,然后按浏览器上的刷新按钮。
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
2. 尝试在我们的应用程序中找到其他内容。
|
||||
</a-typography-paragraph>
|
||||
</a-typography>
|
||||
</a-result>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user