xto-fronted 0.1.0 → 0.1.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.
Files changed (54) hide show
  1. package/.env.development +4 -0
  2. package/.env.production +4 -0
  3. package/index.html +13 -0
  4. package/package.json +18 -10
  5. package/public/vite.svg +10 -0
  6. package/src/App.vue +20 -0
  7. package/src/api/auth.ts +26 -0
  8. package/src/api/system.ts +65 -0
  9. package/src/api/user.ts +46 -0
  10. package/src/assets/styles/_dark.scss +407 -0
  11. package/src/assets/styles/_reset.scss +126 -0
  12. package/src/assets/styles/_root.scss +140 -0
  13. package/src/assets/styles/_transition.scss +119 -0
  14. package/src/assets/styles/_variables.scss +45 -0
  15. package/src/assets/styles/index.scss +187 -0
  16. package/src/components/Layout/Footer.vue +17 -0
  17. package/src/components/Layout/Header.vue +390 -0
  18. package/src/components/Layout/Sidebar.vue +297 -0
  19. package/src/components/Layout/Tabs.vue +134 -0
  20. package/src/components/Layout/index.vue +62 -0
  21. package/src/composables/useAuth.ts +45 -0
  22. package/src/composables/useForm.ts +79 -0
  23. package/src/composables/useTable.ts +97 -0
  24. package/src/directives/permission.ts +38 -0
  25. package/src/enums/index.ts +63 -0
  26. package/src/env.d.ts +17 -0
  27. package/src/index.ts +39 -0
  28. package/src/main.ts +34 -0
  29. package/src/router/dynamicRoutes.ts +163 -0
  30. package/src/router/index.ts +81 -0
  31. package/src/router/staticRoutes.ts +43 -0
  32. package/src/stores/app.ts +145 -0
  33. package/src/stores/auth.ts +32 -0
  34. package/src/stores/index.ts +15 -0
  35. package/src/stores/menu.ts +80 -0
  36. package/src/stores/user.ts +73 -0
  37. package/src/types/api.d.ts +84 -0
  38. package/src/types/global.d.ts +45 -0
  39. package/src/types/router.d.ts +48 -0
  40. package/src/types/xto.d.ts +149 -0
  41. package/src/utils/auth.ts +62 -0
  42. package/src/utils/permission.ts +42 -0
  43. package/src/utils/request.ts +126 -0
  44. package/src/utils/storage.ts +63 -0
  45. package/src/views/dashboard/index.vue +284 -0
  46. package/src/views/error/403.vue +57 -0
  47. package/src/views/error/404.vue +57 -0
  48. package/src/views/login/index.vue +248 -0
  49. package/src/views/system/menu/index.vue +381 -0
  50. package/src/views/system/role/index.vue +304 -0
  51. package/src/views/system/user/index.vue +327 -0
  52. package/tsconfig.json +26 -0
  53. package/tsconfig.node.json +11 -0
  54. package/vite.config.ts +139 -0
