@epxing/business 5.3.0-beta.2 → 5.3.0-beta.4

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 (98) hide show
  1. package/API.md +115 -111
  2. package/README.md +44 -44
  3. package/dist/src/base/AiSkillEditor.js +1 -1
  4. package/dist/src/base/AiSkillEditor.vue.js +2 -0
  5. package/dist/src/base/AppConfigFormItem.js +1 -3
  6. package/dist/src/base/AppConfigFormItem.vue.js +210 -85
  7. package/dist/src/base/CommonFieldList.js +1 -1
  8. package/dist/src/base/CommonFieldList.vue.js +2 -1
  9. package/dist/src/base/ConfigModuleIndex.js +38 -1
  10. package/dist/src/base/ManageAppNav.js +1 -1
  11. package/dist/src/base/ManageMenu.js +1 -1
  12. package/dist/src/base/RoleBindTrace.js +253 -0
  13. package/dist/src/base/RoleGroupManage.js +1037 -0
  14. package/dist/src/base/ServiceMenu.js +7 -6
  15. package/dist/src/components/AiList.js +1278 -3
  16. package/dist/src/components/EntityCheckboxFind.vue.js +14 -10
  17. package/dist/src/components/EntityRightFormItem.js +1 -5
  18. package/dist/src/components/EntityRightFormItem.vue.js +3 -1
  19. package/dist/src/components/EntitySystemEr.js +492 -0
  20. package/dist/src/components/HallNav.vue.js +7 -36
  21. package/dist/src/components/IconFontLibrary.js +22 -1
  22. package/dist/src/components/IconFontList.js +21 -1
  23. package/dist/src/components/PrintFormList.js +2 -1
  24. package/dist/src/components/PrintFormList.vue.js +3 -1
  25. package/dist/src/components/WikiWorkbench.js +153 -129
  26. package/dist/src/components/authorization/AuthorizationInterface.js +3 -1
  27. package/dist/src/components/custom-bpmn/PropertiesView.js +1991 -1942
  28. package/dist/src/components/icon-gallery/IconGallery.js +193 -0
  29. package/dist/src/components/icon-gallery/mixin.js +150 -0
  30. package/dist/src/components/wiki-workbench/WikiIntake.js +86 -93
  31. package/dist/src/components/wiki-workbench/WikiIntake.vue.js +153 -73
  32. package/dist/src/index.js +26 -20
  33. package/dist/src/public-components.js +16 -14
  34. package/dist/src/route-components.js +5 -5
  35. package/dist/src/system.component.index.js +1 -1
  36. package/dist/src/system.component.js +8 -3
  37. package/dist/src/system.component.lazy.js +1 -1
  38. package/dist/src/views/Add.js +1 -1
  39. package/dist/src/views/Add.vue.js +8 -3
  40. package/dist/src/views/AiToolManager.js +792 -317
  41. package/dist/src/views/App.js +3 -45
  42. package/dist/src/views/AppRoleSelect.js +3 -230
  43. package/dist/src/views/Error.js +3 -207
  44. package/dist/src/views/ExcelDesign.js +326 -316
  45. package/dist/src/views/JvmMemoryMonitor.js +463 -0
  46. package/dist/src/views/Module.js +3 -170
  47. package/dist/src/views/WikiBinding.js +88 -25
  48. package/dist/style.css +2867 -1487
  49. package/package.json +12 -6
  50. package/src/base/AiSkillEditor.vue +2 -0
  51. package/src/base/AppConfigFormItem.vue +211 -145
  52. package/src/base/CommonFieldList.vue +9 -6
  53. package/src/base/ConfigModuleIndex.vue +37 -1
  54. package/src/base/ManageAppNav.vue +1188 -1188
  55. package/src/base/ManageMenu.vue +933 -933
  56. package/src/base/RoleBindTrace.vue +364 -0
  57. package/src/base/RoleGroupManage.vue +1160 -0
  58. package/src/base/ServiceMenu.vue +6 -5
  59. package/src/components/AiList.vue +439 -113
  60. package/src/components/EntityCheckboxFind.vue +12 -8
  61. package/src/components/EntityRightFormItem.vue +9 -10
  62. package/src/components/EntitySystemEr.vue +517 -0
  63. package/src/components/HallNav.vue +6 -33
  64. package/src/components/IconFontLibrary.vue +27 -60
  65. package/src/components/IconFontList.vue +13 -64
  66. package/src/components/PrintFormList.vue +33 -27
  67. package/src/components/WikiWorkbench.vue +90 -60
  68. package/src/components/authorization/AuthorizationInterface.vue +2 -0
  69. package/src/components/custom-bpmn/CustomPalette.js +1 -1
  70. package/src/components/custom-bpmn/CustomRenderer.js +1 -1
  71. package/src/components/custom-bpmn/PropertiesView.vue +35 -4
  72. package/src/components/custom-bpmn/index.js +1 -1
  73. package/src/components/icon-gallery/IconGallery.vue +130 -0
  74. package/src/components/icon-gallery/mixin.jsx +109 -0
  75. package/src/components/wiki-workbench/WikiIntake.vue +178 -104
  76. package/src/index.js +24 -20
  77. package/src/public-components.js +1 -0
  78. package/src/route-components.js +4 -4
  79. package/src/system.component.index.js +1 -1
  80. package/src/views/Add.vue +8 -5
  81. package/src/views/AiToolManager.vue +326 -4
  82. package/src/views/App.vue +4 -53
  83. package/src/views/AppRoleSelect.vue +4 -334
  84. package/src/views/Error.vue +4 -63
  85. package/src/views/ExcelDesign.vue +24 -7
  86. package/src/views/JvmMemoryMonitor.vue +284 -0
  87. package/src/views/Module.vue +4 -210
  88. package/src/views/WikiBinding.vue +42 -0
  89. package/types/app.d.ts +1 -0
  90. package/types/dept-find.d.ts +1 -1
  91. package/types/index.d.ts +15 -14
  92. package/types/popup.d.ts +2 -2
  93. package/types/route-components.d.ts +1 -1
  94. package/types/user-find.d.ts +1 -1
  95. package/dist/assets/role_select-DTqRlFNz.webp +0 -0
  96. package/dist/src/components/AiList.vue.js +0 -93
  97. package/dist/src/components/IconFontLibrary.vue.js +0 -67
  98. package/dist/src/components/IconFontList.vue.js +0 -79
