|
|
|
|
@ -131,13 +131,26 @@ function totalDraftText(): string {
|
|
|
|
|
return String(totalDraft.value ?? '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 文本输入绑定:过滤非法字符,始终写回 string */
|
|
|
|
|
const SCORE_MAX = 100
|
|
|
|
|
const SCORE_DECIMALS = 2
|
|
|
|
|
|
|
|
|
|
/** 文本输入绑定:仅 0~100,最多两位小数 */
|
|
|
|
|
const totalDraftModel = computed({
|
|
|
|
|
get: () => totalDraft.value,
|
|
|
|
|
set: (v: string | number | null | undefined) => {
|
|
|
|
|
const raw = v == null ? '' : String(v)
|
|
|
|
|
// 允许空、整数/小数、一位前导负号(校验时再拦负数)
|
|
|
|
|
const cleaned = raw.replace(/[^\d.-]/g, '').replace(/(?!^)-/g, '').replace(/(\..*)\./g, '$1')
|
|
|
|
|
let cleaned = raw.replace(/[^\d.]/g, '').replace(/(\..*)\./g, '$1')
|
|
|
|
|
const m = cleaned.match(/^(\d*)(\.\d{0,2})?/)
|
|
|
|
|
cleaned = m ? `${m[1] ?? ''}${m[2] ?? ''}` : ''
|
|
|
|
|
if (cleaned === '' || cleaned === '.') {
|
|
|
|
|
totalDraft.value = cleaned === '.' ? '0.' : ''
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const n = Number(cleaned)
|
|
|
|
|
if (Number.isFinite(n) && n > SCORE_MAX) {
|
|
|
|
|
totalDraft.value = String(SCORE_MAX)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
totalDraft.value = cleaned
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
@ -163,7 +176,7 @@ function syncScoreDraft(): void {
|
|
|
|
|
} else {
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
// Number('') === 0,空分必须保持空串,不能写成默认 0
|
|
|
|
|
totalDraft.value = Number.isFinite(n) ? rtrimZeros(n.toFixed(4)) : ''
|
|
|
|
|
totalDraft.value = Number.isFinite(n) ? rtrimZeros(n.toFixed(SCORE_DECIMALS)) : ''
|
|
|
|
|
}
|
|
|
|
|
totalBlurred.value = false
|
|
|
|
|
scoreShowErrors.value = false
|
|
|
|
|
@ -176,7 +189,11 @@ function rtrimZeros(s: string): string {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatMax(n: number): string {
|
|
|
|
|
return rtrimZeros(Number(n).toFixed(4))
|
|
|
|
|
return rtrimZeros(Number(n).toFixed(SCORE_DECIMALS))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasMoreThanTwoDecimals(raw: string): boolean {
|
|
|
|
|
return /\.\d{3,}/.test(raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scoringItems = computed(() => detail.value?.scoring_sheet?.items ?? [])
|
|
|
|
|
@ -214,23 +231,30 @@ function getTotalError(): string | null {
|
|
|
|
|
if (scoreShowErrors.value || totalBlurred.value) return '请填写项目总分'
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
if (hasMoreThanTwoDecimals(raw)) return '最多保留两位小数'
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
if (!Number.isFinite(n)) return '须为数字'
|
|
|
|
|
if (n < 0) return '不能小于 0'
|
|
|
|
|
if (n > fullScore.value) return `不得超过 ${formatMax(fullScore.value)} 分`
|
|
|
|
|
if (n > SCORE_MAX) return '不得超过 100 分'
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTotalBlur(): void {
|
|
|
|
|
totalBlurred.value = true
|
|
|
|
|
const raw = totalDraftText()
|
|
|
|
|
if (raw === '' || raw.endsWith('.')) return
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
if (!Number.isFinite(n)) return
|
|
|
|
|
const clamped = Math.min(SCORE_MAX, Math.max(0, n))
|
|
|
|
|
totalDraft.value = rtrimZeros(clamped.toFixed(SCORE_DECIMALS))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const canSubmitScore = computed(() => {
|
|
|
|
|
if (!detail.value?.scoring_allowed || !scoringItems.value.length) return false
|
|
|
|
|
const raw = totalDraftText()
|
|
|
|
|
if (raw === '') return false
|
|
|
|
|
if (raw === '' || hasMoreThanTwoDecimals(raw)) return false
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
return Number.isFinite(n) && n >= 0 && n <= fullScore.value
|
|
|
|
|
return Number.isFinite(n) && n >= 0 && n <= SCORE_MAX
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const scoreFormHint = computed(() => {
|
|
|
|
|
@ -364,7 +388,8 @@ async function downloadAttachment(f: FileItem) {
|
|
|
|
|
|
|
|
|
|
function buildSubmitPayload(): Record<string, unknown> {
|
|
|
|
|
const n = Number(totalDraftText())
|
|
|
|
|
return { line_total: Number.isFinite(n) ? n : 0 }
|
|
|
|
|
const score = Number.isFinite(n) ? Math.round(n * 100) / 100 : 0
|
|
|
|
|
return { line_total: score }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cleanupModalArtifacts(): void {
|
|
|
|
|
@ -571,7 +596,7 @@ async function submitReviewScore() {
|
|
|
|
|
}}</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
|
|
|
|
|
{{ rtrimZeros(Number(detail.my_review_score.line_total).toFixed(2)) }} 分</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -640,7 +665,7 @@ async function submitReviewScore() {
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<p class="small text-secondary mb-2 text-center scoring-modal-hint">
|
|
|
|
|
下表为评审参考;请在底部填写项目总分(0~{{ formatMax(fullScore) }},可为 0)。
|
|
|
|
|
下表为评审参考;请在底部填写项目总分(0~100,最多两位小数,可为 0)。
|
|
|
|
|
</p>
|
|
|
|
|
<p v-if="scoreFormHint" class="small text-danger mb-2 text-center" role="alert">{{ scoreFormHint }}</p>
|
|
|
|
|
<div class="table-responsive scoring-sheet-wrap">
|
|
|
|
|
|