This commit is contained in:
2026-01-26 09:44:48 +08:00
parent 42be40ee9f
commit 01e87acfd1
28 changed files with 1016 additions and 1050 deletions

View File

@@ -1,5 +1,12 @@
<template>
<el-config-provider :size="size" :z-index="zIndex">
<router-view />
</el-config-provider>
</template>
<script setup> <script setup>
import { ref } from 'vue'
const size = ref('default')
const zIndex = ref(3000)
</script> </script>
<template><router-view /></template>

View File

@@ -21,97 +21,97 @@ export default {
}, },
users: { users: {
list: { list: {
name: "获得用户列表", name: '获得用户列表',
get: async function (params) { get: async function (params) {
return await request.get('auth/users/index', { params }); return await request.get('auth/users/index', { params })
}, },
}, },
add: { add: {
name: "添加用户", name: '添加用户',
post: async function (params) { post: async function (params) {
return await request.post('auth/users/add', params); return await request.post('auth/users/add', params)
}, },
}, },
edit: { edit: {
name: "编辑用户", name: '编辑用户',
post: async function (params) { post: async function (params) {
return await request.put('auth/users/edit', params); return await request.put('auth/users/edit', params)
}, },
}, },
uppasswd: { uppasswd: {
name: "修改密码", name: '修改密码',
post: async function (params) { post: async function (params) {
return await request.put('auth/users/passwd', params); return await request.put('auth/users/passwd', params)
}, },
}, },
uprole: { uprole: {
name: "设置角色", name: '设置角色',
post: async function (params) { post: async function (params) {
return await request.put('auth/users/uprole', params); return await request.put('auth/users/uprole', params)
}, },
}, },
delete: { delete: {
name: "删除用户", name: '删除用户',
post: async function (params) { post: async function (params) {
return await request.delete('auth/users/delete', params); return await request.delete('auth/users/delete', params)
}, },
}, },
}, },
role: { role: {
list: { list: {
name: "获得角色列表", name: '获得角色列表',
get: async function (params) { get: async function (params) {
return await request.get('auth/role/index', { params }); return await request.get('auth/role/index', { params })
}, },
}, },
add: { add: {
name: "添加角色", name: '添加角色',
post: async function (params) { post: async function (params) {
return await request.post('auth/role/add', params); return await request.post('auth/role/add', params)
}, },
}, },
edit: { edit: {
name: "编辑角色", name: '编辑角色',
post: async function (params) { post: async function (params) {
return await request.put('auth/role/edit', params); return await request.put('auth/role/edit', params)
}, },
}, },
auth: { auth: {
name: "角色授权", name: '角色授权',
post: async function (params) { post: async function (params) {
return await request.put('auth/role/auth', params); return await request.put('auth/role/auth', params)
}, },
}, },
delete: { delete: {
name: "删除角色", name: '删除角色',
post: async function (params) { post: async function (params) {
return await request.delete('auth/role/delete', params); return await request.delete('auth/role/delete', params)
}, },
}, },
}, },
department: { department: {
list: { list: {
name: "获得部门列表", name: '获得部门列表',
get: async function (params) { get: async function (params) {
return await request.get('auth/department/index', { params }); return await request.get('auth/department/index', { params })
}, },
}, },
add: { add: {
name: "添加部门", name: '添加部门',
post: async function (params) { post: async function (params) {
return await request.post('auth/department/add', params); return await request.post('auth/department/add', params)
}, },
}, },
edit: { edit: {
name: "编辑部门", name: '编辑部门',
post: async function (params) { post: async function (params) {
return await request.put('auth/department/edit', params); return await request.put('auth/department/edit', params)
}, },
}, },
delete: { delete: {
name: "删除部门", name: '删除部门',
post: async function (params) { 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: { list: {
name: "获取菜单", name: '获取菜单',
get: async function (params) { get: async function (params) {
return await request.get('auth/menu/index', { params }); return await request.get('auth/menu/index', { params })
}, },
}, },
add: { add: {
name: "添加菜单", name: '添加菜单',
post: async function (params) { post: async function (params) {
return await request.post('auth/menu/add', params); return await request.post('auth/menu/add', params)
}, },
}, },
edit: { edit: {
name: "编辑菜单", name: '编辑菜单',
post: async function (params) { post: async function (params) {
return await request.put('auth/menu/edit', params); return await request.put('auth/menu/edit', params)
}, },
}, },
delete: { delete: {
name: "删除菜单", name: '删除菜单',
post: async function (params) { post: async function (params) {
return await request.delete('auth/menu/delete', params); return await request.delete('auth/menu/delete', params)
}, },
}, },
}, },

View File

@@ -2,333 +2,333 @@
// Warm color palette with tech-inspired design // Warm color palette with tech-inspired design
:root { :root {
--auth-primary: #ff6b35; --auth-primary: #ff6b35;
--auth-primary-light: #ff8c5a; --auth-primary-light: #ff8c5a;
--auth-primary-dark: #e55a2b; --auth-primary-dark: #e55a2b;
--auth-secondary: #ffb347; --auth-secondary: #ffb347;
--accent-orange: #ffa500; --accent-orange: #ffa500;
--accent-coral: #ff7f50; --accent-coral: #ff7f50;
--accent-amber: #ffc107; --accent-amber: #ffc107;
--bg-gradient-start: #fff5f0; --bg-gradient-start: #fff5f0;
--bg-gradient-end: #ffe8dc; --bg-gradient-end: #ffe8dc;
--card-bg: rgba(255, 255, 255, 0.95); --card-bg: rgba(255, 255, 255, 0.95);
--text-primary: #2d1810; --text-primary: #2d1810;
--text-secondary: #6b4423; --text-secondary: #6b4423;
--text-muted: #a67c52; --text-muted: #a67c52;
--border-color: #ffd4b8; --border-color: #ffd4b8;
--shadow-color: rgba(255, 107, 53, 0.15); --shadow-color: rgba(255, 107, 53, 0.15);
--success: #28a745; --success: #28a745;
--warning: #ffc107; --warning: #ffc107;
--error: #dc3545; --error: #dc3545;
--tech-blue: #007bff; --tech-blue: #007bff;
--tech-purple: #6f42c1; --tech-purple: #6f42c1;
} }
.auth-container { .auth-container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
// Tech pattern background // Tech pattern background
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-image: background-image:
radial-gradient(circle at 20% 50%, rgba(255, 107, 53, 0.03) 0%, transparent 50%), 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%);
radial-gradient(circle at 80% 20%, rgba(255, 179, 71, 0.05) 0%, transparent 40%), pointer-events: none;
radial-gradient(circle at 40% 80%, rgba(255, 127, 80, 0.04) 0%, transparent 40%); }
pointer-events: none;
}
// Animated tech elements // Animated tech elements
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 600px; width: 600px;
height: 600px; height: 600px;
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%); background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
top: -200px; top: -200px;
right: -200px; right: -200px;
animation: float 20s ease-in-out infinite; animation: float 20s ease-in-out infinite;
pointer-events: none; pointer-events: none;
} }
} }
@keyframes float { @keyframes float {
0%, 100% { 0%,
transform: translate(0, 0); 100% {
} transform: translate(0, 0);
50% { }
transform: translate(-50px, 50px); 50% {
} transform: translate(-50px, 50px);
}
} }
.auth-card { .auth-card {
width: 100%; width: 100%;
max-width: 440px; max-width: 440px;
background: var(--card-bg); background: var(--card-bg);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
border-radius: 24px; border-radius: 24px;
padding: 48px 40px; padding: 48px 40px;
box-shadow: box-shadow:
0 20px 60px var(--shadow-color), 0 20px 60px var(--shadow-color),
0 8px 24px rgba(0, 0, 0, 0.08); 0 8px 24px rgba(0, 0, 0, 0.08);
position: relative; position: relative;
z-index: 1; z-index: 1;
margin: 20px; margin: 20px;
// Tech accent line // Tech accent line
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 80px; width: 80px;
height: 4px; height: 4px;
background: linear-gradient(90deg, var(--auth-primary), var(--auth-secondary)); background: linear-gradient(90deg, var(--auth-primary), var(--auth-secondary));
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }
} }
.auth-header { .auth-header {
text-align: center; text-align: center;
margin-bottom: 40px; margin-bottom: 40px;
.auth-title { .auth-title {
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 8px; margin-bottom: 8px;
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary)); background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.auth-subtitle { .auth-subtitle {
font-size: 14px; font-size: 14px;
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.6; line-height: 1.6;
} }
} }
.auth-form { .auth-form {
.el-form-item { .el-form-item {
margin-bottom: 24px; margin-bottom: 24px;
} }
.el-input { .el-input {
--el-input-border-radius: 12px; --el-input-border-radius: 12px;
--el-input-border-color: var(--border-color); --el-input-border-color: var(--border-color);
--el-input-hover-border-color: var(--auth-primary-light); --el-input-hover-border-color: var(--auth-primary-light);
--el-input-focus-border-color: var(--auth-primary); --el-input-focus-border-color: var(--auth-primary);
.el-input__wrapper { .el-input__wrapper {
padding: 12px 16px; padding: 12px 16px;
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.08); box-shadow: 0 2px 8px rgba(255, 107, 53, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
&.is-focus { &.is-focus {
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15); box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
} }
} }
.el-input__inner { .el-input__inner {
font-size: 14px; font-size: 14px;
color: var(--text-primary); color: var(--text-primary);
} }
} }
.el-input__prefix { .el-input__prefix {
color: var(--auth-primary); color: var(--auth-primary);
font-size: 18px; font-size: 18px;
} }
.el-input__suffix { .el-input__suffix {
color: var(--text-muted); color: var(--text-muted);
} }
.el-button { .el-button {
--el-button-border-radius: 12px; --el-button-border-radius: 12px;
height: 48px; height: 48px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
&.el-button--primary { &.el-button--primary {
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark)); background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
border: none; border: none;
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35); box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
transition: all 0.3s ease; transition: all 0.3s ease;
&:hover { &:hover {
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary)); background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45); box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
} }
&:active { &:active {
transform: translateY(0); transform: translateY(0);
} }
} }
} }
} }
.auth-links { .auth-links {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 24px; margin-bottom: 24px;
.remember-me { .remember-me {
.el-checkbox__label { .el-checkbox__label {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
} }
} }
.forgot-password { .forgot-password {
color: var(--auth-primary); color: var(--auth-primary);
font-size: 14px; font-size: 14px;
text-decoration: none; text-decoration: none;
transition: color 0.3s ease; transition: color 0.3s ease;
&:hover { &:hover {
color: var(--auth-primary-dark); color: var(--auth-primary-dark);
} }
} }
} }
.auth-divider { .auth-divider {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 32px 0; margin: 32px 0;
color: var(--text-muted); color: var(--text-muted);
font-size: 13px; font-size: 13px;
&::before, &::before,
&::after { &::after {
content: ''; content: '';
flex: 1; flex: 1;
height: 1px; height: 1px;
background: var(--border-color); background: var(--border-color);
} }
span { span {
padding: 0 16px; padding: 0 16px;
} }
} }
.auth-footer { .auth-footer {
text-align: center; text-align: center;
margin-top: 24px; margin-top: 24px;
.auth-footer-text { .auth-footer-text {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
.auth-link { .auth-link {
color: var(--auth-primary); color: var(--auth-primary);
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
margin-left: 4px; margin-left: 4px;
transition: color 0.3s ease; transition: color 0.3s ease;
&:hover { &:hover {
color: var(--auth-primary-dark); color: var(--auth-primary-dark);
} }
} }
} }
} }
.tech-decoration { .tech-decoration {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
.tech-circle { .tech-circle {
position: absolute; position: absolute;
border: 2px solid rgba(255, 107, 53, 0.1); border: 2px solid rgba(255, 107, 53, 0.1);
border-radius: 50%; border-radius: 50%;
animation: pulse 4s ease-in-out infinite; animation: pulse 4s ease-in-out infinite;
} }
.tech-circle:nth-child(1) { .tech-circle:nth-child(1) {
width: 300px; width: 300px;
height: 300px; height: 300px;
top: -150px; top: -150px;
left: -150px; left: -150px;
animation-delay: 0s; animation-delay: 0s;
} }
.tech-circle:nth-child(2) { .tech-circle:nth-child(2) {
width: 200px; width: 200px;
height: 200px; height: 200px;
bottom: -100px; bottom: -100px;
right: -100px; right: -100px;
animation-delay: 1s; animation-delay: 1s;
} }
.tech-circle:nth-child(3) { .tech-circle:nth-child(3) {
width: 150px; width: 150px;
height: 150px; height: 150px;
bottom: 20%; bottom: 20%;
left: -75px; left: -75px;
animation-delay: 2s; animation-delay: 2s;
} }
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
opacity: 0.3; 100% {
transform: scale(1); opacity: 0.3;
} transform: scale(1);
50% { }
opacity: 0.6; 50% {
transform: scale(1.05); opacity: 0.6;
} transform: scale(1.05);
}
} }
// Responsive design // Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.auth-card { .auth-card {
padding: 40px 24px; padding: 40px 24px;
margin: 16px; margin: 16px;
} }
.auth-header { .auth-header {
.auth-title { .auth-title {
font-size: 24px; font-size: 24px;
} }
} }
} }
// Element Plus customizations for auth pages // Element Plus customizations for auth pages
.el-form-item__error { .el-form-item__error {
color: var(--error); color: var(--error);
font-size: 12px; font-size: 12px;
} }
.el-message { .el-message {
--el-message-bg: rgba(255, 255, 255, 0.98); --el-message-bg: rgba(255, 255, 255, 0.98);
--el-message-border-color: var(--border-color); --el-message-border-color: var(--border-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
} }

View File

@@ -3,7 +3,6 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
export default { export default {
install(app) { install(app) {
for (let icon in AIcons) { for (let icon in AIcons) {
app.component(`A${icon}`, AIcons[icon]) app.component(`A${icon}`, AIcons[icon])
} }
@@ -11,5 +10,5 @@ export default {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(`${key}`, component) app.component(`${key}`, component)
} }
} },
} }

