verce-vue-test 0.0.20 → 0.0.22

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.
@@ -0,0 +1,31 @@
1
+ # 登录页面设计
2
+
3
+ ## 概述
4
+
5
+ 新增一个左右分栏式登录页面,独立于现有管理系统布局。
6
+
7
+ ## 布局
8
+
9
+ - **左侧(60%)**:渐变背景(#2b5876 → #4e4376),展示系统名称「管理系统」和欢迎标语
10
+ - **右侧(40%)**:白色背景,居中登录表单卡片
11
+ - **响应式**:小屏幕下左侧隐藏,右侧全宽显示
12
+
13
+ ## 表单
14
+
15
+ - 用户名:`el-input`,带用户图标前缀,必填校验
16
+ - 密码:`el-input type="password"`,带锁图标前缀,必填校验
17
+ - 记住我:`el-checkbox`
18
+ - 登录按钮:`el-button type="primary"`,全宽,点击后 `ElMessage.success` 提示
19
+
20
+ ## 技术实现
21
+
22
+ - 新建文件:`src/views/LoginView.vue`
23
+ - 新增路由:`/login`,设置 `meta: { fullscreen: true }` 跳过 App.vue 的侧边栏布局
24
+ - 使用 Element Plus 组件:`el-form`、`el-input`、`el-button`、`el-checkbox`
25
+ - 不做实际后端请求,仅模拟登录成功
26
+
27
+ ## 不做
28
+
29
+ - 不加路由守卫
30
+ - 不对接后端接口
31
+ - 不做注册、忘记密码等功能
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verce-vue-test",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,406 @@
1
+ <script setup lang="ts">
2
+ import { Calendar, Check, Clock, User } from '@element-plus/icons-vue'
3
+
4
+ interface LoginInfo {
5
+ ip: string
6
+ time?: string
7
+ }
8
+
9
+ withDefaults(
10
+ defineProps<{
11
+ current: LoginInfo
12
+ previous: LoginInfo
13
+ }>(),
14
+ {
15
+ current: () => ({
16
+ ip: '188.253.12.45',
17
+ }),
18
+ previous: () => ({
19
+ ip: '188.253.12.42',
20
+ time: '2026-05-22 14:52:58',
21
+ }),
22
+ },
23
+ )
24
+ </script>
25
+
26
+ <template>
27
+ <section class="login-success-notice">
28
+ <header class="notice-header">
29
+ <div class="notice-mark" aria-hidden="true">
30
+ <el-icon><Check /></el-icon>
31
+ </div>
32
+
33
+ <div>
34
+ <h2 class="notice-title">登录成功</h2>
35
+ <p class="notice-subtitle">您的账号已安全登录</p>
36
+ </div>
37
+ </header>
38
+
39
+ <div class="notice-body">
40
+ <section class="login-section login-section--current" aria-labelledby="current-login-title">
41
+ <div class="section-heading">
42
+ <span class="section-icon section-icon--current" aria-hidden="true">
43
+ <el-icon><User /></el-icon>
44
+ </span>
45
+ <h3 id="current-login-title" class="section-title">当前登录信息</h3>
46
+ </div>
47
+
48
+ <div class="info-card info-card--single">
49
+ <div class="info-row">
50
+ <span class="row-icon row-icon--current">IP</span>
51
+ <span class="row-label">IP 地址</span>
52
+ <span class="row-divider" aria-hidden="true"></span>
53
+ <strong class="row-value">{{ current.ip }}</strong>
54
+ </div>
55
+ </div>
56
+ </section>
57
+
58
+ <div class="section-divider" aria-hidden="true"></div>
59
+
60
+ <section class="login-section login-section--previous" aria-labelledby="previous-login-title">
61
+ <div class="section-heading">
62
+ <span class="section-icon section-icon--previous" aria-hidden="true">
63
+ <el-icon><Clock /></el-icon>
64
+ </span>
65
+ <h3 id="previous-login-title" class="section-title">上次登录信息</h3>
66
+ </div>
67
+
68
+ <div class="info-card">
69
+ <div class="info-row">
70
+ <span class="row-icon row-icon--previous">IP</span>
71
+ <span class="row-label">IP 地址</span>
72
+ <span class="row-divider" aria-hidden="true"></span>
73
+ <strong class="row-value">{{ previous.ip }}</strong>
74
+ </div>
75
+
76
+ <div v-if="previous.time" class="card-divider" aria-hidden="true"></div>
77
+
78
+ <div v-if="previous.time" class="info-row">
79
+ <span class="row-icon row-icon--previous">
80
+ <el-icon><Calendar /></el-icon>
81
+ </span>
82
+ <span class="row-label">时间</span>
83
+ <span class="row-divider" aria-hidden="true"></span>
84
+ <strong class="row-value">{{ previous.time }}</strong>
85
+ </div>
86
+ </div>
87
+ </section>
88
+
89
+ </div>
90
+ </section>
91
+ </template>
92
+
93
+ <style scoped>
94
+ :global(.el-notification.login-success-notification) {
95
+ right: 24px;
96
+ left: auto;
97
+ width: min(380px, calc(100vw - 32px));
98
+ max-height: calc(100vh - 48px);
99
+ padding: 18px 18px 16px;
100
+ overflow-y: auto;
101
+ transform: none;
102
+ border: 1px solid rgba(225, 230, 238, 0.92);
103
+ border-radius: 16px;
104
+ background:
105
+ radial-gradient(circle at 18% 8%, rgba(123, 195, 80, 0.12), transparent 28%),
106
+ linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(252, 254, 255, 0.94));
107
+ box-shadow:
108
+ 0 18px 42px rgba(38, 50, 70, 0.16),
109
+ inset 0 1px 0 rgba(255, 255, 255, 0.82);
110
+ backdrop-filter: blur(14px);
111
+ scrollbar-width: thin;
112
+ scrollbar-color: #c8d2df transparent;
113
+ }
114
+
115
+ :global(.el-notification.login-success-notification::-webkit-scrollbar) {
116
+ width: 8px;
117
+ }
118
+
119
+ :global(.el-notification.login-success-notification::-webkit-scrollbar-thumb) {
120
+ border-radius: 999px;
121
+ background: #c8d2df;
122
+ }
123
+
124
+ :global(.el-notification.login-success-notification .el-notification__group) {
125
+ width: 100%;
126
+ margin: 0;
127
+ }
128
+
129
+ :global(.el-notification.login-success-notification .el-notification__content) {
130
+ margin: 0;
131
+ text-align: left;
132
+ }
133
+
134
+ :global(.el-notification.login-success-notification .el-notification__title) {
135
+ display: none;
136
+ }
137
+
138
+ :global(.el-notification.login-success-notification .el-notification__closeBtn) {
139
+ top: 20px;
140
+ right: 18px;
141
+ color: #5f6675;
142
+ font-size: 22px;
143
+ transition:
144
+ color 0.2s ease,
145
+ transform 0.2s ease;
146
+ }
147
+
148
+ :global(.el-notification.login-success-notification .el-notification__closeBtn:hover) {
149
+ color: #22262f;
150
+ transform: scale(1.04);
151
+ }
152
+
153
+ .login-success-notice {
154
+ color: #20232a;
155
+ font-family:
156
+ 'PingFang SC',
157
+ 'Microsoft YaHei',
158
+ 'Helvetica Neue',
159
+ sans-serif;
160
+ }
161
+
162
+ .notice-header {
163
+ display: flex;
164
+ align-items: flex-start;
165
+ gap: 14px;
166
+ }
167
+
168
+ .notice-mark {
169
+ display: flex;
170
+ width: 42px;
171
+ height: 42px;
172
+ align-items: center;
173
+ justify-content: center;
174
+ border-radius: 50%;
175
+ background:
176
+ radial-gradient(circle at 34% 28%, #a5e56d, transparent 31%),
177
+ linear-gradient(145deg, #74d54f 0%, #35a923 100%);
178
+ box-shadow:
179
+ 0 9px 18px rgba(64, 174, 39, 0.24),
180
+ inset 0 2px 2px rgba(255, 255, 255, 0.42);
181
+ color: #fff;
182
+ font-size: 29px;
183
+ }
184
+
185
+ .notice-title {
186
+ margin: 0 34px 4px 0;
187
+ color: #20232a;
188
+ font-size: 20px;
189
+ font-weight: 900;
190
+ line-height: 1.15;
191
+ letter-spacing: 0;
192
+ }
193
+
194
+ .notice-subtitle {
195
+ margin: 0;
196
+ color: #6f7582;
197
+ font-size: 13px;
198
+ font-weight: 500;
199
+ line-height: 1.25;
200
+ }
201
+
202
+ .notice-body {
203
+ padding: 20px 0 0;
204
+ }
205
+
206
+ .login-section {
207
+ position: relative;
208
+ }
209
+
210
+ .section-heading {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 10px;
214
+ margin-bottom: 11px;
215
+ }
216
+
217
+ .section-icon {
218
+ display: flex;
219
+ width: 30px;
220
+ height: 30px;
221
+ align-items: center;
222
+ justify-content: center;
223
+ border-radius: 50%;
224
+ font-size: 17px;
225
+ }
226
+
227
+ .section-icon--current {
228
+ background: #eef9ed;
229
+ color: #49aa32;
230
+ }
231
+
232
+ .section-icon--previous {
233
+ background: #edf4ff;
234
+ color: #397ee2;
235
+ }
236
+
237
+ .section-title {
238
+ position: relative;
239
+ margin: 0;
240
+ color: #20232a;
241
+ font-size: 15px;
242
+ font-weight: 900;
243
+ line-height: 1.2;
244
+ letter-spacing: 0;
245
+ }
246
+
247
+ .info-card {
248
+ width: 100%;
249
+ padding: 12px 14px;
250
+ border: 1px solid #e2e7ef;
251
+ border-radius: 12px;
252
+ background: rgba(255, 255, 255, 0.82);
253
+ box-shadow:
254
+ 0 8px 18px rgba(38, 50, 70, 0.06),
255
+ inset 0 1px 0 rgba(255, 255, 255, 0.9);
256
+ }
257
+
258
+ .info-row {
259
+ display: grid;
260
+ min-height: 34px;
261
+ grid-template-columns: 36px 66px 1px minmax(0, 1fr);
262
+ align-items: center;
263
+ column-gap: 10px;
264
+ }
265
+
266
+ .row-icon {
267
+ display: flex;
268
+ width: 36px;
269
+ height: 36px;
270
+ align-items: center;
271
+ justify-content: center;
272
+ border-radius: 50%;
273
+ font-size: 14px;
274
+ font-weight: 900;
275
+ line-height: 1;
276
+ }
277
+
278
+ .row-icon--current {
279
+ background: #eef9ed;
280
+ color: #49aa32;
281
+ }
282
+
283
+ .row-icon--previous {
284
+ background: #edf4ff;
285
+ color: #397ee2;
286
+ }
287
+
288
+ .row-label {
289
+ color: #6f7582;
290
+ font-size: 13px;
291
+ font-weight: 600;
292
+ line-height: 1.2;
293
+ }
294
+
295
+ .row-divider {
296
+ display: block;
297
+ width: 1px;
298
+ height: 22px;
299
+ background: #e0e5ed;
300
+ }
301
+
302
+ .row-value {
303
+ min-width: 0;
304
+ color: #20232a;
305
+ font-size: 14px;
306
+ font-weight: 600;
307
+ line-height: 1.2;
308
+ word-break: break-word;
309
+ }
310
+
311
+ .card-divider {
312
+ height: 1px;
313
+ margin: 10px 0 8px;
314
+ background: #e5e9f0;
315
+ }
316
+
317
+ .section-divider {
318
+ height: 1px;
319
+ margin: 18px 0 15px;
320
+ background-image: linear-gradient(to right, #dce3ed 50%, transparent 0);
321
+ background-position: top;
322
+ background-size: 12px 1px;
323
+ background-repeat: repeat-x;
324
+ }
325
+
326
+ @media (max-width: 900px) {
327
+ :global(.el-notification.login-success-notification) {
328
+ right: 16px;
329
+ width: min(380px, calc(100vw - 32px));
330
+ padding: 18px 16px 16px;
331
+ border-radius: 16px;
332
+ }
333
+
334
+ :global(.el-notification.login-success-notification .el-notification__closeBtn) {
335
+ top: 20px;
336
+ right: 16px;
337
+ font-size: 22px;
338
+ }
339
+
340
+ .notice-header {
341
+ gap: 12px;
342
+ }
343
+
344
+ .notice-mark {
345
+ width: 40px;
346
+ height: 40px;
347
+ font-size: 28px;
348
+ }
349
+
350
+ .notice-title {
351
+ margin-right: 36px;
352
+ font-size: 19px;
353
+ }
354
+
355
+ .notice-subtitle {
356
+ font-size: 13px;
357
+ }
358
+
359
+ .notice-body {
360
+ padding: 20px 0 0;
361
+ }
362
+
363
+ .section-heading {
364
+ gap: 10px;
365
+ }
366
+
367
+ .section-icon {
368
+ width: 30px;
369
+ height: 30px;
370
+ font-size: 17px;
371
+ }
372
+
373
+ .section-title {
374
+ font-size: 15px;
375
+ }
376
+
377
+ .info-card {
378
+ padding: 12px 13px;
379
+ }
380
+
381
+ .info-row {
382
+ min-height: 34px;
383
+ grid-template-columns: 36px 66px 1px minmax(0, 1fr);
384
+ column-gap: 10px;
385
+ }
386
+
387
+ .row-icon {
388
+ width: 36px;
389
+ height: 36px;
390
+ font-size: 14px;
391
+ }
392
+
393
+ .row-label {
394
+ font-size: 13px;
395
+ }
396
+
397
+ .row-value {
398
+ font-size: 14px;
399
+ }
400
+
401
+ .section-divider {
402
+ margin: 18px 0 15px;
403
+ }
404
+
405
+ }
406
+ </style>
@@ -4,6 +4,12 @@ import HomeView from '../views/HomeView.vue'
4
4
  const router = createRouter({
5
5
  history: createWebHistory(import.meta.env.BASE_URL),
6
6
  routes: [
7
+ {
8
+ path: '/login',
9
+ name: 'login',
10
+ meta: { fullscreen: true },
11
+ component: () => import('../views/LoginView.vue'),
12
+ },
7
13
  {
8
14
  path: '/',
9
15
  name: 'home',
@@ -0,0 +1,44 @@
1
+ import { h, ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import { useRouter } from 'vue-router'
4
+ import { ElNotification } from 'element-plus'
5
+ import LoginSuccessNotification from '@/components/LoginSuccessNotification.vue'
6
+
7
+ export const useLoginStore = defineStore('login', () => {
8
+ const router = useRouter()
9
+ const username = ref('')
10
+ const password = ref('')
11
+ const remember = ref(false)
12
+ const loading = ref(false)
13
+
14
+ async function login() {
15
+ loading.value = true
16
+ try {
17
+ // 模拟登录请求
18
+ await new Promise((resolve) => setTimeout(resolve, 500))
19
+ const loginInfo = {
20
+ current: {
21
+ ip: '188.253.12.45',
22
+ },
23
+ previous: {
24
+ ip: '188.253.12.42',
25
+ time: '2026-05-22 14:52:58',
26
+ },
27
+ }
28
+
29
+ ElNotification({
30
+ message: h(LoginSuccessNotification, loginInfo),
31
+ customClass: 'login-success-notification',
32
+ duration: 5000,
33
+ offset: 16,
34
+ position: 'bottom-right',
35
+ showClose: true,
36
+ })
37
+ router.push('/')
38
+ } finally {
39
+ loading.value = false
40
+ }
41
+ }
42
+
43
+ return { username, password, remember, loading, login }
44
+ })
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import { ref, reactive } from 'vue'
3
+ import type { FormInstance, FormRules } from 'element-plus'
4
+ import { User, Lock } from '@element-plus/icons-vue'
5
+ import { useLoginStore } from '@/stores/login'
6
+
7
+ const loginStore = useLoginStore()
8
+ const formRef = ref<FormInstance>()
9
+
10
+ const rules = reactive<FormRules>({
11
+ username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
12
+ password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
13
+ })
14
+
15
+ const handleLogin = async () => {
16
+ if (!formRef.value) return
17
+ await formRef.value.validate((valid) => {
18
+ if (valid) {
19
+ loginStore.login()
20
+ }
21
+ })
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div class="login-container">
27
+ <div class="login-left">
28
+ <div class="login-left-content">
29
+ <h1 class="login-title">管理系统</h1>
30
+ <p class="login-subtitle">高效、智能的企业管理平台</p>
31
+ </div>
32
+ </div>
33
+ <div class="login-right">
34
+ <div class="login-form-wrapper">
35
+ <h2 class="form-title">登录</h2>
36
+ <el-form ref="formRef" :model="loginStore" :rules="rules" size="large">
37
+ <el-form-item prop="username">
38
+ <el-input
39
+ v-model="loginStore.username"
40
+ placeholder="请输入用户名"
41
+ :prefix-icon="User"
42
+ />
43
+ </el-form-item>
44
+ <el-form-item prop="password">
45
+ <el-input
46
+ v-model="loginStore.password"
47
+ type="password"
48
+ placeholder="请输入密码"
49
+ :prefix-icon="Lock"
50
+ show-password
51
+ />
52
+ </el-form-item>
53
+ <el-form-item>
54
+ <el-checkbox v-model="loginStore.remember">记住我</el-checkbox>
55
+ </el-form-item>
56
+ <el-form-item>
57
+ <el-button type="primary" class="login-btn" :loading="loginStore.loading" @click="handleLogin">登录</el-button>
58
+ </el-form-item>
59
+ </el-form>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <style scoped>
66
+ .login-container {
67
+ display: flex;
68
+ height: 100vh;
69
+ width: 100vw;
70
+ }
71
+
72
+ .login-left {
73
+ flex: 6;
74
+ background: linear-gradient(135deg, #2b5876 0%, #4e4376 100%);
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ color: #fff;
79
+ }
80
+
81
+ .login-left-content {
82
+ text-align: center;
83
+ }
84
+
85
+ .login-title {
86
+ font-size: 42px;
87
+ font-weight: 700;
88
+ margin-bottom: 16px;
89
+ }
90
+
91
+ .login-subtitle {
92
+ font-size: 18px;
93
+ opacity: 0.85;
94
+ }
95
+
96
+ .login-right {
97
+ flex: 4;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ background: #fff;
102
+ }
103
+
104
+ .login-form-wrapper {
105
+ width: 360px;
106
+ }
107
+
108
+ .form-title {
109
+ font-size: 28px;
110
+ font-weight: 600;
111
+ margin-bottom: 32px;
112
+ color: #303133;
113
+ }
114
+
115
+ .login-btn {
116
+ width: 100%;
117
+ }
118
+
119
+ @media (max-width: 768px) {
120
+ .login-left {
121
+ display: none;
122
+ }
123
+ .login-right {
124
+ flex: 1;
125
+ }
126
+ }
127
+ </style>