116 Commits

Author SHA1 Message Date
谦君
80071ef0f6 Merge pull request #53 from CN-WenYu/main
feat: 为 ID 生成添加随机后缀并增强 GM 面板功能
2026-04-02 16:39:20 +08:00
wenyu
d8dd4e7317 refactor: 统一使用 generateId 函数生成唯一标识符
- 在 GMView、FleetView 中替换 Date.now() 生成 ID 的方式
- 在 DiplomacyView 中优化排序函数,避免重复过滤与排序
2026-03-18 21:04:11 +08:00
wenyu
15eccd8f0d refactor: 简化生成 ID 函数并改进类型安全
- 移除 generateId 函数的 timestamp 参数,改为在函数内部获取当前时间戳
- 在 DiplomacyView 中为 NPC 排序添加类型定义和 exhaustive 检查
- 在 GMView 中为预设管理添加更精确的类型映射
- 重构 migration 工具函数,提取辅助函数并改进类型定义
2026-03-18 20:47:14 +08:00
wenyu
d4f55f6916 docs: 更新修复重复星球 ID 的注释以澄清逻辑
更新 fixDuplicatePlanetIds 函数中的注释,明确说明 buildDuplicatePlanetIdMap 已在上一步修复重复 ID,当前函数仅通过检查 idMap 是否非空来判断迁移是否发生实际修改。
2026-03-18 20:30:51 +08:00
wenyu
2cfa275c7a fix: 修复重复星球 ID 并优化 NPC 列表排序性能
修复玩家星球重复 ID 问题,通过构建映射关系更新相关引用(舰队任务、间谍报告等),避免数据指向错误目标。同时优化外交界面 NPC 列表计算,避免重复排序操作提升性能,并添加空列表检查防止除零错误。
2026-03-18 20:26:06 +08:00
wenyu
b1cf0acaae refactor(logic): 将导弹相关逻辑从 shipLogic 移动到 missileLogic
重构代码结构,将与导弹容量计算和验证相关的函数从 shipLogic 模块提取到新创建的 missileLogic 模块,以提高代码的模块化和可维护性。同时更新所有相关导入路径以引用新的模块。
2026-03-18 19:41:48 +08:00
wenyu
8f29a63756 refactor(视图): 提取导弹发射井容量计算逻辑
将导弹发射井容量计算从 GMView.vue 中提取到专用逻辑模块
简化防御设置中的导弹数量分配逻辑,统一使用提取的函数
2026-03-18 19:37:18 +08:00
wenyu
a8ab2b0f1a feat(本地化与工具): 为排序功能添加升序/降序翻译并统一 ID 生成
- 为所有支持的语言添加排序功能的 "ascending" 和 "descending" 翻译
- 创建统一的 ID 生成工具函数 generateId,用于生成带前缀的业务 ID
- 重构多个逻辑模块(battleLogic、moonLogic 等)使用新的 ID 生成函数
- 改进 GM 视图的类型安全,添加预设数据验证和类型保护
2026-03-18 19:29:08 +08:00
wenyu
28c3da2582 feat(GMView): 添加预设管理功能,支持删除和覆盖确认
- 为所有语言文件添加删除预设、确认覆盖等翻译键
- 在预设选择器旁添加删除按钮,允许删除自定义预设
- 保存同名预设时弹出确认对话框,防止意外覆盖
- 禁止删除默认预设,并提供相应的错误提示
2026-03-18 18:47:22 +08:00
wenyu
b0a7b5ce90 refactor(migration): 简化父行星 ID 的映射更新逻辑
- 移除冗余的 idMap.has 检查,直接使用 idMap.get 获取新 ID
2026-03-18 18:30:54 +08:00
wenyu
bd46c24824 fix: 修复保存预设时未初始化数组和重复星球 ID 迁移逻辑
修复 GM 视图中保存自定义预设时,当对应标签页的预设数组未初始化导致的保存失败问题。同时改进迁移工具中重复星球 ID 的处理逻辑,确保正确分组并更新关联的月球数据。
2026-03-18 18:03:38 +08:00
wenyu
8e49998205 feat: 为 ID 生成添加随机后缀并增强 GM 面板功能
- 为战斗报告、星球、月球、任务通知和舰队任务等 ID 添加随机后缀,避免重复 ID
- 在 GM 面板中添加预设管理系统,支持保存和应用建筑、研究、舰船和防御的配置
- 在外交界面添加 NPC 排序功能,支持按声望、星球数量、难度和盟友数量排序
- 修复数据迁移中的重复星球 ID 问题,确保月球与母星关联正确
- 优化 GM 面板的资源最大化功能,基于实际存储容量设置资源
- 为所有支持的语言添加相关翻译文本
2026-03-18 17:59:00 +08:00
谦君
703563c9b2 1.6.5 2026-01-23 02:24:34 +08:00
谦君
d44ea60ae9 1.6.5 2026-01-23 01:38:12 +08:00
谦君
1fc807915f 1.6.5 2026-01-23 01:28:57 +08:00
谦君
bd6c474913 Merge pull request #38 from coolxitech/main
优化Docker镜像构建
2026-01-11 19:25:30 +08:00
谦君
66c0ed8d0e 更新 2026-01-11 19:25:12 +08:00
coolxitech
9634dcb023 build(ci): 优化 Docker 构建流程并添加多平台支持
- 在 Dockerfile 中添加构建参数和标签信息用于缓存破坏
- 使用 --chown 确保正确的文件权限并验证构建产物
- 添加构建产物时间戳检查以确保最新性
- 获取并使用版本号进行镜像标签管理
- 添加清理冲突镜像标签的步骤
- 配置多平台构建支持(linux/amd64,linux/arm64)
- 添加版本标签和构建参数传递
- 配置构建缓存和镜像推送功能
2026-01-08 17:55:40 +08:00
coolxitech
e4c4cdd63c chore(workflow): 更新 GitHub Actions 工作流配置
- 升级 actions/checkout 从 v4 到 v6
- 升级 pnpm/action-setup 从 v3 到 v4 并更新版本到 latest
- 升级 actions/setup-node 从 v4 到 v6
- 升级 actions/setup-java 从 v4 到 v5
- 升级 actions/cache 从 v4 到 v5
- 升级 softprops/action-gh-release 从 v1 到 v2
- 升级 actions/configure-pages 从 v3 到 v5
- 升级 actions/deploy-pages 从 v2 到 v4
- 添加构建产物验证步骤
- 添加缓存 pnpm 依赖的配置
- 优化 Docker 镜像标签和元数据配置
- 改进条件判断逻辑以优化 Docker 推送流程
2026-01-08 17:40:29 +08:00
coolxitech
7279bcbc89 chore(workflow): 更新 github pages 工作流配置
- 为 pnpm action setup 添加版本配置
- 指定使用最新版本的 pnpm
- 保持 nodejs 安装配置不变
2026-01-08 17:18:25 +08:00
coolxitech
d9c708e0ca feat(docker): 添加完整的 Docker 构建支持
- 重构 Dockerfile 支持本地完整源码构建流程
- 添加 CI 专用的 Dockerfile.ci 使用预构建产物
- 创建 .dockerignore 和 .dockerignore.ci 文件优化构建上下文
- 添加 build-docker.sh 和 build-docker.bat 本地构建脚本
- 更新 GitHub Actions 工作流支持 Node.js 环境和 pnpm 依赖管理
- 添加 DOCKER.md 详细说明文档
- 优化 nginx 配置和端口暴露设置
2026-01-08 17:13:46 +08:00
coolxitech
21cf5762d2 chore: 移除 packageManager 配置项
- 从 package.json 中删除了 pnpm 的 packageManager 指定配置
- 保持项目构建配置的简洁性
- 统一依赖管理方式,避免版本冲突问题
- 确保所有开发者使用相同的包管理器版本
- 减少不必要的配置冗余
- 提高项目的可维护性和一致性
2026-01-08 17:01:19 +08:00
coolxitech
8db70ea674 fix(types): 修复类型检查错误
- 添加类型断言以解决类型不匹配问题
- 确保 typeKey 正确映射到 settings.types 的键类型
2026-01-08 17:01:19 +08:00
酷曦科技
136591a3dd 无缓存构建 2026-01-08 15:40:02 +08:00
谦君
72f37aa435 Create FUNDING.yml 2026-01-06 15:14:32 +08:00
谦君
4c54e1b773 撤回 2026-01-06 11:11:18 +08:00
谦君
9e8ceb0414 优化 2026-01-06 08:15:59 +08:00
谦君
ec96d2541e 修复问题 2026-01-06 03:06:14 +08:00
谦君
9e7560cc4b 1.6.0更新 2026-01-06 03:00:02 +08:00
谦君
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
谦君
b9b2b0966c ci: 构建流程中显式安装ImageMagick
在GitHub Actions构建流程中新增ImageMagick安装步骤,确保生成Android图标时依赖环境一致,提升CI稳定性。
2025-12-25 18:29:19 +08:00
谦君
724a70bebb docs: 新增西班牙语和日语README并优化多语言文档
新增README-ES.md(西班牙语)和README-JA.md(日语)文档,完善多语言README互链。优化各语言README徽章、技术栈、外链格式及语言切换区,提升文档一致性与可读性。
2025-12-25 18:25:08 +08:00
谦君
b24a262ca7 Update build.gradle 2025-12-24 03:44:42 +08:00
谦君
06b878a731 chore: 统一Server文件名与产品名为OGame-Vue-Ts
CI构建流程中Server产物文件名统一为OGame-Vue-Ts前缀,package.json中productName同步改为OGame-Vue-Ts,保持产品命名一致性,便于识别与管理。
2025-12-24 03:36:19 +08:00
谦君
8c799dc3bf chore: 精简README下载区与统一产品名
移除各语言README中的下载区内容,保持文档简洁一致。package.json中productName由“OGame-Vue-Ts”改为“OGame”,统一产品命名。
2025-12-24 03:22:06 +08:00
谦君
4a0734bb04 chore: README下载区样式微调
移除README下载区多余空行,保持文档结构简洁一致。
2025-12-24 03:13:51 +08:00
谦君
c11699706b chore: 多语言README下载区样式优化
统一德语、英文、韩语、俄语、繁体中文README下载区样式,移除多语言下载标题,提升文档结构一致性与可读性。
2025-12-24 03:12:53 +08:00
谦君
11dbdcc82a chore: 统一项目名称为OGame-Vue-Ts
将android打包文件名、package.json中的productName统一为“OGame-Vue-Ts”,去除main.go中的emoji符号,控制台输出更简洁。提升品牌一致性与可读性。
2025-12-24 03:11:18 +08:00
谦君
9ea6fabbd1 fix: 资源操作兼容Partial类型
将addResources和deductResources的参数类型由Resources调整为Partial<Resources>,避免部分字段缺失时报错。同步修正任务奖励发放逻辑,提升资源操作的健壮性。
2025-12-24 02:51:27 +08:00
谦君
5a06022798 chore: 版本号升级至1.5.0
package.json中的version字段由1.4.0提升至1.5.0,为新功能或重要更新做准备。
2025-12-24 02:01:32 +08:00
谦君
b85b84399a chore: 修改go模块名与更新构建日期
go.mod中的模块名由“OGame Vue Ts”更改为“ogame-vue-ts”,package.json的buildDate更新为2025/12/24 01:51:29。
2025-12-24 01:52:58 +08:00
谦君
b6be379702 fix: 优化成就页头部布局适配
将成就页头部容器由响应式flex方向调整为始终横向排列,统一各端显示效果,提升布局一致性。
2025-12-24 01:47:17 +08:00
谦君
5e3557e2da feat: 新增多语言README并优化文档结构
新增德语、俄语、韩语、繁体中文多语言README,英文与简体中文README同步优化,统一下载链接与徽章样式,完善多语言入口。提升国际化支持与文档可读性。
2025-12-24 01:45:17 +08:00
谦君
a475b1b554 chore: 移除多余依赖与PostCSS配置
删除@capacitor/share、@capacitor/status-bar、@capawesome/capacitor-file-picker、postcss及相关依赖,移除postcss.config.js,简化依赖树。README下载链接样式调整为纯文本列表,提升可维护性。
2025-12-20 04:05:20 +08:00
谦君
dc5f1c1370 feat: 优化原生端顶部间距与资源栏样式
根据平台动态调整主内容区与资源栏的顶部间距,原生端顶部间距由60px提升至80px,资源栏与展开栏的padding与定位同步适配,提升Android/Capacitor端显示一致性。
2025-12-20 03:41:16 +08:00
谦君
8e34d08545 feat: 支持Android端导出到Documents并多语言提示
Android端数据导出改为直接保存至Documents目录,导出成功后弹出带路径的多语言提示。引入@capacitor/status-bar与@capawesome/capacitor-file-picker依赖,主入口设置原生状态栏颜色与样式。各语言包补充导出成功带路径提示文案。
2025-12-20 02:55:00 +08:00
谦君
65a143bec2 feat: 支持Android端数据导出与分享
引入@capacitor/filesystem与@capacitor/share,实现Android端通过原生文件系统导出数据并调用系统分享面板。新增colors.xml并调整MainActivity,设置状态栏与导航栏颜色,提升原生端显示一致性。
2025-12-20 02:32:11 +08:00
谦君
9a52bac7f1 feat: 支持Android返回键退出确认与多语言提示
新增Android端返回键退出确认弹窗,防止误触直接退出应用。各语言包补充退出确认标题与提示语,提升多语言体验。依赖新增@capacitor/app,样式与主题适配同步优化。
2025-12-20 02:21:18 +08:00
谦君
c16d264209 fix: 优化Android安全区域顶部间距
将顶部安全区域适配由env(safe-area-inset-top, 0)调整为calc(env(safe-area-inset-top, 0px) + 8px),避免内容过于靠近状态栏,提升Android/Capacitor端显示体验。
2025-12-20 02:02:38 +08:00
谦君
9469486174 feat: 优化Android主题与安全区域适配
调整styles.xml主题色为自定义深色,并实现状态栏与导航栏透明。CSS新增安全区域适配,提升Android/Capacitor端显示效果。
2025-12-20 01:55:20 +08:00
谦君
ba3330c0f3 feat: 优化APK发布流程与更新README下载链接
CI流程中新增自动删除同名APK资源步骤,避免重复上传导致的422错误。README优化各平台下载链接,采用徽章样式并补充Android端下载入口,Go版本号同步为1.23。
2025-12-20 01:39:53 +08:00
谦君
859418e50c feat: 配置Android签名与APK后缀大写
新增release.keystore并在build.gradle中配置签名信息,发布包启用签名。统一Android APK文件后缀为大写.APK,调整CI流程相关路径匹配。
2025-12-20 01:32:48 +08:00
谦君
3fe1e4a347 chore: CI流程中安装ImageMagick依赖
在CI构建流程中新增ImageMagick安装步骤,为后续自动生成Android多尺寸图标提供依赖支持。
2025-12-20 01:21:50 +08:00
谦君
d7dfe3c824 feat: 支持Android明文流量与自定义网络安全配置
AndroidManifest.xml新增明文流量支持及networkSecurityConfig配置,添加network_security_config.xml文件。CI流程集成自动生成多尺寸Android图标脚本,补充logo.png与resources/icon.png资源。
2025-12-20 01:19:21 +08:00
谦君
18843e271f chore: 更新CI构建Go与Java版本
将CI流程中的Go版本由1.25降至1.23,Java版本由17升级至21,确保兼容性与构建环境一致性。
2025-12-20 01:01:27 +08:00
谦君
1185dad4da Update build.yml 2025-12-20 00:52:33 +08:00
谦君
5c4ca2b07c Update build.yml 2025-12-20 00:50:53 +08:00
谦君
1368bb4445 feat: 新增Android平台支持及构建流程
集成Android平台相关目录与配置文件,包含Gradle构建脚本、资源文件、启动图标、Java入口、Proguard规则等,完善.gitignore以排除Android构建产物。更新CI流程,支持自动构建并发布Android APK。移除README中项目结构说明,简化文档。
2025-12-20 00:48:36 +08:00
谦君
20fb2bb6a4 feat: 实现远征任务事件与报告展示
新增远征任务事件逻辑,支持资源、暗物质、舰船发现及遭遇海盗/外星人等多种结果,并生成对应任务报告。MessagesView支持远征任务详情展示,包括获得资源、舰船及损失舰船。补充多语言包相关远征事件提示。
2025-12-19 12:37:34 +08:00
谦君
752cade67c refactor: 优化主界面布局与通知系统
重构App.vue,首页独立无侧边栏,其他页面采用统一侧边栏布局。新增右下角固定通知区,集成返回顶部、队列通知、外交通知和敌方警报。移除新手引导组件,替换为弱引导提示系统。支持星球重命名弹窗。优化NPC成长与行为定时器逻辑,提升性能和可维护性。删除issue模板及相关文档描述。
2025-12-19 12:01:45 +08:00
谦君
a689ce21b7 Merge pull request #15 from StarsEnd33A2D17/notification
feat: 添加了浏览器通知和页面内通知
2025-12-18 19:02:34 +08:00
谦君
37045b432b refactor: 移除冗余的前置条件显示方法
删除BuildingsView.vue和ResearchView.vue中未被使用的getRequirementsDisplay简化版方法,优化代码结构,提升可维护性。同步清理多语言包中无用的build字段。
2025-12-18 04:54:36 +08:00
谦君
a0ab4beaf4 feat: 新手保护及NPC攻击概率优化
为积分低于1000的玩家增加新手保护,NPC不会侦查或攻击。优化NPC攻击概率逻辑:中立NPC按正常概率攻击,敌对NPC攻击概率翻倍,提升游戏平衡性。
2025-12-18 04:47:49 +08:00
谦君
53d5216e88 fix: 优化NPC冷却与成长平衡及多语言提示
为NPC初始化和数据迁移时增加侦查/攻击冷却的随机延迟,避免所有NPC同时行动。调整NPC成长难度参数,降低前期NPC威胁,提升平滑度。修正多语言包中侦查被发现提示内容。优化舰队警报弹窗滚动体验。
2025-12-18 04:41:52 +08:00
谦君
2ed15c4782 refactor: 优化UI组件结构与积分系统
重构部分UI组件脚本结构,统一导入风格,提升可维护性。CardUnlockOverlay解锁条件弹窗改为列表展示,提升可读性。修复QueueNotifications滚动区域高度。ScrollableDialogContent增加最大高度。StarsBackground与ParticlesBg组件代码格式优化。App.vue引入玩家积分定时更新逻辑,NPC成长系统补充间谍探测器修复。
2025-12-18 03:47:38 +08:00
StarsEnd
0da82802b8 移出player 2025-12-18 02:21:00 +08:00
StarsEnd
d2465b5d4b 补全国际化 2025-12-18 02:03:09 +08:00
StarsEnd
e8590d54c7 feat: 添加了浏览器通知和页面内通知
暂包含建造完成和科研完成
2025-12-18 01:36:51 +08:00
谦君
2e3ac1231f Merge pull request #14 from coolxitech/main
feat(ui): 更新背景粒子效果和路由视图布局
2025-12-17 23:42:56 +08:00
coolxitech
99e4dbbb0d feat(ui): 更新背景粒子效果和路由视图布局
- 调整 RouterView 的包装 div 结构以支持 z-index 控制
- 为非暗黑模式下的 ParticlesBg 组件设置固定颜色值
- 修改 ParticlesBg 组件的层级样式确保其在背景中正确显示
- 在默认和暗黑模式下优化视图容器的相对定位与全屏尺寸
- 引入新的嵌套 div 结构来增强页面元素的层次管理
2025-12-17 23:38:07 +08:00
谦君
07ece4412f Merge pull request #13 from coolxitech/main
新增特效背景并优化Docker容器大小
2025-12-17 23:16:11 +08:00
谦君
bde0532dbd Merge branch 'main' into main 2025-12-17 23:16:02 +08:00
谦君
d69b842c80 Update package.json 2025-12-17 23:15:13 +08:00
谦君
57fdc1b637 Update App.vue 2025-12-17 23:15:09 +08:00
酷曦科技
d700216cfc Merge branch 'setube:main' into main 2025-12-17 23:07:53 +08:00
谦君
6813456d12 feat: 资源与舰队安全添加及容量校验优化
实现资源和舰队安全添加函数,防止超出仓储/舰队容量时溢出,超出部分自动丢弃。运输、部署、舰队返回等流程统一使用安全添加逻辑。建造队列纳入容量校验,导弹容量校验支持队列中导弹数量。修复NPC舰船建造极端情况下的除零和NaN问题。
2025-12-17 23:07:48 +08:00
coolxitech
97db1324b6 fix(App): 修复路由切换时的页面过渡动画
- 为 Transition 组件中的 div 元素正确绑定 key 属性
- 移除 main.css 中冗余的 tailwindcss 和 tw-animate-css 导入
- 确保页面切换时动画效果正常显示
2025-12-17 22:55:18 +08:00
谦君
ebd7eb1405 Merge pull request #12 from yruh/fix/gameSpeed-resource
fix: 同步 gameSpeed 倍率展示并修复移动端资源栏遮挡
2025-12-17 22:50:02 +08:00
coolxitech
310372b8e2 chore(build): 更新构建日期时间戳
- 将构建日期从 2025/12/17 21:05:49 更新为 2025/12/17 22:25:06
2025-12-17 22:49:00 +08:00
coolxitech
d5a6dd49a1 build(docker): 更新Docker构建环境配置
- 使用node:lts-alpine替换node:latest基础镜像
- 添加git安装和npm镜像源配置
- 修改apk软件源为中科大镜像站
- 更新构建命令为pnpm run build格式
- 优化构建阶段依赖安装逻辑
2025-12-17 22:48:47 +08:00
coolxitech
f30676df07 feat(ui): 星空背景组件支持过渡动画类型
- 导入 Transition 类型以支持动画过渡配置
- 为星层1添加明确的 Transition 类型注解
- 为星层2添加明确的 Transition 类型注解
- 为星层3添加明确的 Transition 类型注解
- 统一设置缓动函数为常量 "linear"
- 确保各星层动画持续时间按倍数递增
2025-12-17 22:25:36 +08:00
lpj
690e6cbbf5 fix: 同步 gameSpeed 倍率展示并修复移动端资源栏遮挡
- 顶部资源栏/概览页:产量、能耗、明细按 gameSpeed 统一缩放,避免显示与实际产出不一致
- 支持 gameSpeed=0:避免 “|| 1” 抹掉 0,并在循环间隔计算中规避除 0
- 修复移动端资源横向滚动时被菜单按钮遮挡(min-w-0/overflow-hidden + 对齐规则)
2025-12-17 22:23:27 +08:00
coolxitech
bd24ca02ae feat(ui): 添加粒子背景和星空背景组件
- 新增 ParticlesBg 组件,实现动态粒子效果背景
- 新增 StarsBackground 组件,创建可交互的星空背景
- 支持自定义颜色、数量、动画速度等属性配置
- 集成鼠标交互,实现视差效果和动态跟随
- 导出两个新组件便于全局使用
2025-12-17 22:03:12 +08:00
coolxitech
d9a8accad7 chore(deps): 更新 electron/node-gyp 依赖引用方式
- 将 @electron/node-gyp 的 Git 协议引用改为 HTTPS tarball 引用
- 统一依赖源为 GitHub 的代码加载地址
- 避免使用 git+ssh 协议可能带来的权限问题
- 确保依赖版本锁定的一致性
- 提高依赖安装的稳定性和可重复性
2025-12-17 22:02:59 +08:00
coolxitech
b166babf12 feat(ui): 添加粒子背景组件支持
- 在非星空背景模式下引入 ParticlesBg 组件
- 配置粒子数量、缓动效果及颜色适配暗色主题
- 设置粒子静态值并启用刷新功能
- 导入 ParticlesBg 组件并注册使用
2025-12-17 22:02:47 +08:00
coolxitech
4aa4d9d350 feat(ui): 添加页面切换动画和星空背景效果
- 使用 Transition 组件实现页面切换的淡入淡出动画
- 根据路由路径设置组件 key 值以触发过渡效果
- 在暗色模式下添加 StarsBackground 星空背景组件
- 为 RouterView 和 StarsBackground 设置高度样式
- 引入 StarsBackground 组件并注册使用
2025-12-17 21:58:28 +08:00
coolxitech
60fd4135ec chore(deps): 重新组织依赖项并更新 motion-v
- 移除 class-variance-authority 和 clsx 的重复声明
- 移除 tailwind-merge 的重复声明
- 在 devDependencies 中正确添加 class-variance-authority
- 在 devDependencies 中正确添加 clsx
- 在 devDependencies 中正确添加 tailwind-merge
- 添加 motion-v 动画库到 dependencies
- 确保所有依赖项按字母顺序排列
- 更新 vite 使用 rolldown-vite 版本 7.2.5
2025-12-17 21:58:20 +08:00
coolxitech
0bb9244214 feat(styles): 引入暗色主题支持并优化样式结构
- 添加 `main.css` 文件定义基础样式和暗色主题变量
- 在 `style.css` 中导入新的主样式文件
- 更新 `utils.ts` 中的 `cn` 函数实现方式
- 调整 pnpm 锁定文件以反映依赖变化
- 集成 `motion-v` 和相关动画库支持
- 移除旧版不必要的样式依赖项
- 修复 `@electron/node-gyp` 的 Git 引用路径格式问题
2025-12-17 21:58:00 +08:00
谦君
cfcde0b024 feat: 新增队列与外交通知组件及新手引导
引入队列通知(QueueNotifications)和外交通知(DiplomaticNotifications)组件,优化主界面队列与外交报告展示,支持一键查看与跳转。重构App.vue,移除原有队列展示,改为弹出式通知,支持功能解锁提示与新手引导(TutorialOverlay)。完善NPC外交事件处理,导弹攻击等行为影响好感度并生成报告。优化部分UI细节与多语言文本,提升交互体验。
2025-12-17 21:06:34 +08:00
谦君
053bd24855 fix(package): 解决buildDate字段冲突
合并package.json中的buildDate字段,移除合并冲突标记,保持字段一致性。
2025-12-15 22:36:33 +08:00
谦君
7d1f36046d Merge pull request #9 from coolxitech/main
chore(workflow): 更新 GitHub Pages 工作流名称
2025-12-15 22:35:11 +08:00
谦君
22ae07de90 Merge branch 'main' into main 2025-12-15 22:35:02 +08:00
谦君
a76909a2c7 Merge pull request #8 from setube/revert-7-main
Revert "chore(github-pages): 更新GitHub Pages构建工作流"
2025-12-15 22:31:18 +08:00
coolxitech
8144f305e2 chore(workflow): 更新 GitHub Pages 工作流名称
- 将工作流名称从 "构建Github Pages" 更改为 "构建 Github Pages"
- 保持其他配置不变
2025-12-15 22:31:03 +08:00
247 changed files with 45149 additions and 4404 deletions

31
.dockerignore Normal file
View File

@@ -0,0 +1,31 @@
# 排除不需要的文件和目录,减少 Docker 构建上下文大小
# 开发工具
.vscode/
.idea/
*.swp
*.swo
# Git
.git/
.gitignore
# 构建产物(本地构建时会重新生成)
docs/
pkg/
# 临时文件
*.tmp
*.temp
.DS_Store
Thumbs.db
# CI 相关文件
.github/
Dockerfile.ci
# 其他不需要的目录
android/app/build/
android/.gradle/
resources/
electron/dist/

38
.dockerignore.ci Normal file
View File

