This commit is contained in:
wintsa 2026-03-09 17:35:34 +08:00
parent f79e8e0da6
commit 2a2c0fe2d7
11 changed files with 913 additions and 333 deletions

View File

@ -190,6 +190,7 @@ let data1 = {
name: 'test001', name: 'test001',
writer: '张三',// 编制人 writer: '张三',// 编制人
reviewer: '李四',// 复核人 reviewer: '李四',// 复核人
company: '测试公司',// 公司名称
date: '2021-09-24',// 编制日期 date: '2021-09-24',// 编制日期
industry: 0,// 0为公路工程1为铁路工程2为水运工程 industry: 0,// 0为公路工程1为铁路工程2为水运工程
fee: 10000, fee: 10000,
@ -284,8 +285,10 @@ let data1 = {
basicFee_basic: 200, basicFee_basic: 200,
basicFee_optional: 0, basicFee_optional: 0,
fee: 250000, fee: 250000,
proAmount: 3,
det: [ det: [
{ {
proNum: 1,
major: 0, major: 0,
cost: 100000, cost: 100000,
basicFee: 200, basicFee: 200,
@ -308,8 +311,10 @@ let data1 = {
basicFee_basic: 200, basicFee_basic: 200,
basicFee_optional: 0, basicFee_optional: 0,
fee: 250000, fee: 250000,
proAmount: 3,
det: [ det: [
{ {
proNum: 1,
major: 0, major: 0,
area: 1200, area: 1200,
basicFee: 200, basicFee: 200,
@ -375,14 +380,19 @@ let data1 = {
}, },
}, },
], ],
addtional: [// 附加工作费 addtional: {// 附加工作费
{ ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'C' }] },
type: 0,// 0为费率计取1为工时法2为数量单价 name: '附加工作',
coe: 0.03,
fee: 10000, fee: 10000,
}, det: [
{ {
type: 1,// 0为费率计取1为工时法2为数量单价 id: 0,
ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] },
name: '人员驻场服务及其他附加工作',
fee: 10000,
m4: { //工时
person_num: 10,
work_day: 3,
fee: 10000, fee: 10000,
det: [ det: [
{ {
@ -403,8 +413,7 @@ let data1 = {
}, },
], ],
}, },
{ m5: { //数量单价
type: 2,// 0为费率计取1为工时法2为数量单价
fee: 10000, fee: 10000,
det: [ det: [
{ {
@ -424,16 +433,20 @@ let data1 = {
remark: '',// 用户输入的说明 remark: '',// 用户输入的说明
}, },
], ],
} },
], },
reserve: [// 预留费
{ {
type: 0,// 0为费率计取1为工时法2为数量单价 id: 1,
ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }] },
name: '咨询服务协调工作',
fee: 10000,
m0: {
coe: 0.03, coe: 0.03,
fee: 10000, fee: 10000,
}, },
{ m4: {
type: 1,// 0为费率计取1为工时法2为数量单价 person_num: 10,
work_day: 3,
fee: 10000, fee: 10000,
det: [ det: [
{ {
@ -454,8 +467,62 @@ let data1 = {
}, },
], ],
}, },
m5: {
fee: 10000,
det: [
{ {
type: 2,// 0为费率计取1为工时法2为数量单价 name: '×××项',
unit: '项',
amount: 10,
price: 100000,
fee: 100000,
remark: '',// 用户输入的说明
},
{
name: '×××项',
unit: '项',
amount: 10,
price: 100000,
fee: 100000,
remark: '',// 用户输入的说明
},
],
},
},
]
},
reserve: {// 预备费
ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'Y' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'B' }] },
name: '预备费',
fee: 10000,
m0: {
coe: 0.03,
fee: 10000,
},
m4: {
person_num: 10,
work_day: 3,
fee: 10000,
det: [
{
expert: 0,
price: 100000,
person_num: 10,
work_day: 3,
fee: 100000,
remark: '',// 用户输入的说明
},
{
expert: 1,
price: 100000,
person_num: 10,
work_day: 3,
fee: 100000,
remark: '',// 用户输入的说明
},
],
},
m5: {
fee: 10000, fee: 10000,
det: [ det: [
{ {
@ -476,10 +543,11 @@ let data1 = {
}, },
], ],
} }
], },
}, },
], ],
}; };
let data2 = { let data2 = {
name: 'test001', name: 'test001',
scale: [ scale: [

View File

@ -0,0 +1,460 @@
<script setup lang="ts">
import { computed, defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { CellValueChangedEvent, ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
import localforage from 'localforage'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { Pencil, Eraser } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { useTabStore } from '@/pinia/tab'
interface FeeMethodRow {
id: string
name: string
rateFee: number | null
hourlyFee: number | null
quantityUnitPriceFee: number | null
subtotal?: number | null
actions?: unknown
}
interface FeeMethodState {
detailRows: FeeMethodRow[]
}
interface LegacyFeeRow {
id?: string
feeItem?: string
budgetFee?: number | null
quantity?: number | null
unitPrice?: number | null
}
const props = defineProps<{
title: string
storageKey: string
readonly?: boolean
fixedNames?: string[]
}>()
const tabStore = useTabStore()
const createRowId = () => `fee-method-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
const createDefaultRow = (name = ''): FeeMethodRow => ({
id: createRowId(),
name,
rateFee: null,
hourlyFee: null,
quantityUnitPriceFee: null
})
const SUMMARY_ROW_ID = 'fee-method-summary'
const isSummaryRow = (row: FeeMethodRow | null | undefined) => row?.id === SUMMARY_ROW_ID
const toFinite = (value: number | null | undefined) =>
typeof value === 'number' && Number.isFinite(value) ? value : 0
const round3 = (value: number) => Number(value.toFixed(3))
const getRowSubtotal = (row: FeeMethodRow | null | undefined) =>
row ? round3(toFinite(row.rateFee) + toFinite(row.hourlyFee) + toFinite(row.quantityUnitPriceFee)) : null
const isReadonly = computed(() => props.readonly === true)
const fixedNames = computed(() =>
Array.isArray(props.fixedNames)
? props.fixedNames.map(item => String(item || '').trim()).filter(Boolean)
: []
)
const hasFixedNames = computed(() => fixedNames.value.length > 0)
const detailRows = ref<FeeMethodRow[]>([createDefaultRow()])
const summaryRow = computed<FeeMethodRow>(() => {
const totals = detailRows.value.reduce(
(acc, row) => {
acc.rateFee += toFinite(row.rateFee)
acc.hourlyFee += toFinite(row.hourlyFee)
acc.quantityUnitPriceFee += toFinite(row.quantityUnitPriceFee)
return acc
},
{
rateFee: 0,
hourlyFee: 0,
quantityUnitPriceFee: 0
}
)
const result: FeeMethodRow = {
id: SUMMARY_ROW_ID,
name: '小计',
rateFee: round3(totals.rateFee),
hourlyFee: round3(totals.hourlyFee),
quantityUnitPriceFee: round3(totals.quantityUnitPriceFee)
}
result.subtotal = getRowSubtotal(result)
return result
})
const displayRows = computed<FeeMethodRow[]>(() => [...detailRows.value, summaryRow.value])
const gridApi = ref<GridApi<FeeMethodRow> | null>(null)
const formatEditableText = (params: any) => {
if (params.value == null || params.value === '') {
if (params.context?.readonly === true || isSummaryRow(params.data)) return ''
return '点击输入'
}
return String(params.value)
}
const formatEditableNumber = (params: any) => {
if (params.value == null || params.value === '') {
if (params.context?.readonly === true || isSummaryRow(params.data)) return ''
return '点击输入'
}
return formatThousandsFlexible(params.value, 3)
}
const numericParser = (newValue: any): number | null =>
parseNumberOrNull(newValue, { precision: 3 })
const toLegacyQuantityUnitPriceFee = (row: LegacyFeeRow) => {
if (typeof row.budgetFee === 'number' && Number.isFinite(row.budgetFee)) return row.budgetFee
if (
typeof row.quantity === 'number' &&
Number.isFinite(row.quantity) &&
typeof row.unitPrice === 'number' &&
Number.isFinite(row.unitPrice)
) {
return Number((row.quantity * row.unitPrice).toFixed(2))
}
return null
}
const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => {
const sourceRows = (Array.isArray(rowsFromDb) ? rowsFromDb : []).filter(
item => (item as Partial<FeeMethodRow>)?.id !== SUMMARY_ROW_ID
)
const rows = sourceRows.map(item => {
const row = item as Partial<FeeMethodRow> & LegacyFeeRow
return {
id: typeof row.id === 'string' && row.id ? row.id : createRowId(),
name:
typeof row.name === 'string'
? row.name
: (typeof row.feeItem === 'string' ? row.feeItem : ''),
rateFee: typeof row.rateFee === 'number' ? row.rateFee : null,
hourlyFee: typeof row.hourlyFee === 'number' ? row.hourlyFee : null,
quantityUnitPriceFee:
typeof row.quantityUnitPriceFee === 'number'
? row.quantityUnitPriceFee
: toLegacyQuantityUnitPriceFee(row)
} as FeeMethodRow
})
if (hasFixedNames.value) {
const byName = new Map(rows.map(row => [row.name, row]))
return fixedNames.value.map((name, index) => {
const fromDb = byName.get(name)
return {
id: fromDb?.id || `fee-method-fixed-${index}`,
name,
rateFee: fromDb?.rateFee ?? null,
hourlyFee: fromDb?.hourlyFee ?? null,
quantityUnitPriceFee: fromDb?.quantityUnitPriceFee ?? null
}
})
}
return rows.length > 0 ? rows : [createDefaultRow()]
}
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
const saveToIndexedDB = async () => {
try {
const payload: FeeMethodState = {
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
}
await localforage.setItem(props.storageKey, payload)
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
}
const loadFromIndexedDB = async () => {
try {
const data = await localforage.getItem<FeeMethodState>(props.storageKey)
detailRows.value = mergeWithStoredRows(data?.detailRows)
} catch (error) {
console.error('loadFromIndexedDB failed:', error)
detailRows.value = mergeWithStoredRows([])
}
}
const addRow = () => {
if (isReadonly.value) return
detailRows.value = [...detailRows.value, createDefaultRow()]
void saveToIndexedDB()
}
const clearRow = (id: string) => {
if (isReadonly.value) return
detailRows.value = detailRows.value.map(row =>
row.id !== id
? row
: {
...row,
rateFee: null,
hourlyFee: null,
quantityUnitPriceFee: null
}
)
void saveToIndexedDB()
}
const editRow = (id: string) => {
if (isReadonly.value) return
const row = detailRows.value.find(item => item.id === id)
if (!row) return
tabStore.openTab({
id: `ht-fee-edit-${props.storageKey}-${id}`,
title: `费用编辑-${row.name || '未命名'}`,
componentName: 'HtFeeMethodTypeLineView',
props: {
sourceTitle: props.title,
storageKey: props.storageKey,
rowId: id,
rowName: row.name || ''
}
})
}
const ActionCellRenderer = defineComponent({
name: 'HtFeeMethodActionCellRenderer',
props: {
params: {
type: Object as PropType<ICellRendererParams<FeeMethodRow>>,
required: true
}
},
setup(props) {
return () => {
if (isSummaryRow(props.params.data as FeeMethodRow | undefined)) return null
const disabled = props.params.context?.readonly === true
return h('div', { class: 'zxfw-action-wrap' }, [
h('div', { class: 'zxfw-action-group' }, [
h('button', {
class: ['zxfw-action-btn', disabled ? 'zxfw-action-btn--disabled' : ''],
'data-action': 'edit',
type: 'button',
disabled
}, [
h(Pencil, { size: 13, 'aria-hidden': 'true' }),
h('span', '编辑')
]),
h('button', {
class: ['zxfw-action-btn', disabled ? 'zxfw-action-btn--disabled' : ''],
'data-action': 'clear',
type: 'button',
disabled
}, [
h(Eraser, { size: 13, 'aria-hidden': 'true' }),
h('span', '恢复默认')
])
])
])
}
}
})
const columnDefs: ColDef<FeeMethodRow>[] = [
{
headerName: '名字',
field: 'name',
minWidth: 180,
flex: 1.8,
editable: params =>
!(params.context?.readonly === true || params.context?.fixedNames === true || isSummaryRow(params.data)),
valueFormatter: formatEditableText,
cellClass: params =>
params.context?.readonly === true || params.context?.fixedNames === true || isSummaryRow(params.data)
? ''
: 'editable-cell-line',
cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === ''
}
},
{
headerName: '费率计取',
field: 'rateFee',
minWidth: 130,
flex: 1.2,
editable: params => params.context?.readonly !== true && !isSummaryRow(params.data),
headerClass: 'ag-right-aligned-header',
cellClass: params =>
params.context?.readonly === true
? 'ag-right-aligned-cell'
: 'ag-right-aligned-cell editable-cell-line',
valueParser: params => numericParser(params.newValue),
valueFormatter: formatEditableNumber,
cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === ''
}
},
{
headerName: '工时法',
field: 'hourlyFee',
minWidth: 130,
flex: 1.2,
editable: params => params.context?.readonly !== true && !isSummaryRow(params.data),
headerClass: 'ag-right-aligned-header',
cellClass: params =>
params.context?.readonly === true
? 'ag-right-aligned-cell'
: 'ag-right-aligned-cell editable-cell-line',
valueParser: params => numericParser(params.newValue),
valueFormatter: formatEditableNumber,
cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === ''
}
},
{
headerName: '数量单价',
field: 'quantityUnitPriceFee',
minWidth: 130,
flex: 1.2,
editable: params => params.context?.readonly !== true && !isSummaryRow(params.data),
headerClass: 'ag-right-aligned-header',
cellClass: params =>
params.context?.readonly === true
? 'ag-right-aligned-cell'
: 'ag-right-aligned-cell editable-cell-line',
valueParser: params => numericParser(params.newValue),
valueFormatter: formatEditableNumber,
cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === ''
}
},
{
headerName: '小计',
field: 'subtotal',
minWidth: 140,
flex: 1.2,
editable: false,
headerClass: 'ag-right-aligned-header',
cellClass: 'ag-right-aligned-cell',
valueGetter: params => getRowSubtotal(params.data),
valueFormatter: formatEditableNumber
},
{
headerName: '操作',
field: 'actions',
minWidth: 220,
flex: 1.6,
maxWidth: 260,
editable: false,
sortable: false,
filter: false,
suppressMovable: true,
cellRenderer: ActionCellRenderer
}
]
const detailGridOptions: GridOptions<FeeMethodRow> = {
...gridOptions,
treeData: false,
getDataPath: undefined,
context: {
readonly: isReadonly.value,
fixedNames: hasFixedNames.value
},
onCellClicked: params => {
if (params.colDef.field !== 'actions' || !params.data || isSummaryRow(params.data)) return
if (params.context?.readonly === true) return
const target = params.event?.target as HTMLElement | null
const btn = target?.closest('button[data-action]') as HTMLButtonElement | null
const action = btn?.dataset.action
if (action === 'edit') {
editRow(params.data.id)
return
}
if (action === 'clear') {
clearRow(params.data.id)
return
}
}
}
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
const handleGridReady = (event: GridReadyEvent<FeeMethodRow>) => {
gridApi.value = event.api
}
const handleCellValueChanged = (event: CellValueChangedEvent<FeeMethodRow>) => {
if (isReadonly.value) return
if (isSummaryRow(event.data)) return
if (gridPersistTimer) clearTimeout(gridPersistTimer)
gridPersistTimer = setTimeout(() => {
void saveToIndexedDB()
}, 300)
}
onMounted(async () => {
await loadFromIndexedDB()
})
onActivated(() => {
void loadFromIndexedDB()
})
const storageKeyRef = computed(() => props.storageKey)
watch(storageKeyRef, () => {
void loadFromIndexedDB()
})
watch([isReadonly, hasFixedNames], () => {
if (!detailGridOptions.context) return
detailGridOptions.context.readonly = isReadonly.value
detailGridOptions.context.fixedNames = hasFixedNames.value
gridApi.value?.refreshCells({ force: true })
})
onBeforeUnmount(() => {
if (gridPersistTimer) clearTimeout(gridPersistTimer)
gridApi.value = null
void saveToIndexedDB()
})
</script>
<template>
<div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">{{ title }}</h3>
<Button v-if="!isReadonly" type="button" variant="outline" size="sm" @click="addRow">新增</Button>
</div>
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue
:style="{ height: '100%' }"
:rowData="displayRows"
:columnDefs="columnDefs"
:gridOptions="detailGridOptions"
:theme="myTheme"
:treeData="false"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:headerHeight="50"
:suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true"
:cellSelection="{ handle: { mode: 'range' } }"
:enableClipboard="true"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
@grid-ready="handleGridReady"
@cell-value-changed="handleCellValueChanged"
/>
</div>
</div>
</div>
</template>
<style scoped>
.zxfw-action-btn--disabled {
opacity: 0.45;
cursor: not-allowed;
}
</style>

View File

@ -23,7 +23,7 @@ const delegatedProps = reactiveOmit(props, "class")
> >
<ScrollAreaViewport <ScrollAreaViewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 scrollArea-full"
> >
<slot /> <slot />
</ScrollAreaViewport> </ScrollAreaViewport>
@ -31,3 +31,9 @@ const delegatedProps = reactiveOmit(props, "class")
<ScrollAreaCorner /> <ScrollAreaCorner />
</ScrollAreaRoot> </ScrollAreaRoot>
</template> </template>
<style scoped>
:deep(.scrollArea-full > *) {
height: 100%;
}
</style>

View File

@ -1,14 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import HtFeeGrid from '@/components/common/HtFeeGrid.vue' import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
import { additionalWorkList } from '@/sql'
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
}>() }>()
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-additional-work`) const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-additional-work`)
const additionalWorkNames = computed(() => additionalWorkList.map(item => String(item)))
</script> </script>
<template> <template>
<HtFeeGrid title="附加工作费" :storageKey="STORAGE_KEY" /> <HtFeeMethodGrid
title="附加工作费"
:storageKey="STORAGE_KEY"
:readonly="true"
:fixed-names="additionalWorkNames"
/>
</template> </template>

View File

@ -1,10 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
import { serviceList } from '@/sql' import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue' import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
}>() }>()
interface XmBaseInfoState {
projectIndustry?: string
}
type ServiceItem = {
code: string
name: string
defCoe: number | null
desc?: string | null
notshowByzxflxs?: boolean
}
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const projectIndustry = ref('')
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {
console.error('loadProjectIndustry failed:', error)
projectIndustry.value = ''
}
}
const filteredServiceDict = computed<Record<string, ServiceItem>>(() => {
const industry = projectIndustry.value
if (!industry) return {}
const entries = getServiceDictEntries()
.filter(({ item }) => isIndustryEnabledByType(item, getIndustryTypeValue(industry)))
.map(({ id, item }) => [id, item as ServiceItem] as const)
return Object.fromEntries(entries)
})
onMounted(() => {
void loadProjectIndustry()
})
onActivated(() => {
void loadProjectIndustry()
})
</script> </script>
<template> <template>
@ -12,7 +57,7 @@ const props = defineProps<{
title="咨询分类系数明细" title="咨询分类系数明细"
:storage-key="`ht-consult-category-factor-v1-${props.contractId}`" :storage-key="`ht-consult-category-factor-v1-${props.contractId}`"
parent-storage-key="xm-consult-category-factor-v1" parent-storage-key="xm-consult-category-factor-v1"
:dict="serviceList" :dict="filteredServiceDict"
:disable-budget-edit-when-standard-null="true" :disable-budget-edit-when-standard-null="true"
:exclude-notshow-by-zxflxs="true" :exclude-notshow-by-zxflxs="true"
/> />

View File

@ -0,0 +1,55 @@
<template>
<TypeLine
scene="ht-fee-method-type-line"
:title="`${sourceTitleText}${rowNameText}`"
:subtitle="`明细ID${props.rowId}`"
:copy-text="props.rowId"
:storage-key="activeTypeStorageKey"
default-category="rate-fee"
:categories="categories"
/>
</template>
<script setup lang="ts">
import { computed, defineComponent, h, markRaw, type Component } from 'vue'
import TypeLine from '@/layout/typeLine.vue'
interface TypeLineCategoryItem {
key: string
label: string
component: Component
}
const props = defineProps<{
sourceTitle?: string
storageKey: string
rowId: string
rowName?: string
}>()
const sourceTitleText = computed(() => props.sourceTitle || '费用明细')
const rowNameText = computed(() => props.rowName || '未命名')
const activeTypeStorageKey = computed(() => `ht-fee-type-active-cat-${props.storageKey}-${props.rowId}`)
const createMethodPane = (methodLabel: string, key: string) =>
markRaw(
defineComponent({
name: `HtFeeMethodTypePane-${key}`,
setup() {
return () =>
h('div', { class: 'h-full min-h-0 flex flex-col' }, [
h('div', { class: 'rounded-lg border bg-card p-4' }, [
h('h3', { class: 'text-sm font-semibold text-foreground' }, methodLabel),
h('p', { class: 'mt-2 text-xs text-muted-foreground' }, `当前编辑类型:${methodLabel}`)
])
])
}
})
)
const categories: TypeLineCategoryItem[] = [
{ key: 'rate-fee', label: '费率计取', component: createMethodPane('费率计取', 'rate-fee') },
{ key: 'hourly-fee', label: '工时法', component: createMethodPane('工时法', 'hourly-fee') },
{ key: 'quantity-unit-price-fee', label: '数量单价', component: createMethodPane('数量单价', 'quantity-unit-price-fee') }
]
</script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import HtFeeGrid from '@/components/common/HtFeeGrid.vue' import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
@ -10,5 +10,5 @@ const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-reserve`)
</script> </script>
<template> <template>
<HtFeeGrid title="预备费" :storageKey="STORAGE_KEY" /> <HtFeeMethodGrid title="预备费" :storageKey="STORAGE_KEY" />
</template> </template>

View File

@ -139,7 +139,7 @@ const serviceDict = computed<ServiceItem[]>(() => {
const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item]))) const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id]))) const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|')) const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: 'C', name: '合同预算' } const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: '', name: '小计' }
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])

