fix(后台题单): 修掉一串前后端字段名对不上导致的空列与失败操作
Some checks failed
Deploy / deploy (push) Has been cancelled

后台题单接口下发的是扁平结构(displayId / title / isRequired),前端却按前台
题单页的嵌套结构取值,naive-ui 的 key 也不解析点号路径,于是「题目ID」「题目
标题」两列永远是空的。同一处还连累了另外几个操作:

- 编辑题目弹窗读 problem.problem.title,点「编辑」直接抛错;
- 添加题目发 problem_id / is_required,api 层取 problemId,出站是 undefined,
  被后端按参数错误 400 掉;
- 编辑题目发 is_required,「必做」怎么改都不生效;
- 新建/编辑奖章发 condition_type,toBadgeBody 读 conditionType,同样 400。

根因是 admin/api.ts 把这批后台接口的返回值标成了 oj 侧的 ProblemSet /
ProblemSetBadge / ProblemSetProblem,加上 detail.vue 四个 handler 用 data: any
接弹窗数据,字段拼错编译器一句都不吭。现在返回值全部换成 Admin* 契约类型,
handler 换成 AddProblemToSetRequest / UpdateProblemInSetRequest 和一个本地
BadgeFormData(条件类型是「完成所有题目」时不带 conditionValue,后端补 0),
两个奖章弹窗里的 const data: any 也一并拆掉。

验证:dev 栈实跑,列表两列有值、编辑弹窗正常打开、加题 / 改必做 / 建奖章 /
改奖章四条链路走完界面都成功落库;bun run type-check 通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011WmMi9tPrnSAwwAy2i4WAa
This commit is contained in:
2026-09-03 20:49:57 -06:00
parent 1729b9db4d
commit b130d805be
11 changed files with 89 additions and 80 deletions

View File

