feat: save and load button
This commit is contained in:
@@ -1697,6 +1697,9 @@ def validate_save_name(name: str) -> bool:
|
||||
class SaveGameRequest(BaseModel):
|
||||
custom_name: Optional[str] = None # 自定义存档名称
|
||||
|
||||
class DeleteSaveRequest(BaseModel):
|
||||
filename: str
|
||||
|
||||
class LoadGameRequest(BaseModel):
|
||||
filename: str
|
||||
|
||||
@@ -1752,6 +1755,37 @@ def api_save_game(req: SaveGameRequest):
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Save failed")
|
||||
|
||||
@app.post("/api/game/delete")
|
||||
def api_delete_game(req: DeleteSaveRequest):
|
||||
"""删除存档及其关联文件"""
|
||||
# 安全检查
|
||||
if ".." in req.filename or "/" in req.filename or "\\" in req.filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
try:
|
||||
saves_dir = CONFIG.paths.saves
|
||||
target_path = saves_dir / req.filename
|
||||
|
||||
# 1. 删除 JSON 存档文件
|
||||
if target_path.exists():
|
||||
os.remove(target_path)
|
||||
|
||||
# 2. 删除对应的 SQL 数据库文件
|
||||
events_db_path = get_events_db_path(target_path)
|
||||
if os.path.exists(events_db_path):
|
||||
try:
|
||||
os.remove(events_db_path)
|
||||
except Exception as e:
|
||||
print(f"[Warning] Failed to delete db file {events_db_path}: {e}")
|
||||
|
||||
# 3. 删除可能存在的其他关联文件(如果有)
|
||||
|
||||
return {"status": "ok", "message": "Save deleted"}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"Delete failed: {str(e)}")
|
||||
|
||||
@app.post("/api/game/load")
|
||||
async def api_load_game(req: LoadGameRequest):
|
||||
"""加载游戏(异步,支持进度更新)。"""
|
||||
|
||||
@@ -26,6 +26,10 @@ export const systemApi = {
|
||||
);
|
||||
},
|
||||
|
||||
deleteSave(filename: string) {
|
||||
return httpClient.post<{ status: string; message: string }>('/api/game/delete', { filename });
|
||||
},
|
||||
|
||||
loadGame(filename: string) {
|
||||
return httpClient.post<{ status: string; message: string }>('/api/game/load', { filename });
|
||||
},
|
||||
|
||||
@@ -112,6 +112,21 @@ async function handleLoad(filename: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(filename: string) {
|
||||
if (!confirm(t('save_load.delete_confirm', { filename }))) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await systemApi.deleteSave(filename)
|
||||
message.success(t('save_load.delete_success'))
|
||||
await fetchSaves()
|
||||
} catch (e) {
|
||||
message.error(t('save_load.delete_failed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化保存时间
|
||||
function formatSaveTime(isoTime: string): string {
|
||||
if (!isoTime) return ''
|
||||
@@ -199,7 +214,22 @@ onMounted(() => {
|
||||
<span class="version">v{{ save.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mode === 'load'" class="load-btn">{{ t('save_load.load') }}</div>
|
||||
<div v-if="mode === 'load'" class="load-actions">
|
||||
<NButton
|
||||
type="error"
|
||||
size="small"
|
||||
secondary
|
||||
@click.stop="handleDelete(save.filename)"
|
||||
>
|
||||
{{ t('save_load.delete') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
@click.stop="handleLoad(save.filename)"
|
||||
>
|
||||
{{ t('save_load.load') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -250,7 +280,7 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.save-panel {
|
||||
.save-panel, .load-panel {
|
||||
align-items: center;
|
||||
padding-top: 2em;
|
||||
}
|
||||
@@ -392,19 +422,10 @@ onMounted(() => {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.load-btn {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
border: 1px solid #444;
|
||||
padding: 0.4em 1em;
|
||||
border-radius: 0.3em;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.load-btn:hover {
|
||||
background: #444;
|
||||
border-color: #555;
|
||||
.load-actions {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
||||
@@ -49,7 +49,11 @@
|
||||
"name_placeholder": "Enter save name...",
|
||||
"name_tip": "Leave empty to use auto-generated name",
|
||||
"name_too_long": "Name cannot exceed 50 characters",
|
||||
"name_invalid_chars": "Name can only contain letters, numbers, Chinese characters and underscores"
|
||||
"name_invalid_chars": "Name can only contain letters, numbers, Chinese characters and underscores",
|
||||
"delete": "Delete",
|
||||
"delete_confirm": "Are you sure you want to permanently delete save {filename}? This action cannot be undone.",
|
||||
"delete_success": "Save deleted",
|
||||
"delete_failed": "Failed to delete"
|
||||
},
|
||||
"llm": {
|
||||
"loading": "Loading...",
|
||||
|
||||
@@ -49,7 +49,11 @@
|
||||
"name_placeholder": "输入存档名称...",
|
||||
"name_tip": "留空将使用自动生成的名称",
|
||||
"name_too_long": "名称不能超过50个字符",
|
||||
"name_invalid_chars": "名称只能包含中文、字母、数字和下划线"
|
||||
"name_invalid_chars": "名称只能包含中文、字母、数字和下划线",
|
||||
"delete": "删除",
|
||||
"delete_confirm": "确定要彻底删除存档 {filename} 及其所有数据吗?此操作无法撤销。",
|
||||
"delete_success": "存档已删除",
|
||||
"delete_failed": "删除失败"
|
||||
},
|
||||
"llm": {
|
||||
"loading": "加载中...",
|
||||
|
||||
@@ -49,7 +49,11 @@
|
||||
"name_placeholder": "輸入存檔名稱...",
|
||||
"name_tip": "留空將使用自動生成的名稱",
|
||||
"name_too_long": "名稱不能超過50個字元",
|
||||
"name_invalid_chars": "名稱只能包含中文、字母、數字和底線"
|
||||
"name_invalid_chars": "名稱只能包含中文、字母、數字和底線",
|
||||
"delete": "刪除",
|
||||
"delete_confirm": "確定要徹底刪除存檔 {filename} 及其所有資料嗎?此操作無法撤銷。",
|
||||
"delete_success": "存檔已刪除",
|
||||
"delete_failed": "刪除失敗"
|
||||
},
|
||||
"llm": {
|
||||
"loading": "載入中...",
|
||||
|
||||
Reference in New Issue
Block a user