RoleAssignUserForm.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <template>
  2. <Dialog v-model="dialogVisible" title="分配用户" width="780">
  3. <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
  4. <el-form-item label="角色名称">
  5. <el-tag>{{ formData.name }}</el-tag>
  6. </el-form-item>
  7. <el-form-item label="角色标识">
  8. <el-tag>{{ formData.code }}</el-tag>
  9. </el-form-item>
  10. <el-form-item label="选择用户" class="transfer-form-item">
  11. <div class="transfer-wrapper">
  12. <el-transfer
  13. v-model="selectedUserIdList"
  14. :titles="['未选用户', '已选用户']"
  15. filterable
  16. filter-placeholder="搜索用户"
  17. :data="userList"
  18. :props="{ label: 'nickname', key: 'id' }"
  19. />
  20. </div>
  21. </el-form-item>
  22. </el-form>
  23. <template #footer>
  24. <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
  25. <el-button @click="dialogVisible = false">取 消</el-button>
  26. </template>
  27. </Dialog>
  28. </template>
  29. <script lang="ts" setup>
  30. import * as RoleApi from '@/api/system/role'
  31. import * as UserApi from '@/api/system/user'
  32. defineOptions({ name: 'SystemRoleAssignUserForm' })
  33. const message = useMessage()
  34. const dialogVisible = ref(false)
  35. const formLoading = ref(false)
  36. const formData = reactive({
  37. roleId: undefined as number | undefined,
  38. name: '',
  39. code: ''
  40. })
  41. const formRef = ref()
  42. // 所有用户列表(transfer 数据源)
  43. const userList = ref<any[]>([])
  44. // 已选中的用户 ID 列表
  45. const selectedUserIdList = ref<string[]>([])
  46. // 打开弹窗时的原始已选用户 ID(用于计算新增/移除)
  47. const originalUserIds = ref<string[]>([])
  48. /** 打开弹窗 */
  49. const open = async (row: RoleApi.RoleVO) => {
  50. dialogVisible.value = true
  51. resetForm()
  52. formData.roleId = row.id
  53. formData.name = row.name
  54. formData.code = row.code
  55. formLoading.value = true
  56. try {
  57. // 并行加载所有用户和已分配用户 ID
  58. const [allUsers, existingUserIds] = await Promise.all([
  59. UserApi.getSimpleUserList(),
  60. RoleApi.getRoleUserIdList(row.id)
  61. ])
  62. // 统一将 id 转为字符串,避免 el-transfer 因类型不一致(number vs string)导致无法移除
  63. userList.value = allUsers.map((item) => ({
  64. ...item,
  65. id: String(item.id),
  66. nickname: `${item.nickname}${item.employeeNo ? `(${item.employeeNo})` : ''}`
  67. }))
  68. const userIdSet = new Set(userList.value.map((u) => u.id))
  69. selectedUserIdList.value = (existingUserIds || []).map(String).filter((id) => userIdSet.has(id))
  70. // 保存原始已选用户 ID,用于提交时计算新增/移除
  71. originalUserIds.value = [...selectedUserIdList.value]
  72. } finally {
  73. formLoading.value = false
  74. }
  75. }
  76. defineExpose({ open })
  77. /** 提交表单 */
  78. const emit = defineEmits(['success'])
  79. const submitForm = async () => {
  80. if (!formData.roleId) {
  81. message.error('角色信息异常')
  82. return
  83. }
  84. formLoading.value = true
  85. try {
  86. // 计算新增用户(当前选中但原来不在)
  87. const currentSet = new Set(selectedUserIdList.value)
  88. const originalSet = new Set(originalUserIds.value)
  89. const addUserIds = selectedUserIdList.value.filter((id) => !originalSet.has(id))
  90. // 计算移除用户(原来在但当前未选中)
  91. const removeUserIds = originalUserIds.value.filter((id) => !currentSet.has(id))
  92. if (addUserIds.length === 0 && removeUserIds.length === 0) {
  93. message.warning('未做任何更改')
  94. return
  95. }
  96. await RoleApi.assignRoleUsers({
  97. roleId: String(formData.roleId),
  98. userIds: addUserIds.length > 0 ? addUserIds : undefined,
  99. removeUserIds: removeUserIds.length > 0 ? removeUserIds : undefined
  100. })
  101. message.success('分配用户成功')
  102. dialogVisible.value = false
  103. emit('success')
  104. } finally {
  105. formLoading.value = false
  106. }
  107. }
  108. /** 重置 */
  109. const resetForm = () => {
  110. userList.value = []
  111. selectedUserIdList.value = []
  112. originalUserIds.value = []
  113. }
  114. </script>
  115. <style lang="scss" scoped>
  116. .transfer-form-item {
  117. margin-bottom: 0;
  118. }
  119. .transfer-wrapper {
  120. width: 100%;
  121. height: 420px;
  122. }
  123. :deep() {
  124. .el-transfer {
  125. display: flex;
  126. align-items: stretch;
  127. height: 100%;
  128. }
  129. .el-transfer-panel {
  130. flex: 1;
  131. border: 1px solid var(--el-border-color-light);
  132. border-radius: 4px;
  133. overflow: hidden;
  134. display: flex;
  135. flex-direction: column;
  136. }
  137. .el-transfer-panel__header {
  138. background: var(--el-fill-color-light);
  139. border-bottom: 1px solid var(--el-border-color-lighter);
  140. padding: 0 12px;
  141. height: 40px;
  142. line-height: 40px;
  143. .el-checkbox__label {
  144. font-size: 13px;
  145. font-weight: 500;
  146. color: var(--el-text-color-regular);
  147. }
  148. }
  149. .el-transfer-panel__body {
  150. flex: 1;
  151. overflow: hidden;
  152. .el-transfer-panel__list {
  153. height: 100%;
  154. padding: 4px 0;
  155. }
  156. }
  157. .el-transfer-panel__filter {
  158. padding: 8px 12px;
  159. .el-input__wrapper {
  160. border-radius: 4px;
  161. }
  162. }
  163. .el-transfer-panel__item {
  164. padding: 0 12px;
  165. height: 32px;
  166. line-height: 32px;
  167. transition: background-color 0.2s;
  168. &:hover {
  169. background-color: var(--el-fill-color-light);
  170. }
  171. }
  172. .el-transfer__buttons {
  173. display: flex;
  174. flex-direction: column;
  175. align-items: center;
  176. justify-content: center;
  177. gap: 12px;
  178. padding: 0 16px;
  179. flex-shrink: 0;
  180. .el-transfer__button {
  181. width: 32px;
  182. height: 32px;
  183. border-radius: 50%;
  184. padding: 0;
  185. display: flex;
  186. align-items: center;
  187. justify-content: center;
  188. &:nth-child(2) {
  189. margin: 0;
  190. }
  191. }
  192. }
  193. }
  194. </style>