JGJS2026/src/components/views/HtFeeRateMethodForm.vue
2026-03-10 17:16:45 +08:00

188 lines
5.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 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>