@templmf/temp-solf-lmf 0.0.133 → 0.0.134
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HomeView.vue +481 -0
- package/package.json +1 -1
package/HomeView.vue
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dot-grid" style="position: relative;">
|
|
3
|
+
<div class="glow-orb" style="width: 400px; height: 400px; background: rgba(26,111,196,0.2); top: 0; left: 25%;" />
|
|
4
|
+
<div class="glow-orb" style="width: 260px; height: 260px; background: rgba(96,165,250,0.1); top: 140px; right: 25%;" />
|
|
5
|
+
|
|
6
|
+
<!-- Hero -->
|
|
7
|
+
<section style="position: relative; max-width: 860px; margin: 0 auto; padding: 80px 24px 96px; display: flex; flex-direction: column; align-items: center; text-align: center;">
|
|
8
|
+
<div class="animate-fade-up" style="display: inline-flex; align-items: center; gap: 8px; padding: 6px 16px; border-radius: 999px; border: 1px solid var(--tag-border); background: var(--tag-bg); margin-bottom: 28px;">
|
|
9
|
+
<span class="animate-pulse-slow" style="width: 7px; height: 7px; border-radius: 50%; background: #4ade80; display: inline-block;" />
|
|
10
|
+
<span class="font-mono" style="font-size: 12px; color: var(--brand-400);">v1.0 现已上线 · 对话即服务</span>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<h1 class="font-display animate-fade-up stagger-1" style="font-weight: 800; line-height: 1.1; margin-bottom: 16px; font-size: clamp(2.4rem, 5.5vw, 3.8rem);">
|
|
14
|
+
<span class="gradient-text">Citic Frontend</span><br />
|
|
15
|
+
</h1>
|
|
16
|
+
|
|
17
|
+
<p class="animate-fade-up stagger-2" style="color: var(--text-secondary); margin-bottom: 36px; max-width: 480px; font-size: 1rem; line-height: 1.7; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;">
|
|
18
|
+
前端开发全流程智能伙伴
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<div class="glass animate-fade-up stagger-3" style="width: 100%; max-width: 600px; border-radius: 16px; padding: 18px 18px 14px; box-shadow: 0 0 60px rgba(26,111,196,0.08), 0 20px 40px rgba(0,0,0,0.12);">
|
|
22
|
+
<div style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px;">
|
|
23
|
+
<button
|
|
24
|
+
v-for="s in QUICK"
|
|
25
|
+
:key="s"
|
|
26
|
+
class="quick-btn"
|
|
27
|
+
style="font-size: 12px; padding: 5px 10px; border-radius: 999px; border: 1px solid var(--border-subtle); background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; transition: all 0.15s;"
|
|
28
|
+
@click="handleSend(s)"
|
|
29
|
+
>{{ s }}</button>
|
|
30
|
+
</div>
|
|
31
|
+
<InputBar
|
|
32
|
+
:is-loading="false"
|
|
33
|
+
placeholder="描述您的需求,回车即进入 AI 对话..."
|
|
34
|
+
:available-models="props.chatStore.availableModels"
|
|
35
|
+
:selected-model-id="props.chatStore.selectedModel.value?.id || ''"
|
|
36
|
+
:show-model-selector="true"
|
|
37
|
+
:has-image-attachment="homeHasImage"
|
|
38
|
+
@send="handleSend"
|
|
39
|
+
@select-model="props.chatStore.selectModel"
|
|
40
|
+
@attachment-change="onHomeAttachmentChange"
|
|
41
|
+
/>
|
|
42
|
+
<p class="font-mono" style="font-size: 11px; color: var(--text-muted); text-align: center; margin-top: 10px;">
|
|
43
|
+
发送后自动跳转到 AI 对话页面
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<!-- Capability Graph -->
|
|
49
|
+
<section style="max-width: 960px; margin: 0 auto; padding: 0 24px 96px;">
|
|
50
|
+
<h2 class="section-title" style="text-align: center; margin-bottom: 10px;">能力图谱</h2>
|
|
51
|
+
<p style="color: var(--text-secondary); text-align: center; margin-bottom: 32px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;">
|
|
52
|
+
拖拽节点探索 · 点击查看详情
|
|
53
|
+
</p>
|
|
54
|
+
<div class="glass" style="border-radius: 16px; overflow: hidden; position: relative;">
|
|
55
|
+
<div ref="graphRef" style="width: 100%; height: 480px; position: relative;">
|
|
56
|
+
<canvas ref="bgCanvas" style="position:absolute;inset:0;pointer-events:none;border-radius:16px;z-index: 1;"></canvas>
|
|
57
|
+
<svg ref="graphSvg" style="position:absolute;inset:0;width:100%;height:100%;"></svg>
|
|
58
|
+
<div ref="graphTip" style="position:absolute;pointer-events:none;padding:5px 11px;background:var(--bg-secondary);border:1px solid var(--tag-border);border-radius:8px;font-size:12px;color:var(--text-secondary);white-space:nowrap;opacity:0;transition:opacity .15s;z-index:10;">
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div style="padding: 14px 20px; border-top: 1px solid var(--border-subtle); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;">
|
|
62
|
+
<div style="display:flex;gap:16px;flex-wrap:wrap;">
|
|
63
|
+
<span v-for="leg in GRAPH_LEGEND" :key="leg.label" style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--text-muted);">
|
|
64
|
+
<span :style="`width:8px;height:8px;border-radius:50%;background:${leg.color};display:inline-block;flex-shrink:0`" />
|
|
65
|
+
{{ leg.label }}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<button class="btn-ghost" style="font-size:12px;padding:5px 14px;border:1px solid var(--border-subtle);border-radius:999px;" @click="router.push('/features')">
|
|
69
|
+
查看全部能力 →
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
<!-- Features -->
|
|
77
|
+
<section style="max-width: 960px; margin: 0 auto; padding: 0 24px 96px;">
|
|
78
|
+
<h2 class="section-title" style="text-align: center; margin-bottom: 10px;">核心能力</h2>
|
|
79
|
+
<p style="color: var(--text-secondary); text-align: center; margin-bottom: 40px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;">四大场景,全部通过对话驱动</p>
|
|
80
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
|
|
81
|
+
<div v-for="f in FEATURES" :key="f.title" class="card">
|
|
82
|
+
<div style="width: 44px; height: 44px; border-radius: 12px; background: var(--tag-bg); border: 1px solid var(--tag-border); display: flex; align-items: center; justify-content: center; margin-bottom: 14px;">
|
|
83
|
+
<component :is="f.Icon" :size="22" color="var(--brand-400)" :stroke-width="1.75" />
|
|
84
|
+
</div>
|
|
85
|
+
<h3 class="font-display" style="font-weight: 600; color: var(--text-primary); font-size: 15px; margin-bottom: 8px;">{{ f.title }}</h3>
|
|
86
|
+
<p style="color: var(--text-secondary); font-size: 13px; line-height: 1.6; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;">{{ f.desc }}</p>
|
|
87
|
+
<div style="margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border-subtle);">
|
|
88
|
+
<span class="font-mono" style="font-size: 12px; color: var(--brand-400);">{{ f.example }}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<!-- Stats -->
|
|
95
|
+
<section ref="statsRef" style="max-width: 960px; margin: 0 auto; padding: 0 24px 96px;">
|
|
96
|
+
<div class="glass" style="border-radius: 16px; padding: 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 32px; text-align: center;">
|
|
97
|
+
<div v-for="(stat, i) in RAW_STATS" :key="stat.label" style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
|
|
98
|
+
<div class="font-display" style="font-weight: 700; color: var(--text-primary); font-size: 2rem;">
|
|
99
|
+
{{ counters[i].current.value.toLocaleString() }}<span style="color: var(--brand-400);">{{ stat.suffix }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div style="color: var(--text-muted); font-size: 14px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;">{{ stat.label }}</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<!-- CTA -->
|
|
107
|
+
<section style="max-width: 960px; margin: 0 auto; padding: 0 24px 112px; text-align: center;">
|
|
108
|
+
<div class="glass" style="border-radius: 16px; padding: 48px 32px; position: relative; overflow: hidden;">
|
|
109
|
+
<div class="glow-orb" style="width: 256px; height: 256px; background: rgba(26,111,196,0.15); top: -40px; left: 50%; transform: translateX(-50%);" />
|
|
110
|
+
<h2 class="section-title" style="margin-bottom: 14px; position: relative; z-index: 1;">立即体验对话式开发</h2>
|
|
111
|
+
<p style="color: var(--text-secondary); margin-bottom: 28px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; position: relative; z-index: 1;">无需配置,直接对话,从第一个消息开始提速</p>
|
|
112
|
+
<div style="display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; position: relative; z-index: 1;">
|
|
113
|
+
<button class="btn-primary" style="padding: 12px 32px; font-size: 15px;" @click="router.push('/chat')">
|
|
114
|
+
<MessageSquare :size="16" /> 开始 AI 对话
|
|
115
|
+
</button>
|
|
116
|
+
<button class="btn-ghost" style="padding: 12px 32px; font-size: 15px; border: 1px solid var(--border-default); border-radius: 8px;" @click="router.push('/market')">
|
|
117
|
+
<Layers :size="16" /> 浏览组件市场
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
124
|
+
|
|
125
|
+
<script setup>
|
|
126
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
127
|
+
import { useRouter } from 'vue-router'
|
|
128
|
+
import { Rocket, Package, BarChart2, Wrench, MessageSquare, Layers } from 'lucide-vue-next'
|
|
129
|
+
import InputBar from '../chat-sdk/core/components/InputBar.vue'
|
|
130
|
+
import { useCountUp } from '../composables/useCountUp.js'
|
|
131
|
+
|
|
132
|
+
const props = defineProps({
|
|
133
|
+
chatStore: { type: Object, required: true },
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const router = useRouter()
|
|
137
|
+
|
|
138
|
+
const homeHasImage = ref(false)
|
|
139
|
+
function onHomeAttachmentChange(attachments) {
|
|
140
|
+
homeHasImage.value = attachments.some(a => a.type === 'visual')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const FEATURES = [
|
|
144
|
+
{ Icon: Rocket, title: '脚手架生成', desc: '描述项目类型和技术栈,AI 即时生成可运行的工程模板', example: '「生成 Vue 3 + TS + Pinia 项目」' },
|
|
145
|
+
{ Icon: Package, title: '组件搜索', desc: '用自然语言找到最合适的组件,直接获取安装命令', example: '「找一个企业级 Table 组件」' },
|
|
146
|
+
{ Icon: BarChart2, title: '流水线监控', desc: '对话查询 CI/CD 状态,异常时自动推送修复建议', example: '「查看 main 分支构建状态」' },
|
|
147
|
+
{ Icon: Wrench, title: '异常修复', desc: '粘贴报错信息,获取精准定位分析与可执行修复方案', example: '「分析这个 TypeScript 报错」' },
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
const RAW_STATS = [
|
|
151
|
+
{ label: '接入开发者', target: 12000, suffix: '+' },
|
|
152
|
+
{ label: '组件总数', target: 380, suffix: '+' },
|
|
153
|
+
{ label: 'Skills 数量', target: 95, suffix: '+' },
|
|
154
|
+
{ label: 'GitHub Stars', target: 2100, suffix: '★' },
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
const QUICK = ['写一个炫酷的HTML页面', '将截图转为vue3代码', '画一个完整的cdn回源原理图']
|
|
158
|
+
|
|
159
|
+
// ── Stats 滚动计数 ──
|
|
160
|
+
const counters = RAW_STATS.map(s => useCountUp(s.target))
|
|
161
|
+
const statsRef = ref(null)
|
|
162
|
+
let statsObserver = null
|
|
163
|
+
|
|
164
|
+
// ── 能力图谱 ──
|
|
165
|
+
const graphRef = ref(null)
|
|
166
|
+
const bgCanvas = ref(null)
|
|
167
|
+
const graphSvg = ref(null)
|
|
168
|
+
const graphTip = ref(null)
|
|
169
|
+
let graphObserver = null
|
|
170
|
+
let d3Instance = null
|
|
171
|
+
let animFrame = null
|
|
172
|
+
|
|
173
|
+
const GRAPH_LEGEND = [
|
|
174
|
+
{ label: '核心节点', color: '#1a6fc4' },
|
|
175
|
+
{ label: '功能模块 (二级圆盘)', color: '#1D9E75' },
|
|
176
|
+
{ label: '支持能力 (三级微锚点)', color: '#637a99' },
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
const MODULES = [
|
|
180
|
+
{ id: 'comp', name: '组件渲染', color: '#1D9E75' },
|
|
181
|
+
{ id: 'chart', name: '图表图库', color: '#BA7517' },
|
|
182
|
+
{ id: 'tool', name: '工具集成', color: '#7F77DD' },
|
|
183
|
+
{ id: 'md', name: 'Markdown', color: '#D85A30' },
|
|
184
|
+
{ id: 'model', name: '模型对话', color: '#0F6E56' },
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
const LEAVES = [
|
|
188
|
+
{ id: 'vue', name: 'Vue SFC', mod: 'comp' },
|
|
189
|
+
{ id: 'react', name: 'React JSX', mod: 'comp' },
|
|
190
|
+
{ id: 'less', name: 'Less / CSS', mod: 'comp' },
|
|
191
|
+
{ id: 'html', name: 'HTML 沙箱', mod: 'comp' },
|
|
192
|
+
{ id: 'echarts', name: 'ECharts', mod: 'chart' },
|
|
193
|
+
{ id: 'g6', name: 'AntV G6', mod: 'chart' },
|
|
194
|
+
{ id: 'mermaid', name: 'Mermaid', mod: 'chart' },
|
|
195
|
+
{ id: 'd3', name: 'D3.js', mod: 'chart' },
|
|
196
|
+
{ id: 'gitlab', name: 'GitLab', mod: 'tool' },
|
|
197
|
+
{ id: 'upload', name: '文件上传', mod: 'tool' },
|
|
198
|
+
{ id: 'skills', name: 'Skills 插件', mod: 'tool' },
|
|
199
|
+
{ id: 'shiki', name: 'Shiki 高亮', mod: 'md' },
|
|
200
|
+
{ id: 'latex', name: 'LaTeX', mod: 'md' },
|
|
201
|
+
{ id: 'gfm', name: 'GFM 表格', mod: 'md' },
|
|
202
|
+
{ id: 'multi', name: '多模型切换', mod: 'model' },
|
|
203
|
+
{ id: 'sse', name: 'SSE 流式', mod: 'model' },
|
|
204
|
+
{ id: 'idb', name: 'IndexedDB', mod: 'model' },
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
function loadD3() {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
if (window.d3) { resolve(window.d3); return }
|
|
210
|
+
const s = document.createElement('script')
|
|
211
|
+
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js'
|
|
212
|
+
s.onload = () => resolve(window.d3)
|
|
213
|
+
document.head.appendChild(s)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function initGraph() {
|
|
218
|
+
const d3 = await loadD3()
|
|
219
|
+
d3Instance = d3
|
|
220
|
+
const container = graphRef.value
|
|
221
|
+
if (!container) return
|
|
222
|
+
|
|
223
|
+
const W = container.offsetWidth
|
|
224
|
+
const H = 480
|
|
225
|
+
|
|
226
|
+
// ── 1. 星点背景(精简密度,避免与网格线冲突) ──
|
|
227
|
+
// ── 1. 星点背景微调 ──
|
|
228
|
+
const bc = bgCanvas.value
|
|
229
|
+
bc.width = W
|
|
230
|
+
bc.height = H
|
|
231
|
+
const bx = bc.getContext('2d')
|
|
232
|
+
const isDark = document.documentElement.classList.contains('dark')
|
|
233
|
+
|| window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
234
|
+
|
|
235
|
+
const stars = Array.from({ length: 40 }, () => ({
|
|
236
|
+
x: Math.random() * W,
|
|
237
|
+
y: Math.random() * H,
|
|
238
|
+
r: Math.random() * 1.2 + 0.4, // 稍微增加一点物理半径(0.4 ~ 1.6px),提升可见度
|
|
239
|
+
a: Math.random() * 0.12 + 0.04,
|
|
240
|
+
da: (Math.random() - 0.5) * 0.003,
|
|
241
|
+
}))
|
|
242
|
+
|
|
243
|
+
function drawStars() {
|
|
244
|
+
bx.clearRect(0, 0, W, H)
|
|
245
|
+
|
|
246
|
+
// 【新增】采用经典的高级滤色/屏幕叠加混合模式,让微光自然融入底层
|
|
247
|
+
bx.globalCompositeOperation = isDark ? 'screen' : 'source-over'
|
|
248
|
+
|
|
249
|
+
stars.forEach(s => {
|
|
250
|
+
s.a = Math.max(isDark ? 0.08 : 0.05, Math.min(isDark ? 0.25 : 0.15, s.a + s.da))
|
|
251
|
+
bx.beginPath()
|
|
252
|
+
bx.arc(s.x, s.y, s.r, 0, Math.PI * 2)
|
|
253
|
+
|
|
254
|
+
// 【优化】亮色模式下改用带有呼吸感的浅灰蓝/暮灰色,暗色下采用通透的幽蓝色
|
|
255
|
+
bx.fillStyle = isDark ? `rgba(160,200,255,${s.a})` : `rgba(71,114,179,${s.a})`
|
|
256
|
+
bx.fill()
|
|
257
|
+
})
|
|
258
|
+
animFrame = requestAnimationFrame(drawStars)
|
|
259
|
+
}
|
|
260
|
+
drawStars()
|
|
261
|
+
|
|
262
|
+
// ── 2. 严格的层级数据规范(重塑半径与尺寸) ──
|
|
263
|
+
const modColorMap = Object.fromEntries(MODULES.map(m => [m.id, m.color]))
|
|
264
|
+
|
|
265
|
+
const nodes = [
|
|
266
|
+
// 核心大圆
|
|
267
|
+
{ id: 'root', name: 'Frontend AI', type: 'root', color: '#1a6fc4', r: 48 },
|
|
268
|
+
// 二级模块
|
|
269
|
+
...MODULES.map(m => ({ ...m, type: 'module', color: m.color, r: 30 })),
|
|
270
|
+
// 三级技能
|
|
271
|
+
...LEAVES.map(l => ({ ...l, type: 'leaf', color: modColorMap[l.mod], r: 5 })),
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
// 保持清晰的内、中、外三层星环布局
|
|
275
|
+
const links = [
|
|
276
|
+
...MODULES.map(m => ({ source: 'root', target: m.id, dist: 110, str: 0.6, isCore: true })),
|
|
277
|
+
...LEAVES.map(l => ({ source: l.mod, target: l.id, dist: 60, str: 0.8, isCore: false })),
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
// ── 3. SVG 初始化与高级渐变定义 ──
|
|
281
|
+
const svgEl = graphSvg.value
|
|
282
|
+
svgEl.innerHTML = ''
|
|
283
|
+
const svg = d3.select(svgEl).attr('viewBox', `0 0 ${W} ${H}`)
|
|
284
|
+
const defs = svg.append('defs')
|
|
285
|
+
|
|
286
|
+
// 核心圆线性渐变
|
|
287
|
+
const rootGrad = defs.append('linearGradient').attr('id', 'grad-root-solid').attr('x1', '0%').attr('y1', '0%').attr('x2', '100%').attr('y2', '100%')
|
|
288
|
+
rootGrad.append('stop').attr('offset', '0%').attr('stop-color', '#1a6fc4')
|
|
289
|
+
rootGrad.append('stop').attr('offset', '100%').attr('stop-color', '#0b4585')
|
|
290
|
+
|
|
291
|
+
// 【修复后】二级能力节点的不透明渐变底色
|
|
292
|
+
MODULES.forEach(m => {
|
|
293
|
+
const mGrad = defs.append('linearGradient').attr('id', `grad-mod-opaque-${m.id}`).attr('x1', '0%').attr('y1', '0%').attr('x2', '0%').attr('y2', '100%')
|
|
294
|
+
if (isDark) {
|
|
295
|
+
// 暗色模式:从深灰网格色平滑过渡到微弱的模块主题色
|
|
296
|
+
mGrad.append('stop').attr('offset', '0%').attr('stop-color', '#1f2937')
|
|
297
|
+
mGrad.append('stop').attr('offset', '100%').attr('stop-color', d3.hsl(m.color).darker(1.5).toString())
|
|
298
|
+
} else {
|
|
299
|
+
// 明亮模式:从纯白平滑过渡到极其淡雅的浅主题色(修复此处)
|
|
300
|
+
const hslColor = d3.hsl(m.color)
|
|
301
|
+
hslColor.l = 0.96 // 直接修改明度属性 l
|
|
302
|
+
|
|
303
|
+
mGrad.append('stop').attr('offset', '0%').attr('stop-color', '#ffffff')
|
|
304
|
+
mGrad.append('stop').attr('offset', '100%').attr('stop-color', hslColor.toString())
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// ── 3.5 【新增背景层】精密工程网格 + 环形星轨 ──
|
|
309
|
+
const bgGroup = svg.append('g').attr('class', 'graph-bg')
|
|
310
|
+
|
|
311
|
+
// 绘制 1px 极淡工程网格细线(绝对直角网格,填补两侧空白且留白高级)
|
|
312
|
+
const gridSize = 40
|
|
313
|
+
const gridColor = isDark ? 'rgba(51, 65, 85, 0.15)' : 'rgba(226, 232, 240, 0.5)'
|
|
314
|
+
|
|
315
|
+
// 横线
|
|
316
|
+
for (let y = 0; y < H; y += gridSize) {
|
|
317
|
+
bgGroup.append('line').attr('x1', 0).attr('y1', y).attr('x2', W).attr('y2', y).attr('stroke', gridColor).attr('stroke-width', 0.8)
|
|
318
|
+
}
|
|
319
|
+
// 竖线
|
|
320
|
+
for (let x = 0; x < W; x += gridSize) {
|
|
321
|
+
bgGroup.append('line').attr('x1', x).attr('y1', 0).attr('x2', x).attr('y2', H).attr('stroke', gridColor).attr('stroke-width', 0.8)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 绘制二级能力层、三级技能层的同心圆虚线轨道
|
|
325
|
+
const orbitGroup = bgGroup.append('g').attr('class', 'orbits').attr('opacity', isDark ? 0.2 : 0.4)
|
|
326
|
+
const orbitRadii = [110, 170] // 对应核心到能力的 110px 轴距,以及外围延展区
|
|
327
|
+
orbitRadii.forEach(radius => {
|
|
328
|
+
orbitGroup.append('circle')
|
|
329
|
+
.attr('cx', W / 2).attr('cy', H / 2).attr('r', radius)
|
|
330
|
+
.attr('fill', 'none')
|
|
331
|
+
.attr('stroke', isDark ? '#475569' : '#cbd5e1')
|
|
332
|
+
.attr('stroke-width', 0.8)
|
|
333
|
+
.attr('stroke-dasharray', '3, 4') // 精密虚线
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// ── 4. 力导向核心调优(层级隔离) ──
|
|
337
|
+
const sim = d3.forceSimulation(nodes)
|
|
338
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(d => d.dist).strength(d => d.str))
|
|
339
|
+
.force('charge', d3.forceManyBody().strength(d => d.type === 'root' ? -700 : d.type === 'module' ? -350 : -40))
|
|
340
|
+
.force('center', d3.forceCenter(W / 2, H / 2))
|
|
341
|
+
.force('x', d3.forceX(W / 2).strength(0.2))
|
|
342
|
+
.force('y', d3.forceY(H / 2).strength(0.2))
|
|
343
|
+
.force('collision', d3.forceCollide().radius(d => {
|
|
344
|
+
if (d.type === 'root') return 70
|
|
345
|
+
if (d.type === 'module') return 55
|
|
346
|
+
return 30
|
|
347
|
+
}))
|
|
348
|
+
|
|
349
|
+
// ── 5. 先画线(确保线条乖乖呆在圆盘底层) ──
|
|
350
|
+
const linkSel = svg.append('g').selectAll('line').data(links).join('line')
|
|
351
|
+
.attr('stroke', d => {
|
|
352
|
+
const t = nodes.find(n => n.id === (d.target.id ?? d.target))
|
|
353
|
+
return t?.color ?? '#1a6fc4'
|
|
354
|
+
})
|
|
355
|
+
.attr('stroke-width', d => d.isCore ? 1.5 : 0.8)
|
|
356
|
+
.attr('stroke-opacity', d => d.isCore ? 0.35 : 0.15)
|
|
357
|
+
|
|
358
|
+
// ── 6. 渲染节点组 ──
|
|
359
|
+
const nodeSel = svg.append('g').selectAll('g').data(nodes).join('g')
|
|
360
|
+
.attr('cursor', 'pointer')
|
|
361
|
+
.call(
|
|
362
|
+
d3.drag()
|
|
363
|
+
.on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.1).restart(); d.fx = d.x; d.fy = d.y })
|
|
364
|
+
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y })
|
|
365
|
+
.on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null })
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
// ── 7. 后画圆(四方框已删,能力节点启用全新不透明渐变) ──
|
|
369
|
+
nodeSel.append('circle')
|
|
370
|
+
.attr('r', d => d.r)
|
|
371
|
+
.attr('fill', d => {
|
|
372
|
+
if (d.type === 'root') return 'url(#grad-root-solid)'
|
|
373
|
+
if (d.type === 'module') return `url(#grad-mod-opaque-${d.id})` // 注入渐变底色
|
|
374
|
+
return d.color // 三级原子技能:实心原色微锚点
|
|
375
|
+
})
|
|
376
|
+
.attr('stroke', d => d.type === 'leaf' ? 'none' : d.color)
|
|
377
|
+
.attr('stroke-width', d => d.type === 'module' ? 1.5 : 0)
|
|
378
|
+
|
|
379
|
+
// ── 8. 精准文字排版 ──
|
|
380
|
+
// 8.1 核心与能力大类文字:锁在圆盘中央
|
|
381
|
+
nodeSel.filter(d => d.type !== 'leaf').append('text')
|
|
382
|
+
.text(d => d.name)
|
|
383
|
+
.attr('text-anchor', 'middle')
|
|
384
|
+
.attr('dominant-baseline', 'central')
|
|
385
|
+
.attr('font-family', 'Inter, "PingFang SC", sans-serif')
|
|
386
|
+
.attr('font-size', d => d.type === 'root' ? 12 : 10)
|
|
387
|
+
.attr('font-weight', '600')
|
|
388
|
+
.attr('fill', d => {
|
|
389
|
+
if (d.type === 'root') return '#ffffff'
|
|
390
|
+
return isDark ? '#f1f5f9' : '#0f172a'
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// 8.2 具体技能(叶子)文字:优雅悬挂在核心色点下方
|
|
394
|
+
nodeSel.filter(d => d.type === 'leaf').append('text')
|
|
395
|
+
.text(d => d.name)
|
|
396
|
+
.attr('text-anchor', 'middle')
|
|
397
|
+
.attr('dominant-baseline', 'central')
|
|
398
|
+
.attr('font-family', 'Inter, "PingFang SC", sans-serif')
|
|
399
|
+
.attr('font-size', 9)
|
|
400
|
+
.attr('font-weight', '400')
|
|
401
|
+
.attr('fill', isDark ? '#94a3b8' : '#576575')
|
|
402
|
+
.attr('dy', 14)
|
|
403
|
+
.attr('pointer-events', 'none')
|
|
404
|
+
|
|
405
|
+
// ── 9. Tooltip & 交互 ──
|
|
406
|
+
const tip = graphTip.value
|
|
407
|
+
nodeSel
|
|
408
|
+
.on('mouseenter', (e, d) => {
|
|
409
|
+
tip.style.opacity = '1'
|
|
410
|
+
tip.textContent = d.type === 'root' ? 'Frontend AI — 前端开发全流程智能伙伴' : d.name
|
|
411
|
+
})
|
|
412
|
+
.on('mousemove', (e) => {
|
|
413
|
+
const b = container.getBoundingClientRect()
|
|
414
|
+
tip.style.left = (e.clientX - b.left + 14) + 'px'
|
|
415
|
+
tip.style.top = (e.clientY - b.top - 36) + 'px'
|
|
416
|
+
})
|
|
417
|
+
.on('mouseleave', () => { tip.style.opacity = '0' })
|
|
418
|
+
.on('click', (_, d) => {
|
|
419
|
+
if (d.type !== 'root') router.push('/features')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
// ── 10. 每帧坐标更新 ──
|
|
423
|
+
sim.on('tick', () => {
|
|
424
|
+
const pad = 35
|
|
425
|
+
nodes.forEach(d => {
|
|
426
|
+
d.x = Math.max(pad + d.r, Math.min(W - pad - d.r, d.x))
|
|
427
|
+
d.y = Math.max(pad + d.r, Math.min(H - pad - d.r, d.y))
|
|
428
|
+
})
|
|
429
|
+
linkSel
|
|
430
|
+
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
431
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y)
|
|
432
|
+
nodeSel.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function handleSend(val) {
|
|
437
|
+
const sid = props.chatStore.newSession(val)
|
|
438
|
+
router.push('/chat')
|
|
439
|
+
setTimeout(() => props.chatStore.sendMessage(val, sid), 80)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
onMounted(() => {
|
|
443
|
+
// Stats 计数器
|
|
444
|
+
statsObserver = new IntersectionObserver(
|
|
445
|
+
([entry]) => {
|
|
446
|
+
if (entry.isIntersecting) {
|
|
447
|
+
counters.forEach(c => c.start())
|
|
448
|
+
statsObserver.disconnect()
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
{ threshold: 0.3 }
|
|
452
|
+
)
|
|
453
|
+
if (statsRef.value) statsObserver.observe(statsRef.value)
|
|
454
|
+
|
|
455
|
+
// 图谱懒加载
|
|
456
|
+
graphObserver = new IntersectionObserver(
|
|
457
|
+
([entry]) => {
|
|
458
|
+
if (entry.isIntersecting) {
|
|
459
|
+
initGraph()
|
|
460
|
+
graphObserver.disconnect()
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
{ threshold: 0.1 }
|
|
464
|
+
)
|
|
465
|
+
if (graphRef.value) graphObserver.observe(graphRef.value)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
onUnmounted(() => {
|
|
469
|
+
statsObserver?.disconnect()
|
|
470
|
+
graphObserver?.disconnect()
|
|
471
|
+
if (animFrame) cancelAnimationFrame(animFrame)
|
|
472
|
+
})
|
|
473
|
+
</script>
|
|
474
|
+
|
|
475
|
+
<style scoped>
|
|
476
|
+
.quick-btn:hover {
|
|
477
|
+
border-color: rgba(26,111,196,0.4) !important;
|
|
478
|
+
color: var(--brand-400) !important;
|
|
479
|
+
background: var(--tag-bg) !important;
|
|
480
|
+
}
|
|
481
|
+
</style>
|