Skip to content

Commit 6db5363

Browse files
authored
feature(frontend): 添加OIDC选项和移动端适配 (#15)
* feat(email): 在密码修改邮件模板中添加按钮和链接说明 - 增加了一个“立即前往修改密码”的按钮,方便用户直接访问密码修改页面。 - 添加了链接说明,提供了备用链接以防按钮无法点击。 * feat(Dashboard): 添加 OIDC 设置选项并实现打开链接功能 - 在下拉菜单中新增 OIDC 设置项。 - 实现点击 OIDC 设置时打开 Keycloak 账户管理页面的功能。 * feat(HomeHero, UsersPage, Dashboard, Home, Login, NotFound): 优化响应式布局和样式 - 在 HomeHero 组件中增加右边距以改善布局。 - 在 UsersPage 组件中重构搜索框和过滤器样式,增加移动端适配,优化表格和卡片列表展示。 - 在 Dashboard 组件中添加移动端导航,隐藏侧边栏以适应小屏幕。 - 在 Home 组件中调整列的响应式属性,确保在不同屏幕尺寸下的良好展示。 - 在 Login 和 NotFound 组件中移除最小宽度限制,增强小屏幕适配性,增加内边距以改善用户体验。
1 parent 58168f7 commit 6db5363

6 files changed

Lines changed: 159 additions & 35 deletions

File tree

frontend/src/components/HomeHero.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ onBeforeUnmount(() => {
187187
<style scoped>
188188
.home-hero {
189189
width: 98%;
190+
margin-right: 1%;
190191
display: flex;
191192
flex-direction: column;
192193
gap: 18px;

frontend/src/components/dashboard/UsersPage.vue

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,68 @@
99
v-model.trim="searchQuery"
1010
placeholder="搜索用户名或姓名"
1111
clearable
12-
style="width: 260px; margin-left: 12px;"
12+
class="search-input"
1313
/>
1414
</div>
1515
<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">
1718
<el-option label="admin" value="admin" />
1819
<el-option label="default" value="default" />
1920
<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">
2223
<el-option label="system" value="system" />
2324
<el-option label="member" value="member" />
2425
<el-option label="external" value="external" />
25-
</el-select>
26+
</el-select>
27+
</div>
2628
</div>
2729
</div>
2830

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>
4674

4775
<div class="pagination">
4876
<el-pagination
@@ -51,7 +79,8 @@
5179
:page-sizes="pageSizes"
5280
:total="filteredTotal"
5381
background
54-
layout="total, sizes, prev, pager, next, jumper"
82+
:small="isMobile"
83+
:layout="paginationLayout"
5584
/>
5685
</div>
5786
</div>
@@ -128,7 +157,7 @@
128157
</template>
129158

130159
<script setup lang="ts">
131-
import { defineProps, defineEmits, computed, ref, watch } from 'vue'
160+
import { defineProps, defineEmits, computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
132161
import type { User } from '@/api/types'
133162
import { modifyUserRole, modifyUserCategory, deleteUser, changePassword, registerUser } from '@/api/user'
134163
import { useSuccessTip, useFailedTip, useWarningConfirm } from '@/utils/msgTip'
@@ -184,7 +213,30 @@ watch([selectedRoles, selectedCategories, () => props.users], () => {
184213
const searchQuery = ref<string>('')
185214
watch(searchQuery, () => { currentPage.value = 1 })
186215
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+
188240
const editVisible = ref(false)
189241
const savingRole = ref(false)
190242
const savingCategory = ref(false)
@@ -266,7 +318,6 @@ const onDelete = async (row: User) => {
266318
}
267319
}
268320
269-
// ===== 新建用户(管理员) =====
270321
const createVisible = ref(false)
271322
const creating = ref(false)
272323
const createForm = ref<{ username: string; surName: string; givenName: string; mail: string; role: string; category: string }>({
@@ -318,19 +369,49 @@ const onSubmitCreate = async () => {
318369
display: flex;
319370
align-items: center;
320371
}
372+
.search-input { width: 260px; margin-left: 12px; }
321373
.filters {
322374
display: flex;
323375
align-items: center;
324376
}
377+
.filters-row { display: flex; align-items: center; }
378+
.filter-select { width: 260px; }
379+
.ml-12 { margin-left: 12px; }
325380
.pagination {
326381
margin-top: 12px;
327382
display: flex;
328383
justify-content: flex-end;
329384
}
330385
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+
331404
/* 小屏表格滚动与工具栏换行 */
332405
@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; }
334415
}
335416
336417
@media (max-width: 768px) {

frontend/src/views/Dashboard.vue

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,32 @@
1414
<template #dropdown>
1515
<el-dropdown-menu>
1616
<el-dropdown-item command="profile">设置</el-dropdown-item>
17+
<el-dropdown-item divided command="OIDC">OIDC设置</el-dropdown-item>
1718
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
1819
</el-dropdown-menu>
1920
</template>
2021
</el-dropdown>
2122
</div>
2223
</el-header>
2324

25+
<!-- 移动端顶部导航(<= 992px 显示) -->
26+
<div class="mobile-nav">
27+
<el-menu
28+
mode="horizontal"
29+
:default-active="activeMenu"
30+
@select="handleMenuSelect"
31+
>
32+
<el-menu-item index="overview">
33+
<el-icon><House /></el-icon>
34+
<span>概览</span>
35+
</el-menu-item>
36+
<el-menu-item index="users" v-if="!isRestricted">
37+
<el-icon><User /></el-icon>
38+
<span>用户管理</span>
39+
</el-menu-item>
40+
</el-menu>
41+
</div>
42+
2443
<!-- 主要内容区域 -->
2544
<el-container class="dashboard-container">
2645
<!-- 侧边栏 -->
@@ -127,6 +146,9 @@ const handleCommand = async (command: string) => {
127146
case 'logout':
128147
await handleLogout()
129148
break
149+
case 'OIDC':
150+
window.open('https://keycloak.internal.asynclab.club/realms/asynclab/account ', '_blank')
151+
break
130152
}
131153
}
132154
@@ -273,14 +295,24 @@ watchEffect(() => {
273295
274296
@media (max-width: 992px) {
275297
.dashboard-container { display: block; }
276-
.dashboard-sidebar { width: 100%; min-width: auto; border-right: none; border-bottom: 1px solid #e4e7ed; }
298+
.dashboard-sidebar { display: none; }
277299
}
278300
279301
@media (max-width: 768px) {
280302
.dashboard-header { min-height: 56px; }
281303
.dashboard-main { padding: 10px; }
282304
}
283305
306+
.mobile-nav {
307+
display: none;
308+
background: #fff;
309+
border-bottom: 1px solid #e4e7ed;
310+
}
311+
312+
@media (max-width: 992px) {
313+
.mobile-nav { display: block; }
314+
}
315+
284316
/* 概览为空白占位,不添加多余内容 */
285317
286318
.overview-actions {

frontend/src/views/Home.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<div class="welcome-content">
1111
<el-row :gutter="20">
12-
<el-col :span="12">
12+
<el-col :xs="24" :sm="24" :md="12">
1313
<div class="welcome-text">
1414
<h2>异步开发实验室</h2>
1515
<div>
@@ -24,7 +24,7 @@
2424
</div>
2525
</div>
2626
</el-col>
27-
<el-col :span="12">
27+
<el-col :xs="24" :sm="24" :md="12">
2828
<div class="welcome-actions">
2929
<img
3030
class="brand-logo"

frontend/src/views/Login.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,6 @@ const animate = () => {
337337
<style scoped>
338338
.login-container {
339339
height: 100vh;
340-
min-width: 800px;
341340
display: flex;
342341
align-items: center;
343342
justify-content: center;
@@ -370,7 +369,6 @@ const animate = () => {
370369
.login-box {
371370
width: 100%;
372371
max-width: 900px;
373-
min-width: 800px;
374372
position: relative;
375373
z-index: 1;
376374
}
@@ -532,7 +530,6 @@ const animate = () => {
532530
@media (max-width: 1024px) {
533531
.login-box {
534532
max-width: 720px;
535-
min-width: 680px;
536533
}
537534
.card-body {
538535
grid-template-columns: 1fr;
@@ -545,4 +542,17 @@ const animate = () => {
545542
height: 100vh;
546543
}
547544
}
545+
546+
/* 更小屏幕的自适应(手机) */
547+
@media (max-width: 768px) {
548+
.login-container {
549+
padding: 16px;
550+
}
551+
.login-box {
552+
max-width: 520px;
553+
}
554+
.form-pane {
555+
padding: 24px 20px;
556+
}
557+
}
548558
</style>

frontend/src/views/NotFound.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ const animate = () => {
154154
<style scoped>
155155
.not-found {
156156
min-height: 100vh;
157-
min-width: 800px;
158157
display: flex;
159158
align-items: center;
160159
justify-content: center;
@@ -179,7 +178,6 @@ const animate = () => {
179178
.not-found-content {
180179
text-align: center;
181180
max-width: 600px;
182-
min-width: 400px;
183181
margin-right: 60px;
184182
min-height: 400px;
185183
}
@@ -250,11 +248,13 @@ const animate = () => {
250248
.not-found {
251249
flex-direction: column;
252250
text-align: center;
251+
padding: 16px;
253252
}
254253
255254
.not-found-content {
256255
margin-right: 0;
257256
margin-bottom: 40px;
257+
padding: 0 8px;
258258
}
259259
260260
.error-code {

0 commit comments

Comments
 (0)