feat: M3-M7 全模块落地 + 集成接线
- M3 认证/权限/审计/限流 + 全部 REST 接口 - M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制) - M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配) - M6 WebSocket 实时同步中心 - M7 xlsx 导出 + 赛事包 zip + 备份 - 规则包 YAML 导入导出(差异预览) - 字体 vendor 子集化(4 套 x2 字重) - 集成:realtime/export/rules 接入 main+httpapi;backup 子命令 - 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
This commit is contained in:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import MxIcon from './MxIcon.vue'
|
||||
|
||||
/**
|
||||
* 危险操作二次确认(07 第 7 节)。
|
||||
* 只负责「确认」这一件事,loading 态由调用方通过 `pending` 传入,避免弹窗自己管请求。
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
title: string
|
||||
message: string
|
||||
details?: string[]
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
tone?: 'info' | 'warning' | 'danger'
|
||||
pending?: boolean
|
||||
}>(),
|
||||
{
|
||||
details: () => [],
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
tone: 'warning',
|
||||
pending: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ confirm: []; cancel: [] }>()
|
||||
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (!props.open) return
|
||||
if (event.key === 'Escape') emit('cancel')
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', onKeydown))
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-modal-overlay" :class="{ open }" @click.self="emit('cancel')">
|
||||
<div class="mx-modal" role="dialog" aria-modal="true">
|
||||
<div class="mx-modal-head">
|
||||
<span class="mx-modal-title">{{ title }}</span>
|
||||
<button class="mx-modal-close" type="button" @click="emit('cancel')">
|
||||
<MxIcon name="close" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mx-modal-body">
|
||||
<div class="mx-modal-confirm-icon" :class="tone">
|
||||
<MxIcon :name="tone === 'danger' || tone === 'warning' ? 'warning' : 'check'" :size="24" />
|
||||
</div>
|
||||
<p class="mx-modal-confirm-text">{{ message }}</p>
|
||||
<ul v-if="details.length > 0" class="confirm-details">
|
||||
<li v-for="(line, index) in details" :key="index">{{ line }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mx-modal-foot">
|
||||
<button class="mx-btn mx-btn-secondary" type="button" :disabled="pending" @click="emit('cancel')">
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
class="mx-btn"
|
||||
:class="tone === 'danger' ? 'mx-btn-danger' : 'mx-btn-primary'"
|
||||
type="button"
|
||||
:disabled="pending"
|
||||
@click="emit('confirm')"
|
||||
>
|
||||
{{ pending ? '处理中…' : confirmText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.confirm-details {
|
||||
margin-top: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--mx-r-sm);
|
||||
background: var(--mx-s3);
|
||||
font-size: 13px;
|
||||
color: var(--mx-t2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.confirm-details li {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.confirm-details li::before {
|
||||
content: '·';
|
||||
color: var(--mx-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ICON_PATHS, type IconName } from './icons'
|
||||
|
||||
const props = withDefaults(defineProps<{ name: IconName; size?: number }>(), { size: 18 })
|
||||
const d = computed(() => ICON_PATHS[props.name])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path :d="d" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ title: string; subtitle?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="mx-page-head page-head">
|
||||
<div>
|
||||
<h1 class="mx-page-title">{{ title }}</h1>
|
||||
<p v-if="subtitle" class="mx-page-sub">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="page-head-actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { useToastStore, type ToastKind } from '../stores/toast'
|
||||
import MxIcon from './MxIcon.vue'
|
||||
import type { IconName } from './icons'
|
||||
|
||||
const toast = useToastStore()
|
||||
|
||||
const ICONS: Record<ToastKind, IconName> = {
|
||||
success: 'check',
|
||||
warning: 'warning',
|
||||
error: 'warning',
|
||||
info: 'bell',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-toast-container">
|
||||
<div
|
||||
v-for="item in toast.items"
|
||||
:key="item.id"
|
||||
class="mx-toast show"
|
||||
:class="`mx-toast-${item.kind}`"
|
||||
role="status"
|
||||
>
|
||||
<span class="mx-toast-icon"><MxIcon :name="ICONS[item.kind]" :size="18" /></span>
|
||||
<span class="mx-toast-text">
|
||||
{{ item.message }}
|
||||
<small v-if="item.detail">{{ item.detail }}</small>
|
||||
</span>
|
||||
<button class="mx-toast-close" type="button" @click="toast.dismiss(item.id)">
|
||||
<MxIcon name="close" :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mx-toast {
|
||||
position: relative;
|
||||
}
|
||||
.mx-toast-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.mx-toast-text small {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: var(--mx-t3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
/** 管理端用到的内联图标路径。现场无外网,不引图标库。 */
|
||||
export const ICON_PATHS = {
|
||||
dashboard: 'M4 13h6V4H4v9Zm0 7h6v-5H4v5Zm10 0h6V11h-6v9Zm0-16v5h6V4h-6Z',
|
||||
entry: 'M4 5h16M4 12h10M4 19h7M17 15l3 3-3 3',
|
||||
review: 'M5 4h14v16H5zM8 9h8M8 13h8M8 17h4',
|
||||
record: 'M8 21h8M12 17v4M6 4h12v6a6 6 0 0 1-12 0V4Z',
|
||||
adjust: 'M4 8h10M18 8h2M4 16h2M10 16h10M15 5v6M9 13v6',
|
||||
standings: 'M4 20V10h4v10H4Zm6 0V4h4v16h-4Zm6 0v-7h4v7h-4Z',
|
||||
display: 'M3 4h18v12H3zM8 20h8M12 16v4',
|
||||
database:
|
||||
'M12 3c4.4 0 8 1.3 8 3s-3.6 3-8 3-8-1.3-8-3 3.6-3 8-3Zm8 3v12c0 1.7-3.6 3-8 3s-8-1.3-8-3V6M20 12c0 1.7-3.6 3-8 3s-8-1.3-8-3',
|
||||
export: 'M12 3v12M8 11l4 4 4-4M4 19h16',
|
||||
history: 'M4 12a8 8 0 1 0 3-6.2M4 4v4h4M12 8v4l3 2',
|
||||
logs: 'M5 4h14v16H5zM8 8h8M8 12h8M8 16h5',
|
||||
system:
|
||||
'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6Zm8.4 3-1.8-1 .3-2.1-2-1.2-1.7 1.3-1.8-1 .2-2.1-2.3-.7-1 1.9-2.1-.2-1.2 2 1.3 1.7-1 1.8-2.1.2-.7 2.3 1.9 1-.2 2.1 2 1.2 1.7-1.3 1.8 1-.2 2.1 2.3.7 1-1.9 2.1.2 1.2-2-1.3-1.7 1-1.8 2.1-.2.7-2.3Z',
|
||||
refresh: 'M20 12a8 8 0 1 1-2.3-5.6M20 4v4h-4',
|
||||
lock: 'M6 11h12v9H6zM9 11V8a3 3 0 0 1 6 0v3',
|
||||
unlock: 'M6 11h12v9H6zM9 11V8a3 3 0 0 1 5.7-1.4',
|
||||
bell: 'M6 9a6 6 0 1 1 12 0c0 5 2 6 2 6H4s2-1 2-6ZM10 19a2 2 0 0 0 4 0',
|
||||
push: 'M12 20V6M8 10l4-4 4 4M4 20h16',
|
||||
close: 'M6 6l12 12M18 6 6 18',
|
||||
check: 'M5 12.5 10 17l9-10',
|
||||
warning: 'M12 4l9 16H3l9-16Zm0 6v4m0 3v.5',
|
||||
search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm10 2-4.3-4.3',
|
||||
chevron: 'M8 10l4 4 4-4',
|
||||
logout: 'M15 4h4v16h-4M11 8l-4 4 4 4M7 12h9',
|
||||
user: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-8 8a8 8 0 0 1 16 0',
|
||||
keyboard: 'M3 6h18v12H3zM7 10h.01M11 10h.01M15 10h.01M7 14h10',
|
||||
} as const
|
||||
|
||||
export type IconName = keyof typeof ICON_PATHS
|
||||
Reference in new issue
Block a user