@befly-addon/admin 1.0.9 → 1.0.11

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 (40) hide show
  1. package/apis/api/all.ts +5 -12
  2. package/apis/menu/all.ts +8 -14
  3. package/package.json +17 -3
  4. package/tables/admin.json +157 -13
  5. package/tables/api.json +79 -7
  6. package/tables/dict.json +79 -7
  7. package/tables/menu.json +79 -7
  8. package/tables/role.json +79 -7
  9. package/util.ts +1 -150
  10. package/views/403/index.vue +68 -0
  11. package/views/admin/components/edit.vue +150 -0
  12. package/views/admin/components/role.vue +138 -0
  13. package/views/admin/index.vue +179 -0
  14. package/views/dict/components/edit.vue +159 -0
  15. package/views/dict/index.vue +162 -0
  16. package/views/index/components/addonList.vue +127 -0
  17. package/views/index/components/environmentInfo.vue +99 -0
  18. package/views/index/components/operationLogs.vue +114 -0
  19. package/views/index/components/performanceMetrics.vue +150 -0
  20. package/views/index/components/quickActions.vue +27 -0
  21. package/views/index/components/serviceStatus.vue +183 -0
  22. package/views/index/components/systemNotifications.vue +132 -0
  23. package/views/index/components/systemOverview.vue +190 -0
  24. package/views/index/components/systemResources.vue +106 -0
  25. package/views/index/components/userInfo.vue +206 -0
  26. package/views/index/index.vue +29 -0
  27. package/views/login/components/emailLoginForm.vue +167 -0
  28. package/views/login/components/registerForm.vue +170 -0
  29. package/views/login/components/welcomePanel.vue +61 -0
  30. package/views/login/index.vue +191 -0
  31. package/views/menu/components/edit.vue +153 -0
  32. package/views/menu/index.vue +177 -0
  33. package/views/news/detail/index.vue +26 -0
  34. package/views/news/index.vue +26 -0
  35. package/views/role/components/api.vue +283 -0
  36. package/views/role/components/edit.vue +132 -0
  37. package/views/role/components/menu.vue +146 -0
  38. package/views/role/index.vue +179 -0
  39. package/views/user/index.vue +322 -0
  40. package/apis/dashboard/addonList.ts +0 -47
package/util.ts CHANGED
@@ -2,93 +2,11 @@
2
2
  * Admin 插件通用工具函数
3
3
  */
4
4
 
5
- import { Logger, Addon } from 'befly';
5
+ import { Logger } from 'befly';
6
6
  import type { BeflyContext } from 'befly/types/befly';
