create-librex 1.0.1

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,60 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import { computed } from 'vue'
4
+ import router, { allConfigs, setAuthGuard } from './router'
5
+ import App from './App.vue'
6
+ import 'librex/styles/breathing.css'
7
+ import { Librex, useConfig } from 'librex'
8
+ import { flushPendingBricks } from 'librex'
9
+ import { useUserStore } from './stores/user'
10
+ import { useAsyncRouteStore } from './stores/asyncRoute'
11
+
12
+ async function bootstrap() {
13
+ const app = createApp(App)
14
+ const pinia = createPinia()
15
+ app.use(pinia)
16
+
17
+ // Librex framework
18
+ app.use(Librex, {
19
+ provideUser: () => {
20
+ const userStore = useUserStore()
21
+ return {
22
+ displayName: computed(() => userStore.currentUser?.name || 'Not logged in'),
23
+ roleLabel: computed(() => userStore.roleLabel),
24
+ avatarUrl: computed(() => userStore.currentUser?.avatar || null),
25
+ logout: () => userStore.logout(),
26
+ changePassword: (_old: string, _new: string) => userStore.changePassword(_old, _new),
27
+ }
28
+ },
29
+ provideRoute: () => {
30
+ const asyncRouteStore = useAsyncRouteStore()
31
+ return { accessibleConfigs: computed(() => asyncRouteStore.accessibleConfigs) }
32
+ },
33
+ })
34
+
35
+ app.use(router)
36
+
37
+ // Register page configs
38
+ const configStore = useConfig()
39
+ flushPendingBricks()
40
+ for (const config of allConfigs) {
41
+ configStore.registerPageConfig(config)
42
+ }
43
+
44
+ // Auth guard
45
+ const userStore = useUserStore()
46
+ const asyncRouteStore = useAsyncRouteStore()
47
+ setAuthGuard({
48
+ isAuthenticated: () => userStore.isAuthenticated,
49
+ canAccess: (pageId: string) => asyncRouteStore.canAccess(pageId),
50
+ })
51
+
52
+ app.config.errorHandler = (err, _vm, info) => {
53
+ console.error('[LibreX Error]', err, info)
54
+ }
55
+
56
+ app.mount('#app')
57
+ userStore.tryRestoreSession()
58
+ }
59
+
60
+ bootstrap()
@@ -0,0 +1,92 @@
1
+ <template>
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" />
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>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { ref, reactive, onMounted } from 'vue'
26
+ import { useRouter, useRoute } from 'vue-router'
27
+ import { useUserStore } from '@/stores/user'
28
+
29
+ const router = useRouter()
30
+ const route = useRoute()
31
+ const userStore = useUserStore()
32
+
33
+ const form = reactive({ username: '', password: '' })
34
+ const loading = ref(false)
35
+ const error = ref('')
36
+
37
+ onMounted(() => {
38
+ if (userStore.isAuthenticated) {
39
+ router.replace((route.query.redirect as string) || '/')
40
+ }
41
+ })
42
+
43
+ async function handleLogin() {
44
+ error.value = ''
45
+ if (!form.username || !form.password) { error.value = '请输入用户名和密码'; return }
46
+ loading.value = true
47
+ try {
48
+ const ok = await userStore.login({ username: form.username, password: form.password })
49
+ if (ok) router.replace((route.query.redirect as string) || '/')
50
+ else error.value = '用户名或密码错误'
51
+ } catch { error.value = '登录失败' }
52
+ finally { loading.value = false }
53
+ }
54
+ </script>
55
+
56
+ <style scoped>
57
+ .login-page {
58
+ display: flex; align-items: center; justify-content: center;
59
+ width: 100vw; height: 100vh;
60
+ background: var(--color-bg-viewport);
61
+ }
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);
66
+ }
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; }
69
+ .login-form { display: flex; flex-direction: column; gap: 16px; }
70
+ .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;
76
+ }
77
+ .field input:focus { border-color: var(--color-accent); }
78
+ .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;
82
+ }
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;
87
+ }
88
+ button[type="submit"]:hover { opacity: 0.9; }
89
+ button[type="submit"]:disabled { opacity: 0.6; cursor: not-allowed; }
90
+ .fade-enter-active, .fade-leave-active { transition: all .2s ease; }
91
+ .fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(-4px); }
92
+ </style>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="not-found-page">
3
+ <div class="not-found-content">
4
+ <div class="not-found-code">404</div>
5
+ <h1>页面未找到</h1>
6
+ <p>你访问的页面不存在或已被移除</p>
7
+ <button class="nf-btn" @click="$router.push('/')">返回首页</button>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <style scoped>
13
+ .not-found-page {
14
+ display: flex; align-items: center; justify-content: center;
15
+ min-height: calc(100vh - 44px); padding: 40px;
16
+ }
17
+ .not-found-content { text-align: center; }
18
+ .not-found-code {
19
+ font-size: 96px; font-weight: 800; line-height: 1;
20
+ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-purple) 100%);
21
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
22
+ }
23
+ h1 { margin: 12px 0 8px; font-size: 24px; }
24
+ p { margin: 0 0 24px; color: var(--color-text-tertiary); }
25
+ .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
+ }
29
+ </style>
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Librex Router — 编译期路由自动发现 + 权限守卫
3
+ */
4
+
5
+ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
6
+ import type { PageConfig } from 'librex'
7
+ import NotFound from '@/pages/notFound/NotFound.vue'
8
+ import LoginPage from '@/pages/auth/LoginPage.vue'
9
+ import { brickRoutes } from 'virtual:librex-routes'
10
+
11
+ const allConfigs: PageConfig[] = brickRoutes
12
+ .map(r => r.meta?.pageConfig as PageConfig | undefined)
13
+ .filter(Boolean) as PageConfig[]
14
+
15
+ const routes: RouteRecordRaw[] = [
16
+ { path: '/', redirect: '/' },
17
+ { path: '/login', name: 'login', component: LoginPage },
18
+ ...brickRoutes,
19
+ { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
20
+ ]
21
+
22
+ const router = createRouter({ history: createWebHistory(), routes })
23
+
24
+ let _canAccess: ((pageId: string) => boolean) | null = null
25
+ let _isAuthenticated: (() => boolean) | null = null
26
+
27
+ export function setAuthGuard(guards: {
28
+ isAuthenticated: () => boolean
29
+ canAccess: (pageId: string) => boolean
30
+ }): void {
31
+ _isAuthenticated = guards.isAuthenticated
32
+ _canAccess = guards.canAccess
33
+ }
34
+
35
+ const PUBLIC_PATHS = new Set(['/login'])
36
+
37
+ router.beforeEach((to, _from) => {
38
+ if (PUBLIC_PATHS.has(to.path) || to.name === 'not-found' || to.name === 'login') {
39
+ return true
40
+ }
41
+ if (!_isAuthenticated || !_canAccess) {
42
+ console.error('[Router Guard] 守卫未初始化')
43
+ return { path: '/login' }
44
+ }
45
+ if (!_isAuthenticated()) {
46
+ return { path: '/login', query: { redirect: to.fullPath } }
47
+ }
48
+ const pageConfig = to.meta?.pageConfig as PageConfig | undefined
49
+ if (!pageConfig) return { name: 'not-found' }
50
+ if (!_canAccess(pageConfig.id)) return { name: 'not-found' }
51
+ return true
52
+ })
53
+
54
+ declare module 'vue-router' {
55
+ interface RouteMeta {
56
+ title?: string
57
+ icon?: string
58
+ pageConfig?: PageConfig
59
+ pageDir?: string
60
+ }
61
+ }
62
+
63
+ export default router
64
+ export { allConfigs }
@@ -0,0 +1,24 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed } from 'vue'
3
+ import { useConfig } from 'librex'
4
+ import { useUserStore } from './user'
5
+ import type { PageConfig } from 'librex'
6
+
7
+ export const useAsyncRouteStore = defineStore('async-route', () => {
8
+ const configStore = useConfig()
9
+ const userStore = useUserStore()
10
+
11
+ const accessibleConfigs = computed<PageConfig[]>(() => {
12
+ const all = Array.from(configStore.allPageConfigs.values())
13
+ return all.filter((cfg) => {
14
+ if (!cfg.meta?.permissions || cfg.meta.permissions.length === 0) return true
15
+ return cfg.meta.permissions.some(p => userStore.roleLabel === p)
16
+ })
17
+ })
18
+
19
+ function canAccess(pageId: string): boolean {
20
+ return accessibleConfigs.value.some(c => c.id === pageId)
21
+ }
22
+
23
+ return { accessibleConfigs, canAccess }
24
+ })
@@ -0,0 +1,100 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+
4
+ export interface UserInfo {
5
+ id: number
6
+ name: string
7
+ avatar?: string
8
+ role: 'admin' | 'editor' | 'viewer'
9
+ }
10
+
11
+ const TOKEN_KEY = 'librex-token'
12
+ const USER_KEY = 'librex-user'
13
+
14
+ export const useUserStore = defineStore('user', () => {
15
+ const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
16
+ const currentUser = ref<UserInfo | null>(loadUser())
17
+ const loading = ref(false)
18
+
19
+ const isAuthenticated = computed(() => !!token.value && !!currentUser.value)
20
+ const roleLabel = computed(() => currentUser.value?.role ?? '')
21
+
22
+ async function login(credentials: { username: string; password: string }): Promise<boolean> {
23
+ loading.value = true
24
+ try {
25
+ // TODO: 替换为实际登录 API
26
+ const res = await fetch('/api/admin/login', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ account: credentials.username, password: credentials.password }),
30
+ })
31
+ const data = await res.json()
32
+ if (data.code !== 1 || !data.data) return false
33
+
34
+ token.value = data.data.token || data.data.session_id
35
+ localStorage.setItem(TOKEN_KEY, token.value || '')
36
+
37
+ // 获取用户信息
38
+ const user = await fetchUserInfo()
39
+ if (user) {
40
+ currentUser.value = user
41
+ localStorage.setItem(USER_KEY, JSON.stringify(user))
42
+ }
43
+ return true
44
+ } catch {
45
+ return false
46
+ } finally {
47
+ loading.value = false
48
+ }
49
+ }
50
+
51
+ async function fetchUserInfo(): Promise<UserInfo | null> {
52
+ try {
53
+ const res = await fetch('/api/admin/info')
54
+ const data = await res.json()
55
+ if (data.code !== 1 || !data.data) return null
56
+ return {
57
+ id: data.data.id,
58
+ name: data.data.name,
59
+ role: data.data.type === 1 ? 'admin' : 'editor',
60
+ }
61
+ } catch {
62
+ return null
63
+ }
64
+ }
65
+
66
+ function logout() {
67
+ token.value = null
68
+ currentUser.value = null
69
+ localStorage.removeItem(TOKEN_KEY)
70
+ localStorage.removeItem(USER_KEY)
71
+ import('@/router').then(m => m.default.replace('/login'))
72
+ }
73
+
74
+ async function changePassword(_oldPass: string, _newPass: string): Promise<{ ok: boolean; message: string }> {
75
+ return { ok: false, message: '请实现修改密码 API' }
76
+ }
77
+
78
+ async function tryRestoreSession(): Promise<boolean> {
79
+ const savedToken = localStorage.getItem(TOKEN_KEY)
80
+ if (!savedToken) return false
81
+ token.value = savedToken
82
+ const user = await fetchUserInfo()
83
+ if (user) {
84
+ currentUser.value = user
85
+ return true
86
+ }
87
+ const saved = loadUser()
88
+ if (saved) { currentUser.value = saved; return true }
89
+ return false
90
+ }
91
+
92
+ return { token, currentUser, loading, isAuthenticated, roleLabel, login, logout, changePassword, tryRestoreSession }
93
+ })
94
+
95
+ function loadUser(): UserInfo | null {
96
+ try {
97
+ const raw = localStorage.getItem(USER_KEY)
98
+ return raw ? JSON.parse(raw) : null
99
+ } catch { return null }
100
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue'
5
+ const component: DefineComponent<object, object, unknown>
6
+ export default component
7
+ }
8
+
9
+ declare module '*.less' {
10
+ const content: Record<string, string>
11
+ export default content
12
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "env.d.ts"]
4
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "jsxImportSource": "vue",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "esModuleInterop": true,
12
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
13
+ "skipLibCheck": true,
14
+ "noEmit": true,
15
+ "paths": {
16
+ "@/*": ["./src/*"]
17
+ }
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "env.d.ts"],
20
+ "references": [
21
+ { "path": "./tsconfig.app.json" },
22
+ { "path": "./tsconfig.node.json" }
23
+ ]
24
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["vite.config.ts", "vite-plugin-librex.ts"]
4
+ }