feat(joint-team): 联队 + 份额分成 —— 迁移 0024、引擎按份额拆、导出 / 大屏 / 录入三路打通;版本 dev_0.5.3
## 需求
现场 6 项球类比赛里,有两个单位因参赛人数不足**合并成一个单位参赛**。
做法不是新建「联队单位」(那样榜单会多出第 8 个单位,且分和奖牌分不到原单位头上),
而是**一条报名挂多个单位、按份额拆**。
## 拍板口径
- 份额**可配、默认均分**
- 积分**平均拆、接受 .5**(团体第 1 名 15 分 → 两个单位各 7.5 分)
- 奖牌**各记一枚**(两个单位各 1 枚完整金牌,不是各 0.5 枚)
- 显示上把联队名字写全(形如 `第一分公司+第二分公司联队`),成员单位的 logo 都放
- 导入表里单位编码写 `A+B`
## 一、核心层(数据模型 + 引擎 + 存储 + 报名写入 + 导入解析)
- 迁移 `0024_entry_team_shares.sql`:新建 `entry_team_shares(entry_id, team_id, points_share,
medal_share, sort_order)`。**两个口径分开表达**是刻意的——积分要拆成小数(7.5)、奖牌要整枚
各记,用一个数表达就会把「7.5 分」和「半枚金牌」绑在一起,奖牌榜立刻错。
`entries.team_id` 保留为**主单位**;老数据 `INSERT OR IGNORE` 回填成「主单位独占 100%」。
- 引擎:`ResolveShares`(引擎与读侧共用)、`splitSharePoints`(按权重归一、**末位吞余数**,
各份之和恒等于原来那一份)、`TeamShare.Medal`(缺省整枚)。积分按权重拆,奖牌每个份额各记整枚。
- 报名写入:`PUT /api/entries/{id}/team-shares`(recorder 及以上,保存后当场重算该小项组别并推榜)。
- 导入:`team_code` 支持 `A+B`(主单位 = 加号左边,均分,奖牌各整枚);非法形态
(`A+` / `+B` / `A++B` / `A+A` / 成员 >5 / 单位不存在)**行级拦下、不落库**,给人话报错。
## 二、导出与导入模板
- 11 份导出件:联队写全成员单位(`第一分公司+第二分公司联队`),并注明**分拆给了哪几家、各多少**
(份额占比 + 各单位实得积分,取自按份额拆完的流水)。
- 单位奖牌明细:奖牌格写「金牌(联队 …,各单位各记 1 枚)」;含联队时副标题说明
「各单位奖牌数之和会大于实际颁发数」——这是口径 3 的必然结果,**不是 bug**。
- 按单位筛选导出把成员单位一起算上(否则 B 单位导自己的名单会看不到自己参加的那场联队比赛)。
- 分工作表模板:`A+B` 写法挂在「单位编码」表头的**单元格批注 + 输入提示**(中英各一份);
**表头文字、列序、12 列、工作表命名一个字未动**(动了模板就导不回去)。
## 三、前端与大屏
- 录入页 / 复核页「单位」列:联队显示 `联队 第一分公司 67%(权重 2) 第二分公司 33%`,
点该格打开「成员单位与份额」弹窗(加/删成员、改权重、主单位徽标、实时比例预览)。
**普通报名(单单位)的文案、元素、类名一字未动**,只多了一个 `@click` 入口与悬停提示。
- 大屏:分项成绩 / 实时赛况单位列显示联队全名并**放上两个单位的 logo**(单位列宽按该行的
logo 数留位);底栏同理。
## 四、验证(都留了证据)
- **老数据逐字节不变**(改算分引擎的硬门槛):引擎层 golden `a2aa1c97…`、接口层 golden
`aa6cea37…`,两个哈希由主会话**在修前树上亲手重跑确认**;导出层另有 11 份导出件逐行对拍,
差异 **0 处**。
- 前端 773 → **809 例**全绿(旧用例一行未改);服务端 14 包两轮全绿;Console 68 例全绿。
- 真机联调:录入页份额弹窗、大屏两个 logo、奖牌榜 A/B 各 1 金、写路径(移除成员 → 保存 → 200)。
## 五、版本
`dev_0.5.2` → **`dev_0.5.3`**(本批含数据迁移 0024 + 新功能,必须与上一包区分开);
安装器素材重生成(版本号烙在位图里,实测只有 `brand.bmp` 的 1x/2x 两张变化,其余 92 张逐字节未变)。
This commit is contained in:
1 parent
b631418a85
commit
830a4dbdb7
61 files changed
+5649
-121
No files matched your search
@@ -5,7 +5,11 @@ import {
|
||||
eventInputOf,
|
||||
formatPoints,
|
||||
hasAggregateChannel,
|
||||
isJointTeam,
|
||||
jointTeamLabel,
|
||||
normalizeTeamShares,
|
||||
parseResultValue,
|
||||
shareText,
|
||||
type AvgMode,
|
||||
type DraftResultInput,
|
||||
type EntryStatus,
|
||||
@@ -14,6 +18,7 @@ import {
|
||||
type ResultState,
|
||||
type ResultStatus,
|
||||
type TeamRanksView,
|
||||
type TeamShareView,
|
||||
} from '@/shared'
|
||||
import MxIcon from '../components/MxIcon.vue'
|
||||
import PageHeader from '../components/PageHeader.vue'
|
||||
@@ -39,6 +44,19 @@ import {
|
||||
type TeamRankRow,
|
||||
} from './teamRanks'
|
||||
import { athleteCellName, inlineBibLabel } from './entryRowDisplay'
|
||||
import {
|
||||
addShareDraftTeam,
|
||||
removeShareDraftTeam,
|
||||
selectableShareTeams,
|
||||
setShareDraftWeight,
|
||||
shareDraftDirty,
|
||||
shareDraftIssues,
|
||||
shareDraftOf,
|
||||
shareDraftPayload,
|
||||
shareDraftView,
|
||||
submitEntryTeamShares,
|
||||
type ShareDraftRow,
|
||||
} from './entryShares'
|
||||
import {
|
||||
entryBatchOutcome,
|
||||
entryBatchSizeError,
|
||||
@@ -92,6 +110,13 @@ interface EditableRow {
|
||||
athleteId: number | null
|
||||
teamName: string
|
||||
teamShort: string
|
||||
/**
|
||||
* 归属份额(读侧恒为至少一条):单单位就是主单位独占 100%,联队是多条。
|
||||
*
|
||||
* 表格的「单位」列与实时积分预览都读它——联队那条的分要拆给各成员单位,
|
||||
* 只看 teamId 会把整份分记在主单位头上。
|
||||
*/
|
||||
teamShares: TeamShareView[]
|
||||
athleteName: string
|
||||
memberNames: string[] | null
|
||||
/** 批次号(null = 未分批);只影响录入 / 展示分组,不参与算分。 */
|
||||
@@ -352,6 +377,8 @@ function scorableOf(rows: EditableRow[]): ScorableRow[] {
|
||||
rank: row.rank,
|
||||
status: row.status,
|
||||
isTie: row.isTie,
|
||||
// 联队份额随行喂给预览算分:单单位时它就是主单位独占,与升级前同口径。
|
||||
shares: row.teamShares.map((share) => ({ teamId: share.teamId, weight: share.weight })),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -386,10 +413,13 @@ const publishedScore = computed(() => scoreWith(visibleRows.value.filter((row) =
|
||||
*/
|
||||
const publishedContribution = computed(() => {
|
||||
const map = new Map<number, number>()
|
||||
const rowTeamPoints = publishedScore.value?.rowTeamPoints
|
||||
const rowTeamShares = publishedScore.value?.rowTeamShares
|
||||
for (const row of visibleRows.value) {
|
||||
if (row.resultState !== 'published') continue
|
||||
map.set(row.teamId, (map.get(row.teamId) ?? 0) + (rowTeamPoints?.get(row.entryId) ?? 0))
|
||||
// 按份额逐份累加到单位:联队那条的分不是整份记在主单位头上的。
|
||||
for (const part of rowTeamShares?.get(row.entryId) ?? []) {
|
||||
map.set(part.teamId, (map.get(part.teamId) ?? 0) + part.points)
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
@@ -752,14 +782,22 @@ function snapshotOf(entry: EntryWithResult): string {
|
||||
|
||||
function toRow(entry: EntryWithResult): EditableRow {
|
||||
const result = entry.result
|
||||
const teamName = entry.teamName ?? data.teamById(entry.teamId)?.name ?? '未知单位'
|
||||
const row: EditableRow = {
|
||||
entryId: entry.id,
|
||||
categoryId: entry.categoryId,
|
||||
bib: entry.bib,
|
||||
teamId: entry.teamId,
|
||||
athleteId: entry.athleteId,
|
||||
teamName: entry.teamName ?? data.teamById(entry.teamId)?.name ?? '未知单位',
|
||||
teamName,
|
||||
teamShort: entry.shortName ?? data.teamById(entry.teamId)?.shortName ?? '',
|
||||
// 老服务端不下发 teamShares 时按主单位独占补一条(与升级前逐字段一致)。
|
||||
teamShares: normalizeTeamShares(entry.teamShares, {
|
||||
teamId: entry.teamId,
|
||||
weight: 1,
|
||||
teamCode: entry.teamCode ?? data.teamById(entry.teamId)?.code ?? '',
|
||||
teamName,
|
||||
}),
|
||||
athleteName: entry.athleteName ?? '',
|
||||
memberNames: entry.memberNames,
|
||||
heat: entry.heat,
|
||||
@@ -956,6 +994,148 @@ async function onSupplementSubmitted(eventId: number, categoryId: number | null)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 成员单位与份额(联队) ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 份额编辑器(devdocs/19)。
|
||||
*
|
||||
* 打开时把这条报名**当前**的成员单位摆出来:单单位报名看到的就是孤零零一个主单位、
|
||||
* 权重留空,什么都不改直接关掉等于没动过——常规报名流程一点没被碰到。
|
||||
* 需要时在弹窗里加单位、填权重(留空 = 均分),保存后服务端当场重算本组并推榜。
|
||||
*
|
||||
* `original` 留着是为了判断「改了没有」:没改就禁用保存按钮,避免把一次没改动的整组替换
|
||||
* 发出去(服务端会照着做,只是白重算一遍本组)。
|
||||
*/
|
||||
interface ShareEditor {
|
||||
entryId: number
|
||||
/** 展示用的报名身份:运动员 / 队 + 号码布,让操作员确认改的是哪一条。 */
|
||||
label: string
|
||||
/** 这条报名的主单位(entries.team_id):不能从份额里去掉。 */
|
||||
primaryTeamId: number
|
||||
primaryName: string
|
||||
original: ShareDraftRow[]
|
||||
draft: ShareDraftRow[]
|
||||
saving: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const shareEditor = ref<ShareEditor | null>(null)
|
||||
const pendingShareTeamId = ref('')
|
||||
|
||||
const shareIssues = computed(() => {
|
||||
const editor = shareEditor.value
|
||||
if (editor === null) return []
|
||||
return shareDraftIssues(editor.draft, editor.primaryTeamId, editor.primaryName)
|
||||
})
|
||||
const shareChanged = computed(() => {
|
||||
const editor = shareEditor.value
|
||||
return editor === null ? false : shareDraftDirty(editor.draft, editor.original)
|
||||
})
|
||||
const shareView = computed(() => {
|
||||
const editor = shareEditor.value
|
||||
return editor === null ? [] : shareDraftView(editor.draft)
|
||||
})
|
||||
/** 比例预览:`第一分公司 67% · 第二分公司 33%`,保存前就看得到分怎么拆。 */
|
||||
const shareRatioText = computed(() => shareView.value.map(shareText).join(' · '))
|
||||
/** 还能加进来的单位:不在成员里的都行(已停用的可以,假删除的不要)。 */
|
||||
const shareCandidates = computed(() => {
|
||||
const editor = shareEditor.value
|
||||
return editor === null ? [] : selectableShareTeams(data.teams, editor.draft)
|
||||
})
|
||||
const canSaveShares = computed(
|
||||
() =>
|
||||
canWrite.value &&
|
||||
shareEditor.value !== null &&
|
||||
!shareEditor.value.saving &&
|
||||
shareChanged.value &&
|
||||
shareIssues.value.length === 0,
|
||||
)
|
||||
|
||||
function openShareEditor(row: EditableRow): void {
|
||||
pendingShareTeamId.value = ''
|
||||
shareEditor.value = {
|
||||
entryId: row.entryId,
|
||||
label: `${athleteCellName(row)}${inlineBibLabel(row.bib) === '' ? '' : ` · 号码 ${inlineBibLabel(row.bib)}`}`,
|
||||
primaryTeamId: row.teamId,
|
||||
primaryName: row.teamName,
|
||||
original: shareDraftOf(row),
|
||||
draft: shareDraftOf(row),
|
||||
saving: false,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
function closeShareEditor(): void {
|
||||
shareEditor.value = null
|
||||
pendingShareTeamId.value = ''
|
||||
}
|
||||
|
||||
function addShareMember(): void {
|
||||
const editor = shareEditor.value
|
||||
if (editor === null) return
|
||||
const team = data.teams.find((item) => String(item.id) === pendingShareTeamId.value)
|
||||
if (team === undefined) return
|
||||
editor.draft = addShareDraftTeam(editor.draft, team)
|
||||
}
|
||||
|
||||
function dropShareMember(teamId: number): void {
|
||||
const editor = shareEditor.value
|
||||
if (editor === null || teamId === editor.primaryTeamId) return
|
||||
editor.draft = removeShareDraftTeam(editor.draft, teamId)
|
||||
}
|
||||
|
||||
function editShareWeight(teamId: number, event: InputEvent): void {
|
||||
const editor = shareEditor.value
|
||||
if (editor === null) return
|
||||
const input = event.target
|
||||
if (!(input instanceof HTMLInputElement)) return
|
||||
editor.draft = setShareDraftWeight(editor.draft, teamId, input.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存份额:整组替换一条报名的成员单位。
|
||||
*
|
||||
* 就地更新这一行的份额并刷新积分榜,**不整表重拉**:重拉会把表格里没保存的成绩编辑一并覆盖掉,
|
||||
* 而份额改动只影响分记在谁头上,不影响任何一行的成绩与名次(那些由本地预览算)。
|
||||
* 服务端的 400(单位重复 / 权重为负 / 缺主单位…)原样展示在弹窗里,不吞。
|
||||
*/
|
||||
async function saveShareEditor(): Promise<void> {
|
||||
const editor = shareEditor.value
|
||||
if (editor === null || !canSaveShares.value) return
|
||||
if (data.isMock) {
|
||||
const notice = sampleDataBlockedNotice('保存成员单位')
|
||||
toast.error(notice.title, notice.detail)
|
||||
return
|
||||
}
|
||||
editor.saving = true
|
||||
editor.error = null
|
||||
const payload = shareDraftPayload(editor.draft)
|
||||
try {
|
||||
const outcome = await submitEntryTeamShares(editor.entryId, payload)
|
||||
const shares = outcome.teamShares ?? payload
|
||||
const row = allRows.value.find((item) => item.entryId === editor.entryId)
|
||||
if (row !== undefined) {
|
||||
const fallbackName = row.teamName
|
||||
row.teamShares = normalizeTeamShares(shares, {
|
||||
teamId: row.teamId,
|
||||
weight: 1,
|
||||
teamCode: row.teamShares[0]?.teamCode ?? '',
|
||||
teamName: fallbackName,
|
||||
})
|
||||
}
|
||||
closeShareEditor()
|
||||
void data.refreshStandings()
|
||||
toast.success(
|
||||
'成员单位已保存',
|
||||
isJointTeam(row?.teamShares ?? []) ? '本组已重算,大屏榜单会跟着刷新' : '已改回单个单位',
|
||||
)
|
||||
} catch (caught) {
|
||||
editor.error = caught instanceof Error ? caught.message : '保存成员单位失败'
|
||||
} finally {
|
||||
editor.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 团体名次(按单位录入,只喂给团体 + 手动名次的通道) ────────────────────────
|
||||
|
||||
/** 把随行下发的视图按组别归位;服务端没带 categoryId 时收敛到 0 号键。 */
|
||||
@@ -2137,7 +2317,28 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
<span :class="row.heat === null ? 'heat-none' : 'heat-tag'">{{ heatLabel(row.heat) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="team-cell">{{ row.teamShort || row.teamName }}</span>
|
||||
<!--
|
||||
单位格兼作「成员单位与份额」的入口:点一下打开联队弹窗(默认就是当前这一个
|
||||
单位,什么都不改等于没动过)。普通报名的文案与改造前逐字一样,联队才多一行
|
||||
成员名 + 份额比例——两个单位各占多少分,在表格上就要看得见。
|
||||
-->
|
||||
<span
|
||||
v-if="!isJointTeam(row.teamShares)"
|
||||
class="team-cell is-editable"
|
||||
:title="`${row.teamName}|点击设置成员单位与份额(两个单位合并参赛时用)`"
|
||||
@click="openShareEditor(row)"
|
||||
>{{ row.teamShort || row.teamName }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="team-cell is-editable is-joint"
|
||||
:title="`${jointTeamLabel(row.teamShares)}|点击修改成员单位与份额`"
|
||||
@click="openShareEditor(row)"
|
||||
>
|
||||
<span class="mx-badge mx-badge-info">联队</span>
|
||||
<span v-for="member in row.teamShares" :key="member.teamId" class="joint-member">
|
||||
{{ shareText(member) }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<!-- 号码布紧跟姓名重复一次:左边那列还在(有人习惯了),这一个是给眼睛省路的。 -->
|
||||
<td>{{ athleteCellName(row) }} <span v-if="inlineBibLabel(row.bib)" class="bib-inline mono">{{ inlineBibLabel(row.bib) }}</span></td>
|
||||
@@ -2454,6 +2655,114 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
成员单位与份额(联队,devdocs/19):外壳照页面里既有的弹窗写法自己套一层。
|
||||
默认摆出来的就是这条报名当前的份额——单单位报名打开后只有一行主单位、权重留空,
|
||||
不点「添加」「保存」的话一个请求都不会发出去,常规报名流程不受影响。
|
||||
-->
|
||||
<div v-if="shareEditor !== null" class="mx-modal-overlay open" @click.self="closeShareEditor">
|
||||
<div class="mx-modal" role="dialog" aria-modal="true">
|
||||
<div class="mx-modal-head">
|
||||
<span class="mx-modal-title">成员单位与份额</span>
|
||||
<button class="mx-modal-close" type="button" @click="closeShareEditor">
|
||||
<MxIcon name="close" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mx-modal-body">
|
||||
<p class="share-hint">
|
||||
一条报名可以挂<strong>多个单位</strong>(两个单位因人数不足合并参赛时用):这条成绩的积分按份额
|
||||
拆给各成员单位,奖牌<strong>各记一枚</strong>;分数平均拆、接受小数(团体第 1 名 15 分 → 两个单位各 7.5 分)。
|
||||
份额是<strong>相对权重</strong>:都留空就是均分(各 50%),填 2 与 1 就是 67% 与 33%。
|
||||
</p>
|
||||
<p class="bd-note">
|
||||
正在修改:{{ shareEditor.label }};主单位「{{ shareEditor.primaryName }}」不能去掉。
|
||||
</p>
|
||||
<!-- 访客 / 断网只读:改不了就说明白原因,别让人对着灰按钮猜。 -->
|
||||
<div v-if="!canWrite" class="share-readonly">
|
||||
<MxIcon name="lock" :size="14" />
|
||||
<span>{{ auth.readOnly ? '当前为访客只读视图,成员单位只能查看。' : '服务端未连接,暂时不能修改成员单位。' }}</span>
|
||||
</div>
|
||||
<ul class="share-list">
|
||||
<li v-for="member in shareEditor.draft" :key="member.teamId" class="share-item">
|
||||
<span class="share-code mono">{{ member.teamCode || '—' }}</span>
|
||||
<span class="share-name">{{ member.teamName }}</span>
|
||||
<span v-if="member.teamId === shareEditor.primaryTeamId" class="mx-badge mx-badge-neutral">主单位</span>
|
||||
<input
|
||||
class="mx-cell-input share-weight mono"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.5"
|
||||
placeholder="留空=1"
|
||||
:value="member.weightText"
|
||||
:disabled="!canWrite || shareEditor.saving"
|
||||
title="份额是相对权重:留空或填 0 都按 1 算(均分)"
|
||||
@input="editShareWeight(member.teamId, $event)"
|
||||
/>
|
||||
<button
|
||||
class="mx-btn mx-btn-ghost mx-btn-sm"
|
||||
type="button"
|
||||
:disabled="member.teamId === shareEditor.primaryTeamId || shareEditor.saving"
|
||||
:title="
|
||||
member.teamId === shareEditor.primaryTeamId
|
||||
? '主单位不能去掉:报名挂在它名下,换主单位要重新建报名'
|
||||
: '把这个单位从这个联队里去掉'
|
||||
"
|
||||
@click="dropShareMember(member.teamId)"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="share-add">
|
||||
<select
|
||||
v-model="pendingShareTeamId"
|
||||
class="mx-cell-input bd-select"
|
||||
:disabled="!canWrite || shareEditor.saving || shareCandidates.length === 0"
|
||||
>
|
||||
<option value="">添加成员单位…</option>
|
||||
<option v-for="team in shareCandidates" :key="team.id" :value="String(team.id)">
|
||||
{{ team.name }}{{ team.active ? '' : '(已停用)' }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
class="mx-btn mx-btn-secondary mx-btn-sm"
|
||||
type="button"
|
||||
:disabled="pendingShareTeamId === '' || shareEditor.saving"
|
||||
@click="addShareMember"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<p class="share-ratio">
|
||||
当前比例:<strong>{{ shareRatioText }}</strong>
|
||||
<span v-if="shareView.length > 1" class="mx-badge mx-badge-info">{{ jointTeamLabel(shareView) }}</span>
|
||||
</p>
|
||||
<ul v-if="shareIssues.length > 0" class="issue-list">
|
||||
<li v-for="(message, index) in shareIssues" :key="index" class="error">
|
||||
<MxIcon name="warning" :size="14" />
|
||||
<span>{{ message }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="shareEditor.error !== null" class="mx-badge mx-badge-error share-error">
|
||||
{{ shareEditor.error }}
|
||||
</div>
|
||||
<p class="bd-note">
|
||||
保存后本组的名次与积分会当场重算,大屏榜单跟着刷新。没改动的话保存按钮是灰的,
|
||||
点「关闭」不会发出任何请求。
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-modal-foot">
|
||||
<button class="mx-btn mx-btn-secondary" type="button" @click="closeShareEditor">
|
||||
{{ shareChanged ? '放弃修改' : '关闭' }}
|
||||
</button>
|
||||
<button class="mx-btn mx-btn-primary" type="button" :disabled="!canSaveShares" @click="saveShareEditor">
|
||||
<MxIcon name="check" :size="14" />
|
||||
{{ shareEditor.saving ? '保存中…' : '保存成员单位' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
:open="unpublishDialog"
|
||||
tone="danger"
|
||||
@@ -2822,6 +3131,95 @@ watch([selectedEventId, selectedCategoryId], () => void refreshSettlement(), { i
|
||||
.team-cell {
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 单位格兼作「成员单位与份额」的入口:悬停给出手型与底色,点开的是同一个弹窗。
|
||||
文案本身不动——单单位报名的这一格与改造前逐字一样。 */
|
||||
.team-cell.is-editable {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
border-radius: var(--mx-r-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.team-cell.is-editable:hover {
|
||||
background: var(--mx-primary-glow);
|
||||
}
|
||||
/* 联队:单位名 + 各自份额逐条列出(名字写全,比例看得见),不再压成一行。 */
|
||||
.team-cell.is-joint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.joint-member {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: var(--mx-t2);
|
||||
}
|
||||
/* ── 成员单位与份额弹窗 ── */
|
||||
.share-hint {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--mx-t2);
|
||||
}
|
||||
.share-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.share-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--mx-r-sm);
|
||||
background: var(--mx-s3);
|
||||
}
|
||||
.share-code {
|
||||
min-width: 44px;
|
||||
font-size: 12px;
|
||||
color: var(--mx-t3);
|
||||
}
|
||||
.share-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.share-weight {
|
||||
width: 96px;
|
||||
text-align: center;
|
||||
}
|
||||
.share-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.share-ratio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--mx-t2);
|
||||
}
|
||||
.share-readonly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: var(--mx-amber);
|
||||
}
|
||||
.share-error {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
padding: 10px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/*
|
||||
* 行操作区:已发布的行要并排放「修改本条」+「作废本条」两个按钮,
|
||||
* 列宽按两者之和留(4 字 ×12px + 内边距 + 单元格 padding),否则按钮会换行把这一行撑高一倍。
|
||||
|
||||
Reference in new issue
Block a user