@@ -38,10 +38,10 @@ import type {
Tutorial, Tutorial,
User, User,
WebsiteConfig, WebsiteConfig,
ProblemSet, AdminProblemSetBadge,
ProblemSetBadge, AdminProblemSetList,
ProblemSetList, AdminProblemSet,
ProblemSetProblem, AdminProblemSetProblem,
TutorialListItem, TutorialListItem,
} from "utils/types" } from "utils/types"
@@ -498,20 +498,20 @@ export function getProblemSetList(
difficulty = "", difficulty = "",
status = "", status = "",
) { ) {
return api.get<ProblemSetList>("admin/problem-sets", { return api.get<AdminProblemSetList>("admin/problem-sets", {
params: { offset, limit, keyword, difficulty, status }, params: { offset, limit, keyword, difficulty, status },
}) })
} }
export function getProblemSetDetail(id: number) { export function getProblemSetDetail(id: number) {
return api.get<ProblemSet>(`admin/problem-sets/${id}`) return api.get<AdminProblemSet>(`admin/problem-sets/${id}`)
} }
interface ProblemSetBody { interface ProblemSetBody {
title?: string title?: string
description?: string description?: string
difficulty?: ProblemSet["difficulty"] difficulty?: AdminProblemSet["difficulty"]
status?: ProblemSet["status"] status?: AdminProblemSet["status"]
// 表单里是 Date出站要 ISO 串 // 表单里是 Date出站要 ISO 串
endTime?: Date | null endTime?: Date | null
visible?: boolean visible?: boolean
@@ -529,11 +529,11 @@ function toProblemSetBody(data: ProblemSetBody) {
} }
export function createProblemSet(data: ProblemSetBody) { export function createProblemSet(data: ProblemSetBody) {
return api.post<ProblemSet>("admin/problem-sets", toProblemSetBody(data)) return api.post<AdminProblemSet>("admin/problem-sets", toProblemSetBody(data))
} }
export function editProblemSet(data: ProblemSetBody & { id: number }) { export function editProblemSet(data: ProblemSetBody & { id: number }) {
return api.put<ProblemSet>( return api.put<AdminProblemSet>(
`admin/problem-sets/${data.id}`, `admin/problem-sets/${data.id}`,
toProblemSetBody(data), toProblemSetBody(data),
) )
@@ -544,16 +544,16 @@ export function deleteProblemSet(id: number) {
} }
export function toggleProblemSetVisible(id: number) { export function toggleProblemSetVisible(id: number) {
return api.put<ProblemSet>(`admin/problem-sets/${id}/visibility`) return api.put<AdminProblemSet>(`admin/problem-sets/${id}/visibility`)
} }
export function updateProblemSetStatus(id: number, status: string) { export function updateProblemSetStatus(id: number, status: string) {
return api.put<ProblemSet>(`admin/problem-sets/${id}/status`, { status }) return api.put<AdminProblemSet>(`admin/problem-sets/${id}/status`, { status })
} }
// 题单题目管理 API // 题单题目管理 API
export function getProblemSetProblems(problemSetId: number) { export function getProblemSetProblems(problemSetId: number) {
return api.get<ProblemSetProblem[]>( return api.get<AdminProblemSetProblem[]>(
`admin/problem-sets/${problemSetId}/problems`, `admin/problem-sets/${problemSetId}/problems`,
) )
} }
@@ -604,7 +604,7 @@ export function removeProblemFromSet(
// 题单奖章管理 API // 题单奖章管理 API
export function getProblemSetBadges(problemSetId: number) { export function getProblemSetBadges(problemSetId: number) {
return api.get<ProblemSetBadge[]>( return api.get<AdminProblemSetBadge[]>(
`admin/problem-sets/${problemSetId}/badges`, `admin/problem-sets/${problemSetId}/badges`,
) )
} }
@@ -613,7 +613,7 @@ interface BadgeBody {
name?: string name?: string
description?: string description?: string
icon?: string icon?: string
conditionType?: ProblemSetBadge["conditionType"] conditionType?: AdminProblemSetBadge["conditionType"]
conditionValue?: number conditionValue?: number
} }
@@ -628,7 +628,7 @@ function toBadgeBody(data: BadgeBody) {
} }
export function createProblemSetBadge(problemSetId: number, data: BadgeBody) { export function createProblemSetBadge(problemSetId: number, data: BadgeBody) {
return api.post<ProblemSetBadge>( return api.post<AdminProblemSetBadge>(
`admin/problem-sets/${problemSetId}/badges`, `admin/problem-sets/${problemSetId}/badges`,
toBadgeBody(data), toBadgeBody(data),
) )
@@ -639,7 +639,7 @@ export function editProblemSetBadge(
badgeId: number, badgeId: number,
data: BadgeBody, data: BadgeBody,
) { ) {
return api.put<ProblemSetBadge>( return api.put<AdminProblemSetBadge>(
`admin/problem-sets/${problemSetId}/badges/${badgeId}`, `admin/problem-sets/${problemSetId}/badges/${badgeId}`,
toBadgeBody(data), toBadgeBody(data),
) )

View File

@@ -11,8 +11,8 @@ interface Emits {
name: string name: string
description: string description: string
icon: string icon: string
condition_type: "all_problems" | "problem_count" | "score" conditionType: "all_problems" | "problem_count" | "score"
condition_value?: number conditionValue?: number
}, },
): void ): void
} }
@@ -45,19 +45,16 @@ const conditionTypeOptions = [
] ]
function handleConfirm() { function handleConfirm() {
const data: any = { emit("confirm", {
name: newBadgeName.value, name: newBadgeName.value,
description: newBadgeDescription.value, description: newBadgeDescription.value,
icon: newBadgeIcon.value, icon: newBadgeIcon.value,
condition_type: newBadgeConditionType.value, conditionType: newBadgeConditionType.value,
} // 只有非"完成所有题目"时才带条件值
...(newBadgeConditionType.value === "all_problems"
// 只有非"完成所有题目"时才添加条件值 ? {}
if (newBadgeConditionType.value !== "all_problems") { : { conditionValue: newBadgeConditionValue.value }),
data.conditionValue = newBadgeConditionValue.value })
}
emit("confirm", data)
} }
function handleCancel() { function handleCancel() {

View File

@@ -8,9 +8,9 @@ interface Emits {
( (
e: "confirm", e: "confirm",
data: { data: {
problem_id: string problemId: string
order: number order: number
is_required: boolean isRequired: boolean
score: number score: number
hint: string hint: string
}, },
@@ -28,9 +28,9 @@ const newProblemHint = ref("")
function handleConfirm() { function handleConfirm() {
emit("confirm", { emit("confirm", {
problem_id: newProblemId.value, problemId: newProblemId.value,
order: newProblemOrder.value, order: newProblemOrder.value,
is_required: newProblemRequired.value, isRequired: newProblemRequired.value,
score: newProblemScore.value, score: newProblemScore.value,
hint: newProblemHint.value, hint: newProblemHint.value,
}) })

View File

@@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from "vue" import { h } from "vue"
import type { ProblemSetBadge } from "utils/types" import type { AdminProblemSetBadge } from "utils/types"
import { NButton, NImage } from "naive-ui" import { NButton, NImage } from "naive-ui"
interface Props { interface Props {
badges: ProblemSetBadge[] badges: AdminProblemSetBadge[]
} }
interface Emits { interface Emits {
(e: "add-badge"): void (e: "add-badge"): void
(e: "edit-badge", badge: ProblemSetBadge): void (e: "edit-badge", badge: AdminProblemSetBadge): void
(e: "delete-badge", badgeId: number): void (e: "delete-badge", badgeId: number): void
} }

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ProblemSetBadge } from "utils/types" import type { AdminProblemSetBadge } from "utils/types"
interface Props { interface Props {
show: boolean show: boolean
badge: ProblemSetBadge | null badge: AdminProblemSetBadge | null
} }
interface Emits { interface Emits {
@@ -14,8 +14,8 @@ interface Emits {
name: string name: string
description: string description: string
icon: string icon: string
condition_type: "all_problems" | "problem_count" | "score" conditionType: "all_problems" | "problem_count" | "score"
condition_value?: number conditionValue?: number
}, },
): void ): void
} }
@@ -49,19 +49,16 @@ const conditionTypeOptions = [
] ]
function handleConfirm() { function handleConfirm() {
const data: any = { emit("confirm", {
name: editBadgeName.value, name: editBadgeName.value,
description: editBadgeDescription.value, description: editBadgeDescription.value,
icon: editBadgeIcon.value, icon: editBadgeIcon.value,
condition_type: editBadgeConditionType.value, conditionType: editBadgeConditionType.value,
} // 只有非"完成所有题目"时才带条件值
...(editBadgeConditionType.value === "all_problems"
// 只有非"完成所有题目"时才添加条件值 ? {}
if (editBadgeConditionType.value !== "all_problems") { : { conditionValue: editBadgeConditionValue.value }),
data.conditionValue = editBadgeConditionValue.value })
}
emit("confirm", data)
} }
function handleCancel() { function handleCancel() {

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ProblemSetProblem } from "utils/types" import type { AdminProblemSetProblem } from "utils/types"
interface Props { interface Props {
show: boolean show: boolean
problem: ProblemSetProblem | null problem: AdminProblemSetProblem | null
} }
interface Emits { interface Emits {
@@ -12,7 +12,7 @@ interface Emits {
e: "confirm", e: "confirm",
data: { data: {
order: number order: number
is_required: boolean isRequired: boolean
score: number score: number
hint: string hint: string
}, },
@@ -30,7 +30,7 @@ const editProblemHint = ref("")
function handleConfirm() { function handleConfirm() {
emit("confirm", { emit("confirm", {
order: editProblemOrder.value, order: editProblemOrder.value,
is_required: editProblemRequired.value, isRequired: editProblemRequired.value,
score: editProblemScore.value, score: editProblemScore.value,
hint: editProblemHint.value || "", hint: editProblemHint.value || "",
}) })
@@ -65,7 +65,7 @@ watch(
> >
<n-form v-if="problem"> <n-form v-if="problem">
<n-form-item label="题目标题"> <n-form-item label="题目标题">
<n-input :value="problem.problem.title" disabled /> <n-input :value="problem.title" disabled />
</n-form-item> </n-form-item>
<n-form-item label="顺序"> <n-form-item label="顺序">
<n-input-number <n-input-number

View File

@@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from "vue" import { h } from "vue"
import { NDataTable, NButton, NFlex } from "naive-ui" import { NDataTable, NButton, NFlex } from "naive-ui"
import type { ProblemSetProblem } from "utils/types" import type { AdminProblemSetProblem } from "utils/types"
interface Props { interface Props {
problems: ProblemSetProblem[] problems: AdminProblemSetProblem[]
} }
interface Emits { interface Emits {
(e: "add-problem"): void (e: "add-problem"): void
(e: "edit-problem", problem: ProblemSetProblem): void (e: "edit-problem", problem: AdminProblemSetProblem): void
(e: "remove-problem", problemSetProblemId: number): void (e: "remove-problem", problemSetProblemId: number): void
} }
@@ -27,12 +27,12 @@ defineEmits<Emits>()
</n-flex> </n-flex>
<n-data-table <n-data-table
:columns="[ :columns="[
{ title: '题目ID', key: 'problem._id', width: 80 }, { title: '题目ID', key: 'displayId', width: 80 },
{ title: '题目标题', key: 'problem.title', minWidth: 200 }, { title: '题目标题', key: 'title', minWidth: 200 },
{ title: '顺序', key: 'order', width: 80 }, { title: '顺序', key: 'order', width: 80 },
{ {
title: '必做', title: '必做',
key: 'is_required', key: 'isRequired',
width: 80, width: 80,
render: (row) => (row.isRequired ? '是' : '否'), render: (row) => (row.isRequired ? '是' : '否'),
}, },

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import type { ProblemSet } from "utils/types" import type { AdminProblemSet } from "utils/types"
interface Props { interface Props {
problemSet: ProblemSet problemSet: AdminProblemSet
} }
defineProps<Props>() defineProps<Props>()

View File

@@ -1,9 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, NButton, NFlex } from "naive-ui" import { NTabPane, NTabs, NButton, NFlex } from "naive-ui"
import type { import type {
ProblemSet, AdminProblemSet,
ProblemSetProblem, AdminProblemSetProblem,
ProblemSetBadge, AddProblemToSetRequest,
UpdateProblemInSetRequest,
CreateProblemSetBadgeRequest,
AdminProblemSetBadge,
AdminProblemSetProgress, AdminProblemSetProgress,
} from "utils/types" } from "utils/types"
import { import {
@@ -32,11 +35,16 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
/** 奖章弹窗的表单值:条件类型是「完成所有题目」时不带 conditionValue后端补 0 */
type BadgeFormData = Omit<CreateProblemSetBadgeRequest, "conditionValue"> & {
conditionValue?: number
}
const problemSetId = computed(() => Number(route.params.problemSetId)) const problemSetId = computed(() => Number(route.params.problemSetId))
const problemSet = ref<ProblemSet | null>(null) const problemSet = ref<AdminProblemSet | null>(null)
const problems = ref<ProblemSetProblem[]>([]) const problems = ref<AdminProblemSetProblem[]>([])
const badges = ref<ProblemSetBadge[]>([]) const badges = ref<AdminProblemSetBadge[]>([])
const progress = ref<AdminProblemSetProgress[]>([]) const progress = ref<AdminProblemSetProgress[]>([])
// 模态框状态 // 模态框状态
@@ -46,8 +54,8 @@ const showAddBadgeModal = ref(false)
const showEditBadgeModal = ref(false) const showEditBadgeModal = ref(false)
// 编辑数据 // 编辑数据
const editingProblem = ref<ProblemSetProblem | null>(null) const editingProblem = ref<AdminProblemSetProblem | null>(null)
const editingBadge = ref<ProblemSetBadge | null>(null) const editingBadge = ref<AdminProblemSetBadge | null>(null)
async function loadProblemSetDetail() { async function loadProblemSetDetail() {
try { try {
@@ -85,7 +93,7 @@ async function loadProgress() {
} }
} }
async function handleAddProblem(data: any) { async function handleAddProblem(data: AddProblemToSetRequest) {
try { try {
await addProblemToSet(problemSetId.value, data) await addProblemToSet(problemSetId.value, data)
message.success("题目添加成功") message.success("题目添加成功")
@@ -108,7 +116,7 @@ async function handleRemoveProblem(problemSetProblemId: number) {
} }
} }
async function handleEditProblem(data: any) { async function handleEditProblem(data: UpdateProblemInSetRequest) {
if (!editingProblem.value) return if (!editingProblem.value) return
try { try {
@@ -121,7 +129,7 @@ async function handleEditProblem(data: any) {
} }
} }
async function handleAddBadge(data: any) { async function handleAddBadge(data: BadgeFormData) {
try { try {
await createProblemSetBadge(problemSetId.value, data) await createProblemSetBadge(problemSetId.value, data)
message.success("奖章创建成功") message.success("奖章创建成功")
@@ -142,7 +150,7 @@ async function handleDeleteBadge(badgeId: number) {
} }
} }
async function handleEditBadge(data: any) { async function handleEditBadge(data: BadgeFormData) {
if (!editingBadge.value) return if (!editingBadge.value) return
try { try {
@@ -173,12 +181,12 @@ function openAddBadgeModal() {
showAddBadgeModal.value = true showAddBadgeModal.value = true
} }
function openEditProblemModal(problem: ProblemSetProblem) { function openEditProblemModal(problem: AdminProblemSetProblem) {
editingProblem.value = problem editingProblem.value = problem
showEditProblemModal.value = true showEditProblemModal.value = true
} }
function openEditBadgeModal(badge: ProblemSetBadge) { function openEditBadgeModal(badge: AdminProblemSetBadge) {
editingBadge.value = badge editingBadge.value = badge
showEditBadgeModal.value = true showEditBadgeModal.value = true
} }

View File

@@ -2,13 +2,13 @@
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { usePagination } from "shared/composables/pagination" import { usePagination } from "shared/composables/pagination"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import type { ProblemSet } from "utils/types" import type { AdminProblemSet } from "utils/types"
import { getProblemSetList, toggleProblemSetVisible } from "../api" import { getProblemSetList, toggleProblemSetVisible } from "../api"
import Actions from "./components/Actions.vue" import Actions from "./components/Actions.vue"
import { NTag, NSwitch } from "naive-ui" import { NTag, NSwitch } from "naive-ui"
const total = ref(0) const total = ref(0)
const problemSets = ref<ProblemSet[]>([]) const problemSets = ref<AdminProblemSet[]>([])
interface ProblemSetQuery { interface ProblemSetQuery {
keyword: string keyword: string
@@ -37,7 +37,7 @@ const statusOptions = [
{ label: "草稿", value: "draft" }, { label: "草稿", value: "draft" },
] ]
const columns: DataTableColumn<ProblemSet>[] = [ const columns: DataTableColumn<AdminProblemSet>[] = [
{ title: "ID", key: "id", width: 80 }, { title: "ID", key: "id", width: 80 },
{ title: "标题", key: "title", minWidth: 200 }, { title: "标题", key: "title", minWidth: 200 },
{ title: "描述", key: "description", minWidth: 300, ellipsis: true }, { title: "描述", key: "description", minWidth: 300, ellipsis: true },

View File

@@ -408,6 +408,13 @@ export type {
AcmHelperItem, AcmHelperItem,
AdminContestList, AdminContestList,
AdminProblemSetProgress, AdminProblemSetProgress,
AdminProblemSetProblem,
AdminProblemSet,
AdminProblemSetList,
AdminProblemSetBadge,
AddProblemToSetRequest,
UpdateProblemInSetRequest,
CreateProblemSetBadgeRequest,
AdminAiReport, AdminAiReport,
AdminAiReportListItem, AdminAiReportListItem,
AdminAiReportList, AdminAiReportList,