update at 2026-02-13 23:00:20
This commit is contained in:
@@ -174,6 +174,10 @@ const DEFAULT_THEME_INDEX = (() => {
|
||||
const index = themePresets.findIndex((item) => item && item.id === DEFAULT_THEME_ID);
|
||||
return index >= 0 ? index : 0;
|
||||
})();
|
||||
const THEME_ROW_HEIGHT_PX = 36;
|
||||
const THEME_VISIBLE_ROW_COUNT = 6;
|
||||
const THEME_LIST_HEIGHT_PX = THEME_ROW_HEIGHT_PX * THEME_VISIBLE_ROW_COUNT;
|
||||
const THEME_LIST_EDGE_SPACER_PX = (THEME_LIST_HEIGHT_PX - THEME_ROW_HEIGHT_PX) / 2;
|
||||
|
||||
/**
|
||||
* 数值限制,避免 UI 参数导致布局异常。
|
||||
@@ -186,6 +190,19 @@ function clampNumber(value, min, max, fallback) {
|
||||
return Math.min(max, Math.max(min, normalized));
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题弹层打开时的滚动定位:
|
||||
* 通过上下占位留白,让任意索引都可滚动到视图中心。
|
||||
*/
|
||||
function getThemeListScrollTopByIndex(index, totalCount) {
|
||||
const total = Number(totalCount);
|
||||
if (!Number.isFinite(total) || total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const safeIndex = clampNumber(index, 0, total - 1, 0);
|
||||
return safeIndex * THEME_ROW_HEIGHT_PX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一错误文案:
|
||||
* - xlsx 解析能力缺失时,固定提示用户去“构建 npm”
|
||||
@@ -286,17 +303,23 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
||||
|
||||
const sourceValueMap = {};
|
||||
const targetValueMap = {};
|
||||
const nodeColorIndexMap = {};
|
||||
let nodeColorIndexCursor = 0;
|
||||
links.forEach((link) => {
|
||||
sourceValueMap[link.source] = (sourceValueMap[link.source] || 0) + Number(link.value || 0);
|
||||
targetValueMap[link.target] = (targetValueMap[link.target] || 0) + Number(link.value || 0);
|
||||
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.source)) {
|
||||
nodeColorIndexMap[link.source] = nodeColorIndexCursor;
|
||||
nodeColorIndexCursor += 1;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.target)) {
|
||||
nodeColorIndexMap[link.target] = nodeColorIndexCursor;
|
||||
nodeColorIndexCursor += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const sourceNames = Object.keys(sourceValueMap);
|
||||
const targetNames = Object.keys(targetValueMap);
|
||||
const sourceIndexMap = {};
|
||||
sourceNames.forEach((name, index) => {
|
||||
sourceIndexMap[name] = index;
|
||||
});
|
||||
const totalValue = sourceNames.reduce((sum, name) => sum + sourceValueMap[name], 0);
|
||||
if (totalValue <= 0) {
|
||||
return null;
|
||||
@@ -371,7 +394,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
||||
sy,
|
||||
ty,
|
||||
linkHeight,
|
||||
sourceIndex: Number.isFinite(sourceIndexMap[link.source]) ? sourceIndexMap[link.source] : 0
|
||||
sourceIndex: Number.isFinite(nodeColorIndexMap[link.source]) ? nodeColorIndexMap[link.source] : 0
|
||||
});
|
||||
});
|
||||
|
||||
@@ -383,6 +406,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
||||
targetNames,
|
||||
sourcePos,
|
||||
targetPos,
|
||||
nodeColorIndexMap,
|
||||
linkSegments
|
||||
};
|
||||
}
|
||||
@@ -431,12 +455,15 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
|
||||
layout.sourceNames.forEach((name, index) => {
|
||||
const node = layout.sourcePos[name];
|
||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||
? layout.nodeColorIndexMap[name]
|
||||
: index;
|
||||
const textY = node.y + getNodeLabelCenterY(node.h);
|
||||
const textPlacement = getSvgLabelPlacement(layout, true, labelPositionMode);
|
||||
segments.push(
|
||||
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||
layout.nodeWidth
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index, themeColors)}" />`
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(colorIndex, themeColors)}" />`
|
||||
);
|
||||
segments.push(
|
||||
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
||||
@@ -447,12 +474,15 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
|
||||
layout.targetNames.forEach((name, index) => {
|
||||
const node = layout.targetPos[name];
|
||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||
? layout.nodeColorIndexMap[name]
|
||||
: index;
|
||||
const textY = node.y + getNodeLabelCenterY(node.h);
|
||||
const textPlacement = getSvgLabelPlacement(layout, false, labelPositionMode);
|
||||
segments.push(
|
||||
`<rect x="${formatSvgNumber(layout.rightX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||
layout.nodeWidth
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index, themeColors)}" />`
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(colorIndex, themeColors)}" />`
|
||||
);
|
||||
segments.push(
|
||||
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
||||
@@ -470,6 +500,10 @@ Page({
|
||||
selectedThemeIndex: DEFAULT_THEME_INDEX,
|
||||
themes: Array.isArray(themePresets) ? themePresets : [],
|
||||
showThemeSheet: false,
|
||||
themeListScrollTop: getThemeListScrollTopByIndex(
|
||||
DEFAULT_THEME_INDEX,
|
||||
Array.isArray(themePresets) ? themePresets.length : 0
|
||||
),
|
||||
uploadMessage: '默认加载 data/sankey.xlsx 中...',
|
||||
parseError: '',
|
||||
buildError: '',
|
||||
@@ -478,6 +512,9 @@ Page({
|
||||
sourceDataColumn: null,
|
||||
sourceDescriptionColumns: [],
|
||||
targetDescriptionColumns: [],
|
||||
sectionVisibleSourceData: true,
|
||||
sectionVisibleSourceDesc: true,
|
||||
sectionVisibleTargetDesc: true,
|
||||
nodesCount: 0,
|
||||
linksCount: 0,
|
||||
droppedRows: 0,
|
||||
@@ -567,7 +604,32 @@ Page({
|
||||
* 主题选择按钮点击后,切换底部选择器。
|
||||
*/
|
||||
onToggleThemeSheet() {
|
||||
this.setData({ showThemeSheet: !this.data.showThemeSheet });
|
||||
const nextVisible = !this.data.showThemeSheet;
|
||||
if (!nextVisible) {
|
||||
this.setData({ showThemeSheet: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const targetScrollTop = getThemeListScrollTopByIndex(
|
||||
this.data.selectedThemeIndex,
|
||||
(this.data.themes || []).length
|
||||
);
|
||||
this.setData(
|
||||
{
|
||||
showThemeSheet: true,
|
||||
themeListScrollTop: 0
|
||||
},
|
||||
() => {
|
||||
const applyScrollTop = () => {
|
||||
this.setData({ themeListScrollTop: targetScrollTop });
|
||||
};
|
||||
if (typeof wx.nextTick === 'function') {
|
||||
wx.nextTick(applyScrollTop);
|
||||
return;
|
||||
}
|
||||
setTimeout(applyScrollTop, 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -577,6 +639,24 @@ Page({
|
||||
this.setData({ showThemeSheet: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换数据映射区块展开/收起状态,对齐 Web 的 expand/zhedie 行为。
|
||||
*/
|
||||
onToggleSection(e) {
|
||||
const section = String((e.currentTarget.dataset && e.currentTarget.dataset.section) || '');
|
||||
if (section === 'sourceData') {
|
||||
this.setData({ sectionVisibleSourceData: !this.data.sectionVisibleSourceData });
|
||||
return;
|
||||
}
|
||||
if (section === 'sourceDesc') {
|
||||
this.setData({ sectionVisibleSourceDesc: !this.data.sectionVisibleSourceDesc });
|
||||
return;
|
||||
}
|
||||
if (section === 'targetDesc') {
|
||||
this.setData({ sectionVisibleTargetDesc: !this.data.sectionVisibleTargetDesc });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 返回当前选中主题色带。
|
||||
*/
|
||||
@@ -1063,7 +1143,10 @@ Page({
|
||||
layout.sourceNames.forEach((name, index) => {
|
||||
const node = layout.sourcePos[name];
|
||||
const labelPlacement = getCanvasLabelPlacement(layout, true, this.data.labelPositionMode);
|
||||
ctx.setFillStyle(getNodeColor(index, themeColors));
|
||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||
? layout.nodeColorIndexMap[name]
|
||||
: index;
|
||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
||||
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
|
||||
ctx.setFillStyle('#4e5969');
|
||||
ctx.setFontSize(10);
|
||||
@@ -1075,7 +1158,10 @@ Page({
|
||||
layout.targetNames.forEach((name, index) => {
|
||||
const node = layout.targetPos[name];
|
||||
const labelPlacement = getCanvasLabelPlacement(layout, false, this.data.labelPositionMode);
|
||||
ctx.setFillStyle(getNodeColor(index, themeColors));
|
||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||
? layout.nodeColorIndexMap[name]
|
||||
: index;
|
||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
||||
ctx.fillRect(layout.rightX, node.y, layout.nodeWidth, node.h);
|
||||
ctx.setFillStyle('#4e5969');
|
||||
ctx.setFontSize(10);
|
||||
|
||||
@@ -97,47 +97,68 @@
|
||||
<text wx:if="{{columnHeaders.length === 0}}" class="empty-tip">未加载文件,暂无列信息</text>
|
||||
|
||||
<view class="field-group">
|
||||
<view class="field-title">
|
||||
<image src="../../assets/icons/expand.svg" mode="aspectFit" />
|
||||
<view class="field-title" data-section="sourceData" bindtap="onToggleSection">
|
||||
<image
|
||||
src="{{sectionVisibleSourceData ? '../../assets/icons/zhedie.svg' : '../../assets/icons/expand.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>源数据(link value)</text>
|
||||
</view>
|
||||
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onSelectSourceData">
|
||||
<image src="../../assets/icons/data.svg" mode="aspectFit" />
|
||||
<text class="label">{{item}}</text>
|
||||
<image
|
||||
src="{{sourceDataColumn === index ? '../../assets/icons/radiobutton.svg' : '../../assets/icons/radiobutton-no.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="column-list" wx:if="{{sectionVisibleSourceData}}">
|
||||
<view class="column-list-line" />
|
||||
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onSelectSourceData">
|
||||
<view class="row-link" />
|
||||
<image src="../../assets/icons/data.svg" mode="aspectFit" />
|
||||
<text class="label">{{item}}</text>
|
||||
<image
|
||||
src="{{sourceDataColumn === index ? '../../assets/icons/radiobutton.svg' : '../../assets/icons/radiobutton-no.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-group">
|
||||
<view class="field-title">
|
||||
<image src="../../assets/icons/expand.svg" mode="aspectFit" />
|
||||
<view class="field-title" data-section="sourceDesc" bindtap="onToggleSection">
|
||||
<image
|
||||
src="{{sectionVisibleSourceDesc ? '../../assets/icons/zhedie.svg' : '../../assets/icons/expand.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>源标签(Source label)</text>
|
||||
</view>
|
||||
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onToggleSourceDesc">
|
||||
<image src="../../assets/icons/description.svg" mode="aspectFit" />
|
||||
<text class="label">{{item}}</text>
|
||||
<image
|
||||
src="{{sourceDescriptionColumns.indexOf(index) > -1 ? '../../assets/icons/checkbox.svg' : '../../assets/icons/checkbox-no.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="column-list" wx:if="{{sectionVisibleSourceDesc}}">
|
||||
<view class="column-list-line" />
|
||||
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onToggleSourceDesc">
|
||||
<view class="row-link" />
|
||||
<image src="../../assets/icons/description.svg" mode="aspectFit" />
|
||||
<text class="label">{{item}}</text>
|
||||
<image
|
||||
src="{{sourceDescriptionColumns.indexOf(index) > -1 ? '../../assets/icons/checkbox.svg' : '../../assets/icons/checkbox-no.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-group">
|
||||
<view class="field-title">
|
||||
<image src="../../assets/icons/expand.svg" mode="aspectFit" />
|
||||
<text>目标标签(target label)</text>
|
||||
</view>
|
||||
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onToggleTargetDesc">
|
||||
<image src="../../assets/icons/description.svg" mode="aspectFit" />
|
||||
<text class="label">{{item}}</text>
|
||||
<view class="field-title" data-section="targetDesc" bindtap="onToggleSection">
|
||||
<image
|
||||
src="{{targetDescriptionColumns.indexOf(index) > -1 ? '../../assets/icons/checkbox.svg' : '../../assets/icons/checkbox-no.svg'}}"
|
||||
src="{{sectionVisibleTargetDesc ? '../../assets/icons/zhedie.svg' : '../../assets/icons/expand.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>目标标签(target label)</text>
|
||||
</view>
|
||||
<view class="column-list" wx:if="{{sectionVisibleTargetDesc}}">
|
||||
<view class="column-list-line" />
|
||||
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onToggleTargetDesc">
|
||||
<view class="row-link" />
|
||||
<image src="../../assets/icons/description.svg" mode="aspectFit" />
|
||||
<text class="label">{{item}}</text>
|
||||
<image
|
||||
src="{{targetDescriptionColumns.indexOf(index) > -1 ? '../../assets/icons/checkbox.svg' : '../../assets/icons/checkbox-no.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -156,8 +177,17 @@
|
||||
<view class="theme-sheet-mask" wx:if="{{showThemeSheet}}" bindtap="onCloseThemeSheet" />
|
||||
<view class="theme-sheet" wx:if="{{showThemeSheet}}">
|
||||
<text class="theme-title">选择配色主题</text>
|
||||
<scroll-view class="theme-list" scroll-y enhanced show-scrollbar="true">
|
||||
<scroll-view
|
||||
class="theme-list"
|
||||
scroll-y
|
||||
enhanced
|
||||
show-scrollbar="true"
|
||||
scroll-top="{{themeListScrollTop}}"
|
||||
scroll-with-animation
|
||||
>
|
||||
<view class="theme-list-spacer" />
|
||||
<view
|
||||
id="theme-row-{{themeIndex}}"
|
||||
class="theme-row"
|
||||
wx:for="{{themes}}"
|
||||
wx:key="id"
|
||||
@@ -180,6 +210,7 @@
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="theme-list-spacer" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -315,6 +315,7 @@
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.field-title image {
|
||||
@@ -322,16 +323,42 @@
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.column-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.column-list-line {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 14px;
|
||||
width: 1px;
|
||||
background: #c9cdd4;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 24px;
|
||||
border-bottom: 1px solid #c9cdd4;
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.row-link {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 9px;
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
background: #c9cdd4;
|
||||
}
|
||||
|
||||
.row image {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
@@ -385,7 +412,7 @@
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 12px 12px 16px;
|
||||
padding: 12px 32px 32px;
|
||||
z-index: 11;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -402,6 +429,10 @@
|
||||
min-height: 216px;
|
||||
}
|
||||
|
||||
.theme-list-spacer {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.theme-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user