@@ -0,0 +1,38 @@
# CI 构建专用的 dockerignore
# 只保留构建产物和必要的配置文件
# 排除所有源代码和开发文件
src/
public/
electron/
node_modules/
.vscode/
.idea/
.git/
.github/
# 排除构建工具配置
vite.config.ts
tsconfig*.json
*.config.js
*.config.ts
package.json
package-lock.json
pnpm-lock.yaml
# 排除其他构建产物
pkg/
android/
resources/
# 排除临时文件
*.tmp
*.temp
.DS_Store
Thumbs.db
*.log
# 只保留以下文件:
# - docs/ (构建产物)
# - nginx.conf (nginx配置)
# - Dockerfile.ci

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: ['https://afdian.com/a/setube'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,31 +0,0 @@
---
name: BUG反馈
about: 报告项目中发现的缺陷或问题
title: '[BUG] 简要描述问题'
labels: 'bug'
assignees: ''
---
**问题描述**
清晰准确地描述遇到的问题
**重现步骤**
1. 第一步操作
2. 第二步操作
3. 出现问题的操作
**期望行为**
描述您认为正确的行为应该是怎样的
**实际行为**
描述实际发生的错误行为
**环境信息**
- 操作系统:
- 浏览器(如适用):
- 项目版本:
**截图或日志(可选)**
如果有错误截图或日志,请提供

View File

@@ -1,19 +0,0 @@
---
name: 功能请求
about: 请求添加新功能或改进现有功能
title: '[功能] 简要描述功能'
labels: 'enhancement'
assignees: ''
---
**功能描述**
清晰描述您希望添加的功能
**功能背景**
说明为什么需要这个功能,它能解决什么问题
**建议实现方案(可选)**
如果有具体的实现想法,可以在这里描述
**附加信息**
任何其他有助于理解这个功能的信息

View File

@@ -1,19 +0,0 @@
---
name: 反馈建议
about: 为这个项目提出功能建议或改进意见
title: '[建议] 简要描述您的建议'
labels: 'enhancement'
assignees: ''
---
**您的建议是什么?**
请清晰描述您希望添加的功能或改进点
**为什么需要这个功能/改进?**
说明这个建议会解决什么问题或带来什么价值
**您期望的实现方式(可选)**
如果有具体的实现想法,可以在这里描述
**附加信息(可选)**
任何其他有助于理解这个建议的信息

View File

@@ -1,19 +0,0 @@
---
name: 文档改进
about: 报告文档问题或建议改进
title: '[文档] 简要描述问题'
labels: 'documentation'
assignees: ''
---
**文档位置**
指出需要改进的文档路径或 URL
**当前问题**
描述当前文档存在的问题或不清晰的地方
**改进建议**
提出具体的改进建议
**附加信息(可选)**
任何其他有助于改进文档的信息

View File

@@ -15,26 +15,27 @@ jobs:
include:
- goos: windows
goarch: amd64
executable: ogame-server-win.exe
executable: OGame-Vue-Ts-server-win.exe
- goos: linux
goarch: amd64
executable: ogame-server-linux
executable: OGame-Vue-Ts-server-linux
- goos: linux
goarch: arm64
executable: ogame-server-linux-arm64
executable: OGame-Vue-Ts-server-linux-arm64
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: 8
version: latest
- name: Setup Node & Go
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'
- uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.23'
cache: true
- name: Build Frontend & Server
run: |
@@ -49,7 +50,78 @@ jobs:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ matrix.executable }}
# 2. 构建 Electron 客户端
# 2. 构建 Android APK (ARM64, ARMv7, x86_64)
build-android:
name: Build Android APK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# 使用官方 Gradle Action内置智能缓存
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
- name: Build Frontend
run: |
pnpm install
pnpm run build
- name: Install ImageMagick
run: sudo apt-get update && sudo apt-get install -y imagemagick
- name: Generate Android Icons
run: |
# 使用 ImageMagick 生成各尺寸图标
convert logo.png -resize 48x48 android/app/src/main/res/mipmap-mdpi/ic_launcher.png
convert logo.png -resize 48x48 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
convert logo.png -resize 72x72 android/app/src/main/res/mipmap-hdpi/ic_launcher.png
convert logo.png -resize 72x72 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
convert logo.png -resize 96x96 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
convert logo.png -resize 96x96 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
convert logo.png -resize 144x144 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
convert logo.png -resize 144x144 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
convert logo.png -resize 192x192 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
convert logo.png -resize 192x192 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
# foreground 图标需要更大108dp with 72dp safe zone
convert logo.png -resize 108x108 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
convert logo.png -resize 162x162 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
convert logo.png -resize 216x216 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
convert logo.png -resize 324x324 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
convert logo.png -resize 432x432 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
- name: Sync Capacitor
run: npx cap sync android
- name: Build APK (Release)
working-directory: android
run: |
chmod +x ./gradlew
./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
# 3. 构建 Electron 客户端
build-electron:
name: Build Electron (${{ matrix.os }})
runs-on: ${{ matrix.os }}
@@ -64,16 +136,26 @@ jobs:
- os: ubuntu-latest
platform: linux
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: 8
version: latest
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'
- name: Cache Electron Builder
uses: actions/cache@v5
with:
path: |
~/.cache/electron
~/.cache/electron-builder
key: electron-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
electron-${{ runner.os }}-
- name: Build Electron
run: |
pnpm install
@@ -91,14 +173,14 @@ jobs:
pkg/*.dmg
pkg/*.AppImage
# 3. 发布 Release
# 4. 发布 Release
release:
needs: [ build-server, build-electron ]
needs: [build-server, build-android, build-electron]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get Version
id: get_version
@@ -116,21 +198,35 @@ jobs:
mkdir -p ./final-release
# 移动 Server 文件并确保名字唯一
# 注意根据你之前的附件Artifact 名字是 server-windows-amd64
cp ./raw-assets/server-windows-amd64/ogame-server-win.exe ./final-release/ogame-server-win.exe || cp ./raw-assets/server-windows-amd64/server-windows-amd64.exe ./final-release/ogame-server-win.exe || true
cp ./raw-assets/server-linux-amd64/ogame-server-linux ./final-release/ogame-server-linux || true
cp ./raw-assets/server-linux-arm64/ogame-server-linux-arm64 ./final-release/ogame-server-linux-arm64 || true
cp ./raw-assets/server-windows-amd64/OGame-Vue-Ts-server-win.exe ./final-release/OGame-Vue-Ts-server-win.exe || cp ./raw-assets/server-windows-amd64/server-windows-amd64.exe ./final-release/OGame-Vue-Ts-server-win.exe || true
cp ./raw-assets/server-linux-amd64/OGame-Vue-Ts-server-linux ./final-release/OGame-Vue-Ts-server-linux || true
cp ./raw-assets/server-linux-arm64/OGame-Vue-Ts-server-linux-arm64 ./final-release/OGame-Vue-Ts-server-linux-arm64 || true
# 移动 Electron 安装包 (排除 unpacked 目录)
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
# 检查结果
echo "Final assets to upload:"
ls -R ./final-release
# 3. 一次性上传,禁止重复匹配
# 3. 删除已存在的同名 APK 资源(避免 422 错误)
- name: Delete existing APK assets
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
echo "Deleting existing asset: $asset"
gh release delete-asset "$VERSION" "$asset" -y || true
done
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 4. 一次性上传,禁止重复匹配
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_version.outputs.VERSION }}
name: Release ${{ steps.get_version.outputs.VERSION }}

View File

@@ -1,8 +1,8 @@
name: Deploy Vue Project
name: 构建 Github Pages
on:
push:
branches: [ main ] # 如果你的主分支叫 master请改为 master
branches: [main] # 如果你的主分支叫 master请改为 master
permissions:
contents: read
@@ -19,19 +19,45 @@ jobs:
- name: 检出代码
uses: actions/checkout@v6
- name: 安装 Nodejs
uses: actions/setup-node@v6
with:
node-version: 20 # 建议使用 LTS 版本
- name: 安装 pnpm 并构建前端
- name: 设置 pnpm
uses: pnpm/action-setup@v4
with:
run_install: true
version: latest
- name: 设置 Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
- name: 缓存 pnpm 依赖
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
node_modules
key: ${{ runner.os }}-pnpm-pages-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-pages-
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 构建前端项目
run: pnpm run build
- name: 验证构建产物
run: |
if [ ! -d "docs" ]; then
echo "❌ 构建失败docs 目录不存在"
exit 1
fi
if [ ! -f "docs/index.html" ]; then
echo "❌ 构建失败docs/index.html 不存在"
exit 1
fi
echo "✅ 构建产物验证通过"
ls -la docs/
# 关键步骤:告诉 GitHub Actions 跳过 Jekyll 检查
- name: 配置 Github Pages
uses: actions/configure-pages@v5
@@ -42,4 +68,5 @@ jobs:
path: './docs'
- name: 部署到 GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,9 +1,9 @@
name: Docker 多架构构建并发布
name: 构建并发布 Docker 镜像
on:
push:
branches: [ main ]
tags: [ 'v*.*.*' ] # 打 tag 时也触发
branches: [main]
tags: ['v*.*.*'] # 打 tag 时也触发
workflow_dispatch:
permissions:
@@ -15,10 +15,113 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
# 设置 Node.js 环境
- name: 设置 Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
# 设置 pnpm
- name: 设置 pnpm
uses: pnpm/action-setup@v4
with:
version: latest
# 缓存 pnpm 依赖
- name: 缓存 pnpm 依赖
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
node_modules
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# 安装依赖
- name: 安装依赖
run: pnpm install --frozen-lockfile
# 构建项目
- name: 构建项目
run: pnpm run build
# 验证构建产物
- name: 验证构建产物
run: |
if [ ! -d "docs" ]; then
echo "❌ 构建失败docs 目录不存在"
exit 1
fi
if [ ! -f "docs/index.html" ]; then
echo "❌ 构建失败docs/index.html 不存在"
exit 1
fi
# 检查构建产物的时间戳,确保是最新的
BUILD_TIME=$(stat -c %Y docs/index.html 2>/dev/null || stat -f %m docs/index.html 2>/dev/null || echo "0")
CURRENT_TIME=$(date +%s)
TIME_DIFF=$((CURRENT_TIME - BUILD_TIME))
echo "📊 构建产物信息:"
echo " 构建时间: $(date -d @$BUILD_TIME 2>/dev/null || date -r $BUILD_TIME 2>/dev/null || echo '未知')"
echo " 当前时间: $(date)"
echo " 时间差: ${TIME_DIFF}秒"
if [ $TIME_DIFF -gt 300 ]; then
echo "⚠️ 警告: 构建产物可能不是最新的超过5分钟"
fi
echo "✅ 构建产物验证通过"
ls -la docs/
# 获取当前日期
- name: 获取当前日期
id: date
run: echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
# 获取版本号
- name: 获取版本号
id: version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 当前版本: $VERSION"
# 准备 CI 构建环境
- name: 准备 CI 构建环境
run: |
# 使用 CI 专用的 dockerignore
cp .dockerignore.ci .dockerignore
echo "✅ 已切换到 CI 构建模式"
echo "📁 当前构建上下文文件:"
ls -la | grep -E "(docs|nginx.conf|Dockerfile.ci|\.dockerignore)$"
# 清理可能冲突的镜像标签
- name: 清理可能冲突的镜像标签
continue-on-error: true
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "🧹 尝试清理可能冲突的镜像标签..."
# 尝试删除 GHCR 中的现有标签(如果存在)
echo "清理 GHCR 标签..."
docker buildx imagetools inspect ghcr.io/${{ github.repository_owner }}/ogame-vue-ts:$VERSION 2>/dev/null && \
echo "发现现有版本标签,将被覆盖" || echo "版本标签不存在,可以安全推送"
# 如果配置了 Docker Hub也尝试检查
if [ -n "${{ vars.DOCKERHUB_USERNAME }}" ]; then
echo "检查 Docker Hub 标签..."
docker buildx imagetools inspect ${{ vars.DOCKERHUB_USERNAME }}/ogame-vue-ts:$VERSION 2>/dev/null && \
echo "发现现有 Docker Hub 版本标签,将被覆盖" || echo "Docker Hub 版本标签不存在,可以安全推送"
fi
echo "✅ 标签冲突检查完成,构建将覆盖任何现有标签"
# QEMU 用于支持多架构构建(必须)
- name: 设置 QEMU
uses: docker/setup-qemu-action@v3
@@ -43,18 +146,34 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 真正一键构建 + 推送多架构镜像amd64 + arm64
# 构建并推送多架构镜像(使用构建产物
- name: 构建并推送多架构镜像
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.ci
platforms: linux/amd64,linux/arm64
push: true
no-cache: false
pull: true
tags: |
ghcr.io/${{ github.repository_owner }}/ogame-vue-ts:latest
ghcr.io/${{ github.repository_owner }}/ogame-vue-ts:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository_owner }}/ogame-vue-ts:${{ github.sha }}
${{ vars.DOCKERHUB_USERNAME != '' && format('docker.io/{0}/ogame-vue-ts:latest', vars.DOCKERHUB_USERNAME) || '' }}
${{ vars.DOCKERHUB_USERNAME != '' && format('docker.io/{0}/ogame-vue-ts:{1}', vars.DOCKERHUB_USERNAME, github.sha) || '' }}
${{ vars.DOCKERHUB_USERNAME && format('{0}/ogame-vue-ts:latest', vars.DOCKERHUB_USERNAME) || '' }}
${{ vars.DOCKERHUB_USERNAME && format('{0}/ogame-vue-ts:{1}', vars.DOCKERHUB_USERNAME, steps.version.outputs.version) || '' }}
${{ vars.DOCKERHUB_USERNAME && format('{0}/ogame-vue-ts:{1}', vars.DOCKERHUB_USERNAME, github.sha) || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=OGame Vue
build-args: |
BUILDKIT_INLINE_CACHE=1
BUILD_DATE=${{ steps.date.outputs.date }}
VERSION=${{ steps.version.outputs.version }}
COMMIT_SHA=${{ github.sha }}
labels: |
org.opencontainers.image.title=OGame Vue Ts
org.opencontainers.image.description=OGame Vue TypeScript Implementation
org.opencontainers.image.version=${{ steps.version.outputs.version }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ steps.date.outputs.date }}

9
.gitignore vendored
View File

@@ -28,3 +28,12 @@ docs
*.sw?
/docs
/docs/assets
# Android build outputs
android/.gradle
android/app/build
android/build
android/local.properties
android/.idea
android/*.iml
android/app/*.iml

89
DOCKER.md Normal file
View File

@@ -0,0 +1,89 @@
# Docker 构建说明
本项目支持两种 Docker 构建方式:
## 🏠 本地构建
### 方式一:使用构建脚本(推荐)
**Linux/macOS:**
```bash
chmod +x build-docker.sh
./build-docker.sh
```
**Windows:**
```cmd
build-docker.bat
```
### 方式二:直接使用 Docker 命令
```bash
# 构建镜像
docker build -t ogame-vue-ts:local .
# 运行容器
docker run -p 8080:80 ogame-vue-ts:local
```
## ☁️ GitHub Actions 自动构建
当代码推送到 `main` 分支或创建 tag 时GitHub Actions 会自动:
1. 在 Actions 环境中构建项目
2. 使用构建产物创建 Docker 镜像
3. 推送到 GitHub Container Registry 和 Docker Hub
### 使用预构建镜像
```bash
# 从 GitHub Container Registry 拉取
docker pull ghcr.io/your-username/ogame-vue-ts:latest
# 从 Docker Hub 拉取(如果配置了)
docker pull your-dockerhub-username/ogame-vue-ts:latest
# 运行
docker run -p 8080:80 ghcr.io/your-username/ogame-vue-ts:latest
```
## 📁 文件说明
- `Dockerfile` - 本地构建用,包含完整的源代码构建流程
- `Dockerfile.ci` - GitHub Actions 构建用,使用预构建产物
- `.dockerignore` - 本地构建时排除的文件
- `.dockerignore.ci` - CI 构建时排除的文件
- `build-docker.sh` / `build-docker.bat` - 本地构建便捷脚本
## 🔧 配置说明
### GitHub Actions 环境变量
需要在 GitHub 仓库设置中配置:
**Variables (公开):**
- `DOCKERHUB_USERNAME` - Docker Hub 用户名(可选)
**Secrets (私密):**
- `DOCKERHUB_TOKEN` - Docker Hub 访问令牌(可选)
- `GITHUB_TOKEN` - 自动提供,用于 GHCR
### 本地构建要求
- Docker
- 足够的磁盘空间(构建过程中会下载 Node.js 依赖)
## 🚀 快速开始
1. **本地开发测试:**
```bash
./build-docker.sh
docker run -p 8080:80 ogame-vue-ts:local
```
2. **访问应用:**
打开浏览器访问 `http://localhost:8080`
3. **生产部署:**
使用 GitHub Actions 自动构建的镜像进行部署

View File

@@ -1,21 +1,40 @@
FROM node:latest AS builder
# 本地构建用的 Dockerfile
# 支持完整的源代码构建流程
RUN mkdir -p /workspace
WORKDIR /workspace
RUN npm config set registry https://registry.npmmirror.com
RUN git clone https://github.com/setube/ogame-vue-ts.git
RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/
FROM node:20-alpine AS builder
RUN npm install -g pnpm ; pnpm install;
RUN pnpm build
# 设置工作目录
WORKDIR /app
# --- 第二阶段Nginx ---
# 复制 package 文件
COPY package.json pnpm-lock.yaml ./
# 安装 pnpm
RUN npm install -g pnpm
# 安装依赖
RUN pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 构建项目
RUN pnpm run build
# 生产阶段
FROM nginx:alpine
# 复制 nginx 配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 清理默认的 nginx 静态文件
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /workspace/docs /usr/share/nginx/html
# 复制构建产物到 nginx 静态文件目录
COPY --from=builder /app/docs /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]

35
Dockerfile.ci Normal file
View File

@@ -0,0 +1,35 @@
# GitHub Actions 构建用的 Dockerfile
# 使用预构建的产物,不包含源代码构建过程
FROM nginx:alpine
# 添加构建参数用于缓存破坏
ARG BUILD_DATE
ARG VERSION
ARG COMMIT_SHA
# 添加标签信息
LABEL build.date="${BUILD_DATE}" \
build.version="${VERSION}" \
build.commit="${COMMIT_SHA}"
# 复制 nginx 配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 清理默认的 nginx 静态文件
RUN rm -rf /usr/share/nginx/html/*
# 复制构建产物到 nginx 静态文件目录
# 这里的 docs 目录是在 GitHub Actions 中构建生成的
# 使用 --chown 确保正确的文件权限
COPY --chown=nginx:nginx docs /usr/share/nginx/html
# 验证构建产物
RUN ls -la /usr/share/nginx/html/ && \
test -f /usr/share/nginx/html/index.html || (echo "构建产物验证失败" && exit 1)
# 暴露端口
EXPOSE 80
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]

125
README-DE.md Normal file
View File

@@ -0,0 +1,125 @@
<div align="center">
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
# OGame Vue TS
Ein modernes Weltraum-Strategiespiel basierend auf dem klassischen OGame, entwickelt mit Vue 3 und TypeScript.
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md)| [繁體中文](README-zh-TW.md) | [English](README-EN.md) | Deutsch | [Русский](README-RU.md) | [Español](README-ES.md) | [한국어](README-KO.md) | [日本語](README-JA.md)
</div>
## Über das Projekt
OGame Vue TS ist ein Einzelspieler-Weltraum-Strategiespiel im Browser, inspiriert vom klassischen OGame. Baue dein Imperium in der Galaxie auf, erforsche Technologien, konstruiere Raumschiffe und nimm an epischen Weltraumschlachten teil. Dieses Projekt wurde mit modernen Webtechnologien entwickelt und läuft vollständig im Browser mit lokaler Datenspeicherung.
## Hauptfunktionen
- **Gebäudeverwaltung** - Baue und verbessere verschiedene Gebäude auf Planeten und Monden
- **Technologieforschung** - Schalte fortschrittliche Technologien frei, um dein Imperium zu stärken
- **Flottenverwaltung** - Baue Schiffe, sende Missionen und nimm an taktischen Weltraumkämpfen teil
- **Verteidigungssysteme** - Errichte Verteidigungsanlagen zum Schutz deiner Kolonien
- **Offiziersystem** - Rekrutiere Offiziere für strategische Vorteile
- **Kampfsimulator** - Teste Kampfszenarien, bevor du Ressourcen einsetzt
- **Galaxieansicht** - Erkunde das Universum und interagiere mit anderen Planeten
- **Lokale Datenspeicherung** - Alle Spieldaten werden verschlüsselt im Browser gespeichert
- **Dunkler/Heller Modus** - Wähle dein bevorzugtes visuelles Thema
- **Warteschlangenverwaltung** - Verwalte mehrere Bau- und Forschungswarteschlangen
- **Mondgenerierung** - Wahrscheinlichkeitsbasierte Monderzeugung aus Trümmerfeldern
## Technologie-Stack
- **Frontend-Framework:** [Vue 3](https://vuejs.org) + Composition API (`<script setup>` Syntax)
- **Programmiersprache:** [TypeScript](https://www.typescriptlang.org) (mit strikter Typprüfung)
- **Build-Tool:** [Vite](https://vitejs.dev) (Custom Rolldown-Vite 7.2.5), [Golang](https://golang.org) (für plattformübergreifenden Webserver), [Electron](https://www.electronjs.org) (für plattformübergreifende Desktop-Anwendung)
- **Zustandsverwaltung:** [Pinia](https://pinia.vuejs.org) + Persistenz-Plugin
- **Routing:** [Vue Router 4](https://router.vuejs.org)
- **UI-Komponenten:** [shadcn-vue](https://www.shadcn-vue.com) (New York Stil)
- **Styling:** [Tailwind CSS v4](https://tailwindcss.com) + CSS-Variablen
- **Icons:** [Lucide Vue Next](https://lucide.dev)
- **Animationen:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **Internationalisierung:** Eigene i18n-Implementierung
## Schnellstart
### Voraussetzungen
- [Node.js](https://nodejs.org) (Version 18 oder höher empfohlen)
- [pnpm](https://pnpm.io) (Version 10.13.1 oder höher)
- [Go](https://golang.org) (Version 1.21 oder höher) (optional)
### Installation
```bash
# Repository klonen
git clone https://github.com/setube/ogame-vue-ts.git
# In das Projektverzeichnis wechseln
cd ogame-vue-ts
# Abhängigkeiten installieren
pnpm install
```
### Entwicklung
```bash
# Entwicklungsserver starten (läuft auf Port 25121)
pnpm dev
```
Öffne deinen Browser und besuche `http://localhost:25121`
### Produktions-Build
```bash
# Anwendung bauen
pnpm build
# Produktions-Build vorschauen
pnpm preview
```
## Datensicherheit
Alle Spieldaten werden automatisch mit AES-Verschlüsselung verschlüsselt, bevor sie im lokalen Speicher des Browsers gespeichert werden. Dein Spielfortschritt ist sicher und privat.
## Anpassung
Die Anwendung unterstützt vollständige Theme-Anpassung durch Tailwind CSS-Variablen, die in `src/style.css` definiert sind. Du kannst einfach zwischen hellem und dunklem Modus wechseln.
## Mitwirken
Beiträge sind willkommen! Bitte zögere nicht, Issues oder Pull Requests einzureichen.
## Lizenz
Dieses Werk ist lizenziert unter der [Creative Commons Namensnennung-Nicht kommerziell 4.0 International Lizenz](https://creativecommons.org/licenses/by-nc/4.0).
### Du darfst:
- **Teilen** — das Material in jedwedem Format oder Medium vervielfältigen und weiterverbreiten
- **Bearbeiten** — das Material remixen, verändern und darauf aufbauen
### Unter folgenden Bedingungen:
- **Namensnennung** — Du musst angemessene Urheber- und Rechteangaben machen, einen Link zur Lizenz beifügen und angeben, ob Änderungen vorgenommen wurden
- **Nicht kommerziell** — Du darfst das Material nicht für kommerzielle Zwecke nutzen
## Danksagung
Dieses Projekt wurde vom originalen [OGame](https://ogame.org) Browserspiel inspiriert. Alle Spielmechaniken und Designelemente wurden zu Bildungs- und Unterhaltungszwecken neu implementiert.
## Haftungsausschluss
Dieses Projekt ist nicht mit Gameforge AG oder dem offiziellen OGame-Spiel verbunden, wird nicht von diesen unterstützt oder ist mit diesen verbunden. Es ist ein unabhängiges Fan-Projekt, das zu Bildungszwecken und zur persönlichen Unterhaltung erstellt wurde.
---
<div align="center">
Mit ❤️ erstellt von <a href="https://github.com/setube">setube</a>
<br>
© 2025 - Alle Rechte vorbehalten (außer den durch die CC BY-NC 4.0 Lizenz gewährten Rechten)
</div>

View File

@@ -5,13 +5,11 @@
A modern of the classic OGame space strategy game, built with Vue 3 and TypeScript.
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
[![Vue 3](https://img.shields.io/badge/Vue-3.5-brightgreen.svg)](https://vuejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-7.2-646CFF.svg)](https://vitejs.dev/)
[![Go](https://img.shields.io/badge/Go-1.25-79D4FD.svg)](https://golang.org/)
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md) | English
[简体中文](README.md)| [繁體中文](README-zh-TW.md) | English | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [Español](README-ES.md) | [한국어](README-KO.md) | [日本語](README-JA.md)
</div>
@@ -21,7 +19,6 @@ OGame Vue TS is a single-player, browser-based space strategy game inspired by t
## Features
- **Multi-language Support** - Available in 6 languages: English, Chinese (Simplified & Traditional), German, Russian, and Korean
- **Building Management** - Construct and upgrade various buildings on planets and moons
- **Research Technologies** - Unlock advanced technologies to enhance your empire
- **Fleet Management** - Build ships, send missions, and engage in tactical space battles
@@ -36,46 +33,24 @@ OGame Vue TS is a single-player, browser-based space strategy game inspired by t
## Tech Stack
- **Frontend Framework:** [Vue 3](https://vuejs.org/) with Composition API (`<script setup>`)
- **Language:** [TypeScript](https://www.typescriptlang.org/) with strict type checking
- **Build Tool:** [Vite](https://vitejs.dev/) (Custom Rolldown-Vite 7.2.5)、[Golang](https://golang.org/)(Building cross-platform Web server.)、[Electron](https://www.electronjs.org/)(Building cross-platform visual interfaces)
- **State Management:** [Pinia](https://pinia.vuejs.org/) with persisted state plugin
- **Routing:** [Vue Router 4](https://router.vuejs.org/)
- **UI Components:** [shadcn-vue](https://www.shadcn-vue.com/) (New York style)
- **Styling:** [Tailwind CSS v4](https://tailwindcss.com/) with CSS variables
- **Icons:** [Lucide Vue Next](https://lucide.dev/)
- **Frontend Framework:** [Vue 3](https://vuejs.org) with Composition API (`<script setup>`)
- **Language:** [TypeScript](https://www.typescriptlang.org) with strict type checking
- **Build Tool:** [Vite](https://vitejs.dev) (Custom Rolldown-Vite 7.2.5)、[Golang](https://golang.org)(Building cross-platform Web server.)、[Electron](https://www.electronjs.org)(Building cross-platform visual interfaces)
- **State Management:** [Pinia](https://pinia.vuejs.org) with persisted state plugin
- **Routing:** [Vue Router 4](https://router.vuejs.org)
- **UI Components:** [shadcn-vue](https://www.shadcn-vue.com) (New York style)
- **Styling:** [Tailwind CSS v4](https://tailwindcss.com) with CSS variables
- **Icons:** [Lucide Vue Next](https://lucide.dev)
- **Animations:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **Internationalization:** Custom i18n implementation
## Quick Start
### Download Build Product
#### Server version
[Windows](/releases/latest/download/ogame-windows-amd64.exe)
[Linux amd64](/releases/latest/download/ogame-linux-amd64)
[Linux arm64](/releases/latest/download/ogame-linux-arm64)
[MacOS Intel](/releases/latest/download/ogame-macos-amd64)
[MacOS](/releases/latest/download/ogame-macos-arm64)
#### Desktop version
[Windows](/releases/latest/download/OGame.Setup.exe)
[Ubuntu](/releases/latest/download/OGame.AppImage)
[MacOS](/releases/latest/download/OGame-mac.dmg)
### Prerequisites
- [Node.js](https://nodejs.org/) (version 18 or higher recommended)
- [pnpm](https://pnpm.io/) (version 10.13.1 or higher)
- [Go](https://golang.org/) (version 1.21 or higher recommended) (optional for binary builds)
- [Node.js](https://nodejs.org) (version 18 or higher recommended)
- [pnpm](https://pnpm.io) (version 10.13.1 or higher)
- [Go](https://golang.org) (version 1.21 or higher recommended) (optional for binary builds)
### Installation
@@ -109,98 +84,6 @@ pnpm build
pnpm preview
```
## Project Structure
```
ogame-vue-ts/
├── public/ # Static assets
│ └── logo.svg # Application logo
├── src/
│ ├── assets/ # Dynamic assets
│ ├── components/ # Vue components
│ │ └── ui/ # shadcn-vue UI components
│ ├── composables/ # Vue composables
│ ├── config/ # Game configuration
│ ├── lib/ # Utility libraries
│ ├── locales/ # i18n translation files
│ ├── logic/ # Game logic modules
│ │ ├── buildingLogic.ts
│ │ ├── buildingValidation.ts
│ │ ├── fleetLogic.ts
│ │ ├── moonLogic.ts
│ │ ├── moonValidation.ts
│ │ ├── researchLogic.ts
│ │ ├── researchValidation.ts
│ │ ├── shipLogic.ts
│ │ └── shipValidation.ts
│ ├── router/ # Vue Router configuration
│ ├── stores/ # Pinia state stores
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ ├── views/ # Page components
│ │ ├── OverviewView.vue
│ │ ├── BuildingsView.vue
│ │ ├── ResearchView.vue
│ │ ├── ShipyardView.vue
│ │ ├── DefenseView.vue
│ │ ├── FleetView.vue
│ │ ├── GalaxyView.vue
│ │ ├── OfficersView.vue
│ │ ├── BattleSimulatorView.vue
│ │ ├── MessagesView.vue
│ │ └── SettingsView.vue
│ ├── App.vue # Root component
│ ├── main.ts # Application entry point
│ └── style.css # Global styles
├── .github/
│ └── ISSUE_TEMPLATE/ # GitHub issue templates
├── LICENSE # CC BY-NC 4.0 License
├── package.json # Project dependencies
├── tsconfig.json # TypeScript configuration
└── vite.config.ts # Vite configuration
```
## Available Languages
- 🇺🇸 English
- 🇨🇳 简体中文 (Simplified Chinese)
- 🇹🇼 繁體中文 (Traditional Chinese)
- 🇩🇪 Deutsch (German)
- 🇷🇺 Русский (Russian)
- 🇰🇷 한국어 (Korean)
## Game Features
### Resource Management
- **Metal** - Primary construction material
- **Crystal** - Advanced technology component
- **Deuterium** - Fuel and research resource
- **Dark Matter** - Premium resource
- **Energy** - Powers your facilities
### Building Types
- **Resource Buildings** - Metal Mine, Crystal Mine, Deuterium Synthesizer, Solar Plant
- **Facilities** - Robotics Factory, Shipyard, Research Lab, Storage facilities
- **Special Buildings** - Nanite Factory, Terraformer, and more
### Technologies
- **Energy Technology** - Improves energy efficiency
- **Laser Technology** - Enhances weapon systems
- **Ion Technology** - Advanced propulsion and weapons
- **Hyperspace Technology** - Enables faster travel
- **Plasma Technology** - Ultimate weapon systems
- And many more...
### Ship Classes
- **Civil Ships** - Small/Large Cargo, Colony Ship, Recycler
- **Combat Ships** - Light/Heavy Fighter, Cruiser, Battleship, Bomber
- **Special Ships** - Deathstar, Battlecruiser, Destroyer
### Defense Systems
- Rocket Launcher, Light/Heavy Laser, Gauss Cannon
- Ion Cannon, Plasma Turret
- Small/Large Shield Dome
## Data Security
All game data is automatically encrypted using AES encryption before being stored in your browser's local storage. Your game progress is secure and private.
@@ -213,16 +96,9 @@ The application supports full theme customization through Tailwind CSS variables
Contributions are welcome! Please feel free to submit issues or pull requests.
### Issue Templates
We provide the following issue templates in both Chinese and English:
- Bug Report
- Feature Request
- Documentation Improvement
- eedback & Suggestion
## License
This work is licensed under the [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/).
This work is licensed under the [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0).
### You are free to:
- **Share** — copy and redistribute the material in any medium or format
@@ -236,7 +112,7 @@ This work is licensed under the [Creative Commons Attribution-NonCommercial 4.0
## Acknowledgments
This project is inspired by the original [OGame](https://ogame.org/) browser game. All game mechanics and design elements are reimplemented for educational and entertainment purposes.
This project is inspired by the original [OGame](https://ogame.org) browser game. All game mechanics and design elements are reimplemented for educational and entertainment purposes.
## Disclaimer
@@ -245,7 +121,7 @@ This project is not affiliated with, endorsed by, or connected to Gameforge AG o
---
<div align="center">
Made with ❤️ by Jun Qian
Made with ❤️ by <a href="https://github.com/setube">setube</a>
<br>
© 2025 - All rights reserved (except those granted by CC BY-NC 4.0 License)
</div>

125
README-ES.md Normal file
View File

@@ -0,0 +1,125 @@
<div align="center">
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
# OGame Vue TS
Un juego de estrategia espacial moderno basado en Vue 3 y TypeScript.
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md) | [繁體中文](README-zh-TW.md) | [English](README-EN.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | Español | [한국어](README-KO.md) | [日本語](README-JA.md)
</div>
## Acerca del Proyecto
OGame Vue TS es un juego de estrategia espacial basado en navegador, versión offline, inspirado en el clásico OGame. Construye tu imperio en la galaxia, investiga tecnologías, construye naves y participa en épicas batallas espaciales. Este proyecto está construido con tecnologías web modernas, se ejecuta completamente en el navegador, ofrece una experiencia de juego fluida y responsiva, y todos los datos se almacenan localmente.
## Características Principales
- **Gestión de Edificios** - Construye y mejora varios edificios en planetas y lunas
- **Investigación Tecnológica** - Desbloquea tecnologías avanzadas para fortalecer tu imperio
- **Gestión de Flotas** - Construye naves, envía misiones, participa en batallas espaciales tácticas
- **Sistema de Defensa** - Despliega instalaciones defensivas para proteger tus colonias
- **Sistema de Oficiales** - Recluta oficiales para obtener ventajas estratégicas
- **Simulador de Batallas** - Prueba escenarios de combate antes de invertir recursos
- **Vista Galáctica** - Explora el universo e interactúa con otros planetas
- **Persistencia de Datos Local** - Todos los datos del juego están encriptados y almacenados localmente en el navegador
- **Tema Oscuro/Claro** - Elige tu tema visual preferido
- **Gestión de Colas** - Administra múltiples colas de construcción e investigación
- **Generación de Lunas** - Mecanismo de generación de lunas basado en probabilidad desde campos de escombros
## Stack Tecnológico
- **Framework Frontend:** [Vue 3](https://vuejs.org) + Composition API (sintaxis `<script setup>`)
- **Lenguaje de Desarrollo:** [TypeScript](https://www.typescriptlang.org) (verificación de tipos estricta habilitada)
- **Herramientas de Construcción:** [Vite](https://vitejs.dev) (Rolldown-Vite 7.2.5 personalizado), [Golang](https://golang.org) (servidor web multiplataforma), [Electron](https://www.electronjs.org) (interfaz visual multiplataforma)
- **Gestión de Estado:** [Pinia](https://pinia.vuejs.org) + plugin de persistencia
- **Enrutamiento:** [Vue Router 4](https://router.vuejs.org)
- **Componentes UI:** [shadcn-vue](https://www.shadcn-vue.com) (estilo New York)
- **Estilos:** [Tailwind CSS v4](https://tailwindcss.com) + Variables CSS
- **Iconos:** [Lucide Vue Next](https://lucide.dev)
- **Animaciones:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **Internacionalización:** Implementación i18n personalizada
## Inicio Rápido
### Requisitos
- [Node.js](https://nodejs.org) (versión 18 o superior recomendada)
- [pnpm](https://pnpm.io) (versión 10.13.1 o superior)
- [Go](https://golang.org) (versión 1.21 o superior) (opcional)
### Instalación
```bash
# Clonar el repositorio
git clone https://github.com/setube/ogame-vue-ts.git
# Entrar al directorio del proyecto
cd ogame-vue-ts
# Instalar dependencias
pnpm install
```
### Desarrollo
```bash
# Iniciar servidor de desarrollo (puerto 25121)
pnpm dev
```
Visita `http://localhost:25121` en tu navegador
### Construcción para Producción
```bash
# Construir la aplicación
pnpm build
# Vista previa de la construcción
pnpm preview
```
## Seguridad de Datos
Todos los datos del juego se encriptan automáticamente usando AES antes de almacenarse en el almacenamiento local del navegador. Tu progreso de juego es seguro y privado.
## Personalización
La aplicación soporta personalización completa de temas a través de variables CSS de Tailwind definidas en `src/style.css`. Puedes cambiar fácilmente entre modo claro y oscuro.
## Contribuciones
¡Las contribuciones son bienvenidas! No dudes en enviar issues o pull requests.
## Licencia
Este trabajo está licenciado bajo la [Licencia Creative Commons Atribución-NoComercial 4.0 Internacional](https://creativecommons.org/licenses/by-nc/4.0).
### Eres libre de:
- **Compartir** — Copiar y redistribuir el material en cualquier medio o formato
- **Adaptar** — Remezclar, transformar y construir a partir del material
### Bajo los siguientes términos:
- **Atribución** — Debes dar crédito apropiado, proporcionar un enlace a la licencia e indicar si se realizaron cambios
- **NoComercial** — No puedes usar el material para fines comerciales
## Agradecimientos
Este proyecto está inspirado en el juego de navegador original [OGame](https://ogame.org). Todas las mecánicas de juego y elementos de diseño han sido reimplementados con fines educativos y de entretenimiento.
## Aviso Legal
Este proyecto no está afiliado, respaldado ni conectado con Gameforge AG o el juego oficial OGame. Este es un proyecto independiente de fans creado únicamente con fines educativos y de entretenimiento personal.
---
<div align="center">
Hecho con ❤️ por <a href="https://github.com/setube">setube</a>
<br>
© 2025 - Todos los derechos reservados (excepto los otorgados por la licencia CC BY-NC 4.0)
</div>

125
README-JA.md Normal file
View File

@@ -0,0 +1,125 @@
<div align="center">
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
# OGame Vue TS
Vue 3とTypeScriptで構築されたモダンな宇宙戦略ゲーム。
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md) | [繁體中文](README-zh-TW.md) | [English](README-EN.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [Español](README-ES.md) | [한국어](README-KO.md) | 日本語
</div>
## プロジェクトについて
OGame Vue TSは、クラシックなOGameにインスパイアされたシングルプレイヤー向けブラウザベースの宇宙戦略ゲームです。銀河に帝国を築き、テクロジーを研究し、宇宙船を建造し、壮大な宇宙戦闘に参加しましょう。このプロジェクトはモダンなWeb技術で構築されており、ブラウザ内で完全に動作し、すべてのデータはローカルに保存されます。
## 主な機能
- **建物管理** - 惑星と月で様々な建物を建設・アップグレード
- **技術研究** - 先進技術をアンロックして帝国を強化
- **艦隊管理** - 宇宙船を建造し、ミッションを派遣し、戦術的な宇宙戦闘に参加
- **防衛システム** - 防衛施設を配置してコロニーを守る
- **士官システム** - 士官を雇用して戦略的優位性を獲得
- **戦闘シミュレーター** - 資源を投入する前に戦闘シナリオをテスト
- **銀河ビュー** - 宇宙を探索し、他の惑星と交流
- **ローカルデータ永続化** - すべてのゲームデータは暗号化されブラウザにローカル保存
- **ダーク/ライトテーマ** - お好みのビジュアルテーマを選択
- **キュー管理** - 複数の建設・研究キューを管理
- **月の生成** - デブリフィールドからの確率ベースの月生成メカニズム
## 技術スタック
- **フロントエンドフレームワーク:** [Vue 3](https://vuejs.org) + Composition API (`<script setup>` 構文)
- **開発言語:** [TypeScript](https://www.typescriptlang.org) (厳密な型チェック有効)
- **ビルドツール:** [Vite](https://vitejs.dev) (カスタムRolldown-Vite 7.2.5)、[Golang](https://golang.org) (クロスプラットフォームWebサーバー)、[Electron](https://www.electronjs.org) (クロスプラットフォームビジュアルインターフェース)
- **状態管理:** [Pinia](https://pinia.vuejs.org) + 永続化プラグイン
- **ルーティング:** [Vue Router 4](https://router.vuejs.org)
- **UIコンポーネント:** [shadcn-vue](https://www.shadcn-vue.com) (New Yorkスタイル)
- **スタイリング:** [Tailwind CSS v4](https://tailwindcss.com) + CSS変数
- **アイコン:** [Lucide Vue Next](https://lucide.dev)
- **アニメーション:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **国際化:** カスタムi18n実装
## クイックスタート
### 必要条件
- [Node.js](https://nodejs.org) (バージョン18以上推奨)
- [pnpm](https://pnpm.io) (バージョン10.13.1以上)
- [Go](https://golang.org) (バージョン1.21以上) (オプション)
### インストール
```bash
# リポジトリをクローン
git clone https://github.com/setube/ogame-vue-ts.git
# プロジェクトディレクトリに移動
cd ogame-vue-ts
# 依存関係をインストール
pnpm install
```
### 開発
```bash
# 開発サーバーを起動 (ポート25121)
pnpm dev
```
ブラウザで `http://localhost:25121` にアクセス
### 本番ビルド
```bash
# アプリケーションをビルド
pnpm build
# 本番ビルドをプレビュー
pnpm preview
```
## データセキュリティ
すべてのゲームデータは、ブラウザのローカルストレージに保存される前にAESで自動的に暗号化されます。ゲームの進行状況は安全でプライベートです。
## カスタマイズ
アプリケーションは`src/style.css`で定義されたTailwind CSS変数による完全なテーマカスタマイズをサポートしています。ライトモードとダークモードを簡単に切り替えることができます。
## 貢献
貢献は大歓迎ですお気軽にissueやpull requestを提出してください。
## ライセンス
この作品は[クリエイティブ・コモンズ 表示-非営利 4.0 国際ライセンス](https://creativecommons.org/licenses/by-nc/4.0)の下でライセンスされています。
### あなたは自由に:
- **共有** — どのようなメディアやフォーマットでも資料をコピー・再配布できます
- **翻案** — 資料をリミックス、変形、加工できます
### 以下の条件に従う必要があります:
- **表示** — 適切なクレジットを表示し、ライセンスへのリンクを提供し、変更があったかどうかを示す必要があります
- **非営利** — 資料を営利目的で使用することはできません
## 謝辞
このプロジェクトはオリジナルの[OGame](https://ogame.org)ブラウザゲームにインスパイアされています。すべてのゲームメカニクスとデザイン要素は、教育およびエンターテイメント目的で再実装されています。
## 免責事項
このプロジェクトはGameforge AGや公式OGameゲームとは一切関係がなく、承認や接続もありません。これは教育と個人的なエンターテイメントのみを目的として作成された独立したファンプロジェクトです。
---
<div align="center">
❤️を込めて作成 by <a href="https://github.com/setube">setube</a>
<br>
© 2025 - All rights reserved (CC BY-NC 4.0ライセンスで付与された権利を除く)
</div>

125
README-KO.md Normal file
View File

@@ -0,0 +1,125 @@
<div align="center">
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
# OGame Vue TS
Vue 3와 TypeScript로 제작된 클래식 OGame을 기반으로 한 현대적인 우주 전략 게임입니다.
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md) | [繁體中文](README-zh-TW.md) | [English](README-EN.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [Español](README-ES.md) | 한국어 | [日本語](README-JA.md)
</div>
## 프로젝트 소개
OGame Vue TS는 클래식 OGame에서 영감을 받은 싱글플레이어 브라우저 기반 우주 전략 게임입니다. 은하계에서 제국을 건설하고, 기술을 연구하고, 우주선을 제작하고, 장대한 우주 전투에 참여하세요. 이 프로젝트는 현대 웹 기술로 제작되었으며, 로컬 데이터 저장과 함께 브라우저에서 완전히 실행됩니다.
## 주요 기능
- **건물 관리** - 행성과 달에서 다양한 건물을 건설하고 업그레이드
- **기술 연구** - 제국을 강화하기 위한 첨단 기술 해금
- **함대 관리** - 우주선 건조, 미션 파견, 전술적 우주 전투 참여
- **방어 시스템** - 식민지 보호를 위한 방어 시설 배치
- **장교 시스템** - 전략적 이점을 위한 장교 고용
- **전투 시뮬레이터** - 자원 투입 전 전투 시나리오 테스트
- **은하 뷰** - 우주 탐험 및 다른 행성과의 상호작용
- **로컬 데이터 저장** - 모든 게임 데이터는 암호화되어 브라우저에 로컬 저장
- **다크/라이트 모드** - 선호하는 비주얼 테마 선택
- **대기열 관리** - 여러 건설 및 연구 대기열 관리
- **달 생성** - 잔해 필드에서 확률 기반 달 생성
## 기술 스택
- **프론트엔드 프레임워크:** [Vue 3](https://vuejs.org) + Composition API (`<script setup>` 문법)
- **프로그래밍 언어:** [TypeScript](https://www.typescriptlang.org) (엄격한 타입 검사 활성화)
- **빌드 도구:** [Vite](https://vitejs.dev) (Custom Rolldown-Vite 7.2.5), [Golang](https://golang.org) (크로스 플랫폼 웹 서버 구축), [Electron](https://www.electronjs.org) (크로스 플랫폼 데스크톱 애플리케이션 구축)
- **상태 관리:** [Pinia](https://pinia.vuejs.org) + 지속성 플러그인
- **라우팅:** [Vue Router 4](https://router.vuejs.org)
- **UI 컴포넌트:** [shadcn-vue](https://www.shadcn-vue.com) (New York 스타일)
- **스타일링:** [Tailwind CSS v4](https://tailwindcss.com) + CSS 변수
- **아이콘:** [Lucide Vue Next](https://lucide.dev)
- **애니메이션:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **국제화:** 커스텀 i18n 구현
## 빠른 시작
### 요구 사항
- [Node.js](https://nodejs.org) (버전 18 이상 권장)
- [pnpm](https://pnpm.io) (버전 10.13.1 이상)
- [Go](https://golang.org) (버전 1.21 이상) (선택 사항)
### 설치
```bash
# 저장소 클론
git clone https://github.com/setube/ogame-vue-ts.git
# 프로젝트 디렉토리로 이동
cd ogame-vue-ts
# 의존성 설치
pnpm install
```
### 개발
```bash
# 개발 서버 시작 (포트 25121에서 실행)
pnpm dev
```
브라우저를 열고 `http://localhost:25121`로 이동하세요
### 프로덕션 빌드
```bash
# 애플리케이션 빌드
pnpm build
# 프로덕션 빌드 미리보기
pnpm preview
```
## 데이터 보안
모든 게임 데이터는 브라우저의 로컬 스토리지에 저장되기 전에 AES 암호화로 자동 암호화됩니다. 게임 진행 상황은 안전하고 비공개로 유지됩니다.
## 커스터마이징
애플리케이션은 `src/style.css`에 정의된 Tailwind CSS 변수를 통해 완전한 테마 커스터마이징을 지원합니다. 라이트 모드와 다크 모드 간에 쉽게 전환할 수 있습니다.
## 기여
기여를 환영합니다! 이슈나 풀 리퀘스트를 자유롭게 제출해 주세요.
## 라이선스
이 작품은 [크리에이티브 커먼즈 저작자표시-비영리 4.0 국제 라이선스](https://creativecommons.org/licenses/by-nc/4.0)에 따라 라이선스가 부여됩니다.
### 자유롭게:
- **공유** — 어떤 매체나 포맷으로든 자료를 복사하고 재배포할 수 있습니다
- **변경** — 자료를 리믹스, 변형하고 자료를 기반으로 2차 저작물을 만들 수 있습니다
### 다음 조건을 따라야 합니다:
- **저작자표시** — 적절한 출처를 표시하고, 라이선스 링크를 제공하며, 변경이 있었는지 표시해야 합니다
- **비영리** — 이 자료를 상업적 목적으로 사용할 수 없습니다
## 감사의 말
이 프로젝트는 원작 [OGame](https://ogame.org) 브라우저 게임에서 영감을 받았습니다. 모든 게임 메커니즘과 디자인 요소는 교육 및 오락 목적으로 재구현되었습니다.
## 면책 조항
이 프로젝트는 Gameforge AG 또는 공식 OGame 게임과 제휴, 보증 또는 연결되어 있지 않습니다. 이것은 교육 목적과 개인적인 즐거움을 위해 만들어진 독립적인 팬 프로젝트입니다.
---
<div align="center">
❤️를 담아 제작, 작성자: <a href="https://github.com/setube">setube</a>
<br>
© 2025 - 모든 권리 보유 (CC BY-NC 4.0 라이선스에 의해 부여된 권리 제외)
</div>

125
README-RU.md Normal file
View File

@@ -0,0 +1,125 @@
<div align="center">
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
# OGame Vue TS
Современная космическая стратегическая игра, вдохновлённая классической OGame, созданная на Vue 3 и TypeScript.
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md) | [繁體中文](README-zh-TW.md) | [English](README-EN.md) | [Deutsch](README-DE.md) | Русский | [Español](README-ES.md) | [한국어](README-KO.md) | [日本語](README-JA.md)
</div>
## О проекте
OGame Vue TS — это однопользовательская браузерная космическая стратегическая игра, вдохновлённая классической OGame. Постройте свою империю в галактике, исследуйте технологии, создавайте космические корабли и участвуйте в эпических космических сражениях. Этот проект создан с использованием современных веб-технологий и полностью работает в браузере с локальным хранением данных.
## Основные возможности
- **Управление зданиями** - Стройте и улучшайте различные здания на планетах и лунах
- **Исследование технологий** - Открывайте передовые технологии для усиления вашей империи
- **Управление флотом** - Стройте корабли, отправляйте миссии, участвуйте в тактических космических боях
- **Системы обороны** - Размещайте оборонительные сооружения для защиты ваших колоний
- **Система офицеров** - Нанимайте офицеров для получения стратегических преимуществ
- **Боевой симулятор** - Тестируйте боевые сценарии перед вложением ресурсов
- **Вид галактики** - Исследуйте вселенную и взаимодействуйте с другими планетами
- **Локальное хранение данных** - Все игровые данные зашифрованы и хранятся локально в браузере
- **Тёмный/светлый режим** - Выберите предпочитаемую визуальную тему
- **Управление очередями** - Управляйте несколькими очередями строительства и исследований
- **Генерация луны** - Вероятностное создание луны из поля обломков
## Технологический стек
- **Frontend-фреймворк:** [Vue 3](https://vuejs.org) + Composition API (синтаксис `<script setup>`)
- **Язык программирования:** [TypeScript](https://www.typescriptlang.org) (со строгой проверкой типов)
- **Инструмент сборки:** [Vite](https://vitejs.dev) (Custom Rolldown-Vite 7.2.5), [Golang](https://golang.org) (для кроссплатформенного веб-сервера), [Electron](https://www.electronjs.org) (для кроссплатформенного десктоп-приложения)
- **Управление состоянием:** [Pinia](https://pinia.vuejs.org) + плагин персистентности
- **Маршрутизация:** [Vue Router 4](https://router.vuejs.org)
- **UI-компоненты:** [shadcn-vue](https://www.shadcn-vue.com) (стиль New York)
- **Стилизация:** [Tailwind CSS v4](https://tailwindcss.com) + CSS-переменные
- **Иконки:** [Lucide Vue Next](https://lucide.dev)
- **Анимации:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **Интернационализация:** Собственная реализация i18n
## Быстрый старт
### Требования
- [Node.js](https://nodejs.org) (рекомендуется версия 18 или выше)
- [pnpm](https://pnpm.io) (версия 10.13.1 или выше)
- [Go](https://golang.org) (версия 1.21 или выше) (опционально)
### Установка
```bash
# Клонировать репозиторий
git clone https://github.com/setube/ogame-vue-ts.git
# Перейти в директорию проекта
cd ogame-vue-ts
# Установить зависимости
pnpm install
```
### Разработка
```bash
# Запустить сервер разработки (работает на порту 25121)
pnpm dev
```
Откройте браузер и перейдите по адресу `http://localhost:25121`
### Сборка для продакшена
```bash
# Собрать приложение
pnpm build
# Предпросмотр продакшен-сборки
pnpm preview
```
## Безопасность данных
Все игровые данные автоматически шифруются с помощью AES перед сохранением в локальном хранилище браузера. Ваш игровой прогресс защищён и приватен.
## Кастомизация
Приложение поддерживает полную настройку темы через CSS-переменные Tailwind, определённые в `src/style.css`. Вы можете легко переключаться между светлым и тёмным режимами.
## Участие в разработке
Приветствуем вклад в проект! Пожалуйста, не стесняйтесь создавать issues или pull requests.
## Лицензия
Эта работа лицензирована под [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0).
### Вы можете:
- **Делиться** — копировать и распространять материал в любом формате или на любом носителе
- **Адаптировать** — ремиксовать, преобразовывать и создавать на основе материала
### При соблюдении следующих условий:
- **Атрибуция** — Вы должны указать соответствующую атрибуцию, предоставить ссылку на лицензию и указать, были ли внесены изменения
- **Некоммерческое использование** — Вы не можете использовать материал в коммерческих целях
## Благодарности
Этот проект вдохновлён оригинальной браузерной игрой [OGame](https://ogame.org). Все игровые механики и элементы дизайна были переосмыслены в образовательных и развлекательных целях.
## Отказ от ответственности
Этот проект не связан с Gameforge AG или официальной игрой OGame, не одобрен и не поддерживается ими. Это независимый фан-проект, созданный в образовательных целях и для личного развлечения.
---
<div align="center">
Сделано с ❤️ автором <a href="https://github.com/setube">setube</a>
<br>
© 2025 - Все права защищены (кроме прав, предоставленных лицензией CC BY-NC 4.0)
</div>

125
README-zh-TW.md Normal file
View File

@@ -0,0 +1,125 @@
<div align="center">
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
# OGame Vue TS
一個基於 Vue 3 和 TypeScript 構建的現代化 OGame 太空策略遊戲。
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[简体中文](README.md) | 繁體中文 | [English](README-EN.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [Español](README-ES.md) | [한국어](README-KO.md) | [日本語](README-JA.md)
</div>
## 關於專案
OGame Vue TS 是一款受經典 OGame 遊戲啟發的單機版、基於瀏覽器的太空策略遊戲。在銀河系中建立你的帝國,研究科技,建造艦船,參與史詩般的太空戰鬥。本專案採用現代 Web 技術構建,完全在瀏覽器中運行,提供流暢且響應迅速的遊戲體驗,所有數據都儲存在本地。
## 核心特性
- **建築管理** - 在行星和月球上建造和升級各種建築
- **科技研究** - 解鎖先進科技來增強你的帝國
- **艦隊管理** - 建造艦船、派遣任務、參與戰術太空戰鬥
- **防禦系統** - 部署防禦設施來保護你的殖民地
- **軍官系統** - 招募軍官以獲得戰略優勢
- **戰鬥模擬器** - 在投入資源前測試戰鬥場景
- **銀河視圖** - 探索宇宙並與其他星球互動
- **本地數據持久化** - 所有遊戲數據都經過加密並儲存在瀏覽器本地
- **深色/淺色主題** - 選擇你喜歡的視覺主題
- **隊列管理** - 管理多個建造和研究隊列
- **月球生成** - 基於概率的月球從殘骸場生成機制
## 技術棧
- **前端框架:** [Vue 3](https://vuejs.org) + Composition API (`<script setup>` 語法)
- **開發語言:** [TypeScript](https://www.typescriptlang.org) (啟用嚴格類型檢查)
- **構建工具:** [Vite](https://vitejs.dev) (自定義 Rolldown-Vite 7.2.5)、[Golang](https://golang.org)(構建跨平台的Web服務端)、[Electron](https://www.electronjs.org)(構建跨平台可視化介面)
- **狀態管理:** [Pinia](https://pinia.vuejs.org) + 持久化插件
- **路由管理:** [Vue Router 4](https://router.vuejs.org)
- **UI 組件:** [shadcn-vue](https://www.shadcn-vue.com) (New York 風格)
- **樣式方案:** [Tailwind CSS v4](https://tailwindcss.com) + CSS 變數
- **圖標庫:** [Lucide Vue Next](https://lucide.dev)
- **動畫效果:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **國際化:** 自定義 i18n 實現
## 快速開始
### 環境要求
- [Node.js](https://nodejs.org) (推薦 18 或更高版本)
- [pnpm](https://pnpm.io) (版本 10.13.1 或更高)
- [Go](https://golang.org) (版本 1.21 或更高版本)(可選)
### 安裝
```bash
# 克隆倉庫
git clone https://github.com/setube/ogame-vue-ts.git
# 進入專案目錄
cd ogame-vue-ts
# 安裝依賴
pnpm install
```
### 開發
```bash
# 啟動開發服務器 (運行在端口 25121)
pnpm dev
```
在瀏覽器中訪問 `http://localhost:25121`
### 生產構建
```bash
# 構建應用
pnpm build
# 預覽生產構建
pnpm preview
```
## 數據安全
所有遊戲數據在儲存到瀏覽器的本地存儲之前都會使用 AES 加密自動加密。您的遊戲進度是安全且私密的。
## 自定義
應用支援通過 `src/style.css` 中定義的 Tailwind CSS 變數進行完整的主題自定義。您可以輕鬆地在淺色和深色模式之間切換。
## 貢獻
歡迎貢獻!請隨時提交 issue 或 pull request。
## 許可證
本作品採用 [創用CC 姓名標示-非商業性 4.0 國際 授權條款](https://creativecommons.org/licenses/by-nc/4.0) 授權。
### 您可以自由地:
- **分享** — 以任何媒介或格式重製及散布本素材
- **修改** — 重混、轉換本素材、及依本素材建立新素材
### 惟需遵照下列條件:
- **姓名標示** — 您必須給予適當表彰、提供指向本授權條款的連結,以及指出是否已對本素材進行變更
- **非商業性** — 您不得將本素材用於商業目的
## 致謝
本專案受原版 [OGame](https://ogame.org) 瀏覽器遊戲啟發。所有遊戲機制和設計元素都是為了教育和娛樂目的而重新實現的。
## 免責聲明
本專案與 Gameforge AG 或官方 OGame 遊戲沒有任何關聯、認可或聯繫。這是一個獨立的粉絲專案,創建目的僅用於教育和個人娛樂。
---
<div align="center">
用 ❤️ 製作,作者:<a href="https://github.com/setube">setube</a>
<br>
© 2025 - 保留所有權利(除 CC BY-NC 4.0 許可證授予的權利外)
</div>

160
README.md
View File

@@ -5,13 +5,11 @@
一个基于 Vue 3 和 TypeScript 构建的现代化 OGame 太空策略游戏。
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
[![Vue 3](https://img.shields.io/badge/Vue-3.5-brightgreen.svg)](https://vuejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-7.2-646CFF.svg)](https://vitejs.dev/)
[![Go](https://img.shields.io/badge/Go-1.25-79D4FD.svg)](https://golang.org/)
[![GitHub Release](https://img.shields.io/github/v/release/setube/ogame-vue-ts?style=flat&logo=github&label=Release)](https://github.com/setube/ogame-vue-ts/releases/latest)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0)
[![Tap Tap](https://img.shields.io/badge/TapTap-OGame%20Vue%20Ts-18d6e0)](https://www.taptap.cn/app/801190)
[English](README-EN.md) | 简体中文
简体中文 | [繁體中文](README-zh-TW.md) | [English](README-EN.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [Español](README-ES.md) | [한국어](README-KO.md) | [日本語](README-JA.md)
</div>
@@ -21,7 +19,6 @@ OGame Vue TS 是一款受经典 OGame 游戏启发的单机版、基于浏览器
## 核心特性
- **多语言支持** - 支持 6 种语言:英语、简体中文、繁体中文、德语、俄语和韩语
- **建筑管理** - 在行星和月球上建造和升级各种建筑
- **科技研究** - 解锁先进科技来增强你的帝国
- **舰队管理** - 建造舰船、派遣任务、参与战术太空战斗
@@ -36,46 +33,24 @@ OGame Vue TS 是一款受经典 OGame 游戏启发的单机版、基于浏览器
## 技术栈
- **前端框架:** [Vue 3](https://vuejs.org/) + Composition API (`<script setup>` 语法)
- **开发语言:** [TypeScript](https://www.typescriptlang.org/) (启用严格类型检查)
- **构建工具:** [Vite](https://vitejs.dev/) (自定义 Rolldown-Vite 7.2.5)、[Golang](https://golang.org/)(构建跨平台的Web服务端)、[Electron](https://www.electronjs.org/)(构建跨平台可视化界面)
- **状态管理:** [Pinia](https://pinia.vuejs.org/) + 持久化插件
- **路由管理:** [Vue Router 4](https://router.vuejs.org/)
- **UI 组件:** [shadcn-vue](https://www.shadcn-vue.com/) (New York 风格)
- **样式方案:** [Tailwind CSS v4](https://tailwindcss.com/) + CSS 变量
- **图标库:** [Lucide Vue Next](https://lucide.dev/)
- **前端框架:** [Vue 3](https://vuejs.org) + Composition API (`<script setup>` 语法)
- **开发语言:** [TypeScript](https://www.typescriptlang.org) (启用严格类型检查)
- **构建工具:** [Vite](https://vitejs.dev) (自定义 Rolldown-Vite 7.2.5)、[Golang](https://golang.org)(构建跨平台的Web服务端)、[Electron](https://www.electronjs.org)(构建跨平台可视化界面)
- **状态管理:** [Pinia](https://pinia.vuejs.org) + 持久化插件
- **路由管理:** [Vue Router 4](https://router.vuejs.org)
- **UI 组件:** [shadcn-vue](https://www.shadcn-vue.com) (New York 风格)
- **样式方案:** [Tailwind CSS v4](https://tailwindcss.com) + CSS 变量
- **图标库:** [Lucide Vue Next](https://lucide.dev)
- **动画效果:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
- **国际化:** 自定义 i18n 实现
## 快速开始
### 下载构建版本
#### 服务端
[Windows](/releases/latest/download/ogame-windows-amd64.exe)
[Linux amd64](/releases/latest/download/ogame-linux-amd64)
[Linux arm64](/releases/latest/download/ogame-linux-arm64)
[MacOS Intel](/releases/latest/download/ogame-macos-amd64)
[MacOS](/releases/latest/download/ogame-macos-arm64)
#### 桌面版
[Windows](/releases/latest/download/OGame.Setup.exe)
[Ubuntu](/releases/latest/download/OGame.AppImage)
[MacOS](/releases/latest/download/OGame-mac.dmg)
### 环境要求
- [Node.js](https://nodejs.org/) (推荐 18 或更高版本)
- [pnpm](https://pnpm.io/) (版本 10.13.1 或更高)
- [Go](https://golang.org/) (版本 1.21 或更高版本)(可选)
- [Node.js](https://nodejs.org) (推荐 18 或更高版本)
- [pnpm](https://pnpm.io) (版本 10.13.1 或更高)
- [Go](https://golang.org) (版本 1.21 或更高版本)(可选)
### 安装
@@ -109,98 +84,6 @@ pnpm build
pnpm preview
```
## 项目结构
```
ogame-vue-ts/
├── public/ # 静态资源
│ └── logo.svg # 应用图标
├── src/
│ ├── assets/ # 动态资源
│ ├── components/ # Vue 组件
│ │ └── ui/ # shadcn-vue UI 组件
│ ├── composables/ # Vue 组合式函数
│ ├── config/ # 游戏配置
│ ├── lib/ # 工具库
│ ├── locales/ # 国际化翻译文件
│ ├── logic/ # 游戏逻辑模块
│ │ ├── buildingLogic.ts # 建筑逻辑
│ │ ├── buildingValidation.ts # 建筑验证
│ │ ├── fleetLogic.ts # 舰队逻辑
│ │ ├── moonLogic.ts # 月球逻辑
│ │ ├── moonValidation.ts # 月球验证
│ │ ├── researchLogic.ts # 研究逻辑
│ │ ├── researchValidation.ts # 研究验证
│ │ ├── shipLogic.ts # 舰船逻辑
│ │ └── shipValidation.ts # 舰船验证
│ ├── router/ # Vue Router 路由配置
│ ├── stores/ # Pinia 状态存储
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ │ ├── OverviewView.vue # 概览页面
│ │ ├── BuildingsView.vue # 建筑页面
│ │ ├── ResearchView.vue # 研究页面
│ │ ├── ShipyardView.vue # 船坞页面
│ │ ├── DefenseView.vue # 防御页面
│ │ ├── FleetView.vue # 舰队页面
│ │ ├── GalaxyView.vue # 银河页面
│ │ ├── OfficersView.vue # 军官页面
│ │ ├── BattleSimulatorView.vue # 战斗模拟器
│ │ ├── MessagesView.vue # 消息页面
│ │ └── SettingsView.vue # 设置页面
│ ├── App.vue # 根组件
│ ├── main.ts # 应用入口
│ └── style.css # 全局样式
├── .github/
│ └── ISSUE_TEMPLATE/ # GitHub issue 模板
├── LICENSE # CC BY-NC 4.0 许可证
├── package.json # 项目依赖
├── tsconfig.json # TypeScript 配置
└── vite.config.ts # Vite 配置
```
## 支持的语言
- 🇺🇸 English (英语)
- 🇨🇳 简体中文
- 🇹🇼 繁體中文
- 🇩🇪 Deutsch (德语)
- 🇷🇺 Русский (俄语)
- 🇰🇷 한국어 (韩语)
## 游戏特性
### 资源管理
- **金属** - 主要建筑材料
- **晶体** - 高级科技组件
- **重氢** - 燃料和研究资源
- **暗物质** - 高级资源
- **能量** - 为设施供电
### 建筑类型
- **资源建筑** - 金属矿、晶体矿、重氢合成器、太阳能发电厂
- **设施建筑** - 机器人工厂、船坞、研究实验室、仓储设施
- **特殊建筑** - 纳米机器人工厂、行星改造器等
### 科技系统
- **能量技术** - 提高能量效率
- **激光技术** - 增强武器系统
- **离子技术** - 高级推进和武器
- **超空间技术** - 实现更快的旅行
- **等离子技术** - 终极武器系统
- 还有更多...
### 舰船类别
- **民用舰船** - 小型/大型货船、殖民船、回收船
- **战斗舰船** - 轻型/重型战斗机、巡洋舰、战列舰、轰炸机
- **特殊舰船** - 死星、战列巡洋舰、毁灭者
### 防御系统
- 火箭发射器、轻型/重型激光炮、高斯炮
- 离子炮、等离子炮塔
- 小型/大型防护罩
## 数据安全
所有游戏数据在存储到浏览器的本地存储之前都会使用 AES 加密自动加密。您的游戏进度是安全且私密的。
@@ -213,16 +96,9 @@ ogame-vue-ts/
欢迎贡献!请随时提交 issue 或 pull request。
### Issue 模板
我们提供以下中英文 issue 模板:
- BUG反馈 / Bug Report
- 功能请求 / Feature Request
- 文档改进 / Documentation Improvement
- 反馈建议 / Feedback & Suggestion
## 许可证
本作品采用 [知识共享署名-非商业性使用 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc/4.0/) 进行许可。
本作品采用 [知识共享署名-非商业性使用 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc/4.0) 进行许可。
### 您可以自由地:
- **共享** — 在任何媒介以任何形式复制、发行本作品
@@ -234,7 +110,7 @@ ogame-vue-ts/
## 致谢
本项目受原版 [OGame](https://ogame.org/) 浏览器游戏启发。所有游戏机制和设计元素都是为了教育和娱乐目的而重新实现的。
本项目受原版 [OGame](https://ogame.org) 浏览器游戏启发。所有游戏机制和设计元素都是为了教育和娱乐目的而重新实现的。
## 免责声明
@@ -243,7 +119,7 @@ ogame-vue-ts/
---
<div align="center">
用 ❤️ 制作,作者:谦君
用 ❤️ 制作,作者:<a href="https://github.com/setube">setube</a>
<br>
© 2025 - 保留所有权利(除 CC BY-NC 4.0 许可证授予的权利外)
</div>

101
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

102
android/app/build.gradle Normal file
View File

@@ -0,0 +1,102 @@
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
defaultConfig {
applicationId "games.wenzi.ogame"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
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.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// 按 ABI 拆分 APK (arm64-v8a, armeabi-v7a, x86_64)
splits {
abi {
enable true
reset()
include "arm64-v8a", "armeabi-v7a", "x86_64"
universalApk false
}
}
signingConfigs {
release {
storeFile file('release.keystore')
storePassword 'ogame123'
keyAlias 'ogame'
keyPassword 'ogame123'
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
// 为每个 ABI 设置不同的 versionCode
applicationVariants.configureEach { variant ->
variant.outputs.configureEach { output ->
def abiVersionCode = [
"armeabi-v7a": 1,
"arm64-v8a": 2,
"x86_64": 3
]
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"
}
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,20 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Binary file not shown.

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 旧版本存储权限 (Android 10 及以下) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="30" />
<!-- Android 11+ (API 30+) 完整外部存储访问权限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Android 13+ (API 33+) 细粒度媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest>

View File

@@ -0,0 +1,148 @@
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 android.view.animation.AccelerateDecelerateInterpolator;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.net.Uri;
import android.content.Intent;
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;
private static final int FILE_CHOOSER_REQUEST_CODE = 1001;
private ValueCallback<Uri[]> filePathCallback;
@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);
Window window = getWindow();
// 启用边到边显示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;
}
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
// 清理之前的回调
if (MainActivity.this.filePathCallback != null) {
MainActivity.this.filePathCallback.onReceiveValue(null);
}
MainActivity.this.filePathCallback = filePathCallback;
// 创建文件选择器 Intent
Intent intent = fileChooserParams.createIntent();
try {
startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE);
} catch (android.content.ActivityNotFoundException e) {
MainActivity.this.filePathCallback = null;
return false;
}
return true;
}
});
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
if (filePathCallback == null) {
return;
}
Uri[] results = null;
if (resultCode == RESULT_OK && data != null) {
String dataString = data.getDataString();
if (dataString != null) {
results = new Uri[]{Uri.parse(dataString)};
}
}
filePathCallback.onReceiveValue(results);
filePathCallback = null;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

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

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +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

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">OGame Vue Ts</string>
<string name="title_activity_main">OGame Vue Ts</string>
<string name="package_name">games.wenzi.ogame</string>
<string name="custom_url_scheme">games.wenzi.ogame</string>
</resources>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">#1a1a2e</item>
<item name="colorPrimaryDark">#0f0f1a</item>
<item name="colorAccent">#6366f1</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</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="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

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

29
android/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +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')

22
android/gradle.properties Normal file
View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Normal file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
android/settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

16
android/variables.gradle Normal file
View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}

17
build-docker.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
REM 本地 Docker 构建脚本 (Windows)
REM 使用完整的源代码构建流程
echo 🚀 开始本地 Docker 构建...
REM 构建镜像
docker build -t ogame-vue-ts:local .
if %ERRORLEVEL% EQU 0 (
echo ✅ Docker 镜像构建成功!
echo 📦 镜像标签: ogame-vue-ts:local
echo 🏃 运行命令: docker run -p 8080:80 ogame-vue-ts:local
) else (
echo ❌ Docker 镜像构建失败!
exit /b 1
)

18
build-docker.sh Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# 本地 Docker 构建脚本
# 使用完整的源代码构建流程
echo "🚀 开始本地 Docker 构建..."
# 构建镜像
docker build -t ogame-vue-ts:local .
if [ $? -eq 0 ]; then
echo "✅ Docker 镜像构建成功!"
echo "📦 镜像标签: ogame-vue-ts:local"
echo "🏃 运行命令: docker run -p 8080:80 ogame-vue-ts:local"
else
echo "❌ Docker 镜像构建失败!"
exit 1
fi

18
capacitor.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { CapacitorConfig } from '@capacitor/cli'
const config: CapacitorConfig = {
appId: 'games.wenzi.ogame',
appName: 'OGame Vue Ts',
webDir: 'docs',
server: { androidScheme: 'https', cacheControl: 'max-age=31536000' },
android: {
buildOptions: { keystorePath: undefined, keystoreAlias: undefined },
webContentsDebuggingEnabled: false,
allowMixedContent: false,
hardwareAcceleration: true
},
// 禁用键盘自动调整视口
plugins: { Keyboard: { resize: 'none' } }
}
export default config

2
go.mod
View File

@@ -1,3 +1,3 @@
module ogame
module ogame-vue-ts
go 1.25.4

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>

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

91
main.go
View File

@@ -1,6 +1,7 @@
package main
import (
"bufio"
"embed"
"flag"
"fmt"
@@ -10,6 +11,7 @@ import (
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
)
@@ -20,15 +22,21 @@ var content embed.FS
func main() {
// --- 1. 命令行参数配置 ---
// 定义 -port 参数,默认为 0自动分配
portPtr := flag.Int("port", 0, "指定运行端口 (例如: 8080),不指定则自动分配可用端口")
// 定义 -port 参数,默认为 -1表示未指定需要交互选择
portPtr := flag.Int("port", -1, "指定运行端口 (例如: 8080),不指定则显示交互菜单")
flag.Parse()
// 如果没有通过命令行指定端口,显示交互式菜单
port := *portPtr
if port == -1 {
port = showPortMenu()
}
// --- 2. 静态资源处理 ---
// 获取 docs 子目录的文件系统句柄
distFS, err := fs.Sub(content, "docs")
if err != nil {
fmt.Printf("错误: 无法访问嵌入的 docs 目录: %v\n", err)
fmt.Printf("错误: 无法访问嵌入的 docs 目录: %v\n", err)
return
}
@@ -60,10 +68,10 @@ func main() {
})
// --- 3. 端口监听逻辑 ---
addr := fmt.Sprintf("0.0.0.0:%d", *portPtr)
addr := fmt.Sprintf("0.0.0.0:%d", port)
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("错误: 端口 %d 已被占用或监听失败: %v\n", *portPtr, err)
fmt.Printf("错误: 端口 %d 已被占用或监听失败: %v\n", port, err)
// 停留 5 秒让用户看到错误信息
time.Sleep(5 * time.Second)
os.Exit(1)
@@ -75,17 +83,17 @@ func main() {
// --- 4. 控制台信息展示 ---
fmt.Println("=======================================")
fmt.Printf("🚀 OGame 服务启动成功!\n")
fmt.Printf("📅 启动时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("🔗 本地访问: %s\n", localUrl)
fmt.Printf("🌐 局域网访问: %s\n", lanUrl)
if *portPtr != 0 {
fmt.Printf("📌 运行模式: 固定端口 (%d)\n", *portPtr)
fmt.Printf("OGame Vue Ts 服务启动成功!\n")
fmt.Printf("启动时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("本地访问: %s\n", localUrl)
fmt.Printf("局域网访问: %s\n", lanUrl)
if port != 0 {
fmt.Printf("运行模式: 固定端口 (%d)\n", port)
} else {
fmt.Printf("🎲 运行模式: 自动分配端口\n")
fmt.Printf("运行模式: 自动分配端口\n")
}
fmt.Println("=======================================")
fmt.Println("💡 提示: 请勿关闭此控制台窗口,否则服务将停止。")
fmt.Println("提示: 请勿关闭此控制台窗口,否则服务将停止。")
fmt.Println("--- 实时访问日志 ---")
// --- 5. 自动打开浏览器并启动服务 ---
@@ -93,7 +101,7 @@ func main() {
err = http.Serve(listener, nil)
if err != nil {
fmt.Printf("服务运行异常: %v\n", err)
fmt.Printf("服务运行异常: %v\n", err)
}
}
@@ -132,3 +140,58 @@ func openBrowser(url string) {
_ = exec.Command(cmd, args...).Start()
}
// 显示端口选择菜单
func showPortMenu() int {
reader := bufio.NewReader(os.Stdin)
fmt.Println("=======================================")
fmt.Println(" OGame Vue Ts 服务器启动")
fmt.Println("=======================================")
fmt.Println()
fmt.Println("请选择端口模式:")
fmt.Println(" [1] 随机端口 (自动分配可用端口)")
fmt.Println(" [2] 自定义端口 (指定固定端口)")
fmt.Println()
fmt.Print("请输入选项 (1/2): ")
for {
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
switch input {
case "1", "":
fmt.Println("\n已选择: 随机端口模式")
return 0
case "2":
return inputCustomPort(reader)
default:
fmt.Print("无效输入,请输入 1 或 2: ")
}
}
}
// 输入自定义端口
func inputCustomPort(reader *bufio.Reader) int {
fmt.Print("请输入端口号 (1-65535推荐: 8080): ")
for {
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
// 如果直接回车,使用默认端口 8080
if input == "" {
fmt.Println("\n已选择: 固定端口 8080")
return 8080
}
port, err := strconv.Atoi(input)
if err != nil || port < 1 || port > 65535 {
fmt.Print("无效端口号,请输入 1-65535 之间的数字: ")
continue
}
fmt.Printf("\n已选择: 固定端口 %d\n", port)
return port
}
}

View File

@@ -8,52 +8,59 @@
"email": "1962257451@qq.com"
},
"private": true,
"version": "1.2.5",
<<<<<<< Updated upstream
"buildDate": "2025/12/15 21:21:23",
=======
"buildDate": "2025/12/15 21:59:38",
>>>>>>> Stashed changes
"version": "1.6.5",
"buildDate": "2026/1/23 02:22:24",
"main": "dist-electron/main.js",
"type": "module",
"scripts": {
"dev": "vite --port 25121",
"build": "vue-tsc -b && vite build && node update-build-date.js",
"preview": "vite preview",
"build:check": "pnpm run build",
"build:server": "pnpm run build && go build",
"build:electron": "cross-env ELECTRON_BUILD=1 pnpm run build && electron-builder"
"build:electron": "cross-env ELECTRON_BUILD=1 pnpm run build && electron-builder",
"build:android": "pnpm run build && npx cap sync android",
"build:apk": "pnpm run build:android && cd android && ./gradlew assembleRelease"
},
"dependencies": {
"@capacitor/android": "^8.0.0",
"@capacitor/app": "^8.0.0",
"@capacitor/cli": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@capacitor/filesystem": "^8.0.0",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^14.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"file-saver": "^2.0.5",
"finalhandler": "^2.1.1",
"lightningcss": "^1.30.2",
"lucide-vue-next": "^0.556.0",
"marked": "^17.0.1",
"motion-v": "^1.7.4",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"reka-ui": "^2.6.1",
"serve-static": "^2.2.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"vue": "^3.5.24",
"vue-router": "4",
"vue-sonner": "^2.0.9"
},
"devDependencies": {
"@csstools/postcss-cascade-layers": "^5.0.2",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7",
"@types/node": "^24.10.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.2.5",
@@ -75,10 +82,9 @@
"electron"
]
},
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad",
"build": {
"appId": "games.wenzi.ogame",
"productName": "OGame",
"productName": "OGame-Vue-Ts",
"directories": {
"output": "pkg"
},
@@ -88,6 +94,16 @@
"verifyUpdateCodeSignature": false,
"artifactName": "${productName}-Setup.${ext}"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"allowElevation": true,
"installerIcon": "public/favicon.ico",
"uninstallerIcon": "public/favicon.ico",
"installerHeaderIcon": "public/favicon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"mac": {
"target": [
"dmg",

512
pnpm-lock.yaml generated
View File

@@ -11,6 +11,21 @@ importers:
.:
dependencies:
'@capacitor/android':
specifier: ^8.0.0
version: 8.0.0(@capacitor/core@8.0.0)
'@capacitor/app':
specifier: ^8.0.0
version: 8.0.0(@capacitor/core@8.0.0)
'@capacitor/cli':
specifier: ^8.0.0
version: 8.0.0
'@capacitor/core':
specifier: ^8.0.0
version: 8.0.0
'@capacitor/filesystem':
specifier: ^8.0.0
version: 8.0.0(@capacitor/core@8.0.0)
'@tailwindcss/vite':
specifier: ^4.1.17
version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
@@ -20,12 +35,6 @@ importers:
'@vueuse/core':
specifier: ^14.1.0
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
crypto-js:
specifier: ^4.2.0
version: 4.2.0
@@ -35,12 +44,18 @@ 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))
marked:
specifier: ^17.0.1
version: 17.0.1
motion-v:
specifier: ^1.7.4
version: 1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
@@ -53,9 +68,6 @@ importers:
serve-static:
specifier: ^2.2.0
version: 2.2.0
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
@@ -69,6 +81,9 @@ importers:
specifier: ^2.0.9
version: 2.0.9
devDependencies:
'@csstools/postcss-cascade-layers':
specifier: ^5.0.2
version: 5.0.2(postcss@8.5.6)
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
@@ -84,6 +99,12 @@ importers:
'@vue/tsconfig':
specifier: ^0.8.1
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
cross-env:
specifier: ^7.0.3
version: 7.0.3
@@ -96,6 +117,9 @@ importers:
electron-vite:
specifier: ^5.0.0
version: 5.0.0(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
@@ -627,6 +651,44 @@ packages:
'@canvas/image-data@1.1.0':
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
'@capacitor/android@8.0.0':
resolution: {integrity: sha512-FrBSvVAC5JuLaYHNyDnwQny0/SYnP+xDQbc/KA4wInmRkMXLDv22fkx9aBJIDrxjuUVd+jsRih4SAt8FgMEzCw==}
peerDependencies:
'@capacitor/core': ^8.0.0
'@capacitor/app@8.0.0':
resolution: {integrity: sha512-OwzIkUs4w433Bu9WWAEbEYngXEfJXZ9Wmdb8eoaqzYBgB0W9/3Ed/mh6sAYPNBAZlpyarmewgP7Nb+d3Vrh+xA==}
peerDependencies:
'@capacitor/core': '>=8.0.0'
'@capacitor/cli@8.0.0':
resolution: {integrity: sha512-v9hEBi69xGxuuZhg55N031bMEenKaPSv71Il8C22VOOH6surDyv/MPeImN0oVfFc7eiklaW3rDFYVz6cmXfJWQ==}
engines: {node: '>=22.0.0'}
hasBin: true
'@capacitor/core@8.0.0':
resolution: {integrity: sha512-250HTVd/W/KdMygoqaedisvNbHbpbQTN2Hy/8ZYGm1nAqE0Fx7sGss4l0nDg33STxEdDhtVRoL2fIaaiukKseA==}
'@capacitor/filesystem@8.0.0':
resolution: {integrity: sha512-RRGNLW9xEqvVVHGyGlfS4Oy0R3Na+bEefwZElKbex22S9eZr5cg8wc750BPPVwbcv5lf5fJymkY8x8y6UwKPyg==}
peerDependencies:
'@capacitor/core': '>=8.0.0'
'@capacitor/synapse@1.0.4':
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
'@csstools/postcss-cascade-layers@5.0.2':
resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/selector-specificity@5.0.0':
resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==}
engines: {node: '>=18'}
peerDependencies:
postcss-selector-parser: ^7.0.0
'@develar/schema-utils@2.6.5':
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'}
@@ -981,6 +1043,38 @@ packages:
'@internationalized/number@3.6.5':
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
'@ionic/cli-framework-output@2.2.8':
resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
engines: {node: '>=16.0.0'}
'@ionic/utils-array@2.1.6':
resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-fs@3.1.7':
resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
engines: {node: '>=16.0.0'}
'@ionic/utils-object@2.1.6':
resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
engines: {node: '>=16.0.0'}
'@ionic/utils-process@2.1.12':
resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-stream@3.1.7':
resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
engines: {node: '>=16.0.0'}
'@ionic/utils-subprocess@3.0.1':
resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
engines: {node: '>=16.0.0'}
'@ionic/utils-terminal@2.3.5':
resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
engines: {node: '>=16.0.0'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
@@ -1336,6 +1430,9 @@ packages:
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/fs-extra@8.1.5':
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
'@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
@@ -1363,6 +1460,9 @@ packages:
'@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
'@types/slice-ansi@4.0.0':
resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1616,6 +1716,10 @@ packages:
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
hasBin: true
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
birpc@2.9.0:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
@@ -1626,6 +1730,10 @@ packages:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
bplist-parser@0.3.2:
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
engines: {node: '>= 5.10.0'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -1754,6 +1862,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -1818,6 +1930,11 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1869,6 +1986,10 @@ packages:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
define-properties@1.2.1:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
@@ -1960,6 +2081,10 @@ packages:
engines: {node: '>= 12.20.55'}
hasBin: true
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2103,6 +2228,20 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@@ -2189,6 +2328,10 @@ packages:
engines: {node: 20 || >=22}
hasBin: true
glob@13.0.0:
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
engines: {node: 20 || >=22}
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
@@ -2244,6 +2387,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hey-listen@1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -2317,6 +2463,10 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@4.1.3:
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -2364,6 +2514,11 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'}
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
is-finalizationregistry@1.1.1:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'}
@@ -2454,6 +2609,10 @@ packages:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'}
is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -2529,6 +2688,14 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
lazy-val@1.0.5:
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
@@ -2772,6 +2939,18 @@ packages:
engines: {node: '>=10'}
hasBin: true
motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
motion-v@1.7.4:
resolution: {integrity: sha512-YNDUAsany04wfI7YtHxQK3kxzNvh+OdFUk9GpA3+hMt7j6P+5WrVAAgr8kmPPoVza9EsJiAVhqoN3YYFN0Twrw==}
peerDependencies:
'@vueuse/core': '>=10.0.0'
vue: '>=3.0.0'
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2783,6 +2962,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
native-run@2.0.1:
resolution: {integrity: sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==}
engines: {node: '>=16.0.0'}
hasBin: true
negotiator@0.6.4:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'}
@@ -2835,6 +3019,10 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
@@ -2936,6 +3124,10 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -2973,6 +3165,10 @@ packages:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -3077,6 +3273,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@6.1.2:
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
engines: {node: 20 || >=22}
hasBin: true
roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
@@ -3152,6 +3353,9 @@ packages:
sanitize-filename@1.6.3:
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
sax@1.1.4:
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
@@ -3246,10 +3450,17 @@ packages:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slice-ansi@3.0.0:
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
engines: {node: '>=8'}
slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -3289,6 +3500,10 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
@@ -3401,6 +3616,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
tiny-async-pool@1.3.0:
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
@@ -3425,6 +3643,10 @@ packages:
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
@@ -3515,6 +3737,10 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
upath@1.2.0:
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
engines: {node: '>=4'}
@@ -3698,6 +3924,14 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlbuilder@15.1.1:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
@@ -4395,6 +4629,57 @@ snapshots:
'@canvas/image-data@1.1.0':
optional: true
'@capacitor/android@8.0.0(@capacitor/core@8.0.0)':
dependencies:
'@capacitor/core': 8.0.0
'@capacitor/app@8.0.0(@capacitor/core@8.0.0)':
dependencies:
'@capacitor/core': 8.0.0
'@capacitor/cli@8.0.0':
dependencies:
'@ionic/cli-framework-output': 2.2.8
'@ionic/utils-subprocess': 3.0.1
'@ionic/utils-terminal': 2.3.5
commander: 12.1.0
debug: 4.4.3
env-paths: 2.2.1
fs-extra: 11.3.2
kleur: 4.1.5
native-run: 2.0.1
open: 8.4.2
plist: 3.1.0
prompts: 2.4.2
rimraf: 6.1.2
semver: 7.7.3
tar: 6.2.1
tslib: 2.8.1
xml2js: 0.6.2
transitivePeerDependencies:
- supports-color
'@capacitor/core@8.0.0':
dependencies:
tslib: 2.8.1
'@capacitor/filesystem@8.0.0(@capacitor/core@8.0.0)':
dependencies:
'@capacitor/core': 8.0.0
'@capacitor/synapse': 1.0.4
'@capacitor/synapse@1.0.4': {}
'@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)':
dependencies:
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1)
postcss: 8.5.6
postcss-selector-parser: 7.1.1
'@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)':
dependencies:
postcss-selector-parser: 7.1.1
'@develar/schema-utils@2.6.5':
dependencies:
ajv: 6.12.6
@@ -4709,6 +4994,82 @@ snapshots:
dependencies:
'@swc/helpers': 0.5.17
'@ionic/cli-framework-output@2.2.8':
dependencies:
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-array@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-fs@3.1.7':
dependencies:
'@types/fs-extra': 8.1.5
debug: 4.4.3
fs-extra: 9.1.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-object@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-process@2.1.12':
dependencies:
'@ionic/utils-object': 2.1.6
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
signal-exit: 3.0.7
tree-kill: 1.2.2
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-stream@3.1.7':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-subprocess@3.0.1':
dependencies:
'@ionic/utils-array': 2.1.6
'@ionic/utils-fs': 3.1.7
'@ionic/utils-process': 2.1.12
'@ionic/utils-stream': 3.1.7
'@ionic/utils-terminal': 2.3.5
cross-spawn: 7.0.6
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-terminal@2.3.5':
dependencies:
'@types/slice-ansi': 4.0.0
debug: 4.4.3
signal-exit: 3.0.7
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
tslib: 2.8.1
untildify: 4.0.0
wrap-ansi: 7.0.0
transitivePeerDependencies:
- supports-color
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
@@ -5009,6 +5370,10 @@ snapshots:
'@types/file-saver@2.0.7': {}
'@types/fs-extra@8.1.5':
dependencies:
'@types/node': 24.10.2
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 24.10.2
@@ -5041,6 +5406,8 @@ snapshots:
dependencies:
'@types/node': 24.10.2
'@types/slice-ansi@4.0.0': {}
'@types/trusted-types@2.0.7': {}
'@types/verror@1.10.11':
@@ -5322,8 +5689,7 @@ snapshots:
assert-plus@1.0.0:
optional: true
astral-regex@2.0.0:
optional: true
astral-regex@2.0.0: {}
async-exit-hook@2.0.1: {}
@@ -5369,6 +5735,8 @@ snapshots:
baseline-browser-mapping@2.9.7: {}
big-integer@1.6.52: {}
birpc@2.9.0: {}
bl@4.1.0:
@@ -5380,6 +5748,10 @@ snapshots:
boolean@3.2.0:
optional: true
bplist-parser@0.3.2:
dependencies:
big-integer: 1.6.52
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@@ -5559,6 +5931,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@12.1.0: {}
commander@2.20.3: {}
commander@5.1.0: {}
@@ -5615,6 +5989,8 @@ snapshots:
crypto-random-string@2.0.0: {}
cssesc@3.0.0: {}
csstype@3.2.3: {}
data-view-buffer@1.0.2:
@@ -5670,6 +6046,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
define-lazy-prop@2.0.0: {}
define-properties@1.2.1:
dependencies:
define-data-property: 1.1.4
@@ -5813,6 +6191,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
elementtree@0.1.7:
dependencies:
sax: 1.1.4
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -6027,6 +6409,12 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
framer-motion@12.23.12:
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
fresh@2.0.0: {}
fs-extra@10.1.0:
@@ -6136,6 +6524,12 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 2.0.1
glob@13.0.0:
dependencies:
minimatch: 10.1.1
minipass: 7.1.2
path-scurry: 2.0.1
glob@7.2.3:
dependencies:
fs.realpath: 1.0.0
@@ -6208,6 +6602,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
hey-listen@1.0.8: {}
hookable@5.5.3: {}
hosted-git-info@4.1.0:
@@ -6292,6 +6688,8 @@ snapshots:
inherits@2.0.4: {}
ini@4.1.3: {}
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -6347,6 +6745,8 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-docker@2.2.1: {}
is-finalizationregistry@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -6425,6 +6825,10 @@ snapshots:
is-what@5.5.0: {}
is-wsl@2.2.0:
dependencies:
is-docker: 2.2.1
isarray@2.0.5: {}
isbinaryfile@4.0.10: {}
@@ -6488,6 +6892,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
kleur@3.0.3: {}
kleur@4.1.5: {}
lazy-val@1.0.5: {}
leven@3.1.0: {}
@@ -6694,12 +7102,46 @@ snapshots:
mkdirp@1.0.4: {}
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
motion-v@1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.25(typescript@5.9.3))
framer-motion: 12.23.12
hey-listen: 1.0.8
motion-dom: 12.23.12
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- '@emotion/is-prop-valid'
- react
- react-dom
ms@2.1.3: {}
muggle-string@0.4.1: {}
nanoid@3.3.11: {}
native-run@2.0.1:
dependencies:
'@ionic/utils-fs': 3.1.7
'@ionic/utils-terminal': 2.3.5
bplist-parser: 0.3.2
debug: 4.4.3
elementtree: 0.1.7
ini: 4.1.3
plist: 3.1.0
split2: 4.2.0
through2: 4.0.2
tslib: 2.8.1
yauzl: 2.10.0
transitivePeerDependencies:
- supports-color
negotiator@0.6.4: {}
node-abi@3.85.0:
@@ -6748,6 +7190,12 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
open@8.4.2:
dependencies:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
ora@5.4.1:
dependencies:
bl: 4.1.0
@@ -6831,6 +7279,11 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@7.1.1:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -6857,6 +7310,11 @@ snapshots:
err-code: 2.0.3
retry: 0.12.0
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
@@ -6982,6 +7440,11 @@ snapshots:
dependencies:
glob: 7.2.3
rimraf@6.1.2:
dependencies:
glob: 13.0.0
package-json-from-dist: 1.0.1
roarr@2.15.4:
dependencies:
boolean: 3.2.0
@@ -7059,6 +7522,8 @@ snapshots:
dependencies:
truncate-utf8-bytes: 1.0.2
sax@1.1.4: {}
sax@1.4.3: {}
semver-compare@1.0.0:
@@ -7209,6 +7674,8 @@ snapshots:
dependencies:
semver: 7.7.3
sisteransi@1.0.5: {}
slice-ansi@3.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -7216,6 +7683,12 @@ snapshots:
is-fullwidth-code-point: 3.0.0
optional: true
slice-ansi@4.0.0:
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
smart-buffer@4.2.0: {}
smob@1.5.0: {}
@@ -7250,6 +7723,8 @@ snapshots:
speakingurl@14.0.1: {}
split2@4.2.0: {}
sprintf-js@1.1.3:
optional: true
@@ -7394,6 +7869,10 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
through2@4.0.2:
dependencies:
readable-stream: 3.6.2
tiny-async-pool@1.3.0:
dependencies:
semver: 5.7.2
@@ -7418,6 +7897,8 @@ snapshots:
dependencies:
punycode: 2.3.1
tree-kill@1.2.2: {}
truncate-utf8-bytes@1.0.2:
dependencies:
utf8-byte-length: 1.0.5
@@ -7519,6 +8000,8 @@ snapshots:
universalify@2.0.1: {}
untildify@4.0.0: {}
upath@1.2.0: {}
update-browserslist-db@1.2.2(browserslist@4.28.1):
@@ -7774,6 +8257,13 @@ snapshots:
wrappy@1.0.2: {}
xml2js@0.6.2:
dependencies:
sax: 1.4.3
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
xmlbuilder@15.1.1: {}
y18n@5.0.8: {}

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@@ -1,7 +1,11 @@
<template>
<SidebarProvider :open="sidebarOpen" @update:open="sidebarOpen = $event">
<!-- 首页无侧边栏/头部 -->
<RouterView v-if="isHomePage" />
<!-- 其他页面完整布局含侧边栏 -->
<SidebarProvider v-else :open="sidebarOpen" @update:open="handleSidebarOpenChange">
<Sidebar collapsible="icon">
<!-- Logo -->
<!-- 标志 -->
<SidebarHeader class="border-b">
<div class="flex items-center justify-center p-4 group-data-[collapsible=icon]:p-2">
<img src="@/assets/logo.svg" class="w-10 group-data-[collapsible=icon]:w-8" />
@@ -17,60 +21,70 @@
<Popover>
<PopoverTrigger as-child>
<Button
data-tutorial="planet-selector"
variant="outline"
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
>
<div class="flex items-start gap-2.5 flex-1 min-w-0">
<Globe class="h-5 w-5 flex-shrink-0 mt-0.5 text-primary" />
<Globe class="h-5 w-5 shrink-0 mt-0.5 text-primary" />
<div class="flex-1 min-w-0 text-left">
<div class="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">
{{ t('planet.currentPlanet') }}
</div>
<div class="flex items-center gap-1.5 mb-0.5">
<span class="truncate font-semibold text-sm">{{ planet.name }}</span>
<span class="truncate font-semibold text-sm">
{{ planet.name }}
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
</span>
<Badge v-if="planet.isMoon" variant="secondary" class="text-[10px] px-1 py-0 h-4">
{{ t('planet.moon') }}
</Badge>
</div>
<div class="text-[11px] text-muted-foreground">
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
</div>
</div>
</div>
<ChevronsUpDown class="h-4 w-4 flex-shrink-0 text-muted-foreground ml-2" />
<ChevronsUpDown class="h-4 w-4 shrink-0 text-muted-foreground ml-2" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-72 p-0" side="bottom" align="start">
<PopoverContent class="w-70 p-0" side="bottom" align="start">
<div class="p-2">
<div class="px-2 py-1.5 mb-1 text-xs font-semibold text-muted-foreground">
{{ t('planet.switchPlanet') }}
</div>
<div class="space-y-0.5 max-h-80 overflow-y-auto">
<div v-for="p in gameStore.player.planets" :key="p.id" class="flex items-center gap-1">
<Button
v-for="p in gameStore.player.planets"
:key="p.id"
@click="switchToPlanet(p.id)"
:variant="p.id === planet.id ? 'secondary' : 'ghost'"
class="w-full justify-start h-auto py-2 px-2"
class="flex-1 justify-start h-auto py-2 px-2"
size="sm"
>
<div class="flex items-start gap-2 w-full min-w-0">
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" :class="p.id === planet.id ? 'text-primary' : ''" />
<Globe class="h-4 w-4 shrink-0 mt-0.5" :class="p.id === planet.id ? 'text-primary' : ''" />
<div class="flex-1 min-w-0 text-left">
<div class="flex items-center gap-1.5 mb-0.5">
<span class="truncate font-medium text-sm">{{ p.name }}</span>
<span class="truncate font-medium text-sm">
{{ p.name }}
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
</span>
<Button
variant="ghost"
size="sm"
class="h-2 w-2 p-0 shrink-0"
@click.stop="openRenameDialog(p.id, p.name)"
:title="t('planet.renamePlanet')"
>
<Pencil class="h-2 w-2" />
</Button>
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
{{ t('planet.moon') }}
</Badge>
</div>
<div class="text-[11px] text-muted-foreground">
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
</div>
</div>
</div>
</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
@@ -94,11 +108,16 @@
</SidebarGroup>
<!-- 导航菜单 -->
<SidebarGroup>
<SidebarGroup data-tutorial="navigation">
<SidebarMenu>
<SidebarMenuItem v-for="item in navItems" :key="item.path">
<SidebarMenuButton as-child :is-active="$route.path === item.path" :tooltip="item.name.value">
<RouterLink :to="item.path">
<SidebarMenuButton
:data-nav-path="item.path"
:is-active="$route.path === item.path"
:tooltip="item.name.value"
:disabled="!isFeatureUnlocked(item.path)"
@click="router.push(item.path)"
>
<component :is="item.icon" />
<span>{{ item.name.value }}</span>
<!-- 未读消息数量 -->
@@ -109,13 +128,9 @@
{{ unreadMessagesCount }}
</SidebarMenuBadge>
<!-- 正在执行的舰队任务数量 -->
<SidebarMenuBadge
v-if="item.path === '/fleet' && activeFleetMissionsCount > 0"
class="bg-primary text-primary-foreground"
>
<SidebarMenuBadge v-if="item.path === '/fleet' && activeFleetMissionsCount > 0" class="bg-primary text-primary-foreground">
{{ activeFleetMissionsCount }}
</SidebarMenuBadge>
</RouterLink>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
@@ -177,36 +192,55 @@
<!-- 主内容区 -->
<SidebarInset>
<div class="flex flex-col h-full overflow-hidden pt-[60px]">
<div class="flex flex-col h-full" :class="Capacitor.isNativePlatform() ? 'pt-[80px]' : 'pt-[60px]'">
<!-- 顶部资源栏 - 固定定位 -->
<header
v-if="planet"
class="fixed top-0 right-0 left-0 z-40 bg-card border-b px-4 sm:px-6 py-3 shadow-md"
:class="sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]'"
ref="header"
class="fixed top-0 right-0 left-0 z-40 bg-card border-b px-4 sm:px-6 shadow-md"
:class="[
sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]',
Capacitor.isNativePlatform() ? 'py-6' : 'py-3'
]"
>
<div class="flex flex-col gap-3">
<!-- 第一行菜单资源预览状态 -->
<div class="grid items-center gap-3 sm:gap-6" style="grid-template-columns: auto 1fr auto">
<div
class="grid items-center gap-3 sm:gap-6"
style="grid-template-columns: auto 1fr auto"
:class="{
'relative top-3': Capacitor.isNativePlatform()
}"
>
<!-- 左侧汉堡菜单移动端/ 占位PC端 -->
<div>
<SidebarTrigger class="lg:hidden" />
<SidebarTrigger class="lg:hidden" data-tutorial="mobile-menu" />
</div>
<!-- 资源显示 - PC端居中移动端可折叠 -->
<div :class="['flex items-center gap-3 sm:gap-6 justify-center', resourceBarExpanded ? 'hidden' : 'overflow-x-auto']">
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
<!-- 关键min-w-0 + overflow-hidden避免横向滚动内容溢出覆盖左侧菜单按钮 -->
<div class="min-w-0 overflow-hidden">
<div
class="resource-bar flex items-center gap-3 sm:gap-6 justify-start sm:justify-center"
:class="[resourceBarExpanded ? 'hidden' : 'overflow-x-auto']"
>
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 shrink-0">
<ResourceIcon :type="resourceType.key" size="md" />
<div class="min-w-0">
<!-- 电力显示净产量和效率 -->
<!-- 电力显示当前储量/最大容量净产量/小时 -->
<template v-if="resourceType.key === 'energy'">
<p
class="text-xs sm:text-sm font-medium truncate"
:class="getResourceColor(planet.resources.energy, capacity?.energy || Infinity)"
>
{{ formatNumber(planet.resources.energy) }} /
{{ formatNumber(capacity?.energy || 0) }}
</p>
<p
class="text-[10px] sm:text-xs truncate"
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
{{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(Math.round(netEnergy / 60)) }}/{{ t('resources.perMinute') }}
</p>
</template>
<!-- 其他资源统一显示当前值/容量 -->
@@ -215,7 +249,8 @@
class="text-xs sm:text-sm font-medium truncate"
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
>
{{ formatNumber(planet.resources[resourceType.key]) }} / {{ formatNumber(capacity?.[resourceType.key] || 0) }}
{{ formatNumber(planet.resources[resourceType.key]) }} /
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
</p>
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
+{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{ t('resources.perMinute') }}
@@ -224,24 +259,15 @@
</div>
</div>
</div>
</div>
<!-- 右侧展开按钮仅移动端 + 状态 -->
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0 justify-end">
<!-- 右侧队列通知 + 展开按钮 -->
<div class="flex items-center gap-2 sm:gap-3 shrink-0 justify-end">
<!-- 移动端展开按钮 -->
<Button @click="resourceBarExpanded = !resourceBarExpanded" variant="ghost" size="sm" class="lg:hidden h-8 w-8 p-0">
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
<ChevronUp v-else class="h-4 w-4" />
</Button>
<!-- 建造队列状态 -->
<div v-if="planet.buildQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.building') }}</span>
</div>
<div v-if="gameStore.player.researchQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.researching') }}</span>
</div>
</div>
</div>
</div>
@@ -258,8 +284,11 @@
>
<div
v-if="planet && resourceBarExpanded"
class="fixed top-[60px] right-0 left-0 z-30 bg-card border-b px-4 py-3 shadow-md lg:hidden"
:class="sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]'"
class="fixed right-0 left-0 z-30 bg-card border-b px-4 py-3 shadow-md lg:hidden"
:class="[
sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]',
Capacitor.isNativePlatform() ? 'top-[80px]' : 'top-[60px]'
]"
>
<div class="grid grid-cols-2 gap-3">
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="bg-muted/50 rounded-lg p-2.5">
@@ -268,16 +297,21 @@
<span class="text-xs font-medium text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}</span>
</div>
<div class="space-y-0.5 text-center">
<!-- 电力显示净产量和效率 -->
<!-- 电力显示当前储量容量净产量/分钟 -->
<template v-if="resourceType.key === 'energy'">
<p
class="text-sm font-semibold"
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
<p class="text-sm font-semibold" :class="getResourceColor(planet.resources.energy, capacity?.energy || Infinity)">
{{ formatNumber(planet.resources.energy) }}
</p>
<p class="text-[10px] text-muted-foreground">
{{ t('resources.production') }}: {{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
{{ t('resources.capacity') }}: {{ formatNumber(capacity?.energy || 0) }}
</p>
<p
class="text-[10px]"
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
>
{{ t('resources.production') }}: {{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(Math.round(netEnergy / 60)) }}/{{
t('resources.perMinute')
}}
</p>
</template>
<!-- 其他资源统一显示当前值/容量 -->
@@ -304,85 +338,59 @@
</Transition>
<!-- 即将到来的敌对舰队警告 -->
<IncomingFleetAlerts
v-if="gameStore.player.incomingFleetAlerts && gameStore.player.incomingFleetAlerts.length > 0"
:alerts="gameStore.player.incomingFleetAlerts"
@mark-as-read="removeIncomingFleetAlert"
/>
<IncomingFleetAlerts @open-panel="openEnemyAlertPanel" />
<!-- 建造队列 -->
<div
v-if="planet && (planet.buildQueue.length > 0 || gameStore.player.researchQueue.length > 0)"
class="bg-card border-b px-4 sm:px-6 py-4.5"
>
<div class="space-y-3">
<!-- 建造队列 -->
<div v-for="item in planet.buildQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
<template v-if="item.type === 'ship' || item.type === 'defense'">
{{ t('queue.quantity') }} {{ item.quantity }}
</template>
<template v-else> {{ t('queue.level') }} {{ item.targetLevel }}</template>
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelBuild(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
<!-- 研究队列 -->
<div v-for="item in gameStore.player.researchQueue" :key="item.id" class="space-y-1.5">
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
<span class="font-medium truncate">{{ getItemName(item) }}</span>
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
{{ t('queue.level') }} {{ item.targetLevel }}
</span>
</div>
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
{{ formatTime(getRemainingTime(item)) }}
</span>
<Button
@click="handleCancelResearch(item.id)"
variant="ghost"
size="sm"
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
>
{{ t('queue.cancel') }}
</Button>
</div>
</div>
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
</div>
</div>
</div>
<!-- 低电量警告 -->
<LowEnergyWarning />
<!-- 矿脉储量警告 -->
<OreDepositWarning />
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto">
<div class="animate-fade-in">
<main class="flex-1">
<Transition name="page" mode="out-in">
<div :key="$route.fullPath" class="h-full">
<!-- 背景动画开启时 -->
<template v-if="gameStore.player.backgroundEnabled">
<StarsBackground v-if="isDark" :factor="0.05" :speed="50" star-color="#fff" class="h-full">
<div class="relative z-10 h-full">
<RouterView />
</div>
</StarsBackground>
<div v-else class="relative h-full w-full overflow-hidden">
<div class="relative z-10 h-full">
<RouterView />
</div>
<ParticlesBg class="absolute inset-0 z-0" :quantity="100" :ease="100" color="#000" :staticity="10" refresh />
</div>
</template>
<!-- 背景动画关闭时 -->
<div v-else class="h-full">
<RouterView />
</div>
</div>
</Transition>
</main>
</div>
</SidebarInset>
<!-- 右下角固定通知按钮 -->
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2" :class="{ 'bottom-15': Capacitor.isNativePlatform() }">
<!-- 返回顶部 -->
<BackToTop />
<!-- 队列通知 -->
<QueueNotifications />
<!-- 外交通知 -->
<DiplomaticNotifications />
<!-- 敌方警报 -->
<EnemyAlertNotifications ref="enemyAlertNotificationsRef" />
</div>
<!-- 确认对话框 -->
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
<AlertDialogContent>
@@ -398,32 +406,88 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 详情弹窗 -->
<DetailDialog />
<!-- 更新弹窗 -->
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
<!-- 弱引导提示系统 -->
<HintToast />
<!-- Toast 通知 -->
<Sonner position="top-center" />
<!-- 重命名星球对话框 -->
<Dialog v-model:open="renameDialogOpen">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ t('planet.renamePlanetTitle') }}</DialogTitle>
<DialogDescription class="sr-only">{{ t('planet.renamePlanetTitle') }}</DialogDescription>
</DialogHeader>
<div class="py-4">
<Input v-model="newPlanetName" :placeholder="t('planet.planetNamePlaceholder')" @keyup.enter="confirmRenamePlanet" />
</div>
<DialogFooter>
<Button variant="outline" @click="renameDialogOpen = false">
{{ t('common.cancel') }}
</Button>
<Button @click="confirmRenamePlanet" :disabled="!newPlanetName.trim()">
{{ t('planet.rename') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</SidebarProvider>
<!-- Android 退出确认对话框 -->
<AlertDialog v-model:open="exitDialogOpen">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('common.exitConfirmTitle') }}</AlertDialogTitle>
<AlertDialogDescription>{{ t('common.exitConfirmMessage') }}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="exitApp">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- NPC 名称更新确认对话框 -->
<AlertDialog v-model:open="npcNameUpdateDialogOpen">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('settings.npcNameUpdateTitle') }}</AlertDialogTitle>
<AlertDialogDescription>
{{ t('settings.npcNameUpdateMessage', { count: oldFormatNPCCount }) }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="handleSkipNPCNameUpdate">{{ t('settings.npcNameUpdateCancel') }}</AlertDialogCancel>
<AlertDialogAction @click="handleUpdateNPCNames">{{ t('settings.npcNameUpdateConfirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
import { RouterView, useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useNPCStore } from '@/stores/npcStore'
import { useTheme } from '@/composables/useTheme'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import IncomingFleetAlerts from '@/components/notifications/IncomingFleetAlerts.vue'
import LowEnergyWarning from '@/components/notifications/LowEnergyWarning.vue'
import OreDepositWarning from '@/components/notifications/OreDepositWarning.vue'
import DiplomaticNotifications from '@/components/notifications/DiplomaticNotifications.vue'
import EnemyAlertNotifications from '@/components/notifications/EnemyAlertNotifications.vue'
import QueueNotifications from '@/components/notifications/QueueNotifications.vue'
import {
Sidebar,
SidebarContent,
@@ -438,7 +502,7 @@
SidebarProvider,
SidebarTrigger
} from '@/components/ui/sidebar'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -449,13 +513,17 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import DetailDialog from '@/components/DetailDialog.vue'
import UpdateDialog from '@/components/UpdateDialog.vue'
import DetailDialog from '@/components/dialogs/DetailDialog.vue'
import UpdateDialog from '@/components/dialogs/UpdateDialog.vue'
import HintToast from '@/components/notifications/HintToast.vue'
import BackToTop from '@/components/common/BackToTop.vue'
import Sonner from '@/components/ui/sonner/Sonner.vue'
import { MissionType } from '@/types/game'
import type { BuildQueueItem, FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType, ShipType, DefenseType } from '@/types/game'
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
import type { VersionInfo } from '@/utils/versionCheck'
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
import { formatNumber, getResourceColor } from '@/utils/format'
import { scaleNumber, scaleResources } from '@/utils/speed'
import {
Moon,
Sun,
@@ -476,7 +544,11 @@
ChevronsUpDown,
ChevronDown,
ChevronUp,
Handshake
Handshake,
Pencil,
Trophy,
Crown,
Scroll
} from 'lucide-vue-next'
import * as gameLogic from '@/logic/gameLogic'
import * as planetLogic from '@/logic/planetLogic'
@@ -489,30 +561,254 @@
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
import * as diplomaticLogic from '@/logic/diplomaticLogic'
import * as publicLogic from '@/logic/publicLogic'
import * as oreDepositLogic from '@/logic/oreDepositLogic'
import * as campaignLogic from '@/logic/campaignLogic'
import { generateNPCName, countOldFormatNPCs, updateNPCName } from '@/logic/npcNameGenerator'
import pkg from '../package.json'
import { toast } from 'vue-sonner'
import { migrateGameData } from '@/utils/migration'
import { checkLatestVersion } from '@/utils/versionCheck'
import { StarsBackground } from '@/components/ui/bg-stars'
import { ParticlesBg } from '@/components/ui/particles-bg'
import { App as CapacitorApp } from '@capacitor/app'
import { Capacitor } from '@capacitor/core'
// 执行数据迁移(在 store 初始化之前)
migrateGameData()
const router = useRouter()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const npcStore = useNPCStore()
const { isDark } = useTheme()
const { t } = useI18n()
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
const enemyAlertNotificationsRef = ref<InstanceType<typeof EnemyAlertNotifications> | null>(null)
// ConfirmDialog 状态
const confirmDialogOpen = ref(false)
const confirmDialogTitle = ref('')
const confirmDialogMessage = ref('')
const innerWidth = computed(() => window.innerWidth)
const confirmDialogAction = ref<(() => void) | null>(null)
// 更新弹窗状态
const showUpdateDialog = ref(false)
const updateInfo = ref<VersionInfo | null>(null)
// 所有可用的语言选项
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'es-LA', 'ko', 'ja']
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
// PC端≥1024px默认打开移动端默认关闭
const sidebarOpen = ref(window.innerWidth >= 1024)
// 移动端资源栏展开状态
const resourceBarExpanded = ref(false)
const npcUpdateCounter = ref(0) // 累计秒数
const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC确保发育速度与玩家相当
// NPC行为系统更新函数侦查和攻击决策
const npcBehaviorCounter = ref(0)
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
// 游戏循环定时器
const gameLoop = ref<ReturnType<typeof setInterval> | null>(null)
const pointsUpdateInterval = ref<ReturnType<typeof setInterval> | null>(null)
const konamiCleanup = ref<(() => void) | null>(null)
const versionCheckInterval = ref<ReturnType<typeof setInterval> | null>(null) // 重命名星球相关状态
const renameDialogOpen = ref(false)
const renamingPlanetId = ref<string | null>(null)
const newPlanetName = ref('')
// Android 退出确认对话框状态
const exitDialogOpen = ref(false)
// NPC 名称更新对话框状态
const npcNameUpdateDialogOpen = ref(false)
const oldFormatNPCCount = ref(0)
// 功能解锁要求配置
const featureRequirements: Record<string, { building: BuildingType; level: number }> = {
'/research': { building: BuildingType.ResearchLab, level: 1 },
'/shipyard': { building: BuildingType.Shipyard, level: 1 },
'/defense': { building: BuildingType.Shipyard, level: 1 },
'/fleet': { building: BuildingType.Shipyard, level: 1 },
'/officers': { building: BuildingType.Shipyard, level: 1 }
}
// 判断是否为首页
const isHomePage = computed(() => router.currentRoute.value.path === '/')
// 定义 planet computed需要在 watch 之前定义)
const planet = computed(() => gameStore.currentPlanet)
// 资源类型配置
const resourceTypes = [
{ key: 'metal' as const },
{ key: 'crystal' as const },
{ key: 'deuterium' as const },
{ key: 'energy' as const },
{ key: 'darkMatter' as const }
]
const navItems = computed(() => [
{ name: computed(() => t('nav.overview')), path: '/overview', icon: Home },
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
{ name: computed(() => t('nav.achievements')), path: '/achievements', icon: Trophy },
{ name: computed(() => t('nav.campaign')), path: '/campaign', icon: Scroll },
{ name: computed(() => t('nav.ranking')), path: '/ranking', icon: Crown },
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
// GM菜单在启用GM模式时显示
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
])
// 使用直接计算,不再缓存
const production = computed(() => {
if (!planet.value) return null
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
const base = resourceLogic.calculateResourceProduction(planet.value, {
resourceProductionBonus: bonuses.resourceProductionBonus,
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
energyProductionBonus: bonuses.energyProductionBonus
})
return scaleResources(base, gameStore.gameSpeed)
})
const capacity = computed(() => {
if (!planet.value) return null
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
})
// 电力消耗
const energyConsumption = computed(() => {
if (!planet.value) return 0
return scaleNumber(resourceLogic.calculateEnergyConsumption(planet.value), gameStore.gameSpeed)
})
// 净电力(产量 - 消耗)
const netEnergy = computed(() => {
if (!planet.value || !production.value) return 0
return production.value.energy - energyConsumption.value
})
// 未读消息数量
const unreadMessagesCount = computed(() => {
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
const unreadTradeOffers = gameStore.player.tradeOffers?.filter(o => !o.read).length || 0
const unreadIntelReports = gameStore.player.intelReports?.filter(r => !r.read).length || 0
const unreadJointAttacks = gameStore.player.jointAttackInvites?.filter(i => !i.read).length || 0
return (
unreadBattles +
unreadSpies +
unreadSpied +
unreadMissions +
unreadNPCActivity +
unreadGifts +
unreadGiftRejected +
unreadTradeOffers +
unreadIntelReports +
unreadJointAttacks
)
})
// 正在执行的舰队任务数量(包括飞行中的导弹)
const activeFleetMissionsCount = computed(() => {
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
return fleetMissions + flyingMissiles
})
// 月球相关
const moon = computed(() => {
if (!planet.value || planet.value.isMoon) return null
return gameStore.getMoonForPlanet(planet.value.id)
})
const hasMoon = computed(() => !!moon.value)
const handleNotification = (type: string, itemType: string, level?: number) => {
const settings = gameStore.notificationSettings
if (!settings) return
// 检查主开关
if (!settings.browser && !settings.inApp) return
// 检查具体类型开关
let typeKey: 'construction' | 'research'
let title = ''
let body = ''
if (type === 'building') {
typeKey = 'construction'
const buildingType = itemType as BuildingType
const name = BUILDINGS.value[buildingType]?.name || itemType
title = t('notifications.constructionComplete')
body = `${name} Lv ${level}`
} else if (type === 'technology') {
typeKey = 'research'
const technologyType = itemType as TechnologyType
const name = TECHNOLOGIES.value[technologyType]?.name || itemType
title = t('notifications.researchComplete')
body = `${name} Lv ${level}`
} else {
return
}
if (!settings.types[typeKey as keyof typeof settings.types]) return
// 浏览器通知
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
if (!shouldSuppress) {
new Notification(title, { body, icon: '/favicon.ico' })
}
}
// 页面内 toast 通知
if (settings.inApp) {
toast.success(title, { description: body })
}
}
// 处理解锁通知
const handleUnlockNotification = (unlockedItems: Array<{ type: 'building' | 'technology'; id: string; name: string }>) => {
const settings = gameStore.notificationSettings
if (!settings) return
// 检查主开关和解锁类型开关
if (!settings.browser && !settings.inApp) return
if (!settings.types.unlock) return
unlockedItems.forEach(item => {
const title = t('notifications.newUnlock')
const typeLabel = item.type === 'building' ? t('notifications.building') : t('notifications.technology')
const body = `${typeLabel}: ${item.name}`
// 浏览器通知
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
if (!shouldSuppress) {
new Notification(title, { body, icon: '/favicon.ico' })
}
}
// 页面内 toast 通知
if (settings.inApp) {
toast.info(title, { description: body })
}
})
}
const handleConfirmDialogConfirm = () => {
if (confirmDialogAction.value) {
@@ -521,31 +817,68 @@
confirmDialogOpen.value = false
}
// 所有可用的语言选项
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
// PC端≥1024px默认打开移动端默认关闭
const sidebarOpen = ref(window.innerWidth >= 1024)
// 移动端资源栏展开状态
const resourceBarExpanded = ref(false)
const initGame = async () => {
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
if (!shouldInit) {
const now = Date.now()
// 计算离线收益(直接同步计算)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
// 迁移矿脉储量数据(为没有矿脉数据的星球初始化)
gameStore.player.planets.forEach(planet => {
resourceLogic.updatePlanetResources(planet, now, bonuses)
oreDepositLogic.migrateOreDeposits(planet)
})
// 迁移NPC星球的矿脉储量
npcStore.npcs.forEach(npc => {
npc.planets.forEach(planet => {
oreDepositLogic.migrateOreDeposits(planet)
})
})
// 迁移宇宙地图中的星球NPC星球的副本
Object.values(universeStore.planets).forEach(planet => {
oreDepositLogic.migrateOreDeposits(planet)
})
// 计算离线收益(直接同步计算,应用游戏速度)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
const miningTechLevel = gameStore.player.technologies[TechnologyType.MiningTechnology] || 0
const techBonuses = {
mineralResearchLevel: gameStore.player.technologies[TechnologyType.MineralResearch] || 0,
crystalResearchLevel: gameStore.player.technologies[TechnologyType.CrystalResearch] || 0,
fuelResearchLevel: gameStore.player.technologies[TechnologyType.FuelResearch] || 0
}
gameStore.player.planets.forEach(planet => {
resourceLogic.updatePlanetResources(planet, now, bonuses, gameStore.gameSpeed, miningTechLevel, techBonuses)
})
// 只在没有NPC星球时才生成首次加载已有玩家数据时
if (Object.keys(universeStore.planets).length === 0) {
generateNPCPlanets()
}
// 数据迁移:为没有 bonusPoints 的玩家计算奖励积分
if (gameStore.player.bonusPoints === undefined) {
// 计算基础积分(建筑、科技、舰船、防御)
let totalCost = 0
gameStore.player.planets.forEach(planet => {
Object.entries(planet.buildings).forEach(([buildingType, level]) => {
totalCost += publicLogic.calculateBuildingTotalCost(buildingType as BuildingType, level)
})
Object.entries(planet.fleet).forEach(([shipType, count]) => {
totalCost += publicLogic.calculateShipUnitCost(shipType as ShipType) * count
})
Object.entries(planet.defense).forEach(([defenseType, count]) => {
totalCost += publicLogic.calculateDefenseUnitCost(defenseType as DefenseType) * count
})
})
Object.entries(gameStore.player.technologies).forEach(([techType, level]) => {
totalCost += publicLogic.calculateTechnologyTotalCost(techType as TechnologyType, level)
})
const basePoints = Math.floor(totalCost / 1000)
// bonusPoints = 当前积分 - 基础积分
gameStore.player.bonusPoints = Math.max(0, gameStore.player.points - basePoints)
}
// 初始化或更新玩家积分
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
return
}
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
@@ -554,6 +887,8 @@
gameStore.currentPlanetId = initialPlanet.id
// 新玩家初始化时生成NPC星球
generateNPCPlanets()
// 初始化玩家积分
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
}
const generateNPCPlanets = () => {
@@ -568,13 +903,13 @@
}
const updateGame = async () => {
if (gameStore.isPaused) return
const now = Date.now()
if (gameStore.isPaused) return
gameStore.gameTime = now
// 检查军官过期
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
// 处理游戏更新(建造队列、研究队列等)
const result = gameLogic.processGameUpdate(gameStore.player, now)
const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, handleNotification, handleUnlockNotification)
gameStore.player.researchQueue = result.updatedResearchQueue
// 处理舰队任务
gameStore.player.fleetMissions.forEach(mission => {
@@ -613,6 +948,47 @@
// NPC行为系统更新侦查和攻击决策
updateNPCBehavior(1)
// 检查成就解锁
checkAchievementUnlocks()
// 检查战役任务进度
if (gameStore.player.campaignProgress) {
campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs)
}
// 检查并处理被消灭的NPC所有星球都被摧毁的NPC
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
if (eliminatedNpcIds.length > 0) {
// 从universeStore中移除被消灭NPC的星球数据并收集需要清理的任务ID
const missionIdsToRemove: string[] = []
eliminatedNpcIds.forEach(npcId => {
const npc = npcStore.npcs.find(n => n.id === npcId)
if (npc) {
// 遍历NPC的所有星球从universeStore中删除
if (npc.planets) {
npc.planets.forEach(planet => {
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
if (universeStore.planets[planetKey]) {
delete universeStore.planets[planetKey]
}
})
}
// 收集该NPC所有任务的ID用于清理玩家的警报
if (npc.fleetMissions) {
npc.fleetMissions.forEach(m => missionIdsToRemove.push(m.id))
}
}
})
// 清理玩家的即将到来舰队警报移除已消灭NPC的任务警报
if (gameStore.player.incomingFleetAlerts && missionIdsToRemove.length > 0) {
gameStore.player.incomingFleetAlerts = gameStore.player.incomingFleetAlerts.filter(alert => !missionIdsToRemove.includes(alert.id))
}
// 从NPC列表中移除被消灭的NPC
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
}
}
const processMissionArrival = async (mission: FleetMission) => {
@@ -623,20 +999,62 @@
mission.targetPosition.position
)
// 先从玩家星球中查找,再从宇宙地图中查找
// 如果任务指定了targetIsMoon需要精确匹配行星或月球
const targetPlanet =
gameStore.player.planets.find(p => {
const positionMatch =
p.position.galaxy === mission.targetPosition.galaxy &&
p.position.system === mission.targetPosition.system &&
p.position.position === mission.targetPosition.position
// 如果任务明确指定目标类型,按类型匹配
if (mission.targetIsMoon !== undefined) {
return positionMatch && p.isMoon === mission.targetIsMoon
}
// 兼容旧任务:默认优先匹配行星(非月球)
return positionMatch && !p.isMoon
}) ||
// 如果没有匹配到指定类型,尝试匹配同位置的任何星球
gameStore.player.planets.find(
p =>
p.position.galaxy === mission.targetPosition.galaxy &&
p.position.system === mission.targetPosition.system &&
p.position.position === mission.targetPosition.position
) || universeStore.planets[targetKey]
) ||
universeStore.planets[targetKey]
// 获取起始星球名称(用于报告)
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
if (mission.missionType === MissionType.Transport) {
// 在处理任务之前保存货物信息因为processTransportArrival会清空cargo
const transportedResources = { ...mission.cargo }
const isGiftMission = mission.isGift && mission.giftTargetNpcId
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
// 更新成就统计(仅在成功时追踪)
if (result.success) {
const totalTransported =
transportedResources.metal + transportedResources.crystal + transportedResources.deuterium + transportedResources.darkMatter
if (isGiftMission) {
// 送礼成功
gameLogic.trackDiplomacyStats(gameStore.player, 'gift', { resourcesAmount: totalTransported })
} else {
// 普通运输任务成功
gameLogic.trackMissionStats(gameStore.player, 'transport', { resourcesAmount: totalTransported })
}
}
// 生成失败原因消息
let transportFailMessage = t('missionReports.transportFailed')
if (!result.success && result.failReason) {
if (result.failReason === 'targetNotFound') {
transportFailMessage = t('missionReports.transportFailedTargetNotFound')
} else if (result.failReason === 'giftRejected') {
transportFailMessage = t('missionReports.transportFailedGiftRejected')
}
}
// 生成运输任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -652,9 +1070,10 @@
targetPlanetName:
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
success: result.success,
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
message: result.success ? t('missionReports.transportSuccess') : transportFailMessage,
details: {
transportedResources: mission.cargo
transportedResources,
failReason: result.failReason
},
read: false
})
@@ -663,11 +1082,31 @@
if (attackResult) {
gameStore.player.battleReports.push(attackResult.battleResult)
// 更新成就统计 - 攻击
const debrisValue = attackResult.debrisField
? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal
: 0
const won = attackResult.battleResult.winner === 'attacker'
gameLogic.trackAttackStats(gameStore.player, attackResult.battleResult, won, debrisValue)
// 检查是否攻击了NPC星球更新外交关系
if (targetPlanet) {
const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
if (targetNpc) {
diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, attackResult.battleResult, npcStore.npcs, gameStore.locale)
// 同步战斗损失到NPC的实际星球数据
const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id)
if (npcPlanet) {
// 同步舰队损失
Object.entries(attackResult.battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => {
npcPlanet.fleet[shipType as ShipType] = Math.max(0, (npcPlanet.fleet[shipType as ShipType] || 0) - lost)
})
// 同步防御损失修复后的数据已在targetPlanet中
npcPlanet.defense = { ...targetPlanet.defense }
// 同步资源(被掠夺后的)
npcPlanet.resources = { ...targetPlanet.resources }
}
}
}
@@ -680,7 +1119,24 @@
}
}
} else if (mission.missionType === MissionType.Colonize) {
const newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
const colonizeResult = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
const newPlanet = colonizeResult.planet
// 更新成就统计 - 殖民
if (colonizeResult.success && newPlanet) {
gameLogic.trackMissionStats(gameStore.player, 'colonize')
}
// 生成失败原因消息
let failMessage = t('missionReports.colonizeFailed')
if (!colonizeResult.success && colonizeResult.failReason) {
if (colonizeResult.failReason === 'positionOccupied') {
failMessage = t('missionReports.colonizeFailedOccupied')
} else if (colonizeResult.failReason === 'maxColoniesReached') {
failMessage = t('missionReports.colonizeFailedMaxColonies')
}
}
// 生成殖民任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -694,24 +1150,72 @@
targetPosition: mission.targetPosition,
targetPlanetId: newPlanet?.id,
targetPlanetName: newPlanet?.name,
success: !!newPlanet,
message: newPlanet ? t('missionReports.colonizeSuccess') : t('missionReports.colonizeFailed'),
success: colonizeResult.success,
message: colonizeResult.success ? t('missionReports.colonizeSuccess') : failMessage,
details: newPlanet
? {
newPlanetId: newPlanet.id,
newPlanetName: newPlanet.name
}
: undefined,
: { failReason: colonizeResult.failReason },
read: false
})
if (newPlanet) {
gameStore.player.planets.push(newPlanet)
}
} else if (mission.missionType === MissionType.Spy) {
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
if (spyReport) gameStore.player.spyReports.push(spyReport)
const spyResult = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
if (spyResult.success && spyResult.report) {
gameStore.player.spyReports.push(spyResult.report)
// 更新成就统计 - 侦查
gameLogic.trackMissionStats(gameStore.player, 'spy')
}
// 生成侦查任务报告(即使失败也生成)
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
}
let spyFailMessage = t('missionReports.spyFailed')
if (!spyResult.success && spyResult.failReason) {
if (spyResult.failReason === 'targetNotFound') {
spyFailMessage = t('missionReports.spyFailedTargetNotFound')
}
}
gameStore.player.missionReports.push({
id: `mission-report-${mission.id}`,
timestamp: Date.now(),
missionType: MissionType.Spy,
originPlanetId: mission.originPlanetId,
originPlanetName,
targetPosition: mission.targetPosition,
targetPlanetId: targetPlanet?.id,
targetPlanetName:
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
success: spyResult.success,
message: spyResult.success ? t('missionReports.spySuccess') : spyFailMessage,
details: spyResult.success ? { spyReportId: spyResult.report?.id } : { failReason: spyResult.failReason },
read: false
})
} else if (mission.missionType === MissionType.Deploy) {
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id)
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies)
// 更新成就统计 - 部署
if (deployed.success) {
gameLogic.trackMissionStats(gameStore.player, 'deploy')
}
// 生成失败原因消息
let deployFailMessage = t('missionReports.deployFailed')
if (!deployed.success && deployed.failReason) {
if (deployed.failReason === 'targetNotFound') {
deployFailMessage = t('missionReports.deployFailedTargetNotFound')
} else if (deployed.failReason === 'notOwnPlanet') {
deployFailMessage = t('missionReports.deployFailedNotOwnPlanet')
}
}
// 生成部署任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -726,14 +1230,15 @@
targetPlanetId: targetPlanet?.id,
targetPlanetName:
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
success: deployed,
message: deployed ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
success: deployed.success,
message: deployed.success ? t('missionReports.deploySuccess') : deployFailMessage,
details: {
deployedFleet: mission.fleet
deployedFleet: mission.fleet,
failReason: deployed.failReason
},
read: false
})
if (deployed) {
if (deployed.success && !deployed.overflow) {
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
return
@@ -744,6 +1249,23 @@
const debrisField = universeStore.debrisFields[debrisId]
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
// 更新成就统计 - 回收(无论是否有残骸都算飞行任务,但只有成功回收才计入回收资源量)
const totalRecycled =
recycleResult.success && recycleResult.collectedResources
? recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal
: 0
gameLogic.trackMissionStats(gameStore.player, 'recycle', { resourcesAmount: totalRecycled })
// 生成失败原因消息
let recycleFailMessage = t('missionReports.recycleFailed')
if (!recycleResult.success && recycleResult.failReason) {
if (recycleResult.failReason === 'noDebrisField') {
recycleFailMessage = t('missionReports.recycleFailedNoDebris')
} else if (recycleResult.failReason === 'debrisEmpty') {
recycleFailMessage = t('missionReports.recycleFailedDebrisEmpty')
}
}
// 生成回收任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -755,18 +1277,18 @@
originPlanetId: mission.originPlanetId,
originPlanetName,
targetPosition: mission.targetPosition,
success: !!recycleResult,
message: recycleResult ? t('missionReports.recycleSuccess') : t('missionReports.recycleFailed'),
details: recycleResult
success: recycleResult.success,
message: recycleResult.success ? t('missionReports.recycleSuccess') : recycleFailMessage,
details: recycleResult.success
? {
recycledResources: recycleResult.collectedResources,
remainingDebris: recycleResult.remainingDebris || undefined
}
: undefined,
: { failReason: recycleResult.failReason },
read: false
})
if (recycleResult && debrisField) {
if (recycleResult.success && recycleResult.collectedResources && debrisField) {
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
// 更新残骸场
universeStore.debrisFields[debrisId] = {
@@ -782,8 +1304,60 @@
}
}
} else if (mission.missionType === MissionType.Destroy) {
// 处理行星毁灭任务
const destroyResult = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
// 处理行星毁灭任务(需要先战斗,再计算毁灭概率)
const destroyResult = await fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player, null, gameStore.player.planets)
// 处理战斗报告(如果发生了战斗)
if (destroyResult.battleResult) {
gameStore.player.battleReports.push(destroyResult.battleResult)
// 处理战斗对NPC的影响
if (targetPlanet) {
const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
if (targetNpc) {
diplomaticLogic.handleAttackReputation(gameStore.player, targetNpc, destroyResult.battleResult, npcStore.npcs, gameStore.locale)
// 同步战斗损失到NPC的实际星球数据
const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id)
if (npcPlanet) {
Object.entries(destroyResult.battleResult.defenderLosses.fleet).forEach(([shipType, lost]) => {
npcPlanet.fleet[shipType as ShipType] = Math.max(0, (npcPlanet.fleet[shipType as ShipType] || 0) - lost)
})
npcPlanet.defense = { ...targetPlanet.defense }
npcPlanet.resources = { ...targetPlanet.resources }
}
}
}
}
// 处理新生成的月球
if (destroyResult.moon) {
gameStore.player.planets.push(destroyResult.moon)
}
// 处理残骸场
if (destroyResult.debrisField) {
universeStore.debrisFields[destroyResult.debrisField.id] = destroyResult.debrisField
}
// 更新成就统计 - 行星毁灭
if (destroyResult.success) {
gameLogic.trackMissionStats(gameStore.player, 'destroy')
}
// 生成失败原因消息
let destroyFailMessage = t('missionReports.destroyFailed')
if (!destroyResult.success && destroyResult.failReason) {
if (destroyResult.failReason === 'targetNotFound') {
destroyFailMessage = t('missionReports.destroyFailedTargetNotFound')
} else if (destroyResult.failReason === 'ownPlanet') {
destroyFailMessage = t('missionReports.destroyFailedOwnPlanet')
} else if (destroyResult.failReason === 'noDeathstar') {
destroyFailMessage = t('missionReports.destroyFailedNoDeathstar')
} else if (destroyResult.failReason === 'chanceFailed') {
destroyFailMessage = t('missionReports.destroyFailedChance', { chance: destroyResult.destructionChance.toFixed(1) })
}
}
// 生成毁灭任务报告
if (!gameStore.player.missionReports) {
@@ -798,20 +1372,49 @@
targetPosition: mission.targetPosition,
targetPlanetId: targetPlanet?.id,
targetPlanetName: targetPlanet?.name,
success: destroyResult?.success || false,
message: destroyResult?.success ? t('missionReports.destroySuccess') : t('missionReports.destroyFailed'),
details: destroyResult?.success
success: destroyResult.success,
message: destroyResult.success ? t('missionReports.destroySuccess') : destroyFailMessage,
details: destroyResult.success
? {
destroyedPlanetName:
targetPlanet?.name ||
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
hadBattle: !!destroyResult.battleResult
}
: undefined,
: {
failReason: destroyResult.failReason,
destructionChance: destroyResult.destructionChance,
deathstarsLost: destroyResult.deathstarsLost,
hadBattle: !!destroyResult.battleResult
},
read: false
})
if (destroyResult && destroyResult.success && destroyResult.planetId) {
if (destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
// 处理外交关系如果目标是NPC星球
if (targetPlanet && targetPlanet.ownerId) {
const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
if (planetOwner) {
diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale)
// 从NPC的星球列表中移除被摧毁的星球
const npcPlanetIndex = planetOwner.planets.findIndex(p => p.id === destroyResult.planetId)
if (npcPlanetIndex > -1) {
planetOwner.planets.splice(npcPlanetIndex, 1)
}
// 检查并处理被消灭的NPC所有星球都被摧毁的NPC
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
// 从npcStore中移除被消灭的NPC
if (eliminatedNpcIds.length > 0) {
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
}
}
}
// 从玩家星球列表中移除(如果是玩家的星球)
const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId)
if (planetIndex > -1) {
@@ -820,6 +1423,127 @@
// 不是玩家星球,从宇宙地图中移除
delete universeStore.planets[targetKey]
}
// 取消所有前往该位置的NPC任务回收、攻击、侦查等
const destroyedDebrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
npcStore.npcs.forEach(npc => {
if (npc.fleetMissions) {
// 找到需要取消的任务前往已摧毁星球位置的outbound任务
const missionsToCancel = npc.fleetMissions.filter(m => {
if (m.status !== 'outbound') return false
// 检查回收任务的残骸场ID
if (m.missionType === MissionType.Recycle && m.debrisFieldId === destroyedDebrisId) {
return true
}
// 检查其他任务的目标星球ID
if (m.targetPlanetId === destroyResult.planetId) {
return true
}
return false
})
// 将这些任务的舰队返回给NPC
missionsToCancel.forEach(m => {
const npcOriginPlanet = npc.planets.find(p => p.id === m.originPlanetId)
if (npcOriginPlanet) {
shipLogic.addFleet(npcOriginPlanet.fleet, m.fleet)
}
})
// 从任务列表中移除这些任务
npc.fleetMissions = npc.fleetMissions.filter(m => !missionsToCancel.includes(m))
}
// 清理关于被摧毁星球的侦查报告
if (npc.playerSpyReports && destroyResult.planetId && destroyResult.planetId in npc.playerSpyReports) {
delete npc.playerSpyReports[destroyResult.planetId]
}
})
// 同时删除该位置的残骸场(星球被摧毁后残骸场也消失)
delete universeStore.debrisFields[destroyedDebrisId]
}
} else if (mission.missionType === MissionType.Expedition) {
// 处理探险任务
const expeditionResult = fleetLogic.processExpeditionArrival(mission)
// 确保返回时间正确设置(兼容旧版本任务数据)
// 如果 returnTime 不存在或已过期,重新计算
const now = Date.now()
if (!mission.returnTime || mission.returnTime <= now) {
// 返回时间应该等于当前时间加上单程飞行时间
const flightDuration = mission.arrivalTime - mission.departureTime
mission.returnTime = now + flightDuration
}
// 更新成就统计 - 探险
const isSuccessful =
expeditionResult.eventType === 'resources' || expeditionResult.eventType === 'darkMatter' || expeditionResult.eventType === 'fleet'
gameLogic.trackMissionStats(gameStore.player, 'expedition', { successful: isSuccessful })
// 生成探险任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
}
// 根据事件类型生成不同的报告消息
let reportMessage = ''
let reportDetails: Record<string, unknown> = {
// 保存探险区域信息
expeditionZone: mission.expeditionZone
}
switch (expeditionResult.eventType) {
case 'resources':
reportMessage = t('missionReports.expeditionResources')
reportDetails.foundResources = expeditionResult.resources
break
case 'darkMatter':
reportMessage = t('missionReports.expeditionDarkMatter')
reportDetails.foundResources = expeditionResult.resources
break
case 'fleet':
reportMessage = t('missionReports.expeditionFleet')
reportDetails.foundFleet = expeditionResult.fleet
break
case 'pirates':
reportMessage = expeditionResult.fleetLost
? t('missionReports.expeditionPiratesAttack')
: t('missionReports.expeditionPiratesEscaped')
if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost
break
case 'aliens':
reportMessage = expeditionResult.fleetLost
? t('missionReports.expeditionAliensAttack')
: t('missionReports.expeditionAliensEscaped')
if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost
break
default:
reportMessage = t('missionReports.expeditionNothing')
}
gameStore.player.missionReports.push({
id: `mission-report-${mission.id}`,
timestamp: Date.now(),
missionType: MissionType.Expedition,
originPlanetId: mission.originPlanetId,
originPlanetName,
targetPosition: mission.targetPosition,
success: expeditionResult.eventType !== 'nothing',
message: reportMessage,
details: reportDetails,
read: false
})
}
// 更新任务状态为返回中
// Deploy任务不需要返回舰队已经留在目标星球
if (mission.missionType !== MissionType.Deploy) {
mission.status = 'returning'
// 确保returnTime已设置如果还没设置的话
if (!mission.returnTime) {
const flightTime = mission.arrivalTime - mission.departureTime
mission.returnTime = Date.now() + flightTime
}
}
}
@@ -848,7 +1572,13 @@
const debrisField = universeStore.debrisFields[debrisId]
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
if (recycleResult && debrisField) {
if (recycleResult && debrisField && recycleResult.collectedResources) {
// 更新成就统计 - 被NPC回收残骸如果残骸是玩家战斗产生的
const totalRecycled = recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal
if (totalRecycled > 0) {
gameLogic.trackDiplomacyStats(gameStore.player, 'debrisRecycledByNPC', { resourcesAmount: totalRecycled })
}
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
// 更新残骸场
universeStore.debrisFields[debrisId] = {
@@ -894,6 +1624,9 @@
// NPC侦查到达
const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player)
// 更新成就统计 - 被NPC侦查
gameLogic.trackDiplomacyStats(gameStore.player, 'spiedByNPC')
// 保存侦查报告到NPC用于后续攻击决策
if (!npc.playerSpyReports) {
npc.playerSpyReports = {}
@@ -912,6 +1645,14 @@
// NPC攻击到达 - 使用专门的NPC攻击处理逻辑
fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets).then(attackResult => {
if (attackResult) {
// 更新成就统计 - 被NPC攻击 + 防御统计
gameLogic.trackDiplomacyStats(gameStore.player, 'attackedByNPC')
const debrisValue = attackResult.debrisField
? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal
: 0
const won = attackResult.battleResult.winner === 'defender'
gameLogic.trackDefenseStats(gameStore.player, attackResult.battleResult, won, debrisValue)
// 添加战斗报告给玩家
gameStore.player.battleReports.push(attackResult.battleResult)
@@ -922,9 +1663,22 @@
// 如果生成残骸场,添加到宇宙残骸场列表
if (attackResult.debrisField) {
const existingDebris = universeStore.debrisFields[attackResult.debrisField.id]
if (existingDebris) {
// 累加残骸资源
universeStore.debrisFields[attackResult.debrisField.id] = {
...existingDebris,
resources: {
metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal,
crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal
}
}
} else {
// 新残骸场
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
}
}
}
// 移除即将到来的警告(攻击已到达)
removeIncomingFleetAlertById(mission.id)
@@ -1010,6 +1764,34 @@
// 应用损失到目标星球
missileLogic.applyMissileAttackResult(targetPlanet, impactResult.defenseLosses)
// 如果目标是NPC的星球同步损失到NPC实际数据并扣除外交好感度
if (targetPlanet.ownerId && targetPlanet.ownerId !== gameStore.player.id) {
const targetNpc = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
if (targetNpc) {
// 同步防御损失到NPC的实际星球数据
const npcPlanet = targetNpc.planets.find(p => p.id === targetPlanet.id)
if (npcPlanet) {
missileLogic.applyMissileAttackResult(npcPlanet, impactResult.defenseLosses)
}
// 导弹攻击扣除好感度
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
const reputationLoss = REPUTATION_CHANGES.ATTACK / 2 // 导弹攻击的好感度惩罚是普通攻击的一半
// 更新NPC对玩家的关系统一使用 npc.relations 作为唯一数据源)
if (!targetNpc.relations) {
targetNpc.relations = {}
}
const npcRelation = diplomaticLogic.getOrCreateRelation(targetNpc.relations, targetNpc.id, gameStore.player.id)
targetNpc.relations[gameStore.player.id] = diplomaticLogic.updateReputation(
npcRelation,
reputationLoss,
DiplomaticEventType.Attack,
t('diplomacy.reports.wasAttackedByMissile')
)
}
}
// 标记导弹攻击为已到达
missileAttack.status = 'arrived'
@@ -1043,13 +1825,9 @@
})
}
// 移除即将到来的舰队警告
const removeIncomingFleetAlert = (alert: IncomingFleetAlert) => {
if (!gameStore.player.incomingFleetAlerts) return
const index = gameStore.player.incomingFleetAlerts.indexOf(alert)
if (index > -1) {
gameStore.player.incomingFleetAlerts.splice(index, 1)
}
// 打开敌方警报面板
const openEnemyAlertPanel = () => {
enemyAlertNotificationsRef.value?.open()
}
const removeIncomingFleetAlertById = (missionId: string) => {
@@ -1060,16 +1838,30 @@
}
}
// NPC成长系统更新函数
let npcUpdateCounter = 0 // 累计秒数
const NPC_UPDATE_INTERVAL = 10 // 每10秒更新一次NPC减少性能开销
/**
* 同步NPC星球数据到universeStore
* 解决npcStore和universeStore数据不同步的问题
*/
const syncNPCPlanetToUniverse = (npc: any) => {
npc.planets.forEach((npcPlanet: any) => {
const planetKey = gameLogic.generatePositionKey(npcPlanet.position.galaxy, npcPlanet.position.system, npcPlanet.position.position)
const universePlanet = universeStore.planets[planetKey]
if (universePlanet) {
// 同步所有关键数据
universePlanet.resources = { ...npcPlanet.resources }
universePlanet.buildings = { ...npcPlanet.buildings }
universePlanet.fleet = { ...npcPlanet.fleet }
universePlanet.defense = { ...npcPlanet.defense }
}
})
}
const updateNPCGrowth = (deltaSeconds: number) => {
// 累积时间
npcUpdateCounter += deltaSeconds
npcUpdateCounter.value += deltaSeconds
// 只在达到更新间隔时才执行
if (npcUpdateCounter < NPC_UPDATE_INTERVAL) {
if (npcUpdateCounter.value < NPC_UPDATE_INTERVAL) {
return
}
@@ -1086,15 +1878,35 @@
// 这是NPC的星球
if (!npcMap.has(planet.ownerId)) {
// 为每个NPC设置随机的初始冷却时间避免所有NPC同时行动
const now = Date.now()
const randomSpyOffset = Math.random() * 240 * 1000 // 0-4分钟的随机延迟
const randomAttackOffset = Math.random() * 480 * 1000 // 0-8分钟的随机延迟
// 初始化NPC与玩家的中立关系
const initialRelations: Record<string, any> = {}
initialRelations[gameStore.player.id] = {
fromId: planet.ownerId,
toId: gameStore.player.id,
reputation: 0,
status: 'neutral' as const,
lastUpdated: now,
history: []
}
npcMap.set(planet.ownerId, {
id: planet.ownerId,
name: `NPC-${planet.ownerId.substring(0, 8)}`,
name: generateNPCName(planet.ownerId, gameStore.locale),
planets: [],
technologies: {}, // 初始化空科技树
difficulty: 'medium' as const, // 默认中等难度
relations: {}, // 外交关系
relations: initialRelations, // 外交关系(默认与玩家中立)
allies: [], // 盟友列表
enemies: [] // 敌人列表
enemies: [], // 敌人列表
lastSpyTime: now - randomSpyOffset, // 设置随机的上次侦查时间
lastAttackTime: now - randomAttackOffset, // 设置随机的上次攻击时间
fleetMissions: [], // 舰队任务
playerSpyReports: {} // 对玩家的侦查报告
})
}
@@ -1104,162 +1916,337 @@
// 保存到store
npcStore.npcs = Array.from(npcMap.values())
// 如果有NPC基于玩家实力初始化NPC
// 如果有NPC基于距离初始化NPC实力
if (npcStore.npcs.length > 0) {
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
const playerPower = npcGrowthLogic.calculatePlayerAveragePower(gameState)
// 获取玩家母星(第一个非月球星球)
const homeworld = gameStore.player.planets.find(p => !p.isMoon)
if (homeworld) {
npcStore.npcs.forEach(npc => {
npcGrowthLogic.initializeNPCStartingPower(npc, playerPower)
// 基于距离初始化NPC实力
npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position)
// 同步NPC星球数据到universeStore
syncNPCPlanetToUniverse(npc)
})
}
// 初始化NPC之间的外交关系盟友/敌人)
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
}
}
// 确保所有NPC都有间谍探测器修复旧版本保存的数据
if (npcStore.npcs.length > 0) {
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
}
// 确保所有NPC都有AI类型修复旧版本保存的数据
if (npcStore.npcs.length > 0) {
npcGrowthLogic.ensureAllNPCsAIType(npcStore.npcs)
}
// 确保所有NPC都与玩家建立了关系修复旧版本保存的数据
if (npcStore.npcs.length > 0) {
const now = Date.now()
// 获取玩家母星(用于计算距离)
const homeworld = gameStore.player.planets.find(p => !p.isMoon)
npcStore.npcs.forEach(npc => {
if (!npc.relations) {
npc.relations = {}
}
// 如果NPC没有与玩家的关系建立中立关系
if (!npc.relations[gameStore.player.id]) {
npc.relations[gameStore.player.id] = {
fromId: npc.id,
toId: gameStore.player.id,
reputation: 0,
status: 'neutral' as const,
lastUpdated: now,
history: []
}
}
// 迁移旧存档如果NPC没有距离数据计算并设置
if (homeworld && npc.distanceToHomeworld === undefined) {
const npcPlanet = npc.planets[0]
if (npcPlanet) {
npc.distanceToHomeworld = npcGrowthLogic.calculateDistanceToHomeworld(npcPlanet.position, homeworld.position)
npc.difficultyLevel = npcGrowthLogic.calculateDifficultyLevel(npc.distanceToHomeworld)
// 重新初始化NPC实力以匹配新的距离难度系统
npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position)
// 同步NPC星球数据到universeStore
syncNPCPlanetToUniverse(npc)
}
}
})
}
// 如果没有NPC直接返回
if (npcStore.npcs.length === 0) {
npcUpdateCounter = 0
npcUpdateCounter.value = 0
return
}
// 构建游戏状态
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
// 获取玩家母星用于距离计算
const homeworldForGrowth = gameStore.player.planets.find(p => !p.isMoon)
// 使用累积的时间更新每个NPC
// 使用累积的时间更新每个NPC(基于距离的成长系统)
npcStore.npcs.forEach(npc => {
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter)
if (homeworldForGrowth) {
npcGrowthLogic.updateNPCGrowthByDistance(npc, homeworldForGrowth.position, npcUpdateCounter.value, gameStore.gameSpeed)
// 同步NPC星球数据到universeStore确保侦查报告显示正确数据
syncNPCPlanetToUniverse(npc)
}
})
// 重置计数器
npcUpdateCounter = 0
npcUpdateCounter.value = 0
}
// NPC行为系统更新函数侦查和攻击决策
let npcBehaviorCounter = 0
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
const updateNPCBehavior = (deltaSeconds: number) => {
// 累积时间
npcBehaviorCounter += deltaSeconds
npcBehaviorCounter.value += deltaSeconds
// 只在达到更新间隔时才执行
if (npcBehaviorCounter < NPC_BEHAVIOR_INTERVAL) {
if (npcBehaviorCounter.value < NPC_BEHAVIOR_INTERVAL) {
return
}
// 如果没有NPC直接返回
if (npcStore.npcs.length === 0) {
npcBehaviorCounter = 0
npcBehaviorCounter.value = 0
return
}
const now = Date.now()
const allPlanets = Object.values(universeStore.planets)
// 合并玩家星球和NPC星球到allPlanetsNPC需要能够侦查和攻击玩家星球
const allPlanets = [...gameStore.player.planets, ...Object.values(universeStore.planets)]
// 更新每个NPC的行为
// 计算当前所有正在进行的侦查和攻击任务数量
let activeSpyMissions = 0
let activeAttackMissions = 0
npcStore.npcs.forEach(npc => {
npcBehaviorLogic.updateNPCBehavior(npc, gameStore.player, allPlanets, universeStore.debrisFields, now)
if (npc.fleetMissions) {
npc.fleetMissions.forEach(mission => {
if (mission.status === 'outbound') {
if (mission.missionType === 'spy') {
activeSpyMissions++
} else if (mission.missionType === 'attack') {
activeAttackMissions++
}
}
})
}
})
npcBehaviorCounter = 0
// 获取并发限制配置
const config = npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points)
// 更新每个NPC的行为随机顺序避免总是优先处理同一批NPC
const shuffledNpcs = [...npcStore.npcs].sort(() => Math.random() - 0.5)
shuffledNpcs.forEach(npc => {
// 在更新前检查当前并发数如果已达上限则跳过该NPC
npcBehaviorLogic.updateNPCBehaviorWithLimit(npc, gameStore.player, allPlanets, universeStore.debrisFields, now, {
activeSpyMissions,
activeAttackMissions,
config
})
// 重新计算当前并发数(因为可能新增了任务)
activeSpyMissions = 0
activeAttackMissions = 0
npcStore.npcs.forEach(n => {
if (n.fleetMissions) {
n.fleetMissions.forEach(mission => {
if (mission.status === 'outbound') {
if (mission.missionType === 'spy') activeSpyMissions++
else if (mission.missionType === 'attack') activeAttackMissions++
}
})
}
})
// 处理增强NPC行为中立和友好NPC的特殊行为
const relation = npc.relations?.[gameStore.player.id]
if (relation?.status === 'neutral') {
const neutralResult = npcBehaviorLogic.updateNeutralNPCBehavior(npc, npcStore.npcs, gameStore.player, now)
// 处理贸易提议
if (neutralResult.tradeOffer) {
if (!gameStore.player.tradeOffers) {
gameStore.player.tradeOffers = []
}
gameStore.player.tradeOffers.push(neutralResult.tradeOffer)
toast.info(t('npcBehavior.tradeOfferReceived'), {
description: t('npcBehavior.tradeOfferDesc', { npcName: neutralResult.tradeOffer.npcName })
})
}
// 游戏循环定时器
let gameLoop: ReturnType<typeof setInterval> | null = null
let konamiCleanup: (() => void) | null = null
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
// 处理态度摇摆
if (neutralResult.swingDirection) {
if (!gameStore.player.attitudeChangeNotifications) {
gameStore.player.attitudeChangeNotifications = []
}
gameStore.player.attitudeChangeNotifications.push({
id: `attitude_${Date.now()}_${npc.id}`,
timestamp: now,
npcId: npc.id,
npcName: npc.name,
previousStatus: 'neutral',
newStatus: neutralResult.swingDirection,
reason: 'attitude_swing',
read: false
})
const statusKey = neutralResult.swingDirection === 'friendly' ? 'npcBehavior.becameFriendly' : 'npcBehavior.becameHostile'
toast.info(t('npcBehavior.attitudeChanged'), {
description: t(statusKey, { npcName: npc.name })
})
}
} else if (relation?.status === 'friendly') {
const friendlyResult = npcBehaviorLogic.updateFriendlyNPCBehavior(npc, npcStore.npcs, gameStore.player, now)
// 处理情报报告
if (friendlyResult.intelReport) {
if (!gameStore.player.intelReports) {
gameStore.player.intelReports = []
}
gameStore.player.intelReports.push(friendlyResult.intelReport)
toast.info(t('npcBehavior.intelReceived'), {
description: t('npcBehavior.intelReceivedDesc', { npcName: friendlyResult.intelReport.fromNpcName })
})
}
// 处理联合攻击邀请
if (friendlyResult.jointAttackInvite) {
if (!gameStore.player.jointAttackInvites) {
gameStore.player.jointAttackInvites = []
}
gameStore.player.jointAttackInvites.push(friendlyResult.jointAttackInvite)
toast.info(t('npcBehavior.jointAttackInvite'), {
description: t('npcBehavior.jointAttackInviteDesc', { npcName: friendlyResult.jointAttackInvite.fromNpcName })
})
}
// 处理资源援助
if (friendlyResult.aidProvided) {
if (!gameStore.player.aidNotifications) {
gameStore.player.aidNotifications = []
}
gameStore.player.aidNotifications.push({
id: `aid_${Date.now()}_${npc.id}`,
timestamp: now,
npcId: npc.id,
npcName: npc.name,
aidResources: friendlyResult.aidProvided,
read: false
})
const totalAid = friendlyResult.aidProvided.metal + friendlyResult.aidProvided.crystal + friendlyResult.aidProvided.deuterium
toast.success(t('npcBehavior.aidReceived'), {
description: t('npcBehavior.aidReceivedDesc', { npcName: npc.name, amount: totalAid.toLocaleString() })
})
}
}
})
npcBehaviorCounter.value = 0
}
// 更新NPC关系统计友好/敌对数量)
const updateNPCRelationStats = () => {
let friendlyCount = 0
let hostileCount = 0
const playerId = gameStore.player.id
npcStore.npcs.forEach(npc => {
const relation = npc.relations?.[playerId]
if (relation) {
const status = diplomaticLogic.calculateRelationStatus(relation.reputation)
if (status === 'friendly') {
friendlyCount++
} else if (status === 'hostile') {
hostileCount++
}
}
})
gameLogic.trackDiplomacyStats(gameStore.player, 'updateRelations', { friendlyCount, hostileCount })
}
// 检查成就解锁
const achievementCheckCounter = ref(0)
const ACHIEVEMENT_CHECK_INTERVAL = 5 // 每5秒检查一次成就
const checkAchievementUnlocks = () => {
achievementCheckCounter.value += 1
// 只在达到更新间隔时才执行
if (achievementCheckCounter.value < ACHIEVEMENT_CHECK_INTERVAL) {
return
}
// 更新NPC关系统计
updateNPCRelationStats()
// 检查并解锁成就
const unlocks = gameLogic.checkAndUnlockAchievements(gameStore.player)
// 显示成就解锁通知(奖励已在 checkAndUnlockAchievements 中应用)
unlocks.forEach(unlock => {
// 显示 toast 通知
const tierName = t(`achievements.tiers.${unlock.tier}`)
const achievementName = t(`achievements.names.${unlock.id}`)
toast.success(t('achievements.unlocked'), {
description: `${achievementName} (${tierName})`
})
})
achievementCheckCounter.value = 0
}
// 启动游戏循环
const startGameLoop = () => {
if (gameStore.isPaused) return
// 清理旧的定时器
if (gameLoop) {
clearInterval(gameLoop)
if (gameLoop.value) {
clearInterval(gameLoop.value)
}
// 根据游戏速度计算间隔时间
const interval = 1000 / (gameStore.gameSpeed || 1)
// 游戏循环固定为1秒避免高倍速时的卡顿
// gameSpeed 只作用于资源产出和时间消耗的倍率
const interval = 1000
// 启动新的游戏循环
gameLoop = setInterval(() => {
gameLoop.value = setInterval(() => {
updateGame()
}, interval)
}
// 监听游戏速度变化,重新启动游戏循环
watch(
() => gameStore.gameSpeed,
() => {
if (gameLoop) {
startGameLoop()
// 停止游戏循环
const stopGameLoop = () => {
if (gameLoop.value) {
clearInterval(gameLoop.value)
gameLoop.value = null
}
}
)
// 初始化游戏
onMounted(async () => {
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
const isFirstVisit = gameStore.player.planets.length === 0
if (isFirstVisit) {
gameStore.locale = detectBrowserLocale()
// 启动积分更新定时器每10秒更新一次
const startPointsUpdate = () => {
if (pointsUpdateInterval.value) {
clearInterval(pointsUpdateInterval.value)
}
await initGame()
// 启动游戏循环
startGameLoop()
// 启动科乐美秘籍监听
konamiCleanup = setupKonamiCode()
// 首次检查版本(被动检测)
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
})
if (versionInfo) {
updateInfo.value = versionInfo
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
duration: Infinity,
dismissible: true,
action: {
label: t('settings.viewUpdate'),
onClick: () => {
showUpdateDialog.value = true
pointsUpdateInterval.value = setInterval(() => {
if (!gameStore.isPaused) {
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
}
}, 10000) // 10秒更新一次
}
})
}
// 启动版本检查定时器每5分钟被动检查一次
versionCheckInterval = setInterval(async () => {
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
})
if (versionInfo) {
updateInfo.value = versionInfo
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
duration: Infinity,
dismissible: true,
action: {
label: t('settings.viewUpdate'),
onClick: () => {
showUpdateDialog.value = true
}
}
})
}
}, 5 * 60 * 1000)
})
// 清理定时器
onUnmounted(() => {
if (gameLoop) clearInterval(gameLoop)
if (konamiCleanup) konamiCleanup()
if (versionCheckInterval) clearInterval(versionCheckInterval)
})
// 处理取消建造事件
const handleCancelBuildEvent = (event: CustomEvent) => {
handleCancelBuild(event.detail)
}
// 处理取消研究事件
const handleCancelResearchEvent = (event: CustomEvent) => {
handleCancelResearch(event.detail)
}
// 科乐美秘籍上上下下左左右右BA
const setupKonamiCode = () => {
@@ -1292,96 +2279,42 @@
}
}
// 定义 planet computed需要在 watch 之前定义)
const planet = computed(() => gameStore.currentPlanet)
// 打开重命名对话框
const openRenameDialog = (planetId: string, currentName: string) => {
renamingPlanetId.value = planetId
newPlanetName.value = currentName
renameDialogOpen.value = true
}
const navItems = computed(() => [
{ name: computed(() => t('nav.overview')), path: '/', icon: Home },
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
// GM菜单在启用GM模式时显示
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
])
// 确认重命名
const confirmRenamePlanet = () => {
if (!renamingPlanetId.value || !newPlanetName.value.trim()) return
// 使用直接计算,不再缓存
const production = computed(() => {
if (!planet.value) return null
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
return resourceLogic.calculateResourceProduction(planet.value, {
resourceProductionBonus: bonuses.resourceProductionBonus,
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
energyProductionBonus: bonuses.energyProductionBonus
})
})
const targetPlanet = gameStore.player.planets.find(p => p.id === renamingPlanetId.value)
if (targetPlanet) {
targetPlanet.name = newPlanetName.value.trim()
}
const capacity = computed(() => {
if (!planet.value) return null
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
})
renameDialogOpen.value = false
renamingPlanetId.value = null
newPlanetName.value = ''
}
// 电力消耗
const energyConsumption = computed(() => {
if (!planet.value) return 0
return resourceLogic.calculateEnergyConsumption(planet.value)
})
// 净电力(产量 - 消耗)
const netEnergy = computed(() => {
if (!planet.value || !production.value) return 0
return production.value.energy - energyConsumption.value
})
// 未读消息数量
const unreadMessagesCount = computed(() => {
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
return unreadBattles + unreadSpies + unreadSpied + unreadMissions + unreadNPCActivity + unreadGifts + unreadGiftRejected
})
// 正在执行的舰队任务数量(包括飞行中的导弹)
const activeFleetMissionsCount = computed(() => {
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
return fleetMissions + flyingMissiles
})
// 资源类型配置
const resourceTypes = [
{ key: 'metal' as const },
{ key: 'crystal' as const },
{ key: 'deuterium' as const },
{ key: 'energy' as const },
{ key: 'darkMatter' as const }
]
// 月球相关
const moon = computed(() => {
if (!planet.value || planet.value.isMoon) return null
return gameStore.getMoonForPlanet(planet.value.id)
})
const hasMoon = computed(() => !!moon.value)
// 检查功能是否解锁
const isFeatureUnlocked = (path: string): boolean => {
const requirement = featureRequirements[path]
if (!requirement) {
return true
}
const currentLevel = planet.value?.buildings[requirement.building] || 0
return currentLevel >= requirement.level
}
// 切换到月球
const switchToMoon = () => {
if (moon.value) {
gameStore.currentPlanetId = moon.value.id
router.push('/')
}
}
@@ -1389,12 +2322,14 @@
const switchToParentPlanet = () => {
if (planet.value?.parentPlanetId) {
gameStore.currentPlanetId = planet.value.parentPlanetId
router.push('/')
}
}
// 切换到指定星球
const switchToPlanet = (planetId: string) => {
gameStore.currentPlanetId = planetId
router.push('/')
}
// 切换侧边栏
@@ -1402,33 +2337,9 @@
sidebarOpen.value = !sidebarOpen.value
}
// 获取队列项的名称
const getItemName = (item: BuildQueueItem): string => {
if (item.type === 'building' || item.type === 'demolish') {
const buildingName = t(`buildings.${item.itemType}`)
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
} else if (item.type === 'technology') {
return t(`technologies.${item.itemType}`)
} else if (item.type === 'ship') {
return t(`ships.${item.itemType}`)
} else if (item.type === 'defense') {
return t(`defenses.${item.itemType}`)
}
return item.itemType
}
// 获取剩余时间
const getRemainingTime = (item: BuildQueueItem): number => {
const now = Date.now()
return Math.max(0, Math.floor((item.endTime - now) / 1000))
}
// 获取队列进度
const getQueueProgress = (item: BuildQueueItem): number => {
const now = Date.now()
const total = item.endTime - item.startTime
const elapsed = now - item.startTime
return Math.min(100, Math.max(0, (elapsed / total) * 100))
// 处理侧边栏打开/关闭状态变化
const handleSidebarOpenChange = (open: boolean) => {
sidebarOpen.value = open
}
// 取消建造
@@ -1466,6 +2377,141 @@
}
confirmDialogOpen.value = true
}
// 监听暂停状态变化
watch(
() => gameStore.isPaused,
isPaused => {
if (isPaused) {
stopGameLoop()
} else {
startGameLoop()
}
}
)
// 初始化游戏
onMounted(async () => {
try {
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
const isFirstVisit = gameStore.player.planets.length === 0
if (isFirstVisit) {
gameStore.locale = detectBrowserLocale()
}
await initGame()
// 启动游戏循环
startGameLoop()
// 启动积分更新定时器
startPointsUpdate()
// 启动科乐美秘籍监听
konamiCleanup.value = setupKonamiCode()
// 添加队列取消事件监听
window.addEventListener('cancel-build', handleCancelBuildEvent as EventListener)
window.addEventListener('cancel-research', handleCancelResearchEvent as EventListener)
// 首次检查版本(被动检测)
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
})
if (versionInfo) {
updateInfo.value = versionInfo
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
duration: Infinity,
dismissible: true,
action: {
label: t('settings.viewUpdate'),
onClick: () => {
showUpdateDialog.value = true
}
}
})
}
// 检测旧格式 NPC 名称
if (npcStore.npcs.length > 0) {
const oldCount = countOldFormatNPCs(npcStore.npcs, gameStore.locale)
if (oldCount > 0) {
oldFormatNPCCount.value = oldCount
npcNameUpdateDialogOpen.value = true
}
}
// Android 返回键退出确认
if (Capacitor.isNativePlatform()) {
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
router.back()
} else {
exitDialogOpen.value = true
}
})
}
// 启动版本检查定时器每5分钟被动检查一次
versionCheckInterval.value = setInterval(async () => {
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
gameStore.player.lastVersionCheckTime = time
})
if (versionInfo) {
updateInfo.value = versionInfo
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
duration: Infinity,
dismissible: true,
action: {
label: t('settings.viewUpdate'),
onClick: () => {
showUpdateDialog.value = true
}
}
})
}
}, 5 * 60 * 1000)
} catch (error) {
console.error('Error during game initialization:', error)
// 即使初始化失败,也尝试启动基本的游戏循环
startGameLoop()
}
})
// 清理定时器
onUnmounted(() => {
if (gameLoop.value) clearInterval(gameLoop.value)
if (pointsUpdateInterval.value) clearInterval(pointsUpdateInterval.value)
if (konamiCleanup.value) konamiCleanup.value()
if (versionCheckInterval.value) clearInterval(versionCheckInterval.value)
// 移除队列取消事件监听
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
// 移除 Android 返回键监听
if (Capacitor.isNativePlatform()) {
CapacitorApp.removeAllListeners()
}
})
// Android 退出应用
const exitApp = () => {
CapacitorApp.exitApp()
}
// NPC 名称更新处理
const handleUpdateNPCNames = () => {
let updatedCount = 0
npcStore.npcs.forEach(npc => {
const newName = updateNPCName(npc.id, gameStore.locale)
if (newName !== npc.name) {
npc.name = newName
updatedCount++
}
})
npcNameUpdateDialogOpen.value = false
toast.success(t('settings.npcNameUpdateSuccess', { count: updatedCount }))
}
const handleSkipNPCNameUpdate = () => {
npcNameUpdateDialogOpen.value = false
toast.info(t('settings.npcNameUpdateSkipped'))
}
</script>
<style scoped>

