优化目录
This commit is contained in:
1 parent
4e343cccf1
commit
2b18fe9991
46 files changed
+1061
-1308
No files matched your search
+24
-1
@@ -1,7 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import HomeEntryView from '@/components/views/HomeEntryView.vue'
|
||||
import Tab from '@/layout/tab.vue'
|
||||
import { waitForHydration } from '@/pinia/Plugin/indexdb'
|
||||
|
||||
const tabStore = useTabStore()
|
||||
const isReady = ref(false)
|
||||
|
||||
const showHomeEntry = computed(() => !tabStore.hasCompletedSetup)
|
||||
|
||||
const handleImportComplete = () => {
|
||||
tabStore.hasCompletedSetup = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('home-import-selected', handleImportComplete)
|
||||
waitForHydration('tabs').then(() => {
|
||||
isReady.value = true
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tab />
|
||||
<template v-if="isReady">
|
||||
<HomeEntryView v-if="showHomeEntry" />
|
||||
<Tab v-else />
|
||||
</template>
|
||||
</template>
|
||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
|
||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
||||
import { additionalWorkList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
const props = defineProps<{
|
||||
+3
-3
@@ -148,7 +148,7 @@ const saveForm = async (force = false) => {
|
||||
}
|
||||
|
||||
const applyRateInput = () => {
|
||||
const next = parseNumberOrNull(rateInput.value, { sanitize: true, precision: 3 })
|
||||
const next = parseNumberOrNull(rateInput.value, { sanitize: true, precision: 2 })
|
||||
rate.value = next
|
||||
rateInput.value = next == null ? '' : String(next)
|
||||
}
|
||||
@@ -209,8 +209,8 @@ onBeforeUnmount(() => {
|
||||
</label>
|
||||
|
||||
<label class="space-y-1.5">
|
||||
<div class="text-xs text-muted-foreground">费率(可编辑,三位小数)</div>
|
||||
<input v-model="rateInput" type="text" inputmode="decimal" placeholder="请输入费率,建议0.01 ~ 0.05"
|
||||
<div class="text-xs text-muted-foreground">费率(%)</div>
|
||||
<input v-model="rateInput" type="text" inputmode="decimal" placeholder="请输入费率,建议1 ~ 5"
|
||||
class="h-9 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30"
|
||||
@blur="applyRateInput" />
|
||||
</label>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
|
||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
||||
import { reserveList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -180,7 +180,14 @@ const scheduleRefreshContractBudget = () => {
|
||||
|
||||
// 2. 定义分类项的 TS 类型(核心:明确 categories 结构)
|
||||
interface XmCategoryItem {
|
||||
key: string;
|
||||
key:
|
||||
| 'info'
|
||||
| 'consult-category-factor'
|
||||
| 'major-factor'
|
||||
| 'work-grid'
|
||||
| 'contract'
|
||||
| 'additional-work-fee'
|
||||
| 'reserve-fee';
|
||||
label: string;
|
||||
component: Component; // 标记为 Vue 组件类型
|
||||
}
|
||||
@@ -191,7 +198,7 @@ const htView = markRaw(
|
||||
name: 'HtInfoWithProps',
|
||||
setup() {
|
||||
const AsyncHtInfo = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/htInfo.vue'),
|
||||
loader: () => import('@/components/ht/htInfo.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 htInfo 组件失败:', err);
|
||||
}
|
||||
@@ -210,7 +217,7 @@ const zxfwView = markRaw(
|
||||
name: 'ZxFwWithProps',
|
||||
setup() {
|
||||
const AsyncZxFw = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/zxFw.vue'),
|
||||
loader: () => import('@/components/ht/zxFw.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 zxFw 组件失败:', err);
|
||||
}
|
||||
@@ -229,7 +236,7 @@ const consultCategoryFactorView = markRaw(
|
||||
name: 'HtConsultCategoryFactorWithProps',
|
||||
setup() {
|
||||
const AsyncHtConsultCategoryFactor = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/HtConsultCategoryFactor.vue'),
|
||||
loader: () => import('@/components/ht/HtConsultCategoryFactor.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
||||
}
|
||||
@@ -248,7 +255,7 @@ const majorFactorView = markRaw(
|
||||
name: 'HtMajorFactorWithProps',
|
||||
setup() {
|
||||
const AsyncHtMajorFactor = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/HtMajorFactor.vue'),
|
||||
loader: () => import('@/components/ht/HtMajorFactor.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtMajorFactor 组件失败:', err);
|
||||
}
|
||||
@@ -262,12 +269,14 @@ const majorFactorView = markRaw(
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
|
||||
const additionalWorkFeeView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtAdditionalWorkFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/HtAdditionalWorkFee.vue'),
|
||||
loader: () => import('@/components/ht/HtAdditionalWorkFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
|
||||
}
|
||||
@@ -282,7 +291,7 @@ const reserveFeeView = markRaw(
|
||||
name: 'HtReserveFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtReserveFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/HtReserveFee.vue'),
|
||||
loader: () => import('@/components/ht/HtReserveFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtReserveFee 组件失败:', err);
|
||||
}
|
||||
@@ -297,11 +306,11 @@ const xmCategories: XmCategoryItem[] = [
|
||||
{ key: 'info', label: '规模信息', component: htView },
|
||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||
|
||||
|
||||
];
|
||||
|
||||
watch(budgetRefreshSignature, (next, prev) => {
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'
|
||||
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -32,7 +32,7 @@ import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue }
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
|
||||
import ServiceCheckboxSelector from '@/components/shared/ServiceCheckboxSelector.vue'
|
||||
|
||||
interface ServiceItem {
|
||||
id: string
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HourlyFeeGrid from '@/components/common/HourlyFeeGrid.vue'
|
||||
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -10,7 +10,7 @@ import { parseNumberOrNull } from '@/lib/number'
|
||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,320 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type {
|
||||
CellValueChangedEvent,
|
||||
ColDef,
|
||||
GridApi,
|
||||
GridReadyEvent,
|
||||
ICellRendererParams,
|
||||
ValueFormatterParams
|
||||
} from 'ag-grid-community'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { myTheme } from '@/lib/diyAgGridOptions'
|
||||
import { workList } from '@/sql'
|
||||
import type { WorkType } from '@/sql'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
|
||||
interface WorkContentRow {
|
||||
id: string
|
||||
content: string
|
||||
type: WorkType
|
||||
remark: string
|
||||
checked: boolean
|
||||
custom: boolean
|
||||
path: string[]
|
||||
}
|
||||
|
||||
interface WorkContentState {
|
||||
detailRows: WorkContentRow[]
|
||||
}
|
||||
|
||||
// dictMode: 'service' 按serviceId筛选workList;'additional' 取serviceid=-1的数据;'none' 不加载词典
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
storageKey: string
|
||||
serviceId?: number | string
|
||||
dictMode?: 'service' | 'additional' | 'none'
|
||||
}>(), {
|
||||
title: '工作内容',
|
||||
dictMode: 'none'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
checkedChange: [value: string[]]
|
||||
}>()
|
||||
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const gridApi = ref<GridApi<WorkContentRow> | null>(null)
|
||||
const rowData = ref<WorkContentRow[]>([])
|
||||
|
||||
const TYPE_LABEL_MAP: Record<number, WorkType> = {
|
||||
0: '基本工作',
|
||||
1: '可选工作',
|
||||
2: '日常顾问',
|
||||
3: '专项顾问',
|
||||
4: '附加工作'
|
||||
}
|
||||
|
||||
const buildDefaultRowsFromDict = (): WorkContentRow[] => {
|
||||
const rows: WorkContentRow[] = []
|
||||
const entries = Object.values(workList) as Array<{ text: string; serviceid: number; order: number; type: number }>
|
||||
|
||||
let filtered: typeof entries
|
||||
if (props.dictMode === 'service') {
|
||||
const sid = Number(props.serviceId)
|
||||
filtered = entries.filter(e => e.serviceid === sid)
|
||||
} else if (props.dictMode === 'additional') {
|
||||
filtered = entries.filter(e => e.serviceid === -1)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
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] ?? '基本工作'
|
||||
rows.push({
|
||||
id: `dict-${entry.order}`,
|
||||
content,
|
||||
type: typeLabel,
|
||||
remark: '',
|
||||
checked: false,
|
||||
custom: false,
|
||||
path: [typeLabel, content]
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
const checkedIds = computed(() =>
|
||||
rowData.value.filter(item => item.checked).map(item => item.id)
|
||||
)
|
||||
|
||||
// 导出用:自定义内容全部包含,默认词典内容只含勾选的
|
||||
const selectedTexts = computed(() =>
|
||||
rowData.value
|
||||
.filter(item => item.custom || item.checked)
|
||||
.map(item => item.content)
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
defineExpose({ selectedTexts })
|
||||
|
||||
const emitCheckedChange = () => {
|
||||
emit('checkedChange', [...checkedIds.value])
|
||||
}
|
||||
|
||||
const saveToStore = () => {
|
||||
const payload: WorkContentState = {
|
||||
detailRows: rowData.value.map(item => ({ ...item }))
|
||||
}
|
||||
zxFwPricingStore.setKeyState(props.storageKey, payload)
|
||||
emitCheckedChange()
|
||||
}
|
||||
|
||||
const loadFromStore = async () => {
|
||||
const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey)
|
||||
if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) {
|
||||
rowData.value = state.detailRows.map(item => ({
|
||||
...item,
|
||||
type: item.custom ? '自定义' : (item.type || '基本工作'),
|
||||
path: Array.isArray(item.path) && item.path.length ? item.path : ['自定义', item.content || '未命名']
|
||||
})) as WorkContentRow[]
|
||||
} else {
|
||||
rowData.value = buildDefaultRowsFromDict()
|
||||
saveToStore()
|
||||
}
|
||||
emitCheckedChange()
|
||||
}
|
||||
|
||||
const handleCheckedToggle = (id: string, checked: boolean) => {
|
||||
const target = rowData.value.find(item => item.id === id)
|
||||
if (!target) return
|
||||
target.checked = checked
|
||||
gridApi.value?.refreshCells({ force: true })
|
||||
saveToStore()
|
||||
}
|
||||
|
||||
const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
|
||||
const data = params.data
|
||||
if (!data) return ''
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'work-content-cell'
|
||||
// 自定义行不显示 checkbox,直接显示文本;空时显示 placeholder
|
||||
if (data.custom) {
|
||||
const label = document.createElement('span')
|
||||
if (!data.content) {
|
||||
label.className = 'work-content-placeholder'
|
||||
label.textContent = '点击输入工作内容'
|
||||
} else {
|
||||
label.className = 'work-content-text'
|
||||
label.textContent = data.content
|
||||
}
|
||||
wrapper.appendChild(label)
|
||||
return wrapper
|
||||
}
|
||||
const checkbox = document.createElement('input')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.className = 'work-content-check'
|
||||
checkbox.checked = Boolean(data.checked)
|
||||
checkbox.addEventListener('change', () => {
|
||||
handleCheckedToggle(data.id, checkbox.checked)
|
||||
})
|
||||
const label = document.createElement('span')
|
||||
label.className = 'work-content-text'
|
||||
label.textContent = String(data.content || '')
|
||||
wrapper.appendChild(checkbox)
|
||||
wrapper.appendChild(label)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<WorkContentRow>[] = [
|
||||
{
|
||||
headerName: '序号',
|
||||
minWidth: 60,
|
||||
width: 70,
|
||||
pinned: 'left',
|
||||
suppressMovable: true,
|
||||
editable: false,
|
||||
valueGetter: params => (params.node?.rowIndex ?? 0) + 1
|
||||
},
|
||||
{
|
||||
headerName: '工作内容',
|
||||
field: 'content',
|
||||
minWidth: 320,
|
||||
flex: 2,
|
||||
editable: params => Boolean(params.data?.custom),
|
||||
cellClass: params => (params.data?.custom ? 'editable-cell-line' : ''),
|
||||
valueParser: params => String(params.newValue || '').trim(),
|
||||
cellRenderer: contentCellRenderer
|
||||
},
|
||||
{
|
||||
headerName: '工作类型',
|
||||
field: 'type',
|
||||
minWidth: 100,
|
||||
width: 120,
|
||||
editable: false,
|
||||
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) => String(params.value || '')
|
||||
},
|
||||
{
|
||||
headerName: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
flex: 1.2,
|
||||
editable: true,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||
cellClass: 'editable-cell-line remark-wrap-cell',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
},
|
||||
valueFormatter: params => params.value || '点击输入'
|
||||
}
|
||||
]
|
||||
|
||||
const addCustomRow = () => {
|
||||
const ts = Date.now()
|
||||
rowData.value.push({
|
||||
id: `custom-${ts}`,
|
||||
content: '',
|
||||
type: '自定义' as WorkType,
|
||||
remark: '',
|
||||
checked: false,
|
||||
custom: true,
|
||||
path: ['自定义', `自定义-${ts}`]
|
||||
})
|
||||
saveToStore()
|
||||
}
|
||||
|
||||
const onGridReady = (event: GridReadyEvent<WorkContentRow>) => {
|
||||
gridApi.value = event.api
|
||||
}
|
||||
|
||||
const onCellValueChanged = (event: CellValueChangedEvent<WorkContentRow>) => {
|
||||
const row = event.data
|
||||
if (!row) return
|
||||
if (event.colDef.field === 'content' && row.custom) {
|
||||
row.path = ['自定义', row.content || `自定义-${row.id}`]
|
||||
}
|
||||
if (event.colDef.field === 'type' && row.custom) {
|
||||
row.type = '自定义'
|
||||
}
|
||||
saveToStore()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadFromStore()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
saveToStore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full min-h-0 xmMx">
|
||||
<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
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="rowData"
|
||||
:columnDefs="columnDefs"
|
||||
:theme="myTheme"
|
||||
:getRowId="(params: { data: WorkContentRow }) => params.data.id"
|
||||
:animateRows="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:singleClickEdit="true"
|
||||
:stopEditingWhenCellsLoseFocus="true"
|
||||
:defaultColDef="{ resizable: true, sortable: false, filter: false }"
|
||||
:suppressColumnVirtualisation="false"
|
||||
:suppressRowVirtualisation="false"
|
||||
@grid-ready="onGridReady"
|
||||
@cell-value-changed="onCellValueChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.work-content-placeholder) {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.work-content-cell) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.work-content-text) {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.work-content-check) {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
File renamed without changes.
File renamed without changes.
@@ -4,7 +4,15 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import { Calculator, Check, ChevronDown, Download, FolderKanban, X, Zap } from 'lucide-vue-next'
|
||||
import {
|
||||
Calculator,
|
||||
Check,
|
||||
ChevronDown,
|
||||
Download,
|
||||
FolderKanban,
|
||||
X,
|
||||
Zap
|
||||
} from 'lucide-vue-next'
|
||||
import { industryTypeList } from '@/sql'
|
||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
||||
import {
|
||||
@@ -57,7 +65,7 @@ interface ProjectInfoState {
|
||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||
const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
||||
const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
||||
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
||||
const DEFAULT_PROJECT_NAME = 'xxx 造价咨询服务'
|
||||
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
|
||||
|
||||
const tabStore = useTabStore()
|
||||
@@ -69,6 +77,10 @@ const quickDialogOpen = ref(false)
|
||||
const quickIndustry = ref(String(industryTypeList[0]?.id || ''))
|
||||
const quickContractName = ref(QUICK_CONTRACT_FALLBACK_NAME)
|
||||
const quickSubmitting = ref(false)
|
||||
const homeImportInputRef = ref<HTMLInputElement | null>(null)
|
||||
const projectIconAvailable = ref(false)
|
||||
const quickIconAvailable = ref(false)
|
||||
const importIconAvailable = ref(false)
|
||||
|
||||
const getTodayDateString = () => {
|
||||
const now = new Date()
|
||||
@@ -83,8 +95,9 @@ const enterProjectCalc = () => {
|
||||
tabStore.enterWorkspace({
|
||||
id: PROJECT_TAB_ID,
|
||||
title: '项目计算',
|
||||
componentName: PROJECT_TAB_ID
|
||||
componentName: 'XmView'
|
||||
})
|
||||
tabStore.hasCompletedSetup = true
|
||||
}
|
||||
|
||||
const loadProjectDefaults = async () => {
|
||||
@@ -187,7 +200,6 @@ const confirmQuickCalc = async () => {
|
||||
QUICK_MAJOR_FACTOR_KEY
|
||||
)
|
||||
|
||||
writeWorkspaceMode('quick')
|
||||
tabStore.enterWorkspace({
|
||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
||||
title: contractName,
|
||||
@@ -201,6 +213,7 @@ const confirmQuickCalc = async () => {
|
||||
projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY
|
||||
}
|
||||
})
|
||||
tabStore.hasCompletedSetup = true
|
||||
} finally {
|
||||
quickSubmitting.value = false
|
||||
quickDialogOpen.value = false
|
||||
@@ -219,6 +232,10 @@ const handleHomeImportChange = (event: Event) => {
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const openHomeImport = () => {
|
||||
homeImportInputRef.value?.click()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadProjectDefaults()
|
||||
void loadQuickDefaults()
|
||||
@@ -226,104 +243,133 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input id="home-import-input" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" />
|
||||
<div class="flex min-h-full items-center justify-center px-4 py-8">
|
||||
<div class="w-full max-w-5xl">
|
||||
<div class="mx-auto max-w-2xl text-center">
|
||||
<p class="text-sm font-medium tracking-[0.28em] text-muted-foreground">JGJS 2026</p>
|
||||
<h1 class="mt-4 text-4xl font-semibold tracking-tight text-foreground">选择计算入口</h1>
|
||||
<p class="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
首页支持三种入口:继续项目计算、快速单合同测算,或直接导入已有数据包。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 grid items-stretch gap-5 md:grid-cols-3">
|
||||
<Card
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="flex h-full cursor-pointer flex-col border-border/70 bg-card/90 shadow-[0_18px_48px_rgba(15,23,42,0.08)] transition hover:-translate-y-0.5 hover:shadow-[0_22px_56px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@click="openProjectCalc"
|
||||
@keydown.enter.prevent="openProjectCalc"
|
||||
@keydown.space.prevent="openProjectCalc"
|
||||
<input ref="homeImportInputRef" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" />
|
||||
<div class="flex min-h-full items-center justify-center px-4 py-8 lg:py-10">
|
||||
<div class="w-full max-w-[1240px]">
|
||||
<div class="grid items-stretch gap-6 lg:grid-cols-[320px_1fr]">
|
||||
<div
|
||||
class="relative overflow-hidden rounded-2xl bg-[linear-gradient(145deg,#041c5f_0%,#0b2e86_50%,#354cc7_100%)] p-8 text-white shadow-[0_24px_60px_rgba(11,46,134,0.45)]"
|
||||
>
|
||||
<CardHeader class="flex min-h-[184px] flex-col space-y-4 pb-4">
|
||||
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900 text-white">
|
||||
<FolderKanban class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="min-h-[88px]">
|
||||
<CardTitle class="text-2xl">项目计算</CardTitle>
|
||||
<CardDescription class="mt-2 text-sm leading-6">
|
||||
继续使用当前项目卡片、流程线、合同段管理和整项目导入导出能力。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-auto flex flex-1 flex-col justify-end space-y-4">
|
||||
<div class="flex min-h-[72px] items-center rounded-2xl border border-border/60 bg-muted/35 p-4 text-sm text-muted-foreground">
|
||||
适合多合同段、项目级系数维护、整项目报表和批量导入导出。
|
||||
</div>
|
||||
<Button class="w-full justify-between" @click.stop="openProjectCalc">
|
||||
进入项目计算
|
||||
<Calculator class="h-4 w-4" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="pointer-events-none absolute -right-20 -top-16 h-56 w-56 rounded-full bg-white/12 blur-2xl" />
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(140deg,rgba(255,255,255,0.06)_0,rgba(255,255,255,0.06)_1px,transparent_1px,transparent_20px)]"
|
||||
/>
|
||||
<div class="relative inline-flex h-12 w-12 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/35">
|
||||
<Calculator class="h-6 w-6" />
|
||||
</div>
|
||||
<h2 class="relative mt-10 text-3xl font-semibold leading-tight tracking-tight">智能预算一键生成-></h2>
|
||||
<p class="relative mt-3 text-base text-blue-100">助力《规范》高效落地</p>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="flex h-full cursor-pointer flex-col border-border/70 bg-[linear-gradient(135deg,rgba(15,23,42,0.05),rgba(148,163,184,0.12))] shadow-[0_18px_48px_rgba(15,23,42,0.08)] transition hover:-translate-y-0.5 hover:shadow-[0_22px_56px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@click="openQuickCalcDialog"
|
||||
@keydown.enter.prevent="openQuickCalcDialog"
|
||||
@keydown.space.prevent="openQuickCalcDialog"
|
||||
>
|
||||
<CardHeader class="flex min-h-[184px] flex-col space-y-4 pb-4">
|
||||
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500 text-slate-950">
|
||||
<Zap class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="min-h-[88px]">
|
||||
<CardTitle class="text-2xl">快速计算</CardTitle>
|
||||
<CardDescription class="mt-2 text-sm leading-6">
|
||||
先填写工程行业和合同名称,再直接进入单合同预算费用测算页面。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-auto flex flex-1 flex-col justify-end space-y-4">
|
||||
<div class="flex min-h-[72px] items-center rounded-2xl border border-border/60 bg-background/80 p-4 text-sm text-muted-foreground">
|
||||
适合快速试算、单合同预算复核和不需要项目级合同段管理的场景。
|
||||
</div>
|
||||
<Button variant="outline" class="w-full justify-between" @click.stop="openQuickCalcDialog">
|
||||
进入快速计算
|
||||
<Zap class="h-4 w-4" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-semibold tracking-tight text-slate-900">计算入口</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">支持三种计算方式:项目计算 · 单项速算 · 导入数据</p>
|
||||
</div>
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="group flex cursor-pointer flex-col rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_20px_45px_rgba(15,23,42,0.16)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
|
||||
@click="openProjectCalc"
|
||||
@keydown.enter.prevent="openProjectCalc"
|
||||
@keydown.space.prevent="openProjectCalc"
|
||||
>
|
||||
<CardHeader class="space-y-3 p-0">
|
||||
<div>
|
||||
<CardTitle class="text-2xl font-semibold text-slate-900">项目预算</CardTitle>
|
||||
<CardDescription class="mt-2 text-sm leading-6 text-slate-500">
|
||||
适用:多合同段、项目级整体计算
|
||||
<br>
|
||||
操作:支持当前项目及合同段卡片和流程线,可导出/导入完整项目数据
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-auto flex flex-col items-center p-0 pt-8">
|
||||
<div
|
||||
class="mt-4 inline-flex h-16 w-16 items-center justify-center rounded-full border border-blue-100 bg-[radial-gradient(circle_at_30%_30%,#ffffff,#eef4ff)] shadow-[0_10px_24px_rgba(27,76,176,0.18)]"
|
||||
>
|
||||
<img
|
||||
v-if="projectIconAvailable"
|
||||
:src="'/image_0.png'"
|
||||
alt="项目预算"
|
||||
class="h-8 w-8 object-contain"
|
||||
@error="projectIconAvailable = false"
|
||||
>
|
||||
<FolderKanban v-else class="h-7 w-7 text-blue-700" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<label for="home-import-input" class="block h-full cursor-pointer">
|
||||
<Card
|
||||
class="flex h-full flex-col border-border/70 bg-[linear-gradient(135deg,rgba(14,116,144,0.08),rgba(186,230,253,0.26))] shadow-[0_18px_48px_rgba(15,23,42,0.08)] transition hover:-translate-y-0.5 hover:shadow-[0_22px_56px_rgba(15,23,42,0.12)]"
|
||||
>
|
||||
<CardHeader class="flex min-h-[184px] flex-col space-y-4 pb-4">
|
||||
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-cyan-600 text-white">
|
||||
<Download class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="min-h-[88px]">
|
||||
<CardTitle class="text-2xl">导入数据</CardTitle>
|
||||
<CardDescription class="mt-2 text-sm leading-6">
|
||||
直接导入已有 `.zw` 数据包,恢复项目计算或快速计算的本地工作区状态。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-auto flex flex-1 flex-col justify-end space-y-4">
|
||||
<div class="flex min-h-[72px] items-center rounded-2xl border border-border/60 bg-background/80 p-4 text-sm text-muted-foreground">
|
||||
适合继续上次工作、切换设备恢复数据,或直接打开别人发来的测算包。
|
||||
</div>
|
||||
<div class="inline-flex h-10 w-full items-center justify-between rounded-md border border-input bg-secondary px-4 text-sm font-medium text-secondary-foreground shadow-xs transition-colors">
|
||||
<span>选择导入文件</span>
|
||||
<Download class="h-4 w-4" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</label>
|
||||
<Card
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="group flex cursor-pointer flex-col rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_20px_45px_rgba(15,23,42,0.16)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
|
||||
@click="openQuickCalcDialog"
|
||||
@keydown.enter.prevent="openQuickCalcDialog"
|
||||
@keydown.space.prevent="openQuickCalcDialog"
|
||||
>
|
||||
<CardHeader class="space-y-3 p-0">
|
||||
<div>
|
||||
<CardTitle class="text-2xl font-semibold text-slate-900">单项速算</CardTitle>
|
||||
<CardDescription class="mt-2 text-sm leading-6 text-slate-500">
|
||||
适用:单合同段预算、单项试算
|
||||
<br>
|
||||
操作:选择行业与咨询类型,输入基数,秒出结果
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-auto flex flex-col items-center p-0 pt-8">
|
||||
<div
|
||||
class="mt-4 inline-flex h-16 w-16 items-center justify-center rounded-full border border-blue-100 bg-[radial-gradient(circle_at_30%_30%,#ffffff,#eef4ff)] shadow-[0_10px_24px_rgba(27,76,176,0.18)]"
|
||||
>
|
||||
<img
|
||||
v-if="quickIconAvailable"
|
||||
:src="'/image_1.png'"
|
||||
alt="单项速算"
|
||||
class="h-8 w-8 object-contain"
|
||||
@error="quickIconAvailable = false"
|
||||
>
|
||||
<Zap v-else class="h-7 w-7 text-amber-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="group flex cursor-pointer flex-col rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_20px_45px_rgba(15,23,42,0.16)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
|
||||
@click="openHomeImport"
|
||||
@keydown.enter.prevent="openHomeImport"
|
||||
@keydown.space.prevent="openHomeImport"
|
||||
>
|
||||
<CardHeader class="space-y-3 p-0">
|
||||
<div>
|
||||
<CardTitle class="text-2xl font-semibold text-slate-900">导入数据</CardTitle>
|
||||
<CardDescription class="mt-2 text-sm leading-6 text-slate-500">
|
||||
适用:续未完成工作、跨设备恢复数据
|
||||
<br>
|
||||
操作:导入".zw"数据包,快速恢复项目计算状态
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-auto flex flex-col items-center p-0 pt-8">
|
||||
<div
|
||||
class="mt-4 inline-flex h-16 w-16 items-center justify-center rounded-full border border-blue-100 bg-[radial-gradient(circle_at_30%_30%,#ffffff,#eef4ff)] shadow-[0_10px_24px_rgba(27,76,176,0.18)]"
|
||||
>
|
||||
<img
|
||||
v-if="importIconAvailable"
|
||||
:src="'/image_2.png'"
|
||||
alt="导入数据"
|
||||
class="h-8 w-8 object-contain"
|
||||
@error="importIconAvailable = false"
|
||||
>
|
||||
<Download v-else class="h-7 w-7 text-cyan-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -398,8 +444,8 @@ onMounted(() => {
|
||||
<div class="w-full max-w-md rounded-xl border bg-background shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b px-5 py-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-foreground">快速计算</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">填写工程行业和合同名称后,直接进入单合同计算页面。</p>
|
||||
<h3 class="text-base font-semibold text-foreground">单项速算</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">输入合同名称后,进入单项速算页面。</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeQuickCalcDialog">
|
||||
<X class="h-4 w-4" />
|
||||
@@ -447,18 +493,16 @@ onMounted(() => {
|
||||
<input
|
||||
v-model="quickContractName"
|
||||
type="text"
|
||||
maxlength="40"
|
||||
class="inline-flex h-11 w-full items-center justify-between rounded-xl border border-border/70 bg-[linear-gradient(180deg,rgba(248,250,252,0.98),rgba(241,245,249,0.92))] px-4 text-sm text-foreground shadow-sm outline-none transition placeholder:text-slate-400 hover:border-border focus-visible:ring-2 focus-visible:ring-slate-300"
|
||||
placeholder="请输入合同名称"
|
||||
class="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@keydown.enter="confirmQuickCalc"
|
||||
/>
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 border-t px-5 py-4">
|
||||
<Button variant="outline" @click="closeQuickCalcDialog">取消</Button>
|
||||
<Button :disabled="quickSubmitting || !quickIndustry || !quickContractName.trim()" @click="confirmQuickCalc">
|
||||
{{ quickSubmitting ? '进入中...' : '进入快速计算' }}
|
||||
<Button :disabled="quickSubmitting || !quickContractName" @click="confirmQuickCalc">
|
||||
{{ quickSubmitting ? '进入中...' : '进入计算' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
:subtitle="`合同ID:${contractIdText}`"
|
||||
:copy-text="contractIdText"
|
||||
:storage-key="activeTypeStorageKey"
|
||||
default-category="rate-fee"
|
||||
default-category="work-content"
|
||||
:categories="categories"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, markRaw, type Component } from 'vue'
|
||||
import { computed, defineComponent, h, markRaw, defineAsyncComponent, type Component } from 'vue'
|
||||
import TypeLine from '@/layout/typeLine.vue'
|
||||
import HtFeeGrid from '@/components/common/HtFeeGrid.vue'
|
||||
import HtFeeRateMethodForm from '@/components/views/HtFeeRateMethodForm.vue'
|
||||
import HourlyFeeGrid from '@/components/common/HourlyFeeGrid.vue'
|
||||
import HtFeeGrid from '@/components/shared/HtFeeGrid.vue'
|
||||
import HtFeeRateMethodForm from '@/components/ht/HtFeeRateMethodForm.vue'
|
||||
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
|
||||
|
||||
interface TypeLineCategoryItem {
|
||||
key: string
|
||||
@@ -92,9 +92,43 @@ const hourlyFeePane = markRaw(
|
||||
})
|
||||
)
|
||||
|
||||
const categories: TypeLineCategoryItem[] = [
|
||||
{ key: 'rate-fee', label: '费率计取', component: rateFeePane },
|
||||
{ key: 'hourly-fee', label: '工时法', component: hourlyFeePane },
|
||||
{ key: 'quantity-unit-price-fee', label: '数量单价', component: quantityUnitPricePane }
|
||||
]
|
||||
const isAdditionalWork = computed(() => props.sourceTitle === '附加工作费')
|
||||
const isReserveFee = computed(() => props.sourceTitle === '预备费')
|
||||
const showWorkContent = computed(() => {
|
||||
if (isReserveFee.value) return false
|
||||
if (isAdditionalWork.value) return props.rowName === '咨询服务协调工作'
|
||||
return true
|
||||
})
|
||||
|
||||
const workContentPane = markRaw(
|
||||
defineComponent({
|
||||
name: 'WorkContentPane',
|
||||
setup() {
|
||||
const AsyncWorkContentGrid = defineAsyncComponent({
|
||||
loader: () => import('@/components/shared/WorkContentGrid.vue'),
|
||||
onError: err => {
|
||||
console.error('加载 WorkContentGrid 组件失败:', err)
|
||||
}
|
||||
})
|
||||
|
||||
return () => h(AsyncWorkContentGrid, {
|
||||
title: '工作内容',
|
||||
storageKey: `work-content-${props.storageKey}-${props.rowId}`,
|
||||
dictMode: isAdditionalWork.value ? 'additional' : 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const categories = computed<TypeLineCategoryItem[]>(() => {
|
||||
const base: TypeLineCategoryItem[] = [
|
||||
{ key: 'rate-fee', label: '费率计取', component: rateFeePane },
|
||||
{ key: 'hourly-fee', label: '工时法', component: hourlyFeePane },
|
||||
{ key: 'quantity-unit-price-fee', label: '数量单价', component: quantityUnitPricePane },
|
||||
]
|
||||
if (showWorkContent.value) {
|
||||
base.push({ key: 'work-content', label: '工作内容', component: workContentPane })
|
||||
}
|
||||
return base
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onMounted } from 'vue'
|
||||
import XmCard from '@/components/views/xmCard.vue'
|
||||
import XmCard from '@/components/xm/xmCard.vue'
|
||||
import { writeWorkspaceMode } from '@/lib/workspace'
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
||||
import TypeLine from '@/layout/typeLine.vue'
|
||||
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
|
||||
interface ServiceMethodType {
|
||||
scale?: boolean | null
|
||||
@@ -59,7 +59,7 @@ const createPricingPane = (name: string) =>
|
||||
name,
|
||||
setup() {
|
||||
const AsyncPricingView = defineAsyncComponent({
|
||||
loader: () => import(`@/components/views/pricingView/${name}.vue`),
|
||||
loader: () => import(`@/components/pricing/${name}.vue`),
|
||||
onError: err => {
|
||||
console.error('加载 PricingMethodView 组件失败:', err)
|
||||
}
|
||||
@@ -89,6 +89,27 @@ const landScaleView = createPricingPane('LandScalePricingPane')
|
||||
const workloadView = createPricingPane('WorkloadPricingPane')
|
||||
const hourlyView = createPricingPane('HourlyPricingPane')
|
||||
|
||||
const workContentPane = markRaw(
|
||||
defineComponent({
|
||||
name: 'WorkContentPane',
|
||||
setup() {
|
||||
const AsyncWorkContentGrid = defineAsyncComponent({
|
||||
loader: () => import('@/components/shared/WorkContentGrid.vue'),
|
||||
onError: err => {
|
||||
console.error('加载 WorkContentGrid 组件失败:', err)
|
||||
}
|
||||
})
|
||||
|
||||
return () => h(AsyncWorkContentGrid, {
|
||||
title: '工作内容',
|
||||
storageKey: `work-content-${props.contractId}-${props.serviceId}`,
|
||||
serviceId: props.serviceId,
|
||||
dictMode: 'service'
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const investmentScaleUnavailableView = createMethodUnavailablePane(
|
||||
'该服务不适用投资规模法',
|
||||
'当前服务未启用规模法,投资规模法不可编辑。'
|
||||
@@ -107,6 +128,7 @@ const hourlyUnavailableView = createMethodUnavailablePane(
|
||||
)
|
||||
|
||||
const pricingCategories = computed<PricingCategoryItem[]>(() => [
|
||||
|
||||
{
|
||||
key: 'investment-scale-method',
|
||||
label: '投资规模法',
|
||||
@@ -126,14 +148,15 @@ const pricingCategories = computed<PricingCategoryItem[]>(() => [
|
||||
key: 'hourly-method',
|
||||
label: '工时法',
|
||||
component: methodAvailability.value.hourly ? hourlyView : hourlyUnavailableView
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'work-content',
|
||||
label: '工作内容',
|
||||
component: workContentPane
|
||||
},
|
||||
])
|
||||
|
||||
const defaultCategory = computed(() => {
|
||||
if (methodAvailability.value.investmentScale) return 'investment-scale-method'
|
||||
if (methodAvailability.value.landScale) return 'land-scale-method'
|
||||
if (methodAvailability.value.workload) return 'workload-method'
|
||||
if (methodAvailability.value.hourly) return 'hourly-method'
|
||||
return 'investment-scale-method'
|
||||
return 'work-content'
|
||||
})
|
||||
</script>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
|
||||
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
@@ -37,12 +37,15 @@ interface XmInfoState {
|
||||
reviewedBy?: string
|
||||
preparedCompany?: string
|
||||
preparedDate?: string
|
||||
overview?: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
type MajorParentNode = { id: string; name: string }
|
||||
|
||||
const DB_KEY = 'xm-base-info-v1'
|
||||
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
||||
const DEFAULT_DESC = '在履行造价咨询服务时,宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时,处罚金额为预算费用的10%;其中考评得分在大于及等于80和小于85分时,处罚金额为预算费用的20%;其中考评得分在大于及等于75和小于80分时,处罚金额为预算费用的30%;其中考评得分在大于及等于70和小于75分时,处罚金额为预算费用的40%;其中考评得分小于70分时,处罚金额为预算费用的50%以上。'
|
||||
const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
|
||||
const getTodayDateString = () => {
|
||||
const now = new Date()
|
||||
@@ -61,6 +64,8 @@ const reviewedBy = ref('')
|
||||
const preparedCompany = ref('')
|
||||
const preparedDate = ref(getTodayDateString())
|
||||
const preparedDatePickerValue = ref<any>(undefined)
|
||||
const overview = ref('')
|
||||
const desc = ref(DEFAULT_DESC)
|
||||
|
||||
const normalizeDateString = (value: unknown): string => {
|
||||
if (typeof value !== 'string') return ''
|
||||
@@ -108,7 +113,9 @@ const saveToIndexedDB = async () => {
|
||||
preparedBy: preparedBy.value,
|
||||
reviewedBy: reviewedBy.value,
|
||||
preparedCompany: preparedCompany.value,
|
||||
preparedDate: preparedDate.value
|
||||
preparedDate: preparedDate.value,
|
||||
overview: overview.value,
|
||||
desc: desc.value
|
||||
}
|
||||
await kvStore.setItem(DB_KEY, payload)
|
||||
} catch (error) {
|
||||
@@ -131,6 +138,8 @@ const loadFromIndexedDB = async () => {
|
||||
reviewedBy.value = typeof data.reviewedBy === 'string' ? data.reviewedBy : ''
|
||||
preparedCompany.value = typeof data.preparedCompany === 'string' ? data.preparedCompany : ''
|
||||
preparedDate.value = normalizeDateString(data.preparedDate) || getTodayDateString()
|
||||
overview.value = typeof data.overview === 'string' ? data.overview : ''
|
||||
desc.value = typeof data.desc === 'string' && data.desc.trim() ? data.desc : DEFAULT_DESC
|
||||
syncPreparedDatePickerFromString()
|
||||
return
|
||||
}
|
||||
@@ -142,6 +151,8 @@ const loadFromIndexedDB = async () => {
|
||||
reviewedBy.value = ''
|
||||
preparedCompany.value = ''
|
||||
preparedDate.value = getTodayDateString()
|
||||
overview.value = ''
|
||||
desc.value = DEFAULT_DESC
|
||||
syncPreparedDatePickerFromString()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
@@ -151,6 +162,8 @@ const loadFromIndexedDB = async () => {
|
||||
reviewedBy.value = ''
|
||||
preparedCompany.value = ''
|
||||
preparedDate.value = getTodayDateString()
|
||||
overview.value = ''
|
||||
desc.value = DEFAULT_DESC
|
||||
syncPreparedDatePickerFromString()
|
||||
}
|
||||
}
|
||||
@@ -171,7 +184,7 @@ const handleProjectNameBlur = () => {
|
||||
}
|
||||
|
||||
watch(
|
||||
[projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate],
|
||||
[projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate, overview, desc],
|
||||
schedulePersist
|
||||
)
|
||||
|
||||
@@ -378,6 +391,24 @@ onMounted(async () => {
|
||||
</DatePickerContent>
|
||||
</DatePickerRoot>
|
||||
</div>
|
||||
<div class="md:col-span-2 xl:col-span-4">
|
||||
<label class="block text-sm font-medium text-foreground">项目概况</label>
|
||||
<textarea
|
||||
v-model="overview"
|
||||
rows="3"
|
||||
placeholder="请输入项目概况"
|
||||
class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 xl:col-span-4">
|
||||
<label class="block text-sm font-medium text-foreground">备注</label>
|
||||
<textarea
|
||||
v-model="desc"
|
||||
rows="4"
|
||||
class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
scene="xm-tab"
|
||||
title=""
|
||||
storage-key="project-active-cat"
|
||||
default-category="scale-info"
|
||||
default-category="info"
|
||||
:categories="xmCategories"
|
||||
/>
|
||||
</template>
|
||||
@@ -11,16 +11,18 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
import TypeLine from '@/layout/typeLine.vue'
|
||||
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/views/xmInfo.vue')))
|
||||
const htView = markRaw(defineAsyncComponent(() => import('@/components/views/Ht.vue')))
|
||||
const infoView = markRaw(defineAsyncComponent(() => import('@/components/xm/info.vue')))
|
||||
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/xm/xmInfo.vue')))
|
||||
const htView = markRaw(defineAsyncComponent(() => import('@/components/ht/Ht.vue')))
|
||||
const consultCategoryFactorView = markRaw(
|
||||
defineAsyncComponent(() => import('@/components/views/XmConsultCategoryFactor.vue'))
|
||||
defineAsyncComponent(() => import('@/components/xm/XmConsultCategoryFactor.vue'))
|
||||
)
|
||||
const majorFactorView = markRaw(
|
||||
defineAsyncComponent(() => import('@/components/views/XmMajorFactor.vue'))
|
||||
defineAsyncComponent(() => import('@/components/xm/XmMajorFactor.vue'))
|
||||
)
|
||||
|
||||
const xmCategories = [
|
||||
{ key: 'info', label: '基础信息', component: infoView },
|
||||
{ key: 'scale-info', label: '规模信息', component: scaleInfoView },
|
||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'
|
||||
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
|
||||
const DB_KEY = 'xm-info-v3'
|
||||
|
||||
|
||||
+90
-551
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,5 @@
|
||||
export type WorkspaceMode = 'home' | 'project' | 'quick'
|
||||
|
||||
export const HOME_TAB_ID = 'HomeView'
|
||||
export const PROJECT_TAB_ID = 'ProjectCalcView'
|
||||
export const QUICK_TAB_ID = 'QuickCalcView'
|
||||
export const LEGACY_PROJECT_TAB_ID = 'XmView'
|
||||
@@ -37,16 +36,17 @@ export const readWorkspaceMode = (): WorkspaceMode => {
|
||||
}
|
||||
}
|
||||
|
||||
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
|
||||
try {
|
||||
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, normalizeWorkspaceMode(mode))
|
||||
} catch {
|
||||
// 忽略只读或隐私模式下的写入失败。
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const createDefaultQuickContractMeta = (): QuickContractMeta => ({
|
||||
id: QUICK_CONTRACT_ID,
|
||||
name: QUICK_CONTRACT_FALLBACK_NAME,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
|
||||
try {
|
||||
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, normalizeWorkspaceMode(mode))
|
||||
} catch {
|
||||
// 忽略只读或隐私模式下的写入失败。
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,18 @@ const trimStringValuesDeep = (value: unknown): unknown => {
|
||||
return value
|
||||
}
|
||||
|
||||
const hydratePromises = new Map<string, Promise<void>>()
|
||||
|
||||
export const waitForHydration = (storeId: string): Promise<void> => {
|
||||
const existing = hydratePromises.get(storeId)
|
||||
if (existing) return existing
|
||||
// store 尚未注册,返回一个会在注册后 resolve 的 Promise
|
||||
let resolve!: () => void
|
||||
const p = new Promise<void>(r => { resolve = r })
|
||||
hydratePromises.set(storeId, p)
|
||||
return p
|
||||
}
|
||||
|
||||
export default (config?: PiniaStorageConfig) => {
|
||||
const finalConfig = { ...baseConfig, ...(config || {}) }
|
||||
const { mode = 'single', debounce = 300, ...forageConfig } = finalConfig
|
||||
@@ -123,6 +135,11 @@ export default (config?: PiniaStorageConfig) => {
|
||||
{ detached: true }
|
||||
)
|
||||
|
||||
// 注册或获取当前 store 的水合 Promise
|
||||
let resolveHydration!: () => void
|
||||
const hydrateP = new Promise<void>(r => { resolveHydration = r })
|
||||
hydratePromises.set(storeId, hydrateP)
|
||||
|
||||
hydrating = true
|
||||
void lf.getItem<Record<string, unknown>>(key)
|
||||
.then(state => {
|
||||
@@ -136,6 +153,7 @@ export default (config?: PiniaStorageConfig) => {
|
||||
})
|
||||
.finally(() => {
|
||||
hydrating = false
|
||||
resolveHydration()
|
||||
})
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { HOME_TAB_ID, PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace'
|
||||
import { PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace'
|
||||
|
||||
export interface TabItem<TProps = Record<string, unknown>> {
|
||||
id: string
|
||||
@@ -10,22 +10,23 @@ export interface TabItem<TProps = Record<string, unknown>> {
|
||||
}
|
||||
|
||||
const DEFAULT_TAB: TabItem = {
|
||||
id: HOME_TAB_ID,
|
||||
title: '首页',
|
||||
componentName: HOME_TAB_ID
|
||||
id: PROJECT_TAB_ID,
|
||||
title: '项目卡片',
|
||||
componentName: 'XmView'
|
||||
}
|
||||
|
||||
const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
|
||||
const PROTECTED_TAB_ID_SET = new Set<string>([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID])
|
||||
const createDefaultTabs = (): TabItem[] => [{...DEFAULT_TAB}]
|
||||
const PROTECTED_TAB_ID_SET = new Set<string>([ PROJECT_TAB_ID,QUICK_CONTRACT_TAB_ID])
|
||||
|
||||
export const useTabStore = defineStore(
|
||||
'tabs',
|
||||
() => {
|
||||
const tabs = ref<TabItem[]>(createDefaultTabs())
|
||||
const activeTabId = ref(HOME_TAB_ID)
|
||||
const activeTabId = ref()
|
||||
const hasCompletedSetup = ref(false)
|
||||
|
||||
const ensureHomeTab = () => {
|
||||
if (tabs.value.some(tab => tab.id === HOME_TAB_ID)) return
|
||||
if (tabs.value.some(tab => tab.id === PROJECT_TAB_ID)) return
|
||||
tabs.value = [...createDefaultTabs(), ...tabs.value]
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ export const useTabStore = defineStore(
|
||||
ensureHomeTab()
|
||||
if (tabs.value.length === 0) tabs.value = createDefaultTabs()
|
||||
if (!tabs.value.some(tab => tab.id === activeTabId.value)) {
|
||||
activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
|
||||
activeTabId.value = tabs.value[0]?.id ?? PROJECT_TAB_ID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ export const useTabStore = defineStore(
|
||||
|
||||
if (wasActive) {
|
||||
const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
|
||||
activeTabId.value = tabs.value[fallbackIndex]?.id ?? HOME_TAB_ID
|
||||
activeTabId.value = tabs.value[fallbackIndex]?.id ?? PROJECT_TAB_ID
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ export const useTabStore = defineStore(
|
||||
const closeAllTabs = () => {
|
||||
const protectedTabs = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id))
|
||||
tabs.value = protectedTabs.length > 0 ? protectedTabs : createDefaultTabs()
|
||||
activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
|
||||
activeTabId.value = tabs.value[0]?.id ?? PROJECT_TAB_ID
|
||||
}
|
||||
|
||||
const closeLeftTabs = (targetId: string) => {
|
||||
@@ -91,17 +92,19 @@ export const useTabStore = defineStore(
|
||||
const closeOtherTabs = (targetId: string) => {
|
||||
tabs.value = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id) || tab.id === targetId)
|
||||
ensureHomeTab()
|
||||
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID
|
||||
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : PROJECT_TAB_ID
|
||||
}
|
||||
|
||||
const resetTabs = () => {
|
||||
tabs.value = createDefaultTabs()
|
||||
activeTabId.value = HOME_TAB_ID
|
||||
activeTabId.value = PROJECT_TAB_ID
|
||||
hasCompletedSetup.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTabId,
|
||||
hasCompletedSetup,
|
||||
enterWorkspace,
|
||||
openTab,
|
||||
removeTab,
|
||||
|
||||
+154
@@ -183,6 +183,158 @@ export const reserveList = [
|
||||
name: '预备费'
|
||||
}
|
||||
]
|
||||
//工作内容词典
|
||||
export const workList = {
|
||||
0: { text: '完成投资估算文件及主要技术经济指标,完成与相关单位核对投资估算', serviceid: 6, order: 1, type: 0 },
|
||||
1: { text: '根据项目建议书或可行性研究报告的变化,动态调整投资估算和技术经济指标,并对比各环节投资估算变化,完成项目建议书阶段到可行性研究阶段的量、价对比及原因分析', serviceid: 6, order: 2, type: 0 },
|
||||
2: { text: '参加项目建议书或可行性研究的技术论证会议、评审会议和与确定投资估算相关的会议', serviceid: 6, order: 3, type: 0 },
|
||||
3: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 6, order: 4, type: 1 },
|
||||
4: { text: '根据造价差异分析结果,提出调整工程方案的咨询意见供委托人或关联单位决策', serviceid: 6, order: 5, type: 1 },
|
||||
5: { text: '依据投资估算和项目建设计划,编制项目资金计划,供委托人决策', serviceid: 6, order: 6, type: 1 },
|
||||
6: { text: '完成设计概算文件及主要技术经济指标,完成与相关单位核对设计概算', serviceid: 7, order: 7, type: 0 },
|
||||
7: { text: '依据各版初步设计动态调整设计概算及经济指标,并对比各版设计概算结果,完成可行性研究阶段到初步设计阶段的造价对比,提供造价差异分析报告', serviceid: 7, order: 8, type: 0 },
|
||||
8: { text: '参加初步设计的技术论证会议、评审会议和与设计概算相关的会议', serviceid: 7, order: 9, type: 0 },
|
||||
9: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 7, order: 10, type: 1 },
|
||||
10: { text: '根据造价差异分析结果,提出调整工程设计方案的咨询意见供委托人或关联单位决策', serviceid: 7, order: 11, type: 1 },
|
||||
11: { text: '依据设计概算和项目建设计划,编制或调整项目资金计划,供委托人决策', serviceid: 7, order: 12, type: 1 },
|
||||
12: { text: '完成施工图预算文件及主要技术经济指标,完成与相关单位核对施工图预算', serviceid: 8, order: 13, type: 0 },
|
||||
13: { text: '依据各版施工图动态调整施工图预算,并对比各版施工图预算结果,完成施工图预算与设计概算的造价对比(如有必要应完成与投资估算的造价对比),提供造价差异分析报告', serviceid: 8, order: 14, type: 0 },
|
||||
14: { text: '参加施工图设计的技术论证会议、评审会议和与施工图预算相关的会议', serviceid: 8, order: 15, type: 0 },
|
||||
15: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 8, order: 16, type: 1 },
|
||||
16: { text: '根据造价差异分析结果,提出调整工程设计方案的咨询意见供委托人或关联单位决策', serviceid: 8, order: 17, type: 1 },
|
||||
17: { text: '依据施工图预算和项目建设计划,编制或调整项目资金计划,供委托人决策', serviceid: 8, order: 18, type: 1 },
|
||||
18: { text: '分析专项设计方案的经济合理性,提出咨询意见供委托人决策', serviceid: 8, order: 19, type: 1 },
|
||||
19: { text: '分析施工组织设计和交通组织方案的经济合理性,提出咨询意见供委托人决策', serviceid: 8, order: 20, type: 1 },
|
||||
20: { text: '完成招标工程量清单及工程量清单预算,与相关单位核对工程量清单及工程量清单预算,并完成修改与调整', serviceid: 9, order: 21, type: 0 },
|
||||
21: { text: '对比工程量清单预算和施工图预算,提供造价差异分析报告', serviceid: 9, order: 22, type: 0 },
|
||||
22: { text: '审核计量与支付条款,对招标文件和合同文件涉及合同价款、工程变更费用咨询、结算费用等与造价相关条款提供咨询意见', serviceid: 9, order: 23, type: 0 },
|
||||
23: { text: '协助招标人完成招标答疑涉及的造价问题的解答与回复', serviceid: 9, order: 24, type: 0 },
|
||||
24: { text: '参加招投标阶段与造价确定相关的会议', serviceid: 9, order: 25, type: 0 },
|
||||
25: { text: '对比中标价与工程量清单预算,并分析差异原因,协助招标人完成不平衡报价调整', serviceid: 9, order: 26, type: 1 },
|
||||
26: { text: '参与合同谈判,协助招标人合同谈判过程中相关造价分析', serviceid: 9, order: 27, type: 1 },
|
||||
27: { text: '清理施工承包合同费用、征地拆迁费用、各项价差、其他类费用及费用依据、计算资料等', serviceid: 10, order: 28, type: 0 },
|
||||
28: { text: '清理征地拆迁费用,包括清理建设项目的用地、青苗补偿、房屋拆迁等费用,清理管线迁改、改路、改河、改沟以及构筑物迁改等费用,土地复垦费用及征拆各项规费的清理等', serviceid: 10, order: 29, type: 0 },
|
||||
29: { text: '清理材料价差,包括调整甲供材料设备价差、政策性文件规定的其他材料价差等', serviceid: 10, order: 30, type: 0 },
|
||||
30: { text: '清理政策性调整费用', serviceid: 10, order: 31, type: 0 },
|
||||
31: { text: '清理基本预备费和招标降造费,包括分析降造费的使用情况', serviceid: 10, order: 32, type: 0 },
|
||||
32: { text: '对未进行初步验收的剩余工程及投资情况进行清理', serviceid: 10, order: 33, type: 0 },
|
||||
33: { text: '梳理和清理批复的概算费用,分析费用变化的原因', serviceid: 10, order: 34, type: 0 },
|
||||
34: { text: '根据收集的资料和费用分析结果,编制清理概算文件,包括清理概算申请报告、相关批复文件、设计图纸、施工组织设计、合同、验工计价、设计变更和新增工程等支撑性资料、审计报告(如有)、第三方审价报告及其他相关依据', serviceid: 10, order: 35, type: 0 },
|
||||
35: { text: '招标图纸与实际施工图纸进行验核,核实工程数量,针对分析概算费用变化的原因', serviceid: 10, order: 36, type: 1 },
|
||||
36: { text: '参加与清理概算相关的会议,协助委托人完成清理概算文件预评审工作', serviceid: 10, order: 37, type: 0 },
|
||||
37: { text: '完成工程变更费用的测算,或提出性价比更优的替代方案、材料等意见', serviceid: 11, order: 38, type: 0 },
|
||||
38: { text: '完成新增预算单价或合同单价的计算与核定', serviceid: 11, order: 39, type: 0 },
|
||||
39: { text: '按施工图预算或合同清单方式完成工程变更费用的计算', serviceid: 11, order: 40, type: 0 },
|
||||
40: { text: '完成过程结算,包括建立过程结算多维度清单体系,如:项目清单、工程量清单、分项清单;划分过程结算清单单元;依据经确认的实际完工工程量完成计价计费,完成过程结算文件', serviceid: 11, order: 41, type: 0 },
|
||||
41: { text: '完成价格波动费用的计算或审核', serviceid: 11, order: 42, type: 0 },
|
||||
42: { text: '完成索赔与补偿费用的计算或审核', serviceid: 11, order: 43, type: 0 },
|
||||
43: { text: '完成结算文件及统计技术经济指标,与相关单位核对合同(工程)结算', serviceid: 11, order: 44, type: 0 },
|
||||
44: { text: '完成合同、变更和结算三环节费用的对比,分析合同费用各环节变化原因,提供对比报表和分析报告', serviceid: 11, order: 45, type: 0 },
|
||||
45: { text: '参加与过程结算、工程变更费用确定和合同(工程)结算相关的会议', serviceid: 11, order: 46, type: 0 },
|
||||
46: { text: '现场勘查与测量实际完工工程量', serviceid: 11, order: 47, type: 1 },
|
||||
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 },
|
||||
}
|
||||
|
||||
let costScaleCal = [
|
||||
{ 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 } },
|
||||
@@ -217,6 +369,8 @@ let areaScaleCal = [
|
||||
|
||||
|
||||
|
||||
export type WorkType = '基本工作' | '可选工作' | '日常顾问' | '专项顾问' | '附加工作' | '自定义'
|
||||
|
||||
export type IndustryType = (typeof industryTypeList)[number]['type']
|
||||
type DictItem = Record<string, any>
|
||||
type DictEntry = { id: string; rawId: string; item: DictItem }
|
||||
|
||||
Reference in new issue
Block a user