[feat] user lists fit for many users and 3 display mode
This commit is contained in:
@@ -1,15 +1,41 @@
|
||||
import { Refresh, User, FolderFocusOne, Login, Check } from "@icon-park/react"
|
||||
import { Refresh, User, FolderFocusOne, Login, Check, List, GridFour } from "@icon-park/react"
|
||||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||
import { addToast, Button, Chip } from "@heroui/react"
|
||||
import { addToast, Button, Chip, Tabs, Tab, Tooltip } from "@heroui/react"
|
||||
import { useSteamStore } from "@/store/steam"
|
||||
import { useAppStore } from "@/store/app"
|
||||
import { ToolButton } from "../window/ToolButton"
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager"
|
||||
import path from "path"
|
||||
import { useState, useMemo } from "react"
|
||||
import type { SteamUser } from "@/types/steam"
|
||||
|
||||
type ViewMode = "card" | "list" | "list-large"
|
||||
|
||||
const SteamUsers = ({ className }: { className?: string }) => {
|
||||
const steam = useSteamStore()
|
||||
const app = useAppStore()
|
||||
const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */)
|
||||
const viewMode = app.state.steamUsersViewMode as ViewMode
|
||||
const [mockUsers, setMockUsers] = useState<SteamUser[] | null>(null)
|
||||
|
||||
// 生成模拟用户数据
|
||||
const generateMockUsers = (): SteamUser[] => {
|
||||
return Array.from({ length: 200 }, (_, i) => ({
|
||||
steam_id64: BigInt(76561198000000000 + i),
|
||||
steam_id32: 1000000 + i,
|
||||
account_name: `mockuser${i + 1}`,
|
||||
persona_name: `模拟用户 ${i + 1}`,
|
||||
recent: i < 3 ? 1 : 0,
|
||||
avatar: "",
|
||||
}))
|
||||
}
|
||||
|
||||
// 使用模拟数据或真实数据
|
||||
const displayUsers = useMemo(() => {
|
||||
return mockUsers || steam.state.users
|
||||
}, [mockUsers, steam.state.users])
|
||||
|
||||
const getUsers = async (toast?: boolean) => {
|
||||
if (!steam.state.steamDirValid) {
|
||||
@@ -21,81 +47,382 @@ const SteamUsers = ({ className }: { className?: string }) => {
|
||||
if (toast) addToast({ title: `已获取Steam用户` })
|
||||
}
|
||||
|
||||
const handleMockToggle = () => {
|
||||
if (mockUsers) {
|
||||
setMockUsers(null)
|
||||
addToast({ title: "已切换回真实数据" })
|
||||
} else {
|
||||
setMockUsers(generateMockUsers())
|
||||
addToast({ title: "已切换到模拟数据(200个用户)" })
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到剪贴板的辅助函数
|
||||
const handleCopy = async (text: string, label: string) => {
|
||||
try {
|
||||
await writeText(text)
|
||||
addToast({ title: `已复制${label}` })
|
||||
} catch (error) {
|
||||
addToast({ title: "复制失败", color: "danger" })
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染用户项 - 列表-大样式(当前样式)
|
||||
const renderListLargeItem = (user: SteamUser, id: number, isMock: boolean) => (
|
||||
<li
|
||||
key={`${user.account_name}-${id}`}
|
||||
className="flex gap-2 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50"
|
||||
>
|
||||
<img
|
||||
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||
alt="avatar"
|
||||
className="w-20 h-20 rounded-l-lg"
|
||||
draggable="false"
|
||||
/>
|
||||
<div className="flex flex-col grow justify-center gap-2 p-0.5">
|
||||
<h3
|
||||
className="text-xl font-semibold cursor-pointer hover:underline"
|
||||
onClick={() => handleCopy(user.persona_name, "用户名")}
|
||||
>
|
||||
{user.persona_name}
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<Chip
|
||||
size="sm"
|
||||
className="cursor-pointer bg-zinc-200 dark:bg-zinc-800 hover:opacity-80"
|
||||
onClick={() => handleCopy(user.account_name, "账号名")}
|
||||
>
|
||||
{user.account_name}
|
||||
</Chip>
|
||||
<Chip
|
||||
size="sm"
|
||||
className="cursor-pointer bg-zinc-200 dark:bg-zinc-800 hover:opacity-80"
|
||||
onClick={() => handleCopy(user.steam_id32.toString(), "Steam32位ID")}
|
||||
>
|
||||
{user.steam_id32}
|
||||
</Chip>
|
||||
<Chip
|
||||
size="sm"
|
||||
className="cursor-pointer bg-zinc-200 dark:bg-zinc-800 hover:opacity-80"
|
||||
onClick={() => handleCopy(user.steam_id64.toString(), "Steam64位ID")}
|
||||
>
|
||||
{user.steam_id64.toString()}
|
||||
</Chip>
|
||||
{user.recent > 0 && (
|
||||
<Chip size="sm" color="primary">
|
||||
最近登录
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end gap-2 p-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={async () => {
|
||||
if (!steam.state.steamDirValid) {
|
||||
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||
return
|
||||
}
|
||||
await invoke("open_path", {
|
||||
path: path.resolve(
|
||||
steam.state.steamDir,
|
||||
"userdata",
|
||||
user.steam_id32.toString(),
|
||||
"730",
|
||||
"local",
|
||||
"cfg"
|
||||
),
|
||||
})
|
||||
addToast({ title: "个人CFG" })
|
||||
}}
|
||||
className="gap-1"
|
||||
>
|
||||
个人CFG
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => steam.switchLoginUser(id)}
|
||||
className="gap-1"
|
||||
isDisabled={isMock}
|
||||
>
|
||||
切换登录
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => steam.selectUser(id)}
|
||||
className="gap-1"
|
||||
isDisabled={isMock}
|
||||
>
|
||||
<Check size={14} />
|
||||
选择
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
// 渲染用户项 - 列表样式(缩小版)
|
||||
const renderListItem = (user: SteamUser, id: number, isMock: boolean) => (
|
||||
<li
|
||||
key={`${user.account_name}-${id}`}
|
||||
className="flex gap-2 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50"
|
||||
>
|
||||
<img
|
||||
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||
alt="avatar"
|
||||
className="rounded-l-lg w-14 h-14"
|
||||
draggable="false"
|
||||
/>
|
||||
<div className="flex flex-col grow justify-center gap-1.5 p-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3
|
||||
className="text-base font-semibold cursor-pointer hover:underline"
|
||||
onClick={() => handleCopy(user.persona_name, "用户名")}
|
||||
>
|
||||
{user.persona_name}
|
||||
</h3>
|
||||
{user.recent > 0 && (
|
||||
<Chip size="sm" color="primary" className="text-[10px] px-1 h-5">
|
||||
最近登录
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1.5 flex-wrap items-center">
|
||||
<span
|
||||
className="text-xs cursor-pointer text-zinc-600 dark:text-zinc-400 hover:underline hover:decoration-zinc-300 dark:hover:decoration-zinc-600"
|
||||
onClick={() => handleCopy(user.account_name, "账号名")}
|
||||
>
|
||||
{user.account_name}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs cursor-pointer text-zinc-600 dark:text-zinc-400 hover:underline hover:decoration-zinc-300 dark:hover:decoration-zinc-600"
|
||||
onClick={() => handleCopy(user.steam_id32.toString(), "Steam32位ID")}
|
||||
>
|
||||
{user.steam_id32}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs cursor-pointer text-zinc-600 dark:text-zinc-400 hover:underline hover:decoration-zinc-300 dark:hover:decoration-zinc-600"
|
||||
onClick={() => handleCopy(user.steam_id64.toString(), "Steam64位ID")}
|
||||
>
|
||||
{user.steam_id64.toString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 mr-2">
|
||||
<Button
|
||||
size="sm"
|
||||
radius="full"
|
||||
onPress={async () => {
|
||||
if (!steam.state.steamDirValid) {
|
||||
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||
return
|
||||
}
|
||||
await invoke("open_path", {
|
||||
path: path.resolve(
|
||||
steam.state.steamDir,
|
||||
"userdata",
|
||||
user.steam_id32.toString(),
|
||||
"730",
|
||||
"local",
|
||||
"cfg"
|
||||
),
|
||||
})
|
||||
addToast({ title: "个人CFG" })
|
||||
}}
|
||||
className="gap-1 px-3 h-7"
|
||||
>
|
||||
个人CFG
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
radius="full"
|
||||
onPress={() => steam.switchLoginUser(id)}
|
||||
className="gap-1 px-3 h-7"
|
||||
isDisabled={isMock}
|
||||
>
|
||||
切换登录
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
radius="full"
|
||||
onPress={() => steam.selectUser(id)}
|
||||
className="gap-1 px-3 h-7"
|
||||
isDisabled={isMock}
|
||||
>
|
||||
<Check size={12} />
|
||||
选择
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
// 渲染用户项 - 卡片样式(grid布局)
|
||||
const renderCardItem = (user: SteamUser, id: number, isMock: boolean) => (
|
||||
<li
|
||||
key={`${user.account_name}-${id}`}
|
||||
className="flex flex-col gap-2 p-3 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50 h-fit"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||
alt="avatar"
|
||||
className="w-12 h-12 rounded-lg shrink-0"
|
||||
draggable="false"
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 grow">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3
|
||||
className="text-sm font-semibold leading-relaxed truncate cursor-pointer hover:underline"
|
||||
onClick={() => handleCopy(user.persona_name, "用户名")}
|
||||
>
|
||||
{user.persona_name}
|
||||
</h3>
|
||||
{user.recent > 0 && (
|
||||
<Chip size="sm" color="primary" className="text-[10px] px-1 h-5 shrink-0">
|
||||
最近登录
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className="text-xs leading-normal truncate cursor-pointer text-zinc-500 hover:underline"
|
||||
onClick={() => handleCopy(user.account_name, "账号名")}
|
||||
>
|
||||
{user.account_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
<Tooltip content="Steam32位id" showArrow={true} delay={300}>
|
||||
<span
|
||||
className="text-xs cursor-pointer text-zinc-500 dark:text-zinc-400 hover:underline"
|
||||
onClick={() => handleCopy(user.steam_id32.toString(), "Steam32位ID")}
|
||||
>
|
||||
{user.steam_id32}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip content="Steam64位id" showArrow={true} delay={300}>
|
||||
<span
|
||||
className="text-xs cursor-pointer text-zinc-500 dark:text-zinc-400 hover:underline"
|
||||
onClick={() => handleCopy(user.steam_id64.toString(), "Steam64位ID")}
|
||||
>
|
||||
{user.steam_id64.toString()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex gap-1.5 mt-auto">
|
||||
<Button
|
||||
size="sm"
|
||||
radius="full"
|
||||
onPress={async () => {
|
||||
if (!steam.state.steamDirValid) {
|
||||
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||
return
|
||||
}
|
||||
await invoke("open_path", {
|
||||
path: path.resolve(
|
||||
steam.state.steamDir,
|
||||
"userdata",
|
||||
user.steam_id32.toString(),
|
||||
"730",
|
||||
"local",
|
||||
"cfg"
|
||||
),
|
||||
})
|
||||
addToast({ title: "个人CFG" })
|
||||
}}
|
||||
className="flex h-7"
|
||||
>
|
||||
个人CFG
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
radius="full"
|
||||
onPress={() => steam.switchLoginUser(id)}
|
||||
className="flex h-7"
|
||||
isDisabled={isMock}
|
||||
>
|
||||
切换登录
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
radius="full"
|
||||
onPress={() => steam.selectUser(id)}
|
||||
className="flex h-7"
|
||||
isDisabled={isMock}
|
||||
>
|
||||
<Check size={10} />
|
||||
选择
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
// 根据视图模式渲染用户项
|
||||
const renderUserItem = (user: SteamUser, id: number) => {
|
||||
const isMock = !!mockUsers
|
||||
const realIndex = mockUsers
|
||||
? -1
|
||||
: steam.state.users.findIndex((u) => u.account_name === user.account_name)
|
||||
const index = realIndex >= 0 ? realIndex : id
|
||||
|
||||
switch (viewMode) {
|
||||
case "card":
|
||||
return renderCardItem(user, index, isMock)
|
||||
case "list":
|
||||
return renderListItem(user, index, isMock)
|
||||
case "list-large":
|
||||
default:
|
||||
return renderListLargeItem(user, index, isMock)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<Card className="flex flex-col h-full overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardIcon>
|
||||
<User /> Steam用户
|
||||
</CardIcon>
|
||||
<CardTool>
|
||||
<Tooltip content="切换样式" showArrow={true} delay={300}>
|
||||
<Tabs
|
||||
selectedKey={viewMode}
|
||||
onSelectionChange={(key) => app.setSteamUsersViewMode(key as ViewMode)}
|
||||
size="sm"
|
||||
radius="full"
|
||||
classNames={{
|
||||
base: "min-w-0",
|
||||
tabList: "gap-0 p-0",
|
||||
tab: "min-w-0 px-2",
|
||||
tabContent: "text-xs",
|
||||
}}
|
||||
>
|
||||
<Tab key="card" title={<GridFour size={14} />} />
|
||||
<Tab key="list" title={<List size={14} />} />
|
||||
<Tab key="list-large" title={<List size={16} />} />
|
||||
</Tabs>
|
||||
</Tooltip>
|
||||
{/* <ToolButton onClick={handleMockToggle}>{mockUsers ? "真实" : "模拟"}</ToolButton> */}
|
||||
<ToolButton onClick={() => getUsers(true)}>
|
||||
<Refresh />
|
||||
刷新
|
||||
</ToolButton>
|
||||
</CardTool>
|
||||
</CardHeader>
|
||||
<CardBody className="overflow-y-hidden">
|
||||
<ul
|
||||
className="flex flex-col h-full gap-3 mt-1 overflow-y-auto rounded-lg pb-11 hide-scrollbar"
|
||||
ref={parent}
|
||||
>
|
||||
{steam.state.users.map((user, id) => (
|
||||
<li
|
||||
key={user.account_name}
|
||||
className="flex gap-2 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50"
|
||||
>
|
||||
<img
|
||||
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||
alt="avatar"
|
||||
className="w-20 h-20 rounded-l-lg"
|
||||
draggable="false"
|
||||
/>
|
||||
<div className="flex flex-col flex-grow justify-center gap-2 p-0.5">
|
||||
<h3 className="text-xl font-semibold">{user.persona_name}</h3>
|
||||
<div className="flex gap-2">
|
||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||
{user.account_name}
|
||||
</Chip>
|
||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||
{user.steam_id32}
|
||||
</Chip>
|
||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||
{user.steam_id64.toString()}
|
||||
</Chip>
|
||||
{user.recent > 0 && (
|
||||
<Chip size="sm" color="primary">
|
||||
最近登录
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end gap-2 p-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={async () => {
|
||||
if (!steam.state.steamDirValid) {
|
||||
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||
return
|
||||
}
|
||||
await invoke("open_path", {
|
||||
path: path.resolve(steam.state.steamDir, "userdata", user.steam_id32.toString(), "730", "local", "cfg"),
|
||||
})
|
||||
addToast({ title: "个人CFG" })
|
||||
}} className="gap-1"
|
||||
>
|
||||
个人CFG
|
||||
</Button>
|
||||
<Button size="sm" onPress={() => steam.switchLoginUser(id)} className="gap-1">
|
||||
切换登录
|
||||
</Button>
|
||||
<Button size="sm" onPress={() => steam.selectUser(id)} className="gap-1">
|
||||
<Check size={14} />
|
||||
选择
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<CardBody className="flex-1 min-h-0 overflow-hidden">
|
||||
{viewMode === "card" ? (
|
||||
<ul
|
||||
className="grid grid-cols-2 gap-3 mt-1 overflow-y-auto rounded-lg auto-rows-max md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 pb-11 hide-scrollbar"
|
||||
ref={parent}
|
||||
>
|
||||
{displayUsers.map((user, id) => renderUserItem(user, id))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
className="flex flex-col h-full gap-3 mt-1 overflow-y-auto rounded-lg pb-11 hide-scrollbar"
|
||||
ref={parent}
|
||||
>
|
||||
{displayUsers.map((user, id) => renderUserItem(user, id))}
|
||||
</ul>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ const defaultValue = {
|
||||
autoStart: false,
|
||||
startHidden: false,
|
||||
hiddenOnClose: false,
|
||||
steamUsersViewMode: "list-large" as "card" | "list" | "list-large",
|
||||
}
|
||||
|
||||
export const appStore = store("app", { ...defaultValue }, DEFAULT_STORE_CONFIG)
|
||||
@@ -32,6 +33,7 @@ export const useAppStore = () => {
|
||||
setAutoStart,
|
||||
setStartHidden,
|
||||
setHiddenOnClose,
|
||||
setSteamUsersViewMode,
|
||||
resetAppStore,
|
||||
}
|
||||
}
|
||||
@@ -75,6 +77,10 @@ const setHiddenOnClose = (hiddenOnClose: boolean) => {
|
||||
appStore.state.hiddenOnClose = hiddenOnClose;
|
||||
}
|
||||
|
||||
const setSteamUsersViewMode = (viewMode: "card" | "list" | "list-large") => {
|
||||
appStore.state.steamUsersViewMode = viewMode
|
||||
}
|
||||
|
||||
const resetAppStore = () => {
|
||||
setVersion(defaultValue.version)
|
||||
setHasUpdate(defaultValue.hasUpdate)
|
||||
@@ -84,4 +90,5 @@ const resetAppStore = () => {
|
||||
setAutoStart(defaultValue.autoStart)
|
||||
void setStartHidden(defaultValue.startHidden)
|
||||
setHiddenOnClose(defaultValue.hiddenOnClose)
|
||||
setSteamUsersViewMode(defaultValue.steamUsersViewMode)
|
||||
}
|
||||
Reference in New Issue
Block a user