View File

@@ -1,144 +1,144 @@
export default class UploadAdapter { export default class UploadAdapter {
constructor(loader, options) { constructor(loader, options) {
this.loader = loader; this.loader = loader
this.options = options; this.options = options
this.timeout = 60000; // 60秒超时 this.timeout = 60000 // 60秒超时
} }
upload() { upload() {
return this.loader.file.then( return this.loader.file.then(
(file) => (file) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this._initRequest(); this._initRequest()
this._initListeners(resolve, reject, file); this._initListeners(resolve, reject, file)
this._sendRequest(file); this._sendRequest(file)
this._initTimeout(reject); this._initTimeout(reject)
}), }),
); )
} }
abort() { abort() {
if (this.xhr) { if (this.xhr) {
this.xhr.abort(); this.xhr.abort()
} }
if (this.timeoutId) { if (this.timeoutId) {
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId)
} }
} }
_initRequest() { _initRequest() {
const xhr = (this.xhr = new XMLHttpRequest()); const xhr = (this.xhr = new XMLHttpRequest())
xhr.open("POST", this.options.upload.uploadUrl, true); xhr.open('POST', this.options.upload.uploadUrl, true)
xhr.responseType = "json"; xhr.responseType = 'json'
} }
_initListeners(resolve, reject, file) { _initListeners(resolve, reject, file) {
const xhr = this.xhr; const xhr = this.xhr
const loader = this.loader; const loader = this.loader
const genericErrorText = `Couldn't upload file: ${file.name}.`; const genericErrorText = `Couldn't upload file: ${file.name}.`
xhr.addEventListener("error", () => { xhr.addEventListener('error', () => {
console.error("[UploadAdapter] Upload error for file:", file.name); console.error('[UploadAdapter] Upload error for file:', file.name)
reject(genericErrorText); reject(genericErrorText)
}); })
xhr.addEventListener("abort", () => { xhr.addEventListener('abort', () => {
console.warn("[UploadAdapter] Upload aborted for file:", file.name); console.warn('[UploadAdapter] Upload aborted for file:', file.name)
reject(); reject()
}); })
xhr.addEventListener("timeout", () => { xhr.addEventListener('timeout', () => {
console.error("[UploadAdapter] Upload timeout for file:", file.name); console.error('[UploadAdapter] Upload timeout for file:', file.name)
reject(`Upload timeout: ${file.name}. Please try again.`); reject(`Upload timeout: ${file.name}. Please try again.`)
}); })
xhr.addEventListener("load", () => { xhr.addEventListener('load', () => {
const response = xhr.response; const response = xhr.response
// 检查响应状态码 // 检查响应状态码
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
if (!response) { if (!response) {
console.error("[UploadAdapter] Empty response for file:", file.name); console.error('[UploadAdapter] Empty response for file:', file.name)
reject(genericErrorText); reject(genericErrorText)
return; return
} }
// 检查业务状态码(假设 code=1 表示成功) // 检查业务状态码(假设 code=1 表示成功)
if (response.code == 1 || response.code == undefined) { 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) { if (!url) {
console.error("[UploadAdapter] No URL in response for file:", file.name, response); console.error('[UploadAdapter] No URL in response for file:', file.name, response)
reject("Upload succeeded but no URL returned"); reject('Upload succeeded but no URL returned')
return; return
} }
resolve({ default: url }); resolve({ default: url })
} else { } else {
const errorMessage = response.message || genericErrorText; const errorMessage = response.message || genericErrorText
console.error("[UploadAdapter] Upload failed for file:", file.name, "Error:", errorMessage); console.error('[UploadAdapter] Upload failed for file:', file.name, 'Error:', errorMessage)
reject(errorMessage); reject(errorMessage)
} }
} else { } else {
console.error("[UploadAdapter] HTTP error for file:", file.name, "Status:", xhr.status); console.error('[UploadAdapter] HTTP error for file:', file.name, 'Status:', xhr.status)
reject(`Server error (${xhr.status}): ${file.name}`); reject(`Server error (${xhr.status}): ${file.name}`)
} }
}); })
// 上传进度监听 // 上传进度监听
if (xhr.upload) { if (xhr.upload) {
xhr.upload.addEventListener("progress", (evt) => { xhr.upload.addEventListener('progress', (evt) => {
if (evt.lengthComputable) { if (evt.lengthComputable) {
loader.uploadTotal = evt.total; loader.uploadTotal = evt.total
loader.uploaded = evt.loaded; loader.uploaded = evt.loaded
} }
}); })
} }
} }
_initTimeout(reject) { _initTimeout(reject) {
// 清除之前的超时定时器(如果有) // 清除之前的超时定时器(如果有)
if (this.timeoutId) { if (this.timeoutId) {
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId)
} }
// 设置新的超时定时器 // 设置新的超时定时器
this.timeoutId = setTimeout(() => { this.timeoutId = setTimeout(() => {
if (this.xhr) { if (this.xhr) {
this.xhr.abort(); this.xhr.abort()
reject(new Error("Upload timeout")); reject(new Error('Upload timeout'))
} }
}, this.timeout); }, this.timeout)
} }
_sendRequest(file) { _sendRequest(file) {
// 设置请求超时 // 设置请求超时
this.xhr.timeout = this.timeout; this.xhr.timeout = this.timeout
// Set headers if specified. // Set headers if specified.
const headers = this.options.upload.headers || {}; const headers = this.options.upload.headers || {}
const extendData = this.options.upload.extendData || {}; const extendData = this.options.upload.extendData || {}
// Use the withCredentials flag if specified. // Use the withCredentials flag if specified.
const withCredentials = this.options.upload.withCredentials || false; const withCredentials = this.options.upload.withCredentials || false
const uploadName = this.options.upload.uploadName || "file"; const uploadName = this.options.upload.uploadName || 'file'
for (const headerName of Object.keys(headers)) { 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)) { 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) { export function UploadAdapterPlugin(editor) {
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => { editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new UploadAdapter(loader, editor.config._config); return new UploadAdapter(loader, editor.config._config)
}; }
} }

View File

