| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>创作思维路径可视化</title>
- <script src="https://d3js.org/d3.v7.min.js"></script>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: #1a1a2e;
- color: #eee;
- overflow: hidden;
- }
- .container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- }
- header {
- padding: 12px 20px;
- background: #16213e;
- border-bottom: 1px solid #0f3460;
- display: flex;
- align-items: center;
- gap: 20px;
- }
- h1 {
- font-size: 16px;
- font-weight: 500;
- color: #e94560;
- }
- .controls {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 1;
- }
- .slider-container {
- display: flex;
- align-items: center;
- gap: 8px;
- background: #0f3460;
- padding: 6px 12px;
- border-radius: 6px;
- }
- .slider-container label {
- font-size: 13px;
- color: #aaa;
- }
- #stepSlider {
- width: 200px;
- cursor: pointer;
- }
- #stepDisplay {
- font-size: 14px;
- font-weight: 600;
- color: #e94560;
- min-width: 60px;
- }
- .play-btn {
- background: #e94560;
- border: none;
- color: white;
- padding: 6px 14px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- }
- .play-btn:hover {
- background: #ff6b6b;
- }
- .legend {
- display: flex;
- gap: 16px;
- font-size: 12px;
- color: #888;
- margin-left: auto;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 4px;
- }
- .legend-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- }
- .legend-line {
- width: 20px;
- height: 2px;
- }
- main {
- flex: 1;
- position: relative;
- }
- svg {
- width: 100%;
- height: 100%;
- }
- .node {
- cursor: pointer;
- }
- .node circle {
- stroke-width: 2px;
- transition: all 0.3s;
- }
- .node.unknown circle {
- fill: #2a2a4a !important;
- stroke: #444 !important;
- opacity: 0.4;
- }
- .node.known circle {
- filter: drop-shadow(0 0 6px currentColor);
- }
- .node text {
- font-size: 11px;
- fill: #ccc;
- text-anchor: middle;
- pointer-events: none;
- }
- .node.unknown text {
- fill: #555;
- }
- .node .order-badge {
- font-size: 10px;
- font-weight: bold;
- fill: white;
- }
- .link {
- stroke-opacity: 0.6;
- transition: all 0.3s;
- }
- .link.hidden {
- stroke-opacity: 0 !important;
- }
- .link-label {
- font-size: 9px;
- fill: #888;
- pointer-events: none;
- }
- .link-label.hidden {
- opacity: 0;
- }
- /* 维度颜色 */
- .dim-灵感点 { --color: #ff6b6b; }
- .dim-目的点 { --color: #4ecdc4; }
- .dim-关键点 { --color: #ffe66d; }
- /* 信息面板 */
- .info-panel {
- position: absolute;
- top: 20px;
- right: 20px;
- width: 280px;
- background: #16213e;
- border: 1px solid #0f3460;
- border-radius: 8px;
- padding: 16px;
- font-size: 13px;
- max-height: calc(100vh - 120px);
- overflow-y: auto;
- }
- .info-panel h3 {
- color: #e94560;
- margin-bottom: 12px;
- font-size: 14px;
- }
- .info-item {
- margin-bottom: 8px;
- line-height: 1.5;
- }
- .info-label {
- color: #888;
- }
- .info-value {
- color: #eee;
- }
- .info-reason {
- background: #0f3460;
- padding: 8px;
- border-radius: 4px;
- margin-top: 8px;
- font-size: 12px;
- line-height: 1.6;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <header>
- <h1>创作思维路径</h1>
- <div class="controls">
- <div class="slider-container">
- <label>步骤:</label>
- <input type="range" id="stepSlider" min="0" max="9" value="9">
- <span id="stepDisplay">9/9</span>
- </div>
- <button class="play-btn" id="playBtn">▶ 播放</button>
- </div>
- <div class="legend">
- <div class="legend-item">
- <span class="legend-dot" style="background: #ff6b6b;"></span>
- <span>灵感点</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot" style="background: #4ecdc4;"></span>
- <span>目的点</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot" style="background: #ffe66d;"></span>
- <span>关键点</span>
- </div>
- <div class="legend-item">
- <span class="legend-line" style="background: #e94560;"></span>
- <span>AI推导</span>
- </div>
- </div>
- </header>
- <main>
- <svg id="graph"></svg>
- <div class="info-panel" id="infoPanel" style="display: none;">
- <h3 id="infoTitle">节点详情</h3>
- <div id="infoContent"></div>
- </div>
- </main>
- </div>
- <script>
- // 创作模式数据(内联)
- const DATA = __CREATION_PATTERN_DATA__;
- // 维度颜色映射
- const dimColors = {
- '灵感点': '#ff6b6b',
- '目的点': '#4ecdc4',
- '关键点': '#ffe66d'
- };
- // 从数据中提取最终状态
- const steps = DATA['步骤列表'];
- const lastStep = steps[steps.length - 1];
- const nodes = lastStep['输出']['节点列表'];
- const edges = lastStep['输出']['边列表'].filter(e => e['关系类型'] === 'AI推导');
- const maxOrder = Math.max(...nodes.map(n => n['发现编号'] || 0));
- // 构建节点和边数据
- const nodeById = {};
- const graphNodes = nodes.map(n => {
- const node = {
- id: n['节点ID'],
- name: n['节点名称'],
- dimension: n['节点维度'],
- category: n['节点分类'],
- description: n['节点描述'],
- order: n['发现编号'],
- isKnown: n['是否已知']
- };
- nodeById[node.id] = node;
- return node;
- });
- const graphLinks = edges.map(e => ({
- source: e['来源'],
- target: e['目标'],
- type: e['关系类型'],
- score: e['可能性分数'],
- reason: e['推理说明']
- }));
- // 当前步骤
- let currentStep = maxOrder;
- const slider = document.getElementById('stepSlider');
- const stepDisplay = document.getElementById('stepDisplay');
- const playBtn = document.getElementById('playBtn');
- const infoPanel = document.getElementById('infoPanel');
- const infoTitle = document.getElementById('infoTitle');
- const infoContent = document.getElementById('infoContent');
- slider.max = maxOrder;
- slider.value = maxOrder;
- // 创建 SVG
- const svg = d3.select('#graph');
- const width = window.innerWidth;
- const height = window.innerHeight - 60;
- // 缩放
- const g = svg.append('g');
- const zoom = d3.zoom()
- .scaleExtent([0.2, 3])
- .on('zoom', (event) => g.attr('transform', event.transform));
- svg.call(zoom);
- // 箭头标记
- svg.append('defs').append('marker')
- .attr('id', 'arrow')
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 20)
- .attr('refY', 0)
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5')
- .attr('fill', '#e94560');
- // 力导向图
- const simulation = d3.forceSimulation(graphNodes)
- .force('link', d3.forceLink(graphLinks).id(d => d.id).distance(120))
- .force('charge', d3.forceManyBody().strength(-400))
- .force('center', d3.forceCenter(width / 2, height / 2))
- .force('collision', d3.forceCollide().radius(50));
- // 绘制边
- const link = g.append('g')
- .selectAll('line')
- .data(graphLinks)
- .join('line')
- .attr('class', 'link')
- .attr('stroke', '#e94560')
- .attr('stroke-width', 2)
- .attr('marker-end', 'url(#arrow)');
- // 边标签
- const linkLabel = g.append('g')
- .selectAll('text')
- .data(graphLinks)
- .join('text')
- .attr('class', 'link-label')
- .text(d => d.score?.toFixed(2) || '');
- // 绘制节点
- const node = g.append('g')
- .selectAll('g')
- .data(graphNodes)
- .join('g')
- .attr('class', d => `node dim-${d.dimension}`)
- .call(d3.drag()
- .on('start', dragstarted)
- .on('drag', dragged)
- .on('end', dragended))
- .on('click', (event, d) => showNodeInfo(d))
- .on('mouseenter', (event, d) => {
- // 高亮相关边
- link.classed('highlighted', l => l.source.id === d.id || l.target.id === d.id);
- })
- .on('mouseleave', () => {
- link.classed('highlighted', false);
- });
- // 节点圆形
- node.append('circle')
- .attr('r', 18)
- .attr('fill', d => dimColors[d.dimension] || '#888')
- .attr('stroke', d => dimColors[d.dimension] || '#888');
- // 序号徽章
- node.append('text')
- .attr('class', 'order-badge')
- .attr('dy', 4)
- .text(d => d.order || '');
- // 节点名称
- node.append('text')
- .attr('dy', 35)
- .text(d => d.name.length > 6 ? d.name.slice(0, 6) + '…' : d.name);
- // Tick 更新
- simulation.on('tick', () => {
- link
- .attr('x1', d => d.source.x)
- .attr('y1', d => d.source.y)
- .attr('x2', d => d.target.x)
- .attr('y2', d => d.target.y);
- linkLabel
- .attr('x', d => (d.source.x + d.target.x) / 2)
- .attr('y', d => (d.source.y + d.target.y) / 2 - 8);
- node.attr('transform', d => `translate(${d.x},${d.y})`);
- });
- // 拖拽函数
- function dragstarted(event, d) {
- if (!event.active) simulation.alphaTarget(0.3).restart();
- d.fx = d.x;
- d.fy = d.y;
- }
- function dragged(event, d) {
- d.fx = event.x;
- d.fy = event.y;
- }
- function dragended(event, d) {
- if (!event.active) simulation.alphaTarget(0);
- d.fx = null;
- d.fy = null;
- }
- // 更新显示状态
- function updateDisplay(step) {
- currentStep = step;
- stepDisplay.textContent = `${step}/${maxOrder}`;
- // 更新节点状态
- node.classed('known', d => d.order && d.order <= step);
- node.classed('unknown', d => !d.order || d.order > step);
- // 更新边状态(源和目标都已知才显示)
- link.classed('hidden', d => {
- const srcKnown = nodeById[d.source.id || d.source]?.order <= step;
- const tgtKnown = nodeById[d.target.id || d.target]?.order <= step;
- return !srcKnown || !tgtKnown;
- });
- linkLabel.classed('hidden', d => {
- const srcKnown = nodeById[d.source.id || d.source]?.order <= step;
- const tgtKnown = nodeById[d.target.id || d.target]?.order <= step;
- return !srcKnown || !tgtKnown;
- });
- }
- // 滑块事件
- slider.addEventListener('input', (e) => {
- updateDisplay(parseInt(e.target.value));
- });
- // 播放功能
- let playing = false;
- let playInterval = null;
- playBtn.addEventListener('click', () => {
- if (playing) {
- clearInterval(playInterval);
- playBtn.textContent = '▶ 播放';
- playing = false;
- } else {
- if (currentStep >= maxOrder) {
- currentStep = 0;
- slider.value = 0;
- updateDisplay(0);
- }
- playing = true;
- playBtn.textContent = '⏸ 暂停';
- playInterval = setInterval(() => {
- currentStep++;
- slider.value = currentStep;
- updateDisplay(currentStep);
- if (currentStep >= maxOrder) {
- clearInterval(playInterval);
- playBtn.textContent = '▶ 播放';
- playing = false;
- }
- }, 800);
- }
- });
- // 显示节点信息
- function showNodeInfo(d) {
- infoPanel.style.display = 'block';
- infoTitle.textContent = d.name;
- // 找到指向这个节点的边
- const inEdge = graphLinks.find(e => (e.target.id || e.target) === d.id);
- let html = `
- <div class="info-item">
- <span class="info-label">维度:</span>
- <span class="info-value">${d.dimension}</span>
- </div>
- <div class="info-item">
- <span class="info-label">分类:</span>
- <span class="info-value">${d.category || '-'}</span>
- </div>
- <div class="info-item">
- <span class="info-label">发现顺序:</span>
- <span class="info-value">${d.order || '未知'}</span>
- </div>
- <div class="info-item">
- <span class="info-label">描述:</span>
- <div class="info-reason">${d.description || '-'}</div>
- </div>
- `;
- if (inEdge) {
- const srcNode = nodeById[inEdge.source.id || inEdge.source];
- html += `
- <div class="info-item" style="margin-top: 12px;">
- <span class="info-label">推导来源:</span>
- <span class="info-value">${srcNode?.name || '-'}</span>
- </div>
- <div class="info-item">
- <span class="info-label">可能性:</span>
- <span class="info-value">${(inEdge.score * 100).toFixed(0)}%</span>
- </div>
- <div class="info-item">
- <span class="info-label">推理说明:</span>
- <div class="info-reason">${inEdge.reason || '-'}</div>
- </div>
- `;
- }
- infoContent.innerHTML = html;
- }
- // 点击空白关闭信息面板
- svg.on('click', (event) => {
- if (event.target === svg.node()) {
- infoPanel.style.display = 'none';
- }
- });
- // 初始显示
- updateDisplay(maxOrder);
- // 居中显示
- setTimeout(() => {
- const bounds = g.node().getBBox();
- const scale = Math.min(
- (width - 100) / bounds.width,
- (height - 100) / bounds.height,
- 1.2
- );
- const tx = (width - bounds.width * scale) / 2 - bounds.x * scale;
- const ty = (height - bounds.height * scale) / 2 - bounds.y * scale;
- svg.transition().duration(500).call(
- zoom.transform,
- d3.zoomIdentity.translate(tx, ty).scale(scale)
- );
- }, 1000);
- </script>
- </body>
- </html>
|