i18n
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import draggable from 'vuedraggable'
|
||||
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
SERVICE_KEY_PREFIX,
|
||||
type ContractSegmentPackage
|
||||
} from '@/lib/contractSegment'
|
||||
import { industryTypeList } from '@/sql'
|
||||
import { getIndustryDisplayName, industryTypeList } from '@/sql'
|
||||
import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal'
|
||||
import { formatThousands } from '@/lib/numberFormat'
|
||||
import {
|
||||
@@ -79,6 +80,7 @@ const zxFwPricingStore = useZxFwPricingStore()
|
||||
const zxFwPricingKeysStore = useZxFwPricingKeysStore()
|
||||
const zxFwPricingHtFeeStore = useZxFwPricingHtFeeStore()
|
||||
const kvStore = useKvStore()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +97,7 @@ const showCreateModal = ref(false)
|
||||
const contractNameInput = ref('')
|
||||
const editingContractId = ref<string | null>(null)
|
||||
const toastOpen = ref(false)
|
||||
const toastTitle = ref('操作成功')
|
||||
const toastTitle = ref(t('ht.toastSuccessTitle'))
|
||||
const toastText = ref('')
|
||||
const deleteConfirmOpen = ref(false)
|
||||
const pendingDeleteContractId = ref<string | null>(null)
|
||||
@@ -182,7 +184,7 @@ const budgetRefreshSignature = computed(() => {
|
||||
})
|
||||
|
||||
const notify = (text: string) => {
|
||||
toastTitle.value = '操作成功'
|
||||
toastTitle.value = t('ht.toastSuccessTitle')
|
||||
toastText.value = text
|
||||
toastOpen.value = false
|
||||
requestAnimationFrame(() => {
|
||||
@@ -202,6 +204,11 @@ const pendingDeleteContractName = computed(() => {
|
||||
return target?.name || pendingDeleteContractId.value
|
||||
})
|
||||
|
||||
const batchDeleteCount = computed(() => {
|
||||
const selectedSet = new Set(selectedContractIds.value)
|
||||
return contracts.value.filter(item => selectedSet.has(item.id)).length
|
||||
})
|
||||
|
||||
const handleDeleteConfirmOpenChange = (open: boolean) => {
|
||||
deleteConfirmOpen.value = open
|
||||
}
|
||||
@@ -239,7 +246,9 @@ const getCurrentProjectIndustry = async (): Promise<string> => {
|
||||
}
|
||||
|
||||
const formatBudgetAmount = (value: number | null | undefined) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? `${formatThousands(value, 2)} 元` : '--'
|
||||
typeof value === 'number' && Number.isFinite(value)
|
||||
? `${formatThousands(value, 2)} ${t('htCard.currencySuffix')}`
|
||||
: '--'
|
||||
|
||||
const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
|
||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
||||
@@ -362,17 +371,10 @@ const scheduleRefreshContractBudgets = () => {
|
||||
}, 80)
|
||||
}
|
||||
|
||||
const industryNameByCode = (() => {
|
||||
const map = new Map<string, string>()
|
||||
for (const item of industryTypeList) {
|
||||
map.set(item.id, item.name)
|
||||
}
|
||||
return map
|
||||
})()
|
||||
|
||||
const formatIndustryLabel = (code: string) => {
|
||||
const trimmed = code.trim()
|
||||
const name = industryNameByCode.get(trimmed)
|
||||
const target = industryTypeList.find(item => String(item.id || '').trim() === trimmed)
|
||||
const name = target ? getIndustryDisplayName(target.id, locale.value) : ''
|
||||
return name ? `${trimmed} ${name}` : trimmed
|
||||
}
|
||||
|
||||
@@ -529,7 +531,7 @@ const initializeContractScaleData = async (contractId: string) => {
|
||||
|
||||
const exportSelectedContracts = async () => {
|
||||
if (selectedContractIds.value.length === 0) {
|
||||
showMessageDialog('提示', '请先勾选至少一个合同段。')
|
||||
showMessageDialog(t('ht.tipTitle'), t('ht.selectAtLeastOne'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -554,7 +556,7 @@ const exportSelectedContracts = async () => {
|
||||
|
||||
const projectIndustry = await getCurrentProjectIndustry()
|
||||
if (!projectIndustry) {
|
||||
showMessageDialog('导出失败', '未读取到当前项目工程行业,请先在“基础信息”里新建项目。')
|
||||
showMessageDialog(t('ht.exportFailedTitle'), t('ht.industryMissingForExport'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -583,17 +585,17 @@ const exportSelectedContracts = async () => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `合同段导出-${formatExportTimestamp(now)}${CONTRACT_SEGMENT_FILE_EXTENSION}`
|
||||
link.download = `contract-segments-${formatExportTimestamp(now)}${CONTRACT_SEGMENT_FILE_EXTENSION}`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
notify(`导出成功(${selectedContracts.length} 个合同段)`)
|
||||
notify(t('ht.exportSuccess', { count: selectedContracts.length }))
|
||||
exitContractSelectionMode()
|
||||
} catch (error) {
|
||||
console.error('export selected contracts failed:', error)
|
||||
showMessageDialog('导出失败', '请重试。')
|
||||
showMessageDialog(t('ht.exportFailedTitle'), t('ht.retry'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +693,7 @@ const importContractSegments = async (event: Event) => {
|
||||
zxFwPricingHtFeeStore.$persistNow?.()
|
||||
])
|
||||
await refreshContractBudgets()
|
||||
notify(`导入成功(${nextContracts.length} 个合同段)`)
|
||||
notify(t('ht.importSuccess', { count: nextContracts.length }))
|
||||
await nextTick()
|
||||
scrollContractsToBottom()
|
||||
} catch (error) {
|
||||
@@ -700,15 +702,18 @@ const importContractSegments = async (event: Event) => {
|
||||
if (message.startsWith('PROJECT_INDUSTRY_MISMATCH:')) {
|
||||
const [, importIndustry = '', currentIndustry = ''] = message.split(':')
|
||||
showMessageDialog(
|
||||
'导入失败',
|
||||
`工程行业不一致(导入包:${formatIndustryLabel(importIndustry)},当前项目:${formatIndustryLabel(currentIndustry)})。`
|
||||
t('ht.importFailedTitle'),
|
||||
t('ht.importIndustryMismatch', {
|
||||
importIndustry: formatIndustryLabel(importIndustry),
|
||||
currentIndustry: formatIndustryLabel(currentIndustry)
|
||||
})
|
||||
)
|
||||
} else if (message === 'CURRENT_PROJECT_INDUSTRY_MISSING') {
|
||||
showMessageDialog('导入失败', '当前项目未设置工程行业,请先在“基础信息”里新建项目。')
|
||||
showMessageDialog(t('ht.importFailedTitle'), t('ht.importCurrentIndustryMissing'))
|
||||
} else if (message === 'IMPORT_PACKAGE_INDUSTRY_MISSING') {
|
||||
showMessageDialog('导入失败', '导入包缺少工程行业信息,请使用最新版本重新导出后再导入。')
|
||||
showMessageDialog(t('ht.importFailedTitle'), t('ht.importPackageIndustryMissing'))
|
||||
} else {
|
||||
showMessageDialog('导入失败', '文件无效、已损坏或不是合同段导出文件。')
|
||||
showMessageDialog(t('ht.importFailedTitle'), t('ht.importFileInvalid'))
|
||||
}
|
||||
} finally {
|
||||
input.value = ''
|
||||
@@ -811,7 +816,7 @@ const createContract = async () => {
|
||||
item.id === editingContractId.value ? { ...item, name } : item
|
||||
)
|
||||
await saveContracts()
|
||||
notify('编辑成功')
|
||||
notify(t('ht.editSuccess'))
|
||||
closeCreateModal()
|
||||
return
|
||||
}
|
||||
@@ -831,7 +836,7 @@ const createContract = async () => {
|
||||
console.error('initialize contract scale failed:', error)
|
||||
}
|
||||
await refreshContractBudgets()
|
||||
notify('新建成功')
|
||||
notify(t('ht.createSuccess'))
|
||||
closeCreateModal()
|
||||
await nextTick()
|
||||
scrollContractsToBottom()
|
||||
@@ -850,19 +855,19 @@ const deleteContract = async (id: string) => {
|
||||
selectedContractIds.value = selectedContractIds.value.filter(item => item !== id)
|
||||
await saveContracts()
|
||||
await refreshContractBudgets()
|
||||
notify('删除成功')
|
||||
notify(t('ht.deleteSuccess'))
|
||||
}
|
||||
|
||||
const deleteSelectedContracts = async () => {
|
||||
if (selectedContractIds.value.length === 0) {
|
||||
showMessageDialog('提示', '请先勾选至少一个合同段。')
|
||||
showMessageDialog(t('ht.tipTitle'), t('ht.selectAtLeastOne'))
|
||||
return
|
||||
}
|
||||
|
||||
const selectedSet = new Set(selectedContractIds.value)
|
||||
const targets = contracts.value.filter(item => selectedSet.has(item.id))
|
||||
if (targets.length === 0) {
|
||||
showMessageDialog('提示', '未找到可删除的合同段。')
|
||||
showMessageDialog(t('ht.tipTitle'), t('ht.noContractsToDelete'))
|
||||
return
|
||||
}
|
||||
batchDeleteConfirmOpen.value = true
|
||||
@@ -873,7 +878,7 @@ const confirmDeleteSelectedContracts = async () => {
|
||||
const targets = contracts.value.filter(item => selectedSet.has(item.id))
|
||||
if (targets.length === 0) {
|
||||
batchDeleteConfirmOpen.value = false
|
||||
showMessageDialog('提示', '未找到可删除的合同段。')
|
||||
showMessageDialog(t('ht.tipTitle'), t('ht.noContractsToDelete'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -895,11 +900,11 @@ const confirmDeleteSelectedContracts = async () => {
|
||||
selectedContractIds.value = selectedContractIds.value.filter(item => !selectedSet.has(item))
|
||||
await saveContracts()
|
||||
await refreshContractBudgets()
|
||||
notify(`删除成功(${targetIds.length} 个合同段)`)
|
||||
notify(t('ht.deleteBatchSuccess', { count: targetIds.length }))
|
||||
exitContractSelectionMode()
|
||||
} catch (error) {
|
||||
console.error('delete selected contracts failed:', error)
|
||||
showMessageDialog('批量删除失败', '请重试。')
|
||||
showMessageDialog(t('ht.batchDeleteFailedTitle'), t('ht.retry'))
|
||||
} finally {
|
||||
batchDeleteConfirmOpen.value = false
|
||||
}
|
||||
@@ -916,7 +921,7 @@ const handleDragEnd = async (event: { oldIndex?: number; newIndex?: number }) =>
|
||||
}
|
||||
|
||||
await saveContracts()
|
||||
notify('排序完成')
|
||||
notify(t('ht.sortDone'))
|
||||
}
|
||||
|
||||
const updateDragPointerPosition = (event: MouseEvent | DragEvent) => {
|
||||
@@ -990,7 +995,7 @@ const handleCardClick = (item: ContractItem) => {
|
||||
}
|
||||
tabStore.openTab({
|
||||
id: `contract-${item.id}`,
|
||||
title: `合同段${item.name}`,
|
||||
title: t('ht.contractTabTitle', { name: item.name }),
|
||||
componentName: 'QuickCalcView',
|
||||
props: { contractId: item.id, contractName: item.name }
|
||||
})
|
||||
@@ -1064,32 +1069,32 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<ToastProvider>
|
||||
<TooltipProvider>
|
||||
<div class="flex h-full min-h-0 flex-col overflow-hidden">
|
||||
<div class="shrink-0 border-b bg-background/95 px-1 pb-4 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||
<div class="relative z-30 shrink-0 overflow-visible 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="space-y-1">
|
||||
<h3 class="text-lg font-bold">合同段列表</h3>
|
||||
<h3 class="text-lg font-bold">{{ t('ht.title') }}</h3>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
项目总预算金额:{{ contractBudgetLoading ? '计算中...' : formatBudgetAmount(projectTotalBudget) }}
|
||||
{{ t('ht.projectTotalBudget', { amount: contractBudgetLoading ? t('ht.budgetLoading') : formatBudgetAmount(projectTotalBudget) }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="isSelectingContracts">
|
||||
<div class="text-xs text-muted-foreground">已选 {{ selectedContractCount }} 个</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t('ht.selectedCount', { count: selectedContractCount }) }}</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="selectedContractCount === 0"
|
||||
@click="selectionMode === 'export' ? exportSelectedContracts() : deleteSelectedContracts()"
|
||||
>
|
||||
{{ selectionMode === 'export' ? '导出已选' : '删除已选' }}
|
||||
{{ selectionMode === 'export' ? t('ht.exportSelected') : t('ht.deleteSelected') }}
|
||||
</Button>
|
||||
<Button variant="ghost" @click="exitContractSelectionMode">
|
||||
取消
|
||||
{{ t('ht.cancelSelect') }}
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button :disabled="!canManageContracts" @click="openCreateModal">
|
||||
<Button class="whitespace-nowrap" :disabled="!canManageContracts" @click="openCreateModal">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
添加合同段
|
||||
{{ t('ht.addContract') }}
|
||||
</Button>
|
||||
<div ref="contractDataMenuRef" class="relative">
|
||||
<Button
|
||||
@@ -1102,32 +1107,32 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
</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"
|
||||
class="absolute right-0 top-full z-[80] mt-1 w-max rounded-md border bg-background p-1 shadow-md"
|
||||
>
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm"
|
||||
class="block whitespace-nowrap rounded px-3 py-1.5 text-left text-sm"
|
||||
:class="hasContracts ? 'cursor-pointer hover:bg-muted' : 'cursor-not-allowed text-muted-foreground'"
|
||||
:disabled="!hasContracts"
|
||||
@click="enterContractDeleteMode"
|
||||
>
|
||||
批量删除
|
||||
{{ t('ht.batchDelete') }}
|
||||
</button>
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm"
|
||||
class="block whitespace-nowrap rounded px-3 py-1.5 text-left text-sm"
|
||||
:class="hasContracts ? 'cursor-pointer hover:bg-muted' : 'cursor-not-allowed text-muted-foreground'"
|
||||
:disabled="!hasContracts"
|
||||
@click="enterContractExportMode"
|
||||
>
|
||||
导出合同段
|
||||
{{ t('ht.exportContracts') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm"
|
||||
class="block whitespace-nowrap rounded px-3 py-1.5 text-left text-sm"
|
||||
:class="canManageContracts ? 'cursor-pointer hover:bg-muted' : 'cursor-not-allowed text-muted-foreground'"
|
||||
:disabled="!canManageContracts"
|
||||
@click="triggerContractImport"
|
||||
>
|
||||
导入合同段
|
||||
{{ t('ht.importContracts') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -1148,7 +1153,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<input
|
||||
v-model="contractSearchKeyword"
|
||||
type="text"
|
||||
placeholder="搜索合同段名称或ID"
|
||||
:placeholder="t('ht.searchPlaceholder')"
|
||||
class="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
<Button
|
||||
@@ -1158,22 +1163,22 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
class="h-10 shrink-0 px-3"
|
||||
@click="contractSearchKeyword = ''"
|
||||
>
|
||||
清空筛选
|
||||
{{ t('ht.clearFilter') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="isSearchingContracts" class="mt-1 text-xs text-muted-foreground">
|
||||
搜索中({{ filteredContracts.length }} / {{ contracts.length }}),已关闭拖拽排序
|
||||
{{ t('ht.searchingHint', { filtered: filteredContracts.length, total: contracts.length }) }}
|
||||
</div>
|
||||
<div v-if="isSelectingContracts" class="mt-1 text-xs text-muted-foreground">
|
||||
{{ selectionMode === 'export' ? '导出选择模式:勾选合同段后点击“导出已选”' : '删除选择模式:勾选合同段后点击“删除已选”' }}
|
||||
{{ selectionMode === 'export' ? t('ht.selectModeExportHint') : t('ht.selectModeDeleteHint') }}
|
||||
</div>
|
||||
<div v-if="!canManageContracts" class="mt-1 text-xs text-muted-foreground">
|
||||
请先在“基础信息”里新建项目并选择工程行业后,再新增或导入合同段
|
||||
{{ t('ht.setupRequiredHint') }}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<span>{{ isListLayout ? '列表布局' : '网格布局' }}</span>
|
||||
<span>{{ isListLayout ? t('ht.listLayout') : t('ht.gridLayout') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
@@ -1259,10 +1264,10 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
ID: {{ element.id }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
预算:{{ formatBudgetAmount(contractBudgetById[element.id]) }}
|
||||
{{ t('ht.contractBudget', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
创建时间:{{ formatDateTime(element.createdAt) }}
|
||||
{{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}
|
||||
</span>
|
||||
</template>
|
||||
</CardTitle>
|
||||
@@ -1283,7 +1288,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<GripVertical :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">拖动排序</TooltipContent>
|
||||
<TooltipContent side="top">{{ t('ht.dragSort') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -1296,7 +1301,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<Edit3 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">编辑</TooltipContent>
|
||||
<TooltipContent side="top">{{ t('ht.edit') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -1309,7 +1314,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<Trash2 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">删除</TooltipContent>
|
||||
<TooltipContent side="top">{{ t('ht.remove') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -1320,9 +1325,9 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
'space-y-1 pb-1'
|
||||
]"
|
||||
>
|
||||
<div class="break-all">ID:{{ element.id }}</div>
|
||||
<div>本合同预算金额:{{ formatBudgetAmount(contractBudgetById[element.id]) }}</div>
|
||||
<div>创建时间:{{ formatDateTime(element.createdAt) }}</div>
|
||||
<div class="break-all">{{ t('ht.idLabel', { id: element.id }) }}</div>
|
||||
<div>{{ t('ht.contractBudgetLine', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}</div>
|
||||
<div>{{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -1331,8 +1336,8 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
v-else-if="!isSearchingContracts && filteredContracts.length === 0"
|
||||
class="mx-2 mb-4 rounded-2xl border border-dashed border-primary/30 bg-gradient-to-br from-primary/5 via-background to-muted/30 p-10 text-center shadow-sm"
|
||||
>
|
||||
<div class="text-lg font-semibold tracking-wide text-foreground">暂无合同卡片</div>
|
||||
<div class="mt-2 text-sm text-muted-foreground">赶紧来添加吧</div>
|
||||
<div class="text-lg font-semibold tracking-wide text-foreground">{{ t('ht.emptyTitle') }}</div>
|
||||
<div class="mt-2 text-sm text-muted-foreground">{{ t('ht.emptyDesc') }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
@@ -1389,10 +1394,10 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
ID: {{ element.id }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
预算:{{ formatBudgetAmount(contractBudgetById[element.id]) }}
|
||||
{{ t('ht.contractBudget', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
创建时间:{{ formatDateTime(element.createdAt) }}
|
||||
{{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}
|
||||
</span>
|
||||
</template>
|
||||
</CardTitle>
|
||||
@@ -1411,7 +1416,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<GripVertical :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">拖动排序(搜索时关闭)</TooltipContent>
|
||||
<TooltipContent side="top">{{ t('ht.dragSortSearchOff') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -1424,7 +1429,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<Edit3 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">编辑</TooltipContent>
|
||||
<TooltipContent side="top">{{ t('ht.edit') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -1437,7 +1442,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<Trash2 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">删除</TooltipContent>
|
||||
<TooltipContent side="top">{{ t('ht.remove') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -1448,16 +1453,16 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
'space-y-1 pb-4'
|
||||
]"
|
||||
>
|
||||
<div class="break-all">ID:{{ element.id }}</div>
|
||||
<div>本合同预算金额:{{ formatBudgetAmount(contractBudgetById[element.id]) }}</div>
|
||||
<div>创建时间:{{ formatDateTime(element.createdAt) }}</div>
|
||||
<div class="break-all">{{ t('ht.idLabel', { id: element.id }) }}</div>
|
||||
<div>{{ t('ht.contractBudgetLine', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}</div>
|
||||
<div>{{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
v-if="filteredContracts.length === 0"
|
||||
class="col-span-full rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
未找到匹配的合同段
|
||||
{{ t('ht.notFound') }}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
@@ -1468,7 +1473,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="回到顶部"
|
||||
:aria-label="t('ht.backToTop')"
|
||||
:class="[
|
||||
'fixed bottom-8 right-8 z-40 inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-black/15 bg-white text-black shadow-[0_10px_24px_rgba(0,0,0,0.16)] transition-all duration-300 hover:scale-105 hover:border-black/30 hover:bg-black hover:text-white',
|
||||
showScrollTopFab ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-3 opacity-0'
|
||||
@@ -1478,7 +1483,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<ArrowUp class="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">回到顶部</TooltipContent>
|
||||
<TooltipContent side="left">{{ t('ht.backToTop') }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
|
||||
<div
|
||||
@@ -1494,7 +1499,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
@mousedown.prevent="startDrag"
|
||||
>
|
||||
<h4 class="text-base font-semibold">
|
||||
{{ editingContractId ? '编辑合同段' : '新增合同段' }}
|
||||
{{ editingContractId ? t('ht.editContract') : t('ht.createContract') }}
|
||||
</h4>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeCreateModal">
|
||||
<X class="h-4 w-4" />
|
||||
@@ -1502,20 +1507,20 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 px-5 py-4">
|
||||
<label class="block text-sm font-medium text-foreground">合同段名称</label>
|
||||
<label class="block text-sm font-medium text-foreground">{{ t('ht.contractName') }}</label>
|
||||
<input
|
||||
v-model="contractNameInput"
|
||||
type="text"
|
||||
placeholder="请输入合同段名称"
|
||||
:placeholder="t('ht.contractNamePlaceholder')"
|
||||
class="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@keydown.enter="createContract"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 border-t px-5 py-3">
|
||||
<Button variant="outline" @click="closeCreateModal">取消</Button>
|
||||
<div class="flex items-center justify-end gap-2 px-5 py-3">
|
||||
<Button variant="outline" @click="closeCreateModal">{{ t('common.cancel') }}</Button>
|
||||
<Button :disabled="!contractNameInput.trim()" @click="createContract">
|
||||
{{ editingContractId ? '保存' : '确定' }}
|
||||
{{ editingContractId ? t('ht.save') : t('ht.ok') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1525,16 +1530,51 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认删除合同段</AlertDialogTitle>
|
||||
<AlertDialogTitle class="text-base font-semibold">{{ t('ht.deleteSingleTitle') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
即将删除“{{ pendingDeleteContractName }}”及其关联咨询服务和计价数据,是否继续?
|
||||
{{ t('ht.deleteSingleDesc', { name: pendingDeleteContractName }) }}
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline" @click="pendingDeleteContractId = null">取消</Button>
|
||||
<Button variant="outline" @click="pendingDeleteContractId = null">{{ t('common.cancel') }}</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="confirmDeleteContract">确认删除</Button>
|
||||
<Button variant="destructive" @click="confirmDeleteContract">{{ t('common.confirm') }}</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<AlertDialogRoot :open="batchDeleteConfirmOpen" @update:open="batchDeleteConfirmOpen = $event">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">{{ t('ht.deleteBatchTitle') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
{{ t('ht.deleteBatchDesc', { count: batchDeleteCount }) }}
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">{{ t('common.cancel') }}</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="confirmDeleteSelectedContracts">{{ t('common.confirm') }}</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<AlertDialogRoot :open="messageDialogOpen" @update:open="messageDialogOpen = $event">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">{{ messageDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
{{ messageDialogDesc }}
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogAction as-child>
|
||||
<Button @click="messageDialogOpen = false">{{ t('tab.dialog.iKnow') }}</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
@@ -1550,11 +1590,11 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
<ToastDescription class="text-xs text-muted-foreground">{{ toastText }}</ToastDescription>
|
||||
</div>
|
||||
<ToastAction
|
||||
alt-text="知道了"
|
||||
:alt-text="t('tab.dialog.iKnow')"
|
||||
class="ml-auto cursor-pointer inline-flex h-7 items-center rounded-md border border-border bg-muted px-2 text-xs text-foreground hover:bg-muted/80"
|
||||
@click="toastOpen = false"
|
||||
>
|
||||
知道了
|
||||
{{ t('tab.dialog.iKnow') }}
|
||||
</ToastAction>
|
||||
</ToastRoot>
|
||||
<ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
|
||||
|
||||
Reference in New Issue
Block a user