90
src/assets/main.css Normal file
View File

@@ -0,0 +1,90 @@
@custom-variant dark (&:is(.dark *));
:root {
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.141 0.005 285.823);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
}
/* Theme variables are defined in style.css */
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
html {
color-scheme: light dark;
}
html.dark {
color-scheme: dark;
}
html.light {
color-scheme: light;
}
/* 队列添加动画 - 脉冲效果 */
@keyframes queue-pulse-animation {
0% {
transform: scale3d(1, 1, 1);
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
}
50% {
transform: scale3d(1.1, 1.1, 1);
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
}
100% {
transform: scale3d(1, 1, 1);
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
}
}
.queue-pulse {
animation: queue-pulse-animation 0.3s ease-out;
}

View File

@@ -1,337 +0,0 @@
<template>
<Dialog v-model:open="isOpen">
<ScrollableDialogContent container-class="sm:max-w-4xl max-h-[90vh]">
<template #header>
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Trophy class="h-5 w-5" />
{{ t('messagesView.battleReport') }}
</DialogTitle>
<DialogDescription v-if="report">
{{ formatDate(report.timestamp) }}
</DialogDescription>
</DialogHeader>
</template>
<div v-if="report" class="space-y-4">
<!-- 战斗双方信息 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<!-- 攻击方星球 -->
<div class="p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg">
<p class="font-medium text-blue-600 dark:text-blue-400 mb-1">{{ t('simulatorView.attacker') }}</p>
<p v-if="attackerPlanet" class="text-xs text-muted-foreground">
{{ attackerPlanet.name }} [{{ attackerPlanet.position.galaxy }}:{{ attackerPlanet.position.system }}:{{
attackerPlanet.position.position
}}]
</p>
<p v-else class="text-xs text-muted-foreground">{{ report.attackerPlanetId }}</p>
</div>
<!-- 防守方星球 -->
<div class="p-3 bg-red-50 dark:bg-red-950/20 rounded-lg">
<p class="font-medium text-red-600 dark:text-red-400 mb-1">{{ t('simulatorView.defender') }}</p>
<p v-if="defenderPlanet" class="text-xs text-muted-foreground">
{{ defenderPlanet.name }} [{{ defenderPlanet.position.galaxy }}:{{ defenderPlanet.position.system }}:{{
defenderPlanet.position.position
}}]
</p>
<p v-else class="text-xs text-muted-foreground">{{ report.defenderPlanetId }}</p>
</div>
</div>
<!-- 胜利者 -->
<div class="text-center p-4 rounded-lg" :class="getWinnerStyle(report.winner)">
<p class="text-lg font-bold">
{{
report.winner === 'attacker'
? t('messagesView.victory')
: report.winner === 'defender'
? t('messagesView.defeat')
: t('messagesView.draw')
}}
</p>
<p v-if="report.rounds" class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}</p>
</div>
<!-- 损失对比 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 攻击方损失 -->
<div class="space-y-2">
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('messagesView.attackerLosses') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.attackerLosses" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p v-if="Object.keys(report.attackerLosses).length === 0" class="text-muted-foreground">
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
<!-- 防守方损失 -->
<div class="space-y-2">
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('messagesView.defenderLosses') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.defenderLosses.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<div v-for="(count, defenseType) in report.defenderLosses.defense" :key="defenseType">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<p
v-if="Object.keys(report.defenderLosses.fleet).length === 0 && Object.keys(report.defenderLosses.defense).length === 0"
class="text-muted-foreground"
>
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
</div>
<!-- 剩余单位 -->
<div v-if="report.attackerRemaining || report.defenderRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 攻击方剩余 -->
<div v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0" class="space-y-2">
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.attackerRemaining') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.attackerRemaining" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防守方剩余 -->
<div
v-if="
report.defenderRemaining &&
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
Object.keys(report.defenderRemaining.defense || {}).length > 0)
"
class="space-y-2"
>
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
<div v-for="(count, shipType) in report.defenderRemaining.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
<div v-for="(count, defenseType) in report.defenderRemaining.defense" :key="defenseType">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-2 font-medium">{{ count }}</span>
</div>
</div>
</div>
</div>
<!-- 战利品和残骸 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 掠夺资源 -->
<div
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
>
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('messagesView.plunder') }}</p>
<div class="flex flex-wrap gap-3 text-xs">
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.plunder.metal) }}
</span>
<span v-if="report.plunder.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.plunder.crystal) }}
</span>
<span v-if="report.plunder.deuterium > 0" class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(report.plunder.deuterium) }}
</span>
</div>
</div>
<!-- 残骸场 -->
<div
v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)"
class="p-3 bg-muted rounded-lg"
>
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}</p>
<div class="flex flex-wrap gap-3 text-xs">
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.debrisField.metal) }}
</span>
<span v-if="report.debrisField.crystal > 0" class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.debrisField.crystal) }}
</span>
</div>
<!-- 月球生成概率 -->
<p v-if="report.moonChance && report.moonChance > 0" class="text-xs text-muted-foreground mt-2">
{{ t('messagesView.moonChance') }}: {{ (report.moonChance * 100).toFixed(1) }}%
</p>
</div>
</div>
<!-- 回合详情 -->
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-2">
<Button @click="showRoundDetails = !showRoundDetails" variant="outline" size="sm" class="w-full">
{{ showRoundDetails ? t('messagesView.hideRoundDetails') : t('messagesView.showRoundDetails') }}
</Button>
<div v-if="showRoundDetails" class="relative pl-6 space-y-4">
<!-- 时间线 -->
<div class="absolute left-2 top-0 bottom-0 w-0.5 bg-border" />
<div v-for="detail in report.roundDetails" :key="detail.round" class="relative">
<!-- 时间线节点 -->
<div class="absolute -left-6 top-3 w-4 h-4 rounded-full bg-primary border-2 border-background" />
<!-- 回合内容卡片 -->
<div class="border rounded-lg p-3 bg-card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-semibold">{{ t('messagesView.round').replace('{round}', String(detail.round)) }}</p>
<TooltipProvider :delay-duration="300">
<div class="flex gap-3 text-xs text-muted-foreground">
<Tooltip>
<TooltipTrigger as-child>
<span class="flex items-center gap-1">
<Sword class="h-3 w-3" />
{{ formatNumber(detail.attackerRemainingPower) }}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('messagesView.attackerRemainingPower') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<span class="flex items-center gap-1">
<Shield class="h-3 w-3" />
{{ formatNumber(detail.defenderRemainingPower) }}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('messagesView.defenderRemainingPower') }}</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- 攻击方本回合损失 -->
<div class="bg-red-50 dark:bg-red-950/20 rounded p-2">
<p class="text-xs font-medium text-red-600 dark:text-red-400 mb-1.5">{{ t('messagesView.attackerLosses') }}</p>
<div class="text-xs space-y-0.5">
<div v-for="(count, shipType) in detail.attackerLosses" :key="shipType" class="flex justify-between">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<p v-if="Object.keys(detail.attackerLosses).length === 0" class="text-muted-foreground italic">
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
<!-- 防守方本回合损失 -->
<div class="bg-blue-50 dark:bg-blue-950/20 rounded p-2">
<p class="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1.5">{{ t('messagesView.defenderLosses') }}</p>
<div class="text-xs space-y-0.5">
<div v-for="(count, shipType) in detail.defenderLosses.fleet" :key="shipType" class="flex justify-between">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<div v-for="(count, defenseType) in detail.defenderLosses.defense" :key="defenseType" class="flex justify-between">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}</span>
<span class="font-medium">-{{ count }}</span>
</div>
<p
v-if="
Object.keys(detail.defenderLosses.fleet).length === 0 && Object.keys(detail.defenderLosses.defense).length === 0
"
class="text-muted-foreground italic"
>
{{ t('messagesView.noLosses') }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</ScrollableDialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useUniverseStore } from '@/stores/universeStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { formatNumber, formatDate } from '@/utils/format'
import { Trophy, Sword, Shield } from 'lucide-vue-next'
import type { BattleResult } from '@/types/game'
const props = defineProps<{
report: BattleResult | null
open: boolean
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()
const gameStore = useGameStore()
const universeStore = useUniverseStore()
const { t } = useI18n()
const { SHIPS, DEFENSES } = useGameConfig()
const isOpen = ref(props.open)
const showRoundDetails = ref(false)
// 获取攻击方星球信息
const attackerPlanet = computed(() => {
if (!props.report) return null
return gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
})
// 获取防守方星球信息
const defenderPlanet = computed(() => {
if (!props.report) return null
// 先从玩家星球中查找
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
if (playerPlanet) return playerPlanet
// 再从宇宙星球地图中查找
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
})
watch(
() => props.open,
newValue => {
isOpen.value = newValue
if (newValue) {
showRoundDetails.value = false
}
}
)
watch(isOpen, newValue => {
emit('update:open', newValue)
})
// 获取胜利者样式
const getWinnerStyle = (winner: string) => {
if (winner === 'attacker') return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
if (winner === 'defender') return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
}
</script>

View File

@@ -1,101 +0,0 @@
<template>
<div v-if="alerts.length > 0" class="bg-destructive/10 border-b border-destructive/20">
<div class="px-4 sm:px-6 py-2 space-y-2">
<div
v-for="alert in alerts"
:key="alert.id"
class="flex items-center justify-between gap-3 bg-destructive/5 rounded-lg px-3 py-2 border border-destructive/20"
>
<!-- 警告图标和信息 -->
<div class="flex items-center gap-2 flex-1 min-w-0">
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-destructive truncate">
<template v-if="alert.missionType === 'spy'">
{{ t('alerts.npcSpyIncoming') }}
</template>
<template v-else-if="alert.missionType === 'attack'">
{{ t('alerts.npcAttackIncoming') }}
</template>
<template v-else>
{{ t('alerts.npcFleetIncoming') }}
</template>
</p>
<p class="text-xs text-muted-foreground truncate">
{{ alert.npcName }} {{ alert.targetPlanetName }}
<template v-if="alert.missionType === 'attack'">({{ alert.fleetSize }} {{ t('alerts.ships') }})</template>
</p>
</div>
</div>
<!-- 倒计时 -->
<div class="flex items-center gap-2 flex-shrink-0">
<div class="text-right">
<p class="text-xs font-mono text-destructive">
{{ formatTimeRemaining(alert.arrivalTime) }}
</p>
<p class="text-[10px] text-muted-foreground">
{{ formatTime(alert.arrivalTime) }}
</p>
</div>
<Button @click="markAsRead(alert)" variant="ghost" size="sm" class="h-6 w-6 p-0">
<X class="h-3 w-3" />
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import type { IncomingFleetAlert } from '@/types/game'
import { Button } from '@/components/ui/button'
import { AlertTriangle, X } from 'lucide-vue-next'
import { useI18n } from '@/composables/useI18n'
const props = defineProps<{
alerts: IncomingFleetAlert[]
}>()
const emit = defineEmits<{
(e: 'markAsRead', alert: IncomingFleetAlert): void
}>()
const { t } = useI18n()
// 强制更新倒计时
const now = ref(Date.now())
let updateInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
updateInterval = setInterval(() => {
now.value = Date.now()
}, 1000)
})
onUnmounted(() => {
if (updateInterval) clearInterval(updateInterval)
})
const formatTimeRemaining = (arrivalTime: number): string => {
const remaining = Math.max(0, arrivalTime - now.value)
const seconds = Math.floor((remaining / 1000) % 60)
const minutes = Math.floor((remaining / (1000 * 60)) % 60)
const hours = Math.floor(remaining / (1000 * 60 * 60))
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
return `${minutes}:${String(seconds).padStart(2, '0')}`
}
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
const markAsRead = (alert: IncomingFleetAlert) => {
emit('markAsRead', alert)
}
</script>

