优化目录
This commit is contained in:
parent
693a9628bc
commit
cd9cffe588
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import HomeEntryView from '@/components/views/HomeEntryView.vue'
|
||||
import HomeEntryView from '@/features/workbench/components/HomeEntryView.vue'
|
||||
import Tab from '@/layout/tab.vue'
|
||||
import { waitForHydration } from '@/pinia/Plugin/indexdb'
|
||||
|
||||
|
||||
@ -12,6 +12,27 @@ import { useZxFwPricingHtFeeStore } from '@/pinia/zxFwPricingHtFee'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
||||
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
||||
import {
|
||||
formatDateTime,
|
||||
isEntryRelatedToAnyContract,
|
||||
normalizeContractsFromPayload,
|
||||
normalizeOrder,
|
||||
type ContractItem
|
||||
} from '@/features/ht/contracts'
|
||||
import {
|
||||
applyImportedContractPiniaPayload,
|
||||
buildContractPiniaPayload,
|
||||
readContractRelatedForageEntries,
|
||||
readContractRelatedKeyedEntries
|
||||
} from '@/features/ht/importExport'
|
||||
import type {
|
||||
HourlyMethodStateLike,
|
||||
HtFeeMainRowLike,
|
||||
QuantityMethodStateLike,
|
||||
RateMethodStateLike,
|
||||
XmBaseInfoState,
|
||||
XmScaleState
|
||||
} from '@/features/ht/types'
|
||||
import {
|
||||
cloneJson,
|
||||
CONTRACT_CONSULT_FACTOR_KEY_PREFIX,
|
||||
@ -24,16 +45,13 @@ import {
|
||||
isContractRelatedForageKey,
|
||||
isContractRelatedKeyedStateKey,
|
||||
isContractSegmentPackage,
|
||||
isRecord,
|
||||
normalizeContractSegmentPackage,
|
||||
PROJECT_INFO_KEY,
|
||||
PROJECT_SCALE_KEY,
|
||||
PRICING_KEY_PREFIXES,
|
||||
rewriteKeyWithContractId,
|
||||
SERVICE_KEY_PREFIX,
|
||||
SERVICE_PRICING_METHODS,
|
||||
type ContractSegmentPackage,
|
||||
type DataEntry
|
||||
type ContractSegmentPackage
|
||||
} from '@/lib/contractSegment'
|
||||
import { industryTypeList } from '@/sql'
|
||||
import { roundTo } from '@/lib/decimal'
|
||||
@ -55,53 +73,6 @@ import {
|
||||
ToastViewport
|
||||
} from 'reka-ui'
|
||||
|
||||
interface ContractItem {
|
||||
id: string
|
||||
name: string
|
||||
order: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
interface XmScaleState {
|
||||
detailRows?: unknown[]
|
||||
roughCalcEnabled?: boolean
|
||||
totalAmount?: number | null
|
||||
}
|
||||
|
||||
interface HtFeeMainRowLike {
|
||||
id?: unknown
|
||||
}
|
||||
|
||||
interface RateMethodStateLike {
|
||||
budgetFee?: unknown
|
||||
}
|
||||
|
||||
interface HourlyMethodRowLike {
|
||||
serviceBudget?: unknown
|
||||
adoptedBudgetUnitPrice?: unknown
|
||||
personnelCount?: unknown
|
||||
workdayCount?: unknown
|
||||
}
|
||||
|
||||
interface HourlyMethodStateLike {
|
||||
detailRows?: HourlyMethodRowLike[]
|
||||
}
|
||||
|
||||
interface QuantityMethodRowLike {
|
||||
id?: unknown
|
||||
budgetFee?: unknown
|
||||
quantity?: unknown
|
||||
unitPrice?: unknown
|
||||
}
|
||||
|
||||
interface QuantityMethodStateLike {
|
||||
detailRows?: QuantityMethodRowLike[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'ht-card-v1'
|
||||
const tabStore = useTabStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
@ -206,21 +177,6 @@ const budgetRefreshSignature = computed(() => {
|
||||
.join('|')
|
||||
})
|
||||
|
||||
const normalizeOrder = (list: ContractItem[]): ContractItem[] =>
|
||||
list.map((item, index) => ({
|
||||
...item,
|
||||
order: index,
|
||||
createdAt: item.createdAt || new Date().toISOString()
|
||||
}))
|
||||
|
||||
const formatDateTime = (value: string) => {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
const notify = (text: string) => {
|
||||
toastTitle.value = '操作成功'
|
||||
toastText.value = text
|
||||
@ -570,157 +526,6 @@ const initializeContractScaleData = async (contractId: string) => {
|
||||
await kvStore.setItem(`${CONTRACT_KEY_PREFIX}${contractId}`, payload)
|
||||
}
|
||||
|
||||
const normalizeContractsFromPayload = (value: unknown): ContractItem[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.map((item, index) => {
|
||||
const row = item as Partial<ContractItem>
|
||||
const name = typeof row.name === 'string' ? row.name.trim() : ''
|
||||
const createdAt = typeof row.createdAt === 'string' ? row.createdAt : new Date().toISOString()
|
||||
const id = typeof row.id === 'string' ? row.id : `import-contract-${index}`
|
||||
return {
|
||||
id,
|
||||
name: name || `导入合同段-${index + 1}`,
|
||||
order: index,
|
||||
createdAt
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const buildContractPiniaPayload = async (contractIds: string[]) => {
|
||||
const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean))
|
||||
const payload = {
|
||||
contracts: {} as Record<string, unknown>,
|
||||
servicePricingStates: {} as Record<string, unknown>,
|
||||
htFeeMainStates: {} as Record<string, unknown>,
|
||||
htFeeMethodStates: {} as Record<string, unknown>
|
||||
}
|
||||
if (idSet.size === 0) return payload
|
||||
|
||||
await Promise.all(Array.from(idSet).map(id => zxFwPricingStore.loadContract(id)))
|
||||
|
||||
for (const contractId of idSet) {
|
||||
const contractState = zxFwPricingStore.getContractState(contractId)
|
||||
if (contractState) {
|
||||
payload.contracts[contractId] = cloneJson(contractState)
|
||||
}
|
||||
|
||||
const servicePricingState = zxFwPricingStore.servicePricingStates[contractId]
|
||||
if (isRecord(servicePricingState)) {
|
||||
payload.servicePricingStates[contractId] = cloneJson(servicePricingState)
|
||||
}
|
||||
|
||||
const mainPrefix = `htExtraFee-${contractId}-`
|
||||
for (const [mainKey, mainState] of Object.entries(zxFwPricingStore.htFeeMainStates)) {
|
||||
if (!mainKey.startsWith(mainPrefix)) continue
|
||||
payload.htFeeMainStates[mainKey] = cloneJson(mainState)
|
||||
}
|
||||
|
||||
for (const [mainKey, methodState] of Object.entries(zxFwPricingStore.htFeeMethodStates)) {
|
||||
if (!mainKey.startsWith(mainPrefix)) continue
|
||||
payload.htFeeMethodStates[mainKey] = cloneJson(methodState)
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
const applyImportedContractPiniaPayload = async (
|
||||
piniaPayload: unknown,
|
||||
oldToNewIdMap: Map<string, string>
|
||||
) => {
|
||||
if (!isRecord(piniaPayload)) return
|
||||
const zxFwPayload = isRecord(piniaPayload.zxFwPricing) ? piniaPayload.zxFwPricing : null
|
||||
if (!zxFwPayload) return
|
||||
|
||||
const contractsMap = isRecord(zxFwPayload.contracts) ? zxFwPayload.contracts : {}
|
||||
const servicePricingStatesMap = isRecord(zxFwPayload.servicePricingStates) ? zxFwPayload.servicePricingStates : {}
|
||||
const htFeeMainStatesMap = isRecord(zxFwPayload.htFeeMainStates) ? zxFwPayload.htFeeMainStates : {}
|
||||
const htFeeMethodStatesMap = isRecord(zxFwPayload.htFeeMethodStates) ? zxFwPayload.htFeeMethodStates : {}
|
||||
|
||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||
const rawContractState = contractsMap[oldId]
|
||||
if (isRecord(rawContractState) && Array.isArray(rawContractState.detailRows)) {
|
||||
await zxFwPricingStore.setContractState(newId, rawContractState as any)
|
||||
}
|
||||
|
||||
const rawServicePricingByService = servicePricingStatesMap[oldId]
|
||||
if (isRecord(rawServicePricingByService)) {
|
||||
for (const [serviceId, rawServiceMethods] of Object.entries(rawServicePricingByService)) {
|
||||
if (!isRecord(rawServiceMethods)) continue
|
||||
for (const method of SERVICE_PRICING_METHODS) {
|
||||
const methodState = rawServiceMethods[method]
|
||||
if (!isRecord(methodState) || !Array.isArray(methodState.detailRows)) continue
|
||||
zxFwPricingStore.setServicePricingMethodState(newId, serviceId, method, methodState as any, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldMainPrefix = `htExtraFee-${oldId}-`
|
||||
const newMainPrefix = `htExtraFee-${newId}-`
|
||||
for (const [oldMainKey, rawMainState] of Object.entries(htFeeMainStatesMap)) {
|
||||
if (!oldMainKey.startsWith(oldMainPrefix)) continue
|
||||
if (!isRecord(rawMainState) || !Array.isArray(rawMainState.detailRows)) continue
|
||||
const newMainKey = oldMainKey.replace(oldMainPrefix, newMainPrefix)
|
||||
zxFwPricingStore.setHtFeeMainState(newMainKey, rawMainState as any, { force: true })
|
||||
}
|
||||
|
||||
for (const [oldMainKey, rawByRow] of Object.entries(htFeeMethodStatesMap)) {
|
||||
if (!oldMainKey.startsWith(oldMainPrefix)) continue
|
||||
if (!isRecord(rawByRow)) continue
|
||||
const newMainKey = oldMainKey.replace(oldMainPrefix, newMainPrefix)
|
||||
for (const [rowId, rawByMethod] of Object.entries(rawByRow)) {
|
||||
if (!isRecord(rawByMethod)) continue
|
||||
const ratePayload = rawByMethod['rate-fee']
|
||||
const hourlyPayload = rawByMethod['hourly-fee']
|
||||
const quantityPayload = rawByMethod['quantity-unit-price-fee']
|
||||
if (ratePayload != null) {
|
||||
zxFwPricingStore.setHtFeeMethodState(newMainKey, rowId, 'rate-fee', cloneJson(ratePayload), { force: true })
|
||||
}
|
||||
if (hourlyPayload != null) {
|
||||
zxFwPricingStore.setHtFeeMethodState(newMainKey, rowId, 'hourly-fee', cloneJson(hourlyPayload), { force: true })
|
||||
}
|
||||
if (quantityPayload != null) {
|
||||
zxFwPricingStore.setHtFeeMethodState(newMainKey, rowId, 'quantity-unit-price-fee', cloneJson(quantityPayload), { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const readContractRelatedForageEntries = async (contractIds: string[]) => {
|
||||
const keys = await kvStore.keys()
|
||||
const idSet = new Set(contractIds)
|
||||
const targetKeys = keys.filter(key => {
|
||||
for (const id of idSet) {
|
||||
if (isContractRelatedForageKey(key, id)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return Promise.all(
|
||||
targetKeys.map(async key => ({
|
||||
key,
|
||||
value: await kvStore.getItem(key)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
const readContractRelatedKeyedEntries = (contractIds: string[]) => {
|
||||
const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean))
|
||||
return Object.entries(zxFwPricingStore.keyedStates)
|
||||
.filter(([key]) => {
|
||||
for (const id of idSet) {
|
||||
if (isContractRelatedKeyedStateKey(key, id)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: cloneJson(value)
|
||||
}))
|
||||
}
|
||||
|
||||
const exportSelectedContracts = async () => {
|
||||
if (selectedContractIds.value.length === 0) {
|
||||
window.alert('请先勾选至少一个合同段。')
|
||||
@ -737,12 +542,14 @@ const exportSelectedContracts = async () => {
|
||||
}))
|
||||
|
||||
const localforageEntries = await readContractRelatedForageEntries(
|
||||
kvStore,
|
||||
selectedContracts.map(item => item.id)
|
||||
)
|
||||
const keyedEntries = readContractRelatedKeyedEntries(
|
||||
zxFwPricingStore,
|
||||
selectedContracts.map(item => item.id)
|
||||
)
|
||||
const piniaPayload = await buildContractPiniaPayload(selectedContracts.map(item => item.id))
|
||||
const piniaPayload = await buildContractPiniaPayload(zxFwPricingStore, selectedContracts.map(item => item.id))
|
||||
|
||||
const projectIndustry = await getCurrentProjectIndustry()
|
||||
if (!projectIndustry) {
|
||||
@ -825,6 +632,13 @@ const importContractSegments = async (event: Event) => {
|
||||
|
||||
const importedEntries = normalizedPackage.localforageEntries
|
||||
const importedKeyedEntries = normalizedPackage.keyedEntries
|
||||
const importedContractIdSet = new Set(importedContracts.map(item => String(item.id || '').trim()).filter(Boolean))
|
||||
const filteredImportedEntries = importedEntries.filter(entry =>
|
||||
isEntryRelatedToAnyContract(entry.key, importedContractIdSet, isContractRelatedForageKey)
|
||||
)
|
||||
const filteredImportedKeyedEntries = importedKeyedEntries.filter(entry =>
|
||||
isEntryRelatedToAnyContract(entry.key, importedContractIdSet, isContractRelatedKeyedStateKey)
|
||||
)
|
||||
const usedIds = new Set(contracts.value.map(item => item.id))
|
||||
const oldToNewIdMap = new Map<string, string>()
|
||||
const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
|
||||
@ -838,7 +652,7 @@ const importContractSegments = async (event: Event) => {
|
||||
}
|
||||
})
|
||||
|
||||
const rewrittenEntries = importedEntries.map(entry => {
|
||||
const rewrittenEntries = filteredImportedEntries.map(entry => {
|
||||
let nextKey = entry.key
|
||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||
if (!nextKey.includes(oldId)) continue
|
||||
@ -850,7 +664,7 @@ const importContractSegments = async (event: Event) => {
|
||||
}
|
||||
})
|
||||
|
||||
const rewrittenKeyedEntries = importedKeyedEntries.map(entry => {
|
||||
const rewrittenKeyedEntries = filteredImportedKeyedEntries.map(entry => {
|
||||
let nextKey = entry.key
|
||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||
if (!nextKey.includes(oldId)) continue
|
||||
@ -866,7 +680,7 @@ const importContractSegments = async (event: Event) => {
|
||||
for (const entry of rewrittenKeyedEntries) {
|
||||
zxFwPricingStore.setKeyState(entry.key, cloneJson(entry.value), { force: true })
|
||||
}
|
||||
await applyImportedContractPiniaPayload(normalizedPackage.piniaState, oldToNewIdMap)
|
||||
await applyImportedContractPiniaPayload(zxFwPricingStore, normalizedPackage.piniaState, oldToNewIdMap)
|
||||
|
||||
contracts.value = [...contracts.value, ...nextContracts]
|
||||
await saveContracts()
|
||||
@ -1738,214 +1552,5 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
</ToastProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ht-contract-scroll-area :deep([data-slot='scroll-area-viewport']) {
|
||||
overscroll-behavior: contain;
|
||||
scroll-snap-type: y mandatory;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area.is-dragging :deep([data-slot='scroll-area-viewport']) {
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area :deep(.ht-sortable-ghost) {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area :deep(.ht-sortable-chosen),
|
||||
.ht-contract-scroll-area :deep(.ht-sortable-drag) {
|
||||
will-change: transform, opacity;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.ht-contract-card {
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
isolation: isolate;
|
||||
overflow: visible;
|
||||
z-index: 0;
|
||||
transition:
|
||||
transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
box-shadow 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
border-color 180ms ease;
|
||||
box-shadow:
|
||||
0 1px 2px hsl(var(--foreground) / 0.04),
|
||||
0 6px 16px hsl(var(--foreground) / 0.06);
|
||||
}
|
||||
|
||||
.ht-contract-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
hsl(var(--primary) / 0.42) 0%,
|
||||
hsl(var(--primary) / 0.22) 36%,
|
||||
hsl(var(--foreground) / 0.09) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.ht-contract-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: -30px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse at center,
|
||||
hsl(var(--primary) / 0.42) 0%,
|
||||
hsl(var(--primary) / 0.24) 34%,
|
||||
hsl(var(--foreground) / 0.20) 58%,
|
||||
transparent 86%);
|
||||
filter: blur(18px);
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition:
|
||||
opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.ht-contract-card:hover {
|
||||
transform: translate3d(0, -5px, 0);
|
||||
z-index: 14;
|
||||
box-shadow:
|
||||
0 0 0 1.5px hsl(var(--primary) / 0.62),
|
||||
0 0 28px hsl(var(--primary) / 0.34),
|
||||
0 0 56px hsl(var(--primary) / 0.22),
|
||||
0 16px 34px hsl(var(--foreground) / 0.22),
|
||||
0 32px 60px hsl(var(--foreground) / 0.18);
|
||||
border-color: hsl(var(--primary) / 0.72);
|
||||
}
|
||||
|
||||
.ht-contract-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ht-contract-card:hover::after {
|
||||
opacity: 0.95;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ht-contract-card:active {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
box-shadow:
|
||||
0 5px 12px hsl(var(--foreground) / 0.10),
|
||||
0 10px 20px hsl(var(--foreground) / 0.10);
|
||||
}
|
||||
|
||||
.ht-contract-card--ready {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.ht-contract-card--enter {
|
||||
animation: ht-card-slide-in 560ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||
animation-delay: var(--ht-card-enter-delay, 0ms);
|
||||
}
|
||||
|
||||
.ht-contract-card--selecting {
|
||||
transform-origin: 50% 100%;
|
||||
animation: ht-card-select-wave 2200ms linear infinite both;
|
||||
animation-delay: var(--ht-card-select-delay, 0ms);
|
||||
}
|
||||
|
||||
.ht-contract-card--selecting:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.ht-contract-card--selecting.ht-contract-card--selected {
|
||||
animation: none;
|
||||
transform: translate3d(0, 0, 0) rotate(0deg);
|
||||
}
|
||||
|
||||
.ht-contract-card--selected {
|
||||
border-color: hsl(var(--primary));
|
||||
transform: translate3d(0, -4px, 0);
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--primary) / 0.34),
|
||||
0 12px 24px hsl(var(--primary) / 0.18),
|
||||
0 22px 36px hsl(var(--foreground) / 0.10);
|
||||
}
|
||||
|
||||
.ht-contract-card--selected::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ht-contract-card--selected::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes ht-card-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(44px, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ht-card-select-wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0) rotate(0deg);
|
||||
}
|
||||
11% {
|
||||
transform: translate3d(-0.4px, 0, 0) rotate(-0.7deg);
|
||||
}
|
||||
22% {
|
||||
transform: translate3d(-0.9px, 0, 0) rotate(-1.6deg);
|
||||
}
|
||||
34% {
|
||||
transform: translate3d(-1.2px, 0, 0) rotate(-2.3deg);
|
||||
}
|
||||
48% {
|
||||
transform: translate3d(-0.2px, 0, 0) rotate(-0.4deg);
|
||||
}
|
||||
62% {
|
||||
transform: translate3d(0.8px, 0, 0) rotate(1.5deg);
|
||||
}
|
||||
76% {
|
||||
transform: translate3d(1.25px, 0, 0) rotate(2.35deg);
|
||||
}
|
||||
88% {
|
||||
transform: translate3d(0.35px, 0, 0) rotate(0.65deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ht-contract-card--enter,
|
||||
.ht-contract-card--selecting {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ht-contract-card,
|
||||
.ht-contract-card:hover,
|
||||
.ht-contract-card:active,
|
||||
.ht-contract-card--selected {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ht-contract-card::before,
|
||||
.ht-contract-card::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped src="@/features/ht/ht.css"></style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
||||
import HtFeeMethodGrid from '@/features/shared/components/HtFeeMethodGrid.vue'
|
||||
import { additionalWorkList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import XmFactorGrid from '@/features/shared/components/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import XmFactorGrid from '@/features/shared/components/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
||||
import HtFeeMethodGrid from '@/features/shared/components/HtFeeMethodGrid.vue'
|
||||
import { reserveList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -200,7 +200,7 @@ const htView = markRaw(
|
||||
name: 'HtInfoWithProps',
|
||||
setup() {
|
||||
const AsyncHtInfo = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/htInfo.vue'),
|
||||
loader: () => import('@/features/ht/components/htInfo.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 htInfo 组件失败:', err);
|
||||
}
|
||||
@ -219,7 +219,7 @@ const zxfwView = markRaw(
|
||||
name: 'ZxFwWithProps',
|
||||
setup() {
|
||||
const AsyncZxFw = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/zxFw.vue'),
|
||||
loader: () => import('@/features/ht/components/zxFw.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 zxFw 组件失败:', err);
|
||||
}
|
||||
@ -238,7 +238,7 @@ const consultCategoryFactorView = markRaw(
|
||||
name: 'HtConsultCategoryFactorWithProps',
|
||||
setup() {
|
||||
const AsyncHtConsultCategoryFactor = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtConsultCategoryFactor.vue'),
|
||||
loader: () => import('@/features/ht/components/HtConsultCategoryFactor.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
||||
}
|
||||
@ -257,7 +257,7 @@ const majorFactorView = markRaw(
|
||||
name: 'HtMajorFactorWithProps',
|
||||
setup() {
|
||||
const AsyncHtMajorFactor = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtMajorFactor.vue'),
|
||||
loader: () => import('@/features/ht/components/HtMajorFactor.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtMajorFactor 组件失败:', err);
|
||||
}
|
||||
@ -278,7 +278,7 @@ const htBaseInfoView = markRaw(
|
||||
name: 'HtBaseInfoWithProps',
|
||||
setup() {
|
||||
const AsyncHtBaseInfo = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtBaseInfo.vue'),
|
||||
loader: () => import('@/features/ht/components/HtBaseInfo.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtBaseInfo 组件失败:', err)
|
||||
}
|
||||
@ -293,7 +293,7 @@ const additionalWorkFeeView = markRaw(
|
||||
name: 'HtAdditionalWorkFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtAdditionalWorkFee.vue'),
|
||||
loader: () => import('@/features/ht/components/HtAdditionalWorkFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
|
||||
}
|
||||
@ -308,7 +308,7 @@ const reserveFeeView = markRaw(
|
||||
name: 'HtReserveFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtReserveFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtReserveFee.vue'),
|
||||
loader: () => import('@/features/ht/components/HtReserveFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtReserveFee 组件失败:', err);
|
||||
}
|
||||
@ -323,7 +323,7 @@ const summaryView = markRaw(
|
||||
name: 'HtContractSummaryWithProps',
|
||||
setup() {
|
||||
const AsyncSummary = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtContractSummary.vue'),
|
||||
loader: () => import('@/features/ht/components/HtContractSummary.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtContractSummary 组件失败:', err)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
|
||||
import CommonAgGrid from '@/features/shared/components/xmCommonAgGrid.vue'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
@ -32,7 +32,7 @@ import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue }
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import ServiceCheckboxSelector from '@/components/shared/ServiceCheckboxSelector.vue'
|
||||
import ServiceCheckboxSelector from '@/features/shared/components/ServiceCheckboxSelector.vue'
|
||||
|
||||
interface ServiceItem {
|
||||
id: string
|
||||
50
src/features/ht/contracts.ts
Normal file
50
src/features/ht/contracts.ts
Normal file
@ -0,0 +1,50 @@
|
||||
export interface ContractItem {
|
||||
id: string
|
||||
name: string
|
||||
order: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export const normalizeOrder = (list: ContractItem[]): ContractItem[] =>
|
||||
list.map((item, index) => ({
|
||||
...item,
|
||||
order: index,
|
||||
createdAt: item.createdAt || new Date().toISOString()
|
||||
}))
|
||||
|
||||
export const formatDateTime = (value: string) => {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
export const normalizeContractsFromPayload = (value: unknown): ContractItem[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.map((item, index) => {
|
||||
const row = item as Partial<ContractItem>
|
||||
const name = typeof row.name === 'string' ? row.name.trim() : ''
|
||||
const createdAt = typeof row.createdAt === 'string' ? row.createdAt : new Date().toISOString()
|
||||
const id = typeof row.id === 'string' ? row.id : `import-contract-${index}`
|
||||
return {
|
||||
id,
|
||||
name: name || `导入合同段-${index + 1}`,
|
||||
order: index,
|
||||
createdAt
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const isEntryRelatedToAnyContract = (
|
||||
key: string,
|
||||
contractIds: Set<string>,
|
||||
matcher: (entryKey: string, contractId: string) => boolean
|
||||
) => {
|
||||
for (const contractId of contractIds) {
|
||||
if (matcher(key, contractId)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
210
src/features/ht/ht.css
Normal file
210
src/features/ht/ht.css
Normal file
@ -0,0 +1,210 @@
|
||||
.ht-contract-scroll-area :deep([data-slot='scroll-area-viewport']) {
|
||||
overscroll-behavior: contain;
|
||||
scroll-snap-type: y mandatory;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area.is-dragging :deep([data-slot='scroll-area-viewport']) {
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area :deep(.ht-sortable-ghost) {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area :deep(.ht-sortable-chosen),
|
||||
.ht-contract-scroll-area :deep(.ht-sortable-drag) {
|
||||
will-change: transform, opacity;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.ht-contract-card {
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
isolation: isolate;
|
||||
overflow: visible;
|
||||
z-index: 0;
|
||||
transition:
|
||||
transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
box-shadow 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
border-color 180ms ease;
|
||||
box-shadow:
|
||||
0 1px 2px hsl(var(--foreground) / 0.04),
|
||||
0 6px 16px hsl(var(--foreground) / 0.06);
|
||||
}
|
||||
|
||||
.ht-contract-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
hsl(var(--primary) / 0.42) 0%,
|
||||
hsl(var(--primary) / 0.22) 36%,
|
||||
hsl(var(--foreground) / 0.09) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.ht-contract-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: -30px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse at center,
|
||||
hsl(var(--primary) / 0.42) 0%,
|
||||
hsl(var(--primary) / 0.24) 34%,
|
||||
hsl(var(--foreground) / 0.20) 58%,
|
||||
transparent 86%
|
||||
);
|
||||
filter: blur(18px);
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition:
|
||||
opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.ht-contract-card:hover {
|
||||
transform: translate3d(0, -5px, 0);
|
||||
z-index: 14;
|
||||
box-shadow:
|
||||
0 0 0 1.5px hsl(var(--primary) / 0.62),
|
||||
0 0 28px hsl(var(--primary) / 0.34),
|
||||
0 0 56px hsl(var(--primary) / 0.22),
|
||||
0 16px 34px hsl(var(--foreground) / 0.22),
|
||||
0 32px 60px hsl(var(--foreground) / 0.18);
|
||||
border-color: hsl(var(--primary) / 0.72);
|
||||
}
|
||||
|
||||
.ht-contract-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ht-contract-card:hover::after {
|
||||
opacity: 0.95;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ht-contract-card:active {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
box-shadow:
|
||||
0 5px 12px hsl(var(--foreground) / 0.10),
|
||||
0 10px 20px hsl(var(--foreground) / 0.10);
|
||||
}
|
||||
|
||||
.ht-contract-card--ready {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.ht-contract-card--enter {
|
||||
animation: ht-card-slide-in 560ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||
animation-delay: var(--ht-card-enter-delay, 0ms);
|
||||
}
|
||||
|
||||
.ht-contract-card--selecting {
|
||||
transform-origin: 50% 100%;
|
||||
animation: ht-card-select-wave 2200ms linear infinite both;
|
||||
animation-delay: var(--ht-card-select-delay, 0ms);
|
||||
}
|
||||
|
||||
.ht-contract-card--selecting:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.ht-contract-card--selecting.ht-contract-card--selected {
|
||||
animation: none;
|
||||
transform: translate3d(0, 0, 0) rotate(0deg);
|
||||
}
|
||||
|
||||
.ht-contract-card--selected {
|
||||
border-color: hsl(var(--primary));
|
||||
transform: translate3d(0, -4px, 0);
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--primary) / 0.34),
|
||||
0 12px 24px hsl(var(--primary) / 0.18),
|
||||
0 22px 36px hsl(var(--foreground) / 0.10);
|
||||
}
|
||||
|
||||
.ht-contract-card--selected::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ht-contract-card--selected::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes ht-card-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(44px, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ht-card-select-wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0) rotate(0deg);
|
||||
}
|
||||
11% {
|
||||
transform: translate3d(-0.4px, 0, 0) rotate(-0.7deg);
|
||||
}
|
||||
22% {
|
||||
transform: translate3d(-0.9px, 0, 0) rotate(-1.6deg);
|
||||
}
|
||||
34% {
|
||||
transform: translate3d(-1.2px, 0, 0) rotate(-2.3deg);
|
||||
}
|
||||
48% {
|
||||
transform: translate3d(-0.2px, 0, 0) rotate(-0.4deg);
|
||||
}
|
||||
62% {
|
||||
transform: translate3d(0.8px, 0, 0) rotate(1.5deg);
|
||||
}
|
||||
76% {
|
||||
transform: translate3d(1.25px, 0, 0) rotate(2.35deg);
|
||||
}
|
||||
88% {
|
||||
transform: translate3d(0.35px, 0, 0) rotate(0.65deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ht-contract-card--enter,
|
||||
.ht-contract-card--selecting {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ht-contract-card,
|
||||
.ht-contract-card:hover,
|
||||
.ht-contract-card:active,
|
||||
.ht-contract-card--selected {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ht-contract-card::before,
|
||||
.ht-contract-card::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
172
src/features/ht/importExport.ts
Normal file
172
src/features/ht/importExport.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import {
|
||||
cloneJson,
|
||||
isContractRelatedForageKey,
|
||||
isContractRelatedKeyedStateKey,
|
||||
isRecord,
|
||||
SERVICE_PRICING_METHODS
|
||||
} from '@/lib/contractSegment'
|
||||
|
||||
type AnyRecord = Record<string, unknown>
|
||||
|
||||
export interface KvStoreLike {
|
||||
keys: () => Promise<string[]>
|
||||
getItem: (key: string) => Promise<unknown>
|
||||
}
|
||||
|
||||
export interface ZxFwPricingStoreLike {
|
||||
contracts: Record<string, unknown>
|
||||
servicePricingStates: Record<string, unknown>
|
||||
htFeeMainStates: Record<string, unknown>
|
||||
htFeeMethodStates: Record<string, unknown>
|
||||
keyedStates: Record<string, unknown>
|
||||
loadContract: (...args: any[]) => Promise<unknown>
|
||||
getContractState: (...args: any[]) => unknown
|
||||
setContractState: (...args: any[]) => Promise<unknown>
|
||||
setServicePricingMethodState: (...args: any[]) => unknown
|
||||
setHtFeeMainState: (...args: any[]) => unknown
|
||||
setHtFeeMethodState: (...args: any[]) => unknown
|
||||
setKeyState: (...args: any[]) => unknown
|
||||
}
|
||||
|
||||
export const buildContractPiniaPayload = async (
|
||||
store: ZxFwPricingStoreLike,
|
||||
contractIds: string[]
|
||||
) => {
|
||||
const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean))
|
||||
const payload = {
|
||||
contracts: {} as AnyRecord,
|
||||
servicePricingStates: {} as AnyRecord,
|
||||
htFeeMainStates: {} as AnyRecord,
|
||||
htFeeMethodStates: {} as AnyRecord
|
||||
}
|
||||
if (idSet.size === 0) return payload
|
||||
|
||||
await Promise.all(Array.from(idSet).map(id => store.loadContract(id)))
|
||||
|
||||
for (const contractId of idSet) {
|
||||
const contractState = store.getContractState(contractId)
|
||||
if (contractState) {
|
||||
payload.contracts[contractId] = cloneJson(contractState)
|
||||
}
|
||||
|
||||
const servicePricingState = store.servicePricingStates[contractId]
|
||||
if (isRecord(servicePricingState)) {
|
||||
payload.servicePricingStates[contractId] = cloneJson(servicePricingState)
|
||||
}
|
||||
|
||||
const mainPrefix = `htExtraFee-${contractId}-`
|
||||
for (const [mainKey, mainState] of Object.entries(store.htFeeMainStates)) {
|
||||
if (!mainKey.startsWith(mainPrefix)) continue
|
||||
payload.htFeeMainStates[mainKey] = cloneJson(mainState)
|
||||
}
|
||||
|
||||
for (const [mainKey, methodState] of Object.entries(store.htFeeMethodStates)) {
|
||||
if (!mainKey.startsWith(mainPrefix)) continue
|
||||
payload.htFeeMethodStates[mainKey] = cloneJson(methodState)
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export const applyImportedContractPiniaPayload = async (
|
||||
store: ZxFwPricingStoreLike,
|
||||
piniaPayload: unknown,
|
||||
oldToNewIdMap: Map<string, string>
|
||||
) => {
|
||||
if (!isRecord(piniaPayload)) return
|
||||
const zxFwPayload = isRecord(piniaPayload.zxFwPricing) ? piniaPayload.zxFwPricing : null
|
||||
if (!zxFwPayload) return
|
||||
|
||||
const contractsMap = isRecord(zxFwPayload.contracts) ? zxFwPayload.contracts : {}
|
||||
const servicePricingStatesMap = isRecord(zxFwPayload.servicePricingStates) ? zxFwPayload.servicePricingStates : {}
|
||||
const htFeeMainStatesMap = isRecord(zxFwPayload.htFeeMainStates) ? zxFwPayload.htFeeMainStates : {}
|
||||
const htFeeMethodStatesMap = isRecord(zxFwPayload.htFeeMethodStates) ? zxFwPayload.htFeeMethodStates : {}
|
||||
|
||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||
const rawContractState = contractsMap[oldId]
|
||||
if (isRecord(rawContractState) && Array.isArray(rawContractState.detailRows)) {
|
||||
await store.setContractState(newId, rawContractState as any)
|
||||
}
|
||||
|
||||
const rawServicePricingByService = servicePricingStatesMap[oldId]
|
||||
if (isRecord(rawServicePricingByService)) {
|
||||
for (const [serviceId, rawServiceMethods] of Object.entries(rawServicePricingByService)) {
|
||||
if (!isRecord(rawServiceMethods)) continue
|
||||
for (const method of SERVICE_PRICING_METHODS) {
|
||||
const methodState = rawServiceMethods[method]
|
||||
if (!isRecord(methodState) || !Array.isArray(methodState.detailRows)) continue
|
||||
store.setServicePricingMethodState(newId, serviceId, method, methodState as any, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldMainPrefix = `htExtraFee-${oldId}-`
|
||||
const newMainPrefix = `htExtraFee-${newId}-`
|
||||
for (const [oldMainKey, rawMainState] of Object.entries(htFeeMainStatesMap)) {
|
||||
if (!oldMainKey.startsWith(oldMainPrefix)) continue
|
||||
if (!isRecord(rawMainState) || !Array.isArray(rawMainState.detailRows)) continue
|
||||
const newMainKey = oldMainKey.replace(oldMainPrefix, newMainPrefix)
|
||||
store.setHtFeeMainState(newMainKey, rawMainState as any, { force: true })
|
||||
}
|
||||
|
||||
for (const [oldMainKey, rawByRow] of Object.entries(htFeeMethodStatesMap)) {
|
||||
if (!oldMainKey.startsWith(oldMainPrefix)) continue
|
||||
if (!isRecord(rawByRow)) continue
|
||||
const newMainKey = oldMainKey.replace(oldMainPrefix, newMainPrefix)
|
||||
for (const [rowId, rawByMethod] of Object.entries(rawByRow)) {
|
||||
if (!isRecord(rawByMethod)) continue
|
||||
const ratePayload = rawByMethod['rate-fee']
|
||||
const hourlyPayload = rawByMethod['hourly-fee']
|
||||
const quantityPayload = rawByMethod['quantity-unit-price-fee']
|
||||
if (ratePayload != null) {
|
||||
store.setHtFeeMethodState(newMainKey, rowId, 'rate-fee', cloneJson(ratePayload), { force: true })
|
||||
}
|
||||
if (hourlyPayload != null) {
|
||||
store.setHtFeeMethodState(newMainKey, rowId, 'hourly-fee', cloneJson(hourlyPayload), { force: true })
|
||||
}
|
||||
if (quantityPayload != null) {
|
||||
store.setHtFeeMethodState(newMainKey, rowId, 'quantity-unit-price-fee', cloneJson(quantityPayload), { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const readContractRelatedForageEntries = async (
|
||||
kvStore: KvStoreLike,
|
||||
contractIds: string[]
|
||||
) => {
|
||||
const keys = await kvStore.keys()
|
||||
const idSet = new Set(contractIds)
|
||||
const targetKeys = keys.filter(key => {
|
||||
for (const id of idSet) {
|
||||
if (isContractRelatedForageKey(key, id)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return Promise.all(
|
||||
targetKeys.map(async key => ({
|
||||
key,
|
||||
value: await kvStore.getItem(key)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
export const readContractRelatedKeyedEntries = (
|
||||
store: Pick<ZxFwPricingStoreLike, 'keyedStates'>,
|
||||
contractIds: string[]
|
||||
) => {
|
||||
const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean))
|
||||
return Object.entries(store.keyedStates)
|
||||
.filter(([key]) => {
|
||||
for (const id of idSet) {
|
||||
if (isContractRelatedKeyedStateKey(key, id)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: cloneJson(value)
|
||||
}))
|
||||
}
|
||||
39
src/features/ht/types.ts
Normal file
39
src/features/ht/types.ts
Normal file
@ -0,0 +1,39 @@
|
||||
export interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
export interface XmScaleState {
|
||||
detailRows?: unknown[]
|
||||
roughCalcEnabled?: boolean
|
||||
totalAmount?: number | null
|
||||
}
|
||||
|
||||
export interface HtFeeMainRowLike {
|
||||
id?: unknown
|
||||
}
|
||||
|
||||
export interface RateMethodStateLike {
|
||||
budgetFee?: unknown
|
||||
}
|
||||
|
||||
export interface HourlyMethodRowLike {
|
||||
serviceBudget?: unknown
|
||||
adoptedBudgetUnitPrice?: unknown
|
||||
personnelCount?: unknown
|
||||
workdayCount?: unknown
|
||||
}
|
||||
|
||||
export interface HourlyMethodStateLike {
|
||||
detailRows?: HourlyMethodRowLike[]
|
||||
}
|
||||
|
||||
export interface QuantityMethodRowLike {
|
||||
id?: unknown
|
||||
budgetFee?: unknown
|
||||
quantity?: unknown
|
||||
unitPrice?: unknown
|
||||
}
|
||||
|
||||
export interface QuantityMethodStateLike {
|
||||
detailRows?: QuantityMethodRowLike[]
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
|
||||
import HourlyFeeGrid from '@/features/shared/components/HourlyFeeGrid.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
@ -14,7 +14,7 @@ import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { usePricingPaneLifecycle } from '@/lib/pricingScalePaneLifecycle'
|
||||
import { sumNullableBy } from '@/lib/pricingScaleCalc'
|
||||
import { createPinnedTopRowData } from '@/lib/pricingPinnedRows'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
|
||||
129
src/features/tab/importExport.ts
Normal file
129
src/features/tab/importExport.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import type localforage from 'localforage'
|
||||
|
||||
export interface DataEntry {
|
||||
key: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export interface ForageStoreSnapshot {
|
||||
storeName: string
|
||||
entries: DataEntry[]
|
||||
}
|
||||
|
||||
export interface DataPackage {
|
||||
version: number
|
||||
packageType?: 'project-snapshot'
|
||||
exportedAt: string
|
||||
localStorage: DataEntry[]
|
||||
sessionStorage: DataEntry[]
|
||||
localforageDefault: DataEntry[]
|
||||
localforageStores?: ForageStoreSnapshot[]
|
||||
}
|
||||
|
||||
export type ForageInstance = ReturnType<typeof localforage.createInstance>
|
||||
export type ForageStore = Pick<ForageInstance, 'keys' | 'getItem' | 'setItem' | 'clear'>
|
||||
|
||||
type XmInfoLike = {
|
||||
projectName?: unknown
|
||||
}
|
||||
|
||||
export const readWebStorage = (storageObj: Storage): DataEntry[] => {
|
||||
const entries: DataEntry[] = []
|
||||
for (let i = 0; i < storageObj.length; i++) {
|
||||
const key = storageObj.key(i)
|
||||
if (!key) continue
|
||||
const raw = storageObj.getItem(key)
|
||||
let value: any = raw
|
||||
if (raw != null) {
|
||||
try {
|
||||
value = JSON.parse(raw)
|
||||
} catch {
|
||||
value = raw
|
||||
}
|
||||
}
|
||||
entries.push({ key, value })
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
export const writeWebStorage = (storageObj: Storage, entries: DataEntry[]) => {
|
||||
storageObj.clear()
|
||||
for (const entry of entries || []) {
|
||||
const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value)
|
||||
storageObj.setItem(entry.key, value)
|
||||
}
|
||||
}
|
||||
|
||||
export const toPersistableValue = (value: unknown) => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
} catch (error) {
|
||||
console.error('normalize persist value failed, fallback to null:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
|
||||
const keys = await store.keys()
|
||||
const values = await Promise.all(keys.map(key => store.getItem(key)))
|
||||
return keys.map((key, index) => ({
|
||||
key,
|
||||
value: toPersistableValue(values[index])
|
||||
}))
|
||||
}
|
||||
|
||||
export const writeForage = async (store: ForageStore, entries: DataEntry[]) => {
|
||||
await store.clear()
|
||||
await Promise.all((entries || []).map(entry => store.setItem(entry.key, toPersistableValue(entry.value))))
|
||||
}
|
||||
|
||||
export const normalizeEntries = (value: unknown): DataEntry[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter(item => item && typeof item === 'object' && typeof (item as any).key === 'string')
|
||||
.map(item => ({ key: String((item as any).key), value: (item as any).value }))
|
||||
}
|
||||
|
||||
export const normalizeForageStoreSnapshots = (value: unknown): ForageStoreSnapshot[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter(item =>
|
||||
item
|
||||
&& typeof item === 'object'
|
||||
&& typeof (item as any).storeName === 'string'
|
||||
&& Array.isArray((item as any).entries)
|
||||
)
|
||||
.map(item => ({
|
||||
storeName: String((item as any).storeName),
|
||||
entries: normalizeEntries((item as any).entries)
|
||||
}))
|
||||
}
|
||||
|
||||
export const sanitizeFileNamePart = (value: string): string => {
|
||||
const cleaned = value
|
||||
.replace(/[\\/:*?"<>|]/g, '_')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
return cleaned || '造价项目'
|
||||
}
|
||||
|
||||
export const getExportProjectName = (entries: DataEntry[], projectInfoDbKey: string, legacyProjectDbKey: string) => {
|
||||
const target =
|
||||
entries.find(item => item.key === projectInfoDbKey) ||
|
||||
entries.find(item => item.key === legacyProjectDbKey)
|
||||
const data = (target?.value || {}) as XmInfoLike
|
||||
return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
|
||||
}
|
||||
|
||||
export const isDataPackageLike = (value: unknown): value is DataPackage => {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const payload = value as Partial<DataPackage>
|
||||
const hasRequiredArrays =
|
||||
Array.isArray(payload.localStorage) &&
|
||||
Array.isArray(payload.sessionStorage) &&
|
||||
Array.isArray(payload.localforageDefault)
|
||||
if (!hasRequiredArrays) return false
|
||||
if (typeof payload.version !== 'number' || !Number.isFinite(payload.version)) return false
|
||||
if (payload.packageType != null && payload.packageType !== 'project-snapshot') return false
|
||||
return true
|
||||
}
|
||||
34
src/features/tab/tab.css
Normal file
34
src/features/tab/tab.css
Normal file
@ -0,0 +1,34 @@
|
||||
.tab-strip-sortable > .tab-item {
|
||||
transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.tab-strip-sortable.is-dragging > .tab-item {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.tab-drag-ghost {
|
||||
opacity: 0.32;
|
||||
}
|
||||
|
||||
.tab-drag-chosen {
|
||||
transform: scale(1.015);
|
||||
box-shadow: 0 10px 24px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
.tab-drag-active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tab-strip-scroll-area :deep([data-slot='scroll-area-viewport']) {
|
||||
scrollbar-width: none;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.tab-strip-scroll-area :deep([data-slot='scroll-area-viewport']::-webkit-scrollbar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-strip-scroll-area :deep([data-slot='scroll-area-scrollbar'][data-orientation='vertical']),
|
||||
.tab-strip-scroll-area :deep([data-slot='scroll-area-corner']) {
|
||||
display: none !important;
|
||||
}
|
||||
343
src/features/tab/types.ts
Normal file
343
src/features/tab/types.ts
Normal file
@ -0,0 +1,343 @@
|
||||
export interface UserGuideStep {
|
||||
title: string
|
||||
description: string
|
||||
points: string[]
|
||||
}
|
||||
|
||||
export type XmInfoLike = {
|
||||
projectName?: unknown
|
||||
preparedBy?: unknown
|
||||
reviewedBy?: unknown
|
||||
preparedDate?: unknown
|
||||
projectIndustry?: unknown
|
||||
preparedCompany?: unknown
|
||||
overview?: unknown
|
||||
desc?: unknown
|
||||
}
|
||||
|
||||
export type HtBaseInfoLike = {
|
||||
quality?: unknown
|
||||
duration?: unknown
|
||||
}
|
||||
|
||||
export interface ScaleRowLike {
|
||||
id: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
}
|
||||
|
||||
export interface XmInfoStorageLike extends XmInfoLike {
|
||||
detailRows?: ScaleRowLike[]
|
||||
totalAmount?: number
|
||||
roughCalcEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface XmScaleStorageLike {
|
||||
detailRows?: ScaleRowLike[]
|
||||
}
|
||||
|
||||
export interface ContractCardItem {
|
||||
id: string
|
||||
name?: string
|
||||
order?: number
|
||||
}
|
||||
|
||||
export interface ZxFwRowLike {
|
||||
id: string
|
||||
process?: unknown
|
||||
subtotal?: unknown
|
||||
finalFee?: unknown
|
||||
investScale?: unknown
|
||||
landScale?: unknown
|
||||
workload?: unknown
|
||||
hourly?: unknown
|
||||
}
|
||||
|
||||
export interface WorkContentRowLike {
|
||||
id?: unknown
|
||||
content?: unknown
|
||||
checked?: unknown
|
||||
custom?: unknown
|
||||
serviceGroup?: unknown
|
||||
serviceid?: unknown
|
||||
isAddTrigger?: unknown
|
||||
}
|
||||
|
||||
export interface WorkContentStateLike {
|
||||
detailRows?: WorkContentRowLike[]
|
||||
}
|
||||
|
||||
export interface ZxFwStorageLike {
|
||||
selectedIds?: string[]
|
||||
selectedCodes?: string[]
|
||||
detailRows?: ZxFwRowLike[]
|
||||
}
|
||||
|
||||
export interface ScaleMethodRowLike extends ScaleRowLike {
|
||||
benchmarkBudgetBasicChecked?: unknown
|
||||
benchmarkBudgetOptionalChecked?: unknown
|
||||
basicFormula?: unknown
|
||||
optionalFormula?: unknown
|
||||
budgetFee?: unknown
|
||||
budgetFeeBasic?: unknown
|
||||
budgetFeeOptional?: unknown
|
||||
consultCategoryFactor?: unknown
|
||||
majorFactor?: unknown
|
||||
workStageFactor?: unknown
|
||||
workRatio?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
export interface HtFeeMainRowLike {
|
||||
id?: unknown
|
||||
name?: unknown
|
||||
}
|
||||
|
||||
export interface RateMethodRowLike {
|
||||
rate?: unknown
|
||||
budgetFee?: unknown
|
||||
}
|
||||
|
||||
export interface QuantityMethodRowLike {
|
||||
id?: unknown
|
||||
feeItem?: unknown
|
||||
unit?: unknown
|
||||
quantity?: unknown
|
||||
unitPrice?: unknown
|
||||
budgetFee?: number | null
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
export interface WorkloadMethodRowLike {
|
||||
id: string
|
||||
budgetAdoptedUnitPrice?: unknown
|
||||
workload?: unknown
|
||||
basicFee?: unknown
|
||||
consultCategoryFactor?: unknown
|
||||
serviceFee?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
export interface HourlyMethodRowLike {
|
||||
id: string
|
||||
adoptedBudgetUnitPrice?: unknown
|
||||
personnelCount?: unknown
|
||||
workdayCount?: unknown
|
||||
serviceBudget?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
export interface DetailRowsStorageLike<T> {
|
||||
detailRows?: T[]
|
||||
roughCalcEnabled?: boolean
|
||||
totalAmount?: number
|
||||
}
|
||||
|
||||
export interface FactorRowLike {
|
||||
id: string
|
||||
standardFactor?: unknown
|
||||
budgetValue?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
export interface ExportScaleRow {
|
||||
majorid: number
|
||||
major: number
|
||||
cost: number | null
|
||||
area: number | null
|
||||
}
|
||||
|
||||
export interface ExportMethod1Detail {
|
||||
proNum: number
|
||||
major: number
|
||||
cost: number
|
||||
basicFee: number
|
||||
basicFormula: string
|
||||
basicFee_basic: number
|
||||
optionalFormula: string
|
||||
basicFee_optional: number
|
||||
serviceCoe: number
|
||||
majorCoe: number
|
||||
processCoe: number
|
||||
proportion: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportMethod1 {
|
||||
proAmount: number
|
||||
cost: number
|
||||
basicFee: number
|
||||
basicFee_basic: number
|
||||
basicFee_optional: number
|
||||
fee: number
|
||||
det: ExportMethod1Detail[]
|
||||
}
|
||||
|
||||
export interface ExportMethod2Detail {
|
||||
proNum: number
|
||||
major: number
|
||||
area: number
|
||||
basicFee: number
|
||||
basicFormula: string
|
||||
basicFee_basic: number
|
||||
optionalFormula: string
|
||||
basicFee_optional: number
|
||||
serviceCoe: number
|
||||
majorCoe: number
|
||||
processCoe: number
|
||||
proportion: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportMethod2 {
|
||||
proAmount: number
|
||||
area: number
|
||||
basicFee: number
|
||||
basicFee_basic: number
|
||||
basicFee_optional: number
|
||||
fee: number
|
||||
det: ExportMethod2Detail[]
|
||||
}
|
||||
|
||||
export interface ExportMethod3Detail {
|
||||
task: number
|
||||
price: number
|
||||
amount: number
|
||||
basicFee: number
|
||||
serviceCoe: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportMethod3 {
|
||||
basicFee: number
|
||||
fee: number
|
||||
det: ExportMethod3Detail[]
|
||||
}
|
||||
|
||||
export interface ExportMethod4Detail {
|
||||
expert: number
|
||||
price: number
|
||||
person_num: number
|
||||
work_day: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportMethod4 {
|
||||
person_num: number
|
||||
work_day: number
|
||||
fee: number
|
||||
det: ExportMethod4Detail[]
|
||||
}
|
||||
|
||||
export interface ExportService {
|
||||
id: number
|
||||
fee: number
|
||||
finalFee: number
|
||||
process: number
|
||||
tasks: ExportTaskGroup[]
|
||||
method1?: ExportMethod1
|
||||
method2?: ExportMethod2
|
||||
method3?: ExportMethod3
|
||||
method4?: ExportMethod4
|
||||
}
|
||||
|
||||
export interface ExportTaskGroup {
|
||||
serviceid?: number
|
||||
text: string[]
|
||||
}
|
||||
|
||||
export interface ExportServiceCoe {
|
||||
serviceid: number
|
||||
coe: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportMajorCoe {
|
||||
majorid: number
|
||||
coe: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportContract {
|
||||
name: string
|
||||
serviceFee: number
|
||||
addtionalFee: number
|
||||
reserveFee: number
|
||||
fee: number
|
||||
quality: string
|
||||
duration: string
|
||||
scale: ExportScaleRow[]
|
||||
serviceCoes: ExportServiceCoe[]
|
||||
majorCoes: ExportMajorCoe[]
|
||||
services: ExportService[]
|
||||
addtional: ExportAdditional | null
|
||||
reserve: ExportReserve | null
|
||||
}
|
||||
|
||||
export interface ExportMethod0 {
|
||||
coe: number
|
||||
fee: number
|
||||
}
|
||||
|
||||
export interface ExportMethod5Detail {
|
||||
name: string
|
||||
unit: string
|
||||
amount: number
|
||||
price: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ExportMethod5 {
|
||||
fee: number
|
||||
det: ExportMethod5Detail[]
|
||||
}
|
||||
|
||||
export interface ExportAdditionalDetail {
|
||||
id: number | string
|
||||
code?: unknown
|
||||
name: string
|
||||
fee: number
|
||||
tasks: ExportTaskGroup[]
|
||||
m0?: ExportMethod0
|
||||
m4?: ExportMethod4
|
||||
m5?: ExportMethod5
|
||||
}
|
||||
|
||||
export interface ExportAdditional {
|
||||
code?: unknown
|
||||
name: string
|
||||
fee: number
|
||||
det: ExportAdditionalDetail[]
|
||||
}
|
||||
|
||||
export interface ExportReserve {
|
||||
code?: unknown
|
||||
name: string
|
||||
fee: number
|
||||
tasks: ExportTaskGroup[]
|
||||
m0?: ExportMethod0
|
||||
m4?: ExportMethod4
|
||||
m5?: ExportMethod5
|
||||
}
|
||||
|
||||
export interface ExportReportPayload {
|
||||
name: string
|
||||
writer: string
|
||||
reviewer: string
|
||||
company: string
|
||||
date: string
|
||||
industry: number
|
||||
fee: number
|
||||
scaleCost: number
|
||||
overview: string
|
||||
desc: string
|
||||
scale: ExportScaleRow[]
|
||||
serviceCoes: ExportServiceCoe[]
|
||||
majorCoes: ExportMajorCoe[]
|
||||
contracts: ExportContract[]
|
||||
}
|
||||
@ -13,9 +13,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, markRaw, defineAsyncComponent, type Component } from 'vue'
|
||||
import TypeLine from '@/layout/typeLine.vue'
|
||||
import HtFeeGrid from '@/components/shared/HtFeeGrid.vue'
|
||||
import HtFeeRateMethodForm from '@/components/ht/HtFeeRateMethodForm.vue'
|
||||
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
|
||||
import HtFeeGrid from '@/features/shared/components/HtFeeGrid.vue'
|
||||
import HtFeeRateMethodForm from '@/features/ht/components/HtFeeRateMethodForm.vue'
|
||||
import HourlyFeeGrid from '@/features/shared/components/HourlyFeeGrid.vue'
|
||||
|
||||
interface TypeLineCategoryItem {
|
||||
key: string
|
||||
@ -103,7 +103,7 @@ const workContentPane = markRaw(
|
||||
name: 'WorkContentPane',
|
||||
setup() {
|
||||
const AsyncWorkContentGrid = defineAsyncComponent({
|
||||
loader: () => import('@/components/shared/WorkContentGrid.vue'),
|
||||
loader: () => import('@/features/shared/components/WorkContentGrid.vue'),
|
||||
onError: err => {
|
||||
console.error('加载 WorkContentGrid 组件失败:', err)
|
||||
}
|
||||
@ -13,7 +13,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
||||
import TypeLine from '@/layout/typeLine.vue'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue'
|
||||
|
||||
interface ServiceMethodType {
|
||||
scale?: boolean | null
|
||||
@ -59,7 +59,7 @@ const createPricingPane = (name: string) =>
|
||||
name,
|
||||
setup() {
|
||||
const AsyncPricingView = defineAsyncComponent({
|
||||
loader: () => import(`@/components/pricing/${name}.vue`),
|
||||
loader: () => import(`@/features/pricing/components/${name}.vue`),
|
||||
onError: err => {
|
||||
console.error('加载 PricingMethodView 组件失败:', err)
|
||||
}
|
||||
@ -94,7 +94,7 @@ const workContentPane = markRaw(
|
||||
name: 'WorkContentPane',
|
||||
setup() {
|
||||
const AsyncWorkContentGrid = defineAsyncComponent({
|
||||
loader: () => import('@/components/shared/WorkContentGrid.vue'),
|
||||
loader: () => import('@/features/shared/components/WorkContentGrid.vue'),
|
||||
onError: err => {
|
||||
console.error('加载 WorkContentGrid 组件失败:', err)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import XmFactorGrid from '@/features/shared/components/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
import XmFactorGrid from '@/features/shared/components/XmFactorGrid.vue'
|
||||
import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
@ -11,14 +11,14 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
import TypeLine from '@/layout/typeLine.vue'
|
||||
const infoView = markRaw(defineAsyncComponent(() => import('@/components/xm/info.vue')))
|
||||
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/xm/xmInfo.vue')))
|
||||
const htView = markRaw(defineAsyncComponent(() => import('@/components/ht/Ht.vue')))
|
||||
const infoView = markRaw(defineAsyncComponent(() => import('@/features/xm/components/info.vue')))
|
||||
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmInfo.vue')))
|
||||
const htView = markRaw(defineAsyncComponent(() => import('@/features/ht/components/Ht.vue')))
|
||||
const consultCategoryFactorView = markRaw(
|
||||
defineAsyncComponent(() => import('@/components/xm/XmConsultCategoryFactor.vue'))
|
||||
defineAsyncComponent(() => import('@/features/xm/components/XmConsultCategoryFactor.vue'))
|
||||
)
|
||||
const majorFactorView = markRaw(
|
||||
defineAsyncComponent(() => import('@/components/xm/XmMajorFactor.vue'))
|
||||
defineAsyncComponent(() => import('@/features/xm/components/XmMajorFactor.vue'))
|
||||
)
|
||||
|
||||
const xmCategories = [
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
|
||||
import CommonAgGrid from '@/features/shared/components/xmCommonAgGrid.vue'
|
||||
const DB_KEY = 'xm-info-v3'
|
||||
|
||||
|
||||
@ -30,6 +30,61 @@ import {
|
||||
} from 'reka-ui'
|
||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||
import { formatExportTimestamp } from '@/lib/contractSegment'
|
||||
import {
|
||||
getExportProjectName,
|
||||
isDataPackageLike,
|
||||
normalizeEntries,
|
||||
normalizeForageStoreSnapshots,
|
||||
readForage,
|
||||
readWebStorage,
|
||||
sanitizeFileNamePart,
|
||||
writeForage,
|
||||
writeWebStorage,
|
||||
type DataPackage,
|
||||
type ForageInstance,
|
||||
type ForageStore
|
||||
} from '@/features/tab/importExport'
|
||||
import type {
|
||||
ContractCardItem,
|
||||
DetailRowsStorageLike,
|
||||
ExportAdditional,
|
||||
ExportAdditionalDetail,
|
||||
ExportContract,
|
||||
ExportMajorCoe,
|
||||
ExportMethod0,
|
||||
ExportMethod1,
|
||||
ExportMethod1Detail,
|
||||
ExportMethod2,
|
||||
ExportMethod2Detail,
|
||||
ExportMethod3,
|
||||
ExportMethod3Detail,
|
||||
ExportMethod4,
|
||||
ExportMethod4Detail,
|
||||
ExportMethod5,
|
||||
ExportMethod5Detail,
|
||||
ExportReportPayload,
|
||||
ExportReserve,
|
||||
ExportScaleRow,
|
||||
ExportService,
|
||||
ExportServiceCoe,
|
||||
ExportTaskGroup,
|
||||
FactorRowLike,
|
||||
HourlyMethodRowLike,
|
||||
HtBaseInfoLike,
|
||||
HtFeeMainRowLike,
|
||||
QuantityMethodRowLike,
|
||||
RateMethodRowLike,
|
||||
ScaleMethodRowLike,
|
||||
ScaleRowLike,
|
||||
UserGuideStep,
|
||||
WorkContentStateLike,
|
||||
WorkloadMethodRowLike,
|
||||
XmInfoLike,
|
||||
XmInfoStorageLike,
|
||||
XmScaleStorageLike,
|
||||
ZxFwRowLike,
|
||||
ZxFwStorageLike
|
||||
} from '@/features/tab/types'
|
||||
import {
|
||||
PROJECT_TAB_ID,
|
||||
QUICK_TAB_ID,
|
||||
@ -65,372 +120,6 @@ import {
|
||||
} from '@/lib/reportExportBuilders'
|
||||
import { exportFile } from '@/sql'
|
||||
|
||||
interface DataEntry {
|
||||
key: string
|
||||
value: any
|
||||
}
|
||||
|
||||
interface ForageStoreSnapshot {
|
||||
storeName: string
|
||||
entries: DataEntry[]
|
||||
}
|
||||
|
||||
interface DataPackage {
|
||||
version: number
|
||||
exportedAt: string
|
||||
localStorage: DataEntry[]
|
||||
sessionStorage: DataEntry[]
|
||||
localforageDefault: DataEntry[]
|
||||
localforageStores?: ForageStoreSnapshot[]
|
||||
}
|
||||
|
||||
interface UserGuideStep {
|
||||
title: string
|
||||
description: string
|
||||
points: string[]
|
||||
}
|
||||
|
||||
type XmInfoLike = {
|
||||
projectName?: unknown
|
||||
preparedBy?: unknown
|
||||
reviewedBy?: unknown
|
||||
preparedDate?: unknown
|
||||
projectIndustry?: unknown
|
||||
preparedCompany?: unknown
|
||||
overview?: unknown
|
||||
desc?: unknown
|
||||
}
|
||||
|
||||
type HtBaseInfoLike = {
|
||||
quality?: unknown
|
||||
duration?: unknown
|
||||
}
|
||||
|
||||
interface ScaleRowLike {
|
||||
id: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
}
|
||||
|
||||
interface XmInfoStorageLike extends XmInfoLike {
|
||||
detailRows?: ScaleRowLike[],
|
||||
totalAmount?: number,
|
||||
roughCalcEnabled?: boolean
|
||||
}
|
||||
interface XmScaleStorageLike {
|
||||
detailRows?: ScaleRowLike[]
|
||||
}
|
||||
|
||||
interface ContractCardItem {
|
||||
id: string
|
||||
name?: string
|
||||
order?: number
|
||||
}
|
||||
|
||||
interface ZxFwRowLike {
|
||||
id: string
|
||||
process?: unknown
|
||||
subtotal?: unknown
|
||||
finalFee?: unknown
|
||||
investScale?: unknown
|
||||
landScale?: unknown
|
||||
workload?: unknown
|
||||
hourly?: unknown
|
||||
}
|
||||
|
||||
interface WorkContentRowLike {
|
||||
id?: unknown
|
||||
content?: unknown
|
||||
checked?: unknown
|
||||
custom?: unknown
|
||||
serviceGroup?: unknown
|
||||
serviceid?: unknown
|
||||
isAddTrigger?: unknown
|
||||
}
|
||||
|
||||
interface WorkContentStateLike {
|
||||
detailRows?: WorkContentRowLike[]
|
||||
}
|
||||
|
||||
interface ZxFwStorageLike {
|
||||
selectedIds?: string[]
|
||||
selectedCodes?: string[]
|
||||
detailRows?: ZxFwRowLike[]
|
||||
}
|
||||
|
||||
interface ScaleMethodRowLike extends ScaleRowLike {
|
||||
benchmarkBudgetBasicChecked?: unknown
|
||||
benchmarkBudgetOptionalChecked?: unknown
|
||||
basicFormula?: unknown
|
||||
optionalFormula?: unknown
|
||||
budgetFee?: unknown
|
||||
budgetFeeBasic?: unknown
|
||||
budgetFeeOptional?: unknown
|
||||
consultCategoryFactor?: unknown
|
||||
majorFactor?: unknown
|
||||
workStageFactor?: unknown
|
||||
workRatio?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface HtFeeMainRowLike {
|
||||
id?: unknown
|
||||
name?: unknown
|
||||
}
|
||||
|
||||
interface RateMethodRowLike {
|
||||
rate?: unknown
|
||||
budgetFee?: unknown
|
||||
}
|
||||
|
||||
interface QuantityMethodRowLike {
|
||||
id?: unknown
|
||||
feeItem?: unknown
|
||||
unit?: unknown
|
||||
quantity?: unknown
|
||||
unitPrice?: unknown
|
||||
budgetFee?: number|null
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface WorkloadMethodRowLike {
|
||||
id: string
|
||||
budgetAdoptedUnitPrice?: unknown
|
||||
workload?: unknown
|
||||
basicFee?: unknown
|
||||
consultCategoryFactor?: unknown
|
||||
serviceFee?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface HourlyMethodRowLike {
|
||||
id: string
|
||||
adoptedBudgetUnitPrice?: unknown
|
||||
personnelCount?: unknown
|
||||
workdayCount?: unknown
|
||||
serviceBudget?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface DetailRowsStorageLike<T> {
|
||||
detailRows?: T[],
|
||||
roughCalcEnabled?: boolean,
|
||||
totalAmount?: number,
|
||||
|
||||
|
||||
}
|
||||
|
||||
interface FactorRowLike {
|
||||
id: string
|
||||
standardFactor?: unknown
|
||||
budgetValue?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface ExportScaleRow {
|
||||
majorid: number
|
||||
major: number
|
||||
cost: number | null
|
||||
area: number | null
|
||||
}
|
||||
|
||||
interface ExportMethod1Detail {
|
||||
proNum: number
|
||||
major: number
|
||||
cost: number
|
||||
basicFee: number
|
||||
basicFormula: string
|
||||
basicFee_basic: number
|
||||
optionalFormula: string
|
||||
basicFee_optional: number
|
||||
serviceCoe: number
|
||||
majorCoe: number
|
||||
processCoe: number
|
||||
proportion: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportMethod1 {
|
||||
proAmount: number
|
||||
cost: number
|
||||
basicFee: number
|
||||
basicFee_basic: number
|
||||
basicFee_optional: number
|
||||
fee: number
|
||||
det: ExportMethod1Detail[]
|
||||
}
|
||||
|
||||
interface ExportMethod2Detail {
|
||||
proNum: number
|
||||
major: number
|
||||
area: number
|
||||
basicFee: number
|
||||
basicFormula: string
|
||||
basicFee_basic: number
|
||||
optionalFormula: string
|
||||
basicFee_optional: number
|
||||
serviceCoe: number
|
||||
majorCoe: number
|
||||
processCoe: number
|
||||
proportion: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportMethod2 {
|
||||
proAmount: number
|
||||
area: number
|
||||
basicFee: number
|
||||
basicFee_basic: number
|
||||
basicFee_optional: number
|
||||
fee: number
|
||||
det: ExportMethod2Detail[]
|
||||
}
|
||||
|
||||
interface ExportMethod3Detail {
|
||||
task: number
|
||||
price: number
|
||||
amount: number
|
||||
basicFee: number
|
||||
serviceCoe: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportMethod3 {
|
||||
basicFee: number
|
||||
fee: number
|
||||
det: ExportMethod3Detail[]
|
||||
}
|
||||
|
||||
interface ExportMethod4Detail {
|
||||
expert: number
|
||||
price: number
|
||||
person_num: number
|
||||
work_day: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportMethod4 {
|
||||
person_num: number
|
||||
work_day: number
|
||||
fee: number
|
||||
det: ExportMethod4Detail[]
|
||||
}
|
||||
|
||||
interface ExportService {
|
||||
id: number
|
||||
fee: number
|
||||
finalFee: number
|
||||
process: number
|
||||
tasks: ExportTaskGroup[]
|
||||
method1?: ExportMethod1
|
||||
method2?: ExportMethod2
|
||||
method3?: ExportMethod3
|
||||
method4?: ExportMethod4
|
||||
}
|
||||
|
||||
interface ExportTaskGroup {
|
||||
serviceid?: number
|
||||
text: string[]
|
||||
}
|
||||
|
||||
interface ExportServiceCoe {
|
||||
serviceid: number
|
||||
coe: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportMajorCoe {
|
||||
majorid: number
|
||||
coe: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportContract {
|
||||
name: string
|
||||
serviceFee: number
|
||||
addtionalFee: number
|
||||
reserveFee: number
|
||||
fee: number
|
||||
quality: string
|
||||
duration: string
|
||||
scale: ExportScaleRow[]
|
||||
serviceCoes: ExportServiceCoe[]
|
||||
majorCoes: ExportMajorCoe[]
|
||||
services: ExportService[]
|
||||
addtional: ExportAdditional | null
|
||||
reserve: ExportReserve | null
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface ExportMethod0 {
|
||||
coe: number
|
||||
fee: number
|
||||
}
|
||||
|
||||
interface ExportMethod5Detail {
|
||||
name: string
|
||||
unit: string
|
||||
amount: number
|
||||
price: number
|
||||
fee: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface ExportMethod5 {
|
||||
fee: number
|
||||
det: ExportMethod5Detail[]
|
||||
}
|
||||
|
||||
interface ExportAdditionalDetail {
|
||||
id: number | string
|
||||
code?: unknown
|
||||
name: string
|
||||
fee: number
|
||||
tasks: ExportTaskGroup[]
|
||||
m0?: ExportMethod0
|
||||
m4?: ExportMethod4
|
||||
m5?: ExportMethod5
|
||||
}
|
||||
|
||||
interface ExportAdditional {
|
||||
code?: unknown
|
||||
name: string
|
||||
fee: number
|
||||
det: ExportAdditionalDetail[]
|
||||
}
|
||||
|
||||
interface ExportReserve {
|
||||
code?: unknown
|
||||
name: string
|
||||
fee: number
|
||||
tasks: ExportTaskGroup[]
|
||||
m0?: ExportMethod0
|
||||
m4?: ExportMethod4
|
||||
m5?: ExportMethod5
|
||||
}
|
||||
|
||||
interface ExportReportPayload {
|
||||
name: string
|
||||
writer: string
|
||||
reviewer: string
|
||||
company: string
|
||||
date: string
|
||||
industry: number
|
||||
fee: number
|
||||
scaleCost: number
|
||||
overview: string
|
||||
desc: string
|
||||
scale: ExportScaleRow[]
|
||||
serviceCoes: ExportServiceCoe[]
|
||||
majorCoes: ExportMajorCoe[]
|
||||
contracts: ExportContract[]
|
||||
}
|
||||
|
||||
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
|
||||
const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
|
||||
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
|
||||
@ -516,11 +205,11 @@ const userGuideSteps: UserGuideStep[] = [
|
||||
]
|
||||
|
||||
const componentMap: Record<string, any> = {
|
||||
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.vue'))),
|
||||
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/components/ht/htCard.vue'))),
|
||||
QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/components/views/QuickCalcWorkbenchView.vue'))),
|
||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
||||
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmCard.vue'))),
|
||||
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/features/ht/components/htCard.vue'))),
|
||||
QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/QuickCalcWorkbenchView.vue'))),
|
||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/ZxFwView.vue'))),
|
||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/HtFeeMethodTypeLineView.vue'))),
|
||||
}
|
||||
|
||||
const tabStore = useTabStore()
|
||||
@ -936,36 +625,6 @@ const scheduleRestoreTabInnerScrollTop = (tabId?: string | null) => {
|
||||
})
|
||||
}
|
||||
|
||||
const readWebStorage = (storageObj: Storage): DataEntry[] => {
|
||||
const entries: DataEntry[] = []
|
||||
for (let i = 0; i < storageObj.length; i++) {
|
||||
const key = storageObj.key(i)
|
||||
if (!key) continue
|
||||
const raw = storageObj.getItem(key)
|
||||
let value: any = raw
|
||||
if (raw != null) {
|
||||
try {
|
||||
value = JSON.parse(raw)
|
||||
} catch {
|
||||
value = raw
|
||||
}
|
||||
}
|
||||
entries.push({ key, value })
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
const writeWebStorage = (storageObj: Storage, entries: DataEntry[]) => {
|
||||
storageObj.clear()
|
||||
for (const entry of entries || []) {
|
||||
const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value)
|
||||
storageObj.setItem(entry.key, value)
|
||||
}
|
||||
}
|
||||
|
||||
type ForageInstance = ReturnType<typeof localforage.createInstance>
|
||||
type ForageStore = Pick<ForageInstance, 'keys' | 'getItem' | 'setItem' | 'clear'>
|
||||
|
||||
const createForageStore = (storeName: string): ForageInstance =>
|
||||
localforage.createInstance({
|
||||
name: PINIA_PERSIST_DB_NAME,
|
||||
@ -983,70 +642,6 @@ const getPiniaPersistStores = () =>
|
||||
}
|
||||
})
|
||||
|
||||
const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
|
||||
const keys = await store.keys()
|
||||
const entries: DataEntry[] = []
|
||||
for (const key of keys) {
|
||||
const value = await store.getItem(key)
|
||||
entries.push({ key, value: toPersistableValue(value) })
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
const toPersistableValue = (value: unknown) => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
} catch (error) {
|
||||
console.error('normalize persist value failed, fallback to null:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const writeForage = async (store: ForageStore, entries: DataEntry[]) => {
|
||||
await store.clear()
|
||||
for (const entry of entries || []) {
|
||||
await store.setItem(entry.key, toPersistableValue(entry.value))
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeEntries = (value: unknown): DataEntry[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter(item => item && typeof item === 'object' && typeof (item as any).key === 'string')
|
||||
.map(item => ({ key: String((item as any).key), value: (item as any).value }))
|
||||
}
|
||||
|
||||
const normalizeForageStoreSnapshots = (value: unknown): ForageStoreSnapshot[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter(item =>
|
||||
item
|
||||
&& typeof item === 'object'
|
||||
&& typeof (item as any).storeName === 'string'
|
||||
&& Array.isArray((item as any).entries)
|
||||
)
|
||||
.map(item => ({
|
||||
storeName: String((item as any).storeName),
|
||||
entries: normalizeEntries((item as any).entries)
|
||||
}))
|
||||
}
|
||||
|
||||
const sanitizeFileNamePart = (value: string): string => {
|
||||
const cleaned = value
|
||||
.replace(/[\\/:*?"<>|]/g, '_')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
return cleaned || '造价项目'
|
||||
}
|
||||
|
||||
const getExportProjectName = (entries: DataEntry[]): string => {
|
||||
const target =
|
||||
entries.find(item => item.key === PROJECT_INFO_DB_KEY) ||
|
||||
entries.find(item => item.key === LEGACY_PROJECT_DB_KEY)
|
||||
const data = (target?.value || {}) as XmInfoLike
|
||||
return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
|
||||
}
|
||||
|
||||
const loadFactorRowsState = async (storageKey: string) => {
|
||||
const [piniaData, kvData] = await Promise.all([
|
||||
zxFwPricingStore.loadKeyState<DetailRowsStorageLike<FactorRowLike>>(storageKey),
|
||||
@ -1390,6 +985,7 @@ const exportData = async () => {
|
||||
)
|
||||
const payload: DataPackage = {
|
||||
version: 2,
|
||||
packageType: 'project-snapshot',
|
||||
exportedAt: now.toISOString(),
|
||||
localStorage: readWebStorage(localStorage),
|
||||
sessionStorage: readWebStorage(sessionStorage),
|
||||
@ -1404,7 +1000,7 @@ const exportData = async () => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
const projectName = getExportProjectName(payload.localforageDefault)
|
||||
const projectName = getExportProjectName(payload.localforageDefault, PROJECT_INFO_DB_KEY, LEGACY_PROJECT_DB_KEY)
|
||||
const timestamp = formatExportTimestamp(now)
|
||||
link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
|
||||
document.body.appendChild(link)
|
||||
@ -1447,6 +1043,9 @@ const prepareImportPayloadFromFile = async (file: File) => {
|
||||
}
|
||||
const buffer = await file.arrayBuffer()
|
||||
const payload = await decodeZwArchive<DataPackage>(buffer)
|
||||
if (!isDataPackageLike(payload)) {
|
||||
throw new Error('INVALID_DATA_PACKAGE')
|
||||
}
|
||||
pendingImportPayload.value = payload
|
||||
pendingImportFileName.value = file.name
|
||||
importConfirmOpen.value = true
|
||||
@ -1956,39 +1555,4 @@ watch(
|
||||
</ToastProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tab-strip-sortable>.tab-item {
|
||||
transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.tab-strip-sortable.is-dragging>.tab-item {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.tab-drag-ghost {
|
||||
opacity: 0.32;
|
||||
}
|
||||
|
||||
.tab-drag-chosen {
|
||||
transform: scale(1.015);
|
||||
box-shadow: 0 10px 24px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
.tab-drag-active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) {
|
||||
scrollbar-width: none;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]::-webkit-scrollbar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-strip-scroll-area :deep([data-slot="scroll-area-scrollbar"][data-orientation="vertical"]),
|
||||
.tab-strip-scroll-area :deep([data-slot="scroll-area-corner"]) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<style scoped src="@/features/tab/tab.css"></style>
|
||||
|
||||
@ -160,6 +160,9 @@ export const syncContractScaleToPricing = async (
|
||||
await store.loadContract(contractId)
|
||||
const currentState = store.getContractState(contractId)
|
||||
const selectedIds = Array.from(new Set((currentState?.selectedIds || []).map(id => String(id || '').trim()).filter(Boolean)))
|
||||
const changedRowIdSet = options?.changedRowIds?.length
|
||||
? normalizeChangedScaleRowIds(options.changedRowIds)
|
||||
: undefined
|
||||
if (selectedIds.length === 0) {
|
||||
return {
|
||||
updatedServiceCount: 0,
|
||||
@ -167,6 +170,13 @@ export const syncContractScaleToPricing = async (
|
||||
updatedRowCount: 0
|
||||
}
|
||||
}
|
||||
if (changedRowIdSet && changedRowIdSet.size === 0) {
|
||||
return {
|
||||
updatedServiceCount: 0,
|
||||
updatedMethodCount: 0,
|
||||
updatedRowCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
await ensurePricingMethodDetailRowsForServices({
|
||||
contractId,
|
||||
@ -179,9 +189,6 @@ export const syncContractScaleToPricing = async (
|
||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||
const onlyCostScaleFallbackAmount = calcOnlyCostScaleAmountFromRows(sourceRows)
|
||||
const changedRowIdSet = options?.changedRowIds?.length
|
||||
? normalizeChangedScaleRowIds(options.changedRowIds)
|
||||
: undefined
|
||||
const updatedServiceIdSet = new Set<string>()
|
||||
let updatedMethodCount = 0
|
||||
let updatedRowCount = 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user