master
lion 1 month ago
parent ff0f239ac9
commit b614fd09a2

@ -0,0 +1,16 @@
/** 后台封面、轮播图、预约二维码图等:单张图片大小上限(与 UI 文案一致) */
export const ADMIN_UPLOAD_IMAGE_MAX_BYTES = 1024 * 1024
/** 与上传校验配套,跟在「尺寸推荐」后 */
export const ADMIN_IMAGE_RECOMMEND_LABEL =
'图片尺寸推荐 1200×600图片大小不超过 1MB'
/** @returns 可提示用户的中文错误文案;未超限或非图片返回 null */
export function adminUploadImageTooLargeMessage(file: File): string | null {
if (!file.type.startsWith('image/')) return null
if (file.size <= ADMIN_UPLOAD_IMAGE_MAX_BYTES) return null
const mb = file.size / (1024 * 1024)
const cur =
mb >= 1 ? `${mb.toFixed(2)}MB` : `${Math.ceil(file.size / 1024)}KB`
return `单张图片不能超过 1MB当前约 ${cur}`
}

File diff suppressed because it is too large Load Diff

@ -166,6 +166,20 @@ function onSearch() {
loadRows()
}
/** Arco Table 行级 row-class 无效;已取消 / 已过期 用 cell class + inline 背景兜底 */
function registrationInactiveBodyCellClass(record: unknown) {
const st = String((record as { status?: string })?.status ?? '').trim()
return st === 'cancelled' || st === 'expired' ? 'reg-row-inactive-cell' : undefined
}
function registrationInactiveBodyCellStyle(record: unknown) {
const st = String((record as { status?: string })?.status ?? '').trim()
if (st === 'cancelled' || st === 'expired') {
return { backgroundColor: 'var(--color-fill-2)' }
}
return {}
}
async function exportCsv() {
try {
if (exportFields.value.length === 0) {
@ -334,32 +348,42 @@ function onPageChange(p: number) {
@page-change="onPageChange"
>
<template #columns>
<a-table-column title="" :width="50" :ellipsis="true" :tooltip="true">
<a-table-column title="" :width="50" :ellipsis="true" :tooltip="true" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ rowIndex }">{{
listTableRowIndex(rowIndex, pagination.current, pagination.pageSize)
}}</template>
</a-table-column>
<a-table-column title="活动" :width="280" :min-width="220">
<a-table-column title="活动" :width="280" :min-width="220" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ record.activity?.title || '-' }}</template>
</a-table-column>
<a-table-column title="场馆" :width="220" :min-width="180">
<a-table-column title="场馆" :width="220" :min-width="180" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ record.venue?.name || '-' }}</template>
</a-table-column>
<a-table-column title="报名人" data-index="visitor_name" :width="140" :min-width="120" />
<a-table-column title="手机号" data-index="visitor_phone" :width="150" :min-width="130" />
<a-table-column title="预约类型" :width="100">
<a-table-column title="报名人" data-index="visitor_name" :width="140" :min-width="120" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle" />
<a-table-column title="手机号" data-index="visitor_phone" :width="150" :min-width="130" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle" />
<a-table-column title="预约类型" :width="100" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ bookingTypeLabel(record.booking_type, record.ticket_count) }}</template>
</a-table-column>
<a-table-column title="预约票数" :width="110">
<a-table-column title="预约票数" :width="110" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ record.ticket_count ?? 1 }}</template>
</a-table-column>
<a-table-column title="场次名称" :width="160" :min-width="120" :ellipsis="true" :tooltip="true">
<a-table-column title="场次名称" :width="160" :min-width="120" :ellipsis="true" :tooltip="true" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ (record.activity_day?.session_name || '').trim() || '-' }}</template>
</a-table-column>
<a-table-column title="活动时间" :width="220" :min-width="180" :ellipsis="true" :tooltip="true">
<a-table-column title="活动时间" :width="220" :min-width="180" :ellipsis="true" :tooltip="true" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ formatActivitySessionTime(record.activity_day) }}</template>
</a-table-column>
<a-table-column title="状态" :width="120">
<a-table-column title="状态" :width="120" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">
<a-tag
:color="
@ -376,10 +400,12 @@ function onPageChange(p: number) {
</a-tag>
</template>
</a-table-column>
<a-table-column title="下单时间" :width="190" :min-width="175">
<a-table-column title="下单时间" :width="190" :min-width="175" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ formatDateTimeZh(record.created_at) }}</template>
</a-table-column>
<a-table-column title="核销时间" :width="190" :min-width="175">
<a-table-column title="核销时间" :width="190" :min-width="175" :body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ formatDateTimeZh(record.verified_at) }}</template>
</a-table-column>
<a-table-column
@ -391,6 +417,8 @@ function onPageChange(p: number) {
:tooltip="true"
fixed="right"
align="left"
:body-cell-class="registrationInactiveBodyCellClass"
:body-cell-style="registrationInactiveBodyCellStyle"
/>
</template>
</a-table>
@ -464,5 +492,13 @@ function onPageChange(p: number) {
.registrations-table :deep(.arco-table-text-ellipsis) {
white-space: nowrap;
}
.registrations-table :deep(.arco-table-td.reg-row-inactive-cell) {
background-color: var(--color-fill-2) !important;
}
.registrations-table :deep(.arco-table-tr:hover .arco-table-td.reg-row-inactive-cell) {
background-color: var(--color-fill-3) !important;
}
</style>

@ -70,7 +70,9 @@ onMounted(() => {
/>
<a-button type="primary" long size="large" :loading="loadingPin" @click="loginSixPin"> </a-button>
<p class="m-verify-tip">登录状态将保持较长时间失效后会自动退回本页</p>
<p class="m-verify-tip">
未点击核销页上的退出前会一直保持登录退出后下次再打开本链接需重新输入 6 位口令若口令登录态过期或失效也会自动回到本页
</p>
</div>
</div>
</template>

@ -119,6 +119,7 @@ function bookerVerifyLine(r: ReservationRow): string {
const todayList = ref<ReservationRow[]>([])
const listLoading = ref(false)
const headerRefreshing = ref(false)
/** 今日活动日汇总(按预约订单数);与列表同接口权限 */
const todaySummary = ref<TodaySummary | null>(null)
@ -510,6 +511,21 @@ function startScanLoop() {
}, 400)
}
async function refreshVerifyData() {
if (!localStorage.getItem(H5_TOKEN_KEY)) {
router.replace(loginPath.value)
return
}
headerRefreshing.value = true
try {
await loadMe()
await loadTodayList()
Message.success('已刷新')
} finally {
headerRefreshing.value = false
}
}
async function logout() {
closeCamera()
try {
@ -549,7 +565,10 @@ onBeforeUnmount(() => {
<div class="m-scan-sub">扫码核对信息后核销</div>
<div v-if="venueHeadline" class="m-scan-venue">{{ venueHeadline }}</div>
</div>
<a-button size="small" @click="logout">退</a-button>
<div class="m-scan-head-actions">
<a-button size="small" :loading="headerRefreshing" @click="refreshVerifyData"></a-button>
<a-button size="small" @click="logout">退</a-button>
</div>
</header>
<section class="m-scan-stats" aria-label="">
@ -735,6 +754,17 @@ onBeforeUnmount(() => {
gap: 12px;
margin-bottom: 28px;
}
.m-scan-head-actions {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
flex-shrink: 0;
min-width: 72px;
}
.m-scan-head-actions :deep(.arco-btn) {
padding: 0 12px;
}
.m-scan-brand {
font-size: 22px;
font-weight: 700;

@ -5,6 +5,7 @@ import { http } from '../../api/http'
import RichEditorField from '../../components/RichEditorField.vue'
import { listTableRowIndex } from '../../utils/listTableRowIndex'
import { resolvePublicMediaUrl } from '../../utils/mediaUrl'
import { adminUploadImageTooLargeMessage, ADMIN_IMAGE_RECOMMEND_LABEL } from '../../utils/adminMediaLimits'
const STUDY_TOUR_LIST_SCROLL_X = 900
@ -123,6 +124,11 @@ function quillImageHandler(this: { quill: any }) {
input.onchange = async () => {
const file = input.files?.[0]
if (!file) return
const sizeMsg = adminUploadImageTooLargeMessage(file)
if (sizeMsg) {
Message.warning(sizeMsg)
return
}
try {
const url = await uploadEditorFile(file)
const range = quill.getSelection(true)
@ -198,6 +204,11 @@ async function onCoverSelect(fileItem: any) {
Message.warning('未识别到上传文件')
return false
}
const sizeMsg = adminUploadImageTooLargeMessage(file)
if (sizeMsg) {
Message.warning(sizeMsg)
return false
}
form.cover_image = await uploadFile(file)
Message.success('封面上传成功')
} catch (error: any) {
@ -587,7 +598,7 @@ onMounted(async () => {
<a-upload :auto-upload="false" :show-file-list="false" accept="image/*" :before-upload="onCoverSelect" @change="onCoverChange">
<template #upload-button><a-button>上传封面</a-button></template>
</a-upload>
<a-typography-text type="secondary">图片尺寸推荐 1200×600</a-typography-text>
<a-typography-text type="secondary">{{ ADMIN_IMAGE_RECOMMEND_LABEL }}</a-typography-text>
<div v-if="form.cover_image" style="display: flex; flex-direction: column; align-items: flex-start; gap: 8px">
<img
:src="resolvePublicMediaUrl(form.cover_image)"

@ -113,6 +113,19 @@ function onPageSizeChange(s: number) {
void loadRows()
}
function tgRegistrationInactiveBodyCellClass(record: unknown) {
const st = String((record as { status?: string })?.status ?? '').trim()
return st === 'cancelled' || st === 'expired' ? 'reg-row-inactive-cell' : undefined
}
function tgRegistrationInactiveBodyCellStyle(record: unknown) {
const st = String((record as { status?: string })?.status ?? '').trim()
if (st === 'cancelled' || st === 'expired') {
return { backgroundColor: 'var(--color-fill-2)' }
}
return {}
}
watch(filterVenueId, () => {
pagination.current = 1
void loadRows()
@ -178,6 +191,7 @@ onMounted(async () => {
</a-space>
<a-table
class="tg-registrations-table"
:scroll="{ x: TG_REGISTRATIONS_LIST_SCROLL_X }"
:data="rows"
:loading="loading"
@ -192,31 +206,40 @@ onMounted(async () => {
}"
>
<template #columns>
<a-table-column title="" :width="50" :ellipsis="true" :tooltip="true">
<a-table-column title="" :width="50" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ rowIndex }">{{
listTableRowIndex(rowIndex, pagination.current, pagination.pageSize)
}}</template>
</a-table-column>
<a-table-column title="抢票活动" :width="200" :ellipsis="true" :tooltip="true">
<a-table-column title="抢票活动" :width="200" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ record.ticket_grab_event?.title ?? '-' }}</template>
</a-table-column>
<a-table-column title="场馆" :width="160" :ellipsis="true" :tooltip="true">
<a-table-column title="场馆" :width="160" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ record.venue?.name ?? '-' }}</template>
</a-table-column>
<a-table-column title="姓名" data-index="visitor_name" :width="100" />
<a-table-column title="身份证" data-index="id_card" :width="180" :ellipsis="true" :tooltip="true" />
<a-table-column title="入馆日" :width="120">
<a-table-column title="姓名" data-index="visitor_name" :width="100" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle" />
<a-table-column title="身份证" data-index="id_card" :width="180" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle" />
<a-table-column title="入馆日" :width="120" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{
record.entry_date ? formatDateZh(String(record.entry_date)) : '-'
}}</template>
</a-table-column>
<a-table-column title="预约类型" :width="100">
<a-table-column title="预约类型" :width="100" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ bookingTypeLabel(record.booking_type, record.ticket_count) }}</template>
</a-table-column>
<a-table-column title="票数" :width="80">
<a-table-column title="票数" :width="80" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ record.ticket_count ?? 1 }}</template>
</a-table-column>
<a-table-column title="状态" :width="100">
<a-table-column title="状态" :width="100" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">
<a-tag
:color="
@ -230,15 +253,18 @@ onMounted(async () => {
>
</template>
</a-table-column>
<a-table-column title="下单时间" :width="170">
<a-table-column title="下单时间" :width="170" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{ formatDateTimeZh(record.created_at) }}</template>
</a-table-column>
<a-table-column title="核销时间" :width="170">
<a-table-column title="核销时间" :width="170" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">{{
record.verified_at ? formatDateTimeZh(String(record.verified_at)) : '-'
}}</template>
</a-table-column>
<a-table-column title="核销 Token" :width="220" :ellipsis="true" :tooltip="true">
<a-table-column title="核销 Token" :width="220" :ellipsis="true" :tooltip="true" :body-cell-class="tgRegistrationInactiveBodyCellClass"
:body-cell-style="tgRegistrationInactiveBodyCellStyle">
<template #cell="{ record }">
<span style="font-family: monospace; font-size: 12px">{{ record.qr_token }}</span>
</template>
@ -248,3 +274,13 @@ onMounted(async () => {
</a-space>
</a-card>
</template>
<style scoped>
.tg-registrations-table :deep(.arco-table-td.reg-row-inactive-cell) {
background-color: var(--color-fill-2) !important;
}
.tg-registrations-table :deep(.arco-table-tr:hover .arco-table-td.reg-row-inactive-cell) {
background-color: var(--color-fill-3) !important;
}
</style>

@ -3,6 +3,7 @@ import { nextTick, onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { http } from '../../api/http'
import RichEditorField from '../../components/RichEditorField.vue'
import { adminUploadImageTooLargeMessage, ADMIN_IMAGE_RECOMMEND_LABEL } from '../../utils/adminMediaLimits'
import { listTableRowIndex } from '../../utils/listTableRowIndex'
import { resolvePublicMediaUrl } from '../../utils/mediaUrl'
@ -148,6 +149,11 @@ function quillImageHandler(this: { quill: any }) {
input.onchange = async () => {
const file = input.files?.[0]
if (!file) return
const sizeMsg = adminUploadImageTooLargeMessage(file)
if (sizeMsg) {
Message.warning(sizeMsg)
return
}
try {
const url = await uploadFile(file)
const range = quill.getSelection(true)
@ -566,6 +572,11 @@ async function onCoverSelect(fileItem: any) {
Message.warning('未识别到上传文件')
return false
}
const sizeMsg = adminUploadImageTooLargeMessage(file)
if (sizeMsg) {
Message.warning(sizeMsg)
return false
}
form.cover_image = await uploadFile(file)
Message.success('封面上传成功')
} catch (error: any) {
@ -581,6 +592,11 @@ async function onGallerySelect(fileItem: any) {
Message.warning('未识别到上传文件')
return false
}
const sizeMsg = adminUploadImageTooLargeMessage(file)
if (sizeMsg) {
Message.warning(sizeMsg)
return false
}
const url = await uploadFile(file)
if (!url) {
Message.error('上传成功但未返回可用地址')
@ -617,6 +633,11 @@ async function onBookingQrSelect(file: File) {
Message.warning('仅支持图片')
return false
}
const sizeMsg = adminUploadImageTooLargeMessage(file)
if (sizeMsg) {
Message.warning(sizeMsg)
return false
}
const url = await uploadFile(file)
if (!url) {
Message.error('上传成功但未返回可用地址')
@ -1191,7 +1212,9 @@ onMounted(async () => {
>
<template #upload-button><a-button type="primary" size="small">上传图片</a-button></template>
</a-upload>
<a-typography-text type="secondary" style="margin-top: 8px; display: block; font-size: 12px">推荐 1200×600</a-typography-text>
<a-typography-text type="secondary" style="margin-top: 8px; display: block; font-size: 12px">{{
ADMIN_IMAGE_RECOMMEND_LABEL
}}</a-typography-text>
<div v-if="form.booking_qr_media.length" class="venue-gallery-grid" style="margin-top: 8px">
<div v-for="(m, i) in form.booking_qr_media" :key="`booking-qr-${i}`" class="venue-gallery-item">
<img
@ -1251,7 +1274,7 @@ onMounted(async () => {
<a-upload :auto-upload="false" :show-file-list="false" accept="image/*" :before-upload="onCoverSelect" @change="onCoverChange">
<template #upload-button><a-button>上传封面</a-button></template>
</a-upload>
<a-typography-text type="secondary">图片尺寸推荐 1200×600</a-typography-text>
<a-typography-text type="secondary">{{ ADMIN_IMAGE_RECOMMEND_LABEL }}</a-typography-text>
<a-space v-if="form.cover_image" direction="vertical" align="start">
<img
:src="resolvePublicMediaUrl(form.cover_image)"
@ -1278,7 +1301,9 @@ onMounted(async () => {
>
<template #upload-button><a-button type="primary">新增轮播资源</a-button></template>
</a-upload>
<a-typography-text type="secondary" style="margin-top: 12px; display: block">图片尺寸推荐 1200×600</a-typography-text>
<a-typography-text type="secondary" style="margin-top: 12px; display: block">{{
ADMIN_IMAGE_RECOMMEND_LABEL
}}</a-typography-text>
</div>
<div class="venue-gallery-grid">
<div

Loading…
Cancel
Save