feat(后台): 手机上也能用了——菜单收进左边抽屉,列表一行一张卡,不用再横着滑
- 侧栏手机上不再占一列图标,顶栏左边 ☰ 拉出整张菜单,按返回键先关菜单 - 题目、比赛、用户、公告、题单列表手机上一项一张卡:名字一行、小字一行、按钮一行, 「学生看得到」开关单独一列靠右 - 卡点分析、AI 报告手机上点开一项整屏看,返回键关掉;年度趋势、自学情况、首页改成上下排 - 出题页、教程编辑、题单编辑、标签、设置手机上排成一列;测试数据「删掉」手机上常驻 - 弹框一律收进屏幕宽度 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b757e2ac38
commit
02e2fa7d9b
30 files changed
+1328
-183
No files matched your search
@@ -15,6 +15,7 @@ import { useRouteQuery } from "@vueuse/router"
|
||||
import { NO_CLASS } from "@oj2/contract"
|
||||
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
/**
|
||||
* 后台用户列表(设计稿「后台用户与批量建号」UsList)。老师进来多半是三件事:学生忘了密码、
|
||||
@@ -24,6 +25,7 @@ import { useConfigStore } from "shared/store/config"
|
||||
const message = useMessage()
|
||||
const theme = useThemeVars()
|
||||
const configStore = useConfigStore()
|
||||
const { isMobile } = useBreakpoints()
|
||||
|
||||
interface UserQuery {
|
||||
keyword: string
|
||||
@@ -91,7 +93,56 @@ function lastLoginText(row: AdminUserForm) {
|
||||
])
|
||||
}
|
||||
|
||||
const columns: DataTableColumn<AdminUserForm>[] = [
|
||||
function renderPassword(row: AdminUserForm) {
|
||||
return h(PasswordCell, {
|
||||
user: row,
|
||||
revealed: revealedPasswords.value.has(row.id),
|
||||
onToggle: (id: number) => {
|
||||
if (!revealedPasswords.value.delete(id)) revealedPasswords.value.add(id)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function renderActions(row: AdminUserForm) {
|
||||
return h(Actions, {
|
||||
user: row,
|
||||
onDeleteUser: onDeleteUsers,
|
||||
onUserBanned,
|
||||
onOpenEditModal,
|
||||
onResetPassword,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机上一个人一张卡:用户名一行,班级 · 上次登录 · 建号一行小字(每项不断开),
|
||||
* 密码单独一行(学生忘了密码是最常见的事,要一眼找到),操作按钮放不下就折行
|
||||
*/
|
||||
function renderMobileCard(row: AdminUserForm) {
|
||||
return h("div", { class: "m-card" }, [
|
||||
h(UserNameCell, { user: row }),
|
||||
h("div", { class: "m-meta muted" }, [
|
||||
h("span", { class: "m-item" }, row.className ? classLabel(row.className) : "没填班级"),
|
||||
h("span", { class: "m-item" }, [" · ", lastLoginText(row)]),
|
||||
row.createTime
|
||||
? h("span", { class: "m-item" }, ` · 建号 ${parseTime(row.createTime, "YYYY-MM-DD")}`)
|
||||
: null,
|
||||
]),
|
||||
row.rawPassword
|
||||
? h("div", { class: "m-password" }, [
|
||||
h("span", { class: "muted" }, "密码"),
|
||||
renderPassword(row),
|
||||
])
|
||||
: null,
|
||||
renderActions(row),
|
||||
])
|
||||
}
|
||||
|
||||
const mobileColumns: DataTableColumn<AdminUserForm>[] = [
|
||||
{ type: "selection" },
|
||||
{ title: "用户", key: "username", render: renderMobileCard },
|
||||
]
|
||||
|
||||
const desktopColumns: DataTableColumn<AdminUserForm>[] = [
|
||||
{ type: "selection" },
|
||||
{
|
||||
title: "用户名",
|
||||
@@ -111,14 +162,7 @@ const columns: DataTableColumn<AdminUserForm>[] = [
|
||||
title: "密码",
|
||||
key: "raw_password",
|
||||
width: 150,
|
||||
render: (row) =>
|
||||
h(PasswordCell, {
|
||||
user: row,
|
||||
revealed: revealedPasswords.value.has(row.id),
|
||||
onToggle: (id: number) => {
|
||||
if (!revealedPasswords.value.delete(id)) revealedPasswords.value.add(id)
|
||||
},
|
||||
}),
|
||||
render: renderPassword,
|
||||
},
|
||||
{ title: "上次登录", key: "last_login", width: 150, render: lastLoginText },
|
||||
{
|
||||
@@ -136,17 +180,12 @@ const columns: DataTableColumn<AdminUserForm>[] = [
|
||||
key: "actions",
|
||||
title: "",
|
||||
minWidth: 300,
|
||||
render: (row) =>
|
||||
h(Actions, {
|
||||
user: row,
|
||||
onDeleteUser: onDeleteUsers,
|
||||
onUserBanned,
|
||||
onOpenEditModal,
|
||||
onResetPassword,
|
||||
}),
|
||||
render: renderActions,
|
||||
},
|
||||
]
|
||||
|
||||
const columns = computed(() => (isMobile.value ? mobileColumns : desktopColumns))
|
||||
|
||||
const options: SelectOption[] = [
|
||||
{ label: "普通", value: USER_TYPE.REGULAR_USER },
|
||||
{ label: "学生管理员", value: USER_TYPE.STUDENT_ADMIN },
|
||||
@@ -314,7 +353,7 @@ watch(() => [query.page, query.limit, query.type, query.orderBy, query.className
|
||||
</template>
|
||||
<template #filters>
|
||||
<n-input
|
||||
style="width: 220px"
|
||||
:style="{ width: isMobile ? '100%' : '220px' }"
|
||||
v-model:value="query.keyword"
|
||||
placeholder="用户名或名字"
|
||||
clearable
|
||||
@@ -363,7 +402,7 @@ watch(() => [query.page, query.limit, query.type, query.orderBy, query.className
|
||||
@close="onCloseEditModal"
|
||||
>
|
||||
<n-form label-placement="left" v-if="userEditing">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
<n-grid :cols="isMobile ? 1 : 2" :x-gap="16">
|
||||
<n-form-item-gi :span="1" label="用户">
|
||||
<n-input v-model:value="userEditing.username" />
|
||||
</n-form-item-gi>
|
||||
@@ -420,6 +459,29 @@ watch(() => [query.page, query.limit, query.type, query.orderBy, query.className
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.user-table :deep(.m-card) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-table :deep(.m-meta) {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.user-table :deep(.m-item) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-table :deep(.m-password) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user-table :deep(.num) {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
|
||||
Reference in new issue
Block a user