23 Commits

Author SHA1 Message Date
谦君
1ad051cd6d Update ResourceIcon.vue 2025-12-27 04:02:17 +08:00
谦君
fda15646eb Update package.json 2025-12-27 03:58:54 +08:00
谦君
6a9846c6df Update package.json 2025-12-27 03:58:15 +08:00
谦君
49753566c3 优化webdav相关 2025-12-27 01:37:35 +08:00
谦君
66783f896c 补全翻译 2025-12-27 01:04:14 +08:00
谦君
7cc885c62a Delete splash.xml 2025-12-27 00:18:40 +08:00
谦君
5c6404d86a 优化移动端开屏 2025-12-27 00:12:49 +08:00
谦君
010ea137ac perf: 优化安卓WebView性能与调试配置
MainActivity中为WebView启用硬件加速、DOM存储、数据库及默认缓存模式,提升性能与兼容性。capacitor.config.ts开启webContentsDebugging,便于调试排查问题。
2025-12-26 23:44:22 +08:00
谦君
6dbca76252 Update index.html 2025-12-26 23:37:23 +08:00
谦君
c047ffb88e Create favicon.ico 2025-12-26 22:25:12 +08:00
谦君
6f8adfa586 build: 替换autoprefixer为lightningcss并优化依赖
移除autoprefixer,改用lightningcss处理CSS,提升构建兼容性与性能。package.json、pnpm-lock.yaml、vite.config.ts同步调整依赖与配置,支持Android 5+/iOS 10+/Chrome 60+等目标环境。补充PWA苹果图标,删除favicon.ico。多语言任务目标文本细化和丰富,提升本地化体验。安卓端gradle配置补充capacitor-app与capacitor-filesystem依赖。
2025-12-26 22:22:14 +08:00
谦君
94fa2ad57a feat: 多语言完善造船厂与研究相关字段
为de、en、es-LA、ko、ru、zh-CN、zh-TW等多语言文件补充和完善造船厂(shipyard)与研究(research)相关字段,包括攻击、防御、装甲、建造成本、总成本、批量计算等,提升界面一致性与本地化体验。同时优化通知弹窗滚动区域样式,增加overflow-y-auto,提升内容自适应性。
2025-12-26 01:53:19 +08:00
谦君
7ed508945a build: Android版本号自动同步package.json
android/app/build.gradle中通过读取package.json自动设置versionName与versionCode,实现前后端版本号一致,避免手动同步出错。
2025-12-26 01:16:08 +08:00
谦君
fe2e5bfad9 refactor: 优化ResourceIcon样式及兼容性
将ResourceIcon根元素由div改为span,调整样式为inline-block和shrink-0,提升布局灵活性。颜色由渐变改为纯色背景,增强在Android WebView等环境下的显示兼容性。尺寸样式增加min-width/min-height,确保图标不被压缩。
2025-12-25 21:29:38 +08:00
谦君
7f36b6693f style: 优化通知弹窗滚动区域高度样式
将DiplomaticNotifications、EnemyAlertNotifications与QueueNotifications中的ScrollArea高度由固定h-96/h-[420px]调整为h-auto max-h-96,提升内容自适应性,避免内容较少时出现多余空白,增强界面美观与一致性。
2025-12-25 21:17:53 +08:00
谦君
27d60ae71a fix: 禁用WebView文本缩放并修复Portal定位
安卓端MainActivity中强制WebView文本缩放为100%,防止系统字体大小影响布局。capacitor.config.ts同步禁用WebView文本缩放及键盘视口调整。CSS中统一禁用文本大小调整,修复Edge-to-Edge模式下Portal容器定位问题,提升移动端显示一致性。
2025-12-25 20:40:02 +08:00
谦君
ca1aed1e9b style: 优化可滚动Dialog内容与遮罩布局
ScrollableDialogContent重构遮罩与内容结构,遮罩层支持flex居中与内边距,内容容器样式与DialogContent统一,提升弹窗显示一致性与居中效果。DialogContent补充relative定位,便于后续扩展。
2025-12-25 20:12:01 +08:00
谦君
04ee72a33d feat: 安卓端支持沉浸式边到边显示
MainActivity启用Edge-to-Edge,状态栏与导航栏设为透明并强制深色图标,提升沉浸体验。styles.xml同步调整相关颜色为透明。CSS中优化html平滑过渡样式,提升界面切换流畅度。
2025-12-25 20:00:13 +08:00
谦君
d95dffcfcd style: 优化Dialog与AlertDialog居中与间距样式
调整AlertDialogContent、DialogContent及DialogOverlay的布局样式,统一弹窗居中方式,增加flex居中与padding,提升弹窗在不同屏幕下的显示效果与一致性。
2025-12-25 19:51:15 +08:00
谦君
b6bcae3294 fix: 统一APK文件扩展名为小写.apk
将构建产物及相关CI流程中的APK文件扩展名由大写.APK统一为小写.apk,提升平台兼容性并避免文件识别问题。
2025-12-25 19:26:44 +08:00
谦君
ebed10b82f feat: 优化Dialog内容样式并完善多语言“建造”文案
调整AlertDialogContent与DialogContent的宽度与定位样式,提升弹窗显示效果。多语言文件中buildingsView部分新增“build”字段,完善德语、英语、韩语、俄语、简体中文的“建造”相关文案,提升界面一致性与本地化体验。
2025-12-25 19:24:11 +08:00
谦君
f4f5a719f5 ci: 构建流程切换为官方Gradle Action
将原有自定义Gradle缓存步骤替换为gradle/actions/setup-gradle官方Action,简化配置并利用内置智能缓存,提升CI流程维护性与稳定性。
2025-12-25 18:46:52 +08:00
谦君
1686622013 chore: 优化CI缓存与YAML格式统一
构建流程中Gradle缓存新增build-cache目录,并在assembleRelease时启用--build-cache参数,提升构建效率。统一GitHub Actions YAML文件中分支、标签、条件判断等格式,增强可读性与一致性。
2025-12-25 18:38:54 +08:00
47 changed files with 1000 additions and 716 deletions

View File

