774 lines
24 KiB
Vue
774 lines
24 KiB
Vue
<template>
|
||
<ArtTableFullScreen>
|
||
<div class="h5-popup-configuration-page" id="table-full-screen">
|
||
<ArtSearchBar
|
||
v-model:filter="searchForm"
|
||
:items="searchFormItems"
|
||
label-width="110"
|
||
:show-expand="false"
|
||
@reset="handleReset"
|
||
@search="handleSearch"
|
||
/>
|
||
|
||
<ElCard shadow="never" class="art-table-card">
|
||
<ArtTableHeader
|
||
:columnList="columnOptions"
|
||
v-model:columns="columnChecks"
|
||
@refresh="handleRefresh"
|
||
>
|
||
<template #left>
|
||
<ElButton v-if="canCreate" type="primary" :icon="Plus" @click="openCreateDialog">
|
||
新增配置
|
||
</ElButton>
|
||
</template>
|
||
</ArtTableHeader>
|
||
|
||
<ArtTable
|
||
:loading="loading"
|
||
:data="configurationList"
|
||
:currentPage="pagination.page"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:marginTop="10"
|
||
:actions="getActions"
|
||
:actionsWidth="200"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
>
|
||
<template #default>
|
||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||
</template>
|
||
</ArtTable>
|
||
</ElCard>
|
||
</div>
|
||
|
||
<!-- 创建 / 编辑 -->
|
||
<ElDialog
|
||
v-model="formDialogVisible"
|
||
:title="formMode === 'create' ? '新增运营弹窗配置' : '编辑运营弹窗配置'"
|
||
width="680px"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
@closed="resetForm"
|
||
>
|
||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="110px">
|
||
<ElFormItem label="弹窗类型" prop="popup_type">
|
||
<ElSelect v-model="form.popup_type" placeholder="请选择弹窗类型" style="width: 100%">
|
||
<ElOption
|
||
v-for="item in POPUP_TYPE_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="弹窗标题" prop="title">
|
||
<ElInput
|
||
v-model="form.title"
|
||
maxlength="100"
|
||
show-word-limit
|
||
placeholder="请输入弹窗标题(1~100 字符)"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="弹窗正文" prop="content">
|
||
<ElInput
|
||
v-model="form.content"
|
||
type="textarea"
|
||
:rows="4"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
placeholder="请输入弹窗正文(1~2000 字符,不接受 HTML、URL 或前端路由)"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="命中页面" prop="pages">
|
||
<ElCheckboxGroup v-model="form.pages">
|
||
<ElCheckbox v-for="item in PAGE_OPTIONS" :key="item.value" :label="item.value">
|
||
{{ item.label }}
|
||
</ElCheckbox>
|
||
</ElCheckboxGroup>
|
||
</ElFormItem>
|
||
<ElFormItem label="投放频率" prop="frequency">
|
||
<ElSelect v-model="form.frequency" placeholder="请选择投放频率" style="width: 100%">
|
||
<ElOption
|
||
v-for="item in FREQUENCY_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="生效时间" prop="timeRange">
|
||
<ElDatePicker
|
||
v-model="form.timeRange"
|
||
type="datetimerange"
|
||
range-separator="至"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="优先级" prop="priority">
|
||
<ElInputNumber
|
||
v-model="form.priority"
|
||
:min="0"
|
||
:max="1000000"
|
||
:controls="false"
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="是否启用" prop="enabled">
|
||
<ElSwitch v-model="form.enabled" />
|
||
</ElFormItem>
|
||
<ElFormItem label="受控动作" prop="action_type">
|
||
<ElSelect v-model="form.action_type" placeholder="无" style="width: 100%">
|
||
<ElOption
|
||
v-for="item in ACTION_TYPE_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="店铺范围" prop="shop_ids">
|
||
<ElSelect
|
||
v-model="form.shop_ids"
|
||
multiple
|
||
filterable
|
||
remote
|
||
:remote-method="searchShops"
|
||
:loading="shopLoading"
|
||
placeholder="选择店铺(不选表示全量投放)"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="shop in shopOptions"
|
||
:key="shop.id"
|
||
:label="shop.shop_name"
|
||
:value="shop.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="设备类型范围" prop="device_types">
|
||
<ElSelect
|
||
v-model="form.device_types"
|
||
multiple
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
placeholder="选择或输入设备类型(不选表示全量投放)"
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="卡类型范围" prop="card_types">
|
||
<ElSelect
|
||
v-model="form.card_types"
|
||
multiple
|
||
filterable
|
||
placeholder="选择卡类型(不选表示全量投放)"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="item in CARD_TYPE_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="formDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="formLoading" @click="handleSubmit">
|
||
{{ formMode === 'create' ? '创建' : '保存' }}
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox, ElTag, FormInstance, FormRules } from 'element-plus'
|
||
import { Plus } from '@element-plus/icons-vue'
|
||
import { H5PopupConfigurationService, ShopService } from '@/api/modules'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import { normalizeApiError } from '@/utils/business/apiError'
|
||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||
import type { SearchFormItem } from '@/types'
|
||
import type {
|
||
H5PopupActionType,
|
||
H5PopupCardType,
|
||
H5PopupConfiguration,
|
||
H5PopupFrequency,
|
||
H5PopupPage,
|
||
H5PopupType
|
||
} from '@/types/api'
|
||
|
||
defineOptions({ name: 'H5PopupConfiguration' })
|
||
|
||
const { hasAuth } = useAuth()
|
||
const router = useRouter()
|
||
const perms = AUGUST_PERMISSIONS.h5PopupConfiguration
|
||
|
||
const canCreate = computed(() => hasAuth(perms.create))
|
||
const canUpdate = computed(() => hasAuth(perms.update))
|
||
const canEnable = computed(() => hasAuth(perms.enable))
|
||
const canDisable = computed(() => hasAuth(perms.disable))
|
||
|
||
const PAGE_NAMES: Record<H5PopupPage, string> = {
|
||
home: '首页',
|
||
asset_detail: '资产详情',
|
||
package_purchase: '套餐购买',
|
||
asset_wallet_recharge: '资产钱包充值'
|
||
}
|
||
const PAGE_OPTIONS: Array<{ label: string; value: H5PopupPage }> = [
|
||
{ label: '首页', value: 'home' },
|
||
{ label: '资产详情', value: 'asset_detail' },
|
||
{ label: '套餐购买', value: 'package_purchase' },
|
||
{ label: '资产钱包充值', value: 'asset_wallet_recharge' }
|
||
]
|
||
const POPUP_TYPE_NAMES: Record<H5PopupType, string> = {
|
||
promotion: '套餐政策推广',
|
||
announcement: '通用公告'
|
||
}
|
||
const POPUP_TYPE_OPTIONS: Array<{ label: string; value: H5PopupType }> = [
|
||
{ label: '套餐政策推广', value: 'promotion' },
|
||
{ label: '通用公告', value: 'announcement' }
|
||
]
|
||
const FREQUENCY_NAMES: Record<H5PopupFrequency, string> = {
|
||
once: '每客户每配置版本仅一次',
|
||
daily: '每客户每配置版本每个上海自然日一次'
|
||
}
|
||
const FREQUENCY_OPTIONS: Array<{ label: string; value: H5PopupFrequency }> = [
|
||
{ label: '仅一次(每配置版本)', value: 'once' },
|
||
{ label: '每个上海自然日一次(每配置版本)', value: 'daily' }
|
||
]
|
||
const ACTION_TYPE_NAMES: Record<string, string> = {
|
||
'': '无',
|
||
package_purchase: '套餐购买',
|
||
asset_wallet_recharge: '资产钱包充值'
|
||
}
|
||
const ACTION_TYPE_OPTIONS: Array<{ label: string; value: string }> = [
|
||
{ label: '无', value: '' },
|
||
{ label: '套餐购买', value: 'package_purchase' },
|
||
{ label: '资产钱包充值', value: 'asset_wallet_recharge' }
|
||
]
|
||
const CARD_TYPE_NAMES: Record<H5PopupCardType, string> = {
|
||
CMCC: '中国移动',
|
||
CUCC: '中国联通',
|
||
CTCC: '中国电信',
|
||
CBN: '中国广电'
|
||
}
|
||
const CARD_TYPE_OPTIONS: Array<{ label: string; value: H5PopupCardType }> = [
|
||
{ label: '中国移动', value: 'CMCC' },
|
||
{ label: '中国联通', value: 'CUCC' },
|
||
{ label: '中国电信', value: 'CTCC' },
|
||
{ label: '中国广电', value: 'CBN' }
|
||
]
|
||
|
||
const loading = ref(false)
|
||
const configurationList = ref<H5PopupConfiguration[]>([])
|
||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||
|
||
const initialSearchState = {
|
||
enabled: undefined as number | undefined
|
||
}
|
||
const searchForm = reactive({ ...initialSearchState })
|
||
|
||
const statusOptions = [
|
||
{ label: '启用', value: 1 },
|
||
{ label: '停用', value: 0 }
|
||
]
|
||
const searchFormItems: SearchFormItem[] = [
|
||
{
|
||
label: '启停状态',
|
||
prop: 'enabled',
|
||
type: 'select',
|
||
config: { clearable: true, placeholder: '全部' },
|
||
options: () => statusOptions
|
||
}
|
||
]
|
||
|
||
const columnOptions = [
|
||
{ label: '弹窗类型', prop: 'popup_type' },
|
||
{ label: '标题', prop: 'title' },
|
||
{ label: '命中页面', prop: 'pages' },
|
||
{ label: '范围', prop: 'scope' },
|
||
{ label: '优先级', prop: 'priority' },
|
||
{ label: '频率', prop: 'frequency_text' },
|
||
{ label: '启停状态', prop: 'enabled' },
|
||
{ label: '受控动作', prop: 'action_type' },
|
||
{ label: '生效时间', prop: 'effective_time' },
|
||
{ label: '版本', prop: 'version' },
|
||
{ label: '创建人', prop: 'creator' },
|
||
{ label: '最近更新人', prop: 'updater' },
|
||
{ label: '最近更新时间', prop: 'updated_at' }
|
||
]
|
||
|
||
const formatPages = (pages: H5PopupPage[] | null | undefined): string => {
|
||
if (!pages || pages.length === 0) return '全部'
|
||
return pages.map((page) => PAGE_NAMES[page] || page).join('、')
|
||
}
|
||
|
||
const formatCardTypes = (values: H5PopupCardType[] | null | undefined): string => {
|
||
if (!values || values.length === 0) return '全部'
|
||
return values.map((item) => CARD_TYPE_NAMES[item] || item).join('、')
|
||
}
|
||
|
||
const formatScopeColumn = (row: H5PopupConfiguration): string => {
|
||
const parts: string[] = []
|
||
parts.push(`店铺:${row.shop_ids?.length ? `${row.shop_ids.length}家` : '全部'}`)
|
||
parts.push(
|
||
`设备:${
|
||
row.device_types?.length
|
||
? row.device_types.slice(0, 3).join('、') + (row.device_types.length > 3 ? ' 等' : '')
|
||
: '全部'
|
||
}`
|
||
)
|
||
parts.push(`卡型:${formatCardTypes(row.card_types)}`)
|
||
return parts.join(';')
|
||
}
|
||
|
||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||
{
|
||
prop: 'popup_type',
|
||
label: '弹窗类型',
|
||
width: 120,
|
||
formatter: (row: H5PopupConfiguration) =>
|
||
row.popup_type_text || POPUP_TYPE_NAMES[row.popup_type] || row.popup_type
|
||
},
|
||
{
|
||
prop: 'title',
|
||
label: '标题',
|
||
minWidth: 180,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: H5PopupConfiguration) =>
|
||
h(
|
||
'span',
|
||
{
|
||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||
onClick: (event: MouseEvent) => {
|
||
event.stopPropagation()
|
||
goDetail(row)
|
||
}
|
||
},
|
||
row.title
|
||
)
|
||
},
|
||
{
|
||
prop: 'pages',
|
||
label: '命中页面',
|
||
minWidth: 180,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: H5PopupConfiguration) => formatPages(row.pages)
|
||
},
|
||
{
|
||
prop: 'scope',
|
||
label: '范围',
|
||
minWidth: 200,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: H5PopupConfiguration) => formatScopeColumn(row)
|
||
},
|
||
{ prop: 'priority', label: '优先级', width: 90 },
|
||
{
|
||
prop: 'frequency_text',
|
||
label: '频率',
|
||
minWidth: 150,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: H5PopupConfiguration) =>
|
||
row.frequency_text || FREQUENCY_NAMES[row.frequency] || row.frequency
|
||
},
|
||
{
|
||
prop: 'enabled',
|
||
label: '启停状态',
|
||
width: 90,
|
||
formatter: (row: H5PopupConfiguration) =>
|
||
h(
|
||
ElTag,
|
||
{ type: row.enabled ? 'success' : 'info' },
|
||
() => row.enabled_text || (row.enabled ? '启用' : '停用')
|
||
)
|
||
},
|
||
{
|
||
prop: 'action_type',
|
||
label: '受控动作',
|
||
width: 120,
|
||
formatter: (row: H5PopupConfiguration) => ACTION_TYPE_NAMES[row.action_type || ''] || '无'
|
||
},
|
||
{
|
||
prop: 'effective_time',
|
||
label: '生效时间',
|
||
minWidth: 170,
|
||
formatter: (row: H5PopupConfiguration) =>
|
||
`${formatDateTime(row.starts_at)} ~ ${formatDateTime(row.ends_at)}`
|
||
},
|
||
{ prop: 'version', label: '版本', width: 70 },
|
||
{
|
||
prop: 'creator',
|
||
label: '创建人',
|
||
width: 90,
|
||
formatter: (row: H5PopupConfiguration) => String(row.creator || 0)
|
||
},
|
||
{
|
||
prop: 'updater',
|
||
label: '最近更新人',
|
||
width: 110,
|
||
formatter: (row: H5PopupConfiguration) => String(row.updater || 0)
|
||
},
|
||
{
|
||
prop: 'updated_at',
|
||
label: '最近更新时间',
|
||
minWidth: 170,
|
||
formatter: (row: H5PopupConfiguration) => formatDateTime(row.updated_at)
|
||
}
|
||
])
|
||
|
||
// ========== 列表数据 ==========
|
||
|
||
const getTableData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const res = await H5PopupConfigurationService.getConfigurations({
|
||
page: pagination.page,
|
||
page_size: pagination.pageSize,
|
||
enabled:
|
||
searchForm.enabled === undefined || searchForm.enabled === null
|
||
? undefined
|
||
: searchForm.enabled === 1
|
||
})
|
||
if (res.code === 0 && res.data) {
|
||
configurationList.value = res.data.items || []
|
||
pagination.total = res.data.total || 0
|
||
} else {
|
||
ElMessage.error(res.msg || '获取运营弹窗配置列表失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取运营弹窗配置列表失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '获取运营弹窗配置列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleReset = () => {
|
||
Object.assign(searchForm, { ...initialSearchState })
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
const handleSearch = () => {
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
const handleRefresh = () => {
|
||
getTableData()
|
||
}
|
||
|
||
const handleSizeChange = (pageSize: number) => {
|
||
pagination.pageSize = pageSize
|
||
getTableData()
|
||
}
|
||
|
||
const handleCurrentChange = (page: number) => {
|
||
pagination.page = page
|
||
getTableData()
|
||
}
|
||
|
||
const refreshRow = (updated: H5PopupConfiguration) => {
|
||
const index = configurationList.value.findIndex((item) => item.id === updated.id)
|
||
if (index >= 0) {
|
||
configurationList.value.splice(index, 1, updated)
|
||
}
|
||
}
|
||
|
||
// ========== 行操作 ==========
|
||
|
||
const goDetail = (row: H5PopupConfiguration) => {
|
||
router.push({
|
||
name: 'H5PopupConfigurationDetail',
|
||
params: { id: String(row.id) }
|
||
})
|
||
}
|
||
|
||
const getActions = (row: H5PopupConfiguration) => {
|
||
const actions: any[] = []
|
||
if (canUpdate.value) {
|
||
actions.push({ label: '编辑', handler: () => openEditDialog(row) })
|
||
}
|
||
if (canEnable.value && !row.enabled) {
|
||
actions.push({ label: '启用', handler: () => handleEnable(row), type: 'success' })
|
||
}
|
||
if (canDisable.value && row.enabled) {
|
||
actions.push({ label: '停用', handler: () => handleDisable(row), type: 'danger' })
|
||
}
|
||
return actions
|
||
}
|
||
|
||
const handleEnable = async (row: H5PopupConfiguration) => {
|
||
try {
|
||
const res = await H5PopupConfigurationService.enableConfiguration(row.id)
|
||
if (res.code === 0 && res.data) {
|
||
refreshRow(res.data)
|
||
ElMessage.success('启用后参与候选匹配')
|
||
} else {
|
||
ElMessage.error(res.msg || '启用失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('启用运营弹窗配置失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '启用失败')
|
||
}
|
||
}
|
||
|
||
const handleDisable = async (row: H5PopupConfiguration) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'停用后停止新投放,历史通知在展示期内仍可见。确认停用该配置吗?',
|
||
'停用确认',
|
||
{ type: 'warning', confirmButtonText: '确认停用', cancelButtonText: '取消' }
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
const res = await H5PopupConfigurationService.disableConfiguration(row.id)
|
||
if (res.code === 0 && res.data) {
|
||
refreshRow(res.data)
|
||
ElMessage.success('停用后停止新投放,历史通知在展示期内仍可见')
|
||
} else {
|
||
ElMessage.error(res.msg || '停用失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('停用运营弹窗配置失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '停用失败')
|
||
}
|
||
}
|
||
|
||
// ========== 创建 / 编辑 ==========
|
||
|
||
const formDialogVisible = ref(false)
|
||
const formLoading = ref(false)
|
||
const formMode = ref<'create' | 'edit'>('create')
|
||
const formRef = ref<FormInstance>()
|
||
const shopOptions = ref<Array<{ id: number; shop_name: string }>>([])
|
||
const shopLoading = ref(false)
|
||
|
||
const emptyForm = (): {
|
||
id: number
|
||
popup_type: H5PopupType
|
||
title: string
|
||
content: string
|
||
pages: H5PopupPage[]
|
||
priority: number
|
||
frequency: H5PopupFrequency
|
||
enabled: boolean
|
||
action_type: H5PopupActionType | ''
|
||
shop_ids: number[]
|
||
device_types: string[]
|
||
card_types: H5PopupCardType[]
|
||
timeRange: string[]
|
||
} => ({
|
||
id: 0,
|
||
popup_type: 'promotion',
|
||
title: '',
|
||
content: '',
|
||
pages: [],
|
||
priority: 0,
|
||
frequency: 'once',
|
||
enabled: true,
|
||
action_type: '',
|
||
shop_ids: [],
|
||
device_types: [],
|
||
card_types: [],
|
||
timeRange: []
|
||
})
|
||
|
||
const form = reactive(emptyForm())
|
||
|
||
const resetForm = () => {
|
||
Object.assign(form, emptyForm())
|
||
formRef.value?.clearValidate()
|
||
}
|
||
|
||
const toPickerTime = (iso: string): string => {
|
||
if (!iso) return ''
|
||
const date = new Date(iso)
|
||
if (Number.isNaN(date.getTime())) return iso
|
||
const pad = (value: number) => String(value).padStart(2, '0')
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
|
||
date.getHours()
|
||
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}+08:00`
|
||
}
|
||
|
||
const searchShops = async (query: string = '') => {
|
||
shopLoading.value = true
|
||
try {
|
||
const res = await ShopService.getShops({
|
||
page: 1,
|
||
page_size: 20,
|
||
shop_name: query || undefined
|
||
})
|
||
if (res.code === 0) {
|
||
shopOptions.value = res.data.items || []
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索店铺失败:', error)
|
||
} finally {
|
||
shopLoading.value = false
|
||
}
|
||
}
|
||
|
||
const loadShopNames = async (ids: number[]) => {
|
||
const missing = ids.filter((id) => !shopOptions.value.some((shop) => shop.id === id))
|
||
if (missing.length === 0) return
|
||
await Promise.all(
|
||
missing.slice(0, 50).map(async (id) => {
|
||
try {
|
||
const res = await ShopService.getShops({ id, page: 1, page_size: 1 })
|
||
if (res.code === 0 && res.data.items?.length) {
|
||
shopOptions.value = [...shopOptions.value, res.data.items[0]]
|
||
}
|
||
} catch (error) {
|
||
console.error('获取店铺信息失败:', error)
|
||
}
|
||
})
|
||
)
|
||
}
|
||
|
||
const formRules: FormRules = {
|
||
popup_type: [{ required: true, message: '请选择弹窗类型', trigger: 'change' }],
|
||
title: [
|
||
{ required: true, message: '请输入弹窗标题', trigger: 'blur' },
|
||
{ min: 1, max: 100, message: '弹窗标题长度为 1~100 字符', trigger: 'blur' }
|
||
],
|
||
content: [
|
||
{ required: true, message: '请输入弹窗正文', trigger: 'blur' },
|
||
{ min: 1, max: 2000, message: '弹窗正文长度为 1~2000 字符', trigger: 'blur' }
|
||
],
|
||
pages: [
|
||
{
|
||
validator: (_rule, value: H5PopupPage[], callback) => {
|
||
if (!value || value.length === 0) callback(new Error('请至少选择一个命中页面'))
|
||
else callback()
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
],
|
||
frequency: [{ required: true, message: '请选择投放频率', trigger: 'change' }],
|
||
timeRange: [
|
||
{
|
||
validator: (_rule, value: string[] | null, callback) => {
|
||
if (!value || value.length !== 2) {
|
||
callback(new Error('请选择生效时间'))
|
||
return
|
||
}
|
||
if (new Date(value[1]).getTime() < new Date(value[0]).getTime()) {
|
||
callback(new Error('生效结束时间不得早于开始时间'))
|
||
return
|
||
}
|
||
callback()
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
const openCreateDialog = () => {
|
||
formMode.value = 'create'
|
||
resetForm()
|
||
formDialogVisible.value = true
|
||
shopOptions.value = []
|
||
searchShops('')
|
||
}
|
||
|
||
const openEditDialog = async (row: H5PopupConfiguration) => {
|
||
formMode.value = 'edit'
|
||
Object.assign(form, {
|
||
id: row.id,
|
||
popup_type: row.popup_type,
|
||
title: row.title,
|
||
content: row.content,
|
||
pages: [...(row.pages || [])],
|
||
priority: row.priority,
|
||
frequency: row.frequency,
|
||
enabled: row.enabled,
|
||
action_type: row.action_type || '',
|
||
shop_ids: [...(row.shop_ids || [])],
|
||
device_types: [...(row.device_types || [])],
|
||
card_types: [...(row.card_types || [])],
|
||
timeRange: [toPickerTime(row.starts_at), toPickerTime(row.ends_at)]
|
||
})
|
||
formDialogVisible.value = true
|
||
shopOptions.value = []
|
||
await Promise.all([searchShops(''), loadShopNames(form.shop_ids)])
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
if (!(await formRef.value?.validate().catch(() => false))) return
|
||
formLoading.value = true
|
||
const payload = {
|
||
popup_type: form.popup_type,
|
||
title: form.title.trim(),
|
||
content: form.content.trim(),
|
||
pages: [...form.pages],
|
||
priority: form.priority,
|
||
frequency: form.frequency,
|
||
starts_at: form.timeRange[0],
|
||
ends_at: form.timeRange[1],
|
||
enabled: form.enabled,
|
||
shop_ids: [...form.shop_ids],
|
||
device_types: [...form.device_types],
|
||
card_types: [...form.card_types]
|
||
}
|
||
try {
|
||
if (formMode.value === 'create') {
|
||
const res = await H5PopupConfigurationService.createConfiguration({
|
||
...payload,
|
||
action_type: form.action_type || null
|
||
})
|
||
if (res.code === 0 && res.data) {
|
||
ElMessage.success('创建成功')
|
||
formDialogVisible.value = false
|
||
getTableData()
|
||
} else {
|
||
ElMessage.error(res.msg || '创建失败')
|
||
}
|
||
} else {
|
||
const res = await H5PopupConfigurationService.updateConfiguration(form.id, {
|
||
id: form.id,
|
||
...payload,
|
||
action_type: form.action_type
|
||
})
|
||
if (res.code === 0 && res.data) {
|
||
ElMessage.success(
|
||
'保存成功:每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次'
|
||
)
|
||
formDialogVisible.value = false
|
||
getTableData()
|
||
} else {
|
||
ElMessage.error(res.msg || '保存失败')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('保存运营弹窗配置失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '保存失败')
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
getTableData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.h5-popup-configuration-page {
|
||
height: 100%;
|
||
}
|
||
</style>
|