feat(0.5.1): 业务逻辑审查 9 条 P0 全修 + 自绘安装器 + 现场反馈四项
## 算分正确性(6 份只读审查 → 收敛成 5 个病根,均先复现失败再修) - 「人工名次」与「自动名次回写」互斥:原来同一份成绩**发布两次分数不同** (团体报名项 dual 通道 10/8/6 → 25/18/14),现在连发多次逐字节一致 - 删小项时在同一事务作废旧流水并推榜:原来分数**永久残留**在总榜、任何重算都打不到 - 规则包导入整体事务化:原来中途失败会清空名次表且补不回(奖牌全丢); 另加守卫「compare=none 的小项不得配 basis=result/aggregate」 - 末位归零只抹真正落在末位的那一档:原来跨末位的并列组会被整组抹 0 (7 人三人并列第 5 时第 5、6 名的分也一起没了) - 模板「分值表档位超出录取名次」现在拒绝保存:原来静默截断成 0 分 ## 现场反馈(主人实测报的) - 已发布成绩可改:行内「修改本条」+ 成绩格/状态格可点(退回草稿→改→重新发布本组), 与「作废」语义分开;取消路径不产生任何副作用 - 单位积分明细补「运动员 / 来源」列,并显示「已修订 ×N(时间·原因)」: 管理端、大屏覆盖层、xlsx 备注列三处同源 - 单位奖牌榜改为按奖牌独立排名(金→银→铜→单位编码),与积分榜解耦 - 历史孤儿积分流水清理(迁移 0022,只动 result/record_bonus,人工调整分不受影响) ## 新功能 - 团体项目聚合口径二选一:平均(默认)/ 总和(迁移 0023,通道编辑器可切) ## 安装器 - 自绘五页安装界面:无边框、自绘标题栏、DPI 自适应(4K@200% 实测)、 安装/卸载逻辑复用 electron-builder 宏不变;默认「为所有用户安装」并自动放行防火墙 19540 - 构建期校验:安装器素材里的版本号必须与 VERSION 一致,否则构建失败 自检:服务端 14 包 ×2 / 前端 vue-tsc + build + 742 例 ×2 / Console 20 文件 + 68 例 ×2,全绿。 端到端对拍:修前 vs 修后,纯团体 / 双计 / 普通个人三形态**逐字节一致**,仅出 bug 的双通道形态按要求改变。
This commit is contained in:
1 parent
78b14efaa9
commit
877ff15823
181 files changed
+8747
-371
No files matched your search
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
eventInputOf,
|
||||
@@ -55,7 +55,10 @@ import {
|
||||
RESULT_ROW_ACTION_TITLE,
|
||||
RESULT_ROW_ACTION_WORD,
|
||||
resultActionTarget,
|
||||
resultActionTargets,
|
||||
resultCellAction,
|
||||
type ResultActionTarget,
|
||||
type ResultRowAction,
|
||||
} from './entryResultActions'
|
||||
import {
|
||||
DEFAULT_HEAT_SIZE,
|
||||
@@ -125,6 +128,17 @@ const STATUS_OPTIONS: Array<{ value: ResultStatus; label: string }> = [
|
||||
{ value: 'withdrawn', label: '退赛' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 成绩状态文案(下拉选项的反查表)。
|
||||
*
|
||||
* 已发布的行不再渲染下拉(disabled 的表单控件收不到点击,正是「点不动」的来源),
|
||||
* 改用只读按钮显示当前状态,文案必须与下拉选项同源,免得两处各写一份慢慢说岔。
|
||||
*/
|
||||
const STATUS_LABEL = Object.fromEntries(STATUS_OPTIONS.map((option) => [option.value, option.label])) as Record<
|
||||
ResultStatus,
|
||||
string
|
||||
>
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const data = useEventStore()
|
||||
@@ -459,37 +473,49 @@ onBeforeRouteLeave((to) => {
|
||||
return false
|
||||
})
|
||||
|
||||
/** 已发布的行由服务端锁定:只能先撤销发布,不能就地改草稿。 */
|
||||
/**
|
||||
* 已发布的行由服务端锁定:直接改会让「榜单上是旧数、表格里是新数」。
|
||||
* 退回草稿(reopen)后 state 变回 draft,这里随即放行——「修改本条」正是靠这一步解锁。
|
||||
*/
|
||||
function rowLocked(row: EditableRow): boolean {
|
||||
return row.resultState === 'published'
|
||||
}
|
||||
|
||||
/**
|
||||
* 每行的行操作(作废本条 / 重新录入):判定在 entryResultActions 的纯函数里。
|
||||
* 已发布(含已作废)行的锁定提示:这些格子改不了时,悬停要直接指出出路。
|
||||
*
|
||||
* `clickable` 指**这一格自己点得动**(成绩格 / 状态格:点一下弹退回草稿的确认框)——
|
||||
* 名次 / 备注格仍不参与点击,只能说「点成绩格或右侧『修改』」,
|
||||
* 否则提示就变成一句点不出来的空头承诺。
|
||||
*
|
||||
* 已发布行的出路有两条——退回这一条草稿,或整组「撤销发布」;已作废行的出路只有一条——「重新录入」。
|
||||
*/
|
||||
function lockedHint(row: EditableRow, clickable = false): string {
|
||||
if (!rowLocked(row)) return ''
|
||||
if (row.status === 'withdrawn') {
|
||||
return '本条已作废:不计入名次与积分,用「重新录入」退回草稿后才能修改'
|
||||
}
|
||||
return clickable
|
||||
? '已发布:点击这一格可退回草稿修改,改完需重新发布本组;也可以点右侧「修改」或整组「撤销发布」'
|
||||
: '已发布:点成绩格或右侧「修改」退回草稿后可改,改完需重新发布本组'
|
||||
}
|
||||
|
||||
/**
|
||||
* 每行的行操作(修改本条 / 作废本条 / 重新录入):判定在 entryResultActions 的纯函数里。
|
||||
*
|
||||
* 与 judgements 同口径——按当前可见行算,筛选后看不见的行不会残留按钮。
|
||||
*/
|
||||
const rowActions = computed<Map<number, ResultActionTarget>>(() => {
|
||||
const map = new Map<number, ResultActionTarget>()
|
||||
const rowActions = computed<Map<number, ResultActionTarget[]>>(() => {
|
||||
const map = new Map<number, ResultActionTarget[]>()
|
||||
for (const row of visibleRows.value) {
|
||||
const target = resultActionTarget(row)
|
||||
if (target !== null) map.set(row.entryId, target)
|
||||
const targets = resultActionTargets(row)
|
||||
if (targets.length > 0) map.set(row.entryId, targets)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
function rowAction(row: EditableRow): ResultActionTarget | null {
|
||||
return rowActions.value.get(row.entryId) ?? null
|
||||
}
|
||||
|
||||
function rowActionLabel(row: EditableRow): string {
|
||||
const target = rowAction(row)
|
||||
return target === null ? '' : RESULT_ROW_ACTION_LABEL[target.action]
|
||||
}
|
||||
|
||||
function rowActionTitle(row: EditableRow): string {
|
||||
const target = rowAction(row)
|
||||
return target === null ? '' : RESULT_ROW_ACTION_TITLE[target.action]
|
||||
function rowActionsOf(row: EditableRow): ResultActionTarget[] {
|
||||
return rowActions.value.get(row.entryId) ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -545,10 +571,9 @@ const focusedHint = computed<ResultCellHint | null>(() => {
|
||||
/** 失焦后由既有的红框 + title 承载原因;合法成绩不写 title,避免无意义悬浮。 */
|
||||
function resultTitle(row: EditableRow): string {
|
||||
if (rowLocked(row)) {
|
||||
// 已作废的行同样是「锁定」的,但原因是作废而不是发布:提示要指向真正的出路。
|
||||
return row.status === 'withdrawn'
|
||||
? '本条已作废:不计入名次与积分,用「重新录入」退回草稿后才能修改'
|
||||
: '已发布,请先撤销发布'
|
||||
// 已发布 / 已作废的行都锁着,但出路不同:统一由 lockedHint 说清该点哪个按钮。
|
||||
// 成绩格自己点得动,用 clickable 那一份文案(「点击这一格」)。
|
||||
return lockedHint(row, true)
|
||||
}
|
||||
const hint = resultHintOf(row)
|
||||
return hint !== null && hint.level !== 'ok' ? hint.message : ''
|
||||
@@ -1395,24 +1420,27 @@ const BATCH_CONFIRM_TEXT: Record<EntryBatchAction, string> = {
|
||||
remove: '确认移出',
|
||||
}
|
||||
|
||||
// ── 单条作废 / 重新录入(A16:外科手术式改一条,不必整组撤销发布) ──────────────
|
||||
// ── 单条修改 / 作废 / 重新录入(A16:外科手术式改一条,不必整组撤销发布) ──────────
|
||||
|
||||
/** 待确认的行操作;非 null 时确认框打开。 */
|
||||
const resultAction = ref<ResultActionTarget | null>(null)
|
||||
const resultActionBusy = ref(false)
|
||||
|
||||
/** 确认框文案随动作变(作废 / 退回草稿的后果不同)。 */
|
||||
/** 确认框文案随动作变(修改 / 作废 / 退回草稿的后果不同)。 */
|
||||
const resultActionCopy = computed(() => RESULT_ACTION_CONFIRM[resultAction.value?.action ?? 'void'])
|
||||
|
||||
/**
|
||||
* 点行操作按钮:先做前置检查(示例数据 / 只读 / 未保存修改),再弹确认框。
|
||||
*
|
||||
* 一个按钮一次只带一个动作(同一行可能并排着「修改本条」与「作废本条」),
|
||||
* 判定仍由纯函数给出:传进来的动作必须在该行成立,否则当没点。
|
||||
*
|
||||
* 与批量操作同一口径——它会重算并刷新整表,未保存的编辑必须先落草稿,
|
||||
* 否则刚填的名次会被服务端返回的口径覆盖掉。
|
||||
*/
|
||||
function requestResultAction(row: EditableRow): void {
|
||||
const target = rowAction(row)
|
||||
if (target === null) return
|
||||
function requestResultAction(target: ResultActionTarget): void {
|
||||
const current = rowActions.value.get(target.entryId)
|
||||
if (current === undefined || !current.some((item) => item.action === target.action)) return
|
||||
if (data.isMock) {
|
||||
// 示例数据模式下写接口没有后端可落库,提示成功等于谎报(与发布 / 批量操作同口径)。
|
||||
const notice = sampleDataBlockedNotice(RESULT_ROW_ACTION_WORD[target.action])
|
||||
@@ -1424,12 +1452,41 @@ function requestResultAction(row: EditableRow): void {
|
||||
return
|
||||
}
|
||||
if (dirty.value) {
|
||||
toast.error('有未保存的修改', '作废 / 退回会重算并刷新整表,请先「存草稿」/「保存团体名次」再操作')
|
||||
toast.error('有未保存的修改', '修改 / 作废 / 退回会重算并刷新整表,请先「存草稿」/「保存团体名次」再操作')
|
||||
return
|
||||
}
|
||||
resultAction.value = target
|
||||
}
|
||||
|
||||
/** 已发布行的格子点一下走哪个动作;草稿行 / 已作废行返回 null(照常直接编辑 / 保持禁用)。 */
|
||||
function cellAction(row: EditableRow): ResultRowAction | null {
|
||||
return resultCellAction(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 这一格是否保持原来的 disabled 锁定(收不到点击)。
|
||||
*
|
||||
* 已作废的行走这里:它的出路是按钮上的「重新录入」,格子不该装成点得动。
|
||||
* 可点的已发布格子改用 readonly——disabled 的控件连 click 事件都不会派发,弹不出确认框。
|
||||
*/
|
||||
function cellDisabled(row: EditableRow): boolean {
|
||||
return rowLocked(row) && cellAction(row) === null
|
||||
}
|
||||
|
||||
/**
|
||||
* 点已发布行的成绩格 / 状态格 = 点「修改本条」。
|
||||
*
|
||||
* 必须复用 requestResultAction 这一条路:确认框(含原因必填)、示例数据 / 只读 / 未保存修改
|
||||
* 三道拦截、确认后整表重拉都在里面。格子这里绝不能自己调接口——否则就成了
|
||||
* 「点一下成绩就悄悄从榜上退下来」。用户取消时也什么都回不去(见 cancelResultAction)。
|
||||
*/
|
||||
function requestCellAction(row: EditableRow): void {
|
||||
const action = cellAction(row)
|
||||
if (action === null) return
|
||||
const target = resultActionTarget(row, action)
|
||||
if (target !== null) requestResultAction(target)
|
||||
}
|
||||
|
||||
/** 确认框回传的原因;空原因由 ConfirmDialog 拦下,这里拿到的一定是填好的。 */
|
||||
async function confirmResultAction(reason: string): Promise<void> {
|
||||
const target = resultAction.value
|
||||
@@ -1439,25 +1496,48 @@ async function confirmResultAction(reason: string): Promise<void> {
|
||||
if (target.action === 'void') {
|
||||
await adminApi.voidResult(target.resultId, { reason })
|
||||
} else {
|
||||
// 「修改本条」与「重新录入」是同一个接口:两者都把这一条退回草稿,只是入口语境不同。
|
||||
await adminApi.reopenResult(target.resultId, { reason })
|
||||
}
|
||||
resultAction.value = null
|
||||
// 名次 / 积分 / 大屏都由服务端重算:整表 + 榜单一起重拉,不在本地行上打补丁。
|
||||
await reloadAfterSettlementChange()
|
||||
// 退回草稿后把焦点送回那一格,操作员不必再拿鼠标找一遍就能接着改;
|
||||
// 作废没有可改的格子(它的出路是「重新录入」),不聚焦。
|
||||
if (target.action !== 'void') await focusResultCell(target.entryId)
|
||||
const success = RESULT_ACTION_SUCCESS[target.action]
|
||||
toast.success(success.title, success.detail)
|
||||
} catch (caught) {
|
||||
toast.error(target.action === 'void' ? '作废失败' : '退回草稿失败', caught instanceof Error ? caught.message : null)
|
||||
toast.error(`${RESULT_ROW_ACTION_WORD[target.action]}失败`, caught instanceof Error ? caught.message : null)
|
||||
} finally {
|
||||
resultActionBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消确认框:什么都不做。
|
||||
*
|
||||
* 这里只能清掉待办的动作——退回草稿的接口一次都不能提前发出去,
|
||||
* 否则「点一下格子又点了取消」就会把一条已发布成绩悄悄从榜上退下来。
|
||||
*/
|
||||
function cancelResultAction(): void {
|
||||
if (resultActionBusy.value) return
|
||||
resultAction.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 把焦点落到某一行的成绩输入格(退回草稿成功后调用)。
|
||||
*
|
||||
* 必须等 nextTick:整表刚被 reload 换过一轮,DOM 还没更新完,这时候查不到新那一格的节点。
|
||||
*/
|
||||
async function focusResultCell(entryId: number): Promise<void> {
|
||||
await nextTick()
|
||||
const input = tableRef.value?.querySelector<HTMLInputElement>(`[data-result-cell="${entryId}"]`)
|
||||
if (input === null || input === undefined) return
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
|
||||
/** 一键均分:先亮明「动哪些人、怎么切、编到第几批」,确认后才提交。 */
|
||||
function requestSplit(): void {
|
||||
const plan = splitPlan.value
|
||||
@@ -1529,6 +1609,10 @@ function onBeforeUnload(event: BeforeUnloadEvent): void {
|
||||
function onPaste(event: ClipboardEvent, startEntryId: number): void {
|
||||
const text = event.clipboardData?.getData('text/plain') ?? ''
|
||||
if (text.trim() === '') return
|
||||
// 已发布的行现在是 readonly:浏览器照样会派发 paste 事件(只是不插入文本),
|
||||
// 放过去的话这一条成绩会被本地悄悄改掉、连确认框都不弹,必须在这里挡住。
|
||||
const start = visibleRows.value.find((row) => row.entryId === startEntryId)
|
||||
if (start === undefined || rowLocked(start)) return
|
||||
event.preventDefault()
|
||||
|
||||
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '')
|
||||
@@ -1891,6 +1975,7 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
<MxIcon name="lock" :size="15" />
|
||||
{{ scopeWord }}已有 {{ publishedCount }} 条成绩发布并锁定上屏(共 {{ visibleRows.length }} 条):未发布的行可继续补录,
|
||||
录完再点一次「发布本批」即可,名次与积分会按已发布成绩整体重算。
|
||||
要改某一条已发布的成绩,直接点那一格(或右侧「修改本条」)退回草稿,改完再重新发布本组。
|
||||
</div>
|
||||
|
||||
<div class="entry-body">
|
||||
@@ -1972,14 +2057,22 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
<!-- 号码布紧跟姓名重复一次:左边那列还在(有人习惯了),这一个是给眼睛省路的。 -->
|
||||
<td>{{ athleteCellName(row) }} <span v-if="inlineBibLabel(row.bib)" class="bib-inline mono">{{ inlineBibLabel(row.bib) }}</span></td>
|
||||
<td class="result-cell">
|
||||
<!--
|
||||
已发布的行不再用 :disabled 锁(disabled 的表单控件收不到点击,
|
||||
「点不动」正是现场反馈的观感),改成 readonly + 点一下弹确认框:
|
||||
确认后才退回草稿,取消则什么都不会发生。
|
||||
-->
|
||||
<input
|
||||
v-model="row.resultText"
|
||||
data-cell
|
||||
:data-result-cell="row.entryId"
|
||||
class="mx-cell-input mono result-input"
|
||||
:class="{ 'is-invalid': issueFor(row.entryId)?.level === 'error' }"
|
||||
:disabled="!canWrite || rowLocked(row)"
|
||||
:class="{ 'is-invalid': issueFor(row.entryId)?.level === 'error', 'is-locked-cell': cellAction(row) !== null }"
|
||||
:disabled="!canWrite || cellDisabled(row)"
|
||||
:readonly="cellAction(row) !== null"
|
||||
:title="resultTitle(row)"
|
||||
:placeholder="resultPlaceholder"
|
||||
@click="requestCellAction(row)"
|
||||
@focus="onResultFocus(row, $event)"
|
||||
@blur="onResultBlur"
|
||||
@input="onResultInput"
|
||||
@@ -2007,7 +2100,7 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
min="1"
|
||||
class="mx-cell-input rank-input"
|
||||
:disabled="!canWrite || rowLocked(row)"
|
||||
:title="rowLocked(row) ? '已发布,请先撤销发布' : ''"
|
||||
:title="lockedHint(row)"
|
||||
@input="row.rank = rankInputValue($event)"
|
||||
@focus="focusedEntryId = row.entryId"
|
||||
@blur="focusedEntryId = null"
|
||||
@@ -2022,12 +2115,30 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
:class="row.entryStatus === 'scratched' ? 'mx-badge-warning' : 'mx-badge-error'"
|
||||
:title="entryStatusTitle(row.entryStatus)"
|
||||
>{{ ENTRY_STATUS_LABEL[row.entryStatus] }}</span>
|
||||
<!--
|
||||
已发布(未作废)的行不渲染下拉:下拉在锁定态只能 disabled,而 disabled 控件
|
||||
收不到点击。换成一个可点的只读按钮,点一下与「修改本条」同一条路
|
||||
(确认后才退回草稿);已作废的行仍走下面那个 disabled 下拉,保持现状。
|
||||
-->
|
||||
<button
|
||||
v-if="cellAction(row) !== null"
|
||||
type="button"
|
||||
data-cell
|
||||
class="mx-cell-input status-select is-locked-cell"
|
||||
:disabled="!canWrite"
|
||||
:title="lockedHint(row, true)"
|
||||
@click="requestCellAction(row)"
|
||||
@focus="focusedEntryId = row.entryId"
|
||||
@blur="focusedEntryId = null"
|
||||
@keydown="onCellKeydown($event, $event.currentTarget as HTMLElement)"
|
||||
>{{ STATUS_LABEL[row.status] }}</button>
|
||||
<select
|
||||
v-else
|
||||
v-model="row.status"
|
||||
data-cell
|
||||
class="mx-cell-input status-select"
|
||||
:disabled="!canWrite || rowLocked(row)"
|
||||
:title="rowLocked(row) ? '已发布,请先撤销发布' : ''"
|
||||
:title="lockedHint(row)"
|
||||
@focus="focusedEntryId = row.entryId"
|
||||
@blur="focusedEntryId = null"
|
||||
@keydown="onCellKeydown($event, $event.currentTarget as HTMLElement)"
|
||||
@@ -2048,7 +2159,7 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
data-cell
|
||||
class="mx-cell-input"
|
||||
:disabled="!canWrite || rowLocked(row)"
|
||||
:title="rowLocked(row) ? '已发布,请先撤销发布' : ''"
|
||||
:title="lockedHint(row)"
|
||||
placeholder="可留空"
|
||||
@focus="focusedEntryId = row.entryId"
|
||||
@blur="focusedEntryId = null"
|
||||
@@ -2057,21 +2168,24 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
</td>
|
||||
<!--
|
||||
行操作区:只对已发布的行出现(草稿行本来就能直接改)。
|
||||
guest / 断线只读时按钮保留但置灰,让访客看得到「这里能作废」而不是一片空白。
|
||||
正常已发布的行并排两个按钮——「修改本条」(退回草稿改一改,改完重新发布)与
|
||||
「作废本条」(这一条不算了);已作废的行只有一个「重新录入」。
|
||||
guest / 断线只读时按钮保留但置灰,让访客看得到「这里能改 / 能作废」而不是一片空白。
|
||||
-->
|
||||
<td class="center action-cell">
|
||||
<button
|
||||
v-if="rowAction(row) !== null"
|
||||
class="mx-btn mx-btn-sm"
|
||||
:class="rowAction(row)?.action === 'reopen' ? 'mx-btn-secondary' : 'mx-btn-danger'"
|
||||
v-for="target in rowActionsOf(row)"
|
||||
:key="target.action"
|
||||
class="mx-btn mx-btn-sm action-btn"
|
||||
:class="target.action === 'void' ? 'mx-btn-danger' : 'mx-btn-secondary'"
|
||||
type="button"
|
||||
:disabled="!canWrite || resultActionBusy"
|
||||
:title="rowActionTitle(row)"
|
||||
@click="requestResultAction(row)"
|
||||
:title="RESULT_ROW_ACTION_TITLE[target.action]"
|
||||
@click="requestResultAction(target)"
|
||||
>
|
||||
{{ rowActionLabel(row) }}
|
||||
{{ RESULT_ROW_ACTION_LABEL[target.action] }}
|
||||
</button>
|
||||
<span v-else class="muted">—</span>
|
||||
<span v-if="rowActionsOf(row).length === 0" class="muted">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && visibleRows.length === 0">
|
||||
@@ -2584,14 +2698,21 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
.team-cell {
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 行操作区:窄列、不换行,按钮别把整张表撑宽。 */
|
||||
/*
|
||||
* 行操作区:已发布的行要并排放「修改本条」+「作废本条」两个按钮,
|
||||
* 列宽按两者之和留(4 字 ×12px + 内边距 + 单元格 padding),否则按钮会换行把这一行撑高一倍。
|
||||
* 按钮内边距收窄(12 → 10px)是为了把列宽压住,不与上面的列宽估算脱节。
|
||||
*/
|
||||
.action-col,
|
||||
.action-cell {
|
||||
width: 96px;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.action-cell .mx-btn {
|
||||
width: 100%;
|
||||
.action-cell .action-btn {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.action-cell .action-btn + .action-btn {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.action-cell .muted {
|
||||
color: var(--mx-t3);
|
||||
@@ -2732,6 +2853,25 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
.status-select {
|
||||
width: 110px;
|
||||
}
|
||||
/*
|
||||
* 已发布行的可点格子(成绩格 / 状态格):从「彻底禁用」改成「点一下退回草稿改」。
|
||||
* 底色与草稿格(--mx-s3)不同 + 常态描边 + cursor: pointer,一眼看出这里点得动;
|
||||
* 点下去只是弹与「修改本条」同一个确认框,不确认则什么都不会发生。
|
||||
* 已作废行的格子 cellAction 为 null:拿不到这个类,也仍走 disabled(保持原来的禁用观感)。
|
||||
*/
|
||||
.is-locked-cell {
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
background: var(--mx-s2);
|
||||
border-color: var(--mx-border);
|
||||
}
|
||||
.is-locked-cell:hover:not(:disabled) {
|
||||
border-color: var(--mx-primary);
|
||||
box-shadow: 0 0 0 3px var(--mx-primary-glow);
|
||||
}
|
||||
.is-locked-cell:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.rank-auto {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
||||
Reference in new issue
Block a user