feat(admin): 班级号位数校验,规则收敛到 constants
后端现在会拒绝位数不对的班级号,前端提前拦一道并给出明确提示。
- generate.vue 导入前校验班级号范围
- config.vue 校验班级列表每一项,输入框下方补说明
- 位数规则收敛到 utils/constants 的 CLASS_NAME_* 常量,
三个使用方都改为引用
顺带两处修正:
- config.vue 和 list.vue 的保存都没有 try/catch。http 拦截器只对
login-required / permission-denied 自动弹提示,其余业务错误不接住
就什么都不显示,连"用户名已存在"这类老错误也是静默的
- ButtonWithSearch 原来用 startsWith("ks") 决定显不显示按班级筛选,
kstest 这类账号会显示按钮但点了没反应;改用同一个正则判断
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NTag } from "naive-ui"
|
||||
import {
|
||||
CLASS_NAME_MAX_DIGITS,
|
||||
CLASS_NAME_MIN_DIGITS,
|
||||
CLASS_NAME_RE,
|
||||
} from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { Server } from "utils/types"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
@@ -140,7 +145,21 @@ async function getWebsiteConfig() {
|
||||
}
|
||||
|
||||
async function saveWebsiteConfig() {
|
||||
await editWebsite(websiteConfig)
|
||||
// 班级号要和用户名里 ks 后面那段对得上,位数不对登录页会查不到该班学生。
|
||||
// 后端 CreateEditWebsiteConfigSerializer 也会拦,这里先报更明确的错
|
||||
const invalid = websiteConfig.class_list.filter((c) => !CLASS_NAME_RE.test(c))
|
||||
if (invalid.length) {
|
||||
message.error(
|
||||
`班级号 ${invalid.join("、")} 必须是 ${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位数字`,
|
||||
)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await editWebsite(websiteConfig)
|
||||
} catch (err: any) {
|
||||
message.error("保存失败:" + err.data)
|
||||
return
|
||||
}
|
||||
message.success("网站配置保存成功")
|
||||
getWebsiteConfig()
|
||||
configStore.getConfig()
|
||||
@@ -214,7 +233,13 @@ onMounted(() => {
|
||||
</n-form>
|
||||
<n-form label-placement="left">
|
||||
<n-form-item label="班级列表">
|
||||
<n-dynamic-tags v-model:value="websiteConfig.class_list" />
|
||||
<n-flex vertical size="small">
|
||||
<n-dynamic-tags v-model:value="websiteConfig.class_list" />
|
||||
<n-text depth="3" style="font-size: 12px">
|
||||
填 {{ CLASS_NAME_MIN_DIGITS }}~{{ CLASS_NAME_MAX_DIGITS }}
|
||||
位数字,如 251、2510,要和用户名里 ks 后面那段一致
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex align="center">
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CLASS_NAME_MAX_DIGITS,
|
||||
CLASS_NAME_MAX_VALUE,
|
||||
CLASS_NAME_MIN_DIGITS,
|
||||
CLASS_NAME_MIN_VALUE,
|
||||
} from "utils/constants"
|
||||
import { importUsers } from "../api"
|
||||
|
||||
const message = useMessage()
|
||||
@@ -12,6 +18,17 @@ function generateUsers() {
|
||||
message.info("请填写相关内容")
|
||||
return false
|
||||
}
|
||||
// 后端 get_class_name 只认合法位数的班级号,位数不对会整批拒绝导入,
|
||||
// 这里先拦一道,省得填完一屏用户名才被打回来
|
||||
if (
|
||||
prefix.value &&
|
||||
(prefix.value < CLASS_NAME_MIN_VALUE || prefix.value > CLASS_NAME_MAX_VALUE)
|
||||
) {
|
||||
message.error(
|
||||
`班级号 ${prefix.value} 必须是 ${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位数字`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
let className = !!prefix.value ? `ks${prefix.value}` : ""
|
||||
rawInput.value = rawInput.value.trim()
|
||||
const inputs = rawInput.value.split("\n")
|
||||
@@ -64,9 +81,9 @@ async function submit() {
|
||||
style="width: 170px"
|
||||
v-model:value="prefix"
|
||||
clearable
|
||||
:max="9999"
|
||||
:max="CLASS_NAME_MAX_VALUE"
|
||||
:min="0"
|
||||
placeholder="班级号"
|
||||
:placeholder="`班级号(${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位)`"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-input
|
||||
|
||||
@@ -196,20 +196,29 @@ async function handleEditUser() {
|
||||
message.error("密码长度不得小于 6")
|
||||
return
|
||||
}
|
||||
if (create.value) {
|
||||
const newUser = [
|
||||
[
|
||||
userEditing.value.username,
|
||||
password.value,
|
||||
userEditing.value.email,
|
||||
userEditing.value.real_name,
|
||||
],
|
||||
]
|
||||
await importUsers(newUser)
|
||||
listUsers()
|
||||
} else {
|
||||
const user = Object.assign(userEditing.value, { password: password.value })
|
||||
await editUser(user)
|
||||
// http 拦截器只对 login-required / permission-denied 自动弹提示,
|
||||
// 其余业务错误(比如班级号位数不对)不接住就什么都不显示
|
||||
try {
|
||||
if (create.value) {
|
||||
const newUser = [
|
||||
[
|
||||
userEditing.value.username,
|
||||
password.value,
|
||||
userEditing.value.email,
|
||||
userEditing.value.real_name,
|
||||
],
|
||||
]
|
||||
await importUsers(newUser)
|
||||
listUsers()
|
||||
} else {
|
||||
const user = Object.assign(userEditing.value, {
|
||||
password: password.value,
|
||||
})
|
||||
await editUser(user)
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error("保存失败:" + err.data)
|
||||
return
|
||||
}
|
||||
userEditing.value = null
|
||||
password.value = ""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { USERNAME_CLASS_RE } from "utils/constants"
|
||||
|
||||
interface Props {
|
||||
type: "题目" | "用户"
|
||||
@@ -10,12 +11,14 @@ const props = defineProps<Props>()
|
||||
|
||||
const emits = defineEmits(["click", "search", "filterClass"])
|
||||
|
||||
// 用同一个正则判断,避免 kstest 这类 ks 开头但没有班级号的用户名
|
||||
// 也显示出按钮,点了却什么都不发生
|
||||
const showFilterClass = computed(() => {
|
||||
return props.type === "用户" && props.username?.startsWith("ks")
|
||||
return props.type === "用户" && USERNAME_CLASS_RE.test(props.username ?? "")
|
||||
})
|
||||
|
||||
function filterClass() {
|
||||
const match = props.username!.match(/^ks\d{3,4}/)
|
||||
const match = props.username!.match(USERNAME_CLASS_RE)
|
||||
const classname = match ? match[0] : ""
|
||||
if (!classname) return
|
||||
emits("filterClass", classname)
|
||||
|
||||
@@ -325,3 +325,24 @@ export const DURATION_OPTIONS = [
|
||||
{ label: "半年内", value: "months:6" },
|
||||
{ label: "一年内", value: "years:1" },
|
||||
] as const
|
||||
|
||||
// 班级号的位数范围。学生用户名形如 ks<班级号><姓名>,班级号还要跟
|
||||
// 网站配置里的班级列表对得上。
|
||||
// 后端 OnlineJudge/utils/shortcuts.py 的 CLASS_NAME_MIN/MAX_DIGITS
|
||||
// 是同一条规则的另一份,改这里必须同时改那边。
|
||||
export const CLASS_NAME_MIN_DIGITS = 3
|
||||
export const CLASS_NAME_MAX_DIGITS = 4
|
||||
|
||||
/** 合法班级号:3~4 位纯数字 */
|
||||
export const CLASS_NAME_RE = new RegExp(
|
||||
`^\\d{${CLASS_NAME_MIN_DIGITS},${CLASS_NAME_MAX_DIGITS}}$`,
|
||||
)
|
||||
|
||||
/** 用户名开头的 ks<班级号>,用于从用户名里认出班级 */
|
||||
export const USERNAME_CLASS_RE = new RegExp(
|
||||
`^ks\\d{${CLASS_NAME_MIN_DIGITS},${CLASS_NAME_MAX_DIGITS}}`,
|
||||
)
|
||||
|
||||
/** 班级号作为数字时的上下界,给 n-input-number 用 */
|
||||
export const CLASS_NAME_MIN_VALUE = 10 ** (CLASS_NAME_MIN_DIGITS - 1)
|
||||
export const CLASS_NAME_MAX_VALUE = 10 ** CLASS_NAME_MAX_DIGITS - 1
|
||||
|
||||
Reference in New Issue
Block a user