front/src/views/order/myOrders/components/AddOrderFeeModal.vue
zzh b178a2a35d
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
feat: 支付选券、优惠入应收展示、申报产品标识编码、创建9610订单等
- 支付弹窗(PC/H5)支持选券与批量支付体验优化,运单管理复用我的订单支付弹窗;立即支付仅允许待支付订单
- 订单详情费用明细展示优惠券抵扣/会员折扣负数行与「含优惠」;价格配置与手工加费过滤抵扣费用项
- 申报明细新增产品标识编码/非标产品标识编码/标准产品标识编码(PC/H5 下单、改单、详情、开放接口文档、公式目录、Excel 模板)
- 9610 页「批量申报」改为「创建9610订单」:选运单并补充出口资质备案、电商平台
- 店铺订单批量按钮按动作独立 loading,获取面单限并发并显示进度
- 另含:国家管理、登录门户切换、监控与工作台、公告弹窗、nginx/vite 配置等改动
2026-10-19 16:34:06 +08:00

468 lines
14 KiB
Vue

<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { getDictListAll, type DictDto } from '@/api/system/dict'
import { postOrderFeeAdd } from '@/api/order'
import { isDiscountCostItem } from '@/constants/feeCostItems'
import dayjs from 'dayjs'
const props = defineProps<{
visible: boolean
orderId?: number
defaultType?: 'receivable' | 'payable'
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
success: [order: any]
}>()
const { t } = useI18n()
const [registerModal, modalMethods] = useModal()
const formRef = ref<FormInstance>()
const submitting = ref(false)
const dictLoading = ref(false)
const feeItemOptions = ref<Array<{ label: string; value: string }>>([])
const currencyOptions = ref<Array<{ label: string; value: string }>>([])
const formState = reactive({
type: 'receivable' as 'receivable' | 'payable',
costItem: '',
amount: undefined as number | undefined,
currency: 'CNY',
chargingStage: 'forecast',
downstreamProvider: '',
priceNo: '',
priceEffectiveTime: undefined as string | undefined,
})
const isPayable = computed(() => formState.type === 'payable')
const chargingStageOptions = computed(() => [
{ label: t('finance.receivablePayable.chargingStage.forecast'), value: 'forecast' },
{ label: t('finance.receivablePayable.chargingStage.inbound'), value: 'inbound' },
{ label: t('finance.receivablePayable.chargingStage.outbound'), value: 'outbound' },
])
function mapDictOptions(rows: DictDto[]) {
return rows
.map((row) => ({
label: String(row.dictName ?? row.dictCode ?? '').trim(),
value: String(row.dictCode ?? '').trim(),
}))
.filter((item) => item.value)
}
async function ensureDictOptions() {
dictLoading.value = true
try {
const [feeItems, currencies] = await Promise.all([
getDictListAll({ dictType: 'fee_item' }),
getDictListAll({ dictType: 'currency' }),
])
feeItemOptions.value = mapDictOptions(feeItems).filter((item) => !isDiscountCostItem(item.value))
currencyOptions.value = mapDictOptions(currencies)
if (!currencyOptions.value.some((item) => item.value === formState.currency)) {
formState.currency = currencyOptions.value[0]?.value || 'CNY'
}
} finally {
dictLoading.value = false
}
}
function resetForm() {
Object.assign(formState, {
type: props.defaultType || 'receivable',
costItem: '',
amount: undefined,
currency: currencyOptions.value.some((item) => item.value === 'CNY')
? 'CNY'
: (currencyOptions.value[0]?.value || 'CNY'),
chargingStage: 'forecast',
downstreamProvider: '',
priceNo: '',
priceEffectiveTime: undefined,
})
}
function selectFeeType(type: 'receivable' | 'payable') {
formState.type = type
if (type === 'receivable') {
formState.downstreamProvider = ''
}
}
watch(
() => props.visible,
async (open) => {
if (!open) return
if (!props.orderId) {
Message.warning(t('order.myOrders.drawer.fee.addMissingOrder'))
emit('update:visible', false)
return
}
resetForm()
try {
await ensureDictOptions()
} catch {
feeItemOptions.value = []
currencyOptions.value = []
}
modalMethods.setModalProps({
title: t('order.myOrders.drawer.fee.addTitle'),
width: 680,
})
modalMethods.openModal(true)
await nextTick()
formRef.value?.clearValidate()
},
)
async function handleSubmit() {
const errors = await formRef.value?.validate()
if (errors) return
const orderId = props.orderId
if (orderId == null) return
submitting.value = true
try {
const priceEffectiveTime = formState.priceEffectiveTime
? dayjs(formState.priceEffectiveTime).format('YYYY-MM-DD HH:mm:ss')
: undefined
const result = await postOrderFeeAdd({
id: orderId,
type: formState.type,
costItem: String(formState.costItem ?? '').trim(),
amount: Number(formState.amount),
currency: String(formState.currency ?? '').trim(),
chargingStage: String(formState.chargingStage ?? '').trim(),
downstreamProvider: String(formState.downstreamProvider ?? '').trim() || undefined,
priceNo: String(formState.priceNo ?? '').trim() || undefined,
priceEffectiveTime,
})
Message.success(t('order.myOrders.drawer.fee.addSuccess'))
emit('success', result)
emit('update:visible', false)
modalMethods.closeModal()
} catch (e: any) {
Message.error(e?.message || t('order.myOrders.drawer.fee.addFailed'))
} finally {
submitting.value = false
}
}
function handleCancel() {
emit('update:visible', false)
modalMethods.closeModal()
}
</script>
<template>
<ModalPro
@register="registerModal"
:loading="submitting"
@ok="handleSubmit"
@cancel="handleCancel"
>
<div class="add-fee-modal">
<div class="fee-type-picker">
<button
type="button"
class="fee-type-card receivable"
:class="{ active: formState.type === 'receivable' }"
@click="selectFeeType('receivable')"
>
<span class="fee-type-dot" />
<span class="fee-type-label">{{ $t('finance.receivablePayable.feeType.receivable') }}</span>
</button>
<button
type="button"
class="fee-type-card payable"
:class="{ active: formState.type === 'payable' }"
@click="selectFeeType('payable')"
>
<span class="fee-type-dot" />
<span class="fee-type-label">{{ $t('finance.receivablePayable.feeType.payable') }}</span>
</button>
</div>
<a-form
ref="formRef"
:model="formState"
layout="vertical"
class="add-fee-form"
>
<div class="add-fee-section">
<div class="section-header">
<span class="section-dot" />
<span class="section-title">{{ $t('order.myOrders.drawer.fee.addSection.basic') }}</span>
</div>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item
field="costItem"
:label="$t('order.myOrders.drawer.costItem')"
:rules="[{ required: true, message: $t('system.serviceProviders.modal.rules.requiredInput') }]"
>
<a-select
v-model="formState.costItem"
allow-search
:loading="dictLoading"
:placeholder="$t('common.selectPlaceholder')"
>
<a-option
v-for="opt in feeItemOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
field="chargingStage"
:label="$t('order.myOrders.drawer.chargingStage')"
:rules="[{ required: true, message: $t('system.serviceProviders.modal.rules.requiredInput') }]"
>
<a-select
v-model="formState.chargingStage"
:placeholder="$t('common.selectPlaceholder')"
>
<a-option
v-for="opt in chargingStageOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</div>
<div class="add-fee-section">
<div class="section-header">
<span class="section-dot" />
<span class="section-title">{{ $t('order.myOrders.drawer.fee.addSection.amount') }}</span>
</div>
<a-row :gutter="20">
<a-col :span="14">
<a-form-item
field="amount"
:label="$t('order.myOrders.drawer.amount')"
:rules="[
{ required: true, message: $t('system.serviceProviders.modal.rules.requiredInput') },
{
validator: (value: number | undefined, callback: (error?: string) => void) => {
if (value == null || Number(value) <= 0) {
callback($t('system.serviceProviders.modal.rules.requiredInput'))
return
}
callback()
},
},
]"
>
<a-input-number
v-model="formState.amount"
:min="0.01"
:precision="2"
:placeholder="$t('order.myOrders.drawer.fee.amountPlaceholder')"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item
field="currency"
:label="$t('order.myOrders.drawer.currency')"
:rules="[{ required: true, message: $t('system.serviceProviders.modal.rules.requiredInput') }]"
>
<a-select
v-model="formState.currency"
allow-search
:loading="dictLoading"
:placeholder="$t('common.selectPlaceholder')"
>
<a-option
v-for="opt in currencyOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</div>
<div class="add-fee-section optional">
<div class="section-header">
<span class="section-dot optional" />
<span class="section-title">{{ $t('order.myOrders.drawer.fee.addSection.optional') }}</span>
<span class="section-hint">{{ $t('order.myOrders.drawer.fee.addSection.optionalHint') }}</span>
</div>
<a-row v-if="isPayable" :gutter="20">
<a-col :span="24">
<a-form-item
field="downstreamProvider"
:label="$t('order.myOrders.drawer.downstreamProvider')"
>
<a-input
v-model="formState.downstreamProvider"
allow-clear
:placeholder="$t('order.myOrders.drawer.fee.downstreamProviderPlaceholder')"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="priceNo" :label="$t('order.myOrders.drawer.priceNo')">
<a-input
v-model="formState.priceNo"
allow-clear
:placeholder="$t('order.myOrders.drawer.fee.priceNoPlaceholder')"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="priceEffectiveTime" :label="$t('order.myOrders.drawer.priceEffectiveTime')">
<a-date-picker
v-model="formState.priceEffectiveTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
:placeholder="$t('order.myOrders.drawer.fee.priceEffectiveTimePlaceholder')"
/>
</a-form-item>
</a-col>
</a-row>
</div>
</a-form>
</div>
</ModalPro>
</template>
<style lang="less" scoped>
.add-fee-modal {
display: flex;
flex-direction: column;
gap: 16px;
}
.fee-type-picker {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.fee-type-card {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
&:hover {
border-color: var(--app-primary-color, #2d8c8c);
}
&.active {
border-color: var(--app-primary-color, #2d8c8c);
box-shadow: 0 0 0 1px var(--app-primary-color, #2d8c8c);
background: var(--color-fill-2);
}
&.receivable .fee-type-dot {
background: #00b42a;
}
&.payable .fee-type-dot {
background: #f53f3f;
}
}
.fee-type-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.fee-type-label {
font-size: 14px;
font-weight: 600;
color: var(--color-text-1);
}
.add-fee-form {
:deep(.arco-form-item) {
margin-bottom: 0;
}
:deep(.arco-form-item-label) {
font-size: 13px;
color: var(--color-text-3);
}
}
.add-fee-section {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
&.optional {
background: var(--color-fill-2);
}
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
min-height: 22px;
}
.section-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--app-primary-color, #2d8c8c);
flex-shrink: 0;
&.optional {
background: #86909c;
}
}
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
}
.section-hint {
margin-left: auto;
font-size: 12px;
color: var(--color-text-3);
}
</style>