优化目录
This commit is contained in:
parent
693a9628bc
commit
cd9cffe588
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
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 Tab from '@/layout/tab.vue'
|
||||||
import { waitForHydration } from '@/pinia/Plugin/indexdb'
|
import { waitForHydration } from '@/pinia/Plugin/indexdb'
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,27 @@ import { useZxFwPricingHtFeeStore } from '@/pinia/zxFwPricingHtFee'
|
|||||||
import { useKvStore } from '@/pinia/kv'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
||||||
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
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 {
|
import {
|
||||||
cloneJson,
|
cloneJson,
|
||||||
CONTRACT_CONSULT_FACTOR_KEY_PREFIX,
|
CONTRACT_CONSULT_FACTOR_KEY_PREFIX,
|
||||||
@ -24,16 +45,13 @@ import {
|
|||||||
isContractRelatedForageKey,
|
isContractRelatedForageKey,
|
||||||
isContractRelatedKeyedStateKey,
|
isContractRelatedKeyedStateKey,
|
||||||
isContractSegmentPackage,
|
isContractSegmentPackage,
|
||||||
isRecord,
|
|
||||||
normalizeContractSegmentPackage,
|
normalizeContractSegmentPackage,
|
||||||
PROJECT_INFO_KEY,
|
PROJECT_INFO_KEY,
|
||||||
PROJECT_SCALE_KEY,
|
PROJECT_SCALE_KEY,
|
||||||
PRICING_KEY_PREFIXES,
|
PRICING_KEY_PREFIXES,
|
||||||
rewriteKeyWithContractId,
|
rewriteKeyWithContractId,
|
||||||
SERVICE_KEY_PREFIX,
|
SERVICE_KEY_PREFIX,
|
||||||
SERVICE_PRICING_METHODS,
|
type ContractSegmentPackage
|
||||||
type ContractSegmentPackage,
|
|
||||||
type DataEntry
|
|
||||||
} from '@/lib/contractSegment'
|
} from '@/lib/contractSegment'
|
||||||
import { industryTypeList } from '@/sql'
|
import { industryTypeList } from '@/sql'
|
||||||
import { roundTo } from '@/lib/decimal'
|
import { roundTo } from '@/lib/decimal'
|
||||||
@ -55,53 +73,6 @@ import {
|
|||||||
ToastViewport
|
ToastViewport
|
||||||
} from 'reka-ui'
|
} 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 STORAGE_KEY = 'ht-card-v1'
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
@ -206,21 +177,6 @@ const budgetRefreshSignature = computed(() => {
|
|||||||
.join('|')
|
.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) => {
|
const notify = (text: string) => {
|
||||||
toastTitle.value = '操作成功'
|
toastTitle.value = '操作成功'
|
||||||
toastText.value = text
|
toastText.value = text
|
||||||
@ -570,157 +526,6 @@ const initializeContractScaleData = async (contractId: string) => {
|
|||||||
await kvStore.setItem(`${CONTRACT_KEY_PREFIX}${contractId}`, payload)
|
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 () => {
|
const exportSelectedContracts = async () => {
|
||||||
if (selectedContractIds.value.length === 0) {
|
if (selectedContractIds.value.length === 0) {
|
||||||
window.alert('请先勾选至少一个合同段。')
|
window.alert('请先勾选至少一个合同段。')
|
||||||
@ -737,12 +542,14 @@ const exportSelectedContracts = async () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const localforageEntries = await readContractRelatedForageEntries(
|
const localforageEntries = await readContractRelatedForageEntries(
|
||||||
|
kvStore,
|
||||||
selectedContracts.map(item => item.id)
|
selectedContracts.map(item => item.id)
|
||||||
)
|
)
|
||||||
const keyedEntries = readContractRelatedKeyedEntries(
|
const keyedEntries = readContractRelatedKeyedEntries(
|
||||||
|
zxFwPricingStore,
|
||||||
selectedContracts.map(item => item.id)
|
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()
|
const projectIndustry = await getCurrentProjectIndustry()
|
||||||
if (!projectIndustry) {
|
if (!projectIndustry) {
|
||||||
@ -825,6 +632,13 @@ const importContractSegments = async (event: Event) => {
|
|||||||
|
|
||||||
const importedEntries = normalizedPackage.localforageEntries
|
const importedEntries = normalizedPackage.localforageEntries
|
||||||
const importedKeyedEntries = normalizedPackage.keyedEntries
|
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 usedIds = new Set(contracts.value.map(item => item.id))
|
||||||
const oldToNewIdMap = new Map<string, string>()
|
const oldToNewIdMap = new Map<string, string>()
|
||||||
const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
|
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
|
let nextKey = entry.key
|
||||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||||
if (!nextKey.includes(oldId)) continue
|
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
|
let nextKey = entry.key
|
||||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||||
if (!nextKey.includes(oldId)) continue
|
if (!nextKey.includes(oldId)) continue
|
||||||
@ -866,7 +680,7 @@ const importContractSegments = async (event: Event) => {
|
|||||||
for (const entry of rewrittenKeyedEntries) {
|
for (const entry of rewrittenKeyedEntries) {
|
||||||
zxFwPricingStore.setKeyState(entry.key, cloneJson(entry.value), { force: true })
|
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]
|
contracts.value = [...contracts.value, ...nextContracts]
|
||||||
await saveContracts()
|
await saveContracts()
|
||||||
@ -1738,214 +1552,5 @@ watch(budgetRefreshSignature, (next, prev) => {
|
|||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="@/features/ht/ht.css"></style>
|
||||||
.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>
|
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
import HtFeeMethodGrid from '@/features/shared/components/HtFeeMethodGrid.vue'
|
||||||
import { additionalWorkList } from '@/sql'
|
import { additionalWorkList } from '@/sql'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
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'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
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'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
|
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
import HtFeeMethodGrid from '@/features/shared/components/HtFeeMethodGrid.vue'
|
||||||
import { reserveList } from '@/sql'
|
import { reserveList } from '@/sql'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -200,7 +200,7 @@ const htView = markRaw(
|
|||||||
name: 'HtInfoWithProps',
|
name: 'HtInfoWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncHtInfo = defineAsyncComponent({
|
const AsyncHtInfo = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/htInfo.vue'),
|
loader: () => import('@/features/ht/components/htInfo.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 htInfo 组件失败:', err);
|
console.error('加载 htInfo 组件失败:', err);
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ const zxfwView = markRaw(
|
|||||||
name: 'ZxFwWithProps',
|
name: 'ZxFwWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncZxFw = defineAsyncComponent({
|
const AsyncZxFw = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/zxFw.vue'),
|
loader: () => import('@/features/ht/components/zxFw.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 zxFw 组件失败:', err);
|
console.error('加载 zxFw 组件失败:', err);
|
||||||
}
|
}
|
||||||
@ -238,7 +238,7 @@ const consultCategoryFactorView = markRaw(
|
|||||||
name: 'HtConsultCategoryFactorWithProps',
|
name: 'HtConsultCategoryFactorWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncHtConsultCategoryFactor = defineAsyncComponent({
|
const AsyncHtConsultCategoryFactor = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/HtConsultCategoryFactor.vue'),
|
loader: () => import('@/features/ht/components/HtConsultCategoryFactor.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
||||||
}
|
}
|
||||||
@ -257,7 +257,7 @@ const majorFactorView = markRaw(
|
|||||||
name: 'HtMajorFactorWithProps',
|
name: 'HtMajorFactorWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncHtMajorFactor = defineAsyncComponent({
|
const AsyncHtMajorFactor = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/HtMajorFactor.vue'),
|
loader: () => import('@/features/ht/components/HtMajorFactor.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 HtMajorFactor 组件失败:', err);
|
console.error('加载 HtMajorFactor 组件失败:', err);
|
||||||
}
|
}
|
||||||
@ -278,7 +278,7 @@ const htBaseInfoView = markRaw(
|
|||||||
name: 'HtBaseInfoWithProps',
|
name: 'HtBaseInfoWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncHtBaseInfo = defineAsyncComponent({
|
const AsyncHtBaseInfo = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/HtBaseInfo.vue'),
|
loader: () => import('@/features/ht/components/HtBaseInfo.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 HtBaseInfo 组件失败:', err)
|
console.error('加载 HtBaseInfo 组件失败:', err)
|
||||||
}
|
}
|
||||||
@ -293,7 +293,7 @@ const additionalWorkFeeView = markRaw(
|
|||||||
name: 'HtAdditionalWorkFeeWithProps',
|
name: 'HtAdditionalWorkFeeWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
|
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/HtAdditionalWorkFee.vue'),
|
loader: () => import('@/features/ht/components/HtAdditionalWorkFee.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
|
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
|
||||||
}
|
}
|
||||||
@ -308,7 +308,7 @@ const reserveFeeView = markRaw(
|
|||||||
name: 'HtReserveFeeWithProps',
|
name: 'HtReserveFeeWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncHtReserveFee = defineAsyncComponent({
|
const AsyncHtReserveFee = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/HtReserveFee.vue'),
|
loader: () => import('@/features/ht/components/HtReserveFee.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 HtReserveFee 组件失败:', err);
|
console.error('加载 HtReserveFee 组件失败:', err);
|
||||||
}
|
}
|
||||||
@ -323,7 +323,7 @@ const summaryView = markRaw(
|
|||||||
name: 'HtContractSummaryWithProps',
|
name: 'HtContractSummaryWithProps',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncSummary = defineAsyncComponent({
|
const AsyncSummary = defineAsyncComponent({
|
||||||
loader: () => import('@/components/ht/HtContractSummary.vue'),
|
loader: () => import('@/features/ht/components/HtContractSummary.vue'),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('加载 HtContractSummary 组件失败:', err)
|
console.error('加载 HtContractSummary 组件失败:', err)
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
|
import CommonAgGrid from '@/features/shared/components/xmCommonAgGrid.vue'
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -32,7 +32,7 @@ import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue }
|
|||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { useKvStore } from '@/pinia/kv'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
import ServiceCheckboxSelector from '@/components/shared/ServiceCheckboxSelector.vue'
|
import ServiceCheckboxSelector from '@/features/shared/components/ServiceCheckboxSelector.vue'
|
||||||
|
|
||||||
interface ServiceItem {
|
interface ServiceItem {
|
||||||
id: string
|
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">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
|
import HourlyFeeGrid from '@/features/shared/components/HourlyFeeGrid.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
@ -14,7 +14,7 @@ import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
|||||||
import { usePricingPaneLifecycle } from '@/lib/pricingScalePaneLifecycle'
|
import { usePricingPaneLifecycle } from '@/lib/pricingScalePaneLifecycle'
|
||||||
import { sumNullableBy } from '@/lib/pricingScaleCalc'
|
import { sumNullableBy } from '@/lib/pricingScaleCalc'
|
||||||
import { createPinnedTopRowData } from '@/lib/pricingPinnedRows'
|
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';
|
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">
|
<script setup lang="ts">
|
||||||
import { computed, defineComponent, h, markRaw, defineAsyncComponent, type Component } from 'vue'
|
import { computed, defineComponent, h, markRaw, defineAsyncComponent, type Component } from 'vue'
|
||||||
import TypeLine from '@/layout/typeLine.vue'
|
import TypeLine from '@/layout/typeLine.vue'
|
||||||
import HtFeeGrid from '@/components/shared/HtFeeGrid.vue'
|
import HtFeeGrid from '@/features/shared/components/HtFeeGrid.vue'
|
||||||
import HtFeeRateMethodForm from '@/components/ht/HtFeeRateMethodForm.vue'
|
import HtFeeRateMethodForm from '@/features/ht/components/HtFeeRateMethodForm.vue'
|
||||||
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
|
import HourlyFeeGrid from '@/features/shared/components/HourlyFeeGrid.vue'
|
||||||
|
|
||||||
interface TypeLineCategoryItem {
|
interface TypeLineCategoryItem {
|
||||||
key: string
|
key: string
|
||||||
@ -103,7 +103,7 @@ const workContentPane = markRaw(
|
|||||||
name: 'WorkContentPane',
|
name: 'WorkContentPane',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncWorkContentGrid = defineAsyncComponent({
|
const AsyncWorkContentGrid = defineAsyncComponent({
|
||||||
loader: () => import('@/components/shared/WorkContentGrid.vue'),
|
loader: () => import('@/features/shared/components/WorkContentGrid.vue'),
|
||||||
onError: err => {
|
onError: err => {
|
||||||
console.error('加载 WorkContentGrid 组件失败:', err)
|
console.error('加载 WorkContentGrid 组件失败:', err)
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
||||||
import TypeLine from '@/layout/typeLine.vue'
|
import TypeLine from '@/layout/typeLine.vue'
|
||||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue'
|
||||||
|
|
||||||
interface ServiceMethodType {
|
interface ServiceMethodType {
|
||||||
scale?: boolean | null
|
scale?: boolean | null
|
||||||
@ -59,7 +59,7 @@ const createPricingPane = (name: string) =>
|
|||||||
name,
|
name,
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncPricingView = defineAsyncComponent({
|
const AsyncPricingView = defineAsyncComponent({
|
||||||
loader: () => import(`@/components/pricing/${name}.vue`),
|
loader: () => import(`@/features/pricing/components/${name}.vue`),
|
||||||
onError: err => {
|
onError: err => {
|
||||||
console.error('加载 PricingMethodView 组件失败:', err)
|
console.error('加载 PricingMethodView 组件失败:', err)
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ const workContentPane = markRaw(
|
|||||||
name: 'WorkContentPane',
|
name: 'WorkContentPane',
|
||||||
setup() {
|
setup() {
|
||||||
const AsyncWorkContentGrid = defineAsyncComponent({
|
const AsyncWorkContentGrid = defineAsyncComponent({
|
||||||
loader: () => import('@/components/shared/WorkContentGrid.vue'),
|
loader: () => import('@/features/shared/components/WorkContentGrid.vue'),
|
||||||
onError: err => {
|
onError: err => {
|
||||||
console.error('加载 WorkContentGrid 组件失败:', err)
|
console.error('加载 WorkContentGrid 组件失败:', err)
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
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'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
|
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
import XmFactorGrid from '@/features/shared/components/XmFactorGrid.vue'
|
||||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue'
|
||||||
import { useKvStore } from '@/pinia/kv'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
|
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
@ -11,14 +11,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, markRaw } from 'vue'
|
import { defineAsyncComponent, markRaw } from 'vue'
|
||||||
import TypeLine from '@/layout/typeLine.vue'
|
import TypeLine from '@/layout/typeLine.vue'
|
||||||
const infoView = markRaw(defineAsyncComponent(() => import('@/components/xm/info.vue')))
|
const infoView = markRaw(defineAsyncComponent(() => import('@/features/xm/components/info.vue')))
|
||||||
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/xm/xmInfo.vue')))
|
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmInfo.vue')))
|
||||||
const htView = markRaw(defineAsyncComponent(() => import('@/components/ht/Ht.vue')))
|
const htView = markRaw(defineAsyncComponent(() => import('@/features/ht/components/Ht.vue')))
|
||||||
const consultCategoryFactorView = markRaw(
|
const consultCategoryFactorView = markRaw(
|
||||||
defineAsyncComponent(() => import('@/components/xm/XmConsultCategoryFactor.vue'))
|
defineAsyncComponent(() => import('@/features/xm/components/XmConsultCategoryFactor.vue'))
|
||||||
)
|
)
|
||||||
const majorFactorView = markRaw(
|
const majorFactorView = markRaw(
|
||||||
defineAsyncComponent(() => import('@/components/xm/XmMajorFactor.vue'))
|
defineAsyncComponent(() => import('@/features/xm/components/XmMajorFactor.vue'))
|
||||||
)
|
)
|
||||||
|
|
||||||
const xmCategories = [
|
const xmCategories = [
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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'
|
const DB_KEY = 'xm-info-v3'
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +30,61 @@ import {
|
|||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||||
import { formatExportTimestamp } from '@/lib/contractSegment'
|
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 {
|
import {
|
||||||
PROJECT_TAB_ID,
|
PROJECT_TAB_ID,
|
||||||
QUICK_TAB_ID,
|
QUICK_TAB_ID,
|
||||||
@ -65,372 +120,6 @@ import {
|
|||||||
} from '@/lib/reportExportBuilders'
|
} from '@/lib/reportExportBuilders'
|
||||||
import { exportFile } from '@/sql'
|
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 USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
|
||||||
const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
|
const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
|
||||||
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
|
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
|
||||||
@ -516,11 +205,11 @@ const userGuideSteps: UserGuideStep[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const componentMap: Record<string, any> = {
|
const componentMap: Record<string, any> = {
|
||||||
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.vue'))),
|
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmCard.vue'))),
|
||||||
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/components/ht/htCard.vue'))),
|
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/features/ht/components/htCard.vue'))),
|
||||||
QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/components/views/QuickCalcWorkbenchView.vue'))),
|
QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/QuickCalcWorkbenchView.vue'))),
|
||||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
ZxFwView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/ZxFwView.vue'))),
|
||||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/HtFeeMethodTypeLineView.vue'))),
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
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 =>
|
const createForageStore = (storeName: string): ForageInstance =>
|
||||||
localforage.createInstance({
|
localforage.createInstance({
|
||||||
name: PINIA_PERSIST_DB_NAME,
|
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 loadFactorRowsState = async (storageKey: string) => {
|
||||||
const [piniaData, kvData] = await Promise.all([
|
const [piniaData, kvData] = await Promise.all([
|
||||||
zxFwPricingStore.loadKeyState<DetailRowsStorageLike<FactorRowLike>>(storageKey),
|
zxFwPricingStore.loadKeyState<DetailRowsStorageLike<FactorRowLike>>(storageKey),
|
||||||
@ -1390,6 +985,7 @@ const exportData = async () => {
|
|||||||
)
|
)
|
||||||
const payload: DataPackage = {
|
const payload: DataPackage = {
|
||||||
version: 2,
|
version: 2,
|
||||||
|
packageType: 'project-snapshot',
|
||||||
exportedAt: now.toISOString(),
|
exportedAt: now.toISOString(),
|
||||||
localStorage: readWebStorage(localStorage),
|
localStorage: readWebStorage(localStorage),
|
||||||
sessionStorage: readWebStorage(sessionStorage),
|
sessionStorage: readWebStorage(sessionStorage),
|
||||||
@ -1404,7 +1000,7 @@ const exportData = async () => {
|
|||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = url
|
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)
|
const timestamp = formatExportTimestamp(now)
|
||||||
link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
|
link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
@ -1447,6 +1043,9 @@ const prepareImportPayloadFromFile = async (file: File) => {
|
|||||||
}
|
}
|
||||||
const buffer = await file.arrayBuffer()
|
const buffer = await file.arrayBuffer()
|
||||||
const payload = await decodeZwArchive<DataPackage>(buffer)
|
const payload = await decodeZwArchive<DataPackage>(buffer)
|
||||||
|
if (!isDataPackageLike(payload)) {
|
||||||
|
throw new Error('INVALID_DATA_PACKAGE')
|
||||||
|
}
|
||||||
pendingImportPayload.value = payload
|
pendingImportPayload.value = payload
|
||||||
pendingImportFileName.value = file.name
|
pendingImportFileName.value = file.name
|
||||||
importConfirmOpen.value = true
|
importConfirmOpen.value = true
|
||||||
@ -1956,39 +1555,4 @@ watch(
|
|||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="@/features/tab/tab.css"></style>
|
||||||
.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>
|
|
||||||
|
|||||||
@ -160,6 +160,9 @@ export const syncContractScaleToPricing = async (
|
|||||||
await store.loadContract(contractId)
|
await store.loadContract(contractId)
|
||||||
const currentState = store.getContractState(contractId)
|
const currentState = store.getContractState(contractId)
|
||||||
const selectedIds = Array.from(new Set((currentState?.selectedIds || []).map(id => String(id || '').trim()).filter(Boolean)))
|
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) {
|
if (selectedIds.length === 0) {
|
||||||
return {
|
return {
|
||||||
updatedServiceCount: 0,
|
updatedServiceCount: 0,
|
||||||
@ -167,6 +170,13 @@ export const syncContractScaleToPricing = async (
|
|||||||
updatedRowCount: 0
|
updatedRowCount: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changedRowIdSet && changedRowIdSet.size === 0) {
|
||||||
|
return {
|
||||||
|
updatedServiceCount: 0,
|
||||||
|
updatedMethodCount: 0,
|
||||||
|
updatedRowCount: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await ensurePricingMethodDetailRowsForServices({
|
await ensurePricingMethodDetailRowsForServices({
|
||||||
contractId,
|
contractId,
|
||||||
@ -179,9 +189,6 @@ export const syncContractScaleToPricing = async (
|
|||||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||||
const onlyCostScaleFallbackAmount = calcOnlyCostScaleAmountFromRows(sourceRows)
|
const onlyCostScaleFallbackAmount = calcOnlyCostScaleAmountFromRows(sourceRows)
|
||||||
const changedRowIdSet = options?.changedRowIds?.length
|
|
||||||
? normalizeChangedScaleRowIds(options.changedRowIds)
|
|
||||||
: undefined
|
|
||||||
const updatedServiceIdSet = new Set<string>()
|
const updatedServiceIdSet = new Set<string>()
|
||||||
let updatedMethodCount = 0
|
let updatedMethodCount = 0
|
||||||
let updatedRowCount = 0
|
let updatedRowCount = 0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user