-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 实现 Fluent 风格 Steam 成就追踪前端框架 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="zh-CN"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Steam 成就追踪器</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "name": "games-tracker", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build" | ||
| }, | ||
| "dependencies": { | ||
| "vue": "^3.5.13" | ||
| }, | ||
| "devDependencies": { | ||
| "@vitejs/plugin-vue": "^5.2.3", | ||
| "vite": "^6.3.5" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| <template> | ||
| <div class="app" :data-theme="theme"> | ||
| <aside class="nav-rail"> | ||
| <button | ||
| v-for="item in navItems" | ||
| :key="item.key" | ||
| :class="['rail-btn', { active: currentPage === item.key }]" | ||
| :title="item.label" | ||
| @click="currentPage = item.key" | ||
| > | ||
| <span>{{ item.icon }}</span> | ||
| </button> | ||
| </aside> | ||
|
|
||
| <main class="main-layout"> | ||
| <header class="command-bar"> | ||
| <h1>{{ currentPageLabel }}</h1> | ||
| <div class="search-wrap"> | ||
| <span>🔎</span> | ||
| <input v-model="query" placeholder="搜索游戏、标签或备注" /> | ||
| <button v-if="query" class="ghost" @click="query = ''">清除</button> | ||
| </div> | ||
| <button class="primary"> | ||
| <span v-if="syncing">⏳</span> | ||
| {{ currentPage === 'wishlist' ? '添加游戏' : syncing ? '同步中…' : '同步我的库' }} | ||
| </button> | ||
| </header> | ||
|
|
||
| <section class="content"> | ||
| <div v-if="currentPage === 'sync'" class="panel"> | ||
| <h2>同步中心</h2> | ||
| <div class="sync-item" v-for="task in syncTasks" :key="task.name"> | ||
| <div> | ||
| <strong>{{ task.name }}</strong> | ||
| <p>{{ task.stage }}</p> | ||
| </div> | ||
| <div class="progress"><div :style="{ width: task.progress + '%' }"></div></div> | ||
| <button class="ghost">取消</button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div v-else-if="currentPage === 'settings'" class="panel settings"> | ||
| <h2>设置</h2> | ||
| <div class="setting-group"> | ||
| <h3>外观</h3> | ||
| <label>主题 | ||
| <select v-model="theme"> | ||
| <option value="system">跟随系统</option> | ||
| <option value="light">浅色</option> | ||
| <option value="dark">深色</option> | ||
| </select> | ||
| </label> | ||
| <label class="switch"> | ||
| <input type="checkbox" v-model="showEnglish" /> 显示英文名(副标题) | ||
| </label> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div v-else class="grid"> | ||
| <article v-for="game in filteredGames" :key="game.id" class="card" @click="openDrawer(game)"> | ||
| <div class="left-icon">🎮</div> | ||
| <div class="mid"> | ||
| <h3>{{ game.zhName || game.enName }}</h3> | ||
| <p v-if="showEnglish && game.zhName">{{ game.enName }}</p> | ||
| <small v-if="currentPage === 'library'">游玩:{{ game.playtime }} 小时 · 最近:{{ game.lastPlayed }}</small> | ||
| </div> | ||
| <div class="right-ach"> | ||
| <strong>{{ game.ach?.done ?? '—' }} / {{ game.ach?.total ?? '—' }}</strong> | ||
| <div class="progress"><div :style="{ width: (game.ach?.percent || 0) + '%' }"></div></div> | ||
| <small>{{ game.ach ? game.ach.percent + '%' : '未同步' }}</small> | ||
| </div> | ||
| <div class="chips"> | ||
| <span class="chip state">{{ game.state }}</span> | ||
| <span class="chip" v-for="tag in game.tags.slice(0, 3)" :key="tag">{{ tag }}</span> | ||
| <span class="chip" v-if="game.tags.length > 3">+{{ game.tags.length - 3 }}</span> | ||
| </div> | ||
| </article> | ||
| </div> | ||
| </section> | ||
| </main> | ||
|
|
||
| <div class="mask" v-if="selectedGame" @click="selectedGame = null"></div> | ||
| <aside class="drawer" v-if="selectedGame"> | ||
| <header> | ||
| <div> | ||
| <h3>{{ selectedGame.zhName || selectedGame.enName }}</h3> | ||
| <p>{{ selectedGame.ach?.done ?? '—' }} / {{ selectedGame.ach?.total ?? '—' }} · {{ selectedGame.ach?.percent ?? '—' }}%</p> | ||
| </div> | ||
| <button class="ghost" @click="selectedGame = null">关闭</button> | ||
| </header> | ||
| <nav class="segmented"> | ||
| <button :class="{ active: drawerTab === 'overview' }" @click="drawerTab = 'overview'">概览</button> | ||
| <button :class="{ active: drawerTab === 'ach' }" @click="drawerTab = 'ach'">成就</button> | ||
| </nav> | ||
| <div class="drawer-content" v-if="drawerTab === 'overview'"> | ||
| <section class="panel"> | ||
| <h4>你的管理</h4> | ||
| <label>状态 | ||
| <select v-model="selectedGame.state"> | ||
| <option>想玩</option><option>在玩</option><option>已通关</option><option>搁置</option><option>已弃坑</option> | ||
| </select> | ||
| </label> | ||
| <label>标签 | ||
| <input v-model="tagDraft" placeholder="输入标签后回车" @keydown.enter.prevent="addTag" /> | ||
| </label> | ||
| <div class="chips"><span class="chip" v-for="tag in selectedGame.tags" :key="tag">{{ tag }}</span></div> | ||
| <label>备注 | ||
| <textarea v-model="selectedGame.note" placeholder="例如:进度、计划、易错点、刷成就思路…"></textarea> | ||
| </label> | ||
| </section> | ||
| </div> | ||
| <div class="drawer-content" v-else> | ||
| <section class="panel"> | ||
| <h4>成就</h4> | ||
| <div class="search-wrap"><input placeholder="搜索成就" /></div> | ||
| <p v-if="!selectedGame.ach">成就尚未同步。点击“刷新成就”以获取该游戏的成就列表与解锁状态。</p> | ||
| <ul v-else> | ||
| <li v-for="n in 8" :key="n">未解锁成就 {{ n }}</li> | ||
| </ul> | ||
| </section> | ||
| </div> | ||
| </aside> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup> | ||
| import { computed, ref } from 'vue' | ||
|
|
||
| const currentPage = ref('library') | ||
| const query = ref('') | ||
| const syncing = ref(false) | ||
| const showEnglish = ref(true) | ||
| const theme = ref('system') | ||
| const drawerTab = ref('overview') | ||
| const selectedGame = ref(null) | ||
| const tagDraft = ref('') | ||
|
|
||
| const navItems = [ | ||
| { key: 'library', label: '游戏库', icon: '📚' }, | ||
| { key: 'wishlist', label: '我的清单', icon: '⭐' }, | ||
| { key: 'sync', label: '同步中心', icon: '🔄' }, | ||
| { key: 'settings', label: '设置', icon: '⚙️' } | ||
| ] | ||
|
|
||
| const games = ref([ | ||
| { id: 1, zhName: '空洞骑士', enName: 'Hollow Knight', playtime: 41, lastPlayed: '2026-05-12', state: '在玩', tags: ['高难度', '银河恶魔城'], note: '', ach: { done: 34, total: 63, percent: 54 } }, | ||
| { id: 2, zhName: '哈迪斯', enName: 'Hades', playtime: 19, lastPlayed: '2026-05-10', state: '想玩', tags: ['肉鸽', '动作'], note: '', ach: null }, | ||
| { id: 3, zhName: '', enName: 'Portal 2', playtime: 12, lastPlayed: '2026-05-01', state: '搁置', tags: ['解谜', '剧情', '合作', '经典'], note: '补双人', ach: { done: 20, total: 51, percent: 39 } } | ||
| ]) | ||
|
|
||
| const syncTasks = ref([ | ||
| { name: '哈迪斯', stage: '下载图标', progress: 35 }, | ||
| { name: '空洞骑士', stage: '写入本地数据', progress: 92 } | ||
| ]) | ||
|
|
||
| const currentPageLabel = computed(() => navItems.find((i) => i.key === currentPage.value)?.label ?? '') | ||
| const filteredGames = computed(() => games.value.filter((g) => `${g.zhName} ${g.enName} ${g.tags.join(' ')} ${g.note}`.toLowerCase().includes(query.value.toLowerCase()))) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Update the game list computation to include Useful? React with 👍 / 👎. |
||
|
|
||
| const openDrawer = (game) => { | ||
| selectedGame.value = game | ||
| drawerTab.value = 'overview' | ||
| } | ||
|
|
||
| const addTag = () => { | ||
| const next = tagDraft.value.trim() | ||
| if (next && selectedGame.value && !selectedGame.value.tags.includes(next)) { | ||
| selectedGame.value.tags.push(next) | ||
| } | ||
| tagDraft.value = '' | ||
| } | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { createApp } from 'vue' | ||
| import App from './App.vue' | ||
| import './styles.css' | ||
|
|
||
| createApp(App).mount('#app') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| :root { font-family: 'Segoe UI Variable', 'Microsoft YaHei UI', sans-serif; color-scheme: light dark; } | ||
| * { box-sizing: border-box; } | ||
| body { margin: 0; background: #f4f6fb; color: #1f2937; } | ||
| .app { display: flex; min-height: 100vh; } | ||
| .nav-rail { width: 72px; padding: 16px 12px; display: flex; flex-direction: column; gap: 12px; border-right: 1px solid #e5e7eb; background: #fbfcff; } | ||
| .rail-btn,.primary,.ghost,select,input,textarea { height: 36px; border-radius: 10px; border: 1px solid #d1d5db; background: white; } | ||
| .rail-btn { display:grid; place-items:center; } | ||
| .rail-btn.active { border-color: #3b82f6; color: #3b82f6; } | ||
| .main-layout { flex:1; display:flex; flex-direction:column; } | ||
| .command-bar { height: 56px; display:flex; align-items:center; gap:16px; padding: 0 24px; border-bottom:1px solid #e5e7eb; background: #fff; } | ||
| .command-bar h1 { font-size:22px; margin:0; min-width: 120px; } | ||
| .search-wrap { display:flex; align-items:center; gap:8px; width:min(520px, 60vw); border:1px solid #d1d5db; border-radius: 10px; padding: 0 10px; background:#fff; height: 36px; } | ||
| .search-wrap input { flex:1; border:0; outline:none; background:transparent; height: 30px; } | ||
| .content { padding:24px; overflow:auto; } | ||
| .grid { display:grid; grid-template-columns: repeat(auto-fill,minmax(320px,1fr)); gap:16px; } | ||
| .card { background:#fff; border-radius:12px; padding:16px; display:grid; grid-template-columns:64px 1fr auto; gap:12px; box-shadow: 0 1px 3px rgba(0,0,0,.08); cursor:pointer; } | ||
| .left-icon { width:64px; height:64px; border-radius:12px; background:#eef2ff; display:grid; place-items:center; } | ||
| .mid h3 { font-size:16px; margin:0; } | ||
| .mid p { margin:2px 0; font-size:12px; color:#64748b; } | ||
| .mid small { font-size:12px; color:#6b7280; } | ||
| .right-ach { text-align:right; } | ||
| .progress { width:108px; height:6px; border-radius:999px; background:#e5e7eb; overflow:hidden; } | ||
| .progress>div { height:100%; background:#3b82f6; } | ||
| .chips { grid-column:1 / -1; display:flex; gap:8px; flex-wrap:wrap; } | ||
| .chip { border-radius:999px; padding:2px 10px; font-size:12px; background:#eef2f7; } | ||
| .chip.state { background:#dbeafe; color:#1e3a8a; } | ||
| .mask { position:fixed; inset:0; background:rgba(15,23,42,.28); } | ||
| .drawer { position:fixed; top:0; right:0; width:480px; height:100vh; background:#fff; box-shadow:-8px 0 20px rgba(0,0,0,.12); padding:16px; display:flex; flex-direction:column; } | ||
| .segmented { display:flex; gap:8px; background:#f1f5f9; border-radius:999px; padding:4px; } | ||
| .segmented button { flex:1; height:32px; border-radius:999px; border:none; } | ||
| .segmented .active { background:#fff; color:#2563eb; } | ||
| .drawer-content { overflow:auto; padding-top:12px; } | ||
| .panel { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:16px; } | ||
| .settings .setting-group { display:flex; flex-direction:column; gap:12px; } | ||
| textarea { min-height:120px; width:100%; } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { defineConfig } from 'vite' | ||
| import vue from '@vitejs/plugin-vue' | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [vue()] | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Attach a click handler that mutates state for the primary command. The button text and spinner depend on
syncing, but there is no@clickor other state transition here, so users cannot trigger sync/add behavior and the同步中…branch is effectively dead in normal use.Useful? React with 👍 / 👎.