JGJS2026/src/components/views/HtFeeRateMethodForm.vue
2026-03-12 16:52:39 +08:00

233 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
interface RateMethodState {
rate: number | null
budgetFee?: number | null
remark: string
}
const props = defineProps<{
storageKey: string
contractId?: string
htMainStorageKey?: string
htRowId?: string
htMethodType?: 'rate-fee'
}>()
const zxFwPricingStore = useZxFwPricingStore()
const rate = ref<number | null>(null)
const remark = ref('')
const rateInput = ref('')
const baseValue = ref<number | null>(null)
const lastSavedSnapshot = ref('')
const useHtMethodState = computed(
() => Boolean(props.htMainStorageKey && props.htRowId && props.htMethodType)
)
const contractIdText = computed(() => {
const contractId = String(props.contractId || '').trim()
return contractId
})
const isReserveFee = computed(() => {
const mainKey = String(props.htMainStorageKey || '').trim()
if (mainKey) return mainKey.endsWith('-reserve')
return String(props.storageKey || '').includes('-reserve')
})
const contractVersion = computed(() => {
const contractId = contractIdText.value
if (!contractId) return 0
return zxFwPricingStore.contractVersions[contractId] || 0
})
const additionalWorkKeyVersion = computed(() => {
if (!isReserveFee.value) return 0
const contractId = contractIdText.value
if (!contractId) return 0
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
return zxFwPricingStore.getKeyVersion(additionalStorageKey)
})
const baseLabel = computed(() =>
isReserveFee.value ? '基数(咨询服务总计 + 附加工作费总计)' : '基数(所有服务费预算合计)'
)
const budgetFee = computed<number | null>(() => {
if (baseValue.value == null || rate.value == null) return null
return Number((baseValue.value * rate.value).toFixed(3))
})
const formatAmount = (value: number | null) =>
value == null ? '' : formatThousandsFlexible(value, 3)
const ensureContractLoaded = async () => {
const contractId = contractIdText.value
if (!contractId) return
try {
await zxFwPricingStore.loadContract(contractId)
const serviceBase = zxFwPricingStore.getBaseSubtotal(contractId)
if (!isReserveFee.value) {
baseValue.value = serviceBase == null ? null : Number(serviceBase.toFixed(3))
return
}
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
const additionalState = await zxFwPricingStore.loadHtFeeMainState<{
rateFee?: unknown
hourlyFee?: unknown
quantityUnitPriceFee?: unknown
}>(additionalStorageKey)
const additionalTotal = (additionalState?.detailRows || []).reduce((total, row) => {
const rateFee = Number(row?.rateFee)
const hourlyFee = Number(row?.hourlyFee)
const quantityFee = Number(row?.quantityUnitPriceFee)
const safeRateFee = Number.isFinite(rateFee) ? rateFee : 0
const safeHourlyFee = Number.isFinite(hourlyFee) ? hourlyFee : 0
const safeQuantityFee = Number.isFinite(quantityFee) ? quantityFee : 0
return total + safeRateFee + safeHourlyFee + safeQuantityFee
}, 0)
const serviceBaseSafe = typeof serviceBase === 'number' && Number.isFinite(serviceBase) ? serviceBase : 0
const hasAny = (serviceBase != null) || additionalTotal !== 0
baseValue.value = hasAny ? Number((serviceBaseSafe + additionalTotal).toFixed(3)) : null
} catch (error) {
console.error('load contract for rate base failed:', error)
baseValue.value = null
}
}
const loadForm = async () => {
try {
const data = useHtMethodState.value
? await zxFwPricingStore.loadHtFeeMethodState<RateMethodState>(
props.htMainStorageKey!,
props.htRowId!,
props.htMethodType!
)
: await zxFwPricingStore.loadKeyState<RateMethodState>(props.storageKey)
rate.value = typeof data?.rate === 'number' ? data.rate : null
remark.value = typeof data?.remark === 'string' ? data.remark : ''
rateInput.value = rate.value == null ? '' : String(rate.value)
const snapshot: RateMethodState = {
rate: rate.value,
budgetFee: budgetFee.value,
remark: remark.value
}
lastSavedSnapshot.value = JSON.stringify(snapshot)
} catch (error) {
console.error('load rate form failed:', error)
rate.value = null
remark.value = ''
rateInput.value = ''
lastSavedSnapshot.value = ''
}
}
const saveForm = async (force = false) => {
try {
const payload: RateMethodState = {
rate: rate.value,
budgetFee: budgetFee.value,
remark: remark.value
}
const snapshot = JSON.stringify(payload)
if (!force && snapshot === lastSavedSnapshot.value) return
if (useHtMethodState.value) {
zxFwPricingStore.setHtFeeMethodState(
props.htMainStorageKey!,
props.htRowId!,
props.htMethodType!,
payload,
{ force }
)
} else {
zxFwPricingStore.setKeyState(props.storageKey, payload, { force })
}
lastSavedSnapshot.value = snapshot
} catch (error) {
console.error('save rate form failed:', error)
}
}
const applyRateInput = () => {
const next = parseNumberOrNull(rateInput.value, { sanitize: true, precision: 3 })
rate.value = next
rateInput.value = next == null ? '' : String(next)
}
watch([rate, remark, budgetFee], () => {
void saveForm()
})
watch(
() => props.storageKey,
() => {
void loadForm()
}
)
watch(
() => contractIdText.value,
() => {
void ensureContractLoaded()
}
)
watch(
() => [props.storageKey, props.htMainStorageKey, props.htRowId, props.htMethodType],
() => {
void ensureContractLoaded()
}
)
watch([contractVersion, additionalWorkKeyVersion], ([nextContract, nextAdditional], [prevContract, prevAdditional]) => {
if (nextContract === prevContract && nextAdditional === prevAdditional) return
void ensureContractLoaded()
})
onMounted(async () => {
await Promise.all([ensureContractLoaded(), loadForm()])
})
onActivated(async () => {
await Promise.all([ensureContractLoaded(), loadForm()])
})
onBeforeUnmount(() => {
void saveForm(true)
})
</script>
<template>
<div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card p-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class="space-y-1.5">
<div class="text-xs text-muted-foreground">{{ baseLabel }}</div>
<input type="text" :value="baseValue" readonly disabled tabindex="-1"
class="h-9 w-full cursor-not-allowed rounded-md border bg-muted/40 px-3 text-sm text-foreground select-none" />
</label>
<label class="space-y-1.5">
<div class="text-xs text-muted-foreground">费率可编辑三位小数</div>
<input v-model="rateInput" type="text" inputmode="decimal" placeholder="请输入费率建议0.01 ~ 0.05"
class="h-9 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30"
@blur="applyRateInput" />
</label>
<label class="space-y-1.5">
<div class="text-xs text-muted-foreground">预算费用自动计算</div>
<input type="text" :value="formatAmount(budgetFee)" readonly disabled tabindex="-1"
class="h-9 w-full cursor-not-allowed rounded-md border bg-muted/40 px-3 text-sm text-foreground select-none" />
</label>
<label class="space-y-1.5 md:col-span-2">
<div class="text-xs text-muted-foreground">说明</div>
<textarea v-model="remark" rows="4" placeholder="请输入说明"
class="w-full rounded-md border bg-background px-3 py-2 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30" />
</label>
</div>
</div>
</div>
</template>