Sunshine 包含一个现代化的 Web 控制界面,基于 Vue 3 和 Composition API 构建,遵循 Vue 最佳实践。
注意: 本文档已更新以反映最新的项目结构优化。所有页面已重构为使用 Composition API 和模块化架构。
- 前端框架: Vue 3 + Composition API
- 构建工具: Vite 5.4+ (支持 Rolldown)
- 打包器: Rolldown (实验性,更快)
- UI 组件: Bootstrap 5
- 图标库: FontAwesome 6
- 国际化: Vue-i18n 11 (Composition API 模式)
- 拖拽功能: Vuedraggable 4
- 模块系统: ES Modules (
"type": "module")
注意: 本文档已更新以反映最新的项目结构优化。所有页面已重构为使用 Composition API 和模块化架构。
npm install# 开发模式 - 实时构建和监听文件变化
npm run dev
# 开发服务器 - 启动HTTPS开发服务器 (推荐)
npm run dev-server
# 完整开发环境 - 包含模拟API服务
npm run dev-full
# 构建生产版本
npm run build
# 清理构建目录并重新构建
npm run build-clean
# 预览生产构建
npm run preview
# 自动构建并预览生产版本(推荐)
npm run preview:build注意: 项目已配置为使用 Rolldown(Vite 5.1+ 的实验性打包器)以获得更快的构建速度。所有构建命令默认启用 Rolldown。
- HTTPS支持: 自动生成本地SSL证书
- 热重载: 实时更新代码变更
- 代理配置: 自动代理API请求到Sunshine服务
- 模拟数据: 开发模式下提供模拟API响应
- 端口: 默认运行在
https://localhost:3000
src_assets/common/assets/web/
├── views/ # 页面组件(路由级组件)
│ ├── Home.vue # 首页
│ ├── Apps.vue # 应用管理页面
│ ├── Config.vue # 配置管理页面
│ ├── Troubleshooting.vue # 故障排除页面
│ ├── Pin.vue # PIN 配对页面
│ ├── Password.vue # 密码修改页面
│ └── Welcome.vue # 欢迎页面
│
├── components/ # Vue 组件
│ ├── layout/ # 布局组件
│ │ ├── Navbar.vue # 导航栏
│ │ └── PlatformLayout.vue # 平台布局组件
│ ├── common/ # 通用组件
│ │ ├── ThemeToggle.vue # 主题切换
│ │ ├── ResourceCard.vue # 资源卡片
│ │ ├── VersionCard.vue # 版本信息卡片
│ │ ├── ErrorLogs.vue # 错误日志组件
│ │ └── Locale.vue # 语言组件
│ ├── SetupWizard.vue # 设置向导
│ └── ... # 其他功能组件
│
├── composables/ # 组合式函数(可复用逻辑)
│ ├── useVersion.js # 版本管理
│ ├── useLogs.js # 日志管理
│ ├── useSetupWizard.js # 设置向导逻辑
│ ├── useApps.js # 应用管理
│ ├── useConfig.js # 配置管理
│ ├── useTroubleshooting.js # 故障排除
│ ├── usePin.js # PIN 配对
│ ├── useWelcome.js # 欢迎页面
│ └── useTheme.js # 主题管理
│
├── config/ # 配置文件
│ ├── firebase.js # Firebase 配置
│ └── i18n.js # 国际化配置
│
├── services/ # API 服务
│ └── appService.js # 应用服务
│
├── utils/ # 工具函数
│ ├── constants.js # 常量定义
│ ├── helpers.js # 辅助函数
│ ├── validation.js # 表单验证
│ ├── theme.js # 主题工具
│ └── ...
│
├── styles/ # 样式文件
│ ├── apps.css # 应用页面样式
│ ├── welcome.css # 欢迎页面样式
│ └── ...
│
├── public/ # 静态资源
│ ├── assets/
│ │ ├── css/ # 全局样式
│ │ └── locale/ # 国际化文件
│ └── images/ # 图片资源
│
├── configs/ # 配置页面子组件
│ └── tabs/ # 配置标签页组件
│
├── *.html # 页面入口文件(已简化)
└── init.js # 应用初始化
- views/: 页面级组件,对应路由
- components/layout/: 布局相关组件(Navbar, PlatformLayout)
- components/common/: 通用可复用组件
- components/: 功能特定组件
- composables/: 可复用的业务逻辑
- config/: 配置文件
- services/: API 服务层
- utils/: 纯函数工具
- 对应一个完整的页面
- 使用 Composition API (
<script setup>) - 组合多个子组件和 composables
- 处理页面级状态和生命周期
- 页面布局相关(如导航栏)
- 可跨页面复用
- 高度可复用的 UI 组件
- 无业务逻辑或逻辑简单
- 特定功能的组件
- 包含一定业务逻辑
Composables 用于提取可复用的业务逻辑:
// composables/useExample.js
import { ref, computed } from 'vue'
export function useExample() {
const data = ref(null)
const loading = ref(false)
const computedValue = computed(() => {
// 计算逻辑
})
const fetchData = async () => {
// 数据获取逻辑
}
return {
data,
loading,
computedValue,
fetchData,
}
}<!-- views/NewPage.vue -->
<template>
<div>
<Navbar />
<div class="container">
<h1>{{ $t('newpage.title') }}</h1>
<!-- 页面内容 -->
</div>
</div>
</template>
<script setup>
import Navbar from '../components/layout/Navbar.vue'
// 导入需要的 composables
import { useNewPage } from '../composables/useNewPage.js'
const {
// 解构需要的状态和方法
} = useNewPage()
</script>
<style scoped>
/* 页面特定样式 */
</style>// composables/useNewPage.js
import { ref, computed } from 'vue'
export function useNewPage() {
const data = ref(null)
const fetchData = async () => {
// 数据获取逻辑
}
return {
data,
fetchData,
}
}<!-- newpage.html -->
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<%- header %>
</head>
<body id="app" v-cloak>
<!-- Vue 应用挂载点 -->
</body>
<script type="module">
import { createApp } from 'vue'
import { initApp } from './init'
import NewPage from './views/NewPage.vue'
const app = createApp(NewPage)
initApp(app)
</script>
</html>推荐使用 <script setup> 语法:
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
onMounted(() => {
// 初始化逻辑
})
</script><template>
<div>
<!-- 在模板中使用 $t (通过 globalInjection) -->
<h1>{{ $t('common.title') }}</h1>
<p>{{ $t('common.description') }}</p>
<!-- 在属性中使用 -->
<input :placeholder="$t('common.placeholder')" />
<button :title="$t('common.tooltip')">{{ $t('common.button') }}</button>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
// 在 script 中使用 useI18n() 获取 t 函数
const { t } = useI18n()
</script>当需要在 JavaScript 代码中使用翻译(如 alert(), confirm() 等),必须使用 useI18n():
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// 在函数中使用
const handleConfirm = () => {
if (confirm(t('common.confirm_message'))) {
// 处理确认
}
}
const showError = () => {
alert(t('common.error_message'))
}
</script>import { useI18n } from 'vue-i18n'
export function useExample() {
const { t } = useI18n()
const showMessage = (key) => {
alert(t(key))
}
return { showMessage }
}- 全局样式:
public/assets/css/或styles/ - 组件样式: 使用
<style scoped>在组件内 - 页面特定样式: 在对应的页面组件中
使用 services/ 目录组织 API 调用:
// services/exampleService.js
export class ExampleService {
static async getData() {
const response = await fetch('/api/example')
return response.json()
}
static async saveData(data) {
const response = await fetch('/api/example', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return response.json()
}
}- 分析需求:确定是页面、组件还是功能增强
- 创建 Composables:提取可复用的业务逻辑
- 创建组件:实现 UI 和交互
- 创建页面:组合组件和 composables
- 添加路由:创建 HTML 入口文件
- 测试验证:确保功能正常
- ✅ 是否遵循目录结构规范
- ✅ 是否使用 Composition API
- ✅ 业务逻辑是否提取到 composables
- ✅ 组件是否可复用
- ✅ 样式是否合理组织
- ✅ 是否添加了必要的错误处理
<!-- views/Example.vue -->
<template>
<div>
<Navbar />
<div class="container">
<h1>{{ $t('example.title') }}</h1>
<ExampleCard
v-for="item in items"
:key="item.id"
:item="item"
@action="handleAction"
/>
<div v-if="loading" class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import Navbar from '../components/layout/Navbar.vue'
import ExampleCard from '../components/ExampleCard.vue'
import { useExample } from '../composables/useExample.js'
import { trackEvents } from '../config/firebase.js'
const {
items,
loading,
fetchItems,
handleAction,
} = useExample()
onMounted(async () => {
trackEvents.pageView('example')
await fetchItems()
})
</script>
<style scoped>
.container {
padding: 1rem;
}
</style>// composables/useExample.js
import { ref, computed } from 'vue'
import { ExampleService } from '../services/exampleService.js'
import { trackEvents } from '../config/firebase.js'
export function useExample() {
const items = ref([])
const loading = ref(false)
const error = ref(null)
const itemCount = computed(() => items.value.length)
const fetchItems = async () => {
loading.value = true
error.value = null
try {
items.value = await ExampleService.getItems()
trackEvents.userAction('items_loaded', { count: items.value.length })
} catch (err) {
error.value = err.message
trackEvents.errorOccurred('fetch_items', err.message)
} finally {
loading.value = false
}
}
const handleAction = async (itemId) => {
try {
await ExampleService.performAction(itemId)
await fetchItems() // 刷新列表
} catch (err) {
console.error('Action failed:', err)
}
}
return {
items,
loading,
error,
itemCount,
fetchItems,
handleAction,
}
}// config/i18n.js
const i18n = createI18n({
legacy: false, // 使用 Composition API 模式
locale: locale,
fallbackLocale: 'en',
messages: messages,
globalInjection: true, // 允许在模板中使用 $t
})// config/firebase.js
import { initFirebase, trackEvents } from './config/firebase.js'
// 初始化
initFirebase()
// 使用
trackEvents.pageView('page_name')
trackEvents.userAction('action_name', { data })
trackEvents.gpuReported({ platform: 'windows', adapters: [...] })可用事件:
pageView(pageName)- 页面访问userAction(actionName, data)- 用户操作errorOccurred(errorType, message)- 错误发生gpuReported(gpuInfo)- 显卡信息上报(24小时内仅上报一次)
项目使用 Bootstrap 5 作为 UI 框架,优先使用 Bootstrap 组件和工具类。
- 组件特定样式使用
<style scoped> - 全局样式放在
styles/目录 - 使用 CSS 变量进行主题定制
主要依赖:
vue- Vue 3 框架vue-i18n- 国际化(Composition API 模式)bootstrap- UI 框架vuedraggable- 拖拽功能marked- Markdown 解析
- 使用 Vue DevTools:安装 Vue DevTools 浏览器扩展
- 控制台日志:使用
console.log进行调试 - 网络请求:使用浏览器开发者工具查看 API 请求
- 组件检查:在 Vue DevTools 中检查组件状态
- 开发配置:
vite.dev.config.js- 开发环境专用配置 - 生产配置:
vite.config.js- 生产构建配置 - EJS模板: 支持HTML模板预处理
- 路径别名: 配置了Vue和Bootstrap的路径别名
- Rolldown支持: 使用 Rolldown 作为实验性打包器(更快)
- ESM模式: 项目使用 ES 模块(
"type": "module")
开发服务器包含以下代理设置:
/api/*→https://localhost:47990(Sunshine API)/steam-api/*→ Steam API服务/steam-store/*→ Steam商店服务
预览模式用于测试生产构建,但需要注意:
- API 不可用: 预览模式下没有后端 API 服务器
- 错误处理: 代码已优化,在预览模式下会优雅降级
- 使用场景: 主要用于验证构建产物和静态资源
# 构建并预览
npm run preview:build
# 或分步执行
npm run build
npm run preview访问地址:http://localhost:3000
注意: 手动分包 (
manualChunks) 当前已禁用,因为可能导致 Bootstrap 和 Popper.js 的依赖关系问题,影响下拉菜单等功能的正常工作。Vite 会自动进行代码分割优化。
- 支持多语言切换
- 基于 Vue-i18n 11 (Composition API 模式)
- 语言文件位于
public/assets/locale/目录 - 配置在
config/i18n.js中
项目提供了一套完整的国际化(i18n)工具链,用于确保翻译文件的质量和一致性。基准语言文件是 en.json,所有其他语言文件需要与其保持同步。
# 验证所有语言文件的完整性
npm run i18n:validate
# 检查并自动同步缺失的翻译键(仅为补充键,缺失键会用英文占位值填充)
# 注意:sync 只保证“键齐全”,不会做翻译。其他语言文件中的英文占位值仍需人工改为对应语言。
npm run i18n:sync
# 格式化并排序所有语言文件(按字母顺序)
npm run i18n:format
# 检查文件格式
npm run i18n:format:check
# 验证翻译完整性
npm run i18n:validate-
在基准文件中添加新键:首先在
en.json中添加新的翻译键和英文值{ "myfeature": { "title": "My Feature Title", "description": "My feature description", "button_label": "Submit" } } -
同步到其他语言文件:
npm run i18n:sync
这将自动在所有语言文件中添加缺失的键,并用英文值作为占位符。sync 只负责补全键,不负责翻译;各语言文件中的英文占位值需要人工改成对应语言。
-
格式化文件:
npm run i18n:format
这将对所有语言文件进行统一排序和格式化,减少 Git 冲突
-
翻译占位符(必做):将各语言文件中由 sync 填入的英文占位值,手动修改为该语言的实际译文。未修改时界面会显示英文。
-
验证:
npm run i18n:validate
确保所有语言文件都包含完整的翻译键
以下是一个完整的国际化现有组件的示例:
步骤 1:识别硬编码文本
<!-- 原始组件 -->
<template>
<div>
<h2>客户端列表</h2>
<table>
<thead>
<tr>
<th>名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="client in clients" :key="client.id">
<td>{{ client.name || '未知客户端' }}</td>
<td>
<button @click="handleDelete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
const handleDelete = () => {
if (confirm('确定要删除吗?')) {
// 删除逻辑
}
}
</script>步骤 2:在 en.json 中添加翻译键
{
"client": {
"list_title": "Client List",
"name": "Name",
"actions": "Actions",
"unknown_client": "Unknown Client",
"delete": "Delete",
"confirm_delete": "Are you sure you want to delete?"
}
}步骤 3:更新组件使用翻译
<template>
<div>
<h2>{{ $t('client.list_title') }}</h2>
<table>
<thead>
<tr>
<th>{{ $t('client.name') }}</th>
<th>{{ $t('client.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="client in clients" :key="client.id">
<td>{{ client.name || $t('client.unknown_client') }}</td>
<td>
<button @click="handleDelete">{{ $t('client.delete') }}</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const handleDelete = () => {
if (confirm(t('client.confirm_delete'))) {
// 删除逻辑
}
}
</script>步骤 4:同步和验证
npm run i18n:sync
npm run i18n:format
npm run i18n:validate- 提交前验证:在提交代码前运行
npm run i18n:validate确保没有缺失的翻译 - 保持格式一致:定期运行
npm run i18n:format保持文件格式统一 - 避免直接编辑:不要直接删除或重命名翻译键,应先在
en.json中修改,然后同步 - CI 集成:CI 会自动检查翻译文件的完整性和格式,确保代码质量
- validate-i18n.js:验证所有语言文件是否包含
en.json中定义的所有键,并报告缺失或多余的键 - format-i18n.js:对所有语言文件的键进行字母排序,并应用统一的格式化(2 空格缩进)
这些工具确保了:
- ✅ 所有语言文件具有相同的翻译键
- ✅ 文件格式统一,减少不必要的 Git 冲突
- ✅ 翻译缺失可以快速被发现和修复
- ✅ 代码审查更加容易
- 支持明暗主题切换
- 基于 CSS 变量实现
- 主题工具在
utils/theme.js中 - 使用
composables/useTheme.js在组件中管理主题
- 基于 Bootstrap 5 的响应式布局
- 支持桌面端和移动端
- 优化的触摸交互体验
- 开发模式下启用源码映射
- 详细的代理请求日志
- 模拟 API 数据用于前端开发
- 使用 Vue DevTools 进行组件调试
# 生产构建
npm run build
# 构建输出目录: build/assets/web/
# 包含所有静态资源和HTML文件如果遇到旧的 Options API 组件,可以按以下步骤迁移:
- 将
data()改为ref()或reactive() - 将
computed改为computed() - 将
methods改为普通函数 - 将生命周期钩子改为组合式 API 版本
- 使用
<script setup>简化代码
之前 (Options API):
export default {
data() {
return {
count: 0
}
},
computed: {
double() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
}
}之后 (Composition API):
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>- 使用 Composition API (
<script setup>) - 业务逻辑提取到 composables
- 组件按功能分类到正确目录
- 样式使用 scoped 或放在 styles 目录
- 使用 TypeScript 类型(如适用)
- 添加错误处理
- 使用国际化 (
$t或t) - 添加必要的用户反馈
- 代码格式化统一
- 添加必要的注释
- 页面组件:
PascalCase.vue(如Home.vue,Apps.vue) - Composables:
useXxx.js(如useVersion.js,useApps.js) - 服务类:
xxxService.js(如appService.js) - 工具函数:
camelCase.js(如helpers.js,validation.js)
// 页面组件
import Navbar from '../components/layout/Navbar.vue'
// Composables
import { useVersion } from '../composables/useVersion.js'
// 服务
import { AppService } from '../services/appService.js'
// 工具函数
import { debounce } from '../utils/helpers.js'
// 配置
import { trackEvents } from '../config/firebase.js'| Composable | 用途 | 返回内容 |
|---|---|---|
useVersion |
版本管理 | version, githubVersion, fetchVersions |
useLogs |
日志管理 | logs, fatalLogs, fetchLogs |
useApps |
应用管理 | apps, loadApps, save, editApp |
useConfig |
配置管理 | config, save, apply |
useTheme |
主题管理 | - |
usePin |
PIN 配对 | clients, unpairAll, save |
- 考虑添加 TypeScript 支持
- 考虑添加单元测试
- 考虑添加 E2E 测试
- 优化性能(懒加载、代码分割)
欢迎为 WebUI 贡献代码!请确保:
-
遵循代码规范
- 使用 Composition API
- 业务逻辑提取到 composables
- 组件按功能分类
-
代码质量
- 添加必要的错误处理
- 使用国际化
- 添加必要的注释
-
测试验证
- 提交前运行构建命令确保无错误
- 测试新功能在不同浏览器中的表现
-
文档更新
- 更新相关文档
- 添加必要的代码注释
- ✅ 所有页面重构为 Composition API
- ✅ 业务逻辑提取到 composables
- ✅ 组件按功能重新组织
- ✅ 配置文件统一管理
- ✅ Vue I18n 迁移到 Composition API 模式
- ✅ 简化所有 HTML 入口文件
- ✅ 升级 Vite 到 5.4+ 并支持 Rolldown
- ✅ 修复 CJS Node API 弃用警告(添加
"type": "module") - ✅ 添加生产环境预览功能
- ✅ 优化预览模式下的 API 错误处理
- ✅ 添加 GPU 信息上报功能(Firebase Analytics)
- ✅ 改进国际化配置的错误处理
- ✅ 跨平台环境变量支持(使用
cross-env)
- 构建系统: 升级到 Vite 5.4+,支持 Rolldown 实验性打包器
- 模块系统: 迁移到 ES 模块(
"type": "module") - 错误处理: 改进预览模式下的 API 错误处理
- 性能优化: 使用 Rolldown 加速构建过程
- 开发体验: 改进预览功能,支持一键构建并预览