|
|
@@ -103,6 +103,50 @@ function loadPathDockHeight(): number {
|
|
|
const pathDockHeight = ref(loadPathDockHeight())
|
|
|
const pathDockResizing = ref(false)
|
|
|
|
|
|
+const DEMAND_LIST_WIDTH_KEY = 'icicle-demand-list-width'
|
|
|
+const DEMAND_LIST_MIN = 300
|
|
|
+const DEMAND_LIST_DEFAULT = 390
|
|
|
+
|
|
|
+function loadDemandListWidth(): number {
|
|
|
+ const raw = localStorage.getItem(DEMAND_LIST_WIDTH_KEY)
|
|
|
+ const n = raw ? Number(raw) : NaN
|
|
|
+ return Number.isFinite(n) && n >= DEMAND_LIST_MIN ? n : DEMAND_LIST_DEFAULT
|
|
|
+}
|
|
|
+
|
|
|
+const demandListWidth = ref(loadDemandListWidth())
|
|
|
+const demandListResizing = ref(false)
|
|
|
+
|
|
|
+function demandListMaxWidth(): number {
|
|
|
+ return Math.max(DEMAND_LIST_MIN, Math.floor(window.innerWidth * 0.72))
|
|
|
+}
|
|
|
+
|
|
|
+function onDemandListResizeStart(event: PointerEvent) {
|
|
|
+ event.preventDefault()
|
|
|
+ demandListResizing.value = true
|
|
|
+ const startX = event.clientX
|
|
|
+ const startWidth = demandListWidth.value
|
|
|
+
|
|
|
+ const onMove = (e: PointerEvent) => {
|
|
|
+ const delta = startX - e.clientX
|
|
|
+ demandListWidth.value = Math.max(
|
|
|
+ DEMAND_LIST_MIN,
|
|
|
+ Math.min(demandListMaxWidth(), startWidth + delta),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const onUp = () => {
|
|
|
+ demandListResizing.value = false
|
|
|
+ localStorage.setItem(DEMAND_LIST_WIDTH_KEY, String(demandListWidth.value))
|
|
|
+ window.removeEventListener('pointermove', onMove)
|
|
|
+ window.removeEventListener('pointerup', onUp)
|
|
|
+ window.removeEventListener('pointercancel', onUp)
|
|
|
+ }
|
|
|
+
|
|
|
+ window.addEventListener('pointermove', onMove)
|
|
|
+ window.addEventListener('pointerup', onUp)
|
|
|
+ window.addEventListener('pointercancel', onUp)
|
|
|
+}
|
|
|
+
|
|
|
function pathDockMaxHeight(): number {
|
|
|
return Math.floor(window.innerHeight * PATH_DOCK_MAX_RATIO)
|
|
|
}
|
|
|
@@ -858,9 +902,14 @@ onUnmounted(() => {
|
|
|
<p class="subtitle">完整分类树 · 横向为层级,纵向为分支规模</p>
|
|
|
</header>
|
|
|
|
|
|
- <div class="content-grid">
|
|
|
- <section class="map-panel">
|
|
|
- <div class="dim-bar">
|
|
|
+ <div
|
|
|
+ class="content-grid"
|
|
|
+ :class="{ 'demand-list-resizing': demandListResizing }"
|
|
|
+ :style="{ gridTemplateColumns: `minmax(0, 1fr) ${demandListWidth}px` }"
|
|
|
+ >
|
|
|
+ <div class="left-panel">
|
|
|
+ <section class="map-panel">
|
|
|
+ <div class="dim-bar">
|
|
|
<div class="tabs" role="tablist" aria-label="热度维度">
|
|
|
<button
|
|
|
v-for="tab in treeTabs"
|
|
|
@@ -977,61 +1026,72 @@ onUnmounted(() => {
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <aside class="demand-list-aside">
|
|
|
- <DemandGradeListPanel
|
|
|
- v-if="demandCards.length"
|
|
|
- :demand-cards="demandCards"
|
|
|
- :selected-demand-id="listSelectedDemandId"
|
|
|
- :selected-category-id="listSelectedCategoryId"
|
|
|
- @select-demand="onSelectDemandFromList"
|
|
|
- @select-demand-category="onSelectDemandCategoryFromList"
|
|
|
- />
|
|
|
- <div v-else class="demand-empty">暂无需求数据</div>
|
|
|
- </aside>
|
|
|
- </div>
|
|
|
+ </section>
|
|
|
|
|
|
- <div
|
|
|
- v-if="inspectOpen && inspectNode"
|
|
|
- class="path-dock"
|
|
|
- :class="{ resizing: pathDockResizing }"
|
|
|
- :style="{ height: `${pathDockHeight}px` }"
|
|
|
- >
|
|
|
- <div
|
|
|
- class="path-dock-resize"
|
|
|
- role="separator"
|
|
|
- aria-orientation="horizontal"
|
|
|
- aria-label="调整详情区域高度"
|
|
|
- title="上下拖拽调整高度"
|
|
|
- @pointerdown="onPathDockResizeStart"
|
|
|
- />
|
|
|
- <div class="path-dock-head">
|
|
|
- <div class="path-dock-meta">
|
|
|
- <span class="path-dock-label">节点详情</span>
|
|
|
- <span v-if="inspectDemandName" class="path-dock-demand">{{ inspectDemandName }}</span>
|
|
|
- <span class="path-dock-name">{{ inspectNode.name }}</span>
|
|
|
- </div>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="path-dock-close"
|
|
|
- aria-label="关闭节点详情"
|
|
|
- @click="closeInspect"
|
|
|
+ <div
|
|
|
+ v-if="inspectOpen && inspectNode"
|
|
|
+ class="path-dock"
|
|
|
+ :class="{ resizing: pathDockResizing }"
|
|
|
+ :style="{ height: `${pathDockHeight}px` }"
|
|
|
>
|
|
|
- 关闭
|
|
|
- </button>
|
|
|
+ <div
|
|
|
+ class="path-dock-resize"
|
|
|
+ role="separator"
|
|
|
+ aria-orientation="horizontal"
|
|
|
+ aria-label="调整详情区域高度"
|
|
|
+ title="上下拖拽调整高度"
|
|
|
+ @pointerdown="onPathDockResizeStart"
|
|
|
+ />
|
|
|
+ <div class="path-dock-head">
|
|
|
+ <div class="path-dock-meta">
|
|
|
+ <span class="path-dock-label">节点详情</span>
|
|
|
+ <span v-if="inspectDemandName" class="path-dock-demand">{{ inspectDemandName }}</span>
|
|
|
+ <span class="path-dock-name">{{ inspectNode.name }}</span>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="path-dock-close"
|
|
|
+ aria-label="关闭节点详情"
|
|
|
+ @click="closeInspect"
|
|
|
+ >
|
|
|
+ 关闭
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="path-dock-body">
|
|
|
+ <DemandPathPanel
|
|
|
+ :open="true"
|
|
|
+ :category-name="inspectNode.name"
|
|
|
+ :node-name="inspectNode.name"
|
|
|
+ :node-path="inspectNode.path.join(' / ')"
|
|
|
+ :items="inspectItems"
|
|
|
+ :highlight-demand-id="inspectHighlightDemandId"
|
|
|
+ hide-inline-close
|
|
|
+ @close="closeInspect"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="path-dock-body">
|
|
|
- <DemandPathPanel
|
|
|
- :open="true"
|
|
|
- :category-name="inspectNode.name"
|
|
|
- :node-name="inspectNode.name"
|
|
|
- :node-path="inspectNode.path.join(' / ')"
|
|
|
- :items="inspectItems"
|
|
|
- :highlight-demand-id="inspectHighlightDemandId"
|
|
|
- hide-inline-close
|
|
|
- @close="closeInspect"
|
|
|
+
|
|
|
+ <div class="demand-list-shell" :class="{ resizing: demandListResizing }">
|
|
|
+ <div
|
|
|
+ class="demand-list-resize"
|
|
|
+ role="separator"
|
|
|
+ aria-orientation="vertical"
|
|
|
+ aria-label="调整全部需求区域宽度"
|
|
|
+ title="左右拖拽调整宽度"
|
|
|
+ @pointerdown="onDemandListResizeStart"
|
|
|
/>
|
|
|
+ <aside class="demand-list-aside">
|
|
|
+ <DemandGradeListPanel
|
|
|
+ v-if="demandCards.length"
|
|
|
+ :demand-cards="demandCards"
|
|
|
+ :selected-demand-id="listSelectedDemandId"
|
|
|
+ :selected-category-id="listSelectedCategoryId"
|
|
|
+ @select-demand="onSelectDemandFromList"
|
|
|
+ @select-demand-category="onSelectDemandCategoryFromList"
|
|
|
+ />
|
|
|
+ <div v-else class="demand-empty">暂无需求数据</div>
|
|
|
+ </aside>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1092,20 +1152,75 @@ onUnmounted(() => {
|
|
|
|
|
|
.content-grid {
|
|
|
display: grid;
|
|
|
- grid-template-columns: minmax(520px, 1fr) 360px;
|
|
|
- gap: 12px;
|
|
|
+ gap: 0;
|
|
|
flex: 1;
|
|
|
min-height: 0;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
+.content-grid.demand-list-resizing {
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.left-panel {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ min-height: 0;
|
|
|
+ min-width: 0;
|
|
|
+ padding-right: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.demand-list-shell {
|
|
|
+ display: flex;
|
|
|
+ min-height: 0;
|
|
|
+ min-width: 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.demand-list-resize {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 10px;
|
|
|
+ cursor: ew-resize;
|
|
|
+ touch-action: none;
|
|
|
+ position: relative;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.demand-list-resize::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 4px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #cbd5e1;
|
|
|
+ transition: background 0.15s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.demand-list-resize:hover::before,
|
|
|
+.demand-list-shell.resizing .demand-list-resize::before {
|
|
|
+ background: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
.demand-list-aside {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
min-height: 0;
|
|
|
height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.demand-list-aside :deep(.demand-list) {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
}
|
|
|
|
|
|
.demand-empty {
|
|
|
- height: 100%;
|
|
|
+ flex: 1;
|
|
|
min-height: 0;
|
|
|
border: 1px solid #e2e8f0;
|
|
|
border-radius: 10px;
|
|
|
@@ -1119,7 +1234,20 @@ onUnmounted(() => {
|
|
|
|
|
|
@media (max-width: 980px) {
|
|
|
.content-grid {
|
|
|
- grid-template-columns: 1fr;
|
|
|
+ grid-template-columns: 1fr !important;
|
|
|
+ grid-template-rows: minmax(420px, 55vh) minmax(0, 1fr);
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel {
|
|
|
+ padding-right: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .demand-list-resize {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .demand-list-shell {
|
|
|
+ min-height: 360px;
|
|
|
}
|
|
|
}
|
|
|
|