fix: some
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled

This commit is contained in:
luo
2026-09-20 10:10:50 +08:00
parent e37d4902ca
commit 085a83d4e4
19 changed files with 744 additions and 424 deletions

View File

@@ -100,18 +100,6 @@
当前暂无可用在线支付方式,暂时无法提交充值。
</div>
</ElFormItem>
<ElFormItem v-if="createMode === 'offline'" label="店铺" prop="shop_id">
<ElCascader
v-model="createForm.shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺"
filterable
clearable
style="width: 100%"
@change="handleShopChange"
/>
</ElFormItem>
<ElFormItem
v-if="createForm.payment_method === 'offline'"
label="支付凭证"
@@ -127,26 +115,6 @@
@change="handleVoucherChange"
/>
</ElFormItem>
<ElFormItem
v-if="createMode === 'offline'"
label="收款方式"
prop="offline_payment_method_id"
>
<ElSelect
v-model="createForm.offline_payment_method_id"
placeholder="请选择收款方式"
style="width: 100%"
filterable
:loading="paymentMethodOptionsLoading"
>
<ElOption
v-for="item in offlinePaymentMethodOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.payment_method === 'offline'"
label="交易流水号"
@@ -169,6 +137,47 @@
<div class="external-transaction-tip">
识别结果仅供参考,请对照凭证核对后再提交。
</div>
<div v-if="ocrLoading" class="ocr-progress">
<ElProgress
:percentage="100"
:indeterminate="true"
:show-text="false"
:stroke-width="10"
/>
<div class="ocr-progress__text">正在识别支付凭证,请稍候...</div>
</div>
</ElFormItem>
<ElFormItem v-if="createMode === 'offline'" label="店铺" prop="shop_id">
<ElCascader
v-model="createForm.shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺"
filterable
clearable
style="width: 100%"
@change="handleShopChange"
/>
</ElFormItem>
<ElFormItem
v-if="createMode === 'offline'"
label="收款方式"
prop="offline_payment_method_id"
>
<ElSelect
v-model="createForm.offline_payment_method_id"
placeholder="请选择收款方式"
style="width: 100%"
filterable
:loading="paymentMethodOptionsLoading"
>
<ElOption
v-for="item in offlinePaymentMethodOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="createMode === 'offline'" label="其他凭证">
<VoucherUpload
@@ -1624,6 +1633,17 @@
color: var(--el-color-danger);
}
.ocr-progress {
margin-top: 10px;
&__text {
margin-top: 6px;
font-size: 13px;
font-weight: 500;
color: var(--el-color-primary);
}
}
.external-transaction-row {
display: flex;
gap: 8px;

View File

@@ -18,6 +18,15 @@
@uploading-change="voucherUploading = $event"
@change="handleVoucherChange"
/>
<div v-if="ocrLoading" class="ocr-progress">
<div class="ocr-progress__text">正在识别付款凭证,请稍候...</div>
<ElProgress
:percentage="100"
:indeterminate="true"
:show-text="false"
:stroke-width="8"
/>
</div>
</ElFormItem>
<ElFormItem label="外部交易流水号" prop="external_transaction_no">
@@ -67,81 +76,106 @@
<ElFormItem label="核销账单" required>
<div class="bill-selector">
<div class="bill-selector__filters">
<div class="bill-selector__row">
<ElSelect
v-model="billSearch.source_type"
placeholder="来源"
clearable
class="bill-selector__filter"
>
<ElOption
v-for="opt in BILL_SOURCE_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
<ElRow :gutter="8" class="bill-selector__row">
<ElCol :span="6">
<ElSelect
v-model="billSearch.source_type"
placeholder="来源"
clearable
class="bill-selector__filter"
>
<ElOption
v-for="opt in BILL_SOURCE_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</ElSelect>
</ElCol>
<ElCol :span="6">
<ElInput
v-model="billSearch.source_no"
placeholder="来源单号"
clearable
class="bill-selector__filter"
/>
</ElSelect>
<ElInput
v-model="billSearch.source_no"
placeholder="来源单号"
clearable
class="bill-selector__filter"
/>
<ElSelect
v-model="billSearch.status"
placeholder="核销状态"
clearable
class="bill-selector__filter"
>
<ElOption
v-for="opt in BILL_STATUS_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
</ElCol>
<ElCol :span="6">
<ElSelect
v-model="billSearch.status"
placeholder="核销状态"
clearable
class="bill-selector__filter"
>
<ElOption
v-for="opt in BILL_STATUS_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</ElSelect>
</ElCol>
<ElCol :span="6">
<ElSelect
v-model="billSearch.customer_id"
placeholder="店铺"
clearable
filterable
remote
reserve-keyword
:remote-method="searchBillShops"
class="bill-selector__filter"
>
<ElOption
v-for="shop in shopOptions"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElCol>
</ElRow>
<ElRow :gutter="8" class="bill-selector__row">
<ElCol :span="6">
<ElDatePicker
v-model="billSearch.start_time"
type="date"
placeholder="开始日期"
value-format="YYYY-MM-DD"
clearable
class="bill-selector__filter"
style="width: 100%; min-width: 0"
/>
</ElSelect>
<ElSelect
v-model="billSearch.customer_id"
placeholder="店铺"
clearable
filterable
remote
reserve-keyword
:remote-method="searchBillShops"
class="bill-selector__filter"
>
<ElOption
v-for="shop in shopOptions"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
</ElCol>
<ElCol :span="6">
<ElDatePicker
v-model="billSearch.end_time"
type="date"
placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
class="bill-selector__filter"
style="width: 100%; min-width: 0"
/>
</ElSelect>
</div>
<div class="bill-selector__row">
<ElDatePicker
v-model="billSearch.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
class="bill-selector__filter bill-selector__filter--span2"
/>
<ElButton
class="bill-selector__filter bill-selector__action"
@click="handleBillReset"
>
重置
</ElButton>
<ElButton
type="primary"
class="bill-selector__filter bill-selector__action"
@click="handleBillSearch"
>
搜索
</ElButton>
</div>
</ElCol>
<ElCol :span="6">
<ElButton
class="bill-selector__filter bill-selector__action"
@click="handleBillReset"
>
重置
</ElButton>
</ElCol>
<ElCol :span="6">
<ElButton
type="primary"
class="bill-selector__filter bill-selector__action"
@click="handleBillSearch"
>
搜索
</ElButton>
</ElCol>
</ElRow>
</div>
<div v-if="billsLoading" class="bill-selector__empty">加载中...</div>
@@ -340,9 +374,8 @@
source_no: '',
status: undefined as number | undefined,
customer_id: undefined as number | undefined,
dateRange: [] as string[],
created_from: '',
created_to: ''
start_time: '',
end_time: ''
})
const billPagination = reactive({
@@ -520,8 +553,8 @@
source_no: billSearch.source_no.trim() || undefined,
status: billSearch.status,
customer_id: billSearch.customer_id,
created_from: billSearch.created_from || undefined,
created_to: billSearch.created_to || undefined
start_time: billSearch.start_time || undefined,
end_time: billSearch.end_time || undefined
})
const loadCandidateBills = async () => {
@@ -565,18 +598,7 @@
}
}
const syncBillCreatedRange = () => {
if (Array.isArray(billSearch.dateRange) && billSearch.dateRange.length === 2) {
billSearch.created_from = billSearch.dateRange[0]
billSearch.created_to = billSearch.dateRange[1]
} else {
billSearch.created_from = ''
billSearch.created_to = ''
}
}
const handleBillSearch = () => {
syncBillCreatedRange()
billPagination.page = 1
void loadCandidateBills()
}
@@ -586,9 +608,8 @@
billSearch.source_no = ''
billSearch.status = undefined
billSearch.customer_id = undefined
billSearch.dateRange = []
billSearch.created_from = ''
billSearch.created_to = ''
billSearch.start_time = ''
billSearch.end_time = ''
billPagination.page = 1
void loadCandidateBills()
}
@@ -650,9 +671,8 @@
billSearch.source_no = ''
billSearch.status = undefined
billSearch.customer_id = undefined
billSearch.dateRange = []
billSearch.created_from = ''
billSearch.created_to = ''
billSearch.start_time = ''
billSearch.end_time = ''
billPagination.page = 1
billPagination.total = 0
candidateBills.value = []
@@ -863,10 +883,7 @@
}
&__row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
align-items: center;
width: 100%;
}
&__filter {
@@ -874,10 +891,6 @@
min-width: 0;
}
&__filter--span2 {
grid-column: span 2;
}
&__action {
width: 100%;
margin: 0;
@@ -953,4 +966,20 @@
line-height: 18px;
color: var(--el-text-color-secondary);
}
.ocr-progress {
width: 100%;
margin-top: 12px;
padding: 10px 12px;
background: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 8px;
&__text {
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: var(--el-color-primary);
}
}
</style>

