核心代码
数据获取
// composables/useSteamAPIGet.ts
import type { FetchError } from 'ofetch'
import blogConfig from '~~/blog.config'
// ==================== 基础类型定义 ====================
export type SteamStatus = 'offline' | 'online' | 'away' | 'snooze' | 'busy' | 'trading' | 'playing'
export interface SteamAvatar {
small: string
medium: string
large: string
}
export interface SteamCurrentGame {
appid: number
name: string
}
export interface SteamPlaytimeStats {
totalForever: number
totalTwoWeeks: number
}
export interface SteamUser {
steamid: string
username: string
profileUrl: string
avatar: SteamAvatar
status: SteamStatus
statusMessage: string
currentGame?: SteamCurrentGame
playtimeStats: SteamPlaytimeStats
}
export interface SteamGameAchievement {
total: number
unlocked: number
percentage: number
}
export interface SteamGameTwoWeekSummary {
appid: number
name: string
playtimeForever: number
playtimeTwoWeeks: number
price: SteamGamePrice
images: SteamGameImages
releaseDate: string
shortDescription: string
achievements?: SteamGameAchievement
}
export interface SteamGameAllTimeSummary {
appid: number
name: string
playtimeForever: number
playtimeTwoWeeks: number
price: SteamGamePrice
images: SteamGameImages
releaseDate: string
shortDescription: string
achievements?: SteamGameAchievement
}
export interface SteamGamesList {
totalCount: number
recentCount: number
recentGames: SteamGameTwoWeekSummary[]
allGames: SteamGameAllTimeSummary[]
}
export interface SteamGamePrice {
amount: number
currency: string
displayPrice: string
}
export interface SteamGameImages {
icon: string
logo: string
headerImage: string
heroImage: string
libraryHeroImage: string
}
export interface SteamGameDetail {
appid: number
name: string
playtimeForever: number
playtimeTwoWeeks: number
price: SteamGamePrice
images: SteamGameImages
releaseDate: string
shortDescription: string
achievements?: SteamGameAchievement
}
export interface SteamAchievementItem {
name: string
description: string
unlocked: boolean
unlockTime: number
images: {
icon: string
iconGray: string
}
}
export interface SteamGameAchievements {
appid: number
gameName: string
total: number
unlocked: number
percentage: number
items: SteamAchievementItem[]
}
export interface SteamAchievementsData {
totalCount: number
unlockedCount: number
unlockedPercentage: number
byGame: SteamGameAchievements[]
}
// ==================== API 响应类型 ====================
export interface SteamApiMetadata {
cached: boolean
cachedAt: string
cacheExpiry: string
fetchDuration: string
}
export interface SteamApiResponse<T> {
success: boolean
data?: T
metadata?: SteamApiMetadata
error?: string
code?: string
}
export interface SteamUserResponse {
user: SteamUser
}
export interface SteamGamesResponse {
games: SteamGamesList
}
export interface SteamGameResponse {
game: SteamGameDetail
}
export interface SteamAchievementsResponse {
achievements: SteamAchievementsData
}
// ==================== Composable 返回类型 ====================
export interface SteamDataResult {
user?: SteamUser
games?: SteamGamesList
achievements?: SteamAchievementsData
}
export interface SteamAllMetadata {
user?: SteamApiMetadata
games?: SteamApiMetadata
achievements?: SteamApiMetadata
gameDetail?: SteamApiMetadata
}
// ==================== 常量定义 ====================
/**
* 状态文本映射 (原 h)
*/
export const steamStatusTextMap: Record<SteamStatus, string> = {
offline: "离线",
online: "在线",
away: "离开",
snooze: "打盹",
busy: "忙碌",
trading: "交易中",
playing: "游戏中"
} as const
/**
* 状态颜色映射 (原 p)
*/
export const steamStatusColorMap: Record<SteamStatus, string> = {
offline: "#90a0a6",
online: "#4fc951",
away: "#ffc72c",
snooze: "#ffc72c",
busy: "#ff6554",
trading: "#6495ed",
playing: "#26d07c"
} as const
// ==================== 工具函数 ====================
/**
* 格式化游戏时长 (原 l)
* @param hours 小时数
* @returns 格式化后的字符串
*/
export const formatPlaytime = (hours: number): string => {
return hours < 1 ? "< 1 小时" : `${Math.round(hours)} 小时`
}
/**
* 格式化时间戳为可读日期
*/
export const formatSteamTime = (timestamp: number): string => {
return new Date(timestamp * 1000).toLocaleDateString('zh-CN')
}
// ==================== Composable 主函数 ====================
/**
* Steam API 获取 Composable (原 g)
* 适配 Nuxt 3,使用 $fetch 和 useRuntimeConfig
*/
export const useSteamAPIGet = () => {
const config = useRuntimeConfig()
// 响应式状态
const loading = ref(false)
const error = ref<Error | null>(null)
const userData = ref<SteamUser | null>(null)
const gamesData = ref<SteamGamesList | null>(null)
const gameDetail = ref<SteamGameDetail | null>(null)
const achievementsData = ref<SteamAchievementsData | null>(null)
const allMetadata = ref<SteamAllMetadata | null>(null)
// 从 Nuxt Runtime Config 获取 API 基础 URL,提供默认值
const baseURL = computed(() => {
return (blogConfig.Steam.status as string) || "https://steam-api-profile-palomiku.netlify.app/api"
})
// API 端点计算属性
const endpoints = computed(() => ({
user: `${baseURL.value}/steam-user`,
games: `${baseURL.value}/steam-games`,
game: (appid: number) => `${baseURL.value}/steam-game?appid=${appid}`,
achievements: `${baseURL.value}/steam-achievements`
}))
/**
* 基础请求方法 (原 o)
* 使用 Nuxt 的 $fetch (ofetch)
*/
const fetchSteamAPI = async <T>(url: string): Promise<SteamApiResponse<T> | null> => {
try {
const response = await $fetch<SteamApiResponse<T>>(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
},
cache: 'no-store'
})
return response
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
console.error("[Steam] Fetch error:", errorMessage)
return null
}
}
/**
* 获取 Steam 数据(批量获取用户、游戏列表、成就)
* @param limit 游戏数量限制 (1-100),仅影响 allGames
*/
const fetchSteamData = async (limit?: number): Promise<SteamApiResponse<SteamDataResult> & { allMetadata?: SteamAllMetadata }> => {
loading.value = true
error.value = null
try {
// 构建游戏端点 URL,支持限制数量
const gamesEndpoint = (limit && limit > 0 && limit <= 100)
? `${endpoints.value.games}?limit=${limit}`
: endpoints.value.games
// 并行请求三个端点
const [userRes, gamesRes, achievementsRes] = await Promise.all([
fetchSteamAPI<SteamUserResponse>(endpoints.value.user),
fetchSteamAPI<SteamGamesResponse>(gamesEndpoint),
fetchSteamAPI<SteamAchievementsResponse>(endpoints.value.achievements)
])
// 验证必需数据
if (!userRes?.success || !gamesRes?.success) {
const errMsg = "Failed to fetch required Steam data"
error.value = new Error(errMsg)
return {
success: false,
error: errMsg,
code: "FETCH_ERROR"
}
}
// 组装数据
const resultData: SteamDataResult = {
user: userRes.data?.user,
games: gamesRes.data?.games,
achievements: achievementsRes?.data?.achievements
}
const metaData: SteamAllMetadata = {
user: userRes.metadata,
games: gamesRes.metadata,
achievements: achievementsRes?.metadata
}
// 更新响应式状态
userData.value = resultData.user
gamesData.value = resultData.games
achievementsData.value = resultData.achievements
allMetadata.value = metaData
return {
success: true,
data: resultData,
allMetadata: metaData
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
console.error("[Steam] Fetch error:", errorMessage)
const errorObj = err instanceof Error ? err : new Error(String(err))
error.value = errorObj
return {
success: false,
error: `Fetch failed: ${errorMessage}`,
code: "FETCH_ERROR"
}
} finally {
loading.value = false
}
}
/**
* 获取单个游戏详情
* @param appid 游戏 AppID
*/
const fetchGameDetail = async (appid: number): Promise<SteamApiResponse<SteamGameDetail>> => {
loading.value = true
try {
const response = await fetchSteamAPI<SteamGameResponse>(endpoints.value.game(appid))
if (response?.success && response.data) {
gameDetail.value = response.data.game
// 更新元数据
if (allMetadata.value) {
allMetadata.value.gameDetail = response.metadata
} else {
allMetadata.value = { gameDetail: response.metadata }
}
return {
success: true,
data: response.data.game,
metadata: response.metadata
}
}
return {
success: false,
error: response?.error || "Failed to fetch game details",
code: "FETCH_ERROR"
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
console.error("[Steam] Fetch game detail error:", errorMessage)
return {
success: false,
error: `Fetch failed: ${errorMessage}`,
code: "FETCH_ERROR"
}
} finally {
loading.value = false
}
}
/**
* 刷新特定数据(支持部分刷新)
*/
const refreshUser = async () => {
const res = await fetchSteamAPI<SteamUserResponse>(endpoints.value.user)
if (res?.success) {
userData.value = res.data.user
if (allMetadata.value) allMetadata.value.user = res.metadata
}
return res
}
const refreshGames = async (limit?: number) => {
const url = limit ? `${endpoints.value.games}?limit=${limit}` : endpoints.value.games
const res = await fetchSteamAPI<SteamGamesResponse>(url)
if (res?.success) {
gamesData.value = res.data.games
if (allMetadata.value) allMetadata.value.games = res.metadata
}
return res
}
const refreshAchievements = async () => {
const res = await fetchSteamAPI<SteamAchievementsResponse>(endpoints.value.achievements)
if (res?.success) {
achievementsData.value = res.data.achievements
if (allMetadata.value) allMetadata.value.achievements = res.metadata
}
return res
}
return {
// 状态 (readonly)
loading: readonly(loading),
error: readonly(error),
userData: readonly(userData),
gamesData: readonly(gamesData),
gameDetail: readonly(gameDetail),
achievementsData: readonly(achievementsData),
allMetadata: readonly(allMetadata),
// 计算属性
endpoints: readonly(endpoints),
baseURL: readonly(baseURL),
// 方法
fetchSteamData,
fetchGameDetail,
fetchSteamAPI,
formatPlaytime,
formatSteamTime,
refreshUser,
refreshGames,
refreshAchievements,
// 便捷常量
statusTextMap: steamStatusTextMap,
statusColorMap: steamStatusColorMap
}
}
// ==================== 向后兼容的别名导出 ====================
/**
* 状态文本映射别名 (原 h as a)
* @deprecated 建议使用 useSteamAPIGet().statusTextMap 或直接使用 steamStatusTextMap
*/
export const a = steamStatusTextMap
/**
* 格式化函数别名 (原 l as f)
* @deprecated 建议使用 useSteamAPIGet().formatPlaytime 或直接使用 formatPlaytime
*/
export const f = formatPlaytime
/**
* 状态颜色映射别名 (原 p as s)
* @deprecated 建议使用 useSteamAPIGet().statusColorMap 或直接使用 steamStatusColorMap
*/
export const s = steamStatusColorMap
/**
* Composable 别名 (原 g as u)
* @deprecated 建议直接使用 useSteamAPIGet
*/
export const u = useSteamAPIGet
// 默认导出
export default useSteamAPIGet
页面组件
<script setup lang="ts">
const {
fetchSteamData,
fetchGameDetail,
userData,
statusTextMap,
statusColorMap
} = useSteamAPIGet()
// 初始加载数据
onMounted(async () => {
// 获取基础数据
await fetchSteamData(10)
// 如果用户正在游戏中,获取游戏详情
if (userData.value?.currentGame) {
await fetchGameDetail(userData.value.currentGame.appid)
}
})
</script>
<template>
<div class="SteamUser">
<div class="SteamUserHeader">
<NuxtImg class="UserHeaderAvatar" :src="`${userData?.avatar.large}`" />
<div class="UserHeaderInfo">
<div class="HeaderInfoRow">
<h2 class="RowUserName">
{{ userData?.username }}
</h2>
<div class="RowBadgeGroup">
<div class="RowBadgeCard" :style="`--status-color: ${statusColorMap[userData?.status]}`">
<span class="BadgeCardDot" />
{{ statusTextMap[userData?.status] }}
</div>
</div>
</div>
<p class="StatusInfoText" v-show="userData?.status === 'offline'">当前该用户已{{ statusTextMap[userData?.status] }}</p>
<a class="StatusInfoUrl" :href="userData?.profileUrl" target="_blank"> 访问 Steam 个人资料 → </a>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.SteamUser {
background: var(--ld-bg-card);
border: 1px solid var(--c-border);
border-radius: .8em;
padding: 1em;
transition: border-color .3s ease;
@media (max-width: 767px) {
padding: .75em;
}
.SteamUserHeader {
align-items: flex-start;
display: flex;
gap: 1em;
@media (max-width: 767px) {
gap: .75em;
}
.UserHeaderAvatar {
border: 2px solid var(--c-primary);
border-radius: 50%;
flex-shrink: 0;
height: 100px;
-o-object-fit: cover;
object-fit: cover;
width: 100px;
@media (max-width: 767px) {
height: 80px;
width: 80px;
}
@media (max-width: 480px) {
height: 70px;
width: 70px;
}
}
.UserHeaderInfo {
flex: 1;
min-width: 0;
.HeaderInfoRow {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: .75em;
margin-bottom: .5em;
@media (max-width: 767px) {
gap: .5em;
margin-bottom: .4em;
}
.RowUserName {
color: var(--c-text);
font-size: 1.25em;
font-weight: 600;
margin: 0;
word-break: break-word;
@media (max-width: 480px) {
font-size: 1em;
}
@media (max-width: 767px) {
font-size: 1.1em;
}
}
.RowBadgeGroup {
align-items: center;
display: flex;
gap: .5em;
.RowBadgeCard {
align-items: center;
background: color-mix(in srgb, var(--status-color) 15%, transparent);
border-radius: 1em;
color: var(--status-color);
display: inline-flex;
font-size: .875em;
font-weight: 500;
gap: .5em;
padding: .25em .75em;
white-space: nowrap;
@media (max-width: 767px) {
font-size: .8em;
padding: .2em .6em;
}
.BadgeCardDot {
animation: pulse-a9cdcf99 1.5s ease-in-out infinite;
background: var(--status-color);
border-radius: 50%;
display: inline-block;
height: 6px;
width: 6px;
}
}
}
}
.StatusInfoText {
color: var(--c-text-2);
font-size: .85em;
font-weight: 400;
margin: .125em 0 0;
@media (max-width: 480px) {
font-size: .8em;
}
}
.StatusInfoUrl {
color: var(--c-primary);
display: inline-block;
font-size: .875em;
font-weight: 500;
margin-top: .5em;
text-decoration: none;
transition: opacity .2s;
}
}
}
}
</style>
更新日志
V0.20260322.7688.8.0_PRE
- 1.优化
用户面板、信息面板、最近游戏、全部游戏四个模块的移动端不同尺寸的适配 - 2.分离
最近游戏、全部游戏两个模块的顶部信息栏,添加到标题模块中,并且进行特殊化配置项(即defineProps写法) - 3.对
最近游戏、全部游戏的成就显示与具体百分比显示,并且在最近游戏中新增价格显示(即price配置)
V0.20260322.6788.7.0_PRE
- 1.对
游戏面板进行分离模块,新增最近游戏与全游戏的模块,并且优化两个逻辑
V0.20260321.6358.2.0_PRE
- 1.优化
用户面板、信息面板、游戏面板三个模块的加载与逻辑运算,并且接入后端API获取模块(V0.20260321.6358.1.0_PRE更新内容第二项) - 2.优化
用户面板、信息面板、游戏面板三个模块的样式,并且重构vue template与scss的具体写法 - 3.修复在获取API的过程中出现的样式错乱
- 4.优化后端API获取模块中的数据
V0.20260321.6358.1.0_PRE
- 1.新增
用户面板、信息面板、游戏面板模块的基础框架(用于测试数据是否正常) - 2.新增后端API获取模块

评论加载中...