create-librex 1.0.1 → 1.0.2
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/package.json
CHANGED
package/template/src/App.vue
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<script setup lang="ts">
|
|
13
13
|
import { computed } from 'vue'
|
|
14
14
|
import { useRoute } from 'vue-router'
|
|
15
|
-
import SystemShell from 'librex'
|
|
15
|
+
import { SystemShell } from 'librex'
|
|
16
16
|
|
|
17
17
|
const route = useRoute()
|
|
18
18
|
const isLoginPage = computed(() => route.path === '/login')
|
|
@@ -1,22 +1,60 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="login-page">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<div class="
|
|
8
|
-
<
|
|
3
|
+
<!-- Brand Panel -->
|
|
4
|
+
<div class="brand-panel">
|
|
5
|
+
<div class="brand-glow"></div>
|
|
6
|
+
<div class="brand-content">
|
|
7
|
+
<div class="brand-logo">
|
|
8
|
+
<span class="brand-icon">◆</span>
|
|
9
9
|
</div>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
<h1 class="brand-title">LibreX</h1>
|
|
11
|
+
<p class="brand-subtitle">Intent-aware Admin Framework</p>
|
|
12
|
+
<p class="brand-tagline">声明式页面配置,从意图到界面,一步直达</p>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Form Panel -->
|
|
17
|
+
<div class="form-panel">
|
|
18
|
+
<div class="form-card">
|
|
19
|
+
<h2 class="form-title">欢迎回来</h2>
|
|
20
|
+
<p class="form-desc">登录你的账号以继续</p>
|
|
21
|
+
|
|
22
|
+
<form class="login-form" @submit.prevent="handleLogin">
|
|
23
|
+
<div class="field">
|
|
24
|
+
<span class="field-icon">
|
|
25
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></svg>
|
|
26
|
+
</span>
|
|
27
|
+
<input
|
|
28
|
+
v-model="form.username"
|
|
29
|
+
type="text"
|
|
30
|
+
placeholder="用户名"
|
|
31
|
+
autocomplete="username"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="field">
|
|
35
|
+
<span class="field-icon">
|
|
36
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
37
|
+
</span>
|
|
38
|
+
<input
|
|
39
|
+
v-model="form.password"
|
|
40
|
+
type="password"
|
|
41
|
+
placeholder="密码"
|
|
42
|
+
autocomplete="current-password"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<Transition name="fade">
|
|
47
|
+
<div v-if="error" class="login-error">
|
|
48
|
+
<span class="error-icon">!</span> {{ error }}
|
|
49
|
+
</div>
|
|
50
|
+
</Transition>
|
|
51
|
+
|
|
52
|
+
<button type="submit" :disabled="loading" class="submit-btn">
|
|
53
|
+
<span v-if="loading" class="spinner"></span>
|
|
54
|
+
<span v-else>登 录</span>
|
|
55
|
+
</button>
|
|
56
|
+
</form>
|
|
57
|
+
</div>
|
|
20
58
|
</div>
|
|
21
59
|
</div>
|
|
22
60
|
</template>
|
|
@@ -48,45 +86,127 @@ async function handleLogin() {
|
|
|
48
86
|
const ok = await userStore.login({ username: form.username, password: form.password })
|
|
49
87
|
if (ok) router.replace((route.query.redirect as string) || '/')
|
|
50
88
|
else error.value = '用户名或密码错误'
|
|
51
|
-
} catch { error.value = '
|
|
89
|
+
} catch { error.value = '登录失败,请重试' }
|
|
52
90
|
finally { loading.value = false }
|
|
53
91
|
}
|
|
54
92
|
</script>
|
|
55
93
|
|
|
56
94
|
<style scoped>
|
|
95
|
+
/* ── Layout ── */
|
|
57
96
|
.login-page {
|
|
97
|
+
display: flex; width: 100vw; height: 100vh;
|
|
98
|
+
background: var(--color-bg-viewport, #f0f2f5);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── Brand Panel ── */
|
|
102
|
+
.brand-panel {
|
|
103
|
+
position: relative; flex: 0 0 440px;
|
|
104
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
58
105
|
display: flex; align-items: center; justify-content: center;
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
}
|
|
108
|
+
.brand-glow {
|
|
109
|
+
position: absolute; width: 400px; height: 400px;
|
|
110
|
+
border-radius: 50%; top: 50%; left: 50%;
|
|
111
|
+
transform: translate(-50%, -50%);
|
|
112
|
+
background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 70%);
|
|
113
|
+
animation: brandPulse 4s ease-in-out infinite;
|
|
114
|
+
}
|
|
115
|
+
@keyframes brandPulse {
|
|
116
|
+
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
|
117
|
+
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 0.8; }
|
|
118
|
+
}
|
|
119
|
+
.brand-content {
|
|
120
|
+
position: relative; z-index: 1; text-align: center; color: #fff;
|
|
121
|
+
}
|
|
122
|
+
.brand-logo {
|
|
123
|
+
width: 72px; height: 72px; border-radius: 18px;
|
|
124
|
+
background: rgba(255,255,255,0.15);
|
|
125
|
+
-webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
|
|
126
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
127
|
+
display: flex; align-items: center; justify-content: center;
|
|
128
|
+
margin: 0 auto 24px;
|
|
129
|
+
}
|
|
130
|
+
.brand-icon { font-size: 32px; }
|
|
131
|
+
.brand-title { margin: 0; font-size: 36px; font-weight: 800; letter-spacing: -0.5px; }
|
|
132
|
+
.brand-subtitle { margin: 8px 0 0; font-size: 15px; opacity: 0.85; font-weight: 500; }
|
|
133
|
+
.brand-tagline { margin: 20px 0 0; font-size: 13px; opacity: 0.6; }
|
|
134
|
+
|
|
135
|
+
/* ── Form Panel ── */
|
|
136
|
+
.form-panel {
|
|
137
|
+
flex: 1; display: flex; align-items: center; justify-content: center;
|
|
138
|
+
background: var(--color-bg-viewport, #f0f2f5);
|
|
61
139
|
}
|
|
62
|
-
.
|
|
63
|
-
width:
|
|
64
|
-
background: var(--color-bg-surface); border-radius: 16px;
|
|
65
|
-
box-shadow: 0 4px 24px rgba(0,0,0,0.06);
|
|
140
|
+
.form-card {
|
|
141
|
+
width: 380px; padding: 48px 40px;
|
|
66
142
|
}
|
|
67
|
-
.
|
|
68
|
-
.
|
|
143
|
+
.form-title { margin: 0; font-size: 26px; font-weight: 700; color: var(--color-text-primary, #333); }
|
|
144
|
+
.form-desc { margin: 6px 0 36px; font-size: 14px; color: var(--color-text-tertiary, #888); }
|
|
145
|
+
|
|
146
|
+
/* ── Form Fields ── */
|
|
69
147
|
.login-form { display: flex; flex-direction: column; gap: 16px; }
|
|
148
|
+
.field {
|
|
149
|
+
position: relative; display: flex; align-items: center;
|
|
150
|
+
}
|
|
151
|
+
.field-icon {
|
|
152
|
+
position: absolute; left: 14px; z-index: 1;
|
|
153
|
+
color: var(--color-text-tertiary, #888); display: flex;
|
|
154
|
+
}
|
|
70
155
|
.field input {
|
|
71
|
-
width: 100%; padding: 12px 14px;
|
|
72
|
-
border: 1px solid var(--color-border);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
156
|
+
width: 100%; padding: 12px 14px 12px 42px;
|
|
157
|
+
border: 1px solid var(--color-border, rgba(0,0,0,0.08));
|
|
158
|
+
border-radius: 10px; outline: none; box-sizing: border-box;
|
|
159
|
+
background: var(--color-bg-input, #fff); color: var(--color-text-primary, #333);
|
|
160
|
+
font-size: 15px; transition: border-color .2s, box-shadow .2s;
|
|
76
161
|
}
|
|
77
|
-
.field input
|
|
162
|
+
.field input::placeholder { color: var(--color-text-disabled, #bbb); }
|
|
163
|
+
.field input:focus {
|
|
164
|
+
border-color: var(--color-accent, #667eea);
|
|
165
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── Error ── */
|
|
78
169
|
.login-error {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
170
|
+
display: flex; align-items: center; gap: 8px;
|
|
171
|
+
padding: 10px 14px; border-radius: 10px;
|
|
172
|
+
background: color-mix(in srgb, var(--color-accent-danger, #ef4444) 10%, transparent);
|
|
173
|
+
color: var(--color-accent-danger, #ef4444); font-size: 13px;
|
|
174
|
+
}
|
|
175
|
+
.error-icon {
|
|
176
|
+
width: 18px; height: 18px; border-radius: 50%;
|
|
177
|
+
background: var(--color-accent-danger, #ef4444); color: #fff;
|
|
178
|
+
display: flex; align-items: center; justify-content: center;
|
|
179
|
+
font-size: 12px; font-weight: 700; flex-shrink: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ── Submit ── */
|
|
183
|
+
.submit-btn {
|
|
184
|
+
margin-top: 8px; padding: 14px;
|
|
185
|
+
border: none; border-radius: 10px;
|
|
186
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
187
|
+
color: #fff; font-size: 16px; font-weight: 600; cursor: pointer;
|
|
188
|
+
transition: opacity .2s, transform .1s;
|
|
82
189
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
190
|
+
.submit-btn:hover { opacity: 0.92; }
|
|
191
|
+
.submit-btn:active { transform: scale(0.985); }
|
|
192
|
+
.submit-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
193
|
+
|
|
194
|
+
/* ── Spinner ── */
|
|
195
|
+
.spinner {
|
|
196
|
+
display: inline-block; width: 20px; height: 20px;
|
|
197
|
+
border: 2px solid rgba(255,255,255,0.3);
|
|
198
|
+
border-top-color: #fff; border-radius: 50%;
|
|
199
|
+
animation: spin 0.6s linear infinite;
|
|
87
200
|
}
|
|
88
|
-
|
|
89
|
-
|
|
201
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
202
|
+
|
|
203
|
+
/* ── Transitions ── */
|
|
90
204
|
.fade-enter-active, .fade-leave-active { transition: all .2s ease; }
|
|
91
205
|
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(-4px); }
|
|
206
|
+
|
|
207
|
+
/* ── Mobile ── */
|
|
208
|
+
@media (max-width: 768px) {
|
|
209
|
+
.brand-panel { display: none; }
|
|
210
|
+
.form-card { width: 100%; padding: 40px 28px; }
|
|
211
|
+
}
|
|
92
212
|
</style>
|
|
@@ -12,18 +12,22 @@
|
|
|
12
12
|
<style scoped>
|
|
13
13
|
.not-found-page {
|
|
14
14
|
display: flex; align-items: center; justify-content: center;
|
|
15
|
-
min-height:
|
|
15
|
+
min-height: 100vh; padding: 40px;
|
|
16
|
+
background: var(--color-bg-viewport, #f0f2f5);
|
|
17
|
+
color: var(--color-text-primary, #333);
|
|
16
18
|
}
|
|
17
19
|
.not-found-content { text-align: center; }
|
|
18
20
|
.not-found-code {
|
|
19
21
|
font-size: 96px; font-weight: 800; line-height: 1;
|
|
20
|
-
background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-purple) 100%);
|
|
22
|
+
background: linear-gradient(135deg, var(--color-accent, #1890ff) 0%, var(--color-accent-purple, #7c3aed) 100%);
|
|
21
23
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
22
24
|
}
|
|
23
|
-
h1 { margin: 12px 0 8px; font-size: 24px; }
|
|
24
|
-
p { margin: 0 0 24px; color: var(--color-text-tertiary); }
|
|
25
|
+
h1 { margin: 12px 0 8px; font-size: 24px; color: var(--color-text-primary, #333); }
|
|
26
|
+
p { margin: 0 0 24px; color: var(--color-text-tertiary, #888); }
|
|
25
27
|
.nf-btn {
|
|
26
|
-
padding: 10px 24px; border-radius: 10px; border: 1px solid var(--color-border);
|
|
27
|
-
background: var(--color-bg-surface);
|
|
28
|
+
padding: 10px 24px; border-radius: 10px; border: 1px solid var(--color-border, rgba(0,0,0,0.06));
|
|
29
|
+
background: var(--color-bg-surface, #fff); color: var(--color-text-primary, #333);
|
|
30
|
+
font-size: 14px; cursor: pointer; transition: all .15s;
|
|
28
31
|
}
|
|
32
|
+
.nf-btn:hover { border-color: var(--color-accent, #1890ff); color: var(--color-accent, #1890ff); }
|
|
29
33
|
</style>
|
|
@@ -13,7 +13,6 @@ const allConfigs: PageConfig[] = brickRoutes
|
|
|
13
13
|
.filter(Boolean) as PageConfig[]
|
|
14
14
|
|
|
15
15
|
const routes: RouteRecordRaw[] = [
|
|
16
|
-
{ path: '/', redirect: '/' },
|
|
17
16
|
{ path: '/login', name: 'login', component: LoginPage },
|
|
18
17
|
...brickRoutes,
|
|
19
18
|
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
|