|
|
|
|
@ -1,10 +1,9 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
|
|
|
import { Message, Modal } from '@arco-design/web-vue'
|
|
|
|
|
import { IconEye, IconEyeInvisible } from '@arco-design/web-vue/es/icon'
|
|
|
|
|
import RichEditorField from '../../components/RichEditorField.vue'
|
|
|
|
|
import { http } from '../../api/http'
|
|
|
|
|
import { buildVerifyPortalPublicUrl } from '../../api/h5Http'
|
|
|
|
|
import { buildUnifiedActivityVerifyLoginUrl } from '../../api/h5Http'
|
|
|
|
|
import { listTableRowIndex } from '../../utils/listTableRowIndex'
|
|
|
|
|
import { resolvePublicMediaUrl } from '../../utils/mediaUrl'
|
|
|
|
|
|
|
|
|
|
@ -104,7 +103,6 @@ const venueDetailRow = computed(() => {
|
|
|
|
|
return form.venues.find((r) => r._key === k) ?? null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const releaseVisible = ref(false)
|
|
|
|
|
const releaseLoading = ref(false)
|
|
|
|
|
const releaseSaving = ref(false)
|
|
|
|
|
const releaseEventId = ref<number | null>(null)
|
|
|
|
|
@ -126,39 +124,21 @@ const releasePayload = ref<{
|
|
|
|
|
const releaseEdits = ref<Record<number, Record<string, number>>>({})
|
|
|
|
|
const tagInput = ref('')
|
|
|
|
|
|
|
|
|
|
type TgVerifyCredRow = {
|
|
|
|
|
id: number
|
|
|
|
|
/** 详情弹窗(活动信息 + 放票 + 核销) */
|
|
|
|
|
const tgDetailHubVisible = ref(false)
|
|
|
|
|
const tgHubActiveKey = ref<'1' | '2' | '3'>('1')
|
|
|
|
|
const tgHubTitle = ref('')
|
|
|
|
|
const tgHubSummary = ref<Record<string, unknown> | null>(null)
|
|
|
|
|
const tgHubSummaryLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
type TgVenueVerifyPinRow = {
|
|
|
|
|
venue_id: number
|
|
|
|
|
venue_name?: string
|
|
|
|
|
username: string
|
|
|
|
|
password_plain?: string | null
|
|
|
|
|
note?: string | null
|
|
|
|
|
created_at?: string
|
|
|
|
|
}
|
|
|
|
|
const tgVerifyVisible = ref(false)
|
|
|
|
|
const tgVerifyLoading = ref(false)
|
|
|
|
|
const tgVerifyEventId = ref<number | null>(null)
|
|
|
|
|
const tgVerifyVenues = ref<{ id: number; name: string }[]>([])
|
|
|
|
|
const tgVerifyUrl = ref('')
|
|
|
|
|
const tgVerifyCreds = ref<TgVerifyCredRow[]>([])
|
|
|
|
|
const tgVerifyCredForm = reactive({ venue_id: undefined as number | undefined, username: '', password: '', note: '' })
|
|
|
|
|
const tgVerifyCredSaving = ref(false)
|
|
|
|
|
/** 核销登录密码(与后端 Password 规则一致) */
|
|
|
|
|
const TG_VERIFY_PASSWORD_RULE_HINT = '最少 8 位,须含大写、小写字母与特殊字符'
|
|
|
|
|
|
|
|
|
|
function validateTgVerifyPasswordStrength(pw: string): string | null {
|
|
|
|
|
if (pw.length > 72) return '密码最长 72 位'
|
|
|
|
|
if (pw.length < 8) {
|
|
|
|
|
return '密码最少8位,且含大写、小写字母与特殊字符'
|
|
|
|
|
}
|
|
|
|
|
if (!/[a-z]/.test(pw)) return '密码须包含小写字母'
|
|
|
|
|
if (!/[A-Z]/.test(pw)) return '密码须包含大写字母'
|
|
|
|
|
if (!/[\p{S}\p{P}\p{Z}]/u.test(pw)) return '密码须包含特殊字符(标点、符号等)'
|
|
|
|
|
return null
|
|
|
|
|
verify_portal_pin?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 核销账号表格:是否明文显示密码(按行 id) */
|
|
|
|
|
const tgPasswordReveal = ref<Record<number, boolean>>({})
|
|
|
|
|
const tgVerifyUnifiedUrl = ref('')
|
|
|
|
|
const tgVerifyVenuePins = ref<TgVenueVerifyPinRow[]>([])
|
|
|
|
|
const tgVerifyNotice = ref('')
|
|
|
|
|
|
|
|
|
|
const mapVisible = ref(false)
|
|
|
|
|
const mapLoading = ref(false)
|
|
|
|
|
@ -708,6 +688,43 @@ function scheduleLabel(s?: string) {
|
|
|
|
|
return s ?? '-'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleTagColor(s?: string) {
|
|
|
|
|
if (s === 'not_started') return 'arcoblue'
|
|
|
|
|
if (s === 'ongoing') return 'green'
|
|
|
|
|
if (s === 'ended') return 'gray'
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function auditStatusLabel(s?: unknown) {
|
|
|
|
|
const v = String(s ?? '').trim()
|
|
|
|
|
if (v === 'pending') return '待审核'
|
|
|
|
|
if (v === 'approved') return '已通过'
|
|
|
|
|
if (v === 'rejected') return '已驳回'
|
|
|
|
|
return v || '—'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function auditTagColor(s?: unknown) {
|
|
|
|
|
const v = String(s ?? '').trim()
|
|
|
|
|
if (v === 'pending') return 'orangered'
|
|
|
|
|
if (v === 'approved') return 'green'
|
|
|
|
|
if (v === 'rejected') return 'red'
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tgHubDateRange(start: unknown, end: unknown) {
|
|
|
|
|
const a = start != null ? String(start).slice(0, 10) : ''
|
|
|
|
|
const b = end != null ? String(end).slice(0, 10) : ''
|
|
|
|
|
const parts = [a, b].filter(Boolean)
|
|
|
|
|
return parts.length ? parts.join(' ~ ') : '—'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tgHubAgeLimitLabel(s: Record<string, unknown> | null): string {
|
|
|
|
|
if (!s) return '—'
|
|
|
|
|
const a = s.age_limit_start ? String(s.age_limit_start).slice(0, 10) : ''
|
|
|
|
|
const b = s.age_limit_end ? String(s.age_limit_end).slice(0, 10) : ''
|
|
|
|
|
return a || b ? `${a || '—'} ~ ${b || '—'}` : '不限制'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function abbr(s: string, n: number) {
|
|
|
|
|
if (!s) return '—'
|
|
|
|
|
return s.length <= n ? s : `${s.slice(0, n)}…`
|
|
|
|
|
@ -938,18 +955,25 @@ async function saveForm(): Promise<boolean> {
|
|
|
|
|
saving.value = true
|
|
|
|
|
try {
|
|
|
|
|
if (editId.value) {
|
|
|
|
|
await http.put(`/ticket-grab-events/${editId.value}`, body)
|
|
|
|
|
const savedId = editId.value
|
|
|
|
|
await http.put(`/ticket-grab-events/${savedId}`, body)
|
|
|
|
|
Message.success('已保存')
|
|
|
|
|
visible.value = false
|
|
|
|
|
await loadRows()
|
|
|
|
|
await nextTick()
|
|
|
|
|
const row =
|
|
|
|
|
rows.value.find((r) => r.id === savedId) ??
|
|
|
|
|
({ id: savedId, title: form.title.trim(), is_active: form.is_active } as TgRow)
|
|
|
|
|
await openTicketGrabDetailHub(row, '2')
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
const { data: created } = await http.post<TgRow>('/ticket-grab-events', body)
|
|
|
|
|
Message.success('已创建')
|
|
|
|
|
visible.value = false
|
|
|
|
|
await loadRows()
|
|
|
|
|
await nextTick()
|
|
|
|
|
if (created?.id != null) {
|
|
|
|
|
await openRelease(created)
|
|
|
|
|
await openTicketGrabDetailHub(created as TgRow, '2')
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
@ -985,14 +1009,12 @@ async function removeTicketGrab(row: TgRow) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openRelease(row: TgRow) {
|
|
|
|
|
releaseEventId.value = row.id
|
|
|
|
|
releaseVisible.value = true
|
|
|
|
|
async function loadHubReleaseConfig(eventId: number): Promise<boolean> {
|
|
|
|
|
releaseLoading.value = true
|
|
|
|
|
releasePayload.value = null
|
|
|
|
|
releaseEdits.value = {}
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await http.get(`/ticket-grab-events/${row.id}/release-config`)
|
|
|
|
|
const { data } = await http.get(`/ticket-grab-events/${eventId}/release-config`)
|
|
|
|
|
releasePayload.value = data
|
|
|
|
|
for (const b of data.venues ?? []) {
|
|
|
|
|
const vid = b.venue_id
|
|
|
|
|
@ -1001,97 +1023,116 @@ async function openRelease(row: TgRow) {
|
|
|
|
|
releaseEdits.value[vid][d.release_date] = d.day_quota
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '加载放票配置失败')
|
|
|
|
|
releaseVisible.value = false
|
|
|
|
|
return false
|
|
|
|
|
} finally {
|
|
|
|
|
releaseLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadTgVerifyData(eventId: number) {
|
|
|
|
|
const { data } = await http.get<{ verify_portal_code: string; credentials: TgVerifyCredRow[] }>(
|
|
|
|
|
`/ticket-grab-events/${eventId}/verify-portal`,
|
|
|
|
|
)
|
|
|
|
|
tgVerifyUrl.value = buildVerifyPortalPublicUrl(data.verify_portal_code)
|
|
|
|
|
tgVerifyCreds.value = data.credentials || []
|
|
|
|
|
function buildRowFallbackSummary(row: TgRow): Record<string, unknown> {
|
|
|
|
|
return {
|
|
|
|
|
title: row.title,
|
|
|
|
|
tags: row.tags ?? [],
|
|
|
|
|
start_at: row.start_at,
|
|
|
|
|
end_at: row.end_at,
|
|
|
|
|
booking_start_at: row.booking_start_at,
|
|
|
|
|
booking_end_at: row.booking_end_at,
|
|
|
|
|
is_active: row.is_active,
|
|
|
|
|
total_quota: row.total_quota,
|
|
|
|
|
registered_count: row.registered_count,
|
|
|
|
|
venues: row.venues ?? [],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openTicketGrabVerify(row: TgRow) {
|
|
|
|
|
tgVerifyEventId.value = row.id
|
|
|
|
|
tgVerifyVenues.value = row.venues?.length ? row.venues : []
|
|
|
|
|
tgPasswordReveal.value = {}
|
|
|
|
|
tgVerifyVisible.value = true
|
|
|
|
|
tgVerifyLoading.value = true
|
|
|
|
|
tgVerifyCredForm.venue_id = tgVerifyVenues.value[0]?.id
|
|
|
|
|
try {
|
|
|
|
|
await loadTgVerifyData(row.id)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '加载核销配置失败')
|
|
|
|
|
tgVerifyVisible.value = false
|
|
|
|
|
} finally {
|
|
|
|
|
tgVerifyLoading.value = false
|
|
|
|
|
async function openTicketGrabDetailHub(row: TgRow, initialTab: '1' | '2' | '3' = '1') {
|
|
|
|
|
releaseEventId.value = row.id
|
|
|
|
|
tgHubTitle.value = (row.title && String(row.title).trim()) || `抢票 #${row.id}`
|
|
|
|
|
tgHubActiveKey.value = initialTab
|
|
|
|
|
tgDetailHubVisible.value = true
|
|
|
|
|
tgHubSummary.value = null
|
|
|
|
|
tgHubSummaryLoading.value = true
|
|
|
|
|
releasePayload.value = null
|
|
|
|
|
releaseEdits.value = {}
|
|
|
|
|
tgVerifyNotice.value = ''
|
|
|
|
|
tgVerifyUnifiedUrl.value = buildUnifiedActivityVerifyLoginUrl()
|
|
|
|
|
tgVerifyVenuePins.value = []
|
|
|
|
|
|
|
|
|
|
const detailP = http
|
|
|
|
|
.get(`/ticket-grab-events/${row.id}`)
|
|
|
|
|
.then((r) => r.data as Record<string, unknown>)
|
|
|
|
|
.catch(() => null)
|
|
|
|
|
|
|
|
|
|
const verifyP = http
|
|
|
|
|
.get(`/ticket-grab-events/${row.id}/verify-portal`)
|
|
|
|
|
.then((r) => r.data as { unified_verify_notice?: string; venues?: TgVenueVerifyPinRow[] })
|
|
|
|
|
.catch(() => null)
|
|
|
|
|
|
|
|
|
|
const [detail, _relOk, verifyData] = await Promise.all([
|
|
|
|
|
detailP,
|
|
|
|
|
loadHubReleaseConfig(row.id),
|
|
|
|
|
verifyP,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
if (detail) {
|
|
|
|
|
const d = { ...detail } as Record<string, unknown>
|
|
|
|
|
if (d.cover_image) {
|
|
|
|
|
d.cover_image = resolvePublicMediaUrl(String(d.cover_image))
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(d.gallery_media)) {
|
|
|
|
|
d.gallery_media = (d.gallery_media as GalleryItem[]).map((x) => ({
|
|
|
|
|
...x,
|
|
|
|
|
url: resolvePublicMediaUrl(String((x as { url?: string }).url || '')),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
tgHubSummary.value = d
|
|
|
|
|
} else {
|
|
|
|
|
tgHubSummary.value = buildRowFallbackSummary(row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (verifyData) {
|
|
|
|
|
tgVerifyNotice.value = String(verifyData.unified_verify_notice || '').trim()
|
|
|
|
|
tgVerifyVenuePins.value = Array.isArray(verifyData.venues) ? verifyData.venues : []
|
|
|
|
|
}
|
|
|
|
|
tgVerifyUnifiedUrl.value = buildUnifiedActivityVerifyLoginUrl()
|
|
|
|
|
|
|
|
|
|
tgHubSummaryLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function copyTgVerifyUrl() {
|
|
|
|
|
void navigator.clipboard.writeText(tgVerifyUrl.value)
|
|
|
|
|
Message.success('核销链接已复制')
|
|
|
|
|
function closeTicketGrabDetailHub() {
|
|
|
|
|
tgDetailHubVisible.value = false
|
|
|
|
|
releaseEventId.value = null
|
|
|
|
|
releasePayload.value = null
|
|
|
|
|
releaseEdits.value = {}
|
|
|
|
|
tgHubSummary.value = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleTgPasswordReveal(id: number) {
|
|
|
|
|
const cur = { ...tgPasswordReveal.value }
|
|
|
|
|
cur[id] = !cur[id]
|
|
|
|
|
tgPasswordReveal.value = cur
|
|
|
|
|
function tgBookingAudienceLabel(v: unknown): string {
|
|
|
|
|
const s = String(v ?? 'all')
|
|
|
|
|
if (s === 'school_age') return '学龄内学生'
|
|
|
|
|
|
|
|
|
|
return '全部人员'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function addTgVerifyCredential() {
|
|
|
|
|
if (!tgVerifyEventId.value || !tgVerifyCredForm.venue_id) {
|
|
|
|
|
Message.warning('请选择场馆并填写用户名与密码')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!tgVerifyCredForm.username.trim() || !tgVerifyCredForm.password) {
|
|
|
|
|
Message.warning('请填写用户名与密码')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const pwdErr = validateTgVerifyPasswordStrength(tgVerifyCredForm.password)
|
|
|
|
|
if (pwdErr) {
|
|
|
|
|
Message.warning(pwdErr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tgVerifyCredSaving.value = true
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/ticket-grab-events/${tgVerifyEventId.value}/verify-credentials`, {
|
|
|
|
|
venue_id: tgVerifyCredForm.venue_id,
|
|
|
|
|
username: tgVerifyCredForm.username.trim(),
|
|
|
|
|
password: tgVerifyCredForm.password,
|
|
|
|
|
note: tgVerifyCredForm.note.trim() || undefined,
|
|
|
|
|
})
|
|
|
|
|
Message.success('已添加')
|
|
|
|
|
tgVerifyCredForm.username = ''
|
|
|
|
|
tgVerifyCredForm.password = ''
|
|
|
|
|
tgVerifyCredForm.note = ''
|
|
|
|
|
await loadTgVerifyData(tgVerifyEventId.value)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
const errs = e?.response?.data?.errors as Record<string, string[]> | undefined
|
|
|
|
|
const pwErr = errs?.password
|
|
|
|
|
const msg =
|
|
|
|
|
Array.isArray(pwErr) && pwErr.length > 0 ? String(pwErr[0]) : e?.response?.data?.message ?? '添加失败'
|
|
|
|
|
Message.error(msg)
|
|
|
|
|
} finally {
|
|
|
|
|
tgVerifyCredSaving.value = false
|
|
|
|
|
}
|
|
|
|
|
const tgHubPivots = computed(() => {
|
|
|
|
|
const s = tgHubSummary.value
|
|
|
|
|
if (!s) return []
|
|
|
|
|
|
|
|
|
|
const raw = s.event_venue_pivots ?? s.eventVenuePivots
|
|
|
|
|
|
|
|
|
|
return Array.isArray(raw) ? raw : []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function copyTgVerifyUrl() {
|
|
|
|
|
void navigator.clipboard.writeText(tgVerifyUnifiedUrl.value)
|
|
|
|
|
Message.success('核销链接已复制')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeTgVerifyCredential(row: TgVerifyCredRow) {
|
|
|
|
|
if (!tgVerifyEventId.value) return
|
|
|
|
|
try {
|
|
|
|
|
await http.delete(`/ticket-grab-events/${tgVerifyEventId.value}/verify-credentials/${row.id}`)
|
|
|
|
|
Message.success('已删除')
|
|
|
|
|
tgVerifyCreds.value = tgVerifyCreds.value.filter((c) => c.id !== row.id)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
Message.error(e?.response?.data?.message ?? '删除失败')
|
|
|
|
|
}
|
|
|
|
|
function copyVenuePin(pin: string) {
|
|
|
|
|
void navigator.clipboard.writeText(String(pin || '').trim())
|
|
|
|
|
Message.success('口令已复制')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveRelease() {
|
|
|
|
|
@ -1112,7 +1153,9 @@ async function saveRelease() {
|
|
|
|
|
venue_day_quotas: blocks,
|
|
|
|
|
})
|
|
|
|
|
Message.success('放票日配置已保存')
|
|
|
|
|
releaseVisible.value = false
|
|
|
|
|
if (releaseEventId.value != null) {
|
|
|
|
|
await loadHubReleaseConfig(releaseEventId.value)
|
|
|
|
|
}
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
if (e?.response?.data?.errors) {
|
|
|
|
|
@ -1218,8 +1261,7 @@ onMounted(async () => {
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space :size="2" class="tg-list-actions" align="start">
|
|
|
|
|
<a-button type="text" size="small" @click="openEdit(record)">编辑</a-button>
|
|
|
|
|
<a-button type="text" size="small" @click="openRelease(record)">放票</a-button>
|
|
|
|
|
<a-button type="text" size="small" @click="openTicketGrabVerify(record)">核销管理</a-button>
|
|
|
|
|
<a-button type="text" size="small" @click="openTicketGrabDetailHub(record as TgRow, '1')">查看</a-button>
|
|
|
|
|
<a-button type="text" size="small" status="warning" @click="toggleRow(record)">{{
|
|
|
|
|
record.is_active ? '下架' : '上架'
|
|
|
|
|
}}</a-button>
|
|
|
|
|
@ -1237,77 +1279,285 @@ onMounted(async () => {
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-space>
|
|
|
|
|
|
|
|
|
|
<a-modal v-model:visible="tgVerifyVisible" title="核销管理" width="840px" :footer="false">
|
|
|
|
|
<a-spin :loading="tgVerifyLoading" style="width: 100%">
|
|
|
|
|
<a-typography-paragraph type="secondary" style="margin-bottom: 12px">
|
|
|
|
|
不同参与场馆可分别配置多组账号;场馆后台账号<strong>不可</strong>登录核销页。
|
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
<a-form-item label="独立核销链接">
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-input :model-value="tgVerifyUrl" readonly style="width: 500px" />
|
|
|
|
|
<a-button type="primary" @click="copyTgVerifyUrl">复制链接</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
<a-divider orientation="left">按场馆添加核销账号</a-divider>
|
|
|
|
|
<div class="tg-verify-cred-add">
|
|
|
|
|
<a-space style="margin-bottom: 8px; flex-wrap: wrap; align-items: flex-start">
|
|
|
|
|
<a-select
|
|
|
|
|
v-model="tgVerifyCredForm.venue_id"
|
|
|
|
|
placeholder="场馆"
|
|
|
|
|
allow-clear
|
|
|
|
|
style="width: 200px"
|
|
|
|
|
:options="tgVerifyVenues.map((v) => ({ label: v.name, value: v.id }))"
|
|
|
|
|
/>
|
|
|
|
|
<a-input v-model="tgVerifyCredForm.username" placeholder="用户名" style="width: 140px" allow-clear />
|
|
|
|
|
<a-input-password v-model="tgVerifyCredForm.password" placeholder="密码" style="width: 180px" allow-clear />
|
|
|
|
|
<a-input v-model="tgVerifyCredForm.note" placeholder="备注" style="width: 140px" allow-clear />
|
|
|
|
|
<a-button type="primary" :loading="tgVerifyCredSaving" @click="addTgVerifyCredential">添加</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
<div class="tg-verify-cred-add__hint">密码要求:{{ TG_VERIFY_PASSWORD_RULE_HINT }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<a-table :data="tgVerifyCreds" :pagination="false" size="small" row-key="id">
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="场馆" data-index="venue_name" />
|
|
|
|
|
<a-table-column title="用户名" data-index="username" />
|
|
|
|
|
<a-table-column title="密码" :width="168">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space :size="4">
|
|
|
|
|
<span>{{
|
|
|
|
|
tgPasswordReveal[(record as TgVerifyCredRow).id]
|
|
|
|
|
? (record as TgVerifyCredRow).password_plain || '—'
|
|
|
|
|
: '*****'
|
|
|
|
|
}}</span>
|
|
|
|
|
<a-button
|
|
|
|
|
type="text"
|
|
|
|
|
size="mini"
|
|
|
|
|
class="tg-pw-eye"
|
|
|
|
|
@click="toggleTgPasswordReveal((record as TgVerifyCredRow).id)"
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="tgDetailHubVisible"
|
|
|
|
|
:title="tgHubTitle"
|
|
|
|
|
width="960px"
|
|
|
|
|
:footer="false"
|
|
|
|
|
unmount-on-close
|
|
|
|
|
body-class="tg-detail-hub-modal-body"
|
|
|
|
|
@cancel="closeTicketGrabDetailHub"
|
|
|
|
|
>
|
|
|
|
|
<a-tabs v-model:active-key="tgHubActiveKey" type="rounded">
|
|
|
|
|
<a-tab-pane key="1" title="活动信息">
|
|
|
|
|
<div class="tg-detail-hub-pane">
|
|
|
|
|
<a-spin style="width: 100%" :loading="tgHubSummaryLoading">
|
|
|
|
|
<template v-if="tgHubSummary">
|
|
|
|
|
<div class="tg-hub-activity">
|
|
|
|
|
<div class="tg-hub-activity__head">
|
|
|
|
|
<div class="tg-hub-activity__title-row">
|
|
|
|
|
<a-typography-title :heading="5" class="tg-hub-activity__name">
|
|
|
|
|
{{ String(tgHubSummary.title ?? '—') }}
|
|
|
|
|
</a-typography-title>
|
|
|
|
|
<a-space wrap :size="8">
|
|
|
|
|
<a-tag v-if="releaseEventId != null" size="small">#{{ releaseEventId }}</a-tag>
|
|
|
|
|
<a-tag :color="tgHubSummary.is_active ? 'green' : 'gray'" size="small">
|
|
|
|
|
{{ tgHubSummary.is_active ? '已上架' : '已下架' }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
<a-tag
|
|
|
|
|
v-if="tgHubSummary.schedule_status"
|
|
|
|
|
:color="scheduleTagColor(String(tgHubSummary.schedule_status))"
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
{{ scheduleLabel(String(tgHubSummary.schedule_status)) }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
<a-tag
|
|
|
|
|
v-if="tgHubSummary.audit_status"
|
|
|
|
|
:color="auditTagColor(tgHubSummary.audit_status)"
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
{{ auditStatusLabel(tgHubSummary.audit_status) }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
</a-space>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-if="Array.isArray(tgHubSummary.tags) && (tgHubSummary.tags as string[]).length"
|
|
|
|
|
class="tg-hub-activity__tags"
|
|
|
|
|
>
|
|
|
|
|
<a-tag
|
|
|
|
|
v-for="(tag, i) in tgHubSummary.tags as string[]"
|
|
|
|
|
:key="`${tag}-${i}`"
|
|
|
|
|
color="arcoblue"
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
{{ tag }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-divider class="tg-hub-divider" />
|
|
|
|
|
|
|
|
|
|
<section class="tg-hub-section">
|
|
|
|
|
<h6 class="tg-hub-section__title">时间与规则</h6>
|
|
|
|
|
<a-descriptions :column="2" size="small" bordered class="tg-hub-desc">
|
|
|
|
|
<a-descriptions-item label="活动日期">
|
|
|
|
|
{{ tgHubDateRange(tgHubSummary.start_at, tgHubSummary.end_at) }}
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="预约日期">
|
|
|
|
|
{{ tgHubDateRange(tgHubSummary.booking_start_at, tgHubSummary.booking_end_at) }}
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="每日放票时段" :span="2">
|
|
|
|
|
{{ String(tgHubSummary.daily_release_start_time ?? '—') }} ~
|
|
|
|
|
{{ String(tgHubSummary.daily_release_end_time ?? '—') }}
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="预约人群">
|
|
|
|
|
{{ tgBookingAudienceLabel(tgHubSummary.booking_audience) }}
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="年龄限制">
|
|
|
|
|
{{ tgHubAgeLimitLabel(tgHubSummary as Record<string, unknown>) }}
|
|
|
|
|
</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="放票总数">{{ Number(tgHubSummary.total_quota ?? 0) }}</a-descriptions-item>
|
|
|
|
|
<a-descriptions-item label="已预约">{{ Number(tgHubSummary.registered_count ?? 0) }}</a-descriptions-item>
|
|
|
|
|
</a-descriptions>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section
|
|
|
|
|
v-if="
|
|
|
|
|
tgHubSummary.cover_image ||
|
|
|
|
|
(Array.isArray(tgHubSummary.gallery_media) && (tgHubSummary.gallery_media as GalleryItem[]).length)
|
|
|
|
|
"
|
|
|
|
|
class="tg-hub-section"
|
|
|
|
|
>
|
|
|
|
|
<IconEye v-if="!tgPasswordReveal[(record as TgVerifyCredRow).id]" />
|
|
|
|
|
<IconEyeInvisible v-else />
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
<h6 class="tg-hub-section__title">封面与轮播</h6>
|
|
|
|
|
<div class="tg-hub-media">
|
|
|
|
|
<div v-if="tgHubSummary.cover_image" class="tg-hub-media__cover">
|
|
|
|
|
<div class="tg-hub-media__sub">封面</div>
|
|
|
|
|
<img
|
|
|
|
|
:src="String(tgHubSummary.cover_image)"
|
|
|
|
|
alt=""
|
|
|
|
|
class="tg-hub-media__cover-img"
|
|
|
|
|
@click="openMediaPreview('image', String(tgHubSummary.cover_image))"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-if="Array.isArray(tgHubSummary.gallery_media) && (tgHubSummary.gallery_media as GalleryItem[]).length"
|
|
|
|
|
class="tg-hub-media__gallery"
|
|
|
|
|
>
|
|
|
|
|
<div class="tg-hub-media__sub">轮播</div>
|
|
|
|
|
<div class="tg-hub-gallery-scroll">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(item, idx) in tgHubSummary.gallery_media as GalleryItem[]"
|
|
|
|
|
:key="idx + item.url"
|
|
|
|
|
class="tg-hub-gallery-thumb-wrap"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
v-if="item.type === 'image'"
|
|
|
|
|
:src="item.url"
|
|
|
|
|
class="tg-hub-gallery-thumb"
|
|
|
|
|
@click="openMediaPreview('image', item.url)"
|
|
|
|
|
/>
|
|
|
|
|
<video
|
|
|
|
|
v-else
|
|
|
|
|
:src="item.url"
|
|
|
|
|
controls
|
|
|
|
|
class="tg-hub-gallery-thumb tg-hub-gallery-thumb--video"
|
|
|
|
|
@click.stop="openMediaPreview('video', item.url)"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-if="tgHubPivots.length" class="tg-hub-section">
|
|
|
|
|
<h6 class="tg-hub-section__title">参与场馆与配额</h6>
|
|
|
|
|
<div class="tg-hub-table-wrap">
|
|
|
|
|
<a-table
|
|
|
|
|
:data="tgHubPivots"
|
|
|
|
|
size="small"
|
|
|
|
|
:pagination="false"
|
|
|
|
|
row-key="venue_id"
|
|
|
|
|
:bordered="{ cell: true }"
|
|
|
|
|
>
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="场馆" data-index="venue_id">
|
|
|
|
|
<template #cell="{ record }">{{
|
|
|
|
|
venueName((record as Record<string, unknown>).venue_id as number)
|
|
|
|
|
}}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="放票数" :width="96">
|
|
|
|
|
<template #cell="{ record }">{{
|
|
|
|
|
((record as Record<string, unknown>).venue_total_quota as number) ?? '—'
|
|
|
|
|
}}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="开放时间" :ellipsis="true" :tooltip="true">
|
|
|
|
|
<template #cell="{ record }">{{
|
|
|
|
|
abbr(String((record as Record<string, unknown>).opening_hours ?? ''), 36)
|
|
|
|
|
}}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="地址" :ellipsis="true" :tooltip="true">
|
|
|
|
|
<template #cell="{ record }">{{
|
|
|
|
|
abbr(String((record as Record<string, unknown>).address ?? ''), 40)
|
|
|
|
|
}}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-if="String(tgHubSummary.reservation_notice || '').trim()" class="tg-hub-section">
|
|
|
|
|
<h6 class="tg-hub-section__title">预约须知</h6>
|
|
|
|
|
<div class="tg-hub-rich-box">
|
|
|
|
|
<div class="tg-hub-rich" v-html="String(tgHubSummary.reservation_notice)" />
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-if="String(tgHubSummary.detail_html || '').trim()" class="tg-hub-section">
|
|
|
|
|
<h6 class="tg-hub-section__title">活动详情</h6>
|
|
|
|
|
<div class="tg-hub-rich-box">
|
|
|
|
|
<div class="tg-hub-rich" v-html="String(tgHubSummary.detail_html)" />
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="备注" data-index="note" />
|
|
|
|
|
<a-table-column title="创建时间" data-index="created_at" />
|
|
|
|
|
<a-table-column title="操作" :width="90">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-popconfirm content="确认删除?" @ok="removeTgVerifyCredential(record as TgVerifyCredRow)">
|
|
|
|
|
<a-button type="text" size="mini" status="danger">删除</a-button>
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
</a-spin>
|
|
|
|
|
</div>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
<a-tab-pane key="2" title="放票设置">
|
|
|
|
|
<div class="tg-detail-hub-pane">
|
|
|
|
|
<a-spin v-if="releaseLoading" style="width: 100%; padding: 24px" />
|
|
|
|
|
<div v-else-if="releasePayload">
|
|
|
|
|
<a-typography-paragraph v-if="releasePayload.event" type="secondary" style="margin-top: 0">
|
|
|
|
|
预约日 {{ releasePayload.event.booking_start_at }} ~ {{ releasePayload.event.booking_end_at }},每日
|
|
|
|
|
{{ releasePayload.event.daily_release_start_time }} 起开放,至
|
|
|
|
|
{{ releasePayload.event.daily_release_end_time }} 止。
|
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
<a-tabs v-if="releasePayload.venues?.length" default-active-key="0">
|
|
|
|
|
<a-tab-pane
|
|
|
|
|
v-for="(b, idx) in releasePayload.venues"
|
|
|
|
|
:key="String(idx)"
|
|
|
|
|
:title="`${venueName(b.venue_id)} · 总配额 ${b.venue_total_quota}`"
|
|
|
|
|
>
|
|
|
|
|
<a-table :data="b.release_days" :pagination="false" :scroll="{ y: 320 }">
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="抢票日期" data-index="release_date" :width="120" />
|
|
|
|
|
<a-table-column title="昨日余票" :width="88">
|
|
|
|
|
<template #cell="{ record }">{{ record.carry_in }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="基础放票" :width="120">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-input-number
|
|
|
|
|
v-model="releaseEdits[b.venue_id][record.release_date]"
|
|
|
|
|
:min="0"
|
|
|
|
|
mode="button"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="可预约总量" :width="120">
|
|
|
|
|
<template #cell="{ record }">{{ record.total_day_pool }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="已预约" :width="72">
|
|
|
|
|
<template #cell="{ record }">{{ record.booked_count }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="余量" :width="88">
|
|
|
|
|
<template #cell="{ record }">{{ record.current_remaining }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
|
|
|
|
<a-alert v-if="!releasePayload.venues?.length" type="warning">暂无放票日数据,请先保存活动并含预约日期与场馆。</a-alert>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
<a-tab-pane key="3" title="核销说明">
|
|
|
|
|
<div class="tg-detail-hub-pane">
|
|
|
|
|
<a-typography-paragraph v-if="tgVerifyNotice" type="secondary" style="margin-bottom: 12px">
|
|
|
|
|
{{ tgVerifyNotice }}
|
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
<a-form-item label="核销登录页(与活动核销相同)">
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-input :model-value="tgVerifyUnifiedUrl" readonly style="width: 460px" />
|
|
|
|
|
<a-button type="primary" @click="copyTgVerifyUrl">复制链接</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
<a-divider orientation="left">本场参与场馆 · 6 位抢票核销口令</a-divider>
|
|
|
|
|
<a-table :data="tgVerifyVenuePins" :pagination="false" size="small" row-key="venue_id">
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="场馆" data-index="venue_name" />
|
|
|
|
|
<a-table-column title="6 位口令" data-index="verify_portal_pin" />
|
|
|
|
|
<a-table-column title="操作" :width="100">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-button type="text" size="mini" @click="copyVenuePin(String((record as TgVenueVerifyPinRow).verify_portal_pin || ''))"
|
|
|
|
|
>复制口令</a-button
|
|
|
|
|
>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-spin>
|
|
|
|
|
</a-table>
|
|
|
|
|
</div>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
|
|
|
|
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px">
|
|
|
|
|
<a-button @click="closeTicketGrabDetailHub">关闭</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
v-show="tgHubActiveKey === '2'"
|
|
|
|
|
type="primary"
|
|
|
|
|
:loading="releaseSaving"
|
|
|
|
|
:disabled="!releasePayload?.venues?.length"
|
|
|
|
|
@click="saveRelease"
|
|
|
|
|
>
|
|
|
|
|
保存放票配置
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="visible"
|
|
|
|
|
:title="editId ? '编辑抢票' : '新建抢票'"
|
|
|
|
|
ok-text="下一步:放票配置"
|
|
|
|
|
width="70%"
|
|
|
|
|
:body-style="modalBodyStyle"
|
|
|
|
|
:ok-loading="saving"
|
|
|
|
|
@ -1641,71 +1891,10 @@ onMounted(async () => {
|
|
|
|
|
地址:{{ pickedPoint.address || '-' }}
|
|
|
|
|
</a-alert>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="releaseVisible"
|
|
|
|
|
title="放票与每日配置"
|
|
|
|
|
:width="900"
|
|
|
|
|
:ok-loading="releaseSaving"
|
|
|
|
|
@ok="saveRelease"
|
|
|
|
|
@cancel="releaseVisible = false"
|
|
|
|
|
>
|
|
|
|
|
<a-spin v-if="releaseLoading" style="width: 100%; padding: 24px" />
|
|
|
|
|
<div v-else-if="releasePayload">
|
|
|
|
|
<a-typography-paragraph v-if="releasePayload.event" type="secondary" style="margin-top: 0">
|
|
|
|
|
预约日 {{ releasePayload.event.booking_start_at }} ~ {{ releasePayload.event.booking_end_at }},每日
|
|
|
|
|
{{ releasePayload.event.daily_release_start_time }} 起开放,至
|
|
|
|
|
{{ releasePayload.event.daily_release_end_time }} 止。
|
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
<a-tabs v-if="releasePayload.venues?.length" default-active-key="0">
|
|
|
|
|
<a-tab-pane
|
|
|
|
|
v-for="(b, idx) in releasePayload.venues"
|
|
|
|
|
:key="String(idx)"
|
|
|
|
|
:title="`${venueName(b.venue_id)} · 总配额 ${b.venue_total_quota}`"
|
|
|
|
|
>
|
|
|
|
|
<a-table :data="b.release_days" :pagination="false" :scroll="{ y: 320 }">
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="抢票日期" data-index="release_date" :width="120" />
|
|
|
|
|
<a-table-column title="昨日余票" :width="88">
|
|
|
|
|
<template #cell="{ record }">{{ record.carry_in }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="基础放票" :width="120">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-input-number
|
|
|
|
|
v-model="releaseEdits[b.venue_id][record.release_date]"
|
|
|
|
|
:min="0"
|
|
|
|
|
mode="button"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="可预约总量" :width="120">
|
|
|
|
|
<template #cell="{ record }">{{ record.total_day_pool }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="已预约" :width="72">
|
|
|
|
|
<template #cell="{ record }">{{ record.booked_count }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="余量" :width="88">
|
|
|
|
|
<template #cell="{ record }">{{ record.current_remaining }}</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
|
|
|
|
<a-alert v-if="!releasePayload.venues?.length" type="warning">暂无放票日数据,请先保存活动并含预约日期与场馆。</a-alert>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
|
|
|
|
</a-card>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.tg-verify-cred-add {
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
.tg-verify-cred-add__hint {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: var(--color-text-3);
|
|
|
|
|
}
|
|
|
|
|
.tg-venue-block {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
@ -1813,4 +2002,154 @@ onMounted(async () => {
|
|
|
|
|
.activity-address-coord-row__map {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-detail-hub-pane {
|
|
|
|
|
max-height: min(70vh, 680px);
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding-right: 2px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-activity__title-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 12px 16px;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-activity__name {
|
|
|
|
|
flex: 1 1 200px;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-activity__tags {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-divider {
|
|
|
|
|
margin: 14px 0 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-section {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-section:last-child {
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-section__title {
|
|
|
|
|
margin: 0 0 10px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-2);
|
|
|
|
|
letter-spacing: 0.02em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-desc {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-desc :deep(.arco-descriptions-item-label) {
|
|
|
|
|
width: 112px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-media {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-media__sub {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--color-text-3);
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-media__cover-img {
|
|
|
|
|
width: 200px;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
height: 112px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 1px solid var(--color-border-2);
|
|
|
|
|
cursor: zoom-in;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-media__gallery {
|
|
|
|
|
flex: 1 1 280px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-gallery-scroll {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
padding-bottom: 6px;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-gallery-thumb-wrap {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-gallery-thumb {
|
|
|
|
|
width: 128px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 1px solid var(--color-border-2);
|
|
|
|
|
cursor: zoom-in;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-gallery-thumb--video {
|
|
|
|
|
cursor: default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-table-wrap {
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
border: 1px solid var(--color-border-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-rich-box {
|
|
|
|
|
max-height: 280px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 1px solid var(--color-border-2);
|
|
|
|
|
background: var(--color-fill-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-rich {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-rich :deep(img) {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
height: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-rich :deep(p) {
|
|
|
|
|
margin: 0.5em 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-rich :deep(p:first-child) {
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tg-hub-rich :deep(p:last-child) {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|