@templmf/temp-solf-lmf 0.0.55 → 0.0.56
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 +1 -1
- package//345/242/236/351/207/217/351/234/200/346/261/202prompt +72 -0
- package/guanwang/README.md +0 -95
- package/guanwang/docs/changelog.md +0 -145
- package/guanwang/docs/doc-maintenance.md +0 -229
- package/guanwang/docs/product.md +0 -181
- package/guanwang/docs/test-cases.md +0 -395
- package/guanwang/docs/usage.md +0 -291
- package/guanwang/env.example +0 -27
- package/guanwang/index.html +0 -13
- package/guanwang/package-lock.json +0 -3825
- package/guanwang/package.json +0 -32
- package/guanwang/public/favicon.svg +0 -4
- package/guanwang/public/react-runtime/babel.min.js +0 -4
- package/guanwang/public/react-runtime/react-dom.min.js +0 -267
- package/guanwang/public/react-runtime/react.min.js +0 -31
- package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +0 -50795
- package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +0 -12758
- package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +0 -8600
- package/guanwang/public/vue-repl-assets/vue.esm-browser.js +0 -18672
- package/guanwang/src/App.vue +0 -61
- package/guanwang/src/chat-sdk/core/components/ChatBox.vue +0 -305
- package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +0 -84
- package/guanwang/src/chat-sdk/core/components/InputBar.vue +0 -354
- package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +0 -703
- package/guanwang/src/chat-sdk/core/useTheme.js +0 -31
- package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +0 -172
- package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +0 -963
- package/guanwang/src/chat-sdk/features/artifact/index.js +0 -13
- package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +0 -275
- package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +0 -523
- package/guanwang/src/chat-sdk/features/codepreview/index.js +0 -7
- package/guanwang/src/chat-sdk/features/markdown/index.js +0 -13
- package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +0 -724
- package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +0 -254
- package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +0 -142
- package/guanwang/src/chat-sdk/features/upload/index.js +0 -17
- package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +0 -336
- package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +0 -76
- package/guanwang/src/chat-sdk/headless/api/chatApi.js +0 -126
- package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +0 -351
- package/guanwang/src/chat-sdk/headless/index.js +0 -15
- package/guanwang/src/chat-sdk/headless/useChat.js +0 -77
- package/guanwang/src/chat-sdk/headless/useChatDB.js +0 -147
- package/guanwang/src/chat-sdk/headless/useChatStore.js +0 -529
- package/guanwang/src/chat-sdk/index.js +0 -79
- package/guanwang/src/chat-sdk/modes/architect.js +0 -27
- package/guanwang/src/chat-sdk/modes/ask.js +0 -26
- package/guanwang/src/chat-sdk/modes/code.js +0 -25
- package/guanwang/src/chat-sdk/modes/index.js +0 -36
- package/guanwang/src/chat-sdk/modes/requirements.js +0 -175
- package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +0 -170
- package/guanwang/src/chat-sdk/settings/index.js +0 -9
- package/guanwang/src/chat-sdk/settings/useSettings.js +0 -122
- package/guanwang/src/chat-sdk/tools/defaults.js +0 -89
- package/guanwang/src/chat-sdk/tools/index.js +0 -16
- package/guanwang/src/chat-sdk/tools/parser.js +0 -116
- package/guanwang/src/components/CustomCursor.vue +0 -69
- package/guanwang/src/components/Footer.vue +0 -24
- package/guanwang/src/components/LoginModal.vue +0 -109
- package/guanwang/src/components/Navbar.vue +0 -193
- package/guanwang/src/components/ThemeToggle.vue +0 -25
- package/guanwang/src/composables/useArtifactStore.js +0 -253
- package/guanwang/src/composables/useAuth.js +0 -88
- package/guanwang/src/composables/useChatDB.js +0 -147
- package/guanwang/src/composables/useCountUp.js +0 -24
- package/guanwang/src/composables/useFileHandler.js +0 -345
- package/guanwang/src/composables/useTheme.js +0 -31
- package/guanwang/src/config/api.js +0 -71
- package/guanwang/src/main.js +0 -23
- package/guanwang/src/router/index.js +0 -23
- package/guanwang/src/services/authApi.js +0 -27
- package/guanwang/src/services/chatApi.js +0 -66
- package/guanwang/src/styles/global.css +0 -478
- package/guanwang/src/tracker/analyze.js +0 -73
- package/guanwang/src/tracker/config.js +0 -82
- package/guanwang/src/tracker/index.js +0 -18
- package/guanwang/src/tracker/service.js +0 -102
- package/guanwang/src/tracker/useChatTracker.js +0 -179
- package/guanwang/src/tracker/useTracker.js +0 -45
- package/guanwang/src/views/ChatView.vue +0 -65
- package/guanwang/src/views/HomeView.vue +0 -156
- package/guanwang/src/views/MarketView.vue +0 -143
- package/guanwang/src/views/PracticesView.vue +0 -190
- package/guanwang/src/views/SkillsView.vue +0 -129
- package/guanwang/temp +0 -19
- package/guanwang/vite.config.js +0 -6
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<el-dialog
|
|
3
|
-
v-model="show"
|
|
4
|
-
:title="`代码预览 · ${langLabel}`"
|
|
5
|
-
width="960px"
|
|
6
|
-
top="4vh"
|
|
7
|
-
align-center
|
|
8
|
-
destroy-on-close
|
|
9
|
-
@closed="emit('close')"
|
|
10
|
-
>
|
|
11
|
-
<!-- 工具栏 -->
|
|
12
|
-
<div class="preview-toolbar">
|
|
13
|
-
<div class="preview-tabs">
|
|
14
|
-
<button
|
|
15
|
-
v-for="tab in availableTabs"
|
|
16
|
-
:key="tab.value"
|
|
17
|
-
class="preview-tab"
|
|
18
|
-
:class="{ active: activeTab === tab.value }"
|
|
19
|
-
@click="activeTab = tab.value"
|
|
20
|
-
>{{ tab.label }}</button>
|
|
21
|
-
</div>
|
|
22
|
-
<div style="display:flex;gap:8px;align-items:center;">
|
|
23
|
-
<el-tooltip v-if="!isVueSfc" content="刷新预览" placement="top">
|
|
24
|
-
<el-button text circle size="small" @click="refreshIframe">
|
|
25
|
-
<RefreshCw :size="14" />
|
|
26
|
-
</el-button>
|
|
27
|
-
</el-tooltip>
|
|
28
|
-
<el-tooltip content="复制代码" placement="top">
|
|
29
|
-
<el-button text circle size="small" @click="copyCode">
|
|
30
|
-
<Check v-if="copied" :size="14" color="#4ade80" />
|
|
31
|
-
<Copy v-else :size="14" />
|
|
32
|
-
</el-button>
|
|
33
|
-
</el-tooltip>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<!-- 内容区 -->
|
|
38
|
-
<div class="preview-body">
|
|
39
|
-
|
|
40
|
-
<!-- ══ Vue SFC:完全交给 <Repl>,tab 切换控制其布局 ══ -->
|
|
41
|
-
<template v-if="isVueSfc">
|
|
42
|
-
<div v-if="!replStore" class="vue-repl-loading">
|
|
43
|
-
<span>正在加载 Vue Playground...</span>
|
|
44
|
-
</div>
|
|
45
|
-
<Repl
|
|
46
|
-
v-else
|
|
47
|
-
:store="replStore"
|
|
48
|
-
:editor="CodeMirror"
|
|
49
|
-
:layout="replLayout"
|
|
50
|
-
:show-compile-output="false"
|
|
51
|
-
:show-import-map="false"
|
|
52
|
-
:show-ts-config="false"
|
|
53
|
-
:clear-console="false"
|
|
54
|
-
:editorOptions="{
|
|
55
|
-
showErrorText: false,
|
|
56
|
-
autoSaveText: false
|
|
57
|
-
}"
|
|
58
|
-
:theme="isDark ? 'dark' : 'light'"
|
|
59
|
-
class="vue-repl-full"
|
|
60
|
-
/>
|
|
61
|
-
</template>
|
|
62
|
-
|
|
63
|
-
<!-- ══ 非 Vue SFC:自己的代码 + 预览双面板 ══ -->
|
|
64
|
-
<template v-else>
|
|
65
|
-
<!-- 代码面板 -->
|
|
66
|
-
<div
|
|
67
|
-
v-show="activeTab === 'code' || activeTab === 'split'"
|
|
68
|
-
class="preview-pane code-pane"
|
|
69
|
-
>
|
|
70
|
-
<div class="shiki-scroll" @click="onCodeClick">
|
|
71
|
-
<div ref="codeContainerRef" class="shiki-container" v-html="highlightedCode" />
|
|
72
|
-
</div>
|
|
73
|
-
<div class="code-statusbar">
|
|
74
|
-
<span class="code-statusbar__lang">{{ langLabel }}</span>
|
|
75
|
-
<span class="code-statusbar__sep">·</span>
|
|
76
|
-
<span>{{ lineCount }} 行</span>
|
|
77
|
-
<span class="code-statusbar__sep">·</span>
|
|
78
|
-
<span>行 {{ cursorLine }},列 {{ cursorCol }}</span>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<!-- 预览面板 -->
|
|
83
|
-
<div
|
|
84
|
-
v-show="activeTab === 'preview' || activeTab === 'split'"
|
|
85
|
-
class="preview-pane render-pane"
|
|
86
|
-
>
|
|
87
|
-
<div v-if="isSvg" class="svg-preview" v-html="sanitizedSvg" />
|
|
88
|
-
<iframe
|
|
89
|
-
v-else
|
|
90
|
-
:key="iframeKey"
|
|
91
|
-
ref="iframeRef"
|
|
92
|
-
sandbox="allow-scripts allow-modals allow-forms"
|
|
93
|
-
style="width:100%;height:100%;border:none;background:#fff;"
|
|
94
|
-
/>
|
|
95
|
-
</div>
|
|
96
|
-
</template>
|
|
97
|
-
|
|
98
|
-
</div>
|
|
99
|
-
</el-dialog>
|
|
100
|
-
</template>
|
|
101
|
-
|
|
102
|
-
<script setup>
|
|
103
|
-
import { ref, computed, watch, nextTick, onMounted, defineAsyncComponent } from 'vue'
|
|
104
|
-
import { RefreshCw, Copy, Check } from 'lucide-vue-next'
|
|
105
|
-
import DOMPurify from 'dompurify'
|
|
106
|
-
import { marked } from 'marked'
|
|
107
|
-
|
|
108
|
-
// 样式单独顶层加载
|
|
109
|
-
import('@vue/repl/style.css').catch(() => {})
|
|
110
|
-
import('@vue/repl/codemirror-editor').catch(() => {}) // 预加载 codemirror CSS
|
|
111
|
-
|
|
112
|
-
// ─────────────────────────────────────────────────────────────────
|
|
113
|
-
// 内网 Vue ESM 资源基础路径
|
|
114
|
-
// ⚠️ 将下面的路径替换为你们内网实际托管地址,例如:
|
|
115
|
-
// '/static/vue-assets' 或 'http://cdn.internal/vue'
|
|
116
|
-
// 需要托管的文件:执行项目根目录的 scripts/copy-vue-assets.sh 复制到该目录
|
|
117
|
-
// ─────────────────────────────────────────────────────────────────
|
|
118
|
-
const VUE_ASSETS_BASE = '/vue-repl-assets'
|
|
119
|
-
|
|
120
|
-
// JSX/TSX iframe 预览用的本地 React 运行时(同 ArtifactPanel.vue)
|
|
121
|
-
// 文件由 vite-plugin-static-copy 构建时自动复制,开发时需手动执行:
|
|
122
|
-
// mkdir -p public/react-runtime
|
|
123
|
-
// cp node_modules/react/umd/react.production.min.js public/react-runtime/react.min.js
|
|
124
|
-
// cp node_modules/react-dom/umd/react-dom.production.min.js public/react-runtime/react-dom.min.js
|
|
125
|
-
// cp node_modules/@babel/standalone/babel.min.js public/react-runtime/babel.min.js
|
|
126
|
-
const REACT_RUNTIME_BASE = '/react-runtime'
|
|
127
|
-
|
|
128
|
-
const INTERNAL_IMPORT_MAP = {
|
|
129
|
-
imports: {
|
|
130
|
-
'vue': `${VUE_ASSETS_BASE}/vue.esm-browser.js`,
|
|
131
|
-
'vue/server-renderer':`${VUE_ASSETS_BASE}/server-renderer.esm-browser.js`,
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// @vue/repl 按需异步加载(新版 ≥4.x 需要显式传 editor prop)
|
|
136
|
-
const Repl = defineAsyncComponent(() => import('@vue/repl').then(m => m.Repl))
|
|
137
|
-
const CodeMirror = defineAsyncComponent(() =>
|
|
138
|
-
import('@vue/repl/codemirror-editor').then(m => m.default)
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
const props = defineProps({
|
|
142
|
-
visible: { type: Boolean, default: false },
|
|
143
|
-
lang: { type: String, default: 'html' },
|
|
144
|
-
code: { type: String, default: '' },
|
|
145
|
-
})
|
|
146
|
-
const emit = defineEmits(['close'])
|
|
147
|
-
|
|
148
|
-
const show = ref(props.visible)
|
|
149
|
-
const activeTab = ref('split')
|
|
150
|
-
const copied = ref(false)
|
|
151
|
-
const iframeKey = ref(0)
|
|
152
|
-
const iframeRef = ref(null)
|
|
153
|
-
const codeContainerRef = ref(null)
|
|
154
|
-
const highlightedCode = ref('')
|
|
155
|
-
const replStore = ref(null) // <Repl> 需要 store 实例本身
|
|
156
|
-
|
|
157
|
-
const isDark = computed(() => document.documentElement.getAttribute('data-theme') !== 'light')
|
|
158
|
-
|
|
159
|
-
// 语言判断
|
|
160
|
-
const isVueSfc = computed(() => props.lang === 'vue')
|
|
161
|
-
const isSvg = computed(() => props.lang === 'svg')
|
|
162
|
-
const isHtml = computed(() => ['html', 'javascript', 'js', 'jsx', 'tsx', 'css'].includes(props.lang))
|
|
163
|
-
const isMarkdown = computed(() => ['markdown', 'md'].includes(props.lang))
|
|
164
|
-
|
|
165
|
-
const langLabel = computed(() => {
|
|
166
|
-
const map = {
|
|
167
|
-
vue: 'Vue SFC', html: 'HTML', javascript: 'JavaScript', js: 'JavaScript',
|
|
168
|
-
jsx: 'React JSX', tsx: 'React TSX', css: 'CSS', svg: 'SVG',
|
|
169
|
-
markdown: 'Markdown', md: 'Markdown',
|
|
170
|
-
}
|
|
171
|
-
return map[props.lang] || props.lang.toUpperCase()
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
// Vue SFC 时:tab 切换映射到 <Repl layout> prop
|
|
175
|
-
// 'split' → 'horizontal'(左代码右预览)
|
|
176
|
-
// 'code' → 'horizontal' + layoutReverse=false,但隐藏 preview 用 showOutput=false
|
|
177
|
-
// 'preview' → 只看右侧 PREVIEW 面板
|
|
178
|
-
// @vue/repl 没有 "仅代码" 模式,用 layout-reverse + 宽度比例控制
|
|
179
|
-
const replLayout = computed(() => {
|
|
180
|
-
if (activeTab.value === 'preview') return 'horizontal' // 切到 PREVIEW tab 由 <Repl> 内部 tab 控制
|
|
181
|
-
return 'horizontal' // split / code 都用水平分屏
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
// Vue SFC tab 初始值:进来默认分屏
|
|
185
|
-
watch(isVueSfc, (v) => { if (v) activeTab.value = 'split' }, { immediate: true })
|
|
186
|
-
|
|
187
|
-
const availableTabs = computed(() => {
|
|
188
|
-
if (isVueSfc.value) {
|
|
189
|
-
return [
|
|
190
|
-
{ label: '分屏', value: 'split' },
|
|
191
|
-
{ label: '代码', value: 'code' },
|
|
192
|
-
{ label: '预览', value: 'preview' },
|
|
193
|
-
]
|
|
194
|
-
}
|
|
195
|
-
const tabs = [{ label: '代码', value: 'code' }]
|
|
196
|
-
if (isHtml.value || isSvg.value || isMarkdown.value) {
|
|
197
|
-
tabs.push({ label: '预览', value: 'preview' })
|
|
198
|
-
tabs.unshift({ label: '分屏', value: 'split' })
|
|
199
|
-
}
|
|
200
|
-
return tabs
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
const sanitizedSvg = computed(() =>
|
|
204
|
-
isSvg.value ? DOMPurify.sanitize(props.code, { USE_PROFILES: { svg: true } }) : ''
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
watch(() => props.visible, v => { show.value = v })
|
|
208
|
-
watch(show, v => { if (!v) emit('close') })
|
|
209
|
-
|
|
210
|
-
// 代码高亮
|
|
211
|
-
watch(() => [props.code, props.lang], async () => {
|
|
212
|
-
try {
|
|
213
|
-
const { createHighlighter } = await import('shiki')
|
|
214
|
-
const hl = await createHighlighter({
|
|
215
|
-
themes: ['one-dark-pro', 'github-light'],
|
|
216
|
-
langs: [props.lang || 'text'],
|
|
217
|
-
})
|
|
218
|
-
const theme = isDark.value ? 'one-dark-pro' : 'github-light'
|
|
219
|
-
const loaded = hl.getLoadedLanguages()
|
|
220
|
-
highlightedCode.value = hl.codeToHtml(props.code, {
|
|
221
|
-
lang: loaded.includes(props.lang) ? props.lang : 'text',
|
|
222
|
-
theme,
|
|
223
|
-
})
|
|
224
|
-
} catch {
|
|
225
|
-
highlightedCode.value = `<pre style="margin:0;padding:16px;font-size:13px;">${props.code.replace(/</g,'<')}</pre>`
|
|
226
|
-
}
|
|
227
|
-
}, { immediate: true })
|
|
228
|
-
|
|
229
|
-
// Vue SFC repl
|
|
230
|
-
// <Repl> 内部会自己调用 store.init(),这里只负责创建 store 并写入代码
|
|
231
|
-
// store 实例只创建一次,代码变化时直接修改 files[mainFile].code
|
|
232
|
-
let _replStore = null
|
|
233
|
-
watch(() => [props.code, props.lang], async () => {
|
|
234
|
-
if (!isVueSfc.value) return
|
|
235
|
-
try {
|
|
236
|
-
const { useStore } = await import('@vue/repl')
|
|
237
|
-
if (!_replStore) {
|
|
238
|
-
_replStore = useStore({ builtinImportMap: ref(INTERNAL_IMPORT_MAP) })
|
|
239
|
-
const mainFile = _replStore.mainFile?.value ?? _replStore.mainFile ?? 'src/App.vue'
|
|
240
|
-
if (_replStore.files?.[mainFile]) {
|
|
241
|
-
_replStore.files[mainFile].code = props.code
|
|
242
|
-
}
|
|
243
|
-
replStore.value = _replStore
|
|
244
|
-
} else {
|
|
245
|
-
const mainFile = _replStore.mainFile?.value ?? _replStore.mainFile ?? 'src/App.vue'
|
|
246
|
-
if (_replStore.files?.[mainFile]) {
|
|
247
|
-
_replStore.files[mainFile].code = props.code
|
|
248
|
-
} else {
|
|
249
|
-
await _replStore.setFiles({ [mainFile]: props.code }, mainFile)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} catch (e) {
|
|
253
|
-
console.warn('[CodePreview] @vue/repl 加载失败', e)
|
|
254
|
-
replStore.value = null
|
|
255
|
-
}
|
|
256
|
-
}, { immediate: true })
|
|
257
|
-
|
|
258
|
-
// tab 切换时同步 <Repl> 内部的 showOutput(控制是否显示 PREVIEW 面板)
|
|
259
|
-
// 'preview' → showOutput=true;'code' → showOutput=false;'split' 保持默认
|
|
260
|
-
watch(activeTab, (tab) => {
|
|
261
|
-
if (!_replStore) return
|
|
262
|
-
if (tab === 'preview') _replStore.showOutput = true
|
|
263
|
-
else if (tab === 'code') _replStore.showOutput = false
|
|
264
|
-
// split 时保持当前状态
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
// iframe 预览
|
|
268
|
-
watch([() => props.code, () => props.lang, show], ([code, lang, visible]) => {
|
|
269
|
-
if (!visible || isVueSfc.value || isSvg.value) return
|
|
270
|
-
nextTick(() => renderIframe(code, lang))
|
|
271
|
-
}, { immediate: false })
|
|
272
|
-
|
|
273
|
-
function buildIframeDoc(code, lang) {
|
|
274
|
-
if (lang === 'html') return code
|
|
275
|
-
if (['javascript', 'js'].includes(lang)) {
|
|
276
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:sans-serif;padding:16px;background:#1e1e2e;color:#cdd6f4;}</style></head><body><script type="module">\n${code}\n<\/script></body></html>`
|
|
277
|
-
}
|
|
278
|
-
if (lang === 'jsx' || lang === 'tsx') {
|
|
279
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
280
|
-
<script crossorigin src="${REACT_RUNTIME_BASE}/react.min.js"><\/script>
|
|
281
|
-
<script crossorigin src="${REACT_RUNTIME_BASE}/react-dom.min.js"><\/script>
|
|
282
|
-
<script src="${REACT_RUNTIME_BASE}/babel.min.js"><\/script>
|
|
283
|
-
<style>body{font-family:sans-serif;padding:16px;}</style>
|
|
284
|
-
</head><body><div id="root"></div>
|
|
285
|
-
<script type="text/babel">\n${code}\nconst _el=document.getElementById('root');if(typeof App!=='undefined')ReactDOM.createRoot(_el).render(React.createElement(App))\n<\/script></body></html>`
|
|
286
|
-
}
|
|
287
|
-
if (lang === 'css') {
|
|
288
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>${code}</style></head><body><p>CSS 已应用</p><div class="demo">示例元素</div></body></html>`
|
|
289
|
-
}
|
|
290
|
-
if (lang === 'markdown' || lang === 'md') {
|
|
291
|
-
// 用项目内的 marked 渲染,不依赖外部 CDN
|
|
292
|
-
const html = marked.parse(code, { gfm: true, breaks: true })
|
|
293
|
-
const sanitized = DOMPurify.sanitize(html)
|
|
294
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
295
|
-
body { font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; padding: 28px 36px; line-height: 1.75; color: #1a1a1a; font-size: 14px; max-width: 860px; margin: 0 auto; }
|
|
296
|
-
h1 { font-size: 1.5em; font-weight: 700; border-bottom: 1px solid #e5e7eb; padding-bottom: 8px; margin: 0 0 16px; }
|
|
297
|
-
h2 { font-size: 1.25em; font-weight: 700; margin: 24px 0 8px; }
|
|
298
|
-
h3 { font-size: 1.1em; font-weight: 600; margin: 18px 0 6px; }
|
|
299
|
-
h4,h5,h6 { font-size: 1em; font-weight: 600; margin: 14px 0 4px; }
|
|
300
|
-
p { margin: 0 0 12px; }
|
|
301
|
-
code { font-family: 'Cascadia Code', Consolas, monospace; font-size: 12px; background: #f3f4f6; padding: 1px 5px; border-radius: 3px; }
|
|
302
|
-
pre { background: #1e1e2e; border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 12px 0; }
|
|
303
|
-
pre code { background: none; color: #cdd6f4; font-size: 13px; padding: 0; }
|
|
304
|
-
blockquote { border-left: 3px solid #3b82f6; margin: 12px 0; padding: 6px 14px; color: #555; background: #f8fafc; border-radius: 0 6px 6px 0; }
|
|
305
|
-
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
|
306
|
-
th, td { border: 1px solid #e5e7eb; padding: 8px 12px; text-align: left; }
|
|
307
|
-
th { background: #f9fafb; font-weight: 600; }
|
|
308
|
-
hr { border: none; border-top: 1px solid #e5e7eb; margin: 18px 0; }
|
|
309
|
-
a { color: #3b82f6; text-decoration: underline; }
|
|
310
|
-
img { max-width: 100%; border-radius: 6px; }
|
|
311
|
-
ul, ol { padding-left: 1.5em; margin: 0 0 12px; }
|
|
312
|
-
li { margin: 4px 0; }
|
|
313
|
-
input[type=checkbox] { margin-right: 6px; }
|
|
314
|
-
</style></head><body>${sanitized}</body></html>`
|
|
315
|
-
}
|
|
316
|
-
return `<!DOCTYPE html><html><body><pre>${code.replace(/</g,'<')}</pre></body></html>`
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function renderIframe(code, lang) {
|
|
320
|
-
if (!iframeRef.value) return
|
|
321
|
-
const doc = buildIframeDoc(code, lang)
|
|
322
|
-
const blob = new Blob([doc], { type: 'text/html' })
|
|
323
|
-
const url = URL.createObjectURL(blob)
|
|
324
|
-
iframeRef.value.src = url
|
|
325
|
-
setTimeout(() => URL.revokeObjectURL(url), 5000)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function refreshIframe() {
|
|
329
|
-
if (isVueSfc.value || isSvg.value) return
|
|
330
|
-
iframeKey.value++
|
|
331
|
-
nextTick(() => renderIframe(props.code, props.lang))
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function copyCode() {
|
|
335
|
-
navigator.clipboard?.writeText(props.code).catch(() => {})
|
|
336
|
-
copied.value = true
|
|
337
|
-
setTimeout(() => { copied.value = false }, 2000)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ── 行列号追踪 ────────────────────────────────────────────────────
|
|
341
|
-
const cursorLine = ref(1)
|
|
342
|
-
const cursorCol = ref(1)
|
|
343
|
-
const lineCount = computed(() => props.code ? props.code.split('\n').length : 0)
|
|
344
|
-
|
|
345
|
-
function onCodeClick(e) {
|
|
346
|
-
const lineEl = e.target.closest('.line')
|
|
347
|
-
if (lineEl) {
|
|
348
|
-
const pre = lineEl.closest('pre')
|
|
349
|
-
if (pre) {
|
|
350
|
-
const lines = [...pre.querySelectorAll('.line')]
|
|
351
|
-
const idx = lines.indexOf(lineEl)
|
|
352
|
-
if (idx !== -1) cursorLine.value = idx + 1
|
|
353
|
-
}
|
|
354
|
-
// 估算列号:点击 X - 行号区宽度(行号 3em≈33px + 分隔线 + margin ≈ 58px)
|
|
355
|
-
const rect = lineEl.getBoundingClientRect()
|
|
356
|
-
const charW = 7.8 // 13px monospace 约 7.8px/字符
|
|
357
|
-
cursorCol.value = Math.max(1, Math.round((e.clientX - rect.left - 58) / charW) + 1)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
onMounted(() => {
|
|
362
|
-
if (props.visible && !isVueSfc.value && !isSvg.value) {
|
|
363
|
-
nextTick(() => renderIframe(props.code, props.lang))
|
|
364
|
-
}
|
|
365
|
-
})
|
|
366
|
-
</script>
|
|
367
|
-
|
|
368
|
-
<style scoped>
|
|
369
|
-
.preview-toolbar {
|
|
370
|
-
display: flex;
|
|
371
|
-
align-items: center;
|
|
372
|
-
justify-content: space-between;
|
|
373
|
-
padding: 8px 12px;
|
|
374
|
-
border-bottom: 1px solid var(--border-subtle);
|
|
375
|
-
background: var(--bg-secondary);
|
|
376
|
-
border-radius: 8px 8px 0 0;
|
|
377
|
-
}
|
|
378
|
-
.preview-tabs { display: flex; gap: 2px; }
|
|
379
|
-
.preview-tab {
|
|
380
|
-
padding: 5px 14px;
|
|
381
|
-
font-size: 12px;
|
|
382
|
-
font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
|
383
|
-
border: none;
|
|
384
|
-
background: transparent;
|
|
385
|
-
border-radius: 6px;
|
|
386
|
-
cursor: pointer;
|
|
387
|
-
color: var(--text-muted);
|
|
388
|
-
transition: all 0.15s;
|
|
389
|
-
}
|
|
390
|
-
.preview-tab:hover { background: var(--border-subtle); color: var(--text-secondary); }
|
|
391
|
-
.preview-tab.active { background: var(--brand-500); color: white; }
|
|
392
|
-
|
|
393
|
-
.preview-body {
|
|
394
|
-
display: flex;
|
|
395
|
-
height: 520px;
|
|
396
|
-
border-radius: 0 0 8px 8px;
|
|
397
|
-
overflow: hidden;
|
|
398
|
-
border: 1px solid var(--border-subtle);
|
|
399
|
-
}
|
|
400
|
-
/* preview-pane 基础:flex child,不要自带 overflow */
|
|
401
|
-
.preview-pane { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
|
402
|
-
|
|
403
|
-
/* code-pane:flex 列,代码区滚动,状态栏固定底部 */
|
|
404
|
-
.code-pane {
|
|
405
|
-
border-right: 1px solid var(--border-subtle);
|
|
406
|
-
background: var(--bg-tertiary);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/* 代码滚动区:撑满剩余高度,独立双向滚动 */
|
|
410
|
-
.shiki-scroll {
|
|
411
|
-
flex: 1;
|
|
412
|
-
overflow: auto;
|
|
413
|
-
min-height: 0; /* 关键:消除 flex 子元素默认 min-height: auto */
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/* ── Shiki 输出:行号 + 合适内边距 ── */
|
|
417
|
-
.shiki-container {
|
|
418
|
-
display: inline-block; /* 让宽度跟随内容,横向滚动正确 */
|
|
419
|
-
min-width: 100%;
|
|
420
|
-
}
|
|
421
|
-
:deep(.shiki-container .shiki) {
|
|
422
|
-
margin: 0 !important;
|
|
423
|
-
border-radius: 0 !important;
|
|
424
|
-
font-size: 13px !important;
|
|
425
|
-
line-height: 1.6 !important;
|
|
426
|
-
padding: 0 !important;
|
|
427
|
-
tab-size: 2;
|
|
428
|
-
background: transparent !important;
|
|
429
|
-
}
|
|
430
|
-
:deep(.shiki-container pre) {
|
|
431
|
-
margin: 0 !important;
|
|
432
|
-
padding: 0 !important;
|
|
433
|
-
border-radius: 0 !important;
|
|
434
|
-
counter-reset: line;
|
|
435
|
-
background: transparent !important;
|
|
436
|
-
}
|
|
437
|
-
/* code 容器:font-size: 0 消除匿名行盒间距 */
|
|
438
|
-
:deep(.shiki-container .shiki code) {
|
|
439
|
-
display: block;
|
|
440
|
-
padding: 16px 24px 16px 0;
|
|
441
|
-
font-size: 0;
|
|
442
|
-
font-family: 'Cascadia Code', 'Consolas', monospace !important;
|
|
443
|
-
}
|
|
444
|
-
/* 每行 */
|
|
445
|
-
:deep(.shiki-container .shiki code .line) {
|
|
446
|
-
display: block;
|
|
447
|
-
white-space: pre;
|
|
448
|
-
line-height: 22px;
|
|
449
|
-
font-size: 13px;
|
|
450
|
-
}
|
|
451
|
-
/* 行号伪元素 */
|
|
452
|
-
:deep(.shiki-container .shiki code .line::before) {
|
|
453
|
-
counter-increment: line;
|
|
454
|
-
content: counter(line);
|
|
455
|
-
display: inline-block;
|
|
456
|
-
width: 3em;
|
|
457
|
-
padding-right: 0.9em;
|
|
458
|
-
margin-right: 1em;
|
|
459
|
-
text-align: right;
|
|
460
|
-
color: var(--text-faint, #334155);
|
|
461
|
-
border-right: 1px solid var(--border-subtle);
|
|
462
|
-
user-select: none;
|
|
463
|
-
font-size: 11px;
|
|
464
|
-
line-height: 22px;
|
|
465
|
-
vertical-align: top;
|
|
466
|
-
opacity: 0.55;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/* 行列号状态栏:固定在 flex 列底部,不随代码滚动 */
|
|
470
|
-
.code-statusbar {
|
|
471
|
-
flex-shrink: 0;
|
|
472
|
-
display: flex;
|
|
473
|
-
align-items: center;
|
|
474
|
-
gap: 10px;
|
|
475
|
-
padding: 4px 16px;
|
|
476
|
-
background: var(--bg-secondary);
|
|
477
|
-
border-top: 1px solid var(--border-subtle);
|
|
478
|
-
font-size: 11px;
|
|
479
|
-
font-family: 'Cascadia Code', 'Consolas', monospace;
|
|
480
|
-
color: var(--text-muted);
|
|
481
|
-
user-select: none;
|
|
482
|
-
}
|
|
483
|
-
.code-statusbar__lang { color: var(--brand-400); }
|
|
484
|
-
.code-statusbar__sep { opacity: 0.35; }
|
|
485
|
-
|
|
486
|
-
.render-pane { background: #ffffff; }
|
|
487
|
-
.svg-preview {
|
|
488
|
-
display: flex;
|
|
489
|
-
align-items: center;
|
|
490
|
-
justify-content: center;
|
|
491
|
-
padding: 24px;
|
|
492
|
-
flex: 1;
|
|
493
|
-
overflow: auto;
|
|
494
|
-
}
|
|
495
|
-
:deep(.svg-preview svg) { max-width: 100%; max-height: 100%; }
|
|
496
|
-
|
|
497
|
-
/* Vue SFC:<Repl> 撑满整个内容区,覆盖 preview-body 的全部空间 */
|
|
498
|
-
.vue-repl-full {
|
|
499
|
-
width: 100%;
|
|
500
|
-
height: 100%;
|
|
501
|
-
border: none;
|
|
502
|
-
}
|
|
503
|
-
/* 去掉 @vue/repl 自带的上边框和圆角,与我们的工具栏无缝衔接 */
|
|
504
|
-
:deep(.vue-repl) {
|
|
505
|
-
border: none !important;
|
|
506
|
-
border-radius: 0 !important;
|
|
507
|
-
height: 100% !important;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
.vue-repl-loading {
|
|
511
|
-
display: flex;
|
|
512
|
-
align-items: center;
|
|
513
|
-
justify-content: center;
|
|
514
|
-
width: 100%;
|
|
515
|
-
height: 100%;
|
|
516
|
-
color: var(--text-muted);
|
|
517
|
-
font-size: 13px;
|
|
518
|
-
font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/* Dialog 背景对齐 */
|
|
522
|
-
:deep(.el-dialog__body) { padding: 0 !important; }
|
|
523
|
-
</style>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* features/markdown
|
|
3
|
-
*
|
|
4
|
-
* core — Shiki 高亮、marked 渲染、KaTeX 数学、Mermaid 图表
|
|
5
|
-
* headless 和 createAIChat 都需要 markdown 渲染能力,统一从这里导出
|
|
6
|
-
*/
|
|
7
|
-
export {
|
|
8
|
-
renderMarkdown,
|
|
9
|
-
createStreamingParser,
|
|
10
|
-
postRender,
|
|
11
|
-
handleCodeAction,
|
|
12
|
-
shikiReady,
|
|
13
|
-
} from './useMarkdown.js'
|