Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "autopcr_web",
"private": true,
"version": "1.8.0",
"version": "1.8.0-tw",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
19 changes: 15 additions & 4 deletions src/api/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,24 @@ export async function delAccount(alias: string) {
return response.data;
}

export async function putAccount(alias: string, account: string, password: string, channel: string, batch_accounts: (string | number)[]) {
const response = await API.put<DefaultResponse>(`/account/${alias}`, {
export async function putAccount(
alias: string,
account: string,
password: string,
channel: string,
batch_accounts: (string | number)[],
viewer_id?: number,
) {
const payload: Record<string, unknown> = {
username: account,
password: password,
channel: channel,
batch_accounts: batch_accounts
});
batch_accounts: batch_accounts,
};
if (channel === '台服') {
payload.viewer_id = viewer_id ?? 0;
}
const response = await API.put<DefaultResponse>(`/account/${alias}`, payload);
return response.data;
}

Expand Down
96 changes: 60 additions & 36 deletions src/components/Account/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import { toaster } from '../../components/ui/toaster';

interface InfoProps {
accountInfo: AccountResponse;
onSaveSuccess?: () => void; // 添加回调函数属性
onSaveSuccess?: () => void;
}

const TW_CHANNEL = '台服';

const fadeEntry = keyframes`
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
Expand All @@ -41,32 +43,42 @@ export default function Info({ accountInfo, onSaveSuccess }: InfoProps) {
const [username, setUsername] = useState<string>(accountInfo?.username);
const [password, setPassword] = useState<string>(accountInfo?.password);
const [channel, setChannel] = useState<string>(accountInfo?.channel);
const [viewerId, setViewerId] = useState<string>(
accountInfo?.viewer_id ? String(accountInfo.viewer_id) : '',
);
const [batchAccounts, setBatchAccounts] = useState<(string | number)[]>(accountInfo?.batch_accounts || []);
const { open: isOpen, onOpen, onClose } = useDisclosure();

// 全选状态
const [allChecked, setAllChecked] = useState<boolean>(false);
// 未选择的账号列表
const [unselectedAccounts, setUnselectedAccounts] = useState<(string | number)[]>([]);
const isTw = channel === TW_CHANNEL;

useEffect(() => {
setUsername(accountInfo?.username);
setPassword(accountInfo?.password);
setChannel(accountInfo?.channel);
setViewerId(accountInfo?.viewer_id ? String(accountInfo.viewer_id) : '');
}, [accountInfo]);

// 初始化未选择的账号列表
useEffect(() => {
if (accountInfo?.all_accounts && accountInfo?.batch_accounts) {
const unselected = accountInfo.all_accounts.filter((account) => !accountInfo.batch_accounts.includes(account));
setUnselectedAccounts(unselected);

// 检查是否全选
setAllChecked(accountInfo.batch_accounts.length === accountInfo.all_accounts.length);
}
}, [accountInfo]);

const handleSave = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isTw && !viewerId.trim()) {
toaster.create({ title: '保存失败', description: '台服账号需要填写 Viewer ID', type: 'error' });
return;
}
onOpen();
putAccount(accountInfo?.alias, username, password, channel, batchAccounts)
const parsedViewerId = isTw ? Number(viewerId.trim()) : undefined;
putAccount(accountInfo?.alias, username, password, channel, batchAccounts, parsedViewerId)
.then((res) => {
toaster.create({ title: '保存成功', description: res, type: 'success' });
// 调用回调函数通知父组件更新数据
if (onSaveSuccess) {
onSaveSuccess();
}
Expand All @@ -84,30 +96,24 @@ export default function Info({ accountInfo, onSaveSuccess }: InfoProps) {
setAllChecked(isChecked);

if (isChecked) {
// 全选
setBatchAccounts(accountInfo?.all_accounts ? [...accountInfo.all_accounts] : []);
setUnselectedAccounts([]);
} else {
// 取消全选
setBatchAccounts([]);
setUnselectedAccounts(accountInfo?.all_accounts ? [...accountInfo.all_accounts] : []);
}
}

// 处理单个账号选择
const handleAccountToggle = (account: string | number) => {
if (batchAccounts.includes(account)) {
// 取消选择
setBatchAccounts(batchAccounts.filter((item) => item !== account));
setUnselectedAccounts([...unselectedAccounts, account]);
} else {
// 选择
setBatchAccounts([...batchAccounts, account]);
setUnselectedAccounts(unselectedAccounts.filter((item) => item !== account));
}
};

// 当选择变化时,更新全选状态
useEffect(() => {
if (accountInfo?.all_accounts) {
setAllChecked(batchAccounts.length === accountInfo.all_accounts.length);
Expand Down Expand Up @@ -142,40 +148,60 @@ export default function Info({ accountInfo, onSaveSuccess }: InfoProps) {
<Stack gap={6}>
{accountInfo?.alias !== 'BATCH_RUNNER' && (
<>
<Field label="账号" required>
<Field label="平台" required>
<NativeSelect.Root size="lg" variant="subtle">
<NativeSelect.Field
value={channel}
onChange={(e) => setChannel(e.target.value)}
>
{accountInfo?.channel_option.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</NativeSelect.Field>
</NativeSelect.Root>
</Field>
{isTw && (
<Field label="Viewer ID" required helperText="游戏内 UID,与提取器中的 viewer_id 一致">
<Input
size="lg"
placeholder="例如 1444587588"
type="number"
variant="subtle"
value={viewerId}
onChange={(e) => setViewerId(e.target.value)}
/>
</Field>
)}
<Field
label={isTw ? 'SHORT_UDID' : '账号'}
required
helperText={isTw ? '提取器给出的 SHORT_UDID 编码串' : undefined}
>
<Input
size="lg"
placeholder="请输入手机号或账号"
placeholder={isTw ? '请输入 SHORT_UDID' : '请输入手机号或账号'}
type="text"
variant="subtle"
defaultValue={accountInfo?.username || ''}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</Field>
<Field label="密码" required>
<Field
label={isTw ? 'UDID' : '密码'}
required
helperText={isTw ? '32 位十六进制 UDID' : undefined}
>
<Input
size="lg"
placeholder="请输入密码"
placeholder={isTw ? '请输入 UDID' : '请输入密码'}
type="password"
variant="subtle"
defaultValue={accountInfo?.password || ''}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Field>
<Field label="平台" required>
<NativeSelect.Root size="lg" variant="subtle">
<NativeSelect.Field
defaultValue={accountInfo?.channel}
onChange={(e) => setChannel(e.target.value)}
>
{accountInfo?.channel_option.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</NativeSelect.Field>
</NativeSelect.Root>
</Field>

<Button
size="lg"
Expand Down Expand Up @@ -214,7 +240,6 @@ export default function Info({ accountInfo, onSaveSuccess }: InfoProps) {
</Flex>

<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
{/* 左侧:未选择的账号 */}
<Card.Root variant="subtle" size="sm">
<Card.Header pb={2}>
<Text fontWeight="semibold" color="fg.muted">未选择的账号 ({unselectedAccounts.length})</Text>
Expand All @@ -241,7 +266,6 @@ export default function Info({ accountInfo, onSaveSuccess }: InfoProps) {
</Card.Body>
</Card.Root>

{/* 右侧:已选择的账号 */}
<Card.Root variant="outline" borderColor="blue.solid/20" size="sm">
<Card.Header pb={2} bg="blue.subtle/20" borderTopRadius="md">
<Text fontWeight="semibold" color="blue.fg">已选择的账号 ({batchAccounts.length})</Text>
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface AccountResponse {
* 渠道选项
*/
channel_option: string[];
/**
* 台服 Viewer ID
*/
viewer_id?: number;
/**
* 功能类型
*/
Expand Down
13 changes: 11 additions & 2 deletions src/routes/daily/_sidebar/account/$account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@ export const Route = createFileRoute('/daily/_sidebar/account/$account')({
errorComponent: () => <div> Not Found </div>,
});

function hasAccountCredentials(accountInfo?: AccountResponse) {
if (!accountInfo?.username || !accountInfo?.password) {
return false;
}
if (accountInfo.channel === '台服') {
return !!accountInfo.viewer_id;
}
return true;
}

function AccountComponent() {
const { account } = Route.useParams();
const initialAccountInfo = Route.useLoaderData<AccountResponse>();
const [accountInfo, setAccountInfo] = useState<AccountResponse>(initialAccountInfo);

const initialTab =
initialAccountInfo?.username !== '' && initialAccountInfo?.password !== '' ? '1' : '0';
const initialTab = hasAccountCredentials(initialAccountInfo) ? '1' : '0';

const [activeTab, setActiveTab] = useState<string>(initialTab);
const [favOnlyMap, setFavOnlyMap] = useState<Record<string, boolean>>({});
Expand Down
40 changes: 19 additions & 21 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,27 +100,25 @@ export default defineConfig(({ mode }) => {
}
})
],
build: {
target: 'es2020',
sourcemap: false,
brotliSize: true,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('xlsx') || id.includes('file-saver')) return 'xlsx';
if (id.includes('react-icons')) return 'icons';
if (id.includes('@chakra-ui') || id.includes('@emotion') || id.includes('framer-motion')) return 'chakra';
if (id.includes('@tanstack') || id.includes('react-location')) return 'tanstack';
if (id.includes('axios')) return 'utils';
if (id.includes('react') || id.includes('react-dom')) return 'react-vendor';

return 'vendor'
}
}
}
}
},
build: {
target: 'es2020',
sourcemap: false,
brotliSize: true,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('xlsx') || id.includes('file-saver')) return 'xlsx';
if (id.includes('react-icons')) return 'icons';
if (id.includes('@chakra-ui') || id.includes('@emotion') || id.includes('framer-motion')) return 'chakra';
if (id.includes('@tanstack') || id.includes('react-location')) return 'tanstack';
if (id.includes('axios')) return 'utils';
return 'vendor'
}
}
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
Expand Down