|
9 | 9 | v-model.trim="searchQuery" |
10 | 10 | placeholder="搜索用户名或姓名" |
11 | 11 | clearable |
12 | | - style="width: 260px; margin-left: 12px;" |
| 12 | + class="search-input" |
13 | 13 | /> |
14 | 14 | </div> |
15 | 15 | <div class="filters" v-if="isAdmin"> |
16 | | - <el-select v-model="selectedRoles" multiple collapse-tags collapse-tags-tooltip placeholder="角色" clearable style="width: 260px;"> |
| 16 | + <div class="filters-row"> |
| 17 | + <el-select v-model="selectedRoles" multiple collapse-tags collapse-tags-tooltip placeholder="角色" clearable class="filter-select"> |
17 | 18 | <el-option label="admin" value="admin" /> |
18 | 19 | <el-option label="default" value="default" /> |
19 | 20 | <el-option label="restricted" value="restricted" /> |
20 | | - </el-select> |
21 | | - <el-select v-model="selectedCategories" multiple collapse-tags collapse-tags-tooltip placeholder="账号类型" clearable style="width: 280px; margin-left: 12px;"> |
| 21 | + </el-select> |
| 22 | + <el-select v-model="selectedCategories" multiple collapse-tags collapse-tags-tooltip placeholder="账号类型" clearable class="filter-select ml-12"> |
22 | 23 | <el-option label="system" value="system" /> |
23 | 24 | <el-option label="member" value="member" /> |
24 | 25 | <el-option label="external" value="external" /> |
25 | | - </el-select> |
| 26 | + </el-select> |
| 27 | + </div> |
26 | 28 | </div> |
27 | 29 | </div> |
28 | 30 |
|
29 | | - <el-table :data="pagedUsers" style="margin-top: 12px;" v-loading="loading"> |
30 | | - <el-table-column prop="username" label="用户名" /> |
31 | | - <el-table-column label="姓名"> |
32 | | - <template #default="scope"> |
33 | | - {{ (scope.row.surName || '') + (scope.row.givenName || '') }} |
34 | | - </template> |
35 | | - </el-table-column> |
36 | | - <el-table-column prop="mail" label="邮箱" /> |
37 | | - <el-table-column prop="role" label="角色" /> |
38 | | - <el-table-column prop="category" label="账号类型" /> |
39 | | - <el-table-column v-if="isAdmin" label="操作" width="180"> |
40 | | - <template #default="scope"> |
41 | | - <el-button v-if="isAdmin" size="small" @click="onEdit(scope.row)">编辑</el-button> |
42 | | - <el-button v-if="isAdmin" size="small" type="danger" @click="onDelete(scope.row)">删除</el-button> |
43 | | - </template> |
44 | | - </el-table-column> |
45 | | - </el-table> |
| 31 | + <!-- 桌面端表格 --> |
| 32 | + <div class="table-wrapper" v-show="!isMobile"> |
| 33 | + <el-table :data="pagedUsers" style="margin-top: 12px;" v-loading="loading"> |
| 34 | + <el-table-column prop="username" label="用户名" /> |
| 35 | + <el-table-column label="姓名"> |
| 36 | + <template #default="scope"> |
| 37 | + {{ (scope.row.surName || '') + (scope.row.givenName || '') }} |
| 38 | + </template> |
| 39 | + </el-table-column> |
| 40 | + <el-table-column prop="mail" label="邮箱" /> |
| 41 | + <el-table-column prop="role" label="角色" /> |
| 42 | + <el-table-column prop="category" label="账号类型" /> |
| 43 | + <el-table-column v-if="isAdmin" label="操作" width="180"> |
| 44 | + <template #default="scope"> |
| 45 | + <el-button v-if="isAdmin" size="small" @click="onEdit(scope.row)">编辑</el-button> |
| 46 | + <el-button v-if="isAdmin" size="small" type="danger" @click="onDelete(scope.row)">删除</el-button> |
| 47 | + </template> |
| 48 | + </el-table-column> |
| 49 | + </el-table> |
| 50 | + </div> |
| 51 | + |
| 52 | + <!-- 移动端卡片列表 --> |
| 53 | + <div class="mobile-cards" v-show="isMobile"> |
| 54 | + <el-empty v-if="!loading && pagedUsers.length === 0" description="暂无数据" /> |
| 55 | + <el-card v-for="user in pagedUsers" :key="user.username" class="user-card" shadow="hover"> |
| 56 | + <div class="card-row main"> |
| 57 | + <div class="name">{{ (user.surName || '') + (user.givenName || '') || '未命名' }}</div> |
| 58 | + <div class="username">@{{ user.username }}</div> |
| 59 | + </div> |
| 60 | + <div class="card-row"> |
| 61 | + <span class="label">邮箱</span> |
| 62 | + <span class="value break">{{ user.mail || '-' }}</span> |
| 63 | + </div> |
| 64 | + <div class="card-row meta"> |
| 65 | + <span class="tag">{{ user.role }}</span> |
| 66 | + <span class="tag">{{ user.category }}</span> |
| 67 | + </div> |
| 68 | + <div class="card-actions" v-if="isAdmin"> |
| 69 | + <el-button size="small" @click="onEdit(user)">编辑</el-button> |
| 70 | + <el-button size="small" type="danger" @click="onDelete(user)">删除</el-button> |
| 71 | + </div> |
| 72 | + </el-card> |
| 73 | + </div> |
46 | 74 |
|
47 | 75 | <div class="pagination"> |
48 | 76 | <el-pagination |
|
51 | 79 | :page-sizes="pageSizes" |
52 | 80 | :total="filteredTotal" |
53 | 81 | background |
54 | | - layout="total, sizes, prev, pager, next, jumper" |
| 82 | + :small="isMobile" |
| 83 | + :layout="paginationLayout" |
55 | 84 | /> |
56 | 85 | </div> |
57 | 86 | </div> |
|
128 | 157 | </template> |
129 | 158 |
|
130 | 159 | <script setup lang="ts"> |
131 | | -import { defineProps, defineEmits, computed, ref, watch } from 'vue' |
| 160 | +import { defineProps, defineEmits, computed, ref, watch, onMounted, onBeforeUnmount } from 'vue' |
132 | 161 | import type { User } from '@/api/types' |
133 | 162 | import { modifyUserRole, modifyUserCategory, deleteUser, changePassword, registerUser } from '@/api/user' |
134 | 163 | import { useSuccessTip, useFailedTip, useWarningConfirm } from '@/utils/msgTip' |
@@ -184,7 +213,30 @@ watch([selectedRoles, selectedCategories, () => props.users], () => { |
184 | 213 | const searchQuery = ref<string>('') |
185 | 214 | watch(searchQuery, () => { currentPage.value = 1 }) |
186 | 215 |
|
187 | | -// ===== 编辑逻辑(管理员) ===== |
| 216 | +const isMobile = ref<boolean>(false) |
| 217 | +
|
| 218 | +const updateIsMobile = () => { |
| 219 | + isMobile.value = window.matchMedia('(max-width: 768px)').matches |
| 220 | +} |
| 221 | +
|
| 222 | +onMounted(() => { |
| 223 | + updateIsMobile() |
| 224 | + // 根据当前设备类型设置分页大小 |
| 225 | + pageSize.value = isMobile.value ? 10 : 20 |
| 226 | + window.addEventListener('resize', updateIsMobile) |
| 227 | +}) |
| 228 | +
|
| 229 | +onBeforeUnmount(() => { |
| 230 | + window.removeEventListener('resize', updateIsMobile) |
| 231 | +}) |
| 232 | +
|
| 233 | +// 监听移动端切换动态调整每页数量 |
| 234 | +watch(isMobile, (v) => { |
| 235 | + pageSize.value = v ? 10 : 20 |
| 236 | +}) |
| 237 | +
|
| 238 | +const paginationLayout = computed(() => isMobile.value ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper') |
| 239 | +
|
188 | 240 | const editVisible = ref(false) |
189 | 241 | const savingRole = ref(false) |
190 | 242 | const savingCategory = ref(false) |
@@ -266,7 +318,6 @@ const onDelete = async (row: User) => { |
266 | 318 | } |
267 | 319 | } |
268 | 320 |
|
269 | | -// ===== 新建用户(管理员) ===== |
270 | 321 | const createVisible = ref(false) |
271 | 322 | const creating = ref(false) |
272 | 323 | const createForm = ref<{ username: string; surName: string; givenName: string; mail: string; role: string; category: string }>({ |
@@ -318,19 +369,49 @@ const onSubmitCreate = async () => { |
318 | 369 | display: flex; |
319 | 370 | align-items: center; |
320 | 371 | } |
| 372 | +.search-input { width: 260px; margin-left: 12px; } |
321 | 373 | .filters { |
322 | 374 | display: flex; |
323 | 375 | align-items: center; |
324 | 376 | } |
| 377 | +.filters-row { display: flex; align-items: center; } |
| 378 | +.filter-select { width: 260px; } |
| 379 | +.ml-12 { margin-left: 12px; } |
325 | 380 | .pagination { |
326 | 381 | margin-top: 12px; |
327 | 382 | display: flex; |
328 | 383 | justify-content: flex-end; |
329 | 384 | } |
330 | 385 |
|
| 386 | +/* 表格容器横向滚动,避免列过多溢出 */ |
| 387 | +.table-wrapper { overflow-x: auto; } |
| 388 | +.table-wrapper .el-table { min-width: 720px; } |
| 389 | +
|
| 390 | +/* 移动端卡片列表样式 */ |
| 391 | +.mobile-cards { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 12px; } |
| 392 | +.user-card { border-radius: 10px; } |
| 393 | +.card-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; } |
| 394 | +.card-row.main { margin-bottom: 4px; } |
| 395 | +.name { font-weight: 600; color: #303133; } |
| 396 | +.username { color: #909399; font-size: 12px; } |
| 397 | +.label { color: #909399; min-width: 48px; } |
| 398 | +.value { color: #606266; } |
| 399 | +.value.break { word-break: break-all; overflow-wrap: anywhere; } |
| 400 | +.card-row.meta { justify-content: flex-start; gap: 8px; } |
| 401 | +.tag { background: #f4f4f5; color: #606266; padding: 2px 8px; border-radius: 10px; font-size: 12px; } |
| 402 | +.card-actions { display: flex; gap: 8px; margin-top: 8px; } |
| 403 | +
|
331 | 404 | /* 小屏表格滚动与工具栏换行 */ |
332 | 405 | @media (max-width: 992px) { |
333 | | - .toolbar { flex-wrap: wrap; gap: 8px; } |
| 406 | + .toolbar { flex-wrap: wrap; gap: 8px; align-items: stretch; } |
| 407 | + .left-actions { width: 100%; } |
| 408 | + /* 搜索框小屏固定宽度,不跟随页面变化 */ |
| 409 | + .search-input { width: 240px; margin-left: 0; } |
| 410 | + .filters { width: 100%; justify-content: space-between; } |
| 411 | + .filters-row { width: 100%; gap: 8px; margin-top: 4px; flex-wrap: wrap; } |
| 412 | + .filter-select { width: 100%; } |
| 413 | + .ml-12 { margin-left: 0; } |
| 414 | + .pagination { justify-content: center; } |
334 | 415 | } |
335 | 416 |
|
336 | 417 | @media (max-width: 768px) { |
|
0 commit comments