View File

@@ -56,15 +56,14 @@
<template #header>
<div class="block-title">付款凭证</div>
</template>
<ElButton
<PaymentVoucherDialog
v-if="
voucherKeys.length &&
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationViewVoucher)
"
@click="voucherVisible = true"
>
查看付款凭证
</ElButton>
inline
:file-keys="voucherKeys"
/>
<span v-else class="empty-text">暂无付款凭证</span>
</ElCard>
@@ -102,8 +101,6 @@
</ElTimelineItem>
</ElTimeline>
</ElCard>
<PaymentVoucherDialog :file-keys="voucherKeys" @close="voucherVisible = false" />
</div>
</template>
@@ -150,7 +147,6 @@
const loading = ref(false)
const detail = ref<EmployeeCollectionApplicationDetailData | null>(null)
const voucherVisible = ref(false)
const application = computed<EmployeeCollectionApplication | null>(
() => detail.value?.application || null
@@ -169,7 +165,11 @@
{
title: '申请信息',
fields: [
{ label: '申请编号', prop: 'id', formatter: (value) => (value ? `#${value}` : '-') },
{
label: '外部交易流水号',
prop: 'external_transaction_no',
formatter: (value) => value || '-'
},
{ label: '收款方式', prop: 'payment_method_name', formatter: (value) => value || '-' },
{
label: '付款金额',
@@ -181,11 +181,6 @@
prop: 'paid_at',
formatter: (value) => formatDateTime(value)
},
{
label: '外部交易流水号',
prop: 'external_transaction_no',
formatter: (value) => value || '-'
},
{
label: '状态',
prop: 'status',

View File

@@ -136,7 +136,7 @@
]
const columnOptions = [
{ label: '申请编号', prop: 'id' },
{ label: '外部交易流水号', prop: 'external_transaction_no' },
{ label: '收款方式', prop: 'payment_method_name' },
{ label: '付款金额', prop: 'paid_amount' },
{ label: '状态', prop: 'status' },
@@ -149,9 +149,9 @@
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: '申请编号',
minWidth: 140,
prop: 'external_transaction_no',
label: '外部交易流水号',
minWidth: 220,
formatter: (row: EmployeeCollectionApplication) =>
h(
'span',
@@ -159,7 +159,7 @@
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: () => handleViewDetail(row)
},
`#${row.id}`
row.external_transaction_no || '-'
)
},
{

View File

@@ -167,8 +167,8 @@
status: undefined as number | undefined,
customer_id: undefined as number | undefined,
dateRange: [] as string[],
created_from: '',
created_to: ''
start_time: '',
end_time: ''
}
const searchForm = reactive({ ...initialSearchState })
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
@@ -258,8 +258,8 @@
source_no: searchForm.source_no.trim() || undefined,
status: searchForm.status,
customer_id: searchForm.customer_id,
created_from: searchForm.created_from || undefined,
created_to: searchForm.created_to || undefined
start_time: searchForm.start_time || undefined,
end_time: searchForm.end_time || undefined
})
const searchShops = async (query: string) => {
@@ -391,11 +391,11 @@
const handleSearch = () => {
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
searchForm.created_from = searchForm.dateRange[0]
searchForm.created_to = searchForm.dateRange[1]
searchForm.start_time = searchForm.dateRange[0]
searchForm.end_time = searchForm.dateRange[1]
} else {
searchForm.created_from = ''
searchForm.created_to = ''
searchForm.start_time = ''
searchForm.end_time = ''
}
pagination.page = 1
reload()

View File

@@ -0,0 +1,226 @@
<template>
<div class="h5-popup-configuration-detail-page">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">H5运营弹窗配置详情</h2>
</div>
<DetailPage v-if="configuration" :sections="detailSections" :data="configuration" />
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
<ElEmpty v-else-if="!configuration" description="未找到运营弹窗配置" />
</ElCard>
</div>
</template>
<script setup lang="ts">
import { computed, h, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElButton, ElCard, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { H5PopupConfigurationService } from '@/api/modules'
import type { H5PopupConfiguration, H5PopupPage, H5PopupType } from '@/types/api'
import { RoutesAlias } from '@/router/routesAlias'
import { formatDateTime } from '@/utils/business/format'
import { getErrorMessage } from '@/utils/business'
defineOptions({ name: 'H5PopupConfigurationDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const configuration = ref<H5PopupConfiguration | null>(null)
const popupTypeNames: Record<H5PopupType, string> = {
promotion: '套餐政策推广',
announcement: '通用公告'
}
const pageNames: Record<H5PopupPage, string> = {
home: '首页',
asset_detail: '资产详情',
package_purchase: '套餐购买',
asset_wallet_recharge: '资产钱包充值'
}
const frequencyNames = {
once: '每客户每配置版本仅一次',
daily: '每客户每配置版本每个上海自然日一次'
}
const actionTypeNames: Record<string, string> = {
package_purchase: '套餐购买',
asset_wallet_recharge: '资产钱包充值'
}
const cardTypeNames: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
}
const formatList = (
values: Array<string | number> | null | undefined,
formatter?: (value: string) => string
) => {
if (!values?.length) return '全部'
return values.map((value) => formatter?.(String(value)) || String(value)).join('、')
}
const detailSections = computed((): DetailSection[] => [
{
title: '基础信息',
fields: [
{ label: '配置ID', prop: 'id' },
{ label: '版本', prop: 'version' },
{
label: '弹窗类型',
render: (data: H5PopupConfiguration) =>
h(
ElTag,
{ type: 'primary' },
() => data.popup_type_text || popupTypeNames[data.popup_type]
)
},
{
label: '启停状态',
render: (data: H5PopupConfiguration) =>
h(
ElTag,
{ type: data.enabled ? 'success' : 'info' },
() => data.enabled_text || (data.enabled ? '启用' : '停用')
)
},
{ label: '标题', prop: 'title', fullWidth: true },
{ label: '正文', prop: 'content', fullWidth: true }
]
},
{
title: '投放配置',
fields: [
{
label: '命中页面',
formatter: (_value: unknown, data: H5PopupConfiguration) =>
formatList(data.pages, (value) => pageNames[value as H5PopupPage] || value),
fullWidth: true
},
{ label: '优先级', prop: 'priority' },
{
label: '投放频率',
formatter: (_value: unknown, data: H5PopupConfiguration) =>
data.frequency_text || frequencyNames[data.frequency] || data.frequency
},
{
label: '受控动作',
formatter: (_value: unknown, data: H5PopupConfiguration) =>
actionTypeNames[data.action_type || ''] || '无'
},
{
label: '生效开始',
formatter: (_value: unknown, data: H5PopupConfiguration) => formatDateTime(data.starts_at)
},
{
label: '生效结束',
formatter: (_value: unknown, data: H5PopupConfiguration) => formatDateTime(data.ends_at)
}
]
},
{
title: '投放范围与审计信息',
fields: [
{
label: '店铺范围',
formatter: (_value: unknown, data: H5PopupConfiguration) => formatList(data.shop_ids),
fullWidth: true
},
{
label: '设备类型范围',
formatter: (_value: unknown, data: H5PopupConfiguration) => formatList(data.device_types),
fullWidth: true
},
{
label: '卡类型范围',
formatter: (_value: unknown, data: H5PopupConfiguration) =>
formatList(data.card_types, (value) => cardTypeNames[value] || value),
fullWidth: true
},
{ label: '创建人', prop: 'creator' },
{
label: '创建时间',
formatter: (_value: unknown, data: H5PopupConfiguration) =>
formatDateTime(data.created_at)
},
{ label: '最近更新人', prop: 'updater' },
{
label: '最近更新时间',
formatter: (_value: unknown, data: H5PopupConfiguration) =>
formatDateTime(data.updated_at)
}
]
}
])
const loadDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('无效的配置ID')
return
}
loading.value = true
try {
const res = await H5PopupConfigurationService.getConfiguration(id)
if (res.code === 0) configuration.value = res.data
else ElMessage.error(res.msg || '获取运营弹窗配置详情失败')
} catch (error) {
ElMessage.error(getErrorMessage(error, '获取运营弹窗配置详情失败'))
} finally {
loading.value = false
}
}
const handleBack = () => {
router.push(RoutesAlias.H5PopupConfigurations)
}
onMounted(() => void loadDetail())
</script>
<style scoped lang="scss">
.h5-popup-configuration-detail-page {
height: 100%;
}
.detail-header {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.detail-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.loading-container {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
min-height: 240px;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -52,6 +52,16 @@
@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"
@@ -174,60 +184,12 @@
</ElButton>
</template>
</ElDialog>
<!-- 详情 -->
<ElDialog v-model="detailDialogVisible" title="运营弹窗配置详情" width="680px">
<div v-if="detailLoading" class="detail-loading">加载中...</div>
<ElDescriptions v-else-if="detail" :column="2" border>
<ElDescriptionsItem label="配置ID">{{ detail.id }}</ElDescriptionsItem>
<ElDescriptionsItem label="版本">{{ detail.version }}</ElDescriptionsItem>
<ElDescriptionsItem label="标题" :span="2">{{ detail.title }}</ElDescriptionsItem>
<ElDescriptionsItem label="正文" :span="2">
<span class="detail-content">{{ detail.content }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="命中页面" :span="2">
{{ formatPages(detail.pages) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="店铺范围">
{{ formatScope(detail.shop_ids) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="设备类型范围">
{{ formatScope(detail.device_types) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="卡类型范围">
{{ formatCardTypes(detail.card_types) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="优先级">{{ detail.priority }}</ElDescriptionsItem>
<ElDescriptionsItem label="投放频率">
{{ detail.frequency_text || FREQUENCY_NAMES[detail.frequency] || detail.frequency }}
</ElDescriptionsItem>
<ElDescriptionsItem label="启停状态">
{{ detail.enabled_text || (detail.enabled ? '启用' : '停用') }}
</ElDescriptionsItem>
<ElDescriptionsItem label="受控动作">
{{ ACTION_TYPE_NAMES[detail.action_type] || '无' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="生效开始" :span="2">
{{ formatDateTime(detail.starts_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="生效结束" :span="2">
{{ formatDateTime(detail.ends_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人">{{ detail.creator }}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(detail.created_at)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最近更新人">{{ detail.updater }}</ElDescriptionsItem>
<ElDescriptionsItem label="最近更新时间">{{
formatDateTime(detail.updated_at)
}}</ElDescriptionsItem>
</ElDescriptions>
</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'
@@ -242,12 +204,14 @@
H5PopupCardType,
H5PopupConfiguration,
H5PopupFrequency,
H5PopupPage
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))
@@ -267,6 +231,14 @@
{ 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: '每客户每配置版本每个上海自然日一次'
@@ -322,6 +294,7 @@
]
const columnOptions = [
{ label: '弹窗类型', prop: 'popup_type' },
{ label: '标题', prop: 'title' },
{ label: '命中页面', prop: 'pages' },
{ label: '范围', prop: 'scope' },
@@ -341,15 +314,6 @@
return pages.map((page) => PAGE_NAMES[page] || page).join('、')
}
const formatScope = (values: string[] | number[] | null | undefined): string => {
if (!values || values.length === 0) return '全部'
const text = values
.slice(0, 3)
.map((item) => (typeof item === 'number' && item !== 0 ? `#${item}` : String(item)))
.join('、')
return values.length > 3 ? `${text} 等${values.length}项` : text
}
const formatCardTypes = (values: H5PopupCardType[] | null | undefined): string => {
if (!values || values.length === 0) return '全部'
return values.map((item) => CARD_TYPE_NAMES[item] || item).join('、')
@@ -370,7 +334,31 @@
}
const { columnChecks, columns } = useCheckedColumns(() => [
{ prop: 'title', label: '标题', minWidth: 180, showOverflowTooltip: true },
{
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: '命中页面',
@@ -409,7 +397,7 @@
prop: 'action_type',
label: '受控动作',
width: 120,
formatter: (row: H5PopupConfiguration) => ACTION_TYPE_NAMES[row.action_type] || '无'
formatter: (row: H5PopupConfiguration) => ACTION_TYPE_NAMES[row.action_type || ''] || '无'
},
{
prop: 'effective_time',
@@ -500,10 +488,15 @@
// ========== 行操作 ==========
const goDetail = (row: H5PopupConfiguration) => {
router.push({
name: 'H5PopupConfigurationDetail',
params: { id: String(row.id) }
})
}
const getActions = (row: H5PopupConfiguration) => {
const actions: any[] = [
{ label: '详情', handler: () => openDetailDialog(row), type: 'primary' }
]
const actions: any[] = []
if (canUpdate.value) {
actions.push({ label: '编辑', handler: () => openEditDialog(row) })
}
@@ -555,33 +548,6 @@
}
}
// ========== 详情 ==========
const detailDialogVisible = ref(false)
const detailLoading = ref(false)
const detail = ref<H5PopupConfiguration | null>(null)
const openDetailDialog = async (row: H5PopupConfiguration) => {
detail.value = null
detailDialogVisible.value = true
detailLoading.value = true
try {
const res = await H5PopupConfigurationService.getConfiguration(row.id)
if (res.code === 0 && res.data) {
detail.value = res.data
} else {
ElMessage.error(res.msg || '获取配置详情失败')
detailDialogVisible.value = false
}
} catch (error) {
console.error('获取运营弹窗配置详情失败:', error)
ElMessage.error(normalizeApiError(error).message || '获取配置详情失败')
detailDialogVisible.value = false
} finally {
detailLoading.value = false
}
}
// ========== 创建 / 编辑 ==========
const formDialogVisible = ref(false)
@@ -593,19 +559,21 @@
const emptyForm = (): {
id: number
popup_type: H5PopupType
title: string
content: string
pages: H5PopupPage[]
priority: number
frequency: H5PopupFrequency
enabled: boolean
action_type: H5PopupActionType
action_type: H5PopupActionType | ''
shop_ids: number[]
device_types: string[]
card_types: H5PopupCardType[]
timeRange: string[]
} => ({
id: 0,
popup_type: 'promotion',
title: '',
content: '',
pages: [],
@@ -672,6 +640,7 @@
}
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' }
@@ -720,13 +689,14 @@
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 || '') as H5PopupActionType,
action_type: row.action_type || '',
shop_ids: [...(row.shop_ids || [])],
device_types: [...(row.device_types || [])],
card_types: [...(row.card_types || [])],
@@ -741,6 +711,7 @@
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],
@@ -749,14 +720,16 @@
starts_at: form.timeRange[0],
ends_at: form.timeRange[1],
enabled: form.enabled,
action_type: form.action_type,
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)
const res = await H5PopupConfigurationService.createConfiguration({
...payload,
action_type: form.action_type || null
})
if (res.code === 0 && res.data) {
ElMessage.success('创建成功')
formDialogVisible.value = false
@@ -767,7 +740,8 @@
} else {
const res = await H5PopupConfigurationService.updateConfiguration(form.id, {
id: form.id,
...payload
...payload,
action_type: form.action_type
})
if (res.code === 0 && res.data) {
ElMessage.success(
@@ -796,15 +770,4 @@
.h5-popup-configuration-page {
height: 100%;
}
.detail-loading {
padding: 24px 0;
text-align: center;
color: var(--el-text-color-secondary);
}
.detail-content {
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@@ -1,34 +1,6 @@
<template>
<ArtTableFullScreen>
<div class="business-owner-import-page" id="table-full-screen">
<!-- 导入入口卡片 -->
<ElCard shadow="never" class="import-entry-card">
<div class="import-entry">
<div class="import-tip">
<p>通过 CSV 批量交接/清空店铺平台业务员。</p>
<p
>表头固定:<code>店铺编码,操作类型,业务员登录账号,备注</code>;操作类型仅支持「换绑」或「清空」。</p
>
<p>支持 UTF-8(可带 BOM),一行失败不影响其他行。</p>
</div>
<div class="import-actions">
<ElButton
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.importPage"
@click="handleDownloadTemplate"
>
下载模板
</ElButton>
<ElButton
type="primary"
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.importCreate"
@click="importDialogVisible = true"
>
导入业务员
</ElButton>
</div>
</div>
</ElCard>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
@@ -37,20 +9,29 @@
@refresh="handleRefresh"
>
<template #left>
<ElSelect
v-model="searchForm.status"
placeholder="状态:全部"
clearable
style="width: 160px"
@change="handleSearch"
>
<ElOption
v-for="option in importStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
<div class="header-actions">
<ElSelect
v-model="searchForm.status"
placeholder="状态:全部"
clearable
style="width: 160px"
@change="handleSearch"
>
<ElOption
v-for="option in importStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
<ElButton
type="primary"
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.importCreate"
@click="importDialogVisible = true"
>
导入业务员
</ElButton>
</div>
</template>
</ArtTableHeader>
@@ -63,8 +44,6 @@
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:actions="getActions"
:actionsWidth="120"
:marginTop="10"
:height="400"
@size-change="handleSizeChange"
@@ -80,7 +59,12 @@
<!-- 新建导入弹窗 -->
<ElDialog v-model="importDialogVisible" title="导入业务员" width="560px" destroy-on-close>
<ElAlert type="info" :closable="false" show-icon class="import-alert">
请先下载模板,按模板填写店铺编码与操作类型后再上传 CSV。
<template #title>导入说明</template>
<div>通过 CSV 批量交接或清空店铺平台业务员。</div>
<div>
表头固定:<code>店铺编码,操作类型,业务员登录账号,备注</code>;操作类型仅支持「换绑」或「清空」。
</div>
<div>支持 UTF-8(可带 BOM),一行失败不影响其他行。</div>
</ElAlert>
<div class="upload-tip-row">
<ElButton @click="handleDownloadTemplate">下载模板</ElButton>
@@ -118,11 +102,11 @@
<!-- 导入结果抽屉 -->
<ElDrawer
v-model="detailDrawerVisible"
:title="`导入结果 - ${currentTask?.id || ''}`"
size="70%"
:title="`导入结果${currentTask?.task_no ? ` - ${currentTask.task_no}` : ''}`"
size="820px"
destroy-on-close
>
<div v-if="currentTask" class="import-detail">
<div v-if="currentTask" v-loading="detailLoading" class="import-detail">
<ElAlert
:type="detailAlertType"
:closable="false"
@@ -136,19 +120,19 @@
</ElAlert>
<div class="detail-stats">
<div class="stat-item">
<div class="stat-item stat-item--total">
<span class="stat-label">总数</span>
<strong>{{ currentTask.total_count ?? 0 }}</strong>
</div>
<div class="stat-item">
<div class="stat-item stat-item--success">
<span class="stat-label">成功</span>
<strong class="stat-success">{{ currentTask.success_count ?? 0 }}</strong>
</div>
<div class="stat-item">
<div class="stat-item stat-item--fail">
<span class="stat-label">失败</span>
<strong class="stat-fail">{{ currentTask.fail_count ?? 0 }}</strong>
</div>
<div class="stat-item">
<div class="stat-item stat-item--status">
<span class="stat-label">状态</span>
<strong>{{ currentTask.status_name || importStatusLabel(currentTask.status) }}</strong>
</div>
@@ -181,6 +165,10 @@
</div>
</div>
<div class="detail-section-title">
<span>逐行处理结果</span>
<ElTag size="small" type="info">{{ rowItems.length }} 条</ElTag>
</div>
<ElTable v-if="rowItems.length" :data="rowItems" border stripe max-height="420">
<ElTableColumn label="行号" prop="line" width="80" align="center" />
<ElTableColumn label="店铺编码" prop="shop_code" min-width="140" />
@@ -195,8 +183,8 @@
<ElTableColumn label="失败原因" prop="reason" min-width="220" show-overflow-tooltip />
</ElTable>
<ElEmpty
v-else-if="!detailLoading && !rowItems.length && currentTask.status === 4"
description="任务级失败,无行级明细"
v-else-if="!detailLoading"
:description="currentTask.status === 4 ? '任务级失败,无行级明细' : '暂无逐行处理结果'"
/>
</div>
</ElDrawer>
@@ -211,14 +199,11 @@
import { ShopService, StorageService } from '@/api/modules'
import type { BusinessOwnerImportDetail } from '@/types/api'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import { SHOP_BUSINESS_OWNER_PERMISSIONS } from '@/config/constants'
defineOptions({ name: 'BusinessOwnerImport' })
const { hasAuth } = useAuth()
const importStatusOptions = [
{ label: '待处理', value: 1 },
{ label: '处理中', value: 2 },
@@ -244,7 +229,6 @@
})
const columnOptions = [
{ label: '任务ID', prop: 'id', width: 90 },
{ label: '任务编号', prop: 'task_no', width: 160 },
{ label: '文件名', prop: 'file_name', minWidth: 160, showOverflowTooltip: true },
{ label: '创建人', prop: 'creator_name', width: 100 },
@@ -258,16 +242,23 @@
]
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: '任务ID',
width: 90
},
{
prop: 'task_no',
label: '任务编号',
width: 160,
showOverflowTooltip: true
showOverflowTooltip: true,
formatter: (row: BusinessOwnerImportDetail) =>
h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (event: MouseEvent) => {
event.stopPropagation()
openDetail(row)
}
},
row.task_no || '--'
)
},
{
prop: 'file_name',
@@ -318,17 +309,6 @@
}
])
const getActions = (row: BusinessOwnerImportDetail) => {
if (!hasAuth(SHOP_BUSINESS_OWNER_PERMISSIONS.importPage)) return []
return [
{
label: '查看结果',
handler: () => openDetail(row),
type: 'primary' as const
}
]
}
const getList = async () => {
loading.value = true
try {
@@ -337,8 +317,13 @@
page_size: pagination.pageSize,
status: searchForm.status
})
tableData.value = res.data?.items || []
pagination.total = res.data?.total || 0
if (res.code === 0 && res.data) {
const { items, total, page, size } = res.data
tableData.value = items || []
pagination.total = total || 0
pagination.page = page ?? pagination.page
pagination.pageSize = size ?? pagination.pageSize
}
} catch {
// 错误提示由请求层统一处理
} finally {
@@ -537,38 +522,10 @@
</script>
<style scoped lang="scss">
.import-entry-card {
margin-bottom: 12px;
.import-entry {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
.import-tip {
font-size: 13px;
color: var(--el-text-color-secondary);
line-height: 1.8;
p {
margin: 0;
}
code {
padding: 2px 6px;
font-size: 12px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
}
.import-actions {
display: flex;
gap: 8px;
}
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.import-alert {
@@ -596,8 +553,11 @@
}
.import-detail {
padding: 0 4px 20px;
.detail-alert {
margin-bottom: 16px;
border-radius: 10px;
}
.detail-error {
@@ -607,14 +567,35 @@
}
.detail-stats {
display: flex;
gap: 24px;
margin-bottom: 16px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 20px;
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
min-width: 0;
padding: 14px 16px;
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
&--success {
background: var(--el-color-success-light-9);
border-color: var(--el-color-success-light-7);
}
&--fail {
background: var(--el-color-danger-light-9);
border-color: var(--el-color-danger-light-7);
}
&--status {
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-7);
}
.stat-label {
font-size: 12px;
@@ -622,7 +603,8 @@
}
strong {
font-size: 18px;
font-size: 22px;
line-height: 1.2;
}
.stat-success {
@@ -638,11 +620,12 @@
.detail-meta {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px 24px;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-radius: 6px;
gap: 14px 24px;
margin-bottom: 20px;
padding: 16px;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
.meta-item {
display: flex;
@@ -662,5 +645,15 @@
}
}
}
.detail-section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
</style>