188 lines
5.8 KiB
Vue
188 lines
5.8 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import localforage from 'localforage'
|
||
import { parseNumberOrNull } from '@/lib/number'
|
||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||
|
||
interface ZxFwRowLike {
|
||
id?: unknown
|
||
subtotal?: unknown
|
||
}
|
||
|
||
interface ZxFwStateLike {
|
||
detailRows?: ZxFwRowLike[]
|
||
}
|
||
|
||
interface RateMethodState {
|
||
rate: number | null
|
||
budgetFee?: number | null
|
||
remark: string
|
||
}
|
||
|
||
const props = defineProps<{
|
||
storageKey: string
|
||
contractId?: string
|
||
syncMainStorageKey?: string
|
||
syncRowId?: string
|
||
}>()
|
||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||
|
||
const base = ref<number | null>(null)
|
||
const rate = ref<number | null>(null)
|
||
const remark = ref('')
|
||
const rateInput = ref('')
|
||
|
||
const toFinite = (value: unknown): number | null => {
|
||
const numeric = Number(value)
|
||
return Number.isFinite(numeric) ? numeric : null
|
||
}
|
||
|
||
const round3 = (value: number) => Number(value.toFixed(3))
|
||
const budgetFee = computed<number | null>(() => {
|
||
if (base.value == null || rate.value == null) return null
|
||
return round3(base.value * rate.value)
|
||
})
|
||
|
||
const formatAmount = (value: number | null) =>
|
||
value == null ? '' : formatThousandsFlexible(value, 3)
|
||
|
||
const loadBase = async () => {
|
||
|
||
const contractId = String(props.contractId || '').trim()
|
||
if (!contractId) {
|
||
base.value = null
|
||
return
|
||
}
|
||
try {
|
||
const data = await localforage.getItem<ZxFwStateLike>(`zxFW-${contractId}`)
|
||
const rows = Array.isArray(data?.detailRows) ? data.detailRows : []
|
||
const fixedRow = rows.find(row => String(row?.id || '') === 'fixed-budget-c')
|
||
const fixedSubtotal = toFinite(fixedRow?.subtotal)
|
||
if (fixedSubtotal != null) {
|
||
base.value = round3(fixedSubtotal)
|
||
return
|
||
}
|
||
const sum = rows.reduce((acc, row) => {
|
||
if (String(row?.id || '') === 'fixed-budget-c') return acc
|
||
const subtotal = toFinite(row?.subtotal)
|
||
return subtotal == null ? acc : acc + subtotal
|
||
}, 0)
|
||
base.value = round3(sum)
|
||
} catch (error) {
|
||
console.error('load rate base failed:', error)
|
||
base.value = null
|
||
}
|
||
}
|
||
|
||
const loadForm = async () => {
|
||
try {
|
||
const data = await localforage.getItem<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)
|
||
} catch (error) {
|
||
console.error('load rate form failed:', error)
|
||
rate.value = null
|
||
remark.value = ''
|
||
rateInput.value = ''
|
||
}
|
||
}
|
||
|
||
const saveForm = async () => {
|
||
try {
|
||
await localforage.setItem<RateMethodState>(props.storageKey, {
|
||
rate: rate.value,
|
||
budgetFee: budgetFee.value,
|
||
remark: remark.value
|
||
})
|
||
if (props.syncMainStorageKey && props.syncRowId) {
|
||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
||
}
|
||
} 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(
|
||
() => pricingPaneReloadStore.persistedSeq,
|
||
(nextVersion, prevVersion) => {
|
||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||
const contractId = String(props.contractId || '').trim()
|
||
if (!contractId) return
|
||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastPersistedEvent, contractId, ZXFW_RELOAD_SERVICE_KEY)) return
|
||
void loadBase()
|
||
}
|
||
)
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([loadBase(), loadForm()])
|
||
|
||
})
|
||
|
||
onActivated(async () => {
|
||
await Promise.all([loadBase(), loadForm()])
|
||
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
|
||
void saveForm()
|
||
})
|
||
</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">基数(所有服务费预算合计)</div>
|
||
<input type="text" :value="base" 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>
|