|
|
|
|
@ -19,11 +19,20 @@ interface FileItem {
|
|
|
|
|
url: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ReviewSchemaField {
|
|
|
|
|
interface ScoringSheetItem {
|
|
|
|
|
key: string
|
|
|
|
|
type: 'number' | 'text' | 'textarea'
|
|
|
|
|
label: string
|
|
|
|
|
required?: boolean
|
|
|
|
|
sort: number
|
|
|
|
|
category: string
|
|
|
|
|
title: string
|
|
|
|
|
criteria: string
|
|
|
|
|
max_score: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ScoringSheet {
|
|
|
|
|
version: number
|
|
|
|
|
title: string
|
|
|
|
|
full_score: number
|
|
|
|
|
items: ScoringSheetItem[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface MyReviewScorePayload {
|
|
|
|
|
@ -54,11 +63,9 @@ interface DetailPayload {
|
|
|
|
|
submitted_at: string
|
|
|
|
|
score_display: string
|
|
|
|
|
score_is_pending: boolean
|
|
|
|
|
review_schema: {
|
|
|
|
|
id: number | null
|
|
|
|
|
schema_json: ReviewSchemaField[]
|
|
|
|
|
is_default: boolean
|
|
|
|
|
}
|
|
|
|
|
scoring_allowed: boolean
|
|
|
|
|
scoring_blocked_reason: string | null
|
|
|
|
|
scoring_sheet: ScoringSheet | null
|
|
|
|
|
my_review_score: MyReviewScorePayload | null
|
|
|
|
|
files: FileItem[]
|
|
|
|
|
promise_signed: boolean
|
|
|
|
|
@ -83,7 +90,6 @@ const pledgeModalHeading = computed(() => {
|
|
|
|
|
return n ? `${n} 赛事承诺书` : '赛事承诺书'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/** 与选手端承诺书弹窗「日期」行一致:`2026年5月11日`(仅日期,取自接口签署时间字符串) */
|
|
|
|
|
function formatPledgeCnDate(at: string | undefined): string {
|
|
|
|
|
const t = (at ?? '').trim()
|
|
|
|
|
if (!t) return '—'
|
|
|
|
|
@ -107,33 +113,47 @@ const loadError = ref('')
|
|
|
|
|
|
|
|
|
|
const pledgeSignedDateCn = computed(() => formatPledgeCnDate(detail.value?.promise_signed_at))
|
|
|
|
|
|
|
|
|
|
/** 项目评分弹窗内编辑草稿(key → 字符串,便于绑定 input) */
|
|
|
|
|
/** 各项得分草稿 key → 字符串 */
|
|
|
|
|
const scoreDraft = ref<Record<string, string>>({})
|
|
|
|
|
const scoreSubmitting = ref(false)
|
|
|
|
|
/** 失焦过的得分项,用于空值提示 */
|
|
|
|
|
const scoreBlurred = ref<Record<string, boolean>>({})
|
|
|
|
|
/** 点击提交后展示全部未填提示 */
|
|
|
|
|
const scoreShowErrors = ref(false)
|
|
|
|
|
|
|
|
|
|
function extractScoresFromPayload(payload: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
|
|
|
if (!payload) return {}
|
|
|
|
|
const nested = payload.scores
|
|
|
|
|
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
|
|
|
return nested as Record<string, unknown>
|
|
|
|
|
}
|
|
|
|
|
return payload
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncScoreDraft(): void {
|
|
|
|
|
const d = detail.value
|
|
|
|
|
if (!d?.review_schema?.schema_json?.length) {
|
|
|
|
|
const items = d?.scoring_sheet?.items
|
|
|
|
|
if (!items?.length) {
|
|
|
|
|
scoreDraft.value = {}
|
|
|
|
|
scoreBlurred.value = {}
|
|
|
|
|
scoreShowErrors.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const src = d.my_review_score?.payload_json ?? {}
|
|
|
|
|
const src = extractScoresFromPayload(d?.my_review_score?.payload_json)
|
|
|
|
|
const next: Record<string, string> = {}
|
|
|
|
|
for (const f of d.review_schema.schema_json) {
|
|
|
|
|
const v = src[f.key]
|
|
|
|
|
if (f.type === 'number') {
|
|
|
|
|
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
|
|
|
next[f.key] = String(v)
|
|
|
|
|
} else if (typeof v === 'string' && v.trim() !== '') {
|
|
|
|
|
next[f.key] = v.trim()
|
|
|
|
|
} else {
|
|
|
|
|
next[f.key] = ''
|
|
|
|
|
}
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
const v = src[item.key]
|
|
|
|
|
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
|
|
|
next[item.key] = String(v)
|
|
|
|
|
} else if (typeof v === 'string' && v.trim() !== '') {
|
|
|
|
|
next[item.key] = v.trim()
|
|
|
|
|
} else {
|
|
|
|
|
next[f.key] = typeof v === 'string' ? v : v != null ? String(v) : ''
|
|
|
|
|
next[item.key] = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
scoreDraft.value = next
|
|
|
|
|
scoreBlurred.value = {}
|
|
|
|
|
scoreShowErrors.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(detail, () => syncScoreDraft(), { deep: true })
|
|
|
|
|
@ -142,6 +162,108 @@ function rtrimZeros(s: string): string {
|
|
|
|
|
return s.replace(/\.?0+$/, '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatMax(n: number): string {
|
|
|
|
|
return rtrimZeros(Number(n).toFixed(4))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scoringItems = computed(() => detail.value?.scoring_sheet?.items ?? [])
|
|
|
|
|
|
|
|
|
|
/** 合并相同「指标分类」单元格 */
|
|
|
|
|
const scoringRows = computed(() => {
|
|
|
|
|
const items = scoringItems.value
|
|
|
|
|
const rows: Array<{
|
|
|
|
|
item: ScoringSheetItem
|
|
|
|
|
idx: number
|
|
|
|
|
categoryRowspan: number
|
|
|
|
|
}> = []
|
|
|
|
|
let i = 0
|
|
|
|
|
while (i < items.length) {
|
|
|
|
|
const cat = items[i].category
|
|
|
|
|
let span = 1
|
|
|
|
|
while (i + span < items.length && items[i + span].category === cat) {
|
|
|
|
|
span += 1
|
|
|
|
|
}
|
|
|
|
|
for (let j = 0; j < span; j++) {
|
|
|
|
|
rows.push({
|
|
|
|
|
item: items[i + j],
|
|
|
|
|
idx: i + j,
|
|
|
|
|
categoryRowspan: j === 0 ? span : 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
i += span
|
|
|
|
|
}
|
|
|
|
|
return rows
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function getScoreError(item: ScoringSheetItem): string | null {
|
|
|
|
|
const raw = String(scoreDraft.value[item.key] ?? '').trim()
|
|
|
|
|
if (raw === '') {
|
|
|
|
|
if (scoreShowErrors.value || scoreBlurred.value[item.key]) {
|
|
|
|
|
return '请填写得分'
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
if (!Number.isFinite(n)) return '须为数字'
|
|
|
|
|
if (n < 0) return '不能小于 0'
|
|
|
|
|
if (n > item.max_score) return `不得超过 ${formatMax(item.max_score)} 分`
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onScoreBlur(key: string): void {
|
|
|
|
|
scoreBlurred.value = { ...scoreBlurred.value, [key]: true }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const draftLineTotal = computed(() => {
|
|
|
|
|
let sum = 0
|
|
|
|
|
let allValid = scoringItems.value.length > 0
|
|
|
|
|
for (const item of scoringItems.value) {
|
|
|
|
|
const raw = String(scoreDraft.value[item.key] ?? '').trim()
|
|
|
|
|
if (raw === '') {
|
|
|
|
|
allValid = false
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
if (!Number.isFinite(n) || n < 0 || n > item.max_score) {
|
|
|
|
|
allValid = false
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
sum += n
|
|
|
|
|
}
|
|
|
|
|
return { sum, allValid }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const canSubmitScore = computed(() => {
|
|
|
|
|
if (!detail.value?.scoring_allowed || !scoringItems.value.length) return false
|
|
|
|
|
for (const item of scoringItems.value) {
|
|
|
|
|
const raw = String(scoreDraft.value[item.key] ?? '').trim()
|
|
|
|
|
if (raw === '') return false
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
if (!Number.isFinite(n) || n < 0 || n > item.max_score) return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const scoreFormHint = computed(() => {
|
|
|
|
|
if (!scoringItems.value.length) return ''
|
|
|
|
|
let empty = 0
|
|
|
|
|
let over = 0
|
|
|
|
|
for (const item of scoringItems.value) {
|
|
|
|
|
const raw = String(scoreDraft.value[item.key] ?? '').trim()
|
|
|
|
|
if (raw === '') {
|
|
|
|
|
empty += 1
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
if (!Number.isFinite(n) || n < 0 || n > item.max_score) over += 1
|
|
|
|
|
}
|
|
|
|
|
const parts: string[] = []
|
|
|
|
|
// 未填写仅在点击提交后汇总提示;超分/无效随时提示
|
|
|
|
|
if (scoreShowErrors.value && empty > 0) parts.push(`${empty} 项未填写`)
|
|
|
|
|
if (over > 0) parts.push(`${over} 项超出分值或无效`)
|
|
|
|
|
return parts.join(',')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const statusBadgeText = computed(() => {
|
|
|
|
|
const d = detail.value
|
|
|
|
|
if (!d) return ''
|
|
|
|
|
@ -152,7 +274,6 @@ const planFile = computed(() => detail.value?.files.find((f) => f.kind === 'plan
|
|
|
|
|
|
|
|
|
|
const supportingFiles = computed(() => detail.value?.files.filter((f) => f.kind === 'supporting') ?? [])
|
|
|
|
|
|
|
|
|
|
/** 手写签名为选手端 canvas 导出的 data URL,或未带前缀的 base64 PNG */
|
|
|
|
|
const signatureDataUrl = computed((): string => {
|
|
|
|
|
const raw = detail.value?.promise_signature
|
|
|
|
|
if (raw == null || typeof raw !== 'string') return ''
|
|
|
|
|
@ -241,28 +362,12 @@ async function downloadAttachment(f: FileItem) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSubmitPayload(): Record<string, unknown> {
|
|
|
|
|
const d = detail.value
|
|
|
|
|
const schema = d?.review_schema?.schema_json
|
|
|
|
|
if (!schema?.length) return {}
|
|
|
|
|
|
|
|
|
|
const out: Record<string, unknown> = {}
|
|
|
|
|
for (const f of schema) {
|
|
|
|
|
const raw = scoreDraft.value[f.key] ?? ''
|
|
|
|
|
if (f.type === 'number') {
|
|
|
|
|
const t = String(raw).trim()
|
|
|
|
|
if (t === '') {
|
|
|
|
|
if (f.required) {
|
|
|
|
|
out[f.key] = ''
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const n = Number(t)
|
|
|
|
|
out[f.key] = Number.isFinite(n) ? n : 0
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
out[f.key] = String(raw)
|
|
|
|
|
}
|
|
|
|
|
const scores: Record<string, number> = {}
|
|
|
|
|
for (const item of scoringItems.value) {
|
|
|
|
|
const n = Number(String(scoreDraft.value[item.key] ?? '').trim())
|
|
|
|
|
scores[item.key] = Number.isFinite(n) ? n : 0
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
return { scores }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideScoreModal(): void {
|
|
|
|
|
@ -272,6 +377,18 @@ function hideScoreModal(): void {
|
|
|
|
|
Modal.getOrCreateInstance(el).hide()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openScoreModal(): void {
|
|
|
|
|
if (!detail.value?.scoring_allowed) {
|
|
|
|
|
ElMessage.warning(detail.value?.scoring_blocked_reason || '该赛道未配置打分表,暂不可评审')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
scoreShowErrors.value = false
|
|
|
|
|
if (typeof document === 'undefined') return
|
|
|
|
|
const el = document.getElementById('reviewerScoreModal')
|
|
|
|
|
if (!el) return
|
|
|
|
|
Modal.getOrCreateInstance(el).show()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitReviewScore() {
|
|
|
|
|
const id = applicationId.value
|
|
|
|
|
const s = slug.value
|
|
|
|
|
@ -280,9 +397,13 @@ async function submitReviewScore() {
|
|
|
|
|
ElMessage.warning('未登录或参数无效')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const d = detail.value
|
|
|
|
|
if (!d?.review_schema?.schema_json?.length) {
|
|
|
|
|
ElMessage.warning('未加载评审表单')
|
|
|
|
|
if (!detail.value?.scoring_allowed || !scoringItems.value.length) {
|
|
|
|
|
ElMessage.warning(detail.value?.scoring_blocked_reason || '该赛道未配置打分表,暂不可评审')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!canSubmitScore.value) {
|
|
|
|
|
scoreShowErrors.value = true
|
|
|
|
|
ElMessage.warning(scoreFormHint.value || '请为每一项填写得分(可为 0,且不得超过该项分值)')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -325,7 +446,11 @@ async function submitReviewScore() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
syncScoreDraft()
|
|
|
|
|
ElMessage.success(typeof (body as { message?: unknown }).message === 'string' ? String((body as { message: string }).message) : '提交成功')
|
|
|
|
|
ElMessage.success(
|
|
|
|
|
typeof (body as { message?: unknown }).message === 'string'
|
|
|
|
|
? String((body as { message: string }).message)
|
|
|
|
|
: '提交成功',
|
|
|
|
|
)
|
|
|
|
|
hideScoreModal()
|
|
|
|
|
} catch {
|
|
|
|
|
ElMessage.error('网络错误,提交失败')
|
|
|
|
|
@ -333,6 +458,7 @@ async function submitReviewScore() {
|
|
|
|
|
scoreSubmitting.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
@ -405,13 +531,16 @@ async function submitReviewScore() {
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-primary px-4"
|
|
|
|
|
style="background-color: var(--primary, #052d62); border-color: var(--primary, #052d62)"
|
|
|
|
|
data-bs-toggle="modal"
|
|
|
|
|
data-bs-target="#reviewerScoreModal"
|
|
|
|
|
:disabled="!detail.scoring_allowed"
|
|
|
|
|
@click="openScoreModal"
|
|
|
|
|
>
|
|
|
|
|
{{ detail.score_is_pending ? '项目评分' : '修改项目评分' }}
|
|
|
|
|
</button>
|
|
|
|
|
<span v-if="detail.my_review_score" class="small text-secondary"
|
|
|
|
|
>最近保存:{{ detail.my_review_score.updated_at }} · 合计
|
|
|
|
|
<span v-if="!detail.scoring_allowed" class="small text-danger">{{
|
|
|
|
|
detail.scoring_blocked_reason || '该赛道未配置打分表,暂不可评审'
|
|
|
|
|
}}</span>
|
|
|
|
|
<span v-else-if="detail.my_review_score" class="small text-secondary"
|
|
|
|
|
>最近保存:{{ detail.my_review_score.updated_at }} · 项目得分
|
|
|
|
|
{{ rtrimZeros(Number(detail.my_review_score.line_total).toFixed(4)) }} 分</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
@ -419,7 +548,6 @@ async function submitReviewScore() {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 参赛承诺书弹窗(与选手端结构与样式一致) -->
|
|
|
|
|
<div
|
|
|
|
|
id="reviewerPledgeModal"
|
|
|
|
|
class="modal fade"
|
|
|
|
|
@ -470,39 +598,86 @@ async function submitReviewScore() {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 项目评分(Schema 驱动) -->
|
|
|
|
|
<!-- 赛道打分表 -->
|
|
|
|
|
<div id="reviewerScoreModal" class="modal fade" tabindex="-1" aria-labelledby="reviewerScoreModalLabel" aria-hidden="true">
|
|
|
|
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable reviewer-detail-modal reviewer-detail-modal--score">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 id="reviewerScoreModalLabel" class="modal-title">项目评分</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭" />
|
|
|
|
|
<div class="modal-header scoring-modal-header">
|
|
|
|
|
<h5 id="reviewerScoreModalLabel" class="modal-title scoring-modal-title w-100 text-center">
|
|
|
|
|
{{ detail?.scoring_sheet?.title || '项目评分' }}
|
|
|
|
|
</h5>
|
|
|
|
|
<button type="button" class="btn-close scoring-modal-close" data-bs-dismiss="modal" aria-label="关闭" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<p v-if="detail?.review_schema?.is_default" class="small text-secondary mb-3">
|
|
|
|
|
本场赛事未绑定自定义评审表,已使用系统默认维度。
|
|
|
|
|
<p class="small text-secondary mb-2 text-center">
|
|
|
|
|
请为每一项填写得分(可为 0);单项不得超过「分值」上限;提交后自动汇总为项目得分。
|
|
|
|
|
</p>
|
|
|
|
|
<div v-for="field in detail?.review_schema?.schema_json ?? []" :key="field.key" class="mb-3">
|
|
|
|
|
<label class="form-label">
|
|
|
|
|
{{ field.label }}
|
|
|
|
|
<span v-if="field.required" class="text-danger">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
v-if="field.type === 'number'"
|
|
|
|
|
v-model="scoreDraft[field.key]"
|
|
|
|
|
class="form-control"
|
|
|
|
|
type="number"
|
|
|
|
|
step="any"
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
v-else-if="field.type === 'text'"
|
|
|
|
|
v-model="scoreDraft[field.key]"
|
|
|
|
|
class="form-control"
|
|
|
|
|
type="text"
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
/>
|
|
|
|
|
<textarea v-else v-model="scoreDraft[field.key]" class="form-control" rows="4" />
|
|
|
|
|
<p v-if="scoreFormHint" class="small text-danger mb-3 text-center" role="alert">{{ scoreFormHint }}</p>
|
|
|
|
|
<div class="table-responsive scoring-sheet-wrap">
|
|
|
|
|
<table class="table table-bordered align-middle mb-0 scoring-sheet-table">
|
|
|
|
|
<colgroup>
|
|
|
|
|
<col class="col-seq" />
|
|
|
|
|
<col class="col-category" />
|
|
|
|
|
<col class="col-indicator" />
|
|
|
|
|
<col class="col-criteria" />
|
|
|
|
|
<col class="col-max" />
|
|
|
|
|
<col class="col-score" />
|
|
|
|
|
</colgroup>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th class="text-nowrap text-center">序号</th>
|
|
|
|
|
<th class="text-nowrap text-center">指标分类</th>
|
|
|
|
|
<th class="text-nowrap text-center">评审指标</th>
|
|
|
|
|
<th class="text-center">核心评审要素(评委打分依据)</th>
|
|
|
|
|
<th class="text-nowrap text-center">分值</th>
|
|
|
|
|
<th class="text-nowrap text-center">得分</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="row in scoringRows" :key="row.item.key">
|
|
|
|
|
<td class="text-center text-secondary">{{ row.item.sort || row.idx + 1 }}</td>
|
|
|
|
|
<td
|
|
|
|
|
v-if="row.categoryRowspan > 0"
|
|
|
|
|
:rowspan="row.categoryRowspan"
|
|
|
|
|
class="text-center scoring-category"
|
|
|
|
|
>
|
|
|
|
|
{{ row.item.category }}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-center fw-semibold scoring-indicator">{{ row.item.title }}</td>
|
|
|
|
|
<td class="small text-secondary scoring-criteria">{{ row.item.criteria || '—' }}</td>
|
|
|
|
|
<td class="text-center">{{ formatMax(row.item.max_score) }}</td>
|
|
|
|
|
<td class="scoring-score-cell text-center">
|
|
|
|
|
<input
|
|
|
|
|
v-model="scoreDraft[row.item.key]"
|
|
|
|
|
class="form-control form-control-sm text-center scoring-score-input"
|
|
|
|
|
:class="{ 'is-invalid': !!getScoreError(row.item) }"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
:max="row.item.max_score"
|
|
|
|
|
step="any"
|
|
|
|
|
required
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
:aria-label="`${row.item.title}得分`"
|
|
|
|
|
@blur="onScoreBlur(row.item.key)"
|
|
|
|
|
/>
|
|
|
|
|
<div v-if="getScoreError(row.item)" class="invalid-feedback d-block text-center">
|
|
|
|
|
{{ getScoreError(row.item) }}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr class="scoring-total-row">
|
|
|
|
|
<td colspan="4" class="text-end fw-semibold">项目得分</td>
|
|
|
|
|
<td class="text-center text-secondary">{{ detail?.scoring_sheet?.full_score ?? 100 }}</td>
|
|
|
|
|
<td class="text-center fw-semibold">
|
|
|
|
|
{{
|
|
|
|
|
draftLineTotal.allValid
|
|
|
|
|
? rtrimZeros(draftLineTotal.sum.toFixed(4))
|
|
|
|
|
: '—'
|
|
|
|
|
}}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer flex-wrap gap-2">
|
|
|
|
|
@ -580,10 +755,117 @@ async function submitReviewScore() {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin: 0.75rem 0 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-modal-header {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-modal-title {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding-inline: 2rem;
|
|
|
|
|
font-size: 1.05rem;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-modal-close {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 1rem;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-wrap {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
max-height: min(58vh, calc(100dvh - 14rem));
|
|
|
|
|
overflow: auto;
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
overscroll-behavior: contain;
|
|
|
|
|
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
|
|
|
border-radius: 0.375rem;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table {
|
|
|
|
|
table-layout: fixed;
|
|
|
|
|
width: 100%;
|
|
|
|
|
/* 窄屏保底宽度,横向滚动;宽屏由 100% 撑满弹窗 */
|
|
|
|
|
min-width: 52rem;
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table .col-seq {
|
|
|
|
|
width: 3.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table .col-category {
|
|
|
|
|
width: 6rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table .col-indicator {
|
|
|
|
|
width: 8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table .col-criteria {
|
|
|
|
|
width: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table .col-max {
|
|
|
|
|
width: 4.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table .col-score {
|
|
|
|
|
width: 10rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-sheet-table th {
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-align: center;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
white-space: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-category {
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
writing-mode: horizontal-tb;
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-indicator {
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-criteria {
|
|
|
|
|
line-height: 1.45;
|
|
|
|
|
word-break: normal;
|
|
|
|
|
overflow-wrap: break-word;
|
|
|
|
|
white-space: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-score-cell {
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-score-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 4.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-score-cell .invalid-feedback {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
margin-top: 0.2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scoring-total-row td {
|
|
|
|
|
background: #f3f6fa;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
/* 与选手端 #promiseSignModal 对齐,便于评审承诺书过长时正文区滚动 */
|
|
|
|
|
#reviewerPledgeModal .modal-body {
|
|
|
|
|
max-height: none;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
@ -608,10 +890,30 @@ async function submitReviewScore() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerScoreModal .reviewer-detail-modal--score {
|
|
|
|
|
--bs-modal-width: min(24rem, calc(100vw - 1.25rem));
|
|
|
|
|
max-width: min(24rem, calc(100vw - 1.25rem));
|
|
|
|
|
margin-top: 0.65rem;
|
|
|
|
|
margin-bottom: 0.65rem;
|
|
|
|
|
--bs-modal-width: min(72rem, calc(100vw - 1.25rem));
|
|
|
|
|
max-width: min(72rem, calc(100vw - 1.25rem));
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerScoreModal .modal-content {
|
|
|
|
|
max-height: calc(100dvh - 1rem);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerScoreModal .modal-header,
|
|
|
|
|
#reviewerScoreModal .modal-footer {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerScoreModal .modal-body {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerPledgeModal .promise-doc-scroll {
|
|
|
|
|
@ -620,4 +922,20 @@ async function submitReviewScore() {
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 767.98px) {
|
|
|
|
|
#reviewerScoreModal .reviewer-detail-modal--score {
|
|
|
|
|
--bs-modal-width: calc(100vw - 0.75rem);
|
|
|
|
|
max-width: calc(100vw - 0.75rem);
|
|
|
|
|
margin: 0.35rem auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerScoreModal .modal-content {
|
|
|
|
|
max-height: calc(100dvh - 0.7rem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#reviewerScoreModal .modal-body {
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|