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">
|
<script setup lang="ts">
|
||||||
import { NButton, NTag } from "naive-ui"
|
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 { parseTime } from "utils/functions"
|
||||||
import type { Server } from "utils/types"
|
import type { Server } from "utils/types"
|
||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
@@ -140,7 +145,21 @@ async function getWebsiteConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveWebsiteConfig() {
|
async function saveWebsiteConfig() {
|
||||||
|
// 班级号要和用户名里 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)
|
await editWebsite(websiteConfig)
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("保存失败:" + err.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
message.success("网站配置保存成功")
|
message.success("网站配置保存成功")
|
||||||
getWebsiteConfig()
|
getWebsiteConfig()
|
||||||
configStore.getConfig()
|
configStore.getConfig()
|
||||||
@@ -214,7 +233,13 @@ onMounted(() => {
|
|||||||
</n-form>
|
</n-form>
|
||||||
<n-form label-placement="left">
|
<n-form label-placement="left">
|
||||||
<n-form-item label="班级列表">
|
<n-form-item label="班级列表">
|
||||||
|
<n-flex vertical size="small">
|
||||||
<n-dynamic-tags v-model:value="websiteConfig.class_list" />
|
<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-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<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"
|
import { importUsers } from "../api"
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -12,6 +18,17 @@ function generateUsers() {
|
|||||||
message.info("请填写相关内容")
|
message.info("请填写相关内容")
|
||||||
return false
|
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}` : ""
|
let className = !!prefix.value ? `ks${prefix.value}` : ""
|
||||||
rawInput.value = rawInput.value.trim()
|
rawInput.value = rawInput.value.trim()
|
||||||
const inputs = rawInput.value.split("\n")
|
const inputs = rawInput.value.split("\n")
|
||||||
@@ -64,9 +81,9 @@ async function submit() {
|
|||||||
style="width: 170px"
|
style="width: 170px"
|
||||||
v-model:value="prefix"
|
v-model:value="prefix"
|
||||||
clearable
|
clearable
|
||||||
:max="9999"
|
:max="CLASS_NAME_MAX_VALUE"
|
||||||
:min="0"
|
:min="0"
|
||||||
placeholder="班级号"
|
:placeholder="`班级号(${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位)`"
|
||||||
/>
|
/>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-input
|
<n-input
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ async function handleEditUser() {
|
|||||||
message.error("密码长度不得小于 6")
|
message.error("密码长度不得小于 6")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// http 拦截器只对 login-required / permission-denied 自动弹提示,
|
||||||
|
// 其余业务错误(比如班级号位数不对)不接住就什么都不显示
|
||||||
|
try {
|
||||||
if (create.value) {
|
if (create.value) {
|
||||||
const newUser = [
|
const newUser = [
|
||||||
[
|
[
|
||||||
@@ -208,9 +211,15 @@ async function handleEditUser() {
|
|||||||
await importUsers(newUser)
|
await importUsers(newUser)
|
||||||
listUsers()
|
listUsers()
|
||||||
} else {
|
} else {
|
||||||
const user = Object.assign(userEditing.value, { password: password.value })
|
const user = Object.assign(userEditing.value, {
|
||||||
|
password: password.value,
|
||||||
|
})
|
||||||
await editUser(user)
|
await editUser(user)
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("保存失败:" + err.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
userEditing.value = null
|
userEditing.value = null
|
||||||
password.value = ""
|
password.value = ""
|
||||||
toggleCreate(false)
|
toggleCreate(false)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
|
import { USERNAME_CLASS_RE } from "utils/constants"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "题目" | "用户"
|
type: "题目" | "用户"
|
||||||
@@ -10,12 +11,14 @@ const props = defineProps<Props>()
|
|||||||
|
|
||||||
const emits = defineEmits(["click", "search", "filterClass"])
|
const emits = defineEmits(["click", "search", "filterClass"])
|
||||||
|
|
||||||
|
// 用同一个正则判断,避免 kstest 这类 ks 开头但没有班级号的用户名
|
||||||
|
// 也显示出按钮,点了却什么都不发生
|
||||||
const showFilterClass = computed(() => {
|
const showFilterClass = computed(() => {
|
||||||
return props.type === "用户" && props.username?.startsWith("ks")
|
return props.type === "用户" && USERNAME_CLASS_RE.test(props.username ?? "")
|
||||||
})
|
})
|
||||||
|
|
||||||
function filterClass() {
|
function filterClass() {
|
||||||
const match = props.username!.match(/^ks\d{3,4}/)
|
const match = props.username!.match(USERNAME_CLASS_RE)
|
||||||
const classname = match ? match[0] : ""
|
const classname = match ? match[0] : ""
|
||||||
if (!classname) return
|
if (!classname) return
|
||||||
emits("filterClass", classname)
|
emits("filterClass", classname)
|
||||||
|
|||||||
@@ -325,3 +325,24 @@ export const DURATION_OPTIONS = [
|
|||||||
{ label: "半年内", value: "months:6" },
|
{ label: "半年内", value: "months:6" },
|
||||||
{ label: "一年内", value: "years:1" },
|
{ label: "一年内", value: "years:1" },
|
||||||
] as const
|
] 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