hlq-cli 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.
Files changed (88) hide show
  1. package/README.md +18 -0
  2. package/bin/index.js +16 -0
  3. package/lib/aesCreate.js +11 -0
  4. package/lib/axiosCreate.js +11 -0
  5. package/lib/create.js +172 -0
  6. package/lib/echartCreate.js +12 -0
  7. package/lib/jwtDecodeCreate.js +16 -0
  8. package/lib/rsaCreate.js +11 -0
  9. package/lib/stateCreate.js +34 -0
  10. package/lib/websocketCreate.js +16 -0
  11. package/package.json +21 -0
  12. package/templates/.env +1 -0
  13. package/templates/.env.dev +2 -0
  14. package/templates/.env.pro +2 -0
  15. package/templates/index.html +15 -0
  16. package/templates/package-lock.json +4058 -0
  17. package/templates/package.json +31 -0
  18. package/templates/public/config.js +1 -0
  19. package/templates/public/font/iconfont.css +579 -0
  20. package/templates/public/font/iconfont.js +1 -0
  21. package/templates/public/font/iconfont.ttf +0 -0
  22. package/templates/public/font/iconfont.woff +0 -0
  23. package/templates/public/font/iconfont.woff2 +0 -0
  24. package/templates/src/App.vue +35 -0
  25. package/templates/src/components/chart/barChart.vue +103 -0
  26. package/templates/src/components/chart/color.ts +43 -0
  27. package/templates/src/components/chart/lineChart.vue +114 -0
  28. package/templates/src/components/chart/mapChart.vue +135 -0
  29. package/templates/src/components/chart/mixedChart.vue +148 -0
  30. package/templates/src/components/chart/pieChart.vue +104 -0
  31. package/templates/src/components/chart/radarChart.vue +112 -0
  32. package/templates/src/components/chart/scatterChart.vue +144 -0
  33. package/templates/src/components/chart/sunburstChart.vue +183 -0
  34. package/templates/src/components/descript/index.vue +45 -0
  35. package/templates/src/components/dialog/index.vue +54 -0
  36. package/templates/src/components/drawer/index.vue +53 -0
  37. package/templates/src/components/form/component/cascader.vue +65 -0
  38. package/templates/src/components/form/component/checkbox.vue +31 -0
  39. package/templates/src/components/form/component/datePicker.vue +39 -0
  40. package/templates/src/components/form/component/dateRange.vue +36 -0
  41. package/templates/src/components/form/component/datetimePicker.vue +25 -0
  42. package/templates/src/components/form/component/fileUpload.vue +80 -0
  43. package/templates/src/components/form/component/formFun.ts +132 -0
  44. package/templates/src/components/form/component/imageUpload.vue +92 -0
  45. package/templates/src/components/form/component/input.vue +41 -0
  46. package/templates/src/components/form/component/location.vue +79 -0
  47. package/templates/src/components/form/component/radio.vue +31 -0
  48. package/templates/src/components/form/component/select.vue +66 -0
  49. package/templates/src/components/form/component/textarea.vue +26 -0
  50. package/templates/src/components/form/component/timePicker.vue +28 -0
  51. package/templates/src/components/form/component/upload.ts +20 -0
  52. package/templates/src/components/form/formInterface.ts +115 -0
  53. package/templates/src/components/form/index.vue +193 -0
  54. package/templates/src/components/form/item.vue +323 -0
  55. package/templates/src/components/groupForm/index.vue +91 -0
  56. package/templates/src/components/icon/index.vue +29 -0
  57. package/templates/src/components/layout/header.vue +238 -0
  58. package/templates/src/components/layout/index.vue +167 -0
  59. package/templates/src/components/layout/menu.vue +130 -0
  60. package/templates/src/components/layout/sideBarItem.vue +49 -0
  61. package/templates/src/components/searchBox/height.ts +9 -0
  62. package/templates/src/components/searchBox/index.vue +265 -0
  63. package/templates/src/components/table/index.vue +371 -0
  64. package/templates/src/components/table/table.ts +23 -0
  65. package/templates/src/components/tree/index.vue +222 -0
  66. package/templates/src/components/tree/lazyTree.vue +136 -0
  67. package/templates/src/data.d.ts +4 -0
  68. package/templates/src/main.ts +18 -0
  69. package/templates/src/router/index.ts +60 -0
  70. package/templates/src/store/menuInterface.ts +10 -0
  71. package/templates/src/store/permission.ts +59 -0
  72. package/templates/src/store/user.ts +24 -0
  73. package/templates/src/utils/alioss/index.ts +0 -0
  74. package/templates/src/utils/axios/http.ts +99 -0
  75. package/templates/src/utils/axios/index.ts +112 -0
  76. package/templates/src/utils/axios/service.ts +8 -0
  77. package/templates/src/utils/crypto/index.ts +28 -0
  78. package/templates/src/utils/rsa/index.ts +18 -0
  79. package/templates/src/utils/token/index.ts +6 -0
  80. package/templates/src/utils/tree/index.ts +74 -0
  81. package/templates/src/utils/websocket/index.ts +136 -0
  82. package/templates/src/views/login/index.vue +248 -0
  83. package/templates/src/views/templete/table.vue +122 -0
  84. package/templates/src/views/templete/tableConfig.ts +153 -0
  85. package/templates/tsconfig.app.json +19 -0
  86. package/templates/tsconfig.json +7 -0
  87. package/templates/tsconfig.node.json +23 -0
  88. package/templates/vite.config.ts +34 -0
