正在修复一些问题
This commit is contained in:
parent
ad363041c3
commit
66069ef0f1
@ -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
138
CLAUDE.md
@ -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 (E1–E4 codes) with coefficient ranges
|
|
||||||
- `serviceList`: consulting service types (D1–D5 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
BIN
debug-screenshot-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
60
src/sql.ts
60
src/sql.ts
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user