@@ -0,0 +1,1160 @@
1
+ <template>
2
+ <div class="rgm-page">
3
+ <div class="split">
4
+ <!-- 左:角色组列表 -->
5
+ <div class="split__left">
6
+ <div class="v9-card">
7
+ <div class="v9-block-title">角色组</div>
8
+ <div class="group-list">
9
+ <div
10
+ v-for="g in groupList"
11
+ :key="g.ID"
12
+ :class="['grp-item', { 'is-active': g.ID === curGroupId }]"
13
+ @click="selectGroup(g.ID)"
14
+ >
15
+ <div class="grp-item__body">
16
+ <div class="grp-item__name">{{ g.NAME }}</div>
17
+ <div class="grp-item__meta">{{ g.CODE }} · {{ g.roleCount }} 个角色 · 已授 {{ g.userCount }} 人</div>
18
+ </div>
19
+ <div class="grp-item__ops">
20
+ <button class="v9-btn v9-btn--text" @click.stop="openGroupForm(g)">编辑</button>
21
+ <button class="v9-btn v9-btn--text is-danger" @click.stop="openDelGroup(g)">删除</button>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <button class="v9-btn v9-btn--default grp-add-btn" @click="openGroupForm(null)">
26
+ <i class="el-icon-plus"></i> 新建角色组
27
+ </button>
28
+ </div>
29
+ <div class="v9-form-item__tip grp-left-tip">角色组为租户级定义,不属于任何业务系统;一个角色同一时间只能属于一个角色组。</div>
30
+ </div>
31
+
32
+ <!-- 右:组详情 -->
33
+ <div class="split__right">
34
+ <!-- 未选组 -->
35
+ <div v-if="!curGroup" class="v9-card">
36
+ <div class="v9-empty">
37
+ <i class="el-icon-box v9-empty__icon"></i>
38
+ <div>请先新建角色组</div>
39
+ </div>
40
+ </div>
41
+
42
+ <template v-else>
43
+ <!-- 重新应用提示条(G-04,历史差量兜底:正常流程加角色已自动补授) -->
44
+ <div v-if="pendingApply && pendingApply.length && curGroup.userCount > 0" class="tipbar tipbar--info">
45
+ <i class="el-icon-info"></i>
46
+ <div class="tipbar__main">检测到 {{ pendingApply.length }} 个角色的历史差量:已通过本组授权的 {{ curGroup.userCount }} 个用户尚未获得这些角色,可手动补授。</div>
47
+ <button class="v9-btn v9-btn--primary" @click="applyGroup">应用到 {{ curGroup.userCount }} 个用户</button>
48
+ </div>
49
+
50
+ <!-- 组信息 -->
51
+ <div class="v9-card">
52
+ <div class="v9-block-title">组信息</div>
53
+ <table class="v9-desc-table">
54
+ <tbody>
55
+ <tr>
56
+ <th>组名称</th><td>{{ curGroup.NAME }}</td>
57
+ <th>组编码</th><td>{{ curGroup.CODE }}</td>
58
+ <th>排序号</th><td>{{ curGroup.ORDER_NUM }}</td>
59
+ </tr>
60
+ <tr>
61
+ <th>描述</th><td colspan="5">{{ curGroup.DESC || '—' }}</td>
62
+ </tr>
63
+ </tbody>
64
+ </table>
65
+ <div class="grp-edit-row">
66
+ <button class="v9-btn v9-btn--default" @click="openGroupForm(curGroup)"><i class="el-icon-edit"></i> 编辑</button>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- 组概况统计 -->
71
+ <div class="v9-figure-grid v9-figure-grid--3">
72
+ <div class="v9-figure">
73
+ <div class="v9-figure__label">组内角色</div>
74
+ <div class="v9-figure__value">{{ curGroup.roleCount }}</div>
75
+ </div>
76
+ <div class="v9-figure">
77
+ <div class="v9-figure__label">已授权用户</div>
78
+ <div class="v9-figure__value">{{ curGroup.userCount }}</div>
79
+ </div>
80
+ <div class="v9-figure">
81
+ <div class="v9-figure__label">覆盖系统</div>
82
+ <div class="v9-figure__value">{{ detail.sysCount }}</div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- 组内角色 / 已授权用户页签 -->
87
+ <div class="v9-card">
88
+ <div class="v9-tabs">
89
+ <div :class="['v9-tabs__item', { 'is-active': activeTab === 'roles' }]" @click="switchGroupTab('roles')">组内角色({{ curGroup.roleCount }})</div>
90
+ <div :class="['v9-tabs__item', { 'is-active': activeTab === 'users' }]" @click="switchGroupTab('users')">已授权用户({{ curGroup.userCount }})</div>
91
+ </div>
92
+
93
+ <!-- 页签一:组内角色 -->
94
+ <div v-show="activeTab === 'roles'" class="grp-tab-body">
95
+ <div class="grp-tab-toolbar">
96
+ <button class="v9-btn v9-btn--primary" @click="openPickRole"><i class="el-icon-plus"></i> 添加角色</button>
97
+ <span class="v9-form-item__tip grp-inline-tip">组不需要覆盖全部系统,内容按需;组内新增角色自动补授已授权用户,移除角色自动按位收回</span>
98
+ </div>
99
+ <div v-if="!groupRoles.length" class="v9-empty">
100
+ <i class="el-icon-box v9-empty__icon"></i>
101
+ <div>组内暂无角色,点击「添加角色」开始</div>
102
+ </div>
103
+ <table v-else class="v9-table">
104
+ <thead>
105
+ <tr>
106
+ <th style="width:60px">序号</th>
107
+ <th>角色名称</th>
108
+ <th>所属系统</th>
109
+ <th>角色编码</th>
110
+ <th style="width:100px">操作</th>
111
+ </tr>
112
+ </thead>
113
+ <tbody>
114
+ <tr v-for="(r, i) in groupRoles" :key="r.ID">
115
+ <td>{{ i + 1 }}</td>
116
+ <td>{{ r.NAME }}</td>
117
+ <td><i :class="r.systemIcon" class="grp-sys-icon"></i>{{ r.SYSTEM_NAME }}</td>
118
+ <td><span class="v9-tag v9-tag--info">{{ r.CODE }}</span></td>
119
+ <td><button class="v9-btn v9-btn--text is-danger" @click="openRemoveRole(r)">移除</button></td>
120
+ </tr>
121
+ </tbody>
122
+ </table>
123
+ </div>
124
+
125
+ <!-- 页签二:已授权用户 -->
126
+ <div v-show="activeTab === 'users'" class="grp-tab-body">
127
+ <div class="grp-tab-toolbar">
128
+ <button class="v9-btn v9-btn--primary" @click="openAddUser"><i class="el-icon-plus"></i> 添加用户</button>
129
+ <span class="v9-form-item__tip grp-inline-tip">添加后用户立即获得组内全部角色;后续组内新增角色也会自动补授这些用户</span>
130
+ </div>
131
+ <div v-if="!groupUsers.length" class="v9-empty">
132
+ <i class="el-icon-box v9-empty__icon"></i>
133
+ <div>暂无通过本组授权的用户,点击「添加用户」开始</div>
134
+ </div>
135
+ <template v-else>
136
+ <table class="v9-table">
137
+ <thead>
138
+ <tr>
139
+ <th style="width:60px">序号</th>
140
+ <th>姓名</th>
141
+ <th>工作证号</th>
142
+ <th>部门</th>
143
+ <th>持有本组角色数</th>
144
+ <th style="width:160px">操作</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody>
148
+ <tr v-for="(u, i) in groupUsers" :key="u.ID">
149
+ <td>{{ i + 1 }}</td>
150
+ <td>{{ u.NAME }}</td>
151
+ <td>{{ u.USER_NO }}</td>
152
+ <td>{{ u.DEPT_NAME }}</td>
153
+ <td>{{ u.roleCount }}</td>
154
+ <td><button class="v9-btn v9-btn--text is-danger" @click="openRemoveUser(u)">移除</button></td>
155
+ </tr>
156
+ </tbody>
157
+ </table>
158
+ <div class="v9-form-item__tip grp-table-foot-tip">按组来源(SOURCE_GROUP)反查,用于影响面评估与审计。</div>
159
+ </template>
160
+ </div>
161
+ </div>
162
+ </template>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- ================= 弹窗:新建 / 编辑角色组 ================= -->
167
+ <div v-if="dlg.groupForm" class="v9-overlay" @click.self="dlg.groupForm = false">
168
+ <div class="v9-dialog">
169
+ <div class="v9-dialog__header">
170
+ <span>{{ formGroup.ID ? '编辑角色组' : '新建角色组' }}</span>
171
+ <span class="v9-dialog__close" @click="dlg.groupForm = false"><i class="el-icon-close"></i></span>
172
+ </div>
173
+ <div class="v9-dialog__body">
174
+ <div class="v9-form-item">
175
+ <label class="v9-form-item__label is-required">组名称</label>
176
+ <input v-model="formGroup.NAME" :class="['v9-input', { 'is-error': formErr.name }]" placeholder="租户内唯一,如:教师">
177
+ <div v-if="formErr.name" class="v9-form-item__error">组名称必填且不能与现有组重复</div>
178
+ </div>
179
+ <div class="v9-form-item">
180
+ <label class="v9-form-item__label is-required">组编码</label>
181
+ <input v-model="formGroup.CODE" :class="['v9-input', { 'is-error': formErr.code }]" placeholder="大小写字母 / 下划线,如:TEACHER">
182
+ <div v-if="formErr.code" class="v9-form-item__error">编码必填,仅限大小写字母、数字与下划线,且不能重复</div>
183
+ </div>
184
+ <div class="v9-form-item">
185
+ <label class="v9-form-item__label">描述</label>
186
+ <input v-model="formGroup.DESC" class="v9-input" placeholder="组的业务含义,授权人可见">
187
+ </div>
188
+ <div class="v9-form-item">
189
+ <label class="v9-form-item__label">排序号</label>
190
+ <input v-model="formGroup.ORDER_NUM" class="v9-input">
191
+ </div>
192
+ </div>
193
+ <div class="v9-dialog__footer">
194
+ <button class="v9-btn v9-btn--default" @click="dlg.groupForm = false">取消</button>
195
+ <button class="v9-btn v9-btn--primary" @click="saveGroupForm">保存</button>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ <!-- ================= 弹窗:添加角色(按系统分组 + 单组归属置灰) ================= -->
201
+ <div v-if="dlg.pickRole" class="v9-overlay" @click.self="dlg.pickRole = false">
202
+ <div class="v9-dialog v9-dialog--lg">
203
+ <div class="v9-dialog__header">
204
+ <span>向「{{ curGroup && curGroup.NAME }}」添加角色</span>
205
+ <span class="v9-dialog__close" @click="dlg.pickRole = false"><i class="el-icon-close"></i></span>
206
+ </div>
207
+ <div class="v9-dialog__body">
208
+ <div class="v9-form-item__tip pick-tip">按系统分组展示本租户全部可选角色(含租户公共角色),可跨系统勾选;已属于其他组的角色置灰,保存时后端同样会校验拦截(角色单组归属)。</div>
209
+ <div class="pick-scroll">
210
+ <div v-for="sys in selectableRoles" :key="sys.SYSTEM" class="pick-group">
211
+ <div class="pick-group__title"><i :class="sys.icon"></i>{{ sys.SYSTEM_NAME }}</div>
212
+ <label
213
+ v-for="r in sys.roles"
214
+ :key="r.ID"
215
+ :class="['pick-item', { 'is-disabled': r.disabled }]"
216
+ >
217
+ <input
218
+ v-model="selRoleIds"
219
+ type="checkbox"
220
+ :value="r.ID"
221
+ :disabled="r.disabled"
222
+ >
223
+ <span class="pick-item__name">{{ r.NAME }}</span>
224
+ <span class="v9-tag v9-tag--info">{{ r.CODE }}</span>
225
+ <span v-if="r.inGroup" class="v9-tag v9-tag--success">已在组内</span>
226
+ <span v-else-if="r.ownerGroupName" class="v9-tag v9-tag--warning">已属组 {{ r.ownerGroupName }}</span>
227
+ </label>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ <div class="v9-dialog__footer">
232
+ <span class="v9-form-item__tip pick-count">{{ pickCountText }}</span>
233
+ <button class="v9-btn v9-btn--default" @click="dlg.pickRole = false">取消</button>
234
+ <button class="v9-btn v9-btn--primary" @click="savePickRole">加入组内</button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- ================= 弹窗:移除角色(移出并收回,按位) ================= -->
240
+ <div v-if="dlg.removeRole" class="v9-overlay" @click.self="dlg.removeRole = false">
241
+ <div class="v9-dialog">
242
+ <div class="v9-dialog__header">
243
+ <span>移出并收回:「{{ rmRole.label }}」</span>
244
+ <span class="v9-dialog__close" @click="dlg.removeRole = false"><i class="el-icon-close"></i></span>
245
+ </div>
246
+ <div class="v9-dialog__body">
247
+ <div class="tipbar tipbar--warn">
248
+ <i class="el-icon-warning-outline"></i>
249
+ <div class="tipbar__main">该角色已通过本组授予 <b>{{ rmRole.total }}</b> 个用户({{ rmRole.total - rmRole.withManual }} 条仅组来源、{{ rmRole.withManual }} 条另有手工授予)</div>
250
+ </div>
251
+ <div class="v9-form-item__tip">移出后按位收回该角色的组来源:同时有手工授予的绑定仅清除组来源、记录保留;仅组来源的绑定整条删除、权限收回。</div>
252
+ </div>
253
+ <div class="v9-dialog__footer">
254
+ <button class="v9-btn v9-btn--default" @click="dlg.removeRole = false">取消</button>
255
+ <button class="v9-btn v9-btn--primary" @click="doRemoveRole">移出并收回</button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <!-- ================= 弹窗:删除角色组 ================= -->
261
+ <div v-if="dlg.delGroup" class="v9-overlay" @click.self="dlg.delGroup = false">
262
+ <div class="v9-dialog">
263
+ <div class="v9-dialog__header">
264
+ <span>删除角色组「{{ delGroupData.NAME }}」</span>
265
+ <span class="v9-dialog__close" @click="dlg.delGroup = false"><i class="el-icon-close"></i></span>
266
+ </div>
267
+ <div class="v9-dialog__body">
268
+ <div class="tipbar tipbar--danger del-tipbar">
269
+ <i class="el-icon-warning-outline"></i>
270
+ <div class="tipbar__main">删除组不可恢复。已通过本组授予的授权将按位收回:</div>
271
+ </div>
272
+ <table class="v9-desc-table del-impact">
273
+ <tbody>
274
+ <tr><th>仅组来源绑定</th><td>{{ delGroupData.onlyGroup }} 条 → 整条删除(权限收回)</td></tr>
275
+ <tr><th>组 + 手工并存绑定</th><td>{{ delGroupData.both }} 条 → 仅清除组来源,手工授权保留</td></tr>
276
+ <tr><th>涉及用户</th><td>{{ delGroupData.userCount }} 人</td></tr>
277
+ </tbody>
278
+ </table>
279
+ <div class="v9-form-item__tip">同时持有手工授予的绑定仅清除组来源、记录保留,手工授权不受影响;仅组来源的绑定整条删除。</div>
280
+ </div>
281
+ <div class="v9-dialog__footer">
282
+ <button class="v9-btn v9-btn--default" @click="dlg.delGroup = false">取消</button>
283
+ <button class="v9-btn v9-btn--danger" @click="doDelGroup"><i class="el-icon-delete"></i> 删除并收回授权</button>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <!-- ================= 弹窗:向组添加用户 ================= -->
289
+ <div v-if="dlg.addUser" class="v9-overlay" @click.self="dlg.addUser = false">
290
+ <div class="v9-dialog v9-dialog--md">
291
+ <div class="v9-dialog__header">
292
+ <span>向「{{ curGroup && curGroup.NAME }}」添加用户</span>
293
+ <span class="v9-dialog__close" @click="dlg.addUser = false"><i class="el-icon-close"></i></span>
294
+ </div>
295
+ <div class="v9-dialog__body">
296
+ <div class="v9-form-item__tip pick-tip">选中用户将立即获得组内全部角色(物化展开写入用户角色绑定,权限即时生效);已手工授予的角色会并列保留手工来源,不跳过、不覆盖。</div>
297
+ <div class="v9-input-wrap au-search">
298
+ <span class="v9-input-prefix"><i class="el-icon-search"></i></span>
299
+ <input v-model="addUserKeyword" class="v9-input" placeholder="输入姓名、工号过滤" @input="filterAddUser">
300
+ </div>
301
+ <div class="pick-scroll au-scroll" v-loading="addUserLoading">
302
+ <template v-if="selectableUsers.length">
303
+ <label
304
+ v-for="u in selectableUsers"
305
+ :key="u.ID"
306
+ :class="['pick-item', { 'is-disabled': u.inGroup }]"
307
+ >
308
+ <input
309
+ v-model="selUserIds"
310
+ type="checkbox"
311
+ :value="u.ID"
312
+ :disabled="u.inGroup"
313
+ >
314
+ <span class="pick-item__name">{{ u.NAME }}</span>
315
+ <span class="v9-tag v9-tag--info">{{ u.USER_NO }}</span>
316
+ <span class="v9-form-item__tip grp-inline-tip">{{ u.DEPT_NAME }}</span>
317
+ <span v-if="u.inGroup" class="v9-tag v9-tag--success">已在本组</span>
318
+ </label>
319
+ </template>
320
+ <div v-else class="v9-empty">
321
+ <i class="el-icon-search v9-empty__icon"></i>
322
+ <div>没有匹配的用户</div>
323
+ </div>
324
+ </div>
325
+ <el-pagination
326
+ v-if="addUserTotal > addUserLimit"
327
+ small
328
+ layout="prev, pager, next"
329
+ v-model:current-page="addUserPage"
330
+ :page-size="addUserLimit"
331
+ :total="addUserTotal"
332
+ @current-change="handleAddUserPage"
333
+ />
334
+ </div>
335
+ <div class="v9-dialog__footer">
336
+ <span class="v9-form-item__tip pick-count">{{ userCountText }}</span>
337
+ <button class="v9-btn v9-btn--default" @click="dlg.addUser = false">取消</button>
338
+ <button class="v9-btn v9-btn--primary" @click="doAddUsers"><i class="el-icon-circle-plus"></i> 授予组内全部角色</button>
339
+ </div>
340
+ </div>
341
+ </div>
342
+
343
+ <!-- ================= 弹窗:从组移除用户 ================= -->
344
+ <div v-if="dlg.removeUser" class="v9-overlay" @click.self="dlg.removeUser = false">
345
+ <div class="v9-dialog">
346
+ <div class="v9-dialog__header">
347
+ <span>从「{{ curGroup && curGroup.NAME }}」移除「{{ rmUser.label }}」</span>
348
+ <span class="v9-dialog__close" @click="dlg.removeUser = false"><i class="el-icon-close"></i></span>
349
+ </div>
350
+ <div class="v9-dialog__body">
351
+ <table class="v9-desc-table ru-impact">
352
+ <tbody>
353
+ <tr><th>通过本组持有的角色</th><td>{{ rmUser.total }} 个</td></tr>
354
+ <tr><th>仅组来源</th><td>{{ rmUser.total - rmUser.withManual }} 个 → 整条删除(权限收回)</td></tr>
355
+ <tr><th>另有手工授予</th><td>{{ rmUser.withManual }} 个 → 仅清除组来源,手工权限保留</td></tr>
356
+ </tbody>
357
+ </table>
358
+ <div class="v9-form-item__tip">按位收回:另有手工授予的角色仅清除组来源、手工权限保留;仅组来源的角色整条删除、权限收回。</div>
359
+ </div>
360
+ <div class="v9-dialog__footer">
361
+ <button class="v9-btn v9-btn--default" @click="dlg.removeUser = false">取消</button>
362
+ <button class="v9-btn v9-btn--danger" @click="doRemoveUser"><i class="el-icon-delete"></i> 移除并收回</button>
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <!-- 轻提示 -->
368
+ <div class="toast-box">
369
+ <div v-if="toast.show" :class="['v9-message', toastClass]">
370
+ <i :class="toastIcon" class="v9-message__icon"></i>
371
+ <span>{{ toast.text }}</span>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </template>
376
+
377
+ <script>
378
+ import component from '@epxing/core/src/mixins/component.js'
379
+
380
+ export default {
381
+ name: 'epx-role-group-manage',
382
+ mixins: [component],
383
+ data () {
384
+ return {
385
+ curGroupId: '', // 当前查看的组
386
+ groupList: [], // 组列表(含 roleCount / userCount 聚合)
387
+ curGroup: null, // 当前组基本信息
388
+ detail: { sysCount: 0 }, // 组详情附加统计
389
+ groupRoles: [], // 组内角色
390
+ groupUsers: [], // 已授权用户
391
+ activeTab: 'roles', // 当前页签
392
+ pendingApply: [], // 本次新增待重新应用的角色
393
+ // 弹窗开关
394
+ dlg: {
395
+ groupForm: false,
396
+ pickRole: false,
397
+ removeRole: false,
398
+ delGroup: false,
399
+ addUser: false,
400
+ removeUser: false
401
+ },
402
+ // 新建/编辑表单
403
+ formGroup: { ID: '', NAME: '', CODE: '', DESC: '', ORDER_NUM: 100 },
404
+ formErr: { name: false, code: false },
405
+ // 添加角色
406
+ selectableRoles: [], // 按系统分组
407
+ selRoleIds: [],
408
+ // 移除角色
409
+ rmRole: { id: '', label: '', total: 0, withManual: 0 },
410
+ // 删除组
411
+ delGroupData: { ID: '', NAME: '', onlyGroup: 0, both: 0, userCount: 0 },
412
+ // 添加用户(后端分页,避免租户用户量大时全量渲染拖垮页面)
413
+ selectableUsers: [],
414
+ selUserIds: [],
415
+ addUserKeyword: '',
416
+ addUserTotal: 0,
417
+ addUserPage: 1,
418
+ addUserLimit: 50,
419
+ addUserLoading: false,
420
+ addUserTimer: null,
421
+ // 移除用户
422
+ rmUser: { id: '', label: '', total: 0, withManual: 0 },
423
+ // 轻提示
424
+ toast: { show: false, type: 'success', text: '' },
425
+ toastTimer: null
426
+ }
427
+ },
428
+ computed: {
429
+ pickCountText () {
430
+ return this.selRoleIds.length ? '已选 ' + this.selRoleIds.length + ' 个角色' : '尚未选择角色'
431
+ },
432
+ userCountText () {
433
+ return this.selUserIds.length ? '已选 ' + this.selUserIds.length + ' 个用户' : '尚未选择用户'
434
+ },
435
+ toastClass () {
436
+ return { success: 'v9-message--success', error: 'v9-message--error', warn: 'v9-message--warning', info: 'v9-message--info' }[this.toast.type] || 'v9-message--info'
437
+ },
438
+ toastIcon () {
439
+ return { success: 'el-icon-success', error: 'el-icon-error', warn: 'el-icon-warning-outline', info: 'el-icon-info' }[this.toast.type] || 'el-icon-info'
440
+ }
441
+ },
442
+ created () {
443
+ this.getGroupList(() => {
444
+ // 支持从绑定追溯页带组 ID 进入
445
+ const qid = this.$query && this.$query.groupId
446
+ if (qid && this.groupList.some(g => g.ID === qid)) {
447
+ this.selectGroup(qid)
448
+ } else if (this.groupList.length) {
449
+ this.selectGroup(this.groupList[0].ID)
450
+ }
451
+ })
452
+ },
453
+ methods: {
454
+ msg (type, text) {
455
+ this.toast = { show: true, type, text }
456
+ clearTimeout(this.toastTimer)
457
+ this.toastTimer = setTimeout(() => { this.toast.show = false }, type === 'error' ? 6000 : 3600)
458
+ },
459
+ // ---------- 数据加载 ----------
460
+ getGroupList (cb) {
461
+ this.$http.$request({ action: 'getGroupList', entityId: 'FM_ROLE_GROUP' })
462
+ .then(data => {
463
+ this.groupList = data.groups || []
464
+ cb && cb()
465
+ })
466
+ .catch(err => this.msg('error', err.message || '获取角色组列表失败'))
467
+ },
468
+ getGroupDetail () {
469
+ if (!this.curGroupId) return
470
+ this.$http.$request({ action: 'getGroupDetail', entityId: 'FM_ROLE_GROUP', groupId: this.curGroupId })
471
+ .then(data => {
472
+ this.curGroup = data.group || null
473
+ this.groupRoles = data.roles || []
474
+ this.groupUsers = data.users || []
475
+ this.detail.sysCount = (data.stats && data.stats.sysCount) || 0
476
+ this.pendingApply = data.pendingApply || []
477
+ })
478
+ .catch(err => this.msg('error', err.message || '获取组详情失败'))
479
+ },
480
+ selectGroup (gid) {
481
+ this.curGroupId = gid
482
+ this.activeTab = 'roles'
483
+ this.getGroupList()
484
+ this.getGroupDetail()
485
+ },
486
+ switchGroupTab (tab) {
487
+ this.activeTab = tab
488
+ },
489
+ // ---------- 新建 / 编辑组 ----------
490
+ openGroupForm (g) {
491
+ this.formGroup = g
492
+ ? { ID: g.ID, NAME: g.NAME, CODE: g.CODE, DESC: g.DESC || '', ORDER_NUM: g.ORDER_NUM }
493
+ : { ID: '', NAME: '', CODE: '', DESC: '', ORDER_NUM: 100 }
494
+ this.formErr = { name: false, code: false }
495
+ this.dlg.groupForm = true
496
+ },
497
+ saveGroupForm () {
498
+ const name = (this.formGroup.NAME || '').trim()
499
+ const code = (this.formGroup.CODE || '').trim()
500
+ this.formErr.name = !name
501
+ this.formErr.code = !/^[A-Za-z][A-Za-z0-9_]*$/.test(code)
502
+ if (this.formErr.name || this.formErr.code) return
503
+ this.$http.$request({
504
+ action: 'saveGroup',
505
+ entityId: 'FM_ROLE_GROUP',
506
+ 'bean.ID': this.formGroup.ID || '',
507
+ 'bean.NAME': name,
508
+ 'bean.CODE': code,
509
+ 'bean.DESC': (this.formGroup.DESC || '').trim(),
510
+ 'bean.ORDER_NUM': parseInt(this.formGroup.ORDER_NUM, 10) || 0
511
+ })
512
+ .then(() => {
513
+ const isNew = !this.formGroup.ID
514
+ this.dlg.groupForm = false
515
+ this.getGroupList(() => {
516
+ if (isNew) {
517
+ // 新建后定位到新组,引导添加角色
518
+ const created = this.groupList[this.groupList.length - 1]
519
+ if (created) this.selectGroup(created.ID)
520
+ } else {
521
+ this.getGroupDetail()
522
+ }
523
+ })
524
+ this.msg('success', '角色组「' + name + '」' + (isNew ? '已创建,接下来向组内添加角色' : '已保存'))
525
+ })
526
+ .catch(err => {
527
+ // 后端唯一性校验拦截
528
+ const msgText = err.message || ''
529
+ if (msgText.indexOf('名称') >= 0) this.formErr.name = true
530
+ if (msgText.indexOf('编码') >= 0) this.formErr.code = true
531
+ this.msg('error', msgText || '保存失败')
532
+ })
533
+ },
534
+ // ---------- 添加角色 ----------
535
+ openPickRole () {
536
+ this.selRoleIds = []
537
+ this.$http.$request({ action: 'getSelectableRoles', entityId: 'FM_ROLE_GROUP', groupId: this.curGroupId })
538
+ .then(data => {
539
+ this.selectableRoles = data.systems || []
540
+ this.dlg.pickRole = true
541
+ })
542
+ .catch(err => this.msg('error', err.message || '获取可选角色失败'))
543
+ },
544
+ savePickRole () {
545
+ if (!this.selRoleIds.length) { this.msg('warn', '请先勾选要加入的角色'); return }
546
+ this.$http.$request({
547
+ action: 'addRolesToGroup',
548
+ entityId: 'FM_ROLE_GROUP',
549
+ groupId: this.curGroupId,
550
+ roleIds: this.selRoleIds.join(',')
551
+ })
552
+ .then(data => {
553
+ this.dlg.pickRole = false
554
+ this.getGroupList()
555
+ this.getGroupDetail()
556
+ this.msg('success', '已加入 ' + this.selRoleIds.length + ' 个角色' + (data.appliedCreated ? ',已自动补授给本组已授权用户(新建绑定 ' + data.appliedCreated + ' 条)' : ''))
557
+ })
558
+ .catch(err => this.msg('error', err.message || '保存被拦截(角色单组归属校验)'))
559
+ },
560
+ // ---------- 移除角色(按位收回) ----------
561
+ openRemoveRole (r) {
562
+ this.$http.$request({
563
+ action: 'getRemoveRoleImpact',
564
+ entityId: 'FM_ROLE_GROUP',
565
+ groupId: this.curGroupId,
566
+ roleId: r.ID
567
+ })
568
+ .then(data => {
569
+ this.rmRole = { id: r.ID, label: r.NAME + '(' + r.SYSTEM_NAME + ')', total: data.total || 0, withManual: data.withManual || 0 }
570
+ this.dlg.removeRole = true
571
+ })
572
+ .catch(err => this.msg('error', err.message || '获取影响统计失败'))
573
+ },
574
+ doRemoveRole () {
575
+ this.$http.$request({
576
+ action: 'removeRoleFromGroup',
577
+ entityId: 'FM_ROLE_GROUP',
578
+ groupId: this.curGroupId,
579
+ roleId: this.rmRole.id
580
+ })
581
+ .then(data => {
582
+ this.dlg.removeRole = false
583
+ this.getGroupList()
584
+ this.getGroupDetail()
585
+ this.msg('success', '「' + this.rmRole.label + '」已移出并收回组来源:整条删除 ' + (data.removed || 0) + ' 条;另有手工授予的 ' + (data.kept || 0) + ' 条仅清组位、绑定保留', 6000)
586
+ })
587
+ .catch(err => this.msg('error', err.message || '移除失败'))
588
+ },
589
+ // ---------- 重新应用(差量补绑) ----------
590
+ applyGroup () {
591
+ this.$http.$request({ action: 'applyGroup', entityId: 'FM_ROLE_GROUP', groupId: this.curGroupId })
592
+ .then(data => {
593
+ this.getGroupDetail()
594
+ this.msg('success', '已按发起时刻的组内角色与用户快照补绑:新建 ' + (data.created || 0) + ' 条,判重跳过 ' + (data.skipped || 0) + ' 条')
595
+ })
596
+ .catch(err => this.msg('error', err.message || '重新应用失败'))
597
+ },
598
+ // ---------- 删除组(按位收回) ----------
599
+ openDelGroup (g) {
600
+ this.$http.$request({ action: 'getDeleteGroupImpact', entityId: 'FM_ROLE_GROUP', groupId: g.ID })
601
+ .then(data => {
602
+ this.delGroupData = {
603
+ ID: g.ID,
604
+ NAME: g.NAME,
605
+ onlyGroup: data.onlyGroup || 0,
606
+ both: data.both || 0,
607
+ userCount: data.userCount || 0
608
+ }
609
+ this.dlg.delGroup = true
610
+ })
611
+ .catch(err => this.msg('error', err.message || '获取影响统计失败'))
612
+ },
613
+ doDelGroup () {
614
+ this.$http.$request({ action: 'deleteGroup', entityId: 'FM_ROLE_GROUP', groupId: this.delGroupData.ID })
615
+ .then(data => {
616
+ this.dlg.delGroup = false
617
+ this.msg('success', '角色组「' + this.delGroupData.NAME + '」已删除:收回绑定 ' + (data.removed || 0) + ' 条,另有手工授予的 ' + (data.kept || 0) + ' 条仅清组位、记录保留', 6000)
618
+ this.curGroupId = ''
619
+ this.curGroup = null
620
+ this.getGroupList(() => {
621
+ if (this.groupList.length) this.selectGroup(this.groupList[0].ID)
622
+ })
623
+ })
624
+ .catch(err => this.msg('error', err.message || '删除失败'))
625
+ },
626
+ // ---------- 添加用户(物化展开) ----------
627
+ openAddUser () {
628
+ this.selUserIds = []
629
+ this.addUserKeyword = ''
630
+ this.addUserPage = 1
631
+ this.getSelectableUsers()
632
+ this.dlg.addUser = true
633
+ },
634
+ getSelectableUsers () {
635
+ this.addUserLoading = true
636
+ this.$http.$request({
637
+ action: 'getSelectableUsers',
638
+ entityId: 'FM_ROLE_GROUP',
639
+ groupId: this.curGroupId,
640
+ keyword: this.addUserKeyword,
641
+ page: this.addUserPage,
642
+ limit: this.addUserLimit
643
+ })
644
+ .then(data => {
645
+ this.selectableUsers = data.users || []
646
+ this.addUserTotal = data.total || 0
647
+ })
648
+ .catch(err => this.msg('error', err.message || '获取用户失败'))
649
+ .finally(() => { this.addUserLoading = false })
650
+ },
651
+ filterAddUser () {
652
+ // 输入防抖:每敲一键都全量检索会打爆后端
653
+ clearTimeout(this.addUserTimer)
654
+ this.addUserTimer = setTimeout(() => {
655
+ this.addUserPage = 1
656
+ this.getSelectableUsers()
657
+ }, 300)
658
+ },
659
+ handleAddUserPage (page) {
660
+ this.addUserPage = page
661
+ this.getSelectableUsers()
662
+ },
663
+ doAddUsers () {
664
+ if (!this.selUserIds.length) { this.msg('warn', '请先勾选要添加的用户'); return }
665
+ this.$http.$request({
666
+ action: 'addUsersToGroup',
667
+ entityId: 'FM_ROLE_GROUP',
668
+ groupId: this.curGroupId,
669
+ userIds: this.selUserIds.join(',')
670
+ })
671
+ .then(data => {
672
+ this.dlg.addUser = false
673
+ this.getGroupList()
674
+ this.getGroupDetail()
675
+ const both = data.both ? '其中 ' + data.both + ' 个角色另有手工授予,已并列保留(两来源并存)' : ''
676
+ this.msg('success', '已添加 ' + this.selUserIds.length + ' 个用户并展开组内角色:新建 ' + (data.created || 0) + ' 条,组位置为本组 ' + (data.moved || 0) + ' 条,判重跳过 ' + (data.skipped || 0) + ' 条。' + both, 6000)
677
+ })
678
+ .catch(err => this.msg('error', err.message || '添加失败'))
679
+ },
680
+ // ---------- 移除用户(按位收回) ----------
681
+ openRemoveUser (u) {
682
+ this.$http.$request({
683
+ action: 'getRemoveUserImpact',
684
+ entityId: 'FM_ROLE_GROUP',
685
+ groupId: this.curGroupId,
686
+ userId: u.ID
687
+ })
688
+ .then(data => {
689
+ this.rmUser = { id: u.ID, label: u.NAME + '(' + u.USER_NO + ')', total: data.total || 0, withManual: data.withManual || 0 }
690
+ this.dlg.removeUser = true
691
+ })
692
+ .catch(err => this.msg('error', err.message || '获取影响统计失败'))
693
+ },
694
+ doRemoveUser () {
695
+ this.$http.$request({
696
+ action: 'removeUserFromGroup',
697
+ entityId: 'FM_ROLE_GROUP',
698
+ groupId: this.curGroupId,
699
+ userId: this.rmUser.id
700
+ })
701
+ .then(data => {
702
+ this.dlg.removeUser = false
703
+ this.getGroupList()
704
+ this.getGroupDetail()
705
+ this.msg('success', '已移除该用户:整条删除 ' + (data.removed || 0) + ' 条;另有手工授予的 ' + (data.kept || 0) + ' 条仅清组位、绑定保留', 6000)
706
+ })
707
+ .catch(err => this.msg('error', err.message || '移除失败'))
708
+ }
709
+ }
710
+ }
711
+ </script>
712
+
713
+ <style lang="scss" scoped>
714
+ /* ===== 以下样式值均取自原型 V9 设计系统,保持逐项一致 ===== */
715
+ .rgm-page {
716
+ min-height: 100%;
717
+ padding: 24px;
718
+ background: var(--epx-bg-card, #f2f3f7);
719
+ font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
720
+ font-size: 14px;
721
+ line-height: 22px;
722
+ color: var(--epx-text, #444);
723
+ }
724
+
725
+ /* 左右布局 */
726
+ .split {
727
+ display: flex;
728
+ gap: 16px;
729
+ align-items: flex-start;
730
+ }
731
+ .split__left { flex: 0 0 280px; }
732
+ .split__right { flex: 1 1 auto; min-width: 0; }
733
+ @media (max-width: 992px) {
734
+ .split { flex-direction: column; }
735
+ .split__left { flex: none; width: 100%; }
736
+ }
737
+
738
+ /* 卡片 */
739
+ .v9-card {
740
+ background: var(--epx-bg-card, #fff);
741
+ border-radius: 4px;
742
+ padding: 24px;
743
+ }
744
+ /* 区块标题(主题色竖条) */
745
+ .v9-block-title {
746
+ display: flex;
747
+ align-items: center;
748
+ gap: 8px;
749
+ font-size: 16px;
750
+ font-weight: 600;
751
+ color: var(--epx-text, #222);
752
+ margin-bottom: 16px;
753
+ &::before {
754
+ content: '';
755
+ width: 4px;
756
+ height: 16px;
757
+ border-radius: 2px;
758
+ background: #0874e7;
759
+ }
760
+ }
761
+
762
+ /* 组列表项 */
763
+ .group-list { display: flex; flex-direction: column; gap: 2px; }
764
+ .grp-item {
765
+ display: flex;
766
+ align-items: center;
767
+ gap: 8px;
768
+ padding: 12px 16px;
769
+ border-radius: 4px;
770
+ cursor: pointer;
771
+ border: 1px solid transparent;
772
+ border-left: 3px solid transparent;
773
+ &:hover {
774
+ background: var(--epx-bg-card, #f5f7fa);
775
+ .grp-item__ops { visibility: visible; }
776
+ }
777
+ &.is-active {
778
+ background: var(--epx-bg, #e6f6ff);
779
+ border-left-color: var(--el-color-primary, #0874e7);
780
+ }
781
+ }
782
+ .grp-item__name { font-weight: 500; color: var(--epx-text, #222); }
783
+ .grp-item__meta { font-size: 12px; color: var(--epx-text-secondary, #888); margin-top: 2px; }
784
+ .grp-item__ops { margin-left: auto; visibility: hidden; white-space: nowrap; }
785
+ .grp-add-btn { width: 100%; margin-top: 12px; }
786
+ .grp-left-tip { margin-top: 8px; }
787
+
788
+ /* 按钮 */
789
+ .v9-btn {
790
+ display: inline-flex;
791
+ align-items: center;
792
+ justify-content: center;
793
+ gap: 6px;
794
+ height: 32px;
795
+ padding: 0 16px;
796
+ font-size: 14px;
797
+ line-height: 1;
798
+ border: 1px solid transparent;
799
+ border-radius: 4px;
800
+ cursor: pointer;
801
+ user-select: none;
802
+ white-space: nowrap;
803
+ transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
804
+ background: var(--epx-bg-card, #fff);
805
+ color: var(--epx-text, #444);
806
+ outline: none;
807
+ font-family: inherit;
808
+ &--default {
809
+ background: var(--epx-bg-card, #fff);
810
+ border-color: var(--epx-border, #dcdfe6);
811
+ color: var(--epx-text, #444);
812
+ &:hover { color: var(--el-color-primary, #0874e7); border-color: var(--epx-border, #82cbff); background: var(--epx-bg, #e6f6ff); }
813
+ }
814
+ &--primary {
815
+ background: #0874e7;
816
+ color: #fff;
817
+ &:hover { background: var(--epx-bg, #2f95f5); }
818
+ &:active { background: #0057c2; }
819
+ }
820
+ &--danger {
821
+ background: var(--epx-bg, #f56c6c);
822
+ color: #fff;
823
+ &:hover { background: var(--epx-bg, #f78989); }
824
+ &:active { background: var(--epx-bg, #dd6161); }
825
+ }
826
+ &--text {
827
+ background: transparent;
828
+ border-color: transparent;
829
+ color: var(--el-color-primary, #0874e7);
830
+ padding: 0 4px;
831
+ height: auto;
832
+ &:hover { color: var(--epx-text-secondary, #2f95f5); background: transparent; }
833
+ &.is-danger { color: var(--el-color-danger, #f56c6c); }
834
+ }
835
+ }
836
+
837
+ /* 提示条 */
838
+ .tipbar {
839
+ display: flex;
840
+ align-items: center;
841
+ gap: 8px;
842
+ padding: 12px 16px;
843
+ border-radius: 4px;
844
+ font-size: 14px;
845
+ margin-bottom: 16px;
846
+ &--info { background: var(--epx-bg, #e6f6ff); color: var(--el-color-primary, #0874e7); }
847
+ &--warn { background: var(--epx-bg-card, #fdf6ec); color: var(--epx-text-secondary, #e6a23a); }
848
+ &--danger { background: var(--epx-bg-card, #fef0f0); color: var(--el-color-danger, #f56c6c); }
849
+ .tipbar__main { flex: 1; }
850
+ b { font-weight: 600; }
851
+ }
852
+
853
+ /* 数字统计卡 */
854
+ .v9-figure-grid {
855
+ display: grid;
856
+ grid-template-columns: repeat(3, minmax(0, 1fr));
857
+ gap: 12px;
858
+ }
859
+ .v9-figure {
860
+ background: var(--epx-bg-card, #f5f7fa);
861
+ border-radius: 4px;
862
+ padding: 16px;
863
+ min-width: 0;
864
+ transition: box-shadow 0.15s, background 0.15s;
865
+ &:hover { box-shadow: 0 4px 12px rgba(17, 24, 39, 0.08), 0 2px 6px -2px rgba(17, 24, 39, 0.05); }
866
+ }
867
+ .v9-figure__label {
868
+ font-size: 12px;
869
+ color: var(--epx-text-secondary, #888);
870
+ margin-bottom: 4px;
871
+ white-space: nowrap;
872
+ overflow: hidden;
873
+ text-overflow: ellipsis;
874
+ }
875
+ .v9-figure__value {
876
+ font-size: 20px;
877
+ font-weight: 600;
878
+ color: var(--epx-text, #222);
879
+ line-height: 28px;
880
+ }
881
+ @media (max-width: 1100px) {
882
+ .v9-figure-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
883
+ }
884
+
885
+ /* 下划线页签 */
886
+ .v9-tabs {
887
+ display: flex;
888
+ gap: 24px;
889
+ border-bottom: 1px solid var(--epx-border, #e4e7ed);
890
+ }
891
+ .v9-tabs__item {
892
+ position: relative;
893
+ padding: 12px 0;
894
+ cursor: pointer;
895
+ color: var(--epx-text, #444);
896
+ &.is-active {
897
+ color: var(--el-color-primary, #0874e7);
898
+ &::after {
899
+ content: '';
900
+ position: absolute;
901
+ left: 0;
902
+ right: 0;
903
+ bottom: -1px;
904
+ height: 2px;
905
+ background: #0874e7;
906
+ }
907
+ }
908
+ }
909
+ .grp-tab-body { margin-top: 16px; }
910
+ .grp-tab-toolbar { margin-bottom: 12px; display: flex; align-items: center; }
911
+ .grp-inline-tip { display: inline; margin-left: 12px; margin-top: 0; }
912
+ .grp-sys-icon { margin-right: 4px; }
913
+ .grp-table-foot-tip { margin-top: 8px; }
914
+ .grp-edit-row { margin-top: 12px; }
915
+
916
+ /* 表格 */
917
+ .v9-table {
918
+ width: 100%;
919
+ border-collapse: collapse;
920
+ background: var(--epx-bg-card, #fff);
921
+ font-size: 14px;
922
+ th, td {
923
+ padding: 0 16px;
924
+ height: 48px;
925
+ text-align: left;
926
+ border-bottom: 1px solid var(--epx-border, #e4e7ed);
927
+ }
928
+ thead th {
929
+ background: var(--epx-bg-card, #f2f3f7);
930
+ color: var(--epx-text, #444);
931
+ font-weight: 500;
932
+ white-space: nowrap;
933
+ }
934
+ tbody td { color: var(--epx-text, #444); }
935
+ tbody tr:hover > td { background: var(--epx-bg, #e6f6ff); }
936
+ }
937
+
938
+ /* 描述型表格 */
939
+ .v9-desc-table {
940
+ width: 100%;
941
+ border-collapse: collapse;
942
+ border: 1px solid var(--epx-border, #e4e7ed);
943
+ th {
944
+ width: 120px;
945
+ text-align: right;
946
+ padding: 12px 16px;
947
+ background: var(--epx-bg-card, #f5f7fa);
948
+ color: var(--epx-text, #444);
949
+ font-weight: 400;
950
+ border: 1px solid var(--epx-border, #e4e7ed);
951
+ vertical-align: middle;
952
+ }
953
+ td {
954
+ padding: 12px 16px;
955
+ color: var(--epx-text, #222);
956
+ border: 1px solid var(--epx-border, #e4e7ed);
957
+ vertical-align: middle;
958
+ }
959
+ }
960
+
961
+ /* 标签 */
962
+ .v9-tag {
963
+ display: inline-flex;
964
+ align-items: center;
965
+ height: 22px;
966
+ padding: 0 8px;
967
+ font-size: 12px;
968
+ border-radius: 4px;
969
+ border: 1px solid transparent;
970
+ line-height: 1;
971
+ &--primary { background: var(--epx-bg, #e6f6ff); color: var(--el-color-primary, #0874e7); border-color: var(--epx-border, #82cbff); }
972
+ &--success { background: var(--epx-bg-card, #f0f9eb); color: var(--epx-text-secondary, #67c23a); border-color: var(--epx-border, #c2e7b0); }
973
+ &--warning { background: var(--epx-bg-card, #fdf6ec); color: var(--epx-text-secondary, #e6a23a); border-color: var(--epx-border, #f5dab1); }
974
+ &--danger { background: var(--epx-bg-card, #fef0f0); color: var(--el-color-danger, #f56c6c); border-color: var(--epx-border, #fab6b6); }
975
+ &--info { background: var(--epx-bg-card, #f4f4f5); color: var(--epx-text-secondary, #888); border-color: var(--epx-border, #e9e9eb); }
976
+ }
977
+
978
+ /* 空状态 */
979
+ .v9-empty {
980
+ display: flex;
981
+ flex-direction: column;
982
+ align-items: center;
983
+ justify-content: center;
984
+ padding: 40px 0;
985
+ color: var(--epx-text-secondary, #888);
986
+ gap: 12px;
987
+ }
988
+ .v9-empty__icon { font-size: 64px; color: var(--epx-text-secondary, #c0c4cc); }
989
+
990
+ /* 表单 */
991
+ .v9-form-item { margin-bottom: 16px; }
992
+ .v9-form-item__label {
993
+ display: block;
994
+ margin-bottom: 8px;
995
+ font-size: 14px;
996
+ color: var(--epx-text, #444);
997
+ &.is-required::before {
998
+ content: '*';
999
+ color: var(--el-color-danger, #f56c6c);
1000
+ margin-right: 4px;
1001
+ }
1002
+ }
1003
+ .v9-form-item__error { margin-top: 4px; font-size: 12px; color: var(--el-color-danger, #f56c6c); }
1004
+ .v9-form-item__tip { margin-top: 4px; font-size: 12px; color: var(--epx-text-secondary, #888); }
1005
+
1006
+ /* 输入框 */
1007
+ .v9-input {
1008
+ width: 100%;
1009
+ height: 32px;
1010
+ padding: 0 12px;
1011
+ font-size: 14px;
1012
+ color: var(--epx-text, #444);
1013
+ background: var(--epx-bg-card, #fff);
1014
+ border: 1px solid var(--epx-border, #dcdfe6);
1015
+ border-radius: 4px;
1016
+ outline: none;
1017
+ font-family: inherit;
1018
+ transition: border-color 0.15s, box-shadow 0.15s;
1019
+ &::placeholder { color: var(--epx-text-secondary, #aaa); }
1020
+ &:hover { border-color: var(--epx-border, #c0c4cc); }
1021
+ &:focus { border-color: var(--epx-border, #0874e7); box-shadow: 0 0 0 3px rgba(8, 116, 231, 0.15); }
1022
+ &.is-error { border-color: var(--epx-border, #f56c6c); }
1023
+ }
1024
+ .v9-input-wrap {
1025
+ position: relative;
1026
+ display: inline-flex;
1027
+ align-items: center;
1028
+ width: 100%;
1029
+ .v9-input { padding-left: 32px; }
1030
+ .v9-input-prefix {
1031
+ position: absolute;
1032
+ top: 50%;
1033
+ transform: translateY(-50%);
1034
+ left: 12px;
1035
+ display: inline-flex;
1036
+ align-items: center;
1037
+ height: 16px;
1038
+ font-size: 16px;
1039
+ line-height: 1;
1040
+ color: var(--epx-text-secondary, #888);
1041
+ pointer-events: none;
1042
+ }
1043
+ }
1044
+
1045
+ /* 弹窗 */
1046
+ .v9-overlay {
1047
+ position: fixed;
1048
+ top: 0;
1049
+ right: 0;
1050
+ bottom: 0;
1051
+ left: 0;
1052
+ z-index: 2000;
1053
+ background: rgba(0, 0, 0, 0.5);
1054
+ display: flex;
1055
+ align-items: flex-start;
1056
+ justify-content: center;
1057
+ padding-top: 15vh;
1058
+ }
1059
+ .v9-dialog {
1060
+ background: var(--epx-bg-card, #fff);
1061
+ width: 520px;
1062
+ max-width: 90vw;
1063
+ border-radius: 8px;
1064
+ box-shadow: 0 12px 32px rgba(17, 24, 39, 0.10), 0 6px 12px -4px rgba(17, 24, 39, 0.06);
1065
+ overflow: hidden;
1066
+ &--md { width: 680px; }
1067
+ &--lg { width: 900px; }
1068
+ }
1069
+ .v9-dialog__header {
1070
+ height: 56px;
1071
+ display: flex;
1072
+ align-items: center;
1073
+ padding: 0 24px;
1074
+ font-size: 16px;
1075
+ font-weight: 600;
1076
+ color: var(--epx-text, #222);
1077
+ }
1078
+ .v9-dialog__close {
1079
+ margin-left: auto;
1080
+ cursor: pointer;
1081
+ color: var(--epx-text-secondary, #888);
1082
+ font-size: 18px;
1083
+ }
1084
+ .v9-dialog__body {
1085
+ padding: 20px 24px;
1086
+ color: var(--epx-text, #444);
1087
+ }
1088
+ .v9-dialog__footer {
1089
+ height: 56px;
1090
+ display: flex;
1091
+ align-items: center;
1092
+ justify-content: flex-end;
1093
+ gap: 12px;
1094
+ padding: 0 24px;
1095
+ }
1096
+
1097
+ /* 角色选择器(按系统分组) */
1098
+ .pick-tip { margin-bottom: 12px; margin-top: 0; }
1099
+ .pick-scroll { max-height: 380px; overflow: auto; }
1100
+ .pick-group { margin-bottom: 16px; }
1101
+ .pick-group__title {
1102
+ display: flex;
1103
+ align-items: center;
1104
+ gap: 8px;
1105
+ font-weight: 500;
1106
+ color: var(--epx-text, #222);
1107
+ padding: 8px 0;
1108
+ border-bottom: 1px solid var(--epx-border, #e4e7ed);
1109
+ margin-bottom: 8px;
1110
+ i { color: var(--epx-text, #5c6066); }
1111
+ }
1112
+ .pick-item {
1113
+ display: flex;
1114
+ align-items: center;
1115
+ gap: 8px;
1116
+ padding: 8px 8px 8px 16px;
1117
+ border-radius: 4px;
1118
+ cursor: pointer;
1119
+ &:hover { background: var(--epx-bg-card, #f5f7fa); }
1120
+ &.is-disabled {
1121
+ color: var(--epx-text-secondary, #aaa);
1122
+ cursor: not-allowed;
1123
+ background: transparent;
1124
+ &:hover { background: transparent; }
1125
+ }
1126
+ }
1127
+ .pick-item__name { min-width: 120px; }
1128
+ .pick-count { flex: 1; margin-top: 0; }
1129
+
1130
+ /* 添加用户 */
1131
+ .au-search { margin-bottom: 12px; }
1132
+ .au-scroll { max-height: 320px; }
1133
+ .ru-impact { margin-bottom: 16px; }
1134
+ .del-tipbar { margin-bottom: 12px; }
1135
+ .del-impact { margin-bottom: 12px; }
1136
+
1137
+ /* 轻提示 */
1138
+ .toast-box {
1139
+ position: fixed;
1140
+ top: 76px;
1141
+ left: 50%;
1142
+ transform: translateX(-50%);
1143
+ z-index: 3000;
1144
+ max-width: 640px;
1145
+ }
1146
+ .v9-message {
1147
+ display: inline-flex;
1148
+ align-items: center;
1149
+ gap: 8px;
1150
+ max-width: 640px;
1151
+ padding: 10px 16px;
1152
+ border-radius: 4px;
1153
+ font-size: 14px;
1154
+ box-shadow: 0 4px 12px rgba(17, 24, 39, 0.08), 0 2px 6px -2px rgba(17, 24, 39, 0.05);
1155
+ &--success { background: var(--epx-bg-card, #f0f9eb); color: var(--epx-text, #444); .v9-message__icon { color: var(--epx-text-secondary, #67c23a); } }
1156
+ &--warning { background: var(--epx-bg-card, #fdf6ec); color: var(--epx-text, #444); .v9-message__icon { color: var(--epx-text-secondary, #e6a23a); } }
1157
+ &--error { background: var(--epx-bg-card, #fef0f0); color: var(--epx-text, #444); .v9-message__icon { color: var(--el-color-danger, #f56c6c); } }
1158
+ &--info { background: var(--epx-bg, #edf2fc); color: var(--epx-text, #444); .v9-message__icon { color: var(--el-color-primary, #0874e7); } }
1159
+ }
1160
+ </style>