1
This commit is contained in:
parent
ea6a244942
commit
757de9a43f
@ -1093,18 +1093,22 @@ onBeforeUnmount(() => {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<TooltipRoot>
|
||||||
type="button"
|
<TooltipTrigger as-child>
|
||||||
title="回到顶部"
|
<button
|
||||||
aria-label="回到顶部"
|
type="button"
|
||||||
:class="[
|
aria-label="回到顶部"
|
||||||
'fixed bottom-8 right-8 z-40 inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-black/15 bg-white text-black shadow-[0_10px_24px_rgba(0,0,0,0.16)] transition-all duration-300 hover:scale-105 hover:border-black/30 hover:bg-black hover:text-white',
|
:class="[
|
||||||
showScrollTopFab ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-3 opacity-0'
|
'fixed bottom-8 right-8 z-40 inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-black/15 bg-white text-black shadow-[0_10px_24px_rgba(0,0,0,0.16)] transition-all duration-300 hover:scale-105 hover:border-black/30 hover:bg-black hover:text-white',
|
||||||
]"
|
showScrollTopFab ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-3 opacity-0'
|
||||||
@click="scrollContractsToTop()"
|
]"
|
||||||
>
|
@click="scrollContractsToTop()"
|
||||||
<ArrowUp class="h-5 w-5" />
|
>
|
||||||
</button>
|
<ArrowUp class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left">回到顶部</TooltipContent>
|
||||||
|
</TooltipRoot>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showCreateModal"
|
v-if="showCreateModal"
|
||||||
|
|||||||
@ -260,25 +260,6 @@ const processCellFromClipboard = (params: any) => {
|
|||||||
return params.value
|
return params.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const fitColumnsToGrid = () => {
|
|
||||||
if (!gridApi.value) return
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
gridApi.value?.sizeColumnsToFit({ defaultMinWidth: 68 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<FactorRow>) => {
|
|
||||||
gridApi.value = event.api
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGridSizeChanged = (_event: GridSizeChangedEvent<FactorRow>) => {
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFirstDataRendered = (_event: FirstDataRenderedEvent<FactorRow>) => {
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
@ -321,9 +302,7 @@ onBeforeUnmount(() => {
|
|||||||
:processCellFromClipboard="processCellFromClipboard"
|
:processCellFromClipboard="processCellFromClipboard"
|
||||||
:undoRedoCellEditing="true"
|
:undoRedoCellEditing="true"
|
||||||
:undoRedoCellEditingLimit="20"
|
:undoRedoCellEditingLimit="20"
|
||||||
@grid-ready="handleGridReady"
|
|
||||||
@grid-size-changed="handleGridSizeChanged"
|
|
||||||
@first-data-rendered="handleFirstDataRendered"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -317,25 +317,7 @@ const processCellFromClipboard = (params:any) => {
|
|||||||
return params.value;
|
return params.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitColumnsToGrid = () => {
|
|
||||||
if (!gridApi.value) return
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
gridApi.value?.sizeColumnsToFit({ defaultMinWidth: 80 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
|
||||||
gridApi.value = event.api
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGridSizeChanged = (_event: GridSizeChangedEvent<DetailRow>) => {
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFirstDataRendered = (_event: FirstDataRenderedEvent<DetailRow>) => {
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -370,9 +352,7 @@ const handleFirstDataRendered = (_event: FirstDataRenderedEvent<DetailRow>) => {
|
|||||||
:processCellFromClipboard="processCellFromClipboard"
|
:processCellFromClipboard="processCellFromClipboard"
|
||||||
:undoRedoCellEditing="true"
|
:undoRedoCellEditing="true"
|
||||||
:undoRedoCellEditingLimit="20"
|
:undoRedoCellEditingLimit="20"
|
||||||
@grid-ready="handleGridReady"
|
|
||||||
@grid-size-changed="handleGridSizeChanged"
|
|
||||||
@first-data-rendered="handleFirstDataRendered"
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -220,8 +220,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
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'),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'ag-right-aligned-cell editable-cell-line' : 'ag-right-aligned-cell'),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>{
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
// 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,
|
aggFunc: decimalAggSum,
|
||||||
valueParser: params => {
|
valueParser: params => {
|
||||||
@ -396,25 +397,6 @@ const processCellFromClipboard = (params:any) => {
|
|||||||
return params.value;
|
return params.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitColumnsToGrid = () => {
|
|
||||||
if (!gridApi.value) return
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
gridApi.value?.sizeColumnsToFit({ defaultMinWidth: 80 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
|
||||||
gridApi.value = event.api
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGridSizeChanged = (_event: GridSizeChangedEvent<DetailRow>) => {
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFirstDataRendered = (_event: FirstDataRenderedEvent<DetailRow>) => {
|
|
||||||
fitColumnsToGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollToGridSection = () => {
|
const scrollToGridSection = () => {
|
||||||
const target = gridSectionRef.value || agGridRef.value
|
const target = gridSectionRef.value || agGridRef.value
|
||||||
@ -476,9 +458,6 @@ const scrollToGridSection = () => {
|
|||||||
:processCellFromClipboard="processCellFromClipboard"
|
:processCellFromClipboard="processCellFromClipboard"
|
||||||
:undoRedoCellEditing="true"
|
:undoRedoCellEditing="true"
|
||||||
:undoRedoCellEditingLimit="20"
|
:undoRedoCellEditingLimit="20"
|
||||||
@grid-ready="handleGridReady"
|
|
||||||
@grid-size-changed="handleGridSizeChanged"
|
|
||||||
@first-data-rendered="handleFirstDataRendered"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import draggable from 'vuedraggable'
|
|||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
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 localforage from 'localforage'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -33,10 +33,92 @@ interface DataPackage {
|
|||||||
localforageDefault: DataEntry[]
|
localforageDefault: DataEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserGuideStep {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
points: string[]
|
||||||
|
}
|
||||||
|
|
||||||
type XmInfoLike = {
|
type XmInfoLike = {
|
||||||
projectName?: unknown
|
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<string, any> = {
|
const componentMap: Record<string, any> = {
|
||||||
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/Xm.vue'))),
|
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/Xm.vue'))),
|
||||||
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/ContractDetailView.vue'))),
|
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/ContractDetailView.vue'))),
|
||||||
@ -56,6 +138,8 @@ const tabContextRef = ref<HTMLElement | null>(null)
|
|||||||
const dataMenuOpen = ref(false)
|
const dataMenuOpen = ref(false)
|
||||||
const dataMenuRef = ref<HTMLElement | null>(null)
|
const dataMenuRef = ref<HTMLElement | null>(null)
|
||||||
const importFileRef = ref<HTMLInputElement | null>(null)
|
const importFileRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const userGuideOpen = ref(false)
|
||||||
|
const userGuideStepIndex = ref(0)
|
||||||
const tabItemElMap = new Map<string, HTMLElement>()
|
const tabItemElMap = new Map<string, HTMLElement>()
|
||||||
const tabPanelElMap = new Map<string, HTMLElement>()
|
const tabPanelElMap = new Map<string, HTMLElement>()
|
||||||
|
|
||||||
@ -69,6 +153,12 @@ const tabsModel = computed({
|
|||||||
const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))
|
const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))
|
||||||
|
|
||||||
const hasClosableTabs = computed(() => tabStore.tabs.some(t => t.id !== 'XmView'))
|
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(() => {
|
const canCloseLeft = computed(() => {
|
||||||
if (contextTabIndex.value <= 0) return false
|
if (contextTabIndex.value <= 0) return false
|
||||||
return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView')
|
return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView')
|
||||||
@ -86,6 +176,78 @@ const closeMenus = () => {
|
|||||||
dataMenuOpen.value = false
|
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) => {
|
const openTabContextMenu = (event: MouseEvent, tabId: string) => {
|
||||||
contextTabId.value = tabId
|
contextTabId.value = tabId
|
||||||
tabContextX.value = event.clientX
|
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') => {
|
const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
|
||||||
if (action === 'all') tabStore.closeAllTabs()
|
if (action === 'all') tabStore.closeAllTabs()
|
||||||
if (action === 'left') tabStore.closeLeftTabs(contextTabId.value)
|
if (action === 'left') tabStore.closeLeftTabs(contextTabId.value)
|
||||||
@ -352,14 +532,21 @@ const handleReset = async () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('mousedown', handleGlobalMouseDown)
|
window.addEventListener('mousedown', handleGlobalMouseDown)
|
||||||
|
window.addEventListener('keydown', handleGlobalKeyDown)
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
ensureActiveTabVisible()
|
ensureActiveTabVisible()
|
||||||
scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
|
scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
|
||||||
})
|
})
|
||||||
|
void (async () => {
|
||||||
|
if (await shouldAutoOpenGuide()) {
|
||||||
|
openUserGuide(0)
|
||||||
|
}
|
||||||
|
})()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
||||||
|
window.removeEventListener('keydown', handleGlobalKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -464,6 +651,11 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm" class="mb-2 shrink-0 cursor-pointer" @click="openUserGuide(0)">
|
||||||
|
<CircleHelp class="h-4 w-4 mr-1" />
|
||||||
|
使用引导
|
||||||
|
</Button>
|
||||||
|
|
||||||
<AlertDialogRoot>
|
<AlertDialogRoot>
|
||||||
<AlertDialogTrigger as-child>
|
<AlertDialogTrigger as-child>
|
||||||
<Button variant="destructive" size="sm" class="mb-2 shrink-0">
|
<Button variant="destructive" size="sm" class="mb-2 shrink-0">
|
||||||
@ -538,5 +730,50 @@ watch(
|
|||||||
删除其他
|
删除其他
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="userGuideOpen"
|
||||||
|
class="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 p-4"
|
||||||
|
@click.self="closeUserGuide(false)"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-3xl rounded-xl border bg-background shadow-2xl">
|
||||||
|
<div class="flex items-start justify-between border-b px-6 py-5">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-muted-foreground">新用户引导 · {{ guideProgressText }}</p>
|
||||||
|
<h3 class="mt-1 text-lg font-semibold">{{ activeGuideStep.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeUserGuide(false)">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 px-6 py-5">
|
||||||
|
<p class="text-sm leading-6 text-foreground">{{ activeGuideStep.description }}</p>
|
||||||
|
<ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
|
||||||
|
<li v-for="(point, index) in activeGuideStep.points" :key="`${activeGuideStep.title}-${index}`">
|
||||||
|
{{ point }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
v-for="(_step, index) in userGuideSteps"
|
||||||
|
:key="`guide-dot-${index}`"
|
||||||
|
class="h-2.5 w-2.5 cursor-pointer rounded-full transition-colors"
|
||||||
|
:class="index === userGuideStepIndex ? 'bg-primary' : 'bg-muted'"
|
||||||
|
:aria-label="`跳转到第 ${index + 1} 步`"
|
||||||
|
@click="jumpToGuideStep(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<Button variant="ghost" @click="closeUserGuide(false)">稍后再看</Button>
|
||||||
|
<Button variant="outline" :disabled="isFirstGuideStep" @click="prevUserGuideStep">上一步</Button>
|
||||||
|
<Button @click="nextUserGuideStep">{{ isLastGuideStep ? '完成并不再自动弹出' : '下一步' }}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -37,10 +37,10 @@ export const gridOptions: GridOptions<any> = {
|
|||||||
singleClickEdit: true,
|
singleClickEdit: true,
|
||||||
suppressClickEdit: false,
|
suppressClickEdit: false,
|
||||||
suppressContextMenu: false,
|
suppressContextMenu: false,
|
||||||
autoSizeStrategy: {
|
// autoSizeStrategy: {
|
||||||
type: 'fitGridWidth',
|
// type: 'fitGridWidth',
|
||||||
defaultMinWidth: 100,
|
// defaultMinWidth: 100,
|
||||||
},
|
// },
|
||||||
groupDefaultExpanded: -1,
|
groupDefaultExpanded: -1,
|
||||||
suppressFieldDotNotation: true,
|
suppressFieldDotNotation: true,
|
||||||
getDataPath: data => data.path,
|
getDataPath: data => data.path,
|
||||||
|
|||||||
46
src/main.ts
46
src/main.ts
@ -2,12 +2,50 @@ import { createApp } from 'vue'
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
|
import {
|
||||||
import { TreeDataModule ,LicenseManager,CellSelectionModule,ContextMenuModule,ClipboardModule } from 'ag-grid-enterprise';
|
ModuleRegistry,
|
||||||
LicenseManager.setLicenseKey("[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b");
|
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' // 引入
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPluginPersistedstate)
|
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')
|
createApp(App).use(pinia).mount('#app')
|
||||||
|
|||||||
@ -226,6 +226,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.xmMx .ag-cell.editable-cell-empty,
|
.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;
|
color: #94a3b8 !important;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
chunkSizeWarningLimit: 800,
|
||||||
// 5. 生产环境是否生成 sourcemap(默认 false,关闭可减小包体积)
|
// 5. 生产环境是否生成 sourcemap(默认 false,关闭可减小包体积)
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
// 6. 清空输出目录(默认 true)
|
// 6. 清空输出目录(默认 true)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user