fix: 回调配置, 调整信用位置, 套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s
This commit is contained in:
@@ -512,6 +512,8 @@
|
||||
{ label: '账号类型', prop: 'user_type' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '企业名称', prop: 'enterprise_name' },
|
||||
{ label: '企微绑定', prop: 'wecom_bound' },
|
||||
{ label: '企微用户名称', prop: 'wecom_name' },
|
||||
...(canModifyAccountStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
@@ -620,6 +622,21 @@
|
||||
return row.enterprise_name || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'wecom_bound',
|
||||
label: '企微绑定',
|
||||
width: 100,
|
||||
formatter: (row: any) =>
|
||||
h(ElTag, { type: row.wecom_bound ? 'success' : 'info', size: 'small' }, () =>
|
||||
row.wecom_bound ? '已绑定' : '未绑定'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'wecom_name',
|
||||
label: '企微用户名称',
|
||||
minWidth: 130,
|
||||
formatter: (row: any) => row.wecom_name || '-'
|
||||
},
|
||||
...(canModifyAccountStatus
|
||||
? [
|
||||
{
|
||||
@@ -688,7 +705,7 @@
|
||||
return actions
|
||||
}
|
||||
|
||||
const showWecomBindingDialog = (row: any) => {
|
||||
const showWecomBindingDialog = async (row: any) => {
|
||||
if (row.user_type !== 2) {
|
||||
ElMessage.warning('只有平台用户可以绑定企微账号')
|
||||
return
|
||||
@@ -699,7 +716,17 @@
|
||||
wecomBindingForm.userid = ''
|
||||
wecomMembers.value = []
|
||||
wecomBindingDialogVisible.value = true
|
||||
void loadWecomApplications()
|
||||
await loadWecomApplications()
|
||||
|
||||
if (row.wecom_bound && row.wecom_corp_id) {
|
||||
const boundApplication = wecomApplications.value.find(
|
||||
(application) => application.corp_id === row.wecom_corp_id
|
||||
)
|
||||
if (boundApplication) {
|
||||
wecomBindingForm.application_id = boundApplication.id
|
||||
await handleWecomApplicationChange(boundApplication.id, row.wecom_userid || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadWecomApplications = async () => {
|
||||
@@ -712,8 +739,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleWecomApplicationChange = async (applicationId?: number) => {
|
||||
wecomBindingForm.userid = ''
|
||||
const handleWecomApplicationChange = async (applicationId?: number, selectedUserid = '') => {
|
||||
wecomBindingForm.userid = selectedUserid
|
||||
wecomMembers.value = []
|
||||
if (!applicationId) return
|
||||
|
||||
@@ -723,7 +750,15 @@
|
||||
page: 1,
|
||||
page_size: 100
|
||||
})
|
||||
if (response.code === 0) wecomMembers.value = response.data.items || []
|
||||
if (response.code === 0) {
|
||||
wecomMembers.value = response.data.items || []
|
||||
if (
|
||||
selectedUserid &&
|
||||
!wecomMembers.value.some((member) => member.userid === selectedUserid)
|
||||
) {
|
||||
wecomBindingForm.userid = selectedUserid
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
wecomMembersLoading.value = false
|
||||
}
|
||||
@@ -753,6 +788,7 @@
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('账号企微绑定成功')
|
||||
wecomBindingDialogVisible.value = false
|
||||
await getAccountList()
|
||||
}
|
||||
} finally {
|
||||
wecomBindingSubmitting.value = false
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<ElFormItem label="固定档位" prop="code">
|
||||
<ElSelect
|
||||
v-model="form.code"
|
||||
placeholder="请选择限速档位"
|
||||
style="width: 100%"
|
||||
:teleported="true"
|
||||
append-to="body"
|
||||
@@ -50,9 +51,11 @@
|
||||
confirmLoading?: boolean
|
||||
}
|
||||
|
||||
type SpeedTierCode = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }): void
|
||||
(e: 'confirm', data: { code: SpeedTierCode }): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -74,7 +77,7 @@
|
||||
{ code: 8, label: '100Mbps' }
|
||||
] as const
|
||||
|
||||
const form = reactive({ code: 3 as (typeof speedTiers)[number]['code'] })
|
||||
const form = reactive<{ code: SpeedTierCode | undefined }>({ code: undefined })
|
||||
|
||||
const rules: FormRules = {
|
||||
code: [{ required: true, message: '请选择限速档位', trigger: 'change' }]
|
||||
@@ -97,7 +100,7 @@
|
||||
(newVal) => {
|
||||
setLayoutScrollLock(newVal)
|
||||
if (newVal) {
|
||||
form.code = 3
|
||||
form.code = undefined
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -117,6 +120,7 @@
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (form.code === undefined) return
|
||||
emit('confirm', {
|
||||
code: form.code
|
||||
})
|
||||
|
||||
@@ -32,6 +32,14 @@
|
||||
>
|
||||
批量回收
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="info"
|
||||
:disabled="!selectedDevices.length"
|
||||
@click="handleBatchSetSeries"
|
||||
v-permission="'device:batch_set_series'"
|
||||
>
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="!selectedDevices.length"
|
||||
@@ -2656,6 +2664,18 @@
|
||||
await loadPackageSeriesList(query || undefined)
|
||||
}
|
||||
|
||||
// 显示批量设置套餐系列对话框
|
||||
const handleBatchSetSeries = async () => {
|
||||
seriesBindingResult.value = null
|
||||
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
|
||||
selection_type:
|
||||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
|
||||
})
|
||||
await Promise.all([loadPackageSeriesList(), loadSearchBatchNoOptions()])
|
||||
seriesBindingDialogVisible.value = true
|
||||
seriesBindingFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
|
||||
const request: BatchSetDeviceSeriesBindingRequest = {
|
||||
selection_type: seriesBindingForm.selection_type,
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@change="handleOldIdentifierChange"
|
||||
@focus="handleOldIotCardsFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="card in oldIotCardOptions"
|
||||
@@ -151,6 +152,9 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div v-if="oldIotCardEmptyMessage" class="exchange-field-error">
|
||||
{{ oldIotCardEmptyMessage }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.old_asset_type === 'device'"
|
||||
@@ -169,6 +173,7 @@
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@change="handleOldIdentifierChange"
|
||||
@focus="handleOldDevicesFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="device in oldDeviceOptions"
|
||||
@@ -184,6 +189,9 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div v-if="oldDeviceEmptyMessage" class="exchange-field-error">
|
||||
{{ oldDeviceEmptyMessage }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="createOldAssetShopDisplay" label="旧资产所属店铺">
|
||||
<ElInput :model-value="createOldAssetShopDisplay" readonly disabled />
|
||||
@@ -204,6 +212,7 @@
|
||||
no-data-text="暂无同店铺可用的新资产"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@focus="handleCreateNewIotCardsFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="card in newIotCardOptions"
|
||||
@@ -219,6 +228,9 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div v-if="createNewIotCardEmptyMessage" class="exchange-field-error">
|
||||
{{ createNewIotCardEmptyMessage }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.flow_type === 'direct' && createForm.old_asset_type === 'device'"
|
||||
@@ -236,6 +248,7 @@
|
||||
no-data-text="暂无同店铺可用的新资产"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@focus="handleCreateNewDevicesFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="device in newDeviceOptions"
|
||||
@@ -251,6 +264,9 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div v-if="createNewDeviceEmptyMessage" class="exchange-field-error">
|
||||
{{ createNewDeviceEmptyMessage }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="createDirectInheritedShopHint" label="归属提示">
|
||||
<div class="exchange-shop-hint">{{ createDirectInheritedShopHint }}</div>
|
||||
@@ -308,6 +324,8 @@
|
||||
no-data-text="暂无可用的新资产"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@change="handleShipNewIdentifierChange"
|
||||
@focus="handleShipNewIotCardsFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="card in newIotCardOptions"
|
||||
@@ -323,6 +341,9 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div v-if="shipNewIotCardEmptyMessage" class="exchange-field-error">
|
||||
{{ shipNewIotCardEmptyMessage }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-else-if="shipAssetType === 'device'"
|
||||
@@ -340,6 +361,8 @@
|
||||
no-data-text="暂无可用的新资产"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@change="handleShipNewIdentifierChange"
|
||||
@focus="handleShipNewDevicesFocus"
|
||||
>
|
||||
<ElOption
|
||||
v-for="device in newDeviceOptions"
|
||||
@@ -355,6 +378,9 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div v-if="shipNewDeviceEmptyMessage" class="exchange-field-error">
|
||||
{{ shipNewDeviceEmptyMessage }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="shipInheritedShopDisplayText" label="归属提示">
|
||||
<div class="exchange-shop-hint">{{ shipInheritedShopDisplayText }}</div>
|
||||
@@ -534,6 +560,42 @@
|
||||
const newDeviceSearchLoading = ref(false)
|
||||
const completeLoadingId = ref<number | null>(null)
|
||||
|
||||
type AssetFilterOption = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const oldAssetFilterOptions = ref<AssetFilterOption[]>([])
|
||||
const newAssetFilterOptions = ref<AssetFilterOption[]>([])
|
||||
const oldAssetFilterLoading = ref(false)
|
||||
const newAssetFilterLoading = ref(false)
|
||||
|
||||
const oldIotCardEmptyMessage = computed(() =>
|
||||
oldCardSearchLoading.value || oldIotCardOptions.value.length > 0 ? '' : '暂无可用的旧资产'
|
||||
)
|
||||
|
||||
const oldDeviceEmptyMessage = computed(() =>
|
||||
oldDeviceSearchLoading.value || oldDeviceOptions.value.length > 0 ? '' : '暂无可用的旧资产'
|
||||
)
|
||||
|
||||
const createNewIotCardEmptyMessage = computed(() => {
|
||||
if (newCardSearchLoading.value || newIotCardOptions.value.length > 0) return ''
|
||||
return createForm.old_identifier ? '暂无同店铺可用的新资产' : '请先选择旧资产'
|
||||
})
|
||||
|
||||
const createNewDeviceEmptyMessage = computed(() => {
|
||||
if (newDeviceSearchLoading.value || newDeviceOptions.value.length > 0) return ''
|
||||
return createForm.old_identifier ? '暂无同店铺可用的新资产' : '请先选择旧资产'
|
||||
})
|
||||
|
||||
const shipNewIotCardEmptyMessage = computed(() =>
|
||||
newCardSearchLoading.value || newIotCardOptions.value.length > 0 ? '' : '暂无可用的新资产'
|
||||
)
|
||||
|
||||
const shipNewDeviceEmptyMessage = computed(() =>
|
||||
newDeviceSearchLoading.value || newDeviceOptions.value.length > 0 ? '' : '暂无可用的新资产'
|
||||
)
|
||||
|
||||
// 发货表单
|
||||
const shipForm = reactive({
|
||||
new_identifier: '',
|
||||
@@ -543,11 +605,90 @@
|
||||
})
|
||||
|
||||
const shipRules = reactive<FormRules>({
|
||||
new_identifier: [{ required: true, message: '请输入新资产标识符', trigger: 'blur' }],
|
||||
new_identifier: [{ required: true, message: '请选择新资产标识符', trigger: 'change' }],
|
||||
express_company: [{ required: true, message: '请输入快递公司', trigger: 'blur' }],
|
||||
express_no: [{ required: true, message: '请输入快递单号', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const handleShipNewIdentifierChange = () => {
|
||||
shipFormRef.value?.clearValidate('new_identifier')
|
||||
}
|
||||
|
||||
const handleShipNewIotCardsFocus = async () => {
|
||||
if (newCardSearchLoading.value || newIotCardOptions.value.length > 0) return
|
||||
await loadDefaultShipNewIotCards()
|
||||
}
|
||||
|
||||
const handleShipNewDevicesFocus = async () => {
|
||||
if (newDeviceSearchLoading.value || newDeviceOptions.value.length > 0) return
|
||||
await loadDefaultShipNewDevices()
|
||||
}
|
||||
|
||||
const handleOldIotCardsFocus = async () => {
|
||||
if (oldCardSearchLoading.value || oldIotCardOptions.value.length > 0) return
|
||||
await loadDefaultIotCards()
|
||||
}
|
||||
|
||||
const handleOldDevicesFocus = async () => {
|
||||
if (oldDeviceSearchLoading.value || oldDeviceOptions.value.length > 0) return
|
||||
await loadDefaultDevices()
|
||||
}
|
||||
|
||||
const handleCreateNewIotCardsFocus = async () => {
|
||||
if (newCardSearchLoading.value || newIotCardOptions.value.length > 0) return
|
||||
await loadDefaultNewIotCards()
|
||||
}
|
||||
|
||||
const handleCreateNewDevicesFocus = async () => {
|
||||
if (newDeviceSearchLoading.value || newDeviceOptions.value.length > 0) return
|
||||
await loadDefaultNewDevices()
|
||||
}
|
||||
|
||||
const searchFilterAssets = async (query: string, target: 'old' | 'new') => {
|
||||
const options = target === 'old' ? oldAssetFilterOptions : newAssetFilterOptions
|
||||
const loading = target === 'old' ? oldAssetFilterLoading : newAssetFilterLoading
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const keyword = query.trim() || undefined
|
||||
const [cardRes, deviceRes] = await Promise.all([
|
||||
CardService.getStandaloneIotCards({
|
||||
is_standalone: true,
|
||||
keyword,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
}),
|
||||
DeviceService.getDevices({
|
||||
keyword,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
])
|
||||
|
||||
const cardOptions: AssetFilterOption[] =
|
||||
cardRes.code === 0
|
||||
? (cardRes.data?.items || []).map((card) => ({
|
||||
label: `${card.iccid} (IoT卡)`,
|
||||
value: card.iccid
|
||||
}))
|
||||
: []
|
||||
const deviceOptions: AssetFilterOption[] =
|
||||
deviceRes.code === 0
|
||||
? (deviceRes.data?.items || []).map((device) => ({
|
||||
label: `${device.virtual_no} (设备)`,
|
||||
value: device.virtual_no
|
||||
}))
|
||||
: []
|
||||
|
||||
options.value = [...cardOptions, ...deviceOptions]
|
||||
} catch (error) {
|
||||
console.error('加载资产筛选选项失败:', error)
|
||||
options.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
type ShopSelectableOption = {
|
||||
shop_id?: number | null
|
||||
shop_name?: string | null
|
||||
@@ -644,7 +785,7 @@
|
||||
})
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '换货状态',
|
||||
prop: 'status',
|
||||
@@ -677,19 +818,31 @@
|
||||
{
|
||||
label: '旧资产',
|
||||
prop: 'old_asset_keyword',
|
||||
type: 'input',
|
||||
type: 'select',
|
||||
options: () => oldAssetFilterOptions.value,
|
||||
config: {
|
||||
placeholder: '请输入旧资产标识',
|
||||
clearable: true
|
||||
placeholder: '请选择或搜索旧资产标识',
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
reserveKeyword: true,
|
||||
loading: oldAssetFilterLoading.value,
|
||||
remoteMethod: (query: string) => searchFilterAssets(query, 'old')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '新资产',
|
||||
prop: 'new_asset_keyword',
|
||||
type: 'input',
|
||||
type: 'select',
|
||||
options: () => newAssetFilterOptions.value,
|
||||
config: {
|
||||
placeholder: '请输入新资产标识',
|
||||
clearable: true
|
||||
placeholder: '请选择或搜索新资产标识',
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
reserveKeyword: true,
|
||||
loading: newAssetFilterLoading.value,
|
||||
remoteMethod: (query: string) => searchFilterAssets(query, 'new')
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -704,7 +857,7 @@
|
||||
valueFormat: 'YYYY-MM-DD'
|
||||
}
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
@@ -1037,9 +1190,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldIotCardOptions.value = res.data.items || []
|
||||
if (oldIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('暂无可用的IoT卡')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
@@ -1059,9 +1209,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldDeviceOptions.value = res.data.items || []
|
||||
if (oldDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('暂无可用的设备')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
@@ -1089,9 +1236,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldIotCardOptions.value = res.data.items || []
|
||||
if (oldIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的IoT卡')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索IoT卡失败:', error)
|
||||
@@ -1118,9 +1262,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
oldDeviceOptions.value = res.data.items || []
|
||||
if (oldDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的设备')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
@@ -1141,7 +1282,6 @@
|
||||
const shopId = getCreateOldAssetShopId()
|
||||
if (shopId === undefined) {
|
||||
newIotCardOptions.value = []
|
||||
ElMessage.warning('请先选择旧资产')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1155,9 +1295,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newIotCardOptions.value = res.data.items || []
|
||||
if (newIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的IoT卡')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索IoT卡失败:', error)
|
||||
@@ -1184,9 +1321,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newIotCardOptions.value = res.data.items || []
|
||||
if (newIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的IoT卡')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索IoT卡失败:', error)
|
||||
@@ -1213,9 +1347,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newIotCardOptions.value = res.data.items || []
|
||||
if (newIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('暂无同店铺可用的新资产')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
@@ -1234,8 +1365,8 @@
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newIotCardOptions.value = res.data.items || []
|
||||
if (res.code === 0) {
|
||||
newIotCardOptions.value = res.data?.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
@@ -1256,7 +1387,6 @@
|
||||
const shopId = getCreateOldAssetShopId()
|
||||
if (shopId === undefined) {
|
||||
newDeviceOptions.value = []
|
||||
ElMessage.warning('请先选择旧资产')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1269,9 +1399,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newDeviceOptions.value = res.data.items || []
|
||||
if (newDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的设备')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
@@ -1297,9 +1424,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newDeviceOptions.value = res.data.items || []
|
||||
if (newDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的设备')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
@@ -1325,9 +1449,6 @@
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newDeviceOptions.value = res.data.items || []
|
||||
if (newDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('暂无同店铺可用的新资产')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
@@ -1345,8 +1466,8 @@
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newDeviceOptions.value = res.data.items || []
|
||||
if (res.code === 0) {
|
||||
newDeviceOptions.value = res.data?.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
@@ -1641,6 +1762,8 @@
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadExchangeList()
|
||||
searchFilterAssets('', 'old')
|
||||
searchFilterAssets('', 'new')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1657,5 +1780,13 @@
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.exchange-field-error {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
color: var(--el-color-danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<div class="authorization-record-detail">
|
||||
<ElCard shadow="never">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">授权记录详情</h2>
|
||||
</div>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElCard, ElButton, ElIcon, ElMessage } 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 { AuthorizationService } from '@/api/modules'
|
||||
import type { AuthorizationItem } from '@/types/api/authorization'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'AuthorizationRecordDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<AuthorizationItem | null>(null)
|
||||
|
||||
// 详情页配置
|
||||
const detailSections: DetailSection[] = [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '企业ID', prop: 'enterprise_id' },
|
||||
{ label: '企业名称', prop: 'enterprise_name' },
|
||||
{ label: '卡ID', prop: 'card_id' },
|
||||
{ label: 'ICCID', prop: 'iccid' },
|
||||
{ label: '手机号', prop: 'msisdn', formatter: (value) => value || '-' },
|
||||
{ label: '授权人ID', prop: 'authorized_by' },
|
||||
{ label: '授权人', prop: 'authorizer_name' },
|
||||
{
|
||||
label: '授权人类型',
|
||||
formatter: (_, data) => {
|
||||
return data.authorizer_type === 2 ? '平台' : '代理'
|
||||
}
|
||||
},
|
||||
{ label: '授权时间', prop: 'authorized_at', formatter: (value) => formatDateTime(value) },
|
||||
{
|
||||
label: '状态',
|
||||
formatter: (_, data) => {
|
||||
return data.status === 1 ? '有效' : '已回收'
|
||||
}
|
||||
},
|
||||
{ label: '备注', prop: 'remark', formatter: (value) => value || '-' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 获取详情数据
|
||||
const fetchDetail = async () => {
|
||||
const id = Number(route.params.id)
|
||||
if (!id) {
|
||||
ElMessage.error('缺少ID参数')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AuthorizationService.getAuthorizationDetail(id)
|
||||
if (res.code === 0) {
|
||||
detailData.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log('获取授权记录详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.authorization-record-detail {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -28,29 +28,16 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="100"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 修改备注对话框 -->
|
||||
<ElDialog v-model="remarkDialogVisible" title="修改备注" width="500px">
|
||||
<ElForm ref="remarkFormRef" :model="remarkForm" :rules="remarkRules" label-width="80px">
|
||||
@@ -81,40 +68,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { AuthorizationService, EnterpriseService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import type {
|
||||
AuthorizationItem,
|
||||
AuthorizationStatus,
|
||||
AuthorizerType
|
||||
} from '@/types/api/authorization'
|
||||
import { CommonStatus } from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'AuthorizationRecords' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const remarkDialogVisible = ref(false)
|
||||
@@ -122,8 +91,6 @@
|
||||
const tableRef = ref()
|
||||
const remarkFormRef = ref<FormInstance>()
|
||||
const currentRecordId = ref<number>(0)
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<AuthorizationItem | null>(null)
|
||||
|
||||
// 企业搜索选项
|
||||
const enterpriseOptions = ref<any[]>([])
|
||||
@@ -252,7 +219,8 @@
|
||||
{ label: '授权人类型', prop: 'authorizer_type' },
|
||||
{ label: '授权时间', prop: 'authorized_at' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '备注', prop: 'remark' }
|
||||
{ label: '备注', prop: 'remark' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
const authorizationList = ref<AuthorizationItem[]>([])
|
||||
@@ -284,17 +252,7 @@
|
||||
label: 'ICCID',
|
||||
width: 200,
|
||||
formatter: (row: AuthorizationItem) => {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.iccid
|
||||
)
|
||||
return row.iccid || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -346,6 +304,21 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 操作列配置,样式与店铺列表保持一致
|
||||
const getActions = (row: AuthorizationItem) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('authorization_records:update_remark')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showRemarkDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
searchEnterprises('')
|
||||
@@ -417,22 +390,6 @@
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: AuthorizationItem) => {
|
||||
router.push({
|
||||
path: `${RoutesAlias.AuthorizationRecords}/detail/${row.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 处理名称点击
|
||||
const handleNameClick = (row: AuthorizationItem) => {
|
||||
if (hasAuth('authorization_records:view_detail')) {
|
||||
viewDetail(row)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
// 显示修改备注对话框
|
||||
const showRemarkDialog = (row: AuthorizationItem) => {
|
||||
currentRecordId.value = row.id
|
||||
@@ -462,44 +419,10 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (hasAuth('authorization_records:update_remark')) {
|
||||
items.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: AuthorizationItem, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showRemarkDialog(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.authorization-records-page {
|
||||
// Authorization records page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
v-model="createDrawerVisible"
|
||||
title="批量订购套餐"
|
||||
direction="rtl"
|
||||
size="520px"
|
||||
size="40%"
|
||||
:close-on-click-modal="!createDrawerBusy"
|
||||
:close-on-press-escape="!createDrawerBusy"
|
||||
@closed="handleCreateDrawerClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="90px" class="create-form">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="create-form">
|
||||
<ElFormItem label="套餐系列" prop="series_id">
|
||||
<ElSelect
|
||||
v-model="form.series_id"
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
v-model="createDrawerVisible"
|
||||
title="设备批量任务"
|
||||
direction="rtl"
|
||||
size="520px"
|
||||
size="40%"
|
||||
:close-on-click-modal="!createDrawerBusy"
|
||||
:close-on-press-escape="!createDrawerBusy"
|
||||
@closed="handleCreateDrawerClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" label-width="110px" class="create-form">
|
||||
<ElForm ref="formRef" :model="form" label-width="80px" class="create-form">
|
||||
<ElFormItem label="操作类型" prop="operation_type">
|
||||
<ElSelect
|
||||
v-model="form.operation_type"
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -429,74 +427,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 店铺实际信用额度调整弹框 -->
|
||||
<ElDialog
|
||||
v-model="creditDialogVisible"
|
||||
:title="`调整实际信用额度 - ${currentCreditShop?.shop_name || ''}`"
|
||||
width="520px"
|
||||
@closed="resetCreditDialog"
|
||||
>
|
||||
<ElDescriptions :column="1" border class="credit-preview">
|
||||
<ElDescriptionsItem label="店铺名称">
|
||||
{{ currentCreditShop?.shop_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="当前版本">
|
||||
{{ currentCreditShop?.version ?? '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="修改前">
|
||||
{{
|
||||
formatCreditPreview(currentCreditShop?.credit_enabled, currentCreditShop?.credit_limit)
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="修改后">
|
||||
{{ formatCreditPreview(creditForm.credit_enabled, creditFormCreditLimitFen) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElAlert
|
||||
title="实际可用金额、欠款金额和欠款状态以后端刷新后的资金概况为准。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="credit-dialog-alert"
|
||||
/>
|
||||
|
||||
<ElForm ref="creditFormRef" :model="creditForm" :rules="creditRules" label-width="110px">
|
||||
<ElFormItem label="启用信用">
|
||||
<ElSwitch v-model="creditForm.credit_enabled" @change="handleCreditEnabledChange" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实际信用额度" prop="credit_limit_yuan">
|
||||
<ElInputNumber
|
||||
v-model="creditForm.credit_limit_yuan"
|
||||
:disabled="!creditForm.credit_enabled"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
placeholder="请输入实际信用额度"
|
||||
/>
|
||||
<div class="credit-dialog-tip">单位:元;关闭信用时额度将自动归零</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="creditDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="creditSubmitting" @click="handleCreditSubmit">
|
||||
确认调整
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, watch, onBeforeUnmount, ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { h, watch, onBeforeUnmount, ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { CommissionService, ShopService } from '@/api/modules'
|
||||
import { CommissionService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { MainWalletTransactionType } from '@/types/api/commission'
|
||||
@@ -512,7 +449,7 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { fenToYuan, formatDateTime, formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import {
|
||||
CommissionStatusMap,
|
||||
WithdrawalStatusMap,
|
||||
@@ -520,7 +457,6 @@
|
||||
CommissionSourceMap
|
||||
} from '@/config/constants/commission'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
|
||||
defineOptions({ name: 'AgentCommission' })
|
||||
@@ -584,20 +520,6 @@
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 实际信用额度调整弹框状态
|
||||
const creditDialogVisible = ref(false)
|
||||
const creditFormRef = ref<FormInstance>()
|
||||
const creditSubmitting = ref(false)
|
||||
const currentCreditShop = ref<ShopFundSummaryItem | null>(null)
|
||||
const creditForm = reactive({
|
||||
credit_enabled: false,
|
||||
credit_limit_yuan: 0
|
||||
})
|
||||
|
||||
const creditFormCreditLimitFen = computed(() =>
|
||||
creditForm.credit_enabled ? yuanToFen(creditForm.credit_limit_yuan) || 0 : 0
|
||||
)
|
||||
|
||||
// 佣金修正表单验证规则
|
||||
const resolveRules = computed<FormRules>(() => ({
|
||||
amount:
|
||||
@@ -615,36 +537,6 @@
|
||||
remark: [{ max: 500, message: '备注最多500字符', trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const creditRules = computed<FormRules>(() => ({
|
||||
credit_limit_yuan: [
|
||||
{
|
||||
validator: (
|
||||
_rule: unknown,
|
||||
value: number | undefined,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (!creditForm.credit_enabled) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
if (value === undefined || value === null || Number.isNaN(value)) {
|
||||
callback(new Error('请输入实际信用额度'))
|
||||
return
|
||||
}
|
||||
|
||||
if (value <= 0) {
|
||||
callback(new Error('实际信用额度必须大于0'))
|
||||
return
|
||||
}
|
||||
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
// 预充值钱包流水状态
|
||||
const mainWalletLoading = ref(false)
|
||||
const mainWalletTableRef = ref()
|
||||
@@ -987,120 +879,6 @@
|
||||
})
|
||||
|
||||
// 获取操作按钮
|
||||
const getActions = (row: ShopFundSummaryItem) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('shop:credit_limit_manage')) {
|
||||
actions.push({
|
||||
label: '调整额度',
|
||||
handler: () => showCreditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
const formatCreditPreview = (enabled?: boolean, creditLimit?: number) => {
|
||||
return enabled ? `启用 / ${formatMoney(creditLimit || 0)}` : '关闭 / ¥0.00'
|
||||
}
|
||||
|
||||
const showCreditDialog = (row: ShopFundSummaryItem) => {
|
||||
currentCreditShop.value = row
|
||||
creditForm.credit_enabled = Boolean(row.credit_enabled)
|
||||
creditForm.credit_limit_yuan = row.credit_enabled ? fenToYuan(row.credit_limit) : 0
|
||||
creditDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
creditFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreditEnabledChange = (enabled: boolean | string | number) => {
|
||||
if (!enabled) {
|
||||
creditForm.credit_limit_yuan = 0
|
||||
creditFormRef.value?.clearValidate('credit_limit_yuan')
|
||||
}
|
||||
}
|
||||
|
||||
const resetCreditDialog = () => {
|
||||
creditFormRef.value?.resetFields()
|
||||
currentCreditShop.value = null
|
||||
creditForm.credit_enabled = false
|
||||
creditForm.credit_limit_yuan = 0
|
||||
}
|
||||
|
||||
const isCreditConflictMessage = (message?: string) => {
|
||||
if (!message) return false
|
||||
return /版本|冲突|并发|过期|conflict/i.test(message)
|
||||
}
|
||||
|
||||
const isCreditConflictResponse = (response: any) => {
|
||||
return response?.code === 409 || isCreditConflictMessage(response?.msg)
|
||||
}
|
||||
|
||||
const isCreditConflictError = (error: any) => {
|
||||
return (
|
||||
error?.response?.status === 409 ||
|
||||
error?.response?.data?.code === 409 ||
|
||||
isCreditConflictMessage(error?.response?.data?.msg || error?.message)
|
||||
)
|
||||
}
|
||||
|
||||
const handleCreditConflict = async () => {
|
||||
const shopId = currentCreditShop.value?.shop_id
|
||||
await getTableData()
|
||||
|
||||
if (shopId) {
|
||||
const latest = summaryList.value.find((item) => item.shop_id === shopId)
|
||||
if (latest) {
|
||||
// 仅刷新后端版本和当前余额,保留用户刚填写的表单值,方便确认后重试。
|
||||
currentCreditShop.value = latest
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.warning('资金概况已被其他操作更新,已刷新最新版本;请确认后重新提交')
|
||||
}
|
||||
|
||||
const handleCreditSubmit = async () => {
|
||||
if (!creditFormRef.value || !currentCreditShop.value) return
|
||||
|
||||
await creditFormRef.value.validate(async (valid) => {
|
||||
if (!valid || !currentCreditShop.value) return
|
||||
|
||||
creditSubmitting.value = true
|
||||
try {
|
||||
const res = await ShopService.updateShopCreditLimit(currentCreditShop.value.shop_id, {
|
||||
credit_enabled: creditForm.credit_enabled,
|
||||
credit_limit: creditFormCreditLimitFen.value,
|
||||
version: currentCreditShop.value.version
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('实际信用额度调整成功')
|
||||
creditDialogVisible.value = false
|
||||
await getTableData()
|
||||
return
|
||||
}
|
||||
|
||||
if (isCreditConflictResponse(res)) {
|
||||
await handleCreditConflict()
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.error(res.msg || '实际信用额度调整失败')
|
||||
} catch (error: any) {
|
||||
if (isCreditConflictError(error)) {
|
||||
await handleCreditConflict()
|
||||
} else {
|
||||
console.error('实际信用额度调整失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
}
|
||||
} finally {
|
||||
creditSubmitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听tab切换
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'commission') {
|
||||
@@ -1379,21 +1157,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.credit-preview {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.credit-dialog-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.credit-dialog-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
.main-wallet-filter__grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
|
||||
@@ -120,7 +120,11 @@
|
||||
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
|
||||
<template v-else-if="isPlatformUser">
|
||||
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
||||
<div v-for="role in filteredAvailableRoles" :key="getRoleId(role)" class="role-item">
|
||||
<div
|
||||
v-for="role in filteredAvailableRoles"
|
||||
:key="getRoleId(role)"
|
||||
class="role-item"
|
||||
>
|
||||
<ElCheckbox
|
||||
:label="getRoleId(role)"
|
||||
:disabled="selectedRoles.includes(getRoleId(role))"
|
||||
@@ -137,7 +141,11 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElRadioGroup v-model="roleToAdd" class="role-list">
|
||||
<div v-for="role in filteredAvailableRoles" :key="getRoleId(role)" class="role-item">
|
||||
<div
|
||||
v-for="role in filteredAvailableRoles"
|
||||
:key="getRoleId(role)"
|
||||
class="role-item"
|
||||
>
|
||||
<ElRadio
|
||||
:label="getRoleId(role)"
|
||||
:disabled="selectedRoles.includes(getRoleId(role))"
|
||||
@@ -404,6 +412,22 @@
|
||||
})
|
||||
}
|
||||
|
||||
baseColumns.push(
|
||||
{
|
||||
prop: 'wecom_bound',
|
||||
label: '企微绑定',
|
||||
formatter: (row: PlatformAccount) =>
|
||||
h(ElTag, { type: row.wecom_bound ? 'success' : 'info' }, () =>
|
||||
row.wecom_bound ? '已绑定' : '未绑定'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'wecom_name',
|
||||
label: '企微用户名称',
|
||||
formatter: (row: PlatformAccount) => h('span', row.wecom_name || '-')
|
||||
}
|
||||
)
|
||||
|
||||
// 添加状态和创建时间
|
||||
baseColumns.push(
|
||||
{
|
||||
|
||||
@@ -53,6 +53,7 @@ export const buildAgentRechargeActions = (
|
||||
if (
|
||||
!hasApprovalRecord &&
|
||||
row.status === AgentRechargeStatus.PENDING &&
|
||||
row.payment_method === 'offline' &&
|
||||
options.hasAuth('agent_recharge:reject')
|
||||
) {
|
||||
actions.push({
|
||||
|
||||
23
src/views/finance/agent-recharge/agentRechargeOnline.ts
Normal file
23
src/views/finance/agent-recharge/agentRechargeOnline.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import type { AgentRechargeStatus } from '@/types/api'
|
||||
|
||||
export const ONLINE_RECHARGE_TERMINAL_STATUSES: AgentRechargeStatus[] = [3, 4, 5, 6]
|
||||
|
||||
export const isOnlineRechargeTerminal = (status: AgentRechargeStatus): boolean => {
|
||||
return ONLINE_RECHARGE_TERMINAL_STATUSES.includes(status)
|
||||
}
|
||||
|
||||
export const createOnlineRechargeRequestId = (): string => {
|
||||
const randomId = globalThis.crypto?.randomUUID?.()
|
||||
const suffix = randomId || Math.random().toString(36).slice(2, 12)
|
||||
return `recharge-${Date.now()}-${suffix}`
|
||||
}
|
||||
|
||||
export const amountYuanToFen = (amount: number): number => {
|
||||
return Math.round(amount * 100)
|
||||
}
|
||||
|
||||
export const shouldRetryOnlineCreate = (error: unknown): boolean => {
|
||||
const kind = normalizeApiError(error).kind
|
||||
return kind === 'timeout' || kind === 'server' || kind === 'unknown'
|
||||
}
|
||||
@@ -78,11 +78,19 @@
|
||||
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
||||
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
||||
wechat: '微信在线支付',
|
||||
alipay: '支付宝在线支付',
|
||||
offline: '线下转账'
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
|
||||
const getRechargeSourceText = (data: AgentRecharge): string => {
|
||||
if (data.recharge_source_name) return data.recharge_source_name
|
||||
if (data.recharge_source === 'agent_online') return '代理在线自充'
|
||||
if (data.recharge_source === 'platform_offline') return '平台线下代充'
|
||||
return '-'
|
||||
}
|
||||
|
||||
const getApprovalProviderText = (data: AgentRecharge) => {
|
||||
if (data.approval_provider === 'wecom' || data.approval_source === 'wecom') return '企微'
|
||||
if (data.approval_source === 'legacy') return '历史审批'
|
||||
@@ -90,124 +98,155 @@
|
||||
}
|
||||
|
||||
// 详情配置
|
||||
const detailSections = computed((): DetailSection[] => [
|
||||
{
|
||||
title: '订单信息',
|
||||
fields: [
|
||||
{ label: '充值单号', prop: 'recharge_no' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
|
||||
{
|
||||
label: '充值金额',
|
||||
formatter: (_, data) => formatCurrency(data.amount)
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
render: (data) =>
|
||||
h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-')
|
||||
},
|
||||
{
|
||||
label: '驳回原因',
|
||||
prop: 'rejection_reason',
|
||||
formatter: (value) => formatRejectionReason(value),
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '企微审批信息',
|
||||
fields: [
|
||||
{ label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) },
|
||||
{
|
||||
label: '审批状态',
|
||||
formatter: (_, data) => data.approval_status_name || '-'
|
||||
},
|
||||
{
|
||||
label: '当前审批人摘要',
|
||||
formatter: (_, data) => data.current_approver_summary || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '业务处理结果',
|
||||
fields: [
|
||||
{
|
||||
label: '处理状态',
|
||||
formatter: (_, data) => data.processing_status_name || '-'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '支付信息',
|
||||
fields: [
|
||||
{
|
||||
label: '支付方式',
|
||||
formatter: (_, data) => getPaymentMethodText(data.payment_method)
|
||||
},
|
||||
{
|
||||
label: '支付通道',
|
||||
prop: 'payment_channel',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '第三方支付流水号',
|
||||
prop: 'payment_transaction_id',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '支付凭证',
|
||||
fullWidth: true,
|
||||
render: (data) =>
|
||||
hasVoucherKeys(data.payment_voucher_key)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
|
||||
}
|
||||
},
|
||||
() => '查看支付凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
{
|
||||
label: '运营备注',
|
||||
prop: 'remark',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '时间信息',
|
||||
fields: [
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
},
|
||||
{
|
||||
label: '支付时间',
|
||||
prop: 'paid_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
const detailSections = computed((): DetailSection[] => {
|
||||
if (!detailData.value) return []
|
||||
|
||||
const isOfflineRecharge =
|
||||
detailData.value.recharge_source === 'platform_offline' ||
|
||||
detailData.value.payment_method === 'offline'
|
||||
|
||||
const sections: DetailSection[] = [
|
||||
{
|
||||
title: '订单信息',
|
||||
fields: [
|
||||
{ label: '充值单号', prop: 'recharge_no' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '充值来源', formatter: (_, data) => getRechargeSourceText(data) },
|
||||
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
|
||||
{
|
||||
label: '充值金额',
|
||||
formatter: (_, data) => formatCurrency(data.amount)
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
render: (data) =>
|
||||
h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-')
|
||||
},
|
||||
{
|
||||
label: '驳回原因',
|
||||
prop: 'rejection_reason',
|
||||
formatter: (value) => formatRejectionReason(value),
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
...(isOfflineRecharge
|
||||
? [
|
||||
{
|
||||
title: '企微审批信息',
|
||||
fields: [
|
||||
{
|
||||
label: '审批渠道',
|
||||
formatter: (_: unknown, data: AgentRecharge) => getApprovalProviderText(data)
|
||||
},
|
||||
{
|
||||
label: '审批状态',
|
||||
formatter: (_: unknown, data: AgentRecharge) => data.approval_status_name || '-'
|
||||
},
|
||||
{
|
||||
label: '当前审批人摘要',
|
||||
formatter: (_: unknown, data: AgentRecharge) =>
|
||||
data.current_approver_summary || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: '业务处理结果',
|
||||
fields: [
|
||||
{
|
||||
label: '处理状态',
|
||||
formatter: (_, data) => data.processing_status_name || '-'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '支付信息',
|
||||
fields: [
|
||||
{
|
||||
label: '支付方式',
|
||||
formatter: (_, data) => getPaymentMethodText(data.payment_method)
|
||||
},
|
||||
{
|
||||
label: '支付通道',
|
||||
prop: 'payment_channel',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '第三方支付流水号',
|
||||
prop: 'payment_transaction_id',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '支付单号',
|
||||
prop: 'payment_no',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
...(isOfflineRecharge
|
||||
? [
|
||||
{
|
||||
label: '支付凭证',
|
||||
fullWidth: true,
|
||||
render: (data: AgentRecharge) =>
|
||||
hasVoucherKeys(data.payment_voucher_key)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(
|
||||
data.payment_voucher_key
|
||||
)
|
||||
}
|
||||
},
|
||||
() => '查看支付凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: '运营备注',
|
||||
prop: 'remark',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '时间信息',
|
||||
fields: [
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
},
|
||||
{
|
||||
label: '支付时间',
|
||||
prop: 'paid_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return sections
|
||||
})
|
||||
|
||||
// 加载详情数据
|
||||
const loadDetailData = async () => {
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="showCreateDialog"
|
||||
v-if="hasAuth('agent_recharge:create')"
|
||||
>创建充值订单</ElButton
|
||||
v-if="hasAuth('agent_recharge:create') && canCreateRecharge"
|
||||
>{{ createButtonLabel }}</ElButton
|
||||
>
|
||||
<ElButton v-if="hasAuth('agent_recharge:export')" @click="exportDialogVisible = true">
|
||||
导出
|
||||
@@ -54,7 +54,7 @@
|
||||
<!-- 创建充值订单对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
title="创建充值订单"
|
||||
:title="createDialogTitle"
|
||||
width="500px"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
@@ -62,14 +62,17 @@
|
||||
<ElFormItem label="充值金额" prop="amount">
|
||||
<ElInputNumber
|
||||
v-model="createForm.amount"
|
||||
:min="0.01"
|
||||
:min="minimumAmountYuan"
|
||||
:max="maximumAmountYuan"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
style="width: 100%"
|
||||
placeholder="请输入充值金额(元)"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
最小: ¥0.01,最大: ¥1,000,000.00
|
||||
金额范围:¥{{ minimumAmountYuan.toFixed(2) }} - ¥{{
|
||||
maximumAmountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
@@ -77,11 +80,27 @@
|
||||
v-model="createForm.payment_method"
|
||||
placeholder="请选择支付方式"
|
||||
style="width: 100%"
|
||||
:loading="paymentMethodsLoading"
|
||||
>
|
||||
<ElOption label="线下转账" value="offline" />
|
||||
<ElOption
|
||||
v-for="option in availablePaymentMethodOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div
|
||||
v-if="
|
||||
createMode === 'online' &&
|
||||
!paymentMethodsLoading &&
|
||||
onlinePaymentMethods.length === 0
|
||||
"
|
||||
class="online-recharge-empty-hint"
|
||||
>
|
||||
当前暂无可用在线支付方式,暂时无法提交充值。
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺" prop="shop_id">
|
||||
<ElFormItem v-if="createMode === 'offline'" label="店铺" prop="shop_id">
|
||||
<ElCascader
|
||||
v-model="createForm.shop_id"
|
||||
:options="shopCascadeOptions"
|
||||
@@ -106,7 +125,7 @@
|
||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="运营备注" prop="remark">
|
||||
<ElFormItem v-if="createMode === 'offline'" label="运营备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="createForm.remark"
|
||||
type="textarea"
|
||||
@@ -123,15 +142,54 @@
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleCreateRecharge"
|
||||
:loading="createLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
:loading="createLoading || voucherUploading || paymentMethodsLoading"
|
||||
:disabled="createSubmitDisabled"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
{{ voucherUploading ? '凭证上传中...' : createButtonLabel }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 在线充值二维码对话框 -->
|
||||
<ElDialog
|
||||
v-model="onlineQrDialogVisible"
|
||||
title="扫码完成充值"
|
||||
width="430px"
|
||||
align-center
|
||||
@closed="handleQrDialogClosed"
|
||||
>
|
||||
<div class="online-recharge-qr-dialog">
|
||||
<QrcodeVue
|
||||
v-if="qrContent"
|
||||
:value="qrContent"
|
||||
:size="240"
|
||||
level="H"
|
||||
render-as="canvas"
|
||||
/>
|
||||
<ElEmpty v-else description="暂未获取到支付二维码" />
|
||||
<div v-if="onlineQrRecharge" class="online-recharge-qr-dialog__summary">
|
||||
<div>充值单号:{{ onlineQrRecharge.recharge_no || '-' }}</div>
|
||||
<div>充值金额:{{ formatCurrency(onlineQrRecharge.amount) }}</div>
|
||||
<div v-if="onlineWalletBalance !== null">
|
||||
当前主钱包余额:{{ formatCurrency(onlineWalletBalance) }}
|
||||
</div>
|
||||
<ElTag :type="getStatusType(onlinePaymentStatus?.status || onlineQrRecharge.status)">
|
||||
{{ onlinePaymentStatus?.status_name || onlineQrRecharge.status_name || '-' }}
|
||||
</ElTag>
|
||||
<div class="online-recharge-qr-dialog__hint">
|
||||
{{ onlinePaymentStatusMessage }}
|
||||
</div>
|
||||
<div v-if="paymentStatusLoading" class="online-recharge-qr-dialog__loading">
|
||||
正在查询支付状态...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="onlineQrDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 导出任务对话框 -->
|
||||
<ExportTaskCreateDialog
|
||||
v-model="exportDialogVisible"
|
||||
@@ -224,7 +282,8 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { AgentRechargeService, ShopService } from '@/api/modules'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { AgentRechargeService, CommissionService, ShopService } from '@/api/modules'
|
||||
import {
|
||||
ElMessage,
|
||||
ElTag,
|
||||
@@ -240,7 +299,9 @@
|
||||
AgentRecharge,
|
||||
AgentRechargeQueryParams,
|
||||
AgentRechargeStatus,
|
||||
AgentRechargeOnlinePaymentMethod,
|
||||
AgentRechargePaymentMethod,
|
||||
AgentRechargePaymentStatusResponse,
|
||||
CreateAgentRechargeRequest,
|
||||
ConfirmOfflinePaymentRequest,
|
||||
RejectAgentRechargeRequest
|
||||
@@ -248,6 +309,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import {
|
||||
getApprovalStatusText,
|
||||
getCurrentApproverSummaryText,
|
||||
@@ -255,16 +317,36 @@
|
||||
} from '@/utils/business/approvalSummary'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
import { buildAgentRechargeActions } from './agentRechargeActions'
|
||||
import { formatRejectionReason } from './agentRechargeDisplay'
|
||||
import {
|
||||
amountYuanToFen,
|
||||
createOnlineRechargeRequestId,
|
||||
isOnlineRechargeTerminal,
|
||||
shouldRetryOnlineCreate
|
||||
} from './agentRechargeOnline'
|
||||
|
||||
defineOptions({ name: 'AgentRechargeList' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAgentAccount = computed(() => Number(userStore.info.user_type) === 3)
|
||||
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
|
||||
const canViewRecharge = computed(() => [1, 2, 3].includes(Number(userStore.info.user_type)))
|
||||
const canCreateRecharge = computed(() => isAgentAccount.value || isPlatformAccount.value)
|
||||
const createMode = ref<'online' | 'offline'>('offline')
|
||||
const createDialogTitle = computed(() =>
|
||||
createMode.value === 'online' ? '代理钱包在线扫码充值' : '创建平台线下代充'
|
||||
)
|
||||
const createButtonLabel = computed(() =>
|
||||
createMode.value === 'online' ? '立即充值' : '创建充值订单'
|
||||
)
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
@@ -276,13 +358,29 @@
|
||||
const exportDialogVisible = ref(false)
|
||||
const confirmPayDialogVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const onlineQrDialogVisible = ref(false)
|
||||
const currentRecharge = ref<AgentRecharge | null>(null)
|
||||
const onlineQrRecharge = ref<AgentRecharge | null>(null)
|
||||
const onlinePaymentStatus = ref<AgentRechargePaymentStatusResponse | null>(null)
|
||||
const onlineWalletBalance = ref<number | null>(null)
|
||||
const onlinePaymentMethods = ref<AgentRechargeOnlinePaymentMethod[]>([])
|
||||
const paymentMethodsLoading = ref(false)
|
||||
const paymentStatusLoading = ref(false)
|
||||
const qrContent = ref('')
|
||||
const onlineRequestId = ref<string | null>(null)
|
||||
const onlineCreateRetried = ref(false)
|
||||
const paymentStatusTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const paymentMethodsBounds = reactive({
|
||||
min_amount: 10000,
|
||||
max_amount: 100000000
|
||||
})
|
||||
const paymentVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: AgentRechargeQueryParams = {
|
||||
shop_id: undefined,
|
||||
status: undefined,
|
||||
recharge_source: undefined,
|
||||
dateRange: [],
|
||||
start_date: '',
|
||||
end_date: ''
|
||||
@@ -325,54 +423,72 @@
|
||||
}
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '店铺名称',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
placeholder: '请选择店铺',
|
||||
options: () =>
|
||||
shopOptions.value.map((shop) => ({
|
||||
label: shop.shop_name,
|
||||
value: shop.id
|
||||
})),
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: (query: string) => searchShops(query)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '支付状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '待支付', value: 1 },
|
||||
{ label: '已支付', value: 2 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '已关闭', value: 4 },
|
||||
{ label: '已退款', value: 5 },
|
||||
{ label: '已驳回', value: 6 }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '起止时间',
|
||||
prop: 'dateRange',
|
||||
type: 'date',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
valueFormat: 'YYYY-MM-DD'
|
||||
}
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => {
|
||||
const items: SearchFormItem[] = []
|
||||
|
||||
if (isPlatformAccount.value) {
|
||||
items.push({
|
||||
label: '店铺名称',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
placeholder: '请选择店铺',
|
||||
options: () =>
|
||||
shopOptions.value.map((shop) => ({
|
||||
label: shop.shop_name,
|
||||
value: shop.id
|
||||
})),
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: (query: string) => searchShops(query)
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
items.push(
|
||||
{
|
||||
label: '充值状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '待支付', value: 1 },
|
||||
{ label: '已支付', value: 2 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '已关闭', value: 4 },
|
||||
{ label: '已退款', value: 5 },
|
||||
{ label: '已驳回', value: 6 }
|
||||
],
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '充值来源',
|
||||
prop: 'recharge_source',
|
||||
type: 'select',
|
||||
placeholder: '请选择充值来源',
|
||||
options: [
|
||||
{ label: '代理在线自充', value: 'agent_online' },
|
||||
{ label: '平台线下代充', value: 'platform_offline' }
|
||||
],
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '起止时间',
|
||||
prop: 'dateRange',
|
||||
type: 'date',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
valueFormat: 'YYYY-MM-DD'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
@@ -385,6 +501,7 @@
|
||||
const columnOptions = [
|
||||
{ label: '充值单号', prop: 'recharge_no' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '充值来源', prop: 'recharge_source' },
|
||||
{ label: '充值金额', prop: 'amount' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '审批渠道', prop: 'approval_provider' },
|
||||
@@ -406,8 +523,43 @@
|
||||
const confirmPayFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const MIN_RECHARGE_AMOUNT = 0.01
|
||||
const MAX_RECHARGE_AMOUNT = 1_000_000
|
||||
const OFFLINE_MIN_RECHARGE_AMOUNT = 0.01
|
||||
const OFFLINE_MAX_RECHARGE_AMOUNT = 1_000_000
|
||||
|
||||
const minimumAmountYuan = computed(() =>
|
||||
createMode.value === 'online'
|
||||
? paymentMethodsBounds.min_amount / 100
|
||||
: OFFLINE_MIN_RECHARGE_AMOUNT
|
||||
)
|
||||
const maximumAmountYuan = computed(() =>
|
||||
createMode.value === 'online'
|
||||
? paymentMethodsBounds.max_amount / 100
|
||||
: OFFLINE_MAX_RECHARGE_AMOUNT
|
||||
)
|
||||
const availablePaymentMethodOptions = computed(() => {
|
||||
if (createMode.value === 'offline') return [{ label: '线下转账', value: 'offline' }]
|
||||
|
||||
return onlinePaymentMethods.value.map((method) => ({
|
||||
label: method === 'wechat' ? '微信支付' : '支付宝',
|
||||
value: method
|
||||
}))
|
||||
})
|
||||
const createSubmitDisabled = computed(
|
||||
() =>
|
||||
voucherUploading.value ||
|
||||
paymentMethodsLoading.value ||
|
||||
(createMode.value === 'online' && onlinePaymentMethods.value.length === 0)
|
||||
)
|
||||
const onlinePaymentStatusMessage = computed(() => {
|
||||
const status = onlinePaymentStatus.value?.status ?? onlineQrRecharge.value?.status
|
||||
if (status === 1) return '请使用对应支付方式扫码完成支付'
|
||||
if (status === 2) return '支付已成功,钱包正在入账,请稍候'
|
||||
if (status === 3) return '充值成功,钱包已到账'
|
||||
if (status === 4) return '充值订单已关闭,请重新发起充值'
|
||||
if (status === 5) return '充值订单已退款'
|
||||
if (status === 6) return '充值订单已驳回'
|
||||
return '正在获取支付状态'
|
||||
})
|
||||
|
||||
const createRules = computed<FormRules>(() => {
|
||||
const rules: FormRules = {
|
||||
@@ -419,12 +571,16 @@
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (Number(value) < MIN_RECHARGE_AMOUNT) {
|
||||
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
|
||||
if (Number(value) < minimumAmountYuan.value) {
|
||||
callback(new Error(`充值金额最小为 ¥${minimumAmountYuan.value.toFixed(2)}`))
|
||||
return
|
||||
}
|
||||
if (Number(value) > MAX_RECHARGE_AMOUNT) {
|
||||
callback(new Error(`充值金额最大为 ¥${MAX_RECHARGE_AMOUNT.toFixed(2)}`))
|
||||
if (Number(value) > maximumAmountYuan.value) {
|
||||
callback(
|
||||
new Error(
|
||||
`充值金额最大为 ¥${maximumAmountYuan.value.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
callback()
|
||||
@@ -435,9 +591,10 @@
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
|
||||
}
|
||||
if (createForm.payment_method === 'offline') {
|
||||
if (createMode.value === 'offline') {
|
||||
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
|
||||
}
|
||||
if (createMode.value === 'online') delete rules.shop_id
|
||||
return rules
|
||||
})
|
||||
|
||||
@@ -452,13 +609,13 @@
|
||||
})
|
||||
|
||||
const createForm = reactive<{
|
||||
amount: number
|
||||
payment_method: string
|
||||
amount: number | null
|
||||
payment_method: AgentRechargePaymentMethod | ''
|
||||
shop_id: number | null
|
||||
payment_voucher_key: string[]
|
||||
remark: string
|
||||
}>({
|
||||
amount: MIN_RECHARGE_AMOUNT,
|
||||
amount: OFFLINE_MIN_RECHARGE_AMOUNT,
|
||||
payment_method: '',
|
||||
shop_id: null,
|
||||
payment_voucher_key: [],
|
||||
@@ -499,11 +656,19 @@
|
||||
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
||||
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
||||
wechat: '微信在线支付',
|
||||
alipay: '支付宝在线支付',
|
||||
offline: '线下转账'
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
|
||||
const getRechargeSourceText = (row: AgentRecharge): string => {
|
||||
if (row.recharge_source_name) return row.recharge_source_name
|
||||
if (row.recharge_source === 'agent_online') return '代理在线自充'
|
||||
if (row.recharge_source === 'platform_offline') return '平台线下代充'
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
@@ -530,6 +695,12 @@
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'recharge_source',
|
||||
label: '充值来源',
|
||||
width: 140,
|
||||
formatter: (row: AgentRecharge) => getRechargeSourceText(row)
|
||||
},
|
||||
{
|
||||
prop: 'amount',
|
||||
label: '充值金额',
|
||||
@@ -635,7 +806,17 @@
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
loadShops()
|
||||
if (isPlatformAccount.value) loadShops()
|
||||
document.addEventListener('visibilitychange', handleDocumentVisibilityChange)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPaymentStatusPolling()
|
||||
document.removeEventListener('visibilitychange', handleDocumentVisibilityChange)
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
stopPaymentStatusPolling()
|
||||
})
|
||||
|
||||
let isFirstActivation = true
|
||||
@@ -643,6 +824,9 @@
|
||||
if (!isFirstActivation) {
|
||||
getTableData()
|
||||
}
|
||||
if (onlineQrDialogVisible.value && document.visibilityState === 'visible') {
|
||||
startPaymentStatusPolling()
|
||||
}
|
||||
isFirstActivation = false
|
||||
})
|
||||
|
||||
@@ -693,13 +877,20 @@
|
||||
|
||||
// 获取充值订单列表
|
||||
const getTableData = async () => {
|
||||
if (!canViewRecharge.value) {
|
||||
rechargeList.value = []
|
||||
pagination.total = 0
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params: AgentRechargeQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
shop_id: searchForm.shop_id,
|
||||
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
|
||||
status: searchForm.status,
|
||||
recharge_source: searchForm.recharge_source,
|
||||
start_date: searchForm.start_date || undefined,
|
||||
end_date: searchForm.end_date || undefined
|
||||
}
|
||||
@@ -737,8 +928,9 @@
|
||||
}
|
||||
|
||||
const exportQuery = computed(() => ({
|
||||
shop_id: searchForm.shop_id,
|
||||
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
|
||||
status: searchForm.status,
|
||||
recharge_source: searchForm.recharge_source,
|
||||
start_date: searchForm.start_date || searchForm.dateRange?.[0],
|
||||
end_date: searchForm.end_date || searchForm.dateRange?.[1]
|
||||
}))
|
||||
@@ -761,22 +953,56 @@
|
||||
|
||||
// 显示创建订单对话框
|
||||
const showCreateDialog = async () => {
|
||||
// 重新加载店铺列表,确保获取最新数据
|
||||
await loadShops()
|
||||
createForm.payment_method = ''
|
||||
createMode.value = isAgentAccount.value ? 'online' : 'offline'
|
||||
resetCreateForm()
|
||||
|
||||
if (createMode.value === 'online') {
|
||||
await loadPaymentMethods()
|
||||
} else {
|
||||
// 重新加载店铺列表,确保获取最新数据
|
||||
await loadShops()
|
||||
}
|
||||
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const resetCreateForm = () => {
|
||||
createForm.amount = minimumAmountYuan.value
|
||||
createForm.payment_method = ''
|
||||
createForm.shop_id = null
|
||||
createForm.payment_voucher_key = []
|
||||
createForm.remark = ''
|
||||
onlineRequestId.value = null
|
||||
onlineCreateRetried.value = false
|
||||
voucherUploading.value = false
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
// 对话框关闭后的清理
|
||||
const handleCreateDialogClosed = () => {
|
||||
createFormRef.value?.resetFields()
|
||||
createForm.amount = MIN_RECHARGE_AMOUNT
|
||||
createForm.payment_method = ''
|
||||
createForm.shop_id = null
|
||||
createForm.payment_voucher_key = []
|
||||
createForm.remark = ''
|
||||
voucherUploading.value = false
|
||||
uploadRef.value?.clearFiles(false)
|
||||
resetCreateForm()
|
||||
}
|
||||
|
||||
// 加载代理在线充值可用支付方式
|
||||
const loadPaymentMethods = async () => {
|
||||
paymentMethodsLoading.value = true
|
||||
onlinePaymentMethods.value = []
|
||||
try {
|
||||
const res = await AgentRechargeService.getPaymentMethods()
|
||||
if (res.code === 0) {
|
||||
onlinePaymentMethods.value = Array.isArray(res.data?.methods) ? res.data.methods : []
|
||||
paymentMethodsBounds.min_amount = Number(res.data?.min_amount) || 10000
|
||||
paymentMethodsBounds.max_amount = Number(res.data?.max_amount) || 100000000
|
||||
createForm.amount = minimumAmountYuan.value
|
||||
} else {
|
||||
ElMessage.warning(res.msg || '当前暂无可用在线支付方式')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载在线支付方式失败:', error)
|
||||
} finally {
|
||||
paymentMethodsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建充值订单
|
||||
@@ -788,36 +1014,199 @@
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.validate(async (valid) => {
|
||||
if (valid) {
|
||||
createLoading.value = true
|
||||
try {
|
||||
const data: CreateAgentRechargeRequest = {
|
||||
amount: Math.round(createForm.amount * 100), // 元转分
|
||||
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
|
||||
shop_id: createForm.shop_id!,
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
const valid = await formRef.validate().catch(() => false)
|
||||
if (!valid || createForm.amount == null || !createForm.payment_method) return
|
||||
|
||||
if (
|
||||
createForm.payment_method === 'offline' &&
|
||||
hasVoucherKeys(createForm.payment_voucher_key)
|
||||
) {
|
||||
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
|
||||
}
|
||||
createLoading.value = true
|
||||
try {
|
||||
if (createMode.value === 'online') {
|
||||
await handleOnlineRechargeCreate(createForm.amount, createForm.payment_method)
|
||||
return
|
||||
}
|
||||
|
||||
await AgentRechargeService.createAgentRecharge(data)
|
||||
ElMessage.success('充值订单创建成功')
|
||||
createDialogVisible.value = false
|
||||
formRef.resetFields()
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
if (!createForm.shop_id) {
|
||||
ElMessage.warning('请选择目标店铺')
|
||||
return
|
||||
}
|
||||
|
||||
const voucherKeys = toVoucherKeyList(createForm.payment_voucher_key)
|
||||
if (!hasVoucherKeys(voucherKeys)) {
|
||||
ElMessage.warning('请上传支付凭证')
|
||||
return
|
||||
}
|
||||
|
||||
const data: CreateAgentRechargeRequest = {
|
||||
amount: amountYuanToFen(createForm.amount),
|
||||
payment_method: 'offline',
|
||||
shop_id: createForm.shop_id,
|
||||
payment_voucher_key: voucherKeys,
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
|
||||
const res = await AgentRechargeService.createAgentRecharge(data)
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '充值订单创建失败')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('充值订单创建成功')
|
||||
createDialogVisible.value = false
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnlineRechargeCreate = async (
|
||||
amountYuan: number,
|
||||
paymentMethod: AgentRechargePaymentMethod
|
||||
) => {
|
||||
if (paymentMethod === 'offline') return
|
||||
|
||||
const requestId = onlineRequestId.value || createOnlineRechargeRequestId()
|
||||
onlineRequestId.value = requestId
|
||||
onlineCreateRetried.value = false
|
||||
|
||||
const request: CreateAgentRechargeRequest = {
|
||||
amount: amountYuanToFen(amountYuan),
|
||||
payment_method: paymentMethod,
|
||||
request_id: requestId
|
||||
}
|
||||
|
||||
let attempt = 0
|
||||
while (attempt < 2) {
|
||||
try {
|
||||
const res = await AgentRechargeService.createAgentRecharge(request)
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '在线充值创建失败')
|
||||
onlineRequestId.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (!res.data?.qr_content) {
|
||||
ElMessage.error('支付接口未返回有效二维码,请刷新后重试')
|
||||
return
|
||||
}
|
||||
|
||||
onlineQrRecharge.value = res.data
|
||||
onlinePaymentStatus.value = null
|
||||
qrContent.value = res.data.qr_content
|
||||
onlineQrDialogVisible.value = true
|
||||
createDialogVisible.value = false
|
||||
onlineRequestId.value = null
|
||||
await getTableData()
|
||||
return
|
||||
} catch (error) {
|
||||
if (attempt === 0 && shouldRetryOnlineCreate(error)) {
|
||||
attempt += 1
|
||||
onlineCreateRetried.value = true
|
||||
continue
|
||||
}
|
||||
|
||||
const normalized = normalizeApiError(error)
|
||||
if (normalized.kind === 'validation' || normalized.kind === 'conflict') {
|
||||
ElMessage.error(normalized.message)
|
||||
} else {
|
||||
ElMessage.warning('在线充值请求结果未知,请勿重复提交;可关闭后刷新列表确认')
|
||||
}
|
||||
onlineRequestId.value = null
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stopPaymentStatusPolling = () => {
|
||||
if (paymentStatusTimer.value) {
|
||||
clearInterval(paymentStatusTimer.value)
|
||||
paymentStatusTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const loadOnlinePaymentStatus = async () => {
|
||||
const rechargeId = onlineQrRecharge.value?.id
|
||||
if (!rechargeId || !onlineQrDialogVisible.value || document.visibilityState !== 'visible')
|
||||
return
|
||||
|
||||
paymentStatusLoading.value = true
|
||||
try {
|
||||
const res = await AgentRechargeService.getPaymentStatus(rechargeId)
|
||||
if (res.code !== 0) return
|
||||
|
||||
onlinePaymentStatus.value = res.data
|
||||
if (onlineQrRecharge.value) {
|
||||
onlineQrRecharge.value = {
|
||||
...onlineQrRecharge.value,
|
||||
status: res.data.status,
|
||||
status_name: res.data.status_name || onlineQrRecharge.value.status_name,
|
||||
paid_at: res.data.paid_at,
|
||||
completed_at: res.data.completed_at
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (isOnlineRechargeTerminal(res.data.status)) {
|
||||
stopPaymentStatusPolling()
|
||||
if (res.data.status === 3) {
|
||||
await Promise.all([getTableData(), refreshAgentWalletBalance()])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询在线充值状态失败:', error)
|
||||
} finally {
|
||||
paymentStatusLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startPaymentStatusPolling = () => {
|
||||
stopPaymentStatusPolling()
|
||||
void loadOnlinePaymentStatus()
|
||||
paymentStatusTimer.value = setInterval(() => {
|
||||
if (!onlineQrDialogVisible.value || document.visibilityState !== 'visible') {
|
||||
stopPaymentStatusPolling()
|
||||
return
|
||||
}
|
||||
void loadOnlinePaymentStatus()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const handleDocumentVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible' && onlineQrDialogVisible.value) {
|
||||
startPaymentStatusPolling()
|
||||
} else if (document.visibilityState === 'hidden') {
|
||||
stopPaymentStatusPolling()
|
||||
}
|
||||
}
|
||||
|
||||
watch(onlineQrDialogVisible, (visible) => {
|
||||
if (visible) {
|
||||
startPaymentStatusPolling()
|
||||
} else {
|
||||
stopPaymentStatusPolling()
|
||||
}
|
||||
})
|
||||
|
||||
const handleQrDialogClosed = () => {
|
||||
stopPaymentStatusPolling()
|
||||
qrContent.value = ''
|
||||
onlineQrRecharge.value = null
|
||||
onlinePaymentStatus.value = null
|
||||
onlineWalletBalance.value = null
|
||||
paymentStatusLoading.value = false
|
||||
}
|
||||
|
||||
const refreshAgentWalletBalance = async () => {
|
||||
if (!isAgentAccount.value) return
|
||||
|
||||
try {
|
||||
const res = await CommissionService.getShopFundSummary({ page: 1, page_size: 100 })
|
||||
const currentShopId = Number(userStore.info.shop_id)
|
||||
const currentShop =
|
||||
res.code === 0 ? res.data.items?.find((item) => item.shop_id === currentShopId) : null
|
||||
if (currentShop) onlineWalletBalance.value = currentShop.main_balance
|
||||
} catch (error) {
|
||||
console.error('刷新代理钱包余额失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示确认支付对话框
|
||||
@@ -935,4 +1324,38 @@
|
||||
.agent-recharge-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.online-recharge-empty-hint {
|
||||
margin-top: 8px;
|
||||
color: var(--el-color-danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.online-recharge-qr-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__loading {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -727,10 +727,10 @@
|
||||
package_name: '',
|
||||
series_id: undefined,
|
||||
package_type: '',
|
||||
calendar_type: undefined,
|
||||
calendar_type: null,
|
||||
duration_days: undefined,
|
||||
duration_months: 1,
|
||||
data_reset_cycle: undefined,
|
||||
data_reset_cycle: null,
|
||||
enable_virtual_data: false,
|
||||
expiry_base: 'from_activation',
|
||||
real_data_mb: 0,
|
||||
@@ -1345,10 +1345,10 @@
|
||||
form.package_name = data.package_name
|
||||
form.series_id = data.series_id
|
||||
form.package_type = data.package_type
|
||||
form.calendar_type = data.calendar_type || undefined
|
||||
form.calendar_type = data.calendar_type ?? null
|
||||
form.duration_days = data.duration_days
|
||||
form.duration_months = data.duration_months
|
||||
form.data_reset_cycle = data.data_reset_cycle || undefined
|
||||
form.data_reset_cycle = data.data_reset_cycle ?? null
|
||||
form.enable_virtual_data = data.enable_virtual_data || false
|
||||
form.expiry_base = data.expiry_base || 'from_activation'
|
||||
form.real_data_mb = data.real_data_mb || 0
|
||||
@@ -1384,10 +1384,10 @@
|
||||
form.package_name = ''
|
||||
form.series_id = undefined
|
||||
form.package_type = ''
|
||||
form.calendar_type = undefined
|
||||
form.calendar_type = null
|
||||
form.duration_days = undefined
|
||||
form.duration_months = 1
|
||||
form.data_reset_cycle = undefined
|
||||
form.data_reset_cycle = null
|
||||
form.enable_virtual_data = false
|
||||
form.expiry_base = 'from_activation'
|
||||
form.real_data_mb = 0
|
||||
@@ -1428,10 +1428,10 @@
|
||||
form.package_name = ''
|
||||
form.series_id = undefined
|
||||
form.package_type = ''
|
||||
form.calendar_type = undefined
|
||||
form.calendar_type = null
|
||||
form.duration_days = undefined
|
||||
form.duration_months = 1
|
||||
form.data_reset_cycle = undefined
|
||||
form.data_reset_cycle = null
|
||||
form.enable_virtual_data = false
|
||||
form.expiry_base = 'from_activation'
|
||||
form.real_data_mb = 0
|
||||
@@ -1483,6 +1483,8 @@
|
||||
package_type: form.package_type,
|
||||
duration_months: form.duration_months,
|
||||
cost_price: costPriceInCents,
|
||||
calendar_type: form.calendar_type ?? null,
|
||||
data_reset_cycle: form.data_reset_cycle ?? null,
|
||||
is_gift: form.is_gift
|
||||
}
|
||||
|
||||
@@ -1494,12 +1496,6 @@
|
||||
if (form.series_id !== undefined) {
|
||||
data.series_id = form.series_id
|
||||
}
|
||||
if (form.calendar_type) {
|
||||
data.calendar_type = form.calendar_type
|
||||
}
|
||||
if (form.data_reset_cycle) {
|
||||
data.data_reset_cycle = form.data_reset_cycle
|
||||
}
|
||||
// 只有 calendar_type 为 by_day 时才传 duration_days
|
||||
if (form.calendar_type === 'by_day' && form.duration_days !== undefined) {
|
||||
data.duration_days = form.duration_days
|
||||
|
||||
@@ -143,45 +143,23 @@
|
||||
|
||||
<!-- 套餐列表 -->
|
||||
<div v-if="detailData.packages && detailData.packages.length > 0" class="package-table">
|
||||
<ElTable :data="detailData.packages" border stripe>
|
||||
<ElTableColumn prop="package_name" label="套餐名称" />
|
||||
<ElTableColumn prop="package_code" label="套餐编码" />
|
||||
<ElTableColumn label="成本价">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐默认生效条件">
|
||||
<template #default="{ row }">
|
||||
{{ row.default_expiry_base_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="覆盖生效条件">
|
||||
<template #default="{ row }">
|
||||
{{ row.expiry_base_override_name || '跟随套餐默认' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="最终生效条件">
|
||||
<template #default="{ row }">
|
||||
{{ row.effective_expiry_base_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上架状态" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
||||
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<SeriesGrantPackageTable
|
||||
:data="detailData.packages"
|
||||
:loading="loading"
|
||||
:show-actions="true"
|
||||
:actions-width="240"
|
||||
>
|
||||
<template #actions="{ row }">
|
||||
<div class="package-table-actions">
|
||||
<ElButton
|
||||
v-if="canUpdateExpiryBase"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
@click="showExpiryBaseDialog(row)"
|
||||
>
|
||||
修改生效条件
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -200,9 +178,9 @@
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
</SeriesGrantPackageTable>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无套餐" :image-size="80" />
|
||||
</div>
|
||||
@@ -214,95 +192,58 @@
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑套餐对话框 -->
|
||||
<ElDialog
|
||||
<SeriesGrantPackageDialog
|
||||
v-model="packageDialogVisible"
|
||||
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
v-model:form="packageForm"
|
||||
:dialog-type="packageDialogType"
|
||||
:rules="packageRules"
|
||||
:available-packages="availablePackages"
|
||||
:package-loading="packageLoading"
|
||||
:submit-loading="submitLoading"
|
||||
:can-update-expiry-base="canUpdateExpiryBase"
|
||||
:has-series="Boolean(detailData?.series_id)"
|
||||
@search="searchPackages"
|
||||
@submit="handleSavePackage"
|
||||
@closed="handlePackageDialogClosed"
|
||||
/>
|
||||
|
||||
<ElDialog
|
||||
v-model="expiryDialogVisible"
|
||||
title="修改生效条件"
|
||||
width="460px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="100px">
|
||||
<!-- 添加模式:选择套餐 -->
|
||||
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
|
||||
<ElSelect
|
||||
v-model="packageForm.package_id"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchPackages"
|
||||
:loading="packageLoading"
|
||||
clearable
|
||||
>
|
||||
<template
|
||||
v-if="availablePackages.length === 0 && !packageLoading && detailData?.series_id"
|
||||
>
|
||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="pkg in availablePackages"
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
||||
:value="pkg.id"
|
||||
:disabled="pkg.is_authorized"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElForm label-width="130px">
|
||||
<ElFormItem label="套餐名称">
|
||||
<span class="ellipsis-value" :title="expiryForm.package_name || ''">
|
||||
{{ expiryForm.package_name || '-' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 编辑模式:显示套餐信息 -->
|
||||
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
||||
<span>{{ packageForm.package_name }}</span>
|
||||
<ElFormItem label="套餐编码">
|
||||
<span class="ellipsis-value" :title="expiryForm.package_code || ''">
|
||||
{{ expiryForm.package_code || '-' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
|
||||
<span>{{ packageForm.package_code }}</span>
|
||||
<ElFormItem label="套餐默认生效条件">
|
||||
{{ expiryForm.default_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 成本价 -->
|
||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
||||
<ElInputNumber
|
||||
v-model="packageForm.cost_price_yuan"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入成本价"
|
||||
/>
|
||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
||||
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
|
||||
packageForm.original_cost_price.toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
|
||||
<ElSelect
|
||||
v-model="packageForm.expiry_base_override"
|
||||
placeholder="请选择生效条件"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElFormItem label="覆盖生效条件">
|
||||
<ElSelect v-model="expiryForm.expiry_base_override" style="width: 100%">
|
||||
<ElOption label="跟随套餐默认" value="default" />
|
||||
<ElOption label="购买即生效" value="from_purchase" />
|
||||
<ElOption label="实名激活时生效" value="from_activation" />
|
||||
</ElSelect>
|
||||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||
</ElFormItem>
|
||||
<template v-if="packageDialogType === 'edit'">
|
||||
<ElFormItem label="套餐默认生效条件">
|
||||
{{ packageForm.default_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
<ElFormItem label="覆盖生效条件">
|
||||
{{ packageForm.expiry_base_override_name || '跟随套餐默认' }}
|
||||
</ElFormItem>
|
||||
<ElFormItem label="最终生效条件">
|
||||
{{ packageForm.effective_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="最终生效条件">
|
||||
{{ expiryForm.effective_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="packageDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSavePackage" :loading="submitLoading">
|
||||
<ElButton @click="expiryDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="expirySubmitLoading" @click="saveExpiryBase">
|
||||
保存
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -330,10 +271,8 @@
|
||||
ElDialog,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElInputNumber,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
@@ -354,9 +293,13 @@
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getEnableStatusText, JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import SeriesGrantPackageTable from '@/components/business/SeriesGrantPackageTable.vue'
|
||||
import SeriesGrantPackageDialog from '@/components/business/SeriesGrantPackageDialog.vue'
|
||||
import {
|
||||
mergeGrantPackageCandidates,
|
||||
type GrantPackageCandidate
|
||||
type GrantPackageCandidate,
|
||||
type PackageCostFormItem,
|
||||
type SeriesGrantPackageForm
|
||||
} from '@/utils/business/seriesGrantPackage'
|
||||
|
||||
defineOptions({ name: 'SeriesGrantsDetail' })
|
||||
@@ -374,61 +317,79 @@
|
||||
const packageDialogVisible = ref(false)
|
||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||
const availablePackages = ref<GrantPackageCandidate[]>([])
|
||||
const packageFormRef = ref<FormInstance>()
|
||||
const expiryDialogVisible = ref(false)
|
||||
const expirySubmitLoading = ref(false)
|
||||
|
||||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||
|
||||
// 套餐表单
|
||||
const packageForm = ref<{
|
||||
type ExpiryFormState = {
|
||||
package_id?: number
|
||||
allocation_id?: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
original_cost_price?: number
|
||||
cost_price_yuan: number
|
||||
expiry_base_override: ExpiryBaseSelection
|
||||
initial_expiry_base_override: ExpiryBaseSelection
|
||||
default_expiry_base_name?: string | null
|
||||
expiry_base_override: ExpiryBaseSelection
|
||||
expiry_base_override_name?: string | null
|
||||
effective_expiry_base_name?: string | null
|
||||
}>({
|
||||
}
|
||||
|
||||
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
|
||||
|
||||
const createDefaultPackageForm = (): SeriesGrantPackageForm => ({
|
||||
package_id: undefined,
|
||||
allocation_id: undefined,
|
||||
package_ids: [],
|
||||
package_name: undefined,
|
||||
package_code: undefined,
|
||||
cost_price_yuan: 0,
|
||||
expiry_base_override: 'default',
|
||||
initial_expiry_base_override: 'default'
|
||||
initial_expiry_base_override: 'default',
|
||||
packages: []
|
||||
})
|
||||
const packageForm = ref<SeriesGrantPackageForm>(createDefaultPackageForm())
|
||||
|
||||
// 表单验证规则
|
||||
const packageRules = computed<FormRules>(() => ({
|
||||
package_id: [
|
||||
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
|
||||
package_ids: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (packageDialogType.value !== 'add') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
callback(new Error('请至少选择一个套餐'))
|
||||
} else if (value.length > 100) {
|
||||
callback(new Error('最多选择100个套餐'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
cost_price_yuan: [
|
||||
{ required: true, message: '请输入成本价', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '成本价不能小于0', trigger: 'blur' }
|
||||
{
|
||||
required: packageDialogType.value === 'edit',
|
||||
message: '请输入成本价',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
callback(new Error('请输入成本价'))
|
||||
} else if (value < 0) {
|
||||
callback(new Error('成本价不能小于0'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
// 梯度配置表格数据(合并授权数据和系列数据)
|
||||
watch(
|
||||
() => packageForm.value.package_id,
|
||||
(packageId) => {
|
||||
if (packageDialogType.value !== 'add' || !packageId) return
|
||||
|
||||
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
|
||||
if (!selectedPackage) return
|
||||
|
||||
const originalCostPrice =
|
||||
selectedPackage.cost_price !== undefined && selectedPackage.cost_price !== null
|
||||
? selectedPackage.cost_price / 100
|
||||
: 0
|
||||
|
||||
packageForm.value.package_name = selectedPackage.package_name
|
||||
packageForm.value.package_code = selectedPackage.package_code
|
||||
packageForm.value.original_cost_price = originalCostPrice
|
||||
packageForm.value.cost_price_yuan = originalCostPrice
|
||||
}
|
||||
)
|
||||
|
||||
const tierTableData = computed(() => {
|
||||
if (
|
||||
!detailData.value?.commission_tiers ||
|
||||
@@ -507,19 +468,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加套餐对话框
|
||||
const assignPackageForm = (values: Partial<SeriesGrantPackageForm> = {}) => {
|
||||
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
||||
}
|
||||
|
||||
const resolvePackagePricing = (row?: GrantPackageInfo) => {
|
||||
const pkgOption = availablePackages.value.find((pkg) => pkg.id === row?.package_id)
|
||||
const currentCostPrice = row?.cost_price !== undefined ? row.cost_price / 100 : 0
|
||||
|
||||
return {
|
||||
original_cost_price:
|
||||
pkgOption?.cost_price !== undefined && pkgOption.cost_price !== null
|
||||
? pkgOption.cost_price / 100
|
||||
: currentCostPrice,
|
||||
suggested_retail_price:
|
||||
pkgOption?.suggested_retail_price !== undefined && pkgOption.suggested_retail_price !== null
|
||||
? pkgOption.suggested_retail_price / 100
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加套餐对话框
|
||||
const showAddPackageDialog = () => {
|
||||
packageDialogType.value = 'add'
|
||||
packageForm.value = {
|
||||
package_id: undefined,
|
||||
allocation_id: undefined,
|
||||
package_name: undefined,
|
||||
package_code: undefined,
|
||||
original_cost_price: undefined,
|
||||
cost_price_yuan: 0,
|
||||
expiry_base_override: 'default',
|
||||
initial_expiry_base_override: 'default'
|
||||
}
|
||||
assignPackageForm({ package_ids: [], packages: [] })
|
||||
|
||||
// 加载可用套餐
|
||||
if (detailData.value?.series_id) {
|
||||
@@ -532,23 +505,44 @@
|
||||
// 显示编辑套餐对话框
|
||||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||||
packageDialogType.value = 'edit'
|
||||
packageForm.value = {
|
||||
const packagePricing = resolvePackagePricing(row)
|
||||
|
||||
assignPackageForm({
|
||||
package_id: row.package_id,
|
||||
allocation_id: row.allocation_id,
|
||||
package_name: row.package_name,
|
||||
package_code: row.package_code,
|
||||
original_cost_price: row.cost_price / 100,
|
||||
original_cost_price: packagePricing.original_cost_price,
|
||||
cost_price_yuan: row.cost_price / 100,
|
||||
suggested_retail_price: packagePricing.suggested_retail_price,
|
||||
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
||||
default_expiry_base_name: row.default_expiry_base_name,
|
||||
expiry_base_override_name: row.expiry_base_override_name,
|
||||
effective_expiry_base_name: row.effective_expiry_base_name
|
||||
}
|
||||
})
|
||||
|
||||
packageDialogVisible.value = true
|
||||
}
|
||||
|
||||
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
|
||||
if (row.allocation_id === undefined || row.allocation_id === null) {
|
||||
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||
return
|
||||
}
|
||||
expiryForm.value = {
|
||||
package_id: row.package_id,
|
||||
allocation_id: row.allocation_id,
|
||||
package_name: row.package_name,
|
||||
package_code: row.package_code,
|
||||
default_expiry_base_name: row.default_expiry_base_name,
|
||||
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||
expiry_base_override_name: row.expiry_base_override_name,
|
||||
effective_expiry_base_name: row.effective_expiry_base_name
|
||||
}
|
||||
expiryDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 加载可用套餐
|
||||
const loadAvailablePackages = async (packageName?: string) => {
|
||||
if (!detailData.value?.series_id) return
|
||||
@@ -596,79 +590,139 @@
|
||||
}
|
||||
}
|
||||
|
||||
const createPackageCostFormItem = (packageId: number): PackageCostFormItem => {
|
||||
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
|
||||
const originalCostPrice = selectedPackage?.cost_price ? selectedPackage.cost_price / 100 : 0
|
||||
return {
|
||||
package_id: packageId,
|
||||
package_name: selectedPackage?.package_name,
|
||||
package_code: selectedPackage?.package_code,
|
||||
cost_price_yuan: originalCostPrice,
|
||||
original_cost_price: originalCostPrice,
|
||||
suggested_retail_price:
|
||||
selectedPackage?.suggested_retail_price !== undefined &&
|
||||
selectedPackage.suggested_retail_price !== null
|
||||
? selectedPackage.suggested_retail_price / 100
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getPackageCostPriceMax = (pkg?: {
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number | null
|
||||
}) => {
|
||||
if (pkg?.suggested_retail_price !== undefined && pkg.suggested_retail_price !== null) {
|
||||
return Number((pkg.suggested_retail_price * 1.5).toFixed(2))
|
||||
}
|
||||
if (pkg?.original_cost_price !== undefined && pkg.original_cost_price !== null) {
|
||||
return Number((pkg.original_cost_price * 1.5).toFixed(2))
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
watch(
|
||||
() => packageForm.value.package_ids,
|
||||
(packageIds) => {
|
||||
if (packageDialogType.value !== 'add') return
|
||||
const existingItems = new Map(
|
||||
packageForm.value.packages.map((item) => [item.package_id, item])
|
||||
)
|
||||
packageForm.value.packages = packageIds.map(
|
||||
(packageId) => existingItems.get(packageId) || createPackageCostFormItem(packageId)
|
||||
)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 保存套餐
|
||||
const handleSavePackage = async () => {
|
||||
if (!packageFormRef.value || !detailData.value) return
|
||||
if (!detailData.value) return
|
||||
|
||||
const grantId = detailData.value.id // 保存到常量中避免类型检查问题
|
||||
await packageFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (packageDialogType.value === 'add') {
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages: [
|
||||
{
|
||||
package_id: packageForm.value.package_id!,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
const expiryBaseChanged =
|
||||
packageForm.value.expiry_base_override !==
|
||||
packageForm.value.initial_expiry_base_override
|
||||
if (
|
||||
expiryBaseChanged &&
|
||||
(packageForm.value.allocation_id === undefined ||
|
||||
packageForm.value.allocation_id === null)
|
||||
) {
|
||||
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||
return
|
||||
const grantId = detailData.value.id
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (packageDialogType.value === 'add') {
|
||||
if (packageForm.value.packages.length === 0) {
|
||||
ElMessage.error('请至少选择一个套餐')
|
||||
return
|
||||
}
|
||||
const packages: GrantPackageItem[] = packageForm.value.packages.map((pkg) => {
|
||||
const currentCostPrice = Number(pkg.cost_price_yuan)
|
||||
if (!Number.isFinite(currentCostPrice) || currentCostPrice < 0) {
|
||||
throw new Error(`套餐 ${pkg.package_name || pkg.package_id} 的成本价无效`)
|
||||
}
|
||||
|
||||
const packages: GrantPackageItem[] = [
|
||||
{
|
||||
package_id: packageForm.value.package_id!,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
}
|
||||
]
|
||||
if (expiryBaseChanged) {
|
||||
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||
packageForm.value.allocation_id!,
|
||||
{
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override
|
||||
}
|
||||
const maxCostPrice = getPackageCostPriceMax(pkg)
|
||||
if (maxCostPrice !== undefined && currentCostPrice > maxCostPrice) {
|
||||
throw new Error(
|
||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
if (pkg.original_cost_price !== undefined && currentCostPrice < pkg.original_cost_price) {
|
||||
throw new Error(
|
||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
return {
|
||||
package_id: pkg.package_id,
|
||||
cost_price: Math.round(currentCostPrice * 100)
|
||||
}
|
||||
})
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
} else {
|
||||
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
||||
if (Number.isNaN(currentCostPriceYuan)) {
|
||||
throw new Error('当前成本价无效,无法保存')
|
||||
}
|
||||
const costPrice = Math.round(currentCostPriceYuan * 100)
|
||||
const expiryBaseChanged =
|
||||
packageForm.value.expiry_base_override !== packageForm.value.initial_expiry_base_override
|
||||
if (
|
||||
expiryBaseChanged &&
|
||||
(packageForm.value.allocation_id === undefined ||
|
||||
packageForm.value.allocation_id === null)
|
||||
) {
|
||||
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||
return
|
||||
}
|
||||
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
packageDialogVisible.value = false
|
||||
|
||||
// 刷新详情数据
|
||||
await fetchDetail()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log(packageDialogType.value === 'add' ? '添加失败' : '更新失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
const packages: GrantPackageItem[] = [
|
||||
{ package_id: packageForm.value.package_id!, cost_price: costPrice }
|
||||
]
|
||||
if (expiryBaseChanged) {
|
||||
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||
packageForm.value.allocation_id!,
|
||||
{
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override
|
||||
}
|
||||
)
|
||||
}
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
}
|
||||
})
|
||||
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
packageDialogVisible.value = false
|
||||
|
||||
// 刷新详情数据
|
||||
await fetchDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存套餐失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除套餐
|
||||
@@ -690,7 +744,10 @@
|
||||
}
|
||||
]
|
||||
|
||||
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
|
||||
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, {
|
||||
expiry_base_override: row.expiry_base_override ?? null,
|
||||
packages
|
||||
})
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 刷新详情数据
|
||||
@@ -707,17 +764,35 @@
|
||||
})
|
||||
}
|
||||
|
||||
const saveExpiryBase = async () => {
|
||||
if (expiryForm.value.allocation_id === undefined || expiryForm.value.allocation_id === null) {
|
||||
return
|
||||
}
|
||||
|
||||
expirySubmitLoading.value = true
|
||||
try {
|
||||
const response = await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||
expiryForm.value.allocation_id,
|
||||
{
|
||||
expiry_base_override:
|
||||
expiryForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: expiryForm.value.expiry_base_override
|
||||
}
|
||||
)
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('生效条件更新成功')
|
||||
expiryDialogVisible.value = false
|
||||
await fetchDetail()
|
||||
}
|
||||
} finally {
|
||||
expirySubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handlePackageDialogClosed = () => {
|
||||
packageFormRef.value?.resetFields()
|
||||
packageForm.value = {
|
||||
package_name: undefined,
|
||||
package_code: undefined,
|
||||
original_cost_price: undefined,
|
||||
cost_price_yuan: 0,
|
||||
expiry_base_override: 'default',
|
||||
initial_expiry_base_override: 'default'
|
||||
}
|
||||
assignPackageForm()
|
||||
availablePackages.value = []
|
||||
}
|
||||
|
||||
@@ -764,6 +839,13 @@
|
||||
|
||||
.package-table {
|
||||
margin-top: 12px;
|
||||
|
||||
.package-table-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tier-table-wrapper {
|
||||
@@ -794,6 +876,21 @@
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.ellipsis-value {
|
||||
display: block;
|
||||
width: 260px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -21,189 +21,60 @@
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="package_id"
|
||||
:loading="loading"
|
||||
<SeriesGrantPackageTable
|
||||
:data="packageList"
|
||||
:marginTop="10"
|
||||
:pagination="false"
|
||||
:actions="getActions"
|
||||
:actionsWidth="240"
|
||||
:inlineActionsCount="3"
|
||||
:loading="loading"
|
||||
:show-actions="true"
|
||||
:actions-width="240"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn
|
||||
prop="package_name"
|
||||
label="套餐名称"
|
||||
minWidth="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="package_code"
|
||||
label="套餐编码"
|
||||
minWidth="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn label="成本价" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐默认生效条件" minWidth="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.default_expiry_base_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="覆盖生效条件" minWidth="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.expiry_base_override_name || '跟随套餐默认' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="最终生效条件" minWidth="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.effective_expiry_base_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上架状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
||||
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<template #actions="{ row }">
|
||||
<div class="package-table-actions">
|
||||
<ElButton
|
||||
v-if="canUpdateExpiryBase"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
@click="showExpiryBaseDialog(row)"
|
||||
>
|
||||
修改生效条件
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="hasAuth('series_grants:edit_packages_list')"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
@click="showEditPackageDialog(row)"
|
||||
>
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="hasAuth('series_grants:delete_packages_list')"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="handleDeletePackage(row)"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</SeriesGrantPackageTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDialog
|
||||
<SeriesGrantPackageDialog
|
||||
v-model="packageDialogVisible"
|
||||
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
||||
width="40%"
|
||||
:close-on-click-modal="false"
|
||||
v-model:form="packageForm"
|
||||
:dialog-type="packageDialogType"
|
||||
:rules="packageRules"
|
||||
:available-packages="availablePackages"
|
||||
:package-loading="packageLoading"
|
||||
:submit-loading="submitLoading"
|
||||
:can-update-expiry-base="canUpdateExpiryBase"
|
||||
:has-series="Boolean(seriesId)"
|
||||
@search="searchAvailablePackages"
|
||||
@submit="handleSavePackage"
|
||||
@closed="handlePackageDialogClosed"
|
||||
>
|
||||
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="140px">
|
||||
<ElFormItem label="选择套餐" prop="package_ids" v-if="packageDialogType === 'add'">
|
||||
<ElSelect
|
||||
v-model="packageForm.package_ids"
|
||||
placeholder="请选择套餐(可多选)"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchAvailablePackages"
|
||||
:loading="packageLoading"
|
||||
clearable
|
||||
multiple
|
||||
:multiple-limit="100"
|
||||
>
|
||||
<template v-if="availablePackages.length === 0 && !packageLoading && seriesId">
|
||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="pkg in availablePackages"
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
||||
:value="pkg.id"
|
||||
:disabled="pkg.is_authorized"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="packageDialogType === 'add' && packageForm.packages.length"
|
||||
label="套餐配置"
|
||||
prop="packages"
|
||||
>
|
||||
<div class="package-config-list">
|
||||
<div
|
||||
v-for="pkg in packageForm.packages"
|
||||
:key="pkg.package_id"
|
||||
class="package-config-item"
|
||||
>
|
||||
<div class="package-config-name" :title="pkg.package_name || ''">
|
||||
{{ pkg.package_name || '套餐名称不可用' }}
|
||||
</div>
|
||||
<div class="package-config-cost">
|
||||
<span class="package-config-field-label">套餐成本价(元)</span>
|
||||
<ElInputNumber
|
||||
v-model="pkg.cost_price_yuan"
|
||||
:min="pkg.original_cost_price || 0"
|
||||
:max="getPackageCostPriceMax(pkg)"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
placeholder="请输入成本价"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
||||
<span>{{ packageForm.package_name }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
|
||||
<span>{{ packageForm.package_code }}</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
||||
<ElInputNumber
|
||||
v-model="packageForm.cost_price_yuan"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入成本价"
|
||||
/>
|
||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
||||
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
|
||||
packageForm.original_cost_price.toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
|
||||
<ElSelect
|
||||
v-model="packageForm.expiry_base_override"
|
||||
placeholder="请选择生效条件"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="跟随套餐默认" value="default" />
|
||||
<ElOption label="购买即生效" value="from_purchase" />
|
||||
<ElOption label="实名激活时生效" value="from_activation" />
|
||||
</ElSelect>
|
||||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||
</ElFormItem>
|
||||
<template v-if="packageDialogType === 'edit'">
|
||||
<ElFormItem label="套餐默认生效条件">
|
||||
{{ packageForm.default_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
<ElFormItem label="覆盖生效条件">
|
||||
{{ packageForm.expiry_base_override_name || '跟随套餐默认' }}
|
||||
</ElFormItem>
|
||||
<ElFormItem label="最终生效条件">
|
||||
{{ packageForm.effective_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="packageDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSavePackage" :loading="submitLoading">
|
||||
保存
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
/>
|
||||
|
||||
<ElDialog
|
||||
v-model="expiryDialogVisible"
|
||||
@@ -252,22 +123,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElCard,
|
||||
ElButton,
|
||||
ElIcon,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
ElDialog,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElInputNumber,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElMessage,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
@@ -283,9 +150,13 @@
|
||||
} from '@/types/api'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import SeriesGrantPackageTable from '@/components/business/SeriesGrantPackageTable.vue'
|
||||
import SeriesGrantPackageDialog from '@/components/business/SeriesGrantPackageDialog.vue'
|
||||
import {
|
||||
mergeGrantPackageCandidates,
|
||||
type GrantPackageCandidate
|
||||
type GrantPackageCandidate,
|
||||
type SeriesGrantPackageForm,
|
||||
type PackageCostFormItem
|
||||
} from '@/utils/business/seriesGrantPackage'
|
||||
|
||||
defineOptions({ name: 'SeriesGrantPackages' })
|
||||
@@ -298,15 +169,11 @@
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const packageLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
const grantId = computed(() => Number(route.params.id))
|
||||
const seriesId = ref<number>(0)
|
||||
const grantName = ref<string>('')
|
||||
const packageList = ref<GrantPackageInfo[]>([])
|
||||
const availablePackages = ref<GrantPackageCandidate[]>([])
|
||||
const packageFormRef = ref<FormInstance>()
|
||||
|
||||
const packageDialogVisible = ref(false)
|
||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||
const expiryDialogVisible = ref(false)
|
||||
@@ -314,32 +181,6 @@
|
||||
|
||||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||
|
||||
type PackageCostFormItem = {
|
||||
package_id: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price_yuan: number
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number
|
||||
}
|
||||
|
||||
type PackageFormState = {
|
||||
package_id?: number
|
||||
allocation_id?: number
|
||||
package_ids: number[]
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price_yuan: number
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number
|
||||
expiry_base_override: ExpiryBaseSelection
|
||||
initial_expiry_base_override: ExpiryBaseSelection
|
||||
default_expiry_base_name?: string | null
|
||||
expiry_base_override_name?: string | null
|
||||
effective_expiry_base_name?: string | null
|
||||
packages: PackageCostFormItem[]
|
||||
}
|
||||
|
||||
type ExpiryFormState = {
|
||||
package_id?: number
|
||||
allocation_id?: number
|
||||
@@ -351,7 +192,7 @@
|
||||
effective_expiry_base_name?: string | null
|
||||
}
|
||||
|
||||
const createDefaultPackageForm = (): PackageFormState => ({
|
||||
const createDefaultPackageForm = (): SeriesGrantPackageForm => ({
|
||||
package_id: undefined,
|
||||
allocation_id: undefined,
|
||||
package_ids: [],
|
||||
@@ -362,10 +203,10 @@
|
||||
initial_expiry_base_override: 'default',
|
||||
packages: []
|
||||
})
|
||||
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
|
||||
const packageForm = ref<SeriesGrantPackageForm>(createDefaultPackageForm())
|
||||
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
|
||||
|
||||
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
|
||||
const assignPackageForm = (values: Partial<SeriesGrantPackageForm> = {}) => {
|
||||
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
||||
}
|
||||
|
||||
@@ -426,36 +267,6 @@
|
||||
]
|
||||
}))
|
||||
|
||||
const getActions = (row: GrantPackageInfo) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (canUpdateExpiryBase) {
|
||||
actions.push({
|
||||
label: '修改生效条件',
|
||||
handler: () => showExpiryBaseDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:edit_packages_list')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showEditPackageDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('series_grants:delete_packages_list')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleDeletePackage(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
@@ -484,7 +295,6 @@
|
||||
assignPackageForm({ package_ids: [], packages: [] })
|
||||
loadAvailablePackages()
|
||||
packageDialogVisible.value = true
|
||||
nextTick(() => packageFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||||
@@ -506,7 +316,6 @@
|
||||
effective_expiry_base_name: row.effective_expiry_base_name
|
||||
})
|
||||
packageDialogVisible.value = true
|
||||
nextTick(() => packageFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
|
||||
@@ -605,120 +414,90 @@
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => packageForm.value.package_id,
|
||||
(packageId) => {
|
||||
if (packageDialogType.value === 'add' && packageId) {
|
||||
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
|
||||
if (selectedPackage) {
|
||||
const originalCostPrice = selectedPackage.cost_price
|
||||
? selectedPackage.cost_price / 100
|
||||
: 0
|
||||
const suggestedRetailPrice =
|
||||
selectedPackage.suggested_retail_price !== undefined &&
|
||||
selectedPackage.suggested_retail_price !== null
|
||||
? selectedPackage.suggested_retail_price / 100
|
||||
: undefined
|
||||
packageForm.value.original_cost_price = originalCostPrice
|
||||
packageForm.value.suggested_retail_price = suggestedRetailPrice
|
||||
packageForm.value.cost_price_yuan = originalCostPrice
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSavePackage = async () => {
|
||||
if (!packageFormRef.value || !grantId.value) return
|
||||
if (!grantId.value) return
|
||||
|
||||
await packageFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (packageDialogType.value === 'add') {
|
||||
if (packageForm.value.packages.length === 0) {
|
||||
ElMessage.error('请至少选择一个套餐')
|
||||
return
|
||||
}
|
||||
const packages: GrantPackageItem[] = packageForm.value.packages.map((pkg) => {
|
||||
const currentCostPrice = Number(pkg.cost_price_yuan)
|
||||
if (!Number.isFinite(currentCostPrice) || currentCostPrice < 0) {
|
||||
throw new Error(`套餐 ${pkg.package_name || pkg.package_id} 的成本价无效`)
|
||||
}
|
||||
const maxCostPrice = getPackageCostPriceMax(pkg)
|
||||
if (maxCostPrice !== undefined && currentCostPrice > maxCostPrice) {
|
||||
throw new Error(
|
||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
if (pkg.original_cost_price !== undefined && currentCostPrice < pkg.original_cost_price) {
|
||||
throw new Error(
|
||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
return {
|
||||
package_id: pkg.package_id,
|
||||
cost_price: Math.round(currentCostPrice * 100)
|
||||
}
|
||||
})
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
} else {
|
||||
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
||||
if (Number.isNaN(currentCostPriceYuan)) {
|
||||
throw new Error('当前成本价无效,无法保存')
|
||||
}
|
||||
const costPrice = Math.round(currentCostPriceYuan * 100)
|
||||
if (packageDialogType.value === 'add') {
|
||||
if (packageForm.value.packages.length === 0) {
|
||||
ElMessage.error('请至少选择一个套餐')
|
||||
return
|
||||
}
|
||||
const packages: GrantPackageItem[] = packageForm.value.packages.map((pkg) => {
|
||||
const currentCostPrice = Number(pkg.cost_price_yuan)
|
||||
if (!Number.isFinite(currentCostPrice) || currentCostPrice < 0) {
|
||||
throw new Error(`套餐 ${pkg.package_name || pkg.package_id} 的成本价无效`)
|
||||
}
|
||||
const maxCostPrice = getPackageCostPriceMax(pkg)
|
||||
if (maxCostPrice !== undefined && currentCostPrice > maxCostPrice) {
|
||||
throw new Error(
|
||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
if (
|
||||
pkg.original_cost_price !== undefined &&
|
||||
currentCostPrice < pkg.original_cost_price
|
||||
) {
|
||||
throw new Error(
|
||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
||||
)
|
||||
}
|
||||
return {
|
||||
package_id: pkg.package_id,
|
||||
cost_price: Math.round(currentCostPrice * 100)
|
||||
}
|
||||
})
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
} else {
|
||||
const expiryBaseChanged =
|
||||
packageForm.value.expiry_base_override !==
|
||||
packageForm.value.initial_expiry_base_override
|
||||
if (
|
||||
expiryBaseChanged &&
|
||||
(packageForm.value.allocation_id === undefined ||
|
||||
packageForm.value.allocation_id === null)
|
||||
) {
|
||||
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||
return
|
||||
}
|
||||
|
||||
const packages: GrantPackageItem[] = [
|
||||
{ package_id: packageForm.value.package_id!, cost_price: costPrice }
|
||||
]
|
||||
if (expiryBaseChanged) {
|
||||
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||
packageForm.value.allocation_id!,
|
||||
{
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override
|
||||
}
|
||||
)
|
||||
}
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
const expiryBaseChanged =
|
||||
packageForm.value.expiry_base_override !== packageForm.value.initial_expiry_base_override
|
||||
if (
|
||||
expiryBaseChanged &&
|
||||
(packageForm.value.allocation_id === undefined ||
|
||||
packageForm.value.allocation_id === null)
|
||||
) {
|
||||
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||
return
|
||||
}
|
||||
packageDialogVisible.value = false
|
||||
await fetchPackageList()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存套餐失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
|
||||
const packages: GrantPackageItem[] = [
|
||||
{ package_id: packageForm.value.package_id!, cost_price: costPrice }
|
||||
]
|
||||
if (expiryBaseChanged) {
|
||||
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||
packageForm.value.allocation_id!,
|
||||
{
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override
|
||||
}
|
||||
)
|
||||
}
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
||||
expiry_base_override:
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages
|
||||
})
|
||||
}
|
||||
})
|
||||
packageDialogVisible.value = false
|
||||
await fetchPackageList()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存套餐失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveExpiryBase = async () => {
|
||||
@@ -779,7 +558,6 @@
|
||||
}
|
||||
|
||||
const handlePackageDialogClosed = () => {
|
||||
packageFormRef.value?.clearValidate()
|
||||
assignPackageForm()
|
||||
availablePackages.value = []
|
||||
}
|
||||
@@ -827,9 +605,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-warning);
|
||||
.package-table-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
@@ -838,51 +618,15 @@
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.package-config-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.package-config-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.package-config-name,
|
||||
.ellipsis-value {
|
||||
display: block;
|
||||
width: 260px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.package-config-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.package-config-cost {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.package-config-field-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ellipsis-value {
|
||||
display: block;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="编辑系统配置"
|
||||
width="560px"
|
||||
width="30%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm label-width="120px">
|
||||
<ElForm label-width="80px">
|
||||
<ElFormItem label="配置说明">
|
||||
<span>{{ currentConfig?.description || '-' }}</span>
|
||||
</ElFormItem>
|
||||
@@ -90,7 +90,14 @@
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit"> 保存 </ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
@@ -111,6 +118,7 @@
|
||||
type SystemConfigModule
|
||||
} from '@/types/api/systemConfig'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
@@ -258,7 +266,8 @@
|
||||
const validateValue = (value: string) => {
|
||||
const config = currentConfig.value
|
||||
if (!config) return '配置不存在'
|
||||
if (config.enum_values.length > 0 && !config.enum_values.includes(value)) {
|
||||
const enumValues = config.enum_values ?? []
|
||||
if (enumValues.length > 0 && !enumValues.includes(value)) {
|
||||
return '配置值不在允许的枚举范围内'
|
||||
}
|
||||
if (config.value_type === 'int') {
|
||||
@@ -324,7 +333,14 @@
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentConfig.value || currentConfig.value.readonly) return
|
||||
if (!currentConfig.value) {
|
||||
ElMessage.error('未选择要编辑的系统配置')
|
||||
return
|
||||
}
|
||||
if (currentConfig.value.readonly) {
|
||||
ElMessage.warning('当前配置为只读配置,不能保存')
|
||||
return
|
||||
}
|
||||
const value = getSubmittedValue()
|
||||
if (currentConfig.value.sensitive && value === originalValue.value) {
|
||||
ElMessage.warning('敏感配置未输入新值,无需保存')
|
||||
@@ -351,6 +367,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新系统配置失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -303,6 +303,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ShopCreditLimitDialog
|
||||
v-model="creditDialogVisible"
|
||||
:shop="currentCreditShop"
|
||||
@submitted="getShopList"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -326,21 +332,23 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { generateShopCode } from '@/utils/codeGenerator'
|
||||
import { ShopService, RoleService } from '@/api/modules'
|
||||
import { CommissionService, ShopService, RoleService } from '@/api/modules'
|
||||
import ShopCreditLimitDialog from '@/components/business/ShopCreditLimitDialog.vue'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type {
|
||||
CreateShopParams,
|
||||
ShopBusinessOwnerCandidate,
|
||||
ShopResponse,
|
||||
ShopRoleResponse,
|
||||
UpdateShopParams
|
||||
} from '@/types/api'
|
||||
import { RoleType, RoleStatus } from '@/types/api'
|
||||
import type {
|
||||
CreateShopParams,
|
||||
ShopBusinessOwnerCandidate,
|
||||
ShopResponse,
|
||||
ShopRoleResponse,
|
||||
UpdateShopParams
|
||||
} from '@/types/api'
|
||||
import type { ShopFundSummaryItem } from '@/types/api/commission'
|
||||
import { RoleType, RoleStatus } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getCompatibleNumericId } from '@/utils/business/id'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'Shop' })
|
||||
|
||||
@@ -355,7 +363,10 @@
|
||||
const defaultRoleLoading = ref(false)
|
||||
const defaultRoleList = ref<any[]>([])
|
||||
const salespersonLoading = ref(false)
|
||||
const salespersonOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const salespersonOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const creditDialogVisible = ref(false)
|
||||
const currentCreditShop = ref<ShopFundSummaryItem | null>(null)
|
||||
const creditLoadingShopId = ref<number | null>(null)
|
||||
|
||||
// 级联选择相关
|
||||
const parentShopCascadeOptions = ref<any[]>([])
|
||||
@@ -422,11 +433,11 @@
|
||||
const searchPlatformSalespeople = async (query: string) => {
|
||||
salespersonLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword: query || undefined
|
||||
})
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword: query || undefined
|
||||
})
|
||||
if (res.code === 0) {
|
||||
salespersonOptions.value = res.data.items || []
|
||||
}
|
||||
@@ -438,20 +449,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
const formatBusinessOwner = (shop: ShopResponse) => {
|
||||
if (!shop.business_owner_username) return '-'
|
||||
return shop.business_owner_phone_summary
|
||||
? `${shop.business_owner_username} (${shop.business_owner_phone_summary})`
|
||||
: shop.business_owner_username
|
||||
}
|
||||
const formatBusinessOwner = (shop: ShopResponse) => {
|
||||
if (!shop.business_owner_username) return '-'
|
||||
return shop.business_owner_phone_summary
|
||||
? `${shop.business_owner_username} (${shop.business_owner_phone_summary})`
|
||||
: shop.business_owner_username
|
||||
}
|
||||
|
||||
const handleNameClick = (row: ShopResponse) => {
|
||||
if (hasAuth('shop:detail')) {
|
||||
void router.push(`${RoutesAlias.ShopDetail}/${row.id}`)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
const handleNameClick = (row: ShopResponse) => {
|
||||
if (hasAuth('shop:detail')) {
|
||||
void router.push(`${RoutesAlias.ShopDetail}/${row.id}`)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
@@ -575,8 +586,8 @@
|
||||
},
|
||||
options: () =>
|
||||
salespersonOptions.value.map((salesperson) => ({
|
||||
label: `${salesperson.username} (${salesperson.phone_summary})`,
|
||||
value: salesperson.id
|
||||
label: `${salesperson.username} (${salesperson.phone_summary})`,
|
||||
value: salesperson.id
|
||||
}))
|
||||
}
|
||||
])
|
||||
@@ -619,8 +630,8 @@
|
||||
formData.address = row.address || ''
|
||||
formData.contact_name = row.contact_name || ''
|
||||
formData.contact_phone = row.contact_phone || ''
|
||||
formData.business_owner_account_id = row.business_owner_account_id ?? null
|
||||
formData.client_login_disabled = row.client_login_disabled
|
||||
formData.business_owner_account_id = row.business_owner_account_id ?? null
|
||||
formData.client_login_disabled = row.client_login_disabled
|
||||
formData.status = row.status
|
||||
formData.init_username = ''
|
||||
formData.init_password = ''
|
||||
@@ -638,8 +649,8 @@
|
||||
formData.address = ''
|
||||
formData.contact_name = ''
|
||||
formData.contact_phone = ''
|
||||
formData.business_owner_account_id = null
|
||||
formData.client_login_disabled = false
|
||||
formData.business_owner_account_id = null
|
||||
formData.client_login_disabled = false
|
||||
formData.status = CommonStatus.ENABLED
|
||||
formData.init_username = ''
|
||||
formData.init_password = ''
|
||||
@@ -680,29 +691,29 @@
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => {
|
||||
const hasPermission = hasAuth('shop:detail')
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: hasPermission
|
||||
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
|
||||
: '',
|
||||
onClick: hasPermission
|
||||
? (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
row.shop_name
|
||||
)
|
||||
}
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => {
|
||||
const hasPermission = hasAuth('shop:detail')
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: hasPermission
|
||||
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
|
||||
: '',
|
||||
onClick: hasPermission
|
||||
? (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
row.shop_name
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'shop_code',
|
||||
@@ -741,19 +752,19 @@
|
||||
label: '联系电话',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'business_owner_username',
|
||||
label: '平台业务员',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => formatBusinessOwner(row)
|
||||
},
|
||||
{
|
||||
prop: 'client_login_disabled',
|
||||
label: 'C 端登录',
|
||||
width: 110,
|
||||
formatter: (row: ShopResponse) => (row.client_login_disabled ? '已限制' : '正常')
|
||||
},
|
||||
{
|
||||
prop: 'business_owner_username',
|
||||
label: '平台业务员',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => formatBusinessOwner(row)
|
||||
},
|
||||
{
|
||||
prop: 'client_login_disabled',
|
||||
label: 'C 端登录',
|
||||
width: 110,
|
||||
formatter: (row: ShopResponse) => (row.client_login_disabled ? '已限制' : '正常')
|
||||
},
|
||||
...(canModifyShopStatus
|
||||
? [
|
||||
{
|
||||
@@ -803,6 +814,14 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('shop:credit_limit_manage')) {
|
||||
actions.push({
|
||||
label: '调整实际信用额度',
|
||||
handler: () => showCreditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('shop:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
@@ -822,6 +841,34 @@
|
||||
return actions
|
||||
}
|
||||
|
||||
const showCreditDialog = async (row: ShopResponse) => {
|
||||
if (creditLoadingShopId.value === row.id) return
|
||||
|
||||
creditLoadingShopId.value = row.id
|
||||
try {
|
||||
const res = await CommissionService.getShopFundSummary({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
shop_name: row.shop_name
|
||||
})
|
||||
const summary =
|
||||
res.code === 0 ? (res.data.items || []).find((item) => item.shop_id === row.id) : null
|
||||
|
||||
if (!summary) {
|
||||
ElMessage.warning('未获取到该店铺的资金概况,请刷新店铺列表后重试')
|
||||
return
|
||||
}
|
||||
|
||||
currentCreditShop.value = summary
|
||||
creditDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取店铺资金概况失败:', error)
|
||||
ElMessage.error('获取店铺资金概况失败,请稍后重试')
|
||||
} finally {
|
||||
creditLoadingShopId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
@@ -843,8 +890,8 @@
|
||||
init_password: '',
|
||||
init_phone: '',
|
||||
default_role_id: undefined as number | undefined,
|
||||
business_owner_account_id: null as number | null,
|
||||
client_login_disabled: false
|
||||
business_owner_account_id: null as number | null,
|
||||
client_login_disabled: false
|
||||
})
|
||||
|
||||
// 处理编码生成
|
||||
@@ -874,10 +921,10 @@
|
||||
const searchDefaultRoles = async (query: string) => {
|
||||
defaultRoleLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
role_type: RoleType.CUSTOMER, // 仅客户角色
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
role_type: RoleType.CUSTOMER, // 仅客户角色
|
||||
status: RoleStatus.ENABLED // 仅启用的角色
|
||||
}
|
||||
if (query) {
|
||||
@@ -1013,8 +1060,8 @@
|
||||
init_password: formData.init_password,
|
||||
init_phone: formData.init_phone,
|
||||
default_role_id: formData.default_role_id!,
|
||||
business_owner_account_id: formData.business_owner_account_id,
|
||||
client_login_disabled: formData.client_login_disabled
|
||||
business_owner_account_id: formData.business_owner_account_id,
|
||||
client_login_disabled: formData.client_login_disabled
|
||||
}
|
||||
|
||||
// 可选字段 - parent_id 可能是数组(级联选择器)或数字
|
||||
@@ -1036,8 +1083,8 @@
|
||||
const data: UpdateShopParams = {
|
||||
shop_name: formData.shop_name,
|
||||
status: formData.status,
|
||||
business_owner_account_id: formData.business_owner_account_id,
|
||||
client_login_disabled: formData.client_login_disabled
|
||||
business_owner_account_id: formData.business_owner_account_id,
|
||||
client_login_disabled: formData.client_login_disabled
|
||||
}
|
||||
|
||||
// 可选字段
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<ElDescriptionsItem label="状态">
|
||||
{{ role.status === 1 ? '启用' : '禁用' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="新建代理默认信用">
|
||||
<ElDescriptionsItem label="代理默认信用">
|
||||
{{ role.default_credit_enabled ? '启用' : '未启用' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="默认信用额度">
|
||||
<ElDescriptionsItem label="默认信用额度(元)">
|
||||
{{ formatCredit(role.default_credit_limit) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="信用生效范围">
|
||||
@@ -60,7 +60,10 @@
|
||||
|
||||
const formatCredit = (value?: number) => {
|
||||
if (value === undefined || value === null) return '-'
|
||||
return `${(value / 100).toFixed(2)} 元`
|
||||
return (value / 100).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
const handleBack = () => router.back()
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="默认信用额度">
|
||||
<div class="credit-form-unit">单位:元</div>
|
||||
<ElInputNumber
|
||||
v-model="form.credit_limit_yuan"
|
||||
:disabled="!form.credit_enabled"
|
||||
@@ -97,8 +98,10 @@
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
placeholder="请输入默认信用额度"
|
||||
:formatter="formatCreditInput"
|
||||
:parser="parseCreditInput"
|
||||
/>
|
||||
<div class="credit-form-tip">单位:元。修改后不会影响已有店铺</div>
|
||||
<div class="credit-form-tip">修改后不会影响已有店铺</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
@@ -349,8 +352,8 @@
|
||||
{ label: '角色名称', prop: 'role_name' },
|
||||
{ label: '角色描述', prop: 'role_desc' },
|
||||
{ label: '角色类型', prop: 'role_type' },
|
||||
{ label: '新建代理默认信用', prop: 'default_credit_enabled' },
|
||||
{ label: '默认信用额度', prop: 'default_credit_limit' },
|
||||
{ label: '代理默认信用', prop: 'default_credit_enabled' },
|
||||
{ label: '默认信用额度(元)', prop: 'default_credit_limit' },
|
||||
...(canUpdateRoleStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
@@ -414,6 +417,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
const formatCreditInput = (value: string | number | undefined) => {
|
||||
if (value === undefined || value === null || value === '') return ''
|
||||
|
||||
const normalizedValue = String(value).replace(/,/g, '')
|
||||
const [integerPart, decimalPart] = normalizedValue.split('.')
|
||||
const formattedIntegerPart = (integerPart || '0').replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
|
||||
return decimalPart === undefined
|
||||
? formattedIntegerPart
|
||||
: `${formattedIntegerPart}.${decimalPart}`
|
||||
}
|
||||
|
||||
const parseCreditInput = (value: string) => value.replace(/,/g, '')
|
||||
|
||||
const formatCreditYuan = (value?: number | null) => {
|
||||
if (value === undefined || value === null) return '-'
|
||||
|
||||
return fenToYuan(value).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
const roleList = ref<PlatformRole[]>([])
|
||||
|
||||
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
|
||||
@@ -463,7 +489,7 @@
|
||||
},
|
||||
{
|
||||
prop: 'default_credit_enabled',
|
||||
label: '新建代理默认信用',
|
||||
label: '代理默认信用',
|
||||
width: 150,
|
||||
formatter: (row: PlatformRole) => {
|
||||
if (row.role_type !== RoleType.CUSTOMER) return '-'
|
||||
@@ -474,11 +500,11 @@
|
||||
},
|
||||
{
|
||||
prop: 'default_credit_limit',
|
||||
label: '默认信用额度',
|
||||
width: 140,
|
||||
label: '默认信用额度(元)',
|
||||
width: 160,
|
||||
formatter: (row: PlatformRole) => {
|
||||
if (row.role_type !== RoleType.CUSTOMER || !row.default_credit_enabled) return '-'
|
||||
return `${fenToYuan(row.default_credit_limit)} 元`
|
||||
return formatCreditYuan(row.default_credit_limit)
|
||||
}
|
||||
},
|
||||
...(canUpdateRoleStatus
|
||||
@@ -1267,6 +1293,13 @@
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.credit-form-unit {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.permission-tree-transfer-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
Reference in New Issue
Block a user