mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
补全翻译
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
package games.wenzi.ogame;
|
package games.wenzi.ogame;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
import android.webkit.WebChromeClient;
|
import android.webkit.WebChromeClient;
|
||||||
import android.webkit.WebSettings;
|
import android.webkit.WebSettings;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
@@ -22,6 +27,29 @@ public class MainActivity extends BridgeActivity {
|
|||||||
// 保持 SplashScreen 直到 WebView 加载完成
|
// 保持 SplashScreen 直到 WebView 加载完成
|
||||||
splashScreen.setKeepOnScreenCondition(() -> !isWebViewReady);
|
splashScreen.setKeepOnScreenCondition(() -> !isWebViewReady);
|
||||||
|
|
||||||
|
// 设置淡出退出动画
|
||||||
|
splashScreen.setOnExitAnimationListener(splashScreenView -> {
|
||||||
|
// 创建淡出动画
|
||||||
|
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(
|
||||||
|
splashScreenView.getView(),
|
||||||
|
View.ALPHA,
|
||||||
|
1f,
|
||||||
|
0f
|
||||||
|
);
|
||||||
|
fadeOut.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||||
|
fadeOut.setDuration(300);
|
||||||
|
|
||||||
|
// 动画结束后移除 SplashScreen
|
||||||
|
fadeOut.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
splashScreenView.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fadeOut.start();
|
||||||
|
});
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
Window window = getWindow();
|
Window window = getWindow();
|
||||||
|
|||||||
9
android/app/src/main/res/drawable/splash_icon.xml
Normal file
9
android/app/src/main/res/drawable/splash_icon.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 启动画面专用图标,使用较小的尺寸避免模糊 -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="96dp"
|
||||||
|
android:height="96dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
|
</layer-list>
|
||||||
@@ -22,7 +22,12 @@
|
|||||||
<!-- 启动画面主题 - 延长显示直到 WebView 加载完成 -->
|
<!-- 启动画面主题 - 延长显示直到 WebView 加载完成 -->
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<item name="windowSplashScreenBackground">@color/splash_background</item>
|
<item name="windowSplashScreenBackground">@color/splash_background</item>
|
||||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
|
<!-- 使用较小的自定义图标避免模糊 -->
|
||||||
|
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||||
|
<!-- 图标大小限制 -->
|
||||||
|
<item name="windowSplashScreenIconBackgroundColor">@color/splash_background</item>
|
||||||
|
<!-- 退出动画时长 -->
|
||||||
|
<item name="android:windowSplashScreenAnimationDuration">500</item>
|
||||||
<item name="postSplashScreenTheme">@style/AppTheme.NoActionBar</item>
|
<item name="postSplashScreenTheme">@style/AppTheme.NoActionBar</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
16
src/App.vue
16
src/App.vue
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 首页:无侧边栏/头部 -->
|
<!-- 首页:无侧边栏/头部 -->
|
||||||
<template v-if="isHomePage">
|
<RouterView v-if="isHomePage" />
|
||||||
<RouterView />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 其他页面:完整布局(含侧边栏) -->
|
<!-- 其他页面:完整布局(含侧边栏) -->
|
||||||
<SidebarProvider v-else :open="sidebarOpen" @update:open="handleSidebarOpenChange">
|
<SidebarProvider v-else :open="sidebarOpen" @update:open="handleSidebarOpenChange">
|
||||||
@@ -133,13 +131,6 @@
|
|||||||
<SidebarMenuBadge v-if="item.path === '/fleet' && activeFleetMissionsCount > 0" class="bg-primary text-primary-foreground">
|
<SidebarMenuBadge v-if="item.path === '/fleet' && activeFleetMissionsCount > 0" class="bg-primary text-primary-foreground">
|
||||||
{{ activeFleetMissionsCount }}
|
{{ activeFleetMissionsCount }}
|
||||||
</SidebarMenuBadge>
|
</SidebarMenuBadge>
|
||||||
<!-- 未读外交报告数量 -->
|
|
||||||
<SidebarMenuBadge
|
|
||||||
v-if="item.path === '/diplomacy' && unreadDiplomaticReportsCount > 0"
|
|
||||||
class="bg-destructive text-destructive-foreground"
|
|
||||||
>
|
|
||||||
{{ unreadDiplomaticReportsCount }}
|
|
||||||
</SidebarMenuBadge>
|
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -738,11 +729,6 @@
|
|||||||
return fleetMissions + flyingMissiles
|
return fleetMissions + flyingMissiles
|
||||||
})
|
})
|
||||||
|
|
||||||
// 未读外交报告数量
|
|
||||||
const unreadDiplomaticReportsCount = computed(() => {
|
|
||||||
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 月球相关
|
// 月球相关
|
||||||
const moon = computed(() => {
|
const moon = computed(() => {
|
||||||
if (!planet.value || planet.value.isMoon) return null
|
if (!planet.value || planet.value.isMoon) return null
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :open="true" @update:open="handleClose">
|
<Dialog :open="true" @update:open="handleClose">
|
||||||
<DialogContent class="max-w-2xl p-0 overflow-hidden bg-gradient-to-b from-background to-background/95">
|
<DialogContent class="max-w-2xl p-0 overflow-hidden bg-gradient-to-b from-background to-background/95">
|
||||||
|
<!-- 可访问性标题(隐藏) -->
|
||||||
|
<VisuallyHidden>
|
||||||
|
<DialogTitle>{{ t('campaign.dialogue.title') }}</DialogTitle>
|
||||||
|
<DialogDescription>{{ t('campaign.dialogue.description') }}</DialogDescription>
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
<!-- 对话框头部 - 星空效果 -->
|
<!-- 对话框头部 - 星空效果 -->
|
||||||
<div class="absolute inset-0 pointer-events-none overflow-hidden">
|
<div class="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
<div class="stars-bg" />
|
<div class="stars-bg" />
|
||||||
@@ -80,7 +86,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||||
|
import { VisuallyHidden } from 'reka-ui'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import type { StoryDialogue, DialogueChoice } from '@/types/game'
|
import type { StoryDialogue, DialogueChoice } from '@/types/game'
|
||||||
import { User, Bot, HelpCircle, MessageCircle, ChevronRight } from 'lucide-vue-next'
|
import { User, Bot, HelpCircle, MessageCircle, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
// 使用纯色背景,在 Android WebView 中更可靠
|
// 使用纯色背景,在 Android WebView 中更可靠
|
||||||
const colors = {
|
const colors = {
|
||||||
metal: 'bg-slate-500',
|
metal: 'bg-gradient-to-br from-slate-400 to-slate-600',
|
||||||
crystal: 'bg-cyan-500',
|
crystal: 'bg-gradient-to-br from-cyan-400 to-blue-600',
|
||||||
deuterium: 'bg-green-500',
|
deuterium: 'bg-gradient-to-br from-green-400 to-emerald-600',
|
||||||
darkMatter: 'bg-purple-700',
|
darkMatter: 'bg-gradient-to-br from-purple-600 to-indigo-900',
|
||||||
energy: 'bg-yellow-500'
|
energy: 'bg-gradient-to-br from-yellow-400 to-orange-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
|
|||||||
@@ -152,7 +152,11 @@ const handleTest = async () => {
|
|||||||
testResult.value = null
|
testResult.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
testResult.value = await testWebDAVConnection(config.value)
|
const result = await testWebDAVConnection(config.value)
|
||||||
|
testResult.value = {
|
||||||
|
success: result.success,
|
||||||
|
message: t(result.messageKey)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isTesting.value = false
|
isTesting.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const loadFiles = async () => {
|
|||||||
if (result.success && result.files) {
|
if (result.success && result.files) {
|
||||||
files.value = result.files
|
files.value = result.files
|
||||||
} else {
|
} else {
|
||||||
error.value = result.message || t('settings.webdav.loadFailed')
|
error.value = t(result.messageKey) || t('settings.webdav.loadFailed')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : String(e)
|
error.value = e instanceof Error ? e.message : String(e)
|
||||||
@@ -174,7 +174,7 @@ const handleDelete = async (fileName: string) => {
|
|||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || t('settings.webdav.deleteFailed'))
|
toast.error(t(result.messageKey) || t('settings.webdav.deleteFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -549,6 +549,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
recycle: 'Recyceln',
|
recycle: 'Recyceln',
|
||||||
|
harvestDarkMatter: 'Dunkle Materie ernten',
|
||||||
|
station: 'Stationieren',
|
||||||
transportResources: 'Ressourcen transportieren',
|
transportResources: 'Ressourcen transportieren',
|
||||||
totalCargoCapacity: 'Gesamtladekapazität',
|
totalCargoCapacity: 'Gesamtladekapazität',
|
||||||
used: 'Verwendet',
|
used: 'Verwendet',
|
||||||
@@ -1823,6 +1825,27 @@ export default {
|
|||||||
reputationBonusDesc: 'Dein Verbündeter {npcName} spricht gut von dir zu {targetNpc}'
|
reputationBonusDesc: 'Dein Verbündeter {npcName} spricht gut von dir zu {targetNpc}'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webdav: {
|
||||||
|
connectionSuccess: 'WebDAV-Verbindung erfolgreich',
|
||||||
|
connectionSuccessDirectoryCreated: 'WebDAV-Verbindung erfolgreich, Speicherverzeichnis erstellt',
|
||||||
|
authFailed: 'Authentifizierung fehlgeschlagen, bitte Benutzername und Passwort überprüfen',
|
||||||
|
directoryNotExist: 'Verzeichnis existiert nicht und konnte nicht erstellt werden',
|
||||||
|
networkError: 'Netzwerkfehler, bitte Serveradresse und Netzwerk überprüfen',
|
||||||
|
unknownError: 'Unbekannter Fehler',
|
||||||
|
uploadSuccess: 'Spielstand erfolgreich hochgeladen',
|
||||||
|
uploadFailed: 'Hochladen fehlgeschlagen',
|
||||||
|
downloadSuccess: 'Spielstand erfolgreich heruntergeladen',
|
||||||
|
downloadFailed: 'Herunterladen fehlgeschlagen',
|
||||||
|
noSaveFiles: 'Keine Spielstände auf dem Server',
|
||||||
|
fileListSuccess: 'Spielstandliste erfolgreich abgerufen',
|
||||||
|
fileListFailed: 'Abrufen der Spielstandliste fehlgeschlagen',
|
||||||
|
deleteSuccess: 'Spielstand erfolgreich gelöscht',
|
||||||
|
deleteFailed: 'Löschen fehlgeschlagen',
|
||||||
|
serverError: 'Serverfehler',
|
||||||
|
notConfigured: 'WebDAV nicht konfiguriert',
|
||||||
|
invalidUrl: 'Ungültige WebDAV-URL',
|
||||||
|
timeout: 'Verbindungszeitüberschreitung'
|
||||||
|
},
|
||||||
campaign: {
|
campaign: {
|
||||||
name: 'Kampagne',
|
name: 'Kampagne',
|
||||||
description: 'Erkunde die mysteriöse Galaxie und entdecke antike Geheimnisse',
|
description: 'Erkunde die mysteriöse Galaxie und entdecke antike Geheimnisse',
|
||||||
@@ -1849,6 +1872,8 @@ export default {
|
|||||||
branchUnlocked: 'Neuer Storyzweig freigeschaltet!'
|
branchUnlocked: 'Neuer Storyzweig freigeschaltet!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: 'Story-Dialog',
|
||||||
|
description: 'Kampagnen-Story-Dialoginhalt',
|
||||||
skip: 'Überspringen',
|
skip: 'Überspringen',
|
||||||
continue: 'Weiter',
|
continue: 'Weiter',
|
||||||
finish: 'Beenden',
|
finish: 'Beenden',
|
||||||
|
|||||||
@@ -532,6 +532,8 @@ export default {
|
|||||||
},
|
},
|
||||||
recycle: 'Recycle',
|
recycle: 'Recycle',
|
||||||
destroy: 'Planet Destruction',
|
destroy: 'Planet Destruction',
|
||||||
|
harvestDarkMatter: 'Harvest Dark Matter',
|
||||||
|
station: 'Station',
|
||||||
transportResources: 'Transport Resources',
|
transportResources: 'Transport Resources',
|
||||||
totalCargoCapacity: 'Total Cargo Capacity',
|
totalCargoCapacity: 'Total Cargo Capacity',
|
||||||
used: 'Used',
|
used: 'Used',
|
||||||
@@ -1781,6 +1783,8 @@ export default {
|
|||||||
branchUnlocked: 'New story branch unlocked!'
|
branchUnlocked: 'New story branch unlocked!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: 'Story Dialogue',
|
||||||
|
description: 'Campaign story dialogue content',
|
||||||
skip: 'Skip',
|
skip: 'Skip',
|
||||||
continue: 'Continue',
|
continue: 'Continue',
|
||||||
finish: 'Finish',
|
finish: 'Finish',
|
||||||
@@ -2207,5 +2211,31 @@ export default {
|
|||||||
reputationBonus: 'Reputation Bonus',
|
reputationBonus: 'Reputation Bonus',
|
||||||
reputationBonusDesc: 'Your ally {npcName} speaks well of you to {targetNpc}'
|
reputationBonusDesc: 'Your ally {npcName} speaks well of you to {targetNpc}'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
webdav: {
|
||||||
|
// Connection
|
||||||
|
connectionSuccess: 'WebDAV connection successful',
|
||||||
|
connectionSuccessDirectoryCreated: 'WebDAV connection successful, save directory created',
|
||||||
|
authFailed: 'Authentication failed, please check username and password',
|
||||||
|
directoryNotExist: 'Directory does not exist and cannot be created',
|
||||||
|
connectionFailedHttp: 'Connection failed: HTTP {status}',
|
||||||
|
networkError: 'Network error, possibly CORS restriction. Try using a CORS-enabled WebDAV service or proxy.',
|
||||||
|
connectionError: 'Connection error: {error}',
|
||||||
|
// Upload
|
||||||
|
uploadSuccess: 'Upload successful',
|
||||||
|
noWritePermission: 'No write permission',
|
||||||
|
insufficientStorage: 'Insufficient storage space',
|
||||||
|
uploadFailedHttp: 'Upload failed: HTTP {status}',
|
||||||
|
uploadError: 'Upload error: {error}',
|
||||||
|
// Download
|
||||||
|
fileNotExist: 'File does not exist',
|
||||||
|
downloadFailedHttp: 'Download failed: HTTP {status}',
|
||||||
|
downloadError: 'Download error: {error}',
|
||||||
|
// List
|
||||||
|
listFailedHttp: 'Failed to get file list: HTTP {status}',
|
||||||
|
listError: 'Error getting file list: {error}',
|
||||||
|
// Delete
|
||||||
|
deleteFailedHttp: 'Delete failed: HTTP {status}',
|
||||||
|
deleteError: 'Delete error: {error}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,6 +538,8 @@ export default {
|
|||||||
},
|
},
|
||||||
recycle: 'Reciclar',
|
recycle: 'Reciclar',
|
||||||
destroy: 'Destrucción Planetaria',
|
destroy: 'Destrucción Planetaria',
|
||||||
|
harvestDarkMatter: 'Recolectar Materia Oscura',
|
||||||
|
station: 'Estacionar',
|
||||||
transportResources: 'Transportar Recursos',
|
transportResources: 'Transportar Recursos',
|
||||||
totalCargoCapacity: 'Capacidad de Carga Total',
|
totalCargoCapacity: 'Capacidad de Carga Total',
|
||||||
used: 'Usado',
|
used: 'Usado',
|
||||||
@@ -1785,6 +1787,8 @@ export default {
|
|||||||
branchUnlocked: '¡Nueva rama de historia desbloqueada!'
|
branchUnlocked: '¡Nueva rama de historia desbloqueada!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: 'Diálogo de historia',
|
||||||
|
description: 'Contenido del diálogo de la campaña',
|
||||||
skip: 'Saltar',
|
skip: 'Saltar',
|
||||||
continue: 'Continuar',
|
continue: 'Continuar',
|
||||||
finish: 'Finalizar',
|
finish: 'Finalizar',
|
||||||
@@ -2215,5 +2219,26 @@ export default {
|
|||||||
reputationBonus: 'Bonificación de Reputación',
|
reputationBonus: 'Bonificación de Reputación',
|
||||||
reputationBonusDesc: 'Tu aliado {npcName} habla bien de ti ante {targetNpc}'
|
reputationBonusDesc: 'Tu aliado {npcName} habla bien de ti ante {targetNpc}'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
webdav: {
|
||||||
|
connectionSuccess: 'Conexión WebDAV exitosa',
|
||||||
|
connectionSuccessDirectoryCreated: 'Conexión WebDAV exitosa, directorio de guardado creado',
|
||||||
|
authFailed: 'Autenticación fallida, verifica el nombre de usuario y contraseña',
|
||||||
|
directoryNotExist: 'El directorio no existe y no se puede crear',
|
||||||
|
networkError: 'Error de red, verifica la dirección del servidor y la red',
|
||||||
|
unknownError: 'Error desconocido',
|
||||||
|
uploadSuccess: 'Guardado subido exitosamente',
|
||||||
|
uploadFailed: 'Error al subir',
|
||||||
|
downloadSuccess: 'Guardado descargado exitosamente',
|
||||||
|
downloadFailed: 'Error al descargar',
|
||||||
|
noSaveFiles: 'No hay archivos de guardado en el servidor',
|
||||||
|
fileListSuccess: 'Lista de guardados obtenida exitosamente',
|
||||||
|
fileListFailed: 'Error al obtener la lista de guardados',
|
||||||
|
deleteSuccess: 'Guardado eliminado exitosamente',
|
||||||
|
deleteFailed: 'Error al eliminar',
|
||||||
|
serverError: 'Error del servidor',
|
||||||
|
notConfigured: 'WebDAV no configurado',
|
||||||
|
invalidUrl: 'URL de WebDAV inválida',
|
||||||
|
timeout: 'Tiempo de conexión agotado'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,7 +641,9 @@ export default {
|
|||||||
jumpGateSuccessMessage: '艦隊は{target}へ瞬時に転送されました',
|
jumpGateSuccessMessage: '艦隊は{target}へ瞬時に転送されました',
|
||||||
jumpGateFailed: 'ジャンプゲート転送失敗',
|
jumpGateFailed: 'ジャンプゲート転送失敗',
|
||||||
jumpGateFailedMessage: 'ジャンプゲートの状態と艦隊構成を確認してください',
|
jumpGateFailedMessage: 'ジャンプゲートの状態と艦隊構成を確認してください',
|
||||||
destroy: '破壊'
|
destroy: '破壊',
|
||||||
|
harvestDarkMatter: 'ダークマター採取',
|
||||||
|
station: '駐留'
|
||||||
},
|
},
|
||||||
officersView: {
|
officersView: {
|
||||||
title: '士官',
|
title: '士官',
|
||||||
@@ -1854,6 +1856,27 @@ export default {
|
|||||||
reputationBonusDesc: '同盟の{npcName}が{targetNpc}にあなたのことを良く言っています'
|
reputationBonusDesc: '同盟の{npcName}が{targetNpc}にあなたのことを良く言っています'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webdav: {
|
||||||
|
connectionSuccess: 'WebDAV接続成功',
|
||||||
|
connectionSuccessDirectoryCreated: 'WebDAV接続成功、保存ディレクトリを作成しました',
|
||||||
|
authFailed: '認証失敗、ユーザー名とパスワードを確認してください',
|
||||||
|
directoryNotExist: 'ディレクトリが存在せず、作成できませんでした',
|
||||||
|
networkError: 'ネットワークエラー、サーバーアドレスとネットワークを確認してください',
|
||||||
|
unknownError: '不明なエラー',
|
||||||
|
uploadSuccess: 'セーブデータのアップロード成功',
|
||||||
|
uploadFailed: 'アップロード失敗',
|
||||||
|
downloadSuccess: 'セーブデータのダウンロード成功',
|
||||||
|
downloadFailed: 'ダウンロード失敗',
|
||||||
|
noSaveFiles: 'サーバーにセーブデータがありません',
|
||||||
|
fileListSuccess: 'セーブデータリストの取得成功',
|
||||||
|
fileListFailed: 'セーブデータリストの取得失敗',
|
||||||
|
deleteSuccess: 'セーブデータの削除成功',
|
||||||
|
deleteFailed: '削除失敗',
|
||||||
|
serverError: 'サーバーエラー',
|
||||||
|
notConfigured: 'WebDAVが設定されていません',
|
||||||
|
invalidUrl: '無効なWebDAV URL',
|
||||||
|
timeout: '接続タイムアウト'
|
||||||
|
},
|
||||||
campaign: {
|
campaign: {
|
||||||
name: 'キャンペーン',
|
name: 'キャンペーン',
|
||||||
description: '神秘的な銀河を探索し、古代の秘密を解き明かす',
|
description: '神秘的な銀河を探索し、古代の秘密を解き明かす',
|
||||||
@@ -1881,6 +1904,8 @@ export default {
|
|||||||
branchUnlocked: '新しいストーリー分岐が解放されました!'
|
branchUnlocked: '新しいストーリー分岐が解放されました!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: 'ストーリー対話',
|
||||||
|
description: 'キャンペーンストーリーの対話内容',
|
||||||
skip: 'スキップ',
|
skip: 'スキップ',
|
||||||
continue: '続ける',
|
continue: '続ける',
|
||||||
finish: '完了',
|
finish: '完了',
|
||||||
|
|||||||
@@ -532,6 +532,8 @@ export default {
|
|||||||
},
|
},
|
||||||
recycle: '회수',
|
recycle: '회수',
|
||||||
destroy: '행성 파괴',
|
destroy: '행성 파괴',
|
||||||
|
harvestDarkMatter: '암흑 물질 수확',
|
||||||
|
station: '주둔',
|
||||||
transportResources: '자원 수송',
|
transportResources: '자원 수송',
|
||||||
totalCargoCapacity: '총 적재량',
|
totalCargoCapacity: '총 적재량',
|
||||||
used: '사용됨',
|
used: '사용됨',
|
||||||
@@ -1795,6 +1797,27 @@ export default {
|
|||||||
reputationBonusDesc: '동맹 {npcName}이(가) {targetNpc}에게 당신을 좋게 말하고 있습니다'
|
reputationBonusDesc: '동맹 {npcName}이(가) {targetNpc}에게 당신을 좋게 말하고 있습니다'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webdav: {
|
||||||
|
connectionSuccess: 'WebDAV 연결 성공',
|
||||||
|
connectionSuccessDirectoryCreated: 'WebDAV 연결 성공, 저장 디렉토리 생성됨',
|
||||||
|
authFailed: '인증 실패, 사용자 이름과 비밀번호를 확인하세요',
|
||||||
|
directoryNotExist: '디렉토리가 존재하지 않으며 생성할 수 없습니다',
|
||||||
|
networkError: '네트워크 오류, 서버 주소와 네트워크를 확인하세요',
|
||||||
|
unknownError: '알 수 없는 오류',
|
||||||
|
uploadSuccess: '저장 파일 업로드 성공',
|
||||||
|
uploadFailed: '업로드 실패',
|
||||||
|
downloadSuccess: '저장 파일 다운로드 성공',
|
||||||
|
downloadFailed: '다운로드 실패',
|
||||||
|
noSaveFiles: '서버에 저장 파일이 없습니다',
|
||||||
|
fileListSuccess: '저장 파일 목록 가져오기 성공',
|
||||||
|
fileListFailed: '저장 파일 목록 가져오기 실패',
|
||||||
|
deleteSuccess: '저장 파일 삭제 성공',
|
||||||
|
deleteFailed: '삭제 실패',
|
||||||
|
serverError: '서버 오류',
|
||||||
|
notConfigured: 'WebDAV가 구성되지 않았습니다',
|
||||||
|
invalidUrl: '잘못된 WebDAV URL',
|
||||||
|
timeout: '연결 시간 초과'
|
||||||
|
},
|
||||||
campaign: {
|
campaign: {
|
||||||
name: '캠페인',
|
name: '캠페인',
|
||||||
description: '신비로운 은하를 탐험하고 고대의 비밀을 밝혀내세요',
|
description: '신비로운 은하를 탐험하고 고대의 비밀을 밝혀내세요',
|
||||||
@@ -1821,6 +1844,8 @@ export default {
|
|||||||
branchUnlocked: '새로운 스토리 분기가 해금되었습니다!'
|
branchUnlocked: '새로운 스토리 분기가 해금되었습니다!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: '스토리 대화',
|
||||||
|
description: '캠페인 스토리 대화 내용',
|
||||||
skip: '건너뛰기',
|
skip: '건너뛰기',
|
||||||
continue: '계속',
|
continue: '계속',
|
||||||
finish: '완료',
|
finish: '완료',
|
||||||
|
|||||||
@@ -622,7 +622,9 @@ export default {
|
|||||||
jumpGateSuccessMessage: 'Флот мгновенно переброшен к {target}',
|
jumpGateSuccessMessage: 'Флот мгновенно переброшен к {target}',
|
||||||
jumpGateFailed: 'Переброска через ворота не удалась',
|
jumpGateFailed: 'Переброска через ворота не удалась',
|
||||||
jumpGateFailedMessage: 'Проверьте состояние ворот и конфигурацию флота',
|
jumpGateFailedMessage: 'Проверьте состояние ворот и конфигурацию флота',
|
||||||
destroy: 'Уничтожение'
|
destroy: 'Уничтожение',
|
||||||
|
harvestDarkMatter: 'Сбор тёмной материи',
|
||||||
|
station: 'Расположение'
|
||||||
},
|
},
|
||||||
officersView: {
|
officersView: {
|
||||||
title: 'Офицеры',
|
title: 'Офицеры',
|
||||||
@@ -1851,6 +1853,27 @@ export default {
|
|||||||
reputationBonusDesc: 'Ваш союзник {npcName} хорошо отзывается о вас {targetNpc}'
|
reputationBonusDesc: 'Ваш союзник {npcName} хорошо отзывается о вас {targetNpc}'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webdav: {
|
||||||
|
connectionSuccess: 'Подключение WebDAV успешно',
|
||||||
|
connectionSuccessDirectoryCreated: 'Подключение WebDAV успешно, каталог сохранений создан',
|
||||||
|
authFailed: 'Ошибка аутентификации, проверьте имя пользователя и пароль',
|
||||||
|
directoryNotExist: 'Каталог не существует и не может быть создан',
|
||||||
|
networkError: 'Ошибка сети, проверьте адрес сервера и сеть',
|
||||||
|
unknownError: 'Неизвестная ошибка',
|
||||||
|
uploadSuccess: 'Сохранение успешно загружено',
|
||||||
|
uploadFailed: 'Ошибка загрузки',
|
||||||
|
downloadSuccess: 'Сохранение успешно скачано',
|
||||||
|
downloadFailed: 'Ошибка скачивания',
|
||||||
|
noSaveFiles: 'На сервере нет сохранений',
|
||||||
|
fileListSuccess: 'Список сохранений получен',
|
||||||
|
fileListFailed: 'Ошибка получения списка сохранений',
|
||||||
|
deleteSuccess: 'Сохранение успешно удалено',
|
||||||
|
deleteFailed: 'Ошибка удаления',
|
||||||
|
serverError: 'Ошибка сервера',
|
||||||
|
notConfigured: 'WebDAV не настроен',
|
||||||
|
invalidUrl: 'Недействительный URL WebDAV',
|
||||||
|
timeout: 'Превышено время ожидания'
|
||||||
|
},
|
||||||
campaign: {
|
campaign: {
|
||||||
name: 'Кампания',
|
name: 'Кампания',
|
||||||
description: 'Исследуйте загадочную галактику и раскройте древние тайны',
|
description: 'Исследуйте загадочную галактику и раскройте древние тайны',
|
||||||
@@ -1878,6 +1901,8 @@ export default {
|
|||||||
branchUnlocked: 'Новая сюжетная ветка разблокирована!'
|
branchUnlocked: 'Новая сюжетная ветка разблокирована!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: 'Сюжетный диалог',
|
||||||
|
description: 'Содержание сюжетного диалога кампании',
|
||||||
skip: 'Пропустить',
|
skip: 'Пропустить',
|
||||||
continue: 'Продолжить',
|
continue: 'Продолжить',
|
||||||
finish: 'Завершить',
|
finish: 'Завершить',
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export default {
|
|||||||
exitConfirmTitle: '退出游戏',
|
exitConfirmTitle: '退出游戏',
|
||||||
exitConfirmMessage: '确定要退出游戏吗?游戏进度会自动保存。',
|
exitConfirmMessage: '确定要退出游戏吗?游戏进度会自动保存。',
|
||||||
points: '积分',
|
points: '积分',
|
||||||
retry: '重试'
|
retry: '重试',
|
||||||
|
playerName: '玩家名称'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: '不满足前置条件',
|
requirementsNotMet: '不满足前置条件',
|
||||||
@@ -525,6 +526,8 @@ export default {
|
|||||||
},
|
},
|
||||||
recycle: '回收',
|
recycle: '回收',
|
||||||
destroy: '行星毁灭',
|
destroy: '行星毁灭',
|
||||||
|
harvestDarkMatter: '暗物质采集',
|
||||||
|
station: '驻守协防',
|
||||||
transportResources: '运输资源',
|
transportResources: '运输资源',
|
||||||
totalCargoCapacity: '总载货量',
|
totalCargoCapacity: '总载货量',
|
||||||
used: '已用',
|
used: '已用',
|
||||||
@@ -1796,6 +1799,8 @@ export default {
|
|||||||
branchUnlocked: '新的故事分支已解锁!'
|
branchUnlocked: '新的故事分支已解锁!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: '剧情对话',
|
||||||
|
description: '战役剧情对话内容',
|
||||||
skip: '跳过',
|
skip: '跳过',
|
||||||
continue: '继续',
|
continue: '继续',
|
||||||
finish: '完成',
|
finish: '完成',
|
||||||
@@ -2106,5 +2111,31 @@ export default {
|
|||||||
epilogue_1: '银河系广阔无垠,还有无数秘密等待你去发现...'
|
epilogue_1: '银河系广阔无垠,还有无数秘密等待你去发现...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
webdav: {
|
||||||
|
// 连接相关
|
||||||
|
connectionSuccess: 'WebDAV 连接成功',
|
||||||
|
connectionSuccessDirectoryCreated: 'WebDAV 连接成功,已创建存档目录',
|
||||||
|
authFailed: '认证失败,请检查用户名和密码',
|
||||||
|
directoryNotExist: '目录不存在且无法创建',
|
||||||
|
connectionFailedHttp: '连接失败: HTTP {status}',
|
||||||
|
networkError: '网络错误,可能是 CORS 限制。建议使用支持 CORS 的 WebDAV 服务或通过代理访问。',
|
||||||
|
connectionError: '连接错误: {error}',
|
||||||
|
// 上传相关
|
||||||
|
uploadSuccess: '上传成功',
|
||||||
|
noWritePermission: '没有写入权限',
|
||||||
|
insufficientStorage: '存储空间不足',
|
||||||
|
uploadFailedHttp: '上传失败: HTTP {status}',
|
||||||
|
uploadError: '上传错误: {error}',
|
||||||
|
// 下载相关
|
||||||
|
fileNotExist: '文件不存在',
|
||||||
|
downloadFailedHttp: '下载失败: HTTP {status}',
|
||||||
|
downloadError: '下载错误: {error}',
|
||||||
|
// 列表相关
|
||||||
|
listFailedHttp: '获取文件列表失败: HTTP {status}',
|
||||||
|
listError: '获取文件列表错误: {error}',
|
||||||
|
// 删除相关
|
||||||
|
deleteFailedHttp: '删除失败: HTTP {status}',
|
||||||
|
deleteError: '删除错误: {error}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -542,6 +542,8 @@ export default {
|
|||||||
},
|
},
|
||||||
recycle: '回收',
|
recycle: '回收',
|
||||||
destroy: '行星毀滅',
|
destroy: '行星毀滅',
|
||||||
|
harvestDarkMatter: '暗物質採集',
|
||||||
|
station: '駐守協防',
|
||||||
transportResources: '運輸資源',
|
transportResources: '運輸資源',
|
||||||
totalCargoCapacity: '總載貨量',
|
totalCargoCapacity: '總載貨量',
|
||||||
used: '已用',
|
used: '已用',
|
||||||
@@ -1945,6 +1947,27 @@ export default {
|
|||||||
reputationBonusDesc: '你的盟友{npcName}向{targetNpc}說了你的好話'
|
reputationBonusDesc: '你的盟友{npcName}向{targetNpc}說了你的好話'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webdav: {
|
||||||
|
connectionSuccess: 'WebDAV 連線成功',
|
||||||
|
connectionSuccessDirectoryCreated: 'WebDAV 連線成功,已建立存檔目錄',
|
||||||
|
authFailed: '認證失敗,請檢查使用者名稱和密碼',
|
||||||
|
directoryNotExist: '目錄不存在且無法建立',
|
||||||
|
networkError: '網路錯誤,請檢查伺服器位址和網路連線',
|
||||||
|
unknownError: '未知錯誤',
|
||||||
|
uploadSuccess: '存檔上傳成功',
|
||||||
|
uploadFailed: '上傳失敗',
|
||||||
|
downloadSuccess: '存檔下載成功',
|
||||||
|
downloadFailed: '下載失敗',
|
||||||
|
noSaveFiles: '伺服器上沒有存檔',
|
||||||
|
fileListSuccess: '取得存檔列表成功',
|
||||||
|
fileListFailed: '取得存檔列表失敗',
|
||||||
|
deleteSuccess: '存檔刪除成功',
|
||||||
|
deleteFailed: '刪除失敗',
|
||||||
|
serverError: '伺服器錯誤',
|
||||||
|
notConfigured: 'WebDAV 未設定',
|
||||||
|
invalidUrl: '無效的 WebDAV URL',
|
||||||
|
timeout: '連線逾時'
|
||||||
|
},
|
||||||
campaign: {
|
campaign: {
|
||||||
name: '戰役',
|
name: '戰役',
|
||||||
description: '探索神秘的銀河系,揭開古代文明的秘密',
|
description: '探索神秘的銀河系,揭開古代文明的秘密',
|
||||||
@@ -1972,6 +1995,8 @@ export default {
|
|||||||
branchUnlocked: '新的故事分支已解鎖!'
|
branchUnlocked: '新的故事分支已解鎖!'
|
||||||
},
|
},
|
||||||
dialogue: {
|
dialogue: {
|
||||||
|
title: '劇情對話',
|
||||||
|
description: '戰役劇情對話內容',
|
||||||
skip: '跳過',
|
skip: '跳過',
|
||||||
continue: '繼續',
|
continue: '繼續',
|
||||||
finish: '完成',
|
finish: '完成',
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface WebDAVConfig {
|
export interface WebDAVConfig {
|
||||||
serverUrl: string // WebDAV 服务器地址,如 https://dav.jianguoyun.com/dav/
|
serverUrl: string // WebDAV 服务器地址
|
||||||
username: string // 用户名
|
username: string // 用户名
|
||||||
password: string // 密码或应用专用密码
|
password: string // 密码或应用专用密码
|
||||||
basePath: string // 存档存放路径,如 /ogame-saves/
|
basePath: string // 存档存放路径
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebDAVFile {
|
export interface WebDAVFile {
|
||||||
@@ -18,6 +18,49 @@ export interface WebDAVFile {
|
|||||||
isDirectory: boolean
|
isDirectory: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebDAV 消息 key(用于 i18n)
|
||||||
|
export const WebDAVMessageKey = {
|
||||||
|
// 连接相关
|
||||||
|
connectionSuccess: 'webdav.connectionSuccess',
|
||||||
|
connectionSuccessDirectoryCreated: 'webdav.connectionSuccessDirectoryCreated',
|
||||||
|
authFailed: 'webdav.authFailed',
|
||||||
|
directoryNotExist: 'webdav.directoryNotExist',
|
||||||
|
connectionFailedHttp: 'webdav.connectionFailedHttp',
|
||||||
|
networkError: 'webdav.networkError',
|
||||||
|
connectionError: 'webdav.connectionError',
|
||||||
|
|
||||||
|
// 上传相关
|
||||||
|
uploadSuccess: 'webdav.uploadSuccess',
|
||||||
|
noWritePermission: 'webdav.noWritePermission',
|
||||||
|
insufficientStorage: 'webdav.insufficientStorage',
|
||||||
|
uploadFailedHttp: 'webdav.uploadFailedHttp',
|
||||||
|
uploadError: 'webdav.uploadError',
|
||||||
|
|
||||||
|
// 下载相关
|
||||||
|
fileNotExist: 'webdav.fileNotExist',
|
||||||
|
downloadFailedHttp: 'webdav.downloadFailedHttp',
|
||||||
|
downloadError: 'webdav.downloadError',
|
||||||
|
|
||||||
|
// 列表相关
|
||||||
|
listFailedHttp: 'webdav.listFailedHttp',
|
||||||
|
listError: 'webdav.listError',
|
||||||
|
|
||||||
|
// 删除相关
|
||||||
|
deleteFailedHttp: 'webdav.deleteFailedHttp',
|
||||||
|
deleteError: 'webdav.deleteError'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type WebDAVMessageKeyType = (typeof WebDAVMessageKey)[keyof typeof WebDAVMessageKey]
|
||||||
|
|
||||||
|
export interface WebDAVResult {
|
||||||
|
success: boolean
|
||||||
|
messageKey: WebDAVMessageKeyType
|
||||||
|
messageParams?: Record<string, string | number>
|
||||||
|
fileName?: string
|
||||||
|
data?: string
|
||||||
|
files?: WebDAVFile[]
|
||||||
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'ogame-webdav-config'
|
const STORAGE_KEY = 'ogame-webdav-config'
|
||||||
|
|
||||||
// 获取保存的 WebDAV 配置
|
// 获取保存的 WebDAV 配置
|
||||||
@@ -66,7 +109,7 @@ const normalizePath = (serverUrl: string, basePath: string, fileName?: string):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 测试 WebDAV 连接
|
// 测试 WebDAV 连接
|
||||||
export const testWebDAVConnection = async (config: WebDAVConfig): Promise<{ success: boolean; message: string }> => {
|
export const testWebDAVConnection = async (config: WebDAVConfig): Promise<WebDAVResult> => {
|
||||||
try {
|
try {
|
||||||
const url = normalizePath(config.serverUrl, config.basePath)
|
const url = normalizePath(config.serverUrl, config.basePath)
|
||||||
|
|
||||||
@@ -80,33 +123,30 @@ export const testWebDAVConnection = async (config: WebDAVConfig): Promise<{ succ
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok || response.status === 207) {
|
if (response.ok || response.status === 207) {
|
||||||
return { success: true, message: 'WebDAV 连接成功' }
|
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return { success: false, message: '认证失败,请检查用户名和密码' }
|
return { success: false, messageKey: WebDAVMessageKey.authFailed }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
// 尝试创建目录
|
// 尝试创建目录
|
||||||
const createResult = await createDirectory(config, config.basePath)
|
const createResult = await createDirectory(config, config.basePath)
|
||||||
if (createResult) {
|
if (createResult) {
|
||||||
return { success: true, message: 'WebDAV 连接成功,已创建存档目录' }
|
return { success: true, messageKey: WebDAVMessageKey.connectionSuccessDirectoryCreated }
|
||||||
}
|
}
|
||||||
return { success: false, message: '目录不存在且无法创建' }
|
return { success: false, messageKey: WebDAVMessageKey.directoryNotExist }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, message: `连接失败: HTTP ${response.status}` }
|
return { success: false, messageKey: WebDAVMessageKey.connectionFailedHttp, messageParams: { status: response.status } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
// CORS 错误的处理
|
// CORS 错误的处理
|
||||||
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
|
if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) {
|
||||||
return {
|
return { success: false, messageKey: WebDAVMessageKey.networkError }
|
||||||
success: false,
|
|
||||||
message: '网络错误,可能是 CORS 限制。建议使用支持 CORS 的 WebDAV 服务或通过代理访问。'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { success: false, message: `连接错误: ${message}` }
|
return { success: false, messageKey: WebDAVMessageKey.connectionError, messageParams: { error: errorMessage } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,11 +170,7 @@ const createDirectory = async (config: WebDAVConfig, path: string): Promise<bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传存档到 WebDAV
|
// 上传存档到 WebDAV
|
||||||
export const uploadToWebDAV = async (
|
export const uploadToWebDAV = async (config: WebDAVConfig, data: string, fileName?: string): Promise<WebDAVResult> => {
|
||||||
config: WebDAVConfig,
|
|
||||||
data: string,
|
|
||||||
fileName?: string
|
|
||||||
): Promise<{ success: boolean; message: string; fileName?: string }> => {
|
|
||||||
try {
|
try {
|
||||||
// 生成带时间戳的文件名
|
// 生成带时间戳的文件名
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
||||||
@@ -151,25 +187,25 @@ export const uploadToWebDAV = async (
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok || response.status === 201 || response.status === 204) {
|
if (response.ok || response.status === 201 || response.status === 204) {
|
||||||
return { success: true, message: '上传成功', fileName: actualFileName }
|
return { success: true, messageKey: WebDAVMessageKey.uploadSuccess, fileName: actualFileName }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return { success: false, message: '认证失败' }
|
return { success: false, messageKey: WebDAVMessageKey.authFailed }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
return { success: false, message: '没有写入权限' }
|
return { success: false, messageKey: WebDAVMessageKey.noWritePermission }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 507) {
|
if (response.status === 507) {
|
||||||
return { success: false, message: '存储空间不足' }
|
return { success: false, messageKey: WebDAVMessageKey.insufficientStorage }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, message: `上传失败: HTTP ${response.status}` }
|
return { success: false, messageKey: WebDAVMessageKey.uploadFailedHttp, messageParams: { status: response.status } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
return { success: false, message: `上传错误: ${message}` }
|
return { success: false, messageKey: WebDAVMessageKey.uploadError, messageParams: { error: errorMessage } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +248,7 @@ const parsePropfindResponse = (xml: string, _basePath: string): WebDAVFile[] =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 列出 WebDAV 目录中的存档文件
|
// 列出 WebDAV 目录中的存档文件
|
||||||
export const listWebDAVFiles = async (config: WebDAVConfig): Promise<{ success: boolean; files?: WebDAVFile[]; message?: string }> => {
|
export const listWebDAVFiles = async (config: WebDAVConfig): Promise<WebDAVResult> => {
|
||||||
try {
|
try {
|
||||||
const url = normalizePath(config.serverUrl, config.basePath)
|
const url = normalizePath(config.serverUrl, config.basePath)
|
||||||
|
|
||||||
@@ -227,29 +263,26 @@ export const listWebDAVFiles = async (config: WebDAVConfig): Promise<{ success:
|
|||||||
|
|
||||||
if (!response.ok && response.status !== 207) {
|
if (!response.ok && response.status !== 207) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return { success: false, message: '认证失败' }
|
return { success: false, messageKey: WebDAVMessageKey.authFailed }
|
||||||
}
|
}
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return { success: false, message: '目录不存在' }
|
return { success: false, messageKey: WebDAVMessageKey.directoryNotExist }
|
||||||
}
|
}
|
||||||
return { success: false, message: `获取文件列表失败: HTTP ${response.status}` }
|
return { success: false, messageKey: WebDAVMessageKey.listFailedHttp, messageParams: { status: response.status } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const xml = await response.text()
|
const xml = await response.text()
|
||||||
const files = parsePropfindResponse(xml, config.basePath)
|
const files = parsePropfindResponse(xml, config.basePath)
|
||||||
|
|
||||||
return { success: true, files }
|
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess, files }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
return { success: false, message: `获取文件列表错误: ${message}` }
|
return { success: false, messageKey: WebDAVMessageKey.listError, messageParams: { error: errorMessage } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 WebDAV 下载存档
|
// 从 WebDAV 下载存档
|
||||||
export const downloadFromWebDAV = async (
|
export const downloadFromWebDAV = async (config: WebDAVConfig, fileName: string): Promise<WebDAVResult> => {
|
||||||
config: WebDAVConfig,
|
|
||||||
fileName: string
|
|
||||||
): Promise<{ success: boolean; data?: string; message?: string }> => {
|
|
||||||
try {
|
try {
|
||||||
const url = normalizePath(config.serverUrl, config.basePath, fileName)
|
const url = normalizePath(config.serverUrl, config.basePath, fileName)
|
||||||
|
|
||||||
@@ -262,27 +295,24 @@ export const downloadFromWebDAV = async (
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return { success: false, message: '认证失败' }
|
return { success: false, messageKey: WebDAVMessageKey.authFailed }
|
||||||
}
|
}
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return { success: false, message: '文件不存在' }
|
return { success: false, messageKey: WebDAVMessageKey.fileNotExist }
|
||||||
}
|
}
|
||||||
return { success: false, message: `下载失败: HTTP ${response.status}` }
|
return { success: false, messageKey: WebDAVMessageKey.downloadFailedHttp, messageParams: { status: response.status } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.text()
|
const data = await response.text()
|
||||||
return { success: true, data }
|
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess, data }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
return { success: false, message: `下载错误: ${message}` }
|
return { success: false, messageKey: WebDAVMessageKey.downloadError, messageParams: { error: errorMessage } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除 WebDAV 文件
|
// 删除 WebDAV 文件
|
||||||
export const deleteFromWebDAV = async (
|
export const deleteFromWebDAV = async (config: WebDAVConfig, fileName: string): Promise<WebDAVResult> => {
|
||||||
config: WebDAVConfig,
|
|
||||||
fileName: string
|
|
||||||
): Promise<{ success: boolean; message?: string }> => {
|
|
||||||
try {
|
try {
|
||||||
const url = normalizePath(config.serverUrl, config.basePath, fileName)
|
const url = normalizePath(config.serverUrl, config.basePath, fileName)
|
||||||
|
|
||||||
@@ -294,20 +324,20 @@ export const deleteFromWebDAV = async (
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok || response.status === 204) {
|
if (response.ok || response.status === 204) {
|
||||||
return { success: true }
|
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return { success: false, message: '认证失败' }
|
return { success: false, messageKey: WebDAVMessageKey.authFailed }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return { success: true } // 文件不存在也视为删除成功
|
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess } // 文件不存在也视为删除成功
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, message: `删除失败: HTTP ${response.status}` }
|
return { success: false, messageKey: WebDAVMessageKey.deleteFailedHttp, messageParams: { status: response.status } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
return { success: false, message: `删除错误: ${message}` }
|
return { success: false, messageKey: WebDAVMessageKey.deleteError, messageParams: { error: errorMessage } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,133 +181,6 @@
|
|||||||
</ScrollableDialogContent>
|
</ScrollableDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- NPC互动面板 - 贸易提议、情报、联合攻击邀请 -->
|
|
||||||
<div v-if="hasNpcInteractions" class="space-y-4">
|
|
||||||
<Collapsible v-model:open="interactionPanelOpen" class="border rounded-lg">
|
|
||||||
<CollapsibleTrigger class="flex items-center justify-between w-full p-4 hover:bg-accent/50 transition-colors">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Handshake class="h-5 w-5 text-primary" />
|
|
||||||
<span class="font-semibold">{{ t('npcBehavior.trade.title') }} & {{ t('npcBehavior.intel.title') }}</span>
|
|
||||||
<Badge variant="destructive" v-if="totalInteractionCount > 0">{{ totalInteractionCount }}</Badge>
|
|
||||||
</div>
|
|
||||||
<ChevronDown class="h-4 w-4 transition-transform" :class="{ 'rotate-180': interactionPanelOpen }" />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent class="px-4 pb-4 space-y-4">
|
|
||||||
<!-- 贸易提议 -->
|
|
||||||
<div v-if="activeTradeOffers.length > 0">
|
|
||||||
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2">
|
|
||||||
<ArrowLeftRight class="h-4 w-4" />
|
|
||||||
{{ t('npcBehavior.trade.title') }} ({{ activeTradeOffers.length }})
|
|
||||||
</h3>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Card v-for="offer in activeTradeOffers" :key="offer.id" class="p-3">
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<div class="font-medium">{{ getNpcName(offer.npcId) }}</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
<span class="text-green-600 dark:text-green-400">{{ t('npcBehavior.trade.offers') }}:</span>
|
|
||||||
{{ formatResources(offer.offeredResources) }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
<span class="text-red-600 dark:text-red-400">{{ t('npcBehavior.trade.requests') }}:</span>
|
|
||||||
{{ formatResources(offer.requestedResources) }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.trade.expiresIn') }}: {{ formatTimeRemaining(offer.expiresAt) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Button size="sm" variant="default" @click="acceptTradeOffer(offer)" :disabled="!canAcceptTrade(offer)">
|
|
||||||
{{ t('npcBehavior.trade.accept') }}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" @click="declineTradeOffer(offer)">
|
|
||||||
{{ t('npcBehavior.trade.decline') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 情报报告 -->
|
|
||||||
<div v-if="unreadIntelReports.length > 0">
|
|
||||||
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2">
|
|
||||||
<Eye class="h-4 w-4" />
|
|
||||||
{{ t('npcBehavior.intel.title') }} ({{ unreadIntelReports.length }})
|
|
||||||
</h3>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Card v-for="intel in unreadIntelReports" :key="intel.id" class="p-3">
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<div class="font-medium">{{ t('npcBehavior.intel.from') }}: {{ getNpcName(intel.fromNpcId) }}</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.intel.target') }}: {{ getNpcName(intel.targetNpcId) }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm">
|
|
||||||
<Badge variant="outline">{{ t(`npcBehavior.intel.types.${intel.intelType}`) }}</Badge>
|
|
||||||
</div>
|
|
||||||
<div v-if="intel.data?.fleet" class="text-sm text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.intel.fleetInfo') }}: {{ formatFleetInfo(intel.data.fleet) }}
|
|
||||||
</div>
|
|
||||||
<div v-if="intel.data?.resources" class="text-sm text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.intel.resourceInfo') }}: {{ formatResources(intel.data.resources as Resources) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" variant="ghost" @click="markIntelAsRead(intel)">
|
|
||||||
{{ t('npcBehavior.intel.markAsRead') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 联合攻击邀请 -->
|
|
||||||
<div v-if="activeJointAttackInvites.length > 0">
|
|
||||||
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2">
|
|
||||||
<Swords class="h-4 w-4" />
|
|
||||||
{{ t('npcBehavior.jointAttack.title') }} ({{ activeJointAttackInvites.length }})
|
|
||||||
</h3>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Card v-for="invite in activeJointAttackInvites" :key="invite.id" class="p-3">
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div class="flex-1 space-y-1">
|
|
||||||
<div class="font-medium">{{ t('npcBehavior.jointAttack.from') }}: {{ getNpcName(invite.fromNpcId) }}</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.jointAttack.target') }}: {{ getNpcName(invite.targetNpcId) }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.jointAttack.targetPlanet') }}: [{{ invite.targetPosition.galaxy }}:{{
|
|
||||||
invite.targetPosition.system
|
|
||||||
}}:{{ invite.targetPosition.position }}]
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.jointAttack.lootShare') }}: {{ (invite.expectedLootRatio * 100).toFixed(0) }}%
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.jointAttack.expiresIn') }}: {{ formatTimeRemaining(invite.expiresAt) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Button size="sm" variant="default" @click="acceptJointAttack(invite)">
|
|
||||||
{{ t('npcBehavior.jointAttack.accept') }}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" @click="declineJointAttack(invite)">
|
|
||||||
{{ t('npcBehavior.jointAttack.decline') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无互动内容提示 -->
|
|
||||||
<div v-if="!hasActiveInteractions" class="text-center py-4 text-muted-foreground">
|
|
||||||
{{ t('npcBehavior.trade.noOffers') }}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
@@ -503,14 +376,11 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useNPCStore } from '@/stores/npcStore'
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
|
||||||
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
||||||
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
|
||||||
import {
|
import {
|
||||||
FixedPagination,
|
FixedPagination,
|
||||||
Pagination,
|
Pagination,
|
||||||
@@ -523,7 +393,7 @@
|
|||||||
import NpcRelationCard from '@/components/npc/NpcRelationCard.vue'
|
import NpcRelationCard from '@/components/npc/NpcRelationCard.vue'
|
||||||
import NpcRelationRow from '@/components/npc/NpcRelationRow.vue'
|
import NpcRelationRow from '@/components/npc/NpcRelationRow.vue'
|
||||||
import { RelationStatus } from '@/types/game'
|
import { RelationStatus } from '@/types/game'
|
||||||
import type { DiplomaticRelation, TradeOffer, IntelReport, JointAttackInvite, Resources } from '@/types/game'
|
import type { DiplomaticRelation } from '@/types/game'
|
||||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
@@ -533,11 +403,7 @@
|
|||||||
Swords,
|
Swords,
|
||||||
Activity,
|
Activity,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
List,
|
List
|
||||||
Handshake,
|
|
||||||
ChevronDown,
|
|
||||||
ArrowLeftRight,
|
|
||||||
Eye
|
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
|
|
||||||
@@ -548,9 +414,6 @@
|
|||||||
|
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
|
||||||
// NPC互动面板状态
|
|
||||||
const interactionPanelOpen = ref(true)
|
|
||||||
|
|
||||||
// 视图模式: 'card' | 'list'
|
// 视图模式: 'card' | 'list'
|
||||||
const viewMode = ref<'card' | 'list'>('list')
|
const viewMode = ref<'card' | 'list'>('list')
|
||||||
|
|
||||||
@@ -864,188 +727,6 @@
|
|||||||
currentPage.value[activeTab.value] = val
|
currentPage.value[activeTab.value] = val
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// ========== NPC互动面板相关 ==========
|
|
||||||
|
|
||||||
// 获取当前时间戳
|
|
||||||
const now = computed(() => Date.now())
|
|
||||||
|
|
||||||
// 有效的贸易提议(未过期)
|
|
||||||
const activeTradeOffers = computed(() => {
|
|
||||||
return (gameStore.player.tradeOffers || []).filter(offer => offer.expiresAt > now.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 未读的情报报告
|
|
||||||
const unreadIntelReports = computed(() => {
|
|
||||||
return (gameStore.player.intelReports || []).filter(report => !report.read)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 有效的联合攻击邀请(未过期)
|
|
||||||
const activeJointAttackInvites = computed(() => {
|
|
||||||
return (gameStore.player.jointAttackInvites || []).filter(invite => invite.expiresAt > now.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 是否有NPC互动数据
|
|
||||||
const hasNpcInteractions = computed(() => {
|
|
||||||
return (
|
|
||||||
(gameStore.player.tradeOffers?.length || 0) > 0 ||
|
|
||||||
(gameStore.player.intelReports?.length || 0) > 0 ||
|
|
||||||
(gameStore.player.jointAttackInvites?.length || 0) > 0
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 是否有有效的互动内容
|
|
||||||
const hasActiveInteractions = computed(() => {
|
|
||||||
return activeTradeOffers.value.length > 0 || unreadIntelReports.value.length > 0 || activeJointAttackInvites.value.length > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 总互动数量(用于显示徽章)
|
|
||||||
const totalInteractionCount = computed(() => {
|
|
||||||
return activeTradeOffers.value.length + unreadIntelReports.value.length + activeJointAttackInvites.value.length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取NPC名称
|
|
||||||
const getNpcName = (npcId: string): string => {
|
|
||||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
|
||||||
return npc?.name || npcId
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化资源显示
|
|
||||||
// 格式化资源(兼容新旧格式)
|
|
||||||
const formatResources = (resources: Resources | { type: string; amount: number }): string => {
|
|
||||||
// 新格式:{ type: 'metal', amount: 1000 }
|
|
||||||
if ('type' in resources && 'amount' in resources) {
|
|
||||||
const typeLabels: Record<string, string> = {
|
|
||||||
metal: 'M',
|
|
||||||
crystal: 'C',
|
|
||||||
deuterium: 'D'
|
|
||||||
}
|
|
||||||
return `${Math.floor(resources.amount).toLocaleString()} ${typeLabels[resources.type] || resources.type}`
|
|
||||||
}
|
|
||||||
// 旧格式:{ metal: 1000, crystal: 0, deuterium: 0 }
|
|
||||||
const parts: string[] = []
|
|
||||||
if ((resources as Resources).metal > 0) parts.push(`${Math.floor((resources as Resources).metal).toLocaleString()} M`)
|
|
||||||
if ((resources as Resources).crystal > 0) parts.push(`${Math.floor((resources as Resources).crystal).toLocaleString()} C`)
|
|
||||||
if ((resources as Resources).deuterium > 0) parts.push(`${Math.floor((resources as Resources).deuterium).toLocaleString()} D`)
|
|
||||||
return parts.join(' / ') || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化舰队信息
|
|
||||||
const formatFleetInfo = (fleetInfo: Record<string, number>): string => {
|
|
||||||
const parts: string[] = []
|
|
||||||
for (const [shipType, count] of Object.entries(fleetInfo)) {
|
|
||||||
if (count > 0) {
|
|
||||||
parts.push(`${shipType}: ${count}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts.join(', ') || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化剩余时间
|
|
||||||
const formatTimeRemaining = (expiresAt: number): string => {
|
|
||||||
const remaining = expiresAt - now.value
|
|
||||||
if (remaining <= 0) return t('npcBehavior.trade.expired')
|
|
||||||
|
|
||||||
const minutes = Math.floor(remaining / 60000)
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
const mins = minutes % 60
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${mins}m`
|
|
||||||
}
|
|
||||||
return `${mins}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否可以接受贸易(兼容新格式 { type, amount })
|
|
||||||
const canAcceptTrade = (offer: TradeOffer): boolean => {
|
|
||||||
const planet = gameStore.player.planets[0]
|
|
||||||
if (!planet) return false
|
|
||||||
|
|
||||||
// 新格式:{ type: 'metal', amount: 1000 }
|
|
||||||
const requestedType = offer.requestedResources.type
|
|
||||||
const requestedAmount = offer.requestedResources.amount
|
|
||||||
return planet.resources[requestedType] >= requestedAmount
|
|
||||||
}
|
|
||||||
|
|
||||||
// 接受贸易提议
|
|
||||||
const acceptTradeOffer = (offer: TradeOffer) => {
|
|
||||||
if (!canAcceptTrade(offer)) {
|
|
||||||
toast.error(t('npcBehavior.trade.acceptFailed'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const planet = gameStore.player.planets[0]
|
|
||||||
if (!planet) return
|
|
||||||
|
|
||||||
// 新格式:{ type: 'metal', amount: 1000 }
|
|
||||||
const requestedType = offer.requestedResources.type
|
|
||||||
const requestedAmount = offer.requestedResources.amount
|
|
||||||
const offeredType = offer.offeredResources.type
|
|
||||||
const offeredAmount = offer.offeredResources.amount
|
|
||||||
|
|
||||||
// 扣除请求的资源
|
|
||||||
planet.resources[requestedType] -= requestedAmount
|
|
||||||
|
|
||||||
// 添加获得的资源
|
|
||||||
planet.resources[offeredType] += offeredAmount
|
|
||||||
|
|
||||||
// 移除贸易提议
|
|
||||||
const index = gameStore.player.tradeOffers?.indexOf(offer)
|
|
||||||
if (index !== undefined && index >= 0) {
|
|
||||||
gameStore.player.tradeOffers?.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提高与该NPC的好感度(使用 npcId 而不是 fromNpcId)
|
|
||||||
const npcRelation = npcStore.npcs.find(n => n.id === offer.npcId)?.relations?.[gameStore.player.id]
|
|
||||||
if (npcRelation) {
|
|
||||||
npcRelation.reputation += 10
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(t('npcBehavior.trade.acceptSuccess'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拒绝贸易提议
|
|
||||||
const declineTradeOffer = (offer: TradeOffer) => {
|
|
||||||
const index = gameStore.player.tradeOffers?.indexOf(offer)
|
|
||||||
if (index !== undefined && index >= 0) {
|
|
||||||
gameStore.player.tradeOffers?.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.info(t('npcBehavior.trade.declined'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记情报为已读
|
|
||||||
const markIntelAsRead = (intel: IntelReport) => {
|
|
||||||
intel.read = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 接受联合攻击邀请
|
|
||||||
const acceptJointAttack = (invite: JointAttackInvite) => {
|
|
||||||
// 这里可以添加联合攻击的逻辑
|
|
||||||
// 目前只是简单地移除邀请并显示提示
|
|
||||||
const index = gameStore.player.jointAttackInvites?.indexOf(invite)
|
|
||||||
if (index !== undefined && index >= 0) {
|
|
||||||
gameStore.player.jointAttackInvites?.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提高与该NPC的好感度(使用 npcStore)
|
|
||||||
const npcRelation = npcStore.npcs.find(n => n.id === invite.fromNpcId)?.relations?.[gameStore.player.id]
|
|
||||||
if (npcRelation) {
|
|
||||||
npcRelation.reputation += 15
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(t('npcBehavior.jointAttack.acceptSuccess'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拒绝联合攻击邀请
|
|
||||||
const declineJointAttack = (invite: JointAttackInvite) => {
|
|
||||||
const index = gameStore.player.jointAttackInvites?.indexOf(invite)
|
|
||||||
if (index !== undefined && index >= 0) {
|
|
||||||
gameStore.player.jointAttackInvites?.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.info(t('npcBehavior.jointAttack.declined'))
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1117,13 +1117,17 @@
|
|||||||
// 获取任务类型名称
|
// 获取任务类型名称
|
||||||
const getMissionTypeName = (missionType: string): string => {
|
const getMissionTypeName = (missionType: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
|
[MissionType.Attack]: t('fleetView.attack'),
|
||||||
[MissionType.Transport]: t('fleetView.transport'),
|
[MissionType.Transport]: t('fleetView.transport'),
|
||||||
[MissionType.Colonize]: t('fleetView.colonize'),
|
[MissionType.Colonize]: t('fleetView.colonize'),
|
||||||
|
[MissionType.Spy]: t('fleetView.spy'),
|
||||||
[MissionType.Deploy]: t('fleetView.deploy'),
|
[MissionType.Deploy]: t('fleetView.deploy'),
|
||||||
[MissionType.Expedition]: t('fleetView.expedition'),
|
[MissionType.Expedition]: t('fleetView.expedition'),
|
||||||
[MissionType.Recycle]: t('fleetView.recycle'),
|
[MissionType.Recycle]: t('fleetView.recycle'),
|
||||||
[MissionType.Destroy]: t('fleetView.destroy'),
|
[MissionType.Destroy]: t('fleetView.destroy'),
|
||||||
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
|
[MissionType.MissileAttack]: t('galaxyView.missileAttack'),
|
||||||
|
[MissionType.HarvestDarkMatter]: t('fleetView.harvestDarkMatter'),
|
||||||
|
[MissionType.Station]: t('fleetView.station')
|
||||||
}
|
}
|
||||||
return typeMap[missionType] || missionType
|
return typeMap[missionType] || missionType
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -792,7 +792,7 @@
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(t('settings.webdav.uploadSuccess'))
|
toast.success(t('settings.webdav.uploadSuccess'))
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || t('settings.webdav.uploadFailed'))
|
toast.error(t(result.messageKey) || t('settings.webdav.uploadFailed'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('WebDAV upload failed:', error)
|
console.error('WebDAV upload failed:', error)
|
||||||
@@ -810,7 +810,7 @@
|
|||||||
const result = await downloadFromWebDAV(webdavConfig.value, fileName)
|
const result = await downloadFromWebDAV(webdavConfig.value, fileName)
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
if (!result.success || !result.data) {
|
||||||
toast.error(result.message || t('settings.webdav.downloadFailed'))
|
toast.error(t(result.messageKey) || t('settings.webdav.downloadFailed'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user