@@ -72,16 +72,11 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# 缓存 Gradle 依赖 (可节省 3-5 分钟)
- name: Cache Gradle
uses: actions/cache@v4
# 使用官方 Gradle Action内置智能缓存
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
cache-read-only: false
- name: Build Frontend
run: |
@@ -118,13 +113,13 @@ jobs:
working-directory: android
run: |
chmod +x ./gradlew
./gradlew assembleRelease --no-daemon
./gradlew assembleRelease --build-cache
- name: Upload APK Artifacts
uses: actions/upload-artifact@v4
with:
name: android-apk
path: android/app/build/outputs/apk/release/*.APK
path: android/app/build/outputs/apk/release/*.apk
# 3. 构建 Electron 客户端
build-electron:
@@ -211,7 +206,7 @@ jobs:
find ./raw-assets/electron-* -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.zip" \) -exec cp {} ./final-release/ \;
# 移动 Android APK
find ./raw-assets/android-apk -type f -name "*.APK" -exec cp {} ./final-release/ \; || true
find ./raw-assets/android-apk -type f -name "*.apk" -exec cp {} ./final-release/ \; || true
# 检查结果
echo "Final assets to upload:"
@@ -222,7 +217,7 @@ jobs:
run: |
VERSION=${{ steps.get_version.outputs.VERSION }}
# 获取 release 中的现有 assets 并删除 APK 文件
gh release view "$VERSION" --json assets -q '.assets[].name' 2>/dev/null | grep -i '\.APK$' | while read asset; do
gh release view "$VERSION" --json assets -q '.assets[].name' 2>/dev/null | grep -i '\.apk$' | while read asset; do
echo "Deleting existing asset: $asset"
gh release delete-asset "$VERSION" "$asset" -y || true
done

View File

@@ -1,5 +1,15 @@
apply plugin: 'com.android.application'
// 从 package.json 读取版本号
def packageJsonFile = file('../../package.json')
def packageJsonText = packageJsonFile.text
// 使用正则提取版本号
def versionMatcher = packageJsonText =~ /"version"\s*:\s*"([^"]+)"/
def appVersionName = versionMatcher ? versionMatcher[0][1] : "1.0.0"
// 将版本号转换为 versionCode例如 "1.5.5" -> 1*10000 + 5*100 + 5 = 10505
def versionParts = appVersionName.split('\\.')
def appVersionCode = versionParts[0].toInteger() * 10000 + versionParts[1].toInteger() * 100 + versionParts[2].toInteger()
android {
namespace = "games.wenzi.ogame"
compileSdk = rootProject.ext.compileSdkVersion
@@ -7,8 +17,8 @@ android {
applicationId "games.wenzi.ogame"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 15
versionName "1.5.0"
versionCode appVersionCode
versionName appVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -56,7 +66,7 @@ android {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) {
output.versionCodeOverride = abiVersionCode[abi] * 1000 + defaultConfig.versionCode
output.outputFileName = "OGame-Vue-Ts-${abi}.APK"
output.outputFileName = "OGame-Vue-Ts-${abi}.apk"
}
}
}

View File

@@ -9,7 +9,8 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
}

View File

@@ -8,6 +8,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme">

View File

@@ -1,18 +1,100 @@
package games.wenzi.ogame;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import androidx.core.content.ContextCompat;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.core.splashscreen.SplashScreen;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
private boolean isWebViewReady = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 安装 SplashScreen必须在 super.onCreate 之前调用
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
// 保持 SplashScreen 直到 WebView 加载完成
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);
// 设置状态栏颜色,防止 Capacitor 强制透明
Window window = getWindow();
window.setStatusBarColor(ContextCompat.getColor(this, R.color.status_bar_color));
window.setNavigationBarColor(ContextCompat.getColor(this, R.color.status_bar_color));
// 启用边到边显示Edge-to-Edge
WindowCompat.setDecorFitsSystemWindows(window, false);
// 设置透明状态栏和导航栏
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
// 设置状态栏图标为浅色(因为背景是深色)
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, window.getDecorView());
if (controller != null) {
controller.setAppearanceLightStatusBars(false);
controller.setAppearanceLightNavigationBars(false);
}
}
@Override
public void onStart() {
super.onStart();
WebView webView = getBridge().getWebView();
if (webView != null) {
WebSettings settings = webView.getSettings();
// 禁用 WebView 文本缩放,防止系统字体大小设置影响布局
settings.setTextZoom(100);
// 优化 WebView 性能
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
// 启用硬件加速渲染
webView.setLayerType(android.view.View.LAYER_TYPE_HARDWARE, null);
// 监听页面加载进度,加载完成后隐藏 SplashScreen
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
// 当页面加载达到 80% 时认为可以显示
if (newProgress >= 80) {
isWebViewReady = true;
}
}
});
}
}
}

View 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>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="status_bar_color">#1a1a2e</color>
<color name="splash_background">#0f0f1a</color>
</resources>

View File

@@ -13,12 +13,21 @@
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:statusBarColor">#1a1a2e</item>
<item name="android:navigationBarColor">#1a1a2e</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
</style>
<!-- 启动画面主题 - 延长显示直到 WebView 加载完成 -->
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
<item name="windowSplashScreenBackground">@color/splash_background</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>
</style>
</resources>

View File

@@ -1,3 +1,9 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/filesystem/android')

View File

@@ -5,12 +5,22 @@ const config: CapacitorConfig = {
appName: 'OGame Vue Ts',
webDir: 'docs',
server: {
androidScheme: 'https'
androidScheme: 'https',
cacheControl: 'max-age=31536000'
},
android: {
buildOptions: {
keystorePath: undefined,
keystoreAlias: undefined
},
webContentsDebuggingEnabled: false,
allowMixedContent: false,
hardwareAcceleration: true
},
plugins: {
// 禁用键盘自动调整视口
Keyboard: {
resize: 'none'
}
}
}

View File

@@ -6,6 +6,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="apple-touch-icon" href="/logo.svg" />
<title>OGame-Vue-Ts</title>
</head>
@@ -13,8 +14,9 @@
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<!-- 统计勿删 -->
<script charset="UTF-8" id="LA_COLLECT" src="https://sdk.51.la/js-sdk-pro.min.js"></script>
<script>LA.init({ id: "L298GYqn6JhAO0VU", ck: "L298GYqn6JhAO0VU", autoTrack: true, hashMode: true })</script>
<script>
!function (p) { "use strict"; !function (t) { var s = window, e = document, i = p, c = "".concat("https:" === e.location.protocol ? "https://" : "http://", "sdk.51.la/js-sdk-pro.min.js"), n = e.createElement("script"), r = e.getElementsByTagName("script")[0]; n.type = "text/javascript", n.setAttribute("charset", "UTF-8"), n.async = !0, n.src = c, n.id = "LA_COLLECT", i.d = n; var o = function () { s.LA.ids.push(i) }; s.LA ? s.LA.ids && o() : (s.LA = p, s.LA.ids = [], o()), r.parentNode.insertBefore(n, r) }() }({ id: "L298GYqn6JhAO0VU", ck: "L298GYqn6JhAO0VU", autoTrack: true, hashMode: true });
</script>
</body>
</html>

View File

@@ -8,8 +8,8 @@
"email": "1962257451@qq.com"
},
"private": true,
"version": "1.5.5",
"buildDate": "2025/12/25 18:23:43",
"version": "1.5.6",
"buildDate": "2025/12/27 03:58:44",
"main": "dist-electron/main.js",
"type": "module",
"scripts": {
@@ -44,7 +44,8 @@
"tailwindcss": "^4.1.17",
"vue": "^3.5.24",
"vue-router": "4",
"vue-sonner": "^2.0.9"
"vue-sonner": "^2.0.9",
"lightningcss": "^1.30.2"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
@@ -52,7 +53,6 @@
"@types/node": "^24.10.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.23",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",

32
pnpm-lock.yaml generated
View File

@@ -44,6 +44,9 @@ importers:
finalhandler:
specifier: ^2.1.1
version: 2.1.1
lightningcss:
specifier: ^1.30.2
version: 1.30.2
lucide-vue-next:
specifier: ^0.556.0
version: 0.556.0(vue@3.5.25(typescript@5.9.3))
@@ -93,9 +96,6 @@ importers:
'@vue/tsconfig':
specifier: ^0.8.1
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
autoprefixer:
specifier: ^10.4.23
version: 10.4.23(postcss@8.5.6)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -1672,13 +1672,6 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
autoprefixer@10.4.23:
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
postcss: ^8.1.0
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -2215,9 +2208,6 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
@@ -3114,9 +3104,6 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -5680,15 +5667,6 @@ snapshots:
at-least-node@1.0.0: {}
autoprefixer@10.4.23(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
caniuse-lite: 1.0.30001760
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.6
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -6395,8 +6373,6 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
fraction.js@5.3.4: {}
framer-motion@12.23.12:
dependencies:
motion-dom: 12.23.12
@@ -7267,8 +7243,6 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-value-parser@4.2.0: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11

View File

@@ -1,8 +1,6 @@
<template>
<!-- 首页无侧边栏/头部 -->
<template v-if="isHomePage">
<RouterView />
</template>
<RouterView v-if="isHomePage" />
<!-- 其他页面完整布局含侧边栏 -->
<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">
{{ activeFleetMissionsCount }}
</SidebarMenuBadge>
<!-- 未读外交报告数量 -->
<SidebarMenuBadge
v-if="item.path === '/diplomacy' && unreadDiplomaticReportsCount > 0"
class="bg-destructive text-destructive-foreground"
>
{{ unreadDiplomaticReportsCount }}
</SidebarMenuBadge>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
@@ -738,11 +729,6 @@
return fleetMissions + flyingMissiles
})
// 未读外交报告数量
const unreadDiplomaticReportsCount = computed(() => {
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
})
// 月球相关
const moon = computed(() => {
if (!planet.value || planet.value.isMoon) return null

View File

@@ -1,6 +1,12 @@
<template>
<Dialog :open="true" @update:open="handleClose">
<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="stars-bg" />
@@ -80,7 +86,8 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
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 type { StoryDialogue, DialogueChoice } from '@/types/game'
import { User, Bot, HelpCircle, MessageCircle, ChevronRight } from 'lucide-vue-next'

View File

@@ -1,5 +1,5 @@
<template>
<div :class="[colors[type], sizes[size], 'rounded shadow-sm']" />
<span :class="[colors[type], sizes[size], 'inline-block rounded shrink-0']" />
</template>
<script setup lang="ts">
@@ -8,21 +8,22 @@
size?: 'sm' | 'md' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
withDefaults(defineProps<Props>(), {
size: 'md'
})
// 使用纯色背景,在 Android WebView 中更可靠
const colors = {
metal: 'bg-gradient-to-br from-slate-400 to-slate-600',
crystal: 'bg-gradient-to-br from-cyan-400 to-blue-600',
deuterium: 'bg-gradient-to-br from-green-400 to-emerald-600',
darkMatter: 'bg-gradient-to-br from-purple-600 to-indigo-900',
energy: 'bg-gradient-to-br from-yellow-400 to-orange-500'
metal: 'bg-slate-500',
crystal: 'bg-cyan-500',
deuterium: 'bg-green-500',
darkMatter: 'bg-purple-700',
energy: 'bg-yellow-500'
}
const sizes = {
sm: 'w-3 h-3',
md: 'w-4 h-4',
lg: 'w-5 h-5'
sm: 'w-3 h-3 min-w-3 min-h-3',
md: 'w-4 h-4 min-w-4 min-h-4',
lg: 'w-5 h-5 min-w-5 min-h-5'
}
</script>

View File

@@ -19,7 +19,7 @@
{{ t('diplomacy.markAllRead') }}
</Button>
</div>
<ScrollArea class="h-96">
<ScrollArea class="h-auto max-h-96 overflow-y-auto">
<Empty v-if="allNotifications.length === 0" class="border-0">
<EmptyContent>
<ScrollText class="h-10 w-10 text-muted-foreground" />

View File

@@ -19,7 +19,7 @@
{{ t('enemyAlert.markAllRead') }}
</Button>
</div>
<ScrollArea class="h-96">
<ScrollArea class="h-auto max-h-96 overflow-y-auto">
<Empty v-if="activeAlerts.length === 0" class="border-0">
<EmptyContent>
<Shield class="h-10 w-10 text-muted-foreground" />

View File

@@ -32,7 +32,7 @@
</TabsTrigger>
</TabsList>
<ScrollArea class="h-[420px]">
<ScrollArea class="h-auto max-h-96 overflow-y-auto">
<TabsContent v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="mt-0">
<Empty v-if="tab.items.length === 0" class="border-0">
<EmptyContent>
@@ -63,7 +63,9 @@
class="text-[10px] sm:text-xs whitespace-nowrap"
:class="isWaitingItemResourcesReady(item as WaitingQueueItem) ? 'text-green-500' : 'text-yellow-500'"
>
{{ isWaitingItemResourcesReady(item as WaitingQueueItem) ? t('queue.resourcesReady') : t('queue.waitingResources') }}
{{
isWaitingItemResourcesReady(item as WaitingQueueItem) ? t('queue.resourcesReady') : t('queue.waitingResources')
}}
</span>
<Button
variant="ghost"

View File

@@ -81,6 +81,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from '@/composables/useI18n'
import { useGameStore } from '@/stores/gameStore'
import {
Dialog,
DialogContent,
@@ -95,13 +96,11 @@ import { Label } from '@/components/ui/label'
import { Loader2 } from 'lucide-vue-next'
import {
type WebDAVConfig,
getWebDAVConfig,
saveWebDAVConfig,
clearWebDAVConfig,
testWebDAVConnection
} from '@/services/webdavService'
const { t } = useI18n()
const gameStore = useGameStore()
const isOpen = defineModel<boolean>('open', { default: false })
@@ -130,7 +129,7 @@ const isConfigValid = computed(() => {
// 加载已保存的配置
watch(isOpen, (open) => {
if (open) {
const saved = getWebDAVConfig()
const saved = gameStore.webdavConfig
if (saved) {
config.value = { ...saved }
hasExistingConfig.value = true
@@ -152,21 +151,25 @@ const handleTest = async () => {
testResult.value = null
try {
testResult.value = await testWebDAVConnection(config.value)
const result = await testWebDAVConnection(config.value)
testResult.value = {
success: result.success,
message: t(result.messageKey)
}
} finally {
isTesting.value = false
}
}
const handleSave = () => {
saveWebDAVConfig(config.value)
gameStore.webdavConfig = { ...config.value }
hasExistingConfig.value = true
emit('saved', config.value)
isOpen.value = false
}
const handleClear = () => {
clearWebDAVConfig()
gameStore.webdavConfig = null
hasExistingConfig.value = false
config.value = {
serverUrl: '',

View File

@@ -121,7 +121,7 @@ const loadFiles = async () => {
if (result.success && result.files) {
files.value = result.files
} else {
error.value = result.message || t('settings.webdav.loadFailed')
error.value = t(result.messageKey) || t('settings.webdav.loadFailed')
}
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
@@ -174,7 +174,7 @@ const handleDelete = async (fileName: string) => {
selectedFile.value = null
}
} else {
toast.error(result.message || t('settings.webdav.deleteFailed'))
toast.error(t(result.messageKey) || t('settings.webdav.deleteFailed'))
}
}
</script>

View File

@@ -2,20 +2,21 @@
<AlertDialogPortal>
<AlertDialogOverlay
data-slot="alert-dialog-overlay"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
/>
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"
>
<AlertDialogContent
data-slot="alert-dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 z-50 grid w-full max-w-lg gap-4 rounded-lg border p-6 shadow-lg duration-200',
props.class
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialogPortal>
</template>

View File

@@ -1,12 +1,15 @@
<template>
<DialogPortal>
<DialogOverlay />
<DialogOverlay
data-slot="dialog-overlay"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-60 bg-black/80 flex items-center justify-center p-4"
>
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-60 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 z-60 grid w-full max-w-2xl gap-4 rounded-lg border p-6 shadow-lg duration-200 relative',
props.class
)
"
@@ -22,6 +25,7 @@
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>
@@ -30,9 +34,8 @@
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { X } from 'lucide-vue-next'
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import DialogOverlay from './DialogOverlay.vue'
defineOptions({
inheritAttrs: false

View File

@@ -4,7 +4,7 @@
v-bind="delegatedProps"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-60 bg-black/80',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-60 bg-black/80 flex items-center justify-center p-4',
props.class
)
"

View File

@@ -1,12 +1,15 @@
<template>
<DialogPortal>
<DialogOverlay />
<DialogOverlay
data-slot="dialog-overlay"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-60 bg-black/80 flex items-center justify-center p-4"
>
<DialogContent
data-slot="scrollable-dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-60 w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 z-60 w-full max-w-3xl rounded-lg border shadow-lg duration-200 flex flex-col p-0 relative',
containerClass
)
"
@@ -36,6 +39,7 @@
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>
@@ -44,9 +48,8 @@
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { X } from 'lucide-vue-next'
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import DialogOverlay from './DialogOverlay.vue'
defineOptions({
inheritAttrs: false

View File

@@ -157,6 +157,10 @@ export default {
build: 'Bauen',
production: 'Produktion',
consumption: 'Verbrauch',
levelRange: 'Stufenbereich',
totalCost: 'Gesamtkosten',
totalPoints: 'Gesamtpunkte',
researchSpeedBonus: 'Forschungsgeschwindigkeitsbonus',
capacity: 'Capacity/Effect',
storageCapacity: 'Capacity',
energyProduction: 'Energy Production',
@@ -265,6 +269,9 @@ export default {
},
research: {
researchTime: 'Forschungszeit',
levelRange: 'Stufenbereich',
totalCost: 'Gesamtkosten',
totalPoints: 'Gesamtpunkte',
capacity: 'Capacity/Effect',
storageCapacity: 'Capacity',
energyProduction: 'Energy Production',
@@ -392,8 +399,10 @@ export default {
tabConsumption: 'Verbrauchsdetails'
},
buildingsView: {
title: 'Gebäude',
spaceUsage: 'Platzbedarf',
upgradeCost: 'Ausbaukosten',
build: 'Bauen',
upgrade: 'Ausbauen',
maxLevelReached: 'Maximale Stufe erreicht',
requirementsNotMet: 'Anforderungen nicht erfüllt',
@@ -415,10 +424,20 @@ export default {
'Bitte überprüfen Sie, ob Sie genügend Ressourcen haben, die Voraussetzungen erfüllt sind oder keine anderen Forschungsaufträge vorhanden sind.'
},
shipyard: {
attack: 'Angriff',
shield: 'Schild',
armor: 'Panzerung',
missileAttack: 'Raketenangriff',
speed: 'Geschwindigkeit',
cargoCapacity: 'Ladekapazität',
fuelConsumption: 'Treibstoffverbrauch'
fuelConsumption: 'Treibstoffverbrauch',
buildCost: 'Baukosten',
buildTime: 'Bauzeit',
perUnit: 'Pro Einheit',
batchCalculator: 'Stapelrechner',
quantity: 'Menge',
totalCost: 'Gesamtkosten',
totalTime: 'Gesamtzeit'
},
shipyardView: {
title: 'Raumschiffwerft',
@@ -530,6 +549,8 @@ export default {
}
},
recycle: 'Recyceln',
harvestDarkMatter: 'Dunkle Materie ernten',
station: 'Stationieren',
transportResources: 'Ressourcen transportieren',
totalCargoCapacity: 'Gesamtladekapazität',
used: 'Verwendet',
@@ -1804,6 +1825,27 @@ export default {
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: {
name: 'Kampagne',
description: 'Erkunde die mysteriöse Galaxie und entdecke antike Geheimnisse',
@@ -1830,6 +1872,8 @@ export default {
branchUnlocked: 'Neuer Storyzweig freigeschaltet!'
},
dialogue: {
title: 'Story-Dialog',
description: 'Kampagnen-Story-Dialoginhalt',
skip: 'Überspringen',
continue: 'Weiter',
finish: 'Beenden',
@@ -1942,25 +1986,42 @@ export default {
spyAnyNPC: 'Spioniere einen NPC-Planeten aus',
sendGiftToNPC: 'Sende ein Geschenk an einen NPC',
researchAstrophysics: 'Erforsche Astrophysik auf Stufe 1',
researchAstrophysicsHigher: 'Erforsche Astrophysik auf Stufe 3',
buildColonyShip: 'Baue ein Kolonieschiff',
colonizeNewPlanet: 'Kolonisiere einen neuen Planeten',
completeExpedition: 'Schließe eine Expeditionsmission ab',
colonizeMultiple: 'Kolonisiere 5 Planeten',
completeExpedition: 'Schließe 3 Expeditionsmissionen ab',
expeditionDeepSpace: 'Schließe 2 Tiefenraum-Expeditionen ab',
expeditionUncharted: 'Erkunde 1 unbekannte Region',
expeditionDangerous: 'Schließe 3 gefährliche Nebel-Expeditionen ab',
discoverRuins: 'Entdecke antike Ruinen',
researchComputer: 'Erforsche Computertechnik auf Stufe 4',
researchImpulse: 'Erforsche Impulsantrieb auf Stufe 3',
researchLaser: 'Erforsche Lasertechnik auf Stufe 5',
researchIntergalactic: 'Erforsche Computertechnik auf Stufe 10',
researchGraviton: 'Erforsche Gravitontechnik auf Stufe 1',
improveRelation: 'Verbessere Beziehungen zu einem NPC',
reachFriendly: 'Erreiche freundlichen Status mit einem NPC',
spyHostileNPC: 'Spioniere einen feindlichen NPC aus',
reachFriendlyRelation: 'Erreiche freundlichen Status mit einem beliebigen NPC',
sendMultipleGifts: 'Sende 3 Geschenke an NPCs',
spyHostileNPC: 'Spioniere 2 feindliche NPCs aus',
formAlliance: 'Schließe Allianz mit einem freundlichen NPC',
buildDefenses: 'Baue Verteidigungsanlagen',
buildMissileSilo: 'Baue Raketensilo auf Stufe 2',
buildCruisers: 'Baue 10 Kreuzer',
winDefenseBattle: 'Gewinne eine Verteidigungsschlacht',
defendAgainstAttack: 'Verteidige erfolgreich gegen 1 Angriff',
spyEnemyPlanet: 'Spioniere Feindplaneten aus',
spyEnemyPlanets: 'Spioniere 5 Feindplaneten aus',
winAttackBattles: 'Gewinne 3 Angriffsschlachten',
attackEnemy: 'Greife den Feind an',
recycleDebris: 'Recycel Trümmer',
buildBattleships: 'Baue 10 Schlachtschiffe',
recycleDebris: 'Recycel 5 Mal Trümmer',
buildBattleships: 'Baue 20 Schlachtschiffe',
exploreDeepRuins: 'Erkunde tiefe Ruinen',
researchHyperspace: 'Erforsche Hyperraumtechnologie',
defeatBoss: 'Besiege den mysteriösen Feind',
researchHyperspace: 'Erforsche Hyperraumantrieb auf Stufe 3',
defeatBoss: 'Besiege den Antiken Wächter',
colonizeSpecial: 'Kolonisiere besonderen Standort',
accumulateWealth: 'Sammle 1 Million Gesamtressourcen',
continueDevelopment: 'Setze Entwicklung fort'
},
dialogues: {

View File

@@ -155,6 +155,10 @@ export default {
buildTime: 'Build Time',
production: 'Production',
consumption: 'Consumption',
levelRange: 'Level Range',
totalCost: 'Total Cost',
totalPoints: 'Total Points',
researchSpeedBonus: 'Research Speed Bonus',
storageCapacity: 'Capacity',
energyProduction: 'Energy Production',
@@ -260,6 +264,9 @@ export default {
},
research: {
researchTime: 'Research Time',
levelRange: 'Level Range',
totalCost: 'Total Cost',
totalPoints: 'Total Points',
attackBonus: 'Attack Bonus',
shieldBonus: 'Shield Bonus',
@@ -382,6 +389,7 @@ export default {
title: 'Buildings',
spaceUsage: 'Space Usage',
upgradeCost: 'Upgrade Cost',
build: 'Build',
upgrade: 'Upgrade',
maxLevelReached: 'Max Level Reached',
requirementsNotMet: 'Requirements Not Met',
@@ -402,10 +410,20 @@ export default {
researchFailedMessage: 'Please check if you have enough resources, prerequisites are met, or if there are other research tasks.'
},
shipyard: {
attack: 'Attack',
shield: 'Shield',
armor: 'Armor',
missileAttack: 'Missile Attack',
speed: 'Speed',
cargoCapacity: 'Cargo Capacity',
fuelConsumption: 'Fuel Consumption'
fuelConsumption: 'Fuel Consumption',
buildCost: 'Build Cost',
buildTime: 'Build Time',
perUnit: 'Per Unit',
batchCalculator: 'Batch Calculator',
quantity: 'Quantity',
totalCost: 'Total Cost',
totalTime: 'Total Time'
},
shipyardView: {
title: 'Shipyard',
@@ -425,7 +443,17 @@ export default {
buildFailedMessage: 'Please check if you have enough resources or if prerequisites are met.'
},
defense: {
missileAttack: 'Missile Attack'
attack: 'Attack',
shield: 'Shield',
armor: 'Armor',
missileAttack: 'Missile Attack',
buildCost: 'Build Cost',
buildTime: 'Build Time',
perUnit: 'Per Unit',
batchCalculator: 'Batch Calculator',
quantity: 'Quantity',
totalCost: 'Total Cost',
totalTime: 'Total Time'
},
defenseView: {
title: 'Defense',
@@ -504,6 +532,8 @@ export default {
},
recycle: 'Recycle',
destroy: 'Planet Destruction',
harvestDarkMatter: 'Harvest Dark Matter',
station: 'Station',
transportResources: 'Transport Resources',
totalCargoCapacity: 'Total Cargo Capacity',
used: 'Used',
@@ -1753,6 +1783,8 @@ export default {
branchUnlocked: 'New story branch unlocked!'
},
dialogue: {
title: 'Story Dialogue',
description: 'Campaign story dialogue content',
skip: 'Skip',
continue: 'Continue',
finish: 'Finish',
@@ -1940,25 +1972,42 @@ export default {
spyAnyNPC: 'Spy on any NPC planet',
sendGiftToNPC: 'Send a gift to any NPC',
researchAstrophysics: 'Research Astrophysics to level 1',
researchAstrophysicsHigher: 'Research Astrophysics to level 3',
buildColonyShip: 'Build a Colony Ship',
colonizeNewPlanet: 'Colonize a new planet',
completeExpedition: 'Complete an expedition mission',
colonizeMultiple: 'Colonize 5 planets',
completeExpedition: 'Complete 3 expedition missions',
expeditionDeepSpace: 'Complete 2 deep space expeditions',
expeditionUncharted: 'Explore 1 uncharted region',
expeditionDangerous: 'Complete 3 dangerous nebula expeditions',
discoverRuins: 'Discover ancient ruins',
researchComputer: 'Research Computer Technology to level 4',
researchImpulse: 'Research Impulse Drive to level 3',
researchLaser: 'Research Laser Technology to level 5',
researchIntergalactic: 'Research Computer Technology to level 10',
researchGraviton: 'Research Graviton Technology to level 1',
improveRelation: 'Improve relations with an NPC',
reachFriendly: 'Reach friendly status with an NPC',
spyHostileNPC: 'Spy on a hostile NPC',
reachFriendlyRelation: 'Reach friendly status with any NPC',
sendMultipleGifts: 'Send 3 gifts to NPCs',
spyHostileNPC: 'Spy on 2 hostile NPCs',
formAlliance: 'Form alliance with a friendly NPC',
buildDefenses: 'Build defense facilities',
buildMissileSilo: 'Build Missile Silo to level 2',
buildCruisers: 'Build 10 Cruisers',
winDefenseBattle: 'Win a defensive battle',
defendAgainstAttack: 'Successfully defend against 1 attack',
spyEnemyPlanet: 'Spy on enemy planet',
spyEnemyPlanets: 'Spy on 5 enemy planets',
winAttackBattles: 'Win 3 attack battles',
attackEnemy: 'Attack the enemy',
recycleDebris: 'Recycle debris',
buildBattleships: 'Build 10 Battleships',
recycleDebris: 'Recycle debris 5 times',
buildBattleships: 'Build 20 Battleships',
exploreDeepRuins: 'Explore deep ruins',
researchHyperspace: 'Research Hyperspace Technology',
defeatBoss: 'Defeat the mysterious enemy',
researchHyperspace: 'Research Hyperspace Drive to level 3',
defeatBoss: 'Defeat the Ancient Guardian',
colonizeSpecial: 'Colonize special location',
accumulateWealth: 'Accumulate 1 million total resources',
continueDevelopment: 'Continue development'
},
dialogues: {
@@ -2162,5 +2211,31 @@ export default {
reputationBonus: 'Reputation Bonus',
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}'
}
}

View File

@@ -155,6 +155,10 @@ export default {
buildTime: 'Tiempo de Construcción',
production: 'Producción',
consumption: 'Consumo',
levelRange: 'Rango de Nivel',
totalCost: 'Costo Total',
totalPoints: 'Puntos Totales',
researchSpeedBonus: 'Bono de Velocidad de Investigación',
storageCapacity: 'Capacidad',
energyProduction: 'Producción de Energía',
@@ -411,10 +415,20 @@ export default {
'Por favor verifica si tienes suficientes recursos, si se cumplen los prerrequisitos, o si hay otras tareas de investigación.'
},
shipyard: {
attack: 'Ataque',
shield: 'Escudo',
armor: 'Blindaje',
missileAttack: 'Ataque de Misil',
speed: 'Velocidad',
cargoCapacity: 'Capacidad de Carga',
fuelConsumption: 'Consumo de Combustible'
fuelConsumption: 'Consumo de Combustible',
buildCost: 'Costo de Construcción',
buildTime: 'Tiempo de Construcción',
perUnit: 'Por Unidad',
batchCalculator: 'Calculadora de Lotes',
quantity: 'Cantidad',
totalCost: 'Costo Total',
totalTime: 'Tiempo Total'
},
shipyardView: {
title: 'Astillero',
@@ -524,6 +538,8 @@ export default {
},
recycle: 'Reciclar',
destroy: 'Destrucción Planetaria',
harvestDarkMatter: 'Recolectar Materia Oscura',
station: 'Estacionar',
transportResources: 'Transportar Recursos',
totalCargoCapacity: 'Capacidad de Carga Total',
used: 'Usado',
@@ -1771,6 +1787,8 @@ export default {
branchUnlocked: '¡Nueva rama de historia desbloqueada!'
},
dialogue: {
title: 'Diálogo de historia',
description: 'Contenido del diálogo de la campaña',
skip: 'Saltar',
continue: 'Continuar',
finish: 'Finalizar',
@@ -1958,25 +1976,42 @@ export default {
spyAnyNPC: 'Espiar cualquier planeta NPC',
sendGiftToNPC: 'Enviar un regalo a cualquier NPC',
researchAstrophysics: 'Investigar Astrofísica al nivel 1',
researchAstrophysicsHigher: 'Investigar Astrofísica al nivel 3',
buildColonyShip: 'Construir una Nave Colonial',
colonizeNewPlanet: 'Colonizar un nuevo planeta',
completeExpedition: 'Completar una misión de expedición',
colonizeMultiple: 'Colonizar 5 planetas',
completeExpedition: 'Completar 3 misiones de expedición',
expeditionDeepSpace: 'Completar 2 expediciones al espacio profundo',
expeditionUncharted: 'Explorar 1 región desconocida',
expeditionDangerous: 'Completar 3 expediciones a nebulosas peligrosas',
discoverRuins: 'Descubrir ruinas antiguas',
researchComputer: 'Investigar Tecnología de Computación al nivel 4',
researchImpulse: 'Investigar Motor de Impulso al nivel 3',
researchLaser: 'Investigar Tecnología Láser al nivel 5',
researchIntergalactic: 'Investigar Tecnología de Computación al nivel 10',
researchGraviton: 'Investigar Tecnología Gravitón al nivel 1',
improveRelation: 'Mejorar relaciones con un NPC',
reachFriendly: 'Alcanzar estatus amigable con un NPC',
spyHostileNPC: 'Espiar un NPC hostil',
reachFriendlyRelation: 'Alcanzar estatus amigable con cualquier NPC',
sendMultipleGifts: 'Enviar 3 regalos a NPCs',
spyHostileNPC: 'Espiar 2 NPCs hostiles',
formAlliance: 'Formar alianza con un NPC amigable',
buildDefenses: 'Construir instalaciones de defensa',
buildMissileSilo: 'Construir Silo de Misiles al nivel 2',
buildCruisers: 'Construir 10 Cruceros',
winDefenseBattle: 'Ganar una batalla defensiva',
defendAgainstAttack: 'Defender exitosamente contra 1 ataque',
spyEnemyPlanet: 'Espiar planeta enemigo',
spyEnemyPlanets: 'Espiar 5 planetas enemigos',
winAttackBattles: 'Ganar 3 batallas de ataque',
attackEnemy: 'Atacar al enemigo',
recycleDebris: 'Reciclar escombros',
buildBattleships: 'Construir 10 Naves de Batalla',
recycleDebris: 'Reciclar escombros 5 veces',
buildBattleships: 'Construir 20 Naves de Batalla',
exploreDeepRuins: 'Explorar ruinas profundas',
researchHyperspace: 'Investigar Tecnología de Hiperespacio',
defeatBoss: 'Derrotar al enemigo misterioso',
researchHyperspace: 'Investigar Motor de Hiperespacio al nivel 3',
defeatBoss: 'Derrotar al Guardián Antiguo',
colonizeSpecial: 'Colonizar ubicación especial',
accumulateWealth: 'Acumular 1 millón de recursos totales',
continueDevelopment: 'Continuar desarrollo'
},
dialogues: {
@@ -2184,5 +2219,26 @@ export default {
reputationBonus: 'Bonificación de Reputación',
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'
}
}

View File

@@ -641,7 +641,9 @@ export default {
jumpGateSuccessMessage: '艦隊は{target}へ瞬時に転送されました',
jumpGateFailed: 'ジャンプゲート転送失敗',
jumpGateFailedMessage: 'ジャンプゲートの状態と艦隊構成を確認してください',
destroy: '破壊'
destroy: '破壊',
harvestDarkMatter: 'ダークマター採取',
station: '駐留'
},
officersView: {
title: '士官',
@@ -1854,6 +1856,27 @@ export default {
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: {
name: 'キャンペーン',
description: '神秘的な銀河を探索し、古代の秘密を解き明かす',
@@ -1881,6 +1904,8 @@ export default {
branchUnlocked: '新しいストーリー分岐が解放されました!'
},
dialogue: {
title: 'ストーリー対話',
description: 'キャンペーンストーリーの対話内容',
skip: 'スキップ',
continue: '続ける',
finish: '完了',
@@ -1993,25 +2018,42 @@ export default {
spyAnyNPC: 'NPC惑星をスパイ',
sendGiftToNPC: 'NPCに贈り物を送る',
researchAstrophysics: '宇宙物理学をレベル1まで研究',
researchAstrophysicsHigher: '宇宙物理学をレベル3まで研究',
buildColonyShip: 'コロニーシップを建造',
colonizeNewPlanet: '新惑星を植民地化',
completeExpedition: '遠征ミッションを完了',
colonizeMultiple: '5つの惑星を植民地化',
completeExpedition: '3回の遠征ミッションを完了',
expeditionDeepSpace: '2回の深宇宙遠征を完了',
expeditionUncharted: '1回の未知領域探索',
expeditionDangerous: '3回の危険な星雲遠征を完了',
discoverRuins: '古代遺跡を発見',
researchComputer: 'コンピュータ技術をレベル4まで研究',
researchImpulse: 'インパルスドライブをレベル3まで研究',
researchLaser: 'レーザー技術をレベル5まで研究',
researchIntergalactic: 'コンピュータ技術をレベル10まで研究',
researchGraviton: 'グラビトン技術をレベル1まで研究',
improveRelation: 'NPCとの関係を改善',
reachFriendly: 'NPCと友好ステータスに到達',
spyHostileNPC: '敵対NPCをスパイ',
reachFriendlyRelation: '任意のNPCと友好ステータスに到達',
sendMultipleGifts: 'NPCに3回贈り物を送る',
spyHostileNPC: '2つの敵対NPCをスパイ',
formAlliance: '友好NPCと同盟を結ぶ',
buildDefenses: '防衛施設を建設',
buildMissileSilo: 'ミサイルサイロをレベル2に建設',
buildCruisers: 'クルーザー10隻を建造',
winDefenseBattle: '防衛戦に勝利',
defendAgainstAttack: '1回の攻撃を防御成功',
spyEnemyPlanet: '敵惑星をスパイ',
spyEnemyPlanets: '5つの敵惑星をスパイ',
winAttackBattles: '3回の攻撃戦に勝利',
attackEnemy: '敵を攻撃',
recycleDebris: 'デブリをリサイクル',
buildBattleships: 'バトルシップ10隻を建造',
recycleDebris: '5回デブリをリサイクル',
buildBattleships: 'バトルシップ20隻を建造',
exploreDeepRuins: '深部遺跡を探索',
researchHyperspace: 'ハイパースペース技術を研究',
defeatBoss: '謎の敵を撃破',
researchHyperspace: 'ハイパースペースドライブをレベル3まで研究',
defeatBoss: '古代の守護者を撃破',
colonizeSpecial: '特別な場所を植民地化',
accumulateWealth: '総資源100万を蓄積',
continueDevelopment: '発展を継続'
},
dialogues: {

View File

@@ -388,6 +388,7 @@ export default {
title: '건물',
spaceUsage: '공간 사용',
upgradeCost: '업그레이드 비용',
build: '건설',
upgrade: '업그레이드',
maxLevelReached: '최대 레벨 도달',
requirementsNotMet: '요구 사항 미충족',
@@ -408,9 +409,19 @@ export default {
researchFailedMessage: '자원이 충분한지, 전제 조건이 충족되었는지, 또는 다른 연구 작업이 있는지 확인하세요.'
},
shipyard: {
attack: '공격력',
shield: '실드',
armor: '장갑',
speed: '속도',
cargoCapacity: '화물 용량',
fuelConsumption: '연료 소비'
fuelConsumption: '연료 소비',
buildCost: '건조 비용',
buildTime: '건조 시간',
perUnit: '단위당',
batchCalculator: '일괄 계산기',
quantity: '수량',
totalCost: '총 비용',
totalTime: '총 시간'
},
shipyardView: {
title: '조선소',
@@ -521,6 +532,8 @@ export default {
},
recycle: '회수',
destroy: '행성 파괴',
harvestDarkMatter: '암흑 물질 수확',
station: '주둔',
transportResources: '자원 수송',
totalCargoCapacity: '총 적재량',
used: '사용됨',
@@ -1784,6 +1797,27 @@ export default {
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: {
name: '캠페인',
description: '신비로운 은하를 탐험하고 고대의 비밀을 밝혀내세요',
@@ -1810,6 +1844,8 @@ export default {
branchUnlocked: '새로운 스토리 분기가 해금되었습니다!'
},
dialogue: {
title: '스토리 대화',
description: '캠페인 스토리 대화 내용',
skip: '건너뛰기',
continue: '계속',
finish: '완료',
@@ -1922,25 +1958,42 @@ export default {
spyAnyNPC: 'NPC 행성 정찰',
sendGiftToNPC: 'NPC에게 선물 보내기',
researchAstrophysics: '천체물리학을 레벨 1까지 연구',
researchAstrophysicsHigher: '천체물리학을 레벨 3까지 연구',
buildColonyShip: '식민선 건조',
colonizeNewPlanet: '새 행성 식민지화',
completeExpedition: '탐험 임무 완료',
colonizeMultiple: '5개 행성 식민지화',
completeExpedition: '3회 탐험 임무 완료',
expeditionDeepSpace: '2회 심우주 탐험 완료',
expeditionUncharted: '1회 미지 영역 탐험',
expeditionDangerous: '3회 위험 성운 탐험 완료',
discoverRuins: '고대 유적 발견',
researchComputer: '컴퓨터 기술을 레벨 4까지 연구',
researchImpulse: '임펄스 드라이브를 레벨 3까지 연구',
researchLaser: '레이저 기술을 레벨 5까지 연구',
researchIntergalactic: '컴퓨터 기술을 레벨 10까지 연구',
researchGraviton: '그래비톤 기술을 레벨 1까지 연구',
improveRelation: 'NPC와의 관계 개선',
reachFriendly: 'NPC와 우호 상태 도달',
spyHostileNPC: '적대 NPC 정찰',
reachFriendlyRelation: '임의의 NPC와 우호 상태 도달',
sendMultipleGifts: 'NPC에게 3회 선물 보내기',
spyHostileNPC: '적대 NPC 2곳 정찰',
formAlliance: '우호 NPC와 동맹 체결',
buildDefenses: '방어 시설 건설',
buildMissileSilo: '미사일 사일로를 레벨 2로 건설',
buildCruisers: '순양함 10대 건조',
winDefenseBattle: '방어전 승리',
defendAgainstAttack: '1회 공격 방어 성공',
spyEnemyPlanet: '적 행성 정찰',
spyEnemyPlanets: '적 행성 5곳 정찰',
winAttackBattles: '3회 공격전 승리',
attackEnemy: '적 공격',
recycleDebris: '잔해 재활용',
buildBattleships: '전함 10대 건조',
recycleDebris: '5회 잔해 재활용',
buildBattleships: '전함 20대 건조',
exploreDeepRuins: '깊은 유적 탐험',
researchHyperspace: '하이퍼스페이스 기술 연구',
defeatBoss: '신비한 적 격파',
researchHyperspace: '하이퍼스페이스 드라이브를 레벨 3까지 연구',
defeatBoss: '고대 수호자 격파',
colonizeSpecial: '특별한 장소 식민지화',
accumulateWealth: '총 자원 100만 축적',
continueDevelopment: '개발 계속'
},
dialogues: {

View File

@@ -161,6 +161,7 @@ export default {
totalCost: 'Общая стоимость',
totalPoints: 'Общие очки',
levelRange: 'Диапазон уровней',
researchSpeedBonus: 'Бонус скорости исследований',
capacity: 'Capacity/Effect',
storageCapacity: 'Capacity',
energyProduction: 'Energy Production',
@@ -269,6 +270,9 @@ export default {
},
research: {
researchTime: 'Время исследования',
levelRange: 'Диапазон уровней',
totalCost: 'Общая стоимость',
totalPoints: 'Всего очков',
capacity: 'Capacity/Effect',
storageCapacity: 'Capacity',
energyProduction: 'Energy Production',
@@ -402,6 +406,7 @@ export default {
spaceUsage: 'Использование полей',
level: 'Уровень',
upgradeCost: 'Стоимость улучшения',
build: 'Построить',
upgrade: 'Улучшить',
maxLevelReached: 'Достигнут максимальный уровень',
requirementsNotMet: 'Требования не выполнены',
@@ -423,9 +428,19 @@ export default {
'Пожалуйста, проверьте, достаточно ли у вас ресурсов, выполнены ли предварительные условия или нет других исследовательских задач.'
},
shipyard: {
attack: 'Атака',
shield: 'Щит',
armor: 'Броня',
speed: 'Скорость',
cargoCapacity: 'Грузоподъёмность',
fuelConsumption: 'Расход топлива'
fuelConsumption: 'Расход топлива',
buildCost: 'Стоимость постройки',
buildTime: 'Время постройки',
perUnit: 'За единицу',
batchCalculator: 'Калькулятор партии',
quantity: 'Количество',
totalCost: 'Общая стоимость',
totalTime: 'Общее время'
},
shipyardView: {
title: 'Верфь',
@@ -607,7 +622,9 @@ export default {
jumpGateSuccessMessage: 'Флот мгновенно переброшен к {target}',
jumpGateFailed: 'Переброска через ворота не удалась',
jumpGateFailedMessage: 'Проверьте состояние ворот и конфигурацию флота',
destroy: 'Уничтожение'
destroy: 'Уничтожение',
harvestDarkMatter: 'Сбор тёмной материи',
station: 'Расположение'
},
officersView: {
title: 'Офицеры',
@@ -1836,6 +1853,27 @@ export default {
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: {
name: 'Кампания',
description: 'Исследуйте загадочную галактику и раскройте древние тайны',
@@ -1863,6 +1901,8 @@ export default {
branchUnlocked: 'Новая сюжетная ветка разблокирована!'
},
dialogue: {
title: 'Сюжетный диалог',
description: 'Содержание сюжетного диалога кампании',
skip: 'Пропустить',
continue: 'Продолжить',
finish: 'Завершить',
@@ -1975,25 +2015,42 @@ export default {
spyAnyNPC: 'Разведать планету NPC',
sendGiftToNPC: 'Отправить подарок NPC',
researchAstrophysics: 'Исследовать Астрофизику до уровня 1',
researchAstrophysicsHigher: 'Исследовать Астрофизику до уровня 3',
buildColonyShip: 'Построить Колониальный корабль',
colonizeNewPlanet: 'Колонизировать новую планету',
completeExpedition: 'Завершить экспедиционную миссию',
colonizeMultiple: 'Колонизировать 5 планет',
completeExpedition: 'Завершить 3 экспедиционные миссии',
expeditionDeepSpace: 'Завершить 2 экспедиции в глубокий космос',
expeditionUncharted: 'Исследовать 1 неизвестный регион',
expeditionDangerous: 'Завершить 3 экспедиции в опасные туманности',
discoverRuins: 'Обнаружить древние руины',
researchComputer: 'Исследовать Компьютерную технологию до уровня 4',
researchImpulse: 'Исследовать Импульсный двигатель до уровня 3',
researchLaser: 'Исследовать Лазерную технологию до уровня 5',
researchIntergalactic: 'Исследовать Компьютерную технологию до уровня 10',
researchGraviton: 'Исследовать Гравитонную технологию до уровня 1',
improveRelation: 'Улучшить отношения с NPC',
reachFriendly: 'Достичь дружеского статуса с NPC',
spyHostileNPC: 'Разведать враждебного NPC',
reachFriendlyRelation: 'Достичь дружеского статуса с любым NPC',
sendMultipleGifts: 'Отправить 3 подарка NPC',
spyHostileNPC: 'Разведать 2 враждебных NPC',
formAlliance: 'Заключить альянс с дружественным NPC',
buildDefenses: 'Построить оборонительные сооружения',
buildMissileSilo: 'Построить Ракетную шахту до уровня 2',
buildCruisers: 'Построить 10 Крейсеров',
winDefenseBattle: 'Выиграть оборонительное сражение',
defendAgainstAttack: 'Успешно отразить 1 атаку',
spyEnemyPlanet: 'Разведать вражескую планету',
spyEnemyPlanets: 'Разведать 5 вражеских планет',
winAttackBattles: 'Выиграть 3 атакующих сражения',
attackEnemy: 'Атаковать врага',
recycleDebris: 'Переработать обломки',
buildBattleships: 'Построить 10 Линкоров',
recycleDebris: 'Переработать обломки 5 раз',
buildBattleships: 'Построить 20 Линкоров',
exploreDeepRuins: 'Исследовать глубокие руины',
researchHyperspace: 'Исследовать Гиперпространственную технологию',
defeatBoss: 'Победить загадочного врага',
researchHyperspace: 'Исследовать Гиперпространственный двигатель до уровня 3',
defeatBoss: 'Победить Древнего Стража',
colonizeSpecial: 'Колонизировать особое место',
accumulateWealth: 'Накопить 1 миллион ресурсов',
continueDevelopment: 'Продолжить развитие'
},
dialogues: {

View File

@@ -32,7 +32,8 @@ export default {
exitConfirmTitle: '退出游戏',
exitConfirmMessage: '确定要退出游戏吗?游戏进度会自动保存。',
points: '积分',
retry: '重试'
retry: '重试',
playerName: '玩家名称'
},
errors: {
requirementsNotMet: '不满足前置条件',
@@ -264,6 +265,9 @@ export default {
},
research: {
researchTime: '研究时间',
levelRange: '等级范围',
totalCost: '总成本',
totalPoints: '总积分',
attackBonus: '攻击加成',
shieldBonus: '护盾加成',
@@ -384,6 +388,7 @@ export default {
title: '建筑',
spaceUsage: '占用空间',
upgradeCost: '升级消耗',
build: '建造',
upgrade: '升级',
maxLevelReached: '等级已满',
requirementsNotMet: '条件不足',
@@ -404,9 +409,19 @@ export default {
researchFailedMessage: '请检查资源是否足够、前置条件是否满足,或是否有其他研究任务。'
},
shipyard: {
attack: '攻击力',
shield: '护盾',
armor: '装甲',
speed: '速度',
cargoCapacity: '载货量',
fuelConsumption: '燃料消耗'
fuelConsumption: '燃料消耗',
buildCost: '建造成本',
buildTime: '建造时间',
perUnit: '单位',
batchCalculator: '批量计算器',
quantity: '数量',
totalCost: '总成本',
totalTime: '总时间'
},
shipyardView: {
title: '船坞',
@@ -511,6 +526,8 @@ export default {
},
recycle: '回收',
destroy: '行星毁灭',
harvestDarkMatter: '暗物质采集',
station: '驻守协防',
transportResources: '运输资源',
totalCargoCapacity: '总载货量',
used: '已用',
@@ -1782,6 +1799,8 @@ export default {
branchUnlocked: '新的故事分支已解锁!'
},
dialogue: {
title: '剧情对话',
description: '战役剧情对话内容',
skip: '跳过',
continue: '继续',
finish: '完成',
@@ -1966,25 +1985,42 @@ export default {
spyAnyNPC: '侦查任意NPC星球',
sendGiftToNPC: '向任意NPC送礼',
researchAstrophysics: '研究天体物理学到1级',
researchAstrophysicsHigher: '研究天体物理学到3级',
buildColonyShip: '建造殖民船',
colonizeNewPlanet: '殖民新星球',
completeExpedition: '完成远征任务',
colonizeMultiple: '殖民5个星球',
completeExpedition: '完成3次远征任务',
expeditionDeepSpace: '完成2次深空远征',
expeditionUncharted: '探索1次未知区域',
expeditionDangerous: '完成3次危险星云远征',
discoverRuins: '发现古代遗迹',
researchComputer: '研究电脑技术到4级',
researchImpulse: '研究脉冲驱动到3级',
researchLaser: '研究激光技术到5级',
researchIntergalactic: '研究电脑技术到10级',
researchGraviton: '研究引力子技术到1级',
improveRelation: '提升与NPC的关系',
reachFriendly: '与NPC达到友好关系',
spyHostileNPC: '侦查敌对NPC',
reachFriendlyRelation: '与任意NPC达到友好关系',
sendMultipleGifts: '向NPC发送3次礼物',
spyHostileNPC: '侦查2个敌对NPC',
formAlliance: '与友好NPC结盟',
buildDefenses: '建造防御设施',
buildMissileSilo: '建造导弹发射井到2级',
buildCruisers: '建造10艘巡洋舰',
winDefenseBattle: '赢得防御战斗',
defendAgainstAttack: '成功防御1次攻击',
spyEnemyPlanet: '侦查敌方星球',
spyEnemyPlanets: '侦查5个敌方星球',
winAttackBattles: '赢得3次进攻战斗',
attackEnemy: '攻击敌方',
recycleDebris: '回收残骸',
buildBattleships: '建造10艘战列舰',
recycleDebris: '回收5次残骸',
buildBattleships: '建造20艘战列舰',
exploreDeepRuins: '探索遗迹深处',
researchHyperspace: '研究超空间技术',
defeatBoss: '击败神秘敌人',
researchHyperspace: '研究超空间驱动到3级',
defeatBoss: '击败古代守护者',
colonizeSpecial: '殖民特殊位置',
accumulateWealth: '积累总资源达到100万',
continueDevelopment: '继续发展'
},
dialogues: {
@@ -2075,5 +2111,31 @@ export default {
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}'
}
}

View File

@@ -270,6 +270,9 @@ export default {
},
research: {
researchTime: '研究時間',
levelRange: '等級範圍',
totalCost: '總成本',
totalPoints: '總積分',
capacity: '容量/效果',
attackBonus: '攻擊加成',
shieldBonus: '護盾加成',
@@ -414,11 +417,20 @@ export default {
researchFailedMessage: '請檢查資源是否足夠、前置條件是否滿足,或是否有其他研究任務。'
},
shipyard: {
missileAttack: '導彈攻擊',
attack: '攻擊',
shield: '護盾',
armor: '裝甲',
missileAttack: '導彈攻擊',
speed: '速度',
cargoCapacity: '載貨量',
fuelConsumption: '燃料消耗',
buildCost: '建造成本',
buildTime: '建造時間',
perUnit: '單位',
batchCalculator: '批量計算器',
quantity: '數量',
totalCost: '總成本',
totalTime: '總時間',
build: '建造'
},
shipyardView: {
@@ -530,6 +542,8 @@ export default {
},
recycle: '回收',
destroy: '行星毀滅',
harvestDarkMatter: '暗物質採集',
station: '駐守協防',
transportResources: '運輸資源',
totalCargoCapacity: '總載貨量',
used: '已用',
@@ -1933,6 +1947,27 @@ export default {
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: {
name: '戰役',
description: '探索神秘的銀河系,揭開古代文明的秘密',
@@ -1960,6 +1995,8 @@ export default {
branchUnlocked: '新的故事分支已解鎖!'
},
dialogue: {
title: '劇情對話',
description: '戰役劇情對話內容',
skip: '跳過',
continue: '繼續',
finish: '完成',
@@ -2144,25 +2181,42 @@ export default {
spyAnyNPC: '偵查任意NPC星球',
sendGiftToNPC: '向任意NPC送禮',
researchAstrophysics: '研究天體物理學到1級',
researchAstrophysicsHigher: '研究天體物理學到3級',
buildColonyShip: '建造殖民船',
colonizeNewPlanet: '殖民新星球',
completeExpedition: '完成遠征任務',
colonizeMultiple: '殖民5個星球',
completeExpedition: '完成3次遠征任務',
expeditionDeepSpace: '完成2次深空遠征',
expeditionUncharted: '探索1次未知區域',
expeditionDangerous: '完成3次危險星雲遠征',
discoverRuins: '發現古代遺跡',
researchComputer: '研究電腦技術到4級',
researchImpulse: '研究脈衝驅動到3級',
researchLaser: '研究雷射技術到5級',
researchIntergalactic: '研究電腦技術到10級',
researchGraviton: '研究引力子技術到1級',
improveRelation: '提升與NPC的關係',
reachFriendly: '與NPC達到友好關係',
spyHostileNPC: '偵查敵對NPC',
reachFriendlyRelation: '與任意NPC達到友好關係',
sendMultipleGifts: '向NPC發送3次禮物',
spyHostileNPC: '偵查2個敵對NPC',
formAlliance: '與友好NPC結盟',
buildDefenses: '建造防禦設施',
buildMissileSilo: '建造飛彈發射井到2級',
buildCruisers: '建造10艘巡洋艦',
winDefenseBattle: '贏得防禦戰鬥',
defendAgainstAttack: '成功防禦1次攻擊',
spyEnemyPlanet: '偵查敵方星球',
spyEnemyPlanets: '偵查5個敵方星球',
winAttackBattles: '贏得3次進攻戰鬥',
attackEnemy: '攻擊敵方',
recycleDebris: '回收殘骸',
buildBattleships: '建造10艘戰列艦',
recycleDebris: '回收5次殘骸',
buildBattleships: '建造20艘戰列艦',
exploreDeepRuins: '探索遺跡深處',
researchHyperspace: '研究超空間技術',
defeatBoss: '擊敗神秘敵人',
researchHyperspace: '研究超空間驅動到3級',
defeatBoss: '擊敗古代守護者',
colonizeSpecial: '殖民特殊位置',
accumulateWealth: '積累總資源達到100萬',
continueDevelopment: '繼續發展'
},
dialogues: {

View File

@@ -1,14 +1,13 @@
/**
* WebDAV 同步服务
* 支持将存档上传到 WebDAV 服务器如坚果云、Nextcloud、NAS等
* 注意WebDAV 配置存储在 gameStore.webdavConfig 中,与用户数据一起持久化
*/
export interface WebDAVConfig {
serverUrl: string // WebDAV 服务器地址,如 https://dav.jianguoyun.com/dav/
username: string // 用户名
password: string // 密码或应用专用密码
basePath: string // 存档存放路径,如 /ogame-saves/
}
import type { WebDAVConfig } from '@/types/game'
// 重新导出类型以保持向后兼容
export type { WebDAVConfig }
export interface WebDAVFile {
name: string
@@ -18,29 +17,47 @@ export interface WebDAVFile {
isDirectory: boolean
}
const STORAGE_KEY = 'ogame-webdav-config'
// 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',
// 获取保存的 WebDAV 配置
export const getWebDAVConfig = (): WebDAVConfig | null => {
try {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
return JSON.parse(saved)
}
} catch (e) {
console.error('Failed to load WebDAV config:', e)
}
return null
}
// 上传相关
uploadSuccess: 'webdav.uploadSuccess',
noWritePermission: 'webdav.noWritePermission',
insufficientStorage: 'webdav.insufficientStorage',
uploadFailedHttp: 'webdav.uploadFailedHttp',
uploadError: 'webdav.uploadError',
// 保存 WebDAV 配置
export const saveWebDAVConfig = (config: WebDAVConfig): void => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
}
// 下载相关
fileNotExist: 'webdav.fileNotExist',
downloadFailedHttp: 'webdav.downloadFailedHttp',
downloadError: 'webdav.downloadError',
// 清除 WebDAV 配置
export const clearWebDAVConfig = (): void => {
localStorage.removeItem(STORAGE_KEY)
// 列表相关
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[]
}
// 构建 Authorization header
@@ -66,7 +83,7 @@ const normalizePath = (serverUrl: string, basePath: string, fileName?: string):
}
// 测试 WebDAV 连接
export const testWebDAVConnection = async (config: WebDAVConfig): Promise<{ success: boolean; message: string }> => {
export const testWebDAVConnection = async (config: WebDAVConfig): Promise<WebDAVResult> => {
try {
const url = normalizePath(config.serverUrl, config.basePath)
@@ -80,33 +97,30 @@ export const testWebDAVConnection = async (config: WebDAVConfig): Promise<{ succ
})
if (response.ok || response.status === 207) {
return { success: true, message: 'WebDAV 连接成功' }
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess }
}
if (response.status === 401) {
return { success: false, message: '认证失败,请检查用户名和密码' }
return { success: false, messageKey: WebDAVMessageKey.authFailed }
}
if (response.status === 404) {
// 尝试创建目录
const createResult = await createDirectory(config, config.basePath)
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) {
const message = error instanceof Error ? error.message : String(error)
const errorMessage = error instanceof Error ? error.message : String(error)
// CORS 错误的处理
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
return {
success: false,
message: '网络错误,可能是 CORS 限制。建议使用支持 CORS 的 WebDAV 服务或通过代理访问。'
if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) {
return { success: false, messageKey: WebDAVMessageKey.networkError }
}
}
return { success: false, message: `连接错误: ${message}` }
return { success: false, messageKey: WebDAVMessageKey.connectionError, messageParams: { error: errorMessage } }
}
}
@@ -130,11 +144,7 @@ const createDirectory = async (config: WebDAVConfig, path: string): Promise<bool
}
// 上传存档到 WebDAV
export const uploadToWebDAV = async (
config: WebDAVConfig,
data: string,
fileName?: string
): Promise<{ success: boolean; message: string; fileName?: string }> => {
export const uploadToWebDAV = async (config: WebDAVConfig, data: string, fileName?: string): Promise<WebDAVResult> => {
try {
// 生成带时间戳的文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
@@ -151,25 +161,25 @@ export const uploadToWebDAV = async (
})
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) {
return { success: false, message: '认证失败' }
return { success: false, messageKey: WebDAVMessageKey.authFailed }
}
if (response.status === 403) {
return { success: false, message: '没有写入权限' }
return { success: false, messageKey: WebDAVMessageKey.noWritePermission }
}
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) {
const message = error instanceof Error ? error.message : String(error)
return { success: false, message: `上传错误: ${message}` }
const errorMessage = error instanceof Error ? error.message : String(error)
return { success: false, messageKey: WebDAVMessageKey.uploadError, messageParams: { error: errorMessage } }
}
}
@@ -212,7 +222,7 @@ const parsePropfindResponse = (xml: string, _basePath: string): WebDAVFile[] =>
}
// 列出 WebDAV 目录中的存档文件
export const listWebDAVFiles = async (config: WebDAVConfig): Promise<{ success: boolean; files?: WebDAVFile[]; message?: string }> => {
export const listWebDAVFiles = async (config: WebDAVConfig): Promise<WebDAVResult> => {
try {
const url = normalizePath(config.serverUrl, config.basePath)
@@ -227,29 +237,26 @@ export const listWebDAVFiles = async (config: WebDAVConfig): Promise<{ success:
if (!response.ok && response.status !== 207) {
if (response.status === 401) {
return { success: false, message: '认证失败' }
return { success: false, messageKey: WebDAVMessageKey.authFailed }
}
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 files = parsePropfindResponse(xml, config.basePath)
return { success: true, files }
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess, files }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { success: false, message: `获取文件列表错误: ${message}` }
const errorMessage = error instanceof Error ? error.message : String(error)
return { success: false, messageKey: WebDAVMessageKey.listError, messageParams: { error: errorMessage } }
}
}
// 从 WebDAV 下载存档
export const downloadFromWebDAV = async (
config: WebDAVConfig,
fileName: string
): Promise<{ success: boolean; data?: string; message?: string }> => {
export const downloadFromWebDAV = async (config: WebDAVConfig, fileName: string): Promise<WebDAVResult> => {
try {
const url = normalizePath(config.serverUrl, config.basePath, fileName)
@@ -262,27 +269,24 @@ export const downloadFromWebDAV = async (
if (!response.ok) {
if (response.status === 401) {
return { success: false, message: '认证失败' }
return { success: false, messageKey: WebDAVMessageKey.authFailed }
}
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()
return { success: true, data }
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess, data }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { success: false, message: `下载错误: ${message}` }
const errorMessage = error instanceof Error ? error.message : String(error)
return { success: false, messageKey: WebDAVMessageKey.downloadError, messageParams: { error: errorMessage } }
}
}
// 删除 WebDAV 文件
export const deleteFromWebDAV = async (
config: WebDAVConfig,
fileName: string
): Promise<{ success: boolean; message?: string }> => {
export const deleteFromWebDAV = async (config: WebDAVConfig, fileName: string): Promise<WebDAVResult> => {
try {
const url = normalizePath(config.serverUrl, config.basePath, fileName)
@@ -294,20 +298,20 @@ export const deleteFromWebDAV = async (
})
if (response.ok || response.status === 204) {
return { success: true }
return { success: true, messageKey: WebDAVMessageKey.connectionSuccess }
}
if (response.status === 401) {
return { success: false, message: '认证失败' }
return { success: false, messageKey: WebDAVMessageKey.authFailed }
}
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) {
const message = error instanceof Error ? error.message : String(error)
return { success: false, message: `删除错误: ${message}` }
const errorMessage = error instanceof Error ? error.message : String(error)
return { success: false, messageKey: WebDAVMessageKey.deleteError, messageParams: { error: errorMessage } }
}
}

View File

@@ -12,7 +12,8 @@ import type {
IncomingFleetAlert,
MissileAttack,
AchievementStats,
AchievementProgress
AchievementProgress,
WebDAVConfig
} from '@/types/game'
import { TechnologyType, OfficerType } from '@/types/game'
import { initializeAchievementStats, initializeAchievements } from '@/logic/achievementLogic'
@@ -61,7 +62,8 @@ export const useGameStore = defineStore('game', {
research: true,
unlock: true
}
}
},
webdavConfig: null as WebDAVConfig | null
}),
actions: {
async requestBrowserPermission(): Promise<boolean> {

View File

@@ -120,6 +120,15 @@
@apply border-border outline-ring/50;
}
html {
/* 平滑过渡 */
transition: background-color 0.3s ease, color 0.3s ease;
/* 禁用文本大小调整,防止移动端自动放大文本 */
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
@apply bg-background text-foreground;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@@ -128,11 +137,6 @@
-moz-osx-font-smoothing: grayscale;
}
/* 平滑过渡 */
html {
transition: background-color 0.3s ease, color 0.3s ease;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
@@ -218,3 +222,21 @@ aside nav a:hover button {
background-color: oklch(0.3 0.02 85) !important;
color: oklch(0.95 0.008 85) !important;
}
/* 修复 Edge-to-Edge 模式下 Portal 容器的定位问题 */
[data-reka-portal],
[data-radix-portal] {
position: fixed !important;
left: 0 !important;
top: 0 !important;
right: 0 !important;
bottom: 0 !important;
pointer-events: none;
padding: 0 !important;
margin: 0 !important;
}
[data-reka-portal] > *,
[data-radix-portal] > * {
pointer-events: auto;
}

View File

@@ -1226,3 +1226,11 @@ export interface QuestNotification {
rewards?: QuestReward
read?: boolean
}
// WebDAV 配置
export interface WebDAVConfig {
serverUrl: string // WebDAV 服务器地址
username: string // 用户名
password: string // 密码或应用专用密码
basePath: string // 存档存放路径
}

View File

@@ -181,133 +181,6 @@
</ScrollableDialogContent>
</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">
<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 { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
import { toast } from 'vue-sonner'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import {
FixedPagination,
Pagination,
@@ -523,7 +393,7 @@
import NpcRelationCard from '@/components/npc/NpcRelationCard.vue'
import NpcRelationRow from '@/components/npc/NpcRelationRow.vue'
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 {
Search,
@@ -533,11 +403,7 @@
Swords,
Activity,
LayoutGrid,
List,
Handshake,
ChevronDown,
ArrowLeftRight,
Eye
List
} from 'lucide-vue-next'
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
@@ -548,9 +414,6 @@
const activeTab = ref('all')
// NPC互动面板状态
const interactionPanelOpen = ref(true)
// 视图模式: 'card' | 'list'
const viewMode = ref<'card' | 'list'>('list')
@@ -864,188 +727,6 @@
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>
<style>

View File

@@ -1117,13 +1117,17 @@
// 获取任务类型名称
const getMissionTypeName = (missionType: string): string => {
const typeMap: Record<string, string> = {
[MissionType.Attack]: t('fleetView.attack'),
[MissionType.Transport]: t('fleetView.transport'),
[MissionType.Colonize]: t('fleetView.colonize'),
[MissionType.Spy]: t('fleetView.spy'),
[MissionType.Deploy]: t('fleetView.deploy'),
[MissionType.Expedition]: t('fleetView.expedition'),
[MissionType.Recycle]: t('fleetView.recycle'),
[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
}

View File

@@ -334,7 +334,7 @@
<PrivacyDialog v-model:open="showPrivacyDialog" />
<!-- WebDAV 配置对话框 -->
<WebDAVConfigDialog v-model:open="showWebDAVConfig" @saved="onWebDAVConfigSaved" />
<WebDAVConfigDialog v-model:open="showWebDAVConfig" />
<!-- WebDAV 文件列表对话框 -->
<WebDAVFileListDialog v-model:open="showWebDAVFiles" :config="webdavConfig" @select="handleWebDAVDownload" />
@@ -389,12 +389,7 @@
import WebDAVConfigDialog from '@/components/settings/WebDAVConfigDialog.vue'
import WebDAVFileListDialog from '@/components/settings/WebDAVFileListDialog.vue'
import { useHints } from '@/composables/useHints'
import {
type WebDAVConfig,
getWebDAVConfig,
uploadToWebDAV,
downloadFromWebDAV
} from '@/services/webdavService'
import { uploadToWebDAV, downloadFromWebDAV } from '@/services/webdavService'
const { t } = useI18n()
const { hintsEnabled, setHintsEnabled, resetHints } = useHints()
@@ -415,7 +410,7 @@
// WebDAV 相关状态
const showWebDAVConfig = ref(false)
const showWebDAVFiles = ref(false)
const webdavConfig = ref<WebDAVConfig | null>(getWebDAVConfig())
const webdavConfig = computed(() => gameStore.webdavConfig)
const isWebDAVUploading = ref(false)
// 确保通知设置存在
@@ -758,11 +753,6 @@
gameStore.player.backgroundEnabled = val
}
// WebDAV 配置保存回调
const onWebDAVConfigSaved = () => {
webdavConfig.value = getWebDAVConfig()
}
// WebDAV 上传
const handleWebDAVUpload = async () => {
if (!webdavConfig.value) return
@@ -792,7 +782,7 @@
if (result.success) {
toast.success(t('settings.webdav.uploadSuccess'))
} else {
toast.error(result.message || t('settings.webdav.uploadFailed'))
toast.error(t(result.messageKey) || t('settings.webdav.uploadFailed'))
}
} catch (error) {
console.error('WebDAV upload failed:', error)
@@ -810,7 +800,7 @@
const result = await downloadFromWebDAV(webdavConfig.value, fileName)
if (!result.success || !result.data) {
toast.error(result.message || t('settings.webdav.downloadFailed'))
toast.error(t(result.messageKey) || t('settings.webdav.downloadFailed'))
return
}

View File

@@ -4,11 +4,11 @@ import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { VitePWA } from 'vite-plugin-pwa'
import autoprefixer from 'autoprefixer'
import pkg from './package.json'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// @ts-expect-error - Vite CSS transformer type
export default defineConfig(async () => {
const plugins = [
vue(),
@@ -110,12 +110,19 @@ export default defineConfig(async () => {
plugins,
resolve: { alias: { '@': path.resolve(__dirname, './src') } },
css: {
postcss: {
plugins: [
autoprefixer({
overrideBrowserslist: ['Android >= 4.1', 'iOS >= 7.1', 'Chrome >= 31', 'Firefox >= 31', 'ie >= 8']
})
]
// 使用 lightningcss 处理 CSS自动转换 oklch 等新语法为兼容格式
transformer: 'lightningcss',
lightningcss: {
// 目标浏览器Android 5+, iOS 10+, Chrome 60+
targets: {
android: 5 << 16, // Android 5.0
chrome: 60 << 16, // Chrome 60
ios_saf: 10 << 16 // iOS Safari 10
},
// 禁用现代 CSS 特性,确保兼容旧版浏览器
drafts: {
customMedia: false
}
}
},
// 优化依赖预构建