View File

@ -344,6 +344,7 @@ const componentMap: Record<string, any> = {
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/xmCard.vue'))), XmView: markRaw(defineAsyncComponent(() => import('@/components/views/xmCard.vue'))),
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/htCard.vue'))), ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/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'))),
} }
const tabStore = useTabStore() const tabStore = useTabStore()
@ -1350,149 +1351,100 @@ watch(
<template> <template>
<TooltipProvider> <TooltipProvider>
<div class="flex flex-col w-full h-screen bg-background overflow-hidden"> <div class="flex flex-col w-full h-screen bg-background overflow-hidden">
<div class="flex items-start gap-2 border-b bg-muted/30 px-2 pt-1 min-h-14 flex-none"> <div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
<div <div class="flex min-w-0 items-start gap-1 h-full" @mouseenter="isTabStripHover = true"
class="flex min-w-0 flex-1 items-start gap-1 h-full self-start" @mouseleave="isTabStripHover = false">
@mouseenter="isTabStripHover = true" <button type="button" :class="[
@mouseleave="isTabStripHover = false" 'h-9 w-8 self-center shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
>
<button
type="button"
:class="[
'h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
isTabStripHover && showTabScrollLeft ? 'opacity-100' : 'pointer-events-none opacity-0' isTabStripHover && showTabScrollLeft ? 'opacity-100' : 'pointer-events-none opacity-0'
]" ]" @click="scrollTabStripBy(-260)">
@click="scrollTabStripBy(-260)"
>
&lt; &lt;
</button> </button>
<ScrollArea :ref="setTabScrollAreaRef" type="auto" class="tab-strip-scroll-area min-w-0 flex-1 whitespace-nowrap"> <ScrollArea :ref="setTabScrollAreaRef" type="auto"
<draggable class="h-full tab-strip-scroll-area min-w-0 flex-1 whitespace-nowrap">
v-model="tabsModel" <draggable v-model="tabsModel" item-key="id" tag="div"
item-key="id" :class="['tab-strip-sortable h-[calc(3.49rem)] flex w-max gap-0', isTabDragging ? 'is-dragging' : '']"
tag="div" :animation="260" easing="cubic-bezier(0.22, 1, 0.36, 1)" ghost-class="tab-drag-ghost"
:class="['tab-strip-sortable flex w-max gap-0', isTabDragging ? 'is-dragging' : '']" chosen-class="tab-drag-chosen" drag-class="tab-drag-active" :move="canMoveTab" @start="handleTabDragStart"
:animation="260" @end="handleTabDragEnd">
easing="cubic-bezier(0.22, 1, 0.36, 1)"
ghost-class="tab-drag-ghost"
chosen-class="tab-drag-chosen"
drag-class="tab-drag-active"
:move="canMoveTab"
@start="handleTabDragStart"
@end="handleTabDragEnd"
>
<template #item="{ element: tab }"> <template #item="{ element: tab }">
<div <div :ref="el => setTabItemRef(tab.id, el)" @mousedown.left="tabStore.activeTabId = tab.id"
:ref="el => setTabItemRef(tab.id, el)" @contextmenu.prevent="openTabContextMenu($event, tab.id)" :class="[
@mousedown.left="tabStore.activeTabId = tab.id" 'tab-item group relative -mb-px -ml-px first:ml-0 flex items-center h-full px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border border-transparent transition-[background-color,border-color,color,box-shadow,transform] duration-200 text-sm',
@contextmenu.prevent="openTabContextMenu($event, tab.id)"
:class="[
'tab-item group relative -mb-px -ml-px first:ml-0 flex items-center h-10 px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border border-transparent transition-[background-color,border-color,color,box-shadow,transform] duration-200 text-sm',
tabStore.activeTabId === tab.id && !isTabDragging tabStore.activeTabId === tab.id && !isTabDragging
? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium' ? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
: 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70', : 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
tab.id !== 'XmView' ? 'cursor-move' : '' tab.id !== 'XmView' ? 'cursor-move' : ''
]" ]">
>
<TooltipRoot> <TooltipRoot>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<span <span :ref="el => setTabTitleRef(tab.id, el)" class="truncate mr-2">
:ref="el => setTabTitleRef(tab.id, el)"
class="truncate mr-2"
>
{{ tab.title }} {{ tab.title }}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent> <TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
</TooltipRoot> </TooltipRoot>
<Button <Button v-if="tab.id !== 'XmView'" variant="ghost" size="icon"
v-if="tab.id !== 'XmView'"
variant="ghost"
size="icon"
class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity" class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
@click.stop="tabStore.removeTab(tab.id)" @click.stop="tabStore.removeTab(tab.id)">
>
<X class="h-3 w-3" /> <X class="h-3 w-3" />
</Button> </Button>
</div> </div>
</template> </template>
</draggable> </draggable>
</ScrollArea> </ScrollArea>
<button <button type="button" :class="[
type="button" ' self-center h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted ',
:class="[
'h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
isTabStripHover && showTabScrollRight ? 'opacity-100' : 'pointer-events-none opacity-0' isTabStripHover && showTabScrollRight ? 'opacity-100' : 'pointer-events-none opacity-0'
]" ]" @click="scrollTabStripBy(260)">
@click="scrollTabStripBy(260)"
>
&gt; &gt;
</button> </button>
</div> </div>
<div class="flex shrink-0 self-start items-start gap-1 mb-1"> <div class="flex shrink-0 self-center items-center gap-1">
<div ref="dataMenuRef" class="relative shrink-0"> <div ref="dataMenuRef" class="relative shrink-0">
<Button <Button variant="outline" size="sm"
variant="outline"
size="sm"
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer" class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
@click="dataMenuOpen = !dataMenuOpen" @click="dataMenuOpen = !dataMenuOpen">
>
<ChevronDown class="h-4 w-4 mr-1" /> <ChevronDown class="h-4 w-4 mr-1" />
导入/导出 导入/导出
</Button> </Button>
<div <div v-if="dataMenuOpen"
v-if="dataMenuOpen" class="absolute right-0 top-full mt-1 z-50 rounded-md border bg-background p-1 shadow-md">
class="absolute right-0 top-full mt-1 z-50 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="triggerImport">
<button
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
@click="triggerImport"
>
导入 导入
</button> </button>
<button <button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted" @click="exportData">
@click="exportData"
>
导出 导出
</button> </button>
<button <button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted" @click="exportReport">
@click="exportReport"
>
导出报表 导出报表
</button> </button>
</div> </div>
<input <input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
ref="importFileRef"
type="file"
accept=".zw"
class="hidden"
@change="importData"
/>
</div> </div>
<Button <Button variant="outline" size="sm" class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
variant="outline" @click="openUserGuide(0)">
size="sm"
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
@click="openUserGuide(0)"
>
<CircleHelp class="h-4 w-4 mr-1" /> <CircleHelp class="h-4 w-4 mr-1" />
使用引导 使用引导
</Button> </Button>
<AlertDialogRoot> <AlertDialogRoot>
<AlertDialogTrigger as-child> <AlertDialogTrigger as-child>
<Button variant="destructive" size="sm" class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"> <Button variant="destructive" size="sm"
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
<RotateCcw class="h-4 w-4 mr-1" /> <RotateCcw class="h-4 w-4 mr-1" />
重置 重置
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" /> <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl"> <AlertDialogContent
class="fixed left-1/2 top-1/2 z-50 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">确认重置</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground"> <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
将清空所有项目数据并恢复默认页面确认继续吗 将清空所有项目数据并恢复默认页面确认继续吗
@ -1512,7 +1464,8 @@ watch(
<AlertDialogRoot :open="importConfirmOpen" @update:open="importConfirmOpen = $event"> <AlertDialogRoot :open="importConfirmOpen" @update:open="importConfirmOpen = $event">
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" /> <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl"> <AlertDialogContent
class="fixed left-1/2 top-1/2 z-50 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">确认导入覆盖</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground"> <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
将使用{{ pendingImportFileName || '所选文件' }}覆盖当前本地全部数据是否继续 将使用{{ pendingImportFileName || '所选文件' }}覆盖当前本地全部数据是否继续
@ -1533,58 +1486,39 @@ watch(
</div> </div>
<div class="flex-1 overflow-auto relative"> <div class="flex-1 overflow-auto relative">
<div <div v-for="tab in tabStore.tabs" :key="tab.id" :ref="el => setTabPanelRef(tab.id, el)"
v-for="tab in tabStore.tabs" v-show="tabStore.activeTabId === tab.id" class="h-full w-full p-4 animate-in fade-in duration-300">
:key="tab.id"
:ref="el => setTabPanelRef(tab.id, el)"
v-show="tabStore.activeTabId === tab.id"
class="h-full w-full p-4 animate-in fade-in duration-300"
>
<component :is="componentMap[tab.componentName]" v-bind="tab.props || {}" /> <component :is="componentMap[tab.componentName]" v-bind="tab.props || {}" />
</div> </div>
</div> </div>
<div <div v-if="tabContextOpen" ref="tabContextRef"
v-if="tabContextOpen"
ref="tabContextRef"
class="fixed z-[70] w-max rounded-lg border border-border/80 bg-background/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur-sm" class="fixed z-[70] w-max rounded-lg border border-border/80 bg-background/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur-sm"
:style="{ left: `${tabContextX}px`, top: `${tabContextY}px` }" :style="{ left: `${tabContextX}px`, top: `${tabContextY}px` }">
>
<button <button
class="flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50" class="flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="!hasClosableTabs" :disabled="!hasClosableTabs" @click="runTabMenuAction('all')">
@click="runTabMenuAction('all')"
>
删除所有 删除所有
</button> </button>
<button <button
class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50" class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="!canCloseLeft" :disabled="!canCloseLeft" @click="runTabMenuAction('left')">
@click="runTabMenuAction('left')"
>
删除左侧 删除左侧
</button> </button>
<button <button
class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50" class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="!canCloseRight" :disabled="!canCloseRight" @click="runTabMenuAction('right')">
@click="runTabMenuAction('right')"
>
删除右侧 删除右侧
</button> </button>
<button <button
class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50" class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="!canCloseOther" :disabled="!canCloseOther" @click="runTabMenuAction('other')">
@click="runTabMenuAction('other')"
>
删除其他 删除其他
</button> </button>
</div> </div>
<div <div v-if="userGuideOpen" class="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 p-4"
v-if="userGuideOpen" @click.self="closeUserGuide(false)">
class="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 p-4"
@click.self="closeUserGuide(false)"
>
<div class="w-full max-w-3xl rounded-xl border bg-background shadow-2xl"> <div class="w-full max-w-3xl rounded-xl border bg-background shadow-2xl">
<div class="flex items-start justify-between border-b px-6 py-5"> <div class="flex items-start justify-between border-b px-6 py-5">
<div> <div>
@ -1607,14 +1541,10 @@ watch(
<div class="flex flex-col gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<button <button v-for="(_step, index) in userGuideSteps" :key="`guide-dot-${index}`"
v-for="(_step, index) in userGuideSteps"
:key="`guide-dot-${index}`"
class="h-2.5 w-2.5 cursor-pointer rounded-full transition-colors" class="h-2.5 w-2.5 cursor-pointer rounded-full transition-colors"
:class="index === userGuideStepIndex ? 'bg-primary' : 'bg-muted'" :class="index === userGuideStepIndex ? 'bg-primary' : 'bg-muted'" :aria-label="`跳转到第 ${index + 1} 步`"
:aria-label="`跳转到第 ${index + 1} 步`" @click="jumpToGuideStep(index)" />
@click="jumpToGuideStep(index)"
/>
</div> </div>
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<Button variant="ghost" @click="closeUserGuide(false)">稍后再看</Button> <Button variant="ghost" @click="closeUserGuide(false)">稍后再看</Button>
@ -1629,11 +1559,11 @@ watch(
</template> </template>
<style scoped> <style scoped>
.tab-strip-sortable > .tab-item { .tab-strip-sortable>.tab-item {
transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1); transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
} }
.tab-strip-sortable.is-dragging > .tab-item { .tab-strip-sortable.is-dragging>.tab-item {
will-change: transform; will-change: transform;
} }
@ -1652,9 +1582,15 @@ watch(
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) { .tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) {
scrollbar-width: none; scrollbar-width: none;
overflow-y: hidden !important;
} }
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]::-webkit-scrollbar) { .tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]::-webkit-scrollbar) {
display: none; display: none;
} }
.tab-strip-scroll-area :deep([data-slot="scroll-area-scrollbar"][data-orientation="vertical"]),
.tab-strip-scroll-area :deep([data-slot="scroll-area-corner"]) {
display: none !important;
}
</style> </style>

