@templmf/temp-solf-lmf 0.0.135 → 0.0.136

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@templmf/temp-solf-lmf",
3
- "version": "0.0.135",
3
+ "version": "0.0.136",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server.js ADDED
@@ -0,0 +1,58 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import { ChatAnthropic } from "@langchain/anthropic";
4
+ import { StateGraph, MessagesAnnotation } from "@langchain/langgraph";
5
+ import { HumanMessage, AIMessage } from "@langchain/core/messages";
6
+
7
+ const app = express();
8
+ app.use(cors());
9
+ app.use(express.json());
10
+
11
+ const llm = new ChatAnthropic({
12
+ model: "claude-3-5-sonnet-20241022",
13
+ apiKey: process.env.ANTHROPIC_API_KEY,
14
+ });
15
+
16
+ const graph = new StateGraph(MessagesAnnotation)
17
+ .addNode("chat", async (state) => ({
18
+ messages: [await llm.invoke(state.messages)],
19
+ }))
20
+ .addEdge("__start__", "chat")
21
+ .addEdge("chat", "__end__")
22
+ .compile();
23
+
24
+ app.post("/chat/stream", async (req, res) => {
25
+ const { messages } = req.body;
26
+ // messages 格式: [{ role: "user"|"assistant", content: "..." }]
27
+
28
+ // SSE headers
29
+ res.setHeader("Content-Type", "text/event-stream");
30
+ res.setHeader("Cache-Control", "no-cache");
31
+ res.setHeader("Connection", "keep-alive");
32
+
33
+ // 将前端传来的历史消息转换为 LangChain 格式
34
+ const history = messages.map((m) =>
35
+ m.role === "user" ? new HumanMessage(m.content) : new AIMessage(m.content)
36
+ );
37
+
38
+ try {
39
+ const stream = await graph.stream(
40
+ { messages: history },
41
+ { streamMode: "messages" }
42
+ );
43
+
44
+ for await (const [chunk, metadata] of stream) {
45
+ if (metadata?.langgraph_node === "chat" && chunk.content) {
46
+ res.write(`data: ${JSON.stringify({ token: chunk.content })}\n\n`);
47
+ }
48
+ }
49
+
50
+ res.write(`data: [DONE]\n\n`);
51
+ } catch (err) {
52
+ res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
53
+ } finally {
54
+ res.end();
55
+ }
56
+ });
57
+
58
+ app.listen(3000, () => console.log("Server running on http://localhost:3000"));
package/HomeView.vue DELETED
@@ -1,481 +0,0 @@
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>
@@ -1,177 +0,0 @@
1
- // src/data/features.js
2
- // 新增能力只需在对应模块的 items 数组里追加一条记录即可
3
- // date 格式: "YYYY-MM"
4
- // isNew: true → 显示「新」标签
5
- // isBeta: true → 显示「Beta」标签
6
-
7
- export const FEATURE_MODULES = [
8
- {
9
- id: 'component',
10
- cls: 'blue',
11
- icon: 'ti-components',
12
- name: '组件渲染',
13
- desc: 'Vue、ElementPlus、HTML 实时渲染',
14
- items: [
15
- {
16
- name: 'Vue SFC',
17
- desc: '支持 &lt;script setup&gt;、&lt;style scoped&gt;,右侧 Artifact 面板实时预览。',
18
- tags: ['Vue 3', 'Composition API'],
19
- date: '2024-09',
20
- },
21
- {
22
- name: 'React JSX',
23
- desc: 'Artifact 面板渲染函数组件,支持 useState、useEffect 等 Hooks。',
24
- tags: ['React 18', 'JSX'],
25
- date: '2024-09',
26
- },
27
- {
28
- name: 'Element Plus',
29
- desc: 'AI 生成组件自动引入 Element Plus,无需手动配置依赖。',
30
- tags: ['Element Plus'],
31
- date: '2025-01',
32
- },
33
- {
34
- name: 'Less / CSS',
35
- desc: '内置 Less 编译器,支持嵌套、变量、mixin,实时编译输出。',
36
- tags: ['Less', 'CSS Variables'],
37
- date: '2024-11',
38
- },
39
- {
40
- name: 'HTML 片段',
41
- desc: '任意 HTML + JS 代码段,iframe 沙箱隔离运行,安全预览。',
42
- tags: ['iframe 沙箱'],
43
- date: '2024-09',
44
- },
45
- ],
46
- },
47
- {
48
- id: 'chart',
49
- cls: 'green',
50
- icon: 'ti-chart-dots',
51
- name: '图表 & 图库',
52
- desc: 'ECharts、G6、Mermaid、D3',
53
- items: [
54
- {
55
- name: 'ECharts',
56
- desc: '折线、柱状、饼图、散点、热力图,支持大数据量渲染。',
57
- tags: ['Apache ECharts 5'],
58
- date: '2024-10',
59
- },
60
- {
61
- name: 'AntV G6',
62
- desc: '关系图、拓扑图、树图,适合节点连线类可视化场景。',
63
- tags: ['@antv/g6'],
64
- date: '2025-03',
65
- isNew: true,
66
- },
67
- {
68
- name: 'Mermaid',
69
- desc: 'Markdown 消息中直接渲染流程图、时序图、甘特图,支持缩放查看。',
70
- tags: ['Mermaid 10', '流程图', '时序图'],
71
- date: '2024-12',
72
- },
73
- {
74
- name: 'D3.js',
75
- desc: '支持自定义数据驱动图形,适合复杂定制化可视化需求。',
76
- tags: ['D3 v7'],
77
- date: '2025-04',
78
- isNew: true,
79
- },
80
- {
81
- name: 'SVG / Canvas',
82
- desc: 'AI 生成原生 SVG 矢量图或 Canvas 绘图代码,直接嵌入渲染。',
83
- tags: ['SVG', 'Canvas 2D'],
84
- date: '2024-09',
85
- },
86
- ],
87
- },
88
- {
89
- id: 'tool',
90
- cls: 'amber',
91
- icon: 'ti-plug',
92
- name: '工具集成',
93
- desc: 'GitLab、文件上传、Skills 插件',
94
- items: [
95
- {
96
- name: 'GitLab',
97
- desc: '读取项目列表、MR、Issue,AI 直接基于仓库内容回答和生成代码。',
98
- tags: ['GitLab API', '工具调用'],
99
- date: '2025-02',
100
- },
101
- {
102
- name: '文件上传',
103
- desc: '支持图片、PDF 上传,AI 读取内容结合对话上下文分析。',
104
- tags: ['图片', 'PDF'],
105
- date: '2024-11',
106
- },
107
- {
108
- name: 'Skills 插件',
109
- desc: '从 Skills 市场安装能力插件,扩展 AI 在特定场景下的执行能力。',
110
- tags: ['官方', '社区'],
111
- date: '2025-05',
112
- isBeta: true,
113
- },
114
- ],
115
- },
116
- {
117
- id: 'markdown',
118
- cls: 'purple',
119
- icon: 'ti-markdown',
120
- name: 'Markdown 增强',
121
- desc: '代码高亮、LaTeX、GFM',
122
- items: [
123
- {
124
- name: '代码高亮',
125
- desc: '基于 Shiki,支持 100+ 语言语法高亮,深浅主题自动切换。',
126
- tags: ['Shiki', '100+ 语言'],
127
- date: '2024-09',
128
- },
129
- {
130
- name: 'LaTeX 公式',
131
- desc: '行内 $ 和块级 $$ 公式均可渲染,适合数学、算法类内容。',
132
- tags: ['KaTeX', '行内 & 块级'],
133
- date: '2024-10',
134
- },
135
- {
136
- name: '表格 / 任务列表',
137
- desc: '完整 GFM 支持,表格、任务列表、引用块、分割线均正确渲染。',
138
- tags: ['GFM'],
139
- date: '2024-09',
140
- },
141
- ],
142
- },
143
- {
144
- id: 'model',
145
- cls: 'coral',
146
- icon: 'ti-brain',
147
- name: '模型 & 对话',
148
- desc: '多模型、流式输出、持久化',
149
- items: [
150
- {
151
- name: '多模型切换',
152
- desc: '对话中随时切换模型,配置即时生效,不打断上下文。',
153
- tags: ['DashScope'],
154
- date: '2025-01',
155
- },
156
- {
157
- name: '流式输出',
158
- desc: 'SSE 实时流式返回,逐字渲染,响应不等待完整结果。',
159
- tags: ['SSE', 'Stream'],
160
- date: '2024-09',
161
- },
162
- {
163
- name: '对话历史持久化',
164
- desc: 'IndexedDB 本地存储,刷新不丢记录,支持跨会话回溯。',
165
- tags: ['IndexedDB'],
166
- date: '2024-12',
167
- },
168
- {
169
- name: '思考链展示',
170
- desc: 'AI 推理过程可折叠展示,方便理解模型决策路径。',
171
- tags: ['CoT'],
172
- date: '2025-03',
173
- isNew: true,
174
- },
175
- ],
176
- },
177
- ]
@@ -1,44 +0,0 @@
1
- import { createRouter, createWebHistory } from 'vue-router'
2
-
3
- const routes = [
4
- { path: '/', component: () => import('../views/HomeView.vue') },
5
- { path: '/chat', component: () => import('../views/ChatView.vue') },
6
- { path: '/chat/share/:uuid', component: () => import('../views/ChatView.vue') },
7
- { path: '/market', component: () => import('../views/MarketView.vue') },
8
- { path: '/skills', component: () => import('../views/SkillsView.vue') },
9
- { path: '/practices', component: () => import('../views/PracticesView.vue') },
10
- // ── 新增 ──
11
- {
12
- path: '/features',
13
- name: 'features',
14
- component: () => import('../views/FeaturesView.vue')
15
- },
16
- { path: '/:pathMatch(.*)*', redirect: '/' },
17
- ]
18
-
19
- const router = createRouter({
20
- history: createWebHistory(),
21
- routes,
22
- scrollBehavior: () => ({ top: 0 }),
23
- })
24
-
25
- /**
26
- * 路由守卫:首页和 public 页面跳过鉴权,其他页面检查登录态
27
- */
28
- router.beforeEach(async (to) => {
29
- if (to.path === '/' || to.meta.public) return true
30
-
31
- const { useAuthStore } = await import('../composables/useAuth.js')
32
- const authStore = useAuthStore()
33
-
34
- const result = await authStore.checkAuth(to.fullPath)
35
- if (result.ok) return true
36
-
37
- if (result.redirectUrl) {
38
- window.location.href = result.redirectUrl
39
- return false
40
- }
41
- return false
42
- })
43
-
44
- export default router
@@ -1,475 +0,0 @@
1
- <template>
2
- <div class="features-page">
3
- <!-- Hero -->
4
- <section class="hero" :class="{ visible: mounted }">
5
- <p class="eyebrow">AI对话能力地图</p>
6
- <h1 class="hero-title">
7
- 我们支持<br />
8
- <em>哪些能力</em>
9
- </h1>
10
- <div class="hero-stats">
11
- <div class="stat">
12
- <span class="stat-n">{{ visibleStats.total }}</span>
13
- <span class="stat-l">支持项</span>
14
- </div>
15
- <div class="stat">
16
- <span class="stat-n">{{ visibleStats.newCount }}</span>
17
- <span class="stat-l">近期新增</span>
18
- </div>
19
- <div class="stat">
20
- <span class="stat-n">{{ visibleStats.modCount }}</span>
21
- <span class="stat-l">功能模块</span>
22
- </div>
23
- </div>
24
- </section>
25
-
26
- <!-- 筛选栏 -->
27
- <div class="filter-bar" :class="{ visible: mounted }" style="animation-delay: 0.08s">
28
- <button
29
- v-for="f in filters"
30
- :key="f.id"
31
- class="fbtn"
32
- :class="{ on: activeFilter === f.id }"
33
- @click="setFilter(f.id)"
34
- >
35
- {{ f.label }}
36
- </button>
37
- </div>
38
-
39
- <!-- 模块列表 -->
40
- <div class="modules" :class="{ visible: mounted }" style="animation-delay: 0.14s">
41
- <div
42
- v-for="mod in visibleModules"
43
- :key="mod.id"
44
- class="module"
45
- :class="mod.cls"
46
- >
47
- <!-- 模块标题行 -->
48
- <div class="mhead" @click="handleModuleClick(mod.id)">
49
- <div class="micon">
50
- <el-icon :size="18"><component :is="iconMap[mod.icon] || 'Grid'" /></el-icon>
51
- </div>
52
- <div class="minfo">
53
- <span class="mname">{{ mod.name }}</span>
54
- <span class="mdesc">{{ mod.desc }}</span>
55
- </div>
56
- <div class="mright">
57
- <span class="mcount">{{ mod.items.length }} 项</span>
58
- <i
59
- class="ti ti-chevron-down mchevron"
60
- :class="{ open: openState[mod.id] }"
61
- />
62
- </div>
63
- </div>
64
-
65
- <!-- 模块内容 -->
66
- <Transition name="slide">
67
- <div v-if="openState[mod.id]" class="mbody">
68
- <div
69
- v-for="(item, idx) in getVisibleItems(mod)"
70
- :key="idx"
71
- class="row"
72
- >
73
- <div class="row-main">
74
- <div class="rname">{{ item.name }}</div>
75
- <div class="rdesc" v-html="item.desc" />
76
- <div class="rtags">
77
- <span v-for="t in item.tags" :key="t" class="tag">{{ t }}</span>
78
- <span v-if="item.isNew" class="tag new">新</span>
79
- <span v-if="item.isBeta" class="tag beta">Beta</span>
80
- </div>
81
- </div>
82
- <div class="rdate">
83
- <span class="rdate-mon">{{ formatDate(item.date).mon }}</span>
84
- <span class="rdate-yr">{{ formatDate(item.date).yr }}</span>
85
- </div>
86
- </div>
87
-
88
- <!-- 展开/收起更多 -->
89
- <div
90
- v-if="mod.items.length > COLLAPSE_AT"
91
- class="more-row"
92
- @click="toggleExpand(mod.id)"
93
- >
94
- <i
95
- class="ti"
96
- :class="expandState[mod.id] ? 'ti-chevron-up' : 'ti-chevron-down'"
97
- />
98
- <span v-if="expandState[mod.id]">收起</span>
99
- <span v-else>还有 {{ mod.items.length - COLLAPSE_AT }} 项,点击展开</span>
100
- </div>
101
- </div>
102
- </Transition>
103
- </div>
104
- </div>
105
- </div>
106
- </template>
107
-
108
- <script setup>
109
- import { ref, computed, onMounted } from 'vue'
110
- import { FEATURE_MODULES } from '../data/features.js'
111
-
112
- const COLLAPSE_AT = 5
113
-
114
- const MONTHS = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']
115
-
116
- function formatDate(d) {
117
- const [y, mo] = d.split('-')
118
- return { mon: MONTHS[parseInt(mo, 10) - 1], yr: y }
119
- }
120
-
121
- // 入场动画
122
- const mounted = ref(false)
123
- onMounted(() => {
124
- requestAnimationFrame(() => {
125
- mounted.value = true
126
- })
127
- })
128
-
129
- // 筛选
130
- const activeFilter = ref('all')
131
- const filters = computed(() => [
132
- { id: 'all', label: '全部模块' },
133
- ...FEATURE_MODULES.map(m => ({ id: m.id, label: m.name })),
134
- ])
135
-
136
- function setFilter(id) {
137
- activeFilter.value = id
138
- if (id === 'all') {
139
- FEATURE_MODULES.forEach(m => { openState.value[m.id] = false })
140
- } else {
141
- openState.value[id] = true
142
- }
143
- }
144
-
145
- const visibleModules = computed(() =>
146
- activeFilter.value === 'all'
147
- ? FEATURE_MODULES
148
- : FEATURE_MODULES.filter(m => m.id === activeFilter.value)
149
- )
150
-
151
- // 顶部统计数字
152
- const visibleStats = computed(() => {
153
- const mods = visibleModules.value
154
- return {
155
- total: mods.reduce((a, m) => a + m.items.length, 0),
156
- newCount: mods.reduce((a, m) => a + m.items.filter(i => i.isNew || i.isBeta).length, 0),
157
- modCount: mods.length,
158
- }
159
- })
160
-
161
- // 模块折叠状态
162
- const openState = ref(
163
- Object.fromEntries(FEATURE_MODULES.map(m => [m.id, false]))
164
- )
165
- const expandState = ref(
166
- Object.fromEntries(FEATURE_MODULES.map(m => [m.id, false]))
167
- )
168
-
169
- function handleModuleClick(id) {
170
- if (activeFilter.value !== id) {
171
- setFilter(id)
172
- } else {
173
- toggleOpen(id)
174
- }
175
- }
176
- function toggleOpen(id) {
177
- openState.value[id] = !openState.value[id]
178
- }
179
- function toggleExpand(id) {
180
- expandState.value[id] = !expandState.value[id]
181
- }
182
-
183
- function getVisibleItems(mod) {
184
- if (mod.items.length <= COLLAPSE_AT || expandState.value[mod.id]) {
185
- return mod.items
186
- }
187
- return mod.items.slice(0, COLLAPSE_AT)
188
- }
189
-
190
- // Element Plus 图标映射(按需替换为项目中实际已注册的图标)
191
- const iconMap = {
192
- 'ti-components': 'Grid',
193
- 'ti-chart-dots': 'TrendCharts',
194
- 'ti-plug': 'Connection',
195
- 'ti-markdown': 'Document',
196
- 'ti-brain': 'Cpu',
197
- }
198
- </script>
199
-
200
- <style scoped>
201
- /* ── 入场动画 ── */
202
- @keyframes fadeUp {
203
- from { opacity: 0; transform: translateY(18px); }
204
- to { opacity: 1; transform: translateY(0); }
205
- }
206
- .hero,
207
- .filter-bar,
208
- .modules {
209
- opacity: 0;
210
- transition: none;
211
- }
212
- .hero.visible,
213
- .filter-bar.visible,
214
- .modules.visible {
215
- animation: fadeUp 0.45s ease both;
216
- }
217
- .filter-bar.visible { animation-delay: 0.08s; }
218
- .modules.visible { animation-delay: 0.14s; }
219
-
220
- /* ── 页面容器 ── */
221
- .features-page {
222
- max-width: 800px;
223
- margin: 0 auto;
224
- padding: 2.5rem 1.5rem 4rem;
225
- }
226
-
227
- /* ── Hero ── */
228
- .hero {
229
- padding-bottom: 2rem;
230
- margin-bottom: 2rem;
231
- border-bottom: 0.5px solid var(--el-border-color-light);
232
- }
233
- .eyebrow {
234
- font-size: 11px;
235
- letter-spacing: 0.14em;
236
- text-transform: uppercase;
237
- color: var(--el-text-color-placeholder);
238
- margin-bottom: 1rem;
239
- }
240
- .hero-title {
241
- font-size: 48px;
242
- font-weight: 500;
243
- line-height: 1.05;
244
- letter-spacing: -1.5px;
245
- color: var(--el-text-color-primary);
246
- margin-bottom: 1.5rem;
247
- }
248
- .hero-title em {
249
- font-style: normal;
250
- color: var(--el-color-primary);
251
- }
252
- .hero-stats {
253
- display: flex;
254
- gap: 2.5rem;
255
- }
256
- .stat {
257
- display: flex;
258
- flex-direction: column;
259
- gap: 3px;
260
- }
261
- .stat-n {
262
- font-size: 28px;
263
- font-weight: 500;
264
- color: var(--el-text-color-primary);
265
- letter-spacing: -0.5px;
266
- line-height: 1;
267
- }
268
- .stat-l {
269
- font-size: 12px;
270
- color: var(--el-text-color-placeholder);
271
- }
272
-
273
- /* ── 筛选栏 ── */
274
- .filter-bar {
275
- display: flex;
276
- gap: 6px;
277
- flex-wrap: wrap;
278
- margin-bottom: 1.75rem;
279
- }
280
- .fbtn {
281
- font-size: 12px;
282
- padding: 5px 14px;
283
- border-radius: 999px;
284
- border: 0.5px solid var(--el-border-color);
285
- background: transparent;
286
- color: var(--el-text-color-secondary);
287
- cursor: pointer;
288
- transition: all 0.12s;
289
- font-family: inherit;
290
- }
291
- .fbtn:hover {
292
- border-color: var(--el-border-color-darker);
293
- color: var(--el-text-color-primary);
294
- }
295
- .fbtn.on {
296
- background: var(--el-text-color-primary);
297
- color: var(--el-bg-color);
298
- border-color: transparent;
299
- }
300
-
301
- /* ── 模块卡片 ── */
302
- .module {
303
- margin-bottom: 1rem;
304
- border: 0.5px solid var(--el-border-color-light);
305
- border-radius: 12px;
306
- overflow: hidden;
307
- }
308
- .mhead {
309
- display: flex;
310
- align-items: center;
311
- gap: 12px;
312
- padding: 1rem 1.25rem;
313
- cursor: pointer;
314
- user-select: none;
315
- transition: background 0.1s;
316
- }
317
- .mhead:hover {
318
- background: var(--el-fill-color-light);
319
- }
320
- .micon {
321
- width: 36px;
322
- height: 36px;
323
- border-radius: 8px;
324
- display: flex;
325
- align-items: center;
326
- justify-content: center;
327
- flex-shrink: 0;
328
- border: 0.5px solid transparent;
329
- }
330
- .minfo {
331
- display: flex;
332
- flex-direction: column;
333
- gap: 2px;
334
- }
335
- .mname {
336
- font-size: 15px;
337
- font-weight: 500;
338
- color: var(--el-text-color-primary);
339
- }
340
- .mdesc {
341
- font-size: 12px;
342
- color: var(--el-text-color-secondary);
343
- }
344
- .mright {
345
- margin-left: auto;
346
- display: flex;
347
- align-items: center;
348
- gap: 10px;
349
- }
350
- .mcount {
351
- font-size: 12px;
352
- color: var(--el-text-color-placeholder);
353
- }
354
- .mchevron {
355
- font-size: 14px;
356
- color: var(--el-text-color-placeholder);
357
- transition: transform 0.2s;
358
- }
359
- .mchevron.open {
360
- transform: rotate(180deg);
361
- }
362
-
363
- /* 模块主题色 */
364
- .blue .micon { background: #E6F1FB; color: #185FA5; border-color: #B5D4F4; }
365
- .green .micon { background: #EAF3DE; color: #3B6D11; border-color: #C0DD97; }
366
- .amber .micon { background: #FAEEDA; color: #854F0B; border-color: #FAC775; }
367
- .purple .micon{ background: #EEEDFE; color: #534AB7; border-color: #CECBF6; }
368
- .coral .micon { background: #FAECE7; color: #993C1D; border-color: #F5C4B3; }
369
-
370
- /* ── 模块内容体 ── */
371
- .mbody {
372
- border-top: 0.5px solid var(--el-border-color-light);
373
- }
374
-
375
- /* 展开动画 */
376
- .slide-enter-active,
377
- .slide-leave-active {
378
- transition: max-height 0.25s ease, opacity 0.2s ease;
379
- overflow: hidden;
380
- max-height: 2000px;
381
- }
382
- .slide-enter-from,
383
- .slide-leave-to {
384
- max-height: 0;
385
- opacity: 0;
386
- }
387
-
388
- /* ── 每行能力 ── */
389
- .row {
390
- display: grid;
391
- grid-template-columns: 1fr 80px;
392
- gap: 1rem;
393
- padding: 0.75rem 1.25rem;
394
- border-bottom: 0.5px solid var(--el-border-color-lighter);
395
- align-items: start;
396
- transition: background 0.1s;
397
- }
398
- .row:last-of-type {
399
- border-bottom: none;
400
- }
401
- .row:hover {
402
- background: var(--el-fill-color-light);
403
- }
404
- .rname {
405
- font-size: 14px;
406
- font-weight: 500;
407
- color: var(--el-text-color-primary);
408
- margin-bottom: 3px;
409
- }
410
- .rdesc {
411
- font-size: 13px;
412
- color: var(--el-text-color-secondary);
413
- line-height: 1.55;
414
- }
415
- .rtags {
416
- display: flex;
417
- gap: 5px;
418
- flex-wrap: wrap;
419
- margin-top: 6px;
420
- }
421
- .tag {
422
- font-size: 11px;
423
- padding: 2px 8px;
424
- border-radius: 999px;
425
- border: 0.5px solid var(--el-border-color);
426
- color: var(--el-text-color-secondary);
427
- }
428
- .tag.new { background: #E6F1FB; border-color: #B5D4F4; color: #0C447C; }
429
- .tag.beta { background: #FAEEDA; border-color: #FAC775; color: #633806; }
430
-
431
- .rdate {
432
- text-align: right;
433
- display: flex;
434
- flex-direction: column;
435
- gap: 2px;
436
- padding-top: 2px;
437
- }
438
- .rdate-mon {
439
- font-size: 13px;
440
- font-weight: 500;
441
- color: var(--el-text-color-secondary);
442
- }
443
- .rdate-yr {
444
- font-size: 11px;
445
- color: var(--el-text-color-placeholder);
446
- }
447
-
448
- /* ── 展开更多行 ── */
449
- .more-row {
450
- display: flex;
451
- align-items: center;
452
- justify-content: center;
453
- gap: 6px;
454
- padding: 0.6rem 1.25rem;
455
- font-size: 12px;
456
- color: var(--el-text-color-placeholder);
457
- background: var(--el-fill-color-light);
458
- cursor: pointer;
459
- border-top: 0.5px solid var(--el-border-color-lighter);
460
- transition: color 0.1s;
461
- }
462
- .more-row:hover {
463
- color: var(--el-text-color-secondary);
464
- }
465
- .more-row .ti {
466
- font-size: 13px;
467
- }
468
-
469
- /* ── 响应式 ── */
470
- @media (max-width: 600px) {
471
- .hero-title { font-size: 32px; }
472
- .hero-stats { gap: 1.5rem; }
473
- .stat-n { font-size: 22px; }
474
- }
475
- </style>