fix: 视频持久化、会话排序、重命名等多项修复
- 修复视频 URL 存入会话数据,切换/重启不丢失 - 保存时从磁盘重载会话列表,消除内存缓存不一致 - 会话列表按 updatedAt 降序,最新在顶部 - 会话标题自动加序号区分同名 - 角色库和场景库支持重命名 - 尾帧保存时弹窗命名 - DeepSeek 仅脚本模式可用,生图模式过滤 - Seedance 2.0 适配,支持 return_last_frame - 图片下载改用 https.get 避免 IPC 序列化问题 - 脚本模式工具栏仅显示对话+视频模型 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+7
-7
@@ -384,18 +384,17 @@ export async function createVideoTask(
|
||||
const content: Record<string, unknown>[] = []
|
||||
const isV2 = model.includes('2-0') || model.includes('2.0')
|
||||
|
||||
// 首帧图:场景图优先,否则用第一个角色图
|
||||
const frameUrl = firstFrameUrl || (refUrls && refUrls.length > 0 ? refUrls[0] : '')
|
||||
if (frameUrl) {
|
||||
const resolved = resolveImageUrl(frameUrl)
|
||||
// 首帧:仅场景图(让视频从该场景开始),不填则 AI 自由生成
|
||||
if (firstFrameUrl) {
|
||||
const resolved = resolveImageUrl(firstFrameUrl)
|
||||
if (resolved) content.push({ type: 'image_url', image_url: { url: resolved }, role: 'first_frame' })
|
||||
}
|
||||
|
||||
// 参考图:仅 2.0 支持
|
||||
// 角色参考图:仅 2.0 支持 reference_image,告诉 AI 角色长啥样
|
||||
if (isV2 && refUrls) {
|
||||
for (const refUrl of refUrls) {
|
||||
const resolved = resolveImageUrl(refUrl)
|
||||
if (resolved && resolved !== resolveImageUrl(frameUrl)) {
|
||||
if (resolved) {
|
||||
content.push({ type: 'image_url', image_url: { url: resolved }, role: 'reference_image' })
|
||||
}
|
||||
}
|
||||
@@ -406,7 +405,7 @@ export async function createVideoTask(
|
||||
const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
|
||||
body: JSON.stringify({ model, content, duration: durationSec, resolution, generate_audio: false })
|
||||
body: JSON.stringify({ model, content, duration: durationSec, resolution, generate_audio: false, return_last_frame: true })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -434,6 +433,7 @@ export async function pollVideoTask(taskId: string): Promise<{ status: string; v
|
||||
return {
|
||||
status: result.status,
|
||||
videoUrl: result.content?.video_url,
|
||||
lastFrameUrl: result.last_frame_url || '',
|
||||
duration: result.duration,
|
||||
resolution: result.resolution
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ipcMain } from 'electron'
|
||||
import {
|
||||
getApiKeys, getUiApiKeys, getConfigFileApiKeys,
|
||||
setApiKey, deleteApiKey, openConfigFile, getConfigPath, getEndpoints,
|
||||
getCharacters, saveCharacter, deleteCharacter,
|
||||
getScenes, saveScene, deleteScene,
|
||||
getCharacters, saveCharacter, deleteCharacter, renameCharacter,
|
||||
getScenes, saveScene, deleteScene, renameScene,
|
||||
getDataDir, getImagesDir, setDataDir, openFolder, pickFolder,
|
||||
getSessions, getSession, saveSession, deleteSession,
|
||||
downloadImage, readImageBase64, imagesDir
|
||||
@@ -49,6 +49,7 @@ export function registerIpcHandlers(): void {
|
||||
})
|
||||
})
|
||||
ipcMain.handle('characters:delete', (_e, id) => wrap(() => { deleteCharacter(id); return { success: true } }))
|
||||
ipcMain.handle('characters:rename', (_e, id, newName) => wrap(() => { renameCharacter(id, newName); return { success: true } }))
|
||||
|
||||
// === 场景库 ===
|
||||
ipcMain.handle('scenes:getAll', () => wrap(() => getScenes()))
|
||||
@@ -61,6 +62,7 @@ export function registerIpcHandlers(): void {
|
||||
})
|
||||
})
|
||||
ipcMain.handle('scenes:delete', (_e, id) => wrap(() => { deleteScene(id); return { success: true } }))
|
||||
ipcMain.handle('scenes:rename', (_e, id, newName) => wrap(() => { renameScene(id, newName); return { success: true } }))
|
||||
|
||||
// === 图片 ===
|
||||
ipcMain.handle('image:readBase64', (_e, path: string) => {
|
||||
|
||||
+19
-1
@@ -208,6 +208,15 @@ export function deleteCharacter(id: string): void {
|
||||
writeInternal(data)
|
||||
}
|
||||
|
||||
export function renameCharacter(id: string, newName: string): void {
|
||||
const data = readInternal()
|
||||
const chars = data.characters || []
|
||||
if (chars.some(c => c.name === newName && c.id !== id)) throw new Error('角色「' + newName + '」已存在')
|
||||
const c = chars.find(c => c.id === id)
|
||||
if (c) c.name = newName
|
||||
writeInternal(data)
|
||||
}
|
||||
|
||||
// ===== 场景库 =====
|
||||
|
||||
export function getScenes(): StoredScene[] {
|
||||
@@ -231,6 +240,15 @@ export function deleteScene(id: string): void {
|
||||
writeInternal(data)
|
||||
}
|
||||
|
||||
export function renameScene(id: string, newName: string): void {
|
||||
const data = readInternal()
|
||||
const sc = data.scenes || []
|
||||
if (sc.some(s => s.name === newName && s.id !== id)) throw new Error('场景「' + newName + '」已存在')
|
||||
const s = sc.find(s => s.id === id)
|
||||
if (s) s.name = newName
|
||||
writeInternal(data)
|
||||
}
|
||||
|
||||
export function getDataDir(): string {
|
||||
return baseDir()
|
||||
}
|
||||
@@ -362,7 +380,7 @@ export function readImageBase64(localPath: string): string {
|
||||
// ===== 会话管理 =====
|
||||
|
||||
export function getSessions(): ChatSession[] {
|
||||
return readInternal().sessions || []
|
||||
return (readInternal().sessions || []).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
}
|
||||
|
||||
export function getSession(id: string): ChatSession | undefined {
|
||||
|
||||
@@ -22,11 +22,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getCharacters: () => ipcRenderer.invoke('characters:getAll'),
|
||||
saveCharacter: (c) => ipcRenderer.invoke('characters:save', c),
|
||||
deleteCharacter: (id) => ipcRenderer.invoke('characters:delete', id),
|
||||
renameCharacter: (id, newName) => ipcRenderer.invoke('characters:rename', id, newName),
|
||||
|
||||
// 场景库
|
||||
getScenes: () => ipcRenderer.invoke('scenes:getAll'),
|
||||
saveScene: (s) => ipcRenderer.invoke('scenes:save', s),
|
||||
deleteScene: (id) => ipcRenderer.invoke('scenes:delete', id),
|
||||
renameScene: (id, newName) => ipcRenderer.invoke('scenes:rename', id, newName),
|
||||
|
||||
// 图片
|
||||
readImageBase64: (path: string) => ipcRenderer.invoke('image:readBase64', path),
|
||||
|
||||
Vendored
+3
-1
@@ -60,10 +60,12 @@ interface ElectronAPI {
|
||||
getCharacters: () => Promise<StoredCharacter[]>
|
||||
saveCharacter: (char: StoredCharacter) => Promise<StoredCharacter>
|
||||
deleteCharacter: (id: string) => Promise<void>
|
||||
renameCharacter: (id: string, newName: string) => Promise<void>
|
||||
|
||||
getScenes: () => Promise<StoredScene[]>
|
||||
saveScene: (scene: StoredScene) => Promise<StoredScene>
|
||||
deleteScene: (id: string) => Promise<void>
|
||||
renameScene: (id: string, newName: string) => Promise<void>
|
||||
|
||||
readImageBase64: (path: string) => Promise<string>
|
||||
|
||||
@@ -77,7 +79,7 @@ interface ElectronAPI {
|
||||
splitNovel: (novelText: string) => Promise<{ title: string; content: string }[]>
|
||||
generateScript: (novelText: string, duration: number, style?: string, model?: string) => Promise<string>
|
||||
createVideo: (prompt: string, duration: number, resolution: string, model?: string, firstFrameUrl?: string, refUrls?: string[]) => Promise<string>
|
||||
pollVideo: (taskId: string) => Promise<{ status: string; videoUrl?: string; duration?: number; resolution?: string }>
|
||||
pollVideo: (taskId: string) => Promise<{ status: string; videoUrl?: string; lastFrameUrl?: string; duration?: number; resolution?: string }>
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<p class="char-desc">{{ previewDesc(char.description) }}</p>
|
||||
<div class="char-footer">
|
||||
<span class="char-date">{{ formatDate(char.createdAt) }}</span>
|
||||
<el-button size="small" text @click="handleRename(char)">重命名</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(char)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,6 +106,21 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRename(char: StoredCharacter) {
|
||||
try {
|
||||
const r = await ElMessageBox.prompt('新名称', '重命名角色', { inputValue: char.name, confirmButtonText: '确定' })
|
||||
const name = r.value.trim()
|
||||
if (!name || name === char.name) return
|
||||
await window.electronAPI.renameCharacter(char.id, name)
|
||||
char.name = name
|
||||
ElMessage.success('已重命名')
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : ''
|
||||
if (msg.includes('已存在')) ElMessage.warning(msg)
|
||||
else if (msg && !msg.includes('cancel')) ElMessage.error('重命名失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(char: StoredCharacter) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${char.name}」?`, '确认删除')
|
||||
|
||||
@@ -127,6 +127,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频:每条消息独立展示,不能在 imageUrl 条件内 -->
|
||||
<div v-if="msg.videoUrl || (videoResult && i === messages.length - 1)" class="video-result" style="margin-top:8px">
|
||||
<video v-if="msg.videoUrl || videoResult?.videoUrl" :src="msg.videoUrl || videoResult?.videoUrl" controls style="max-width:100%;max-height:300px;border-radius:8px" />
|
||||
<el-button v-if="msg.videoUrl || videoResult?.videoUrl" size="small" type="success" style="margin-top:8px" @click="saveLastFrame((msg.videoUrl || videoResult?.videoUrl)!)">
|
||||
保存尾帧到场景库
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="msg.videoTaskId && !msg.videoUrl && !videoResult" style="font-size:12px;color:#999;margin-top:4px">
|
||||
视频任务: {{ msg.videoTaskId }}
|
||||
</div>
|
||||
|
||||
<!-- 脚本模式:视频生成控制(不在 imageUrl 条件内) -->
|
||||
<div v-if="mode === 'script' && msg.role === 'assistant' && i === messages.length - 1" class="script-actions">
|
||||
<el-divider />
|
||||
@@ -174,13 +185,6 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 视频结果 -->
|
||||
<div v-if="videoResult" class="video-result">
|
||||
<video v-if="videoResult.videoUrl" :src="videoResult.videoUrl" controls style="max-width:100%;max-height:300px;border-radius:8px" />
|
||||
<el-button v-if="videoResult.videoUrl" size="small" type="success" style="margin-top:8px" @click="saveLastFrame(videoResult.videoUrl!)">
|
||||
保存尾帧到场景库
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,6 +255,8 @@ interface Message {
|
||||
text: string
|
||||
imageUrl?: string
|
||||
saved?: boolean
|
||||
videoTaskId?: string
|
||||
videoUrl?: string
|
||||
}
|
||||
|
||||
// ===== 状态 =====
|
||||
@@ -270,7 +276,7 @@ const generatingVideo = ref(false)
|
||||
const videoTaskId = ref('')
|
||||
const videoResolution = ref('720p')
|
||||
const videoModel = ref('doubao-seedance-2-0-fast-260128')
|
||||
const videoResult = ref<{ videoUrl?: string } | null>(null)
|
||||
const videoResult = ref<{ videoUrl?: string; lastFrameUrl?: string } | null>(null)
|
||||
const selectedSceneImage = ref('')
|
||||
const mappedCharacter = ref('')
|
||||
const characterList = ref<StoredCharacter[]>([])
|
||||
@@ -325,9 +331,12 @@ const suggestions = [
|
||||
// ===== 会话管理 =====
|
||||
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/** 生成会话标题(取第一条用户消息) */
|
||||
/** 生成会话标题(加序号区分同名) */
|
||||
function makeTitle(text: string): string {
|
||||
return text.length > 30 ? text.slice(0, 30) + '...' : text
|
||||
const base = text.length > 30 ? text.slice(0, 30) + '...' : text
|
||||
// 统计同名会话数,追加序号
|
||||
const sameCount = sessions.value.filter(s => s.title.startsWith(base)).length + 1
|
||||
return sameCount > 1 ? base + ` (#${sameCount})` : base
|
||||
}
|
||||
|
||||
/** 格式化会话时间显示 */
|
||||
@@ -335,8 +344,7 @@ function formatSessionTime(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
const now = new Date()
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
// 今天的只显示时间,非今天的加日期
|
||||
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
if (d.toDateString() === now.toDateString()) return time
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${time}`
|
||||
}
|
||||
@@ -358,13 +366,28 @@ async function switchSession(id: string) {
|
||||
}
|
||||
if (id === currentSessionId.value) return
|
||||
currentSessionId.value = id
|
||||
const s = sessions.value.find(x => x.id === id)
|
||||
// 从后端重新加载最新数据
|
||||
const all = await window.electronAPI.getSessions()
|
||||
sessions.value = all
|
||||
const s = all.find(x => x.id === id)
|
||||
if (s) {
|
||||
messages.value = s.messages.map(m => ({ ...m }))
|
||||
mode.value = s.mode as Mode
|
||||
currentStyle.value = s.style || 'anime'
|
||||
chatModel.value = s.chatModel
|
||||
imageModel.value = s.imageModel
|
||||
// 恢复视频状态
|
||||
videoResult.value = null
|
||||
videoTaskId.value = ''
|
||||
const lastMsg = messages.value[messages.value.length - 1]
|
||||
if (lastMsg?.videoUrl) {
|
||||
videoResult.value = { videoUrl: lastMsg.videoUrl, lastFrameUrl: (lastMsg as Record<string,unknown>).lastFrameUrl as string || '' }
|
||||
videoTaskId.value = lastMsg.videoTaskId || ''
|
||||
} else if (lastMsg?.videoTaskId) {
|
||||
// 有任务 ID 但没 URL,自动查询状态
|
||||
videoTaskId.value = lastMsg.videoTaskId
|
||||
await pollVideoTask(lastMsg)
|
||||
}
|
||||
}
|
||||
await scrollToBottom()
|
||||
}
|
||||
@@ -388,27 +411,33 @@ async function handleDeleteSession(s: ChatSession) {
|
||||
async function saveCurrentSession(options?: { title?: string }): Promise<string | undefined> {
|
||||
if (messages.value.length === 0) return
|
||||
|
||||
// 从第一条用户消息取标题
|
||||
const firstUser = messages.value.find(m => m.role === 'user')
|
||||
const title = options?.title || (firstUser ? makeTitle(firstUser.text) : '新会话')
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const sid = currentSessionId.value || (Date.now().toString(36) + Math.random().toString(36).slice(2, 10))
|
||||
const existing = sessions.value.find(s => s.id === sid)
|
||||
const msgCount = messages.value.length
|
||||
|
||||
// 内容没变不更新时间(避免浏览时乱排)
|
||||
const shouldUpdate = !existing || existing.messages.length !== msgCount
|
||||
const updatedAt = shouldUpdate ? now : (existing?.updatedAt || now)
|
||||
|
||||
const session: ChatSession = {
|
||||
id: currentSessionId.value || (Date.now().toString(36) + Math.random().toString(36).slice(2, 6)),
|
||||
id: sid,
|
||||
title,
|
||||
mode: mode.value,
|
||||
style: currentStyle.value,
|
||||
chatModel: chatModel.value,
|
||||
imageModel: imageModel.value,
|
||||
messages: JSON.parse(JSON.stringify(messages.value)),
|
||||
createdAt: currentSessionId.value ? (sessions.value.find(s => s.id === currentSessionId.value)?.createdAt || now) : now,
|
||||
updatedAt: now
|
||||
createdAt: existing?.createdAt || now,
|
||||
updatedAt
|
||||
}
|
||||
|
||||
await window.electronAPI.saveSession(session)
|
||||
currentSessionId.value = session.id
|
||||
|
||||
// 刷新会话列表
|
||||
// 每次保存都从磁盘重载,保证数据一致
|
||||
sessions.value = await window.electronAPI.getSessions()
|
||||
return session.id
|
||||
}
|
||||
@@ -430,7 +459,7 @@ async function sendMessage() {
|
||||
// 如果是新会话的第一条消息,创建会话并生成标题
|
||||
if (messages.value.length === 0) {
|
||||
// 先保存一个空会话,让用户能切走
|
||||
const tempId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
||||
const tempId = Date.now().toString(36) + Math.random().toString(36).slice(2, 10)
|
||||
currentSessionId.value = tempId
|
||||
await window.electronAPI.saveSession({
|
||||
id: tempId,
|
||||
@@ -561,26 +590,36 @@ async function startVideoGeneration(msg: Message) {
|
||||
|
||||
generatingVideo.value = true
|
||||
videoResult.value = null
|
||||
const sid = currentSessionId.value // 记录当前会话,防止切换后污染
|
||||
try {
|
||||
videoTaskId.value = await window.electronAPI.createVideo(prompt, videoDuration.value, videoResolution.value, videoModel.value, firstFrameUrl, refUrls)
|
||||
msg.videoTaskId = videoTaskId.value
|
||||
// 立刻存 taskId
|
||||
await saveCurrentSession()
|
||||
ElMessage.success('视频任务已创建: ' + videoTaskId.value)
|
||||
await pollForVideo()
|
||||
await pollForVideo(sid, msg)
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '视频生成失败')
|
||||
} finally {
|
||||
generatingVideo.value = false
|
||||
if (currentSessionId.value === sid) await saveCurrentSession()
|
||||
}
|
||||
}
|
||||
|
||||
async function pollForVideo() {
|
||||
async function pollForVideo(sessionId: string, msg: Message) {
|
||||
if (!videoTaskId.value) return
|
||||
const maxPolls = 40
|
||||
for (let i = 0; i < maxPolls; i++) {
|
||||
await new Promise(r => setTimeout(r, 5000))
|
||||
if (currentSessionId.value !== sessionId) return
|
||||
try {
|
||||
const result = await window.electronAPI.pollVideo(videoTaskId.value)
|
||||
if (result.status === 'succeeded') {
|
||||
videoResult.value = result
|
||||
if (currentSessionId.value === sessionId) videoResult.value = result
|
||||
// 立即存到消息和会话,绝不丢失
|
||||
msg.videoUrl = result.videoUrl
|
||||
msg.videoTaskId = videoTaskId.value
|
||||
await saveCurrentSession()
|
||||
ElMessage.success('视频生成完成!')
|
||||
return
|
||||
}
|
||||
@@ -590,7 +629,7 @@ async function pollForVideo() {
|
||||
}
|
||||
} catch { /* retry */ }
|
||||
}
|
||||
ElMessage.warning('视频生成超时,请稍后查询')
|
||||
if (currentSessionId.value === sessionId) ElMessage.warning('视频生成超时,请稍后查询')
|
||||
}
|
||||
|
||||
async function checkVideoStatus() {
|
||||
@@ -608,19 +647,50 @@ async function checkVideoStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从已保存的 taskId 恢复视频 URL */
|
||||
async function pollVideoTask(msg: Message) {
|
||||
if (!msg.videoTaskId) return
|
||||
try {
|
||||
const result = await window.electronAPI.pollVideo(msg.videoTaskId)
|
||||
if (result.status === 'succeeded' && result.videoUrl) {
|
||||
msg.videoUrl = result.videoUrl
|
||||
if (result.lastFrameUrl) (msg as Record<string,unknown>).lastFrameUrl = result.lastFrameUrl
|
||||
videoResult.value = { videoUrl: result.videoUrl, lastFrameUrl: result.lastFrameUrl }
|
||||
await saveCurrentSession()
|
||||
}
|
||||
} catch { /* 稍后重试 */ }
|
||||
}
|
||||
|
||||
async function saveLastFrame(videoUrl: string) {
|
||||
try {
|
||||
const name = '视频尾帧_' + Date.now().toString(36)
|
||||
let lfUrl = videoResult.value?.lastFrameUrl || ''
|
||||
// 没尾帧 URL,尝试从任务查询
|
||||
if (!lfUrl && videoTaskId.value) {
|
||||
try {
|
||||
const r = await window.electronAPI.pollVideo(videoTaskId.value)
|
||||
lfUrl = r.lastFrameUrl || ''
|
||||
if (lfUrl && videoResult.value) videoResult.value.lastFrameUrl = lfUrl
|
||||
} catch {}
|
||||
}
|
||||
if (lfUrl) {
|
||||
let name = '视频尾帧_' + Date.now().toString(36)
|
||||
try {
|
||||
const r = await ElMessageBox.prompt('给尾帧起个名字', '保存尾帧', { inputValue: name, confirmButtonText: '保存' })
|
||||
if (r.value.trim()) name = r.value.trim()
|
||||
} catch { return }
|
||||
await window.electronAPI.saveScene({
|
||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
||||
name,
|
||||
description: '视频生成尾帧 | 任务ID: ' + videoTaskId.value,
|
||||
imageUrl: videoUrl + '?return_last_frame=true',
|
||||
imageUrl: lfUrl,
|
||||
createdAt: new Date().toISOString()
|
||||
})
|
||||
ElMessage.success('尾帧已保存到场景库')
|
||||
} else {
|
||||
ElMessage.warning('此视频未生成尾帧,请等待下次视频完成后再保存')
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error('保存失败,请尝试手动截图')
|
||||
ElMessage.error('保存失败: ' + (e instanceof Error ? e.message : ''))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<p class="scene-desc">{{ previewDesc(scene.description) }}</p>
|
||||
<div class="scene-footer">
|
||||
<span class="scene-date">{{ formatDate(scene.createdAt) }}</span>
|
||||
<el-button size="small" text @click="handleRename(scene)">重命名</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(scene)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,6 +97,21 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRename(scene: StoredScene) {
|
||||
try {
|
||||
const r = await ElMessageBox.prompt('新名称', '重命名场景', { inputValue: scene.name, confirmButtonText: '确定' })
|
||||
const name = r.value.trim()
|
||||
if (!name || name === scene.name) return
|
||||
await window.electronAPI.renameScene(scene.id, name)
|
||||
scene.name = name
|
||||
ElMessage.success('已重命名')
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : ''
|
||||
if (msg.includes('已存在')) ElMessage.warning(msg)
|
||||
else if (msg && !msg.includes('cancel')) ElMessage.error('重命名失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(scene: StoredScene) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${scene.name}」?`, '确认删除')
|
||||
|
||||
Reference in New Issue
Block a user