7
7
  import { readdirSync, statSync, readFileSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
 
10
- /**
11
- * 递归扫描目录下的所有 .ts 文件
12
- * @param dir - 目录路径
13
- * @param fileList - 文件列表
14
- * @returns 文件路径数组
15
- */
16
- export function scanTsFiles(dir: string, fileList: string[] = []): string[] {
17
- try {
18
- const files = readdirSync(dir);
19
-
20
- for (const file of files) {
21
- const filePath = path.join(dir, file);
22
- const stat = statSync(filePath);
23
-
24
- if (stat.isDirectory()) {
25
- // 递归扫描子目录
26
- scanTsFiles(filePath, fileList);
27
- } else if (file.endsWith('.ts') && !file.endsWith('.d.ts')) {
28
- fileList.push(filePath);
29
- }
30
- }
31
- } catch (error: any) {
32
- Logger.warn(`扫描目录失败: ${dir}`, error.message);
33
- }
34
-
35
- return fileList;
36
- }
37
-
38
- /**
39
- * 检查数据表是否存在
40
- * @param helper - DbHelper 实例
41
- * @param tableName - 表名
42
- * @returns 是否存在
43
- */
44
- export async function checkTableExists(helper: any, tableName: string): Promise<boolean> {
45
- Logger.info('=== 检查数据表 ===');
46
- const exists = await helper.tableExists(tableName);
47
-
48
- if (!exists) {
49
- Logger.error(`❌ 表 ${tableName} 不存在,请先运行 befly syncDb 同步数据库`);
50
- return false;
51
- }
52
-
53
- Logger.info(`✅ 表 ${tableName} 存在`);
54
- return true;
55
- }
56
-
57
- /**
58
- * 删除数据库中不存在于配置的记录
59
- * @param helper - DbHelper 实例
60
- * @param tableName - 表名
61
- * @param configPaths - 配置中的路径集合
62
- * @param pathField - 路径字段名(默认为 'path')
63
- * @returns 删除数量
64
- */
65
- export async function deleteObsoleteRecords(helper: any, tableName: string, configPaths: Set<string>, pathField: string = 'path'): Promise<number> {
66
- Logger.info(`\n=== 删除配置中不存在的记录 ===`);
67
-
68
- const allRecords = await helper.getAll({
69
- table: tableName,
70
- fields: ['id', pathField, 'name']
71
- });
72
-
73
- let deletedCount = 0;
74
- for (const record of allRecords) {
75
- if (record[pathField] && !configPaths.has(record[pathField])) {
76
- await helper.delData({
77
- table: tableName,
78
- where: { id: record.id }
79
- });
80
- deletedCount++;
81
- Logger.info(` └ 删除记录: ${record.name} (ID: ${record.id}, ${pathField}: ${record[pathField]})`);
82
- }
83
- }
84
-
85
- if (deletedCount === 0) {
86
- Logger.info(' ✅ 无需删除的记录');
87
- }
88
-
89
- return deletedCount;
90
- }
91
-
92
10
  /**
93
11
  * 输出同步统计信息
94
12
  * @param stats - 统计对象
@@ -102,73 +20,6 @@ export function logSyncStats(stats: { created: number; updated: number }, delete
102
20
  Logger.info(`删除${resourceName}: ${deletedCount} 个`);
103
21
  }
104
22
 
105
- /**
106
- * 缓存角色权限到 Redis Set(增量更新)
107
- * @param befly - Befly 上下文
108
- * @param roleCode - 角色代码
109
- * @param apiIds - 接口 ID 数组(逗号分隔的字符串)
110
- */
111
- export async function cacheRolePermissions(befly: BeflyContext, roleCode: string, apiIds: string): Promise<void> {
112
- try {
113
- if (!apiIds) {
114
- // 如果没有权限,删除缓存
115
- await befly.redis.del(`role:apis:${roleCode}`);
116
- Logger.debug(`已删除角色 ${roleCode} 的权限缓存(无权限)`);
117
- return;
118
- }
119
-
120
- // 解析接口 ID 列表
121
- const apiIdArray = apiIds
122
- .split(',')
123
- .map((id: string) => parseInt(id.trim()))
124
- .filter((id: number) => !isNaN(id));
125
-
126
- if (apiIdArray.length === 0) {
127
- await befly.redis.del(`role:apis:${roleCode}`);
128
- Logger.debug(`已删除角色 ${roleCode} 的权限缓存(ID 列表为空)`);
129
- return;
130
- }
131
-
132
- // 查询所有接口
133
- const allApis = await befly.db.getAll({
134
- table: 'addon_admin_api',
135
- fields: ['id', 'path', 'method']
136
- });
137
-
138
- // 根据 ID 过滤出接口路径
139
- const roleApiPaths = allApis.filter((api: any) => apiIdArray.includes(api.id)).map((api: any) => `${api.method}${api.path}`);
140
-
141
- if (roleApiPaths.length === 0) {
142
- await befly.redis.del(`role:apis:${roleCode}`);
143
- Logger.debug(`已删除角色 ${roleCode} 的权限缓存(无匹配接口)`);
144
- return;
145
- }
146
-
147
- // 使用 Redis Set 缓存(先删除再添加,确保数据一致性)
148
- const redisKey = `role:apis:${roleCode}`;
149
- await befly.redis.del(redisKey);
150
- const result = await befly.redis.sadd(redisKey, roleApiPaths);
151
-
152
- Logger.debug(`已缓存角色 ${roleCode} 的权限: ${result} 个接口`);
153
- } catch (error: any) {
154
- Logger.warn(`缓存角色 ${roleCode} 权限失败:`, error?.message || '未知错误');
155
- }
156
- }
157
-
158
- /**
159
- * 删除角色权限缓存
160
- * @param befly - Befly 上下文
161
- * @param roleCode - 角色代码
162
- */
163
- export async function deleteRolePermissions(befly: BeflyContext, roleCode: string): Promise<void> {
164
- try {
165
- await befly.redis.del(`role:apis:${roleCode}`);
166
- Logger.debug(`已删除角色 ${roleCode} 的权限缓存`);
167
- } catch (error: any) {
168
- Logger.warn(`删除角色 ${roleCode} 权限缓存失败:`, error?.message || '未知错误');
169
- }
170
- }
171
-
172
23
  /**
173
24
  * 获取插件列表
174
25
  * @returns 插件列表
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <div class="error-page">
3
+ <div class="error-content">
4
+ <div class="error-code">403</div>
5
+ <h1 class="error-title">无权限访问</h1>
6
+ <p class="error-description">抱歉,您没有访问该页面的权限</p>
7
+ <div class="error-actions">
8
+ <tiny-button type="primary" @click="$Method.goHome">返回首页</tiny-button>
9
+ <tiny-button @click="$Method.goBack">返回上一页</tiny-button>
10
+ </div>
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { useRouter } from 'vue-router';
17
+
18
+ const router = useRouter();
19
+
20
+ const $Method = {
21
+ goHome() {
22
+ router.push('/');
23
+ },
24
+ goBack() {
25
+ router.back();
26
+ }
27
+ };
28
+ </script>
29
+
30
+ <style scoped lang="scss">
31
+ .error-page {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ min-height: 100vh;
36
+ background: $bg-color-page;
37
+
38
+ .error-content {
39
+ text-align: center;
40
+
41
+ .error-code {
42
+ font-size: 120px;
43
+ font-weight: bold;
44
+ color: #ff6b6b;
45
+ margin-bottom: 20px;
46
+ }
47
+
48
+ .error-title {
49
+ font-size: 24px;
50
+ font-weight: 600;
51
+ color: $text-primary;
52
+ margin-bottom: 12px;
53
+ }
54
+
55
+ .error-description {
56
+ font-size: 14px;
57
+ color: $text-secondary;
58
+ margin-bottom: 30px;
59
+ }
60
+
61
+ .error-actions {
62
+ display: flex;
63
+ gap: 12px;
64
+ justify-content: center;
65
+ }
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <tiny-dialog-box v-model:visible="$Data.visible" :title="$Prop.actionType === 'upd' ? '编辑管理员' : '添加管理员'" width="600px" :append-to-body="true" :show-footer="true" :esc-closable="false" top="10vh" @close="$Method.onClose">
3
+ <tiny-form :model="$Data.formData" label-width="120px" label-position="left" :rules="$Data2.formRules" :ref="(el) => ($From.form = el)">
4
+ <tiny-form-item label="用户名" prop="username">
5
+ <tiny-input v-model="$Data.formData.username" placeholder="请输入用户名" :disabled="$Prop.actionType === 'upd'" />
6
+ </tiny-form-item>
7
+ <tiny-form-item label="邮箱" prop="email">
8
+ <tiny-input v-model="$Data.formData.email" placeholder="请输入邮箱" />
9
+ </tiny-form-item>
10
+ <tiny-form-item v-if="$Prop.actionType === 'add'" label="密码" prop="password">
11
+ <tiny-input v-model="$Data.formData.password" type="password" placeholder="请输入密码,至少6位" />
12
+ </tiny-form-item>
13
+ <tiny-form-item label="姓名" prop="name">
14
+ <tiny-input v-model="$Data.formData.name" placeholder="请输入姓名" />
15
+ </tiny-form-item>
16
+ <tiny-form-item label="昵称" prop="nickname">
17
+ <tiny-input v-model="$Data.formData.nickname" placeholder="请输入昵称" />
18
+ </tiny-form-item>
19
+ <tiny-form-item label="手机号" prop="phone">
20
+ <tiny-input v-model="$Data.formData.phone" placeholder="请输入手机号" />
21
+ </tiny-form-item>
22
+ <tiny-form-item v-if="$Prop.actionType === 'upd'" label="状态" prop="state">
23
+ <tiny-radio-group v-model="$Data.formData.state">
24
+ <tiny-radio :label="1">正常</tiny-radio>
25
+ <tiny-radio :label="2">禁用</tiny-radio>
26
+ </tiny-radio-group>
27
+ </tiny-form-item>
28
+ </tiny-form>
29
+ <template #footer>
30
+ <tiny-button @click="$Method.onClose">取消</tiny-button>
31
+ <tiny-button type="primary" @click="$Method.onSubmit">确定</tiny-button>
32
+ </template>
33
+ </tiny-dialog-box>
34
+ </template>
35
+
36
+ <script setup>
37
+ import { ref, shallowRef } from 'vue';
38
+ import { Modal } from '@opentiny/vue';
39
+
40
+ const $Prop = defineProps({
41
+ modelValue: {
42
+ type: Boolean,
43
+ default: false
44
+ },
45
+ actionType: {
46
+ type: String,
47
+ default: 'add'
48
+ },
49
+ rowData: {
50
+ type: Object,
51
+ default: {}
52
+ }
53
+ });
54
+
55
+ const $Emit = defineEmits(['update:modelValue', 'success']);
56
+
57
+ // 表单引用
58
+ const $From = $shallowRef({
59
+ form: null
60
+ });
61
+
62
+ const $Data = $ref({
63
+ visible: false,
64
+ formData: {
65
+ id: 0,
66
+ username: '',
67
+ email: '',
68
+ password: '',
69
+ name: '',
70
+ nickname: '',
71
+ phone: '',
72
+ state: 1
73
+ }
74
+ });
75
+
76
+ const $Data2 = $shallowRef({
77
+ formRules: {
78
+ username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
79
+ email: [
80
+ { required: true, message: '请输入邮箱', trigger: 'blur' },
81
+ { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
82
+ ],
83
+ password: [
84
+ { required: true, message: '请输入密码', trigger: 'blur' },
85
+ { min: 6, message: '密码至少6位', trigger: 'blur' }
86
+ ],
87
+ name: [{ min: 2, max: 50, message: '姓名长度在 2 到 50 个字符', trigger: 'blur' }],
88
+ nickname: [{ min: 2, max: 50, message: '昵称长度在 2 到 50 个字符', trigger: 'blur' }],
89
+ phone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }]
90
+ }
91
+ });
92
+
93
+ // 方法集合
94
+ const $Method = {
95
+ async initData() {
96
+ $Method.onShow();
97
+ if ($Prop.actionType === 'upd' && $Prop.rowData.id) {
98
+ // 编辑模式:复制数据
99
+ $Data.formData.id = $Prop.rowData.id || 0;
100
+ $Data.formData.username = $Prop.rowData.username || '';
101
+ $Data.formData.email = $Prop.rowData.email || '';
102
+ $Data.formData.name = $Prop.rowData.name || '';
103
+ $Data.formData.nickname = $Prop.rowData.nickname || '';
104
+ $Data.formData.phone = $Prop.rowData.phone || '';
105
+ $Data.formData.state = $Prop.rowData.state ?? 1;
106
+ }
107
+ },
108
+
109
+ onShow() {
110
+ setTimeout(() => {
111
+ $Data.visible = $Prop.modelValue;
112
+ }, 100);
113
+ },
114
+
115
+ onClose() {
116
+ $Data.visible = false;
117
+ setTimeout(() => {
118
+ $Emit('up:modelValue', false);
119
+ }, 300);
120
+ },
121
+
122
+ async onSubmit() {
123
+ try {
124
+ const valid = await date$From.form.validate();
125
+ if (!valid) return;
126
+
127
+ const res = await $Http($Prop.actionType === 'upd' ? '/addon/admin/adminUpd' : '/addon/admin/adminIns', $Data.formData);
128
+
129
+ Modal.message({
130
+ message: $Prop.actionType === 'upd' ? '编辑成功' : '添加成功',
131
+ status: 'success'
132
+ });
133
+ $Emit('success');
134
+ $Method.onClose();
135
+ } catch (error) {
136
+ console.error('提交失败:', error);
137
+ Modal.message({
138
+ message: '提交失败',
139
+ status: 'error'
140
+ });
141
+ }
142
+ }
143
+ };
144
+
145
+ $Method.initData();
146
+ </script>
147
+
148
+ <style scoped lang="scss">
149
+ // 可根据需要添加样式
150
+ </style>
@@ -0,0 +1,138 @@
1
+ <template>
2
+ <tiny-dialog-box v-model:visible="$Data.visible" title="分配角色" width="600px" :append-to-body="true" :show-footer="true" :esc-closable="false" top="20vh" @close="$Method.onClose">
3
+ <div class="role-dialog">
4
+ <div class="user-info">
5
+ <tiny-tag type="info">{{ $Prop.rowData.username }}</tiny-tag>
6
+ <span class="user-email">{{ $Prop.rowData.email }}</span>
7
+ </div>
8
+ <tiny-divider />
9
+ <tiny-select v-model="$Data.checkedRoleCode" :options="$Data.roleOptions" placeholder="请选择角色" />
10
+ </div>
11
+ <template #footer>
12
+ <tiny-button @click="$Method.onClose">取消</tiny-button>
13
+ <tiny-button type="primary" @click="$Method.onSubmit">确定</tiny-button>
14
+ </template>
15
+ </tiny-dialog-box>
16
+ </template>
17
+
18
+ <script setup>
19
+ import { ref } from 'vue';
20
+ import { Modal } from '@opentiny/vue';
21
+
22
+ const $Prop = defineProps({
23
+ modelValue: {
24
+ type: Boolean,
25
+ default: false
26
+ },
27
+ rowData: {
28
+ type: Object,
29
+ default: {}
30
+ }
31
+ });
32
+
33
+ const $Emit = defineEmits(['update:modelValue', 'success']);
34
+
35
+ const $Data = $ref({
36
+ visible: false,
37
+ roleOptions: [],
38
+ checkedRoleCode: ''
39
+ });
40
+
41
+ // 方法集合
42
+ const $Method = {
43
+ async initData() {
44
+ $Method.onShow();
45
+ await Promise.all([$Method.apiRoleList(), $Method.apiAdminRoleDetail()]);
46
+ },
47
+
48
+ onShow() {
49
+ setTimeout(() => {
50
+ $Data.visible = $Prop.modelValue;
51
+ }, 100);
52
+ },
53
+
54
+ onClose() {
55
+ $Data.visible = false;
56
+ setTimeout(() => {
57
+ $Emit('update:modelValue', false);
58
+ }, 300);
59
+ },
60
+
61
+ // 加载角色列表
62
+ async apiRoleList() {
63
+ try {
64
+ const res = await $Http('/addon/admin/role/list', {
65
+ page: 1,
66
+ limit: 1000
67
+ });
68
+ const roleList = res.data.lists || [];
69
+ $Data.roleOptions = roleList
70
+ .filter((role) => role.state === 1)
71
+ .map((role) => ({
72
+ label: role.name,
73
+ value: role.code
74
+ }));
75
+ } catch (error) {
76
+ console.error('加载角色列表失败:', error);
77
+ Modal.message({ message: '加载角色列表失败', status: 'error' });
78
+ }
79
+ },
80
+
81
+ // 加载管理员角色
82
+ async apiAdminRoleDetail() {
83
+ if (!$Prop.rowData.id) return;
84
+
85
+ try {
86
+ const res = await $Http('/addon/admin/roleDetail', {
87
+ adminId: $Prop.rowData.id
88
+ });
89
+ $Data.checkedRoleCode = res.data.roleCode || '';
90
+ } catch (error) {
91
+ console.error('加载用户角色失败:', error);
92
+ }
93
+ },
94
+
95
+ // 提交角色分配
96
+ async onSubmit() {
97
+ if (!$Data.checkedRoleCode) {
98
+ Modal.message({ message: '请选择角色', status: 'warning' });
99
+ return;
100
+ }
101
+
102
+ try {
103
+ const res = await $Http('/addon/admin/roleSave', {
104
+ adminId: $Prop.rowData.id,
105
+ roleCode: $Data.checkedRoleCode
106
+ });
107
+
108
+ if (res.code === 0) {
109
+ Modal.message({ message: '角色分配成功', status: 'success' });
110
+ $Method.onClose();
111
+ $Emit('success');
112
+ } else {
113
+ Modal.message({ message: res.msg || '分配失败', status: 'error' });
114
+ }
115
+ } catch (error) {
116
+ console.error('分配失败:', error);
117
+ Modal.message({ message: '分配失败', status: 'error' });
118
+ }
119
+ }
120
+ };
121
+
122
+ $Method.initData();
123
+ </script>
124
+
125
+ <style scoped lang="scss">
126
+ .role-dialog {
127
+ .user-info {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 12px;
131
+ margin-bottom: 16px;
132
+
133
+ .user-email {
134
+ color: $text-secondary;
135
+ }
136
+ }
137
+ }
138
+ </style>