Users.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944
  1. <template>
  2. <div class="users-management">
  3. <div class="page-header">
  4. <h2>用户管理</h2>
  5. <p>管理系统用户和角色分配</p>
  6. </div>
  7. <!-- 操作栏 -->
  8. <div class="toolbar">
  9. <div class="toolbar-left">
  10. <el-button type="primary" @click="showCreateDialog = true">
  11. <el-icon><Plus /></el-icon>
  12. 创建用户
  13. </el-button>
  14. <!--<el-button
  15. type="danger"
  16. :disabled="selectedUsers.length === 0"
  17. @click="batchDelete"
  18. >
  19. <el-icon><Delete /></el-icon>
  20. 批量删除
  21. </el-button> -->
  22. </div>
  23. <div class="toolbar-right">
  24. <el-input
  25. v-model="searchKeyword"
  26. placeholder="搜索用户名、邮箱、姓名..."
  27. style="width: 300px"
  28. @input="handleSearch"
  29. >
  30. <template #prefix>
  31. <el-icon><Search /></el-icon>
  32. </template>
  33. </el-input>
  34. </div>
  35. </div>
  36. <!-- 用户列表 -->
  37. <div class="table-container">
  38. <el-table
  39. v-loading="loading"
  40. :data="users"
  41. style="width: 100%"
  42. @selection-change="handleSelectionChange"
  43. :scroll-x="true"
  44. >
  45. <el-table-column type="selection" width="55" />
  46. <el-table-column prop="username" label="用户信息" min-width="160" fixed="left">
  47. <template #default="{ row }">
  48. <div class="user-info">
  49. <el-avatar :size="28" :src="row.avatar_url">
  50. {{ row.username.charAt(0).toUpperCase() }}
  51. </el-avatar>
  52. <div class="user-details">
  53. <div class="username">{{ row.username }}</div>
  54. <div class="real-name" v-if="row.real_name">{{ row.real_name }}</div>
  55. </div>
  56. </div>
  57. </template>
  58. </el-table-column>
  59. <el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip />
  60. <el-table-column prop="phone" label="手机号" min-width="130" />
  61. <el-table-column prop="roles" label="角色" min-width="140">
  62. <template #default="{ row }">
  63. <div class="roles-container">
  64. <el-tag
  65. v-if="row.roles"
  66. size="small"
  67. style="margin-right: 4px; margin-bottom: 2px;"
  68. >
  69. {{ row.roles }}
  70. </el-tag>
  71. <el-tag v-else size="small" type="info">未分配</el-tag>
  72. </div>
  73. </template>
  74. </el-table-column>
  75. <el-table-column prop="is_superuser" label="管理员" width="80" align="center">
  76. <template #default="{ row }">
  77. <el-tag :type="row.is_superuser ? 'danger' : 'info'" size="small">
  78. {{ row.is_superuser ? '是' : '否' }}
  79. </el-tag>
  80. </template>
  81. </el-table-column>
  82. <el-table-column prop="is_active" label="状态" width="80" align="center">
  83. <template #default="{ row }">
  84. <el-switch
  85. v-model="row.is_active"
  86. @change="handleStatusChange(row)"
  87. size="small"
  88. />
  89. </template>
  90. </el-table-column>
  91. <el-table-column prop="last_login_at" label="最后登录" min-width="140">
  92. <template #default="{ row }">
  93. <div class="time-info">
  94. {{ formatDateTime(row.last_login_at) }}
  95. </div>
  96. </template>
  97. </el-table-column>
  98. <el-table-column prop="created_by" label="创建人" min-width="100">
  99. <template #default="{ row }">
  100. <div class="user-info-simple">
  101. {{ row.created_by_name || '-' }}
  102. </div>
  103. </template>
  104. </el-table-column>
  105. <el-table-column prop="created_time" label="创建时间" min-width="140">
  106. <template #default="{ row }">
  107. <div class="time-info">
  108. {{ formatDateTime(row.created_time) }}
  109. </div>
  110. </template>
  111. </el-table-column>
  112. <el-table-column prop="updated_by" label="修改人" min-width="100">
  113. <template #default="{ row }">
  114. <div class="user-info-simple">
  115. {{ row.updated_by_name || '-' }}
  116. </div>
  117. </template>
  118. </el-table-column>
  119. <el-table-column prop="updated_time" label="修改时间" min-width="140">
  120. <template #default="{ row }">
  121. <div class="time-info">
  122. {{ formatDateTime(row.updated_time) }}
  123. </div>
  124. </template>
  125. </el-table-column>
  126. <el-table-column label="操作" width="280" fixed="right">
  127. <template #default="{ row }">
  128. <div class="action-buttons">
  129. <el-button type="primary" size="small" @click="editUser(row)">
  130. 编辑
  131. </el-button>
  132. <el-button type="success" size="small" @click="assignRoles(row)">
  133. 分配角色
  134. </el-button>
  135. <el-button
  136. type="danger"
  137. size="small"
  138. @click="deleteUser(row)"
  139. :disabled="row.is_superuser || isCurrentUser(row.id)"
  140. >
  141. 删除
  142. </el-button>
  143. </div>
  144. </template>
  145. </el-table-column>
  146. </el-table>
  147. </div>
  148. <!-- 分页 -->
  149. <div class="pagination">
  150. <el-pagination
  151. v-model:current-page="currentPage"
  152. v-model:page-size="pageSize"
  153. :page-sizes="[10, 20, 50, 100]"
  154. :total="total"
  155. layout="total, sizes, prev, pager, next, jumper"
  156. @size-change="handleSizeChange"
  157. @current-change="handleCurrentChange"
  158. />
  159. </div>
  160. <!-- 创建/编辑用户对话框 -->
  161. <el-dialog
  162. v-model="showCreateDialog"
  163. :title="editingUser ? '编辑用户' : '创建用户'"
  164. width="660px"
  165. @close="resetForm"
  166. >
  167. <el-form
  168. ref="userFormRef"
  169. :model="userForm"
  170. :rules="userRules"
  171. label-width="88px"
  172. >
  173. <el-row :gutter="18">
  174. <el-col :span="12">
  175. <el-form-item label="用户名" prop="username">
  176. <el-input
  177. v-model="userForm.username"
  178. placeholder="请输入用户名"
  179. :disabled="!!editingUser"
  180. size="default"
  181. />
  182. </el-form-item>
  183. </el-col>
  184. <el-col :span="12">
  185. <el-form-item label="邮箱" prop="email">
  186. <el-input
  187. v-model="userForm.email"
  188. placeholder="请输入邮箱"
  189. size="default"
  190. />
  191. </el-form-item>
  192. </el-col>
  193. </el-row>
  194. <el-row :gutter="18">
  195. <el-col :span="12">
  196. <el-form-item label="密码" :prop="editingUser ? '' : 'password'">
  197. <el-input
  198. v-model="userForm.password"
  199. type="password"
  200. :placeholder="editingUser ? '留空则不修改密码' : '请输入密码'"
  201. show-password
  202. size="default"
  203. />
  204. </el-form-item>
  205. </el-col>
  206. <el-col :span="12">
  207. <el-form-item label="手机号" prop="phone">
  208. <el-input
  209. v-model="userForm.phone"
  210. placeholder="请输入手机号"
  211. size="default"
  212. />
  213. </el-form-item>
  214. </el-col>
  215. </el-row>
  216. <el-row :gutter="18">
  217. <el-col :span="12">
  218. <el-form-item label="真实姓名" prop="real_name">
  219. <el-input
  220. v-model="userForm.real_name"
  221. placeholder="请输入真实姓名"
  222. size="default"
  223. />
  224. </el-form-item>
  225. </el-col>
  226. <el-col :span="12">
  227. <el-form-item label="公司" prop="company">
  228. <el-input
  229. v-model="userForm.company"
  230. placeholder="请输入公司名称"
  231. size="default"
  232. />
  233. </el-form-item>
  234. </el-col>
  235. </el-row>
  236. <el-row :gutter="18">
  237. <el-col :span="12">
  238. <el-form-item label="部门" prop="department">
  239. <el-input
  240. v-model="userForm.department"
  241. placeholder="请输入部门"
  242. size="default"
  243. />
  244. </el-form-item>
  245. </el-col>
  246. <el-col :span="12">
  247. <el-form-item label="角色" prop="role_ids">
  248. <el-select
  249. v-model="userForm.role_ids"
  250. multiple
  251. placeholder="请选择角色"
  252. style="width: 100%"
  253. size="default"
  254. >
  255. <el-option
  256. v-for="role in allRoles"
  257. :key="role.id"
  258. :label="role.name"
  259. :value="role.id"
  260. />
  261. </el-select>
  262. </el-form-item>
  263. </el-col>
  264. </el-row>
  265. <el-row :gutter="18">
  266. <el-col :span="12">
  267. <el-form-item label="管理员">
  268. <el-switch v-model="userForm.is_superuser" size="default" />
  269. <span style="margin-left: 8px; font-size: 12px; color: #666;">
  270. 拥有系统所有权限
  271. </span>
  272. </el-form-item>
  273. </el-col>
  274. <el-col :span="12">
  275. <el-form-item label="启用状态">
  276. <el-switch v-model="userForm.is_active" size="default" />
  277. <span style="margin-left: 8px; font-size: 12px; color: #666;">
  278. 用户是否可以登录
  279. </span>
  280. </el-form-item>
  281. </el-col>
  282. </el-row>
  283. </el-form>
  284. <template #footer>
  285. <el-button @click="showCreateDialog = false">取消</el-button>
  286. <el-button type="primary" @click="saveUser" :loading="saving">
  287. {{ editingUser ? '更新' : '创建' }}
  288. </el-button>
  289. </template>
  290. </el-dialog>
  291. <!-- 角色分配对话框 -->
  292. <el-dialog
  293. v-model="showRoleDialog"
  294. title="分配角色"
  295. width="550px"
  296. >
  297. <div v-if="currentUser">
  298. <h4>为用户 "{{ currentUser.username }}" 分配角色</h4>
  299. <el-checkbox-group v-model="selectedRoleIds">
  300. <el-checkbox
  301. v-for="role in allRoles"
  302. :key="role.id"
  303. :label="role.id"
  304. :value="role.id"
  305. >
  306. {{ role.name }}
  307. <el-tag v-if="role.is_system" type="info" size="small" style="margin-left: 8px;">
  308. 系统角色
  309. </el-tag>
  310. </el-checkbox>
  311. </el-checkbox-group>
  312. </div>
  313. <template #footer>
  314. <el-button @click="showRoleDialog = false">取消</el-button>
  315. <el-button type="primary" @click="saveUserRoles" :loading="savingRoles">
  316. 保存
  317. </el-button>
  318. </template>
  319. </el-dialog>
  320. </div>
  321. </template>
  322. <script setup lang="ts">
  323. import { ref, reactive, onMounted } from 'vue'
  324. import { ElMessage, ElMessageBox } from 'element-plus'
  325. import { Plus, Search, Delete } from '@element-plus/icons-vue'
  326. import { useAuthStore } from '@/stores/auth'
  327. import request from '@/api/request'
  328. const authStore = useAuthStore()
  329. // 响应式数据
  330. const loading = ref(false)
  331. const saving = ref(false)
  332. const savingRoles = ref(false)
  333. const users = ref<any[]>([])
  334. const allRoles = ref<any[]>([])
  335. const selectedUsers = ref<any[]>([])
  336. const searchKeyword = ref('')
  337. const currentPage = ref(1)
  338. const pageSize = ref(20)
  339. const total = ref(0)
  340. // 对话框状态
  341. const showCreateDialog = ref(false)
  342. const showRoleDialog = ref(false)
  343. const editingUser = ref<any>(null)
  344. const currentUser = ref<any>(null)
  345. const selectedRoleIds = ref<string[]>([])
  346. // 表单数据
  347. const userForm = reactive({
  348. username: '',
  349. email: '',
  350. password: '',
  351. phone: '',
  352. real_name: '',
  353. company: '',
  354. department: '',
  355. role_ids: [] as string[],
  356. is_superuser: false,
  357. is_active: true
  358. })
  359. // 表单验证规则
  360. const userRules = {
  361. username: [
  362. { required: true, message: '请输入用户名', trigger: 'blur' },
  363. { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
  364. ],
  365. email: [
  366. { required: true, message: '请输入邮箱', trigger: 'blur' },
  367. { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
  368. ],
  369. password: [
  370. { required: true, message: '请输入密码', trigger: 'blur' },
  371. { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
  372. ],
  373. real_name: [
  374. { required: true, message: '请输入真实姓名', trigger: 'blur' }
  375. ]
  376. }
  377. // 引用
  378. const userFormRef = ref()
  379. // 判断是否为当前用户
  380. const isCurrentUser = (userId: string) => {
  381. return userId === authStore.user?.id
  382. }
  383. // 加载用户列表
  384. const loadUsers = async () => {
  385. loading.value = true
  386. try {
  387. const result = await request.get('/api/v1/system/admin/users', {
  388. params: {
  389. page: currentPage.value,
  390. page_size: pageSize.value,
  391. keyword: searchKeyword.value || undefined
  392. }
  393. })
  394. if (result.code === '000000' || result.code === 0) {
  395. users.value = result.data.items
  396. total.value = result.data.total
  397. } else {
  398. throw new Error(result.message)
  399. }
  400. } catch (error) {
  401. console.error('加载用户列表失败:', error)
  402. ElMessage.error('加载用户列表失败')
  403. } finally {
  404. loading.value = false
  405. }
  406. }
  407. // 加载所有角色
  408. const loadRoles = async () => {
  409. try {
  410. const result = await request.get('/api/v1/system/roles/all')
  411. if (result.code === '000000' || result.code === 0) {
  412. allRoles.value = result.data
  413. }
  414. } catch (error) {
  415. console.error('加载角色列表失败:', error)
  416. }
  417. }
  418. // 搜索处理
  419. const handleSearch = () => {
  420. currentPage.value = 1
  421. loadUsers()
  422. }
  423. // 分页处理
  424. const handleSizeChange = (size: number) => {
  425. pageSize.value = size
  426. currentPage.value = 1
  427. loadUsers()
  428. }
  429. const handleCurrentChange = (page: number) => {
  430. currentPage.value = page
  431. loadUsers()
  432. }
  433. // 选择处理
  434. const handleSelectionChange = (selection: any[]) => {
  435. selectedUsers.value = selection
  436. }
  437. // 状态切换
  438. const handleStatusChange = async (user: any) => {
  439. try {
  440. await request.put(`/api/v1/system/admin/users/${user.id}`, {
  441. is_active: user.is_active
  442. })
  443. ElMessage.success('用户状态更新成功')
  444. } catch (error) {
  445. console.error('更新用户状态失败:', error)
  446. ElMessage.error('更新用户状态失败')
  447. // 恢复原状态
  448. user.is_active = !user.is_active
  449. }
  450. }
  451. // 编辑用户
  452. const editUser = async (user: any) => {
  453. editingUser.value = user
  454. try {
  455. // 获取用户详情(包含角色信息)
  456. const result = await request.get(`/api/v1/system/admin/users/${user.id}`)
  457. if (result.code === '000000' || result.code === 0) {
  458. const userDetail = result.data
  459. Object.assign(userForm, {
  460. username: userDetail.username,
  461. email: userDetail.email,
  462. password: '',
  463. phone: userDetail.phone || '',
  464. real_name: userDetail.real_name || '',
  465. company: userDetail.company || '',
  466. department: userDetail.department || '',
  467. role_ids: userDetail.role_ids || [],
  468. is_superuser: userDetail.is_superuser,
  469. is_active: userDetail.is_active
  470. })
  471. } else {
  472. throw new Error(result.message)
  473. }
  474. } catch (error) {
  475. console.error('获取用户详情失败:', error)
  476. ElMessage.error('获取用户详情失败')
  477. // 如果获取详情失败,使用列表中的基本信息
  478. Object.assign(userForm, {
  479. username: user.username,
  480. email: user.email,
  481. password: '',
  482. phone: user.phone || '',
  483. real_name: user.real_name || '',
  484. company: user.company || '',
  485. department: user.department || '',
  486. role_ids: [],
  487. is_superuser: user.is_superuser,
  488. is_active: user.is_active
  489. })
  490. }
  491. showCreateDialog.value = true
  492. }
  493. // 删除用户
  494. const deleteUser = async (user: any) => {
  495. try {
  496. await ElMessageBox.confirm(
  497. `确定要删除用户 "${user.username}" 吗?此操作不可恢复。`,
  498. '确认删除',
  499. {
  500. confirmButtonText: '确定',
  501. cancelButtonText: '取消',
  502. type: 'warning'
  503. }
  504. )
  505. await request.delete(`/api/v1/system/admin/users/${user.id}`)
  506. ElMessage.success('用户删除成功')
  507. loadUsers()
  508. } catch (error: any) {
  509. if (error.response?.data?.message) {
  510. ElMessage.error(error.response.data.message)
  511. } else if (error !== 'cancel') {
  512. ElMessage.error('删除用户失败')
  513. }
  514. }
  515. }
  516. // 批量删除
  517. const batchDelete = async () => {
  518. try {
  519. await ElMessageBox.confirm(
  520. `确定要删除选中的 ${selectedUsers.value.length} 个用户吗?此操作不可恢复。`,
  521. '确认批量删除',
  522. {
  523. confirmButtonText: '确定',
  524. cancelButtonText: '取消',
  525. type: 'warning'
  526. }
  527. )
  528. // 逐个删除(可以优化为批量API)
  529. for (const user of selectedUsers.value) {
  530. await request.delete(`/api/v1/system/admin/users/${user.id}`)
  531. }
  532. ElMessage.success('批量删除成功')
  533. loadUsers()
  534. } catch (error) {
  535. if (error !== 'cancel') {
  536. ElMessage.error('批量删除失败')
  537. }
  538. }
  539. }
  540. // 分配角色
  541. const assignRoles = async (user: any) => {
  542. currentUser.value = user
  543. try {
  544. // 获取用户详情(包含角色信息)
  545. const result = await request.get(`/api/v1/system/admin/users/${user.id}`)
  546. if (result.code === '000000' || result.code === 0) {
  547. const userDetail = result.data
  548. selectedRoleIds.value = userDetail.role_ids || []
  549. } else {
  550. throw new Error(result.message)
  551. }
  552. } catch (error) {
  553. console.error('获取用户角色失败:', error)
  554. ElMessage.error('获取用户角色失败')
  555. selectedRoleIds.value = []
  556. }
  557. showRoleDialog.value = true
  558. }
  559. // 保存用户
  560. const saveUser = async () => {
  561. try {
  562. await userFormRef.value.validate()
  563. saving.value = true
  564. if (editingUser.value) {
  565. // 更新用户
  566. const updateData: any = {
  567. email: userForm.email,
  568. phone: userForm.phone || null, // 确保空字符串转为null
  569. real_name: userForm.real_name,
  570. company: userForm.company || null,
  571. department: userForm.department || null,
  572. role_ids: userForm.role_ids,
  573. is_superuser: userForm.is_superuser,
  574. is_active: userForm.is_active
  575. }
  576. // 如果填写了密码,则更新密码
  577. if (userForm.password && userForm.password.trim()) {
  578. updateData.password = userForm.password
  579. }
  580. await request.put(`/api/v1/system/admin/users/${editingUser.value.id}`, updateData)
  581. ElMessage.success('用户更新成功')
  582. } else {
  583. // 创建用户
  584. const createData = {
  585. ...userForm,
  586. phone: userForm.phone || null, // 确保空字符串转为null
  587. company: userForm.company || null,
  588. department: userForm.department || null
  589. }
  590. await request.post('/api/v1/system/admin/users', createData)
  591. ElMessage.success('用户创建成功')
  592. }
  593. showCreateDialog.value = false
  594. loadUsers()
  595. } catch (error: any) {
  596. console.error('保存用户失败:', error)
  597. if (error.response?.data?.message) {
  598. ElMessage.error(error.response.data.message)
  599. } else {
  600. ElMessage.error('保存用户失败')
  601. }
  602. } finally {
  603. saving.value = false
  604. }
  605. }
  606. // 保存用户角色
  607. const saveUserRoles = async () => {
  608. try {
  609. savingRoles.value = true
  610. await request.put(`/api/v1/system/admin/users/${currentUser.value.id}`, {
  611. role_ids: selectedRoleIds.value
  612. })
  613. ElMessage.success('角色分配成功')
  614. showRoleDialog.value = false
  615. loadUsers()
  616. } catch (error) {
  617. console.error('保存用户角色失败:', error)
  618. ElMessage.error('保存用户角色失败')
  619. } finally {
  620. savingRoles.value = false
  621. }
  622. }
  623. // 重置表单
  624. const resetForm = () => {
  625. editingUser.value = null
  626. Object.assign(userForm, {
  627. username: '',
  628. email: '',
  629. password: '',
  630. phone: '',
  631. real_name: '',
  632. company: '',
  633. department: '',
  634. role_ids: [],
  635. is_superuser: false,
  636. is_active: true
  637. })
  638. userFormRef.value?.clearValidate()
  639. }
  640. // 格式化日期时间
  641. const formatDateTime = (dateTime: string) => {
  642. if (!dateTime) return '-'
  643. const date = new Date(dateTime)
  644. // 格式化为 YYYY-MM-DD HH:mm:ss
  645. const year = date.getFullYear()
  646. const month = String(date.getMonth() + 1).padStart(2, '0')
  647. const day = String(date.getDate()).padStart(2, '0')
  648. const hours = String(date.getHours()).padStart(2, '0')
  649. const minutes = String(date.getMinutes()).padStart(2, '0')
  650. const seconds = String(date.getSeconds()).padStart(2, '0')
  651. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  652. }
  653. // 组件挂载
  654. onMounted(() => {
  655. loadUsers()
  656. loadRoles()
  657. })
  658. </script>
  659. <style scoped>
  660. .users-management {
  661. padding: 20px;
  662. }
  663. .page-header {
  664. margin-bottom: 20px;
  665. }
  666. .page-header h2 {
  667. margin: 0 0 8px 0;
  668. color: #333;
  669. }
  670. .page-header p {
  671. margin: 0;
  672. color: #666;
  673. font-size: 14px;
  674. }
  675. .toolbar {
  676. display: flex;
  677. justify-content: space-between;
  678. align-items: center;
  679. margin-bottom: 20px;
  680. padding: 16px;
  681. background: #fff;
  682. border-radius: 8px;
  683. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  684. }
  685. .toolbar-left {
  686. display: flex;
  687. gap: 12px;
  688. }
  689. .toolbar-right {
  690. display: flex;
  691. gap: 12px;
  692. align-items: center;
  693. }
  694. .table-container {
  695. overflow-x: auto;
  696. background: #fff;
  697. border-radius: 8px;
  698. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  699. min-width: 1200px; /* 设置最小宽度确保表格不会过度压缩 */
  700. }
  701. .user-info {
  702. display: flex;
  703. align-items: center;
  704. gap: 8px;
  705. min-width: 120px; /* 确保用户信息列有足够宽度 */
  706. }
  707. .user-info-simple {
  708. font-size: 12px;
  709. color: #666;
  710. white-space: nowrap;
  711. overflow: hidden;
  712. text-overflow: ellipsis;
  713. }
  714. .user-details {
  715. display: flex;
  716. flex-direction: column;
  717. min-width: 0; /* 允许文本截断 */
  718. flex: 1;
  719. }
  720. .username {
  721. font-weight: 500;
  722. color: #333;
  723. font-size: 13px;
  724. white-space: nowrap;
  725. overflow: hidden;
  726. text-overflow: ellipsis;
  727. max-width: 100px; /* 限制用户名最大宽度 */
  728. }
  729. .real-name {
  730. font-size: 11px;
  731. color: #666;
  732. white-space: nowrap;
  733. overflow: hidden;
  734. text-overflow: ellipsis;
  735. max-width: 100px; /* 限制真实姓名最大宽度 */
  736. }
  737. .roles-container {
  738. display: flex;
  739. flex-wrap: wrap;
  740. gap: 2px;
  741. align-items: center;
  742. min-width: 80px; /* 确保角色列有最小宽度 */
  743. }
  744. .time-info {
  745. font-size: 12px;
  746. color: #666;
  747. white-space: nowrap;
  748. }
  749. .action-buttons {
  750. display: flex;
  751. gap: 6px;
  752. align-items: center;
  753. justify-content: flex-start; /* 左对齐操作按钮 */
  754. flex-wrap: nowrap; /* 不允许换行 */
  755. }
  756. .pagination {
  757. margin-top: 20px;
  758. display: flex;
  759. justify-content: center;
  760. }
  761. :deep(.el-table) {
  762. background: #fff;
  763. border-radius: 8px;
  764. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  765. }
  766. :deep(.el-table .el-table__cell) {
  767. padding: 8px 0;
  768. }
  769. :deep(.el-dialog__body) {
  770. padding: 22px; /* 增加对话框内边距 10% */
  771. }
  772. :deep(.el-checkbox-group) {
  773. display: flex;
  774. flex-direction: column;
  775. gap: 12px;
  776. }
  777. :deep(.el-checkbox) {
  778. margin-right: 0;
  779. }
  780. /* 响应式设计 */
  781. @media (max-width: 1400px) {
  782. .table-container {
  783. min-width: 1300px; /* 增加最小宽度以适应更多列 */
  784. }
  785. .username, .real-name {
  786. max-width: 80px;
  787. }
  788. }
  789. @media (max-width: 1200px) {
  790. .table-container {
  791. min-width: 1200px; /* 增加最小宽度 */
  792. }
  793. .username, .real-name {
  794. max-width: 70px;
  795. }
  796. .toolbar {
  797. flex-direction: column;
  798. gap: 12px;
  799. align-items: stretch;
  800. }
  801. .toolbar-left, .toolbar-right {
  802. justify-content: center;
  803. }
  804. /* 中等屏幕下操作按钮调整 */
  805. .action-buttons {
  806. gap: 4px; /* 减少间距 */
  807. }
  808. .action-buttons .el-button {
  809. font-size: 12px;
  810. padding: 4px 8px;
  811. }
  812. }
  813. @media (max-width: 1000px) {
  814. .table-container {
  815. min-width: 1000px;
  816. }
  817. /* 小屏幕下操作按钮垂直排列 */
  818. .action-buttons {
  819. flex-direction: column;
  820. gap: 2px;
  821. flex-wrap: nowrap;
  822. }
  823. .action-buttons .el-button {
  824. font-size: 11px;
  825. padding: 2px 6px;
  826. min-width: 70px;
  827. }
  828. }
  829. @media (max-width: 768px) {
  830. .users-management {
  831. padding: 10px;
  832. }
  833. .table-container {
  834. min-width: 900px;
  835. }
  836. .toolbar {
  837. padding: 12px;
  838. }
  839. .toolbar-left {
  840. flex-wrap: wrap;
  841. gap: 8px;
  842. }
  843. .pagination {
  844. margin-top: 15px;
  845. }
  846. :deep(.el-pagination) {
  847. justify-content: center;
  848. flex-wrap: wrap;
  849. }
  850. }
  851. </style>