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