@@ -0,0 +1,112 @@
1
+ import axios from 'axios'
2
+ import { useUserStore } from '@/store/user'
3
+ import router from '@/router'
4
+ import { ElLoading, ElMessage, type LoadingParentElement } from 'element-plus'
5
+
6
+ declare module 'axios' {
7
+ interface AxiosRequestConfig {
8
+ noloading?: boolean
9
+ }
10
+ }
11
+
12
+ let loading: any
13
+ const count = {
14
+ count: 0,
15
+ add: () => {
16
+ loading = ElLoading.service({ fullscreen: true })
17
+ count.count += 1
18
+ },
19
+ del: () => {
20
+ count.count -= 1
21
+ if (count.count === 0) {
22
+ loading.close()
23
+ }
24
+ },
25
+ }
26
+
27
+ export const request = axios.create({
28
+ baseURL: import.meta.env.VITE_BASE_URL,
29
+ timeout: 10000,
30
+ })
31
+ let list: string[] = []
32
+ request.interceptors.request.use(
33
+ (config: any) => {
34
+ const store = useUserStore()
35
+ const controller = new AbortController()
36
+ config.signal = controller.signal
37
+ config.headers['Authorization'] = store.token
38
+ const key =
39
+ config.url +
40
+ JSON.stringify(config.data) +
41
+ JSON.stringify(config.params) +
42
+ (router && router.currentRoute && router.currentRoute.value.path) || ''
43
+ if (list.includes(key)) {
44
+ controller.abort()
45
+ } else {
46
+ list.push(key)
47
+ }
48
+ config.my = key
49
+ if (!config.noloading) {
50
+ count.add()
51
+ }
52
+ return config
53
+ },
54
+ (error) => {
55
+ return Promise.reject(error)
56
+ },
57
+ )
58
+ const errorMsg = new Map([
59
+ ['Request failed with status code 500', '后端服务出错'],
60
+ ['Request failed with status code 404', '接口不存在'],
61
+ ['Request failed with status code 401', '登录信息失效'],
62
+ ['success', '操作成功'],
63
+ ['timeout of 10000ms exceeded', '请求超时'],
64
+ ])
65
+ request.interceptors.response.use(
66
+ (response: any) => {
67
+ if (!response.config.noloading) {
68
+ count.del()
69
+ }
70
+ if (response.request.responseType === 'blob') {
71
+ return response.data
72
+ }
73
+ if (response.config.my) {
74
+ list = list.filter((item) => item !== response.config.my)
75
+ }
76
+ if (response.data.status === 401) {
77
+ console.log('401')
78
+ const store = useUserStore()
79
+ store.setToken('')
80
+ router.push('/login')
81
+ }
82
+ if (response.data.code !== 0) {
83
+ ElMessage.error({
84
+ message: response.data.msg || '请求出错',
85
+ })
86
+ return Promise.reject(response.data.msg)
87
+ }
88
+ if (response.data.code === 0) {
89
+ if (response.config.auto && response.config.method !== 'get') {
90
+ ElMessage.success(errorMsg.get(response.data.msg) || response.data.msg)
91
+ }
92
+ return response.data
93
+ }
94
+ },
95
+ (error) => {
96
+ if (error.status === 401) {
97
+ console.log('401')
98
+ const store = useUserStore()
99
+ store.setToken('')
100
+ router.push('/login')
101
+ }
102
+ count.del()
103
+ if (error.config && error.config.my) {
104
+ list = list.filter((item) => item !== error.config.my)
105
+ }
106
+ if (error.message === 'canceled') return Promise.reject(error)
107
+ ElMessage.error({
108
+ message: errorMsg.get(error.message) || error.message,
109
+ })
110
+ return Promise.reject(error)
111
+ },
112
+ )
@@ -0,0 +1,8 @@
1
+ import { HttpService } from './http'
2
+
3
+ // 主服务(默认)
4
+ export const mainService = new HttpService({
5
+ baseURL: '/api',
6
+ timeout: 10000,
7
+ preserveLongIntegers: true,
8
+ })
@@ -0,0 +1,28 @@
1
+ import CryptoJS from "crypto-js";
2
+
3
+ const key = "1234567887654321";
4
+ export const PublicData = (data: string) => {
5
+ let encrypted = CryptoJS.AES.encrypt(
6
+ CryptoJS.enc.Utf8.parse(data),
7
+ CryptoJS.enc.Utf8.parse(key),
8
+ {
9
+ iv: CryptoJS.enc.Utf8.parse(key),
10
+ mode: CryptoJS.mode.ECB,
11
+ padding: CryptoJS.pad.Pkcs7,
12
+ }
13
+ );
14
+ return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
15
+ };
16
+ export const PrivateData = (data: string) => {
17
+ let decrypt = CryptoJS.AES.decrypt(
18
+ CryptoJS.enc.Base64.stringify(CryptoJS.enc.Base64.parse(data)),
19
+ CryptoJS.enc.Utf8.parse(key),
20
+ {
21
+ iv: CryptoJS.enc.Utf8.parse(key),
22
+ mode: CryptoJS.mode.ECB,
23
+ padding: CryptoJS.pad.Pkcs7,
24
+ }
25
+ );
26
+ let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
27
+ return decryptedStr.toString();
28
+ };
@@ -0,0 +1,18 @@
1
+ import JSEncrypt from "jsencrypt";
2
+ const publicKey =
3
+ "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALzt+PFt/RMrOCCdxHFEL1TG+mMoYs/ijAEjO1oJrXTQWPMolqBN50Ud/a3B/PmKo/MFsrZKV2SkQiqohz/cBxECAwEAAQ==";
4
+ const privateKey =
5
+ "MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAvO348W39Eys4IJ3EcUQvVMb6Yyhiz+KMASM7WgmtdNBY8yiWoE3nRR39rcH8+Yqj8wWytkpXZKRCKqiHP9wHEQIDAQABAkABfwT0rIFPkI0OPRGcMAUL79N1y5EUwl+Hdsb2jJMSFLzSBJEsvkqybOa+gHK7xdKGLn/M1U6a2VxH0HCXTkABAiEA6pazruvnKfugK+qhOSF8G8aoj/3sn6mmxAo7DUepGxECIQDOLGtwLv3zr0znREQp1svLv1e4A0nFdvAcYDIV9swsAQIgAxT5xVmKDu4kW49YkOSUudSxUDr4ydwzua2cuv7vBNECIHGAVLlStW2k8RJUK65Y2KLXOMRN0xwJSVBlyMz8rBQBAiBt6ja7sUE15YBKuCh8BpfONfqxBZNqeO0aZEedTVpqTw==";
6
+
7
+ export const rsaPublicData = (data: string) => {
8
+ const jsencrypt = new JSEncrypt();
9
+ jsencrypt.setPublicKey(publicKey);
10
+ const result = jsencrypt.encrypt(data);
11
+ return result;
12
+ };
13
+ export const rsaPrivateData = (data: string) => {
14
+ const jsencrypt = new JSEncrypt();
15
+ jsencrypt.setPrivateKey(privateKey);
16
+ const result = jsencrypt.decrypt(data);
17
+ return result;
18
+ };
@@ -0,0 +1,6 @@
1
+ import { jwtDecode } from "jwt-decode";
2
+
3
+ export const decode = (token: string) => {
4
+ const result = jwtDecode(token);
5
+ return result;
6
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 获取树形数据子节点数据
3
+ *
4
+ * @param {Data} node
5
+ * @returns {Data[]}
6
+ */
7
+ export const treeTolist = (node: Data | Data[]) => {
8
+ const result: Data[] = []
9
+ function traverse(node: Data) {
10
+ if (node.children && node.children.length > 0) {
11
+ node.children.forEach((item: Data) => {
12
+ traverse(item)
13
+ })
14
+ } else {
15
+ result.push(node)
16
+ }
17
+ }
18
+ if (Array.isArray(node)) {
19
+ node.forEach((item: Data) => {
20
+ traverse(item)
21
+ })
22
+ } else {
23
+ traverse(node)
24
+ }
25
+ return result
26
+ }
27
+
28
+ export const listTotree = (
29
+ list: Data[],
30
+ parentKey?: string,
31
+ idKey?: string,
32
+ sortKey?: string,
33
+ isSearch?: boolean,
34
+ ): Data[] => {
35
+ const map = new Map()
36
+ const roots: Data[] = []
37
+
38
+ list.forEach((item) => {
39
+ map.set(item[idKey || 'id'], { ...item, children: [] })
40
+ })
41
+
42
+ list
43
+ .sort((a: Data, b: Data) => {
44
+ return (a[sortKey || 'index'] || 99) - (b[sortKey || 'index'] || 99)
45
+ })
46
+ .forEach((item) => {
47
+ const node = map.get(item[idKey || 'id'])
48
+ if (item[parentKey || 'parentId']) {
49
+ const parent = map.get(item[parentKey || 'parentId'])
50
+ if (parent) {
51
+ parent.children.push(node)
52
+ } else if (isSearch) {
53
+ roots.push(node)
54
+ }
55
+ } else {
56
+ roots.push(node)
57
+ }
58
+ })
59
+
60
+ return roots
61
+ }
62
+
63
+ // 获取树第一个子节点
64
+ export const getTreeFirstChild = (tree: Array<Data>): Data => {
65
+ if (tree && tree.length > 0) {
66
+ let node = tree[0]
67
+ while (node.children && node.children.length > 0) {
68
+ node = node.children[0]
69
+ }
70
+ return node
71
+ } else {
72
+ return {} as Data
73
+ }
74
+ }
@@ -0,0 +1,136 @@
1
+ import { onUnmounted } from "vue";
2
+
3
+ //设置
4
+ interface SocketOptions {
5
+ //心跳间隔
6
+ heartbeatInterval?: number;
7
+ //超时重传
8
+ reconnectInterval?: number;
9
+ //最大重传次数
10
+ maxReconnectAttempts?: number;
11
+ }
12
+
13
+ class Socket {
14
+ //路径
15
+ url: string;
16
+ ws: WebSocket | null = null;
17
+ opts: SocketOptions;
18
+ //重传次数
19
+ reconnectAttempts: number = 0;
20
+ listeners: { [key: string]: Function[] } = {};
21
+ //心跳间隔
22
+ heartbeatInterval: number | null = null;
23
+ //构造函数
24
+ constructor(url: string, opts: SocketOptions = {}) {
25
+ this.url = url;
26
+ this.opts = {
27
+ //心跳间隔
28
+ heartbeatInterval: 30000,
29
+ //超时重传
30
+ reconnectInterval: 5000,
31
+ //最大重传次数
32
+ maxReconnectAttempts: 5,
33
+ ...opts,
34
+ };
35
+
36
+ this.init();
37
+ }
38
+ //初始化
39
+ init() {
40
+ this.ws = new WebSocket(this.url);
41
+ this.ws.onopen = this.onOpen.bind(this);
42
+ this.ws.onmessage = this.onMessage.bind(this);
43
+ this.ws.onerror = this.onError.bind(this);
44
+ this.ws.onclose = this.onClose.bind(this);
45
+ }
46
+ //打开
47
+ onOpen(event: Event) {
48
+ this.reconnectAttempts = 0;
49
+ this.startHeartbeat();
50
+ this.emit("open", event);
51
+ }
52
+ //收到的WebSocket消息
53
+ onMessage(event: MessageEvent) {
54
+ // console.log('WebSocket message received:', event.data);
55
+ this.emit("message", event.data);
56
+ }
57
+ //错误
58
+ onError(event: Event) {
59
+ this.emit("error", event);
60
+ }
61
+ //重连逻辑中,在连接失败后自动重新连接
62
+ onClose(event: CloseEvent) {
63
+ this.stopHeartbeat();
64
+ this.emit("close", event);
65
+ //重连逻辑中,在连接失败后自动重新连接,但会限制重连的次数和每次重连之间的间隔时间
66
+ if (
67
+ this.opts.maxReconnectAttempts !== 0 &&
68
+ this.reconnectAttempts < this.opts.maxReconnectAttempts!
69
+ ) {
70
+ setTimeout(() => {
71
+ this.reconnectAttempts++;
72
+ this.init();
73
+ }, this.opts.reconnectInterval);
74
+ }
75
+ }
76
+ //开始心跳检测
77
+ startHeartbeat() {
78
+ if (!this.opts.heartbeatInterval) return;
79
+
80
+ this.heartbeatInterval = window.setInterval(() => {
81
+ if (this.ws?.readyState === WebSocket.OPEN) {
82
+ this.ws.send("ping");
83
+ }
84
+ }, this.opts.heartbeatInterval);
85
+ }
86
+ //停止心跳检测
87
+ stopHeartbeat() {
88
+ if (this.heartbeatInterval) {
89
+ clearInterval(this.heartbeatInterval);
90
+ this.heartbeatInterval = null;
91
+ }
92
+ }
93
+ //发送消息
94
+ send(data: string) {
95
+ if (this.ws?.readyState === WebSocket.OPEN) {
96
+ this.ws.send(data);
97
+ } else {
98
+ }
99
+ }
100
+ //事件监听器注册功能的实现
101
+ on(event: string, callback: Function) {
102
+ if (!this.listeners[event]) {
103
+ this.listeners[event] = [];
104
+ }
105
+ this.listeners[event].push(callback);
106
+ }
107
+ //从事件监听器中移除
108
+ off(event: string) {
109
+ if (this.listeners[event]) {
110
+ delete this.listeners[event];
111
+ }
112
+ }
113
+ //在事件监听器中触发一个指定的事件
114
+ emit(event: string, data: any) {
115
+ this.listeners[event]?.forEach((callback) => callback(data));
116
+ }
117
+ }
118
+
119
+ export function useSocket(url: string, opts?: SocketOptions) {
120
+ const socket = new Socket(url, opts);
121
+
122
+ onUnmounted(() => {
123
+ socket.off("open");
124
+ socket.off("message");
125
+ socket.off("error");
126
+ socket.off("close");
127
+ socket.ws?.close(); // 关闭WebSocket连接
128
+ });
129
+
130
+ return {
131
+ socket,
132
+ send: socket.send.bind(socket),
133
+ on: socket.on.bind(socket),
134
+ off: socket.off.bind(socket),
135
+ };
136
+ }
@@ -0,0 +1,248 @@
1
+ <template>
2
+ <div class="login-container">
3
+ <div class="login-box">
4
+ <div class="login-header">
5
+ <h2>系统登录</h2>
6
+ <p>欢迎使用管理系统</p>
7
+ </div>
8
+ <el-form
9
+ ref="loginForm"
10
+ :model="formData"
11
+ :rules="rules"
12
+ class="login-form"
13
+ >
14
+ <el-form-item prop="username">
15
+ <el-input
16
+ v-model="formData.username"
17
+ placeholder="请输入用户名"
18
+ prefix-icon="User"
19
+ size="large"
20
+ />
21
+ </el-form-item>
22
+ <el-form-item prop="password">
23
+ <el-input
24
+ v-model="formData.password"
25
+ type="password"
26
+ placeholder="请输入密码"
27
+ prefix-icon="Lock"
28
+ size="large"
29
+ show-password
30
+ />
31
+ </el-form-item>
32
+ <el-form-item prop="captcha">
33
+ <div class="captcha-wrapper">
34
+ <el-input
35
+ v-model="formData.captcha"
36
+ placeholder="请输入验证码"
37
+ prefix-icon="Code"
38
+ size="large"
39
+ />
40
+ <div class="captcha-image" @click="refreshCaptcha">
41
+ <span>{{ captchaCode }}</span>
42
+ </div>
43
+ </div>
44
+ </el-form-item>
45
+ <el-form-item>
46
+ <el-checkbox v-model="formData.remember">记住我</el-checkbox>
47
+ <span class="forgot-password">忘记密码?</span>
48
+ </el-form-item>
49
+ <el-form-item>
50
+ <el-button
51
+ type="primary"
52
+ size="large"
53
+ class="login-btn"
54
+ @click="handleLogin"
55
+ :loading="loading"
56
+ >
57
+ {{ loading ? '登录中...' : '登录' }}
58
+ </el-button>
59
+ </el-form-item>
60
+ </el-form>
61
+ <div class="login-footer">
62
+ <span>© 2024 管理系统</span>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, reactive } from 'vue'
70
+ import { ElMessage } from 'element-plus'
71
+ import { useRouter } from 'vue-router'
72
+
73
+ const loginForm = ref()
74
+ const loading = ref(false)
75
+
76
+ const formData = reactive({
77
+ username: '',
78
+ password: '',
79
+ captcha: '',
80
+ remember: false,
81
+ })
82
+
83
+ const rules = {
84
+ username: [
85
+ { required: true, message: '请输入用户名', trigger: 'blur' },
86
+ {
87
+ min: 3,
88
+ max: 20,
89
+ message: '用户名长度在3到20个字符之间',
90
+ trigger: 'blur',
91
+ },
92
+ ],
93
+ password: [
94
+ { required: true, message: '请输入密码', trigger: 'blur' },
95
+ { min: 6, max: 30, message: '密码长度在6到30个字符之间', trigger: 'blur' },
96
+ ],
97
+ captcha: [
98
+ { required: true, message: '请输入验证码', trigger: 'blur' },
99
+ { len: 4, message: '验证码长度为4位', trigger: 'blur' },
100
+ ],
101
+ }
102
+
103
+ const captchaCode = ref('')
104
+
105
+ const generateCaptcha = () => {
106
+ const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
107
+ let result = ''
108
+ for (let i = 0; i < 4; i++) {
109
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
110
+ }
111
+ return result
112
+ }
113
+
114
+ const refreshCaptcha = () => {
115
+ captchaCode.value = generateCaptcha()
116
+ }
117
+
118
+ refreshCaptcha()
119
+ const router = useRouter()
120
+
121
+ const handleLogin = async () => {
122
+ if (!loginForm.value) return
123
+
124
+ loginForm.value.validate((valid: boolean) => {
125
+ if (valid) {
126
+ loading.value = true
127
+ setTimeout(() => {
128
+ if (
129
+ formData.captcha.toLowerCase() !== captchaCode.value.toLowerCase()
130
+ ) {
131
+ ElMessage.error('验证码错误')
132
+ refreshCaptcha()
133
+ loading.value = false
134
+ return
135
+ }
136
+ if (formData.username === 'admin' && formData.password === '123456') {
137
+ ElMessage.success('登录成功')
138
+ loading.value = false
139
+ router.push({ path: '/table' })
140
+ } else {
141
+ ElMessage.error('用户名或密码错误')
142
+ loading.value = false
143
+ }
144
+ }, 1000)
145
+ }
146
+ })
147
+ }
148
+
149
+ // 页面加载时检查是否记住用户名
150
+ const savedUsername = localStorage.getItem('username')
151
+ const remember = localStorage.getItem('remember')
152
+ if (savedUsername && remember === 'true') {
153
+ formData.username = savedUsername
154
+ formData.remember = true
155
+ }
156
+ </script>
157
+
158
+ <style lang="scss" scoped>
159
+ .login-container {
160
+ min-height: 100vh;
161
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ padding: 20px;
166
+ }
167
+
168
+ .login-box {
169
+ width: 100%;
170
+ max-width: 420px;
171
+ background: #fff;
172
+ border-radius: 16px;
173
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
174
+ padding: 40px;
175
+ }
176
+
177
+ .login-header {
178
+ text-align: center;
179
+ margin-bottom: 30px;
180
+
181
+ h2 {
182
+ margin: 0 0 10px 0;
183
+ font-size: 28px;
184
+ font-weight: 600;
185
+ color: #333;
186
+ }
187
+
188
+ p {
189
+ margin: 0;
190
+ color: #999;
191
+ font-size: 14px;
192
+ }
193
+ }
194
+
195
+ .login-form {
196
+ margin-bottom: 20px;
197
+ }
198
+
199
+ .captcha-wrapper {
200
+ display: flex;
201
+ gap: 12px;
202
+ }
203
+
204
+ .captcha-image {
205
+ flex-shrink: 0;
206
+ width: 100px;
207
+ height: 44px;
208
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
209
+ border-radius: 8px;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ font-size: 18px;
214
+ font-weight: bold;
215
+ color: #666;
216
+ letter-spacing: 2px;
217
+ cursor: pointer;
218
+ user-select: none;
219
+
220
+ &:hover {
221
+ opacity: 0.8;
222
+ }
223
+ }
224
+
225
+ .forgot-password {
226
+ float: right;
227
+ color: #667eea;
228
+ font-size: 14px;
229
+ cursor: pointer;
230
+
231
+ &:hover {
232
+ text-decoration: underline;
233
+ }
234
+ }
235
+
236
+ .login-btn {
237
+ width: 100%;
238
+ height: 48px;
239
+ font-size: 16px;
240
+ border-radius: 8px;
241
+ }
242
+
243
+ .login-footer {
244
+ text-align: center;
245
+ color: #999;
246
+ font-size: 12px;
247
+ }
248
+ </style>