@@ -0,0 +1,327 @@
1
+ <script setup lang="ts">
2
+ import { ref, reactive, computed, onMounted } from 'vue'
3
+ import { Form, FormItem, Input, Select, Switch } from '@xto/form'
4
+ import { Tag, Card, Pagination } from '@xto/data'
5
+ import { Modal, Message, Popconfirm } from '@xto/feedback'
6
+ import { Space, Button } from '@xto/base'
7
+ import { Status, StatusOptions } from '@/enums'
8
+
9
+ // 用户数据类型
10
+ interface User {
11
+ id: number
12
+ username: string
13
+ nickname: string
14
+ email: string
15
+ phone: string
16
+ status: Status
17
+ roles: string[]
18
+ createTime: string
19
+ }
20
+
21
+ // Mock 数据
22
+ const mockUsers: User[] = [
23
+ { id: 1, username: 'admin', nickname: '管理员', email: 'admin@example.com', phone: '13800138001', status: Status.ENABLED, roles: ['admin'], createTime: '2024-01-01 10:00:00' },
24
+ { id: 2, username: 'zhangsan', nickname: '张三', email: 'zhangsan@example.com', phone: '13800138002', status: Status.ENABLED, roles: ['editor'], createTime: '2024-01-02 10:00:00' },
25
+ { id: 3, username: 'lisi', nickname: '李四', email: 'lisi@example.com', phone: '13800138003', status: Status.DISABLED, roles: ['viewer'], createTime: '2024-01-03 10:00:00' },
26
+ { id: 4, username: 'wangwu', nickname: '王五', email: 'wangwu@example.com', phone: '13800138004', status: Status.ENABLED, roles: ['editor'], createTime: '2024-01-04 10:00:00' },
27
+ { id: 5, username: 'zhaoliu', nickname: '赵六', email: 'zhaoliu@example.com', phone: '13800138005', status: Status.ENABLED, roles: ['viewer'], createTime: '2024-01-05 10:00:00' }
28
+ ]
29
+
30
+ const loading = ref(false)
31
+ const userList = ref<User[]>([])
32
+ const total = ref(0)
33
+ const currentPage = ref(1)
34
+ const pageSize = ref(10)
35
+
36
+ // 搜索条件
37
+ const searchForm = reactive({
38
+ keyword: '',
39
+ status: undefined as Status | undefined
40
+ })
41
+
42
+ // 弹窗
43
+ const modalVisible = ref(false)
44
+ const modalTitle = computed(() => formData.id ? '编辑用户' : '新增用户')
45
+ const formData = reactive({
46
+ id: 0,
47
+ username: '',
48
+ nickname: '',
49
+ email: '',
50
+ phone: '',
51
+ status: Status.ENABLED,
52
+ roles: [] as string[]
53
+ })
54
+
55
+ const rules: Record<string, any[]> = {
56
+ username: [
57
+ { required: true, message: '请输入用户名', trigger: 'blur' }
58
+ ],
59
+ nickname: [
60
+ { required: true, message: '请输入昵称', trigger: 'blur' }
61
+ ],
62
+ email: [
63
+ { required: true, message: '请输入邮箱', trigger: 'blur' },
64
+ { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
65
+ ]
66
+ }
67
+
68
+ const formRef = ref()
69
+
70
+ // 获取用户列表
71
+ const getUserList = () => {
72
+ loading.value = true
73
+ setTimeout(() => {
74
+ let list = [...mockUsers]
75
+
76
+ // 搜索过滤
77
+ if (searchForm.keyword) {
78
+ list = list.filter(user =>
79
+ user.username.includes(searchForm.keyword) ||
80
+ user.nickname.includes(searchForm.keyword)
81
+ )
82
+ }
83
+ if (searchForm.status !== undefined) {
84
+ list = list.filter(user => user.status === searchForm.status)
85
+ }
86
+
87
+ total.value = list.length
88
+ userList.value = list.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
89
+ loading.value = false
90
+ }, 300)
91
+ }
92
+
93
+ // 搜索
94
+ const handleSearch = () => {
95
+ currentPage.value = 1
96
+ getUserList()
97
+ }
98
+
99
+ // 重置
100
+ const handleReset = () => {
101
+ searchForm.keyword = ''
102
+ searchForm.status = undefined
103
+ currentPage.value = 1
104
+ getUserList()
105
+ }
106
+
107
+ // 新增
108
+ const handleAdd = () => {
109
+ Object.assign(formData, {
110
+ id: 0,
111
+ username: '',
112
+ nickname: '',
113
+ email: '',
114
+ phone: '',
115
+ status: Status.ENABLED,
116
+ roles: []
117
+ })
118
+ modalVisible.value = true
119
+ }
120
+
121
+ // 编辑
122
+ const handleEdit = (row: User) => {
123
+ Object.assign(formData, row)
124
+ modalVisible.value = true
125
+ }
126
+
127
+ // 删除
128
+ const handleDelete = (_id: number) => {
129
+ Message.success('删除成功')
130
+ getUserList()
131
+ }
132
+
133
+ // 提交
134
+ const handleSubmit = async () => {
135
+ try {
136
+ await formRef.value?.validate()
137
+ Message.success(formData.id ? '编辑成功' : '新增成功')
138
+ modalVisible.value = false
139
+ getUserList()
140
+ } catch (error) {
141
+ console.error(error)
142
+ }
143
+ }
144
+
145
+ // 状态切换
146
+ const handleStatusChange = (row: User) => {
147
+ Message.success(`已${row.status === Status.ENABLED ? '启用' : '禁用'}用户 ${row.username}`)
148
+ }
149
+
150
+ onMounted(() => {
151
+ getUserList()
152
+ })
153
+ </script>
154
+
155
+ <template>
156
+ <div class="user-page">
157
+ <!-- 搜索栏 -->
158
+ <Card class="search-card">
159
+ <Form :model="searchForm" inline>
160
+ <FormItem label="关键词">
161
+ <Input
162
+ v-model="searchForm.keyword"
163
+ placeholder="用户名/昵称"
164
+ clearable
165
+ @keyup.enter="handleSearch"
166
+ />
167
+ </FormItem>
168
+ <FormItem label="状态">
169
+ <Select
170
+ v-model="searchForm.status"
171
+ :options="StatusOptions"
172
+ placeholder="请选择"
173
+ clearable
174
+ />
175
+ </FormItem>
176
+ <FormItem>
177
+ <Space>
178
+ <Button type="primary" @click="handleSearch">搜索</Button>
179
+ <Button @click="handleReset">重置</Button>
180
+ </Space>
181
+ </FormItem>
182
+ </Form>
183
+ </Card>
184
+
185
+ <!-- 表格 -->
186
+ <Card class="table-card">
187
+ <!-- 工具栏 -->
188
+ <div class="toolbar">
189
+ <Button type="primary" @click="handleAdd">新增用户</Button>
190
+ </div>
191
+
192
+ <!-- 表格 -->
193
+ <table class="data-table">
194
+ <thead>
195
+ <tr>
196
+ <th>ID</th>
197
+ <th>用户名</th>
198
+ <th>昵称</th>
199
+ <th>邮箱</th>
200
+ <th>手机号</th>
201
+ <th>角色</th>
202
+ <th>状态</th>
203
+ <th>创建时间</th>
204
+ <th>操作</th>
205
+ </tr>
206
+ </thead>
207
+ <tbody>
208
+ <tr v-if="loading">
209
+ <td colspan="9" class="loading-cell">加载中...</td>
210
+ </tr>
211
+ <tr v-else-if="userList.length === 0">
212
+ <td colspan="9" class="empty-cell">暂无数据</td>
213
+ </tr>
214
+ <tr v-else v-for="row in userList" :key="row.id">
215
+ <td>{{ row.id }}</td>
216
+ <td>{{ row.username }}</td>
217
+ <td>{{ row.nickname }}</td>
218
+ <td>{{ row.email }}</td>
219
+ <td>{{ row.phone }}</td>
220
+ <td>
221
+ <Tag v-for="role in row.roles" :key="role" size="small">{{ role }}</Tag>
222
+ </td>
223
+ <td>
224
+ <Switch
225
+ :model-value="row.status === Status.ENABLED"
226
+ @update:model-value="row.status = $event ? Status.ENABLED : Status.DISABLED; handleStatusChange(row)"
227
+ />
228
+ </td>
229
+ <td>{{ row.createTime }}</td>
230
+ <td>
231
+ <Space>
232
+ <Button type="primary" link size="small" @click="handleEdit(row)">编辑</Button>
233
+ <Popconfirm title="确定删除该用户吗?" @confirm="handleDelete(row.id)">
234
+ <Button type="danger" link size="small">删除</Button>
235
+ </Popconfirm>
236
+ </Space>
237
+ </td>
238
+ </tr>
239
+ </tbody>
240
+ </table>
241
+
242
+ <!-- 分页 -->
243
+ <div class="pagination-wrapper">
244
+ <Pagination
245
+ v-model:current-page="currentPage"
246
+ v-model:page-size="pageSize"
247
+ :total="total"
248
+ :page-sizes="[10, 20, 50, 100]"
249
+ layout="total, sizes, prev, pager, next"
250
+ @current-change="getUserList"
251
+ @size-change="getUserList"
252
+ />
253
+ </div>
254
+ </Card>
255
+
256
+ <!-- 编辑弹窗 -->
257
+ <Modal v-model="modalVisible" :title="modalTitle" width="500px">
258
+ <Form ref="formRef" :model="formData" :rules="rules" label-width="80px">
259
+ <FormItem label="用户名" prop="username">
260
+ <Input v-model="formData.username" placeholder="请输入用户名" />
261
+ </FormItem>
262
+ <FormItem label="昵称" prop="nickname">
263
+ <Input v-model="formData.nickname" placeholder="请输入昵称" />
264
+ </FormItem>
265
+ <FormItem label="邮箱" prop="email">
266
+ <Input v-model="formData.email" placeholder="请输入邮箱" />
267
+ </FormItem>
268
+ <FormItem label="手机号" prop="phone">
269
+ <Input v-model="formData.phone" placeholder="请输入手机号" />
270
+ </FormItem>
271
+ <FormItem label="状态">
272
+ <Switch v-model="formData.status" :active-value="Status.ENABLED" :inactive-value="Status.DISABLED" />
273
+ </FormItem>
274
+ </Form>
275
+ <template #footer>
276
+ <Space>
277
+ <Button @click="modalVisible = false">取消</Button>
278
+ <Button type="primary" @click="handleSubmit">确定</Button>
279
+ </Space>
280
+ </template>
281
+ </Modal>
282
+ </div>
283
+ </template>
284
+
285
+ <style lang="scss" scoped>
286
+ .user-page {
287
+ padding: 20px;
288
+
289
+ .search-card {
290
+ margin-bottom: 20px;
291
+ }
292
+
293
+ .toolbar {
294
+ margin-bottom: 15px;
295
+ }
296
+ }
297
+
298
+ .data-table {
299
+ width: 100%;
300
+ border-collapse: collapse;
301
+
302
+ th, td {
303
+ padding: 12px;
304
+ text-align: left;
305
+ border-bottom: 1px solid var(--color-border-lighter);
306
+ }
307
+
308
+ th {
309
+ font-weight: 500;
310
+ color: var(--color-text-regular);
311
+ background-color: var(--color-fill-light);
312
+ }
313
+
314
+ .loading-cell,
315
+ .empty-cell {
316
+ text-align: center;
317
+ color: var(--color-text-secondary);
318
+ padding: 40px;
319
+ }
320
+ }
321
+
322
+ .pagination-wrapper {
323
+ display: flex;
324
+ justify-content: flex-end;
325
+ margin-top: 20px;
326
+ }
327
+ </style>
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "preserve",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@/*": ["src/*"]
21
+ },
22
+ "types": ["vite/client", "node"]
23
+ },
24
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
25
+ "references": [{ "path": "./tsconfig.node.json" }]
26
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { defineConfig, loadEnv } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import dts from 'vite-plugin-dts'
4
+ import { resolve } from 'path'
5
+
6
+ export default defineConfig(({ mode }) => {
7
+ const env = loadEnv(mode, process.cwd())
8
+ const isLib = mode === 'lib'
9
+
10
+ return {
11
+ plugins: [
12
+ vue(),
13
+ ...(isLib ? [dts({ insertTypesEntry: true, outDir: 'dist' })] : [])
14
+ ],
15
+ resolve: {
16
+ alias: {
17
+ '@': resolve(__dirname, 'src'),
18
+ '@xto/core': resolve(__dirname, 'node_modules/@xto/core/es/index/index.mjs'),
19
+ '@xto/core/theme': resolve(__dirname, 'node_modules/@xto/core/es/theme/index.mjs'),
20
+ '@xto/core/hooks': resolve(__dirname, 'node_modules/@xto/core/es/hooks/index.mjs'),
21
+ '@xto/core/utils': resolve(__dirname, 'node_modules/@xto/core/es/utils/index.mjs'),
22
+ '@xto/base/es/style.css': resolve(__dirname, 'node_modules/@xto/base/es/style.css'),
23
+ '@xto/form/es/style.css': resolve(__dirname, 'node_modules/@xto/form/es/style.css'),
24
+ '@xto/data/es/style.css': resolve(__dirname, 'node_modules/@xto/data/es/style.css'),
25
+ '@xto/feedback/es/style.css': resolve(__dirname, 'node_modules/@xto/feedback/es/style.css'),
26
+ '@xto/navigation/es/style.css': resolve(__dirname, 'node_modules/@xto/navigation/es/style.css'),
27
+ '@xto/layout/es/style.css': resolve(__dirname, 'node_modules/@xto/layout/es/style.css'),
28
+ '@xto/business/es/style.css': resolve(__dirname, 'node_modules/@xto/business/es/style.css'),
29
+ }
30
+ },
31
+ // 开发模式优化
32
+ optimizeDeps: {
33
+ include: [
34
+ 'vue',
35
+ 'vue-router',
36
+ 'pinia',
37
+ 'axios'
38
+ ],
39
+ exclude: [
40
+ '@xto/core',
41
+ '@xto/base',
42
+ '@xto/form',
43
+ '@xto/data',
44
+ '@xto/feedback',
45
+ '@xto/navigation',
46
+ '@xto/layout',
47
+ '@xto/business'
48
+ ]
49
+ },
50
+ server: {
51
+ host: '0.0.0.0',
52
+ port: 3000,
53
+ open: true,
54
+ proxy: {
55
+ '/api': {
56
+ target: env.VITE_API_BASE_URL || 'http://localhost:8080',
57
+ changeOrigin: true,
58
+ rewrite: (path) => path.replace(/^\/api/, '')
59
+ }
60
+ }
61
+ },
62
+ build: {
63
+ outDir: 'dist',
64
+ sourcemap: false,
65
+ chunkSizeWarningLimit: 1500,
66
+ // 库模式配置
67
+ lib: isLib ? {
68
+ entry: resolve(__dirname, 'src/index.ts'),
69
+ name: 'XtoFronted',
70
+ fileName: (format: string) => `index.${format === 'es' ? 'es.js' : 'umd.js'}`
71
+ } : undefined,
72
+ rollupOptions: {
73
+ // 库模式下排除外部依赖
74
+ external: isLib ? [
75
+ 'vue',
76
+ 'vue-router',
77
+ 'pinia',
78
+ 'axios',
79
+ /^@xto\//
80
+ ] : undefined,
81
+ output: isLib ? {
82
+ globals: {
83
+ vue: 'Vue',
84
+ 'vue-router': 'VueRouter',
85
+ pinia: 'Pinia',
86
+ axios: 'axios'
87
+ },
88
+ exports: 'named'
89
+ } : {
90
+ manualChunks(id) {
91
+ // Vue 全家桶单独打包
92
+ if (id.includes('node_modules/vue/') ||
93
+ id.includes('node_modules/@vue/') ||
94
+ id.includes('node_modules/vue-router/') ||
95
+ id.includes('node_modules/pinia/')) {
96
+ return 'vue-vendor'
97
+ }
98
+ // 组件库按包分割,实现按需加载
99
+ if (id.includes('@xto/core')) {
100
+ return 'xto-core'
101
+ }
102
+ if (id.includes('@xto/base')) {
103
+ return 'xto-base'
104
+ }
105
+ if (id.includes('@xto/form')) {
106
+ return 'xto-form'
107
+ }
108
+ if (id.includes('@xto/data')) {
109
+ return 'xto-data'
110
+ }
111
+ if (id.includes('@xto/feedback')) {
112
+ return 'xto-feedback'
113
+ }
114
+ if (id.includes('@xto/navigation')) {
115
+ return 'xto-navigation'
116
+ }
117
+ if (id.includes('@xto/layout')) {
118
+ return 'xto-layout'
119
+ }
120
+ if (id.includes('@xto/business')) {
121
+ return 'xto-business'
122
+ }
123
+ // 其他第三方库
124
+ if (id.includes('node_modules/')) {
125
+ return 'vendor'
126
+ }
127
+ }
128
+ }
129
+ }
130
+ },
131
+ css: {
132
+ preprocessorOptions: {
133
+ scss: {
134
+ api: 'modern-compiler'
135
+ }
136
+ }
137
+ }
138
+ }
139
+ })