工作内容
This commit is contained in:
parent
1a0e97011f
commit
75a6209010
@ -1418,9 +1418,10 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
</div>
|
||||
|
||||
<div ref="contractListScrollWrapRef" class="mt-4 flex-1 min-h-0">
|
||||
<!-- 合同卡片列表滚动容器:由 ScrollArea 承载滚动行为与滚动条样式。 -->
|
||||
<ScrollArea :class="['ht-contract-scroll-area h-full', isDraggingContracts && 'is-dragging']">
|
||||
<draggable
|
||||
v-if="!isSearchingContracts"
|
||||
v-if="!isSearchingContracts && filteredContracts.length > 0"
|
||||
:key="`contracts-${isListLayout ? 'list' : 'grid'}`"
|
||||
v-model="contracts"
|
||||
item-key="id"
|
||||
@ -1430,7 +1431,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
chosen-class="ht-sortable-chosen"
|
||||
drag-class="ht-sortable-drag"
|
||||
:class="[
|
||||
'grid grid-cols-1 pb-4 pr-4',
|
||||
'grid grid-cols-1 pb-4 pr-4 pt-3',
|
||||
isListLayout ? 'gap-2' : 'gap-4',
|
||||
!isListLayout && 'md:grid-cols-2 lg:grid-cols-3'
|
||||
]"
|
||||
@ -1551,12 +1552,18 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
</Card>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div
|
||||
v-else-if="!isSearchingContracts && filteredContracts.length === 0"
|
||||
class="mx-2 mb-4 rounded-2xl border border-dashed border-primary/30 bg-gradient-to-br from-primary/5 via-background to-muted/30 p-10 text-center shadow-sm"
|
||||
>
|
||||
<div class="text-lg font-semibold tracking-wide text-foreground">暂无合同卡片</div>
|
||||
<div class="mt-2 text-sm text-muted-foreground">赶紧来添加吧</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:key="`contracts-search-${isListLayout ? 'list' : 'grid'}`"
|
||||
:class="[
|
||||
'grid grid-cols-1 pb-4 pr-4',
|
||||
'grid grid-cols-1 pb-4 pr-4 pt-3',
|
||||
isListLayout ? 'gap-2' : 'gap-4',
|
||||
!isListLayout && 'md:grid-cols-2 lg:grid-cols-3'
|
||||
]"
|
||||
@ -1679,6 +1686,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
</div>
|
||||
|
||||
<TooltipRoot>
|
||||
@ -1783,6 +1791,7 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
.ht-contract-scroll-area :deep([data-slot='scroll-area-viewport']) {
|
||||
overscroll-behavior: contain;
|
||||
scroll-snap-type: y mandatory;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.ht-contract-scroll-area.is-dragging :deep([data-slot='scroll-area-viewport']) {
|
||||
@ -1804,7 +1813,84 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
contain: paint;
|
||||
isolation: isolate;
|
||||
overflow: visible;
|
||||
z-index: 0;
|
||||
transition:
|
||||
transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
box-shadow 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
border-color 180ms ease;
|
||||
box-shadow:
|
||||
0 1px 2px hsl(var(--foreground) / 0.04),
|
||||
0 6px 16px hsl(var(--foreground) / 0.06);
|
||||
}
|
||||
|
||||
.ht-contract-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
hsl(var(--primary) / 0.42) 0%,
|
||||
hsl(var(--primary) / 0.22) 36%,
|
||||
hsl(var(--foreground) / 0.09) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.ht-contract-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: -30px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse at center,
|
||||
hsl(var(--primary) / 0.42) 0%,
|
||||
hsl(var(--primary) / 0.24) 34%,
|
||||
hsl(var(--foreground) / 0.20) 58%,
|
||||
transparent 86%);
|
||||
filter: blur(18px);
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition:
|
||||
opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.ht-contract-card:hover {
|
||||
transform: translate3d(0, -5px, 0);
|
||||
z-index: 14;
|
||||
box-shadow:
|
||||
0 0 0 1.5px hsl(var(--primary) / 0.62),
|
||||
0 0 28px hsl(var(--primary) / 0.34),
|
||||
0 0 56px hsl(var(--primary) / 0.22),
|
||||
0 16px 34px hsl(var(--foreground) / 0.22),
|
||||
0 32px 60px hsl(var(--foreground) / 0.18);
|
||||
border-color: hsl(var(--primary) / 0.72);
|
||||
}
|
||||
|
||||
.ht-contract-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ht-contract-card:hover::after {
|
||||
opacity: 0.95;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ht-contract-card:active {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
box-shadow:
|
||||
0 5px 12px hsl(var(--foreground) / 0.10),
|
||||
0 10px 20px hsl(var(--foreground) / 0.10);
|
||||
}
|
||||
|
||||
.ht-contract-card--ready {
|
||||
@ -1834,7 +1920,20 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
|
||||
.ht-contract-card--selected {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.22);
|
||||
transform: translate3d(0, -4px, 0);
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--primary) / 0.34),
|
||||
0 12px 24px hsl(var(--primary) / 0.18),
|
||||
0 22px 36px hsl(var(--foreground) / 0.10);
|
||||
}
|
||||
|
||||
.ht-contract-card--selected::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ht-contract-card--selected::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes ht-card-slide-in {
|
||||
@ -1883,6 +1982,19 @@ watch(budgetRefreshSignature, (next, prev) => {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ht-contract-card,
|
||||
.ht-contract-card:hover,
|
||||
.ht-contract-card:active,
|
||||
.ht-contract-card--selected {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ht-contract-card::before,
|
||||
.ht-contract-card::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, onActivated, onMounted, ref, shallowRef, watch, type PropType } from 'vue'
|
||||
import { computed, defineComponent, h, nextTick, onActivated, onMounted, ref, shallowRef, watch, type PropType } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||||
import type { ColDef, FirstDataRenderedEvent, GridApi, GridOptions, GridReadyEvent, ICellRendererParams, RowDataUpdatedEvent } from 'ag-grid-community'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
@ -237,8 +237,7 @@ const refreshSignature = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const totalRow = computed<SummaryRow | null>(() => {
|
||||
if (rowData.value.length === 0) return null
|
||||
const totalRow = computed<SummaryRow>(() => {
|
||||
const sumField = (pick: (row: SummaryRow) => number | null | undefined) =>
|
||||
sum3(rowData.value.map(pick))
|
||||
return {
|
||||
@ -265,6 +264,9 @@ const RichCodeRenderer = defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
if (props.params.data?.rowType === 'total') {
|
||||
return h('span', props.params.data.name || '合计')
|
||||
}
|
||||
const value = props.params.value as SummaryRow['code']
|
||||
if (!value || typeof value === 'string') {
|
||||
return h('span', value || '')
|
||||
@ -298,13 +300,21 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
field: 'code',
|
||||
minWidth: 90,
|
||||
maxWidth: 140,
|
||||
colSpan: params => (params.data?.rowType === 'total' ? 2 : 1),
|
||||
valueFormatter: params => {
|
||||
if (params.data?.rowType === 'total') return params.data.name || '合计'
|
||||
return typeof params.value === 'string' ? params.value : ''
|
||||
},
|
||||
cellRenderer: RichCodeRenderer
|
||||
},
|
||||
{
|
||||
headerName: '名称',
|
||||
field: 'name',
|
||||
minWidth: 220,
|
||||
flex: 2
|
||||
flex: 2,
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { 'line-height': 1.6 }
|
||||
},
|
||||
{
|
||||
headerName: '投资规模法',
|
||||
@ -313,7 +323,12 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
colSpan: params => (params.data && params.data.rowType !== 'service' ? 5 : 1),
|
||||
colSpan: params => {
|
||||
if (!params.data) return 1
|
||||
if (params.data.rowType === 'total') return 4
|
||||
if (params.data.rowType === 'additional' || params.data.rowType === 'reserve') return 5
|
||||
return 1
|
||||
},
|
||||
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
|
||||
},
|
||||
{
|
||||
@ -368,6 +383,7 @@ const summaryGridOptions: GridOptions<SummaryRow> = {
|
||||
treeData: false,
|
||||
getDataPath: undefined,
|
||||
domLayout: 'autoHeight',
|
||||
suppressNoRowsOverlay: true,
|
||||
rowSelection: {
|
||||
mode: 'singleRow',
|
||||
checkboxes: false,
|
||||
@ -379,6 +395,23 @@ const summaryGridOptions: GridOptions<SummaryRow> = {
|
||||
|
||||
const onGridReady = (event: GridReadyEvent<SummaryRow>) => {
|
||||
gridApi.value = event.api
|
||||
void syncAutoRowHeights()
|
||||
}
|
||||
|
||||
const syncAutoRowHeights = async () => {
|
||||
await nextTick()
|
||||
const api = gridApi.value
|
||||
if (!api) return
|
||||
api.resetRowHeights()
|
||||
api.refreshCells({ force: true })
|
||||
}
|
||||
|
||||
const onFirstDataRendered = (_event: FirstDataRenderedEvent<SummaryRow>) => {
|
||||
void syncAutoRowHeights()
|
||||
}
|
||||
|
||||
const onRowDataUpdated = (_event: RowDataUpdatedEvent<SummaryRow>) => {
|
||||
void syncAutoRowHeights()
|
||||
}
|
||||
|
||||
watch(refreshSignature, (next, prev) => {
|
||||
@ -386,6 +419,13 @@ watch(refreshSignature, (next, prev) => {
|
||||
scheduleReload()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => rowData.value.length,
|
||||
() => {
|
||||
void syncAutoRowHeights()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
void reloadRows()
|
||||
})
|
||||
@ -403,16 +443,19 @@ onActivated(() => {
|
||||
合同段汇总
|
||||
</h3>
|
||||
</div>
|
||||
<div class="ag-theme-quartz w-full" :style="{ minHeight: '120px' }">
|
||||
<div class="ag-theme-quartz w-full">
|
||||
<AgGridVue
|
||||
:style="{ width: '100%' }"
|
||||
:rowData="rowData"
|
||||
:pinnedBottomRowData="totalRow ? [totalRow] : []"
|
||||
:pinnedBottomRowData="[totalRow]"
|
||||
:columnDefs="columnDefs"
|
||||
:gridOptions="summaryGridOptions"
|
||||
:theme="myTheme"
|
||||
:animateRows="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
@grid-ready="onGridReady"
|
||||
@first-data-rendered="onFirstDataRendered"
|
||||
@row-data-updated="onRowDataUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -422,7 +465,7 @@ onActivated(() => {
|
||||
<textarea
|
||||
:value="explanationText"
|
||||
rows="3"
|
||||
placeholder="自动生成说明"
|
||||
placeholder="请先填咨询服务/附加工作费/预备费的数据"
|
||||
readonly
|
||||
class="w-full rounded-md border bg-muted/40 px-3 py-2 text-sm text-foreground outline-none"
|
||||
/>
|
||||
@ -431,6 +474,11 @@ onActivated(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ag-layout-auto-height .ag-center-cols-viewport),
|
||||
:deep(.ag-layout-auto-height .ag-center-cols-container) {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.ht-summary-fee-row .ag-cell) {
|
||||
background: color-mix(in oklab, var(--muted) 45%, transparent);
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, nextTick, onActivated, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||||
import type { FirstDataRenderedEvent } from 'ag-grid-community'
|
||||
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||
@ -197,6 +198,28 @@ const gridApi = shallowRef<GridApi<DetailRow> | null>(null)
|
||||
/** 记录 grid api,供编辑后局部刷新固定行使用。 */
|
||||
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
gridApi.value = event.api
|
||||
scheduleAutoRowHeights()
|
||||
}
|
||||
|
||||
let autoHeightSyncTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const syncAutoRowHeights = async () => {
|
||||
await nextTick()
|
||||
const api = gridApi.value
|
||||
if (!api) return
|
||||
api.resetRowHeights()
|
||||
api.onRowHeightChanged()
|
||||
}
|
||||
|
||||
const scheduleAutoRowHeights = () => {
|
||||
if (autoHeightSyncTimer) clearTimeout(autoHeightSyncTimer)
|
||||
autoHeightSyncTimer = setTimeout(() => {
|
||||
autoHeightSyncTimer = null
|
||||
void syncAutoRowHeights()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const onFirstDataRendered = (_event: FirstDataRenderedEvent<DetailRow>) => {
|
||||
scheduleAutoRowHeights()
|
||||
}
|
||||
|
||||
const clearConfirmOpen = ref(false)
|
||||
@ -555,6 +578,23 @@ const ProcessCellRenderer = defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const NameCellRenderer = defineComponent({
|
||||
name: 'NameCellRenderer',
|
||||
props: {
|
||||
params: {
|
||||
type: Object as PropType<ICellRendererParams<DetailRow>>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
const row = props.params.data
|
||||
if (!row || isFixedRow(row)) return ''
|
||||
return h('div', { class: 'zxfw-name-wrap' }, String(props.params.value || row.name || ''))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{
|
||||
headerName: '编码',
|
||||
@ -566,16 +606,18 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
if (isFixedRow(params.data)) return '小计'
|
||||
return params.data.code
|
||||
},
|
||||
colSpan: params => (params.data && isFixedRow(params.data) ? 2 : 1)
|
||||
colSpan: params => (params.data && isFixedRow(params.data) ? 3 : 1)
|
||||
},
|
||||
{
|
||||
headerName: '名称',
|
||||
field: 'name',
|
||||
minWidth: 150,
|
||||
flex: 3,
|
||||
cellClass: 'zxfw-name-cell',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { 'line-height': 1.6 },
|
||||
cellRenderer: NameCellRenderer,
|
||||
valueGetter: params => {
|
||||
if (!params.data) return ''
|
||||
if (isFixedRow(params.data)) return ''
|
||||
@ -974,6 +1016,20 @@ watch(serviceIdSignature, () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => detailRows.value.map(row => `${row.id}:${row.name}`).join('|'),
|
||||
() => {
|
||||
scheduleAutoRowHeights()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (autoHeightSyncTimer) {
|
||||
clearTimeout(autoHeightSyncTimer)
|
||||
autoHeightSyncTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理表格单元格编辑:当前只接管 finalFee 列。
|
||||
* 编辑后仅重算固定行,避免覆盖用户刚输入的确认金额。
|
||||
@ -1032,7 +1088,9 @@ onActivated(async () => {
|
||||
<div :class="agGridWrapClass">
|
||||
<AgGridVue :style="agGridStyle" :rowData="detailRows" :columnDefs="columnDefs"
|
||||
:gridOptions="detailGridOptions" :theme="myTheme" @cell-value-changed="handleCellValueChanged"
|
||||
:animateRows="true"
|
||||
@grid-ready="onGridReady"
|
||||
@first-data-rendered="onFirstDataRendered"
|
||||
:enableClipboard="true" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="30"
|
||||
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
||||
</div>
|
||||
@ -1092,4 +1150,32 @@ onActivated(async () => {
|
||||
:deep(.zxfw-process-header .ag-header-cell-text) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.ag-cell:not(.ag-cell-auto-height)) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.zxfw-name-cell.ag-cell-auto-height) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.zxfw-name-cell.ag-cell-auto-height .ag-cell-wrapper),
|
||||
:deep(.zxfw-name-cell.ag-cell-auto-height .ag-cell-value) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
:deep(.zxfw-name-wrap) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1368,6 +1368,7 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div :class="agGridWrapClass">
|
||||
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
:animateRows="true"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
||||
|
||||
@ -1197,6 +1197,7 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div :class="agGridWrapClass">
|
||||
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
:animateRows="true"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
||||
|
||||
@ -261,7 +261,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
minWidth: 100,
|
||||
width: 120,
|
||||
pinned: 'left',
|
||||
colSpan: params => (params.node?.rowPinned ? 3 : 1),
|
||||
colSpan: params => (params.node?.rowPinned ? 2 : 1),
|
||||
valueFormatter: params => (params.node?.rowPinned ? '总合计' : params.value || '')
|
||||
},
|
||||
{
|
||||
@ -282,7 +282,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
autoHeight: true,
|
||||
|
||||
width: 180,
|
||||
pinned: 'left',
|
||||
colSpan: params => (params.node?.rowPinned ? 3 : 1),
|
||||
spanRows: spanRowsByTaskName,
|
||||
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
|
||||
},
|
||||
@ -540,6 +540,7 @@ const mydiyTheme = myTheme.withParams({
|
||||
<div v-if="isWorkloadMethodApplicable" :class="agGridWrapClass">
|
||||
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false"
|
||||
:animateRows="true"
|
||||
:enableCellSpan="true"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
@ -559,4 +560,3 @@ const mydiyTheme = myTheme.withParams({
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -373,7 +373,10 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
|
||||
headerName: '预算参考单价',
|
||||
marryChildren: true,
|
||||
children: [
|
||||
readonlyTextCol('laborBudgetUnitPrice', '人工预算单价(元/工日)'),
|
||||
readonlyTextCol('laborBudgetUnitPrice', '人工预算单价(元/工日)', {
|
||||
colSpan: params => (params.node?.rowPinned ? 3 : 1),
|
||||
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
|
||||
}),
|
||||
readonlyTextCol('compositeBudgetUnitPrice', '综合预算单价(元/工日)')
|
||||
]
|
||||
},
|
||||
@ -584,6 +587,7 @@ onBeforeUnmount(() => {
|
||||
:columnDefs="columnDefs"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
:animateRows="true"
|
||||
:treeData="false"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
|
||||
@ -450,6 +450,7 @@ onBeforeUnmount(() => {
|
||||
:columnDefs="columnDefs"
|
||||
:gridOptions="detailGridOptions"
|
||||
:theme="myTheme"
|
||||
:animateRows="true"
|
||||
:treeData="false"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
|
||||
@ -695,6 +695,7 @@ onBeforeUnmount(() => {
|
||||
:columnDefs="columnDefs"
|
||||
:gridOptions="detailGridOptions"
|
||||
:theme="myTheme"
|
||||
:animateRows="true"
|
||||
:treeData="false"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, onBeforeUnmount, onMounted, PropType, ref } from 'vue'
|
||||
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, PropType, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type {
|
||||
CellValueChangedEvent,
|
||||
ColDef,
|
||||
FirstDataRenderedEvent,
|
||||
GridApi,
|
||||
GridReadyEvent,
|
||||
ICellRendererParams,
|
||||
@ -22,18 +23,21 @@ import {
|
||||
} from 'reka-ui'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { myTheme, agGridStyle } from '@/lib/diyAgGridOptions'
|
||||
import { workList } from '@/sql'
|
||||
import { getServiceDictItemById, wholeProcessTasks, workList } from '@/sql'
|
||||
import { WorkType,TYPE_LABEL_MAP } from '@/sql'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import { Trash2 } from 'lucide-vue-next'
|
||||
|
||||
interface WorkContentRow {
|
||||
id: string
|
||||
content: string
|
||||
type: WorkType
|
||||
serviceGroup?: string
|
||||
remark: string
|
||||
checked: boolean
|
||||
custom: boolean
|
||||
isAddTrigger?: boolean
|
||||
path: string[]
|
||||
}
|
||||
|
||||
@ -46,9 +50,12 @@ const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
storageKey: string
|
||||
serviceId?: number | string
|
||||
contractId?: string
|
||||
projectInfoKey?: string
|
||||
dictMode?: 'service' | 'additional' | 'none'
|
||||
}>(), {
|
||||
title: '工作内容',
|
||||
projectInfoKey: 'xm-base-info-v1',
|
||||
dictMode: 'none'
|
||||
})
|
||||
|
||||
@ -57,38 +64,141 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const kvStore = useKvStore()
|
||||
const gridApi = ref<GridApi<WorkContentRow> | null>(null)
|
||||
const rowData = ref<WorkContentRow[]>([])
|
||||
const isWholeProcessGroupedMode = ref(false)
|
||||
const groupedServiceGroups = ref<string[]>([])
|
||||
|
||||
const syncGroupedRowsRender = async () => {
|
||||
await nextTick()
|
||||
const api = gridApi.value
|
||||
if (!api) return
|
||||
if (isWholeProcessGroupedMode.value) {
|
||||
api.expandAll()
|
||||
}
|
||||
api.refreshClientSideRowModel('group')
|
||||
api.resetRowHeights()
|
||||
api.refreshCells({ force: true })
|
||||
api.redrawRows()
|
||||
setTimeout(() => {
|
||||
const liveApi = gridApi.value
|
||||
if (!liveApi) return
|
||||
if (isWholeProcessGroupedMode.value) {
|
||||
liveApi.expandAll()
|
||||
}
|
||||
liveApi.resetRowHeights()
|
||||
liveApi.refreshCells({ force: true })
|
||||
liveApi.redrawRows()
|
||||
}, 16)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const buildDefaultRowsFromDict = (): WorkContentRow[] => {
|
||||
const loadProjectIndustryId = async () => {
|
||||
try {
|
||||
const baseInfo = await kvStore.getItem<{ projectIndustry?: unknown }>(props.projectInfoKey)
|
||||
const raw = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||
if (raw.toUpperCase() === 'E2') return 0
|
||||
if (raw.toUpperCase() === 'E3') return 1
|
||||
if (raw.toUpperCase() === 'E4') return 2
|
||||
const n = Number(raw)
|
||||
return Number.isFinite(n) ? n : null
|
||||
} catch (_error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const buildDefaultRowsFromDict = async (): Promise<WorkContentRow[]> => {
|
||||
const rows: WorkContentRow[] = []
|
||||
const entries = Object.values(workList) as Array<{ text: string; serviceid: number; order: number; type: number }>
|
||||
|
||||
let filtered: typeof entries
|
||||
let filtered: typeof entries = []
|
||||
let groupedServiceIds: number[] = []
|
||||
let groupedBy: 'fid' | 'sid' | null = null
|
||||
let matchedWholeProcessGroup: { fid: number; industry: number; sid: number[] } | null = null
|
||||
isWholeProcessGroupedMode.value = false
|
||||
groupedServiceGroups.value = []
|
||||
if (props.dictMode === 'service') {
|
||||
const sid = Number(props.serviceId)
|
||||
const industryId = await loadProjectIndustryId()
|
||||
const wholeProcessGroupByFid = wholeProcessTasks.find(
|
||||
item => Number(item.fid) === sid && Number(item.industry) === industryId
|
||||
)
|
||||
|
||||
const wholeProcessGroup = wholeProcessGroupByFid
|
||||
groupedBy = wholeProcessGroupByFid ? 'fid' : null
|
||||
if (wholeProcessGroup) {
|
||||
groupedServiceIds = Array.isArray(wholeProcessGroup.sid)
|
||||
? wholeProcessGroup.sid.map(id => Number(id)).filter(Number.isFinite)
|
||||
: []
|
||||
matchedWholeProcessGroup = {
|
||||
fid: Number(wholeProcessGroup.fid),
|
||||
industry: Number(wholeProcessGroup.industry),
|
||||
sid: [...groupedServiceIds]
|
||||
}
|
||||
const groupedSet = new Set(groupedServiceIds)
|
||||
filtered = entries.filter(e => groupedSet.has(Number(e.serviceid)))
|
||||
isWholeProcessGroupedMode.value = groupedServiceIds.length > 0
|
||||
groupedServiceGroups.value = groupedServiceIds.map(sid => {
|
||||
const serviceItem = getServiceDictItemById(sid) as { code?: string; name?: string } | undefined
|
||||
return serviceItem
|
||||
? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim()
|
||||
: String(sid)
|
||||
})
|
||||
} else {
|
||||
filtered = entries.filter(e => e.serviceid === sid)
|
||||
}
|
||||
console.log('[WorkContentGrid][init-group]', {
|
||||
storageKey: props.storageKey,
|
||||
dictMode: props.dictMode,
|
||||
serviceId: sid,
|
||||
industryId,
|
||||
groupedBy,
|
||||
matchedWholeProcessGroup,
|
||||
groupedServiceIds,
|
||||
groupedEnabled: isWholeProcessGroupedMode.value,
|
||||
filteredCount: filtered.length
|
||||
})
|
||||
} else if (props.dictMode === 'additional') {
|
||||
filtered = entries.filter(e => e.serviceid === -1 && props.storageKey.split('-').at(-1) =='2')
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
if (isWholeProcessGroupedMode.value) {
|
||||
const sidIndex = new Map(groupedServiceIds.map((sid, index) => [sid, index]))
|
||||
filtered.sort((a, b) => {
|
||||
const indexA = sidIndex.get(Number(a.serviceid)) ?? Number.MAX_SAFE_INTEGER
|
||||
const indexB = sidIndex.get(Number(b.serviceid)) ?? Number.MAX_SAFE_INTEGER
|
||||
if (indexA !== indexB) return indexA - indexB
|
||||
return a.order - b.order
|
||||
})
|
||||
} else {
|
||||
filtered.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
for (const entry of filtered) {
|
||||
const content = String(entry.text || '').trim()
|
||||
if (!content) continue
|
||||
const typeLabel = TYPE_LABEL_MAP[entry.type] ?? '基本工作'
|
||||
const serviceItem = getServiceDictItemById(entry.serviceid) as { code?: string; name?: string } | undefined
|
||||
const serviceGroup = serviceItem
|
||||
? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim()
|
||||
: ''
|
||||
rows.push({
|
||||
id: `dict-${entry.order}`,
|
||||
id: isWholeProcessGroupedMode.value
|
||||
? `dict-${entry.serviceid}-${entry.order}-${content}`
|
||||
: `dict-${entry.order}`,
|
||||
content,
|
||||
type: typeLabel,
|
||||
serviceGroup,
|
||||
remark: '',
|
||||
checked: false,
|
||||
custom: false,
|
||||
path: [typeLabel, content]
|
||||
path: isWholeProcessGroupedMode.value && serviceGroup
|
||||
? [serviceGroup, content]
|
||||
: [typeLabel, content]
|
||||
})
|
||||
}
|
||||
return rows
|
||||
@ -102,13 +212,13 @@ const requestDeleteRow = (id: string, name?: string) => {
|
||||
deleteConfirmOpen.value = true
|
||||
}
|
||||
const checkedIds = computed(() =>
|
||||
rowData.value.filter(item => item.checked).map(item => item.id)
|
||||
rowData.value.filter(item => !isAddTriggerRow(item) && item.checked).map(item => item.id)
|
||||
)
|
||||
|
||||
// 导出用:自定义内容全部包含,默认词典内容只含勾选的
|
||||
const selectedTexts = computed(() =>
|
||||
rowData.value
|
||||
.filter(item => item.custom || item.checked)
|
||||
.filter(item => !isAddTriggerRow(item) && (item.custom || item.checked))
|
||||
.map(item => item.content)
|
||||
.filter(Boolean)
|
||||
)
|
||||
@ -121,30 +231,62 @@ const emitCheckedChange = () => {
|
||||
|
||||
const saveToStore = () => {
|
||||
const payload: WorkContentState = {
|
||||
detailRows: rowData.value.map(item => ({ ...item }))
|
||||
detailRows: getPersistableRows(rowData.value).map(item => ({ ...item }))
|
||||
}
|
||||
zxFwPricingStore.setKeyState(props.storageKey, payload)
|
||||
emitCheckedChange()
|
||||
}
|
||||
|
||||
const loadFromStore = async () => {
|
||||
const defaultRows =
|
||||
props.dictMode === 'none'
|
||||
? []
|
||||
: await buildDefaultRowsFromDict()
|
||||
const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey)
|
||||
if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) {
|
||||
rowData.value = state.detailRows.map(item => ({
|
||||
const persistedRows = state.detailRows.map(item => ({
|
||||
...item,
|
||||
type: item.custom ? '自定义' : (item.type || '基本工作'),
|
||||
path: Array.isArray(item.path) && item.path.length ? item.path : ['自定义', item.content || '未命名']
|
||||
})) as WorkContentRow[]
|
||||
|
||||
// 按最新词典规则重建默认行,再合并历史勾选/备注,保证分组规则变更后立即生效。
|
||||
if (defaultRows.length > 0) {
|
||||
const persistedCustomRows = persistedRows.filter(item => item.custom)
|
||||
const persistedDictRows = persistedRows.filter(item => !item.custom)
|
||||
const persistedByContent = new Map(
|
||||
persistedDictRows.map(item => [String(item.content || '').trim(), item])
|
||||
)
|
||||
const mergedDictRows = defaultRows.map(item => {
|
||||
const key = String(item.content || '').trim()
|
||||
const old = persistedByContent.get(key)
|
||||
if (!old) return item
|
||||
return {
|
||||
...item,
|
||||
checked: Boolean(old.checked),
|
||||
remark: String(old.remark || ''),
|
||||
type: old.type || item.type
|
||||
}
|
||||
})
|
||||
rowData.value = withAddTriggerRows([...mergedDictRows, ...persistedCustomRows])
|
||||
saveToStore()
|
||||
} else {
|
||||
rowData.value = buildDefaultRowsFromDict()
|
||||
rowData.value = withAddTriggerRows(persistedRows)
|
||||
isWholeProcessGroupedMode.value = rowData.value.some(
|
||||
item => !item.custom && Boolean(String(item.serviceGroup || '').trim())
|
||||
)
|
||||
}
|
||||
} else {
|
||||
rowData.value = withAddTriggerRows(defaultRows)
|
||||
saveToStore()
|
||||
}
|
||||
emitCheckedChange()
|
||||
await syncGroupedRowsRender()
|
||||
}
|
||||
|
||||
const handleCheckedToggle = (id: string, checked: boolean) => {
|
||||
const target = rowData.value.find(item => item.id === id)
|
||||
if (!target) return
|
||||
if (!target || isAddTriggerRow(target)) return
|
||||
target.checked = checked
|
||||
gridApi.value?.refreshCells({ force: true })
|
||||
saveToStore()
|
||||
@ -160,6 +302,13 @@ const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
|
||||
wrapper.style.gap = '6px'
|
||||
wrapper.style.width = '100%'
|
||||
wrapper.className = 'work-content-cell'
|
||||
if (isAddTriggerRow(data)) {
|
||||
const label = document.createElement('span')
|
||||
label.className = 'work-content-placeholder'
|
||||
label.textContent = String(data.content || '点击添加自定义内容')
|
||||
wrapper.appendChild(label)
|
||||
return wrapper
|
||||
}
|
||||
// 自定义行不显示 checkbox,直接显示文本;空时显示 placeholder
|
||||
if (data.custom) {
|
||||
const label = document.createElement('span')
|
||||
@ -193,17 +342,40 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
||||
headerName: '序号',
|
||||
minWidth: 60,
|
||||
width: 70,
|
||||
pinned: 'left',
|
||||
suppressMovable: true,
|
||||
editable: false,
|
||||
valueGetter: params => (params.node?.rowIndex ?? 0) + 1
|
||||
colSpan: params => (isAddTriggerRow(params.data) ? 5 : 1),
|
||||
valueGetter: params => {
|
||||
if (!params.node || params.node.group || isAddTriggerRow(params.data)) return ''
|
||||
if (!isWholeProcessGroupedMode.value) return (params.node.rowIndex ?? 0) + 1
|
||||
const siblings = params.node.parent?.childrenAfterSort || []
|
||||
const visibleLeafSiblings = siblings.filter(node => !node.group && !isAddTriggerRow(node.data as WorkContentRow))
|
||||
const index = visibleLeafSiblings.findIndex(node => node.id === params.node?.id)
|
||||
return index >= 0 ? index + 1 : ''
|
||||
},
|
||||
cellRenderer: (params: ICellRendererParams<WorkContentRow>) => {
|
||||
const row = params.data
|
||||
if (!isAddTriggerRow(row)) return params.value
|
||||
const button = document.createElement('button')
|
||||
button.type = 'button'
|
||||
button.className =
|
||||
'inline-flex h-full w-full cursor-pointer items-center justify-center rounded-none border-0 bg-transparent px-3 py-3 text-sm font-medium text-blue-700 hover:bg-transparent focus:outline-none'
|
||||
button.textContent = '+ 添加自定义内容'
|
||||
button.addEventListener('click', event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
addCustomRow(String(row?.serviceGroup || '').trim())
|
||||
})
|
||||
return button
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '工作内容',
|
||||
field: 'content',
|
||||
minWidth: 320,
|
||||
flex: 2,
|
||||
editable: params => Boolean(params.data?.custom),
|
||||
cellClass: 'work-content-main-cell',
|
||||
editable: params => Boolean(params.data?.custom && !isAddTriggerRow(params.data)),
|
||||
|
||||
valueParser: params => String(params.newValue || '').trim(),
|
||||
wrapText: true,
|
||||
@ -218,14 +390,15 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
||||
minWidth: 100,
|
||||
width: 120,
|
||||
editable: false,
|
||||
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) => String(params.value || '')
|
||||
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) =>
|
||||
isAddTriggerRow(params.data) ? '' : String(params.value || '')
|
||||
},
|
||||
{
|
||||
headerName: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
flex: 1.2,
|
||||
editable: true,
|
||||
editable: params => !isAddTriggerRow(params.data),
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
@ -234,7 +407,7 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
},
|
||||
valueFormatter: params => params.value || '点击输入'
|
||||
valueFormatter: params => (isAddTriggerRow(params.data) ? '' : (params.value || '点击输入'))
|
||||
},
|
||||
{
|
||||
headerName: '操作',
|
||||
@ -279,29 +452,106 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
||||
}
|
||||
]
|
||||
|
||||
const addCustomRow = () => {
|
||||
const isAddTriggerRow = (row?: WorkContentRow | null) => Boolean(row?.isAddTrigger)
|
||||
const getPersistableRows = (rows: WorkContentRow[]) => rows.filter(item => !isAddTriggerRow(item))
|
||||
|
||||
const createAddTriggerRow = (groupName?: string): WorkContentRow => {
|
||||
const suffix = groupName ? String(groupName).trim() : 'root'
|
||||
return {
|
||||
id: `add-trigger-${suffix}`,
|
||||
content: '点击添加自定义内容',
|
||||
type: '自定义' as WorkType,
|
||||
serviceGroup: groupName || '',
|
||||
remark: '',
|
||||
checked: false,
|
||||
custom: false,
|
||||
isAddTrigger: true,
|
||||
path: groupName ? [groupName, '__add__'] : ['__add__']
|
||||
}
|
||||
}
|
||||
|
||||
const withAddTriggerRows = (rows: WorkContentRow[]) => {
|
||||
const pureRows = getPersistableRows(rows)
|
||||
if (isWholeProcessGroupedMode.value) {
|
||||
const groupedMap = new Map<string, WorkContentRow[]>()
|
||||
for (const row of pureRows) {
|
||||
const groupName = String(row.serviceGroup || '').trim() || '未分组'
|
||||
if (!groupedMap.has(groupName)) groupedMap.set(groupName, [])
|
||||
groupedMap.get(groupName)?.push(row)
|
||||
}
|
||||
const groupOrder = groupedServiceGroups.value.length
|
||||
? [...groupedServiceGroups.value]
|
||||
: [...groupedMap.keys()]
|
||||
const result: WorkContentRow[] = []
|
||||
const used = new Set<string>()
|
||||
for (const groupName of groupOrder) {
|
||||
used.add(groupName)
|
||||
result.push(...(groupedMap.get(groupName) || []))
|
||||
result.push(createAddTriggerRow(groupName))
|
||||
}
|
||||
for (const [groupName, groupRows] of groupedMap.entries()) {
|
||||
if (used.has(groupName)) continue
|
||||
result.push(...groupRows)
|
||||
result.push(createAddTriggerRow(groupName))
|
||||
}
|
||||
return result
|
||||
}
|
||||
return [...pureRows, createAddTriggerRow()]
|
||||
}
|
||||
|
||||
const getDataPath = (data: WorkContentRow) => {
|
||||
const path = Array.isArray(data?.path)
|
||||
? data.path.map(segment => String(segment || '').trim()).filter(Boolean)
|
||||
: []
|
||||
if (path.length > 0) return path
|
||||
const fallback = String(data?.id || '').trim()
|
||||
return [fallback || '__row__']
|
||||
}
|
||||
|
||||
const addCustomRow = (groupName?: string) => {
|
||||
const ts = Date.now()
|
||||
rowData.value.push({
|
||||
const finalGroupName = isWholeProcessGroupedMode.value
|
||||
? String(groupName || groupedServiceGroups.value[0] || '').trim()
|
||||
: ''
|
||||
const nextRow: WorkContentRow = {
|
||||
id: `custom-${ts}`,
|
||||
content: '',
|
||||
type: '自定义' as WorkType,
|
||||
serviceGroup: finalGroupName,
|
||||
remark: '',
|
||||
checked: false,
|
||||
custom: true,
|
||||
path: ['自定义', `自定义-${ts}`]
|
||||
})
|
||||
path: isWholeProcessGroupedMode.value && finalGroupName
|
||||
? [finalGroupName, `自定义-${ts}`]
|
||||
: ['自定义', `自定义-${ts}`]
|
||||
}
|
||||
const pureRows = getPersistableRows(rowData.value)
|
||||
pureRows.push(nextRow)
|
||||
rowData.value = withAddTriggerRows(pureRows)
|
||||
saveToStore()
|
||||
setTimeout(() => {
|
||||
const rowIndex = rowData.value.findIndex(item => item.id === nextRow.id)
|
||||
if (rowIndex >= 0) gridApi.value?.startEditingCell({ rowIndex, colKey: 'content' })
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const onGridReady = (event: GridReadyEvent<WorkContentRow>) => {
|
||||
gridApi.value = event.api
|
||||
void syncGroupedRowsRender()
|
||||
}
|
||||
|
||||
const onFirstDataRendered = (_event: FirstDataRenderedEvent<WorkContentRow>) => {
|
||||
void syncGroupedRowsRender()
|
||||
}
|
||||
|
||||
const onCellValueChanged = (event: CellValueChangedEvent<WorkContentRow>) => {
|
||||
const row = event.data
|
||||
if (!row) return
|
||||
if (!row || isAddTriggerRow(row)) return
|
||||
if (event.colDef.field === 'content' && row.custom) {
|
||||
row.path = ['自定义', row.content || `自定义-${row.id}`]
|
||||
const groupName = String(row.serviceGroup || '').trim()
|
||||
row.path = isWholeProcessGroupedMode.value && groupName
|
||||
? [groupName, row.content || `自定义-${row.id}`]
|
||||
: ['自定义', row.content || `自定义-${row.id}`]
|
||||
}
|
||||
if (event.colDef.field === 'type' && row.custom) {
|
||||
row.type = '自定义'
|
||||
@ -313,6 +563,17 @@ onMounted(() => {
|
||||
void loadFromStore()
|
||||
})
|
||||
|
||||
watch(isWholeProcessGroupedMode, () => {
|
||||
void syncGroupedRowsRender()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => rowData.value.length,
|
||||
() => {
|
||||
void syncGroupedRowsRender()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
saveToStore()
|
||||
})
|
||||
@ -322,7 +583,7 @@ const handleDeleteConfirmOpenChange = (open: boolean) => {
|
||||
|
||||
|
||||
const deleteRow = (id: string) => {
|
||||
rowData.value = rowData.value.filter(item => item.id !== id)
|
||||
rowData.value = withAddTriggerRows(rowData.value.filter(item => item.id !== id))
|
||||
saveToStore()
|
||||
}
|
||||
|
||||
@ -341,9 +602,6 @@ const confirmDeleteRow = () => {
|
||||
<div class="h-full min-h-0 rounded-2xl border border-border/60 bg-card/90 shadow-sm backdrop-blur-sm">
|
||||
<div class="flex items-center justify-between border-b border-border/60 px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">{{ props.title }}</h3>
|
||||
<Button type="button" size="sm" variant="outline" class="cursor-pointer" @click="addCustomRow">
|
||||
添加自定义内容
|
||||
</Button>
|
||||
</div>
|
||||
<div class="ag-theme-quartz h-[calc(100%-56px)] min-h-0 w-full">
|
||||
<AgGridVue
|
||||
@ -352,6 +610,11 @@ const confirmDeleteRow = () => {
|
||||
:columnDefs="columnDefs"
|
||||
:theme="myTheme"
|
||||
:getRowId="(params: { data: WorkContentRow }) => params.data.id"
|
||||
:treeData="isWholeProcessGroupedMode"
|
||||
:getDataPath="getDataPath"
|
||||
:groupDefaultExpanded="isWholeProcessGroupedMode ? -1 : 0"
|
||||
:groupDisplayType="isWholeProcessGroupedMode ? 'groupRows' : undefined"
|
||||
:groupRowRendererParams="isWholeProcessGroupedMode ? { suppressCount: true } : undefined"
|
||||
:animateRows="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
@ -361,6 +624,7 @@ const confirmDeleteRow = () => {
|
||||
:suppressColumnVirtualisation="false"
|
||||
:suppressRowVirtualisation="false"
|
||||
@grid-ready="onGridReady"
|
||||
@first-data-rendered="onFirstDataRendered"
|
||||
@cell-value-changed="onCellValueChanged"
|
||||
/>
|
||||
</div>
|
||||
@ -388,6 +652,20 @@ const confirmDeleteRow = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ag-cell) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ag-cell .ag-cell-wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ag-cell .ag-cell-value) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.work-content-placeholder) {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
|
||||
@ -333,6 +333,7 @@ onBeforeUnmount(() => {
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
:animateRows="true"
|
||||
:treeData="true"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
|
||||
@ -629,6 +629,7 @@ onMounted(() => {
|
||||
<div :class="agGridWrapClass">
|
||||
<AgGridVue :style="agGridStyle" :rowData="visibleRowData" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
:animateRows="true"
|
||||
@grid-ready="onGridReady" @cell-value-changed="onCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :suppressHorizontalScroll="true"
|
||||
|
||||
@ -103,6 +103,8 @@ const workContentPane = markRaw(
|
||||
return () => h(AsyncWorkContentGrid, {
|
||||
title: '工作内容',
|
||||
storageKey: `work-content-${props.contractId}-${props.serviceId}`,
|
||||
contractId: props.contractId,
|
||||
projectInfoKey: props.projectInfoKey,
|
||||
serviceId: props.serviceId,
|
||||
dictMode: 'service'
|
||||
})
|
||||
|
||||
@ -27,7 +27,7 @@ import {
|
||||
ToastViewport
|
||||
} from 'reka-ui'
|
||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||
import { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode } from '@/lib/workspace'
|
||||
import { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode, writeWorkspaceMode } from '@/lib/workspace'
|
||||
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||
import { exportFile, serviceList } from '@/sql'
|
||||
|
||||
@ -1731,6 +1731,32 @@ const confirmImportOverride = async () => {
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
const deleteIndexedDBByName = (dbName: string) =>
|
||||
new Promise<void>((resolve) => {
|
||||
try {
|
||||
const request = window.indexedDB?.deleteDatabase(dbName)
|
||||
if (!request) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => resolve()
|
||||
request.onblocked = () => {
|
||||
// 被阻塞时也继续流程,刷新后浏览器会重试清理。
|
||||
resolve()
|
||||
}
|
||||
} catch (_error) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
const purgeKnownIndexedDB = async () => {
|
||||
await Promise.all([
|
||||
deleteIndexedDBByName('DB'),
|
||||
deleteIndexedDBByName('localforage')
|
||||
])
|
||||
}
|
||||
|
||||
try {
|
||||
dataMenuOpen.value = false
|
||||
|
||||
@ -1765,15 +1791,14 @@ const handleReset = async () => {
|
||||
if (kvStore.$clearPersisted) clearTasks.push(kvStore.$clearPersisted())
|
||||
await Promise.all(clearTasks)
|
||||
|
||||
// 5) 需要保留的引导标记恢复
|
||||
// 5) 强制删除已知 IndexedDB,避免页面卸载阶段旧组件回写脏数据
|
||||
await purgeKnownIndexedDB()
|
||||
|
||||
// 6) 需要保留的最小标记恢复
|
||||
writeWorkspaceMode('project')
|
||||
localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
|
||||
|
||||
// 6) 持久化当前“空状态”并刷新页面
|
||||
const persistTasks: Promise<void>[] = []
|
||||
if (tabStore.$persistNow) persistTasks.push(tabStore.$persistNow())
|
||||
if (zxFwPricingStore.$persistNow) persistTasks.push(zxFwPricingStore.$persistNow())
|
||||
if (kvStore.$persistNow) persistTasks.push(kvStore.$persistNow())
|
||||
await Promise.all(persistTasks)
|
||||
// 7) 直接刷新,交由默认初始化重建空状态
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error('reset failed:', error)
|
||||
|
||||
216
src/sql.ts
216
src/sql.ts
@ -247,111 +247,125 @@ export const workList = {
|
||||
47: { text: '复核竣工图纸数量与结算数量的差异,提交数量差异报告', serviceid: 11, order: 48, type: 1 },
|
||||
48: { text: '协助委托人对项目工程变更费用审批情况进行清理,编制交工验收造价文件', serviceid: 11, order: 49, type: 1 },
|
||||
49: { text: '协助委托人处理工期索赔、造价费用等费用的争议与纠纷、仲裁与诉讼', serviceid: 11, order: 50, type: 1 },
|
||||
50: { text: '编制项目竣工决算文件', serviceid: 13, order: 51, type: 0 },
|
||||
51: { text: '编制竣工决算备案送审文件', serviceid: 13, order: 52, type: 0 },
|
||||
52: { text: '参与竣工决算相关的会议', serviceid: 13, order: 53, type: 0 },
|
||||
53: { text: '根据审计意见和决算备案意见调整竣工决算文件', serviceid: 13, order: 54, type: 0 },
|
||||
54: { text: '协助委托人编制建设项目造价执行情况报告', serviceid: 13, order: 55, type: 1 },
|
||||
55: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 13, order: 56, type: 1 },
|
||||
56: { text: '根据造价差异分析结果,提出调整工程方案的咨询意见供委托人或关联单位决策', serviceid: 13, order: 57, type: 1 },
|
||||
57: { text: '协助委托人指导参建单位完成与决算相关辅助文件的编制', serviceid: 13, order: 58, type: 1 },
|
||||
58: { text: '协助委托人开展项目造价管理及投资效益后评估', serviceid: 13, order: 59, type: 1 },
|
||||
59: { text: '定期向委托人介绍并解读国家及地方最新发布的关于造价管理有关的法律法规、政策文件和技术标准等信息,针对风险控制方面,提供专业的造价管理建议及实施对策', serviceid: 15, order: 60, type: 2 },
|
||||
60: { text: '为委托人在日常建设、运营及经营管理中遇到的造价编制与审核问题,提供专业咨询建议', serviceid: 15, order: 61, type: 2 },
|
||||
61: { text: '对委托人起草的工程勘察设计、工程监理、施工、物资采购和技术服务等方面的合同与协议文本,从造价控制与投资风险角度提供审阅与修改建议', serviceid: 15, order: 62, type: 2 },
|
||||
62: { text: '审查各类合同履行情况时,对识别出的潜在漏洞或风险,提出相应的修改建议或补救措施', serviceid: 15, order: 63, type: 2 },
|
||||
63: { text: '对委托人提供的造价管理相关的制度,进行审阅与修改,或提供专业咨询建议', serviceid: 15, order: 64, type: 2 },
|
||||
64: { text: '针对具体建设工程项目的重大投资决策,出具独立的造价专业意见;或根据委托人的要求, 对造价争议事项提供专业依据并出具顾问报告', serviceid: 15, order: 65, type: 3 },
|
||||
65: { text: '为委托人起草与工程建设相关的合同、协议等文书,包括且不限于工程勘察设计、监理、施 工、采购及技术服务等领域', serviceid: 15, order: 66, type: 3 },
|
||||
66: { text: '独立对各类合同的履行情况进行审查,识别潜在漏洞或风险,并提出系统性的修改建议或补 救方案', serviceid: 15, order: 67, type: 3 },
|
||||
67: { text: '为委托人制订或修订与造价管理相关的制度', serviceid: 15, order: 68, type: 3 },
|
||||
68: { text: '受托处理涉及委托人的造价纠纷、仲裁及诉讼案件,提供专业意见与支持,并出具咨询顾问 报告', serviceid: 15, order: 69, type: 3 },
|
||||
69: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 16, order: 70, type: 0 },
|
||||
70: { text: '完成各阶段文件的起草、修订、调整与校对工作;整理并分析各阶段的 反馈意见、完成反馈意见的采纳情况,编制必要的采纳情况报告或说明;完成工作报告', serviceid: 16, order: 71, type: 0 },
|
||||
71: { text: '完成评审汇报材料,编写报告;整理并汇总评审意见、复核评审意见,完成评审 意见的采纳及反馈工作', serviceid: 16, order: 72, type: 0 },
|
||||
72: { text: '负责评审会议的全过程组织工作, 包括准备会议材料、发出通知、组织召开并主持评审会。', serviceid: 16, order: 73, type: 1 },
|
||||
73: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 17, order: 74, type: 0 },
|
||||
74: { text: '完成工作大纲和编写大纲;开展专项研究工作,完成各阶段研究报告的起 草、修订、调整与校对工作;分研究阶段进行技术报告的编写、修改、调整以及相应的报审 报批工作,包括大纲与成果的评审及验收等;整理并分析各阶段的反馈意见、完成反馈意见 的采纳情况,编制必要的采纳情况报告或说明;编制工作报告', serviceid: 17, order: 75, type: 0 },
|
||||
75: { text: '确定测试与验证项目,完成测试与验证工作,完成测试与验证报告', serviceid: 17, order: 76, type: 0 },
|
||||
76: { text: '完成评审与验收所需汇报材料,编写报告;整理并汇总评审意见、复核评审意见,完成评审意见的采纳及反馈工作', serviceid: 17, order: 77, type: 0 },
|
||||
77: { text: '编写培训与宣贯相关的材料,组织研究成果的宣贯与推广工作', serviceid: 17, order: 78, type: 1 },
|
||||
78: { text: '负责评审与验收会议的全过程组织工作,包括准备会议材料、发出通知、 组织召开并主持评审与验收会议', serviceid: 17, order: 79, type: 1 },
|
||||
79: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 18, order: 80, type: 0 },
|
||||
80: { text: '制定工作计划并完成编制大纲,编制大纲包括工作大纲和编写大纲', serviceid: 18, order: 81, type: 0 },
|
||||
81: { text: '完成数据采集与测定工作,编制相应的数据采集与测定报告', serviceid: 18, order: 82, type: 0 },
|
||||
82: { text: '完成数据整理与分析工作,编制相应的数据分析报告,整理支撑性资料', serviceid: 18, order: 83, type: 0 },
|
||||
83: { text: '完成定额文本的起草、修改及调整工作;完成各阶段研究报告的起草、修订、 调整与校对工作,编制必要的采纳情况报告或说明;编制工作报告或报批报告', serviceid: 18, order: 84, type: 0 },
|
||||
84: { text: '确定测试与验证项目,完成测试与验证工作,完成测试与验证报告', serviceid: 18, order: 85, type: 0 },
|
||||
85: { text: '完成评审与验收所需汇报材料,编写报告;整理并汇总评审意见、复核评 审意见,完成评审意见的采纳及反馈工作', serviceid: 18, order: 86, type: 0 },
|
||||
86: { text: '编写培训与宣贯相关的材料,组织定额成果的培训与宣贯工作', serviceid: 18, order: 87, type: 1 },
|
||||
87: { text: '负责评审与验收会议的全过程组织工作,包括准备会议材料、发出通知、 组织召开并主持评审与验收会议', serviceid: 18, order: 88, type: 1 },
|
||||
88: { text: '开展各类造价信息的搜集、筛选与整理工作', serviceid: 19, order: 89, type: 0 },
|
||||
89: { text: '对整理后的信息与数据进行对比与分析', serviceid: 19, order: 90, type: 0 },
|
||||
90: { text: '依据分析结果,对特定价格或费用进行评估与论证,编制咨询报告', serviceid: 19, order: 91, type: 0 },
|
||||
91: { text: '预测未来特定时期的价格或费用,编制预测报告', serviceid: 19, order: 92, type: 1 },
|
||||
92: { text: '研究价格或费用的长期变动规律,编制趋势分析报告', serviceid: 19, order: 93, type: 1 },
|
||||
93: { text: '根据委托人的目标,确定鉴定工作范围', serviceid: 20, order: 94, type: 0 },
|
||||
94: { text: '收集与复核鉴定资料', serviceid: 20, order: 95, type: 0 },
|
||||
95: { text: '组织或参与必要的现场勘查工作', serviceid: 20, order: 96, type: 0 },
|
||||
96: { text: '工程量的计算与工程费用的计价或估价', serviceid: 20, order: 97, type: 0 },
|
||||
97: { text: '对争议焦点进行鉴定,提出解决争议的意见', serviceid: 20, order: 98, type: 0 },
|
||||
98: { text: '回复各方当事人的质证意见', serviceid: 20, order: 99, type: 0 },
|
||||
99: { text: '编制并出具造价鉴定报告', serviceid: 20, order: 100, type: 0 },
|
||||
100: { text: '组织或参与现场数据复测或特殊鉴定', serviceid: 20, order: 101, type: 1 },
|
||||
101: { text: '与当事人相关方进行必要的造价数据核对', serviceid: 20, order: 102, type: 1 },
|
||||
102: { text: '出庭就鉴定报告及相关专业问题接受质证', serviceid: 20, order: 103, type: 1 },
|
||||
103: { text: '编制影响工程成本的资源要素清单', serviceid: 21, order: 104, type: 0 },
|
||||
104: { text: '调查人工、材料、设备和施工机械的市场供应情况,主要是价格情况', serviceid: 21, order: 105, type: 0 },
|
||||
105: { text: '测算、分析、计算工程成本费用,结合其他相关因素,编制并出具工程成本测算报告', serviceid: 21, order: 106, type: 0 },
|
||||
106: { text: '参加与工程成本测算相关的会议', serviceid: 21, order: 107, type: 0 },
|
||||
107: { text: '审核及分析资源配置、施工措施、施工管理方案的经济合理性', serviceid: 21, order: 108, type: 1 },
|
||||
108: { text: '对比工程成本与投资估算、设计概算、施工图预算、工程量清单预算、合同价,并分析费用差异,提交分析报告', serviceid: 21, order: 109, type: 1 },
|
||||
109: { text: '编制影响工程成本的资源要素清单或成本组成科目', serviceid: 22, order: 110, type: 0 },
|
||||
110: { text: '收集人工、材料、设备、施工机械、措施、税费等实际发生的费用', serviceid: 22, order: 111, type: 0 },
|
||||
111: { text: '统计与汇总、拆解与合并、对比与分析工程成本各项费用,编制并出具工程成本核算报告', serviceid: 22, order: 112, type: 0 },
|
||||
112: { text: '参加与工程成本核算相关的会议', serviceid: 22, order: 113, type: 0 },
|
||||
113: { text: '分析实际工程成本与测算工程成本差异的合理性,提交分析报告', serviceid: 22, order: 114, type: 1 },
|
||||
114: { text: '归纳与总结实际工程成本与测算工程成本的差异原因,提出改进建议', serviceid: 22, order: 115, type: 1 },
|
||||
115: { text: '依据本项目在招标阶段确认的设计图纸工程量,结合工程量清单计量支付规则,对设计工程量进行复核,包括构件工程量、明细表工程量和汇总表工程量,同时与相关方核对工程量', serviceid: 23, order: 116, type: 0 },
|
||||
116: { text: '依据核对后确认的设计图纸数量,细化与合并招标工程量清单或合同工程量清单,建立各维度清单间的数据链接,与相关单位完成核对与确认', serviceid: 23, order: 117, type: 0 },
|
||||
117: { text: '依据确定的招标工程量清单或合同工程量清单,拆解与合并相应的清单费用,与相关单位完成核对与确认', serviceid: 23, order: 118, type: 1 },
|
||||
118: { text: '现场勘查与测量现场实施工程量', serviceid: 23, order: 119, type: 1 },
|
||||
119: { text: '完成设计图纸所载数量与复核后的数量进行对比与分析,提供对比分析报告', serviceid: 23, order: 120, type: 1 },
|
||||
120: { text: '参加计算工程量相关的会议', serviceid: 23, order: 121, type: 0 },
|
||||
121: { text: '协助委托人处理涉及工程数量的争议、纠纷、仲裁或诉讼事务', serviceid: 23, order: 122, type: 1 },
|
||||
122: { text: '完成工程变更费用的测算', serviceid: 24, order: 123, type: 0 },
|
||||
123: { text: '完成新增预算单价或合同单价的计算与核定', serviceid: 24, order: 124, type: 0 },
|
||||
124: { text: '按施工图预算或合同清单方式完成工程变更费用的计算', serviceid: 24, order: 125, type: 0 },
|
||||
125: { text: '完成价格波动引起的价差费用的计算', serviceid: 24, order: 126, type: 1 },
|
||||
126: { text: '完成索赔与补偿费用的计算,如加速施工费用、暂停施工补偿等', serviceid: 24, order: 127, type: 1 },
|
||||
127: { text: '协助委托人处理涉及工程变更费用的争议与纠纷、仲裁与诉讼', serviceid: 24, order: 128, type: 1 },
|
||||
128: { text: '分析、论证及评估影响投资估算的主要因素', serviceid: 25, order: 129, type: 0 },
|
||||
129: { text: '根据分析论证结果,完成调整后投资估算文件及主要技术经济指标,与相关单位核对数据', serviceid: 25, order: 130, type: 0 },
|
||||
130: { text: '完成调整后投资估算与原批复估算的对比及原因分析', serviceid: 25, order: 131, type: 0 },
|
||||
131: { text: '出席与投资估算调整工作相关的会议', serviceid: 25, order: 132, type: 0 },
|
||||
132: { text: '协助调规报告编制单位完成报告的造价专业部分的内容', serviceid: 25, order: 133, type: 1 },
|
||||
133: { text: '分析、论证及评估影响设计概算的主要因素', serviceid: 26, order: 134, type: 0 },
|
||||
134: { text: '根据分析论证结果,编制调整后设计概算文件及主要技术经济指标,与相关单位核对数据', serviceid: 26, order: 135, type: 0 },
|
||||
135: { text: '完成调整后概算与原批复概算的对比及原因分析,完成调整概算报告', serviceid: 26, order: 136, type: 0 },
|
||||
136: { text: '出席与概算调整工作相关的会议', serviceid: 26, order: 137, type: 0 },
|
||||
137: { text: '协助委托人开展项目预估决算费用的测算、估算工作', serviceid: 26, order: 138, type: 1 },
|
||||
138: { text: '检查相关单位对工程造价管理法律法规、规章制度以及工程造价依据的执行情况', serviceid: 27, order: 139, type: 0 },
|
||||
139: { text: '检查各阶段造价文件编制、审查(批)或备案以及对批复意见的落实情况', serviceid: 27, order: 140, type: 0 },
|
||||
140: { text: '检查造价管理台账和计量支付制度的建立与执行、造价全过程管理与控制情况', serviceid: 27, order: 141, type: 0 },
|
||||
141: { text: '检查工程变更管理情况', serviceid: 27, order: 142, type: 0 },
|
||||
142: { text: '检查项目造价信息的收集、分析及报送情况', serviceid: 27, order: 143, type: 0 },
|
||||
143: { text: '检查项目从业单位的造价人员执业情况', serviceid: 27, order: 144, type: 0 },
|
||||
144: { text: '协助委托人进行现场核查、资料抽检和台账复核工作', serviceid: 27, order: 145, type: 0 },
|
||||
145: { text: '协助委托人整理检查结果和起草检查报告等工作', serviceid: 27, order: 146, type: 0 },
|
||||
146: { text: '作为造价咨询服务总体协调单位,依据造价技术标准的具体条款或委托方的个性化需求,进一步细化各项工作的具体要求,检查其他服务单位的造价文件的组成完整性、电子文件格式是否符合要求、电子版与纸质版是否对应、造价文件报表的规范性', serviceid: -1, order: 147, type: 4 },
|
||||
147: { text: '作为造价咨询服务总体协调单位,负责总体协调其他咨询人或专家团队的工作,确保各方在项目服务中的沟通顺畅,监控造价咨询服务的进展情况,确保各咨询人按时完成工作', serviceid: -1, order: 148, type: 4 },
|
||||
50: { text: '完成工程变更费用的测算,或提出性价比更优的替代方案、材料等意见', serviceid: 12, order: 51, type: 0 },
|
||||
51: { text: '完成新增预算单价或合同单价的计算与核定', serviceid: 12, order: 52, type: 0 },
|
||||
52: { text: '按施工图预算或合同清单方式完成工程变更费用的计算', serviceid: 12, order: 53, type: 0 },
|
||||
53: { text: '完成过程结算,包括建立过程结算多维度清单体系,如:项目清单、工程量清单、分项清单;划分过程结算清单单元;依据经确认的实际完工工程量完成计价计费,完成过程结算文件', serviceid: 12, order: 54, type: 0 },
|
||||
54: { text: '完成价格波动费用的计算或审核', serviceid: 12, order: 55, type: 0 },
|
||||
55: { text: '完成索赔与补偿费用的计算或审核', serviceid: 12, order: 56, type: 0 },
|
||||
56: { text: '完成结算文件及统计技术经济指标,与相关单位核对合同(工程)结算', serviceid: 12, order: 57, type: 0 },
|
||||
57: { text: '完成合同、变更和结算三环节费用的对比,分析合同费用各环节变化原因,提供对比报表和分析报告', serviceid: 12, order: 58, type: 0 },
|
||||
58: { text: '参加与过程结算、工程变更费用确定和合同(工程)结算相关的会议', serviceid: 12, order: 59, type: 0 },
|
||||
59: { text: '现场勘查与测量实际完工工程量', serviceid: 12, order: 60, type: 1 },
|
||||
60: { text: '复核竣工图纸数量与结算数量的差异,提交数量差异报告', serviceid: 12, order: 61, type: 1 },
|
||||
61: { text: '协助委托人对项目工程变更费用审批情况进行清理,编制交工验收造价文件', serviceid: 12, order: 62, type: 1 },
|
||||
62: { text: '协助委托人处理工期索赔、造价费用等费用的争议与纠纷、仲裁与诉讼', serviceid: 12, order: 63, type: 1 },
|
||||
63: { text: '编制项目竣工决算文件', serviceid: 13, order: 64, type: 0 },
|
||||
64: { text: '编制竣工决算备案送审文件', serviceid: 13, order: 65, type: 0 },
|
||||
65: { text: '参与竣工决算相关的会议', serviceid: 13, order: 66, type: 0 },
|
||||
66: { text: '根据审计意见和决算备案意见调整竣工决算文件', serviceid: 13, order: 67, type: 0 },
|
||||
67: { text: '协助委托人编制建设项目造价执行情况报告', serviceid: 13, order: 68, type: 1 },
|
||||
68: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 13, order: 69, type: 1 },
|
||||
69: { text: '根据造价差异分析结果,提出调整工程方案的咨询意见供委托人或关联单位决策', serviceid: 13, order: 70, type: 1 },
|
||||
70: { text: '协助委托人指导参建单位完成与决算相关辅助文件的编制', serviceid: 13, order: 71, type: 1 },
|
||||
71: { text: '协助委托人开展项目造价管理及投资效益后评估', serviceid: 13, order: 72, type: 1 },
|
||||
72: { text: '定期向委托人介绍并解读国家及地方最新发布的关于造价管理有关的法律法规、政策文件和技术标准等信息,针对风险控制方面,提供专业的造价管理建议及实施对策', serviceid: 15, order: 73, type: 2 },
|
||||
73: { text: '为委托人在日常建设、运营及经营管理中遇到的造价编制与审核问题,提供专业咨询建议', serviceid: 15, order: 74, type: 2 },
|
||||
74: { text: '对委托人起草的工程勘察设计、工程监理、施工、物资采购和技术服务等方面的合同与协议文本,从造价控制与投资风险角度提供审阅与修改建议', serviceid: 15, order: 75, type: 2 },
|
||||
75: { text: '审查各类合同履行情况时,对识别出的潜在漏洞或风险,提出相应的修改建议或补救措施', serviceid: 15, order: 76, type: 2 },
|
||||
76: { text: '对委托人提供的造价管理相关的制度,进行审阅与修改,或提供专业咨询建议', serviceid: 15, order: 77, type: 2 },
|
||||
77: { text: '针对具体建设工程项目的重大投资决策,出具独立的造价专业意见;或根据委托人的要求, 对造价争议事项提供专业依据并出具顾问报告', serviceid: 15, order: 78, type: 3 },
|
||||
78: { text: '为委托人起草与工程建设相关的合同、协议等文书,包括且不限于工程勘察设计、监理、施 工、采购及技术服务等领域', serviceid: 15, order: 79, type: 3 },
|
||||
79: { text: '独立对各类合同的履行情况进行审查,识别潜在漏洞或风险,并提出系统性的修改建议或补 救方案', serviceid: 15, order: 80, type: 3 },
|
||||
80: { text: '为委托人制订或修订与造价管理相关的制度', serviceid: 15, order: 81, type: 3 },
|
||||
81: { text: '受托处理涉及委托人的造价纠纷、仲裁及诉讼案件,提供专业意见与支持,并出具咨询顾问 报告', serviceid: 15, order: 82, type: 3 },
|
||||
82: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 16, order: 83, type: 0 },
|
||||
83: { text: '完成各阶段文件的起草、修订、调整与校对工作;整理并分析各阶段的 反馈意见、完成反馈意见的采纳情况,编制必要的采纳情况报告或说明;完成工作报告', serviceid: 16, order: 84, type: 0 },
|
||||
84: { text: '完成评审汇报材料,编写报告;整理并汇总评审意见、复核评审意见,完成评审 意见的采纳及反馈工作', serviceid: 16, order: 85, type: 0 },
|
||||
85: { text: '负责评审会议的全过程组织工作, 包括准备会议材料、发出通知、组织召开并主持评审会。', serviceid: 16, order: 86, type: 1 },
|
||||
86: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 17, order: 87, type: 0 },
|
||||
87: { text: '完成工作大纲和编写大纲;开展专项研究工作,完成各阶段研究报告的起 草、修订、调整与校对工作;分研究阶段进行技术报告的编写、修改、调整以及相应的报审 报批工作,包括大纲与成果的评审及验收等;整理并分析各阶段的反馈意见、完成反馈意见 的采纳情况,编制必要的采纳情况报告或说明;编制工作报告', serviceid: 17, order: 88, type: 0 },
|
||||
88: { text: '确定测试与验证项目,完成测试与验证工作,完成测试与验证报告', serviceid: 17, order: 89, type: 0 },
|
||||
89: { text: '完成评审与验收所需汇报材料,编写报告;整理并汇总评审意见、复核评审意见,完成评审意见的采纳及反馈工作', serviceid: 17, order: 90, type: 0 },
|
||||
90: { text: '编写培训与宣贯相关的材料,组织研究成果的宣贯与推广工作', serviceid: 17, order: 91, type: 1 },
|
||||
91: { text: '负责评审与验收会议的全过程组织工作,包括准备会议材料、发出通知、 组织召开并主持评审与验收会议', serviceid: 17, order: 92, type: 1 },
|
||||
92: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 18, order: 93, type: 0 },
|
||||
93: { text: '制定工作计划并完成编制大纲,编制大纲包括工作大纲和编写大纲', serviceid: 18, order: 94, type: 0 },
|
||||
94: { text: '完成数据采集与测定工作,编制相应的数据采集与测定报告', serviceid: 18, order: 95, type: 0 },
|
||||
95: { text: '完成数据整理与分析工作,编制相应的数据分析报告,整理支撑性资料', serviceid: 18, order: 96, type: 0 },
|
||||
96: { text: '完成定额文本的起草、修改及调整工作;完成各阶段研究报告的起草、修订、 调整与校对工作,编制必要的采纳情况报告或说明;编制工作报告或报批报告', serviceid: 18, order: 97, type: 0 },
|
||||
97: { text: '确定测试与验证项目,完成测试与验证工作,完成测试与验证报告', serviceid: 18, order: 98, type: 0 },
|
||||
98: { text: '完成评审与验收所需汇报材料,编写报告;整理并汇总评审意见、复核评 审意见,完成评审意见的采纳及反馈工作', serviceid: 18, order: 99, type: 0 },
|
||||
99: { text: '编写培训与宣贯相关的材料,组织定额成果的培训与宣贯工作', serviceid: 18, order: 100, type: 1 },
|
||||
100: { text: '负责评审与验收会议的全过程组织工作,包括准备会议材料、发出通知、 组织召开并主持评审与验收会议', serviceid: 18, order: 101, type: 1 },
|
||||
101: { text: '开展各类造价信息的搜集、筛选与整理工作', serviceid: 19, order: 102, type: 0 },
|
||||
102: { text: '对整理后的信息与数据进行对比与分析', serviceid: 19, order: 103, type: 0 },
|
||||
103: { text: '依据分析结果,对特定价格或费用进行评估与论证,编制咨询报告', serviceid: 19, order: 104, type: 0 },
|
||||
104: { text: '预测未来特定时期的价格或费用,编制预测报告', serviceid: 19, order: 105, type: 1 },
|
||||
105: { text: '研究价格或费用的长期变动规律,编制趋势分析报告', serviceid: 19, order: 106, type: 1 },
|
||||
106: { text: '根据委托人的目标,确定鉴定工作范围', serviceid: 20, order: 107, type: 0 },
|
||||
107: { text: '收集与复核鉴定资料', serviceid: 20, order: 108, type: 0 },
|
||||
108: { text: '组织或参与必要的现场勘查工作', serviceid: 20, order: 109, type: 0 },
|
||||
109: { text: '工程量的计算与工程费用的计价或估价', serviceid: 20, order: 110, type: 0 },
|
||||
110: { text: '对争议焦点进行鉴定,提出解决争议的意见', serviceid: 20, order: 111, type: 0 },
|
||||
111: { text: '回复各方当事人的质证意见', serviceid: 20, order: 112, type: 0 },
|
||||
112: { text: '编制并出具造价鉴定报告', serviceid: 20, order: 113, type: 0 },
|
||||
113: { text: '组织或参与现场数据复测或特殊鉴定', serviceid: 20, order: 114, type: 1 },
|
||||
114: { text: '与当事人相关方进行必要的造价数据核对', serviceid: 20, order: 115, type: 1 },
|
||||
115: { text: '出庭就鉴定报告及相关专业问题接受质证', serviceid: 20, order: 116, type: 1 },
|
||||
116: { text: '编制影响工程成本的资源要素清单', serviceid: 21, order: 117, type: 0 },
|
||||
117: { text: '调查人工、材料、设备和施工机械的市场供应情况,主要是价格情况', serviceid: 21, order: 118, type: 0 },
|
||||
118: { text: '测算、分析、计算工程成本费用,结合其他相关因素,编制并出具工程成本测算报告', serviceid: 21, order: 119, type: 0 },
|
||||
119: { text: '参加与工程成本测算相关的会议', serviceid: 21, order: 120, type: 0 },
|
||||
120: { text: '审核及分析资源配置、施工措施、施工管理方案的经济合理性', serviceid: 21, order: 121, type: 1 },
|
||||
121: { text: '对比工程成本与投资估算、设计概算、施工图预算、工程量清单预算、合同价,并分析费用差异,提交分析报告', serviceid: 21, order: 122, type: 1 },
|
||||
122: { text: '编制影响工程成本的资源要素清单或成本组成科目', serviceid: 22, order: 123, type: 0 },
|
||||
123: { text: '收集人工、材料、设备、施工机械、措施、税费等实际发生的费用', serviceid: 22, order: 124, type: 0 },
|
||||
124: { text: '统计与汇总、拆解与合并、对比与分析工程成本各项费用,编制并出具工程成本核算报告', serviceid: 22, order: 125, type: 0 },
|
||||
125: { text: '参加与工程成本核算相关的会议', serviceid: 22, order: 126, type: 0 },
|
||||
126: { text: '分析实际工程成本与测算工程成本差异的合理性,提交分析报告', serviceid: 22, order: 127, type: 1 },
|
||||
127: { text: '归纳与总结实际工程成本与测算工程成本的差异原因,提出改进建议', serviceid: 22, order: 128, type: 1 },
|
||||
128: { text: '依据本项目在招标阶段确认的设计图纸工程量,结合工程量清单计量支付规则,对设计工程量进行复核,包括构件工程量、明细表工程量和汇总表工程量,同时与相关方核对工程量', serviceid: 23, order: 129, type: 0 },
|
||||
129: { text: '依据核对后确认的设计图纸数量,细化与合并招标工程量清单或合同工程量清单,建立各维度清单间的数据链接,与相关单位完成核对与确认', serviceid: 23, order: 130, type: 0 },
|
||||
130: { text: '依据确定的招标工程量清单或合同工程量清单,拆解与合并相应的清单费用,与相关单位完成核对与确认', serviceid: 23, order: 131, type: 1 },
|
||||
131: { text: '现场勘查与测量现场实施工程量', serviceid: 23, order: 132, type: 1 },
|
||||
132: { text: '完成设计图纸所载数量与复核后的数量进行对比与分析,提供对比分析报告', serviceid: 23, order: 133, type: 1 },
|
||||
133: { text: '参加计算工程量相关的会议', serviceid: 23, order: 134, type: 0 },
|
||||
134: { text: '协助委托人处理涉及工程数量的争议、纠纷、仲裁或诉讼事务', serviceid: 23, order: 135, type: 1 },
|
||||
135: { text: '完成工程变更费用的测算', serviceid: 24, order: 136, type: 0 },
|
||||
136: { text: '完成新增预算单价或合同单价的计算与核定', serviceid: 24, order: 137, type: 0 },
|
||||
137: { text: '按施工图预算或合同清单方式完成工程变更费用的计算', serviceid: 24, order: 138, type: 0 },
|
||||
138: { text: '完成价格波动引起的价差费用的计算', serviceid: 24, order: 139, type: 1 },
|
||||
139: { text: '完成索赔与补偿费用的计算,如加速施工费用、暂停施工补偿等', serviceid: 24, order: 140, type: 1 },
|
||||
140: { text: '协助委托人处理涉及工程变更费用的争议与纠纷、仲裁与诉讼', serviceid: 24, order: 141, type: 1 },
|
||||
141: { text: '分析、论证及评估影响投资估算的主要因素', serviceid: 25, order: 142, type: 0 },
|
||||
142: { text: '根据分析论证结果,完成调整后投资估算文件及主要技术经济指标,与相关单位核对数据', serviceid: 25, order: 143, type: 0 },
|
||||
143: { text: '完成调整后投资估算与原批复估算的对比及原因分析', serviceid: 25, order: 144, type: 0 },
|
||||
144: { text: '出席与投资估算调整工作相关的会议', serviceid: 25, order: 145, type: 0 },
|
||||
145: { text: '协助调规报告编制单位完成报告的造价专业部分的内容', serviceid: 25, order: 146, type: 1 },
|
||||
146: { text: '分析、论证及评估影响设计概算的主要因素', serviceid: 26, order: 147, type: 0 },
|
||||
147: { text: '根据分析论证结果,编制调整后设计概算文件及主要技术经济指标,与相关单位核对数据', serviceid: 26, order: 148, type: 0 },
|
||||
148: { text: '完成调整后概算与原批复概算的对比及原因分析,完成调整概算报告', serviceid: 26, order: 149, type: 0 },
|
||||
149: { text: '出席与概算调整工作相关的会议', serviceid: 26, order: 150, type: 0 },
|
||||
150: { text: '协助委托人开展项目预估决算费用的测算、估算工作', serviceid: 26, order: 151, type: 1 },
|
||||
151: { text: '检查相关单位对工程造价管理法律法规、规章制度以及工程造价依据的执行情况', serviceid: 27, order: 152, type: 0 },
|
||||
152: { text: '检查各阶段造价文件编制、审查(批)或备案以及对批复意见的落实情况', serviceid: 27, order: 153, type: 0 },
|
||||
153: { text: '检查造价管理台账和计量支付制度的建立与执行、造价全过程管理与控制情况', serviceid: 27, order: 154, type: 0 },
|
||||
154: { text: '检查工程变更管理情况', serviceid: 27, order: 155, type: 0 },
|
||||
155: { text: '检查项目造价信息的收集、分析及报送情况', serviceid: 27, order: 156, type: 0 },
|
||||
156: { text: '检查项目从业单位的造价人员执业情况', serviceid: 27, order: 157, type: 0 },
|
||||
157: { text: '协助委托人进行现场核查、资料抽检和台账复核工作', serviceid: 27, order: 158, type: 0 },
|
||||
158: { text: '协助委托人整理检查结果和起草检查报告等工作', serviceid: 27, order: 159, type: 0 },
|
||||
159: { text: '作为造价咨询服务总体协调单位,依据造价技术标准的具体条款或委托方的个性化需求,进一步细化各项工作的具体要求,检查其他服务单位的造价文件的组成完整性、电子文件格式是否符合要求、电子版与纸质版是否对应、造价文件报表的规范性', serviceid: -1, order: 160, type: 4 },
|
||||
160: { text: '作为造价咨询服务总体协调单位,负责总体协调其他咨询人或专家团队的工作,确保各方在项目服务中的沟通顺畅,监控造价咨询服务的进展情况,确保各咨询人按时完成工作', serviceid: -1, order: 161, type: 4 },
|
||||
}
|
||||
|
||||
//工作内容树形关系表
|
||||
export const wholeProcessTasks = [
|
||||
{
|
||||
fid: 0,
|
||||
industry: 0,
|
||||
sid: [6, 7, 8, 9, 11, 13],
|
||||
fid: 0, //seviceid
|
||||
industry: 0,//行业
|
||||
sid: [6, 7, 8, 9, 11, 13], //seviceidid
|
||||
},
|
||||
{
|
||||
fid: 0,
|
||||
|
||||
@ -162,7 +162,6 @@ html {
|
||||
}
|
||||
|
||||
|
||||
|
||||
.zxfw-action-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -170,7 +169,7 @@ html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ag-cell{display: flex;}
|
||||
.zxfw-action-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user