View File

@@ -1,232 +0,0 @@
<template>
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div class="flex-1">
<CardTitle class="flex items-center gap-2">
{{ npc.name }}
<Badge :variant="statusBadgeVariant">
{{ statusText }}
</Badge>
</CardTitle>
<CardDescription class="mt-1">
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
<span v-if="npc.allies && npc.allies.length > 0" class="ml-2">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent class="space-y-4">
<!-- 好感度进度条 -->
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}</span>
<span class="font-semibold" :class="reputationColor">{{ reputation > 0 ? '+' : '' }}{{ reputation }}</span>
</div>
<div class="relative">
<!-- 背景进度条 -->
<div class="h-2 bg-muted rounded-full overflow-hidden">
<!-- 负值部分左侧红色 -->
<div
v-if="reputation < 0"
class="h-full bg-red-500 dark:bg-red-600 absolute right-1/2"
:style="{ width: `${Math.abs(reputation) / 2}%` }"
/>
<!-- 正值部分右侧绿色 -->
<div
v-if="reputation > 0"
class="h-full bg-green-500 dark:bg-green-600 absolute left-1/2"
:style="{ width: `${reputation / 2}%` }"
/>
</div>
<!-- 中心线 -->
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-border" />
</div>
<div class="flex justify-between text-xs text-muted-foreground">
<span>-100</span>
<span>0</span>
<span>+100</span>
</div>
</div>
<!-- 盟友信息 -->
<div v-if="npc.allies && npc.allies.length > 0" class="pt-2 border-t">
<p class="text-sm text-muted-foreground mb-2">{{ t('diplomacy.alliedWith') }}:</p>
<div class="flex flex-wrap gap-1">
<Badge v-for="allyId in npc.allies.slice(0, 3)" :key="allyId" variant="outline" class="text-xs">
{{ getAllyName(allyId) }}
</Badge>
<Badge v-if="npc.allies.length > 3" variant="outline" class="text-xs">
+{{ npc.allies.length - 3 }} {{ t('diplomacy.more') }}
</Badge>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-2 pt-2">
<Button size="sm" variant="outline" class="flex-1" @click="handleGiftResources">
<Gift class="h-4 w-4 mr-2" />
{{ t('diplomacy.actions.gift') }}
</Button>
<Button size="sm" variant="outline" class="flex-1" @click="handleViewPlanets">
<Globe class="h-4 w-4 mr-2" />
{{ t('diplomacy.actions.viewPlanets') }}
</Button>
</div>
<!-- 最近活动 -->
<div v-if="recentEvent" class="pt-2 border-t">
<p class="text-xs text-muted-foreground mb-1">{{ t('diplomacy.lastEvent') }}:</p>
<div class="flex items-center gap-2 text-xs">
<component :is="getEventIcon(recentEvent.reason)" class="h-3 w-3" />
<span>{{ getEventText(recentEvent.reason) }}</span>
<span class="text-muted-foreground">{{ formatTime(Date.now() - recentEvent.timestamp) }} {{ t('diplomacy.ago') }}</span>
</div>
</div>
</CardContent>
</Card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Gift, Globe, Sword, Eye, Trash2 } from 'lucide-vue-next'
import { RelationStatus, DiplomaticEventType } from '@/types/game'
import type { DiplomaticRelation, NPC } from '@/types/game'
import { formatTime } from '@/utils/format'
const props = defineProps<{
npc: NPC
relation?: DiplomaticRelation
}>()
const router = useRouter()
const npcStore = useNPCStore()
const { t } = useI18n()
// 好感度值
const reputation = computed(() => props.relation?.reputation || 0)
// 关系状态
const status = computed(() => props.relation?.status || RelationStatus.Neutral)
// 关系状态文本
const statusText = computed(() => {
switch (status.value) {
case RelationStatus.Friendly:
return t('diplomacy.status.friendly')
case RelationStatus.Hostile:
return t('diplomacy.status.hostile')
default:
return t('diplomacy.status.neutral')
}
})
// 关系状态Badge样式
const statusBadgeVariant = computed(() => {
switch (status.value) {
case RelationStatus.Friendly:
return 'default'
case RelationStatus.Hostile:
return 'destructive'
default:
return 'secondary'
}
})
// 好感度颜色
const reputationColor = computed(() => {
if (reputation.value >= 20) return 'text-green-600 dark:text-green-400'
if (reputation.value <= -20) return 'text-red-600 dark:text-red-400'
return 'text-muted-foreground'
})
// 最近的外交事件
const recentEvent = computed(() => {
if (!props.relation?.history || props.relation.history.length === 0) return null
return props.relation.history[props.relation.history.length - 1]
})
// 获取盟友名称
const getAllyName = (allyId: string) => {
const ally = npcStore.npcs.find(n => n.id === allyId)
return ally?.name || allyId.substring(0, 8)
}
// 获取事件图标
const getEventIcon = (eventType: string) => {
switch (eventType) {
case DiplomaticEventType.GiftResources:
return Gift
case DiplomaticEventType.Attack:
case DiplomaticEventType.AllyAttacked:
return Sword
case DiplomaticEventType.Spy:
return Eye
case DiplomaticEventType.StealDebris:
return Trash2
default:
return Gift
}
}
// 获取事件文本
const getEventText = (eventType: string) => {
switch (eventType) {
case DiplomaticEventType.GiftResources:
return t('diplomacy.events.gift')
case DiplomaticEventType.Attack:
return t('diplomacy.events.attack')
case DiplomaticEventType.AllyAttacked:
return t('diplomacy.events.allyAttacked')
case DiplomaticEventType.Spy:
return t('diplomacy.events.spy')
case DiplomaticEventType.StealDebris:
return t('diplomacy.events.stealDebris')
default:
return eventType
}
}
// 赠送资源
const handleGiftResources = () => {
// 跳转到舰队页面自动选择第一个NPC星球
if (props.npc.planets.length > 0) {
const targetPlanet = props.npc.planets[0]
if (!targetPlanet) return
router.push({
path: '/fleet',
query: {
galaxy: targetPlanet.position.galaxy,
system: targetPlanet.position.system,
position: targetPlanet.position.position,
gift: '1'
}
})
}
}
// 查看星球
const handleViewPlanets = () => {
// 跳转到星系视图定位到第一个NPC星球并传递NPC ID用于高亮
if (props.npc.planets.length > 0) {
const targetPlanet = props.npc.planets[0]
if (!targetPlanet) return
router.push({
path: '/galaxy',
query: {
galaxy: targetPlanet.position.galaxy,
system: targetPlanet.position.system,
highlightNpc: props.npc.id
}
})
}
}
</script>

