1
This commit is contained in:
1 parent
d5cae61861
commit
eb415cd0d0
33 files changed
+2642
-382
No files matched your search
+450
-82
@@ -27,8 +27,28 @@ import {
|
||||
ToastViewport,
|
||||
} from 'reka-ui'
|
||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||
import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
|
||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||
import { exportFile, serviceList, additionalWorkList, reserveList } from '@/sql'
|
||||
import {
|
||||
HOME_TAB_ID,
|
||||
LEGACY_PROJECT_TAB_ID,
|
||||
PROJECT_TAB_ID,
|
||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
||||
QUICK_CONTRACT_FALLBACK_NAME,
|
||||
QUICK_CONTRACT_ID,
|
||||
QUICK_CONTRACT_META_KEY,
|
||||
QUICK_CONTRACT_TAB_ID,
|
||||
QUICK_MAJOR_FACTOR_KEY,
|
||||
QUICK_PROJECT_INFO_KEY,
|
||||
QUICK_PROJECT_SCALE_KEY,
|
||||
QUICK_TAB_ID,
|
||||
type QuickContractMeta,
|
||||
normalizeWorkspaceMode,
|
||||
readWorkspaceMode,
|
||||
writeWorkspaceMode,
|
||||
type WorkspaceMode
|
||||
} from '@/lib/workspace'
|
||||
|
||||
interface DataEntry {
|
||||
key: string
|
||||
@@ -40,15 +60,29 @@ interface ForageStoreSnapshot {
|
||||
entries: DataEntry[]
|
||||
}
|
||||
|
||||
interface DataPackage {
|
||||
version: number
|
||||
exportedAt: string
|
||||
interface DataPackageStorage {
|
||||
localStorage: DataEntry[]
|
||||
sessionStorage: DataEntry[]
|
||||
localforageDefault: DataEntry[]
|
||||
localforageStores?: ForageStoreSnapshot[]
|
||||
}
|
||||
|
||||
interface DataPackageWorkspace {
|
||||
mode: WorkspaceMode
|
||||
}
|
||||
|
||||
interface DataPackage {
|
||||
version: number
|
||||
exportedAt: string
|
||||
packageType?: 'workspace-snapshot'
|
||||
workspace?: DataPackageWorkspace
|
||||
storage?: DataPackageStorage
|
||||
localStorage?: DataEntry[]
|
||||
sessionStorage?: DataEntry[]
|
||||
localforageDefault?: DataEntry[]
|
||||
localforageStores?: ForageStoreSnapshot[]
|
||||
}
|
||||
|
||||
interface UserGuideStep {
|
||||
title: string
|
||||
description: string
|
||||
@@ -85,6 +119,16 @@ interface ContractCardItem {
|
||||
order?: number
|
||||
}
|
||||
|
||||
interface ReportWorkspaceConfig {
|
||||
mode: WorkspaceMode
|
||||
projectInfoKey: string
|
||||
projectScaleKey: string
|
||||
consultCategoryFactorKey: string
|
||||
majorFactorKey: string
|
||||
contractCardsKey?: string
|
||||
quickContractKey?: string
|
||||
}
|
||||
|
||||
interface ZxFwRowLike {
|
||||
id: string
|
||||
process?: unknown
|
||||
@@ -107,6 +151,8 @@ interface ScaleMethodRowLike extends ScaleRowLike {
|
||||
benchmarkBudget?: unknown
|
||||
benchmarkBudgetBasic?: unknown
|
||||
benchmarkBudgetOptional?: unknown
|
||||
benchmarkBudgetBasicChecked?: unknown
|
||||
benchmarkBudgetOptionalChecked?: unknown
|
||||
budgetFee?: unknown
|
||||
budgetFeeBasic?: unknown
|
||||
budgetFeeOptional?: unknown
|
||||
@@ -139,6 +185,7 @@ interface QuantityMethodRowLike {
|
||||
|
||||
interface WorkloadMethodRowLike {
|
||||
id: string
|
||||
conversion?: unknown
|
||||
budgetAdoptedUnitPrice?: unknown
|
||||
workload?: unknown
|
||||
basicFee?: unknown
|
||||
@@ -343,17 +390,32 @@ interface ExportReserve {
|
||||
}
|
||||
|
||||
interface ExportReportPayload {
|
||||
version: number
|
||||
reportType: 'budget-report'
|
||||
mode: WorkspaceMode
|
||||
name: string
|
||||
writer: string
|
||||
reviewer: string
|
||||
date: string
|
||||
industry: number
|
||||
fee: number
|
||||
scaleCost: number
|
||||
scaleCost: number | null
|
||||
scale: ExportScaleRow[]
|
||||
serviceCoes: ExportServiceCoe[]
|
||||
majorCoes: ExportMajorCoe[]
|
||||
contracts: ExportContract[]
|
||||
project: {
|
||||
name: string
|
||||
writer: string
|
||||
reviewer: string
|
||||
date: string
|
||||
industry: number
|
||||
fee: number
|
||||
scaleCost: number | null
|
||||
scale: ExportScaleRow[]
|
||||
serviceCoes: ExportServiceCoe[]
|
||||
majorCoes: ExportMajorCoe[]
|
||||
}
|
||||
}
|
||||
|
||||
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
|
||||
@@ -364,6 +426,7 @@ const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1'
|
||||
const PINIA_PERSIST_DB_NAME = 'DB'
|
||||
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
|
||||
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const
|
||||
const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request'
|
||||
const userGuideSteps: UserGuideStep[] = [
|
||||
{
|
||||
title: '欢迎使用',
|
||||
@@ -440,7 +503,9 @@ const userGuideSteps: UserGuideStep[] = [
|
||||
]
|
||||
|
||||
const componentMap: Record<string, any> = {
|
||||
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/xmCard.vue'))),
|
||||
[HOME_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/HomeEntryView.vue'))),
|
||||
[PROJECT_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/ProjectWorkspaceView.vue'))),
|
||||
[LEGACY_PROJECT_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/ProjectWorkspaceView.vue'))),
|
||||
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/htCard.vue'))),
|
||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
||||
@@ -449,13 +514,15 @@ const componentMap: Record<string, any> = {
|
||||
const tabStore = useTabStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const kvStore = useKvStore()
|
||||
const protectedTabIdSet = new Set<string>([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID])
|
||||
const isTabClosable = (tabId: string) => !protectedTabIdSet.has(tabId)
|
||||
|
||||
|
||||
|
||||
const tabContextOpen = ref(false)
|
||||
const tabContextX = ref(0)
|
||||
const tabContextY = ref(0)
|
||||
const contextTabId = ref<string>('XmView')
|
||||
const contextTabId = ref<string>(HOME_TAB_ID)
|
||||
const tabContextRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const dataMenuOpen = ref(false)
|
||||
@@ -491,8 +558,13 @@ const tabsModel = computed({
|
||||
})
|
||||
|
||||
const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))
|
||||
const isHomeOnlyView = computed(
|
||||
() => tabStore.tabs.length === 1 && tabStore.tabs[0]?.id === HOME_TAB_ID && tabStore.activeTabId === HOME_TAB_ID
|
||||
)
|
||||
const showTabStrip = computed(() => !isHomeOnlyView.value)
|
||||
const showWorkspaceActions = computed(() => !isHomeOnlyView.value)
|
||||
|
||||
const hasClosableTabs = computed(() => tabStore.tabs.some(t => t.id !== 'XmView'))
|
||||
const hasClosableTabs = computed(() => tabStore.tabs.some(t => isTabClosable(t.id)))
|
||||
const activeGuideStep = computed(
|
||||
() => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
|
||||
)
|
||||
@@ -501,14 +573,14 @@ const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideStep
|
||||
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')
|
||||
return tabStore.tabs.slice(0, contextTabIndex.value).some(t => isTabClosable(t.id))
|
||||
})
|
||||
const canCloseRight = computed(() => {
|
||||
if (contextTabIndex.value < 0) return false
|
||||
return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => t.id !== 'XmView')
|
||||
return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => isTabClosable(t.id))
|
||||
})
|
||||
const canCloseOther = computed(() =>
|
||||
tabStore.tabs.some(t => t.id !== 'XmView' && t.id !== contextTabId.value)
|
||||
tabStore.tabs.some(t => isTabClosable(t.id) && t.id !== contextTabId.value)
|
||||
)
|
||||
|
||||
const closeMenus = () => {
|
||||
@@ -516,6 +588,79 @@ const closeMenus = () => {
|
||||
dataMenuOpen.value = false
|
||||
}
|
||||
|
||||
const normalizeTabItem = (tab: Record<string, unknown>) => {
|
||||
const rawId = typeof tab.id === 'string' ? tab.id : ''
|
||||
const rawComponentName = typeof tab.componentName === 'string' ? tab.componentName : rawId
|
||||
if (!rawId && !rawComponentName) return null
|
||||
|
||||
if (rawId === LEGACY_PROJECT_TAB_ID || rawComponentName === LEGACY_PROJECT_TAB_ID) {
|
||||
return {
|
||||
...tab,
|
||||
id: PROJECT_TAB_ID,
|
||||
title: '项目计算',
|
||||
componentName: PROJECT_TAB_ID
|
||||
}
|
||||
}
|
||||
|
||||
if (rawId === HOME_TAB_ID || rawComponentName === HOME_TAB_ID) {
|
||||
return {
|
||||
...tab,
|
||||
id: HOME_TAB_ID,
|
||||
title: typeof tab.title === 'string' && tab.title.trim() ? tab.title : '首页',
|
||||
componentName: HOME_TAB_ID
|
||||
}
|
||||
}
|
||||
|
||||
if (rawId === PROJECT_TAB_ID || rawComponentName === PROJECT_TAB_ID) {
|
||||
return {
|
||||
...tab,
|
||||
id: PROJECT_TAB_ID,
|
||||
title: typeof tab.title === 'string' && tab.title.trim() ? tab.title : '项目计算',
|
||||
componentName: PROJECT_TAB_ID
|
||||
}
|
||||
}
|
||||
|
||||
if (rawId === QUICK_TAB_ID || rawComponentName === QUICK_TAB_ID) {
|
||||
return {
|
||||
...tab,
|
||||
id: HOME_TAB_ID,
|
||||
title: '首页',
|
||||
componentName: HOME_TAB_ID
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...tab,
|
||||
id: rawId || rawComponentName,
|
||||
componentName: rawComponentName || rawId
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTabStoreState = () => {
|
||||
const normalizedTabs = (Array.isArray(tabStore.tabs) ? tabStore.tabs : [])
|
||||
.map(tab => normalizeTabItem(tab as unknown as Record<string, unknown>))
|
||||
.filter((tab): tab is NonNullable<ReturnType<typeof normalizeTabItem>> => Boolean(tab))
|
||||
|
||||
const uniqueTabs = normalizedTabs.filter((tab, index, source) =>
|
||||
source.findIndex(item => item.id === tab.id) === index
|
||||
)
|
||||
|
||||
const nextTabs = uniqueTabs.filter(tab => !(tab.id === HOME_TAB_ID && uniqueTabs.length > 1))
|
||||
tabStore.tabs = (nextTabs.length > 0
|
||||
? nextTabs
|
||||
: [{
|
||||
id: HOME_TAB_ID,
|
||||
title: '首页',
|
||||
componentName: HOME_TAB_ID
|
||||
}]) as any
|
||||
if (tabStore.activeTabId === LEGACY_PROJECT_TAB_ID) {
|
||||
tabStore.activeTabId = PROJECT_TAB_ID
|
||||
}
|
||||
if (!tabStore.tabs.some(tab => tab.id === tabStore.activeTabId)) {
|
||||
tabStore.activeTabId = tabStore.tabs[0]?.id || HOME_TAB_ID
|
||||
}
|
||||
}
|
||||
|
||||
const clearReportExportToastTimer = () => {
|
||||
if (!reportExportToastTimer) return
|
||||
clearTimeout(reportExportToastTimer)
|
||||
@@ -564,9 +709,9 @@ const hasNonDefaultTabState = () => {
|
||||
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 hasCustomTabs = tabs.some(item => item?.id && isTabClosable(item.id))
|
||||
const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
|
||||
return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView')
|
||||
return hasCustomTabs || (activeTabId !== '' && isTabClosable(activeTabId))
|
||||
} catch (error) {
|
||||
console.error('parse tabs cache failed:', error)
|
||||
return false
|
||||
@@ -574,6 +719,7 @@ const hasNonDefaultTabState = () => {
|
||||
}
|
||||
|
||||
const shouldAutoOpenGuide = async () => {
|
||||
if (readWorkspaceMode() === 'home') return false
|
||||
if (hasGuideCompleted()) return false
|
||||
if (hasNonDefaultTabState()) return false
|
||||
try {
|
||||
@@ -672,7 +818,7 @@ const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
|
||||
const canMoveTab = (event: any) => {
|
||||
const draggedId = event?.draggedContext?.element?.id
|
||||
const targetIndex = event?.relatedContext?.index
|
||||
if (draggedId === 'XmView') return false
|
||||
if (protectedTabIdSet.has(draggedId)) return false
|
||||
if (typeof targetIndex === 'number' && targetIndex === 0) return false
|
||||
return true
|
||||
}
|
||||
@@ -889,6 +1035,27 @@ const flushPiniaPersistNow = async () => {
|
||||
])
|
||||
}
|
||||
|
||||
const requestXmScalePersistFlush = async () => {
|
||||
await new Promise<void>(resolve => {
|
||||
let settled = false
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
resolve()
|
||||
}, 350)
|
||||
window.dispatchEvent(new CustomEvent(XM_SCALE_FLUSH_EVENT, {
|
||||
detail: {
|
||||
done: () => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
|
||||
const keys = await store.keys()
|
||||
const entries: DataEntry[] = []
|
||||
@@ -954,12 +1121,80 @@ const formatExportTimestamp = (date: Date): string => {
|
||||
return `${yyyy}${mm}${dd}-${hh}${mi}`
|
||||
}
|
||||
|
||||
const getExportProjectName = (entries: DataEntry[]): string => {
|
||||
const getExportProjectName = (entries: DataEntry[], forageStores: ForageStoreSnapshot[] = []): string => {
|
||||
const target =
|
||||
entries.find(item => item.key === PROJECT_INFO_DB_KEY) ||
|
||||
entries.find(item => item.key === LEGACY_PROJECT_DB_KEY)
|
||||
const data = (target?.value || {}) as XmInfoLike
|
||||
return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
|
||||
if (typeof data.projectName === 'string' && data.projectName.trim()) {
|
||||
return sanitizeFileNamePart(data.projectName)
|
||||
}
|
||||
|
||||
const quickContract =
|
||||
entries.find(item => item.key === QUICK_CONTRACT_META_KEY) ||
|
||||
forageStores.flatMap(store => store.entries).find(item => item.key === QUICK_CONTRACT_META_KEY)
|
||||
const quickName = (quickContract?.value as QuickContractMeta | null)?.name
|
||||
return typeof quickName === 'string' && quickName.trim()
|
||||
? sanitizeFileNamePart(quickName)
|
||||
: '造价项目'
|
||||
}
|
||||
|
||||
const normalizeDataPackageStorage = (payload: DataPackage): DataPackageStorage => {
|
||||
const storage = payload.storage
|
||||
return {
|
||||
localStorage: normalizeEntries(storage?.localStorage ?? payload.localStorage),
|
||||
sessionStorage: normalizeEntries(storage?.sessionStorage ?? payload.sessionStorage),
|
||||
localforageDefault: normalizeEntries(storage?.localforageDefault ?? payload.localforageDefault),
|
||||
localforageStores: normalizeForageStoreSnapshots(storage?.localforageStores ?? payload.localforageStores)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeDataPackageWorkspace = (payload: DataPackage): DataPackageWorkspace => ({
|
||||
mode: normalizeWorkspaceMode(payload.workspace?.mode ?? readWorkspaceMode())
|
||||
})
|
||||
|
||||
const resolveWorkspaceModeForExport = () => {
|
||||
const activeTab = tabStore.tabs.find(tab => tab.id === tabStore.activeTabId)
|
||||
const activeContractId = typeof activeTab?.props?.contractId === 'string' ? activeTab.props.contractId : ''
|
||||
if (tabStore.activeTabId === QUICK_TAB_ID || activeContractId === QUICK_CONTRACT_ID) return 'quick'
|
||||
if (activeContractId) return 'project'
|
||||
if (tabStore.activeTabId === PROJECT_TAB_ID || tabStore.activeTabId === LEGACY_PROJECT_TAB_ID) return 'project'
|
||||
return normalizeWorkspaceMode(readWorkspaceMode())
|
||||
}
|
||||
|
||||
const getReportWorkspaceConfig = (): ReportWorkspaceConfig => {
|
||||
const mode = resolveWorkspaceModeForExport()
|
||||
if (mode === 'quick') {
|
||||
return {
|
||||
mode,
|
||||
projectInfoKey: QUICK_PROJECT_INFO_KEY,
|
||||
projectScaleKey: QUICK_PROJECT_SCALE_KEY,
|
||||
consultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
||||
majorFactorKey: QUICK_MAJOR_FACTOR_KEY,
|
||||
quickContractKey: QUICK_CONTRACT_META_KEY
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'project',
|
||||
projectInfoKey: PROJECT_INFO_DB_KEY,
|
||||
projectScaleKey: LEGACY_PROJECT_DB_KEY,
|
||||
consultCategoryFactorKey: CONSULT_CATEGORY_FACTOR_DB_KEY,
|
||||
majorFactorKey: MAJOR_FACTOR_DB_KEY,
|
||||
contractCardsKey: 'ht-card-v1'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeQuickContractMeta = (value: unknown): QuickContractMeta => {
|
||||
const raw = (value || {}) as Partial<QuickContractMeta>
|
||||
return {
|
||||
id: typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : QUICK_CONTRACT_ID,
|
||||
name: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : QUICK_CONTRACT_FALLBACK_NAME,
|
||||
updatedAt:
|
||||
typeof raw.updatedAt === 'string' && raw.updatedAt.trim()
|
||||
? raw.updatedAt
|
||||
: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const toFiniteNumber = (value: unknown): number | null => {
|
||||
@@ -985,6 +1220,74 @@ const sumNumbers = (values: Array<number | null | undefined>): number =>
|
||||
const isNonEmptyString = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0
|
||||
|
||||
const resolveScaleMethodComputedValues = (
|
||||
row: ScaleMethodRowLike,
|
||||
mode: 'cost' | 'area'
|
||||
) => {
|
||||
const scaleValue = mode === 'cost' ? row.amount : row.landArea
|
||||
const rawSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
|
||||
const basicChecked = row.benchmarkBudgetBasicChecked !== false
|
||||
const optionalChecked = row.benchmarkBudgetOptionalChecked !== false
|
||||
const fallbackBasic = rawSplit ? (basicChecked ? rawSplit.basic : 0) : null
|
||||
const fallbackOptional = rawSplit ? (optionalChecked ? rawSplit.optional : 0) : null
|
||||
const benchmarkBudgetBasic = toFiniteNumber(row.benchmarkBudgetBasic) ?? fallbackBasic
|
||||
const benchmarkBudgetOptional = toFiniteNumber(row.benchmarkBudgetOptional) ?? fallbackOptional
|
||||
const benchmarkBudget =
|
||||
toFiniteNumber(row.benchmarkBudget) ??
|
||||
(
|
||||
benchmarkBudgetBasic != null || benchmarkBudgetOptional != null
|
||||
? roundTo(addNumbers(benchmarkBudgetBasic ?? 0, benchmarkBudgetOptional ?? 0), 2)
|
||||
: null
|
||||
)
|
||||
const budgetFeeSplit = getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic,
|
||||
benchmarkBudgetOptional,
|
||||
majorFactor: row.majorFactor,
|
||||
consultCategoryFactor: row.consultCategoryFactor,
|
||||
workStageFactor: row.workStageFactor,
|
||||
workRatio: row.workRatio
|
||||
})
|
||||
|
||||
return {
|
||||
benchmarkBudget,
|
||||
benchmarkBudgetBasic,
|
||||
benchmarkBudgetOptional,
|
||||
basicFormula:
|
||||
typeof row.basicFormula === 'string'
|
||||
? row.basicFormula
|
||||
: (basicChecked ? (rawSplit?.basicFormula ?? '') : ''),
|
||||
optionalFormula:
|
||||
typeof row.optionalFormula === 'string'
|
||||
? row.optionalFormula
|
||||
: (optionalChecked ? (rawSplit?.optionalFormula ?? '') : ''),
|
||||
budgetFee: toFiniteNumber(row.budgetFee) ?? budgetFeeSplit?.total ?? null,
|
||||
budgetFeeBasic: toFiniteNumber(row.budgetFeeBasic) ?? budgetFeeSplit?.basic ?? null,
|
||||
budgetFeeOptional: toFiniteNumber(row.budgetFeeOptional) ?? budgetFeeSplit?.optional ?? null
|
||||
}
|
||||
}
|
||||
|
||||
const calcWorkloadBasicFeeFromRow = (row: WorkloadMethodRowLike) => {
|
||||
const price = toFiniteNumber(row.budgetAdoptedUnitPrice)
|
||||
const conversion = toFiniteNumber(row.conversion)
|
||||
const workload = toFiniteNumber(row.workload)
|
||||
if (price == null || conversion == null || workload == null) return null
|
||||
return roundTo(toDecimal(price).mul(conversion).mul(workload), 2)
|
||||
}
|
||||
|
||||
const calcWorkloadServiceFeeFromRow = (row: WorkloadMethodRowLike, basicFee: number | null) => {
|
||||
const serviceCoe = toFiniteNumber(row.consultCategoryFactor)
|
||||
if (basicFee == null || serviceCoe == null) return null
|
||||
return roundTo(toDecimal(basicFee).mul(serviceCoe), 2)
|
||||
}
|
||||
|
||||
const calcHourlyServiceFeeFromRow = (row: HourlyMethodRowLike) => {
|
||||
const price = toFiniteNumber(row.adoptedBudgetUnitPrice)
|
||||
const personNum = toFiniteNumber(row.personnelCount)
|
||||
const workDay = toFiniteNumber(row.workdayCount)
|
||||
if (price == null || personNum == null || workDay == null) return null
|
||||
return roundTo(toDecimal(price).mul(personNum).mul(workDay), 2)
|
||||
}
|
||||
|
||||
const getTaskIdFromRowId = (value: string): number | null => {
|
||||
const match = /^task-(\d+)-\d+$/.exec(value)
|
||||
return match ? toSafeInteger(match[1]) : null
|
||||
@@ -1063,18 +1366,6 @@ const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] =
|
||||
.filter((item): item is ExportScaleRow => Boolean(item))
|
||||
}
|
||||
|
||||
const sumLeafScaleCost = (rows: ScaleRowLike[] | undefined) => {
|
||||
if (!Array.isArray(rows)) return 0
|
||||
return sumNumbers(
|
||||
rows.map(row => {
|
||||
if (row?.isGroupRow === true) return null
|
||||
return toFiniteNumber(row?.amount)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
|
||||
if (!Array.isArray(rows)) return null
|
||||
let hasTotalValue = false
|
||||
@@ -1083,10 +1374,11 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
||||
const major = toSafeInteger(row.id)
|
||||
if (major == null) return null
|
||||
const cost = toFiniteNumber(row.amount)
|
||||
const basicFee = toFiniteNumber(row.benchmarkBudget)
|
||||
const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
|
||||
const fee = toFiniteNumber(row.budgetFee)
|
||||
const computed = resolveScaleMethodComputedValues(row, 'cost')
|
||||
const basicFee = computed.benchmarkBudget
|
||||
const basicFeeBasic = computed.benchmarkBudgetBasic
|
||||
const basicFeeOptional = computed.benchmarkBudgetOptional
|
||||
const fee = computed.budgetFee
|
||||
if (basicFee != null || fee != null) hasTotalValue = true
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
@@ -1101,9 +1393,9 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
||||
major,
|
||||
cost: cost ?? 0,
|
||||
basicFee: basicFee ?? 0,
|
||||
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
|
||||
basicFormula: computed.basicFormula,
|
||||
basicFee_basic: basicFeeBasic ?? 0,
|
||||
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
|
||||
optionalFormula: computed.optionalFormula,
|
||||
basicFee_optional: basicFeeOptional ?? 0,
|
||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||
@@ -1134,10 +1426,11 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
||||
const major = toSafeInteger(row.id)
|
||||
if (major == null) return null
|
||||
const area = toFiniteNumber(row.landArea)
|
||||
const basicFee = toFiniteNumber(row.benchmarkBudget)
|
||||
const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
|
||||
const fee = toFiniteNumber(row.budgetFee)
|
||||
const computed = resolveScaleMethodComputedValues(row, 'area')
|
||||
const basicFee = computed.benchmarkBudget
|
||||
const basicFeeBasic = computed.benchmarkBudgetBasic
|
||||
const basicFeeOptional = computed.benchmarkBudgetOptional
|
||||
const fee = computed.budgetFee
|
||||
if (basicFee != null || fee != null) hasTotalValue = true
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
@@ -1152,9 +1445,9 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
||||
major,
|
||||
area: area ?? 0,
|
||||
basicFee: basicFee ?? 0,
|
||||
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
|
||||
basicFormula: computed.basicFormula,
|
||||
basicFee_basic: basicFeeBasic ?? 0,
|
||||
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
|
||||
optionalFormula: computed.optionalFormula,
|
||||
basicFee_optional: basicFeeOptional ?? 0,
|
||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||
@@ -1183,10 +1476,10 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const task = getTaskIdFromRowId(row.id)
|
||||
if (task == null || row.basicFee == null) return null
|
||||
if (task == null) return null
|
||||
const amount = toFiniteNumber(row.workload)
|
||||
const basicFee = toFiniteNumber(row.basicFee)
|
||||
const fee = toFiniteNumber(row.serviceFee)
|
||||
const basicFee = toFiniteNumber(row.basicFee) ?? calcWorkloadBasicFeeFromRow(row)
|
||||
const fee = toFiniteNumber(row.serviceFee) ?? calcWorkloadServiceFeeFromRow(row, basicFee)
|
||||
if (fee != null) hasTotalValue = true
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
|
||||
@@ -1217,10 +1510,10 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 |
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const expert = getExpertIdFromRowId(row.id)
|
||||
if (expert == null || row.serviceBudget == null) return null
|
||||
if (expert == null) return null
|
||||
const personNum = toFiniteNumber(row.personnelCount)
|
||||
const workDay = toFiniteNumber(row.workdayCount)
|
||||
const fee = toFiniteNumber(row.serviceBudget)
|
||||
const fee = toFiniteNumber(row.serviceBudget) ?? calcHourlyServiceFeeFromRow(row)
|
||||
if (fee != null) hasTotalValue = true
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
|
||||
@@ -1416,18 +1709,20 @@ const buildReserveExport = async (contractId: string): Promise<ExportReserve | n
|
||||
}
|
||||
|
||||
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([
|
||||
kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
|
||||
kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
|
||||
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(CONSULT_CATEGORY_FACTOR_DB_KEY),
|
||||
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(MAJOR_FACTOR_DB_KEY),
|
||||
kvStore.getItem<ContractCardItem[]>('ht-card-v1')
|
||||
const config = getReportWorkspaceConfig()
|
||||
const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw, quickContractRaw] = await Promise.all([
|
||||
kvStore.getItem<XmInfoLike>(config.projectInfoKey),
|
||||
kvStore.getItem<XmInfoStorageLike>(config.projectScaleKey),
|
||||
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(config.consultCategoryFactorKey),
|
||||
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(config.majorFactorKey),
|
||||
config.contractCardsKey ? kvStore.getItem<ContractCardItem[]>(config.contractCardsKey) : Promise.resolve(null),
|
||||
config.quickContractKey ? kvStore.getItem<QuickContractMeta>(config.quickContractKey) : Promise.resolve(null)
|
||||
])
|
||||
|
||||
const projectInfo = projectInfoRaw || {}
|
||||
const projectScaleSource = projectScaleRaw || {}
|
||||
const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
|
||||
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumLeafScaleCost(projectScaleSource.detailRows)
|
||||
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount)
|
||||
projectScale.push({
|
||||
major: -1, cost: projectScaleCost,
|
||||
area: null
|
||||
@@ -1435,13 +1730,21 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
|
||||
const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows)
|
||||
const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows)
|
||||
const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目'
|
||||
const quickContract = normalizeQuickContractMeta(quickContractRaw)
|
||||
const projectName =
|
||||
config.mode === 'quick'
|
||||
? `${quickContract.name}-快速计算`
|
||||
: (isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目')
|
||||
const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
|
||||
const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
|
||||
const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
|
||||
const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)
|
||||
|
||||
const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
|
||||
const contractCards = (
|
||||
config.mode === 'quick'
|
||||
? [{ id: quickContract.id, name: quickContract.name, order: 0 }]
|
||||
: (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
|
||||
)
|
||||
.filter(item => item && typeof item.id === 'string')
|
||||
.sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))
|
||||
|
||||
@@ -1576,6 +1879,9 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
}
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
reportType: 'budget-report',
|
||||
mode: config.mode,
|
||||
name: projectName,
|
||||
writer,
|
||||
reviewer,
|
||||
@@ -1586,7 +1892,19 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
scale: projectScale,
|
||||
serviceCoes: projectServiceCoes,
|
||||
majorCoes: projectMajorCoes,
|
||||
contracts
|
||||
contracts,
|
||||
project: {
|
||||
name: projectName,
|
||||
writer,
|
||||
reviewer,
|
||||
date,
|
||||
industry,
|
||||
fee: sumNumbers(contracts.map(item => item.fee)),
|
||||
scaleCost: projectScaleCost,
|
||||
scale: projectScale,
|
||||
serviceCoes: projectServiceCoes,
|
||||
majorCoes: projectMajorCoes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1602,12 +1920,18 @@ const exportData = async () => {
|
||||
}))
|
||||
)
|
||||
const payload: DataPackage = {
|
||||
version: 2,
|
||||
version: 3,
|
||||
packageType: 'workspace-snapshot',
|
||||
exportedAt: now.toISOString(),
|
||||
localStorage: readWebStorage(localStorage),
|
||||
sessionStorage: readWebStorage(sessionStorage),
|
||||
localforageDefault: await readForage(localforage),
|
||||
localforageStores: piniaForageStores
|
||||
workspace: {
|
||||
mode: resolveWorkspaceModeForExport()
|
||||
},
|
||||
storage: {
|
||||
localStorage: readWebStorage(localStorage),
|
||||
sessionStorage: readWebStorage(sessionStorage),
|
||||
localforageDefault: await readForage(localforage),
|
||||
localforageStores: piniaForageStores
|
||||
}
|
||||
}
|
||||
|
||||
const content = await encodeZwArchive(payload)
|
||||
@@ -1617,7 +1941,7 @@ const exportData = async () => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
const projectName = getExportProjectName(payload.localforageDefault)
|
||||
const projectName = getExportProjectName(payload.storage?.localforageDefault || [], piniaForageStores)
|
||||
const timestamp = formatExportTimestamp(now)
|
||||
link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
|
||||
document.body.appendChild(link)
|
||||
@@ -1635,12 +1959,12 @@ const exportData = async () => {
|
||||
const exportReport = async () => {
|
||||
try {
|
||||
showReportExportProgress(10, '正在准备报表导出...')
|
||||
await requestXmScalePersistFlush()
|
||||
const now = new Date()
|
||||
showReportExportProgress(40, '正在汇总报表数据...')
|
||||
const payload = await buildExportReportPayload()
|
||||
showReportExportProgress(80, '正在生成并写出报表文件...')
|
||||
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
||||
console.log(payload)
|
||||
await exportFile(fileName, payload)
|
||||
finishReportExportProgress(true, '报表导出完成')
|
||||
} catch (error) {
|
||||
@@ -1654,9 +1978,7 @@ const triggerImport = () => {
|
||||
importFileRef.value?.click()
|
||||
}
|
||||
|
||||
const importData = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
const handleSelectedImportFile = async (file: File) => {
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
@@ -1665,12 +1987,34 @@ const importData = async (event: Event) => {
|
||||
}
|
||||
const buffer = await file.arrayBuffer()
|
||||
const payload = await decodeZwArchive<DataPackage>(buffer)
|
||||
pendingImportPayload.value = payload
|
||||
const normalizedStorage = normalizeDataPackageStorage(payload)
|
||||
pendingImportPayload.value = {
|
||||
...payload,
|
||||
workspace: normalizeDataPackageWorkspace(payload),
|
||||
storage: normalizedStorage
|
||||
}
|
||||
pendingImportFileName.value = file.name
|
||||
importConfirmOpen.value = true
|
||||
} catch (error) {
|
||||
console.error('import failed:', error)
|
||||
window.alert('导入失败:文件无效、已损坏或被修改。')
|
||||
}
|
||||
}
|
||||
|
||||
const handleHomeImportSelected = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ file?: File | null }>
|
||||
const file = customEvent.detail?.file
|
||||
if (!file) return
|
||||
void handleSelectedImportFile(file)
|
||||
}
|
||||
|
||||
const importData = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
await handleSelectedImportFile(file)
|
||||
} finally {
|
||||
input.value = ''
|
||||
}
|
||||
@@ -1686,10 +2030,12 @@ const confirmImportOverride = async () => {
|
||||
const payload = pendingImportPayload.value
|
||||
if (!payload) return
|
||||
try {
|
||||
writeWebStorage(localStorage, normalizeEntries(payload.localStorage))
|
||||
writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage))
|
||||
await writeForage(localforage, normalizeEntries(payload.localforageDefault))
|
||||
const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores)
|
||||
const normalizedStorage = normalizeDataPackageStorage(payload)
|
||||
const normalizedWorkspace = normalizeDataPackageWorkspace(payload)
|
||||
writeWebStorage(localStorage, normalizedStorage.localStorage)
|
||||
writeWebStorage(sessionStorage, normalizedStorage.sessionStorage)
|
||||
await writeForage(localforage, normalizedStorage.localforageDefault)
|
||||
const piniaSnapshots = normalizeForageStoreSnapshots(normalizedStorage.localforageStores)
|
||||
const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
|
||||
await Promise.all(
|
||||
getPiniaPersistStores().map(async ({ storeName, store }) => {
|
||||
@@ -1714,6 +2060,7 @@ const confirmImportOverride = async () => {
|
||||
} else {
|
||||
tabStore.resetTabs()
|
||||
}
|
||||
normalizeTabStoreState()
|
||||
|
||||
const zxFwPricingState = readPersistedState('zxFwPricing')
|
||||
if (zxFwPricingState) {
|
||||
@@ -1730,6 +2077,7 @@ const confirmImportOverride = async () => {
|
||||
zxFwPricingStore.$persistNow?.(),
|
||||
kvStore.$persistNow?.()
|
||||
])
|
||||
writeWorkspaceMode(normalizedWorkspace.mode)
|
||||
dataMenuOpen.value = false
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
@@ -1761,8 +2109,10 @@ const handleReset = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
normalizeTabStoreState()
|
||||
window.addEventListener('mousedown', handleGlobalMouseDown)
|
||||
window.addEventListener('keydown', handleGlobalKeyDown)
|
||||
window.addEventListener('home-import-selected', handleHomeImportSelected as EventListener)
|
||||
window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
|
||||
void nextTick(() => {
|
||||
bindTabStripScroll()
|
||||
@@ -1782,6 +2132,7 @@ onBeforeUnmount(() => {
|
||||
clearReportExportToastTimer()
|
||||
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
||||
window.removeEventListener('keydown', handleGlobalKeyDown)
|
||||
window.removeEventListener('home-import-selected', handleHomeImportSelected as EventListener)
|
||||
window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
|
||||
if (tabStripViewportEl) {
|
||||
tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
|
||||
@@ -1834,8 +2185,15 @@ watch(
|
||||
<ToastProvider>
|
||||
<TooltipProvider>
|
||||
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
|
||||
<div class="flex min-w-0 items-start gap-1 h-full" @mouseenter="isTabStripHover = true"
|
||||
<div
|
||||
v-if="!isHomeOnlyView"
|
||||
class="grid items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none"
|
||||
:class="showTabStrip ? 'grid-cols-[minmax(0,1fr)_auto]' : 'grid-cols-[1fr_auto]'"
|
||||
>
|
||||
<div
|
||||
v-if="showTabStrip"
|
||||
class="flex min-w-0 items-start gap-1 h-full"
|
||||
@mouseenter="isTabStripHover = true"
|
||||
@mouseleave="isTabStripHover = false">
|
||||
<button type="button" :class="[
|
||||
'h-9 w-8 self-center shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
|
||||
@@ -1857,7 +2215,7 @@ watch(
|
||||
tabStore.activeTabId === tab.id && !isTabDragging
|
||||
? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
|
||||
: 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
|
||||
tab.id !== 'XmView' ? 'cursor-move' : ''
|
||||
isTabClosable(tab.id) ? 'cursor-move' : ''
|
||||
]">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -1868,7 +2226,7 @@ watch(
|
||||
<TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
|
||||
<Button v-if="tab.id !== 'XmView'" variant="ghost" size="icon"
|
||||
<Button v-if="isTabClosable(tab.id)" variant="ghost" size="icon"
|
||||
class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
|
||||
@click.stop="tabStore.removeTab(tab.id)">
|
||||
<X class="h-3 w-3" />
|
||||
@@ -1885,14 +2243,18 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex shrink-0 self-center items-center gap-1">
|
||||
<div ref="dataMenuRef" class="relative shrink-0">
|
||||
<Button variant="outline" size="sm"
|
||||
<div v-if="showWorkspaceActions" ref="dataMenuRef" class="relative shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||
@click="dataMenuOpen = !dataMenuOpen">
|
||||
@click="dataMenuOpen = !dataMenuOpen"
|
||||
>
|
||||
<ChevronDown class="h-4 w-4 mr-1" />
|
||||
导入/导出
|
||||
</Button>
|
||||
<div v-if="dataMenuOpen"
|
||||
<div
|
||||
v-if="dataMenuOpen"
|
||||
class="absolute right-0 top-full mt-1 z-50 rounded-md border bg-background p-1 shadow-md">
|
||||
<button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||
@click="triggerImport">
|
||||
@@ -1907,16 +2269,22 @@ watch(
|
||||
导出报表
|
||||
</button>
|
||||
</div>
|
||||
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||
@click="openUserGuide(0)">
|
||||
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
||||
|
||||
<Button
|
||||
v-if="showWorkspaceActions"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||
@click="openUserGuide(0)"
|
||||
>
|
||||
<CircleHelp class="h-4 w-4 mr-1" />
|
||||
使用引导
|
||||
</Button>
|
||||
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogRoot v-if="showWorkspaceActions">
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button variant="destructive" size="sm"
|
||||
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
|
||||
|
||||
@@ -25,6 +25,7 @@ const props = withDefaults(
|
||||
scene?: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
metaText?: string
|
||||
copyText?: string
|
||||
categories: TypeLineCategory[]
|
||||
storageKey?: string
|
||||
@@ -34,6 +35,7 @@ const props = withDefaults(
|
||||
scene: 'default',
|
||||
title: '配置',
|
||||
subtitle: '',
|
||||
metaText: '',
|
||||
copyText: '',
|
||||
storageKey: '',
|
||||
defaultCategory: ''
|
||||
@@ -201,7 +203,7 @@ useMotionValueEvent(
|
||||
<TooltipProvider>
|
||||
<div class="flex h-full w-full bg-background">
|
||||
<div class="w-12/100 border-r p-2 flex flex-col gap-8 relative">
|
||||
<div v-if="props.title || props.subtitle" class="space-y-1">
|
||||
<div v-if="props.title || props.subtitle || props.metaText" class="space-y-1">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<div v-if="props.title" ref="titleRef" class="title-ellipsis-2 max-w-full font-bold text-base leading-6 text-primary">
|
||||
@@ -224,6 +226,9 @@ useMotionValueEvent(
|
||||
{{ copyBtnText }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="props.metaText" class="text-xs leading-5 text-muted-foreground">
|
||||
{{ props.metaText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-10 relative">
|
||||
|
||||
Reference in new issue
Block a user