init: 初始系统模板

This commit is contained in:
TsMask
2023-09-05 14:38:23 +08:00
parent a5bc16ae4f
commit 1075c8ae4f
130 changed files with 22531 additions and 1 deletions

11
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,18 @@
# 基于 Ant-Design-Vue + Vue3 的管理系统
[![star](https://gitee.com/TsMask/mask_antd_vue3/badge/star.svg?theme=dark)](https://gitee.com/TsMask/mask_antd_vue3/stargazers)
![Build Vite](https://img.shields.io/badge/Build-Vite-green.svg)
![Build Vue3](https://img.shields.io/badge/Build-Vue3-green.svg)
![Build MaskApi](https://img.shields.io/badge/Build-MaskApi-orange.svg)
![Release V0.2.1](https://img.shields.io/badge/Release-V0.2.1-orange.svg)
## 简介
该项目选择 [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
View 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>

View File

@@ -1 +0,0 @@
ok

42
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

205
public/loading.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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',
});
}

View 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
View 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',
});
}

View 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',
});
}

View 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
View 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
View 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
View 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
View 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',
});
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,5 @@
/**管理员-系统指定角色KEY */
export const ADMIN_ROLE_KEY = 'admin';
/**管理员-系统指定权限 */
export const ADMIN_PERMISSION = '*:*:*';

View File

@@ -0,0 +1,5 @@
/**应用-请求头-系统标识 */
export const APP_REQUEST_HEADER_CODE = 'X-App-Code';
/**应用-请求头-系统版本 */
export const APP_REQUEST_HEADER_VERSION = 'X-App-Version';

View 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';

View 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';

View 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
View 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);
},
};

View 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);
}
}

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

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
// 承载目录下级菜单页面需要声明才会生成name
</script>
<template>
<RouterView />
</template>

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

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

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

@@ -0,0 +1,5 @@
import { createPinia } from 'pinia';
const store = createPinia();
export default store;

31
src/store/modules/app.ts Normal file
View 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
View 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;

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

@@ -0,0 +1,7 @@
/**字段类型 */
type DictType = {
label: string;
value: string;
elTagType: string;
elTagClass: string;
};

13
src/typings/router.d.ts vendored Normal file
View 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
View 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;
}

View 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);
}

View 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
View 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;
}

View 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;
}

View 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);
}

View 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>请选择等比大小图片作为头像如200x200400x400</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>

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

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

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

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

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

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

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