|
|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import { Message } from '@arco-design/web-vue'
|
|
|
|
|
import RichEditorField from '../../components/RichEditorField.vue'
|
|
|
|
|
import { http } from '../../api/http'
|
|
|
|
|
@ -8,7 +9,7 @@ import { useUnsavedChangesGuard } from '../../composables/useUnsavedChangesGuard
|
|
|
|
|
import { listTableRowIndex } from '../../utils/listTableRowIndex'
|
|
|
|
|
|
|
|
|
|
/** 主列表列宽约 1174px,勿用全局 3220 撑宽列 */
|
|
|
|
|
const ACTIVITY_LIST_SCROLL_X = 1420
|
|
|
|
|
const ACTIVITY_LIST_SCROLL_X = 1520
|
|
|
|
|
|
|
|
|
|
type Venue = {
|
|
|
|
|
id: number
|
|
|
|
|
@ -30,6 +31,7 @@ type Activity = {
|
|
|
|
|
contact_name?: string | null
|
|
|
|
|
summary?: string
|
|
|
|
|
booking_audience?: string | null
|
|
|
|
|
location?: string | null
|
|
|
|
|
total_quota?: number
|
|
|
|
|
start_at?: string
|
|
|
|
|
end_at?: string
|
|
|
|
|
@ -51,7 +53,6 @@ type Activity = {
|
|
|
|
|
audit_status?: 'approved' | 'pending' | 'rejected'
|
|
|
|
|
audit_remark?: string | null
|
|
|
|
|
reservation_type?: string
|
|
|
|
|
location?: string | null
|
|
|
|
|
specific_time?: string | null
|
|
|
|
|
offline_reservation_method?: string | null
|
|
|
|
|
external_url?: string | null
|
|
|
|
|
@ -61,6 +62,8 @@ type Activity = {
|
|
|
|
|
view_count?: number
|
|
|
|
|
/** 外链跳转预约:外链点击 */
|
|
|
|
|
external_link_click_count?: number
|
|
|
|
|
is_hot?: boolean
|
|
|
|
|
behind_scenes_media?: Array<{ type: 'image' | 'video'; url: string }>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = ref<Activity[]>([])
|
|
|
|
|
@ -79,13 +82,25 @@ const filters = reactive({
|
|
|
|
|
audit_status: undefined as 'approved' | 'pending' | 'rejected' | undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const rejectActivityId = ref<number | null>(null)
|
|
|
|
|
const rejectActivityRemark = ref('')
|
|
|
|
|
const rejectActivityVisible = ref(false)
|
|
|
|
|
const auditActivityVisible = ref(false)
|
|
|
|
|
const auditActivityRecord = ref<Activity | null>(null)
|
|
|
|
|
const btsVisible = ref(false)
|
|
|
|
|
const btsActivityId = ref<number | null>(null)
|
|
|
|
|
const btsModalTitle = ref('上传花絮')
|
|
|
|
|
const btsImages = ref<Array<{ type: 'image'; url: string }>>([])
|
|
|
|
|
const btsSaving = ref(false)
|
|
|
|
|
const isCreate = ref(true)
|
|
|
|
|
const editId = ref<number | null>(null)
|
|
|
|
|
const formBaseline = ref('')
|
|
|
|
|
const modalBodyStyle = { maxHeight: '70vh', overflow: 'auto' }
|
|
|
|
|
const auditModalBodyStyle = { maxHeight: '72vh', overflow: 'auto' }
|
|
|
|
|
/** 上传花絮:固定内容区高度,内层滚动 */
|
|
|
|
|
const btsModalBodyStyle = {
|
|
|
|
|
height: '520px',
|
|
|
|
|
overflowY: 'auto' as const,
|
|
|
|
|
overflowX: 'hidden' as const,
|
|
|
|
|
boxSizing: 'border-box' as const,
|
|
|
|
|
}
|
|
|
|
|
const editorRenderKey = ref(0)
|
|
|
|
|
const mediaPreviewVisible = ref(false)
|
|
|
|
|
const mediaPreviewType = ref<'image' | 'video'>('image')
|
|
|
|
|
@ -101,6 +116,8 @@ let markerLayer: any = null
|
|
|
|
|
const pickedPoint = ref<{ lat: number; lng: number; address: string } | null>(null)
|
|
|
|
|
const DEFAULT_CENTER = { lat: 31.299379, lng: 120.585315 }
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
|
|
|
|
/** 接口 ISO8601 → 东八区日历日 YYYY-MM-DD(与后端 APP_TIMEZONE 一致)。勿对 ISO 用 slice(0,10),UTC 日界会导致与后端差一天。 */
|
|
|
|
|
function formatYmdInShanghai(iso: string | undefined | null): string {
|
|
|
|
|
if (!iso) return ''
|
|
|
|
|
@ -127,7 +144,8 @@ function formatActivityTableDateRange(record: Activity): string {
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
venue_id: undefined as number | undefined,
|
|
|
|
|
reservation_type: 'phone' as 'phone' | 'wechat_mp' | 'offline_visit',
|
|
|
|
|
reservation_type: 'phone' as 'phone' | 'wechat_mp' | 'offline_visit' | 'none',
|
|
|
|
|
is_hot: false,
|
|
|
|
|
/** 门票说明:免费/收费,提交至 offline_reservation_method */
|
|
|
|
|
ticket_note: 'free' as 'free' | 'paid',
|
|
|
|
|
location: '',
|
|
|
|
|
@ -363,36 +381,131 @@ function scheduleStatusColor(s?: 'not_started' | 'ongoing' | 'ended' | null) {
|
|
|
|
|
return 'green'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function approveActivity(row: Activity) {
|
|
|
|
|
function openAuditActivity(row: Activity) {
|
|
|
|
|
auditActivityRecord.value = row
|
|
|
|
|
auditActivityVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function auditSubmitApprove(markHot: boolean) {
|
|
|
|
|
const row = auditActivityRecord.value
|
|
|
|
|
if (!row?.id) return
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${row.id}/audit/approve`, { mark_hot: markHot })
|
|
|
|
|
Message.success('审核已通过')
|
|
|
|
|
auditActivityVisible.value = false
|
|
|
|
|
await loadAll()
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '操作失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function auditSubmitReject() {
|
|
|
|
|
const row = auditActivityRecord.value
|
|
|
|
|
if (!row?.id) return
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${row.id}/audit/approve`)
|
|
|
|
|
Message.success('已通过审核')
|
|
|
|
|
await http.post(`/activities/${row.id}/audit/reject`, {})
|
|
|
|
|
Message.success('已退回')
|
|
|
|
|
auditActivityVisible.value = false
|
|
|
|
|
await loadAll()
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '操作失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openRejectActivity(row: Activity) {
|
|
|
|
|
rejectActivityId.value = row.id
|
|
|
|
|
rejectActivityRemark.value = ''
|
|
|
|
|
rejectActivityVisible.value = true
|
|
|
|
|
async function onHotFlagChange(row: Activity, val: unknown) {
|
|
|
|
|
if (typeof val !== 'boolean') return
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${row.id}/hot-flag`, { is_hot: val })
|
|
|
|
|
Message.success('已更新热门状态')
|
|
|
|
|
row.is_hot = val
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '操作失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function activityHasBehindScenesImages(row: Activity): boolean {
|
|
|
|
|
const raw = row.behind_scenes_media
|
|
|
|
|
if (!Array.isArray(raw)) return false
|
|
|
|
|
return raw.some((x) => x && x.type === 'image' && String((x as { url?: string }).url || '').trim().length > 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openBehindScenesModal(row: Activity) {
|
|
|
|
|
btsActivityId.value = row.id
|
|
|
|
|
btsModalTitle.value = activityHasBehindScenesImages(row) ? '查看花絮' : '上传花絮'
|
|
|
|
|
const raw = Array.isArray(row.behind_scenes_media) ? row.behind_scenes_media : []
|
|
|
|
|
btsImages.value = raw
|
|
|
|
|
.filter((x) => x && x.type === 'image' && String(x.url || '').trim())
|
|
|
|
|
.map((x) => ({ type: 'image' as const, url: String(x.url) }))
|
|
|
|
|
btsVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitRejectActivity(): Promise<boolean> {
|
|
|
|
|
if (!rejectActivityId.value) return false
|
|
|
|
|
async function onBtsBeforeUpload(file: unknown) {
|
|
|
|
|
try {
|
|
|
|
|
await http.post(`/activities/${rejectActivityId.value}/audit/reject`, { remark: rejectActivityRemark.value || undefined })
|
|
|
|
|
Message.success('已退回')
|
|
|
|
|
rejectActivityVisible.value = false
|
|
|
|
|
let f: File | null = file instanceof File ? file : pickUploadFile(file)
|
|
|
|
|
if (!f) return false
|
|
|
|
|
const url = await uploadFile(f)
|
|
|
|
|
btsImages.value = [...btsImages.value, { type: 'image', url }]
|
|
|
|
|
Message.success('已添加图片')
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '上传失败')
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeBtsImage(i: number) {
|
|
|
|
|
const next = [...btsImages.value]
|
|
|
|
|
next.splice(i, 1)
|
|
|
|
|
btsImages.value = next
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function moveBtsImage(i: number, dir: -1 | 1) {
|
|
|
|
|
const j = i + dir
|
|
|
|
|
if (j < 0 || j >= btsImages.value.length) return
|
|
|
|
|
const next = [...btsImages.value]
|
|
|
|
|
const t = next[i]!
|
|
|
|
|
next[i] = next[j]!
|
|
|
|
|
next[j] = t
|
|
|
|
|
btsImages.value = next
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveBehindScenes(): Promise<boolean> {
|
|
|
|
|
if (!btsActivityId.value) return false
|
|
|
|
|
btsSaving.value = true
|
|
|
|
|
try {
|
|
|
|
|
await http.put(`/activities/${btsActivityId.value}/behind-scenes`, {
|
|
|
|
|
behind_scenes_media: btsImages.value,
|
|
|
|
|
})
|
|
|
|
|
Message.success('花絮已保存')
|
|
|
|
|
btsVisible.value = false
|
|
|
|
|
await loadAll()
|
|
|
|
|
return true
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '操作失败')
|
|
|
|
|
Message.error(error?.response?.data?.message ?? '保存失败')
|
|
|
|
|
return false
|
|
|
|
|
} finally {
|
|
|
|
|
btsSaving.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function auditTicketNoteLabel(row: Activity): string {
|
|
|
|
|
const t = row.reservation_type || ''
|
|
|
|
|
if (t === 'none') return '—'
|
|
|
|
|
const m = String(row.offline_reservation_method || '')
|
|
|
|
|
if (m === 'paid') return '收费'
|
|
|
|
|
if (m === 'free') return '免费'
|
|
|
|
|
return m || '—'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function auditCoordCell(v: unknown): string {
|
|
|
|
|
const n = parseCoord(v)
|
|
|
|
|
return n != null ? n.toFixed(6) : '—'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function auditShowExternalUrl(row: Activity): boolean {
|
|
|
|
|
const t = row.reservation_type || ''
|
|
|
|
|
return t === 'other' || !!String(row.external_url || '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseCoord(v: unknown): number | undefined {
|
|
|
|
|
if (v === null || v === undefined || v === '') return undefined
|
|
|
|
|
const n = typeof v === 'number' ? v : parseFloat(String(v).trim())
|
|
|
|
|
@ -578,6 +691,7 @@ function reservationTypeLabel(t?: string | null) {
|
|
|
|
|
if (t === 'phone') return '电话预约'
|
|
|
|
|
if (t === 'wechat_mp') return '公众号预约'
|
|
|
|
|
if (t === 'offline_visit') return '线下预约'
|
|
|
|
|
if (t === 'none') return '无需预约'
|
|
|
|
|
if (t === 'offline') return '线下预约'
|
|
|
|
|
if (t === 'other') return '外链跳转'
|
|
|
|
|
if (t === 'online') return '线上预约'
|
|
|
|
|
@ -654,6 +768,7 @@ function openCreate() {
|
|
|
|
|
form.gallery_media = []
|
|
|
|
|
form.tags = []
|
|
|
|
|
form.summary = ''
|
|
|
|
|
form.is_hot = false
|
|
|
|
|
form.sort = 0
|
|
|
|
|
form.is_active = true
|
|
|
|
|
resetEditors()
|
|
|
|
|
@ -677,7 +792,7 @@ function openEdit(row: Activity) {
|
|
|
|
|
} else if (t === 'other') {
|
|
|
|
|
form.reservation_type = 'wechat_mp'
|
|
|
|
|
form.ticket_note = 'free'
|
|
|
|
|
} else if (t === 'phone' || t === 'wechat_mp' || t === 'offline_visit') {
|
|
|
|
|
} else if (t === 'phone' || t === 'wechat_mp' || t === 'offline_visit' || t === 'none') {
|
|
|
|
|
form.reservation_type = t
|
|
|
|
|
form.ticket_note = row.offline_reservation_method === 'paid' ? 'paid' : 'free'
|
|
|
|
|
} else {
|
|
|
|
|
@ -702,6 +817,7 @@ function openEdit(row: Activity) {
|
|
|
|
|
form.sort = row.sort ?? 0
|
|
|
|
|
form.summary = row.summary || ''
|
|
|
|
|
form.is_active = row.is_active
|
|
|
|
|
form.is_hot = isSuperAdmin() ? row.is_hot === true : false
|
|
|
|
|
resetEditors()
|
|
|
|
|
captureFormBaseline()
|
|
|
|
|
visible.value = true
|
|
|
|
|
@ -712,17 +828,6 @@ function onSearch() {
|
|
|
|
|
void loadAll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetFilters() {
|
|
|
|
|
filters.keyword = ''
|
|
|
|
|
filters.venue_id = isVenueAdmin() ? currentUser.value?.venues?.[0]?.id : undefined
|
|
|
|
|
filters.reservation_type = undefined
|
|
|
|
|
filters.is_active = undefined
|
|
|
|
|
filters.schedule_status = undefined
|
|
|
|
|
filters.audit_status = undefined
|
|
|
|
|
pagination.current = 1
|
|
|
|
|
void loadAll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pickUploadFile(payload: any): File | null {
|
|
|
|
|
const visited = new Set<any>()
|
|
|
|
|
const queue: any[] = [payload]
|
|
|
|
|
@ -836,7 +941,7 @@ function validateForm(): boolean {
|
|
|
|
|
formErrors.location = '请填写活动地点'
|
|
|
|
|
isValid = false
|
|
|
|
|
}
|
|
|
|
|
if (!form.ticket_note) {
|
|
|
|
|
if (!form.ticket_note && form.reservation_type !== 'none') {
|
|
|
|
|
formErrors.ticket_note = '请选择门票说明'
|
|
|
|
|
isValid = false
|
|
|
|
|
}
|
|
|
|
|
@ -856,7 +961,8 @@ async function submit() {
|
|
|
|
|
reservation_type: form.reservation_type,
|
|
|
|
|
location: form.location.trim(),
|
|
|
|
|
specific_time: form.specific_time.trim() || null,
|
|
|
|
|
offline_reservation_method: form.ticket_note === 'paid' ? 'paid' : 'free',
|
|
|
|
|
offline_reservation_method:
|
|
|
|
|
form.reservation_type === 'none' ? null : form.ticket_note === 'paid' ? 'paid' : 'free',
|
|
|
|
|
external_url: null,
|
|
|
|
|
title: form.title.trim(),
|
|
|
|
|
summary: form.summary.trim() || null,
|
|
|
|
|
@ -876,6 +982,7 @@ async function submit() {
|
|
|
|
|
reservation_notice: null,
|
|
|
|
|
open_time: null,
|
|
|
|
|
sort: isSuperAdmin() ? form.sort : undefined,
|
|
|
|
|
is_hot: isSuperAdmin() ? form.is_hot : undefined,
|
|
|
|
|
is_active: form.is_active,
|
|
|
|
|
}
|
|
|
|
|
if (isCreate.value) {
|
|
|
|
|
@ -899,6 +1006,11 @@ useUnsavedChangesGuard(hasUnsavedChanges, '活动表单有未保存改动,确
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await loadMe().catch(() => undefined)
|
|
|
|
|
const q = route.query.audit_status
|
|
|
|
|
const audit = Array.isArray(q) ? q[0] : q
|
|
|
|
|
if (audit === 'pending' || audit === 'approved' || audit === 'rejected') {
|
|
|
|
|
filters.audit_status = audit
|
|
|
|
|
}
|
|
|
|
|
await loadAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -938,12 +1050,12 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<a-space style="margin-bottom: 12px" wrap>
|
|
|
|
|
<a-input v-model="filters.keyword" placeholder="搜索活动名称" style="width: 220px" />
|
|
|
|
|
<a-select
|
|
|
|
|
v-if="!isVenueAdmin()"
|
|
|
|
|
v-model="filters.venue_id"
|
|
|
|
|
allow-clear
|
|
|
|
|
allow-search
|
|
|
|
|
placeholder="筛选场馆"
|
|
|
|
|
style="width: 220px"
|
|
|
|
|
:disabled="isVenueAdmin()"
|
|
|
|
|
>
|
|
|
|
|
<a-option v-for="v in venues" :key="v.id" :value="v.id">{{ v.name }}</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
@ -960,6 +1072,7 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<a-option value="phone">电话预约</a-option>
|
|
|
|
|
<a-option value="wechat_mp">公众号预约</a-option>
|
|
|
|
|
<a-option value="offline_visit">线下预约</a-option>
|
|
|
|
|
<a-option value="none">无需预约</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<a-select v-model="filters.audit_status" allow-clear placeholder="审核状态" style="width: 130px">
|
|
|
|
|
<a-option value="approved">已通过</a-option>
|
|
|
|
|
@ -967,7 +1080,6 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<a-option value="rejected">已退回</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<a-button type="primary" @click="onSearch">查询</a-button>
|
|
|
|
|
<a-button @click="resetFilters">重置</a-button>
|
|
|
|
|
<a-button type="primary" @click="openCreate">新增活动</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
|
|
|
|
|
@ -1026,14 +1138,27 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<a-tag v-else :color="auditStatusColor(record.audit_status)">{{ auditStatusLabel(record.audit_status) }}</a-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="操作" :width="300" :min-width="280" fixed="right" align="left">
|
|
|
|
|
<a-table-column v-if="isSuperAdmin()" title="热门" :width="80">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-switch
|
|
|
|
|
:model-value="(record as Activity).is_hot === true"
|
|
|
|
|
size="small"
|
|
|
|
|
@change="(v: unknown) => onHotFlagChange(record as Activity, v)"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="操作" :width="340" :min-width="320" fixed="right" align="left">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space wrap :size="4" justify="start">
|
|
|
|
|
<a-button type="text" @click="openEdit(record)">编辑</a-button>
|
|
|
|
|
<template v-if="isSuperAdmin() && (record.audit_status === 'pending' || record.audit_status === 'rejected')">
|
|
|
|
|
<a-button type="text" status="success" @click="approveActivity(record)">通过</a-button>
|
|
|
|
|
<a-button type="text" status="danger" @click="openRejectActivity(record)">退回</a-button>
|
|
|
|
|
<a-button type="text" @click="openAuditActivity(record)">审核</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="record.schedule_status === 'ended'"
|
|
|
|
|
type="text"
|
|
|
|
|
@click="openBehindScenesModal(record as Activity)"
|
|
|
|
|
>{{ activityHasBehindScenesImages(record as Activity) ? '查看花絮' : '上传花絮' }}</a-button>
|
|
|
|
|
<a-button type="text" status="warning" @click="toggleStatus(record)">{{ record.is_active ? '下架' : '上架' }}</a-button>
|
|
|
|
|
<a-popconfirm content="确认删除该活动?" @ok="removeActivity(record)">
|
|
|
|
|
<a-button type="text" status="danger">删除</a-button>
|
|
|
|
|
@ -1045,8 +1170,194 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<a-modal v-model:visible="rejectActivityVisible" title="退回活动" :on-before-ok="submitRejectActivity">
|
|
|
|
|
<a-textarea v-model="rejectActivityRemark" placeholder="退回说明(选填)" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="auditActivityVisible"
|
|
|
|
|
title="审核活动"
|
|
|
|
|
width="72%"
|
|
|
|
|
:body-style="auditModalBodyStyle"
|
|
|
|
|
>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<a-space wrap>
|
|
|
|
|
<a-button type="primary" status="success" @click="auditSubmitApprove(false)">通过</a-button>
|
|
|
|
|
<a-button type="primary" @click="auditSubmitApprove(true)">通过且为热门活动</a-button>
|
|
|
|
|
<a-button status="danger" @click="auditSubmitReject">退回</a-button>
|
|
|
|
|
<a-button @click="auditActivityVisible = false">取消</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
<div v-if="auditActivityRecord" class="activity-audit-readonly">
|
|
|
|
|
<div class="activity-audit-inline">
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动名称</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.title || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">举办场馆</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.venue?.name || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">联系人</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.contact_name || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">联系电话</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.contact_phone || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动日期</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ formatActivityTableDateRange(auditActivityRecord) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">具体时间</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.specific_time?.trim() ? auditActivityRecord.specific_time : '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">报名方式</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ reservationTypeLabel(auditActivityRecord.reservation_type) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="auditActivityRecord.reservation_type !== 'none'" class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">门票说明</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditTicketNoteLabel(auditActivityRecord) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="auditShowExternalUrl(auditActivityRecord)" class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">外链地址</span>
|
|
|
|
|
<div class="activity-audit-inline-value activity-audit-break">{{ auditActivityRecord.external_url || '—' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row activity-audit-inline-row--tags">
|
|
|
|
|
<span class="activity-audit-inline-label">标签</span>
|
|
|
|
|
<div class="activity-audit-inline-value">
|
|
|
|
|
<div class="activity-form-tags__chips activity-form-tags__chips--readonly activity-form-tags__chips--inline">
|
|
|
|
|
<a-tag v-for="(tag, index) in auditActivityRecord.tags || []" :key="`${tag}-${index}`" style="margin-bottom: 0">
|
|
|
|
|
{{ tag }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
<span v-if="!auditActivityRecord.tags?.length" class="activity-form-tags__empty">暂无标签</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">活动状态</span>
|
|
|
|
|
<div class="activity-audit-inline-value">
|
|
|
|
|
<a-tag v-if="auditActivityRecord.schedule_status" :color="scheduleStatusColor(auditActivityRecord.schedule_status)">
|
|
|
|
|
{{ scheduleStatusLabel(auditActivityRecord.schedule_status) }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
<span v-else class="activity-audit-muted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">上架状态</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.is_active ? '上架' : '下架' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">排序</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ String(auditActivityRecord.sort ?? 0) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="isSuperAdmin()" class="activity-audit-inline-row">
|
|
|
|
|
<span class="activity-audit-inline-label">热门活动</span>
|
|
|
|
|
<div class="activity-audit-inline-value">{{ auditActivityRecord.is_hot === true ? '是' : '否' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="activity-audit-stack">
|
|
|
|
|
<div class="activity-audit-stack__label">活动地点</div>
|
|
|
|
|
<div class="activity-audit-stack__body">
|
|
|
|
|
<div class="activity-address-coord-row">
|
|
|
|
|
<div class="activity-address-coord-row__address activity-audit-static-text activity-audit-static-text--fill">
|
|
|
|
|
{{ auditActivityRecord.location?.trim() ? auditActivityRecord.location : (auditActivityRecord.address || '—') }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-address-coord-row__lng activity-audit-static-coord">{{ auditCoordCell(auditActivityRecord.lng) }}</div>
|
|
|
|
|
<div class="activity-address-coord-row__lat activity-audit-static-coord">{{ auditCoordCell(auditActivityRecord.lat) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="activity-audit-stack">
|
|
|
|
|
<div class="activity-audit-stack__label">活动图片</div>
|
|
|
|
|
<div class="activity-audit-stack__body">
|
|
|
|
|
<div class="activity-cover-carousel-wrap">
|
|
|
|
|
<div class="activity-cover-carousel-row__col">
|
|
|
|
|
<div class="activity-cover-carousel-row__sub">活动封面图</div>
|
|
|
|
|
<template v-if="auditActivityRecord.cover_image">
|
|
|
|
|
<img
|
|
|
|
|
:src="auditActivityRecord.cover_image"
|
|
|
|
|
class="activity-cover-thumb"
|
|
|
|
|
alt=""
|
|
|
|
|
@click="openMediaPreview('image', auditActivityRecord.cover_image!)"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
<span v-else class="activity-audit-muted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="activity-cover-carousel-row__col">
|
|
|
|
|
<div class="activity-cover-carousel-row__sub">活动展示图片</div>
|
|
|
|
|
<div v-if="auditActivityRecord.gallery_media?.length" class="activity-gallery-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(item, idx) in auditActivityRecord.gallery_media"
|
|
|
|
|
:key="item.url + idx"
|
|
|
|
|
class="activity-gallery-item"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
v-if="item.type === 'image'"
|
|
|
|
|
:src="item.url"
|
|
|
|
|
class="activity-gallery-thumb"
|
|
|
|
|
alt=""
|
|
|
|
|
@click="openMediaPreview('image', item.url)"
|
|
|
|
|
/>
|
|
|
|
|
<video
|
|
|
|
|
v-else
|
|
|
|
|
:src="item.url"
|
|
|
|
|
controls
|
|
|
|
|
class="activity-gallery-thumb activity-gallery-thumb--video"
|
|
|
|
|
@click.stop="openMediaPreview('video', item.url)"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="activity-audit-muted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="activity-audit-stack">
|
|
|
|
|
<div class="activity-audit-stack__label">活动详情</div>
|
|
|
|
|
<div class="activity-audit-stack__body">
|
|
|
|
|
<div
|
|
|
|
|
v-if="auditActivityRecord.detail_html?.trim()"
|
|
|
|
|
class="activity-audit-rich"
|
|
|
|
|
v-html="auditActivityRecord.detail_html"
|
|
|
|
|
/>
|
|
|
|
|
<span v-else class="activity-audit-muted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="btsVisible"
|
|
|
|
|
:title="btsModalTitle"
|
|
|
|
|
width="1000px"
|
|
|
|
|
:body-style="btsModalBodyStyle"
|
|
|
|
|
:confirm-loading="btsSaving"
|
|
|
|
|
:on-before-ok="saveBehindScenes"
|
|
|
|
|
>
|
|
|
|
|
<a-space direction="vertical" fill style="width: 100%">
|
|
|
|
|
<a-upload
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
multiple
|
|
|
|
|
:on-before-upload="onBtsBeforeUpload"
|
|
|
|
|
>
|
|
|
|
|
<template #upload-button><a-button type="outline">添加图片</a-button></template>
|
|
|
|
|
</a-upload>
|
|
|
|
|
<div v-if="btsImages.length" class="bts-preview-grid">
|
|
|
|
|
<div v-for="(img, i) in btsImages" :key="i + '-' + img.url" class="bts-preview-item">
|
|
|
|
|
<img class="bts-preview-img" :src="img.url" @click="openMediaPreview('image', img.url)" />
|
|
|
|
|
<div class="bts-preview-actions">
|
|
|
|
|
<a-button size="mini" class="bts-act-btn" :disabled="i === 0" @click="moveBtsImage(i, -1)">上移</a-button>
|
|
|
|
|
<a-button size="mini" class="bts-act-btn" :disabled="i === btsImages.length - 1" @click="moveBtsImage(i, 1)">下移</a-button>
|
|
|
|
|
<a-button size="mini" class="bts-act-btn" status="danger" @click="removeBtsImage(i)">删除</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
@ -1109,12 +1420,13 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
<a-option value="phone">电话预约</a-option>
|
|
|
|
|
<a-option value="wechat_mp">公众号预约</a-option>
|
|
|
|
|
<a-option value="offline_visit">线下预约</a-option>
|
|
|
|
|
<a-option value="none">无需预约</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
<template v-if="formErrors.reservation_type" #help>
|
|
|
|
|
<span style="color: #f53f3f;">{{ formErrors.reservation_type }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="门票说明" required :help="formErrors.ticket_note">
|
|
|
|
|
<a-form-item v-if="form.reservation_type !== 'none'" label="门票说明" required :help="formErrors.ticket_note">
|
|
|
|
|
<a-select v-model="form.ticket_note">
|
|
|
|
|
<a-option value="free">免费</a-option>
|
|
|
|
|
<a-option value="paid">收费</a-option>
|
|
|
|
|
@ -1162,6 +1474,9 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
<a-form-item v-if="isSuperAdmin()" label="热门活动">
|
|
|
|
|
<a-switch v-model="form.is_hot" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="活动地点" required class="admin-modal-form__full" :help="formErrors.location">
|
|
|
|
|
<div class="activity-address-coord-row">
|
|
|
|
|
<a-input
|
|
|
|
|
@ -1412,5 +1727,158 @@ async function removeActivity(row: Activity) {
|
|
|
|
|
color: #86909c;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
.activity-form-tags__chips--readonly {
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
}
|
|
|
|
|
.activity-form-tags__chips--inline {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.activity-audit-inline {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
column-gap: 16px;
|
|
|
|
|
row-gap: 10px;
|
|
|
|
|
padding-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-inline-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.5715;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-inline-row--tags {
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-inline-label {
|
|
|
|
|
flex: 0 0 74px;
|
|
|
|
|
color: var(--color-text-3);
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-inline-value {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-stack {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-stack__label {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-stack__body {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
@media (max-width: 1400px) {
|
|
|
|
|
.activity-audit-inline {
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@media (max-width: 980px) {
|
|
|
|
|
.activity-audit-inline {
|
|
|
|
|
grid-template-columns: minmax(0, 1fr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.activity-audit-static-text {
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.5715;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-static-text--fill {
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
padding: 4px 11px;
|
|
|
|
|
border: 1px solid var(--color-neutral-3);
|
|
|
|
|
border-radius: var(--border-radius-small);
|
|
|
|
|
background: var(--color-fill-2);
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-static-coord {
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
padding: 4px 11px;
|
|
|
|
|
border: 1px solid var(--color-neutral-3);
|
|
|
|
|
border-radius: var(--border-radius-small);
|
|
|
|
|
background: var(--color-fill-2);
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
color: var(--color-text-2);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.5715;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-muted {
|
|
|
|
|
color: var(--color-text-3);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-break {
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-rich {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
border: 1px solid #e5e6eb;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
max-height: 380px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-rich :deep(img) {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
height: auto;
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
}
|
|
|
|
|
.activity-audit-rich :deep(video) {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.bts-preview-grid {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.bts-preview-item {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
width: 220px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
.bts-preview-img {
|
|
|
|
|
width: 220px;
|
|
|
|
|
height: 220px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e6eb;
|
|
|
|
|
cursor: zoom-in;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
.bts-preview-actions {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.bts-preview-actions :deep(.bts-act-btn.arco-btn-size-mini) {
|
|
|
|
|
flex: 1 1 0;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
padding-left: 6px;
|
|
|
|
|
padding-right: 6px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|