正在修复一些问题

This commit is contained in:
wintsa 2026-03-18 17:57:20 +08:00
parent ad363041c3
commit 66069ef0f1
11 changed files with 207 additions and 142 deletions

View File

@ -14,7 +14,18 @@
"mcp__context7__query-docs", "mcp__context7__query-docs",
"mcp__ag-mcp__detect_version", "mcp__ag-mcp__detect_version",
"WebSearch", "WebSearch",
"WebFetch(domain:reka-ui.com)" "WebFetch(domain:reka-ui.com)",
"mcp__ag-mcp__set_versions",
"mcp__ag-mcp__search_docs",
"Bash(find /c/Users/77077/Desktop/JGJS2026/node_modules/ag-grid-community -name *.css -exec grep -l auto-height {})",
"Bash(2)",
"Bash(bunx vue-tsc:*)",
"Bash(curl -s http://localhost:5173)",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_pages",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__navigate_page",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__take_screenshot",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__evaluate_script",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__press_key"
] ]
} }
} }

138
CLAUDE.md
View File

@ -1,115 +1,85 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# 任何项目都务必遵守的规则(极其重要!!!) ## Project Overview
保持中文回复和中文询问
## Communication Offline, client-side Vue 3 application for calculating transportation infrastructure consulting fees (交通工程造价咨询收费). All data persists to IndexedDB via localforage — there is no backend API.
- 永远使用简体中文进行思考和对话 ## Tech Stack
## Documentation - **Framework**: Vue 3 with Composition API (`<script setup>` SFCs)
- **Language**: TypeScript (strict mode)
- **Build**: Vite 8 (beta) with Rolldown
- **Package Manager**: Bun
- **Styling**: Tailwind CSS v4 via `@tailwindcss/vite` plugin
- **UI Primitives**: Reka UI (headless), Lucide icons
- **Grids**: AG Grid Enterprise v35 (modules registered globally in `main.ts`)
- **State**: Pinia with custom IndexedDB persistence plugin (`src/pinia/Plugin/indexdb.ts`)
- **Math**: decimal.js for precise fee calculations
- 编写 .md 文档时,也要用中文
- 正式文档写到项目的 docs/ 目录下
- 用于讨论和评审的计划、方案等文档,写到项目的 discuss/ 目录下
## Code Architecture
- 编写代码的硬性指标,包括以下原则:
1对于 Python、JavaScript、TypeScript 等动态语言,尽可能确保每个代码文件不要超过 500 行
3每层文件夹中的文件尽可能不超过 8 个。如有超过,需要规划为多层子文件夹
- 除了硬性指标以外,还需要时刻关注优雅的架构设计,避免出现以下可能侵蚀我们代码质量的「坏味道」:
1僵化 (Rigidity): 系统难以变更,任何微小的改动都会引发一连串的连锁修改。
2冗余 (Redundancy): 同样的代码逻辑在多处重复出现,导致维护困难且容易产生不一致。
3循环依赖 (Circular Dependency): 两个或多个模块互相纠缠,形成无法解耦的“死结”,导致难以测试与复用。
4脆弱性 (Fragility): 对代码一处的修改,导致了系统中其他看似无关部分功能的意外损坏。
5晦涩性 (Obscurity): 代码意图不明,结构混乱,导致阅读者难以理解其功能和设计。
6数据泥团 (Data Clump): 多个数据项总是一起出现在不同方法的参数中,暗示着它们应该被组合成一个独立的对象。
7不必要的复杂性 (Needless Complexity): 用“杀牛刀”去解决“杀鸡”的问题,过度设计使系统变得臃肿且难以理解。
- 【非常重要!!】无论是你自己编写代码,还是阅读或审核他人代码时,都要严格遵守上述硬性指标,以及时刻关注优雅的架构设计。
- 【非常重要!!】无论何时,一旦你识别出那些可能侵蚀我们代码质量的「坏味道」,都应当立即询问用户是否需要优化,并给出合理的优化建议。
## Commands ## Commands
- **Dev server**: `bun run dev` (uses `bunx --bun vite`) ```bash
- **Build**: `bun run build` (type-checks then bundles via Vite) bun run dev # Dev server (bunx --bun vite)
- **Type check only**: `bun run type-check` bun run build # Type-check then build (vue-tsc -b && vite build)
- **Preview build**: `bun run preview` bun run preview # Preview production build
bun run type-check # Type-check only (vue-tsc --noEmit)
```
No test runner is configured. Use Playwright for UI automation when needed (see AGENTS.md). No test framework is configured.
## Architecture Overview ## Architecture
This is a **browser-only Vue 3 SPA** — no backend, no SSR. All data is persisted client-side via IndexedDB (through localforage). The domain is **交通建设项目工程造价咨询** (transport infrastructure cost-consulting fee calculation for road, railway, and waterway projects). ### Navigation (No Vue Router)
### App Entry & Routing Tab-based navigation managed by `useTabStore` (`src/pinia/tab.ts`). Each tab has a `componentName` resolved via `defineAsyncComponent` in `src/layout/tab.vue`. Protected tabs cannot be closed by the user.
There is no Vue Router. Navigation is tab-based, managed entirely by `useTabStore` (`src/pinia/tab.ts`). `src/App.vue` shows either `HomeEntryView` (first-launch onboarding) or the main `Tab` layout depending on `tabStore.hasCompletedSetup`.
The `Tab` layout (`src/layout/tab.vue`) renders the active tab's component by name using `defineAsyncComponent` with a map of component names → import paths.
### Workspace Modes ### Workspace Modes
Defined in `src/lib/workspace.ts`. Three modes: `home`, `project`, `quick`. The current mode is persisted to `localStorage` under key `jgjs-workspace-mode-v1`. Two fixed/protected tab IDs exist: `ProjectCalcView` and `QuickCalcView` (and the quick-contract tab), which cannot be closed. Two modes stored in localStorage under `jgjs-workspace-mode-v1`:
- `project` — multi-contract project workspace (default)
- `quick` — single quick-calculation mode
### State & Persistence Mode logic and tab ID constants live in `src/lib/workspace.ts`.
- **Pinia** is used for all state management with `pinia-plugin-persistedstate`. ### Pinia Stores
- The persistence plugin is customized at `src/pinia/Plugin/indexdb` — it stores pinia state in IndexedDB (not localStorage), using localforage with `mode: 'multiple'` (each store in its own IndexedDB store named `pinia`).
- `useTabStore` (`src/pinia/tab.ts`): tab list, active tab, setup flag. Persisted.
- `useZxFwPricingStore` (`src/pinia/zxFwPricing.ts`): the core domain store. Manages contract-level 咨询服务 (consulting service) pricing state, per-service pricing method states, and contract extra-fee states. Uses a generic key/value layer (`getKeyState`/`setKeyState`/`loadKeyState`) backed by `useKvStore`.
- `useKvStore` (`src/pinia/kv.ts`): raw key-value access to IndexedDB via localforage.
### Data Layer (`src/sql.ts`) | Store | File | Purpose |
|-------|------|---------|
| `useTabStore` | `src/pinia/tab.ts` | Tab list, active tab, workspace entry |
| `useKvStore` | `src/pinia/kv.ts` | Generic key-value persistence layer |
| `useZxFwPricingStore` | `src/pinia/zxFwPricing.ts` | Core pricing/contract data with versioned snapshot diffing |
Despite the name, there is no SQL database. `src/sql.ts` is a large static data file containing: All stores persist to IndexedDB via the custom plugin in `src/pinia/Plugin/indexdb.ts` (uses localforage).
- `industryTypeList`: road / railway / waterway
- `majorList`: engineering specialties (E1E4 codes) with coefficient ranges
- `serviceList`: consulting service types (D1D5 codes) with pricing method flags
- `additionalWorkList`: extra work item definitions
- `exportFile`: Excel export logic using ExcelJS
All fee calculation formulas and coefficient tables live here. ### Domain Data
### Lib Utilities (`src/lib/`) `src/sql.ts` contains the full fee schedule: majors (专业), services (咨询服务), work types, and pricing method lookup tables. Four pricing methods: 投资规模法, 用地规模法, 工作量法, 工时法.
- `decimal.ts` / `number.ts` / `numberFormat.ts`: safe arithmetic using `decimal.js`, thousand-separator formatting ### Key Directories
- `zwArchive.ts`: AES-GCM encryption/decryption for `.zw` save files (import/export format). The key is derived from a fixed seed via SHA-256. File magic bytes: `JGJSZW`.
- `workspace.ts`: workspace mode constants and storage helpers
- `diyAgGridOptions.ts`: shared AG Grid configuration defaults
- `pricingScaleFee.ts` / `pricingMethodTotals.ts`: fee calculation helpers
- `projectWorkspace.ts` / `xmFactorDefaults.ts`: project-level workspace helpers
- `zxFwPricingSync.ts`: syncs pricing store state to/from IndexedDB on demand
### Views (`src/components/views/`) - `src/components/views/` — top-level view components (rendered in tabs)
- `src/components/ht/` — contract (合同) components
- `src/components/xm/` — project (项目) components
- `src/components/pricing/` — pricing method panes (one per method)
- `src/components/shared/` — reusable AG Grid wrappers
- `src/components/ui/` — primitive UI components (shadcn-vue style)
- `src/lib/` — utilities: decimal math, number formatting, AG Grid config, workspace logic, `.zw` archive encode/decode
- `src/layout/` — tab shell and layout components
Key views: ### Import/Export
- `HomeEntryView.vue`: onboarding screen shown on first launch; dispatches `home-import-selected` DOM event on completion
- `ProjectWorkspaceView.vue` / `xmCard.vue`: project card workspace (项目卡片)
- `QuickCalcView.vue`: quick calculation mode
- `ZxFwView.vue`: 咨询服务 (consulting services) grid — the primary fee input view
- `Ht.vue` / `htCard.vue`: contract (合同) views
- `HtFeeMethodTypeLineView.vue`: per-service fee method line (rate/hourly/quantity-unit-price)
- `WorkContentGrid.vue`: AG Grid-based work content table
### AG Grid Custom `.zw` binary archive format (`src/lib/zwArchive.ts`) for project data. Excel export via ExcelJS (`src/sql.ts`).
AG Grid Enterprise is used throughout. Modules are registered once globally in `src/main.ts`. A license key is set there. Shared grid option defaults live in `src/lib/diyAgGridOptions.ts`. ## Path Alias
### UI Components `@` maps to `src/` (configured in both `vite.config.ts` and `tsconfig.json`).
Reka UI (headless component library) + Tailwind CSS v4 + `lucide-vue-next` icons + `@iconify/vue`. Shared UI primitives are in `src/components/ui/`. Tailwind is integrated as a Vite plugin (`@tailwindcss/vite`). ## Conventions
### Build - All UI text is in Chinese (zh-CN)
- Immutable state updates in Pinia stores (create new arrays/objects, don't mutate)
Vite 8 with rolldown. Output goes to `dist/` with `base: './'` (relative paths — important for local file:// deployment). Code-splitting separates `ag-grid`, `vue/pinia`, and `reka-ui` into distinct vendor chunks. - AG Grid modules are registered once globally in `main.ts` — don't re-register in components
- Decimal.js is used for all fee arithmetic to avoid floating-point errors (`src/lib/decimal.ts`)
### Code Conventions
- All code comments are in Chinese.
- Composition API (`<script setup>`) everywhere.
- No Vue Router — use `useTabStore.openTab()` to navigate.
- Numeric calculations always go through `src/lib/decimal.ts` helpers to avoid floating-point errors.
- Storage keys follow patterns like `zxFW-{contractId}`, `tzGMF-{contractId}-{serviceId}`, `htExtraFee-{contractId}-{feeType}`.

BIN
debug-screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import type { ComponentPublicInstance, PropType } from 'vue' import type { ComponentPublicInstance, PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community' import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { addNumbers, roundTo } from '@/lib/decimal' import { addNumbers, roundTo } from '@/lib/decimal'
@ -191,6 +191,11 @@ const setCurrentContractState = async (nextState: ZxFwViewState) => {
await zxFwPricingStore.setContractState(props.contractId, nextState) await zxFwPricingStore.setContractState(props.contractId, nextState)
} }
const gridApi = shallowRef<GridApi<DetailRow> | null>(null)
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
gridApi.value = event.api
}
const pickerOpen = ref(false) const pickerOpen = ref(false)
const pickerTempIds = ref<string[]>([]) const pickerTempIds = ref<string[]>([])
const pickerSearch = ref('') const pickerSearch = ref('')
@ -594,11 +599,12 @@ const columnDefs: ColDef<DetailRow>[] = [
{ {
headerName: '名称', headerName: '名称',
field: 'name', field: 'name',
minWidth: 180, minWidth: 150,
flex: 3, flex: 3,
wrapText: true, wrapText: true,
autoHeight: true, autoHeight: true,
cellStyle: { lineHeight: '1.4', paddingTop: '4px', paddingBottom: '4px' }, cellStyle:{ 'line-height': 1.6
},
valueGetter: params => { valueGetter: params => {
if (!params.data) return '' if (!params.data) return ''
if (isFixedRow(params.data)) return '' if (isFixedRow(params.data)) return ''
@ -609,8 +615,8 @@ const columnDefs: ColDef<DetailRow>[] = [
headerName: '工作环节', headerName: '工作环节',
field: 'process', field: 'process',
headerClass: 'ag-center-header zxfw-process-header', headerClass: 'ag-center-header zxfw-process-header',
minWidth: 80, minWidth: 150,
maxWidth: 100, maxWidth: 200,
flex: 1, flex: 1,
editable: false, editable: false,
sortable: false, sortable: false,
@ -707,15 +713,9 @@ const columnDefs: ColDef<DetailRow>[] = [
editable: false, editable: false,
valueGetter: params => { valueGetter: params => {
if (!params.data) return null if (!params.data) return null
if (isFixedRow(params.data)) return getFixedRowSubtotal() return params.data.subtotal
return sumNullableNumbers([
params.data.investScale,
params.data.landScale,
params.data.workload,
params.data.hourly
])
}, },
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
}, },
{ {
headerName: '确认金额', headerName: '确认金额',
@ -727,18 +727,15 @@ const columnDefs: ColDef<DetailRow>[] = [
editable: params => !isFixedRow(params.data), editable: params => !isFixedRow(params.data),
valueGetter: params => { valueGetter: params => {
if (!params.data) return null if (!params.data) return null
if (isFixedRow(params.data)) {
return sumNullableNumbers( return params.data.finalFee
detailRows.value.filter(r => !isFixedRow(r)).map(r => r.finalFee) },
) valueSetter: params => {
} const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
if (params.data.finalFee != null) return params.data.finalFee const val = parsed != null ? roundTo(parsed, 2) : null
return sumNullableNumbers([ if (params.data.finalFee === val) return false
params.data.investScale, params.data.finalFee = val
params.data.landScale, return true
params.data.workload,
params.data.hourly
])
}, },
valueParser: params => { valueParser: params => {
const parsed = parseNumberOrNull(params.newValue, { precision: 2 }) const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
@ -749,7 +746,7 @@ const columnDefs: ColDef<DetailRow>[] = [
{ {
headerName: '操作', headerName: '操作',
field: 'actions', field: 'actions',
minWidth: 180, minWidth: 200,
flex: 1.5, flex: 1.5,
maxWidth: 220, maxWidth: 220,
editable: false, editable: false,
@ -885,13 +882,17 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
if (!totals) return row if (!totals) return row
const newSubtotal = sumNullableNumbers([totals.investScale, totals.landScale, totals.workload, totals.hourly]) const newSubtotal = sumNullableNumbers([totals.investScale, totals.landScale, totals.workload, totals.hourly])
// finalFee
const oldSubtotal = sumNullableNumbers([row.investScale, row.landScale, row.workload, row.hourly])
const userEdited = row.finalFee != null && oldSubtotal != null
&& roundTo(row.finalFee, 2) !== roundTo(oldSubtotal, 2)
return { return {
...row, ...row,
investScale: totals.investScale, investScale: totals.investScale,
landScale: totals.landScale, landScale: totals.landScale,
workload: totals.workload, workload: totals.workload,
hourly: totals.hourly, hourly: totals.hourly,
finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null finalFee: userEdited ? row.finalFee : (newSubtotal != null ? roundTo(newSubtotal, 2) : null)
} }
}) })
@ -1110,6 +1111,11 @@ const initializeContractState = async () => {
|| (data?.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id)) || (data?.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
await applySelection(idsFromStorage || []) await applySelection(idsFromStorage || [])
await ensurePricingDetailRowsForCurrentSelection() await ensurePricingDetailRowsForCurrentSelection()
// finalFee
const allServiceIds = getSelectedServiceIdsWithoutFixed()
if (allServiceIds.length > 0) {
await fillPricingTotalsForServiceIds(allServiceIds)
}
} catch (error) { } catch (error) {
console.error('initializeContractState failed:', error) console.error('initializeContractState failed:', error)
await setCurrentContractState({ await setCurrentContractState({
@ -1159,10 +1165,20 @@ const handleCellValueChanged = async (event: any) => {
const nextRows = currentState.detailRows.map(item => const nextRows = currentState.detailRows.map(item =>
item.id === row.id ? { ...item, finalFee: newValue } : item item.id === row.id ? { ...item, finalFee: newValue } : item
) )
const finalRows = applyFixedRowTotals(nextRows)
await setCurrentContractState({ await setCurrentContractState({
...currentState, ...currentState,
detailRows: applyFixedRowTotals(nextRows) detailRows: finalRows
}) })
// rowNode.data AG Grid
const api = gridApi.value
if (api) {
const fixedRowData = finalRows.find(r => isFixedRow(r))
const fixedNode = api.getRowNode(fixedBudgetRow.id)
if (fixedNode && fixedRowData) {
fixedNode.setData(fixedRowData)
}
}
} }
onMounted(async () => { onMounted(async () => {
@ -1199,6 +1215,7 @@ onBeforeUnmount(() => {
<div :class="agGridWrapClass"> <div :class="agGridWrapClass">
<AgGridVue :style="agGridStyle" :rowData="detailRows" :columnDefs="columnDefs" <AgGridVue :style="agGridStyle" :rowData="detailRows" :columnDefs="columnDefs"
:gridOptions="detailGridOptions" :theme="myTheme" @cell-value-changed="handleCellValueChanged" :gridOptions="detailGridOptions" :theme="myTheme" @cell-value-changed="handleCellValueChanged"
@grid-ready="onGridReady"
:enableClipboard="true" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="30" :enableClipboard="true" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="30"
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" /> :undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
</div> </div>

View File

@ -23,7 +23,7 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { myTheme, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, agGridStyle } from '@/lib/diyAgGridOptions'
import { workList } from '@/sql' import { workList } from '@/sql'
import type { WorkType } from '@/sql' import { WorkType,TYPE_LABEL_MAP } from '@/sql'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { Trash2 } from 'lucide-vue-next' import { Trash2 } from 'lucide-vue-next'
@ -60,13 +60,7 @@ const zxFwPricingStore = useZxFwPricingStore()
const gridApi = ref<GridApi<WorkContentRow> | null>(null) const gridApi = ref<GridApi<WorkContentRow> | null>(null)
const rowData = ref<WorkContentRow[]>([]) const rowData = ref<WorkContentRow[]>([])
const TYPE_LABEL_MAP: Record<number, WorkType> = {
0: '基本工作',
1: '可选工作',
2: '日常顾问',
3: '专项顾问',
4: '附加工作'
}
const buildDefaultRowsFromDict = (): WorkContentRow[] => { const buildDefaultRowsFromDict = (): WorkContentRow[] => {
const rows: WorkContentRow[] = [] const rows: WorkContentRow[] = []
@ -160,6 +154,11 @@ const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
const data = params.data const data = params.data
if (!data) return '' if (!data) return ''
const wrapper = document.createElement('div') const wrapper = document.createElement('div')
wrapper.style.display = 'flex'
wrapper.style.alignItems = 'center'
wrapper.style.justifyContent = 'space-between'
wrapper.style.gap = '6px'
wrapper.style.width = '100%'
wrapper.className = 'work-content-cell' wrapper.className = 'work-content-cell'
// checkbox placeholder // checkbox placeholder
if (data.custom) { if (data.custom) {

View File

@ -5,7 +5,7 @@
:subtitle="`合同ID${contractIdText}`" :subtitle="`合同ID${contractIdText}`"
:copy-text="contractIdText" :copy-text="contractIdText"
:storage-key="activeTypeStorageKey" :storage-key="activeTypeStorageKey"
default-category="work-content" default-category="rate-fee"
:categories="categories" :categories="categories"
/> />
</template> </template>

View File

@ -157,6 +157,11 @@ const pricingCategories = computed<PricingCategoryItem[]>(() => [
]) ])
const defaultCategory = computed(() => { const defaultCategory = computed(() => {
const m = methodAvailability.value
if (m.investmentScale) return 'investment-scale-method'
if (m.landScale) return 'land-scale-method'
if (m.workload) return 'workload-method'
if (m.hourly) return 'hourly-method'
return 'work-content' return 'work-content'
}) })
</script> </script>

View File

@ -1637,9 +1637,8 @@ const exportReport = async () => {
finishReportExportProgress(true, '报表导出完成', blobUrl) finishReportExportProgress(true, '报表导出完成', blobUrl)
} catch (error) { } catch (error) {
console.error('export report failed:', error) console.error('export report failed:', error)
if (reportExportToastOpen.value) { finishReportExportProgress(false, '报表导出失败,请重试')
finishReportExportProgress(false, '报表导出失败,请重试')
}
} finally { } finally {
dataMenuOpen.value = false dataMenuOpen.value = false
} }
@ -2038,15 +2037,15 @@ watch(
{{ reportExportStatus === 'running' ? '导出报表' : (reportExportStatus === 'success' ? '导出成功' : '导出失败') }} {{ reportExportStatus === 'running' ? '导出报表' : (reportExportStatus === 'success' ? '导出成功' : '导出失败') }}
</ToastTitle> </ToastTitle>
<ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription> <ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription>
<div v-if="reportExportStatus === 'success' && reportExportBlobUrl" class="mt-2 flex items-center gap-2"> <!-- <div v-if="reportExportStatus === 'success' && reportExportBlobUrl" class="mt-2 flex items-center gap-2">
<Button size="sm" class="h-7 rounded-md px-3 text-xs" @click="openExportedReport"> <Button size="sm" class="h-7 rounded-md px-3 text-xs" @click="openExportedReport">
打开文件 打开文件
</Button> </Button>
<Button variant="ghost" size="sm" class="h-7 rounded-md px-2 text-xs text-muted-foreground" @click="dismissReportToast"> <Button variant="ghost" size="sm" class="h-7 rounded-md px-2 text-xs text-muted-foreground" @click="dismissReportToast">
关闭 关闭
</Button> </Button>
</div> </div> -->
<div v-else class="mt-2 flex items-center gap-2"> <div class="mt-2 flex items-center gap-2">
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-muted"> <div class="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
<div <div
class="h-full transition-all duration-300" class="h-full transition-all duration-300"

View File

@ -17,6 +17,7 @@ export interface ZxFwDetailRow {
workload: number | null workload: number | null
hourly: number | null hourly: number | null
subtotal?: number | null subtotal?: number | null
finalFee?: number | null
actions?: unknown actions?: unknown
} }
@ -95,6 +96,7 @@ const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
workload: toFiniteNumberOrNull(row.workload), workload: toFiniteNumberOrNull(row.workload),
hourly: toFiniteNumberOrNull(row.hourly), hourly: toFiniteNumberOrNull(row.hourly),
subtotal: toFiniteNumberOrNull(row.subtotal), subtotal: toFiniteNumberOrNull(row.subtotal),
finalFee: toFiniteNumberOrNull(row.finalFee),
actions: row.actions actions: row.actions
} }
}).filter(row => row.id) }).filter(row => row.id)
@ -107,7 +109,6 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
const totalWorkload = sumNullableNumbers(nonFixedRows.map(row => row.workload)) const totalWorkload = sumNullableNumbers(nonFixedRows.map(row => row.workload))
const totalHourly = sumNullableNumbers(nonFixedRows.map(row => row.hourly)) const totalHourly = sumNullableNumbers(nonFixedRows.map(row => row.hourly))
const fixedSubtotal = sumNullableNumbers([totalInvestScale, totalLandScale, totalWorkload, totalHourly]) const fixedSubtotal = sumNullableNumbers([totalInvestScale, totalLandScale, totalWorkload, totalHourly])
return normalized.map(row => { return normalized.map(row => {
if (row.id === FIXED_ROW_ID) { if (row.id === FIXED_ROW_ID) {
return { return {
@ -116,7 +117,9 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
landScale: round3Nullable(totalLandScale), landScale: round3Nullable(totalLandScale),
workload: round3Nullable(totalWorkload), workload: round3Nullable(totalWorkload),
hourly: round3Nullable(totalHourly), hourly: round3Nullable(totalHourly),
subtotal: round3Nullable(fixedSubtotal) subtotal: round3Nullable(fixedSubtotal),
finalFee: row.finalFee,
} }
} }
const subtotal = sumNullableNumbers([ const subtotal = sumNullableNumbers([
@ -127,7 +130,9 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
]) ])
return { return {
...row, ...row,
subtotal: round3Nullable(subtotal) subtotal: round3Nullable(subtotal),
finalFee: round3Nullable(subtotal),
} }
}) })
} }
@ -181,6 +186,7 @@ const isSameRows = (a: ZxFwDetailRow[] | undefined, b: ZxFwDetailRow[] | undefin
if (!isSameNullableNumber(l.workload, r.workload)) return false if (!isSameNullableNumber(l.workload, r.workload)) return false
if (!isSameNullableNumber(l.hourly, r.hourly)) return false if (!isSameNullableNumber(l.hourly, r.hourly)) return false
if (!isSameNullableNumber(l.subtotal, r.subtotal)) return false if (!isSameNullableNumber(l.subtotal, r.subtotal)) return false
if (!isSameNullableNumber(l.finalFee, r.finalFee)) return false
} }
return true return true
} }
@ -986,7 +992,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
: null : null
const current = contracts.value[contractId] const current = contracts.value[contractId]
if (raw) { if (raw) {
console.log(raw,'init')
const normalized = normalizeState(raw) const normalized = normalizeState(raw)
console.log(normalized)
if (!current || !isSameState(current, normalized)) { if (!current || !isSameState(current, normalized)) {
contracts.value[contractId] = normalized contracts.value[contractId] = normalized
touchVersion(contractId) touchVersion(contractId)
@ -1039,12 +1048,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
field: ZxFwPricingField field: ZxFwPricingField
value: number | null | undefined value: number | null | undefined
}) => { }) => {
const contractId = toKey(params.contractId) const contractId = toKey(params.contractId)
if (!contractId) return false if (!contractId) return false
if (!contracts.value[contractId]) {
await loadContract(contractId)
}
const current = contracts.value[contractId] const current = contracts.value[contractId]
if (!current?.detailRows?.length) return false if (!current?.detailRows?.length) return false
@ -1090,7 +1097,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (!contractId) return null if (!contractId) return null
const state = contracts.value[contractId] const state = contracts.value[contractId]
if (!state?.detailRows?.length) return null if (!state?.detailRows?.length) return null
const fixedRow = state.detailRows.find(row => String(row.id || '') === FIXED_ROW_ID) const fixedRow = state.detailRows.find(row => String(row.id || '') === FIXED_ROW_ID)
const fixedFinalFee = toFiniteNumberOrNull(fixedRow?.finalFee) const fixedFinalFee = toFiniteNumberOrNull(fixedRow?.finalFee)
if (fixedFinalFee != null) return round3(fixedFinalFee) if (fixedFinalFee != null) return round3(fixedFinalFee)

View File

@ -11,7 +11,16 @@ const toFiniteNumber = (value: unknown) => {
const num = Number(value) const num = Number(value)
return Number.isFinite(num) ? num : 0 return Number.isFinite(num) ? num : 0
} }
export type WorkType = '基本工作' | '可选工作' | '日常顾问' | '专项顾问' | '附加工作' | '自定义'
export const TYPE_LABEL_MAP: Record<number, WorkType> = {
0: '基本工作',
1: '可选工作',
2: '日常顾问',
3: '专项顾问',
4: '附加工作',
5:'自定义'
}
export const industryTypeList = [ export const industryTypeList = [
{ id: '0', name: '公路工程', type: 'isRoad' }, { id: '0', name: '公路工程', type: 'isRoad' },
{ id: '1', name: '铁路工程', type: 'isRailway' }, { id: '1', name: '铁路工程', type: 'isRailway' },
@ -337,6 +346,56 @@ export const workList = {
146: { text: '作为造价咨询服务总体协调单位,依据造价技术标准的具体条款或委托方的个性化需求,进一步细化各项工作的具体要求,检查其他服务单位的造价文件的组成完整性、电子文件格式是否符合要求、电子版与纸质版是否对应、造价文件报表的规范性', serviceid: -1, order: 147, type: 4 }, 146: { text: '作为造价咨询服务总体协调单位,依据造价技术标准的具体条款或委托方的个性化需求,进一步细化各项工作的具体要求,检查其他服务单位的造价文件的组成完整性、电子文件格式是否符合要求、电子版与纸质版是否对应、造价文件报表的规范性', serviceid: -1, order: 147, type: 4 },
147: { text: '作为造价咨询服务总体协调单位,负责总体协调其他咨询人或专家团队的工作,确保各方在项目服务中的沟通顺畅,监控造价咨询服务的进展情况,确保各咨询人按时完成工作', serviceid: -1, order: 148, type: 4 }, 147: { text: '作为造价咨询服务总体协调单位,负责总体协调其他咨询人或专家团队的工作,确保各方在项目服务中的沟通顺畅,监控造价咨询服务的进展情况,确保各咨询人按时完成工作', serviceid: -1, order: 148, type: 4 },
} }
//工作内容树形关系表
export const wholeProcessTasks = [
{
fid: 0,
industry: 0,
sid: [6, 7, 8, 9, 11, 13],
},
{
fid: 0,
industry: 1,
sid: [6, 7, 8, 9, 10, 12, 13],
},
{
fid: 0,
industry: 2,
sid: [6, 7, 8, 9, 11, 13],
},
{
fid: 2,
industry: 0,
sid: [6, 7, 8],
},
{
fid: 2,
industry: 1,
sid: [6, 7, 8],
},
{
fid: 2,
industry: 2,
sid: [6, 7, 8],
},
{
fid: 3,
industry: 0,
sid: [9, 11, 13],
},
{
fid: 4,
industry: 1,
sid: [9, 10, 12, 13],
},
{
fid: 3,
industry: 2,
sid: [9, 11, 13],
},
];
let costScaleCal = [ let costScaleCal = [
{ code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } }, { code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
@ -372,7 +431,6 @@ let areaScaleCal = [
export type WorkType = '基本工作' | '可选工作' | '日常顾问' | '专项顾问' | '附加工作' | '自定义'
export type IndustryType = (typeof industryTypeList)[number]['type'] export type IndustryType = (typeof industryTypeList)[number]['type']
type DictItem = Record<string, any> type DictItem = Record<string, any>

View File

@ -144,10 +144,10 @@ html {
} }
/* When one column uses auto-height rows, keep other columns vertically centered. */ /* When one column uses auto-height rows, keep other columns vertically centered. */
.xmMx .ag-row .ag-cell-wrapper { /* .xmMx .ag-row .ag-cell-wrapper {
height: 100%; height: 100%;
} } */
.xmMx .ag-row .ag-cell:not(.ag-cell-auto-height) .ag-cell-wrapper.ag-row-group { .xmMx .ag-row .ag-cell:not(.ag-cell-auto-height) .ag-cell-wrapper.ag-row-group {
align-items: center; align-items: center;