@@ -1,7 +1,6 @@
<template> <template>
<div :style="{ '--editor-height': editorHeight }"> <div :style="{ '--editor-height': editorHeight }">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur" <ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur" @focus="onFocus"></ckeditor>
@focus="onFocus"></ckeditor>
</div> </div>
</template> </template>
@@ -69,137 +68,119 @@ import {
Underline, Underline,
Undo, Undo,
WordCount, WordCount,
} from "ckeditor5"; } from 'ckeditor5'
import { Ckeditor } from "@ckeditor/ckeditor5-vue"; import { Ckeditor } from '@ckeditor/ckeditor5-vue'
import { UploadAdapterPlugin } from "./UploadAdapter.js"; import { UploadAdapterPlugin } from './UploadAdapter.js'
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from 'vue'
import { useCurrentInstance } from "@/utils/tool"; import { useCurrentInstance } from '@/utils/tool'
import coreTranslations from "ckeditor5/translations/zh-cn.js"; import coreTranslations from 'ckeditor5/translations/zh-cn.js'
import "ckeditor5/ckeditor5.css"; import 'ckeditor5/ckeditor5.css'
const { proxy } = useCurrentInstance(); const { proxy } = useCurrentInstance()
// 组件名称 // 组件名称
defineOptions({ defineOptions({
name: "scCkeditor" name: 'scCkeditor',
}); })
// Props 定义 // Props 定义
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
default: "", default: '',
}, },
placeholder: { placeholder: {
type: String, type: String,
default: "请输入内容……", default: '请输入内容……',
}, },
toolbar: { toolbar: {
type: String, type: String,
default: "basic", default: 'basic',
}, },
height: { height: {
type: String, type: String,
default: "400px", default: '400px',
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); })
// Emits 定义 // Emits 定义
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(['update:modelValue'])
// 工具栏配置常量 // 工具栏配置常量
const TOOLBARS = { const TOOLBARS = {
full: [ full: [
"sourceEditing", 'sourceEditing',
"undo", 'undo',
"redo", 'redo',
"heading", 'heading',
"style", 'style',
"|", '|',
"superscript", 'superscript',
"subscript", 'subscript',
"removeFormat", 'removeFormat',
"bold", 'bold',
"italic", 'italic',
"underline", 'underline',
"link", 'link',
"fontBackgroundColor", 'fontBackgroundColor',
"fontFamily", 'fontFamily',
"fontSize", 'fontSize',
"fontColor", 'fontColor',
"|", '|',
"outdent", 'outdent',
"indent", 'indent',
"alignment", 'alignment',
"bulletedList", 'bulletedList',
"numberedList", 'numberedList',
"todoList", 'todoList',
"|", '|',
"blockQuote", 'blockQuote',
"insertTable", 'insertTable',
"imageInsert", 'imageInsert',
"mediaEmbed", 'mediaEmbed',
"highlight", 'highlight',
"horizontalLine", 'horizontalLine',
"selectAll", 'selectAll',
"showBlocks", 'showBlocks',
"specialCharacters", 'specialCharacters',
"codeBlock", 'codeBlock',
"findAndReplace", 'findAndReplace',
], ],
basic: [ basic: [
"sourceEditing", 'sourceEditing',
"undo", 'undo',
"redo", 'redo',
"heading", 'heading',
"|", '|',
"removeFormat", 'removeFormat',
"bold", 'bold',
"italic", 'italic',
"underline", 'underline',
"link", 'link',
"fontBackgroundColor", 'fontBackgroundColor',
"fontFamily", 'fontFamily',
"fontSize", 'fontSize',
"fontColor", 'fontColor',
"|", '|',
"outdent", 'outdent',
"indent", 'indent',
"alignment", 'alignment',
"bulletedList", 'bulletedList',
"numberedList", 'numberedList',
"todoList", 'todoList',
"|", '|',
"insertTable", 'insertTable',
"imageInsert", 'imageInsert',
"mediaEmbed", 'mediaEmbed',
], ],
simple: [ simple: ['undo', 'redo', 'heading', '|', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor', '|', 'insertTable', 'imageInsert', 'mediaEmbed'],
"undo", }
"redo",
"heading",
"|",
"removeFormat",
"bold",
"italic",
"underline",
"link",
"fontBackgroundColor",
"fontFamily",
"fontSize",
"fontColor",
"|",
"insertTable",
"imageInsert",
"mediaEmbed",
],
};
// 插件配置常量 // 插件配置常量
const PLUGINS = [ const PLUGINS = [
@@ -265,16 +246,16 @@ const PLUGINS = [
Undo, Undo,
WordCount, WordCount,
UploadAdapterPlugin, UploadAdapterPlugin,
]; ]
// 响应式数据 // 响应式数据
const editorData = ref(""); const editorData = ref('')
const editorHeight = ref(props.height); const editorHeight = ref(props.height)
const editor = ClassicEditor; const editor = ClassicEditor
// 编辑器配置 // 编辑器配置
const editorConfig = computed(() => ({ const editorConfig = computed(() => ({
language: { ui: "zh-cn", content: "zh-cn" }, language: { ui: 'zh-cn', content: 'zh-cn' },
translations: [coreTranslations], translations: [coreTranslations],
plugins: PLUGINS, plugins: PLUGINS,
toolbar: { toolbar: {
@@ -283,27 +264,18 @@ const editorConfig = computed(() => ({
}, },
placeholder: props.placeholder, placeholder: props.placeholder,
image: { image: {
styles: ["alignLeft", "alignCenter", "alignRight"], styles: ['alignLeft', 'alignCenter', 'alignRight'],
toolbar: [ toolbar: ['imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight', '|', 'linkImage'],
"imageTextAlternative",
"toggleImageCaption",
"|",
"imageStyle:alignLeft",
"imageStyle:alignCenter",
"imageStyle:alignRight",
"|",
"linkImage",
],
}, },
mediaEmbed: { mediaEmbed: {
previewsInData: true, previewsInData: true,
providers: [ providers: [
{ {
name: "mp4", name: 'mp4',
url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i, url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i,
html: match => { html: (match) => {
const url = match["input"]; const url = match['input']
return ('<video controls width="100%" height="100%" src="' + url + '"></video>') return '<video controls width="100%" height="100%" src="' + url + '"></video>'
}, },
}, },
], ],
@@ -314,59 +286,57 @@ const editorConfig = computed(() => ({
style: { style: {
definitions: [ definitions: [
{ {
name: "Article category", name: 'Article category',
element: "h3", element: 'h3',
classes: ["category"], classes: ['category'],
}, },
{ {
name: "Info box", name: 'Info box',
element: "p", element: 'p',
classes: ["info-box"], classes: ['info-box'],
}, },
], ],
}, },
upload: { upload: {
uploadUrl: proxy?.$API?.common?.upload?.url || "", uploadUrl: proxy?.$API?.common?.upload?.url || '',
withCredentials: false, withCredentials: false,
extendData: { type: "images" }, extendData: { type: 'images' },
headers: { headers: {
Authorization: "Bearer " + proxy?.$TOOL?.data?.get("TOKEN"), Authorization: 'Bearer ' + proxy?.$TOOL?.data?.get('TOKEN'),
}, },
}, },
})); }))
// 监听 modelValue 变化 // 监听 modelValue 变化
watch( watch(
() => props.modelValue, () => props.modelValue,
(newVal) => { (newVal) => {
editorData.value = newVal ?? ""; editorData.value = newVal ?? ''
}, },
{ immediate: true } { immediate: true },
); )
// 监听 height 变化 // 监听 height 变化
watch( watch(
() => props.height, () => props.height,
(newVal) => { (newVal) => {
editorHeight.value = newVal; editorHeight.value = newVal
} },
); )
// 移除图片宽高的正则替换函数 // 移除图片宽高的正则替换函数
const stripImageDimensions = (html) => { const stripImageDimensions = (html) => {
return html.replace(/<img[^>]*>/gi, (match) => { return html.replace(/<img[^>]*>/gi, (match) => {
return match return match.replace(/width="[^"]*"/gi, '').replace(/height="[^"]*"/gi, '')
.replace(/width="[^"]*"/gi, "") })
.replace(/height="[^"]*"/gi, ""); }
});
};
// 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题 // 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题
const onBlur = () => { const onBlur = () => {
const cleanedData = stripImageDimensions(editorData.value); const cleanedData = stripImageDimensions(editorData.value)
editorData.value = cleanedData; editorData.value = cleanedData
emit("update:modelValue", cleanedData); emit('update:modelValue', cleanedData)
}; }
</script> </script>
<style> <style>

View File

@@ -35,7 +35,7 @@ export default {
//默认分栏数量和宽度 例如 [24] [18,6] [8,8,8] [6,12,6] //默认分栏数量和宽度 例如 [24] [18,6] [8,8,8] [6,12,6]
layout: [24, 12, 12], layout: [24, 12, 12],
//小组件分布com取值:pages/home/components 文件名 //小组件分布com取值:pages/home/components 文件名
compsList: [["welcome"], ["info"], ["ver"]], compsList: [['welcome'], ['info'], ['ver']],
}, },
//是否加密localStorage, 为空不加密 //是否加密localStorage, 为空不加密

View File

@@ -1,20 +1,20 @@
import systemApi from "@/api/system"; import systemApi from '@/api/system'
//上传配置 //上传配置
export default { export default {
apiObj: systemApi.upload.post, //上传请求API对象 apiObj: systemApi.upload.post, //上传请求API对象
filename: "file", //form请求时文件的key filename: 'file', //form请求时文件的key
successCode: 1, //请求完成代码 successCode: 1, //请求完成代码
maxSize: 10, //最大文件大小 默认10MB maxSize: 10, //最大文件大小 默认10MB
parseData: function (res) { parseData: function (res) {
return { return {
code: res.code, //分析状态字段结构 code: res.code, //分析状态字段结构
fileName: res.data.name,//分析文件名称 fileName: res.data.name, //分析文件名称
src: res.data.url, //分析图片远程地址结构 src: res.data.url, //分析图片远程地址结构
msg: res.message //分析描述字段结构 msg: res.message, //分析描述字段结构
} }
}, },
apiObjFile: systemApi.upload.post, //附件上传请求API对象 apiObjFile: systemApi.upload.post, //附件上传请求API对象
maxSizeFile: 10 //最大文件大小 默认10MB maxSizeFile: 10, //最大文件大小 默认10MB
} }

View File

@@ -11,6 +11,6 @@ export function useI18n() {
availableLocales, availableLocales,
setLocale: i18nStore.setLocale, setLocale: i18nStore.setLocale,
currentLocale: i18nStore.currentLocale, currentLocale: i18nStore.currentLocale,
localeLabel: i18nStore.localeLabel localeLabel: i18nStore.localeLabel,
} }
} }

View File

@@ -8,8 +8,8 @@ const i18n = createI18n({
fallbackLocale: 'en-US', fallbackLocale: 'en-US',
messages: { messages: {
'zh-CN': zh, 'zh-CN': zh,
'en-US': en 'en-US': en,
} },
}) })
export default i18n export default i18n

View File

@@ -157,7 +157,7 @@ export default {
selectAll: 'Select All', selectAll: 'Select All',
unselectAll: 'Unselect All', unselectAll: 'Unselect All',
retry: 'Retry', retry: 'Retry',
fetchDataFailed: 'Failed to fetch data' fetchDataFailed: 'Failed to fetch data',
}, },
menu: { menu: {
dashboard: 'Dashboard', dashboard: 'Dashboard',
@@ -165,7 +165,7 @@ export default {
roleManagement: 'Role Management', roleManagement: 'Role Management',
permissionManagement: 'Permission Management', permissionManagement: 'Permission Management',
systemSettings: 'System Settings', systemSettings: 'System Settings',
logManagement: 'Log Management' logManagement: 'Log Management',
}, },
login: { login: {
title: 'User Login', title: 'User Login',
@@ -178,7 +178,7 @@ export default {
noAccount: "Don't have an account?", noAccount: "Don't have an account?",
registerNow: 'Register Now', registerNow: 'Register Now',
forgotPassword: 'Forgot Password?', forgotPassword: 'Forgot Password?',
rememberMe: 'Remember Me' rememberMe: 'Remember Me',
}, },
register: { register: {
title: 'User Registration', title: 'User Registration',
@@ -197,7 +197,7 @@ export default {
agreeTerms: 'I have read and agree to the', agreeTerms: 'I have read and agree to the',
terms: 'User Agreement', terms: 'User Agreement',
hasAccount: 'Already have an account?', hasAccount: 'Already have an account?',
loginNow: 'Login Now' loginNow: 'Login Now',
}, },
resetPassword: { resetPassword: {
title: 'Reset Password', title: 'Reset Password',
@@ -216,13 +216,13 @@ export default {
codeSent: 'Verification code has been sent to your email', codeSent: 'Verification code has been sent to your email',
resendCode: 'Resend in {seconds} seconds', resendCode: 'Resend in {seconds} seconds',
sendCodeFirst: 'Please enter email address first', sendCodeFirst: 'Please enter email address first',
backToLogin: 'Back to Login' backToLogin: 'Back to Login',
}, },
layout: { layout: {
toggleSidebar: 'Toggle Sidebar', toggleSidebar: 'Toggle Sidebar',
collapse: 'Collapse', collapse: 'Collapse',
expand: 'Expand', expand: 'Expand',
logout: 'Logout' logout: 'Logout',
}, },
table: { table: {
total: 'Total {total} items', total: 'Total {total} items',
@@ -230,13 +230,13 @@ export default {
actions: 'Actions', actions: 'Actions',
noData: 'No Data', noData: 'No Data',
sort: 'Sort', sort: 'Sort',
filter: 'Filter' filter: 'Filter',
}, },
pagination: { pagination: {
goTo: 'Go to', goTo: 'Go to',
page: 'Page', page: 'Page',
total: 'Total {total} items', total: 'Total {total} items',
itemsPerPage: '{size} items per page' itemsPerPage: '{size} items per page',
}, },
form: { form: {
required: 'This field is required', required: 'This field is required',
@@ -244,6 +244,6 @@ export default {
invalidPhone: 'Please enter a valid phone number', invalidPhone: 'Please enter a valid phone number',
passwordMismatch: 'Passwords do not match', passwordMismatch: 'Passwords do not match',
minLength: 'Minimum {min} characters required', minLength: 'Minimum {min} characters required',
maxLength: 'Maximum {max} characters allowed' maxLength: 'Maximum {max} characters allowed',
} },
} }

View File

@@ -156,7 +156,7 @@ export default {
columnSettings: '列显示设置', columnSettings: '列显示设置',
selectAll: '全选', selectAll: '全选',
unselectAll: '取消全选', unselectAll: '取消全选',
retry: '重试' retry: '重试',
}, },
menu: { menu: {
dashboard: '仪表板', dashboard: '仪表板',
@@ -164,7 +164,7 @@ export default {
roleManagement: '角色管理', roleManagement: '角色管理',
permissionManagement: '权限管理', permissionManagement: '权限管理',
systemSettings: '系统设置', systemSettings: '系统设置',
logManagement: '日志管理' logManagement: '日志管理',
}, },
login: { login: {
title: '用户登录', title: '用户登录',
@@ -177,7 +177,7 @@ export default {
noAccount: '还没有账户?', noAccount: '还没有账户?',
registerNow: '立即注册', registerNow: '立即注册',
forgotPassword: '忘记密码?', forgotPassword: '忘记密码?',
rememberMe: '记住我' rememberMe: '记住我',
}, },
register: { register: {
title: '用户注册', title: '用户注册',
@@ -196,7 +196,7 @@ export default {
agreeTerms: '我已阅读并同意', agreeTerms: '我已阅读并同意',
terms: '用户协议', terms: '用户协议',
hasAccount: '已有账户?', hasAccount: '已有账户?',
loginNow: '立即登录' loginNow: '立即登录',
}, },
resetPassword: { resetPassword: {
title: '重置密码', title: '重置密码',
@@ -215,13 +215,13 @@ export default {
codeSent: '验证码已发送到您的邮箱', codeSent: '验证码已发送到您的邮箱',
resendCode: '{seconds}秒后重新发送', resendCode: '{seconds}秒后重新发送',
sendCodeFirst: '请先输入邮箱地址', sendCodeFirst: '请先输入邮箱地址',
backToLogin: '返回登录' backToLogin: '返回登录',
}, },
layout: { layout: {
toggleSidebar: '切换侧边栏', toggleSidebar: '切换侧边栏',
collapse: '折叠', collapse: '折叠',
expand: '展开', expand: '展开',
logout: '退出登录' logout: '退出登录',
}, },
table: { table: {
total: '共 {total} 条', total: '共 {total} 条',
@@ -229,13 +229,13 @@ export default {
actions: '操作', actions: '操作',
noData: '暂无数据', noData: '暂无数据',
sort: '排序', sort: '排序',
filter: '筛选' filter: '筛选',
}, },
pagination: { pagination: {
goTo: '前往', goTo: '前往',
page: '页', page: '页',
total: '共 {total} 条', total: '共 {total} 条',
itemsPerPage: '每页 {size} 条' itemsPerPage: '每页 {size} 条',
}, },
form: { form: {
required: '此项为必填项', required: '此项为必填项',
@@ -243,6 +243,6 @@ export default {
invalidPhone: '请输入有效的手机号', invalidPhone: '请输入有效的手机号',
passwordMismatch: '两次输入的密码不一致', passwordMismatch: '两次输入的密码不一致',
minLength: '最少需要 {min} 个字符', minLength: '最少需要 {min} 个字符',
maxLength: '最多允许 {max} 个字符' maxLength: '最多允许 {max} 个字符',
} },
} }

View File

View File

View File

View File

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

View File

@@ -1,34 +1,32 @@
<template> <template>
<div class="not-found-container"> <div class="not-found-container">
<div class="tech-decoration"> <div class="tech-decoration">
<div class="tech-circle"></div> <div class="tech-circle"></div>
<div class="tech-circle"></div> <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> </div>
<div class="action-buttons"> <div class="not-found-content">
<el-button type="primary" size="large" @click="goBack"> <div class="error-code">404</div>
<el-icon> <div class="error-title">页面未找到</div>
<ArrowLeft /> <div class="error-description">抱歉您访问的页面不存在或已被移除</div>
</el-icon>
返回上一页 <div class="action-buttons">
</el-button> <el-button type="primary" size="large" @click="goBack">
<el-button size="large" @click="goHome"> <el-icon>
<el-icon> <ArrowLeft />
<HomeFilled /> </el-icon>
</el-icon> 返回上一页
返回首页 </el-button>
</el-button> <el-button size="large" @click="goHome">
<el-icon>
<HomeFilled />
</el-icon>
返回首页
</el-button>
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
@@ -104,7 +102,6 @@ const goHome = () => {
} }
@keyframes pulse { @keyframes pulse {
0%, 0%,
100% { 100% {
opacity: 0.3; opacity: 0.3;
@@ -136,7 +133,6 @@ const goHome = () => {
} }
@keyframes float { @keyframes float {
0%, 0%,
100% { 100% {
transform: translateY(0); transform: translateY(0);

View File

@@ -1,31 +1,31 @@
<template> <template>
<div class="empty-container"> <div class="empty-container">
<div class="tech-decoration"> <div class="tech-decoration">
<div class="tech-circle"></div> <div class="tech-circle"></div>
<div class="tech-circle"></div> <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> </div>
<div class="empty-title">暂无数据</div> <div class="empty-content">
<div class="empty-description"> <div class="empty-icon">
{{ description || '当前页面暂无数据,请稍后再试' }} <el-icon :size="120" color="#ff6b35">
</div> <Box />
</el-icon>
</div>
<el-button v-if="showButton" type="primary" size="large" @click="handleAction"> <div class="empty-title">暂无数据</div>
<el-icon v-if="buttonIcon"> <div class="empty-description">
<component :is="buttonIcon" /> {{ description || '当前页面暂无数据,请稍后再试' }}
</el-icon> </div>
{{ buttonText || '刷新页面' }}
</el-button> <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>
</div>
</template> </template>
<script setup> <script setup>
@@ -35,20 +35,20 @@ import '@/assets/style/auth.scss'
defineProps({ defineProps({
description: { description: {
type: String, type: String,
default: '当前页面暂无数据,请稍后再试' default: '当前页面暂无数据,请稍后再试',
}, },
showButton: { showButton: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
buttonText: { buttonText: {
type: String, type: String,
default: '刷新页面' default: '刷新页面',
}, },
buttonIcon: { buttonIcon: {
type: [String, Object], type: [String, Object],
default: null default: null,
} },
}) })
const emit = defineEmits(['action']) const emit = defineEmits(['action'])
@@ -113,7 +113,6 @@ const handleAction = () => {
} }
@keyframes pulse { @keyframes pulse {
0%, 0%,
100% { 100% {
opacity: 0.3; opacity: 0.3;
@@ -138,7 +137,6 @@ const handleAction = () => {
} }
@keyframes float { @keyframes float {
0%, 0%,
100% { 100% {
transform: translateY(0); transform: translateY(0);

8
src/pages/home/index.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<div></div>
</template>
<script setup>
defineOptions({
name: 'HomeIndex'
})
</script>

View File

@@ -1,60 +1,56 @@
<template> <template>
<div class="auth-container"> <div class="auth-container">
<div class="tech-decoration"> <div class="tech-decoration">
<div class="tech-circle"></div> <div class="tech-circle"></div>
<div class="tech-circle"></div> <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> </div>
<el-form ref="forgotFormRef" :model="forgotForm" :rules="forgotRules" class="auth-form" <div class="auth-card">
@submit.prevent="handleSubmit"> <div class="auth-header">
<el-form-item prop="email"> <h1 class="auth-title">找回密码</h1>
<el-input v-model="forgotForm.email" placeholder="请输入注册邮箱" size="large" clearable <p class="auth-subtitle">输入您的邮箱我们将发送重置密码链接</p>
@keyup.enter="handleSubmit"> </div>
<template #prefix>
<el-icon>
<Message />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="captcha" v-if="showCaptcha"> <el-form ref="forgotFormRef" :model="forgotForm" :rules="forgotRules" class="auth-form" @submit.prevent="handleSubmit">
<div style="display: flex; gap: 12px"> <el-form-item prop="email">
<el-input v-model="forgotForm.captcha" placeholder="请输入验证码" size="large" style="flex: 1"> <el-input v-model="forgotForm.email" placeholder="请输入注册邮箱" size="large" clearable @keyup.enter="handleSubmit">
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<Key /> <Message />
</el-icon> </el-icon>
</template> </template>
</el-input> </el-input>
<el-button type="info" size="large" :disabled="captchaDisabled" @click="sendCaptcha"> </el-form-item>
{{ captchaButtonText }}
</el-button>
</div>
</el-form-item>
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleSubmit"> <el-form-item prop="captcha" v-if="showCaptcha">
{{ loading ? '提交中...' : '发送重置链接' }} <div style="display: flex; gap: 12px">
</el-button> <el-input v-model="forgotForm.captcha" placeholder="请输入验证码" size="large" style="flex: 1">
</el-form> <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"> <el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleSubmit">
<p class="auth-footer-text"> {{ loading ? '提交中...' : '发送重置链接' }}
想起密码了 </el-button>
<router-link to="/login" class="auth-link"> </el-form>
返回登录
</router-link> <div class="auth-footer">
</p> <p class="auth-footer-text">
想起密码了
<router-link to="/login" class="auth-link"> 返回登录 </router-link>
</p>
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
@@ -73,7 +69,7 @@ const countdown = ref(60)
// Forgot password form data // Forgot password form data
const forgotForm = reactive({ const forgotForm = reactive({
email: '', email: '',
captcha: '' captcha: '',
}) })
// Captcha button text // Captcha button text
@@ -83,12 +79,12 @@ const captchaButtonText = ref('获取验证码')
const forgotRules = { const forgotRules = {
email: [ email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' }, { required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' } { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
], ],
captcha: [ captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' }, { required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 6, message: '验证码为6位数字', trigger: 'blur' } { len: 6, message: '验证码为6位数字', trigger: 'blur' },
] ],
} }
// Send captcha code // Send captcha code
@@ -109,7 +105,7 @@ const sendCaptcha = async () => {
// Simulate API call - Replace with actual API call // Simulate API call - Replace with actual API call
// Example: const response = await sendCaptchaApi(forgotForm.email) // Example: const response = await sendCaptchaApi(forgotForm.email)
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
ElMessage.success('验证码已发送至您的邮箱') ElMessage.success('验证码已发送至您的邮箱')
@@ -128,7 +124,6 @@ const sendCaptcha = async () => {
}, 1000) }, 1000)
showCaptcha.value = true showCaptcha.value = true
} catch (error) { } catch (error) {
console.error('Send captcha failed:', error) console.error('Send captcha failed:', error)
ElMessage.error('发送验证码失败,请稍后重试') ElMessage.error('发送验证码失败,请稍后重试')
@@ -147,7 +142,7 @@ const handleSubmit = async () => {
// Example: const response = await forgotPasswordApi(forgotForm) // Example: const response = await forgotPasswordApi(forgotForm)
// Simulated delay // Simulated delay
await new Promise(resolve => setTimeout(resolve, 1500)) await new Promise((resolve) => setTimeout(resolve, 1500))
// Success message // Success message
ElMessage.success('密码重置链接已发送至您的邮箱,请注意查收') ElMessage.success('密码重置链接已发送至您的邮箱,请注意查收')
@@ -156,7 +151,6 @@ const handleSubmit = async () => {
setTimeout(() => { setTimeout(() => {
router.push('/login') router.push('/login')
}, 2000) }, 2000)
} catch (error) { } catch (error) {
console.error('Forgot password failed:', error) console.error('Forgot password failed:', error)
ElMessage.error('提交失败,请检查邮箱地址和验证码') ElMessage.error('提交失败,请检查邮箱地址和验证码')

View File

@@ -36,12 +36,8 @@
</el-form-item> </el-form-item>
<div class="auth-links"> <div class="auth-links">
<el-checkbox v-model="loginForm.rememberMe" class="remember-me"> <el-checkbox v-model="loginForm.rememberMe" class="remember-me"> 记住我 </el-checkbox>
记住我 <router-link to="/forgot-password" class="forgot-password"> 忘记密码 </router-link>
</el-checkbox>
<router-link to="/forgot-password" class="forgot-password">
忘记密码
</router-link>
</div> </div>
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleLogin"> <el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleLogin">
@@ -52,9 +48,7 @@
<div class="auth-footer"> <div class="auth-footer">
<p class="auth-footer-text"> <p class="auth-footer-text">
还没有账户 还没有账户
<router-link to="/register" class="auth-link"> <router-link to="/register" class="auth-link"> 立即注册 </router-link>
立即注册
</router-link>
</p> </p>
</div> </div>
</div> </div>
@@ -63,10 +57,9 @@
<script setup> <script setup>
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { defineOptions } from 'vue'
defineOptions({ defineOptions({
name: 'LoginPage' name: 'LoginPage',
}) })
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@@ -88,19 +81,19 @@ const userStore = useUserStore()
const loginForm = reactive({ const loginForm = reactive({
username: '', username: '',
password: '', password: '',
rememberMe: false rememberMe: false,
}) })
// Form validation rules // Form validation rules
const loginRules = { const loginRules = {
username: [ username: [
{ required: true, message: '请输入用户名或邮箱', trigger: 'blur' }, { required: true, message: '请输入用户名或邮箱', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' } { min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' },
], ],
password: [ password: [
{ required: true, message: '请输入密码', trigger: 'blur' }, { required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' } { min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' },
] ],
} }
// Handle login // Handle login
@@ -115,7 +108,7 @@ const handleLogin = async () => {
// 1. Call login API // 1. Call login API
const loginResponse = await auth.login.post({ const loginResponse = await auth.login.post({
username: loginForm.username, username: loginForm.username,
password: loginForm.password password: loginForm.password,
}) })
// Check if login was successful // Check if login was successful
@@ -146,38 +139,39 @@ const handleLogin = async () => {
const menuResponse = await auth.menu.my.get() const menuResponse = await auth.menu.my.get()
if (menuResponse && menuResponse.data) { if (menuResponse && menuResponse.data) {
userStore.setMenu(menuResponse.data) userStore.setMenu(menuResponse.data.menu)
userStore.setPermissions(menuResponse.data.permissions)
} }
// 5. Cache system configuration data // // 5. Cache system configuration data
try { // try {
const settingResponse = await system.setting.list.get() // const settingResponse = await system.setting.list.get()
if (settingResponse && settingResponse.data) { // if (settingResponse && settingResponse.data) {
tool.data.set('system_setting', settingResponse.data) // tool.data.set('system_setting', settingResponse.data)
} // }
} catch (error) { // } catch (error) {
console.error('Failed to cache system settings:', error) // console.error('Failed to cache system settings:', error)
} // }
// 6. Cache dictionary data // // 6. Cache dictionary data
try { // try {
const dictResponse = await system.dictionary.list.get() // const dictResponse = await system.dictionary.list.get()
if (dictResponse && dictResponse.data) { // if (dictResponse && dictResponse.data) {
tool.data.set('system_dictionary', dictResponse.data) // tool.data.set('system_dictionary', dictResponse.data)
} // }
} catch (error) { // } catch (error) {
console.error('Failed to cache dictionary data:', error) // console.error('Failed to cache dictionary data:', error)
} // }
// 7. Cache area data // // 7. Cache area data
try { // try {
const areaResponse = await system.area.list.get() // const areaResponse = await system.area.list.get()
if (areaResponse && areaResponse.data) { // if (areaResponse && areaResponse.data) {
tool.data.set('system_area', areaResponse.data) // tool.data.set('system_area', areaResponse.data)
} // }
} catch (error) { // } catch (error) {
console.error('Failed to cache area data:', error) // console.error('Failed to cache area data:', error)
} // }
// Success message // Success message
ElMessage.success('登录成功!') ElMessage.success('登录成功!')
@@ -195,7 +189,6 @@ const handleLogin = async () => {
router.push(config.DASHBOARD_URL) router.push(config.DASHBOARD_URL)
} }
}, 500) }, 500)
} catch (error) { } catch (error) {
console.error('Login failed:', error) console.error('Login failed:', error)

View File

@@ -1,85 +1,80 @@
<template> <template>
<div class="auth-container"> <div class="auth-container">
<div class="tech-decoration"> <div class="tech-decoration">
<div class="tech-circle"></div> <div class="tech-circle"></div>
<div class="tech-circle"></div> <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> </div>
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" <div class="auth-card">
@submit.prevent="handleRegister"> <div class="auth-header">
<el-form-item prop="username"> <h1 class="auth-title">创建账户</h1>
<el-input v-model="registerForm.username" placeholder="请输入用户名" size="large" clearable> <p class="auth-subtitle">加入我们开启科技之旅</p>
<template #prefix> </div>
<el-icon>
<User />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="email"> <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" @submit.prevent="handleRegister">
<el-input v-model="registerForm.email" placeholder="请输入邮箱地址" size="large" clearable> <el-form-item prop="username">
<template #prefix> <el-input v-model="registerForm.username" placeholder="请输入用户名" size="large" clearable>
<el-icon> <template #prefix>
<Message /> <el-icon>
</el-icon> <User />
</template> </el-icon>
</el-input> </template>
</el-form-item> </el-input>
</el-form-item>
<el-form-item prop="password"> <el-form-item prop="email">
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码至少6位" size="large" <el-input v-model="registerForm.email" placeholder="请输入邮箱地址" size="large" clearable>
show-password> <template #prefix>
<template #prefix> <el-icon>
<el-icon> <Message />
<Lock /> </el-icon>
</el-icon> </template>
</template> </el-input>
</el-input> </el-form-item>
</el-form-item>
<el-form-item prop="confirmPassword"> <el-form-item prop="password">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" size="large" <el-input v-model="registerForm.password" type="password" placeholder="请输入密码至少6位" size="large" show-password>
show-password @keyup.enter="handleRegister"> <template #prefix>
<template #prefix> <el-icon>
<el-icon> <Lock />
<Lock /> </el-icon>
</el-icon> </template>
</template> </el-input>
</el-input> </el-form-item>
</el-form-item>
<el-form-item prop="agreeTerms"> <el-form-item prop="confirmPassword">
<el-checkbox v-model="registerForm.agreeTerms" class="remember-me"> <el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" size="large" show-password @keyup.enter="handleRegister">
我已阅读并同意 <template #prefix>
<a href="#" class="auth-link">服务条款</a> <el-icon>
<Lock />
<a href="#" class="auth-link">隐私政策</a> </el-icon>
</el-checkbox> </template>
</el-form-item> </el-input>
</el-form-item>
<el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleRegister"> <el-form-item prop="agreeTerms">
{{ loading ? '注册中...' : '注册' }} <el-checkbox v-model="registerForm.agreeTerms" class="remember-me">
</el-button> 我已阅读并同意
</el-form> <a href="#" class="auth-link">服务条款</a>
<a href="#" class="auth-link">隐私政策</a>
</el-checkbox>
</el-form-item>
<div class="auth-footer"> <el-button type="primary" :loading="loading" size="large" style="width: 100%" @click="handleRegister">
<p class="auth-footer-text"> {{ loading ? '注册中...' : '注册' }}
已有账户 </el-button>
<router-link to="/login" class="auth-link"> </el-form>
立即登录
</router-link> <div class="auth-footer">
</p> <p class="auth-footer-text">
已有账户
<router-link to="/login" class="auth-link"> 立即登录 </router-link>
</p>
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
@@ -98,7 +93,7 @@ const registerForm = reactive({
email: '', email: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
agreeTerms: false agreeTerms: false,
}) })
// Custom password validation // Custom password validation
@@ -127,26 +122,22 @@ const validateConfirmPassword = (rule, value, callback) => {
const registerRules = { const registerRules = {
username: [ username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }, { required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' } { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
], ],
email: [ email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' }, { required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' } { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
],
password: [
{ required: true, validator: validatePassword, trigger: 'blur' }
],
confirmPassword: [
{ required: true, validator: validateConfirmPassword, trigger: 'blur' }
], ],
password: [{ required: true, validator: validatePassword, trigger: 'blur' }],
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
agreeTerms: [ agreeTerms: [
{ {
type: 'enum', type: 'enum',
enum: [true], enum: [true],
message: '请阅读并同意服务条款和隐私政策', message: '请阅读并同意服务条款和隐私政策',
trigger: 'change' trigger: 'change',
} },
] ],
} }
// Handle register // Handle register
@@ -161,7 +152,7 @@ const handleRegister = async () => {
// Example: const response = await registerApi(registerForm) // Example: const response = await registerApi(registerForm)
// Simulated delay // Simulated delay
await new Promise(resolve => setTimeout(resolve, 1500)) await new Promise((resolve) => setTimeout(resolve, 1500))
// Success message // Success message
ElMessage.success('注册成功!正在跳转到登录页面...') ElMessage.success('注册成功!正在跳转到登录页面...')
@@ -170,7 +161,6 @@ const handleRegister = async () => {
setTimeout(() => { setTimeout(() => {
router.push('/login') router.push('/login')
}, 1500) }, 1500)
} catch (error) { } catch (error) {
console.error('Register failed:', error) console.error('Register failed:', error)
ElMessage.error('注册失败,请稍后重试') ElMessage.error('注册失败,请稍后重试')

View File

@@ -9,7 +9,7 @@ import systemRoutes from './systemRoutes'
NProgress.configure({ NProgress.configure({
showSpinner: false, showSpinner: false,
trickleSpeed: 200, trickleSpeed: 200,
minimum: 0.3 minimum: 0.3,
}) })
/** /**
@@ -21,14 +21,14 @@ const notFoundRoute = {
component: () => import('../layouts/other/404.vue'), component: () => import('../layouts/other/404.vue'),
meta: { meta: {
title: '404', title: '404',
hidden: true hidden: true,
} },
} }
// 创建路由实例 // 创建路由实例
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: systemRoutes routes: systemRoutes,
}) })
/** /**
@@ -63,8 +63,8 @@ function transformMenusToRoutes(menus) {
} }
return menus return menus
.filter(menu => menu && menu.path) .filter((menu) => menu && menu.path)
.map(menu => { .map((menu) => {
const route = { const route = {
path: menu.path, path: menu.path,
name: menu.name || menu.path.replace(/\//g, '-'), name: menu.name || menu.path.replace(/\//g, '-'),
@@ -74,8 +74,8 @@ function transformMenusToRoutes(menus) {
hidden: menu.hidden || menu.meta?.hidden, hidden: menu.hidden || menu.meta?.hidden,
keepAlive: menu.meta?.keepAlive || false, keepAlive: menu.meta?.keepAlive || false,
affix: menu.meta?.affix || 0, 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() NProgress.start()
// 设置页面标题 // 设置页面标题
document.title = to.meta.title document.title = to.meta.title ? `${to.meta.title} - ${config.APP_NAME}` : config.APP_NAME
? `${to.meta.title} - ${config.APP_NAME}`
: config.APP_NAME
const userStore = useUserStore() const userStore = useUserStore()
const isLoggedIn = userStore.isLoggedIn() const isLoggedIn = userStore.isLoggedIn()
@@ -126,7 +124,7 @@ router.beforeEach(async (to, from, next) => {
// 保存目标路由,登录后跳转 // 保存目标路由,登录后跳转
next({ next({
path: '/login', path: '/login',
query: { redirect: to.fullPath } query: { redirect: to.fullPath },
}) })
return return
} }
@@ -149,7 +147,7 @@ router.beforeEach(async (to, from, next) => {
const dynamicRoutes = transformMenusToRoutes(mergedMenus) const dynamicRoutes = transformMenusToRoutes(mergedMenus)
// 添加动态路由到 Layout 的子路由 // 添加动态路由到 Layout 的子路由
dynamicRoutes.forEach(route => { dynamicRoutes.forEach((route) => {
router.addRoute('Layout', route) router.addRoute('Layout', route)
}) })
@@ -172,7 +170,7 @@ router.beforeEach(async (to, from, next) => {
userStore.logout() userStore.logout()
next({ next({
path: '/login', path: '/login',
query: { redirect: to.fullPath } query: { redirect: to.fullPath },
}) })
} }
} else { } else {
@@ -196,7 +194,7 @@ export function resetRouter() {
// 重置为初始路由 // 重置为初始路由
const newRouter = createRouter({ const newRouter = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: systemRoutes routes: systemRoutes,
}) })
router.matcher = newRouter.matcher router.matcher = newRouter.matcher

View File

@@ -1,34 +1,31 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import i18n from '@/i18n' import i18n from '@/i18n'
export const useI18nStore = defineStore( export const useI18nStore = defineStore('i18n', {
'i18n', state: () => ({
{ currentLocale: 'zh-CN',
state: () => ({ availableLocales: [
currentLocale: 'zh-CN', { label: '简体中文', value: 'zh-CN' },
availableLocales: [ { label: 'English', value: 'en-US' },
{ label: '简体中文', value: 'zh-CN' }, ],
{ label: 'English', value: 'en-US' } }),
]
}),
getters: { getters: {
localeLabel: (state) => { localeLabel: (state) => {
const locale = state.availableLocales.find((item) => item.value === state.currentLocale) const locale = state.availableLocales.find((item) => item.value === state.currentLocale)
return locale ? locale.label : '' return locale ? locale.label : ''
}
}, },
},
actions: { actions: {
setLocale(locale) { setLocale(locale) {
this.currentLocale = locale this.currentLocale = locale
i18n.global.locale.value = locale i18n.global.locale.value = locale
}
}, },
},
persist: { persist: {
key: 'i18n-store', key: 'i18n-store',
pick: ['currentLocale'] pick: ['currentLocale'],
} },
} })
)

View File

@@ -41,14 +41,14 @@ export const useUserStore = defineStore(
const menuMap = new Map() const menuMap = new Map()
// 先添加静态菜单 // 先添加静态菜单
staticMenus.forEach(menu => { staticMenus.forEach((menu) => {
if (menu.path) { if (menu.path) {
menuMap.set(menu.path, menu) menuMap.set(menu.path, menu)
} }
}) })
// 添加后端菜单,如果路径重复则覆盖 // 添加后端菜单,如果路径重复则覆盖
newMenu.forEach(menu => { newMenu.forEach((menu) => {
if (menu.path) { if (menu.path) {
menuMap.set(menu.path, menu) menuMap.set(menu.path, menu)
} }
@@ -110,7 +110,7 @@ export const useUserStore = defineStore(
{ {
persist: { persist: {
key: 'user-store', key: 'user-store',
pick: ['token', 'refreshToken', 'userInfo', 'menu'] pick: ['token', 'refreshToken', 'userInfo', 'menu'],
} },
} },
) )

View File

@@ -6,7 +6,7 @@ import router from '@/router'
const http = axios.create({ const http = axios.create({
timeout: 30000, timeout: 30000,
baseURL: config.API_URL baseURL: config.API_URL,
}) })
// 是否正在刷新 token // 是否正在刷新 token
@@ -29,7 +29,7 @@ http.interceptors.request.use(
}, },
(error) => { (error) => {
return Promise.reject(error) return Promise.reject(error)
} },
) )
// 响应拦截器 // 响应拦截器
@@ -124,7 +124,7 @@ http.interceptors.response.use(
const errorMessage = data?.message || error.message || '请求失败' const errorMessage = data?.message || error.message || '请求失败'
ElMessage.error(errorMessage) ElMessage.error(errorMessage)
return Promise.reject(error) return Promise.reject(error)
} },
) )
// 刷新 token 的方法 // 刷新 token 的方法
@@ -136,7 +136,7 @@ async function refreshToken() {
const refreshTokenValue = userStore.refreshToken const refreshTokenValue = userStore.refreshToken
const response = await axios.post(refreshUrl, { const response = await axios.post(refreshUrl, {
refreshToken: refreshTokenValue refreshToken: refreshTokenValue,
}) })
// 假设返回格式为 { code, data: { token, refreshToken } } // 假设返回格式为 { code, data: { token, refreshToken } }

View File

@@ -5,10 +5,10 @@
* @LastEditTime: 2026年1月15日 * @LastEditTime: 2026年1月15日
*/ */
import CryptoJS from "crypto-js"; import CryptoJS from 'crypto-js'
import sysConfig from "@/config"; import sysConfig from '@/config'
const tool = {}; const tool = {}
/** /**
* 检查是否为有效的值非null、非undefined、非空字符串、非空数组、非空对象 * 检查是否为有效的值非null、非undefined、非空字符串、非空数组、非空对象
@@ -17,19 +17,19 @@ const tool = {};
*/ */
tool.isValid = function (value) { tool.isValid = function (value) {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return false; return false
} }
if (typeof value === "string" && value.trim() === "") { if (typeof value === 'string' && value.trim() === '') {
return false; return false
} }
if (Array.isArray(value) && value.length === 0) { if (Array.isArray(value) && value.length === 0) {
return false; return false
} }
if (typeof value === "object" && Object.keys(value).length === 0) { if (typeof value === 'object' && Object.keys(value).length === 0) {
return false; return false
} }
return true; return true
}; }
/** /**
* 防抖函数 * 防抖函数
@@ -39,21 +39,21 @@ tool.isValid = function (value) {
* @returns {Function} * @returns {Function}
*/ */
tool.debounce = function (func, wait = 300, immediate = false) { tool.debounce = function (func, wait = 300, immediate = false) {
let timeout; let timeout
return function (...args) { return function (...args) {
const context = this; const context = this
clearTimeout(timeout); clearTimeout(timeout)
if (immediate && !timeout) { if (immediate && !timeout) {
func.apply(context, args); func.apply(context, args)
} }
timeout = setTimeout(() => { timeout = setTimeout(() => {
timeout = null; timeout = null
if (!immediate) { 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} * @returns {Function}
*/ */
tool.throttle = function (func, wait = 300, options = {}) { tool.throttle = function (func, wait = 300, options = {}) {
let timeout; let timeout
let previous = 0; let previous = 0
const { leading = true, trailing = true } = options; const { leading = true, trailing = true } = options
return function (...args) { return function (...args) {
const context = this; const context = this
const now = Date.now(); const now = Date.now()
if (!previous && !leading) { if (!previous && !leading) {
previous = now; previous = now
} }
const remaining = wait - (now - previous); const remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) { if (remaining <= 0 || remaining > wait) {
if (timeout) { if (timeout) {
clearTimeout(timeout); clearTimeout(timeout)
timeout = null; timeout = null
} }
previous = now; previous = now
func.apply(context, args); func.apply(context, args)
} else if (!timeout && trailing) { } else if (!timeout && trailing) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
previous = leading ? Date.now() : 0; previous = leading ? Date.now() : 0
timeout = null; timeout = null
func.apply(context, args); func.apply(context, args)
}, remaining); }, remaining)
} }
}; }
}; }
/** /**
* 深拷贝对象(支持循环引用) * 深拷贝对象(支持循环引用)
@@ -101,99 +101,88 @@ tool.throttle = function (func, wait = 300, options = {}) {
* @returns {*} * @returns {*}
*/ */
tool.deepClone = function (obj, hash = new WeakMap()) { tool.deepClone = function (obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== "object") { if (obj === null || typeof obj !== 'object') {
return obj; return obj
} }
if (hash.has(obj)) { if (hash.has(obj)) {
return hash.get(obj); return hash.get(obj)
} }
const clone = Array.isArray(obj) ? [] : {}; const clone = Array.isArray(obj) ? [] : {}
hash.set(obj, clone); hash.set(obj, clone)
for (const key in obj) { for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) { 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 */ /* localStorage */
tool.data = { tool.data = {
set(key, data, datetime = 0) { set(key, data, datetime = 0) {
//加密 //加密
if (sysConfig.LS_ENCRYPTION == "AES") { if (sysConfig.LS_ENCRYPTION == 'AES') {
data = tool.crypto.AES.encrypt( data = tool.crypto.AES.encrypt(JSON.stringify(data), sysConfig.LS_ENCRYPTION_key)
JSON.stringify(data),
sysConfig.LS_ENCRYPTION_key,
);
} }
let cacheValue = { let cacheValue = {
content: data, content: data,
datetime: datetime: parseInt(datetime) === 0 ? 0 : new Date().getTime() + parseInt(datetime) * 1000,
parseInt(datetime) === 0 }
? 0 return localStorage.setItem(key, JSON.stringify(cacheValue))
: new Date().getTime() + parseInt(datetime) * 1000,
};
return localStorage.setItem(key, JSON.stringify(cacheValue));
}, },
get(key) { get(key) {
try { try {
const value = JSON.parse(localStorage.getItem(key)); const value = JSON.parse(localStorage.getItem(key))
if (value) { if (value) {
let nowTime = new Date().getTime(); let nowTime = new Date().getTime()
if (nowTime > value.datetime && value.datetime != 0) { if (nowTime > value.datetime && value.datetime != 0) {
localStorage.removeItem(key); localStorage.removeItem(key)
return null; return null
} }
//解密 //解密
if (sysConfig.LS_ENCRYPTION == "AES") { if (sysConfig.LS_ENCRYPTION == 'AES') {
value.content = JSON.parse( value.content = JSON.parse(tool.crypto.AES.decrypt(value.content, sysConfig.LS_ENCRYPTION_key))
tool.crypto.AES.decrypt(
value.content,
sysConfig.LS_ENCRYPTION_key,
),
);
} }
return value.content; return value.content
} }
return null; return null
} catch { } catch {
return null; return null
} }
}, },
remove(key) { remove(key) {
return localStorage.removeItem(key); return localStorage.removeItem(key)
}, },
clear() { clear() {
return localStorage.clear(); return localStorage.clear()
}, },
}; }
/*sessionStorage*/ /*sessionStorage*/
tool.session = { tool.session = {
set(table, settings) { set(table, settings) {
const _set = JSON.stringify(settings); const _set = JSON.stringify(settings)
return sessionStorage.setItem(table, _set); return sessionStorage.setItem(table, _set)
}, },
get(table) { get(table) {
const data = sessionStorage.getItem(table); const data = sessionStorage.getItem(table)
try { try {
return JSON.parse(data); return JSON.parse(data)
} catch { } catch {
return null; return null
} }
}, },
remove(table) { remove(table) {
return sessionStorage.removeItem(table); return sessionStorage.removeItem(table)
}, },
clear() { clear() {
return sessionStorage.clear(); return sessionStorage.clear()
}, },
}; }
/*cookie*/ /*cookie*/
tool.cookie = { tool.cookie = {
@@ -210,28 +199,28 @@ tool.cookie = {
domain: null, domain: null,
secure: false, secure: false,
httpOnly: false, httpOnly: false,
sameSite: "Lax", sameSite: 'Lax',
...config, ...config,
}; }
let cookieStr = `${name}=${encodeURIComponent(value)}`; let cookieStr = `${name}=${encodeURIComponent(value)}`
if (cfg.expires) { if (cfg.expires) {
const exp = new Date(); const exp = new Date()
exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000); exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000)
cookieStr += `;expires=${exp.toUTCString()}`; cookieStr += `;expires=${exp.toUTCString()}`
} }
if (cfg.path) { if (cfg.path) {
cookieStr += `;path=${cfg.path}`; cookieStr += `;path=${cfg.path}`
} }
if (cfg.domain) { if (cfg.domain) {
cookieStr += `;domain=${cfg.domain}`; cookieStr += `;domain=${cfg.domain}`
} }
if (cfg.secure) { if (cfg.secure) {
cookieStr += `;secure`; cookieStr += `;secure`
} }
if (cfg.sameSite) { if (cfg.sameSite) {
cookieStr += `;SameSite=${cfg.sameSite}`; cookieStr += `;SameSite=${cfg.sameSite}`
} }
document.cookie = cookieStr; document.cookie = cookieStr
}, },
/** /**
* 获取cookie * 获取cookie
@@ -239,24 +228,22 @@ tool.cookie = {
* @returns {string|null} * @returns {string|null}
*/ */
get(name) { get(name) {
const arr = document.cookie.match( const arr = document.cookie.match(new RegExp('(^| )' + name + '=([^;]*)(;|$)'))
new RegExp("(^| )" + name + "=([^;]*)(;|$)"),
);
if (arr != null) { if (arr != null) {
return decodeURIComponent(arr[2]); return decodeURIComponent(arr[2])
} }
return null; return null
}, },
/** /**
* 删除cookie * 删除cookie
* @param {string} name - cookie名称 * @param {string} name - cookie名称
*/ */
remove(name) { remove(name) {
const exp = new Date(); const exp = new Date()
exp.setTime(exp.getTime() - 1); exp.setTime(exp.getTime() - 1)
document.cookie = `${name}=;expires=${exp.toUTCString()}`; document.cookie = `${name}=;expires=${exp.toUTCString()}`
}, },
}; }
/* Fullscreen */ /* Fullscreen */
/** /**
@@ -264,34 +251,29 @@ tool.cookie = {
* @param {HTMLElement} element - 要全屏的元素 * @param {HTMLElement} element - 要全屏的元素
*/ */
tool.screen = function (element) { tool.screen = function (element) {
const isFull = !!( const isFull = !!(document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement || document.fullscreenElement)
document.webkitIsFullScreen ||
document.mozFullScreen ||
document.msFullscreenElement ||
document.fullscreenElement
);
if (isFull) { if (isFull) {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen()
} else if (document.msExitFullscreen) { } else if (document.msExitFullscreen) {
document.msExitFullscreen(); document.msExitFullscreen()
} else if (document.mozCancelFullScreen) { } else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen(); document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) { } else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen(); document.webkitExitFullscreen()
} }
} else { } else {
if (element.requestFullscreen) { if (element.requestFullscreen) {
element.requestFullscreen(); element.requestFullscreen()
} else if (element.msRequestFullscreen) { } else if (element.msRequestFullscreen) {
element.msRequestFullscreen(); element.msRequestFullscreen()
} else if (element.mozRequestFullScreen) { } else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen(); element.mozRequestFullScreen()
} else if (element.webkitRequestFullscreen) { } else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(); element.webkitRequestFullscreen()
} }
} }
}; }
/* 复制对象(浅拷贝) */ /* 复制对象(浅拷贝) */
/** /**
@@ -300,11 +282,11 @@ tool.screen = function (element) {
* @returns {*} - 拷贝后的对象 * @returns {*} - 拷贝后的对象
*/ */
tool.objCopy = function (obj) { tool.objCopy = function (obj) {
if (obj === null || typeof obj !== "object") { if (obj === null || typeof obj !== 'object') {
return obj; 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" * @param {string} fmt - 格式化字符串,默认 "yyyy-MM-dd hh:mm:ss"
* @returns {string} - 格式化后的日期字符串 * @returns {string} - 格式化后的日期字符串
*/ */
tool.dateFormat = function (date, fmt = "yyyy-MM-dd hh:mm:ss") { tool.dateFormat = function (date, fmt = 'yyyy-MM-dd hh:mm:ss') {
if (!date) return ""; if (!date) return ''
const dateObj = new Date(date); const dateObj = new Date(date)
if (isNaN(dateObj.getTime())) return ""; if (isNaN(dateObj.getTime())) return ''
const o = { const o = {
"M+": dateObj.getMonth() + 1, // 月份 'M+': dateObj.getMonth() + 1, // 月份
"d+": dateObj.getDate(), // 日 'd+': dateObj.getDate(), // 日
"h+": dateObj.getHours(), // 小时 'h+': dateObj.getHours(), // 小时
"m+": dateObj.getMinutes(), // 分 'm+': dateObj.getMinutes(), // 分
"s+": dateObj.getSeconds(), // 秒 's+': dateObj.getSeconds(), // 秒
"q+": Math.floor((dateObj.getMonth() + 3) / 3), // 季度 'q+': Math.floor((dateObj.getMonth() + 3) / 3), // 季度
S: dateObj.getMilliseconds(), // 毫秒 S: dateObj.getMilliseconds(), // 毫秒
}; }
if (/(y+)/.test(fmt)) { if (/(y+)/.test(fmt)) {
fmt = fmt.replace( fmt = fmt.replace(RegExp.$1, (dateObj.getFullYear() + '').substr(4 - RegExp.$1.length))
RegExp.$1,
(dateObj.getFullYear() + "").substr(4 - RegExp.$1.length),
);
} }
for (const k in o) { for (const k in o) {
if (new RegExp("(" + k + ")").test(fmt)) { if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace( fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
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} - 格式化后的字符串 * @returns {string} - 格式化后的字符串
*/ */
tool.groupSeparator = function (num, decimals = 0) { tool.groupSeparator = function (num, decimals = 0) {
if (num === null || num === undefined || num === "") return ""; if (num === null || num === undefined || num === '') return ''
const numStr = Number(num).toFixed(decimals); const numStr = Number(num).toFixed(decimals)
const parts = numStr.split("."); const parts = numStr.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join("."); return parts.join('.')
}; }
/* 常用加解密 */ /* 常用加解密 */
tool.crypto = { tool.crypto = {
//MD5加密 //MD5加密
MD5(data) { MD5(data) {
return CryptoJS.MD5(data).toString(); return CryptoJS.MD5(data).toString()
}, },
//BASE64加解密 //BASE64加解密
BASE64: { BASE64: {
encrypt(data) { encrypt(data) {
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)); return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data))
}, },
decrypt(cipher) { decrypt(cipher) {
return CryptoJS.enc.Base64.parse(cipher).toString( return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8)
CryptoJS.enc.Utf8,
);
}, },
}, },
//AES加解密 //AES加解密
AES: { AES: {
encrypt(data, secretKey, config = {}) { encrypt(data, secretKey, config = {}) {
if (secretKey.length % 8 != 0) { if (secretKey.length % 8 != 0) {
console.warn( console.warn('[SCUI error]: 秘钥长度需为8的倍数否则解密将会失败。')
"[SCUI error]: 秘钥长度需为8的倍数否则解密将会失败。",
);
} }
const result = CryptoJS.AES.encrypt( const result = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(secretKey), {
data, iv: CryptoJS.enc.Utf8.parse(config.iv || ''),
CryptoJS.enc.Utf8.parse(secretKey), mode: CryptoJS.mode[config.mode || 'ECB'],
{ padding: CryptoJS.pad[config.padding || 'Pkcs7'],
iv: CryptoJS.enc.Utf8.parse(config.iv || ""), })
mode: CryptoJS.mode[config.mode || "ECB"], return result.toString()
padding: CryptoJS.pad[config.padding || "Pkcs7"],
},
);
return result.toString();
}, },
decrypt(cipher, secretKey, config = {}) { decrypt(cipher, secretKey, config = {}) {
const result = CryptoJS.AES.decrypt( const result = CryptoJS.AES.decrypt(cipher, CryptoJS.enc.Utf8.parse(secretKey), {
cipher, iv: CryptoJS.enc.Utf8.parse(config.iv || ''),
CryptoJS.enc.Utf8.parse(secretKey), mode: CryptoJS.mode[config.mode || 'ECB'],
{ padding: CryptoJS.pad[config.padding || 'Pkcs7'],
iv: CryptoJS.enc.Utf8.parse(config.iv || ""), })
mode: CryptoJS.mode[config.mode || "ECB"], return CryptoJS.enc.Utf8.stringify(result)
padding: CryptoJS.pad[config.padding || "Pkcs7"],
},
);
return CryptoJS.enc.Utf8.stringify(result);
}, },
}, },
}; }
/* 树形数据转扁平数组 */ /* 树形数据转扁平数组 */
/** /**
@@ -419,22 +381,22 @@ tool.crypto = {
* @param {Object} config - 配置项 { children: "children" } * @param {Object} config - 配置项 { children: "children" }
* @returns {Array} - 扁平化后的数组 * @returns {Array} - 扁平化后的数组
*/ */
tool.treeToList = function (tree, config = { children: "children" }) { tool.treeToList = function (tree, config = { children: 'children' }) {
const result = []; const result = []
tree.forEach((item) => { tree.forEach((item) => {
const tmp = { ...item }; const tmp = { ...item }
const childrenKey = config.children || "children"; const childrenKey = config.children || 'children'
if (tmp[childrenKey] && tmp[childrenKey].length > 0) { if (tmp[childrenKey] && tmp[childrenKey].length > 0) {
result.push({ ...item }); result.push({ ...item })
const childrenRoutes = tool.treeToList(tmp[childrenKey], config); const childrenRoutes = tool.treeToList(tmp[childrenKey], config)
result.push(...childrenRoutes); result.push(...childrenRoutes)
} else { } 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: [] } * @param {Object} config - 配置项 { pid: "parent_id", idField: "id", field: [] }
* @returns {*} - 父节点数据或指定字段 * @returns {*} - 父节点数据或指定字段
*/ */
tool.get_parents = function ( tool.get_parents = function (list, targetId = 0, config = { pid: 'parent_id', idField: 'id', field: [] }) {
list, let res = null
targetId = 0,
config = { pid: "parent_id", idField: "id", field: [] },
) {
let res = null;
list.forEach((item) => { list.forEach((item) => {
if (item[config.idField || "id"] === targetId) { if (item[config.idField || 'id'] === targetId) {
if (config.field && config.field.length > 1) { if (config.field && config.field.length > 1) {
res = {}; res = {}
config.field.forEach((field) => { config.field.forEach((field) => {
res[field] = item[field]; res[field] = item[field]
}); })
} else if (config.field && config.field.length === 1) { } else if (config.field && config.field.length === 1) {
res = item[config.field[0]]; res = item[config.field[0]]
} else { } else {
res = item; res = item
} }
} }
}); })
return res; return res
}; }
/* 获取数据字段 */ /* 获取数据字段 */
/** /**
@@ -475,25 +433,25 @@ tool.get_parents = function (
* @returns {*} - 提取的字段数据 * @returns {*} - 提取的字段数据
*/ */
tool.getDataField = function (data, fields = []) { tool.getDataField = function (data, fields = []) {
if (!data || typeof data !== "object") { if (!data || typeof data !== 'object') {
return data; return data
} }
if (fields.length === 0) { if (fields.length === 0) {
return data; return data
} }
if (fields.length === 1) { if (fields.length === 1) {
return data[fields[0]]; return data[fields[0]]
} else { } else {
const result = {}; const result = {}
fields.forEach((field) => { fields.forEach((field) => {
result[field] = data[field]; result[field] = data[field]
}); })
return result; return result
} }
}; }
// 兼容旧函数名 // 兼容旧函数名
tool.tree_to_list = tool.treeToList; tool.tree_to_list = tool.treeToList
tool.get_data_field = tool.getDataField; tool.get_data_field = tool.getDataField
export default tool; export default tool