From 7b3a5212e6d899a9c601aa2424be479bc11f4183 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sun, 18 Oct 2026 10:25:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E7=8E=A9=E5=AE=B6?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E8=AF=95=E5=8D=B7=E7=BC=96=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增试卷管理 Tab:列表展示、启用状态、题数/总分/通过分、目标权限标签、复制直链 - 新增试卷编辑弹窗:标题、描述、阅卷模式、通过分数、启用开关 - 目标权限多选、前置身份开关 + 判断方式(ANY/ALL)+ 权限多选 - 题目编辑:五种题型、上下移、删除、信息题、选答题、生死题、自动批复 - 选择题选项增删改、默认选项、正确选项、多选计分模式、选项得分/扣分 - 填空/简答作答字数上限与标准答案 - 查重开关与通过邮件配置(标题/正文/补充说明/附带邀请码) - 保存前校验:标题、题目、权限、通过分、邮件必填项 - 公开端审核类型改名 Public 前缀,避免与后台同名冲突 - 补充中英文 locale 共 37 个键 --- .../ui/skin-site/SkinSiteReviewView.vue | 14 +- .../ui/skin-site/admin/AdminReviewPage.vue | 1518 +++++++++++++++-- .../app-frontend/src/helpers/skin-site-api.ts | 125 +- .../app-frontend/src/locales/en-US/index.json | 39 +- .../app-frontend/src/locales/zh-CN/index.json | 39 +- 5 files changed, 1534 insertions(+), 201 deletions(-) diff --git a/apps/app-frontend/src/components/ui/skin-site/SkinSiteReviewView.vue b/apps/app-frontend/src/components/ui/skin-site/SkinSiteReviewView.vue index 558757f..759e041 100644 --- a/apps/app-frontend/src/components/ui/skin-site/SkinSiteReviewView.vue +++ b/apps/app-frontend/src/components/ui/skin-site/SkinSiteReviewView.vue @@ -5,8 +5,8 @@ import { computed, onMounted, ref } from 'vue' import { skinSiteReviewCatalog, - type SkinSiteReviewPaper, - type SkinSiteReviewQuestion, + type SkinSiteReviewPublicPaper, + type SkinSiteReviewPublicQuestion, type SkinSiteReviewReceipt, skinSiteSubmitReview, } from '@/helpers/skin-site-api' @@ -46,10 +46,10 @@ const messages = defineMessages({ const emit = defineEmits<{ (e: 'back'): void }>() -const papers = ref([]) +const papers = ref([]) const loading = ref(true) const failed = ref(false) -const activePaper = ref(null) +const activePaper = ref(null) const email = ref('') const inviteCode = ref('') const answers = ref>({}) @@ -70,7 +70,7 @@ const requiredAnswered = computed(() => { const canSubmit = computed(() => emailValid.value && requiredAnswered.value && !submitting.value) -function defaultAnswer(q: SkinSiteReviewQuestion): string | string[] { +function defaultAnswer(q: SkinSiteReviewPublicQuestion): string | string[] { if (q.type === 'MULTIPLE' || q.type === 'SINGLE') { const preset = (q.options ?? []).filter((o) => o.defaultSelected).map((o) => o.id) return q.type === 'MULTIPLE' ? preset : (preset[0] ?? '') @@ -78,7 +78,7 @@ function defaultAnswer(q: SkinSiteReviewQuestion): string | string[] { return '' } -function openPaper(paper: SkinSiteReviewPaper) { +function openPaper(paper: SkinSiteReviewPublicPaper) { activePaper.value = paper answers.value = {} for (const q of paper.questions) answers.value[q.id] = defaultAnswer(q) @@ -93,7 +93,7 @@ function closePaper() { receipt.value = null } -function toggleMultiple(q: SkinSiteReviewQuestion, optionId: string, checked: boolean) { +function toggleMultiple(q: SkinSiteReviewPublicQuestion, optionId: string, checked: boolean) { const current = Array.isArray(answers.value[q.id]) ? (answers.value[q.id] as string[]) : [] answers.value[q.id] = checked ? [...current.filter((id) => id !== optionId), optionId] diff --git a/apps/app-frontend/src/components/ui/skin-site/admin/AdminReviewPage.vue b/apps/app-frontend/src/components/ui/skin-site/admin/AdminReviewPage.vue index 3d17779..4829457 100644 --- a/apps/app-frontend/src/components/ui/skin-site/admin/AdminReviewPage.vue +++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminReviewPage.vue @@ -8,8 +8,17 @@ import { skinSiteRegenerateReviewInvite, type SkinSiteReviewResult, type SkinSiteReviewSubmission, + skinSiteReviewPaperAdmin, + skinSiteReviewPaperList, skinSiteReviewSubmissionCount, skinSiteReviewSubmissions, + skinSiteCreateReviewPaperAdmin, + skinSiteUpdateReviewPaperAdmin, + type SkinSiteReviewAnswer, + type SkinSiteReviewConfigData, + type SkinSiteReviewQuestion, + type SkinSiteReviewMailConfig, + type SkinSiteReviewPaperSummary, } from '@/helpers/skin-site-api' const { formatMessage } = useVIntl() @@ -40,6 +49,10 @@ const messages = defineMessages({ details: { id: 'app.skin-site.admin.review.details', defaultMessage: 'Details' }, hide: { id: 'app.skin-site.admin.review.hide', defaultMessage: 'Hide' }, answers: { id: 'app.skin-site.admin.review.answers', defaultMessage: 'Answers' }, + questionNo: { + id: 'app.skin-site.admin.review.question-no', + defaultMessage: 'Question {n}', + }, noAnswers: { id: 'app.skin-site.admin.review.no-answers', defaultMessage: 'No answers.' }, autoChecks: { id: 'app.skin-site.admin.review.auto-checks', defaultMessage: 'Automatic checks' }, context: { id: 'app.skin-site.admin.review.context', defaultMessage: 'Submission context' }, @@ -83,8 +96,118 @@ const messages = defineMessages({ prev: { id: 'app.skin-site.admin.review.prev', defaultMessage: 'Previous' }, next: { id: 'app.skin-site.admin.review.next', defaultMessage: 'Next' }, page: { id: 'app.skin-site.admin.review.page', defaultMessage: 'Page {page}' }, + tabList: { id: 'app.skin-site.admin.review.tab-list', defaultMessage: 'Submissions' }, + tabPapers: { id: 'app.skin-site.admin.review.tab-papers', defaultMessage: 'Papers' }, + papersEmpty: { + id: 'app.skin-site.admin.review.papers-empty', + defaultMessage: 'No review papers yet.', + }, + newPaper: { id: 'app.skin-site.admin.review.new-paper', defaultMessage: 'New paper' }, + editPaper: { id: 'app.skin-site.admin.review.edit-paper', defaultMessage: 'Edit' }, + paperTitle: { id: 'app.skin-site.admin.review.paper-title', defaultMessage: 'Title' }, + paperAnnotation: { + id: 'app.skin-site.admin.review.paper-annotation', + defaultMessage: 'Description', + }, + paperEnabled: { id: 'app.skin-site.admin.review.paper-enabled', defaultMessage: 'Enabled' }, + paperMode: { id: 'app.skin-site.admin.review.paper-mode', defaultMessage: 'Mode' }, + paperPassScore: { + id: 'app.skin-site.admin.review.paper-pass-score', + defaultMessage: 'Pass score', + }, + paperTargetPermissions: { + id: 'app.skin-site.admin.review.paper-target-permissions', + defaultMessage: 'Target permissions', + }, + paperQuestions: { + id: 'app.skin-site.admin.review.paper-questions', + defaultMessage: 'Questions', + }, + addQuestion: { id: 'app.skin-site.admin.review.add-question', defaultMessage: 'Add question' }, + questionTitle: { + id: 'app.skin-site.admin.review.question-title', + defaultMessage: 'Question title', + }, + questionType: { id: 'app.skin-site.admin.review.question-type', defaultMessage: 'Type' }, + questionScore: { id: 'app.skin-site.admin.review.question-score', defaultMessage: 'Score' }, + questionAnnotation: { + id: 'app.skin-site.admin.review.question-annotation', + defaultMessage: 'Annotation', + }, + addOption: { id: 'app.skin-site.admin.review.add-option', defaultMessage: 'Add option' }, + optionDescription: { + id: 'app.skin-site.admin.review.option-description', + defaultMessage: 'Option text', + }, + optionCorrect: { + id: 'app.skin-site.admin.review.option-correct', + defaultMessage: 'Correct', + }, + removeQuestion: { + id: 'app.skin-site.admin.review.remove-question', + defaultMessage: 'Remove', + }, + paperCreated: { + id: 'app.skin-site.admin.review.paper-created', + defaultMessage: 'Paper created.', + }, + paperSaved: { id: 'app.skin-site.admin.review.paper-saved', defaultMessage: 'Paper saved.' }, + mailTitle: { id: 'app.skin-site.admin.review.mail-title', defaultMessage: 'Pass mail' }, + mailEnabled: { id: 'app.skin-site.admin.review.mail-enabled', defaultMessage: 'Send mail on pass' }, + mailSubject: { id: 'app.skin-site.admin.review.mail-subject', defaultMessage: 'Subject' }, + mailMessage: { id: 'app.skin-site.admin.review.mail-message', defaultMessage: 'Body' }, + mailExtra: { id: 'app.skin-site.admin.review.mail-extra', defaultMessage: 'Extra text' }, + mailIncludeInvite: { + id: 'app.skin-site.admin.review.mail-include-invite', + defaultMessage: 'Include invite code', + }, + preventDuplicate: { + id: 'app.skin-site.admin.review.prevent-duplicate', + defaultMessage: 'Reject duplicate submissions', + }, + copyLink: { id: 'app.skin-site.admin.review.copy-link', defaultMessage: 'Copy link' }, + linkCopied: { id: 'app.skin-site.admin.review.link-copied', defaultMessage: 'Link copied.' }, + moveUp: { id: 'app.skin-site.admin.review.move-up', defaultMessage: 'Move up' }, + moveDown: { id: 'app.skin-site.admin.review.move-down', defaultMessage: 'Move down' }, + mailSubjectRequired: { + id: 'app.skin-site.admin.review.mail-subject-required', + defaultMessage: 'Subject is required when mail is enabled.', + }, + mailMessageRequired: { + id: 'app.skin-site.admin.review.mail-message-required', + defaultMessage: 'Body is required when mail is enabled.', + }, }) +const availablePermissions: Array<{ value: string; label: string }> = [ + { value: 'EXTERNAL_MEMBER', label: 'Mod服玩家' }, + { value: 'INTERNAL_MEMBER', label: '原版服玩家' }, + { value: 'SERVER_ADMIN', label: '服务器管理' }, + { value: 'WEB_ADMIN', label: 'Web管理' }, + { value: 'SPONSOR', label: '捐赠者' }, + { value: 'DEVELOPER', label: '开发者' }, + { value: 'SENIOR_MEMBER', label: '老资历' }, +] + +const prerequisitePermissionOptions: Array<{ value: string; label: string }> = [ + { value: 'GUEST', label: '游客' }, + ...availablePermissions, +] + +const questionTypeOptions: Array<{ value: SkinSiteReviewQuestionType; label: string }> = [ + { value: 'SINGLE', label: '单选题' }, + { value: 'MULTIPLE', label: '多选题' }, + { value: 'TRUE_FALSE', label: '判断题' }, + { value: 'FILL', label: '填空题' }, + { value: 'ESSAY', label: '简答题' }, +] + +const multipleScoreModeOptions: Array<{ value: 'EXACT' | 'PARTIAL' | 'PARTIAL_WITH_PENALTY'; label: string }> = [ + { value: 'EXACT', label: '多选少选均不给分' }, + { value: 'PARTIAL', label: '少选部分得分' }, + { value: 'PARTIAL_WITH_PENALTY', label: '多选部分扣分' }, +] + const PAGE_SIZE = 20 const items = ref([]) @@ -143,6 +266,16 @@ function formatDuration(ms?: number | null): string { return m > 0 ? m + 'm ' + s + 's' : s + 's' } +function questionFor(submission: SkinSiteReviewSubmission, questionId: string) { + return submission.paper?.questions?.find((q) => q.id === questionId) +} + +function answerText(answer: SkinSiteReviewAnswer): string { + if (answer.text) return answer.text + if (Array.isArray(answer.values) && answer.values.length) return answer.values.join(', ') + return '—' +} + function decisionFor(id: number) { if (!decisions.value[id]) decisions.value[id] = { reason: '', working: false } return decisions.value[id] @@ -190,9 +323,6 @@ async function nextPage() { await load() } -function toggleDetails(id: number) { - expandedId.value = expandedId.value === id ? null : id -} async function decide(submission: SkinSiteReviewSubmission, accepted: boolean) { const state = decisionFor(submission.id) @@ -234,216 +364,461 @@ async function regenerateInvite(submission: SkinSiteReviewSubmission) { } } +const detailOpen = ref(false) +const detailSubmission = ref(null) + +function openDetail(submission: SkinSiteReviewSubmission) { + detailSubmission.value = submission + decisionFor(submission.id) + detailOpen.value = true +} + +function closeDetail() { + detailOpen.value = false + detailSubmission.value = null +} + +const tab = ref<'list' | 'papers'>('list') + +const papers = ref([]) +const papersLoading = ref(false) +const paperEditorOpen = ref(false) +const editingPaperId = ref(null) +const paperSaving = ref(false) +const paperForm = ref({ + id: '', + enabled: true, + title: '', + annotation: '', + mode: 'MANUAL', + passScore: 0, + targetPermissions: [], + version: 0, + questions: [], + updatedAt: 0, + prerequisiteMode: 'ANY', + prerequisitePermissions: [], + mail: defaultMailConfig(), +}) + +function newPaper() { + editingPaperId.value = null + paperForm.value = { + id: crypto.randomUUID(), + enabled: true, + title: '', + annotation: '', + mode: 'MANUAL', + passScore: 0, + targetPermissions: [], + version: 0, + questions: [], + updatedAt: Date.now(), + prerequisiteMode: 'ANY', + prerequisitePermissions: [], + mail: defaultMailConfig(), + } + paperEditorOpen.value = true +} + +async function loadPapers() { + papersLoading.value = true + try { + const result = await skinSiteReviewPaperList() + if (result.success) papers.value = result.payload ?? [] + } catch (error) { + handleError(error) + } finally { + papersLoading.value = false + } +} + +async function editPaper(paper: SkinSiteReviewPaperSummary) { + try { + const result = await skinSiteReviewPaperAdmin(paper.id) + if (!result.success) throw new Error(result.errorMessage) + paperForm.value = { ...result.payload, mail: result.payload.mail ?? defaultMailConfig() } + editingPaperId.value = paper.id + paperEditorOpen.value = true + } catch (error) { + handleError(error) + } +} + +function removeQuestion(index: number) { + paperForm.value.questions.splice(index, 1) +} + +function addOption(question: SkinSiteReviewQuestion) { + question.options = question.options ?? [] + question.options.push({ + id: crypto.randomUUID(), + description: '', + correct: false, + defaultSelected: false, + score: 0, + }) +} + +function removeOption(question: SkinSiteReviewQuestion, index: number) { + question.options?.splice(index, 1) +} + +function defaultMailConfig(): SkinSiteReviewMailConfig { + return { enabled: false, subject: '', message: '', includeInviteCode: false, extraText: '' } +} + +async function copyPaperLink(paperId: string) { + try { + await navigator.clipboard.writeText('https://skin.starlight.cool/review?paper=' + encodeURIComponent(paperId)) + addNotification({ type: 'success', title: formatMessage(messages.linkCopied) }) + } catch (error) { + handleError(error) + } +} + +function moveQuestion(index: number, direction: number) { + const target = index + direction + if (target < 0 || target >= paperForm.value.questions.length) return + const [question] = paperForm.value.questions.splice(index, 1) + paperForm.value.questions.splice(target, 0, question) +} + +function permissionLabel(value: string): string { + return ( + availablePermissions.find((item) => item.value === value)?.label ?? + prerequisitePermissionOptions.find((item) => item.value === value)?.label ?? + value + ) +} + +function isChoice(question: SkinSiteReviewQuestion): boolean { + return ['SINGLE', 'MULTIPLE', 'TRUE_FALSE'].includes(question.type) +} + +function supportsAutoReview(question: SkinSiteReviewQuestion): boolean { + return ['SINGLE', 'MULTIPLE', 'TRUE_FALSE', 'ESSAY'].includes(question.type) +} + +function supportsFatal(question: SkinSiteReviewQuestion): boolean { + return paperForm.value.mode === 'AUTO' + ? question.type !== 'ESSAY' + : supportsAutoReview(question) +} + +function isAutomaticallyReviewed(question: SkinSiteReviewQuestion): boolean { + return paperForm.value.mode === 'AUTO' || question.autoReview +} + +function showOptionScore(question: SkinSiteReviewQuestion): boolean { + if ( + question.type !== 'MULTIPLE' || + !isAutomaticallyReviewed(question) || + question.multipleScoreMode === 'EXACT' + ) { + return false + } + return true +} + +function setCorrectOption(question: SkinSiteReviewQuestion, optionId: string) { + question.options?.forEach((option) => { + option.correct = option.id === optionId + }) +} + +function setDefaultOption(question: SkinSiteReviewQuestion, optionId: string) { + if ( + question.type === 'MULTIPLE' || + !question.options?.find((option) => option.id === optionId)?.defaultSelected + ) + return + question.options?.forEach((option) => { + if (option.id !== optionId) option.defaultSelected = false + }) +} + +function moveOption(question: SkinSiteReviewQuestion, index: number, direction: number) { + if (!question.options) return + const target = index + direction + if (target < 0 || target >= question.options.length) return + const [option] = question.options.splice(index, 1) + question.options.splice(target, 0, option) +} + +function setOptionalQuestion(question: SkinSiteReviewQuestion) { + if (!question.optional) return + question.fatal = false + question.autoReview = false +} + +function setFatalQuestion(question: SkinSiteReviewQuestion) { + if (!question.fatal) return + question.optional = false + if (paperForm.value.mode === 'MANUAL') question.autoReview = true +} + +function addQuestionOfType(type: SkinSiteReviewQuestionType) { + if (paperForm.value.mode === 'AUTO' && type === 'ESSAY') return + if (paperForm.value.questions.length >= 50) return + const isChoiceType = ['SINGLE', 'MULTIPLE', 'TRUE_FALSE'].includes(type) + const baseOptions = isChoiceType + ? type === 'TRUE_FALSE' + ? [ + { id: crypto.randomUUID(), description: '正确', correct: false, defaultSelected: false, score: 0 }, + { id: crypto.randomUUID(), description: '错误', correct: false, defaultSelected: false, score: 0 }, + ] + : [ + { id: crypto.randomUUID(), description: '', correct: false, defaultSelected: false, score: 0 }, + { id: crypto.randomUUID(), description: '', correct: false, defaultSelected: false, score: 0 }, + ] + : [] + paperForm.value.questions.push({ + id: crypto.randomUUID(), + type, + title: '', + annotation: '', + score: 1, + optional: false, + fatal: false, + autoReview: false, + multipleScoreMode: 'EXACT', + options: baseOptions, + answer: null, + maxLength: type === 'FILL' ? 50 : type === 'ESSAY' ? 300 : null, + informationQuestion: false, + informationAlias: null, + }) +} + +function onQuestionTypeSelected(event: Event) { + const el = event.target as HTMLSelectElement + const value = el.value as SkinSiteReviewQuestionType + if (value) addQuestionOfType(value) + el.value = '' +} + +function availableQuestionTypes(): SkinSiteReviewQuestionType[] { + const all: SkinSiteReviewQuestionType[] = ['SINGLE', 'MULTIPLE', 'TRUE_FALSE', 'FILL', 'ESSAY'] + return paperForm.value.mode === 'AUTO' ? all.filter((t) => t !== 'ESSAY') : all +} + +const totalScore = computed(() => + paperForm.value.questions.reduce((sum, q) => sum + Number(q.score || 0), 0), +) + +const prerequisiteEnabled = computed({ + get: () => paperForm.value.prerequisitePermissions.length > 0, + set: (enabled: boolean) => { + paperForm.value.prerequisitePermissions = enabled ? ['EXTERNAL_MEMBER'] : [] + }, +}) + +function validateBeforeSave(): string | null { + if (!paperForm.value.title.trim() || !paperForm.value.questions.length) + return '请填写试卷标题并至少添加一道题目。' + if (!paperForm.value.targetPermissions.length) + return '请至少选择一个审核通过后玩家可获得的身份。' + if (prerequisiteEnabled.value && !paperForm.value.prerequisitePermissions.length) + return '请至少选择一个前置身份。' + if (paperForm.value.passScore < 1 || paperForm.value.passScore > totalScore.value) + return '通过分数必须在 1 至 ' + totalScore.value + ' 之间。' + for (const [index, q] of paperForm.value.questions.entries()) { + if (!q.title.trim() || !Number.isInteger(q.score) || q.score < 0) + return '请完整填写第 ' + (index + 1) + ' 题的题目与整数分数。' + } + const mail = paperForm.value.mail + if (mail?.enabled) { + if (!mail.subject.trim()) return formatMessage(messages.mailSubjectRequired) + if (!mail.message.trim()) return formatMessage(messages.mailMessageRequired) + if (mail.subject.length > 200) return '邮件标题不能超过 200 字。' + if (mail.message.length > 5000) return '邮件正文不能超过 5000 字。' + if (mail.extraText.length > 3000) return '邮件补充说明不能超过 3000 字。' + } + return null +} + +async function savePaper() { + if (paperSaving.value) return + const validationError = validateBeforeSave() + if (validationError) { + addNotification({ type: 'error', title: validationError }) + return + } + paperSaving.value = true + try { + const data: SkinSiteReviewConfigData = { + ...paperForm.value, + targetPermissions: paperForm.value.targetPermissions.filter(Boolean), + prerequisitePermissions: paperForm.value.prerequisitePermissions.filter(Boolean), + updatedAt: Date.now(), + } + const result = + editingPaperId.value === null + ? await skinSiteCreateReviewPaperAdmin(data) + : await skinSiteUpdateReviewPaperAdmin(editingPaperId.value, data) + if (!result.success) throw new Error(result.errorMessage) + addNotification({ + type: 'success', + title: formatMessage( + editingPaperId.value === null ? messages.paperCreated : messages.paperSaved, + ), + }) + paperEditorOpen.value = false + await loadPapers() + } catch (error) { + handleError(error) + } finally { + paperSaving.value = false + } +} + +async function switchTab(next: 'list' | 'papers') { + tab.value = next + if (next === 'papers' && !papers.value.length) await loadPapers() +} + onMounted(() => void load()) + + + +
+
+
+
+

+ {{ formatMessage(messages.details) }} · #{{ detailSubmission.id }} +

+

+ {{ detailSubmission.applicantName }} +

+
+ +
+ +
+ + {{ resultLabel(detailSubmission.result) }} + + + {{ modeLabel(detailSubmission.mode) }} + + + {{ formatMessage(messages.score) }}: {{ detailSubmission.score }} + +
+ +
+ + {{ formatMessage(messages.context) }} + + {{ formatMessage(messages.ip) }}: {{ detailSubmission.context.ip ?? '—' }} + + {{ formatMessage(messages.userAgent) }}: + {{ detailSubmission.context.userAgent }} + + + {{ formatMessage(messages.duration) }}: + {{ formatDuration(detailSubmission.context.activity.durationMs) }} · + {{ formatMessage(messages.hiddenTime) }}: + {{ formatDuration(detailSubmission.context.activity.hiddenMs) }} · + {{ formatMessage(messages.screenSwitches) }}: + {{ detailSubmission.context.activity.screenSwitches }} + +
+ +
+ + {{ formatMessage(messages.answers) }} + +

+ {{ formatMessage(messages.noAnswers) }} +

+
+
+
+ + {{ idx + 1 }}. + {{ + questionFor(detailSubmission, answer.questionId)?.title || + formatMessage(messages.questionNo, { n: idx + 1 }) + }} + + + {{ questionFor(detailSubmission, answer.questionId)?.informationAlias || 'info' }} + + + ({{ formatMessage(messages.score) }}: + {{ questionFor(detailSubmission, answer.questionId)?.score }}) + +
+ + {{ questionFor(detailSubmission, answer.questionId)?.annotation }} + + + {{ answerText(answer) }} + + + {{ formatMessage(messages.score) }}: {{ answer.awardedScore }} + +
+
+
+ +
+ + {{ formatMessage(messages.autoChecks) }} + +
+ + {{ key }}: {{ passed ? '✓' : '✗' }} + +
+
+ +
+ + {{ formatMessage(messages.history) }} + +
+ {{ h.reviewer }} · {{ resultLabel(h.result) }} · + {{ formatDateTime(h.at) }} + · {{ h.reason }} +
+
+ +
+ +
+ + + +
+
+
+ +
+
+
+

+ {{ editingPaperId ? formatMessage(messages.editPaper) : formatMessage(messages.newPaper) }} +

+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + {{ formatMessage(messages.paperTargetPermissions) }} + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+ + + +
+ +
+ + + + +
+
+ +
+ + {{ formatMessage(messages.paperQuestions) }} ({{ paperForm.questions.length }}) + + +
+ +

+ 还没有题目,请从上方下拉框添加。 +

+ +
+
+
+ + {{ qIndex + 1 }} + + + {{ questionTypeOptions.find((q) => q.value === question.type)?.label }} + + + 选答 + + + 生死题 + +
+
+ + + +
+
+ +
+ + +
+ + + +
+ + + + + +
+ +
+
+ 选项 + +
+ +
+
+ + + +
+ +
+ + + +
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+ + diff --git a/apps/app-frontend/src/helpers/skin-site-api.ts b/apps/app-frontend/src/helpers/skin-site-api.ts index 4bc126b..313fa7c 100644 --- a/apps/app-frontend/src/helpers/skin-site-api.ts +++ b/apps/app-frontend/src/helpers/skin-site-api.ts @@ -564,32 +564,32 @@ export function skinSitePublicMapServers(): Promise +> { + return skinSiteRequest('review/admin/papers') +} + +export function skinSiteReviewPaperAdmin( + paperId: string, +): Promise> { + return skinSiteRequest( + 'review/admin/papers/' + encodeURIComponent(paperId), + ) +} + +export function skinSiteCreateReviewPaperAdmin( + data: SkinSiteReviewConfigData, +): Promise> { + return skinSiteRequest('review/admin/papers', { + method: 'POST', + body: data, + }) +} + +export function skinSiteUpdateReviewPaperAdmin( + paperId: string, + data: SkinSiteReviewConfigData, +): Promise> { + return skinSiteRequest( + 'review/admin/papers/' + encodeURIComponent(paperId), + { method: 'PUT', body: data }, + ) +} diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 9e29657..ddfee71 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -12141,5 +12141,42 @@ "app.skin-site.admin.wiki.max-page-bytes": {"message": "Max page bytes"}, "app.skin-site.admin.wiki.max-attachment-bytes": {"message": "Max attachment bytes"}, "app.skin-site.admin.wiki.save-settings": {"message": "Save settings"}, - "app.skin-site.admin.wiki.settings-saved": {"message": "Settings saved."} + "app.skin-site.admin.wiki.settings-saved": {"message": "Settings saved."}, + "app.skin-site.admin.review.tab-list": {"message": "Submissions"}, + "app.skin-site.admin.review.tab-papers": {"message": "Papers"}, + "app.skin-site.admin.review.papers-empty": {"message": "No review papers yet."}, + "app.skin-site.admin.review.new-paper": {"message": "New paper"}, + "app.skin-site.admin.review.edit-paper": {"message": "Edit"}, + "app.skin-site.admin.review.paper-title": {"message": "Title"}, + "app.skin-site.admin.review.paper-annotation": {"message": "Description"}, + "app.skin-site.admin.review.paper-enabled": {"message": "Enabled"}, + "app.skin-site.admin.review.paper-mode": {"message": "Mode"}, + "app.skin-site.admin.review.paper-pass-score": {"message": "Pass score"}, + "app.skin-site.admin.review.paper-target-permissions": {"message": "Target permissions"}, + "app.skin-site.admin.review.paper-questions": {"message": "Questions"}, + "app.skin-site.admin.review.add-question": {"message": "Add question"}, + "app.skin-site.admin.review.question-title": {"message": "Question title"}, + "app.skin-site.admin.review.question-type": {"message": "Type"}, + "app.skin-site.admin.review.question-score": {"message": "Score"}, + "app.skin-site.admin.review.question-annotation": {"message": "Annotation"}, + "app.skin-site.admin.review.add-option": {"message": "Add option"}, + "app.skin-site.admin.review.option-description": {"message": "Option text"}, + "app.skin-site.admin.review.option-correct": {"message": "Correct"}, + "app.skin-site.admin.review.remove-question": {"message": "Remove"}, + "app.skin-site.admin.review.question-no": {"message": "Question {n}"}, + "app.skin-site.admin.review.paper-created": {"message": "Paper created."}, + "app.skin-site.admin.review.paper-saved": {"message": "Paper saved."}, + "app.skin-site.admin.review.mail-title": {"message": "Pass mail"}, + "app.skin-site.admin.review.mail-enabled": {"message": "Send mail on pass"}, + "app.skin-site.admin.review.mail-subject": {"message": "Subject"}, + "app.skin-site.admin.review.mail-message": {"message": "Body"}, + "app.skin-site.admin.review.mail-extra": {"message": "Extra text"}, + "app.skin-site.admin.review.mail-include-invite": {"message": "Include invite code"}, + "app.skin-site.admin.review.prevent-duplicate": {"message": "Reject duplicate submissions"}, + "app.skin-site.admin.review.copy-link": {"message": "Copy link"}, + "app.skin-site.admin.review.link-copied": {"message": "Link copied."}, + "app.skin-site.admin.review.move-up": {"message": "Move up"}, + "app.skin-site.admin.review.move-down": {"message": "Move down"}, + "app.skin-site.admin.review.mail-subject-required": {"message": "Subject is required when mail is enabled."}, + "app.skin-site.admin.review.mail-message-required": {"message": "Body is required when mail is enabled."} } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 2dac68e..3cc6a7b 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -11972,5 +11972,42 @@ "app.skin-site.admin.wiki.max-page-bytes": {"message": "页面最大字节数"}, "app.skin-site.admin.wiki.max-attachment-bytes": {"message": "附件最大字节数"}, "app.skin-site.admin.wiki.save-settings": {"message": "保存设置"}, - "app.skin-site.admin.wiki.settings-saved": {"message": "设置已保存。"} + "app.skin-site.admin.wiki.settings-saved": {"message": "设置已保存。"}, + "app.skin-site.admin.review.tab-list": {"message": "申请列表"}, + "app.skin-site.admin.review.tab-papers": {"message": "试卷管理"}, + "app.skin-site.admin.review.papers-empty": {"message": "还没有审核试卷。"}, + "app.skin-site.admin.review.new-paper": {"message": "新建试卷"}, + "app.skin-site.admin.review.edit-paper": {"message": "编辑"}, + "app.skin-site.admin.review.paper-title": {"message": "试卷标题"}, + "app.skin-site.admin.review.paper-annotation": {"message": "描述"}, + "app.skin-site.admin.review.paper-enabled": {"message": "启用"}, + "app.skin-site.admin.review.paper-mode": {"message": "阅卷模式"}, + "app.skin-site.admin.review.paper-pass-score": {"message": "通过分数"}, + "app.skin-site.admin.review.paper-target-permissions": {"message": "通过后获得的身份"}, + "app.skin-site.admin.review.paper-questions": {"message": "题目"}, + "app.skin-site.admin.review.add-question": {"message": "添加题目"}, + "app.skin-site.admin.review.question-title": {"message": "题目"}, + "app.skin-site.admin.review.question-type": {"message": "题型"}, + "app.skin-site.admin.review.question-score": {"message": "分数"}, + "app.skin-site.admin.review.question-annotation": {"message": "题目注解"}, + "app.skin-site.admin.review.add-option": {"message": "添加选项"}, + "app.skin-site.admin.review.option-description": {"message": "选项文本"}, + "app.skin-site.admin.review.option-correct": {"message": "正确"}, + "app.skin-site.admin.review.remove-question": {"message": "删除"}, + "app.skin-site.admin.review.question-no": {"message": "第 {n} 题"}, + "app.skin-site.admin.review.paper-created": {"message": "试卷已创建。"}, + "app.skin-site.admin.review.paper-saved": {"message": "试卷已保存。"}, + "app.skin-site.admin.review.mail-title": {"message": "通过邮件"}, + "app.skin-site.admin.review.mail-enabled": {"message": "审核通过后发送邮件"}, + "app.skin-site.admin.review.mail-subject": {"message": "邮件标题"}, + "app.skin-site.admin.review.mail-message": {"message": "邮件正文"}, + "app.skin-site.admin.review.mail-extra": {"message": "邮件补充说明"}, + "app.skin-site.admin.review.mail-include-invite": {"message": "附带邀请码"}, + "app.skin-site.admin.review.prevent-duplicate": {"message": "禁止重复作答"}, + "app.skin-site.admin.review.copy-link": {"message": "复制直链"}, + "app.skin-site.admin.review.link-copied": {"message": "直链已复制。"}, + "app.skin-site.admin.review.move-up": {"message": "上移"}, + "app.skin-site.admin.review.move-down": {"message": "下移"}, + "app.skin-site.admin.review.mail-subject-required": {"message": "开启邮件后必须填写邮件标题。"}, + "app.skin-site.admin.review.mail-message-required": {"message": "开启邮件后必须填写邮件正文。"} }