befly-admin-ui 1.8.14

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 (46) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -0
  3. package/jsconfig.json +14 -0
  4. package/package.json +51 -0
  5. package/styles/variables.scss +148 -0
  6. package/utils/arrayToTree.js +115 -0
  7. package/utils/cleanParams.js +29 -0
  8. package/utils/fieldClear.js +62 -0
  9. package/utils/genShortId.js +12 -0
  10. package/utils/hashPassword.js +9 -0
  11. package/utils/scanViewsDir.js +120 -0
  12. package/utils/withDefaultColumns.js +46 -0
  13. package/views/config/dict/components/edit.vue +120 -0
  14. package/views/config/dict/index.vue +188 -0
  15. package/views/config/dictType/components/edit.vue +110 -0
  16. package/views/config/dictType/index.vue +153 -0
  17. package/views/config/index.vue +3 -0
  18. package/views/config/system/components/edit.vue +184 -0
  19. package/views/config/system/index.vue +188 -0
  20. package/views/index/components/addonList.vue +148 -0
  21. package/views/index/components/environmentInfo.vue +116 -0
  22. package/views/index/components/operationLogs.vue +127 -0
  23. package/views/index/components/performanceMetrics.vue +153 -0
  24. package/views/index/components/quickActions.vue +30 -0
  25. package/views/index/components/serviceStatus.vue +197 -0
  26. package/views/index/components/systemNotifications.vue +144 -0
  27. package/views/index/components/systemOverview.vue +194 -0
  28. package/views/index/components/systemResources.vue +121 -0
  29. package/views/index/components/userInfo.vue +210 -0
  30. package/views/index/index.vue +67 -0
  31. package/views/jsconfig.json +15 -0
  32. package/views/log/email/index.vue +221 -0
  33. package/views/log/index.vue +3 -0
  34. package/views/log/login/index.vue +95 -0
  35. package/views/log/operate/index.vue +169 -0
  36. package/views/login_1/index.vue +400 -0
  37. package/views/people/admin/components/edit.vue +173 -0
  38. package/views/people/admin/index.vue +121 -0
  39. package/views/people/index.vue +3 -0
  40. package/views/permission/api/index.vue +146 -0
  41. package/views/permission/index.vue +3 -0
  42. package/views/permission/menu/index.vue +109 -0
  43. package/views/permission/role/components/api.vue +371 -0
  44. package/views/permission/role/components/edit.vue +143 -0
  45. package/views/permission/role/components/menu.vue +310 -0
  46. package/views/permission/role/index.vue +175 -0
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <div class="page-api page-table">
3
+ <div class="main-tool">
4
+ <div class="left">
5
+ <TInput v-model="$Data.searchKeyword" placeholder="搜索接口名称或路径" clearable style="width: 300px" @enter="handleSearch" @clear="handleSearch">
6
+ <template #suffix-icon>
7
+ <SearchIcon />
8
+ </template>
9
+ </TInput>
10
+ <span style="margin-left: 16px; color: var(--text-secondary); font-size: 13px">共 {{ $Data.allData.length }} 个接口</span>
11
+ </div>
12
+ <div class="right">
13
+ <TButton shape="circle" @click="handleRefresh">
14
+ <template #icon>
15
+ <RefreshIcon />
16
+ </template>
17
+ </TButton>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="main-content">
22
+ <div class="main-table">
23
+ <TTable :data="$Data.tableData" :columns="$Data.columns" :loading="$Data.loading" :active-row-keys="$Data.activeRowKeys" row-key="id" height="calc(100vh - var(--search-height) - var(--layout-gap) * 2)" active-row-type="single" @active-change="onActiveChange">
24
+ <template #method="{ row }">
25
+ <TTag v-if="row.method === 'GET'" shape="round" theme="success" variant="light-outline">GET</TTag>
26
+ <TTag v-else-if="row.method === 'POST'" shape="round" theme="primary" variant="light-outline">POST</TTag>
27
+ <TTag v-else-if="row.method === 'PUT'" shape="round" theme="warning" variant="light-outline">PUT</TTag>
28
+ <TTag v-else-if="row.method === 'DELETE'" shape="round" theme="danger" variant="light-outline">DELETE</TTag>
29
+ <TTag v-else shape="round" variant="light-outline">{{ row.method }}</TTag>
30
+ </template>
31
+ <template #auth="{ row }">
32
+ <TTag v-if="row.auth === 0" shape="round" theme="success" variant="light-outline">免登录</TTag>
33
+ <TTag v-else shape="round" theme="warning" variant="light-outline">需登录</TTag>
34
+ </template>
35
+ <template #addonName="{ row }">
36
+ <TTag v-if="row.addonName" shape="round" variant="light-outline">{{ row.addonTitle || row.addonName }}</TTag>
37
+ <span v-else>项目</span>
38
+ </template>
39
+ </TTable>
40
+ </div>
41
+
42
+ <div class="main-detail">
43
+ <DetailPanel :data="$Data.currentRow" :fields="$Data.columns">
44
+ <template #method="{ value }">
45
+ <TTag v-if="value === 'GET'" shape="round" theme="success" variant="light-outline">GET</TTag>
46
+ <TTag v-else-if="value === 'POST'" shape="round" theme="primary" variant="light-outline">POST</TTag>
47
+ <TTag v-else-if="value === 'PUT'" shape="round" theme="warning" variant="light-outline">PUT</TTag>
48
+ <TTag v-else-if="value === 'DELETE'" shape="round" theme="danger" variant="light-outline">DELETE</TTag>
49
+ <TTag v-else shape="round" variant="light-outline">{{ value }}</TTag>
50
+ </template>
51
+ <template #auth="{ value }">
52
+ <TTag v-if="value === 0" shape="round" theme="success" variant="light-outline">免登录</TTag>
53
+ <TTag v-else shape="round" theme="warning" variant="light-outline">需登录</TTag>
54
+ </template>
55
+ </DetailPanel>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <script setup lang="ts">
62
+ import { reactive } from "vue";
63
+ import { Button as TButton, Table as TTable, Tag as TTag, Input as TInput, MessagePlugin } from "tdesign-vue-next";
64
+ import { RefreshIcon, SearchIcon } from "tdesign-icons-vue-next";
65
+ import DetailPanel from "@/components/detailPanel.vue";
66
+ import { $Http } from "@/plugins/http";
67
+ import { withDefaultColumns } from "befly-admin-ui/utils/withDefaultColumns";
68
+
69
+ // 响应式数据
70
+ const $Data = reactive({
71
+ tableData: [],
72
+ allData: [],
73
+ loading: false,
74
+ searchKeyword: "",
75
+ columns: withDefaultColumns([
76
+ { colKey: "name", title: "接口名称" },
77
+ { colKey: "auth", title: "登录" },
78
+ { colKey: "path", title: "接口路径" },
79
+ { colKey: "method", title: "请求方法" },
80
+ { colKey: "addonName", title: "所属组件" }
81
+ ]),
82
+ currentRow: null,
83
+ activeRowKeys: []
84
+ });
85
+
86
+ async function initData(): Promise<void> {
87
+ await loadApiAll();
88
+ }
89
+
90
+ async function loadApiAll(): Promise<void> {
91
+ $Data.loading = true;
92
+ try {
93
+ const res = await $Http.post(
94
+ "/core/api/all",
95
+ {},
96
+ {
97
+ dropValues: [""]
98
+ }
99
+ );
100
+ const list = res.data?.lists || [];
101
+ $Data.allData = list;
102
+ $Data.tableData = list;
103
+
104
+ if ($Data.tableData.length > 0) {
105
+ $Data.currentRow = $Data.tableData[0];
106
+ $Data.activeRowKeys = [$Data.tableData[0].id];
107
+ } else {
108
+ $Data.currentRow = null;
109
+ $Data.activeRowKeys = [];
110
+ }
111
+ } catch (_error) {
112
+ MessagePlugin.error("加载数据失败");
113
+ } finally {
114
+ $Data.loading = false;
115
+ }
116
+ }
117
+
118
+ function handleRefresh(): void {
119
+ loadApiAll();
120
+ }
121
+
122
+ function handleSearch(): void {
123
+ if (!$Data.searchKeyword) {
124
+ $Data.tableData = $Data.allData;
125
+ return;
126
+ }
127
+ const keyword = String($Data.searchKeyword).toLowerCase();
128
+ $Data.tableData = $Data.allData.filter((item) => item.name?.toLowerCase().includes(keyword) || item.path?.toLowerCase().includes(keyword));
129
+ }
130
+
131
+ function onActiveChange(value: unknown[], context: { activeRowList?: Array<{ row: unknown }> }): void {
132
+ if (value.length === 0 && $Data.activeRowKeys.length > 0) {
133
+ return;
134
+ }
135
+ $Data.activeRowKeys = value;
136
+ if (context.activeRowList && context.activeRowList.length > 0) {
137
+ $Data.currentRow = context.activeRowList[0].row as never;
138
+ }
139
+ }
140
+
141
+ initData();
142
+ </script>
143
+
144
+ <style scoped lang="scss">
145
+ // 样式继承自全局 page-table
146
+ </style>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <RouterView />
3
+ </template>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <div class="page-menu page-table">
3
+ <div class="main-tool">
4
+ <div class="left"></div>
5
+ <div class="right">
6
+ <TButton shape="circle" @click="handleRefresh">
7
+ <template #icon>
8
+ <RefreshIcon />
9
+ </template>
10
+ </TButton>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="main-content">
15
+ <div class="main-table">
16
+ <TTable :data="$Data.tableData" :columns="$Data.columns" :loading="$Data.loading" :active-row-keys="$Data.activeRowKeys" row-key="id" height="calc(100vh - var(--search-height) - var(--layout-gap) * 2)" active-row-type="single" :tree="{ childrenKey: 'children', treeNodeColumnIndex: 0, defaultExpandAll: true }" @active-change="onActiveChange">
17
+ <template #state="{ row }">
18
+ <TTag v-if="row.state === 1" shape="round" theme="success" variant="light-outline">正常</TTag>
19
+ <TTag v-else-if="row.state === 2" shape="round" theme="warning" variant="light-outline">禁用</TTag>
20
+ <TTag v-else-if="row.state === 0" shape="round" theme="danger" variant="light-outline">删除</TTag>
21
+ </template>
22
+ </TTable>
23
+ </div>
24
+
25
+ <div class="main-detail">
26
+ <DetailPanel :data="$Data.currentRow" :fields="$Data.columns" />
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { reactive } from "vue";
34
+ import { Button as TButton, Table as TTable, Tag as TTag, MessagePlugin } from "tdesign-vue-next";
35
+ import { RefreshIcon } from "tdesign-icons-vue-next";
36
+ import DetailPanel from "@/components/detailPanel.vue";
37
+ import { $Http } from "@/plugins/http";
38
+ import { arrayToTree } from "befly-admin-ui/utils/arrayToTree";
39
+ import { withDefaultColumns } from "befly-admin-ui/utils/withDefaultColumns";
40
+
41
+ // 响应式数据
42
+ const $Data = reactive({
43
+ tableData: [],
44
+ loading: false,
45
+ columns: withDefaultColumns([
46
+ { colKey: "name", title: "菜单名称" },
47
+ { colKey: "path", title: "路由路径" },
48
+ { colKey: "parentPath", title: "父级路径" },
49
+ { colKey: "sort", title: "排序" },
50
+ { colKey: "state", title: "状态" }
51
+ ]),
52
+ currentRow: null,
53
+ activeRowKeys: []
54
+ });
55
+
56
+ async function initData(): Promise<void> {
57
+ await apiMenuList();
58
+ }
59
+
60
+ async function apiMenuList(): Promise<void> {
61
+ $Data.loading = true;
62
+ try {
63
+ const res = await $Http.post(
64
+ "/core/menu/all",
65
+ {},
66
+ {
67
+ dropValues: [""]
68
+ }
69
+ );
70
+ const lists = Array.isArray(res?.data?.lists) ? res.data.lists : [];
71
+
72
+ const treeResult = arrayToTree(lists, "path", "parentPath", "children", "sort");
73
+
74
+ $Data.tableData = treeResult.tree;
75
+
76
+ if ($Data.tableData.length > 0) {
77
+ $Data.currentRow = $Data.tableData[0];
78
+ $Data.activeRowKeys = [$Data.tableData[0].id];
79
+ } else {
80
+ $Data.currentRow = null;
81
+ $Data.activeRowKeys = [];
82
+ }
83
+ } catch (_error) {
84
+ MessagePlugin.error("加载数据失败");
85
+ } finally {
86
+ $Data.loading = false;
87
+ }
88
+ }
89
+
90
+ function handleRefresh(): void {
91
+ apiMenuList();
92
+ }
93
+
94
+ function onActiveChange(value: unknown[], context: { activeRowList?: Array<{ row: unknown }> }): void {
95
+ if (value.length === 0 && $Data.activeRowKeys.length > 0) {
96
+ return;
97
+ }
98
+ $Data.activeRowKeys = value;
99
+ if (context.activeRowList && context.activeRowList.length > 0) {
100
+ $Data.currentRow = context.activeRowList[0].row as never;
101
+ }
102
+ }
103
+
104
+ initData();
105
+ </script>
106
+
107
+ <style scoped lang="scss">
108
+ // 样式继承自全局 page-table
109
+ </style>
@@ -0,0 +1,371 @@
1
+ <template>
2
+ <PageDialog v-model="dialogVisible" title="接口权限" width="900px" :confirm-loading="$Data.submitting" @confirm="onSubmit">
3
+ <div class="comp-role-api">
4
+ <!-- 搜索框 -->
5
+ <div class="search-box">
6
+ <TInput v-model="$Data.searchText" placeholder="搜索接口名称或路径" clearable @change="onSearch">
7
+ <template #prefix-icon>
8
+ <SearchIcon />
9
+ </template>
10
+ </TInput>
11
+ </div>
12
+
13
+ <!-- 接口分组列表 -->
14
+ <div class="api-container">
15
+ <TCheckboxGroup v-model="$Data.checkedApiPaths">
16
+ <div class="api-group" v-for="group in $Data.filteredApiData" :key="group.name">
17
+ <div class="group-header">{{ group.title }}</div>
18
+ <div class="api-checkbox-list">
19
+ <TCheckbox v-for="api in group.apis" :key="api.value" :value="api.value">
20
+ <div class="api-checkbox-label">
21
+ <div class="api-label-main">
22
+ <div class="api-name" :title="api.path ? `${api.name}\n${api.path}` : api.name">{{ api.name }}</div>
23
+ </div>
24
+ </div>
25
+ </TCheckbox>
26
+ </div>
27
+ </div>
28
+ </TCheckboxGroup>
29
+ </div>
30
+ </div>
31
+ </PageDialog>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { computed, reactive } from "vue";
36
+
37
+ import { Input as TInput, CheckboxGroup as TCheckboxGroup, Checkbox as TCheckbox, MessagePlugin } from "tdesign-vue-next";
38
+ import { SearchIcon } from "tdesign-icons-vue-next";
39
+ import PageDialog from "@/components/pageDialog.vue";
40
+ import { $Http } from "@/plugins/http";
41
+
42
+ const $Prop = defineProps({
43
+ modelValue: {
44
+ type: Boolean,
45
+ default: false
46
+ },
47
+ rowData: {
48
+ type: Object,
49
+ default: () => ({})
50
+ }
51
+ });
52
+
53
+ const $Emit = defineEmits<{
54
+ (e: "update:modelValue", value: boolean): void;
55
+ (e: "success"): void;
56
+ }>();
57
+
58
+ type PageDialogEventContext = {
59
+ close: () => void;
60
+ };
61
+
62
+ const dialogVisible = computed({
63
+ get: () => $Prop.modelValue,
64
+ set: (value) => {
65
+ $Emit("update:modelValue", value);
66
+ }
67
+ });
68
+
69
+ const $Data = reactive({
70
+ submitting: false,
71
+ apiData: [],
72
+ filteredApiData: [],
73
+ searchText: "",
74
+ checkedApiPaths: []
75
+ });
76
+
77
+ async function initData(): Promise<void> {
78
+ await Promise.all([apiApiAll(), apiRoleApiDetail()]);
79
+
80
+ const merged = new Set<string>();
81
+ const current = Array.isArray($Data.checkedApiPaths) ? $Data.checkedApiPaths : [];
82
+ for (const p of current) {
83
+ if (typeof p === "string") {
84
+ merged.add(p);
85
+ }
86
+ }
87
+
88
+ const groups = Array.isArray($Data.apiData) ? ($Data.apiData as unknown[]) : [];
89
+ for (const group of groups) {
90
+ const groupRecord = group && typeof group === "object" ? (group as Record<string, unknown>) : null;
91
+ const apis = groupRecord && Array.isArray(groupRecord["apis"]) ? (groupRecord["apis"] as unknown[]) : [];
92
+ for (const api of apis) {
93
+ const apiRecord = api && typeof api === "object" ? (api as Record<string, unknown>) : null;
94
+ const authValue = apiRecord ? apiRecord["auth"] : undefined;
95
+ const isPublic = authValue === 0 || authValue === "0" || authValue === false;
96
+ const value = apiRecord && typeof apiRecord["value"] === "string" ? String(apiRecord["value"]) : "";
97
+ if (isPublic && value) {
98
+ merged.add(value);
99
+ }
100
+ }
101
+ }
102
+
103
+ $Data.checkedApiPaths = Array.from(merged) as never;
104
+ $Data.filteredApiData = $Data.apiData;
105
+ }
106
+
107
+ async function apiApiAll(): Promise<void> {
108
+ try {
109
+ const res = await $Http.post(
110
+ "/core/api/all",
111
+ {},
112
+ {
113
+ dropValues: [""]
114
+ }
115
+ );
116
+
117
+ const apiMap = new Map<string, { name: string; title: string; apis: unknown[] }>();
118
+
119
+ const resRecord = res && typeof res === "object" ? (res as Record<string, unknown>) : null;
120
+ const resData = resRecord && resRecord["data"] && typeof resRecord["data"] === "object" ? (resRecord["data"] as Record<string, unknown>) : null;
121
+ const lists = resData && Array.isArray(resData["lists"]) ? (resData["lists"] as unknown[]) : [];
122
+ for (const api of lists) {
123
+ const apiRecord = api && typeof api === "object" ? (api as Record<string, unknown>) : null;
124
+ const apiPath = apiRecord && typeof apiRecord["path"] === "string" ? String(apiRecord["path"]) : "";
125
+ if (!apiPath) {
126
+ continue;
127
+ }
128
+
129
+ const parentPath = apiRecord && typeof apiRecord["parentPath"] === "string" ? String(apiRecord["parentPath"]) : "";
130
+ const groupKey = parentPath || "(未分组)";
131
+
132
+ if (!apiMap.has(groupKey)) {
133
+ apiMap.set(groupKey, {
134
+ name: groupKey,
135
+ title: groupKey,
136
+ apis: []
137
+ });
138
+ }
139
+
140
+ apiMap.get(groupKey)?.apis.push({
141
+ value: apiPath,
142
+ name: (apiRecord && typeof apiRecord["name"] === "string" ? String(apiRecord["name"]) : "") || apiPath,
143
+ path: apiPath,
144
+ label: `${(apiRecord && typeof apiRecord["name"] === "string" ? String(apiRecord["name"]) : "") || ""} ${apiPath ? `(${apiPath})` : ""}`.trim(),
145
+ description: apiRecord ? apiRecord["description"] : undefined,
146
+ auth: apiRecord ? apiRecord["auth"] : undefined,
147
+ parentPath: parentPath
148
+ });
149
+ }
150
+
151
+ const groups = Array.from(apiMap.values());
152
+ for (const group of groups) {
153
+ group.apis.sort((a, b) => {
154
+ const ap = typeof a.path === "string" ? a.path : "";
155
+ const bp = typeof b.path === "string" ? b.path : "";
156
+ return ap.localeCompare(bp);
157
+ });
158
+ }
159
+
160
+ groups.sort((a, b) => {
161
+ const at = typeof a.title === "string" ? a.title : "";
162
+ const bt = typeof b.title === "string" ? b.title : "";
163
+ return at.localeCompare(bt);
164
+ });
165
+
166
+ $Data.apiData = groups as never;
167
+ } catch (_error) {
168
+ MessagePlugin.error("加载接口失败");
169
+ }
170
+ }
171
+
172
+ async function apiRoleApiDetail(): Promise<void> {
173
+ if (!$Prop.rowData.id) return;
174
+
175
+ try {
176
+ const res = await $Http.post(
177
+ "/core/role/apis",
178
+ {
179
+ roleCode: $Prop.rowData.code
180
+ },
181
+ {
182
+ dropValues: [""]
183
+ }
184
+ );
185
+
186
+ const resRecord = res && typeof res === "object" ? (res as Record<string, unknown>) : null;
187
+ const resData = resRecord && resRecord["data"] && typeof resRecord["data"] === "object" ? (resRecord["data"] as Record<string, unknown>) : null;
188
+ $Data.checkedApiPaths = resData && Array.isArray(resData["apiPaths"]) ? (resData["apiPaths"] as never) : ([] as never);
189
+ } catch (_error) {
190
+ MessagePlugin.error("加载数据失败");
191
+ }
192
+ }
193
+
194
+ function onSearch(): void {
195
+ if (!$Data.searchText) {
196
+ $Data.filteredApiData = $Data.apiData;
197
+ return;
198
+ }
199
+
200
+ const searchLower = String($Data.searchText).toLowerCase();
201
+ $Data.filteredApiData = ($Data.apiData as unknown[])
202
+ .map((group) => {
203
+ const groupRecord = group && typeof group === "object" ? (group as Record<string, unknown>) : null;
204
+ const groupApis = groupRecord && Array.isArray(groupRecord["apis"]) ? (groupRecord["apis"] as unknown[]) : [];
205
+ const apis = groupApis.filter((api) => {
206
+ const apiRecord = api && typeof api === "object" ? (api as Record<string, unknown>) : null;
207
+ const label = apiRecord && typeof apiRecord["label"] === "string" ? String(apiRecord["label"]) : "";
208
+ const name = apiRecord && typeof apiRecord["name"] === "string" ? String(apiRecord["name"]) : "";
209
+ const path = apiRecord && typeof apiRecord["path"] === "string" ? String(apiRecord["path"]) : "";
210
+ return label.toLowerCase().includes(searchLower) || name.toLowerCase().includes(searchLower) || path.toLowerCase().includes(searchLower);
211
+ });
212
+ return {
213
+ name: groupRecord && typeof groupRecord["name"] === "string" ? String(groupRecord["name"]) : "",
214
+ title: groupRecord && typeof groupRecord["title"] === "string" ? String(groupRecord["title"]) : "",
215
+ apis: apis
216
+ };
217
+ })
218
+ .filter((group) => group.apis.length > 0) as never;
219
+ }
220
+
221
+ async function onSubmit(context?: PageDialogEventContext): Promise<void> {
222
+ try {
223
+ $Data.submitting = true;
224
+
225
+ const res = await $Http.post("/core/role/apiSave", {
226
+ roleCode: $Prop.rowData.code,
227
+ apiPaths: $Data.checkedApiPaths
228
+ });
229
+
230
+ if (res.code === 0) {
231
+ MessagePlugin.success("保存成功");
232
+ $Emit("success");
233
+ if (context && typeof context.close === "function") {
234
+ context.close();
235
+ }
236
+ } else {
237
+ MessagePlugin.error(res.msg || "保存失败");
238
+ }
239
+ } catch (_error) {
240
+ MessagePlugin.error("保存失败");
241
+ } finally {
242
+ $Data.submitting = false;
243
+ }
244
+ }
245
+
246
+ initData();
247
+ </script>
248
+
249
+ <style scoped lang="scss">
250
+ .comp-role-api {
251
+ height: 60vh;
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: 12px;
255
+
256
+ .api-container {
257
+ flex: 1;
258
+ overflow-y: auto;
259
+
260
+ /* CheckboxGroup 默认可能是 inline 布局,容易被内容撑开;这里强制占满容器宽度 */
261
+ :deep(.t-checkbox-group) {
262
+ display: block;
263
+ width: 100%;
264
+ }
265
+
266
+ .api-group {
267
+ margin-bottom: 16px;
268
+ border: 1px solid var(--border-color);
269
+ border-radius: var(--border-radius-small);
270
+ overflow: hidden;
271
+
272
+ .api-checkbox-list {
273
+ padding: 12px 16px;
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ gap: 12px;
277
+
278
+ :deep(.t-checkbox) {
279
+ margin: 0;
280
+ flex: 0 0 calc(33.333% - 8px);
281
+ min-width: 0;
282
+
283
+ .t-checkbox__label {
284
+ white-space: nowrap;
285
+ overflow: hidden;
286
+ text-overflow: ellipsis;
287
+ }
288
+ }
289
+ }
290
+
291
+ &:last-child {
292
+ margin-bottom: 0;
293
+ }
294
+
295
+ .group-header {
296
+ padding: 12px 16px;
297
+ background-color: var(--primary-color-light);
298
+ font-weight: 500;
299
+ font-size: var(--font-size-sm);
300
+ color: var(--text-primary);
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 8px;
304
+
305
+ &::before {
306
+ content: "";
307
+ width: 12px;
308
+ height: 12px;
309
+ border-radius: 50%;
310
+ background-color: var(--primary-color);
311
+ box-shadow: 0 0 0 2px var(--bg-color-container);
312
+ flex-shrink: 0;
313
+ }
314
+ }
315
+
316
+ .api-item {
317
+ padding: 8px 16px;
318
+ cursor: pointer;
319
+ transition: background-color 0.2s;
320
+ background-color: var(--bg-color-container);
321
+
322
+ :deep(.t-checkbox-group) {
323
+ display: flex;
324
+ flex-wrap: wrap;
325
+ gap: 12px;
326
+ width: 100%;
327
+ }
328
+
329
+ :deep(.t-checkbox) {
330
+ flex: 0 0 calc(33.333% - 8px);
331
+ margin: 0;
332
+
333
+ .t-checkbox__label {
334
+ white-space: nowrap;
335
+ overflow: hidden;
336
+ text-overflow: ellipsis;
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ .api-checkbox-label {
345
+ width: 100%;
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: space-between;
349
+ gap: 8px;
350
+ }
351
+
352
+ .api-label-main {
353
+ min-width: 0;
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: 2px;
357
+ }
358
+
359
+ .api-name {
360
+ max-width: 100%;
361
+ overflow: hidden;
362
+ text-overflow: ellipsis;
363
+ white-space: nowrap;
364
+ }
365
+
366
+ .dialog-footer {
367
+ width: 100%;
368
+ display: flex;
369
+ justify-content: center;
370
+ }
371
+ </style>