diff --git a/src/components/views/Ht.vue b/src/components/views/Ht.vue
index b355b9e..e050078 100644
--- a/src/components/views/Ht.vue
+++ b/src/components/views/Ht.vue
@@ -1093,18 +1093,22 @@ onBeforeUnmount(() => {
-
+
+
+
+
+ 回到顶部
+
{
return params.value
}
-const fitColumnsToGrid = () => {
- if (!gridApi.value) return
- requestAnimationFrame(() => {
- gridApi.value?.sizeColumnsToFit({ defaultMinWidth: 68 })
- })
-}
-
-const handleGridReady = (event: GridReadyEvent) => {
- gridApi.value = event.api
- fitColumnsToGrid()
-}
-
-const handleGridSizeChanged = (_event: GridSizeChangedEvent) => {
- fitColumnsToGrid()
-}
-
-const handleFirstDataRendered = (_event: FirstDataRenderedEvent) => {
- fitColumnsToGrid()
-}
onMounted(async () => {
await loadFromIndexedDB()
@@ -321,9 +302,7 @@ onBeforeUnmount(() => {
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
- @grid-ready="handleGridReady"
- @grid-size-changed="handleGridSizeChanged"
- @first-data-rendered="handleFirstDataRendered"
+
/>
diff --git a/src/components/views/htInfo.vue b/src/components/views/htInfo.vue
index a502947..cec1bca 100644
--- a/src/components/views/htInfo.vue
+++ b/src/components/views/htInfo.vue
@@ -317,25 +317,7 @@ const processCellFromClipboard = (params:any) => {
return params.value;
};
-const fitColumnsToGrid = () => {
- if (!gridApi.value) return
- requestAnimationFrame(() => {
- gridApi.value?.sizeColumnsToFit({ defaultMinWidth: 80 })
- })
-}
-const handleGridReady = (event: GridReadyEvent) => {
- gridApi.value = event.api
- fitColumnsToGrid()
-}
-
-const handleGridSizeChanged = (_event: GridSizeChangedEvent) => {
- fitColumnsToGrid()
-}
-
-const handleFirstDataRendered = (_event: FirstDataRenderedEvent) => {
- fitColumnsToGrid()
-}
@@ -370,9 +352,7 @@ const handleFirstDataRendered = (_event: FirstDataRenderedEvent) => {
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
- @grid-ready="handleGridReady"
- @grid-size-changed="handleGridSizeChanged"
- @first-data-rendered="handleFirstDataRendered"
+
/>
diff --git a/src/components/views/xmInfo.vue b/src/components/views/xmInfo.vue
index 9141a49..7fd6e8e 100644
--- a/src/components/views/xmInfo.vue
+++ b/src/components/views/xmInfo.vue
@@ -220,8 +220,9 @@ const columnDefs: ColDef[] = [
editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'ag-right-aligned-cell editable-cell-line' : 'ag-right-aligned-cell'),
cellClassRules: {
- 'editable-cell-empty': params =>
- !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
+ 'editable-cell-empty': params =>{
+ // console.log(!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === ''))
+ return !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')}
},
aggFunc: decimalAggSum,
valueParser: params => {
@@ -396,25 +397,6 @@ const processCellFromClipboard = (params:any) => {
return params.value;
};
-const fitColumnsToGrid = () => {
- if (!gridApi.value) return
- requestAnimationFrame(() => {
- gridApi.value?.sizeColumnsToFit({ defaultMinWidth: 80 })
- })
-}
-
-const handleGridReady = (event: GridReadyEvent) => {
- gridApi.value = event.api
- fitColumnsToGrid()
-}
-
-const handleGridSizeChanged = (_event: GridSizeChangedEvent) => {
- fitColumnsToGrid()
-}
-
-const handleFirstDataRendered = (_event: FirstDataRenderedEvent) => {
- fitColumnsToGrid()
-}
const scrollToGridSection = () => {
const target = gridSectionRef.value || agGridRef.value
@@ -476,9 +458,6 @@ const scrollToGridSection = () => {
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
- @grid-ready="handleGridReady"
- @grid-size-changed="handleGridSizeChanged"
- @first-data-rendered="handleFirstDataRendered"
/>
diff --git a/src/layout/tab.vue b/src/layout/tab.vue
index ff4b9ee..15ca95f 100644
--- a/src/layout/tab.vue
+++ b/src/layout/tab.vue
@@ -5,7 +5,7 @@ import draggable from 'vuedraggable'
import { useTabStore } from '@/pinia/tab'
import { Button } from '@/components/ui/button'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
-import { ChevronDown, RotateCcw, X } from 'lucide-vue-next'
+import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
import localforage from 'localforage'
import {
AlertDialogAction,
@@ -33,10 +33,92 @@ interface DataPackage {
localforageDefault: DataEntry[]
}
+interface UserGuideStep {
+ title: string
+ description: string
+ points: string[]
+}
+
type XmInfoLike = {
projectName?: unknown
}
+const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
+const userGuideSteps: UserGuideStep[] = [
+ {
+ title: '欢迎使用',
+ description: '这个引导会覆盖系统主要功能和完整使用路径,建议按顺序走一遍。',
+ points: [
+ '顶部是标签页区,可在项目、合同段、服务计算页之间快速切换。',
+ '页面里的表格与表单会自动保存到本地,无需手动点击保存。',
+ '你可以随时点击右上角“使用引导”重新打开本教程。'
+ ]
+ },
+ {
+ title: '项目卡片与四个模块',
+ description: '默认标签“项目卡片”是入口,左侧流程线包含四个项目级功能。',
+ points: [
+ '基础信息:填写项目名称与项目规模明细。',
+ '合同段管理:新建、排序、搜索、导入/导出合同段。',
+ '咨询分类系数 / 工程专业系数:维护系数预算取值和备注。'
+ ]
+ },
+ {
+ title: '基础信息填写',
+ description: '先在“基础信息”中补齐项目级数据,再进入后续合同段计算。',
+ points: [
+ '项目名称会用于导出文件名和页面展示。',
+ '项目明细表支持直接编辑、复制粘贴、撤销重做。',
+ '分组行自动汇总,顶部固定行显示总合计。'
+ ]
+ },
+ {
+ title: '合同段管理',
+ description: '在“合同段管理”中完成合同段生命周期操作。',
+ points: [
+ '“添加合同段”用于新增,卡片右上角可编辑或删除。',
+ '支持搜索、网格/列表切换,非搜索状态可拖拽排序。',
+ '更多菜单可导入/导出合同段;点击卡片进入该合同段详情。'
+ ]
+ },
+ {
+ title: '合同段详情',
+ description: '进入合同段后,左侧同样是流程线,包含规模信息和咨询服务两部分。',
+ points: [
+ '规模信息:按工程专业填写当前合同段的规模数据。',
+ '咨询服务:选择服务词典并生成服务费用明细。',
+ '合同段页面会独立缓存,不同合同段互不干扰。'
+ ]
+ },
+ {
+ title: '咨询服务与计算页',
+ description: '咨询服务页面用于管理服务明细,并进入具体计费方法页面。',
+ points: [
+ '先点击“浏览”选择服务,再确认生成明细行。',
+ '明细表“编辑”可打开服务计算页,“清空”会重置该服务计算结果。',
+ '服务计算页包含投资规模法、用地规模法、工作量法、工时法四种方法。'
+ ]
+ },
+ {
+ title: '系数维护',
+ description: '项目级系数用于调节预算取值,可在两个系数页分别维护。',
+ points: [
+ '咨询分类系数页:按咨询分类维护预算取值与说明。',
+ '工程专业系数页:按专业树维护预算取值与说明。',
+ '支持批量粘贴、撤销重做,便于一次性维护多行数据。'
+ ]
+ },
+ {
+ title: '数据管理与恢复',
+ description: '顶部工具栏负责全量数据导入导出与初始化重置。',
+ points: [
+ '“导入/导出”是整项目级别的数据包操作。',
+ '“重置”会清空本地全部数据并恢复默认页面。',
+ '建议在重要调整前先导出备份。'
+ ]
+ }
+]
+
const componentMap: Record = {
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/Xm.vue'))),
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/ContractDetailView.vue'))),
@@ -56,6 +138,8 @@ const tabContextRef = ref(null)
const dataMenuOpen = ref(false)
const dataMenuRef = ref(null)
const importFileRef = ref(null)
+const userGuideOpen = ref(false)
+const userGuideStepIndex = ref(0)
const tabItemElMap = new Map()
const tabPanelElMap = new Map()
@@ -69,6 +153,12 @@ const tabsModel = computed({
const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))
const hasClosableTabs = computed(() => tabStore.tabs.some(t => t.id !== 'XmView'))
+const activeGuideStep = computed(
+ () => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
+)
+const isFirstGuideStep = computed(() => userGuideStepIndex.value === 0)
+const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideSteps.length - 1)
+const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`)
const canCloseLeft = computed(() => {
if (contextTabIndex.value <= 0) return false
return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView')
@@ -86,6 +176,78 @@ const closeMenus = () => {
dataMenuOpen.value = false
}
+const markGuideCompleted = () => {
+ try {
+ localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
+ } catch (error) {
+ console.error('mark guide completed failed:', error)
+ }
+}
+
+const hasGuideCompleted = () => {
+ try {
+ return localStorage.getItem(USER_GUIDE_COMPLETED_KEY) === '1'
+ } catch (error) {
+ console.error('read guide completion failed:', error)
+ return false
+ }
+}
+
+const hasNonDefaultTabState = () => {
+ try {
+ const raw = localStorage.getItem('tabs')
+ if (!raw) return false
+ const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
+ const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
+ const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'XmView')
+ const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
+ return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView')
+ } catch (error) {
+ console.error('parse tabs cache failed:', error)
+ return false
+ }
+}
+
+const shouldAutoOpenGuide = async () => {
+ if (hasGuideCompleted()) return false
+ if (hasNonDefaultTabState()) return false
+ try {
+ const keys = await localforage.keys()
+ return keys.length === 0
+ } catch (error) {
+ console.error('read localforage keys failed:', error)
+ return false
+ }
+}
+
+const openUserGuide = (startAt = 0) => {
+ closeMenus()
+ userGuideStepIndex.value = Math.min(Math.max(startAt, 0), userGuideSteps.length - 1)
+ userGuideOpen.value = true
+}
+
+const closeUserGuide = (completed = false) => {
+ userGuideOpen.value = false
+ if (completed) markGuideCompleted()
+}
+
+const prevUserGuideStep = () => {
+ if (isFirstGuideStep.value) return
+ userGuideStepIndex.value -= 1
+}
+
+const nextUserGuideStep = () => {
+ if (isLastGuideStep.value) {
+ closeUserGuide(true)
+ return
+ }
+ userGuideStepIndex.value += 1
+}
+
+const jumpToGuideStep = (stepIndex: number) => {
+ userGuideStepIndex.value = Math.min(Math.max(stepIndex, 0), userGuideSteps.length - 1)
+}
+
const openTabContextMenu = (event: MouseEvent, tabId: string) => {
contextTabId.value = tabId
tabContextX.value = event.clientX
@@ -116,6 +278,24 @@ const handleGlobalMouseDown = (event: MouseEvent) => {
}
}
+const handleGlobalKeyDown = (event: KeyboardEvent) => {
+ if (!userGuideOpen.value) return
+ if (event.key === 'Escape') {
+ event.preventDefault()
+ closeUserGuide(false)
+ return
+ }
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault()
+ prevUserGuideStep()
+ return
+ }
+ if (event.key === 'ArrowRight') {
+ event.preventDefault()
+ nextUserGuideStep()
+ }
+}
+
const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
if (action === 'all') tabStore.closeAllTabs()
if (action === 'left') tabStore.closeLeftTabs(contextTabId.value)
@@ -352,14 +532,21 @@ const handleReset = async () => {
onMounted(() => {
window.addEventListener('mousedown', handleGlobalMouseDown)
+ window.addEventListener('keydown', handleGlobalKeyDown)
void nextTick(() => {
ensureActiveTabVisible()
scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
})
+ void (async () => {
+ if (await shouldAutoOpenGuide()) {
+ openUserGuide(0)
+ }
+ })()
})
onBeforeUnmount(() => {
window.removeEventListener('mousedown', handleGlobalMouseDown)
+ window.removeEventListener('keydown', handleGlobalKeyDown)
})
watch(
@@ -464,6 +651,11 @@ watch(
/>
+
+
+
+
+
+
+
+
新用户引导 · {{ guideProgressText }}
+
{{ activeGuideStep.title }}
+
+
+
+
+
+
{{ activeGuideStep.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/diyAgGridOptions.ts b/src/lib/diyAgGridOptions.ts
index 5923e82..7bbff94 100644
--- a/src/lib/diyAgGridOptions.ts
+++ b/src/lib/diyAgGridOptions.ts
@@ -37,10 +37,10 @@ export const gridOptions: GridOptions = {
singleClickEdit: true,
suppressClickEdit: false,
suppressContextMenu: false,
- autoSizeStrategy: {
- type: 'fitGridWidth',
- defaultMinWidth: 100,
- },
+ // autoSizeStrategy: {
+ // type: 'fitGridWidth',
+ // defaultMinWidth: 100,
+ // },
groupDefaultExpanded: -1,
suppressFieldDotNotation: true,
getDataPath: data => data.path,
diff --git a/src/main.ts b/src/main.ts
index 84d4398..edde7d7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,12 +2,50 @@ import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
-import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
-import { TreeDataModule ,LicenseManager,CellSelectionModule,ContextMenuModule,ClipboardModule } from 'ag-grid-enterprise';
-LicenseManager.setLicenseKey("[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b");
+import {
+ ModuleRegistry,
+ ClientSideRowModelModule,
+ ColumnAutoSizeModule,
+ CsvExportModule,
+ LargeTextEditorModule,
+ NumberEditorModule,
+ PinnedRowModule,
+ TextEditorModule,
+ TooltipModule,
+ UndoRedoEditModule,ValidationModule,LocaleModule ,CellStyleModule ,RowAutoHeightModule
+} from 'ag-grid-community'
+import {
+ AggregationModule,
+ CellSelectionModule,
+ ClipboardModule,
+ ContextMenuModule,
+ LicenseManager,
+ MenuModule,
+ RowGroupingModule,
+ TreeDataModule
+} from 'ag-grid-enterprise'
+LicenseManager.setLicenseKey("[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b")
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
-ModuleRegistry.registerModules([AllCommunityModule,TreeDataModule,CellSelectionModule,ContextMenuModule ,ClipboardModule ]);
+ModuleRegistry.registerModules([
+ ClientSideRowModelModule,
+ ColumnAutoSizeModule,
+ CsvExportModule,
+ TextEditorModule,
+ NumberEditorModule,RowAutoHeightModule,
+ LargeTextEditorModule,
+ UndoRedoEditModule,CellStyleModule ,
+ PinnedRowModule,
+ TooltipModule,
+ TreeDataModule,
+ AggregationModule,
+ RowGroupingModule,
+ MenuModule,
+ CellSelectionModule,
+ ContextMenuModule,
+ ClipboardModule,LocaleModule ,
+ ValidationModule
+])
createApp(App).use(pinia).mount('#app')
diff --git a/src/style.css b/src/style.css
index 709afb0..91fd6a7 100644
--- a/src/style.css
+++ b/src/style.css
@@ -226,6 +226,10 @@
}
.xmMx .ag-cell.editable-cell-empty,
-.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
+.xmMx .ag-cell.editable-cell-empty .ag-cell-wrapper,
+.xmMx .ag-cell.editable-cell-empty .ag-cell-value,
+.xmMx .ag-cell.editable-cell-empty .ag-cell-value * {
+ --ag-data-color: #94a3b8;
color: #94a3b8 !important;
+ font-style: italic;
}
diff --git a/vite.config.ts b/vite.config.ts
index b611ed0..069a877 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -37,6 +37,7 @@ export default defineConfig({
}
}
},
+ chunkSizeWarningLimit: 800,
// 5. 生产环境是否生成 sourcemap(默认 false,关闭可减小包体积)
sourcemap: false,
// 6. 清空输出目录(默认 true)
@@ -44,4 +45,4 @@ export default defineConfig({
},
// 7. 部署的公共路径(关键!如部署到子路径 https://xxx.com/my-app/,需设为 '/my-app/')
base: './' // 相对路径(推荐),或绝对路径 '/'
-})
\ No newline at end of file
+})