fix: 补 .gitattributes(CRLF for .bat) + README 端口 19540 + 提交 console 锁文件
This commit is contained in:
1 parent
c121111f51
commit
b80c64a115
39 files changed
+4217
-345
No files matched your search
@@ -8,48 +8,51 @@ import { useAuthStore } from '../stores/auth'
|
||||
import { useEventStore } from '../stores/event'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
|
||||
/** 大屏页面表里的规范值;写错会被大屏静默丢弃,所以用字面量联合锁死。 */
|
||||
type DisplayPage = 'standings' | 'team-medals' | 'athlete-standings' | 'event-results'
|
||||
|
||||
interface ScreenNode {
|
||||
key: string
|
||||
name: string
|
||||
/** null = 未知;后端没有实例上报接口,占位节点恒为未知。 */
|
||||
online: boolean | null
|
||||
lastSeen: string | null
|
||||
/** 本机最后一次下发指令的时间,不是大屏心跳。 */
|
||||
lastCommandAt: string | null
|
||||
}
|
||||
|
||||
const PAGES: Array<{ key: string; label: string }> = [
|
||||
const PAGES: Array<{ key: DisplayPage; label: string }> = [
|
||||
{ key: 'standings', label: '总积分榜' },
|
||||
{ key: 'medals', label: '团体奖牌榜' },
|
||||
{ key: 'athletes', label: '个人积分榜' },
|
||||
{ key: 'latest', label: '最新成绩' },
|
||||
{ key: 'team-medals', label: '团体奖牌榜' },
|
||||
{ key: 'athlete-standings', label: '个人积分榜' },
|
||||
{ key: 'event-results', label: '最新成绩' },
|
||||
]
|
||||
|
||||
const data = useEventStore()
|
||||
const auth = useAuthStore()
|
||||
const toast = useToastStore()
|
||||
|
||||
const currentPage = ref('standings')
|
||||
const currentPage = ref<DisplayPage>('standings')
|
||||
const locked = ref(false)
|
||||
const announceText = ref('')
|
||||
const announceSeconds = ref(15)
|
||||
const pending = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* 在线看板。
|
||||
* 在线看板(占位)。
|
||||
*
|
||||
* devdocs/05 §7 未定义「大屏在线列表」接口,管理端拿不到每块屏的心跳,
|
||||
* 因此这里只登记本地已知的屏,并用「最近一次命令是否送达」推断在线状态。
|
||||
* 后端补上接口后,把 screens 换成接口数据即可。
|
||||
* 所以这里只登记本地已知的屏,在线状态恒为「未知」——指令送达不等于大屏在线。
|
||||
* 后端补上实例上报接口后,把 screens 换成接口数据、hasInstanceData 才可能为真。
|
||||
*/
|
||||
const screens = ref<ScreenNode[]>([{ key: 'main', name: '主屏(本地登记)', online: null, lastSeen: null }])
|
||||
const screens = ref<ScreenNode[]>([{ key: 'main', name: '主屏(本地登记)', online: null, lastCommandAt: null }])
|
||||
|
||||
const canWrite = computed(() => !auth.readOnly && data.canWrite)
|
||||
/** 只有真实实例上报(online 非 null)才算有在线数据,占位节点一律不谎报在线。 */
|
||||
const hasInstanceData = computed(() => screens.value.some((screen) => screen.online !== null))
|
||||
const onlineCount = computed(() => screens.value.filter((screen) => screen.online === true).length)
|
||||
|
||||
function markSeen(): void {
|
||||
screens.value = screens.value.map((screen) => ({
|
||||
...screen,
|
||||
online: true,
|
||||
lastSeen: new Date().toISOString(),
|
||||
}))
|
||||
function recordDispatch(): void {
|
||||
screens.value = screens.value.map((screen) => ({ ...screen, lastCommandAt: new Date().toISOString() }))
|
||||
}
|
||||
|
||||
async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'): Promise<void> {
|
||||
@@ -57,9 +60,19 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'):
|
||||
toast.error('当前不可下发指令', auth.readOnly ? '请先登录' : '服务端未连接')
|
||||
return
|
||||
}
|
||||
if (data.isMock) {
|
||||
// 示例数据模式下指令不会真正下发,不能提示「已切换」。
|
||||
toast.warning('示例数据模式未真正下发', '后端未就绪,指令不会到达大屏')
|
||||
return
|
||||
}
|
||||
pending.value = action
|
||||
try {
|
||||
if (action === 'goto') {
|
||||
// 页面值不在大屏别名表内会被静默丢弃,这里先拦下并明确报错,避免谎报成功。
|
||||
if (!PAGES.some((page) => page.key === currentPage.value)) {
|
||||
toast.error('大屏不认识该页面', `页面值「${currentPage.value}」不在大屏页面表内`)
|
||||
return
|
||||
}
|
||||
await adminApi.displayCommand({ action, payload: { page: currentPage.value } })
|
||||
toast.success(`已切换大屏到「${PAGES.find((page) => page.key === currentPage.value)?.label ?? currentPage.value}」`)
|
||||
} else if (action === 'lock' || action === 'unlock') {
|
||||
@@ -79,7 +92,7 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'):
|
||||
announceText.value = ''
|
||||
toast.success('公告已推送')
|
||||
}
|
||||
markSeen()
|
||||
recordDispatch()
|
||||
} catch (caught) {
|
||||
toast.error('指令下发失败', caught instanceof Error ? caught.message : null)
|
||||
} finally {
|
||||
@@ -195,14 +208,14 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'):
|
||||
<section class="mx-card">
|
||||
<div class="mx-card-head">
|
||||
<span class="mx-card-title">在线看板</span>
|
||||
<span class="mx-badge" :class="onlineCount > 0 ? 'mx-badge-success' : 'mx-badge-neutral'">
|
||||
{{ onlineCount }} / {{ screens.length }} 在线
|
||||
<span class="mx-badge" :class="hasInstanceData ? (onlineCount > 0 ? 'mx-badge-success' : 'mx-badge-error') : 'mx-badge-neutral'">
|
||||
{{ hasInstanceData ? `${onlineCount} / ${screens.length} 在线` : '在线状态未知(占位)' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mx-card-body">
|
||||
<div class="board-note">
|
||||
<MxIcon name="warning" :size="14" />
|
||||
<span>后端未提供大屏在线接口(devdocs/05 §7 未定义),下表为本地登记节点,在线状态按最近一次指令送达推断。</span>
|
||||
<span>后端未提供大屏实例上报接口(devdocs/05 §7 未定义),下表是本地占位节点:「在线」无法判定,也不按指令送达推断;「最近下发」只是本机下发时间,不代表大屏在线。</span>
|
||||
</div>
|
||||
<div class="mx-table-wrap">
|
||||
<table class="mx-table mx-table-compact">
|
||||
@@ -210,7 +223,7 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'):
|
||||
<tr>
|
||||
<th>大屏</th>
|
||||
<th>状态</th>
|
||||
<th>最后心跳</th>
|
||||
<th>最近下发</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -224,14 +237,14 @@ async function send(action: 'goto' | 'lock' | 'unlock' | 'reload' | 'announce'):
|
||||
{{ screen.online === true ? '在线' : screen.online === false ? '掉线' : '未知' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="mono muted">{{ screen.lastSeen === null ? '—' : formatDateTime(screen.lastSeen) }}</td>
|
||||
<td class="mono muted">{{ screen.lastCommandAt === null ? '—' : formatDateTime(screen.lastCommandAt) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="board-hint">
|
||||
<p>当前状态:{{ locked ? '已锁定' : '自动轮播' }} · 页面 {{ currentPage }}</p>
|
||||
<p>当前状态:{{ locked ? '已锁定' : '自动轮播' }} · 页面 {{ PAGES.find((page) => page.key === currentPage)?.label ?? currentPage }}</p>
|
||||
<p v-if="data.isMock" class="mock">示例数据模式下指令不会真正下发到大屏。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user