create-librex 1.0.1 → 1.0.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-librex",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "LibreX — 积木式后台管理框架项目脚手架",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- <div class="login-card">
4
- <h1 class="login-title">LibreX</h1>
5
- <p class="login-desc">积木式后台管理系统</p>
6
- <form class="login-form" @submit.prevent="handleLogin">
7
- <div class="field">
8
- <input v-model="form.username" type="text" placeholder="用户名" autocomplete="username" />
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">&#9670;</span>
9
9
  </div>
10
- <div class="field">
11
- <input v-model="form.password" type="password" placeholder="密码" autocomplete="current-password" />
12
- </div>
13
- <Transition name="fade">
14
- <div v-if="error" class="login-error">{{ error }}</div>
15
- </Transition>
16
- <button type="submit" :disabled="loading">
17
- {{ loading ? '登录中...' : '登录' }}
18
- </button>
19
- </form>
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
- width: 100vw; height: 100vh;
60
- background: var(--color-bg-viewport);
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
- .login-card {
63
- width: 360px; padding: 40px;
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
- .login-title { margin: 0 0 4px; font-size: 28px; font-weight: 700; text-align: center; }
68
- .login-desc { margin: 0 0 32px; font-size: 14px; color: var(--color-text-tertiary); text-align: center; }
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); border-radius: 8px;
73
- background: var(--color-bg-input); color: var(--color-text-primary);
74
- font-size: 14px; outline: none;
75
- box-sizing: border-box;
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:focus { border-color: var(--color-accent); }
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
- padding: 10px 14px; border-radius: 8px;
80
- background: color-mix(in srgb, var(--color-accent-danger, #e53e3e) 10%, transparent);
81
- color: var(--color-accent-danger, #e53e3e); font-size: 13px; text-align: center;
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
- button[type="submit"] {
84
- padding: 12px; border: none; border-radius: 8px;
85
- background: var(--color-accent); color: #fff;
86
- font-size: 15px; font-weight: 600; cursor: pointer;
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
- button[type="submit"]:hover { opacity: 0.9; }
89
- button[type="submit"]:disabled { opacity: 0.6; cursor: not-allowed; }
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: calc(100vh - 44px); padding: 40px;
15
+ width: 100%; height: 100%;
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); font-size: 14px; cursor: pointer;
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 },
@@ -22,29 +22,38 @@ export const useUserStore = defineStore('user', () => {
22
22
  async function login(credentials: { username: string; password: string }): Promise<boolean> {
23
23
  loading.value = true
24
24
  try {
25
- // TODO: 替换为实际登录 API
25
+ // DEMO 账号 — 无后端时可用,替换为你的 API 后请删除
26
+ if (credentials.username === 'admin' && credentials.password === 'admin123') {
27
+ token.value = 'demo-token-librex'
28
+ currentUser.value = { id: 1, name: 'Admin', role: 'admin' }
29
+ localStorage.setItem(TOKEN_KEY, token.value)
30
+ localStorage.setItem(USER_KEY, JSON.stringify(currentUser.value))
31
+ loading.value = false
32
+ return true
33
+ }
34
+
35
+ // 实际 API 登录
26
36
  const res = await fetch('/api/admin/login', {
27
37
  method: 'POST',
28
38
  headers: { 'Content-Type': 'application/json' },
29
39
  body: JSON.stringify({ account: credentials.username, password: credentials.password }),
30
40
  })
31
41
  const data = await res.json()
32
- if (data.code !== 1 || !data.data) return false
42
+ if (data.code !== 1 || !data.data) { loading.value = false; return false }
33
43
 
34
44
  token.value = data.data.token || data.data.session_id
35
45
  localStorage.setItem(TOKEN_KEY, token.value || '')
36
46
 
37
- // 获取用户信息
38
47
  const user = await fetchUserInfo()
39
48
  if (user) {
40
49
  currentUser.value = user
41
50
  localStorage.setItem(USER_KEY, JSON.stringify(user))
42
51
  }
52
+ loading.value = false
43
53
  return true
44
54
  } catch {
45
- return false
46
- } finally {
47
55
  loading.value = false
56
+ return false
48
57
  }
49
58
  }
50
59