@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
@@ -0,0 +1,179 @@
1
+ <template>
2
+ <div class="page-admin page-table">
3
+ <div class="main-tool">
4
+ <div class="left">
5
+ <tiny-button type="primary" @click="$Method.onAction('add', {})">
6
+ <template #icon>
7
+ <i-lucide:plus style="width: 16px; height: 16px" />
8
+ </template>
9
+ 添加管理员
10
+ </tiny-button>
11
+ </div>
12
+ <div class="right">
13
+ <tiny-button @click="$Method.handleRefresh">
14
+ <template #icon>
15
+ <i-lucide:rotate-cw style="width: 16px; height: 16px" />
16
+ </template>
17
+ </tiny-button>
18
+ </div>
19
+ <div class="right">
20
+ <tiny-button @click="$Method.handleRefresh">
21
+ <template #icon>
22
+ <i-lucide:rotate-cw style="width: 16px; height: 16px" />
23
+ </template>
24
+ 刷新
25
+ </tiny-button>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="main-table">
30
+ <tiny-grid :data="$Data.tableData" header-cell-class-name="custom-table-cell-class" size="small" height="100%" seq-serial>
31
+ <tiny-grid-column type="index" title="序号" :width="60" />
32
+ <tiny-grid-column field="username" title="用户名" />
33
+ <tiny-grid-column field="email" title="邮箱" :width="200" />
34
+ <tiny-grid-column field="nickname" title="昵称" :width="150" />
35
+ <tiny-grid-column field="roleCode" title="角色" :width="120" />
36
+ <tiny-grid-column field="state" title="状态" :width="100">
37
+ <template #default="{ row }">
38
+ <tiny-tag v-if="row.state === 1" type="success">正常</tiny-tag>
39
+ <tiny-tag v-else-if="row.state === 2" type="warning">禁用</tiny-tag>
40
+ <tiny-tag v-else type="danger">已删除</tiny-tag>
41
+ </template>
42
+ </tiny-grid-column>
43
+ <tiny-grid-column title="操作" :width="120" align="right">
44
+ <template #default="{ row }">
45
+ <tiny-dropdown title="操作" trigger="click" size="small" border visible-arrow @item-click="(data) => $Method.onAction(data.itemData.command, row)">
46
+ <template #dropdown>
47
+ <tiny-dropdown-menu>
48
+ <tiny-dropdown-item :item-data="{ command: 'role' }">
49
+ <i-lucide:user style="width: 14px; height: 14px; margin-right: 6px" />
50
+ 分配角色
51
+ </tiny-dropdown-item>
52
+ <tiny-dropdown-item :item-data="{ command: 'upd' }">
53
+ <i-lucide:pencil style="width: 14px; height: 14px; margin-right: 6px" />
54
+ 编辑
55
+ </tiny-dropdown-item>
56
+ <tiny-dropdown-item :item-data="{ command: 'del' }" divided>
57
+ <i-lucide:trash-2 style="width: 14px; height: 14px; margin-right: 6px" />
58
+ 删除
59
+ </tiny-dropdown-item>
60
+ </tiny-dropdown-menu>
61
+ </template>
62
+ </tiny-dropdown>
63
+ </template>
64
+ </tiny-grid-column>
65
+ </tiny-grid>
66
+ </div>
67
+
68
+ <div class="main-page">
69
+ <tiny-pager :current-page="$Data.pagerConfig.currentPage" :page-size="$Data.pagerConfig.pageSize" :total="$Data.pagerConfig.total" @current-change="$Method.onPageChange" @size-change="$Method.handleSizeChange" />
70
+ </div>
71
+
72
+ <!-- 编辑对话框组件 -->
73
+ <EditDialog v-if="$Data.editVisible" v-model="$Data.editVisible" :action-type="$Data.actionType" :row-data="$Data.rowData" @success="$Method.apiAdminList" />
74
+
75
+ <!-- 角色分配对话框组件 -->
76
+ <RoleDialog v-if="$Data.roleVisible" v-model="$Data.roleVisible" :row-data="$Data.rowData" @success="$Method.apiAdminList" />
77
+ </div>
78
+ </template>
79
+
80
+ <script setup>
81
+ import { ref } from 'vue';
82
+ import { Modal } from '@opentiny/vue';
83
+
84
+ import EditDialog from './components/edit.vue';
85
+ import RoleDialog from './components/role.vue';
86
+
87
+ // 响应式数据
88
+ const $Data = $ref({
89
+ tableData: [],
90
+ pagerConfig: {
91
+ currentPage: 1,
92
+ pageSize: 30,
93
+ total: 0,
94
+ align: 'right',
95
+ layout: 'total, prev, pager, next, jumper'
96
+ },
97
+ editVisible: false,
98
+ roleVisible: false,
99
+ actionType: 'add',
100
+ rowData: {}
101
+ });
102
+
103
+ // 方法
104
+ const $Method = {
105
+ async initData() {
106
+ await $Method.apiAdminList();
107
+ },
108
+
109
+ // 加载管理员列表
110
+ async apiAdminList() {
111
+ try {
112
+ const res = await $Http('/addon/admin/list', {
113
+ page: $Data.pagerConfig.currentPage,
114
+ limit: $Data.pagerConfig.pageSize
115
+ });
116
+ $Data.tableData = res.data.lists || [];
117
+ $Data.pagerConfig.total = res.data.total || 0;
118
+ } catch (error) {
119
+ console.error('加载管理员列表失败:', error);
120
+ Modal.message({
121
+ message: '加载数据失败',
122
+ status: 'error'
123
+ });
124
+ }
125
+ },
126
+
127
+ // 删除管理员
128
+ async apiAdminDel(row) {
129
+ Modal.confirm({
130
+ header: '确认删除',
131
+ body: `确定要删除管理员"${row.username}" 吗?`,
132
+ status: 'warning'
133
+ }).then(async () => {
134
+ try {
135
+ const res = await $Http('/addon/admin/del', { id: row.id });
136
+ if (res.code === 0) {
137
+ Modal.message({ message: '删除成功', status: 'success' });
138
+ $Method.apiAdminList();
139
+ } else {
140
+ Modal.message({ message: res.msg || '删除失败', status: 'error' });
141
+ }
142
+ } catch (error) {
143
+ console.error('删除失败:', error);
144
+ Modal.message({ message: '删除失败', status: 'error' });
145
+ }
146
+ });
147
+ },
148
+
149
+ // 刷新
150
+ handleRefresh() {
151
+ $Method.apiAdminList();
152
+ },
153
+
154
+ // 分页改变
155
+ onPageChange({ currentPage }) {
156
+ $Data.pagerConfig.currentPage = currentPage;
157
+ $Method.apiAdminList();
158
+ },
159
+
160
+ // 操作菜单点击
161
+ onAction(command, rowData) {
162
+ $Data.actionType = command;
163
+ $Data.rowData = rowData;
164
+ if (command === 'add' || command === 'upd') {
165
+ $Data.editVisible = true;
166
+ } else if (command === 'role') {
167
+ $Data.roleVisible = true;
168
+ } else if (command === 'del') {
169
+ $Method.apiAdminDel(rowData);
170
+ }
171
+ }
172
+ };
173
+
174
+ $Method.initData();
175
+ </script>
176
+
177
+ <style scoped lang="scss">
178
+ // 样式继承自全局 page-table
179
+ </style>
@@ -0,0 +1,159 @@
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="name">
5
+ <tiny-input v-model="$Data.formData.name" placeholder="请输入字典名称" />
6
+ </tiny-form-item>
7
+ <tiny-form-item label="字典代码" prop="code">
8
+ <tiny-input v-model="$Data.formData.code" placeholder="请输入字典代码,如:gender" />
9
+ </tiny-form-item>
10
+ <tiny-form-item label="字典值" prop="value">
11
+ <tiny-input v-model="$Data.formData.value" placeholder="请输入字典值" />
12
+ </tiny-form-item>
13
+ <tiny-form-item label="父级ID" prop="pid">
14
+ <tiny-numeric v-model="$Data.formData.pid" :min="0" :max="999999999999999" />
15
+ </tiny-form-item>
16
+ <tiny-form-item label="排序" prop="sort">
17
+ <tiny-numeric v-model="$Data.formData.sort" :min="0" :max="9999" />
18
+ </tiny-form-item>
19
+ <tiny-form-item label="描述" prop="description">
20
+ <tiny-input v-model="$Data.formData.description" type="textarea" placeholder="请输入描述" :rows="3" />
21
+ </tiny-form-item>
22
+ <tiny-form-item 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, watch, 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
+ name: '',
67
+ code: '',
68
+ value: '',
69
+ pid: 0,
70
+ sort: 0,
71
+ description: '',
72
+ state: 1
73
+ }
74
+ });
75
+
76
+ const $Data2 = $shallowRef({
77
+ formRules: {
78
+ name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
79
+ code: [
80
+ { required: true, message: '请输入字典代码', trigger: 'blur' },
81
+ { pattern: /^[a-zA-Z0-9_]+$/, message: '字典代码只能包含字母、数字和下划线', trigger: 'blur' }
82
+ ],
83
+ value: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
84
+ }
85
+ });
86
+
87
+ // 方法集合
88
+ const $Method = {
89
+ async initData() {
90
+ $Method.onShow();
91
+ },
92
+
93
+ onShow() {
94
+ $Data.visible = true;
95
+ if ($Prop.actionType === 'upd' && $Prop.rowData) {
96
+ $Data.formData.id = $Prop.rowData.id || 0;
97
+ $Data.formData.name = $Prop.rowData.name || '';
98
+ $Data.formData.code = $Prop.rowData.code || '';
99
+ $Data.formData.value = $Prop.rowData.value || '';
100
+ $Data.formData.pid = $Prop.rowData.pid || 0;
101
+ $Data.formData.sort = $Prop.rowData.sort || 0;
102
+ $Data.formData.description = $Prop.rowData.description || '';
103
+ $Data.formData.state = $Prop.rowData.state ?? 1;
104
+ } else {
105
+ // 重置表单
106
+ $Data.formData.id = 0;
107
+ $Data.formData.name = '';
108
+ $Data.formData.code = '';
109
+ $Data.formData.value = '';
110
+ $Data.formData.pid = 0;
111
+ $Data.formData.sort = 0;
112
+ $Data.formData.description = '';
113
+ $Data.formData.state = 1;
114
+ }
115
+ },
116
+
117
+ onClose() {
118
+ $Data.visible = false;
119
+ setTimeout(() => {
120
+ $Emit('update:modelValue', false);
121
+ }, 300);
122
+ },
123
+
124
+ async onSubmit() {
125
+ try {
126
+ const valid = await $From.form.validate();
127
+ if (!valid) return;
128
+
129
+ const res = await $Http($Prop.actionType === 'add' ? '/addon/admin/dictIns' : '/addon/admin/dictUpd', $Data.formData);
130
+
131
+ Modal.message({
132
+ message: $Prop.actionType === 'add' ? '添加成功' : '编辑成功',
133
+ status: 'success'
134
+ });
135
+ $Method.onClose();
136
+ $Emit('success');
137
+ } catch (error) {
138
+ console.error('提交失败:', error);
139
+ }
140
+ }
141
+ };
142
+
143
+ // 监听 modelValue 变化
144
+ watch(
145
+ () => $Prop.modelValue,
146
+ (val) => {
147
+ if (val && !$Data.visible) {
148
+ $Method.initData();
149
+ } else if (!val && $Data.visible) {
150
+ $Data.visible = false;
151
+ }
152
+ },
153
+ { immediate: true }
154
+ );
155
+ </script>
156
+
157
+ <style scoped lang="scss">
158
+ // 可根据需要添加样式
159
+ </style>
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <div class="page-dict page-table">
3
+ <div class="main-tool">
4
+ <div class="left">
5
+ <tiny-button type="primary" @click="$Method.onAction('add', {})">
6
+ <template #icon>
7
+ <i-lucide:plus style="width: 16px; height: 16px" />
8
+ </template>
9
+ 添加字典
10
+ </tiny-button>
11
+ </div>
12
+ <div class="right">
13
+ <tiny-button @click="$Method.handleRefresh">
14
+ <template #icon>
15
+ <i-lucide:rotate-cw style="width: 16px; height: 16px" />
16
+ </template>
17
+ 刷新
18
+ </tiny-button>
19
+ </div>
20
+ </div>
21
+ <div class="main-table">
22
+ <tiny-grid :data="$Data.dictList" header-cell-class-name="custom-table-cell-class" size="small" height="100%" seq-serial>
23
+ <tiny-grid-column type="index" title="序号" :width="60" />
24
+ <tiny-grid-column field="name" title="字典名称" />
25
+ <tiny-grid-column field="code" title="字典代码" :width="150" />
26
+ <tiny-grid-column field="value" title="字典值" :width="200" />
27
+ <tiny-grid-column field="pid" title="父级ID" :width="100" />
28
+ <tiny-grid-column field="sort" title="排序" :width="80" />
29
+ <tiny-grid-column field="description" title="描述" />
30
+ <tiny-grid-column field="state" title="状态" :width="100">
31
+ <template #default="{ row }">
32
+ <tiny-tag v-if="row.state === 1" type="success">正常</tiny-tag>
33
+ <tiny-tag v-else-if="row.state === 2" type="warning">禁用</tiny-tag>
34
+ <tiny-tag v-else type="danger">已删除</tiny-tag>
35
+ </template>
36
+ </tiny-grid-column>
37
+ <tiny-grid-column title="操作" :width="120" align="right">
38
+ <template #default="{ row }">
39
+ <tiny-dropdown title="操作" trigger="click" size="small" border visible-arrow @item-click="(data) => $Method.onAction(data.itemData.command, row)">
40
+ <template #dropdown>
41
+ <tiny-dropdown-menu>
42
+ <tiny-dropdown-item :item-data="{ command: 'upd' }">
43
+ <i-lucide:pencil style="width: 14px; height: 14px; margin-right: 6px" />
44
+ 编辑
45
+ </tiny-dropdown-item>
46
+ <tiny-dropdown-item :item-data="{ command: 'del' }" divided>
47
+ <i-lucide:trash-2 style="width: 14px; height: 14px; margin-right: 6px" />
48
+ 删除
49
+ </tiny-dropdown-item>
50
+ </tiny-dropdown-menu>
51
+ </template>
52
+ </tiny-dropdown>
53
+ </template>
54
+ </tiny-grid-column>
55
+ </tiny-grid>
56
+ </div>
57
+
58
+ <div class="main-page">
59
+ <tiny-pager :current-page="$Data.pagerConfig.currentPage" :page-size="$Data.pagerConfig.pageSize" :total="$Data.pagerConfig.total" @current-change="$Method.onPageChange" @size-change="$Method.handleSizeChange" />
60
+ </div>
61
+
62
+ <!-- 编辑对话框组件 -->
63
+ <EditDialog v-if="$Data.editVisible" v-model="$Data.editVisible" :action-type="$Data.actionType" :row-data="$Data.rowData" @success="$Method.apiDictList" />
64
+ </div>
65
+ </template>
66
+
67
+ <script setup>
68
+ import { ref } from 'vue';
69
+ import { Modal } from '@opentiny/vue';
70
+
71
+ import EditDialog from './components/edit.vue';
72
+
73
+ // 响应式数据
74
+ const $Data = $ref({
75
+ dictList: [],
76
+ pagerConfig: {
77
+ currentPage: 1,
78
+ pageSize: 30,
79
+ total: 0,
80
+ align: 'right',
81
+ layout: 'total, prev, pager, next, jumper'
82
+ },
83
+ editVisible: false,
84
+ actionType: 'add',
85
+ rowData: {}
86
+ });
87
+
88
+ // 方法
89
+ const $Method = {
90
+ async initData() {
91
+ await $Method.apiDictList();
92
+ },
93
+
94
+ // 加载字典列表
95
+ async apiDictList() {
96
+ try {
97
+ const res = await $Http('/addon/admin/dict/list', {
98
+ page: $Data.pagerConfig.currentPage,
99
+ limit: $Data.pagerConfig.pageSize
100
+ });
101
+ $Data.dictList = res.data.lists || [];
102
+ $Data.pagerConfig.total = res.data.total || 0;
103
+ } catch (error) {
104
+ console.error('加载字典列表失败:', error);
105
+ Modal.message({
106
+ message: '加载数据失败',
107
+ status: 'error'
108
+ });
109
+ }
110
+ },
111
+
112
+ // 删除字典
113
+ async apiDictDel(row) {
114
+ Modal.confirm({
115
+ header: '确认删除',
116
+ body: `确定要删除字典"${row.name}" 吗?`,
117
+ status: 'warning'
118
+ }).then(async () => {
119
+ try {
120
+ const res = await $Http('/addon/admin/dict/del', { id: row.id });
121
+ if (res.code === 0) {
122
+ Modal.message({ message: '删除成功', status: 'success' });
123
+ $Method.apiDictList();
124
+ } else {
125
+ Modal.message({ message: res.msg || '删除失败', status: 'error' });
126
+ }
127
+ } catch (error) {
128
+ console.error('删除失败:', error);
129
+ Modal.message({ message: '删除失败', status: 'error' });
130
+ }
131
+ });
132
+ },
133
+
134
+ // 刷新
135
+ handleRefresh() {
136
+ $Method.apiDictList();
137
+ },
138
+
139
+ // 分页改变
140
+ onPageChange({ currentPage }) {
141
+ $Data.pagerConfig.currentPage = currentPage;
142
+ $Method.apiDictList();
143
+ },
144
+
145
+ // 操作菜单点击
146
+ onAction(command, rowData) {
147
+ $Data.actionType = command;
148
+ $Data.rowData = rowData;
149
+ if (command === 'add' || command === 'upd') {
150
+ $Data.editVisible = true;
151
+ } else if (command === 'del') {
152
+ $Method.apiDictDel(rowData);
153
+ }
154
+ }
155
+ };
156
+
157
+ $Method.initData();
158
+ </script>
159
+
160
+ <style scoped lang="scss">
161
+ // 样式继承自全局 page-table
162
+ </style>
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <div class="section-block">
3
+ <div class="section-header">
4
+ <i-lucide:package style="width: 20px; height: 20px" />
5
+ <h2>已安装插件</h2>
6
+ </div>
7
+ <div class="section-content">
8
+ <div class="addon-list">
9
+ <div v-for="addon in addonList" :key="addon.name" class="addon-item">
10
+ <div class="addon-status-badge" :class="{ active: addon.enabled }"></div>
11
+ <div class="addon-icon">
12
+ <i-lucide:box style="width: 20px; height: 20px" />
13
+ </div>
14
+ <div class="addon-info">
15
+ <div class="addon-title">
16
+ <span class="addon-name">{{ addon.title }}</span>
17
+ <tiny-tag type="success" size="small">{{ addon.version }}</tiny-tag>
18
+ </div>
19
+ <div class="addon-desc">{{ addon.description }}</div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { ref } from 'vue';
29
+
30
+ // 组件内部数据
31
+ const addonList = $ref([]);
32
+
33
+ // 获取数据
34
+ const fetchData = async () => {
35
+ try {
36
+ const { data } = await $Http('/addon/admin/dashboard/addonList');
37
+ addonList.splice(0, addonList.length, ...data);
38
+ } catch (error) {
39
+ console.error('获取插件列表失败:', error);
40
+ }
41
+ };
42
+
43
+ fetchData();
44
+ </script>
45
+
46
+ <style scoped lang="scss">
47
+ .addon-list {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 8px;
51
+
52
+ .addon-item {
53
+ position: relative;
54
+ background: $bg-color-container;
55
+ border: 1px solid $border-color;
56
+ border-left: 3px solid $primary-color;
57
+ border-radius: $border-radius-small;
58
+ padding: 10px 12px;
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 10px;
62
+ transition: all 0.3s;
63
+
64
+ &:hover {
65
+ border-left-color: $success-color;
66
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
67
+ transform: translateY(-2px);
68
+ }
69
+
70
+ .addon-status-badge {
71
+ position: absolute;
72
+ top: 8px;
73
+ right: 8px;
74
+ width: 8px;
75
+ height: 8px;
76
+ border-radius: 50%;
77
+ background: $text-disabled;
78
+ transition: all 0.3s;
79
+
80
+ &.active {
81
+ background: $success-color;
82
+ box-shadow: 0 0 0 2px rgba($success-color, 0.2);
83
+ }
84
+ }
85
+
86
+ .addon-icon {
87
+ width: 32px;
88
+ height: 32px;
89
+ background: linear-gradient(135deg, $primary-color, #764ba2);
90
+ border-radius: $border-radius-small;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ color: white;
95
+ flex-shrink: 0;
96
+ }
97
+
98
+ .addon-info {
99
+ flex: 1;
100
+ min-width: 0;
101
+ padding-right: 16px;
102
+
103
+ .addon-title {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 6px;
107
+ margin-bottom: 2px;
108
+
109
+ .addon-name {
110
+ font-size: 14px;
111
+ font-weight: 600;
112
+ color: $text-primary;
113
+ }
114
+ }
115
+
116
+ .addon-desc {
117
+ font-size: 14px;
118
+ color: $text-secondary;
119
+ line-height: 1.3;
120
+ overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ white-space: nowrap;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ </style>