gridsum-vue3-pc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/create-vue3-pc.mjs +545 -0
- package/package.json +68 -0
- package/template/base/.dockerignore +12 -0
- package/template/base/.env +5 -0
- package/template/base/.env.production +5 -0
- package/template/base/.eslintrc.cjs +22 -0
- package/template/base/.husky/commit-msg +1 -0
- package/template/base/.husky/pre-commit +1 -0
- package/template/base/.lintstagedrc +7 -0
- package/template/base/.prettierrc +5 -0
- package/template/base/.stylelintrc.cjs +6 -0
- package/template/base/.vscode/settings.json +26 -0
- package/template/base/CHANGELOG.md +6 -0
- package/template/base/Dockerfile +19 -0
- package/template/base/README.md +87 -0
- package/template/base/commitlint.config.cjs +1 -0
- package/template/base/index.html +15 -0
- package/template/base/mock/user.js +393 -0
- package/template/base/nginx.conf +27 -0
- package/template/base/package.json +47 -0
- package/template/base/public/favicon.svg +9 -0
- package/template/base/public/logo.svg +9 -0
- package/template/base/src/App.vue +20 -0
- package/template/base/src/assets/index.css +83 -0
- package/template/base/src/assets/logo.png +0 -0
- package/template/base/src/components/LanguageSwitch.vue +65 -0
- package/template/base/src/components/basic-layout.vue +484 -0
- package/template/base/src/composables/useCrud.ts +172 -0
- package/template/base/src/env.d.ts +28 -0
- package/template/base/src/env.ts +24 -0
- package/template/base/src/locales/en.json +153 -0
- package/template/base/src/locales/index.ts +32 -0
- package/template/base/src/locales/zh.json +153 -0
- package/template/base/src/main.ts +27 -0
- package/template/base/src/router/index.ts +91 -0
- package/template/base/src/services/http.ts +64 -0
- package/template/base/src/services/user.ts +23 -0
- package/template/base/src/store/modules/user.ts +45 -0
- package/template/base/src/views/Admin.vue +326 -0
- package/template/base/src/views/Home.vue +382 -0
- package/template/base/src/views/Login.vue +1252 -0
- package/template/base/src/views/Role.vue +269 -0
- package/template/base/src/views/User.vue +332 -0
- package/template/base/src/views/error/Forbidden.vue +62 -0
- package/template/base/src/views/error/NotFound.vue +60 -0
- package/template/base/src/views/error/ServerError.vue +62 -0
- package/template/base/tests/e2e/example.spec.ts +7 -0
- package/template/base/tests/unit/user.test.ts +15 -0
- package/template/base/vite.config.ts +52 -0
- package/template/cicd-github/.github/workflows/ci.yml +123 -0
- package/template/cicd-gitlab/.gitlab-ci.yml +103 -0
- package/template/cicd-jenkins/Jenkinsfile +107 -0
- package/template/ts/shims-vue.d.ts +5 -0
- package/template/ts/tsconfig.json +23 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"confirm": "Confirm",
|
|
4
|
+
"cancel": "Cancel",
|
|
5
|
+
"submit": "Submit",
|
|
6
|
+
"reset": "Reset",
|
|
7
|
+
"search": "Search",
|
|
8
|
+
"add": "Add",
|
|
9
|
+
"edit": "Edit",
|
|
10
|
+
"delete": "Delete",
|
|
11
|
+
"save": "Save",
|
|
12
|
+
"back": "Back",
|
|
13
|
+
"loading": "Loading...",
|
|
14
|
+
"success": "Success",
|
|
15
|
+
"error": "Error",
|
|
16
|
+
"warning": "Warning",
|
|
17
|
+
"info": "Info",
|
|
18
|
+
"yes": "Yes",
|
|
19
|
+
"no": "No",
|
|
20
|
+
"enable": "Enable",
|
|
21
|
+
"disable": "Disable",
|
|
22
|
+
"total": "Total",
|
|
23
|
+
"items": "items",
|
|
24
|
+
"actions": "Actions",
|
|
25
|
+
"noData": "No Data",
|
|
26
|
+
"reload": "Reload",
|
|
27
|
+
"export": "Export",
|
|
28
|
+
"batchDelete": "Batch Delete",
|
|
29
|
+
"selectItems": "Please select items first",
|
|
30
|
+
"batchDeleteConfirm": "Are you sure to delete selected items?"
|
|
31
|
+
},
|
|
32
|
+
"menu": {
|
|
33
|
+
"home": "Home",
|
|
34
|
+
"admin": "Admin",
|
|
35
|
+
"user": "User Management",
|
|
36
|
+
"role": "Role Management",
|
|
37
|
+
"login": "Login",
|
|
38
|
+
"notFound": "Not Found",
|
|
39
|
+
"system": "System"
|
|
40
|
+
},
|
|
41
|
+
"user": {
|
|
42
|
+
"login": "Login",
|
|
43
|
+
"logout": "Logout",
|
|
44
|
+
"username": "Username",
|
|
45
|
+
"password": "Password",
|
|
46
|
+
"loginSuccess": "Login Success",
|
|
47
|
+
"loginFail": "Login Failed",
|
|
48
|
+
"notLoggedIn": "Not Logged In",
|
|
49
|
+
"welcome": "Welcome to Login",
|
|
50
|
+
"usernamePlaceholder": "Please enter username",
|
|
51
|
+
"passwordPlaceholder": "Please enter password",
|
|
52
|
+
"loggingIn": "Logging in...",
|
|
53
|
+
"loginBtn": "Login",
|
|
54
|
+
"demoHint": "Demo accounts: admin/admin, zhangsan/123456, lisi/123456",
|
|
55
|
+
"name": "Name",
|
|
56
|
+
"email": "Email",
|
|
57
|
+
"phone": "Phone",
|
|
58
|
+
"role": "Role",
|
|
59
|
+
"status": "Status",
|
|
60
|
+
"createTime": "Create Time",
|
|
61
|
+
"searchPlaceholder": "Search username/name/email",
|
|
62
|
+
"addUser": "Add User",
|
|
63
|
+
"editUser": "Edit User",
|
|
64
|
+
"deleteConfirm": "Are you sure to delete this user?",
|
|
65
|
+
"deleteSuccess": "Delete Success",
|
|
66
|
+
"deleteFail": "Delete Failed",
|
|
67
|
+
"editSuccess": "Edit Success",
|
|
68
|
+
"editFail": "Edit Failed",
|
|
69
|
+
"addSuccess": "Add Success",
|
|
70
|
+
"addFail": "Add Failed",
|
|
71
|
+
"roleAdmin": "Admin",
|
|
72
|
+
"roleUser": "User",
|
|
73
|
+
"roleGuest": "Guest",
|
|
74
|
+
"requiredUsername": "Please enter username",
|
|
75
|
+
"requiredName": "Please enter name",
|
|
76
|
+
"requiredEmail": "Please enter email",
|
|
77
|
+
"invalidEmail": "Please enter valid email format",
|
|
78
|
+
"requiredPassword": "Please enter password",
|
|
79
|
+
"passwordTooShort": "Password must be at least 3 characters",
|
|
80
|
+
"requiredCaptcha": "Please enter captcha",
|
|
81
|
+
"captchaError": "Invalid captcha",
|
|
82
|
+
"captchaPlaceholder": "Enter captcha",
|
|
83
|
+
"rememberMe": "Remember Me",
|
|
84
|
+
"retryAfter": "Retry in",
|
|
85
|
+
"resetForm": "Reset Form"
|
|
86
|
+
},
|
|
87
|
+
"role": {
|
|
88
|
+
"name": "Role Name",
|
|
89
|
+
"code": "Role Code",
|
|
90
|
+
"description": "Description",
|
|
91
|
+
"permissions": "Permissions",
|
|
92
|
+
"createTime": "Create Time",
|
|
93
|
+
"searchPlaceholder": "Search role name/code",
|
|
94
|
+
"addRole": "Add Role",
|
|
95
|
+
"editRole": "Edit Role",
|
|
96
|
+
"deleteConfirm": "Are you sure to delete this role?",
|
|
97
|
+
"deleteSuccess": "Delete Success",
|
|
98
|
+
"deleteFail": "Delete Failed",
|
|
99
|
+
"editSuccess": "Edit Success",
|
|
100
|
+
"editFail": "Edit Failed",
|
|
101
|
+
"addSuccess": "Add Success",
|
|
102
|
+
"addFail": "Add Failed",
|
|
103
|
+
"requiredName": "Please enter role name",
|
|
104
|
+
"requiredCode": "Please enter role code",
|
|
105
|
+
"permUserView": "View User",
|
|
106
|
+
"permUserAdd": "Add User",
|
|
107
|
+
"permUserEdit": "Edit User",
|
|
108
|
+
"permUserDelete": "Delete User",
|
|
109
|
+
"permRoleView": "View Role",
|
|
110
|
+
"permRoleAdd": "Add Role",
|
|
111
|
+
"permRoleEdit": "Edit Role",
|
|
112
|
+
"permRoleDelete": "Delete Role"
|
|
113
|
+
},
|
|
114
|
+
"home": {
|
|
115
|
+
"greeting": "Welcome back, {name}",
|
|
116
|
+
"welcome": "Welcome to the new generation frontend engineering template!",
|
|
117
|
+
"trendTitle": "Traffic Trend (This Month)",
|
|
118
|
+
"quickActions": "Quick Actions",
|
|
119
|
+
"activeToday": "Active Today"
|
|
120
|
+
},
|
|
121
|
+
"admin": {
|
|
122
|
+
"userTotal": "Total Users",
|
|
123
|
+
"roleTotal": "Total Roles",
|
|
124
|
+
"permTotal": "Total Permissions",
|
|
125
|
+
"activeSessions": "Active Sessions",
|
|
126
|
+
"distributionTitle": "Role Distribution",
|
|
127
|
+
"recentActivity": "Recent Activity",
|
|
128
|
+
"logLogin": "{user} logged in",
|
|
129
|
+
"logCreateUser": "{user} created user {target}",
|
|
130
|
+
"logUpdateRole": "{user} updated role {target}",
|
|
131
|
+
"logDeleteUser": "{user} deleted user {target}"
|
|
132
|
+
},
|
|
133
|
+
"language": {
|
|
134
|
+
"zh": "中文",
|
|
135
|
+
"en": "English"
|
|
136
|
+
},
|
|
137
|
+
"error": {
|
|
138
|
+
"notFoundTitle": "Page Not Found",
|
|
139
|
+
"notFoundDesc": "Sorry, the page you are looking for does not exist or has been removed",
|
|
140
|
+
"forbiddenTitle": "Access Denied",
|
|
141
|
+
"forbiddenDesc": "Sorry, you do not have permission to access this page",
|
|
142
|
+
"serverErrorTitle": "Server Error",
|
|
143
|
+
"serverErrorDesc": "Sorry, the server encountered an error. Please try again later",
|
|
144
|
+
"backHome": "Back to Home"
|
|
145
|
+
},
|
|
146
|
+
"layout": {
|
|
147
|
+
"darkMode": "Dark Mode",
|
|
148
|
+
"lightMode": "Light Mode",
|
|
149
|
+
"fullscreen": "Fullscreen",
|
|
150
|
+
"exitFullscreen": "Exit Fullscreen",
|
|
151
|
+
"noNotifications": "No notifications"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createI18n } from 'vue-i18n';
|
|
2
|
+
import zh from './zh.json';
|
|
3
|
+
import en from './en.json';
|
|
4
|
+
|
|
5
|
+
const LOCALE_KEY = 'locale';
|
|
6
|
+
|
|
7
|
+
function resolveLocale(raw: string): string {
|
|
8
|
+
return raw.startsWith('zh') ? 'zh' : 'en';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function detectLocale() {
|
|
12
|
+
const saved = localStorage.getItem(LOCALE_KEY);
|
|
13
|
+
if (saved) return resolveLocale(saved);
|
|
14
|
+
|
|
15
|
+
const envLocale = import.meta.env.VITE_APP_LOCALE;
|
|
16
|
+
if (envLocale) return resolveLocale(envLocale);
|
|
17
|
+
|
|
18
|
+
return navigator.language.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function setLocale(locale: string) {
|
|
22
|
+
localStorage.setItem(LOCALE_KEY, locale);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const i18n = createI18n({
|
|
26
|
+
legacy: false,
|
|
27
|
+
locale: detectLocale(),
|
|
28
|
+
fallbackLocale: 'en',
|
|
29
|
+
messages: { zh, en },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export { i18n, setLocale };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"confirm": "确认",
|
|
4
|
+
"cancel": "取消",
|
|
5
|
+
"submit": "提交",
|
|
6
|
+
"reset": "重置",
|
|
7
|
+
"search": "搜索",
|
|
8
|
+
"add": "新增",
|
|
9
|
+
"edit": "编辑",
|
|
10
|
+
"delete": "删除",
|
|
11
|
+
"save": "保存",
|
|
12
|
+
"back": "返回",
|
|
13
|
+
"loading": "加载中...",
|
|
14
|
+
"success": "操作成功",
|
|
15
|
+
"error": "操作失败",
|
|
16
|
+
"warning": "警告",
|
|
17
|
+
"info": "提示",
|
|
18
|
+
"yes": "是",
|
|
19
|
+
"no": "否",
|
|
20
|
+
"enable": "启用",
|
|
21
|
+
"disable": "禁用",
|
|
22
|
+
"total": "共",
|
|
23
|
+
"items": "条",
|
|
24
|
+
"actions": "操作",
|
|
25
|
+
"noData": "暂无数据",
|
|
26
|
+
"reload": "重新加载",
|
|
27
|
+
"export": "导出",
|
|
28
|
+
"batchDelete": "批量删除",
|
|
29
|
+
"selectItems": "请先选择数据项",
|
|
30
|
+
"batchDeleteConfirm": "确定要删除选中的数据吗?"
|
|
31
|
+
},
|
|
32
|
+
"menu": {
|
|
33
|
+
"home": "首页",
|
|
34
|
+
"admin": "管理页",
|
|
35
|
+
"user": "用户管理",
|
|
36
|
+
"role": "角色管理",
|
|
37
|
+
"login": "登录",
|
|
38
|
+
"notFound": "页面未找到",
|
|
39
|
+
"system": "系统管理"
|
|
40
|
+
},
|
|
41
|
+
"user": {
|
|
42
|
+
"login": "登录",
|
|
43
|
+
"logout": "退出登录",
|
|
44
|
+
"username": "用户名",
|
|
45
|
+
"password": "密码",
|
|
46
|
+
"loginSuccess": "登录成功",
|
|
47
|
+
"loginFail": "登录失败",
|
|
48
|
+
"notLoggedIn": "未登录",
|
|
49
|
+
"welcome": "欢迎登录系统",
|
|
50
|
+
"usernamePlaceholder": "请输入用户名",
|
|
51
|
+
"passwordPlaceholder": "请输入密码",
|
|
52
|
+
"loggingIn": "登录中...",
|
|
53
|
+
"loginBtn": "登 录",
|
|
54
|
+
"demoHint": "演示账号: admin/admin, zhangsan/123456, lisi/123456",
|
|
55
|
+
"name": "姓名",
|
|
56
|
+
"email": "邮箱",
|
|
57
|
+
"phone": "手机号",
|
|
58
|
+
"role": "角色",
|
|
59
|
+
"status": "状态",
|
|
60
|
+
"createTime": "创建时间",
|
|
61
|
+
"searchPlaceholder": "搜索用户名/姓名/邮箱",
|
|
62
|
+
"addUser": "新增用户",
|
|
63
|
+
"editUser": "编辑用户",
|
|
64
|
+
"deleteConfirm": "确定要删除该用户吗?",
|
|
65
|
+
"deleteSuccess": "删除成功",
|
|
66
|
+
"deleteFail": "删除失败",
|
|
67
|
+
"editSuccess": "编辑成功",
|
|
68
|
+
"editFail": "编辑失败",
|
|
69
|
+
"addSuccess": "新增成功",
|
|
70
|
+
"addFail": "新增失败",
|
|
71
|
+
"roleAdmin": "管理员",
|
|
72
|
+
"roleUser": "普通用户",
|
|
73
|
+
"roleGuest": "访客",
|
|
74
|
+
"requiredUsername": "请输入用户名",
|
|
75
|
+
"requiredName": "请输入姓名",
|
|
76
|
+
"requiredEmail": "请输入邮箱",
|
|
77
|
+
"invalidEmail": "请输入正确的邮箱格式",
|
|
78
|
+
"requiredPassword": "请输入密码",
|
|
79
|
+
"passwordTooShort": "密码不能少于3位",
|
|
80
|
+
"requiredCaptcha": "请输入验证码",
|
|
81
|
+
"captchaError": "验证码错误",
|
|
82
|
+
"captchaPlaceholder": "请输入验证码",
|
|
83
|
+
"rememberMe": "记住密码",
|
|
84
|
+
"retryAfter": "请",
|
|
85
|
+
"resetForm": "重置表单"
|
|
86
|
+
},
|
|
87
|
+
"role": {
|
|
88
|
+
"name": "角色名称",
|
|
89
|
+
"code": "角色编码",
|
|
90
|
+
"description": "描述",
|
|
91
|
+
"permissions": "权限",
|
|
92
|
+
"createTime": "创建时间",
|
|
93
|
+
"searchPlaceholder": "搜索角色名称/编码",
|
|
94
|
+
"addRole": "新增角色",
|
|
95
|
+
"editRole": "编辑角色",
|
|
96
|
+
"deleteConfirm": "确定要删除该角色吗?",
|
|
97
|
+
"deleteSuccess": "删除成功",
|
|
98
|
+
"deleteFail": "删除失败",
|
|
99
|
+
"editSuccess": "编辑成功",
|
|
100
|
+
"editFail": "编辑失败",
|
|
101
|
+
"addSuccess": "新增成功",
|
|
102
|
+
"addFail": "新增失败",
|
|
103
|
+
"requiredName": "请输入角色名称",
|
|
104
|
+
"requiredCode": "请输入角色编码",
|
|
105
|
+
"permUserView": "用户查看",
|
|
106
|
+
"permUserAdd": "用户新增",
|
|
107
|
+
"permUserEdit": "用户编辑",
|
|
108
|
+
"permUserDelete": "用户删除",
|
|
109
|
+
"permRoleView": "角色查看",
|
|
110
|
+
"permRoleAdd": "角色新增",
|
|
111
|
+
"permRoleEdit": "角色编辑",
|
|
112
|
+
"permRoleDelete": "角色删除"
|
|
113
|
+
},
|
|
114
|
+
"home": {
|
|
115
|
+
"greeting": "欢迎回来,{name}",
|
|
116
|
+
"welcome": "欢迎使用新一代前端工程化模板!",
|
|
117
|
+
"trendTitle": "访问趋势(本月)",
|
|
118
|
+
"quickActions": "快速操作",
|
|
119
|
+
"activeToday": "今日活跃"
|
|
120
|
+
},
|
|
121
|
+
"admin": {
|
|
122
|
+
"userTotal": "用户总数",
|
|
123
|
+
"roleTotal": "角色总数",
|
|
124
|
+
"permTotal": "权限总数",
|
|
125
|
+
"activeSessions": "活跃会话",
|
|
126
|
+
"distributionTitle": "角色分布",
|
|
127
|
+
"recentActivity": "近期活动",
|
|
128
|
+
"logLogin": "{user} 登录系统",
|
|
129
|
+
"logCreateUser": "{user} 创建了用户 {target}",
|
|
130
|
+
"logUpdateRole": "{user} 更新了角色 {target}",
|
|
131
|
+
"logDeleteUser": "{user} 删除了用户 {target}"
|
|
132
|
+
},
|
|
133
|
+
"language": {
|
|
134
|
+
"zh": "中文",
|
|
135
|
+
"en": "English"
|
|
136
|
+
},
|
|
137
|
+
"error": {
|
|
138
|
+
"notFoundTitle": "页面未找到",
|
|
139
|
+
"notFoundDesc": "抱歉,您访问的页面不存在或已被移除",
|
|
140
|
+
"forbiddenTitle": "无权限访问",
|
|
141
|
+
"forbiddenDesc": "抱歉,您没有权限访问此页面",
|
|
142
|
+
"serverErrorTitle": "服务器错误",
|
|
143
|
+
"serverErrorDesc": "抱歉,服务器出现错误,请稍后重试",
|
|
144
|
+
"backHome": "返回首页"
|
|
145
|
+
},
|
|
146
|
+
"layout": {
|
|
147
|
+
"darkMode": "暗黑模式",
|
|
148
|
+
"lightMode": "明亮模式",
|
|
149
|
+
"fullscreen": "全屏",
|
|
150
|
+
"exitFullscreen": "退出全屏",
|
|
151
|
+
"noNotifications": "暂无通知"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import { createPinia } from 'pinia';
|
|
3
|
+
import ElementPlus from 'element-plus';
|
|
4
|
+
import 'element-plus/dist/index.css';
|
|
5
|
+
import 'element-plus/theme-chalk/dark/css-vars.css';
|
|
6
|
+
import App from './App.vue';
|
|
7
|
+
import router from './router';
|
|
8
|
+
import { i18n } from './locales';
|
|
9
|
+
|
|
10
|
+
import './assets/index.css';
|
|
11
|
+
import './env';
|
|
12
|
+
|
|
13
|
+
const app = createApp(App);
|
|
14
|
+
const pinia = createPinia();
|
|
15
|
+
|
|
16
|
+
app.use(pinia);
|
|
17
|
+
app.use(router);
|
|
18
|
+
app.use(ElementPlus);
|
|
19
|
+
app.use(i18n);
|
|
20
|
+
|
|
21
|
+
app.config.errorHandler = (err, instance, info) => {
|
|
22
|
+
console.error('[Global Error]', err);
|
|
23
|
+
console.error('[Component]', instance);
|
|
24
|
+
console.error('[Info]', info);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
app.mount('#app');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
|
|
2
|
+
import { useUserStore } from '@/store/modules/user';
|
|
3
|
+
import { i18n } from '@/locales';
|
|
4
|
+
|
|
5
|
+
const staticRoutes: RouteRecordRaw[] = [
|
|
6
|
+
{
|
|
7
|
+
path: '/login',
|
|
8
|
+
name: 'Login',
|
|
9
|
+
component: () => import('@/views/Login.vue'),
|
|
10
|
+
meta: { titleKey: 'menu.login' },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
path: '/403',
|
|
14
|
+
name: 'Forbidden',
|
|
15
|
+
component: () => import('@/views/error/Forbidden.vue'),
|
|
16
|
+
meta: { titleKey: 'error.forbiddenTitle' },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: '/500',
|
|
20
|
+
name: 'ServerError',
|
|
21
|
+
component: () => import('@/views/error/ServerError.vue'),
|
|
22
|
+
meta: { titleKey: 'error.serverErrorTitle' },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
path: '/',
|
|
26
|
+
component: () => import('@/components/basic-layout.vue'),
|
|
27
|
+
meta: { requiresAuth: true },
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
path: '',
|
|
31
|
+
name: 'Home',
|
|
32
|
+
component: () => import('@/views/Home.vue'),
|
|
33
|
+
meta: { titleKey: 'menu.home', keepAlive: true },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: 'user',
|
|
37
|
+
name: 'User',
|
|
38
|
+
component: () => import('@/views/User.vue'),
|
|
39
|
+
meta: { titleKey: 'menu.user', requiresAdmin: true },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: 'role',
|
|
43
|
+
name: 'Role',
|
|
44
|
+
component: () => import('@/views/Role.vue'),
|
|
45
|
+
meta: { titleKey: 'menu.role', requiresAdmin: true },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: 'admin',
|
|
49
|
+
name: 'Admin',
|
|
50
|
+
component: () => import('@/views/Admin.vue'),
|
|
51
|
+
meta: { titleKey: 'menu.admin', requiresAdmin: true },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: '/:pathMatch(.*)*',
|
|
57
|
+
name: 'NotFound',
|
|
58
|
+
component: () => import('@/views/error/NotFound.vue'),
|
|
59
|
+
meta: { titleKey: 'error.notFoundTitle' },
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const router = createRouter({
|
|
64
|
+
history: createWebHistory(),
|
|
65
|
+
routes: staticRoutes,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
router.beforeEach((to) => {
|
|
69
|
+
const userStore = useUserStore();
|
|
70
|
+
const user = userStore.userInfo ?? userStore.loadFromStorage();
|
|
71
|
+
|
|
72
|
+
const titleKey = to.meta.titleKey as string;
|
|
73
|
+
if (titleKey) {
|
|
74
|
+
document.title = `${i18n.global.t(titleKey)} - ${import.meta.env.VITE_APP_TITLE}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (to.path === '/login') {
|
|
78
|
+
return user ? '/' : true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const requiresAuth = to.matched.some(r => r.meta.requiresAuth);
|
|
82
|
+
if (requiresAuth && !user) {
|
|
83
|
+
return '/login';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (to.meta.requiresAdmin && !userStore.isAdmin()) {
|
|
87
|
+
return '/403';
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export default router;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { ElMessage } from 'element-plus';
|
|
3
|
+
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
|
4
|
+
|
|
5
|
+
export interface ResponseData<T = any> {
|
|
6
|
+
code: number;
|
|
7
|
+
data: T;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ERROR_MESSAGES: Record<string, string> = {
|
|
12
|
+
zh: '请求失败',
|
|
13
|
+
en: 'Request failed',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const UNAUTHORIZED: Record<string, string> = {
|
|
17
|
+
zh: '登录已过期,请重新登录',
|
|
18
|
+
en: 'Session expired, please login again',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getLang(): string {
|
|
22
|
+
const saved = localStorage.getItem('locale');
|
|
23
|
+
if (saved) return saved.startsWith('zh') ? 'zh' : 'en';
|
|
24
|
+
return navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const service = axios.create({
|
|
28
|
+
baseURL: '/api',
|
|
29
|
+
timeout: 10000,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
service.interceptors.request.use(
|
|
33
|
+
(config: InternalAxiosRequestConfig) => {
|
|
34
|
+
const token = localStorage.getItem('token');
|
|
35
|
+
if (token) {
|
|
36
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
37
|
+
}
|
|
38
|
+
return config;
|
|
39
|
+
},
|
|
40
|
+
error => Promise.reject(error),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
service.interceptors.response.use(
|
|
44
|
+
(response: AxiosResponse<ResponseData>) => {
|
|
45
|
+
const res = response.data;
|
|
46
|
+
if (res.code !== 0 && res.code !== 200) {
|
|
47
|
+
const msg = res.message || ERROR_MESSAGES[getLang()];
|
|
48
|
+
return Promise.reject(new Error(msg));
|
|
49
|
+
}
|
|
50
|
+
return res;
|
|
51
|
+
},
|
|
52
|
+
error => {
|
|
53
|
+
if (error.response?.status === 401) {
|
|
54
|
+
localStorage.removeItem('user');
|
|
55
|
+
localStorage.removeItem('token');
|
|
56
|
+
ElMessage.error(UNAUTHORIZED[getLang()]);
|
|
57
|
+
window.location.href = '/login';
|
|
58
|
+
return Promise.reject(error);
|
|
59
|
+
}
|
|
60
|
+
return Promise.reject(error);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export default service;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import http from './http';
|
|
2
|
+
import type { ResponseData } from './http';
|
|
3
|
+
|
|
4
|
+
export interface UserInfo {
|
|
5
|
+
id: string;
|
|
6
|
+
username: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
role: 'admin' | 'user' | 'guest';
|
|
9
|
+
permissions: string[];
|
|
10
|
+
email?: string;
|
|
11
|
+
avatar?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaginatedData<T> {
|
|
15
|
+
list: T[];
|
|
16
|
+
total: number;
|
|
17
|
+
page: number;
|
|
18
|
+
pageSize: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function login(username: string, password: string): Promise<ResponseData<UserInfo>> {
|
|
22
|
+
return http.post('/auth/login', { username, password });
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import type { UserInfo } from '@/services/user';
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = 'user';
|
|
6
|
+
|
|
7
|
+
export const useUserStore = defineStore('user', () => {
|
|
8
|
+
const userInfo = ref<UserInfo | null>(null);
|
|
9
|
+
|
|
10
|
+
function setUserInfo(info: UserInfo) {
|
|
11
|
+
userInfo.value = info;
|
|
12
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(info));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadFromStorage(): UserInfo | null {
|
|
16
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
17
|
+
if (!raw) return null;
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(raw) as UserInfo;
|
|
20
|
+
userInfo.value = data;
|
|
21
|
+
return data;
|
|
22
|
+
} catch {
|
|
23
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAdmin(): boolean {
|
|
29
|
+
return userInfo.value?.role === 'admin';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function logout() {
|
|
33
|
+
userInfo.value = null;
|
|
34
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
35
|
+
window.location.href = '/login';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
userInfo,
|
|
40
|
+
setUserInfo,
|
|
41
|
+
loadFromStorage,
|
|
42
|
+
isAdmin,
|
|
43
|
+
logout,
|
|
44
|
+
};
|
|
45
|
+
});
|