View File

@@ -1,25 +0,0 @@
<template>
<Popover>
<PopoverTrigger as-child>
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ formatNumber(value, 1) }}</span>
</PopoverTrigger>
<PopoverContent class="w-auto p-2" side="top" align="center">
<p class="font-mono text-sm">{{ formattedValue }}</p>
</PopoverContent>
</Popover>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { formatNumber } from '@/utils/format'
const props = defineProps<{
value: number
}>()
// 完整格式化的数字(带千位分隔符)
const formattedValue = computed(() => {
return props.value.toLocaleString()
})
</script>

View File

@@ -1,28 +0,0 @@
<template>
<div :class="[colors[type], sizes[size], 'rounded shadow-sm']" />
</template>
<script setup lang="ts">
interface Props {
type: 'metal' | 'crystal' | 'deuterium' | 'darkMatter' | 'energy'
size?: 'sm' | 'md' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
size: 'md'
})
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'
}
const sizes = {
sm: 'w-3 h-3',
md: 'w-4 h-4',
lg: 'w-5 h-5'
}
</script>

View File

@@ -1,129 +0,0 @@
<template>
<Dialog v-model:open="isOpen">
<ScrollableDialogContent container-class="sm:max-w-2xl max-h-[90vh]">
<template #header>
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Eye class="h-5 w-5" />
{{ t('messagesView.spyReport') }}
</DialogTitle>
<DialogDescription v-if="report">
{{ formatDate(report.timestamp) }}
</DialogDescription>
</DialogHeader>
</template>
<div v-if="report" class="space-y-4">
<!-- 目标星球信息 -->
<div class="p-3 bg-muted rounded-lg">
<p class="text-sm font-medium mb-2">{{ t('messagesView.targetPlanet') }}</p>
<p class="text-xs text-muted-foreground">
{{ report.targetPlanetName }} [{{ report.targetPosition.galaxy }}:{{ report.targetPosition.system }}:{{
report.targetPosition.position
}}]
</p>
</div>
<!-- 资源 -->
<div>
<p class="text-sm font-medium mb-2">{{ t('messagesView.resources') }}:</p>
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
<span class="flex items-center gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(report.resources.metal) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(report.resources.crystal) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(report.resources.deuterium) }}
</span>
<span class="flex items-center gap-1">
<ResourceIcon type="darkMatter" size="sm" />
{{ formatNumber(report.resources.darkMatter) }}
</span>
</div>
</div>
<!-- 舰队如果有 -->
<div v-if="report.fleet && Object.keys(report.fleet).length > 0">
<p class="text-sm font-medium mb-2">{{ t('messagesView.fleet') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
<div v-for="(count, shipType) in report.fleet" :key="shipType">
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</div>
</div>
</div>
<!-- 防御设施如果有 -->
<div v-if="report.defense && hasDefense(report.defense)">
<p class="text-sm font-medium mb-2">{{ t('messagesView.defense') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
<div v-for="(count, defenseType) in report.defense" :key="defenseType">
<span v-if="count && count > 0" class="block">
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
<span class="ml-1 font-medium">{{ count }}</span>
</span>
</div>
</div>
</div>
<!-- 建筑如果有 -->
<div v-if="report.buildings && Object.keys(report.buildings).length > 0">
<p class="text-sm font-medium mb-2">{{ t('messagesView.buildings') }}:</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
<div v-for="(level, buildingType) in report.buildings" :key="buildingType">
<span class="text-muted-foreground">{{ BUILDINGS[buildingType].name }}:</span>
<span class="ml-1 font-medium">Lv.{{ level }}</span>
</div>
</div>
</div>
</div>
</ScrollableDialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { formatNumber, formatDate } from '@/utils/format'
import { Eye } from 'lucide-vue-next'
import type { SpyReport } from '@/types/game'
const props = defineProps<{
report: SpyReport | null
open: boolean
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()
const { t } = useI18n()
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
const isOpen = ref(props.open)
watch(
() => props.open,
newValue => {
isOpen.value = newValue
}
)
watch(isOpen, newValue => {
emit('update:open', newValue)
})
// 检查是否有防御设施
const hasDefense = (defense: any): boolean => {
if (!defense) return false
return Object.values(defense).some((count: any) => count > 0)
}
</script>

View File

@@ -0,0 +1,405 @@
<template>
<div class="quest-map-container relative">
<!-- 可缩放的地图区域 -->
<div
ref="mapContainer"
class="quest-map relative overflow-auto rounded-lg border bg-card/50 backdrop-blur-sm"
:style="{ maxHeight: '450px' }"
>
<!-- 可缩放内容包装器 -->
<div
class="map-content origin-top-left transition-transform duration-200"
:style="{ transform: `scale3d(${zoomLevel}, ${zoomLevel}, 1)`, minWidth: calculatedMapWidth + 'px', minHeight: calculatedMapHeight + 'px' }"
>
<!-- SVG连接线 - 位置与节点容器对齐 -->
<svg
class="absolute pointer-events-none"
:style="{ left: 0, top: 0, width: calculatedMapWidth + 'px', height: calculatedMapHeight + 'px' }"
:viewBox="`0 0 ${calculatedMapWidth} ${calculatedMapHeight}`"
>
<defs>
<!-- 渐变定义 - 垂直方向 -->
<linearGradient id="line-gradient-active" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: hsl(var(--primary)); stop-opacity: 0.5" />
<stop offset="100%" style="stop-color: hsl(var(--primary)); stop-opacity: 1" />
</linearGradient>
<linearGradient id="line-gradient-locked" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: hsl(var(--muted-foreground)); stop-opacity: 0.2" />
<stop offset="100%" style="stop-color: hsl(var(--muted-foreground)); stop-opacity: 0.3" />
</linearGradient>
<linearGradient id="line-gradient-completed" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: rgb(34, 197, 94); stop-opacity: 0.5" />
<stop offset="100%" style="stop-color: rgb(34, 197, 94); stop-opacity: 1" />
</linearGradient>
<!-- 发光滤镜 -->
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<!-- 连接线 -->
<g>
<template v-for="connection in questConnections" :key="connection.id">
<path
:d="connection.path"
fill="none"
:stroke="getConnectionStroke(connection)"
stroke-width="2"
:stroke-dasharray="connection.isLocked ? '5,5' : 'none'"
:filter="connection.isActive ? 'url(#glow)' : 'none'"
class="transition-all duration-300"
/>
<!-- 流动动画点激活状态 -->
<circle v-if="connection.isActive" r="3" fill="#CDD1D7" class="animate-flow">
<animateMotion dur="2s" repeatCount="indefinite" :path="connection.path" />
</circle>
</template>
</g>
</svg>
<!-- 任务节点 -->
<div class="relative" :style="{ width: calculatedMapWidth + 'px', height: calculatedMapHeight + 'px' }">
<div v-for="quest in quests" :key="quest.id" class="absolute transition-all duration-300" :style="getNodeStyle(quest.id)">
<QuestNode :quest="quest" :progress="progress" @select="handleQuestSelect" />
</div>
</div>
</div>
</div>
<!-- 地图控制 -->
<div class="absolute bottom-4 right-4 flex gap-2">
<Button variant="outline" size="icon-sm" @click="zoomIn">
<ZoomIn class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon-sm" @click="zoomOut">
<ZoomOut class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon-sm" @click="resetView">
<Maximize2 class="h-4 w-4" />
</Button>
</div>
<!-- 图例 -->
<div class="absolute top-4 left-4 flex flex-wrap gap-3 text-xs">
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-green-500 dark:bg-green-400" />
<span class="text-muted-foreground">{{ t('campaign.completed') }}</span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-primary" />
<span class="text-muted-foreground">{{ t('campaign.inProgress') }}</span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-blue-400 dark:bg-blue-300 animate-pulse" />
<span class="text-muted-foreground">{{ t('campaign.available') }}</span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-muted-foreground/30" />
<span class="text-muted-foreground">{{ t('campaign.locked') }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import { useI18n } from '@/composables/useI18n'
import { QuestStatus, type CampaignQuestConfig, type PlayerCampaignProgress } from '@/types/game'
import { getQuestStatus } from '@/logic/campaignLogic'
import { Button } from '@/components/ui/button'
import QuestNode from './QuestNode.vue'
import { ZoomIn, ZoomOut, Maximize2 } from 'lucide-vue-next'
const props = defineProps<{
quests: CampaignQuestConfig[]
progress: PlayerCampaignProgress | undefined
}>()
const emit = defineEmits<{
selectQuest: [questId: string]
}>()
const { t } = useI18n()
// 地图容器引用
const mapContainer = ref<HTMLElement | null>(null)
// 布局参数 - 从左到右的工作流布局
const nodeSize = 56 // 节点实际尺寸 (w-14 = 56px)
const nodeRadius = 28 // 节点半径
const horizontalGap = 120 // 水平间距(层级之间,包含连线空间)
const verticalGap = 40 // 垂直间距(同一层级内)
const paddingX = 80
const paddingY = 60
// 缩放级别
const zoomLevel = ref(1)
// 计算工作流布局的节点位置(从左到右)
const nodePositions = computed(() => {
const positions: Record<string, { x: number; y: number; level: number; index: number }> = {}
const levels: Record<number, CampaignQuestConfig[]> = {}
// 根据任务的依赖关系计算层级
const calculateLevel = (quest: CampaignQuestConfig, visited: Set<string> = new Set()): number => {
if (visited.has(quest.id)) return 0
visited.add(quest.id)
if (!quest.requiredQuestIds || quest.requiredQuestIds.length === 0) {
return 0
}
let maxParentLevel = -1
quest.requiredQuestIds.forEach(reqId => {
const parentQuest = props.quests.find(q => q.id === reqId)
if (parentQuest) {
const parentLevel = calculateLevel(parentQuest, visited)
maxParentLevel = Math.max(maxParentLevel, parentLevel)
}
})
return maxParentLevel + 1
}
// 为每个任务计算层级
props.quests.forEach(quest => {
const level = calculateLevel(quest)
if (!levels[level]) {
levels[level] = []
}
levels[level].push(quest)
})
// 按 order 排序每个层级的任务
Object.keys(levels).forEach(levelKey => {
const level = parseInt(levelKey)
const questsAtLevel = levels[level]
if (questsAtLevel) {
questsAtLevel.sort((a, b) => a.order - b.order)
}
})
// 计算每个任务的位置(从左到右布局)
const levelKeys = Object.keys(levels)
.map(Number)
.sort((a, b) => a - b)
levelKeys.forEach(level => {
const questsInLevel = levels[level]
if (!questsInLevel) return
questsInLevel.forEach((quest, index) => {
// 水平位置层级决定X坐标
const x = paddingX + level * (nodeSize + horizontalGap) + nodeRadius
// 垂直位置同层级内的索引决定Y坐标
const startY = paddingY + index * (nodeSize + verticalGap)
const y = startY + nodeRadius
positions[quest.id] = { x, y, level, index }
})
})
return positions
})
// 计算地图尺寸
const calculatedMapWidth = computed(() => {
const positions = Object.values(nodePositions.value)
if (positions.length === 0) return 400
const maxX = Math.max(...positions.map(p => p.x))
return Math.max(maxX + paddingX + nodeRadius, 400)
})
const calculatedMapHeight = computed(() => {
const positions = Object.values(nodePositions.value)
if (positions.length === 0) return 300
const maxY = Math.max(...positions.map(p => p.y))
return Math.max(maxY + paddingY + nodeRadius, 300)
})
// 计算连接线
interface Connection {
id: string
from: string
to: string
path: string
isLocked: boolean
isActive: boolean
isCompleted: boolean
}
const questConnections = computed<Connection[]>(() => {
const connections: Connection[] = []
props.quests.forEach(quest => {
if (quest.requiredQuestIds) {
quest.requiredQuestIds.forEach(requiredId => {
const fromPos = nodePositions.value[requiredId]
const toPos = nodePositions.value[quest.id]
if (fromPos && toPos) {
// 从节点右边缘出发,到下一个节点左边缘
const startX = fromPos.x + nodeRadius
const startY = fromPos.y
const endX = toPos.x - nodeRadius
const endY = toPos.y
// 使用水平控制点创建平滑的S型曲线
const controlOffset = (endX - startX) / 2
const path = `M ${startX} ${startY} C ${startX + controlOffset} ${startY}, ${endX - controlOffset} ${endY}, ${endX} ${endY}`
// 获取状态
const fromQuest = props.quests.find(q => q.id === requiredId)
const fromStatus = props.progress && fromQuest ? getQuestStatus(props.progress, fromQuest.id) : QuestStatus.Locked
const toStatus = props.progress ? getQuestStatus(props.progress, quest.id) : QuestStatus.Locked
connections.push({
id: `${requiredId}-${quest.id}`,
from: requiredId,
to: quest.id,
path,
isLocked: toStatus === QuestStatus.Locked,
isActive: toStatus === QuestStatus.Active || toStatus === QuestStatus.Available,
isCompleted: fromStatus === QuestStatus.Completed && toStatus === QuestStatus.Completed
})
}
})
}
})
return connections
})
// 获取连接线颜色
const getConnectionStroke = (connection: Connection): string => {
if (connection.isCompleted) {
return 'rgb(34, 197, 94)'
}
if (connection.isActive) {
return 'hsl(var(--primary))'
}
// 锁定状态使用更明显的灰色
return 'rgba(156, 163, 175, 0.5)'
}
// 获取节点样式(处理 undefined 情况)
const getNodeStyle = (questId: string) => {
const pos = nodePositions.value[questId]
if (!pos) {
return { left: '0px', top: '0px' }
}
return {
left: pos.x - nodeRadius + 'px',
top: pos.y - nodeRadius + 'px'
}
}
// 缩放控制
const zoomIn = () => {
zoomLevel.value = Math.min(zoomLevel.value * 1.2, 2)
}
const zoomOut = () => {
zoomLevel.value = Math.max(zoomLevel.value / 1.2, 0.5)
}
const resetView = () => {
zoomLevel.value = 1
if (mapContainer.value) {
mapContainer.value.scrollTo({ left: 0, top: 0, behavior: 'smooth' })
}
}
// 处理任务选择
const handleQuestSelect = (questId: string) => {
emit('selectQuest', questId)
}
// 找到当前活动或可用的任务
const findActiveQuest = (): string | null => {
// 优先找 Active 状态的任务
const activeQuest = props.quests.find(quest => {
if (!props.progress) return false
return getQuestStatus(props.progress, quest.id) === QuestStatus.Active
})
if (activeQuest) return activeQuest.id
// 其次找第一个 Available 状态的任务
const availableQuest = props.quests.find(quest => {
if (!props.progress) return false
return getQuestStatus(props.progress, quest.id) === QuestStatus.Available
})
if (availableQuest) return availableQuest.id
return null
}
// 滚动到指定任务位置(居中显示)
const scrollToQuest = (questId: string) => {
const container = mapContainer.value
if (!container) return
const pos = nodePositions.value[questId]
if (!pos) return
// 计算需要滚动的位置,使任务节点居中
const containerWidth = container.clientWidth
const containerHeight = container.clientHeight
// 考虑缩放比例
const scaledX = pos.x * zoomLevel.value
const scaledY = pos.y * zoomLevel.value
// 滚动到节点居中的位置
const scrollLeft = Math.max(0, scaledX - containerWidth / 2)
const scrollTop = Math.max(0, scaledY - containerHeight / 2)
container.scrollTo({
left: scrollLeft,
top: scrollTop,
behavior: 'smooth'
})
}
// 组件挂载时滚动到活动任务
onMounted(async () => {
await nextTick()
const activeQuestId = findActiveQuest()
if (activeQuestId) {
scrollToQuest(activeQuestId)
}
})
</script>
<style scoped>
.quest-map-container {
position: relative;
}
.quest-map {
min-height: 300px;
}
.animate-flow {
filter: drop-shadow(0 0 3px hsl(var(--primary)));
}
/* 星空背景效果 */
.quest-map::before {
content: '';
position: absolute;
inset: 0;
background-image: radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.02) 1px, transparent 1px),
radial-gradient(circle at 40% 80%, rgba(255, 255, 255, 0.02) 1px, transparent 1px),
radial-gradient(circle at 60% 20%, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 100px 100px, 150px 150px, 200px 200px, 120px 120px;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div
class="quest-node"
:class="[statusClass, { 'cursor-pointer': isClickable, 'cursor-not-allowed': !isClickable }]"
@click="handleClick"
>
<!-- 节点主体 -->
<div
:class="[
'relative w-14 h-14 rounded-full flex items-center justify-center transition-all duration-300',
'border-2 shadow-lg',
nodeBackgroundClass
]"
>
<!-- Boss标记 -->
<div v-if="quest.isBoss" class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center">
<Skull class="w-3 h-3 text-white" />
</div>
<!-- 分支标记 -->
<div v-if="quest.isBranch" class="absolute -top-1 -left-1 w-5 h-5 bg-blue-500 dark:bg-blue-400 rounded-full flex items-center justify-center">
<GitBranch class="w-3 h-3 text-white" />
</div>
<!-- 状态图标 -->
<component :is="statusIcon" :class="['w-6 h-6', iconClass]" />
<!-- 进度环 -->
<svg v-if="status === QuestStatus.Active && progress > 0" class="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 56 56">
<circle cx="28" cy="28" r="24" fill="none" stroke="currentColor" stroke-width="3" class="text-primary/30" />
<circle
cx="28"
cy="28"
r="24"
fill="none"
stroke="currentColor"
stroke-width="3"
class="text-primary"
:stroke-dasharray="`${progress * 1.51} 151`"
/>
</svg>
</div>
<!-- 节点标题 -->
<div class="mt-2 text-center max-w-20">
<p :class="['text-xs font-medium truncate', titleClass]">
{{ t(quest.titleKey) }}
</p>
<p v-if="status === QuestStatus.Active" class="text-[10px] text-primary">{{ progress }}%</p>
</div>
<!-- 脉冲动画可用状态 -->
<div v-if="status === QuestStatus.Available" class="absolute inset-0 w-14 h-14 rounded-full animate-ping bg-blue-400/30 dark:bg-blue-300/30" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from '@/composables/useI18n'
import { QuestStatus, type CampaignQuestConfig, type PlayerCampaignProgress } from '@/types/game'
import { getQuestStatus, calculateQuestProgress } from '@/logic/campaignLogic'
import { Lock, Circle, CheckCircle2, Play, Skull, GitBranch } from 'lucide-vue-next'
const props = defineProps<{
quest: CampaignQuestConfig
progress: PlayerCampaignProgress | undefined
}>()
const emit = defineEmits<{
select: [questId: string]
}>()
const { t } = useI18n()
// 计算任务状态
const status = computed(() => {
if (!props.progress) return QuestStatus.Locked
return getQuestStatus(props.progress, props.quest.id)
})
// 计算任务进度百分比
const progress = computed(() => {
if (!props.progress || status.value !== QuestStatus.Active) return 0
return calculateQuestProgress(props.progress, props.quest.id)
})
// 是否可点击
const isClickable = computed(() => {
return status.value !== QuestStatus.Locked
})
// 状态样式类
const statusClass = computed(() => {
switch (status.value) {
case QuestStatus.Completed:
return 'quest-completed'
case QuestStatus.Active:
return 'quest-active'
case QuestStatus.Available:
return 'quest-available'
default:
return 'quest-locked'
}
})
// 节点背景样式
const nodeBackgroundClass = computed(() => {
switch (status.value) {
case QuestStatus.Completed:
return 'bg-green-500/20 border-green-500'
case QuestStatus.Active:
return 'bg-primary/20 border-primary'
case QuestStatus.Available:
return 'bg-blue-500/10 border-blue-400 hover:border-blue-300 hover:bg-blue-500/20'
default:
return 'bg-muted/50 border-muted-foreground/30'
}
})
// 状态图标
const statusIcon = computed(() => {
switch (status.value) {
case QuestStatus.Completed:
return CheckCircle2
case QuestStatus.Active:
return Play
case QuestStatus.Available:
return Circle
default:
return Lock
}
})
// 图标样式
const iconClass = computed(() => {
switch (status.value) {
case QuestStatus.Completed:
return 'text-green-500'
case QuestStatus.Active:
return 'text-primary'
case QuestStatus.Available:
return 'text-blue-400'
default:
return 'text-muted-foreground/50'
}
})
// 标题样式
const titleClass = computed(() => {
switch (status.value) {
case QuestStatus.Completed:
return 'text-green-500'
case QuestStatus.Active:
return 'text-primary'
case QuestStatus.Available:
return 'text-foreground'
default:
return 'text-muted-foreground/50'
}
})
// 处理点击
const handleClick = () => {
if (isClickable.value) {
emit('select', props.quest.id)
}
}
</script>
<style scoped>
.quest-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.quest-available .quest-node:hover {
transform: scale3d(1.05, 1.05, 1);
}
@keyframes pulse-glow {
0%,
100% {
box-shadow: 0 0 5px 2px rgba(59, 130, 246, 0.3);
}
50% {
box-shadow: 0 0 15px 5px rgba(59, 130, 246, 0.5);
}
}
.quest-available > div:first-child {
animation: pulse-glow 2s ease-in-out infinite;
}
</style>

View File

@@ -0,0 +1,278 @@
<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" />
</div>
<!-- 对话内容区 -->
<div class="relative p-6 min-h-[300px] flex flex-col">
<!-- 说话者信息 -->
<div v-if="currentDialogue" class="mb-4">
<div class="flex items-center gap-3">
<!-- 头像 -->
<div :class="['w-12 h-12 rounded-full flex items-center justify-center', speakerStyles.bg]">
<component :is="speakerIcon" :class="['w-6 h-6', speakerStyles.text]" />
</div>
<!-- 说话者名称 -->
<div>
<span :class="['font-semibold', speakerStyles.text]">
{{ getSpeakerName(currentDialogue) }}
</span>
<div v-if="currentDialogue.speaker === 'mysterious'" class="text-xs text-muted-foreground">
{{ t('campaign.dialogue.unknownSource') }}
</div>
</div>
</div>
</div>
<!-- 对话文本 - 打字机效果 -->
<div class="flex-1 min-h-[120px]">
<div class="text-base leading-relaxed">
<span>{{ displayedText }}</span>
<span v-if="isTyping" class="animate-pulse"></span>
</div>
</div>
<!-- 选项按钮 -->
<div v-if="showChoices && currentDialogue?.choices" class="space-y-2 mt-4">
<Button
v-for="(choice, index) in currentDialogue.choices"
:key="index"
variant="outline"
class="w-full justify-start text-left h-auto py-3 px-4"
@click="handleChoice(choice)"
>
<ChevronRight class="w-4 h-4 mr-2 shrink-0" />
<span>{{ t(choice.textKey) }}</span>
</Button>
</div>
<!-- 继续按钮 -->
<div v-if="!showChoices" class="mt-4 flex justify-end gap-2">
<Button v-if="isTyping" variant="ghost" size="sm" @click="skipTyping">
{{ t('campaign.dialogue.skip') }}
</Button>
<Button v-else @click="handleContinue" :disabled="isTyping">
{{ isLastDialogue ? t('campaign.dialogue.finish') : t('campaign.dialogue.continue') }}
<ChevronRight class="w-4 h-4 ml-1" />
</Button>
</div>
<!-- 进度指示器 -->
<div class="mt-4 flex justify-center gap-1">
<div
v-for="(_, index) in dialogues"
:key="index"
:class="[
'w-2 h-2 rounded-full transition-all',
index < currentIndex ? 'bg-primary' : index === currentIndex ? 'bg-primary/50' : 'bg-muted'
]"
/>
</div>
</div>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useI18n } from '@/composables/useI18n'
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'
const props = defineProps<{
dialogues: StoryDialogue[]
}>()
const emit = defineEmits<{
close: []
choice: [choice: DialogueChoice]
}>()
const { t } = useI18n()
// 当前对话索引
const currentIndex = ref(0)
// 打字机效果状态
const displayedText = ref('')
const isTyping = ref(false)
const typewriterInterval = ref<ReturnType<typeof setInterval> | null>(null)
// 当前对话
const currentDialogue = computed(() => props.dialogues[currentIndex.value])
// 是否是最后一个对话
const isLastDialogue = computed(() => currentIndex.value >= props.dialogues.length - 1)
// 是否显示选项
const showChoices = computed(() => {
return !isTyping.value && currentDialogue.value?.choices && currentDialogue.value.choices.length > 0
})
// 说话者图标
const speakerIcon = computed(() => {
switch (currentDialogue.value?.speaker) {
case 'player':
return User
case 'npc':
return Bot
case 'mysterious':
return HelpCircle
default:
return MessageCircle
}
})
// 说话者样式
const speakerStyles = computed(() => {
switch (currentDialogue.value?.speaker) {
case 'player':
return { bg: 'bg-blue-500/20', text: 'text-blue-400' }
case 'npc':
return { bg: 'bg-green-500/20', text: 'text-green-400' }
case 'mysterious':
return { bg: 'bg-purple-500/20', text: 'text-purple-400' }
default:
return { bg: 'bg-muted', text: 'text-muted-foreground' }
}
})
// 获取说话者名称
const getSpeakerName = (dialogue: StoryDialogue): string => {
if (dialogue.speakerNameKey) {
return t(dialogue.speakerNameKey)
}
switch (dialogue.speaker) {
case 'player':
return t('campaign.dialogue.player')
case 'npc':
return t('campaign.dialogue.npc')
case 'mysterious':
return t('campaign.dialogue.mysterious')
default:
return t('campaign.dialogue.narrator')
}
}
// 开始打字机效果
const startTypewriter = () => {
const text = t(currentDialogue.value?.textKey || '')
if (!text) return
displayedText.value = ''
isTyping.value = true
let charIndex = 0
typewriterInterval.value = setInterval(() => {
if (charIndex < text.length) {
displayedText.value += text[charIndex]
charIndex++
} else {
stopTypewriter()
}
}, 30) // 每30ms显示一个字符
}
// 停止打字机效果
const stopTypewriter = () => {
if (typewriterInterval.value) {
clearInterval(typewriterInterval.value)
typewriterInterval.value = null
}
isTyping.value = false
}
// 跳过打字机效果
const skipTyping = () => {
stopTypewriter()
displayedText.value = t(currentDialogue.value?.textKey || '')
}
// 处理继续
const handleContinue = () => {
if (isLastDialogue.value) {
emit('close')
} else {
currentIndex.value++
}
}
// 处理选项选择
const handleChoice = (choice: DialogueChoice) => {
emit('choice', choice)
// 如果有下一个对话ID跳转到对应对话
if (choice.nextDialogueId) {
const nextIndex = props.dialogues.findIndex(d => d.id === choice.nextDialogueId)
if (nextIndex !== -1) {
currentIndex.value = nextIndex
return
}
}
// 否则继续到下一个
handleContinue()
}
// 处理关闭
const handleClose = (open: boolean) => {
if (!open) {
emit('close')
}
}
// 监听对话变化,启动打字机
watch(
currentIndex,
() => {
stopTypewriter()
startTypewriter()
},
{ immediate: false }
)
// 初始化
onMounted(() => {
startTypewriter()
})
// 清理
onUnmounted(() => {
stopTypewriter()
})
</script>
<style scoped>
.stars-bg {
position: absolute;
inset: 0;
background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.02) 2px, transparent 2px);
background-size: 50px 50px, 80px 80px, 120px 120px;
animation: twinkle 3s ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More