feat(节点交互): 实现联动拖拽功能并优化节点聚焦体验
- 新增联动拖拽功能,支持配置开关,拖动父节点时自动移动子节点 - 优化节点聚焦交互,聚焦时放大节点并居中视图 - 重构后代节点查找逻辑,改用迭代方式提高性能 - 在顶部导航栏添加联动拖拽开关按钮 - 为i18n添加相关翻译字段 - 调整markdown渲染样式增加内边距
This commit is contained in:
@@ -91,6 +91,7 @@ const {
|
||||
fitView,
|
||||
resetLayout,
|
||||
centerRoot,
|
||||
handleNodeDrag,
|
||||
startNewSession,
|
||||
executeReset,
|
||||
generateSummary,
|
||||
@@ -139,6 +140,7 @@ const fitToView = () => {
|
||||
:class="{ 'space-pressed': isSpacePressed }"
|
||||
:pan-on-drag="panOnDrag"
|
||||
:selection-key-code="'Shift'"
|
||||
@node-drag="handleNodeDrag"
|
||||
>
|
||||
<Background
|
||||
:variant="config.backgroundVariant"
|
||||
@@ -160,6 +162,7 @@ const fitToView = () => {
|
||||
:selected="selected"
|
||||
:t="t"
|
||||
:config="config"
|
||||
:fitView="fitView"
|
||||
:activeNodeId="activeNodeId"
|
||||
:activePath="activePath"
|
||||
:flowNodes="flowNodes"
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
*/
|
||||
|
||||
// 基础依赖
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// 背景样式枚举(用于切换 Lines/Dots)
|
||||
import { BackgroundVariant } from '@vue-flow/background'
|
||||
|
||||
// 图标:所有按钮与状态展示
|
||||
import {
|
||||
ArrowLeftRight,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Download,
|
||||
@@ -58,6 +59,74 @@ const emit = defineEmits<{
|
||||
*/
|
||||
const isToolsExpanded = ref(false)
|
||||
|
||||
const isEdgeTypeMenuOpen = ref(false)
|
||||
const isBackgroundMenuOpen = ref(false)
|
||||
|
||||
const edgeTypeOptions = [
|
||||
{ value: 'default', labelKey: 'nav.edgeTypes.default' },
|
||||
{ value: 'straight', labelKey: 'nav.edgeTypes.straight' },
|
||||
{ value: 'step', labelKey: 'nav.edgeTypes.step' },
|
||||
{ value: 'smoothstep', labelKey: 'nav.edgeTypes.smoothstep' },
|
||||
{ value: 'simplebezier', labelKey: 'nav.edgeTypes.simplebezier' }
|
||||
]
|
||||
|
||||
const backgroundOptions = [
|
||||
{ value: BackgroundVariant.Lines, labelKey: 'nav.lines' },
|
||||
{ value: BackgroundVariant.Dots, labelKey: 'nav.dots' }
|
||||
]
|
||||
|
||||
const currentEdgeTypeLabel = computed(() => {
|
||||
const option = edgeTypeOptions.find(o => o.value === props.config.edgeType)
|
||||
return option ? props.t(option.labelKey) : props.config.edgeType
|
||||
})
|
||||
|
||||
const currentBackgroundLabel = computed(() => {
|
||||
const option = backgroundOptions.find(o => o.value === props.config.backgroundVariant)
|
||||
return option ? props.t(option.labelKey) : String(props.config.backgroundVariant)
|
||||
})
|
||||
|
||||
const closeMenus = () => {
|
||||
isEdgeTypeMenuOpen.value = false
|
||||
isBackgroundMenuOpen.value = false
|
||||
}
|
||||
|
||||
const toggleEdgeTypeMenu = () => {
|
||||
isEdgeTypeMenuOpen.value = !isEdgeTypeMenuOpen.value
|
||||
if (isEdgeTypeMenuOpen.value) isBackgroundMenuOpen.value = false
|
||||
}
|
||||
|
||||
const toggleBackgroundMenu = () => {
|
||||
isBackgroundMenuOpen.value = !isBackgroundMenuOpen.value
|
||||
if (isBackgroundMenuOpen.value) isEdgeTypeMenuOpen.value = false
|
||||
}
|
||||
|
||||
const setEdgeType = (value: string) => {
|
||||
props.config.edgeType = value
|
||||
closeMenus()
|
||||
}
|
||||
|
||||
const setBackgroundVariant = (value: any) => {
|
||||
props.config.backgroundVariant = value
|
||||
closeMenus()
|
||||
}
|
||||
|
||||
const handleDocumentPointerDown = (e: Event) => {
|
||||
const target = e.target as HTMLElement | null
|
||||
if (!target) return
|
||||
|
||||
if (target.closest('[data-edge-type-menu="true"]')) return
|
||||
if (target.closest('[data-background-menu="true"]')) return
|
||||
closeMenus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
||||
})
|
||||
|
||||
/**
|
||||
* 执行某个工具动作后自动收起移动端面板
|
||||
*/
|
||||
@@ -95,6 +164,18 @@ const callAndClose = (fn: () => void) => {
|
||||
|
||||
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
||||
|
||||
<button
|
||||
@click="props.config.hierarchicalDragging = !props.config.hierarchicalDragging"
|
||||
class="toolbar-btn border-slate-100 flex-shrink-0"
|
||||
:class="props.config.hierarchicalDragging ? 'text-orange-500 bg-orange-50 border-orange-100' : 'text-slate-400 hover:text-slate-600'"
|
||||
:title="props.t('nav.hierarchicalDragging')"
|
||||
>
|
||||
<ArrowLeftRight class="w-3.5 h-3.5 md:w-4 h-4" />
|
||||
<span>{{ props.t('nav.hierarchicalDragging') }}</span>
|
||||
</button>
|
||||
|
||||
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
||||
|
||||
<button @click="props.onStartNewSession" class="toolbar-btn text-red-500 hover:bg-red-50 border-red-100 flex-shrink-0" :title="props.t('nav.reset')">
|
||||
<Trash2 class="w-3.5 h-3.5 md:w-4 h-4" />
|
||||
<span>{{ props.t('nav.reset') }}</span>
|
||||
@@ -108,18 +189,49 @@ const callAndClose = (fn: () => void) => {
|
||||
<span class="text-[9px] md:text-[10px] font-bold text-slate-500 uppercase">{{ props.t('nav.edge') }}</span>
|
||||
</div>
|
||||
|
||||
<select v-model="props.config.edgeType" class="toolbar-select flex-shrink-0">
|
||||
<option value="default">{{ props.t('nav.edgeTypes.default') }}</option>
|
||||
<option value="straight">{{ props.t('nav.edgeTypes.straight') }}</option>
|
||||
<option value="step">{{ props.t('nav.edgeTypes.step') }}</option>
|
||||
<option value="smoothstep">{{ props.t('nav.edgeTypes.smoothstep') }}</option>
|
||||
<option value="simplebezier">{{ props.t('nav.edgeTypes.simplebezier') }}</option>
|
||||
</select>
|
||||
<div data-edge-type-menu="true" class="relative flex-shrink-0">
|
||||
<button type="button" class="toolbar-select flex items-center gap-2" @click="toggleEdgeTypeMenu">
|
||||
<span>{{ currentEdgeTypeLabel }}</span>
|
||||
<ChevronDown class="w-3 h-3 opacity-60" />
|
||||
</button>
|
||||
<div
|
||||
v-if="isEdgeTypeMenuOpen"
|
||||
class="absolute top-full left-0 mt-2 min-w-[180px] bg-white border border-slate-200 rounded-lg shadow-xl p-1 z-50"
|
||||
>
|
||||
<button
|
||||
v-for="opt in edgeTypeOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded-md text-[10px] font-black tracking-widest uppercase transition-colors"
|
||||
:class="opt.value === props.config.edgeType ? 'bg-orange-50 text-orange-600' : 'text-slate-600 hover:bg-slate-50'"
|
||||
@click="setEdgeType(opt.value)"
|
||||
>
|
||||
{{ props.t(opt.labelKey) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select v-model="props.config.backgroundVariant" class="toolbar-select flex-shrink-0">
|
||||
<option :value="BackgroundVariant.Lines">{{ props.t('nav.lines') }}</option>
|
||||
<option :value="BackgroundVariant.Dots">{{ props.t('nav.dots') }}</option>
|
||||
</select>
|
||||
<div data-background-menu="true" class="relative flex-shrink-0">
|
||||
<button type="button" class="toolbar-select flex items-center gap-2" @click="toggleBackgroundMenu">
|
||||
<span>{{ currentBackgroundLabel }}</span>
|
||||
<ChevronDown class="w-3 h-3 opacity-60" />
|
||||
</button>
|
||||
<div
|
||||
v-if="isBackgroundMenuOpen"
|
||||
class="absolute top-full left-0 mt-2 min-w-[140px] bg-white border border-slate-200 rounded-lg shadow-xl p-1 z-50"
|
||||
>
|
||||
<button
|
||||
v-for="opt in backgroundOptions"
|
||||
:key="String(opt.value)"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded-md text-[10px] font-black tracking-widest uppercase transition-colors"
|
||||
:class="opt.value === props.config.backgroundVariant ? 'bg-orange-50 text-orange-600' : 'text-slate-600 hover:bg-slate-50'"
|
||||
@click="setBackgroundVariant(opt.value)"
|
||||
>
|
||||
{{ props.t(opt.labelKey) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-[1px] bg-slate-100 mx-1 flex-shrink-0"></div>
|
||||
|
||||
@@ -212,18 +324,49 @@ const callAndClose = (fn: () => void) => {
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase">{{ props.t('nav.edge') }}</span>
|
||||
</div>
|
||||
|
||||
<select v-model="props.config.edgeType" class="toolbar-select">
|
||||
<option value="default">{{ props.t('nav.edgeTypes.default') }}</option>
|
||||
<option value="straight">{{ props.t('nav.edgeTypes.straight') }}</option>
|
||||
<option value="step">{{ props.t('nav.edgeTypes.step') }}</option>
|
||||
<option value="smoothstep">{{ props.t('nav.edgeTypes.smoothstep') }}</option>
|
||||
<option value="simplebezier">{{ props.t('nav.edgeTypes.simplebezier') }}</option>
|
||||
</select>
|
||||
<div data-edge-type-menu="true" class="relative">
|
||||
<button type="button" class="toolbar-select flex items-center gap-2" @click="toggleEdgeTypeMenu">
|
||||
<span>{{ currentEdgeTypeLabel }}</span>
|
||||
<ChevronDown class="w-3 h-3 opacity-60" />
|
||||
</button>
|
||||
<div
|
||||
v-if="isEdgeTypeMenuOpen"
|
||||
class="absolute top-full left-0 mt-2 min-w-[180px] bg-white border border-slate-200 rounded-lg shadow-xl p-1 z-50"
|
||||
>
|
||||
<button
|
||||
v-for="opt in edgeTypeOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded-md text-[10px] font-black tracking-widest uppercase transition-colors"
|
||||
:class="opt.value === props.config.edgeType ? 'bg-orange-50 text-orange-600' : 'text-slate-600 hover:bg-slate-50'"
|
||||
@click="callAndClose(() => setEdgeType(opt.value))"
|
||||
>
|
||||
{{ props.t(opt.labelKey) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select v-model="props.config.backgroundVariant" class="toolbar-select">
|
||||
<option :value="BackgroundVariant.Lines">{{ props.t('nav.lines') }}</option>
|
||||
<option :value="BackgroundVariant.Dots">{{ props.t('nav.dots') }}</option>
|
||||
</select>
|
||||
<div data-background-menu="true" class="relative">
|
||||
<button type="button" class="toolbar-select flex items-center gap-2" @click="toggleBackgroundMenu">
|
||||
<span>{{ currentBackgroundLabel }}</span>
|
||||
<ChevronDown class="w-3 h-3 opacity-60" />
|
||||
</button>
|
||||
<div
|
||||
v-if="isBackgroundMenuOpen"
|
||||
class="absolute top-full left-0 mt-2 min-w-[140px] bg-white border border-slate-200 rounded-lg shadow-xl p-1 z-50"
|
||||
>
|
||||
<button
|
||||
v-for="opt in backgroundOptions"
|
||||
:key="String(opt.value)"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded-md text-[10px] font-black tracking-widest uppercase transition-colors"
|
||||
:class="opt.value === props.config.backgroundVariant ? 'bg-orange-50 text-orange-600' : 'text-slate-600 hover:bg-slate-50'"
|
||||
@click="callAndClose(() => setBackgroundVariant(opt.value))"
|
||||
>
|
||||
{{ props.t(opt.labelKey) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="props.config.showMiniMap = !props.config.showMiniMap"
|
||||
|
||||
@@ -41,6 +41,7 @@ const props = defineProps<{
|
||||
selected: boolean
|
||||
t: any
|
||||
config: any
|
||||
fitView: (options?: any) => void
|
||||
activeNodeId: string | null
|
||||
activePath: { nodeIds: Set<string>; edgeIds: Set<string> }
|
||||
flowNodes: any[]
|
||||
@@ -72,6 +73,22 @@ const isFocused = ref(false)
|
||||
* 从 flowNodes 中找到节点当前位置,用于扩展时定位新节点生成的参考坐标
|
||||
*/
|
||||
const getNodePosition = (id: string) => props.flowNodes.find(n => n.id === id)?.position
|
||||
|
||||
/**
|
||||
* 输入框聚焦处理:放大节点并居中视图
|
||||
*/
|
||||
const handleFocus = () => {
|
||||
isFocused.value = true
|
||||
// 聚焦时将视图中心对准该节点,并给予适当的 padding 确保放大后完整可见
|
||||
props.fitView({ nodes: [props.id], padding: 1.5, duration: 600 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框失去焦点处理
|
||||
*/
|
||||
const handleBlur = () => {
|
||||
isFocused.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -79,13 +96,14 @@ const getNodePosition = (id: string) => props.flowNodes.find(n => n.id === id)?.
|
||||
class="window-node group transition-all duration-500"
|
||||
:class="{
|
||||
'opacity-40 grayscale-[0.4] blur-[0.5px] scale-[0.98] pointer-events-none': props.activeNodeId && !props.activePath.nodeIds.has(props.id),
|
||||
'opacity-100 grayscale-0 blur-0 scale-105 z-50 ring-2 ring-offset-4': props.activePath.nodeIds.has(props.id),
|
||||
'opacity-100 grayscale-0 blur-0 scale-105 z-50 ring-2 ring-offset-4': props.activePath.nodeIds.has(props.id) && !isFocused,
|
||||
'opacity-100 grayscale-0 blur-0 scale-110 z-[100] shadow-2xl ring-4 ring-offset-8': isFocused,
|
||||
'!w-[450px]': props.data.isDetailExpanded
|
||||
}"
|
||||
:style="{
|
||||
borderColor: props.activePath.nodeIds.has(props.id) ? props.config.edgeColor : props.config.edgeColor + '40',
|
||||
boxShadow: props.activeNodeId === props.id ? `0 20px 50px -12px ${props.config.edgeColor}40` : '',
|
||||
'--tw-ring-color': props.selected ? props.config.edgeColor + '40' : 'transparent'
|
||||
borderColor: isFocused || props.activePath.nodeIds.has(props.id) ? props.config.edgeColor : props.config.edgeColor + '40',
|
||||
boxShadow: isFocused ? `0 25px 50px -12px ${props.config.edgeColor}60` : (props.activeNodeId === props.id ? `0 20px 50px -12px ${props.config.edgeColor}40` : ''),
|
||||
'--tw-ring-color': isFocused || props.selected ? props.config.edgeColor + '40' : 'transparent'
|
||||
}"
|
||||
>
|
||||
<Handle type="target" :position="Position.Left" class="!bg-transparent !border-none" />
|
||||
@@ -226,8 +244,8 @@ const getNodePosition = (id: string) => props.flowNodes.find(n => n.id === id)?.
|
||||
<Terminal v-else class="w-3 h-3" :style="{ color: props.config.edgeColor }" />
|
||||
<input
|
||||
v-model="props.data.followUp"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keyup.enter="props.expandIdea({ id: props.id, data: props.data, position: getNodePosition(props.id) }, props.data.followUp)"
|
||||
:placeholder="props.t('node.followUp')"
|
||||
class="bg-transparent border-none outline-none text-[10px] font-bold text-slate-700 flex-grow placeholder:text-slate-400"
|
||||
|
||||
@@ -107,6 +107,7 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
|
||||
removeEdges,
|
||||
fitView,
|
||||
onNodeDragStart,
|
||||
onNodeDrag,
|
||||
onNodeDragStop
|
||||
} = useVueFlow()
|
||||
|
||||
@@ -122,6 +123,7 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
|
||||
const summaryContent = ref('')
|
||||
|
||||
const draggingNodeId = ref<string | null>(null)
|
||||
const dragLastPositionByNodeId = new Map<string, { x: number; y: number }>()
|
||||
|
||||
/**
|
||||
* 交互:按住 Space 启用“抓手拖拽画布”
|
||||
@@ -149,10 +151,79 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
|
||||
*/
|
||||
onNodeDragStart(e => {
|
||||
draggingNodeId.value = e.node.id
|
||||
dragLastPositionByNodeId.set(e.node.id, {
|
||||
x: e.node.position.x,
|
||||
y: e.node.position.y
|
||||
})
|
||||
})
|
||||
|
||||
onNodeDragStop(() => {
|
||||
/**
|
||||
* 处理节点拖拽事件 (联动移动子节点)
|
||||
*/
|
||||
const handleNodeDrag = (payload: any) => {
|
||||
if (!config.hierarchicalDragging) return
|
||||
|
||||
const node = payload?.node ?? payload
|
||||
if (!node?.id || !node?.position) return
|
||||
|
||||
const lastPosition = dragLastPositionByNodeId.get(node.id)
|
||||
if (!lastPosition) {
|
||||
const fallbackDelta = payload?.delta
|
||||
dragLastPositionByNodeId.set(node.id, { x: node.position.x, y: node.position.y })
|
||||
if (fallbackDelta && typeof fallbackDelta.x === 'number' && typeof fallbackDelta.y === 'number') {
|
||||
const descendantIds = getDescendantIds(node.id)
|
||||
if (descendantIds.size === 0) return
|
||||
|
||||
const selectedNodeIds = new Set(flowNodes.value.filter(n => n.selected).map(n => n.id))
|
||||
descendantIds.forEach(id => {
|
||||
if (!selectedNodeIds.has(id)) {
|
||||
const targetNode = flowNodes.value.find(n => n.id === id)
|
||||
if (targetNode) {
|
||||
targetNode.position = {
|
||||
x: targetNode.position.x + fallbackDelta.x,
|
||||
y: targetNode.position.y + fallbackDelta.y
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const dx = node.position.x - lastPosition.x
|
||||
const dy = node.position.y - lastPosition.y
|
||||
if (dx === 0 && dy === 0) return
|
||||
|
||||
dragLastPositionByNodeId.set(node.id, { x: node.position.x, y: node.position.y })
|
||||
|
||||
const descendantIds = getDescendantIds(node.id)
|
||||
if (descendantIds.size === 0) return
|
||||
|
||||
// 获取当前所有选中的节点,避免重复位移
|
||||
const selectedNodeIds = new Set(flowNodes.value.filter(n => n.selected).map(n => n.id))
|
||||
|
||||
// 批量更新子节点位置
|
||||
descendantIds.forEach(id => {
|
||||
if (!selectedNodeIds.has(id)) {
|
||||
const targetNode = flowNodes.value.find(n => n.id === id)
|
||||
if (targetNode) {
|
||||
// 直接更新位置对象,确保 Vue 能够检测到深层变化
|
||||
targetNode.position = {
|
||||
x: targetNode.position.x + dx,
|
||||
y: targetNode.position.y + dy
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onNodeDrag(handleNodeDrag)
|
||||
|
||||
onNodeDragStop(e => {
|
||||
draggingNodeId.value = null
|
||||
if (e?.node?.id) {
|
||||
dragLastPositionByNodeId.delete(e.node.id)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -166,16 +237,25 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取某节点所有后代节点 id(用于激活路径计算)
|
||||
* 获取节点的所有后代节点 ID (迭代实现,更健壮)
|
||||
*/
|
||||
const getDescendantIds = (nodeId: string, ids: Set<string> = new Set()): Set<string> => {
|
||||
flowEdges.value.forEach(edge => {
|
||||
if (edge.source === nodeId) {
|
||||
ids.add(edge.target)
|
||||
getDescendantIds(edge.target, ids)
|
||||
const getDescendantIds = (nodeId: string): Set<string> => {
|
||||
const descendants = new Set<string>()
|
||||
const stack = [nodeId]
|
||||
const edges = flowEdges.value
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentId = stack.pop()!
|
||||
for (const edge of edges) {
|
||||
if (edge.source === currentId) {
|
||||
if (!descendants.has(edge.target)) {
|
||||
descendants.add(edge.target)
|
||||
stack.push(edge.target)
|
||||
}
|
||||
})
|
||||
return ids
|
||||
}
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,7 +315,8 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
|
||||
edgeType: 'default',
|
||||
backgroundVariant: BackgroundVariant.Lines,
|
||||
showControls: true,
|
||||
showMiniMap: true
|
||||
showMiniMap: true,
|
||||
hierarchicalDragging: true
|
||||
})
|
||||
|
||||
const lastAppliedStatus = ref('')
|
||||
@@ -763,6 +844,7 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
|
||||
fitView,
|
||||
resetLayout,
|
||||
centerRoot,
|
||||
handleNodeDrag,
|
||||
startNewSession,
|
||||
executeReset,
|
||||
generateSummary,
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"summary": "SUMMARY",
|
||||
"export": "EXPORT",
|
||||
"fullscreen": "Fullscreen",
|
||||
"exitFullscreen": "Exit Fullscreen"
|
||||
"exitFullscreen": "Exit Fullscreen",
|
||||
"hierarchicalDragging": "Linked Drag"
|
||||
},
|
||||
"settings": {
|
||||
"title": "API Settings",
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"summary": "总结",
|
||||
"export": "导出",
|
||||
"fullscreen": "全屏",
|
||||
"exitFullscreen": "退出全屏"
|
||||
"exitFullscreen": "退出全屏",
|
||||
"hierarchicalDragging": "联动拖拽"
|
||||
},
|
||||
"settings": {
|
||||
"title": "API 设置",
|
||||
|
||||
@@ -33,6 +33,7 @@ body {
|
||||
/* Markdown 渲染基础样式 */
|
||||
.markdown-body {
|
||||
@apply text-slate-700;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
|
||||
Reference in New Issue
Block a user