feat: add version history and AI diff highlighting in editor

This commit is contained in:
2026-06-16 15:23:16 +08:00
parent c1f7be1507
commit a21652b1ca
7 changed files with 606 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { ref, nextTick, onMounted } from 'vue'
import { useEditorStore } from '../stores/editorStore'
import { sendAIRequest } from '../composables/useAI'
@@ -40,13 +40,19 @@ async function send() {
messages.value.push({ role: 'user', content: userInput })
loading.value = true
if (mode.value === 'json') {
await store.saveVersion('AI 修改前')
}
const oldGameData = mode.value === 'json' ? JSON.parse(JSON.stringify(store.gameData)) : undefined
try {
const { result, sessionId: newSid } = await sendAIRequest(fullMessage, mode.value, store.deepseekKey, store.aiSessionId || undefined)
if (newSid) store.setAISessionId(newSid)
if (mode.value === 'json') {
messages.value.push({ role: 'assistant', content: result || '已完成' })
await store.reloadFromDisk()
await store.reloadFromDisk(oldGameData)
} else {
messages.value.push({ role: 'assistant', content: result || '已完成' })
}
@@ -68,6 +74,10 @@ function newSession() {
messages.value = []
errorMsg.value = ''
}
onMounted(() => {
store.loadVersions()
})
</script>
<template>
@@ -83,6 +93,16 @@ function newSession() {
</div>
</div>
<div v-if="store.versions.length > 0 || store.aiChanges" class="ai-toolbar">
<select v-if="store.versions.length > 0" class="ai-version-select" @change="store.restoreVersion(($event.target as HTMLSelectElement).selectedIndex)">
<option disabled selected>历史版本</option>
<option v-for="v in store.versions" :key="v.timestamp">
{{ new Date(v.timestamp).toLocaleString('zh-CN') }} {{ v.label }}
</option>
</select>
<button v-if="store.aiChanges" class="ai-clear-btn" @click="store.clearAIMarkers()">清除高亮</button>
</div>
<div class="ai-chat" ref="chatRef">
<div v-if="!store.deepseekKey" class="ai-key-setup">
<span class="key-label">DeepSeek API Key</span>
@@ -289,4 +309,36 @@ function newSession() {
font-size: 11px;
color: #666;
}
.ai-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
border-bottom: 1px solid rgba(255,255,255,0.06);
flex-shrink: 0;
}
.ai-version-select {
flex: 1;
padding: 4px 8px;
font-size: 11px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 3px;
color: #ccc;
outline: none;
}
.ai-clear-btn {
padding: 4px 10px;
font-size: 11px;
color: #ccc;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 3px;
cursor: pointer;
white-space: nowrap;
}
.ai-clear-btn:hover { color: #fff; border-color: rgba(255,255,255,0.2); }
</style>

View File

@@ -17,6 +17,7 @@ const store = useEditorStore()
const jsonText = ref('')
const errorMsg = ref('')
const saved = ref(false)
const globalTooltip = ref('')
watch(() => [props.scene, store.gameData] as const, () => {
errorMsg.value = ''
@@ -32,6 +33,7 @@ watch(() => [props.scene, store.gameData] as const, () => {
}, { immediate: true, deep: true })
function onBlur() {
store.clearAIMarkers()
errorMsg.value = ''
try {
const parsed = JSON.parse(jsonText.value)
@@ -54,7 +56,7 @@ function onBlur() {
<template>
<div class="node-editor" v-if="scene || jsonText">
<div class="editor-header">
<h3>{{ scene ? scene.id : '全局配置' }}</h3>
<h3>{{ scene ? scene.id : '全局配置' }}<span v-if="!scene && store.aiChanges?.globalFields?.length" class="global-diff-tag" :title="store.aiChanges.globalFields.join(', ')"> 已修改 {{ store.aiChanges.globalFields.length }} 字段</span></h3>
<div class="header-actions">
<span v-if="saved" class="saved-hint">已保存</span>
<span v-if="errorMsg" class="error-hint">JSON 错误: {{ errorMsg }}</span>
@@ -165,4 +167,17 @@ function onBlur() {
.json-area.error {
border-left: 3px solid #e74c3c;
}
.global-diff-tag {
display: inline-block;
margin-left: 10px;
padding: 1px 8px;
font-size: 11px;
font-weight: 400;
color: #ffe0b2;
background: rgba(230, 81, 0, 0.15);
border: 1px solid rgba(230, 81, 0, 0.3);
border-radius: 3px;
cursor: default;
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { VueFlow, useVueFlow, SmoothStepEdge } from '@vue-flow/core'
import { VueFlow, useVueFlow, SmoothStepEdge, Handle, Position } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import '@vue-flow/core/dist/style.css'
@@ -8,6 +8,7 @@ import '@vue-flow/controls/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import type { Connection } from '@vue-flow/core'
import { computePositions, savePosition } from '../composables/useLayout'
import { useEditorStore } from '../stores/editorStore'
const props = defineProps<{
sceneNodes: { id: string; label: string }[]
@@ -23,6 +24,8 @@ const emit = defineEmits<{
clearSelection: []
}>()
const store = useEditorStore()
const nodes = ref<any[]>([])
const edges = ref<any[]>([])
const { onNodeClick, onConnect, onNodeContextMenu, onNodeDragStop, onPaneClick, fitView } = useVueFlow()
@@ -33,13 +36,23 @@ const ctxMenuNodeId = ref('')
function makeNodes() {
const positions = computePositions(props.sceneNodes, props.sceneEdges, props.startScene)
const changes = store.aiChanges
return props.sceneNodes.map((n) => {
const pos = positions.get(n.id) ?? { x: 0, y: 0 }
let badge = ''
if (changes) {
if (changes.added.includes(n.id)) badge = 'NEW'
else if (changes.modified.includes(n.id)) badge = 'MOD'
else if (changes.deleted.includes(n.id)) badge = 'DEL'
}
return {
id: n.id,
type: 'default',
position: pos,
data: { label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label },
data: {
label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label,
badge,
},
style: n.id === props.startScene
? { background: '#1b5e20', color: '#fff', borderColor: '#388e3c' }
: n.id === props.selectedNodeId
@@ -99,7 +112,7 @@ function inlineRebuild() {
}
watch(
() => [props.sceneNodes, props.sceneEdges, props.startScene, props.selectedNodeId] as const,
() => [props.sceneNodes, props.sceneEdges, props.startScene, props.selectedNodeId, store.aiChanges] as const,
() => {
const nc = props.sceneNodes.length
const ec = props.sceneEdges.length
@@ -152,6 +165,16 @@ onConnect((conn: Connection) => {
:min-zoom="0.2"
:max-zoom="2"
>
<template #node-default="nodeProps">
<div class="custom-node">
<Handle type="target" :position="Position.Left" />
<div class="custom-node-label">{{ nodeProps.data.label }}</div>
<span v-if="nodeProps.data.badge" class="diff-badge" :class="`diff-badge-${nodeProps.data.badge}`">
{{ nodeProps.data.badge }}
</span>
<Handle type="source" :position="Position.Right" />
</div>
</template>
<Background :gap="20" />
<Controls />
</VueFlow>
@@ -207,4 +230,44 @@ onConnect((conn: Connection) => {
background: rgba(100,200,255,0.12);
color: #fff;
}
.custom-node {
position: relative;
padding: 8px 30px 8px 12px;
min-width: 120px;
font-size: 12px;
}
.custom-node-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.diff-badge {
position: absolute;
top: 2px;
right: 4px;
padding: 1px 5px;
font-size: 9px;
font-weight: 600;
border-radius: 2px;
line-height: 1.4;
}
.diff-badge-NEW {
background: #2e7d32;
color: #c8e6c9;
}
.diff-badge-MOD {
background: #e65100;
color: #ffe0b2;
}
.diff-badge-DEL {
background: #c62828;
color: #ffcdd2;
text-decoration: line-through;
}
</style>