fix
This commit is contained in:
parent
13b03e016e
commit
ea6a244942
@ -7,7 +7,8 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
import { ArrowUp, Edit3, GripVertical, 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 {
|
import {
|
||||||
ToastAction,
|
ToastAction,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
@ -24,7 +25,24 @@ interface ContractItem {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DataEntry {
|
||||||
|
key: string
|
||||||
|
value: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContractSegmentPackage {
|
||||||
|
version: number
|
||||||
|
exportedAt: string
|
||||||
|
contracts: ContractItem[]
|
||||||
|
localforageEntries: DataEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'ht-card-v1'
|
const STORAGE_KEY = 'ht-card-v1'
|
||||||
|
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
||||||
|
const CONTRACT_SEGMENT_VERSION = 1
|
||||||
|
const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
||||||
|
const SERVICE_KEY_PREFIX = 'zxFW-'
|
||||||
|
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-']
|
||||||
|
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
@ -34,6 +52,11 @@ const tabStore = useTabStore()
|
|||||||
const contracts = ref<ContractItem[]>([])
|
const contracts = ref<ContractItem[]>([])
|
||||||
const contractSearchKeyword = ref('')
|
const contractSearchKeyword = ref('')
|
||||||
const isListLayout = ref(false)
|
const isListLayout = ref(false)
|
||||||
|
const contractDataMenuOpen = ref(false)
|
||||||
|
const contractDataMenuRef = ref<HTMLElement | null>(null)
|
||||||
|
const contractImportFileRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const isExportSelecting = ref(false)
|
||||||
|
const selectedExportContractIds = ref<string[]>([])
|
||||||
|
|
||||||
const showCreateModal = ref(false)
|
const showCreateModal = ref(false)
|
||||||
const contractNameInput = ref('')
|
const contractNameInput = ref('')
|
||||||
@ -67,6 +90,7 @@ const buildDefaultContracts = (): ContractItem[] => [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const normalizedSearchKeyword = computed(() => contractSearchKeyword.value.trim().toLowerCase())
|
const normalizedSearchKeyword = computed(() => contractSearchKeyword.value.trim().toLowerCase())
|
||||||
|
const selectedExportCount = computed(() => selectedExportContractIds.value.length)
|
||||||
const filteredContracts = computed(() => {
|
const filteredContracts = computed(() => {
|
||||||
if (!normalizedSearchKeyword.value) return contracts.value
|
if (!normalizedSearchKeyword.value) return contracts.value
|
||||||
return contracts.value.filter(item => {
|
return contracts.value.filter(item => {
|
||||||
@ -101,6 +125,47 @@ const notify = (text: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeContractDataMenu = () => {
|
||||||
|
contractDataMenuOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatExportTimestamp = (date: Date): string => {
|
||||||
|
const yyyy = date.getFullYear()
|
||||||
|
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hh = String(date.getHours()).padStart(2, '0')
|
||||||
|
const mi = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${yyyy}${mm}${dd}-${hh}${mi}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const isContractSelectedForExport = (contractId: string) =>
|
||||||
|
selectedExportContractIds.value.includes(contractId)
|
||||||
|
|
||||||
|
const toggleExportContractSelection = (contractId: string) => {
|
||||||
|
if (!isExportSelecting.value) return
|
||||||
|
if (isContractSelectedForExport(contractId)) {
|
||||||
|
selectedExportContractIds.value = selectedExportContractIds.value.filter(id => id !== contractId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedExportContractIds.value = [...selectedExportContractIds.value, contractId]
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitContractExportMode = () => {
|
||||||
|
isExportSelecting.value = false
|
||||||
|
selectedExportContractIds.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterContractExportMode = () => {
|
||||||
|
closeContractDataMenu()
|
||||||
|
isExportSelecting.value = true
|
||||||
|
selectedExportContractIds.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerContractImport = () => {
|
||||||
|
closeContractDataMenu()
|
||||||
|
contractImportFileRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
const getContractListViewport = () => {
|
const getContractListViewport = () => {
|
||||||
const viewport = contractListScrollWrapRef.value?.querySelector<HTMLElement>(
|
const viewport = contractListScrollWrapRef.value?.querySelector<HTMLElement>(
|
||||||
'[data-slot="scroll-area-viewport"]'
|
'[data-slot="scroll-area-viewport"]'
|
||||||
@ -179,6 +244,20 @@ const getCardEnterStyle = (index: number) => ({
|
|||||||
'--ht-card-enter-delay': `${Math.min(index, CARD_ENTER_MAX_INDEX) * CARD_ENTER_STEP_MS}ms`
|
'--ht-card-enter-delay': `${Math.min(index, CARD_ENTER_MAX_INDEX) * CARD_ENTER_STEP_MS}ms`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getCardSelectStyle = (index: number) => ({
|
||||||
|
'--ht-card-select-delay': `${(index % 10) * 70}ms`
|
||||||
|
})
|
||||||
|
|
||||||
|
const getContractCardStyle = (index: number) => {
|
||||||
|
if (isExportSelecting.value) {
|
||||||
|
return getCardSelectStyle(index)
|
||||||
|
}
|
||||||
|
if (cardMotionState.value === 'enter') {
|
||||||
|
return getCardEnterStyle(index)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const saveContracts = async () => {
|
const saveContracts = async () => {
|
||||||
try {
|
try {
|
||||||
contracts.value = normalizeOrder(contracts.value)
|
contracts.value = normalizeOrder(contracts.value)
|
||||||
@ -188,6 +267,194 @@ const saveContracts = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 normalizeDataEntries = (value: unknown): DataEntry[] => {
|
||||||
|
if (!Array.isArray(value)) return []
|
||||||
|
return value
|
||||||
|
.filter(item => item && typeof item === 'object' && typeof (item as DataEntry).key === 'string')
|
||||||
|
.map(item => ({
|
||||||
|
key: String((item as DataEntry).key),
|
||||||
|
value: (item as DataEntry).value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isContractSegmentPackage = (value: unknown): value is ContractSegmentPackage => {
|
||||||
|
const payload = value as Partial<ContractSegmentPackage> | null
|
||||||
|
return Boolean(payload && typeof payload === 'object' && Array.isArray(payload.contracts))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isContractRelatedForageKey = (key: string, contractId: string) => {
|
||||||
|
if (key === `${CONTRACT_KEY_PREFIX}${contractId}`) return true
|
||||||
|
if (key === `${SERVICE_KEY_PREFIX}${contractId}`) return true
|
||||||
|
if (PRICING_KEY_PREFIXES.some(prefix => key.startsWith(`${prefix}${contractId}-`))) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const readContractRelatedForageEntries = async (contractIds: string[]) => {
|
||||||
|
const keys = await localforage.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 localforage.getItem(key)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewriteKeyWithContractId = (key: string, fromId: string, toId: string) => {
|
||||||
|
if (key === `${CONTRACT_KEY_PREFIX}${fromId}`) return `${CONTRACT_KEY_PREFIX}${toId}`
|
||||||
|
if (key === `${SERVICE_KEY_PREFIX}${fromId}`) return `${SERVICE_KEY_PREFIX}${toId}`
|
||||||
|
for (const prefix of PRICING_KEY_PREFIXES) {
|
||||||
|
if (key.startsWith(`${prefix}${fromId}-`)) {
|
||||||
|
return key.replace(`${prefix}${fromId}-`, `${prefix}${toId}-`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateContractId = (usedIds: Set<string>) => {
|
||||||
|
let nextId = ''
|
||||||
|
while (!nextId || usedIds.has(nextId)) {
|
||||||
|
nextId = `ct-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||||
|
}
|
||||||
|
usedIds.add(nextId)
|
||||||
|
return nextId
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportSelectedContracts = async () => {
|
||||||
|
if (selectedExportContractIds.value.length === 0) {
|
||||||
|
window.alert('请先勾选至少一个合同段。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selectedSet = new Set(selectedExportContractIds.value)
|
||||||
|
const selectedContracts = contracts.value
|
||||||
|
.filter(item => selectedSet.has(item.id))
|
||||||
|
.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
order: index
|
||||||
|
}))
|
||||||
|
|
||||||
|
const localforageEntries = await readContractRelatedForageEntries(
|
||||||
|
selectedContracts.map(item => item.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const payload: ContractSegmentPackage = {
|
||||||
|
version: CONTRACT_SEGMENT_VERSION,
|
||||||
|
exportedAt: now.toISOString(),
|
||||||
|
contracts: selectedContracts,
|
||||||
|
localforageEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await encodeZwArchive(payload)
|
||||||
|
const binary = new Uint8Array(content.length)
|
||||||
|
binary.set(content)
|
||||||
|
const blob = new Blob([binary], { type: 'application/octet-stream' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `合同段导出-${formatExportTimestamp(now)}${CONTRACT_SEGMENT_FILE_EXTENSION}`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
notify(`导出成功(${selectedContracts.length} 个合同段)`)
|
||||||
|
exitContractExportMode()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('export selected contracts failed:', error)
|
||||||
|
window.alert('导出失败,请重试。')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importContractSegments = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = file.name.toLowerCase()
|
||||||
|
if (!name.endsWith(CONTRACT_SEGMENT_FILE_EXTENSION) && !name.endsWith('.zw')) {
|
||||||
|
throw new Error('INVALID_FILE_EXTENSION')
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await file.arrayBuffer()
|
||||||
|
const payload = await decodeZwArchive<ContractSegmentPackage>(buffer)
|
||||||
|
if (!isContractSegmentPackage(payload)) {
|
||||||
|
throw new Error('INVALID_CONTRACT_SEGMENT_PAYLOAD')
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedContracts = normalizeContractsFromPayload(payload.contracts)
|
||||||
|
if (importedContracts.length === 0) {
|
||||||
|
throw new Error('EMPTY_CONTRACTS')
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedEntries = normalizeDataEntries(payload.localforageEntries)
|
||||||
|
const usedIds = new Set(contracts.value.map(item => item.id))
|
||||||
|
const oldToNewIdMap = new Map<string, string>()
|
||||||
|
const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
|
||||||
|
const nextId = generateContractId(usedIds)
|
||||||
|
oldToNewIdMap.set(item.id, nextId)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: nextId,
|
||||||
|
order: contracts.value.length + index,
|
||||||
|
createdAt: item.createdAt || new Date().toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rewrittenEntries = importedEntries.map(entry => {
|
||||||
|
let nextKey = entry.key
|
||||||
|
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||||
|
if (!nextKey.includes(oldId)) continue
|
||||||
|
nextKey = rewriteKeyWithContractId(nextKey, oldId, newId)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: nextKey,
|
||||||
|
value: entry.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(rewrittenEntries.map(entry => localforage.setItem(entry.key, entry.value)))
|
||||||
|
|
||||||
|
contracts.value = [...contracts.value, ...nextContracts]
|
||||||
|
await saveContracts()
|
||||||
|
notify(`导入成功(${nextContracts.length} 个合同段)`)
|
||||||
|
await nextTick()
|
||||||
|
scrollContractsToBottom()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('import contract segments failed:', error)
|
||||||
|
window.alert('导入失败:文件无效、已损坏或不是合同段导出文件。')
|
||||||
|
} finally {
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeForageKeysByContractId = async (store: typeof localforage, contractId: string) => {
|
const removeForageKeysByContractId = async (store: typeof localforage, contractId: string) => {
|
||||||
try {
|
try {
|
||||||
const keys = await store.keys()
|
const keys = await store.keys()
|
||||||
@ -239,6 +506,7 @@ const loadContracts = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
|
closeContractDataMenu()
|
||||||
editingContractId.value = null
|
editingContractId.value = null
|
||||||
contractNameInput.value = ''
|
contractNameInput.value = ''
|
||||||
modalOffset.value = { x: 0, y: 0 }
|
modalOffset.value = { x: 0, y: 0 }
|
||||||
@ -246,6 +514,7 @@ const openCreateModal = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openEditModal = (item: ContractItem) => {
|
const openEditModal = (item: ContractItem) => {
|
||||||
|
closeContractDataMenu()
|
||||||
editingContractId.value = item.id
|
editingContractId.value = item.id
|
||||||
contractNameInput.value = item.name
|
contractNameInput.value = item.name
|
||||||
modalOffset.value = { x: 0, y: 0 }
|
modalOffset.value = { x: 0, y: 0 }
|
||||||
@ -307,6 +576,7 @@ const deleteContract = async (id: string) => {
|
|||||||
await cleanupContractRelatedData(id)
|
await cleanupContractRelatedData(id)
|
||||||
|
|
||||||
contracts.value = contracts.value.filter(item => item.id !== id)
|
contracts.value = contracts.value.filter(item => item.id !== id)
|
||||||
|
selectedExportContractIds.value = selectedExportContractIds.value.filter(item => item !== id)
|
||||||
await saveContracts()
|
await saveContracts()
|
||||||
notify('删除成功')
|
notify('删除成功')
|
||||||
}
|
}
|
||||||
@ -390,6 +660,10 @@ const stopContractAutoScroll = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCardClick = (item: ContractItem) => {
|
const handleCardClick = (item: ContractItem) => {
|
||||||
|
if (isExportSelecting.value) {
|
||||||
|
toggleExportContractSelection(item.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
tabStore.openTab({
|
tabStore.openTab({
|
||||||
id: `contract-${item.id}`,
|
id: `contract-${item.id}`,
|
||||||
title: `合同段${item.name}`,
|
title: `合同段${item.name}`,
|
||||||
@ -419,11 +693,20 @@ const startDrag = (event: MouseEvent) => {
|
|||||||
window.addEventListener('mouseup', stopDrag)
|
window.addEventListener('mouseup', stopDrag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGlobalMouseDown = (event: MouseEvent) => {
|
||||||
|
if (!contractDataMenuOpen.value || !contractDataMenuRef.value) return
|
||||||
|
const target = event.target as Node
|
||||||
|
if (!contractDataMenuRef.value.contains(target)) {
|
||||||
|
contractDataMenuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadContracts()
|
await loadContracts()
|
||||||
triggerCardEnterAnimation()
|
triggerCardEnterAnimation()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
bindContractListScroll()
|
bindContractListScroll()
|
||||||
|
window.addEventListener('mousedown', handleGlobalMouseDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
@ -437,6 +720,7 @@ onBeforeUnmount(() => {
|
|||||||
stopDrag()
|
stopDrag()
|
||||||
stopContractAutoScroll()
|
stopContractAutoScroll()
|
||||||
unbindContractListScroll()
|
unbindContractListScroll()
|
||||||
|
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
||||||
if (cardEnterTimer) clearTimeout(cardEnterTimer)
|
if (cardEnterTimer) clearTimeout(cardEnterTimer)
|
||||||
void saveContracts()
|
void saveContracts()
|
||||||
})
|
})
|
||||||
@ -449,10 +733,61 @@ onBeforeUnmount(() => {
|
|||||||
<div class="shrink-0 border-b bg-background/95 px-1 pb-4 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
<div class="shrink-0 border-b bg-background/95 px-1 pb-4 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||||
<div class="mb-6 flex items-center justify-between pt-1">
|
<div class="mb-6 flex items-center justify-between pt-1">
|
||||||
<h3 class="text-lg font-bold">合同段列表</h3>
|
<h3 class="text-lg font-bold">合同段列表</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<template v-if="isExportSelecting">
|
||||||
|
<div class="text-xs text-muted-foreground">已选 {{ selectedExportCount }} 个</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:disabled="selectedExportCount === 0"
|
||||||
|
@click="exportSelectedContracts"
|
||||||
|
>
|
||||||
|
导出已选
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" @click="exitContractExportMode">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<Button @click="openCreateModal">
|
<Button @click="openCreateModal">
|
||||||
<Plus class="mr-2 h-4 w-4" />
|
<Plus class="mr-2 h-4 w-4" />
|
||||||
添加合同段
|
添加合同段
|
||||||
</Button>
|
</Button>
|
||||||
|
<div ref="contractDataMenuRef" class="relative">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="h-10 w-10"
|
||||||
|
@click="contractDataMenuOpen = !contractDataMenuOpen"
|
||||||
|
>
|
||||||
|
<MoreHorizontal class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
v-if="contractDataMenuOpen"
|
||||||
|
class="absolute right-0 top-full z-50 mt-1 min-w-[132px] rounded-md border bg-background p-1 shadow-md"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||||
|
@click="enterContractExportMode"
|
||||||
|
>
|
||||||
|
导出合同段
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||||
|
@click="triggerContractImport"
|
||||||
|
>
|
||||||
|
导入合同段
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="contractImportFileRef"
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
accept=".htzw,.zw"
|
||||||
|
@change="importContractSegments"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:items-start">
|
<div class="flex flex-col gap-2 md:flex-row md:items-start">
|
||||||
@ -477,6 +812,9 @@ onBeforeUnmount(() => {
|
|||||||
<div v-if="isSearchingContracts" class="mt-1 text-xs text-muted-foreground">
|
<div v-if="isSearchingContracts" class="mt-1 text-xs text-muted-foreground">
|
||||||
搜索中({{ filteredContracts.length }} / {{ contracts.length }}),已关闭拖拽排序
|
搜索中({{ filteredContracts.length }} / {{ contracts.length }}),已关闭拖拽排序
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isExportSelecting" class="mt-1 text-xs text-muted-foreground">
|
||||||
|
导出选择模式:勾选合同段后点击“导出已选”
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2 md:ml-auto">
|
<div class="flex flex-wrap items-center gap-2 md:ml-auto">
|
||||||
<label class="inline-flex cursor-pointer items-center gap-2 text-xs text-muted-foreground select-none">
|
<label class="inline-flex cursor-pointer items-center gap-2 text-xs text-muted-foreground select-none">
|
||||||
@ -506,6 +844,7 @@ onBeforeUnmount(() => {
|
|||||||
:key="`contracts-${isListLayout ? 'list' : 'grid'}`"
|
:key="`contracts-${isListLayout ? 'list' : 'grid'}`"
|
||||||
v-model="contracts"
|
v-model="contracts"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
|
:disabled="isExportSelecting"
|
||||||
handle=".contract-drag-handle"
|
handle=".contract-drag-handle"
|
||||||
ghost-class="ht-sortable-ghost"
|
ghost-class="ht-sortable-ghost"
|
||||||
chosen-class="ht-sortable-chosen"
|
chosen-class="ht-sortable-chosen"
|
||||||
@ -522,13 +861,30 @@ onBeforeUnmount(() => {
|
|||||||
<template #item="{ element, index }">
|
<template #item="{ element, index }">
|
||||||
<Card
|
<Card
|
||||||
:class="[
|
:class="[
|
||||||
'group cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
|
'group relative cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
|
||||||
cardMotionState === 'enter' && !isDraggingContracts ? 'ht-contract-card--enter' : 'ht-contract-card--ready',
|
isExportSelecting
|
||||||
|
? 'ht-contract-card--selecting'
|
||||||
|
: cardMotionState === 'enter' && !isDraggingContracts
|
||||||
|
? 'ht-contract-card--enter'
|
||||||
|
: 'ht-contract-card--ready',
|
||||||
|
isContractSelectedForExport(element.id) && 'ht-contract-card--selected',
|
||||||
isListLayout && 'gap-0 py-0'
|
isListLayout && 'gap-0 py-0'
|
||||||
]"
|
]"
|
||||||
:style="cardMotionState === 'enter' ? getCardEnterStyle(index) : undefined"
|
:style="getContractCardStyle(index)"
|
||||||
@click="handleCardClick(element)"
|
@click="handleCardClick(element)"
|
||||||
>
|
>
|
||||||
|
<label
|
||||||
|
v-if="isExportSelecting"
|
||||||
|
class="absolute left-2 top-2 z-10 inline-flex cursor-pointer items-center rounded bg-background/90 p-0.5 shadow-sm"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
:checked="isContractSelectedForExport(element.id)"
|
||||||
|
@change.stop="toggleExportContractSelection(element.id)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
:class="[
|
:class="[
|
||||||
'flex flex-row items-center justify-between gap-0 space-y-0',
|
'flex flex-row items-center justify-between gap-0 space-y-0',
|
||||||
@ -551,7 +907,10 @@ onBeforeUnmount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div :class="['flex shrink-0 opacity-0 transition-opacity group-hover:opacity-100', isListLayout ? 'gap-0.5' : 'gap-1']">
|
<div
|
||||||
|
v-if="!isExportSelecting"
|
||||||
|
:class="['flex shrink-0 opacity-0 transition-opacity group-hover:opacity-100', isListLayout ? 'gap-0.5' : 'gap-1']"
|
||||||
|
>
|
||||||
<TooltipRoot>
|
<TooltipRoot>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
@ -622,13 +981,30 @@ onBeforeUnmount(() => {
|
|||||||
v-for="(element, index) in filteredContracts"
|
v-for="(element, index) in filteredContracts"
|
||||||
:key="element.id"
|
:key="element.id"
|
||||||
:class="[
|
:class="[
|
||||||
'group cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
|
'group relative cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
|
||||||
cardMotionState === 'enter' && !isDraggingContracts ? 'ht-contract-card--enter' : 'ht-contract-card--ready',
|
isExportSelecting
|
||||||
|
? 'ht-contract-card--selecting'
|
||||||
|
: cardMotionState === 'enter' && !isDraggingContracts
|
||||||
|
? 'ht-contract-card--enter'
|
||||||
|
: 'ht-contract-card--ready',
|
||||||
|
isContractSelectedForExport(element.id) && 'ht-contract-card--selected',
|
||||||
isListLayout && 'gap-0 py-0'
|
isListLayout && 'gap-0 py-0'
|
||||||
]"
|
]"
|
||||||
:style="cardMotionState === 'enter' ? getCardEnterStyle(index) : undefined"
|
:style="getContractCardStyle(index)"
|
||||||
@click="handleCardClick(element)"
|
@click="handleCardClick(element)"
|
||||||
>
|
>
|
||||||
|
<label
|
||||||
|
v-if="isExportSelecting"
|
||||||
|
class="absolute left-2 top-2 z-10 inline-flex cursor-pointer items-center rounded bg-background/90 p-0.5 shadow-sm"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
:checked="isContractSelectedForExport(element.id)"
|
||||||
|
@change.stop="toggleExportContractSelection(element.id)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
:class="[
|
:class="[
|
||||||
'flex flex-row items-center justify-between gap-0 space-y-0',
|
'flex flex-row items-center justify-between gap-0 space-y-0',
|
||||||
@ -651,7 +1027,10 @@ onBeforeUnmount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div :class="['flex shrink-0 opacity-0 transition-opacity group-hover:opacity-100', isListLayout ? 'gap-0.5' : 'gap-1']">
|
<div
|
||||||
|
v-if="!isExportSelecting"
|
||||||
|
:class="['flex shrink-0 opacity-0 transition-opacity group-hover:opacity-100', isListLayout ? 'gap-0.5' : 'gap-1']"
|
||||||
|
>
|
||||||
<TooltipRoot>
|
<TooltipRoot>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span
|
<span
|
||||||
@ -812,6 +1191,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.ht-contract-card {
|
.ht-contract-card {
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
contain: paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ht-contract-card--ready {
|
.ht-contract-card--ready {
|
||||||
@ -824,6 +1206,26 @@ onBeforeUnmount(() => {
|
|||||||
animation-delay: var(--ht-card-enter-delay, 0ms);
|
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));
|
||||||
|
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes ht-card-slide-in {
|
@keyframes ht-card-slide-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -835,8 +1237,37 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.ht-contract-card--enter {
|
.ht-contract-card--enter,
|
||||||
|
.ht-contract-card--selecting {
|
||||||
animation: none;
|
animation: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/utils.ts","./src/pinia/pricingpanereload.ts","./src/pinia/tab.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/contractdetailview.vue","./src/components/views/ht.vue","./src/components/views/xm.vue","./src/components/views/zxfwview.vue","./src/components/views/htinfo.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/numberformat.ts","./src/lib/pricingmethodtotals.ts","./src/lib/utils.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/pricingpanereload.ts","./src/pinia/tab.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/contractdetailview.vue","./src/components/views/ht.vue","./src/components/views/xm.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmfactorgrid.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htinfo.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
||||||
Loading…
x
Reference in New Issue
Block a user