View File

@ -151,6 +151,9 @@ export const expertList = {
7: { code: 'C9-3-2', name: '一级造价工程师且具备高级工程师资格', maxPrice: 2000, minPrice: 1800, defPrice: 1900, manageCoe: 2.05 }, 7: { code: 'C9-3-2', name: '一级造价工程师且具备高级工程师资格', maxPrice: 2000, minPrice: 1800, defPrice: 1900, manageCoe: 2.05 },
}; };
export const additionalWorkList
=['人员驻场服务及其他附加工作','咨询服务协调工作']
let costScaleCal = [ let costScaleCal = [
{ code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } }, { code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
{ code: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } }, { code: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } },

View File

@ -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/number.ts","./src/lib/numberformat.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingscalefee.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/common/methodunavailablenotice.vue","./src/components/common/xmfactorgrid.vue","./src/components/common/xmcommonaggrid.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/ht.vue","./src/components/views/htconsultcategoryfactor.vue","./src/components/views/htmajorfactor.vue","./src/components/views/servicecheckboxselector.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htcard.vue","./src/components/views/htinfo.vue","./src/components/views/info.vue","./src/components/views/xmcard.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/number.ts","./src/lib/numberformat.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingscalefee.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/common/htfeegrid.vue","./src/components/common/methodunavailablenotice.vue","./src/components/common/xmfactorgrid.vue","./src/components/common/xmcommonaggrid.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/ht.vue","./src/components/views/htadditionalworkfee.vue","./src/components/views/htconsultcategoryfactor.vue","./src/components/views/htmajorfactor.vue","./src/components/views/htreservefee.vue","./src/components/views/servicecheckboxselector.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htcard.vue","./src/components/views/htinfo.vue","./src/components/views/info.vue","./src/components/views/xmcard.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"}