更新
This commit is contained in:
11
src/App.vue
11
src/App.vue
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<el-config-provider :size="size" :z-index="zIndex">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const size = ref('default')
|
||||
const zIndex = ref(3000)
|
||||
</script>
|
||||
|
||||
<template><router-view /></template>
|
||||
|
||||
@@ -21,97 +21,97 @@ export default {
|
||||
},
|
||||
users: {
|
||||
list: {
|
||||
name: "获得用户列表",
|
||||
name: '获得用户列表',
|
||||
get: async function (params) {
|
||||
return await request.get('auth/users/index', { params });
|
||||
return await request.get('auth/users/index', { params })
|
||||
},
|
||||
},
|
||||
add: {
|
||||
name: "添加用户",
|
||||
name: '添加用户',
|
||||
post: async function (params) {
|
||||
return await request.post('auth/users/add', params);
|
||||
return await request.post('auth/users/add', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
name: "编辑用户",
|
||||
name: '编辑用户',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/users/edit', params);
|
||||
return await request.put('auth/users/edit', params)
|
||||
},
|
||||
},
|
||||
uppasswd: {
|
||||
name: "修改密码",
|
||||
name: '修改密码',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/users/passwd', params);
|
||||
return await request.put('auth/users/passwd', params)
|
||||
},
|
||||
},
|
||||
uprole: {
|
||||
name: "设置角色",
|
||||
name: '设置角色',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/users/uprole', params);
|
||||
return await request.put('auth/users/uprole', params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
name: "删除用户",
|
||||
name: '删除用户',
|
||||
post: async function (params) {
|
||||
return await request.delete('auth/users/delete', params);
|
||||
return await request.delete('auth/users/delete', params)
|
||||
},
|
||||
},
|
||||
},
|
||||
role: {
|
||||
list: {
|
||||
name: "获得角色列表",
|
||||
name: '获得角色列表',
|
||||
get: async function (params) {
|
||||
return await request.get('auth/role/index', { params });
|
||||
return await request.get('auth/role/index', { params })
|
||||
},
|
||||
},
|
||||
add: {
|
||||
name: "添加角色",
|
||||
name: '添加角色',
|
||||
post: async function (params) {
|
||||
return await request.post('auth/role/add', params);
|
||||
return await request.post('auth/role/add', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
name: "编辑角色",
|
||||
name: '编辑角色',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/role/edit', params);
|
||||
return await request.put('auth/role/edit', params)
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
name: "角色授权",
|
||||
name: '角色授权',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/role/auth', params);
|
||||
return await request.put('auth/role/auth', params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
name: "删除角色",
|
||||
name: '删除角色',
|
||||
post: async function (params) {
|
||||
return await request.delete('auth/role/delete', params);
|
||||
return await request.delete('auth/role/delete', params)
|
||||
},
|
||||
},
|
||||
},
|
||||
department: {
|
||||
list: {
|
||||
name: "获得部门列表",
|
||||
name: '获得部门列表',
|
||||
get: async function (params) {
|
||||
return await request.get('auth/department/index', { params });
|
||||
return await request.get('auth/department/index', { params })
|
||||
},
|
||||
},
|
||||
add: {
|
||||
name: "添加部门",
|
||||
name: '添加部门',
|
||||
post: async function (params) {
|
||||
return await request.post('auth/department/add', params);
|
||||
return await request.post('auth/department/add', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
name: "编辑部门",
|
||||
name: '编辑部门',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/department/edit', params);
|
||||
return await request.put('auth/department/edit', params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
name: "删除部门",
|
||||
name: '删除部门',
|
||||
post: async function (params) {
|
||||
return await request.delete('auth/department/delete', params);
|
||||
return await request.delete('auth/department/delete', params)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,27 +123,27 @@ export default {
|
||||
},
|
||||
},
|
||||
list: {
|
||||
name: "获取菜单",
|
||||
name: '获取菜单',
|
||||
get: async function (params) {
|
||||
return await request.get('auth/menu/index', { params });
|
||||
return await request.get('auth/menu/index', { params })
|
||||
},
|
||||
},
|
||||
add: {
|
||||
name: "添加菜单",
|
||||
name: '添加菜单',
|
||||
post: async function (params) {
|
||||
return await request.post('auth/menu/add', params);
|
||||
return await request.post('auth/menu/add', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
name: "编辑菜单",
|
||||
name: '编辑菜单',
|
||||
post: async function (params) {
|
||||
return await request.put('auth/menu/edit', params);
|
||||
return await request.put('auth/menu/edit', params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
name: "删除菜单",
|
||||
name: '删除菜单',
|
||||
post: async function (params) {
|
||||
return await request.delete('auth/menu/delete', params);
|
||||
return await request.delete('auth/menu/delete', params)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,333 +2,333 @@
|
||||
// Warm color palette with tech-inspired design
|
||||
|
||||
:root {
|
||||
--auth-primary: #ff6b35;
|
||||
--auth-primary-light: #ff8c5a;
|
||||
--auth-primary-dark: #e55a2b;
|
||||
--auth-secondary: #ffb347;
|
||||
--accent-orange: #ffa500;
|
||||
--accent-coral: #ff7f50;
|
||||
--accent-amber: #ffc107;
|
||||
--auth-primary: #ff6b35;
|
||||
--auth-primary-light: #ff8c5a;
|
||||
--auth-primary-dark: #e55a2b;
|
||||
--auth-secondary: #ffb347;
|
||||
--accent-orange: #ffa500;
|
||||
--accent-coral: #ff7f50;
|
||||
--accent-amber: #ffc107;
|
||||
|
||||
--bg-gradient-start: #fff5f0;
|
||||
--bg-gradient-end: #ffe8dc;
|
||||
--card-bg: rgba(255, 255, 255, 0.95);
|
||||
--bg-gradient-start: #fff5f0;
|
||||
--bg-gradient-end: #ffe8dc;
|
||||
--card-bg: rgba(255, 255, 255, 0.95);
|
||||
|
||||
--text-primary: #2d1810;
|
||||
--text-secondary: #6b4423;
|
||||
--text-muted: #a67c52;
|
||||
--text-primary: #2d1810;
|
||||
--text-secondary: #6b4423;
|
||||
--text-muted: #a67c52;
|
||||
|
||||
--border-color: #ffd4b8;
|
||||
--shadow-color: rgba(255, 107, 53, 0.15);
|
||||
--border-color: #ffd4b8;
|
||||
--shadow-color: rgba(255, 107, 53, 0.15);
|
||||
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--error: #dc3545;
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--error: #dc3545;
|
||||
|
||||
--tech-blue: #007bff;
|
||||
--tech-purple: #6f42c1;
|
||||
--tech-blue: #007bff;
|
||||
--tech-purple: #6f42c1;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Tech pattern background
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 107, 53, 0.03) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 179, 71, 0.05) 0%, transparent 40%),
|
||||
radial-gradient(circle at 40% 80%, rgba(255, 127, 80, 0.04) 0%, transparent 40%);
|
||||
pointer-events: none;
|
||||
}
|
||||
// Tech pattern background
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 107, 53, 0.03) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 179, 71, 0.05) 0%, transparent 40%), radial-gradient(circle at 40% 80%, rgba(255, 127, 80, 0.04) 0%, transparent 40%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Animated tech elements
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
top: -200px;
|
||||
right: -200px;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
// Animated tech elements
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
top: -200px;
|
||||
right: -200px;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50px, 50px);
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 48px 40px;
|
||||
box-shadow:
|
||||
0 20px 60px var(--shadow-color),
|
||||
0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 20px;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 48px 40px;
|
||||
box-shadow:
|
||||
0 20px 60px var(--shadow-color),
|
||||
0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 20px;
|
||||
|
||||
// Tech accent line
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--auth-primary), var(--auth-secondary));
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
// Tech accent line
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--auth-primary), var(--auth-secondary));
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.auth-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.auth-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.auth-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
--el-input-border-radius: 12px;
|
||||
--el-input-border-color: var(--border-color);
|
||||
--el-input-hover-border-color: var(--auth-primary-light);
|
||||
--el-input-focus-border-color: var(--auth-primary);
|
||||
.el-input {
|
||||
--el-input-border-radius: 12px;
|
||||
--el-input-border-color: var(--border-color);
|
||||
--el-input-hover-border-color: var(--auth-primary-light);
|
||||
--el-input-focus-border-color: var(--auth-primary);
|
||||
|
||||
.el-input__wrapper {
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
.el-input__wrapper {
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
}
|
||||
&.is-focus {
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
.el-input__inner {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__prefix {
|
||||
color: var(--auth-primary);
|
||||
font-size: 18px;
|
||||
}
|
||||
.el-input__prefix {
|
||||
color: var(--auth-primary);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.el-input__suffix {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.el-input__suffix {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.el-button {
|
||||
--el-button-border-radius: 12px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
.el-button {
|
||||
--el-button-border-radius: 12px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&.el-button--primary {
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
border: none;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s ease;
|
||||
&.el-button--primary {
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
border: none;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.remember-me {
|
||||
.el-checkbox__label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.remember-me {
|
||||
.el-checkbox__label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: var(--auth-primary);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
.forgot-password {
|
||||
color: var(--auth-primary);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--auth-primary-dark);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--auth-primary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
}
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 0 16px;
|
||||
}
|
||||
span {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
.auth-footer-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
|
||||
.auth-link {
|
||||
color: var(--auth-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
transition: color 0.3s ease;
|
||||
.auth-link {
|
||||
color: var(--auth-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--auth-primary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--auth-primary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tech-decoration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
.tech-circle {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
.tech-circle {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(1) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -150px;
|
||||
left: -150px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.tech-circle:nth-child(1) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -150px;
|
||||
left: -150px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(2) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
.tech-circle:nth-child(2) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(3) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
.tech-circle:nth-child(3) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.auth-card {
|
||||
padding: 40px 24px;
|
||||
margin: 16px;
|
||||
}
|
||||
.auth-card {
|
||||
padding: 40px 24px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.auth-header {
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus customizations for auth pages
|
||||
.el-form-item__error {
|
||||
color: var(--error);
|
||||
font-size: 12px;
|
||||
color: var(--error);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
--el-message-bg: rgba(255, 255, 255, 0.98);
|
||||
--el-message-border-color: var(--border-color);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--el-message-bg: rgba(255, 255, 255, 0.98);
|
||||
--el-message-border-color: var(--border-color);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
|
||||
for (let icon in AIcons) {
|
||||
app.component(`A${icon}`, AIcons[icon])
|
||||
}
|
||||
@@ -11,5 +10,5 @@ export default {
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(`${key}`, component)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,144 +1,144 @@
|
||||
export default class UploadAdapter {
|
||||
constructor(loader, options) {
|
||||
this.loader = loader;
|
||||
this.options = options;
|
||||
this.timeout = 60000; // 60秒超时
|
||||
this.loader = loader
|
||||
this.options = options
|
||||
this.timeout = 60000 // 60秒超时
|
||||
}
|
||||
|
||||
upload() {
|
||||
return this.loader.file.then(
|
||||
(file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this._initRequest();
|
||||
this._initListeners(resolve, reject, file);
|
||||
this._sendRequest(file);
|
||||
this._initTimeout(reject);
|
||||
this._initRequest()
|
||||
this._initListeners(resolve, reject, file)
|
||||
this._sendRequest(file)
|
||||
this._initTimeout(reject)
|
||||
}),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
this.xhr.abort()
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
clearTimeout(this.timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
_initRequest() {
|
||||
const xhr = (this.xhr = new XMLHttpRequest());
|
||||
const xhr = (this.xhr = new XMLHttpRequest())
|
||||
|
||||
xhr.open("POST", this.options.upload.uploadUrl, true);
|
||||
xhr.responseType = "json";
|
||||
xhr.open('POST', this.options.upload.uploadUrl, true)
|
||||
xhr.responseType = 'json'
|
||||
}
|
||||
|
||||
_initListeners(resolve, reject, file) {
|
||||
const xhr = this.xhr;
|
||||
const loader = this.loader;
|
||||
const genericErrorText = `Couldn't upload file: ${file.name}.`;
|
||||
const xhr = this.xhr
|
||||
const loader = this.loader
|
||||
const genericErrorText = `Couldn't upload file: ${file.name}.`
|
||||
|
||||
xhr.addEventListener("error", () => {
|
||||
console.error("[UploadAdapter] Upload error for file:", file.name);
|
||||
reject(genericErrorText);
|
||||
});
|
||||
xhr.addEventListener('error', () => {
|
||||
console.error('[UploadAdapter] Upload error for file:', file.name)
|
||||
reject(genericErrorText)
|
||||
})
|
||||
|
||||
xhr.addEventListener("abort", () => {
|
||||
console.warn("[UploadAdapter] Upload aborted for file:", file.name);
|
||||
reject();
|
||||
});
|
||||
xhr.addEventListener('abort', () => {
|
||||
console.warn('[UploadAdapter] Upload aborted for file:', file.name)
|
||||
reject()
|
||||
})
|
||||
|
||||
xhr.addEventListener("timeout", () => {
|
||||
console.error("[UploadAdapter] Upload timeout for file:", file.name);
|
||||
reject(`Upload timeout: ${file.name}. Please try again.`);
|
||||
});
|
||||
xhr.addEventListener('timeout', () => {
|
||||
console.error('[UploadAdapter] Upload timeout for file:', file.name)
|
||||
reject(`Upload timeout: ${file.name}. Please try again.`)
|
||||
})
|
||||
|
||||
xhr.addEventListener("load", () => {
|
||||
const response = xhr.response;
|
||||
xhr.addEventListener('load', () => {
|
||||
const response = xhr.response
|
||||
|
||||
// 检查响应状态码
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
if (!response) {
|
||||
console.error("[UploadAdapter] Empty response for file:", file.name);
|
||||
reject(genericErrorText);
|
||||
return;
|
||||
console.error('[UploadAdapter] Empty response for file:', file.name)
|
||||
reject(genericErrorText)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查业务状态码(假设 code=1 表示成功)
|
||||
if (response.code == 1 || response.code == undefined) {
|
||||
const url = response.data?.url || response.data?.src;
|
||||
const url = response.data?.url || response.data?.src
|
||||
if (!url) {
|
||||
console.error("[UploadAdapter] No URL in response for file:", file.name, response);
|
||||
reject("Upload succeeded but no URL returned");
|
||||
return;
|
||||
console.error('[UploadAdapter] No URL in response for file:', file.name, response)
|
||||
reject('Upload succeeded but no URL returned')
|
||||
return
|
||||
}
|
||||
resolve({ default: url });
|
||||
resolve({ default: url })
|
||||
} else {
|
||||
const errorMessage = response.message || genericErrorText;
|
||||
console.error("[UploadAdapter] Upload failed for file:", file.name, "Error:", errorMessage);
|
||||
reject(errorMessage);
|
||||
const errorMessage = response.message || genericErrorText
|
||||
console.error('[UploadAdapter] Upload failed for file:', file.name, 'Error:', errorMessage)
|
||||
reject(errorMessage)
|
||||
}
|
||||
} else {
|
||||
console.error("[UploadAdapter] HTTP error for file:", file.name, "Status:", xhr.status);
|
||||
reject(`Server error (${xhr.status}): ${file.name}`);
|
||||
console.error('[UploadAdapter] HTTP error for file:', file.name, 'Status:', xhr.status)
|
||||
reject(`Server error (${xhr.status}): ${file.name}`)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// 上传进度监听
|
||||
if (xhr.upload) {
|
||||
xhr.upload.addEventListener("progress", (evt) => {
|
||||
xhr.upload.addEventListener('progress', (evt) => {
|
||||
if (evt.lengthComputable) {
|
||||
loader.uploadTotal = evt.total;
|
||||
loader.uploaded = evt.loaded;
|
||||
loader.uploadTotal = evt.total
|
||||
loader.uploaded = evt.loaded
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_initTimeout(reject) {
|
||||
// 清除之前的超时定时器(如果有)
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
clearTimeout(this.timeoutId)
|
||||
}
|
||||
|
||||
// 设置新的超时定时器
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
reject(new Error("Upload timeout"));
|
||||
this.xhr.abort()
|
||||
reject(new Error('Upload timeout'))
|
||||
}
|
||||
}, this.timeout);
|
||||
}, this.timeout)
|
||||
}
|
||||
|
||||
_sendRequest(file) {
|
||||
// 设置请求超时
|
||||
this.xhr.timeout = this.timeout;
|
||||
this.xhr.timeout = this.timeout
|
||||
|
||||
// Set headers if specified.
|
||||
const headers = this.options.upload.headers || {};
|
||||
const extendData = this.options.upload.extendData || {};
|
||||
const headers = this.options.upload.headers || {}
|
||||
const extendData = this.options.upload.extendData || {}
|
||||
// Use the withCredentials flag if specified.
|
||||
const withCredentials = this.options.upload.withCredentials || false;
|
||||
const uploadName = this.options.upload.uploadName || "file";
|
||||
const withCredentials = this.options.upload.withCredentials || false
|
||||
const uploadName = this.options.upload.uploadName || 'file'
|
||||
|
||||
for (const headerName of Object.keys(headers)) {
|
||||
this.xhr.setRequestHeader(headerName, headers[headerName]);
|
||||
this.xhr.setRequestHeader(headerName, headers[headerName])
|
||||
}
|
||||
|
||||
this.xhr.withCredentials = withCredentials;
|
||||
this.xhr.withCredentials = withCredentials
|
||||
|
||||
const data = new FormData();
|
||||
const data = new FormData()
|
||||
for (const key of Object.keys(extendData)) {
|
||||
data.append(key, extendData[key]);
|
||||
data.append(key, extendData[key])
|
||||
}
|
||||
data.append(uploadName, file);
|
||||
data.append(uploadName, file)
|
||||
|
||||
this.xhr.send(data);
|
||||
this.xhr.send(data)
|
||||
}
|
||||
}
|
||||
|
||||
export function UploadAdapterPlugin(editor) {
|
||||
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
|
||||
return new UploadAdapter(loader, editor.config._config);
|
||||
};
|
||||
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
|
||||
return new UploadAdapter(loader, editor.config._config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div :style="{ '--editor-height': editorHeight }">
|
||||
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur"
|
||||
@focus="onFocus"></ckeditor>
|
||||
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur" @focus="onFocus"></ckeditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,137 +68,119 @@ import {
|
||||
Underline,
|
||||
Undo,
|
||||
WordCount,
|
||||
} from "ckeditor5";
|
||||
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
|
||||
import { UploadAdapterPlugin } from "./UploadAdapter.js";
|
||||
} from 'ckeditor5'
|
||||
import { Ckeditor } from '@ckeditor/ckeditor5-vue'
|
||||
import { UploadAdapterPlugin } from './UploadAdapter.js'
|
||||
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useCurrentInstance } from "@/utils/tool";
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useCurrentInstance } from '@/utils/tool'
|
||||
|
||||
import coreTranslations from "ckeditor5/translations/zh-cn.js";
|
||||
import "ckeditor5/ckeditor5.css";
|
||||
import coreTranslations from 'ckeditor5/translations/zh-cn.js'
|
||||
import 'ckeditor5/ckeditor5.css'
|
||||
|
||||
const { proxy } = useCurrentInstance();
|
||||
const { proxy } = useCurrentInstance()
|
||||
|
||||
// 组件名称
|
||||
defineOptions({
|
||||
name: "scCkeditor"
|
||||
});
|
||||
name: 'scCkeditor',
|
||||
})
|
||||
|
||||
// Props 定义
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入内容……",
|
||||
default: '请输入内容……',
|
||||
},
|
||||
toolbar: {
|
||||
type: String,
|
||||
default: "basic",
|
||||
default: 'basic',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "400px",
|
||||
default: '400px',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Emits 定义
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 工具栏配置常量
|
||||
const TOOLBARS = {
|
||||
full: [
|
||||
"sourceEditing",
|
||||
"undo",
|
||||
"redo",
|
||||
"heading",
|
||||
"style",
|
||||
"|",
|
||||
"superscript",
|
||||
"subscript",
|
||||
"removeFormat",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"link",
|
||||
"fontBackgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontColor",
|
||||
"|",
|
||||
"outdent",
|
||||
"indent",
|
||||
"alignment",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"blockQuote",
|
||||
"insertTable",
|
||||
"imageInsert",
|
||||
"mediaEmbed",
|
||||
"highlight",
|
||||
"horizontalLine",
|
||||
"selectAll",
|
||||
"showBlocks",
|
||||
"specialCharacters",
|
||||
"codeBlock",
|
||||
"findAndReplace",
|
||||
'sourceEditing',
|
||||
'undo',
|
||||
'redo',
|
||||
'heading',
|
||||
'style',
|
||||
'|',
|
||||
'superscript',
|
||||
'subscript',
|
||||
'removeFormat',
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'link',
|
||||
'fontBackgroundColor',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontColor',
|
||||
'|',
|
||||
'outdent',
|
||||
'indent',
|
||||
'alignment',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'todoList',
|
||||
'|',
|
||||
'blockQuote',
|
||||
'insertTable',
|
||||
'imageInsert',
|
||||
'mediaEmbed',
|
||||
'highlight',
|
||||
'horizontalLine',
|
||||
'selectAll',
|
||||
'showBlocks',
|
||||
'specialCharacters',
|
||||
'codeBlock',
|
||||
'findAndReplace',
|
||||
],
|
||||
basic: [
|
||||
"sourceEditing",
|
||||
"undo",
|
||||
"redo",
|
||||
"heading",
|
||||
"|",
|
||||
"removeFormat",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"link",
|
||||
"fontBackgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontColor",
|
||||
"|",
|
||||
"outdent",
|
||||
"indent",
|
||||
"alignment",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"insertTable",
|
||||
"imageInsert",
|
||||
"mediaEmbed",
|
||||
'sourceEditing',
|
||||
'undo',
|
||||
'redo',
|
||||
'heading',
|
||||
'|',
|
||||
'removeFormat',
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'link',
|
||||
'fontBackgroundColor',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontColor',
|
||||
'|',
|
||||
'outdent',
|
||||
'indent',
|
||||
'alignment',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'todoList',
|
||||
'|',
|
||||
'insertTable',
|
||||
'imageInsert',
|
||||
'mediaEmbed',
|
||||
],
|
||||
simple: [
|
||||
"undo",
|
||||
"redo",
|
||||
"heading",
|
||||
"|",
|
||||
"removeFormat",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"link",
|
||||
"fontBackgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontColor",
|
||||
"|",
|
||||
"insertTable",
|
||||
"imageInsert",
|
||||
"mediaEmbed",
|
||||
],
|
||||
};
|
||||
simple: ['undo', 'redo', 'heading', '|', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor', '|', 'insertTable', 'imageInsert', 'mediaEmbed'],
|
||||
}
|
||||
|
||||
// 插件配置常量
|
||||
const PLUGINS = [
|
||||
@@ -265,16 +246,16 @@ const PLUGINS = [
|
||||
Undo,
|
||||
WordCount,
|
||||
UploadAdapterPlugin,
|
||||
];
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const editorData = ref("");
|
||||
const editorHeight = ref(props.height);
|
||||
const editor = ClassicEditor;
|
||||
const editorData = ref('')
|
||||
const editorHeight = ref(props.height)
|
||||
const editor = ClassicEditor
|
||||
|
||||
// 编辑器配置
|
||||
const editorConfig = computed(() => ({
|
||||
language: { ui: "zh-cn", content: "zh-cn" },
|
||||
language: { ui: 'zh-cn', content: 'zh-cn' },
|
||||
translations: [coreTranslations],
|
||||
plugins: PLUGINS,
|
||||
toolbar: {
|
||||
@@ -283,27 +264,18 @@ const editorConfig = computed(() => ({
|
||||
},
|
||||
placeholder: props.placeholder,
|
||||
image: {
|
||||
styles: ["alignLeft", "alignCenter", "alignRight"],
|
||||
toolbar: [
|
||||
"imageTextAlternative",
|
||||
"toggleImageCaption",
|
||||
"|",
|
||||
"imageStyle:alignLeft",
|
||||
"imageStyle:alignCenter",
|
||||
"imageStyle:alignRight",
|
||||
"|",
|
||||
"linkImage",
|
||||
],
|
||||
styles: ['alignLeft', 'alignCenter', 'alignRight'],
|
||||
toolbar: ['imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight', '|', 'linkImage'],
|
||||
},
|
||||
mediaEmbed: {
|
||||
previewsInData: true,
|
||||
providers: [
|
||||
{
|
||||
name: "mp4",
|
||||
name: 'mp4',
|
||||
url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i,
|
||||
html: match => {
|
||||
const url = match["input"];
|
||||
return ('<video controls width="100%" height="100%" src="' + url + '"></video>')
|
||||
html: (match) => {
|
||||
const url = match['input']
|
||||
return '<video controls width="100%" height="100%" src="' + url + '"></video>'
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -314,59 +286,57 @@ const editorConfig = computed(() => ({
|
||||
style: {
|
||||
definitions: [
|
||||
{
|
||||
name: "Article category",
|
||||
element: "h3",
|
||||
classes: ["category"],
|
||||
name: 'Article category',
|
||||
element: 'h3',
|
||||
classes: ['category'],
|
||||
},
|
||||
{
|
||||
name: "Info box",
|
||||
element: "p",
|
||||
classes: ["info-box"],
|
||||
name: 'Info box',
|
||||
element: 'p',
|
||||
classes: ['info-box'],
|
||||
},
|
||||
],
|
||||
},
|
||||
upload: {
|
||||
uploadUrl: proxy?.$API?.common?.upload?.url || "",
|
||||
uploadUrl: proxy?.$API?.common?.upload?.url || '',
|
||||
withCredentials: false,
|
||||
extendData: { type: "images" },
|
||||
extendData: { type: 'images' },
|
||||
headers: {
|
||||
Authorization: "Bearer " + proxy?.$TOOL?.data?.get("TOKEN"),
|
||||
Authorization: 'Bearer ' + proxy?.$TOOL?.data?.get('TOKEN'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
}))
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
editorData.value = newVal ?? "";
|
||||
editorData.value = newVal ?? ''
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听 height 变化
|
||||
watch(
|
||||
() => props.height,
|
||||
(newVal) => {
|
||||
editorHeight.value = newVal;
|
||||
}
|
||||
);
|
||||
editorHeight.value = newVal
|
||||
},
|
||||
)
|
||||
|
||||
// 移除图片宽高的正则替换函数
|
||||
const stripImageDimensions = (html) => {
|
||||
return html.replace(/<img[^>]*>/gi, (match) => {
|
||||
return match
|
||||
.replace(/width="[^"]*"/gi, "")
|
||||
.replace(/height="[^"]*"/gi, "");
|
||||
});
|
||||
};
|
||||
return match.replace(/width="[^"]*"/gi, '').replace(/height="[^"]*"/gi, '')
|
||||
})
|
||||
}
|
||||
|
||||
// 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题
|
||||
const onBlur = () => {
|
||||
const cleanedData = stripImageDimensions(editorData.value);
|
||||
editorData.value = cleanedData;
|
||||
emit("update:modelValue", cleanedData);
|
||||
};
|
||||
const cleanedData = stripImageDimensions(editorData.value)
|
||||
editorData.value = cleanedData
|
||||
emit('update:modelValue', cleanedData)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
//默认分栏数量和宽度 例如 [24] [18,6] [8,8,8] [6,12,6]
|
||||
layout: [24, 12, 12],
|
||||
//小组件分布,com取值:pages/home/components 文件名
|
||||
compsList: [["welcome"], ["info"], ["ver"]],
|
||||
compsList: [['welcome'], ['info'], ['ver']],
|
||||
},
|
||||
|
||||
//是否加密localStorage, 为空不加密
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import systemApi from "@/api/system";
|
||||
import systemApi from '@/api/system'
|
||||
|
||||
//上传配置
|
||||
|
||||
export default {
|
||||
apiObj: systemApi.upload.post, //上传请求API对象
|
||||
filename: "file", //form请求时文件的key
|
||||
successCode: 1, //请求完成代码
|
||||
maxSize: 10, //最大文件大小 默认10MB
|
||||
apiObj: systemApi.upload.post, //上传请求API对象
|
||||
filename: 'file', //form请求时文件的key
|
||||
successCode: 1, //请求完成代码
|
||||
maxSize: 10, //最大文件大小 默认10MB
|
||||
parseData: function (res) {
|
||||
return {
|
||||
code: res.code, //分析状态字段结构
|
||||
fileName: res.data.name,//分析文件名称
|
||||
src: res.data.url, //分析图片远程地址结构
|
||||
msg: res.message //分析描述字段结构
|
||||
code: res.code, //分析状态字段结构
|
||||
fileName: res.data.name, //分析文件名称
|
||||
src: res.data.url, //分析图片远程地址结构
|
||||
msg: res.message, //分析描述字段结构
|
||||
}
|
||||
},
|
||||
apiObjFile: systemApi.upload.post, //附件上传请求API对象
|
||||
maxSizeFile: 10 //最大文件大小 默认10MB
|
||||
apiObjFile: systemApi.upload.post, //附件上传请求API对象
|
||||
maxSizeFile: 10, //最大文件大小 默认10MB
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ export function useI18n() {
|
||||
availableLocales,
|
||||
setLocale: i18nStore.setLocale,
|
||||
currentLocale: i18nStore.currentLocale,
|
||||
localeLabel: i18nStore.localeLabel
|
||||
localeLabel: i18nStore.localeLabel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ const i18n = createI18n({
|
||||
fallbackLocale: 'en-US',
|
||||
messages: {
|
||||
'zh-CN': zh,
|
||||
'en-US': en
|
||||
}
|
||||
'en-US': en,
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
||||
@@ -157,7 +157,7 @@ export default {
|
||||
selectAll: 'Select All',
|
||||
unselectAll: 'Unselect All',
|
||||
retry: 'Retry',
|
||||
fetchDataFailed: 'Failed to fetch data'
|
||||
fetchDataFailed: 'Failed to fetch data',
|
||||
},
|
||||
menu: {
|
||||
dashboard: 'Dashboard',
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
roleManagement: 'Role Management',
|
||||
permissionManagement: 'Permission Management',
|
||||
systemSettings: 'System Settings',
|
||||
logManagement: 'Log Management'
|
||||
logManagement: 'Log Management',
|
||||
},
|
||||
login: {
|
||||
title: 'User Login',
|
||||
@@ -178,7 +178,7 @@ export default {
|
||||
noAccount: "Don't have an account?",
|
||||
registerNow: 'Register Now',
|
||||
forgotPassword: 'Forgot Password?',
|
||||
rememberMe: 'Remember Me'
|
||||
rememberMe: 'Remember Me',
|
||||
},
|
||||
register: {
|
||||
title: 'User Registration',
|
||||
@@ -197,7 +197,7 @@ export default {
|
||||
agreeTerms: 'I have read and agree to the',
|
||||
terms: 'User Agreement',
|
||||
hasAccount: 'Already have an account?',
|
||||
loginNow: 'Login Now'
|
||||
loginNow: 'Login Now',
|
||||
},
|
||||
resetPassword: {
|
||||
title: 'Reset Password',
|
||||
@@ -216,13 +216,13 @@ export default {
|
||||
codeSent: 'Verification code has been sent to your email',
|
||||
resendCode: 'Resend in {seconds} seconds',
|
||||
sendCodeFirst: 'Please enter email address first',
|
||||
backToLogin: 'Back to Login'
|
||||
backToLogin: 'Back to Login',
|
||||
},
|
||||
layout: {
|
||||
toggleSidebar: 'Toggle Sidebar',
|
||||
collapse: 'Collapse',
|
||||
expand: 'Expand',
|
||||
logout: 'Logout'
|
||||
logout: 'Logout',
|
||||
},
|
||||
table: {
|
||||
total: 'Total {total} items',
|
||||
@@ -230,13 +230,13 @@ export default {
|
||||
actions: 'Actions',
|
||||
noData: 'No Data',
|
||||
sort: 'Sort',
|
||||
filter: 'Filter'
|
||||
filter: 'Filter',
|
||||
},
|
||||
pagination: {
|
||||
goTo: 'Go to',
|
||||
page: 'Page',
|
||||
total: 'Total {total} items',
|
||||
itemsPerPage: '{size} items per page'
|
||||
itemsPerPage: '{size} items per page',
|
||||
},
|
||||
form: {
|
||||
required: 'This field is required',
|
||||
@@ -244,6 +244,6 @@ export default {
|
||||
invalidPhone: 'Please enter a valid phone number',
|
||||
passwordMismatch: 'Passwords do not match',
|
||||
minLength: 'Minimum {min} characters required',
|
||||
maxLength: 'Maximum {max} characters allowed'
|
||||
}
|
||||
maxLength: 'Maximum {max} characters allowed',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
columnSettings: '列显示设置',
|
||||
selectAll: '全选',
|
||||
unselectAll: '取消全选',
|
||||
retry: '重试'
|
||||
retry: '重试',
|
||||
},
|
||||
menu: {
|
||||
dashboard: '仪表板',
|
||||
@@ -164,7 +164,7 @@ export default {
|
||||
roleManagement: '角色管理',
|
||||
permissionManagement: '权限管理',
|
||||
systemSettings: '系统设置',
|
||||
logManagement: '日志管理'
|
||||
logManagement: '日志管理',
|
||||
},
|
||||
login: {
|
||||
title: '用户登录',
|
||||
@@ -177,7 +177,7 @@ export default {
|
||||
noAccount: '还没有账户?',
|
||||
registerNow: '立即注册',
|
||||
forgotPassword: '忘记密码?',
|
||||
rememberMe: '记住我'
|
||||
rememberMe: '记住我',
|
||||
},
|
||||
register: {
|
||||
title: '用户注册',
|
||||
@@ -196,7 +196,7 @@ export default {
|
||||
agreeTerms: '我已阅读并同意',
|
||||
terms: '用户协议',
|
||||
hasAccount: '已有账户?',
|
||||
loginNow: '立即登录'
|
||||
loginNow: '立即登录',
|
||||
},
|
||||
resetPassword: {
|
||||
title: '重置密码',
|
||||
@@ -215,13 +215,13 @@ export default {
|
||||
codeSent: '验证码已发送到您的邮箱',
|
||||
resendCode: '{seconds}秒后重新发送',
|
||||
sendCodeFirst: '请先输入邮箱地址',
|
||||
backToLogin: '返回登录'
|
||||
backToLogin: '返回登录',
|
||||
},
|
||||
layout: {
|
||||
toggleSidebar: '切换侧边栏',
|
||||
collapse: '折叠',
|
||||
expand: '展开',
|
||||
logout: '退出登录'
|
||||
logout: '退出登录',
|
||||
},
|
||||
table: {
|
||||
total: '共 {total} 条',
|
||||
@@ -229,13 +229,13 @@ export default {
|
||||
actions: '操作',
|
||||
noData: '暂无数据',
|
||||
sort: '排序',
|
||||
filter: '筛选'
|
||||
filter: '筛选',
|
||||
},
|
||||
pagination: {
|
||||
goTo: '前往',
|
||||
page: '页',
|
||||
total: '共 {total} 条',
|
||||
itemsPerPage: '每页 {size} 条'
|
||||
itemsPerPage: '每页 {size} 条',
|
||||
},
|
||||
form: {
|
||||
required: '此项为必填项',
|
||||
@@ -243,6 +243,6 @@ export default {
|
||||
invalidPhone: '请输入有效的手机号',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
minLength: '最少需要 {min} 个字符',
|
||||
maxLength: '最多允许 {max} 个字符'
|
||||
}
|
||||
maxLength: '最多允许 {max} 个字符',
|
||||
},
|
||||
}
|
||||
|
||||
0
src/layouts/components/breadcrumb.vue
Normal file
0
src/layouts/components/breadcrumb.vue
Normal file
0
src/layouts/components/menu.vue
Normal file
0
src/layouts/components/menu.vue
Normal file
0
src/layouts/components/tags.vue
Normal file
0
src/layouts/components/tags.vue
Normal file
0
src/layouts/components/userbar.vue
Normal file
0
src/layouts/components/userbar.vue
Normal file
@@ -1 +1,59 @@
|
||||
<template></template>
|
||||
<template><el-container class="app-wrapper" :class="layoutClass">
|
||||
<!-- 默认布局:左侧双栏布局 -->
|
||||
<template v-if="layoutMode === 'default'">
|
||||
<el-aside width="60px">
|
||||
<!-- logo -->
|
||||
<div class="logo"></div>
|
||||
<!-- 一级菜单 -->
|
||||
<div class="menu-list">
|
||||
<div class="item">
|
||||
图标
|
||||
文字
|
||||
</div>
|
||||
</div>
|
||||
</el-aside>
|
||||
<el-aside>
|
||||
<el-menu default-active="2" @open="handleOpen" @close="handleClose">
|
||||
<menu />
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<breadcrumb />
|
||||
<userbar />
|
||||
</el-header>
|
||||
<el-main><router-view /></el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
<!-- Menu布局:左侧菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'menu'">
|
||||
<el-aside>
|
||||
<!-- logo+系统名称 -->
|
||||
<div class="logo"></div>
|
||||
<el-menu default-active="2" @open="handleOpen" @close="handleClose">
|
||||
<menu />
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<breadcrumb />
|
||||
<userbar />
|
||||
</el-header>
|
||||
<el-main><router-view /></el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
<!-- Top布局:顶部菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'top'">
|
||||
<el-header>
|
||||
<breadcrumb />
|
||||
<userbar />
|
||||
</el-header>
|
||||
<el-main><router-view /></el-main>
|
||||
</template>
|
||||
</el-container></template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'AppLayouts',
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
<template>
|
||||
<div class="not-found-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="not-found-content">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-title">页面未找到</div>
|
||||
<div class="error-description">
|
||||
抱歉,您访问的页面不存在或已被移除
|
||||
<div class="not-found-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="large" @click="goBack">
|
||||
<el-icon>
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
返回上一页
|
||||
</el-button>
|
||||
<el-button size="large" @click="goHome">
|
||||
<el-icon>
|
||||
<HomeFilled />
|
||||
</el-icon>
|
||||
返回首页
|
||||
</el-button>
|
||||
<div class="not-found-content">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-title">页面未找到</div>
|
||||
<div class="error-description">抱歉,您访问的页面不存在或已被移除</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="large" @click="goBack">
|
||||
<el-icon>
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
返回上一页
|
||||
</el-button>
|
||||
<el-button size="large" @click="goHome">
|
||||
<el-icon>
|
||||
<HomeFilled />
|
||||
</el-icon>
|
||||
返回首页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -104,7 +102,6 @@ const goHome = () => {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
@@ -136,7 +133,6 @@ const goHome = () => {
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div class="empty-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="empty-content">
|
||||
<div class="empty-icon">
|
||||
<el-icon :size="120" color="#ff6b35">
|
||||
<Box />
|
||||
</el-icon>
|
||||
<div class="empty-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="empty-title">暂无数据</div>
|
||||
<div class="empty-description">
|
||||
{{ description || '当前页面暂无数据,请稍后再试' }}
|
||||
</div>
|
||||
<div class="empty-content">
|
||||
<div class="empty-icon">
|
||||
<el-icon :size="120" color="#ff6b35">
|
||||
<Box />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<el-button v-if="showButton" type="primary" size="large" @click="handleAction">
|
||||
<el-icon v-if="buttonIcon">
|
||||
<component :is="buttonIcon" />
|
||||
</el-icon>
|
||||
{{ buttonText || '刷新页面' }}
|
||||
</el-button>
|
||||
<div class="empty-title">暂无数据</div>
|
||||
<div class="empty-description">
|
||||
{{ description || '当前页面暂无数据,请稍后再试' }}
|
||||
</div>
|
||||
|
||||
<el-button v-if="showButton" type="primary" size="large" @click="handleAction">
|
||||
<el-icon v-if="buttonIcon">
|
||||
<component :is="buttonIcon" />
|
||||
</el-icon>
|
||||
{{ buttonText || '刷新页面' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -35,20 +35,20 @@ import '@/assets/style/auth.scss'
|
||||
defineProps({
|
||||
description: {
|
||||
type: String,
|
||||
default: '当前页面暂无数据,请稍后再试'
|
||||
default: '当前页面暂无数据,请稍后再试',
|
||||
},
|
||||
showButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '刷新页面'
|
||||
default: '刷新页面',
|
||||
},
|
||||
buttonIcon: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['action'])
|
||||
@@ -113,7 +113,6 @@ const handleAction = () => {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
@@ -138,7 +137,6 @@ const handleAction = () => {
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
|
||||
8
src/pages/home/index.vue
Normal file
8
src/pages/home/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'HomeIndex'
|
||||
})
|
||||
</script>
|
||||
@@ -1,60 +1,56 @@
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">找回密码</h1>
|
||||
<p class="auth-subtitle">输入您的邮箱,我们将发送重置密码链接</p>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<el-form ref="forgotFormRef" :model="forgotForm" :rules="forgotRules" class="auth-form"
|
||||
@submit.prevent="handleSubmit">
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="forgotForm.email" placeholder="请输入注册邮箱" size="large" clearable
|
||||
@keyup.enter="handleSubmit">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">找回密码</h1>
|
||||
<p class="auth-subtitle">输入您的邮箱,我们将发送重置密码链接</p>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="captcha" v-if="showCaptcha">
|
||||
<div style="display: flex; gap: 12px">
|
||||
<el-input v-model="forgotForm.captcha" placeholder="请输入验证码" size="large" style="flex: 1">
|
||||
<el-form ref="forgotFormRef" :model="forgotForm" :rules="forgotRules" class="auth-form" @submit.prevent="handleSubmit">
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="forgotForm.email" placeholder="请输入注册邮箱" size="large" clearable @keyup.enter="handleSubmit">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Key />
|
||||
<Message />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="info" size="large" :disabled="captchaDisabled" @click="sendCaptcha">
|
||||
{{ captchaButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleSubmit">
|
||||
{{ loading ? '提交中...' : '发送重置链接' }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
<el-form-item prop="captcha" v-if="showCaptcha">
|
||||
<div style="display: flex; gap: 12px">
|
||||
<el-input v-model="forgotForm.captcha" placeholder="请输入验证码" size="large" style="flex: 1">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Key />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="info" size="large" :disabled="captchaDisabled" @click="sendCaptcha">
|
||||
{{ captchaButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
想起密码了?
|
||||
<router-link to="/login" class="auth-link">
|
||||
返回登录
|
||||
</router-link>
|
||||
</p>
|
||||
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleSubmit">
|
||||
{{ loading ? '提交中...' : '发送重置链接' }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
想起密码了?
|
||||
<router-link to="/login" class="auth-link"> 返回登录 </router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -73,7 +69,7 @@ const countdown = ref(60)
|
||||
// Forgot password form data
|
||||
const forgotForm = reactive({
|
||||
email: '',
|
||||
captcha: ''
|
||||
captcha: '',
|
||||
})
|
||||
|
||||
// Captcha button text
|
||||
@@ -83,12 +79,12 @@ const captchaButtonText = ref('获取验证码')
|
||||
const forgotRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
captcha: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '验证码为6位数字', trigger: 'blur' }
|
||||
]
|
||||
{ len: 6, message: '验证码为6位数字', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// Send captcha code
|
||||
@@ -109,7 +105,7 @@ const sendCaptcha = async () => {
|
||||
// Simulate API call - Replace with actual API call
|
||||
// Example: const response = await sendCaptchaApi(forgotForm.email)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
ElMessage.success('验证码已发送至您的邮箱')
|
||||
|
||||
@@ -128,7 +124,6 @@ const sendCaptcha = async () => {
|
||||
}, 1000)
|
||||
|
||||
showCaptcha.value = true
|
||||
|
||||
} catch (error) {
|
||||
console.error('Send captcha failed:', error)
|
||||
ElMessage.error('发送验证码失败,请稍后重试')
|
||||
@@ -147,7 +142,7 @@ const handleSubmit = async () => {
|
||||
// Example: const response = await forgotPasswordApi(forgotForm)
|
||||
|
||||
// Simulated delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Success message
|
||||
ElMessage.success('密码重置链接已发送至您的邮箱,请注意查收')
|
||||
@@ -156,7 +151,6 @@ const handleSubmit = async () => {
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 2000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Forgot password failed:', error)
|
||||
ElMessage.error('提交失败,请检查邮箱地址和验证码')
|
||||
|
||||
@@ -36,12 +36,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<div class="auth-links">
|
||||
<el-checkbox v-model="loginForm.rememberMe" class="remember-me">
|
||||
记住我
|
||||
</el-checkbox>
|
||||
<router-link to="/forgot-password" class="forgot-password">
|
||||
忘记密码?
|
||||
</router-link>
|
||||
<el-checkbox v-model="loginForm.rememberMe" class="remember-me"> 记住我 </el-checkbox>
|
||||
<router-link to="/forgot-password" class="forgot-password"> 忘记密码? </router-link>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleLogin">
|
||||
@@ -52,9 +48,7 @@
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
还没有账户?
|
||||
<router-link to="/register" class="auth-link">
|
||||
立即注册
|
||||
</router-link>
|
||||
<router-link to="/register" class="auth-link"> 立即注册 </router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,10 +57,9 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { defineOptions } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginPage'
|
||||
name: 'LoginPage',
|
||||
})
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -88,19 +81,19 @@ const userStore = useUserStore()
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
rememberMe: false,
|
||||
})
|
||||
|
||||
// Form validation rules
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名或邮箱', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' }
|
||||
]
|
||||
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// Handle login
|
||||
@@ -115,7 +108,7 @@ const handleLogin = async () => {
|
||||
// 1. Call login API
|
||||
const loginResponse = await auth.login.post({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
password: loginForm.password,
|
||||
})
|
||||
|
||||
// Check if login was successful
|
||||
@@ -146,38 +139,39 @@ const handleLogin = async () => {
|
||||
const menuResponse = await auth.menu.my.get()
|
||||
|
||||
if (menuResponse && menuResponse.data) {
|
||||
userStore.setMenu(menuResponse.data)
|
||||
userStore.setMenu(menuResponse.data.menu)
|
||||
userStore.setPermissions(menuResponse.data.permissions)
|
||||
}
|
||||
|
||||
// 5. Cache system configuration data
|
||||
try {
|
||||
const settingResponse = await system.setting.list.get()
|
||||
if (settingResponse && settingResponse.data) {
|
||||
tool.data.set('system_setting', settingResponse.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cache system settings:', error)
|
||||
}
|
||||
// // 5. Cache system configuration data
|
||||
// try {
|
||||
// const settingResponse = await system.setting.list.get()
|
||||
// if (settingResponse && settingResponse.data) {
|
||||
// tool.data.set('system_setting', settingResponse.data)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to cache system settings:', error)
|
||||
// }
|
||||
|
||||
// 6. Cache dictionary data
|
||||
try {
|
||||
const dictResponse = await system.dictionary.list.get()
|
||||
if (dictResponse && dictResponse.data) {
|
||||
tool.data.set('system_dictionary', dictResponse.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cache dictionary data:', error)
|
||||
}
|
||||
// // 6. Cache dictionary data
|
||||
// try {
|
||||
// const dictResponse = await system.dictionary.list.get()
|
||||
// if (dictResponse && dictResponse.data) {
|
||||
// tool.data.set('system_dictionary', dictResponse.data)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to cache dictionary data:', error)
|
||||
// }
|
||||
|
||||
// 7. Cache area data
|
||||
try {
|
||||
const areaResponse = await system.area.list.get()
|
||||
if (areaResponse && areaResponse.data) {
|
||||
tool.data.set('system_area', areaResponse.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cache area data:', error)
|
||||
}
|
||||
// // 7. Cache area data
|
||||
// try {
|
||||
// const areaResponse = await system.area.list.get()
|
||||
// if (areaResponse && areaResponse.data) {
|
||||
// tool.data.set('system_area', areaResponse.data)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to cache area data:', error)
|
||||
// }
|
||||
|
||||
// Success message
|
||||
ElMessage.success('登录成功!')
|
||||
@@ -195,7 +189,6 @@ const handleLogin = async () => {
|
||||
router.push(config.DASHBOARD_URL)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error)
|
||||
|
||||
|
||||
@@ -1,85 +1,80 @@
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">创建账户</h1>
|
||||
<p class="auth-subtitle">加入我们,开启科技之旅</p>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form"
|
||||
@submit.prevent="handleRegister">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" placeholder="请输入用户名" size="large" clearable>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">创建账户</h1>
|
||||
<p class="auth-subtitle">加入我们,开启科技之旅</p>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="registerForm.email" placeholder="请输入邮箱地址" size="large" clearable>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" @submit.prevent="handleRegister">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" placeholder="请输入用户名" size="large" clearable>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码(至少6位)" size="large"
|
||||
show-password>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="registerForm.email" placeholder="请输入邮箱地址" size="large" clearable>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" size="large"
|
||||
show-password @keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码(至少6位)" size="large" show-password>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="agreeTerms">
|
||||
<el-checkbox v-model="registerForm.agreeTerms" class="remember-me">
|
||||
我已阅读并同意
|
||||
<a href="#" class="auth-link">服务条款</a>
|
||||
和
|
||||
<a href="#" class="auth-link">隐私政策</a>
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" size="large" show-password @keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleRegister">
|
||||
{{ loading ? '注册中...' : '注册' }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
<el-form-item prop="agreeTerms">
|
||||
<el-checkbox v-model="registerForm.agreeTerms" class="remember-me">
|
||||
我已阅读并同意
|
||||
<a href="#" class="auth-link">服务条款</a>
|
||||
和
|
||||
<a href="#" class="auth-link">隐私政策</a>
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
已有账户?
|
||||
<router-link to="/login" class="auth-link">
|
||||
立即登录
|
||||
</router-link>
|
||||
</p>
|
||||
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleRegister">
|
||||
{{ loading ? '注册中...' : '注册' }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
已有账户?
|
||||
<router-link to="/login" class="auth-link"> 立即登录 </router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -98,7 +93,7 @@ const registerForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreeTerms: false
|
||||
agreeTerms: false,
|
||||
})
|
||||
|
||||
// Custom password validation
|
||||
@@ -127,26 +122,22 @@ const validateConfirmPassword = (rule, value, callback) => {
|
||||
const registerRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
|
||||
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, validator: validatePassword, trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, validator: validateConfirmPassword, trigger: 'blur' }
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
password: [{ required: true, validator: validatePassword, trigger: 'blur' }],
|
||||
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
|
||||
agreeTerms: [
|
||||
{
|
||||
type: 'enum',
|
||||
enum: [true],
|
||||
message: '请阅读并同意服务条款和隐私政策',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Handle register
|
||||
@@ -161,7 +152,7 @@ const handleRegister = async () => {
|
||||
// Example: const response = await registerApi(registerForm)
|
||||
|
||||
// Simulated delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Success message
|
||||
ElMessage.success('注册成功!正在跳转到登录页面...')
|
||||
@@ -170,7 +161,6 @@ const handleRegister = async () => {
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Register failed:', error)
|
||||
ElMessage.error('注册失败,请稍后重试')
|
||||
|
||||
@@ -9,7 +9,7 @@ import systemRoutes from './systemRoutes'
|
||||
NProgress.configure({
|
||||
showSpinner: false,
|
||||
trickleSpeed: 200,
|
||||
minimum: 0.3
|
||||
minimum: 0.3,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -21,14 +21,14 @@ const notFoundRoute = {
|
||||
component: () => import('../layouts/other/404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
hidden: true
|
||||
}
|
||||
hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: systemRoutes
|
||||
routes: systemRoutes,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -63,8 +63,8 @@ function transformMenusToRoutes(menus) {
|
||||
}
|
||||
|
||||
return menus
|
||||
.filter(menu => menu && menu.path)
|
||||
.map(menu => {
|
||||
.filter((menu) => menu && menu.path)
|
||||
.map((menu) => {
|
||||
const route = {
|
||||
path: menu.path,
|
||||
name: menu.name || menu.path.replace(/\//g, '-'),
|
||||
@@ -74,8 +74,8 @@ function transformMenusToRoutes(menus) {
|
||||
hidden: menu.hidden || menu.meta?.hidden,
|
||||
keepAlive: menu.meta?.keepAlive || false,
|
||||
affix: menu.meta?.affix || 0,
|
||||
role: menu.meta?.role || []
|
||||
}
|
||||
role: menu.meta?.role || [],
|
||||
},
|
||||
}
|
||||
|
||||
// 处理组件
|
||||
@@ -107,9 +107,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start()
|
||||
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title
|
||||
? `${to.meta.title} - ${config.APP_NAME}`
|
||||
: config.APP_NAME
|
||||
document.title = to.meta.title ? `${to.meta.title} - ${config.APP_NAME}` : config.APP_NAME
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isLoggedIn = userStore.isLoggedIn()
|
||||
@@ -126,7 +124,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 保存目标路由,登录后跳转
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
query: { redirect: to.fullPath },
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -149,7 +147,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
const dynamicRoutes = transformMenusToRoutes(mergedMenus)
|
||||
|
||||
// 添加动态路由到 Layout 的子路由
|
||||
dynamicRoutes.forEach(route => {
|
||||
dynamicRoutes.forEach((route) => {
|
||||
router.addRoute('Layout', route)
|
||||
})
|
||||
|
||||
@@ -172,7 +170,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
userStore.logout()
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
query: { redirect: to.fullPath },
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -196,7 +194,7 @@ export function resetRouter() {
|
||||
// 重置为初始路由
|
||||
const newRouter = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: systemRoutes
|
||||
routes: systemRoutes,
|
||||
})
|
||||
|
||||
router.matcher = newRouter.matcher
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
export const useI18nStore = defineStore(
|
||||
'i18n',
|
||||
{
|
||||
state: () => ({
|
||||
currentLocale: 'zh-CN',
|
||||
availableLocales: [
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' }
|
||||
]
|
||||
}),
|
||||
export const useI18nStore = defineStore('i18n', {
|
||||
state: () => ({
|
||||
currentLocale: 'zh-CN',
|
||||
availableLocales: [
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
],
|
||||
}),
|
||||
|
||||
getters: {
|
||||
localeLabel: (state) => {
|
||||
const locale = state.availableLocales.find((item) => item.value === state.currentLocale)
|
||||
return locale ? locale.label : ''
|
||||
}
|
||||
getters: {
|
||||
localeLabel: (state) => {
|
||||
const locale = state.availableLocales.find((item) => item.value === state.currentLocale)
|
||||
return locale ? locale.label : ''
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
setLocale(locale) {
|
||||
this.currentLocale = locale
|
||||
i18n.global.locale.value = locale
|
||||
}
|
||||
actions: {
|
||||
setLocale(locale) {
|
||||
this.currentLocale = locale
|
||||
i18n.global.locale.value = locale
|
||||
},
|
||||
},
|
||||
|
||||
persist: {
|
||||
key: 'i18n-store',
|
||||
pick: ['currentLocale']
|
||||
}
|
||||
}
|
||||
)
|
||||
persist: {
|
||||
key: 'i18n-store',
|
||||
pick: ['currentLocale'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -41,14 +41,14 @@ export const useUserStore = defineStore(
|
||||
const menuMap = new Map()
|
||||
|
||||
// 先添加静态菜单
|
||||
staticMenus.forEach(menu => {
|
||||
staticMenus.forEach((menu) => {
|
||||
if (menu.path) {
|
||||
menuMap.set(menu.path, menu)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加后端菜单,如果路径重复则覆盖
|
||||
newMenu.forEach(menu => {
|
||||
newMenu.forEach((menu) => {
|
||||
if (menu.path) {
|
||||
menuMap.set(menu.path, menu)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export const useUserStore = defineStore(
|
||||
{
|
||||
persist: {
|
||||
key: 'user-store',
|
||||
pick: ['token', 'refreshToken', 'userInfo', 'menu']
|
||||
}
|
||||
}
|
||||
pick: ['token', 'refreshToken', 'userInfo', 'menu'],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import router from '@/router'
|
||||
|
||||
const http = axios.create({
|
||||
timeout: 30000,
|
||||
baseURL: config.API_URL
|
||||
baseURL: config.API_URL,
|
||||
})
|
||||
|
||||
// 是否正在刷新 token
|
||||
@@ -29,7 +29,7 @@ http.interceptors.request.use(
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
@@ -124,7 +124,7 @@ http.interceptors.response.use(
|
||||
const errorMessage = data?.message || error.message || '请求失败'
|
||||
ElMessage.error(errorMessage)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 刷新 token 的方法
|
||||
@@ -136,7 +136,7 @@ async function refreshToken() {
|
||||
const refreshTokenValue = userStore.refreshToken
|
||||
|
||||
const response = await axios.post(refreshUrl, {
|
||||
refreshToken: refreshTokenValue
|
||||
refreshToken: refreshTokenValue,
|
||||
})
|
||||
|
||||
// 假设返回格式为 { code, data: { token, refreshToken } }
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* @LastEditTime: 2026年1月15日
|
||||
*/
|
||||
|
||||
import CryptoJS from "crypto-js";
|
||||
import sysConfig from "@/config";
|
||||
import CryptoJS from 'crypto-js'
|
||||
import sysConfig from '@/config'
|
||||
|
||||
const tool = {};
|
||||
const tool = {}
|
||||
|
||||
/**
|
||||
* 检查是否为有效的值(非null、非undefined、非空字符串、非空数组、非空对象)
|
||||
@@ -17,19 +17,19 @@ const tool = {};
|
||||
*/
|
||||
tool.isValid = function (value) {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (typeof value === "string" && value.trim() === "") {
|
||||
return false;
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (typeof value === "object" && Object.keys(value).length === 0) {
|
||||
return false;
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0) {
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
@@ -39,21 +39,21 @@ tool.isValid = function (value) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
tool.debounce = function (func, wait = 300, immediate = false) {
|
||||
let timeout;
|
||||
let timeout
|
||||
return function (...args) {
|
||||
const context = this;
|
||||
clearTimeout(timeout);
|
||||
const context = this
|
||||
clearTimeout(timeout)
|
||||
if (immediate && !timeout) {
|
||||
func.apply(context, args);
|
||||
func.apply(context, args)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
timeout = null
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
func.apply(context, args)
|
||||
}
|
||||
}, wait);
|
||||
};
|
||||
};
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
@@ -63,36 +63,36 @@ tool.debounce = function (func, wait = 300, immediate = false) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
tool.throttle = function (func, wait = 300, options = {}) {
|
||||
let timeout;
|
||||
let previous = 0;
|
||||
const { leading = true, trailing = true } = options;
|
||||
let timeout
|
||||
let previous = 0
|
||||
const { leading = true, trailing = true } = options
|
||||
|
||||
return function (...args) {
|
||||
const context = this;
|
||||
const now = Date.now();
|
||||
const context = this
|
||||
const now = Date.now()
|
||||
|
||||
if (!previous && !leading) {
|
||||
previous = now;
|
||||
previous = now
|
||||
}
|
||||
|
||||
const remaining = wait - (now - previous);
|
||||
const remaining = wait - (now - previous)
|
||||
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
previous = now;
|
||||
func.apply(context, args);
|
||||
previous = now
|
||||
func.apply(context, args)
|
||||
} else if (!timeout && trailing) {
|
||||
timeout = setTimeout(() => {
|
||||
previous = leading ? Date.now() : 0;
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
}, remaining);
|
||||
previous = leading ? Date.now() : 0
|
||||
timeout = null
|
||||
func.apply(context, args)
|
||||
}, remaining)
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝对象(支持循环引用)
|
||||
@@ -101,99 +101,88 @@ tool.throttle = function (func, wait = 300, options = {}) {
|
||||
* @returns {*}
|
||||
*/
|
||||
tool.deepClone = function (obj, hash = new WeakMap()) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (hash.has(obj)) {
|
||||
return hash.get(obj);
|
||||
return hash.get(obj)
|
||||
}
|
||||
|
||||
const clone = Array.isArray(obj) ? [] : {};
|
||||
hash.set(obj, clone);
|
||||
const clone = Array.isArray(obj) ? [] : {}
|
||||
hash.set(obj, clone)
|
||||
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
clone[key] = tool.deepClone(obj[key], hash);
|
||||
clone[key] = tool.deepClone(obj[key], hash)
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
};
|
||||
return clone
|
||||
}
|
||||
|
||||
/* localStorage */
|
||||
tool.data = {
|
||||
set(key, data, datetime = 0) {
|
||||
//加密
|
||||
if (sysConfig.LS_ENCRYPTION == "AES") {
|
||||
data = tool.crypto.AES.encrypt(
|
||||
JSON.stringify(data),
|
||||
sysConfig.LS_ENCRYPTION_key,
|
||||
);
|
||||
if (sysConfig.LS_ENCRYPTION == 'AES') {
|
||||
data = tool.crypto.AES.encrypt(JSON.stringify(data), sysConfig.LS_ENCRYPTION_key)
|
||||
}
|
||||
let cacheValue = {
|
||||
content: data,
|
||||
datetime:
|
||||
parseInt(datetime) === 0
|
||||
? 0
|
||||
: new Date().getTime() + parseInt(datetime) * 1000,
|
||||
};
|
||||
return localStorage.setItem(key, JSON.stringify(cacheValue));
|
||||
datetime: parseInt(datetime) === 0 ? 0 : new Date().getTime() + parseInt(datetime) * 1000,
|
||||
}
|
||||
return localStorage.setItem(key, JSON.stringify(cacheValue))
|
||||
},
|
||||
get(key) {
|
||||
try {
|
||||
const value = JSON.parse(localStorage.getItem(key));
|
||||
const value = JSON.parse(localStorage.getItem(key))
|
||||
if (value) {
|
||||
let nowTime = new Date().getTime();
|
||||
let nowTime = new Date().getTime()
|
||||
if (nowTime > value.datetime && value.datetime != 0) {
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
}
|
||||
//解密
|
||||
if (sysConfig.LS_ENCRYPTION == "AES") {
|
||||
value.content = JSON.parse(
|
||||
tool.crypto.AES.decrypt(
|
||||
value.content,
|
||||
sysConfig.LS_ENCRYPTION_key,
|
||||
),
|
||||
);
|
||||
if (sysConfig.LS_ENCRYPTION == 'AES') {
|
||||
value.content = JSON.parse(tool.crypto.AES.decrypt(value.content, sysConfig.LS_ENCRYPTION_key))
|
||||
}
|
||||
return value.content;
|
||||
return value.content
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
} catch {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
},
|
||||
remove(key) {
|
||||
return localStorage.removeItem(key);
|
||||
return localStorage.removeItem(key)
|
||||
},
|
||||
clear() {
|
||||
return localStorage.clear();
|
||||
return localStorage.clear()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*sessionStorage*/
|
||||
tool.session = {
|
||||
set(table, settings) {
|
||||
const _set = JSON.stringify(settings);
|
||||
return sessionStorage.setItem(table, _set);
|
||||
const _set = JSON.stringify(settings)
|
||||
return sessionStorage.setItem(table, _set)
|
||||
},
|
||||
get(table) {
|
||||
const data = sessionStorage.getItem(table);
|
||||
const data = sessionStorage.getItem(table)
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
return JSON.parse(data)
|
||||
} catch {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
},
|
||||
remove(table) {
|
||||
return sessionStorage.removeItem(table);
|
||||
return sessionStorage.removeItem(table)
|
||||
},
|
||||
clear() {
|
||||
return sessionStorage.clear();
|
||||
return sessionStorage.clear()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*cookie*/
|
||||
tool.cookie = {
|
||||
@@ -210,28 +199,28 @@ tool.cookie = {
|
||||
domain: null,
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
sameSite: "Lax",
|
||||
sameSite: 'Lax',
|
||||
...config,
|
||||
};
|
||||
let cookieStr = `${name}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
let cookieStr = `${name}=${encodeURIComponent(value)}`
|
||||
if (cfg.expires) {
|
||||
const exp = new Date();
|
||||
exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000);
|
||||
cookieStr += `;expires=${exp.toUTCString()}`;
|
||||
const exp = new Date()
|
||||
exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000)
|
||||
cookieStr += `;expires=${exp.toUTCString()}`
|
||||
}
|
||||
if (cfg.path) {
|
||||
cookieStr += `;path=${cfg.path}`;
|
||||
cookieStr += `;path=${cfg.path}`
|
||||
}
|
||||
if (cfg.domain) {
|
||||
cookieStr += `;domain=${cfg.domain}`;
|
||||
cookieStr += `;domain=${cfg.domain}`
|
||||
}
|
||||
if (cfg.secure) {
|
||||
cookieStr += `;secure`;
|
||||
cookieStr += `;secure`
|
||||
}
|
||||
if (cfg.sameSite) {
|
||||
cookieStr += `;SameSite=${cfg.sameSite}`;
|
||||
cookieStr += `;SameSite=${cfg.sameSite}`
|
||||
}
|
||||
document.cookie = cookieStr;
|
||||
document.cookie = cookieStr
|
||||
},
|
||||
/**
|
||||
* 获取cookie
|
||||
@@ -239,24 +228,22 @@ tool.cookie = {
|
||||
* @returns {string|null}
|
||||
*/
|
||||
get(name) {
|
||||
const arr = document.cookie.match(
|
||||
new RegExp("(^| )" + name + "=([^;]*)(;|$)"),
|
||||
);
|
||||
const arr = document.cookie.match(new RegExp('(^| )' + name + '=([^;]*)(;|$)'))
|
||||
if (arr != null) {
|
||||
return decodeURIComponent(arr[2]);
|
||||
return decodeURIComponent(arr[2])
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
},
|
||||
/**
|
||||
* 删除cookie
|
||||
* @param {string} name - cookie名称
|
||||
*/
|
||||
remove(name) {
|
||||
const exp = new Date();
|
||||
exp.setTime(exp.getTime() - 1);
|
||||
document.cookie = `${name}=;expires=${exp.toUTCString()}`;
|
||||
const exp = new Date()
|
||||
exp.setTime(exp.getTime() - 1)
|
||||
document.cookie = `${name}=;expires=${exp.toUTCString()}`
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/* Fullscreen */
|
||||
/**
|
||||
@@ -264,34 +251,29 @@ tool.cookie = {
|
||||
* @param {HTMLElement} element - 要全屏的元素
|
||||
*/
|
||||
tool.screen = function (element) {
|
||||
const isFull = !!(
|
||||
document.webkitIsFullScreen ||
|
||||
document.mozFullScreen ||
|
||||
document.msFullscreenElement ||
|
||||
document.fullscreenElement
|
||||
);
|
||||
const isFull = !!(document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement || document.fullscreenElement)
|
||||
if (isFull) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
document.exitFullscreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
document.msExitFullscreen()
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
document.mozCancelFullScreen()
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
document.webkitExitFullscreen()
|
||||
}
|
||||
} else {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
element.requestFullscreen()
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen();
|
||||
element.msRequestFullscreen()
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen();
|
||||
element.mozRequestFullScreen()
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
element.webkitRequestFullscreen()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* 复制对象(浅拷贝) */
|
||||
/**
|
||||
@@ -300,11 +282,11 @@ tool.screen = function (element) {
|
||||
* @returns {*} - 拷贝后的对象
|
||||
*/
|
||||
tool.objCopy = function (obj) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
/* 日期格式化 */
|
||||
/**
|
||||
@@ -313,38 +295,30 @@ tool.objCopy = function (obj) {
|
||||
* @param {string} fmt - 格式化字符串,默认 "yyyy-MM-dd hh:mm:ss"
|
||||
* @returns {string} - 格式化后的日期字符串
|
||||
*/
|
||||
tool.dateFormat = function (date, fmt = "yyyy-MM-dd hh:mm:ss") {
|
||||
if (!date) return "";
|
||||
const dateObj = new Date(date);
|
||||
if (isNaN(dateObj.getTime())) return "";
|
||||
tool.dateFormat = function (date, fmt = 'yyyy-MM-dd hh:mm:ss') {
|
||||
if (!date) return ''
|
||||
const dateObj = new Date(date)
|
||||
if (isNaN(dateObj.getTime())) return ''
|
||||
|
||||
const o = {
|
||||
"M+": dateObj.getMonth() + 1, // 月份
|
||||
"d+": dateObj.getDate(), // 日
|
||||
"h+": dateObj.getHours(), // 小时
|
||||
"m+": dateObj.getMinutes(), // 分
|
||||
"s+": dateObj.getSeconds(), // 秒
|
||||
"q+": Math.floor((dateObj.getMonth() + 3) / 3), // 季度
|
||||
'M+': dateObj.getMonth() + 1, // 月份
|
||||
'd+': dateObj.getDate(), // 日
|
||||
'h+': dateObj.getHours(), // 小时
|
||||
'm+': dateObj.getMinutes(), // 分
|
||||
's+': dateObj.getSeconds(), // 秒
|
||||
'q+': Math.floor((dateObj.getMonth() + 3) / 3), // 季度
|
||||
S: dateObj.getMilliseconds(), // 毫秒
|
||||
};
|
||||
}
|
||||
if (/(y+)/.test(fmt)) {
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
(dateObj.getFullYear() + "").substr(4 - RegExp.$1.length),
|
||||
);
|
||||
fmt = fmt.replace(RegExp.$1, (dateObj.getFullYear() + '').substr(4 - RegExp.$1.length))
|
||||
}
|
||||
for (const k in o) {
|
||||
if (new RegExp("(" + k + ")").test(fmt)) {
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
RegExp.$1.length == 1
|
||||
? o[k]
|
||||
: ("00" + o[k]).substr(("" + o[k]).length),
|
||||
);
|
||||
if (new RegExp('(' + k + ')').test(fmt)) {
|
||||
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
};
|
||||
return fmt
|
||||
}
|
||||
|
||||
/* 千分符 */
|
||||
/**
|
||||
@@ -354,63 +328,51 @@ tool.dateFormat = function (date, fmt = "yyyy-MM-dd hh:mm:ss") {
|
||||
* @returns {string} - 格式化后的字符串
|
||||
*/
|
||||
tool.groupSeparator = function (num, decimals = 0) {
|
||||
if (num === null || num === undefined || num === "") return "";
|
||||
const numStr = Number(num).toFixed(decimals);
|
||||
const parts = numStr.split(".");
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return parts.join(".");
|
||||
};
|
||||
if (num === null || num === undefined || num === '') return ''
|
||||
const numStr = Number(num).toFixed(decimals)
|
||||
const parts = numStr.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return parts.join('.')
|
||||
}
|
||||
|
||||
/* 常用加解密 */
|
||||
tool.crypto = {
|
||||
//MD5加密
|
||||
MD5(data) {
|
||||
return CryptoJS.MD5(data).toString();
|
||||
return CryptoJS.MD5(data).toString()
|
||||
},
|
||||
//BASE64加解密
|
||||
BASE64: {
|
||||
encrypt(data) {
|
||||
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data));
|
||||
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data))
|
||||
},
|
||||
decrypt(cipher) {
|
||||
return CryptoJS.enc.Base64.parse(cipher).toString(
|
||||
CryptoJS.enc.Utf8,
|
||||
);
|
||||
return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8)
|
||||
},
|
||||
},
|
||||
//AES加解密
|
||||
AES: {
|
||||
encrypt(data, secretKey, config = {}) {
|
||||
if (secretKey.length % 8 != 0) {
|
||||
console.warn(
|
||||
"[SCUI error]: 秘钥长度需为8的倍数,否则解密将会失败。",
|
||||
);
|
||||
console.warn('[SCUI error]: 秘钥长度需为8的倍数,否则解密将会失败。')
|
||||
}
|
||||
const result = CryptoJS.AES.encrypt(
|
||||
data,
|
||||
CryptoJS.enc.Utf8.parse(secretKey),
|
||||
{
|
||||
iv: CryptoJS.enc.Utf8.parse(config.iv || ""),
|
||||
mode: CryptoJS.mode[config.mode || "ECB"],
|
||||
padding: CryptoJS.pad[config.padding || "Pkcs7"],
|
||||
},
|
||||
);
|
||||
return result.toString();
|
||||
const result = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(secretKey), {
|
||||
iv: CryptoJS.enc.Utf8.parse(config.iv || ''),
|
||||
mode: CryptoJS.mode[config.mode || 'ECB'],
|
||||
padding: CryptoJS.pad[config.padding || 'Pkcs7'],
|
||||
})
|
||||
return result.toString()
|
||||
},
|
||||
decrypt(cipher, secretKey, config = {}) {
|
||||
const result = CryptoJS.AES.decrypt(
|
||||
cipher,
|
||||
CryptoJS.enc.Utf8.parse(secretKey),
|
||||
{
|
||||
iv: CryptoJS.enc.Utf8.parse(config.iv || ""),
|
||||
mode: CryptoJS.mode[config.mode || "ECB"],
|
||||
padding: CryptoJS.pad[config.padding || "Pkcs7"],
|
||||
},
|
||||
);
|
||||
return CryptoJS.enc.Utf8.stringify(result);
|
||||
const result = CryptoJS.AES.decrypt(cipher, CryptoJS.enc.Utf8.parse(secretKey), {
|
||||
iv: CryptoJS.enc.Utf8.parse(config.iv || ''),
|
||||
mode: CryptoJS.mode[config.mode || 'ECB'],
|
||||
padding: CryptoJS.pad[config.padding || 'Pkcs7'],
|
||||
})
|
||||
return CryptoJS.enc.Utf8.stringify(result)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/* 树形数据转扁平数组 */
|
||||
/**
|
||||
@@ -419,22 +381,22 @@ tool.crypto = {
|
||||
* @param {Object} config - 配置项 { children: "children" }
|
||||
* @returns {Array} - 扁平化后的数组
|
||||
*/
|
||||
tool.treeToList = function (tree, config = { children: "children" }) {
|
||||
const result = [];
|
||||
tool.treeToList = function (tree, config = { children: 'children' }) {
|
||||
const result = []
|
||||
tree.forEach((item) => {
|
||||
const tmp = { ...item };
|
||||
const childrenKey = config.children || "children";
|
||||
const tmp = { ...item }
|
||||
const childrenKey = config.children || 'children'
|
||||
|
||||
if (tmp[childrenKey] && tmp[childrenKey].length > 0) {
|
||||
result.push({ ...item });
|
||||
const childrenRoutes = tool.treeToList(tmp[childrenKey], config);
|
||||
result.push(...childrenRoutes);
|
||||
result.push({ ...item })
|
||||
const childrenRoutes = tool.treeToList(tmp[childrenKey], config)
|
||||
result.push(...childrenRoutes)
|
||||
} else {
|
||||
result.push(tmp);
|
||||
result.push(tmp)
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/* 获取父节点数据(保留原有函数名) */
|
||||
/**
|
||||
@@ -444,28 +406,24 @@ tool.treeToList = function (tree, config = { children: "children" }) {
|
||||
* @param {Object} config - 配置项 { pid: "parent_id", idField: "id", field: [] }
|
||||
* @returns {*} - 父节点数据或指定字段
|
||||
*/
|
||||
tool.get_parents = function (
|
||||
list,
|
||||
targetId = 0,
|
||||
config = { pid: "parent_id", idField: "id", field: [] },
|
||||
) {
|
||||
let res = null;
|
||||
tool.get_parents = function (list, targetId = 0, config = { pid: 'parent_id', idField: 'id', field: [] }) {
|
||||
let res = null
|
||||
list.forEach((item) => {
|
||||
if (item[config.idField || "id"] === targetId) {
|
||||
if (item[config.idField || 'id'] === targetId) {
|
||||
if (config.field && config.field.length > 1) {
|
||||
res = {};
|
||||
res = {}
|
||||
config.field.forEach((field) => {
|
||||
res[field] = item[field];
|
||||
});
|
||||
res[field] = item[field]
|
||||
})
|
||||
} else if (config.field && config.field.length === 1) {
|
||||
res = item[config.field[0]];
|
||||
res = item[config.field[0]]
|
||||
} else {
|
||||
res = item;
|
||||
res = item
|
||||
}
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/* 获取数据字段 */
|
||||
/**
|
||||
@@ -475,25 +433,25 @@ tool.get_parents = function (
|
||||
* @returns {*} - 提取的字段数据
|
||||
*/
|
||||
tool.getDataField = function (data, fields = []) {
|
||||
if (!data || typeof data !== "object") {
|
||||
return data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data
|
||||
}
|
||||
if (fields.length === 0) {
|
||||
return data;
|
||||
return data
|
||||
}
|
||||
if (fields.length === 1) {
|
||||
return data[fields[0]];
|
||||
return data[fields[0]]
|
||||
} else {
|
||||
const result = {};
|
||||
const result = {}
|
||||
fields.forEach((field) => {
|
||||
result[field] = data[field];
|
||||
});
|
||||
return result;
|
||||
result[field] = data[field]
|
||||
})
|
||||
return result
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 兼容旧函数名
|
||||
tool.tree_to_list = tool.treeToList;
|
||||
tool.get_data_field = tool.getDataField;
|
||||
tool.tree_to_list = tool.treeToList
|
||||
tool.get_data_field = tool.getDataField
|
||||
|
||||
export default tool;
|
||||
export default tool
|
||||
|
||||
Reference in New Issue
Block a user