fix
This commit is contained in:
parent
75f293f877
commit
d8f8b629d2
5
bun.lock
5
bun.lock
@ -7,6 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-grid-community/locale": "^35.1.0",
|
"@ag-grid-community/locale": "^35.1.0",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
|
"@internationalized/date": "^3.12.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
"ag-grid-community": "^35.1.0",
|
"ag-grid-community": "^35.1.0",
|
||||||
@ -72,7 +73,7 @@
|
|||||||
|
|
||||||
"@iconify/vue": ["@iconify/vue@5.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg=="],
|
"@iconify/vue": ["@iconify/vue@5.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg=="],
|
||||||
|
|
||||||
"@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="],
|
"@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="],
|
||||||
|
|
||||||
"@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
|
"@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
|
||||||
|
|
||||||
@ -542,6 +543,8 @@
|
|||||||
|
|
||||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
|
"reka-ui/@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="],
|
||||||
|
|
||||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
|
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
|
||||||
|
|
||||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-grid-community/locale": "^35.1.0",
|
"@ag-grid-community/locale": "^35.1.0",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
|
"@internationalized/date": "^3.12.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
"ag-grid-community": "^35.1.0",
|
"ag-grid-community": "^35.1.0",
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { CellValueChangedEvent, ColDef } from 'ag-grid-community'
|
import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
import { formatThousands } from '@/lib/numberFormat'
|
||||||
import { industryTypeList } from '@/sql'
|
import { industryTypeList } from '@/sql'
|
||||||
|
import { SwitchRoot, SwitchThumb } from 'reka-ui'
|
||||||
|
|
||||||
interface DetailRow {
|
interface DetailRow {
|
||||||
id: string
|
id: string
|
||||||
@ -20,12 +21,19 @@ interface DetailRow {
|
|||||||
amount: number | null
|
amount: number | null
|
||||||
landArea: number | null
|
landArea: number | null
|
||||||
path: string[]
|
path: string[]
|
||||||
|
hide?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
projectIndustry?: string
|
projectIndustry?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GridPersistState {
|
||||||
|
detailRows?: DetailRow[]
|
||||||
|
roughCalcEnabled?: boolean
|
||||||
|
totalAmount?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
rowData: DetailRow[]
|
rowData: DetailRow[]
|
||||||
@ -34,6 +42,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const BASE_INFO_KEY = 'xm-base-info-v1'
|
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const gridApi = ref<GridApi<DetailRow> | null>(null)
|
||||||
const activeIndustryId = ref('')
|
const activeIndustryId = ref('')
|
||||||
const industryNameMap = new Map(
|
const industryNameMap = new Map(
|
||||||
industryTypeList.flatMap(item => [
|
industryTypeList.flatMap(item => [
|
||||||
@ -45,6 +54,18 @@ const totalLabel = computed(() => {
|
|||||||
const industryName = industryNameMap.get(activeIndustryId.value.trim()) || ''
|
const industryName = industryNameMap.get(activeIndustryId.value.trim()) || ''
|
||||||
return industryName ? `${industryName}总投资` : '总投资'
|
return industryName ? `${industryName}总投资` : '总投资'
|
||||||
})
|
})
|
||||||
|
const roughCalcEnabled = ref(false)
|
||||||
|
const visibleRowData = computed(() => props.rowData.filter(row => !row.hide))
|
||||||
|
|
||||||
|
const refreshPinnedTotalLabelCell = () => {
|
||||||
|
if (!gridApi.value) return
|
||||||
|
const pinnedTopNode = gridApi.value.getPinnedTopRow(0)
|
||||||
|
if (!pinnedTopNode) return
|
||||||
|
gridApi.value.refreshCells({
|
||||||
|
rowNodes: [pinnedTopNode],
|
||||||
|
force: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const columnDefs: ColDef<DetailRow>[] = [
|
const columnDefs: ColDef<DetailRow>[] = [
|
||||||
{
|
{
|
||||||
@ -53,14 +74,21 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
|
editable: params => {
|
||||||
|
if (roughCalcEnabled.value) return Boolean(params.node?.rowPinned)
|
||||||
|
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
||||||
|
},
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
roughCalcEnabled.value && params.node?.rowPinned
|
||||||
|
? 'ag-right-aligned-cell editable-cell-line'
|
||||||
|
: !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'ag-right-aligned-cell editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: 'ag-right-aligned-cell',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
roughCalcEnabled.value && params.node?.rowPinned
|
||||||
|
? params.value == null || params.value === ''
|
||||||
|
: !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
aggFunc: decimalAggSum,
|
aggFunc: decimalAggSum,
|
||||||
valueParser: params => {
|
valueParser: params => {
|
||||||
@ -69,6 +97,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
return Number.isFinite(v) ? roundTo(v, 2) : null
|
return Number.isFinite(v) ? roundTo(v, 2) : null
|
||||||
},
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
|
if (roughCalcEnabled.value) {
|
||||||
|
if (!params.node?.rowPinned) return ''
|
||||||
|
if (params.value == null || params.value === '') return '点击输入'
|
||||||
|
return formatThousands(params.value)
|
||||||
|
}
|
||||||
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -85,7 +118,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
editable: params => !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'ag-right-aligned-cell editable-cell-line'
|
||||||
@ -100,6 +133,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
return Number.isFinite(v) ? roundTo(v, 3) : null
|
return Number.isFinite(v) ? roundTo(v, 3) : null
|
||||||
},
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
|
if (roughCalcEnabled.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) {
|
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -129,18 +165,16 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalAmount = computed(() => sumByNumber(props.rowData, row => row.amount))
|
const pinnedTopRowData = ref<DetailRow[]>([
|
||||||
|
|
||||||
const pinnedTopRowData = computed<DetailRow[]>(() => [
|
|
||||||
{
|
{
|
||||||
id: 'pinned-total-row',
|
id: 'pinned-total-row',
|
||||||
groupCode: '',
|
groupCode: '',
|
||||||
groupName: '',
|
groupName: '',
|
||||||
majorCode: '',
|
majorCode: '',
|
||||||
majorName: totalLabel.value,
|
majorName: '',
|
||||||
hasCost: false,
|
hasCost: false,
|
||||||
hasArea: false,
|
hasArea: false,
|
||||||
amount: totalAmount.value,
|
amount: null,
|
||||||
landArea: null,
|
landArea: null,
|
||||||
path: ['TOTAL']
|
path: ['TOTAL']
|
||||||
}
|
}
|
||||||
@ -148,19 +182,74 @@ const pinnedTopRowData = computed<DetailRow[]>(() => [
|
|||||||
|
|
||||||
const saveToIndexedDB = async () => {
|
const saveToIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
await localforage.setItem(props.dbKey, {
|
const payload: GridPersistState = {
|
||||||
detailRows: JSON.parse(JSON.stringify(props.rowData))
|
detailRows: props.rowData.map(row => ({
|
||||||
})
|
...JSON.parse(JSON.stringify(row)),
|
||||||
|
hide: Boolean(row.hide)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
payload.roughCalcEnabled = roughCalcEnabled.value
|
||||||
|
payload.totalAmount = pinnedTopRowData.value[0].amount
|
||||||
|
await localforage.setItem(props.dbKey, payload)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCellValueChanged = (_event: CellValueChangedEvent) => {
|
const schedulePersist = () => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
if (persistTimer) clearTimeout(persistTimer)
|
||||||
persistTimer = setTimeout(() => {
|
persistTimer = setTimeout(() => {
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
}, 1000)
|
}, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDetailRowsHidden = (hidden: boolean) => {
|
||||||
|
for (const row of props.rowData) {
|
||||||
|
row.hide = hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRoughCalcSwitch = (checked: boolean) => {
|
||||||
|
gridApi.value?.stopEditing(true)
|
||||||
|
roughCalcEnabled.value = checked
|
||||||
|
setDetailRowsHidden(checked)
|
||||||
|
if (!checked) {
|
||||||
|
syncPinnedTotalForNormalMode()
|
||||||
|
}else{
|
||||||
|
pinnedTopRowData.value[0].amount = null
|
||||||
|
const pinnedTopNode = gridApi.value?.getPinnedTopRow(0)
|
||||||
|
if (pinnedTopNode) {
|
||||||
|
pinnedTopNode.setDataValue('amount', null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schedulePersist()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const onCellValueChanged = (event: CellValueChangedEvent) => {
|
||||||
|
if (roughCalcEnabled.value && event.node?.rowPinned && event.colDef.field === 'amount') {
|
||||||
|
if (typeof event.newValue === 'number') {
|
||||||
|
pinnedTopRowData.value[0].amount = roundTo(event.newValue, 2)
|
||||||
|
} else {
|
||||||
|
const parsed = Number(event.newValue)
|
||||||
|
pinnedTopRowData.value[0].amount = Number.isFinite(parsed) ? roundTo(parsed, 2) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (!roughCalcEnabled.value) {
|
||||||
|
syncPinnedTotalForNormalMode()
|
||||||
|
}
|
||||||
|
schedulePersist()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||||
|
gridApi.value = event.api
|
||||||
|
void loadIndustryFromBaseInfo()
|
||||||
|
|
||||||
|
|
||||||
|
void loadGridPersistState()
|
||||||
|
void refreshPinnedTotalLabelCell()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
@ -191,16 +280,69 @@ const loadIndustryFromBaseInfo = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const loadGridPersistState = async () => {
|
||||||
void loadIndustryFromBaseInfo()
|
try {
|
||||||
|
const data = await localforage.getItem<GridPersistState>(props.dbKey)
|
||||||
|
roughCalcEnabled.value = Boolean(data?.roughCalcEnabled)
|
||||||
|
const detailRows = Array.isArray(data?.detailRows) ? data.detailRows : []
|
||||||
|
const detailRowById = new Map(detailRows.map(row => [row.id, row]))
|
||||||
|
for (const row of props.rowData) {
|
||||||
|
const persisted = detailRowById.get(row.id)
|
||||||
|
if (!persisted) {
|
||||||
|
row.hide = roughCalcEnabled.value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row.amount = typeof persisted.amount === 'number' ? roundTo(persisted.amount, 2) : null
|
||||||
|
row.landArea = typeof persisted.landArea === 'number' ? roundTo(persisted.landArea, 3) : null
|
||||||
|
row.hide = typeof persisted.hide === 'boolean' ? persisted.hide : roughCalcEnabled.value
|
||||||
|
}
|
||||||
|
pinnedTopRowData.value[0].amount = typeof data?.totalAmount === 'number' ? data.totalAmount : null
|
||||||
|
|
||||||
|
const pinnedTopNode = (gridApi as any).value.getPinnedTopRow(0)
|
||||||
|
if (pinnedTopNode) {
|
||||||
|
pinnedTopNode.setDataValue('amount', pinnedTopRowData.value[0].amount)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('loadGridPersistState failed:', error)
|
||||||
|
roughCalcEnabled.value = false
|
||||||
|
pinnedTopRowData.value[0].amount= null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
watch(totalLabel, () => {
|
||||||
|
refreshPinnedTotalLabelCell()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
const syncPinnedTotalForNormalMode = () => {
|
||||||
void loadIndustryFromBaseInfo()
|
if (roughCalcEnabled.value) return
|
||||||
})
|
|
||||||
|
if (!gridApi.value) {
|
||||||
|
pinnedTopRowData.value[0].amount = sumByNumber(props.rowData, row => row.amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let total = 0
|
||||||
|
let hasValue = false
|
||||||
|
|
||||||
|
props.rowData.forEach(node => {
|
||||||
|
|
||||||
|
const amount = node.amount
|
||||||
|
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
||||||
|
total += amount
|
||||||
|
hasValue = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pinnedTopRowData.value[0].amount = hasValue ? roundTo(total, 2) : null
|
||||||
|
const pinnedTopNode = gridApi.value.getPinnedTopRow(0)
|
||||||
|
if (pinnedTopNode) {
|
||||||
|
pinnedTopNode.setDataValue('amount', hasValue ? roundTo(total, 2) : null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
if (persistTimer) clearTimeout(persistTimer)
|
||||||
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -212,18 +354,30 @@ onBeforeUnmount(() => {
|
|||||||
<h3 class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
|
<h3 class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
|
||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="text-xs text-muted-foreground"></div>
|
<div class="flex items-center gap-2">
|
||||||
|
<span class=" text-xs text-muted-foreground">粗略计算</span>
|
||||||
|
<SwitchRoot
|
||||||
|
class="cursor-pointer peer h-5 w-9 shrink-0 rounded-full border border-transparent bg-muted shadow-sm transition-colors data-[state=checked]:bg-primary"
|
||||||
|
:modelValue="roughCalcEnabled"
|
||||||
|
@update:modelValue="onRoughCalcSwitch"
|
||||||
|
>
|
||||||
|
<SwitchThumb
|
||||||
|
class="block h-4 w-4 translate-x-0.5 rounded-full bg-background shadow transition-transform data-[state=checked]:translate-x-4"
|
||||||
|
/>
|
||||||
|
</SwitchRoot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ag-theme-quartz w-full flex-1 min-h-0 h-full">
|
<div class="ag-theme-quartz w-full flex-1 min-h-0 h-full">
|
||||||
<AgGridVue
|
<AgGridVue
|
||||||
:style="{ height: '100%' }"
|
:style="{ height: '100%' }"
|
||||||
:rowData="props.rowData"
|
:rowData="visibleRowData"
|
||||||
:pinnedTopRowData="pinnedTopRowData"
|
:pinnedTopRowData="pinnedTopRowData"
|
||||||
:columnDefs="columnDefs"
|
:columnDefs="columnDefs"
|
||||||
:autoGroupColumnDef="autoGroupColumnDef"
|
:autoGroupColumnDef="autoGroupColumnDef"
|
||||||
:gridOptions="gridOptions"
|
:gridOptions="gridOptions"
|
||||||
:theme="myTheme"
|
:theme="myTheme"
|
||||||
|
@grid-ready="onGridReady"
|
||||||
@cell-value-changed="onCellValueChanged"
|
@cell-value-changed="onCellValueChanged"
|
||||||
:suppressColumnVirtualisation="true"
|
:suppressColumnVirtualisation="true"
|
||||||
:suppressRowVirtualisation="true"
|
:suppressRowVirtualisation="true"
|
||||||
|
|||||||
@ -1,9 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseDate } from '@internationalized/date'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { industryTypeList } from '@/sql'
|
import { industryTypeList } from '@/sql'
|
||||||
import { CircleHelp } from 'lucide-vue-next'
|
import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next'
|
||||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import {
|
||||||
|
DatePickerAnchor,
|
||||||
|
DatePickerArrow,
|
||||||
|
DatePickerCalendar,
|
||||||
|
DatePickerCell,
|
||||||
|
DatePickerCellTrigger,
|
||||||
|
DatePickerClose,
|
||||||
|
DatePickerContent,
|
||||||
|
DatePickerField,
|
||||||
|
DatePickerGrid,
|
||||||
|
DatePickerGridBody,
|
||||||
|
DatePickerGridHead,
|
||||||
|
DatePickerGridRow,
|
||||||
|
DatePickerHeadCell,
|
||||||
|
DatePickerHeader,
|
||||||
|
DatePickerHeading,
|
||||||
|
DatePickerInput,
|
||||||
|
DatePickerNext,
|
||||||
|
DatePickerPrev,
|
||||||
|
DatePickerRoot,
|
||||||
|
DatePickerTrigger
|
||||||
|
} from 'reka-ui'
|
||||||
|
|
||||||
interface XmInfoState {
|
interface XmInfoState {
|
||||||
projectIndustry?: string
|
projectIndustry?: string
|
||||||
@ -31,6 +54,37 @@ const preparedBy = ref('')
|
|||||||
const reviewedBy = ref('')
|
const reviewedBy = ref('')
|
||||||
const preparedCompany = ref('')
|
const preparedCompany = ref('')
|
||||||
const preparedDate = ref('')
|
const preparedDate = ref('')
|
||||||
|
const preparedDatePickerValue = ref<any>(undefined)
|
||||||
|
|
||||||
|
const normalizeDateString = (value: unknown): string => {
|
||||||
|
if (typeof value !== 'string') return ''
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return ''
|
||||||
|
try {
|
||||||
|
const parsed = parseDate(trimmed)
|
||||||
|
return parsed.toString() === trimmed ? trimmed : ''
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncPreparedDatePickerFromString = () => {
|
||||||
|
if (!preparedDate.value) {
|
||||||
|
preparedDatePickerValue.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
preparedDatePickerValue.value = parseDate(preparedDate.value)
|
||||||
|
} catch {
|
||||||
|
preparedDatePickerValue.value = undefined
|
||||||
|
preparedDate.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreparedDateSelect = (date: any) => {
|
||||||
|
preparedDatePickerValue.value = date
|
||||||
|
preparedDate.value = date?.toString() ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
const majorParentNodes: MajorParentNode[] = industryTypeList.map(item => ({
|
const majorParentNodes: MajorParentNode[] = industryTypeList.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@ -69,7 +123,8 @@ const loadFromIndexedDB = async () => {
|
|||||||
preparedBy.value = typeof data.preparedBy === 'string' ? data.preparedBy : ''
|
preparedBy.value = typeof data.preparedBy === 'string' ? data.preparedBy : ''
|
||||||
reviewedBy.value = typeof data.reviewedBy === 'string' ? data.reviewedBy : ''
|
reviewedBy.value = typeof data.reviewedBy === 'string' ? data.reviewedBy : ''
|
||||||
preparedCompany.value = typeof data.preparedCompany === 'string' ? data.preparedCompany : ''
|
preparedCompany.value = typeof data.preparedCompany === 'string' ? data.preparedCompany : ''
|
||||||
preparedDate.value = typeof data.preparedDate === 'string' ? data.preparedDate : ''
|
preparedDate.value = normalizeDateString(data.preparedDate)
|
||||||
|
syncPreparedDatePickerFromString()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +135,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
reviewedBy.value = ''
|
reviewedBy.value = ''
|
||||||
preparedCompany.value = ''
|
preparedCompany.value = ''
|
||||||
preparedDate.value = ''
|
preparedDate.value = ''
|
||||||
|
syncPreparedDatePickerFromString()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
projectIndustry.value = DEFAULT_PROJECT_INDUSTRY
|
projectIndustry.value = DEFAULT_PROJECT_INDUSTRY
|
||||||
@ -88,6 +144,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
reviewedBy.value = ''
|
reviewedBy.value = ''
|
||||||
preparedCompany.value = ''
|
preparedCompany.value = ''
|
||||||
preparedDate.value = ''
|
preparedDate.value = ''
|
||||||
|
syncPreparedDatePickerFromString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +183,7 @@ const createProject = async () => {
|
|||||||
reviewedBy.value = ''
|
reviewedBy.value = ''
|
||||||
preparedCompany.value = ''
|
preparedCompany.value = ''
|
||||||
preparedDate.value = ''
|
preparedDate.value = ''
|
||||||
|
syncPreparedDatePickerFromString()
|
||||||
isProjectInitialized.value = true
|
isProjectInitialized.value = true
|
||||||
showCreateDialog.value = false
|
showCreateDialog.value = false
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
@ -238,12 +296,112 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-foreground">编制日期</label>
|
<label class="block text-sm font-medium text-foreground">编制日期</label>
|
||||||
<input
|
<DatePickerRoot
|
||||||
v-model="preparedDate"
|
locale="en-CA"
|
||||||
type="text"
|
:model-value="preparedDatePickerValue"
|
||||||
placeholder="请输入编制日期"
|
@update:model-value="handlePreparedDateSelect"
|
||||||
class="mt-2 h-10 w-full rounded-lg border bg-background px-4 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring"
|
>
|
||||||
/>
|
<DatePickerAnchor class="mt-2 block w-full">
|
||||||
|
<DatePickerField
|
||||||
|
v-slot="{ segments }"
|
||||||
|
class="flex h-10 w-full items-center justify-between rounded-lg border bg-background px-4 text-sm outline-none ring-offset-background shadow-sm transition focus-within:border-primary/60 focus-within:ring-2 focus-within:ring-ring"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<template
|
||||||
|
v-for="(segment, index) in segments"
|
||||||
|
:key="`${segment.part}-${index}`"
|
||||||
|
>
|
||||||
|
<DatePickerInput
|
||||||
|
:part="segment.part"
|
||||||
|
:class="segment.part === 'literal' ? 'text-muted-foreground/70' : 'text-foreground'"
|
||||||
|
>
|
||||||
|
{{ segment.part === 'literal' ? '-' : segment.value }}
|
||||||
|
</DatePickerInput>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<DatePickerTrigger as-child>
|
||||||
|
<button type="button" class="inline-flex h-6 w-6 items-center justify-center text-muted-foreground">
|
||||||
|
<CalendarIcon class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</DatePickerTrigger>
|
||||||
|
</DatePickerField>
|
||||||
|
</DatePickerAnchor>
|
||||||
|
<DatePickerContent class="z-50 w-[22rem] rounded-lg border bg-card p-4 shadow-lg">
|
||||||
|
<DatePickerArrow class="fill-border" />
|
||||||
|
<DatePickerCalendar v-slot="{ weekDays, grid }" locale="zh-CN">
|
||||||
|
<DatePickerHeader class="mb-2 flex items-center justify-between">
|
||||||
|
<DatePickerPrev as-child>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition hover:bg-muted"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
</DatePickerPrev>
|
||||||
|
<DatePickerHeading class="text-base font-medium text-foreground" />
|
||||||
|
<DatePickerNext as-child>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition hover:bg-muted"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</DatePickerNext>
|
||||||
|
</DatePickerHeader>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<DatePickerGrid
|
||||||
|
v-for="month in grid"
|
||||||
|
:key="month.value.toString()"
|
||||||
|
class="w-full border-collapse select-none"
|
||||||
|
>
|
||||||
|
<DatePickerGridHead>
|
||||||
|
<DatePickerGridRow class="grid grid-cols-7">
|
||||||
|
<DatePickerHeadCell
|
||||||
|
v-for="day in weekDays"
|
||||||
|
:key="day"
|
||||||
|
class="flex h-9 items-center justify-center text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ day }}
|
||||||
|
</DatePickerHeadCell>
|
||||||
|
</DatePickerGridRow>
|
||||||
|
</DatePickerGridHead>
|
||||||
|
<DatePickerGridBody>
|
||||||
|
<DatePickerGridRow
|
||||||
|
v-for="(weekDates, index) in month.rows"
|
||||||
|
:key="`${month.value.toString()}-${index}`"
|
||||||
|
class="grid grid-cols-7"
|
||||||
|
>
|
||||||
|
<DatePickerCell
|
||||||
|
v-for="dateValue in weekDates"
|
||||||
|
:key="dateValue.toString()"
|
||||||
|
:date="dateValue"
|
||||||
|
class="h-10 w-full p-0.5"
|
||||||
|
>
|
||||||
|
<DatePickerCellTrigger
|
||||||
|
:day="dateValue"
|
||||||
|
:month="month.value"
|
||||||
|
class="h-full w-full rounded-md border border-transparent bg-transparent text-base outline-none transition hover:bg-muted data-[outside-view]:text-muted-foreground/40 data-[selected]:border-primary data-[selected]:bg-transparent data-[selected]:text-foreground data-[disabled]:opacity-40 data-[unavailable]:text-muted-foreground/40"
|
||||||
|
>
|
||||||
|
{{ dateValue.day }}
|
||||||
|
</DatePickerCellTrigger>
|
||||||
|
</DatePickerCell>
|
||||||
|
</DatePickerGridRow>
|
||||||
|
</DatePickerGridBody>
|
||||||
|
</DatePickerGrid>
|
||||||
|
</div>
|
||||||
|
</DatePickerCalendar>
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<DatePickerClose as-child>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-8 rounded-md border px-3 text-xs text-foreground transition hover:bg-muted"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</DatePickerClose>
|
||||||
|
</div>
|
||||||
|
</DatePickerContent>
|
||||||
|
</DatePickerRoot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
RowAutoHeightModule,
|
RowAutoHeightModule,
|
||||||
TextEditorModule,
|
TextEditorModule,
|
||||||
TooltipModule,
|
TooltipModule,
|
||||||
UndoRedoEditModule,
|
UndoRedoEditModule,RenderApiModule
|
||||||
|
|
||||||
} from 'ag-grid-community'
|
} from 'ag-grid-community'
|
||||||
import {
|
import {
|
||||||
@ -40,7 +40,7 @@ const AG_GRID_MODULES = [
|
|||||||
LargeTextEditorModule,
|
LargeTextEditorModule,
|
||||||
UndoRedoEditModule,
|
UndoRedoEditModule,
|
||||||
CellStyleModule,
|
CellStyleModule,
|
||||||
PinnedRowModule,
|
PinnedRowModule,RenderApiModule ,
|
||||||
TooltipModule,
|
TooltipModule,
|
||||||
TreeDataModule,
|
TreeDataModule,
|
||||||
AggregationModule,
|
AggregationModule,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,351 +0,0 @@
|
|||||||
1 <script setup lang="ts">
|
|
||||||
2 import { computed, onBeforeUnmount, ref, watch, type Component ,onMounted} from 'vue'
|
|
||||||
3 import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
4 import { Button } from '@/components/ui/button'
|
|
||||||
5 import {
|
|
||||||
6 DialogClose,
|
|
||||||
7 DialogContent,
|
|
||||||
8 DialogOverlay,
|
|
||||||
9 DialogPortal,
|
|
||||||
10 DialogRoot,
|
|
||||||
11 DialogTitle,
|
|
||||||
12 DialogTrigger,DialogDescription
|
|
||||||
13 } from 'reka-ui'
|
|
||||||
14 import { Icon } from '@iconify/vue'
|
|
||||||
15 import { useWindowSize } from '@vueuse/core'
|
|
||||||
16 import { animate, AnimatePresence, Motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion-v'
|
|
||||||
17 interface TypeLineCategory {
|
|
||||||
18 key: string
|
|
||||||
19 label: string
|
|
||||||
20 component: Component
|
|
||||||
21 }
|
|
||||||
22
|
|
||||||
23 const props = withDefaults(
|
|
||||||
24 defineProps<{
|
|
||||||
25 scene?: string
|
|
||||||
26 title?: string
|
|
||||||
27 subtitle?: string
|
|
||||||
28 copyText?: string
|
|
||||||
29 categories: TypeLineCategory[]
|
|
||||||
30 storageKey?: string
|
|
||||||
31 defaultCategory?: string
|
|
||||||
32 }>(),
|
|
||||||
33 {
|
|
||||||
34 scene: 'default',
|
|
||||||
35 title: '配置',
|
|
||||||
36 subtitle: '',
|
|
||||||
37 copyText: '',
|
|
||||||
38 storageKey: '',
|
|
||||||
39 defaultCategory: ''
|
|
||||||
40 }
|
|
||||||
41 )
|
|
||||||
42
|
|
||||||
43 const cacheKey = computed(() => props.storageKey || `type-line-active-cat-${props.scene}`)
|
|
||||||
44
|
|
||||||
45 const resolveInitialCategory = () => {
|
|
||||||
46 const defaultKey = props.defaultCategory || props.categories[0]?.key || ''
|
|
||||||
47 const savedKey = sessionStorage.getItem(cacheKey.value)
|
|
||||||
48 const validSavedKey = props.categories.some(item => item.key === savedKey)
|
|
||||||
49 return validSavedKey ? (savedKey as string) : defaultKey
|
|
||||||
50 }
|
|
||||||
51
|
|
||||||
52 const activeCategory = ref(resolveInitialCategory())
|
|
||||||
53
|
|
||||||
54 watch(
|
|
||||||
55 () => [props.categories, props.defaultCategory, cacheKey.value],
|
|
||||||
56 () => {
|
|
||||||
57 activeCategory.value = resolveInitialCategory()
|
|
||||||
58 },
|
|
||||||
59 { deep: true }
|
|
||||||
60 )
|
|
||||||
61
|
|
||||||
62 const switchCategory = (cat: string) => {
|
|
||||||
63 activeCategory.value = cat
|
|
||||||
64 sessionStorage.setItem(cacheKey.value, cat)
|
|
||||||
65 }
|
|
||||||
66
|
|
||||||
67 const activeComponent = computed(() => {
|
|
||||||
68 const selected = props.categories.find(item => item.key === activeCategory.value)
|
|
||||||
69 return selected?.component || props.categories[0]?.component || null
|
|
||||||
70 })
|
|
||||||
71
|
|
||||||
72 const copyBtnText = ref('复制')
|
|
||||||
73 const sheetOpen = ref(false)
|
|
||||||
74 let copyBtnTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
75
|
|
||||||
76 const handleCopySubtitle = async () => {
|
|
||||||
77 const text = (props.copyText || '').trim()
|
|
||||||
78 if (!text) return
|
|
||||||
79
|
|
||||||
80 try {
|
|
||||||
81 await navigator.clipboard.writeText(text)
|
|
||||||
82 copyBtnText.value = '已复制'
|
|
||||||
83 } catch (error) {
|
|
||||||
84 console.error('copy failed:', error)
|
|
||||||
85 copyBtnText.value = '复制失败'
|
|
||||||
86 }
|
|
||||||
87
|
|
||||||
88 if (copyBtnTimer) clearTimeout(copyBtnTimer)
|
|
||||||
89 copyBtnTimer = setTimeout(() => {
|
|
||||||
90 copyBtnText.value = '复制'
|
|
||||||
91 }, 1200)
|
|
||||||
92 }
|
|
||||||
93
|
|
||||||
94 onBeforeUnmount(() => {
|
|
||||||
95 if (copyBtnTimer) clearTimeout(copyBtnTimer)
|
|
||||||
96 if (!root) return
|
|
||||||
97 root.style.scale = ''
|
|
||||||
98 root.style.translate = ''
|
|
||||||
99 root.style.borderRadius = ''
|
|
||||||
100 })
|
|
||||||
101
|
|
||||||
102 //
|
|
||||||
103
|
|
||||||
104
|
|
||||||
105 const inertiaTransition = {
|
|
||||||
106 type: 'inertia' as const,
|
|
||||||
107 bounceStiffness: 300,
|
|
||||||
108 bounceDamping: 40,
|
|
||||||
109 timeConstant: 300,
|
|
||||||
110 }
|
|
||||||
111
|
|
||||||
112 const staticTransition = {
|
|
||||||
113 duration: 0.5,
|
|
||||||
114 ease: [0.32, 0.72, 0, 1] as const,
|
|
||||||
115 }
|
|
||||||
116
|
|
||||||
117 const SHEET_TOP_RATIO = 0.1
|
|
||||||
118 const SHEET_RADIUS = 12
|
|
||||||
119 const OFFICIAL_SITE_URL = '/'
|
|
||||||
120
|
|
||||||
121 let root: HTMLElement | null = null
|
|
||||||
122
|
|
||||||
123 onMounted(() => {
|
|
||||||
124 root = document.body.firstElementChild as HTMLElement | null
|
|
||||||
125 })
|
|
||||||
126
|
|
||||||
127 const { height, width } = useWindowSize()
|
|
||||||
128
|
|
||||||
129 const sheetTop = computed(() => Math.round(height.value * SHEET_TOP_RATIO))
|
|
||||||
130 const h = computed(() => Math.max(0, height.value - sheetTop.value))
|
|
||||||
131 const y = useMotionValue(h.value)
|
|
||||||
132
|
|
||||||
133 watch(
|
|
||||||
134 () => h.value,
|
|
||||||
135 (nextHeight) => {
|
|
||||||
136 if (!sheetOpen.value) y.jump(nextHeight)
|
|
||||||
137 }
|
|
||||||
138 )
|
|
||||||
139
|
|
||||||
140 watch(
|
|
||||||
141 () => sheetOpen.value,
|
|
||||||
142 (isOpen) => {
|
|
||||||
143 if (!isOpen) {
|
|
||||||
144 y.jump(h.value)
|
|
||||||
145 return
|
|
||||||
146 }
|
|
||||||
147 y.jump(h.value)
|
|
||||||
148 animate(y, 0, staticTransition)
|
|
||||||
149 }
|
|
||||||
150 )
|
|
||||||
151
|
|
||||||
152 // Scale the body down and adjust the border radius when the sheet is open.
|
|
||||||
153 const bodyScale = useTransform(
|
|
||||||
154 y,
|
|
||||||
155 [0, h.value],
|
|
||||||
156 [(width.value - sheetTop.value) / width.value, 1],
|
|
||||||
157 )
|
|
||||||
158 const bodyTranslate = useTransform(y, [0, h.value], [sheetTop.value - SHEET_RADIUS, 0])
|
|
||||||
159 const bodyBorderRadius = useTransform(y, [0, h.value], [SHEET_RADIUS, 0])
|
|
||||||
160
|
|
||||||
161 useMotionValueEvent(bodyScale, 'change', (v) => {
|
|
||||||
162 if (!root) return
|
|
||||||
163 root.style.scale = `${v}`
|
|
||||||
164 })
|
|
||||||
165 useMotionValueEvent(
|
|
||||||
166 bodyTranslate,
|
|
||||||
167 'change',
|
|
||||||
168 (v) => {
|
|
||||||
169 if (!root) return
|
|
||||||
170 root.style.translate = `0 ${v}px`
|
|
||||||
171 },
|
|
||||||
172 )
|
|
||||||
173 useMotionValueEvent(
|
|
||||||
174 bodyBorderRadius,
|
|
||||||
175 'change',
|
|
||||||
176 (v) => {
|
|
||||||
177 if (!root) return
|
|
||||||
178 root.style.borderRadius = `${v}px`
|
|
||||||
179 },
|
|
||||||
180 )
|
|
||||||
181 </script>
|
|
||||||
182
|
|
||||||
183 <template>
|
|
||||||
184 <div class="flex h-full w-full bg-background">
|
|
||||||
185 <div class="w-12/100 border-r p-6 flex flex-col gap-8 relative">
|
|
||||||
186 <div v-if="props.title || props.subtitle" class="space-y-1">
|
|
||||||
187 <div v-if="props.title" class="font-bold text-base leading-6 text-primary break-words">
|
|
||||||
188 {{ props.title }}
|
|
||||||
189 </div>
|
|
||||||
190 <div
|
|
||||||
191 v-if="props.subtitle"
|
|
||||||
192 class="flex flex-wrap items-center gap-2 text-xs leading-5 text-muted-foreground"
|
|
||||||
193 >
|
|
||||||
194 <span class="break-all">{{ props.subtitle }}</span>
|
|
||||||
195 <Button
|
|
||||||
196 v-if="props.copyText"
|
|
||||||
197 type="button"
|
|
||||||
198 variant="outline"
|
|
||||||
199 size="sm"
|
|
||||||
200 class="h-6 rounded-md px-2 text-[11px]"
|
|
||||||
201 @click.stop="handleCopySubtitle"
|
|
||||||
202 >
|
|
||||||
203 {{ copyBtnText }}
|
|
||||||
204 </Button>
|
|
||||||
205 </div>
|
|
||||||
206 </div>
|
|
||||||
207
|
|
||||||
208 <div class="flex flex-col gap-10 relative">
|
|
||||||
209 <div class="absolute left-[11px] top-2 bottom-2 w-[2px] bg-muted"></div>
|
|
||||||
210
|
|
||||||
211 <div
|
|
||||||
212 v-for="item in props.categories"
|
|
||||||
213 :key="item.key"
|
|
||||||
214 class="relative flex items-center gap-4 cursor-pointer group"
|
|
||||||
215 @click="switchCategory(item.key)"
|
|
||||||
216 >
|
|
||||||
217 <div
|
|
||||||
218 :class="[
|
|
||||||
219 'z-10 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all',
|
|
||||||
220 activeCategory === item.key
|
|
||||||
221 ? 'bg-primary border-primary scale-110'
|
|
||||||
222 : 'bg-background border-muted-foreground'
|
|
||||||
223 ]"
|
|
||||||
224 >
|
|
||||||
225 <div v-if="activeCategory === item.key" class="w-2 h-2 bg-background rounded-full"></div>
|
|
||||||
226 </div>
|
|
||||||
227 <span
|
|
||||||
228 :class="[
|
|
||||||
229 'text-sm transition-colors',
|
|
||||||
230 activeCategory === item.key
|
|
||||||
231 ? 'font-bold text-primary'
|
|
||||||
232 : 'text-muted-foreground group-hover:text-foreground'
|
|
||||||
233 ]"
|
|
||||||
234 >
|
|
||||||
235 {{ item.label }}
|
|
||||||
236 </span>
|
|
||||||
237 </div>
|
|
||||||
238 </div>
|
|
||||||
239
|
|
||||||
240 <DialogRoot v-model:open="sheetOpen">
|
|
||||||
241 <DialogTrigger as-child>
|
|
||||||
242 <button
|
|
||||||
243 type="button"
|
|
||||||
244 class="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 hover:bg-muted/55 hover:text-foreground transition-colors"
|
|
||||||
245 >
|
|
||||||
246 <img src="/favicon.ico" alt="众为咨询" class="h-5 w-5 shrink-0 rounded-sm" />
|
|
||||||
247 <span>本网站由众为工程咨询有限公司提供免费技术支持</span>
|
|
||||||
248 </button>
|
|
||||||
249 </DialogTrigger>
|
|
||||||
250 <DialogPortal>
|
|
||||||
251 <AnimatePresence
|
|
||||||
252 multiple
|
|
||||||
253 as="div"
|
|
||||||
254 >
|
|
||||||
255 <DialogOverlay as-child>
|
|
||||||
256 <Motion
|
|
||||||
257 class="fixed inset-0 z-10 bg-black/45 backdrop-blur-[2px]"
|
|
||||||
258 :initial="{ opacity: 0 }"
|
|
||||||
259 :animate="{ opacity: 1 }"
|
|
||||||
260 :exit="{ opacity: 0 }"
|
|
||||||
261 :transition="staticTransition"
|
|
||||||
262 />
|
|
||||||
263 </DialogOverlay>
|
|
||||||
264
|
|
||||||
265 <DialogContent as-child>
|
|
||||||
266 <Motion
|
|
||||||
267 class="fixed inset-x-0 bottom-0 z-20 overflow-hidden rounded-t-2xl border border-border/60 bg-card/95 shadow-2xl backdrop-blur-xl will-change-transform"
|
|
||||||
268 :style="{
|
|
||||||
269 y,
|
|
||||||
270 top: `${sheetTop}px`,
|
|
||||||
271 }"
|
|
||||||
272 drag="y"
|
|
||||||
273 :drag-constraints="{ top: 0 }"
|
|
||||||
274 @drag-end="(e, { offset, velocity }) => {
|
|
||||||
275 if (offset.y > h * 0.35 || velocity.y > 10) {
|
|
||||||
276 sheetOpen = false;
|
|
||||||
277 }
|
|
||||||
278 else {
|
|
||||||
279 animate(y, 0, { ...inertiaTransition, min: 0, max: 0 });
|
|
||||||
280 }
|
|
||||||
281 }"
|
|
||||||
282 >
|
|
||||||
283 <div class="mx-auto mt-2 h-1.5 w-12 rounded-full bg-muted-foreground/35" />
|
|
||||||
284 <div class="mx-auto flex h-full w-full max-w-2xl flex-col px-4 pb-5 pt-3">
|
|
||||||
285 <div class="mb-3 flex items-center justify-between gap-3">
|
|
||||||
286 <DialogTitle class="m-0">
|
|
||||||
287 <div class="flex items-center gap-3">
|
|
||||||
288 <img src="/favicon.ico" alt="众为咨询" class="h-7 w-7 shrink-0 rounded-sm" />
|
|
||||||
289 <span class="text-2xl font-semibold leading-none">关于我们</span>
|
|
||||||
290 </div>
|
|
||||||
291 </DialogTitle>
|
|
||||||
292 <div class="flex items-center gap-2">
|
|
||||||
293 <a
|
|
||||||
294 :href="OFFICIAL_SITE_URL"
|
|
||||||
295 target="_blank"
|
|
||||||
296 rel="noopener noreferrer"
|
|
||||||
297 class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground"
|
|
||||||
298 aria-label="跳转到官网首页"
|
|
||||||
299 title="官网首页"
|
|
||||||
300 >
|
|
||||||
301 <Icon icon="lucide:arrow-up-right" class="h-4 w-4" />
|
|
||||||
302 </a>
|
|
||||||
303 <DialogClose class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground">
|
|
||||||
304 <Icon icon="lucide:x" class="h-4 w-4" />
|
|
||||||
305 </DialogClose>
|
|
||||||
306 </div>
|
|
||||||
307 </div>
|
|
||||||
308
|
|
||||||
309 <DialogDescription class="mb-4 text-base text-muted-foreground">
|
|
||||||
310 <p class="font-medium text-foreground">众为工程咨询有限公司</p>
|
|
||||||
311 </DialogDescription>
|
|
||||||
312
|
|
||||||
313 <div class="space-y-4 overflow-y-auto pr-1 text-[15px] leading-7">
|
|
||||||
314 <p>
|
|
||||||
315 众为工程咨询有限公司长期专注于工程咨询与数字化服务,致力于为客户提供高效、可靠、可持续的解决方案。
|
|
||||||
316 </p>
|
|
||||||
317 <p>
|
|
||||||
318 我们围绕咨询管理、数据治理、系统建设与运维支持,持续提升项目交付质量,帮助客户降低沟通成本、提升协同效率。
|
|
||||||
319 </p>
|
|
||||||
320 <p>
|
|
||||||
321 本网站由众为工程咨询有限公司提供免费技术支持,如需商务合作或技术咨询,请与我们联系。
|
|
||||||
322 </p>
|
|
||||||
323 </div>
|
|
||||||
324 </div>
|
|
||||||
325 </Motion>
|
|
||||||
326 </DialogContent>
|
|
||||||
327 </AnimatePresence>
|
|
||||||
328 </DialogPortal>
|
|
||||||
329 </DialogRoot>
|
|
||||||
330
|
|
||||||
331 </div>
|
|
||||||
332
|
|
||||||
333 <div class="w-88/100 min-h-0 h-full flex flex-col">
|
|
||||||
334 <ScrollArea class="h-full w-full min-h-0 rightMain">
|
|
||||||
335 <div class="p-3 h-full min-h-0 flex flex-col">
|
|
||||||
336 <keep-alive>
|
|
||||||
337 <component :is="activeComponent" />
|
|
||||||
338 </keep-alive>
|
|
||||||
339 </div>
|
|
||||||
340 </ScrollArea>
|
|
||||||
341 </div>
|
|
||||||
342 </div>
|
|
||||||
343 </template>
|
|
||||||
344 <style scoped>
|
|
||||||
345 /* 核心修改:添加 :deep() 穿透 scoped 作用域 */
|
|
||||||
346 :deep(.rightMain > div > div) {
|
|
||||||
347 height: 100%;
|
|
||||||
348 min-height: 0;
|
|
||||||
349 box-sizing: border-box;
|
|
||||||
350 }
|
|
||||||
351 </style>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user