修复bug
This commit is contained in:
parent
63ebc3f26a
commit
1165ee91ce
@ -1212,7 +1212,7 @@ const handleCardClick = (item: ContractItem) => {
|
|||||||
tabStore.openTab({
|
tabStore.openTab({
|
||||||
id: `contract-${item.id}`,
|
id: `contract-${item.id}`,
|
||||||
title: `合同段${item.name}`,
|
title: `合同段${item.name}`,
|
||||||
componentName: 'ContractDetailView',
|
componentName: 'QuickCalcView',
|
||||||
props: { contractId: item.id, contractName: item.name }
|
props: { contractId: item.id, contractName: item.name }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/components/ht/HtBaseInfo.vue
Normal file
102
src/components/ht/HtBaseInfo.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
|
interface HtBaseInfoState {
|
||||||
|
quality: string
|
||||||
|
duration:
|
||||||
|
string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_QUALITY = '造价咨询服务的综合评价应达到"较好"或综合评分90分'
|
||||||
|
|
||||||
|
const props =
|
||||||
|
defineProps<{
|
||||||
|
contractId: string
|
||||||
|
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
const storageKey = () =>
|
||||||
|
`ht-base-info-${props.contractId}`
|
||||||
|
|
||||||
|
const quality = ref(DEFAULT_QUALITY)
|
||||||
|
const duration = ref('')
|
||||||
|
const
|
||||||
|
lastSavedSnapshot = ref('')
|
||||||
|
|
||||||
|
const saveForm = (force = false) => {
|
||||||
|
const payload: HtBaseInfoState = {
|
||||||
|
|
||||||
|
quality: quality.value,
|
||||||
|
duration: duration.value
|
||||||
|
}
|
||||||
|
const snapshot = JSON.stringify(payload)
|
||||||
|
if (!force
|
||||||
|
&& snapshot === lastSavedSnapshot.value) return
|
||||||
|
zxFwPricingStore.setKeyState(storageKey(), payload)
|
||||||
|
|
||||||
|
lastSavedSnapshot.value = snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadForm = async () => {
|
||||||
|
const data = await
|
||||||
|
zxFwPricingStore.loadKeyState<HtBaseInfoState>(storageKey())
|
||||||
|
quality.value = typeof data?.quality === 'string' &&
|
||||||
|
data.quality ? data.quality : DEFAULT_QUALITY
|
||||||
|
duration.value = typeof data?.duration === 'string' ? data.duration :
|
||||||
|
''
|
||||||
|
const payload: HtBaseInfoState = { quality: quality.value, duration: duration.value }
|
||||||
|
|
||||||
|
lastSavedSnapshot.value = JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([quality, duration], () => { saveForm()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => { void loadForm() })
|
||||||
|
onBeforeUnmount(() => { saveForm(true) })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="h-full min-h-0 flex flex-col">
|
||||||
|
<div class="rounded-lg border bg-card p-5">
|
||||||
|
<div class="mb-4
|
||||||
|
border-b pb-3">
|
||||||
|
<h3 class="text-sm font-semibold text-foreground">基础信息</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 gap-5">
|
||||||
|
<label class="space-y-1.5">
|
||||||
|
<div class="text-xs font-medium
|
||||||
|
text-muted-foreground">质量要求</div>
|
||||||
|
<textarea
|
||||||
|
v-model="quality"
|
||||||
|
rows="3"
|
||||||
|
|
||||||
|
placeholder="请输入质量要求"
|
||||||
|
class="w-full rounded-md border bg-background px-3 py-2
|
||||||
|
text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30 resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</label>
|
||||||
|
<label class="space-y-1.5">
|
||||||
|
<div class="text-xs font-medium
|
||||||
|
text-muted-foreground">工期要求</div>
|
||||||
|
<textarea
|
||||||
|
v-model="duration"
|
||||||
|
|
||||||
|
rows="3"
|
||||||
|
placeholder="请输入工期要求"
|
||||||
|
class="w-full rounded-md border bg-background
|
||||||
|
px-3 py-2 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30 resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
@ -182,6 +182,7 @@ const scheduleRefreshContractBudget = () => {
|
|||||||
interface XmCategoryItem {
|
interface XmCategoryItem {
|
||||||
key:
|
key:
|
||||||
| 'info'
|
| 'info'
|
||||||
|
| 'base-info'
|
||||||
| 'consult-category-factor'
|
| 'consult-category-factor'
|
||||||
| 'major-factor'
|
| 'major-factor'
|
||||||
| 'work-grid'
|
| 'work-grid'
|
||||||
@ -271,6 +272,21 @@ const majorFactorView = markRaw(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const htBaseInfoView = markRaw(
|
||||||
|
defineComponent({
|
||||||
|
name: 'HtBaseInfoWithProps',
|
||||||
|
setup() {
|
||||||
|
const AsyncHtBaseInfo = defineAsyncComponent({
|
||||||
|
loader: () => import('@/components/ht/HtBaseInfo.vue'),
|
||||||
|
onError: (err) => {
|
||||||
|
console.error('加载 HtBaseInfo 组件失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => h(AsyncHtBaseInfo, { contractId: props.contractId })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const additionalWorkFeeView = markRaw(
|
const additionalWorkFeeView = markRaw(
|
||||||
defineComponent({
|
defineComponent({
|
||||||
name: 'HtAdditionalWorkFeeWithProps',
|
name: 'HtAdditionalWorkFeeWithProps',
|
||||||
@ -303,6 +319,7 @@ const reserveFeeView = markRaw(
|
|||||||
|
|
||||||
// 4. 给分类数组添加严格类型标注
|
// 4. 给分类数组添加严格类型标注
|
||||||
const xmCategories: XmCategoryItem[] = [
|
const xmCategories: XmCategoryItem[] = [
|
||||||
|
{ key: 'base-info', label: '基础信息', component: htBaseInfoView },
|
||||||
{ key: 'info', label: '规模信息', component: htView },
|
{ key: 'info', label: '规模信息', component: htView },
|
||||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, defineComponent, h, onBeforeUnmount, onMounted, PropType, ref } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type {
|
import type {
|
||||||
CellValueChangedEvent,
|
CellValueChangedEvent,
|
||||||
@ -10,11 +10,22 @@ import type {
|
|||||||
ValueFormatterParams
|
ValueFormatterParams
|
||||||
} from 'ag-grid-community'
|
} from 'ag-grid-community'
|
||||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
|
import {
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogRoot,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from 'reka-ui'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { myTheme, agGridStyle } from '@/lib/diyAgGridOptions'
|
import { myTheme, agGridStyle } from '@/lib/diyAgGridOptions'
|
||||||
import { workList } from '@/sql'
|
import { workList } from '@/sql'
|
||||||
import type { WorkType } from '@/sql'
|
import type { WorkType } from '@/sql'
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
|
import { Trash2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
interface WorkContentRow {
|
interface WorkContentRow {
|
||||||
id: string
|
id: string
|
||||||
@ -88,7 +99,14 @@ const buildDefaultRowsFromDict = (): WorkContentRow[] => {
|
|||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
const deleteConfirmOpen = ref(false)
|
||||||
|
const pendingDeleteRowId = ref<string | null>(null)
|
||||||
|
const pendingDeleteRowName = ref('')
|
||||||
|
const requestDeleteRow = (id: string, name?: string) => {
|
||||||
|
pendingDeleteRowId.value = id
|
||||||
|
pendingDeleteRowName.value = String(name || '').trim() || '当前行'
|
||||||
|
deleteConfirmOpen.value = true
|
||||||
|
}
|
||||||
const checkedIds = computed(() =>
|
const checkedIds = computed(() =>
|
||||||
rowData.value.filter(item => item.checked).map(item => item.id)
|
rowData.value.filter(item => item.checked).map(item => item.id)
|
||||||
)
|
)
|
||||||
@ -220,6 +238,47 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
|||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
},
|
},
|
||||||
valueFormatter: params => params.value || '点击输入'
|
valueFormatter: params => params.value || '点击输入'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: '操作',
|
||||||
|
colId: 'actions',
|
||||||
|
minWidth: 92,
|
||||||
|
maxWidth: 110,
|
||||||
|
flex: 0.8,
|
||||||
|
editable: false,
|
||||||
|
sortable: false,
|
||||||
|
filter: false,
|
||||||
|
suppressMovable: true,
|
||||||
|
cellRenderer: defineComponent({
|
||||||
|
name: 'HtFeeGridActionCellRenderer',
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
type: Object as PropType<ICellRendererParams<WorkContentRow>>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(rendererProps) {
|
||||||
|
return () => {
|
||||||
|
const row = rendererProps.params.data
|
||||||
|
if (!row?.custom) return null
|
||||||
|
const onDelete = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
requestDeleteRow(row.id, row.content)
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class:
|
||||||
|
'inline-flex cursor-pointer items-center gap-1 rounded border border-red-200 px-2 py-1 text-xs text-red-600 hover:bg-red-50',
|
||||||
|
onClick: onDelete
|
||||||
|
},
|
||||||
|
[h(Trash2, { size: 12, 'aria-hidden': 'true' }), h('span', '删除')]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -260,6 +319,24 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
saveToStore()
|
saveToStore()
|
||||||
})
|
})
|
||||||
|
const handleDeleteConfirmOpenChange = (open: boolean) => {
|
||||||
|
deleteConfirmOpen.value = open
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const deleteRow = (id: string) => {
|
||||||
|
rowData.value = rowData.value.filter(item => item.id !== id)
|
||||||
|
saveToStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteRow = () => {
|
||||||
|
const id = pendingDeleteRowId.value
|
||||||
|
if (!id) return
|
||||||
|
deleteRow(id)
|
||||||
|
deleteConfirmOpen.value = false
|
||||||
|
pendingDeleteRowId.value = null
|
||||||
|
pendingDeleteRowName.value = ''
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -291,6 +368,25 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AlertDialogRoot :open="deleteConfirmOpen" @update:open="handleDeleteConfirmOpenChange">
|
||||||
|
<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>
|
||||||
|
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||||
|
将删除“{{ pendingDeleteRowName }}”这条明细,是否继续?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<div class="mt-4 flex items-center justify-end gap-2">
|
||||||
|
<AlertDialogCancel as-child>
|
||||||
|
<Button variant="outline">取消</Button>
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction as-child>
|
||||||
|
<Button variant="destructive" @click="confirmDeleteRow">确认删除</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,7 @@ const enterProjectCalc = () => {
|
|||||||
tabStore.enterWorkspace({
|
tabStore.enterWorkspace({
|
||||||
id: PROJECT_TAB_ID,
|
id: PROJECT_TAB_ID,
|
||||||
title: '项目计算',
|
title: '项目计算',
|
||||||
componentName: 'XmView'
|
componentName: 'ProjectCalcView'
|
||||||
})
|
})
|
||||||
tabStore.hasCompletedSetup = true
|
tabStore.hasCompletedSetup = true
|
||||||
}
|
}
|
||||||
@ -109,12 +109,7 @@ const loadProjectDefaults = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openProjectCalc = async () => {
|
const openProjectCalc = async () => {
|
||||||
const savedInfo = await kvStore.getItem<ProjectInfoState>(PROJECT_INFO_KEY)
|
|
||||||
if (typeof savedInfo?.projectIndustry === 'string' && savedInfo.projectIndustry.trim()) {
|
|
||||||
enterProjectCalc()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await loadProjectDefaults()
|
|
||||||
projectDialogOpen.value = true
|
projectDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,10 +195,11 @@ const confirmQuickCalc = async () => {
|
|||||||
QUICK_MAJOR_FACTOR_KEY
|
QUICK_MAJOR_FACTOR_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
writeWorkspaceMode('quick')
|
||||||
tabStore.enterWorkspace({
|
tabStore.enterWorkspace({
|
||||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
id: `contract-${QUICK_CONTRACT_ID}`,
|
||||||
title: contractName,
|
title: contractName,
|
||||||
componentName: 'ContractDetailView',
|
componentName: 'QuickCalcView',
|
||||||
props: {
|
props: {
|
||||||
contractId: QUICK_CONTRACT_ID,
|
contractId: QUICK_CONTRACT_ID,
|
||||||
contractName,
|
contractName,
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onActivated, onMounted } from 'vue'
|
|
||||||
import XmCard from '@/components/xm/xmCard.vue'
|
|
||||||
import { writeWorkspaceMode } from '@/lib/workspace'
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
writeWorkspaceMode('project')
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
writeWorkspaceMode('project')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<XmCard />
|
|
||||||
</template>
|
|
||||||
@ -1,413 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, onActivated, onMounted, ref, watch } from 'vue'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useKvStore } from '@/pinia/kv'
|
|
||||||
import { useTabStore } from '@/pinia/tab'
|
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
|
||||||
import { ArrowRight, Calculator, PencilLine } from 'lucide-vue-next'
|
|
||||||
import { industryTypeList } from '@/sql'
|
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
|
||||||
import { roundTo } from '@/lib/decimal'
|
|
||||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
|
||||||
import {
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
QUICK_CONTRACT_ID,
|
|
||||||
QUICK_CONTRACT_META_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY,
|
|
||||||
QUICK_PROJECT_INFO_KEY,
|
|
||||||
QUICK_PROJECT_SCALE_KEY,
|
|
||||||
createDefaultQuickContractMeta,
|
|
||||||
writeWorkspaceMode
|
|
||||||
} from '@/lib/workspace'
|
|
||||||
|
|
||||||
interface QuickProjectInfoState {
|
|
||||||
projectIndustry?: string
|
|
||||||
projectName?: string
|
|
||||||
preparedBy?: string
|
|
||||||
reviewedBy?: string
|
|
||||||
preparedCompany?: string
|
|
||||||
preparedDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuickContractMetaState {
|
|
||||||
id?: string
|
|
||||||
name?: string
|
|
||||||
updatedAt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
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 kvStore = useKvStore()
|
|
||||||
const tabStore = useTabStore()
|
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
|
||||||
|
|
||||||
const contractName = ref(QUICK_CONTRACT_FALLBACK_NAME)
|
|
||||||
const projectIndustry = ref(String(industryTypeList[0]?.id || ''))
|
|
||||||
const quickBudget = ref<number | null>(null)
|
|
||||||
const savingIndustry = ref(false)
|
|
||||||
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const availableIndustries = computed(() =>
|
|
||||||
industryTypeList.map(item => ({
|
|
||||||
id: String(item.id),
|
|
||||||
name: item.name
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const formatBudgetAmount = (value: number | null | undefined) =>
|
|
||||||
typeof value === 'number' && Number.isFinite(value) ? `${formatThousands(value, 2)} 元` : '--'
|
|
||||||
|
|
||||||
const toFiniteNumber = (value: unknown): number | null => {
|
|
||||||
if (value == null || value === '') return null
|
|
||||||
const num = Number(value)
|
|
||||||
return Number.isFinite(num) ? num : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
|
|
||||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
|
||||||
if (rows.length === 0) return null
|
|
||||||
let hasValid = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of rows) {
|
|
||||||
const serviceBudget = toFiniteNumber(row?.serviceBudget)
|
|
||||||
if (serviceBudget != null) {
|
|
||||||
total += serviceBudget
|
|
||||||
hasValid = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const adopted = toFiniteNumber(row?.adoptedBudgetUnitPrice)
|
|
||||||
const personnel = toFiniteNumber(row?.personnelCount)
|
|
||||||
const workday = toFiniteNumber(row?.workdayCount)
|
|
||||||
if (adopted == null || personnel == null || workday == null) continue
|
|
||||||
total += adopted * personnel * workday
|
|
||||||
hasValid = true
|
|
||||||
}
|
|
||||||
return hasValid ? roundTo(total, 2) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => {
|
|
||||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
|
||||||
if (rows.length === 0) return null
|
|
||||||
const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
|
|
||||||
const subtotal = toFiniteNumber(subtotalRow?.budgetFee)
|
|
||||||
if (subtotal != null) return roundTo(subtotal, 2)
|
|
||||||
|
|
||||||
let hasValid = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of rows) {
|
|
||||||
if (String(row?.id || '') === 'fee-subtotal-fixed') continue
|
|
||||||
const budget = toFiniteNumber(row?.budgetFee)
|
|
||||||
if (budget != null) {
|
|
||||||
total += budget
|
|
||||||
hasValid = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const quantity = toFiniteNumber(row?.quantity)
|
|
||||||
const unitPrice = toFiniteNumber(row?.unitPrice)
|
|
||||||
if (quantity == null || unitPrice == null) continue
|
|
||||||
total += quantity * unitPrice
|
|
||||||
hasValid = true
|
|
||||||
}
|
|
||||||
return hasValid ? roundTo(total, 2) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHtMethodTotalByRow = async (mainStorageKey: string, rowId: string) => {
|
|
||||||
const [rateState, hourlyState, quantityState] = await Promise.all([
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<RateMethodStateLike>(mainStorageKey, rowId, 'rate-fee'),
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<HourlyMethodStateLike>(mainStorageKey, rowId, 'hourly-fee'),
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<QuantityMethodStateLike>(mainStorageKey, rowId, 'quantity-unit-price-fee')
|
|
||||||
])
|
|
||||||
const parts = [
|
|
||||||
toFiniteNumber(rateState?.budgetFee),
|
|
||||||
sumHourlyMethodFee(hourlyState),
|
|
||||||
sumQuantityMethodFee(quantityState)
|
|
||||||
]
|
|
||||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
if (validParts.length === 0) return null
|
|
||||||
return roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHtMainTotalFee = async (mainStorageKey: string) => {
|
|
||||||
const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(mainStorageKey)
|
|
||||||
const rows = Array.isArray(mainState?.detailRows) ? mainState.detailRows : []
|
|
||||||
const rowIds = rows.map(row => String(row?.id || '').trim()).filter(Boolean)
|
|
||||||
if (rowIds.length === 0) return null
|
|
||||||
const rowTotals = await Promise.all(rowIds.map(rowId => loadHtMethodTotalByRow(mainStorageKey, rowId)))
|
|
||||||
const validTotals = rowTotals.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
if (validTotals.length === 0) return null
|
|
||||||
return roundTo(validTotals.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshQuickBudget = async () => {
|
|
||||||
await zxFwPricingStore.loadContract(QUICK_CONTRACT_ID)
|
|
||||||
const serviceFee = zxFwPricingStore.getBaseSubtotal(QUICK_CONTRACT_ID)
|
|
||||||
const [additionalFee, reserveFee] = await Promise.all([
|
|
||||||
loadHtMainTotalFee(`htExtraFee-${QUICK_CONTRACT_ID}-additional-work`),
|
|
||||||
loadHtMainTotalFee(`htExtraFee-${QUICK_CONTRACT_ID}-reserve`)
|
|
||||||
])
|
|
||||||
const parts = [serviceFee, additionalFee, reserveFee]
|
|
||||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
quickBudget.value = validParts.length === 0 ? null : roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleRefreshQuickBudget = () => {
|
|
||||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
|
||||||
budgetRefreshTimer = setTimeout(() => {
|
|
||||||
void refreshQuickBudget()
|
|
||||||
}, 80)
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeQuickContractMeta = (value: QuickContractMetaState | null) => ({
|
|
||||||
id: typeof value?.id === 'string' && value.id.trim() ? value.id.trim() : QUICK_CONTRACT_ID,
|
|
||||||
name: typeof value?.name === 'string' && value.name.trim() ? value.name.trim() : QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
updatedAt:
|
|
||||||
typeof value?.updatedAt === 'string' && value.updatedAt.trim()
|
|
||||||
? value.updatedAt
|
|
||||||
: new Date().toISOString()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ensureQuickWorkspaceReady = async () => {
|
|
||||||
const [savedInfo, savedMeta] = await Promise.all([
|
|
||||||
kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY),
|
|
||||||
kvStore.getItem<QuickContractMetaState>(QUICK_CONTRACT_META_KEY)
|
|
||||||
])
|
|
||||||
|
|
||||||
const defaultIndustry = String(industryTypeList[0]?.id || '')
|
|
||||||
const nextIndustry =
|
|
||||||
typeof savedInfo?.projectIndustry === 'string' && savedInfo.projectIndustry.trim()
|
|
||||||
? savedInfo.projectIndustry.trim()
|
|
||||||
: defaultIndustry
|
|
||||||
|
|
||||||
projectIndustry.value = nextIndustry
|
|
||||||
contractName.value = normalizeQuickContractMeta(savedMeta).name
|
|
||||||
|
|
||||||
await kvStore.setItem(QUICK_PROJECT_INFO_KEY, {
|
|
||||||
projectIndustry: nextIndustry,
|
|
||||||
projectName: '快速计算'
|
|
||||||
})
|
|
||||||
|
|
||||||
const consultState = await kvStore.getItem(QUICK_CONSULT_CATEGORY_FACTOR_KEY)
|
|
||||||
const majorState = await kvStore.getItem(QUICK_MAJOR_FACTOR_KEY)
|
|
||||||
if (!consultState || !majorState) {
|
|
||||||
await initializeProjectFactorStates(
|
|
||||||
kvStore,
|
|
||||||
nextIndustry,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await kvStore.setItem(QUICK_CONTRACT_META_KEY, {
|
|
||||||
...createDefaultQuickContractMeta(),
|
|
||||||
...normalizeQuickContractMeta(savedMeta)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistQuickContractMeta = async () => {
|
|
||||||
await kvStore.setItem(QUICK_CONTRACT_META_KEY, {
|
|
||||||
id: QUICK_CONTRACT_ID,
|
|
||||||
name: contractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistQuickIndustry = async (industry: string) => {
|
|
||||||
if (!industry) return
|
|
||||||
savingIndustry.value = true
|
|
||||||
try {
|
|
||||||
const current = await kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY)
|
|
||||||
await kvStore.setItem(QUICK_PROJECT_INFO_KEY, {
|
|
||||||
...current,
|
|
||||||
projectIndustry: industry,
|
|
||||||
projectName: '快速计算'
|
|
||||||
})
|
|
||||||
await initializeProjectFactorStates(
|
|
||||||
kvStore,
|
|
||||||
industry,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
savingIndustry.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openQuickContract = () => {
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
tabStore.openTab({
|
|
||||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
|
||||||
title: `快速计算-${contractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME}`,
|
|
||||||
componentName: 'ContractDetailView',
|
|
||||||
props: {
|
|
||||||
contractId: QUICK_CONTRACT_ID,
|
|
||||||
contractName: contractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
projectInfoKey: QUICK_PROJECT_INFO_KEY,
|
|
||||||
projectScaleKey: QUICK_PROJECT_SCALE_KEY,
|
|
||||||
projectConsultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => contractName.value,
|
|
||||||
() => {
|
|
||||||
void persistQuickContractMeta()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => projectIndustry.value,
|
|
||||||
nextIndustry => {
|
|
||||||
if (!nextIndustry) return
|
|
||||||
void persistQuickIndustry(nextIndustry)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [
|
|
||||||
zxFwPricingStore.contractVersions[QUICK_CONTRACT_ID] || 0,
|
|
||||||
zxFwPricingStore.getKeyVersion(`htExtraFee-${QUICK_CONTRACT_ID}-additional-work`),
|
|
||||||
zxFwPricingStore.getKeyVersion(`htExtraFee-${QUICK_CONTRACT_ID}-reserve`),
|
|
||||||
Object.entries(zxFwPricingStore.keyVersions)
|
|
||||||
.filter(([key]) =>
|
|
||||||
key.startsWith(`htExtraFee-${QUICK_CONTRACT_ID}-additional-work-`) ||
|
|
||||||
key.startsWith(`htExtraFee-${QUICK_CONTRACT_ID}-reserve-`)
|
|
||||||
)
|
|
||||||
.map(([key, version]) => `${key}:${version}`)
|
|
||||||
.join('|')
|
|
||||||
],
|
|
||||||
scheduleRefreshQuickBudget
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
await ensureQuickWorkspaceReady()
|
|
||||||
await refreshQuickBudget()
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
scheduleRefreshQuickBudget()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mx-auto flex h-full w-full max-w-6xl flex-col gap-6">
|
|
||||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1.25fr)_minmax(320px,0.75fr)]">
|
|
||||||
<Card class="border-border/70">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center gap-2 text-2xl">
|
|
||||||
<Calculator class="h-5 w-5" />
|
|
||||||
快速计算
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
保留一个默认合同卡片,不再经过项目卡片入口,直接进入单合同预算费用计算。
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="grid gap-4 md:grid-cols-2">
|
|
||||||
<label class="space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">工程行业</span>
|
|
||||||
<select
|
|
||||||
v-model="projectIndustry"
|
|
||||||
class="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
>
|
|
||||||
<option v-for="item in availableIndustries" :key="item.id" :value="item.id">
|
|
||||||
{{ item.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">合同名称</span>
|
|
||||||
<div class="relative">
|
|
||||||
<PencilLine class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<input
|
|
||||||
v-model="contractName"
|
|
||||||
type="text"
|
|
||||||
maxlength="40"
|
|
||||||
class="h-11 w-full rounded-md border bg-background pl-9 pr-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
placeholder="请输入合同名称"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="border-border/70 bg-muted/25">
|
|
||||||
<CardHeader class="pb-3">
|
|
||||||
<CardTitle class="text-base">当前状态</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="grid gap-2 text-sm text-muted-foreground">
|
|
||||||
<div>模式:单合同快速计算</div>
|
|
||||||
<div>合同ID:{{ QUICK_CONTRACT_ID }}</div>
|
|
||||||
<div>行业切换会同步重建快速计算专用系数基线</div>
|
|
||||||
<div>{{ savingIndustry ? '正在切换行业并刷新系数...' : '行业与合同名称已自动保存' }}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
class="group cursor-pointer border-border/70 transition-colors hover:border-primary"
|
|
||||||
@click="openQuickContract"
|
|
||||||
>
|
|
||||||
<CardHeader class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<CardTitle class="text-xl">
|
|
||||||
{{ contractName.trim() || QUICK_CONTRACT_FALLBACK_NAME }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>默认单合同卡片,点击后进入预算费用计算详情。</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button class="shrink-0 md:self-center" @click.stop="openQuickContract">
|
|
||||||
进入计算
|
|
||||||
<ArrowRight class="ml-1 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="grid gap-4 border-t pt-5 text-sm text-muted-foreground md:grid-cols-3">
|
|
||||||
<div>
|
|
||||||
<div class="text-xs uppercase tracking-[0.24em] text-muted-foreground/80">合同ID</div>
|
|
||||||
<div class="mt-2 break-all text-foreground">{{ QUICK_CONTRACT_ID }}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-xs uppercase tracking-[0.24em] text-muted-foreground/80">预算费用</div>
|
|
||||||
<div class="mt-2 text-foreground">{{ formatBudgetAmount(quickBudget) }}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-xs uppercase tracking-[0.24em] text-muted-foreground/80">行业</div>
|
|
||||||
<div class="mt-2 text-foreground">
|
|
||||||
{{ availableIndustries.find(item => item.id === projectIndustry)?.name || '--' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -22,6 +22,7 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} 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 { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode } from '@/lib/workspace'
|
||||||
import { addNumbers, roundTo } from '@/lib/decimal'
|
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||||
import { exportFile, serviceList } from '@/sql'
|
import { exportFile, serviceList } from '@/sql'
|
||||||
|
|
||||||
@ -431,8 +432,8 @@ const userGuideSteps: UserGuideStep[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const componentMap: Record<string, any> = {
|
const componentMap: Record<string, any> = {
|
||||||
XmView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.vue'))),
|
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.vue'))),
|
||||||
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/ht/htCard.vue'))),
|
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/components/ht/htCard.vue'))),
|
||||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
||||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
||||||
}
|
}
|
||||||
@ -446,7 +447,7 @@ const kvStore = useKvStore()
|
|||||||
const tabContextOpen = ref(false)
|
const tabContextOpen = ref(false)
|
||||||
const tabContextX = ref(0)
|
const tabContextX = ref(0)
|
||||||
const tabContextY = ref(0)
|
const tabContextY = ref(0)
|
||||||
const contextTabId = ref<string>('XmView')
|
const contextTabId = ref<string>('ProjectCalcView')
|
||||||
const tabContextRef = ref<HTMLElement | null>(null)
|
const tabContextRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const dataMenuOpen = ref(false)
|
const dataMenuOpen = ref(false)
|
||||||
@ -478,7 +479,11 @@ const tabsModel = computed({
|
|||||||
|
|
||||||
const contextTabIndex = computed(() => tabStore.tabs.findIndex((t:any) => t.id === contextTabId.value))
|
const contextTabIndex = computed(() => tabStore.tabs.findIndex((t:any) => t.id === contextTabId.value))
|
||||||
|
|
||||||
const hasClosableTabs = computed(() => tabStore.tabs.some((t:any) => t.id !== 'XmView'))
|
const hasClosableTabs = computed(() => {
|
||||||
|
const fixedId = readWorkspaceMode() === 'quick' ? QUICK_TAB_ID : PROJECT_TAB_ID
|
||||||
|
|
||||||
|
return tabStore.tabs.some((t: any) => t.componentName !== fixedId)
|
||||||
|
})
|
||||||
const activeGuideStep = computed(
|
const activeGuideStep = computed(
|
||||||
() => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
|
() => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
|
||||||
)
|
)
|
||||||
@ -494,7 +499,7 @@ const canCloseRight = computed(() => {
|
|||||||
return tabStore.tabs.slice(contextTabIndex.value + 1).length > 0
|
return tabStore.tabs.slice(contextTabIndex.value + 1).length > 0
|
||||||
})
|
})
|
||||||
const canCloseOther = computed(() =>
|
const canCloseOther = computed(() =>
|
||||||
tabStore.tabs.length > 1 && contextTabIndex.value !== 0
|
tabStore.tabs.slice(1, contextTabIndex.value).length > 1 && contextTabIndex.value !== 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const closeMenus = () => {
|
const closeMenus = () => {
|
||||||
@ -525,9 +530,9 @@ const hasNonDefaultTabState = () => {
|
|||||||
if (!raw) return false
|
if (!raw) return false
|
||||||
const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
|
const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
|
||||||
const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
|
const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
|
||||||
const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'XmView')
|
const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'ProjectCalcView')
|
||||||
const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
|
const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
|
||||||
return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView')
|
return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'ProjectCalcView')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('parse tabs cache failed:', error)
|
console.error('parse tabs cache failed:', error)
|
||||||
return false
|
return false
|
||||||
@ -1827,6 +1832,7 @@ watch(
|
|||||||
导出
|
导出
|
||||||
</button>
|
</button>
|
||||||
<button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
<button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||||
|
v-if="readWorkspaceMode() !== 'quick'"
|
||||||
@click="exportReport">
|
@click="exportReport">
|
||||||
导出报表
|
导出报表
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export type WorkspaceMode = 'home' | 'project' | 'quick'
|
export type WorkspaceMode = 'project' | 'quick'
|
||||||
|
|
||||||
export const PROJECT_TAB_ID = 'ProjectCalcView'
|
export const PROJECT_TAB_ID = 'ProjectCalcView'
|
||||||
export const QUICK_TAB_ID = 'QuickCalcView'
|
export const QUICK_TAB_ID = 'QuickCalcView'
|
||||||
export const LEGACY_PROJECT_TAB_ID = 'XmView'
|
export const LEGACY_PROJECT_TAB_ID = 'ProjectCalcView'
|
||||||
export const FIXED_WORKSPACE_TAB_IDS = [PROJECT_TAB_ID, QUICK_TAB_ID] as const
|
export const FIXED_WORKSPACE_TAB_IDS = [PROJECT_TAB_ID, QUICK_TAB_ID] as const
|
||||||
|
|
||||||
export const WORKSPACE_MODE_STORAGE_KEY = 'jgjs-workspace-mode-v1'
|
export const WORKSPACE_MODE_STORAGE_KEY = 'jgjs-workspace-mode-v1'
|
||||||
@ -23,16 +23,13 @@ export interface QuickContractMeta {
|
|||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalizeWorkspaceMode = (value: unknown): WorkspaceMode => {
|
|
||||||
if (value === 'project' || value === 'quick' || value === 'home') return value
|
|
||||||
return 'home'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readWorkspaceMode = (): WorkspaceMode => {
|
export const readWorkspaceMode = (): WorkspaceMode => {
|
||||||
try {
|
try {
|
||||||
return normalizeWorkspaceMode(window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY))
|
return window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY) as WorkspaceMode
|
||||||
} catch {
|
} catch {
|
||||||
return 'home'
|
return 'project'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +42,7 @@ export const createDefaultQuickContractMeta = (): QuickContractMeta => ({
|
|||||||
})
|
})
|
||||||
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
|
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, normalizeWorkspaceMode(mode))
|
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, mode)
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略只读或隐私模式下的写入失败。
|
// 忽略只读或隐私模式下的写入失败。
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace'
|
import {
|
||||||
|
PROJECT_TAB_ID,
|
||||||
|
QUICK_CONTRACT_TAB_ID,
|
||||||
|
QUICK_TAB_ID,
|
||||||
|
readWorkspaceMode
|
||||||
|
} from '@/lib/workspace'
|
||||||
|
|
||||||
export interface TabItem<TProps = Record<string, unknown>> {
|
export interface TabItem<TProps = Record<string, unknown>> {
|
||||||
id: string
|
id: string
|
||||||
@ -9,32 +14,44 @@ export interface TabItem<TProps = Record<string, unknown>> {
|
|||||||
props?: TProps
|
props?: TProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TAB: TabItem = {
|
const DEFAULT_PROJECT_TAB: TabItem = {
|
||||||
id: PROJECT_TAB_ID,
|
id: PROJECT_TAB_ID,
|
||||||
title: '项目卡片',
|
title: '项目卡片',
|
||||||
componentName: 'XmView'
|
componentName: 'ProjectCalcView'
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDefaultTabs = (): TabItem[] => [{...DEFAULT_TAB}]
|
/** 根据当前 workspace mode 返回受保护的 tab ID 集合 */
|
||||||
const PROTECTED_TAB_ID_SET = new Set<string>([ PROJECT_TAB_ID,QUICK_CONTRACT_TAB_ID])
|
const getProtectedIds = (): Set<string> => {
|
||||||
|
return readWorkspaceMode() === 'quick'
|
||||||
|
? new Set([QUICK_TAB_ID, QUICK_CONTRACT_TAB_ID])
|
||||||
|
: new Set([PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据当前 workspace mode 返回首个 tab 的 fallback ID */
|
||||||
|
const getFallbackTabId = (): string => {
|
||||||
|
return readWorkspaceMode() === 'quick' ? QUICK_TAB_ID : PROJECT_TAB_ID
|
||||||
|
}
|
||||||
|
|
||||||
export const useTabStore = defineStore(
|
export const useTabStore = defineStore(
|
||||||
'tabs',
|
'tabs',
|
||||||
() => {
|
() => {
|
||||||
const tabs = ref<TabItem[]>(createDefaultTabs())
|
const tabs = ref<TabItem[]>([{ ...DEFAULT_PROJECT_TAB }])
|
||||||
const activeTabId = ref()
|
const activeTabId = ref()
|
||||||
const hasCompletedSetup = ref(false)
|
const hasCompletedSetup = ref(false)
|
||||||
|
|
||||||
const ensureHomeTab = () => {
|
const ensureHomeTab = () => {
|
||||||
if (tabs.value.some(tab => tab.id === PROJECT_TAB_ID)) return
|
const fallbackId = getFallbackTabId()
|
||||||
tabs.value = [...createDefaultTabs(), ...tabs.value]
|
if (tabs.value.some(tab => tab.id === fallbackId)) return
|
||||||
|
// quick 模式下不自动插入默认 tab,由 enterWorkspace 控制
|
||||||
|
if (readWorkspaceMode() === 'quick') return
|
||||||
|
tabs.value = [{ ...DEFAULT_PROJECT_TAB }, ...tabs.value]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureActiveValid = () => {
|
const ensureActiveValid = () => {
|
||||||
ensureHomeTab()
|
ensureHomeTab()
|
||||||
if (tabs.value.length === 0) tabs.value = createDefaultTabs()
|
if (tabs.value.length === 0) tabs.value = [{ ...DEFAULT_PROJECT_TAB }]
|
||||||
if (!tabs.value.some(tab => tab.id === activeTabId.value)) {
|
if (!tabs.value.some(tab => tab.id === activeTabId.value)) {
|
||||||
activeTabId.value = tabs.value[0]?.id ?? PROJECT_TAB_ID
|
activeTabId.value = tabs.value[0]?.id ?? getFallbackTabId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +68,7 @@ export const useTabStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeTab = (id: string) => {
|
const removeTab = (id: string) => {
|
||||||
if (PROTECTED_TAB_ID_SET.has(id)) return
|
if (getProtectedIds().has(id)) return
|
||||||
|
|
||||||
const index = tabs.value.findIndex(tab => tab.id === id)
|
const index = tabs.value.findIndex(tab => tab.id === id)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
@ -62,7 +79,7 @@ export const useTabStore = defineStore(
|
|||||||
|
|
||||||
if (wasActive) {
|
if (wasActive) {
|
||||||
const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
|
const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
|
||||||
activeTabId.value = tabs.value[fallbackIndex]?.id ?? PROJECT_TAB_ID
|
activeTabId.value = tabs.value[fallbackIndex]?.id ?? getFallbackTabId()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,33 +87,37 @@ export const useTabStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeAllTabs = () => {
|
const closeAllTabs = () => {
|
||||||
const protectedTabs = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id))
|
const protectedIds = getProtectedIds()
|
||||||
tabs.value = protectedTabs.length > 0 ? protectedTabs : createDefaultTabs()
|
const protectedTabs = tabs.value.filter(tab => protectedIds.has(tab.id))
|
||||||
activeTabId.value = tabs.value[0]?.id ?? PROJECT_TAB_ID
|
tabs.value = protectedTabs.length > 0 ? protectedTabs : [{ ...DEFAULT_PROJECT_TAB }]
|
||||||
|
activeTabId.value = tabs.value[0]?.id ?? getFallbackTabId()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeLeftTabs = (targetId: string) => {
|
const closeLeftTabs = (targetId: string) => {
|
||||||
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
||||||
if (targetIndex < 0) return
|
if (targetIndex < 0) return
|
||||||
tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index >= targetIndex)
|
const protectedIds = getProtectedIds()
|
||||||
|
tabs.value = tabs.value.filter((tab, index) => protectedIds.has(tab.id) || index >= targetIndex)
|
||||||
ensureActiveValid()
|
ensureActiveValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeRightTabs = (targetId: string) => {
|
const closeRightTabs = (targetId: string) => {
|
||||||
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
||||||
if (targetIndex < 0) return
|
if (targetIndex < 0) return
|
||||||
tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index <= targetIndex)
|
const protectedIds = getProtectedIds()
|
||||||
|
tabs.value = tabs.value.filter((tab, index) => protectedIds.has(tab.id) || index <= targetIndex)
|
||||||
ensureActiveValid()
|
ensureActiveValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeOtherTabs = (targetId: string) => {
|
const closeOtherTabs = (targetId: string) => {
|
||||||
tabs.value = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id) || tab.id === targetId)
|
const protectedIds = getProtectedIds()
|
||||||
|
tabs.value = tabs.value.filter(tab => protectedIds.has(tab.id) || tab.id === targetId)
|
||||||
ensureHomeTab()
|
ensureHomeTab()
|
||||||
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : PROJECT_TAB_ID
|
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : getFallbackTabId()
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetTabs = () => {
|
const resetTabs = () => {
|
||||||
tabs.value = createDefaultTabs()
|
tabs.value = [{ ...DEFAULT_PROJECT_TAB }]
|
||||||
activeTabId.value = PROJECT_TAB_ID
|
activeTabId.value = PROJECT_TAB_ID
|
||||||
hasCompletedSetup.value = false
|
hasCompletedSetup.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user