This commit is contained in:
zwgczx committed 2026-03-04 17:43:06 +08:00
1 parent 33913c29d2
commit 42fd6e48c4
19 files changed
+1163 -420

No files matched your search

+271 -50
View File
@@ -4,7 +4,8 @@ import type { ComponentPublicInstance } from 'vue'
import draggable from 'vuedraggable'
import { useTabStore } from '@/pinia/tab'
import { Button } from '@/components/ui/button'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
import localforage from 'localforage'
import {
@@ -53,6 +54,9 @@ interface ScaleRowLike {
interface XmInfoStorageLike extends XmInfoLike {
detailRows?: ScaleRowLike[]
}
interface XmScaleStorageLike {
detailRows?: ScaleRowLike[]
}
interface ContractCardItem {
id: string
@@ -215,6 +219,8 @@ interface ExportReportPayload {
}
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
const userGuideSteps: UserGuideStep[] = [
{
title: '欢迎使用',
@@ -309,10 +315,21 @@ const tabContextRef = ref<HTMLElement | null>(null)
const dataMenuOpen = ref(false)
const dataMenuRef = ref<HTMLElement | null>(null)
const importFileRef = ref<HTMLInputElement | null>(null)
const importConfirmOpen = ref(false)
const pendingImportPayload = ref<DataPackage | null>(null)
const pendingImportFileName = ref('')
const userGuideOpen = ref(false)
const userGuideStepIndex = ref(0)
const tabItemElMap = new Map<string, HTMLElement>()
const tabTitleElMap = new Map<string, HTMLElement>()
const tabPanelElMap = new Map<string, HTMLElement>()
const tabScrollAreaRef = ref<HTMLElement | null>(null)
const showTabScrollLeft = ref(false)
const showTabScrollRight = ref(false)
const isTabStripHover = ref(false)
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
let tabStripViewportEl: HTMLElement | null = null
let tabTitleOverflowRafId: number | null = null
const tabsModel = computed({
get: () => tabStore.tabs,
@@ -489,6 +506,36 @@ const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null)
return
}
tabItemElMap.delete(id)
tabTitleElMap.delete(id)
delete tabTitleOverflowMap.value[id]
scheduleUpdateTabTitleOverflow()
}
const setTabTitleRef = (id: string, el: Element | ComponentPublicInstance | null) => {
if (el instanceof HTMLElement) {
tabTitleElMap.set(id, el)
scheduleUpdateTabTitleOverflow()
return
}
tabTitleElMap.delete(id)
delete tabTitleOverflowMap.value[id]
scheduleUpdateTabTitleOverflow()
}
const updateTabTitleOverflow = () => {
const nextMap: Record<string, boolean> = {}
for (const [id, el] of tabTitleElMap.entries()) {
nextMap[id] = el.scrollWidth > el.clientWidth + 1
}
tabTitleOverflowMap.value = nextMap
}
const scheduleUpdateTabTitleOverflow = () => {
if (tabTitleOverflowRafId != null) return
tabTitleOverflowRafId = requestAnimationFrame(() => {
tabTitleOverflowRafId = null
updateTabTitleOverflow()
})
}
const setTabPanelRef = (id: string, el: Element | ComponentPublicInstance | null) => {
@@ -499,6 +546,58 @@ const setTabPanelRef = (id: string, el: Element | ComponentPublicInstance | null
tabPanelElMap.delete(id)
}
const setTabScrollAreaRef = (el: Element | ComponentPublicInstance | null) => {
if (el instanceof HTMLElement) {
tabScrollAreaRef.value = el
return
}
const rootEl =
el && typeof el === 'object' && '$el' in el ? (el as ComponentPublicInstance).$el : null
tabScrollAreaRef.value = rootEl instanceof HTMLElement ? rootEl : null
}
const getTabStripViewport = () =>
tabScrollAreaRef.value?.querySelector<HTMLElement>('[data-slot="scroll-area-viewport"]') || null
const updateTabScrollButtons = () => {
const viewport = getTabStripViewport()
if (!viewport) {
showTabScrollLeft.value = false
showTabScrollRight.value = false
return
}
const maxLeft = Math.max(0, viewport.scrollWidth - viewport.clientWidth)
showTabScrollLeft.value = viewport.scrollLeft > 1
showTabScrollRight.value = viewport.scrollLeft < maxLeft - 1
}
const handleTabStripScroll = () => {
updateTabScrollButtons()
}
const bindTabStripScroll = () => {
const viewport = getTabStripViewport()
if (tabStripViewportEl === viewport) {
updateTabScrollButtons()
return
}
if (tabStripViewportEl) {
tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
}
tabStripViewportEl = viewport
if (tabStripViewportEl) {
tabStripViewportEl.addEventListener('scroll', handleTabStripScroll, { passive: true })
}
updateTabScrollButtons()
}
const scrollTabStripBy = (delta: number) => {
const viewport = getTabStripViewport()
if (!viewport) return
viewport.scrollBy({ left: delta, behavior: 'smooth' })
requestAnimationFrame(updateTabScrollButtons)
}
const ensureActiveTabVisible = () => {
const activeId = tabStore.activeTabId
const el = tabItemElMap.get(activeId)
@@ -619,7 +718,9 @@ const formatExportTimestamp = (date: Date): string => {
}
const getExportProjectName = (entries: DataEntry[]): string => {
const target = entries.find(item => item.key === 'xm-info-v3')
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) : '造价项目'
}
@@ -853,14 +954,16 @@ const buildServiceFee = (
}
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const [xmInfoRaw, contractCardsRaw] = await Promise.all([
localforage.getItem<XmInfoStorageLike>('xm-info-v3'),
const [projectInfoRaw, projectScaleRaw, contractCardsRaw] = await Promise.all([
localforage.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
localforage.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
localforage.getItem<ContractCardItem[]>('ht-card-v1')
])
const xmInfo = xmInfoRaw || {}
const projectScale = buildScaleRows(xmInfo.detailRows)
const projectName = isNonEmptyString(xmInfo.projectName) ? xmInfo.projectName.trim() : '造价项目'
const projectInfo = projectInfoRaw || {}
const projectScaleSource = projectScaleRaw || {}
const projectScale = buildScaleRows(projectScaleSource.detailRows)
const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目'
const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
.filter(item => item && typeof item.id === 'string')
@@ -1000,7 +1103,27 @@ const importData = async (event: Event) => {
}
const buffer = await file.arrayBuffer()
const payload = await decodeZwArchive<DataPackage>(buffer)
pendingImportPayload.value = payload
pendingImportFileName.value = file.name
importConfirmOpen.value = true
} catch (error) {
console.error('import failed:', error)
window.alert('导入失败:文件无效、已损坏或被修改。')
} finally {
input.value = ''
}
}
const cancelImportConfirm = () => {
importConfirmOpen.value = false
pendingImportPayload.value = null
pendingImportFileName.value = ''
}
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))
@@ -1009,10 +1132,10 @@ const importData = async (event: Event) => {
dataMenuOpen.value = false
window.location.reload()
} catch (error) {
console.error('import failed:', error)
window.alert('导入失败:文件无效、已损坏或被修改。')
console.error('import apply failed:', error)
window.alert('导入失败:写入本地数据时发生错误。')
} finally {
input.value = ''
cancelImportConfirm()
}
}
@@ -1032,8 +1155,12 @@ const handleReset = async () => {
onMounted(() => {
window.addEventListener('mousedown', handleGlobalMouseDown)
window.addEventListener('keydown', handleGlobalKeyDown)
window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
void nextTick(() => {
bindTabStripScroll()
ensureActiveTabVisible()
updateTabScrollButtons()
scheduleUpdateTabTitleOverflow()
scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
})
void (async () => {
@@ -1046,6 +1173,15 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('mousedown', handleGlobalMouseDown)
window.removeEventListener('keydown', handleGlobalKeyDown)
window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
if (tabStripViewportEl) {
tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
tabStripViewportEl = null
}
if (tabTitleOverflowRafId != null) {
cancelAnimationFrame(tabTitleOverflowRafId)
tabTitleOverflowRafId = null
}
})
watch(
@@ -1053,7 +1189,10 @@ watch(
(nextId, prevId) => {
saveTabInnerScrollTop(prevId)
void nextTick(() => {
bindTabStripScroll()
ensureActiveTabVisible()
updateTabScrollButtons()
scheduleUpdateTabTitleOverflow()
scheduleRestoreTabInnerScrollTop(nextId)
})
}
@@ -1073,54 +1212,100 @@ watch(
} catch (error) {
console.error('cleanup tab scroll cache failed:', error)
}
void nextTick(() => {
bindTabStripScroll()
updateTabScrollButtons()
scheduleUpdateTabTitleOverflow()
})
}
)
</script>
<template>
<TooltipProvider>
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
<div class="flex items-start gap-2 border-b bg-muted/30 px-2 pt-2 flex-none">
<ScrollArea type="auto" class="min-w-0 flex-1 whitespace-nowrap pb-2">
<draggable
v-model="tabsModel"
item-key="id"
tag="div"
class="flex w-max gap-1"
:animation="180"
:move="canMoveTab"
<div
class="mb-2 flex min-w-0 flex-1 items-center gap-1"
@mouseenter="isTabStripHover = true"
@mouseleave="isTabStripHover = false"
>
<button
type="button"
:class="[
'h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
isTabStripHover && showTabScrollLeft ? 'opacity-100' : 'pointer-events-none opacity-0'
]"
@click="scrollTabStripBy(-260)"
>
<template #item="{ element: tab }">
<div
:ref="el => setTabItemRef(tab.id, el)"
@click="tabStore.activeTabId = tab.id"
@contextmenu.prevent="openTabContextMenu($event, tab.id)"
:class="[
'group relative flex items-center h-9 px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border-x border-t transition-all text-sm',
tabStore.activeTabId === tab.id
? 'bg-background border-border font-medium'
: 'border-transparent hover:bg-muted text-muted-foreground',
tab.id !== 'XmView' ? 'cursor-move' : ''
]"
>
<span class="truncate mr-2">{{ tab.title }}</span>
<Button
v-if="tab.id !== 'XmView'"
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)"
&lt;
</button>
<ScrollArea :ref="setTabScrollAreaRef" type="auto" class="tab-strip-scroll-area min-w-0 flex-1 whitespace-nowrap">
<draggable
v-model="tabsModel"
item-key="id"
tag="div"
class="flex w-max gap-1"
:animation="180"
:move="canMoveTab"
>
<template #item="{ element: tab }">
<div
:ref="el => setTabItemRef(tab.id, el)"
@click="tabStore.activeTabId = tab.id"
@contextmenu.prevent="openTabContextMenu($event, tab.id)"
:class="[
'group relative flex items-center h-9 px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border-x border-t transition-all text-sm',
tabStore.activeTabId === tab.id
? 'bg-background border-border font-medium'
: 'border-transparent hover:bg-muted text-muted-foreground',
tab.id !== 'XmView' ? 'cursor-move' : ''
]"
>
<X class="h-3 w-3" />
</Button>
</div>
</template>
</draggable>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<TooltipRoot>
<TooltipTrigger as-child>
<span
:ref="el => setTabTitleRef(tab.id, el)"
class="truncate mr-2"
>
{{ tab.title }}
</span>
</TooltipTrigger>
<TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
</TooltipRoot>
<Button
v-if="tab.id !== 'XmView'"
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" />
</Button>
</div>
</template>
</draggable>
</ScrollArea>
<button
type="button"
:class="[
'h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
isTabStripHover && showTabScrollRight ? 'opacity-100' : 'pointer-events-none opacity-0'
]"
@click="scrollTabStripBy(260)"
>
&gt;
</button>
</div>
<div ref="dataMenuRef" class="relative mb-2 shrink-0">
<Button variant="outline" size="sm" class="cursor-pointer" @click="dataMenuOpen = !dataMenuOpen">
<Button
variant="outline"
size="sm"
class="h-9 min-h-9 px-3 py-0 text-sm leading-none cursor-pointer"
@click="dataMenuOpen = !dataMenuOpen"
>
<ChevronDown class="h-4 w-4 mr-1" />
导入/导出
</Button>
@@ -1156,14 +1341,19 @@ watch(
/>
</div>
<Button variant="outline" size="sm" class="mb-2 shrink-0 cursor-pointer" @click="openUserGuide(0)">
<Button
variant="outline"
size="sm"
class="mb-2 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>
<AlertDialogTrigger as-child>
<Button variant="destructive" size="sm" class="mb-2 shrink-0">
<Button variant="destructive" size="sm" class="mb-2 h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none">
<RotateCcw class="h-4 w-4 mr-1" />
重置
</Button>
@@ -1186,6 +1376,26 @@ watch(
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialogRoot>
<AlertDialogRoot :open="importConfirmOpen" @update:open="importConfirmOpen = $event">
<AlertDialogPortal>
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
<AlertDialogTitle class="text-base font-semibold">确认导入覆盖</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
将使用“{{ pendingImportFileName || '所选文件' }}”覆盖当前本地全部数据,是否继续?
</AlertDialogDescription>
<div class="mt-4 flex items-center justify-end gap-2">
<AlertDialogCancel as-child>
<Button variant="outline" @click="cancelImportConfirm">取消</Button>
</AlertDialogCancel>
<AlertDialogAction as-child>
<Button variant="destructive" @click="confirmImportOverride">确认覆盖</Button>
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialogRoot>
</div>
<div class="flex-1 overflow-auto relative">
@@ -1194,7 +1404,7 @@ watch(
:key="tab.id"
:ref="el => setTabPanelRef(tab.id, el)"
v-show="tabStore.activeTabId === tab.id"
class="h-full w-full p-6 animate-in fade-in duration-300"
class="h-full w-full p-4 animate-in fade-in duration-300"
>
<component :is="componentMap[tab.componentName]" v-bind="tab.props || {}" />
</div>
@@ -1281,4 +1491,15 @@ watch(
</div>
</div>
</div>
</TooltipProvider>
</template>
<style scoped>
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) {
scrollbar-width: none;
}
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]::-webkit-scrollbar) {
display: none;
}
</style>
+87 -96
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch, type Component ,onMounted} from 'vue'
import { computed, onBeforeUnmount, ref, watch, type Component, onMounted } from 'vue'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import {
DialogClose,
DialogContent,
@@ -9,7 +10,7 @@ import {
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger,DialogDescription
DialogTrigger, DialogDescription
} from 'reka-ui'
import { useWindowSize } from '@vueuse/core'
import { animate, AnimatePresence, Motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion-v'
@@ -58,6 +59,8 @@ watch(
{ deep: true }
)
const switchCategory = (cat: string) => {
activeCategory.value = cat
sessionStorage.setItem(cacheKey.value, cat)
@@ -70,7 +73,18 @@ const activeComponent = computed(() => {
const copyBtnText = ref('复制')
const sheetOpen = ref(false)
const titleRef = ref<HTMLElement | null>(null)
const isTitleOverflow = ref(false)
const subtitleRef = ref<HTMLElement | null>(null)
const isSubtitleOverflow = ref(false)
let copyBtnTimer: ReturnType<typeof setTimeout> | null = null
let titleOverflowRafId: number | null = null
const handleCopySubtitle = async () => {
const text = (props.copyText || '').trim()
@@ -92,6 +106,10 @@ const handleCopySubtitle = async () => {
onBeforeUnmount(() => {
if (copyBtnTimer) clearTimeout(copyBtnTimer)
if (titleOverflowRafId != null) {
cancelAnimationFrame(titleOverflowRafId)
titleOverflowRafId = null
}
if (!root) return
root.style.scale = ''
root.style.translate = ''
@@ -180,25 +198,29 @@ useMotionValueEvent(
</script>
<template>
<TooltipProvider>
<div class="flex h-full w-full bg-background">
<div class="w-12/100 border-r p-6 flex flex-col gap-8 relative">
<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" class="font-bold text-base leading-6 text-primary break-words">
{{ props.title }}
</div>
<div
v-if="props.subtitle"
class="flex flex-wrap items-center gap-2 text-xs leading-5 text-muted-foreground"
>
<span class="break-all">{{ props.subtitle }}</span>
<Button
v-if="props.copyText"
type="button"
variant="outline"
size="sm"
class="h-6 rounded-md px-2 text-[11px]"
@click.stop="handleCopySubtitle"
>
<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">
{{ props.title }}
</div>
</TooltipTrigger>
<TooltipContent side="right" :avoid-collisions="false">{{ props.title }}</TooltipContent>
</TooltipRoot>
<div v-if="props.subtitle" class="flex min-w-0 items-center gap-2 text-xs leading-5 text-muted-foreground">
<div class="min-w-0 flex-1">
<TooltipRoot>
<TooltipTrigger as-child>
<span ref="subtitleRef" class="block max-w-full truncate">{{ props.subtitle }}</span>
</TooltipTrigger>
<TooltipContent side="right" :avoid-collisions="false">{{ props.subtitle }}</TooltipContent>
</TooltipRoot>
</div>
<Button v-if="props.copyText" type="button" variant="outline" size="sm"
class="h-6 rounded-md px-2 text-[11px]" @click.stop="handleCopySubtitle">
{{ copyBtnText }}
</Button>
</div>
@@ -207,30 +229,22 @@ useMotionValueEvent(
<div class="flex flex-col gap-10 relative">
<div class="absolute left-[11px] top-2 bottom-2 w-[2px] bg-muted"></div>
<div
v-for="item in props.categories"
:key="item.key"
class="relative flex items-center gap-4 cursor-pointer group"
@click="switchCategory(item.key)"
>
<div
:class="[
'z-10 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all',
activeCategory === item.key
? 'bg-primary border-primary scale-110'
: 'bg-background border-muted-foreground'
]"
>
<div v-for="item in props.categories" :key="item.key"
class="relative flex items-center gap-4 cursor-pointer group" @click="switchCategory(item.key)">
<div :class="[
'z-10 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all',
activeCategory === item.key
? 'bg-primary border-primary scale-110'
: 'bg-background border-muted-foreground'
]">
<div v-if="activeCategory === item.key" class="w-2 h-2 bg-background rounded-full"></div>
</div>
<span
:class="[
'text-sm transition-colors',
activeCategory === item.key
? 'font-bold text-primary'
: 'text-muted-foreground group-hover:text-foreground'
]"
>
<span :class="[
'text-sm transition-colors',
activeCategory === item.key
? 'font-bold text-primary'
: 'text-muted-foreground group-hover:text-foreground'
]">
{{ item.label }}
</span>
</div>
@@ -238,27 +252,17 @@ useMotionValueEvent(
<DialogRoot v-model:open="sheetOpen">
<DialogTrigger as-child>
<button
type="button"
class="absolute left-4 right-4 bottom-4 flex cursor-pointer flex-col items-center gap-1.5 rounded-lg border bg-muted/35 px-3 py-2 text-center text-[12px] leading-5 text-foreground/85 shadow-sm transition-colors hover:bg-muted/55 hover:text-foreground"
>
<button type="button"
class="cursor-pointer absolute left-4 right-4 bottom-4 flex flex-col items-center gap-1.5 rounded-lg border bg-muted/35 px-3 py-2 text-center text-[12px] leading-5 text-foreground/85 shadow-sm transition-colors hover:bg-muted/55 hover:text-foreground">
<img src="/favicon.ico" alt="众为咨询" class="h-5 w-5 shrink-0 rounded-sm" />
<span>本网站由众为工程咨询有限公司提供免费技术支持</span>
</button>
</DialogTrigger>
<DialogPortal>
<AnimatePresence
multiple
as="div"
>
<AnimatePresence multiple as="div">
<DialogOverlay as-child>
<Motion
class="fixed inset-0 z-10 bg-black/45 backdrop-blur-[2px]"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
:exit="{ opacity: 0 }"
:transition="staticTransition"
/>
<Motion class="fixed inset-0 z-10 bg-black/45 backdrop-blur-[2px]" :initial="{ opacity: 0 }"
:animate="{ opacity: 1 }" :exit="{ opacity: 0 }" :transition="staticTransition" />
</DialogOverlay>
<DialogContent as-child>
@@ -267,32 +271,24 @@ useMotionValueEvent(
:style="{
y,
top: `${sheetTop}px`,
}"
drag="y"
:drag-constraints="{ top: 0 }"
@drag-end="(e, { offset, velocity }) => {
}" drag="y" :drag-constraints="{ top: 0 }" @drag-end="(e, { offset, velocity }) => {
if (offset.y > h * 0.35 || velocity.y > 10) {
sheetOpen = false;
}
else {
animate(y, 0, { ...inertiaTransition, min: 0, max: 0 });
}
}"
>
<div class="mx-auto mt-2 h-1.5 w-12 cursor-grab rounded-full bg-muted-foreground/35 active:cursor-grabbing" />
}">
<div
class="mx-auto mt-2 h-1.5 w-12 cursor-grab rounded-full bg-muted-foreground/35 active:cursor-grabbing" />
<div class="mx-auto flex h-full w-full max-w-2xl flex-col px-4 pb-5 pt-3">
<div class="mb-3">
<div class="flex justify-end">
<DialogClose class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground">
<DialogClose
class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground">
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M18 6L6 18M6 6l12 12" />
</svg>
</DialogClose>
</div>
@@ -306,31 +302,16 @@ useMotionValueEvent(
<DialogDescription class="mb-4 text-base text-muted-foreground">
<div class="flex items-center gap-2">
<a
:href="OFFICIAL_SITE_URL"
target="_blank"
rel="noopener noreferrer"
class="inline-flex cursor-pointer items-center font-medium text-foreground transition-colors hover:text-primary hover:underline"
>
<a :href="OFFICIAL_SITE_URL" target="_blank" rel="noopener noreferrer"
class="inline-flex cursor-pointer items-center font-medium text-foreground transition-colors hover:text-primary hover:underline">
众为工程咨询有限公司
</a>
<a
:href="OFFICIAL_SITE_URL"
target="_blank"
rel="noopener noreferrer"
<a :href="OFFICIAL_SITE_URL" target="_blank" rel="noopener noreferrer"
class="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground"
aria-label="跳转到官网首页"
title="官网首页"
>
aria-label="跳转到官网首页" title="官网首页">
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h10v10M7 17L17 7"
/>
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M7 7h10v10M7 17L17 7" />
</svg>
</a>
</div>
@@ -338,7 +319,8 @@ useMotionValueEvent(
<div class="space-y-4 overflow-y-auto pr-1 text-[15px] leading-7">
<p>
众为工程咨询有限公司 2009 年成立,专注工程造价与工程成本管控全过程咨询,是广东省政府审计入库优选单位。公司服务覆盖多领域、全类型客户,累计服务投资额超万亿元,深度参与港珠澳大桥、澳门大学横琴校区等国家级重点工程,参编三十余项国家及省市行业标准。
众为工程咨询有限公司 2009
年成立,专注工程造价与工程成本管控全过程咨询,是广东省政府审计入库优选单位。公司服务覆盖多领域、全类型客户,累计服务投资额超万亿元,深度参与港珠澳大桥、澳门大学横琴校区等国家级重点工程,参编三十余项国家及省市行业标准。
</p>
<p>
公司立足大湾区,布局全球,设有澳门公司、斯里兰卡分公司,具备跨境与海外项目服务能力,以十五年专业沉淀、万亿级项目经验,为客户提供精准、可靠的工程咨询服务。
@@ -349,8 +331,8 @@ useMotionValueEvent(
</DialogContent>
</AnimatePresence>
</DialogPortal>
</DialogRoot>
</DialogRoot>
</div>
<div class="w-88/100 min-h-0 h-full flex flex-col">
@@ -363,6 +345,7 @@ useMotionValueEvent(
</ScrollArea>
</div>
</div>
</TooltipProvider>
</template>
<style scoped>
/* 核心修改:添加 :deep() 穿透 scoped 作用域 */
@@ -371,4 +354,12 @@ useMotionValueEvent(
min-height: 0;
box-sizing: border-box;
}
.title-ellipsis-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
}
</style>