From 2105ad45fdb640940b8f253b5346abcca288a98f Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:06:34 +0800 Subject: [PATCH 1/9] Update DashBoard.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 多选逻辑增强: 卡片视图下也增加了 Checkbox(原本只有 Radio),使得 selectedAccounts 可在两种视图下使用。 “清理全部”按钮不再依赖 isTableView,改为只要有选中账号就清理选中的,否则清理全部。 UI 细节: 表格视图的列宽、内边距微调。 账号名称输入框增加了自动聚焦和光标定位逻辑(useEffect + ref)。 样式优化(边框、背景、悬停效果)。 默认账号选择: 卡片视图下仍使用 RadioGroup + Radio,与 Checkbox 互不干扰。 合并影响: 新增的 Checkbox 和 Radio 共存在卡片视图中,功能分离明确(Checkbox 用于批量操作,Radio 用于默认账号)。 清理逻辑更统一,需确保 handleCleanDailyAll 正确遍历选中的账号。 输入框聚焦优化不会影响原有交互。 --- src/components/Account/DashBoard.tsx | 518 ++++++++++++++++----------- 1 file changed, 301 insertions(+), 217 deletions(-) diff --git a/src/components/Account/DashBoard.tsx b/src/components/Account/DashBoard.tsx index 8e11eaf..5d9730c 100644 --- a/src/components/Account/DashBoard.tsx +++ b/src/components/Account/DashBoard.tsx @@ -14,7 +14,6 @@ import { Text, } from '@chakra-ui/react'; import { FiActivity, FiBook, FiCheck, FiCopy, FiGrid, FiKey, FiLayers, FiList, FiStar, FiTarget, FiUpload, FiUserMinus, FiUserPlus, FiUserX, FiX } from 'react-icons/fi'; -import { Radio, RadioGroup } from '../../components/ui/radio'; import React, { ChangeEvent, useMemo, useRef } from 'react'; import { Skeleton, SkeletonText } from '../../components/ui/skeleton'; import { clearAccounts, deleteAccount, getAccountDailyResultList, getUserInfo, putUserInfo } from '@api/Account'; @@ -109,16 +108,37 @@ export function DashBoard() { }, [freshAccountInfo.open]); const handleDefaultAccount = (value: string) => { + // 后端 put_info 仅在非空时写入;清空依赖后端接受空串(见 README) putUserInfo({ default_account: value }) .then((res) => { - setUserInfo({ ...userInfo, default_account: value }); - toaster.create({ type: 'success', title: '设置默认账号成功', description: res }); + setUserInfo((prev) => (prev ? { ...prev, default_account: value } : prev)); + toaster.create({ + type: 'success', + title: value ? '设置默认账号成功' : '已取消默认账号', + description: res, + }); }) .catch((err: AxiosError) => { - toaster.create({ type: 'error', title: '设置默认账号失败', description: (err?.response?.data as string) || '网络错误' }); + toaster.create({ + type: 'error', + title: value ? '设置默认账号失败' : '取消默认账号失败', + description: (err?.response?.data as string) || '网络错误', + }); }); }; + const singleSelected = selectedAccounts.length === 1 ? selectedAccounts[0] : ''; + const singleIsDefault = !!singleSelected && userInfo?.default_account === singleSelected; + + const handleToggleDefaultForSelected = () => { + if (!singleSelected) return; + if (singleIsDefault) { + handleDefaultAccount(''); + } else { + handleDefaultAccount(singleSelected); + } + }; + const handleResetPassword = () => { NiceModal.show(resetPasswdModal, {}) .then((value) => { @@ -158,7 +178,7 @@ export function DashBoard() { }; const handleCleanDailyAll = () => { - if (isTableView && selectedAccounts.length > 0) { + if (selectedAccounts.length > 0) { for (const accountName of selectedAccounts) { const fn = handle.get(accountName); if (fn) fn(false); @@ -323,7 +343,7 @@ export function DashBoard() { onClick={handleCleanDailyAll} loading={count != 0} > - {isTableView && selectedAccounts.length > 0 ? `清选择(${selectedAccounts.length})` : '清理全部'} + {selectedAccounts.length > 0 ? `清选择(${selectedAccounts.length})` : '清理全部'} )} - - - + + + - - - - - {info?.description && - - {info?.description} - - } - {info?.description && info?.config_order.length != 0 && } - {info?.config_order.length != 0 && - - - 设置项 - { - info?.config_order.map((key) => ( - - )) - } - - - } - - - - + {/* 折叠时不挂 Config:条件渲染,兼容各版 Chakra */} + {isExpanded && ( + + + {info?.description && + + {info?.description} + + } + {info?.description && info?.config_order.length != 0 && } + {info?.config_order.length != 0 && + + + 设置项 + { + info?.config_order.map((key) => ( + + )) + } + + + } + + + )} + {isDangerous && ( { dangerConfirm.onClose(); handleExecute(); }} /> )} - + ) } From 7d7544be6380905f5d0dd17833c16d23a1b79f40 Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:08:38 +0800 Subject: [PATCH 4/9] Update Area.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增缓存机制: 引入 areaConfigCache Map,提供 getCachedAreaConfig、setCachedAreaConfig,以及导出的 clearAreaConfigCache。 useEffect 优先读取缓存,缓存未命中才请求 API。 handleConfigUpdate 同步更新缓存。 合并影响: 旧代码每次挂载都会请求,新代码会减少冗余请求,完全向后兼容。 导出的 clearAreaConfigCache 供 $account.tsx 使用,需同时更新该文件。 --- src/components/Account/Area.tsx | 143 +++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 48 deletions(-) diff --git a/src/components/Account/Area.tsx b/src/components/Account/Area.tsx index b2311c8..b87d14a 100644 --- a/src/components/Account/Area.tsx +++ b/src/components/Account/Area.tsx @@ -1,11 +1,11 @@ import { Box, Flex, IconButton, Popover, Stack, useDisclosure } from '@chakra-ui/react'; -import { useEffect, useState, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { FiCompass } from 'react-icons/fi'; -import Module from "./Module"; +import Module from './Module'; import { ConfigValue, ModuleResponse } from '@interfaces/Module'; import { Skeleton } from '../../components/ui/skeleton'; -import Toc from "./Toc"; +import Toc from './Toc'; import { getAccountConfig } from '@api/Account'; interface AreaProps { @@ -20,53 +20,98 @@ export interface TocItem { id: string; } +/** 区服配置数据缓存:只存 JSON,不占 React/DOM。UI 卸载后数据仍在。 */ +const areaConfigCache = new Map(); + +function cacheKey(alias: string, key: string) { + return `${alias}::${key}`; +} + +export function getCachedAreaConfig(alias: string, key: string) { + return areaConfigCache.get(cacheKey(alias, key)); +} + +export function setCachedAreaConfig(alias: string, key: string, data: ModuleResponse) { + areaConfigCache.set(cacheKey(alias, key), data); +} + +/** 离开账号详情 / 导入配置成功后调用,释放该账号缓存 */ +export function clearAreaConfigCache(alias?: string) { + if (!alias) { + areaConfigCache.clear(); + return; + } + for (const k of [...areaConfigCache.keys()]) { + if (k.startsWith(`${alias}::`)) areaConfigCache.delete(k); + } +} + +function mergeFavIntoConfig(alias: string, key: string, res: ModuleResponse): ModuleResponse { + const favKey = `autopcr_fav_${alias}`; + const stored = localStorage.getItem(favKey); + if (!stored) return res; + try { + const favMap = JSON.parse(stored) as Record; + const areaFavs = favMap[key] || []; + if (areaFavs.length === 0) return res; + const mergedConfig = { ...res.config }; + areaFavs.forEach((moduleKey) => { + mergedConfig[`_fav_${moduleKey}`] = true; + }); + return { ...res, config: mergedConfig }; + } catch { + return res; + } +} + export default function Area({ alias, keys: key, areaName, showOnlyFav = false }: AreaProps) { + const cached = alias && key ? getCachedAreaConfig(alias, key) : undefined; + const [state, setState] = useState<{ config: ModuleResponse | null; isLoading: boolean; - }>({ - config: null, - isLoading: true, - }); + }>(() => ({ + // 有缓存:立刻有内容,不走骨架 + config: cached ?? null, + isLoading: !cached, + })); const { open, onOpen, onClose } = useDisclosure(); useEffect(() => { let isMounted = true; - - if (alias && key) { - getAccountConfig(alias, key) - .then((res) => { - if (!isMounted) return; - - const favKey = `autopcr_fav_${alias}`; - const stored = localStorage.getItem(favKey); - let finalRes = res; - - if (stored) { - try { - const favMap = JSON.parse(stored) as Record; - const areaFavs = favMap[key] || []; - const mergedConfig = { ...res.config }; - areaFavs.forEach((moduleKey) => { - mergedConfig[`_fav_${moduleKey}`] = true; - }); - finalRes = { ...res, config: mergedConfig }; - } catch { - finalRes = res; - } - } - - setState({ config: finalRes, isLoading: false }); - }) - .catch((err) => { - if (isMounted) { - console.error(err); - setState((prev) => ({ ...prev, isLoading: false })); - } - }); + if (!alias || !key) return; + + // 已有缓存:只恢复 UI,不再请求(改过的设置已在 handleConfigUpdate 写回缓存) + const hit = getCachedAreaConfig(alias, key); + if (hit) { + // 收藏可能在别处改过,再合并一次 localStorage(极轻) + const withFav = mergeFavIntoConfig(alias, key, hit); + if (withFav !== hit) { + setCachedAreaConfig(alias, key, withFav); + } + setState({ config: withFav, isLoading: false }); + return () => { + isMounted = false; + }; } + setState({ config: null, isLoading: true }); + + getAccountConfig(alias, key) + .then((res) => { + if (!isMounted) return; + const finalRes = mergeFavIntoConfig(alias, key, res); + setCachedAreaConfig(alias, key, finalRes); + setState({ config: finalRes, isLoading: false }); + }) + .catch((err) => { + if (isMounted) { + console.error(err); + setState((prev) => ({ ...prev, isLoading: false })); + } + }); + return () => { isMounted = false; }; @@ -75,13 +120,15 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } const handleConfigUpdate = (configKey: string, value: ConfigValue) => { setState((prev) => { if (!prev.config) return prev; - return { - ...prev, - config: { - ...prev.config, - config: { ...prev.config.config, [configKey]: value }, - }, + const nextConfig: ModuleResponse = { + ...prev.config, + config: { ...prev.config.config, [configKey]: value }, }; + // 同步写缓存:切走再回来设置还在,无需重新拉 + if (alias && key) { + setCachedAreaConfig(alias, key, nextConfig); + } + return { ...prev, config: nextConfig }; }); }; @@ -145,7 +192,7 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } From f6282e777f9eb79714cdfff5b2fd71248a3b2f35 Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:23:28 +0800 Subject: [PATCH 5/9] Update Area.tsx --- src/components/Account/Area.tsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/Account/Area.tsx b/src/components/Account/Area.tsx index b87d14a..76d2bab 100644 --- a/src/components/Account/Area.tsx +++ b/src/components/Account/Area.tsx @@ -1,5 +1,5 @@ import { Box, Flex, IconButton, Popover, Stack, useDisclosure } from '@chakra-ui/react'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { FiCompass } from 'react-icons/fi'; import Module from './Module'; @@ -7,6 +7,7 @@ import { ConfigValue, ModuleResponse } from '@interfaces/Module'; import { Skeleton } from '../../components/ui/skeleton'; import Toc from './Toc'; import { getAccountConfig } from '@api/Account'; +import { toaster } from '../../components/ui/toaster'; interface AreaProps { alias: string; @@ -71,7 +72,6 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } config: ModuleResponse | null; isLoading: boolean; }>(() => ({ - // 有缓存:立刻有内容,不走骨架 config: cached ?? null, isLoading: !cached, })); @@ -82,10 +82,8 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } let isMounted = true; if (!alias || !key) return; - // 已有缓存:只恢复 UI,不再请求(改过的设置已在 handleConfigUpdate 写回缓存) const hit = getCachedAreaConfig(alias, key); if (hit) { - // 收藏可能在别处改过,再合并一次 localStorage(极轻) const withFav = mergeFavIntoConfig(alias, key, hit); if (withFav !== hit) { setCachedAreaConfig(alias, key, withFav); @@ -109,6 +107,11 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } if (isMounted) { console.error(err); setState((prev) => ({ ...prev, isLoading: false })); + toaster.create({ + type: 'error', + title: '加载配置失败', + description: '请检查网络后重新进入该区服', + }); } }); @@ -117,20 +120,22 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } }; }, [alias, key]); - const handleConfigUpdate = (configKey: string, value: ConfigValue) => { + const handleConfigUpdate = useCallback((configKey: string, value: ConfigValue) => { setState((prev) => { if (!prev.config) return prev; const nextConfig: ModuleResponse = { ...prev.config, config: { ...prev.config.config, [configKey]: value }, }; - // 同步写缓存:切走再回来设置还在,无需重新拉 - if (alias && key) { - setCachedAreaConfig(alias, key, nextConfig); - } return { ...prev, config: nextConfig }; }); - }; + }, []); + + // state.config 变化时同步到会话缓存(离开区服 UI 后仍能秒开且带最新改动) + useEffect(() => { + if (!alias || !key || !state.config) return; + setCachedAreaConfig(alias, key, state.config); + }, [alias, key, state.config]); const config = state.config; From 46e7f58d1e0979c14d82615d23fc413859613a70 Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:24:46 +0800 Subject: [PATCH 6/9] Update Config.tsx --- src/components/Account/Config.tsx | 181 ++++++++++++++++++++++-------- 1 file changed, 134 insertions(+), 47 deletions(-) diff --git a/src/components/Account/Config.tsx b/src/components/Account/Config.tsx index 51ed1a5..776b045 100644 --- a/src/components/Account/Config.tsx +++ b/src/components/Account/Config.tsx @@ -26,6 +26,8 @@ interface ConfigProps { alias: string; value: ConfigValue; info: ConfigInfo; + /** 回写父级 Area;折叠卸载后再展开必须靠它 */ + onConfigUpdate?: (key: string, value: ConfigValue) => void; } const configSaveChains = new Map>(); @@ -43,6 +45,32 @@ export function enqueueConfigSave(alias: string, task: () => Promise): Pro return next; } +/** 安全解析后端错误文案,避免 Blob/.text 抛错或 [object Object] */ +export async function getErrorDescription(err: unknown, fallback = '网络错误'): Promise { + const data = (err as { response?: { data?: unknown } })?.response?.data; + try { + if (data == null) { + if (err instanceof Error && err.message) return err.message; + return fallback; + } + if (typeof Blob !== 'undefined' && data instanceof Blob) { + const t = await data.text(); + return t || fallback; + } + if (typeof data === 'string') return data || fallback; + if (typeof data === 'object') { + try { + return JSON.stringify(data); + } catch { + return fallback; + } + } + return String(data); + } catch { + return fallback; + } +} + const ROW_H = '2.25rem'; const SINGLE_SEARCH_THRESHOLD = 30; @@ -50,10 +78,15 @@ function useConfigState( alias: string, key: string, propValue: T, + onConfigUpdate?: (key: string, value: ConfigValue) => void, transform?: (val: T) => ConfigValue, ) { const [state, setState] = useState(propValue); const mountedRef = useRef(true); + const propRef = useRef(propValue); + propRef.current = propValue; + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; useEffect(() => { setState(propValue); @@ -69,19 +102,22 @@ function useConfigState( const save = async (newValue: T): Promise => { setState(newValue); const payload = transform ? transform(newValue) : (newValue as ConfigValue); + // 先写父级缓存,折叠/切 Tab 再展开仍是新值 + onUpdateRef.current?.(key, payload); try { const res = await enqueueConfigSave(alias, () => putAccountConfig(alias, key, payload)); if (mountedRef.current) { toaster.create({ type: 'success', title: '保存成功', description: res }); } } catch (err) { - const axiosErr = err as AxiosError; + const rollback = propRef.current; + onUpdateRef.current?.(key, rollback as ConfigValue); if (mountedRef.current) { - setState(propValue); + setState(rollback); toaster.create({ type: 'error', title: '保存失败', - description: (axiosErr.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } } @@ -90,8 +126,9 @@ function useConfigState( return [state, setState, save] as const; } -function ConfigBool({ alias, value, info }: ConfigProps) { - const [checked, , save] = useConfigState(alias, info.key, value as boolean); + +function ConfigBool({ alias, value, info, onConfigUpdate }: ConfigProps) { + const [checked, , save] = useConfigState(alias, info.key, value as boolean, onConfigUpdate); return ( c.value) as number[])); - const max = Math.max(...(info.candidates.map((c) => c.value) as number[])); +function ConfigInt({ alias, value, info, onConfigUpdate }: ConfigProps) { + const candNums = (info.candidates || []).map((c) => c.value).filter((v): v is number => typeof v === 'number' && !isNaN(v)); + const min = candNums.length ? Math.min(...candNums) : 0; + const max = candNums.length ? Math.max(...candNums) : Number.MAX_SAFE_INTEGER; const [numStr, setNumStr] = useState(String(value)); const mountedRef = useRef(true); + const valueRef = useRef(value); + valueRef.current = value; + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; useEffect(() => { setNumStr(String(value)); @@ -139,6 +181,7 @@ function ConfigInt({ alias, value, info }: ConfigProps) { if (finalValue < min) finalValue = min; if (finalValue > max) finalValue = max; setNumStr(String(finalValue)); + onUpdateRef.current?.(info.key, finalValue as ConfigValue); enqueueConfigSave(alias, () => putAccountConfig(alias, info.key, finalValue as ConfigValue)) .then((res) => { @@ -146,13 +189,15 @@ function ConfigInt({ alias, value, info }: ConfigProps) { toaster.create({ type: 'success', title: '保存成功', description: res }); } }) - .catch((err: AxiosError) => { + .catch(async (err: AxiosError) => { + const rollback = valueRef.current; + onUpdateRef.current?.(info.key, rollback as ConfigValue); if (mountedRef.current) { - setNumStr(String(value)); + setNumStr(String(rollback)); toaster.create({ type: 'error', title: '保存失败', - description: (err.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } }); @@ -210,9 +255,12 @@ function ConfigInt({ alias, value, info }: ConfigProps) { ); } -function ConfigSingleSearch({ alias, value, info }: ConfigProps) { + +function ConfigSingleSearch({ alias, value, info, onConfigUpdate }: ConfigProps) { const [localValue, setLocalValue] = useState(value); const mountedRef = useRef(true); + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; useEffect(() => { setLocalValue(value); @@ -245,19 +293,22 @@ function ConfigSingleSearch({ alias, value, info }: ConfigProps) { })) as ConfigValue | undefined; if (ret === undefined) return; + // 乐观更新显示,失败再回滚 + setLocalValue(ret); + onUpdateRef.current?.(info.key, ret); const res = await enqueueConfigSave(alias, () => putAccountConfig(alias, info.key, ret)); if (mountedRef.current) { - setLocalValue(ret); toaster.create({ type: 'success', title: '保存成功', description: res }); } } catch (err) { - const axiosErr = err as AxiosError; + onUpdateRef.current?.(info.key, previousValue); + try { await NiceModal.hide(singleSelectModal); } catch { /* ignore */ } if (mountedRef.current) { setLocalValue(previousValue); toaster.create({ type: 'error', title: '保存失败', - description: (axiosErr.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } } @@ -280,12 +331,8 @@ function ConfigSingleSearch({ alias, value, info }: ConfigProps) { ); } -function ConfigSingle({ alias, value, info }: ConfigProps) { - if ((info.candidates?.length || 0) >= SINGLE_SEARCH_THRESHOLD) { - return ; - } - - const [selectValue, , save] = useConfigState(alias, info.key, value as string | number); +function ConfigSingleSelect({ alias, value, info, onConfigUpdate }: ConfigProps) { + const [selectValue, , save] = useConfigState(alias, info.key, value as string | number, onConfigUpdate); return ( @@ -315,7 +362,16 @@ function ConfigSingle({ alias, value, info }: ConfigProps) { ); } -function ConfigMulti({ alias, value, info }: ConfigProps) { +/** 路由组件:条件分支里不调用 Hook,避免违反 Rules of Hooks */ +function ConfigSingle(props: ConfigProps) { + if ((props.info.candidates?.length || 0) >= SINGLE_SEARCH_THRESHOLD) { + return ; + } + return ; +} + + +function ConfigMulti({ alias, value, info, onConfigUpdate }: ConfigProps) { const initialStrArr = useMemo( () => (value as (string | number)[]).map(String), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -324,6 +380,10 @@ function ConfigMulti({ alias, value, info }: ConfigProps) { const [groupValue, setGroupValue] = useState(initialStrArr); const mountedRef = useRef(true); + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; + const initialRef = useRef(initialStrArr); + initialRef.current = initialStrArr; useEffect(() => { setGroupValue(initialStrArr); @@ -339,22 +399,28 @@ function ConfigMulti({ alias, value, info }: ConfigProps) { const handleSave = (newStrArr: string[]) => { let postValue: ConfigValue = newStrArr; const intArr = newStrArr.map(Number); - if (intArr.length > 0 && !isNaN(intArr[0])) postValue = intArr; + if (intArr.length > 0 && intArr.every((n) => !isNaN(n))) postValue = intArr; setGroupValue(newStrArr); + onUpdateRef.current?.(info.key, postValue); enqueueConfigSave(alias, () => putAccountConfig(alias, info.key, postValue)) .then((res) => { if (mountedRef.current) { toaster.create({ type: 'success', title: '保存成功', description: res }); } }) - .catch((err: AxiosError) => { + .catch(async (err: AxiosError) => { + const rollback = initialRef.current; + let rollbackPayload: ConfigValue = rollback; + const ints = rollback.map(Number); + if (ints.length > 0 && ints.every((n) => !isNaN(n))) rollbackPayload = ints; + onUpdateRef.current?.(info.key, rollbackPayload); if (mountedRef.current) { - setGroupValue(initialStrArr); + setGroupValue(rollback); toaster.create({ type: 'error', title: '保存失败', - description: (err.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } }); @@ -386,9 +452,14 @@ function ConfigMulti({ alias, value, info }: ConfigProps) { ); } -function ConfigTime({ alias, value, info }: ConfigProps) { + +function ConfigTime({ alias, value, info, onConfigUpdate }: ConfigProps) { const [timeStr, setTimeStr] = useState(value as string); const mountedRef = useRef(true); + const valueRef = useRef(value); + valueRef.current = value; + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; useEffect(() => { setTimeStr(value as string); @@ -403,19 +474,22 @@ function ConfigTime({ alias, value, info }: ConfigProps) { const handleBlur = (e: React.FocusEvent) => { const newValue = e.target.value; + onUpdateRef.current?.(info.key, newValue as ConfigValue); enqueueConfigSave(alias, () => putAccountConfig(alias, info.key, newValue as ConfigValue)) .then((res) => { if (mountedRef.current) { toaster.create({ type: 'success', title: '保存成功', description: res }); } }) - .catch((err: AxiosError) => { + .catch(async (err: AxiosError) => { + const rollback = valueRef.current as string; + onUpdateRef.current?.(info.key, rollback as ConfigValue); if (mountedRef.current) { - setTimeStr(value as string); + setTimeStr(rollback); toaster.create({ type: 'error', title: '保存失败', - description: (err.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } }); @@ -436,10 +510,15 @@ function ConfigTime({ alias, value, info }: ConfigProps) { ); } -function ConfigText({ alias, value, info }: ConfigProps) { + +function ConfigText({ alias, value, info, onConfigUpdate }: ConfigProps) { const [textStr, setTextStr] = useState(value as string); const textareaRef = useRef(null); const mountedRef = useRef(true); + const valueRef = useRef(value); + valueRef.current = value; + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; useEffect(() => { setTextStr(value as string); @@ -462,19 +541,22 @@ function ConfigText({ alias, value, info }: ConfigProps) { const handleBlur = (e: React.FocusEvent) => { const newValue = e.target.value; + onUpdateRef.current?.(info.key, newValue as ConfigValue); enqueueConfigSave(alias, () => putAccountConfig(alias, info.key, newValue as ConfigValue)) .then((res) => { if (mountedRef.current) { toaster.create({ type: 'success', title: '保存成功', description: res }); } }) - .catch((err: AxiosError) => { + .catch(async (err: AxiosError) => { + const rollback = valueRef.current as string; + onUpdateRef.current?.(info.key, rollback as ConfigValue); if (mountedRef.current) { - setTextStr(value as string); + setTextStr(rollback); toaster.create({ type: 'error', title: '保存失败', - description: (err.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } }); @@ -497,9 +579,12 @@ function ConfigText({ alias, value, info }: ConfigProps) { ); } -function ConfigMultiSearch({ alias, value, info }: ConfigProps) { + +function ConfigMultiSearch({ alias, value, info, onConfigUpdate }: ConfigProps) { const [localValue, setLocalValue] = useState(value); const mountedRef = useRef(true); + const onUpdateRef = useRef(onConfigUpdate); + onUpdateRef.current = onConfigUpdate; useEffect(() => { setLocalValue(value); @@ -527,20 +612,22 @@ function ConfigMultiSearch({ alias, value, info }: ConfigProps) { })) as ConfigValue; if (ret === undefined) return; + // show 关闭后已 resolve,成功路径不要再 hide,否则可能进 catch 误回滚 + setLocalValue(ret); + onUpdateRef.current?.(info.key, ret); const res = await enqueueConfigSave(alias, () => putAccountConfig(alias, info.key, ret)); if (mountedRef.current) { - setLocalValue(ret); toaster.create({ type: 'success', title: '保存成功', description: res }); } - await NiceModal.hide(multiSelectModal); } catch (err) { - const axiosErr = err as AxiosError; + onUpdateRef.current?.(info.key, previousValue); + try { await NiceModal.hide(multiSelectModal); } catch { /* ignore */ } if (mountedRef.current) { setLocalValue(previousValue); toaster.create({ type: 'error', title: '保存失败', - description: (axiosErr.response?.data as string) || '网络错误', + description: await getErrorDescription(err), }); } } @@ -569,22 +656,22 @@ function ConfigMultiSearch({ alias, value, info }: ConfigProps) { ); } -export default function Config({ alias, value, info }: ConfigProps) { +export default function Config({ alias, value, info, onConfigUpdate }: ConfigProps) { switch (info?.config_type) { case 'bool': - return ; + return ; case 'int': - return ; + return ; case 'single': - return ; + return ; case 'multi': - return ; + return ; case 'time': - return ; + return ; case 'text': - return ; + return ; case 'multi_search': - return ; + return ; default: return null; } From 0de0654384ac0f2db5a2062ff4965982f4c3f7a0 Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:25:39 +0800 Subject: [PATCH 7/9] Update Module.tsx --- src/components/Account/Module.tsx | 72 ++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/components/Account/Module.tsx b/src/components/Account/Module.tsx index 1eeead5..668dab7 100644 --- a/src/components/Account/Module.tsx +++ b/src/components/Account/Module.tsx @@ -1,4 +1,5 @@ import { Box, Button, Card, Flex, HStack, Heading, Separator, Stack, Tag, useDisclosure } from '@chakra-ui/react' +import { useRef } from 'react' import { ConfigValue, ModuleInfo } from '@interfaces/Module'; import { FiChevronDown, FiCopy, FiStar } from 'react-icons/fi'; import { getAccountAreaSingleResultList, postAccountAreaSingle, putAccountConfig, getAccountConfig, putAccountConfigs } from '@api/Account'; @@ -6,7 +7,7 @@ import { getAccountAreaSingleResultList, postAccountAreaSingle, putAccountConfig import Alert from '../alert'; import { AxiosError } from 'axios'; import { Checkbox } from '../../components/ui/checkbox'; -import Config, { enqueueConfigSave } from './Config'; +import Config, { enqueueConfigSave, getErrorDescription } from './Config'; import NiceModal from '@ebay/nice-modal-react'; import ResultInfoModal from './ResultInfoModal'; import ModuleSyncModal from './ModuleSyncModal'; @@ -51,22 +52,33 @@ export default function Module({ alias, areaKey, areaName, config, info, isOpen, } favMap[areaKey] = Array.from(areaFavs); - localStorage.setItem(favKey, JSON.stringify(favMap)); + try { + localStorage.setItem(favKey, JSON.stringify(favMap)); + } catch { + toaster.create({ type: 'error', title: '收藏保存失败', description: '本地存储不可用或已满' }); + return; + } onConfigUpdate?.(`_fav_${info.key}`, isNowFav); }; + // 用 ref 记最新模块开关,快速连点失败回滚不取过期闭包 + const moduleEnabledRef = useRef(config[info.key]); + moduleEnabledRef.current = config[info.key]; + const onCheckedChange = (details: { checked: boolean | "indeterminate" }) => { const isChecked = !!details.checked; - const previousValue = config[info.key]; + const previousValue = moduleEnabledRef.current; onConfigUpdate?.(info.key, isChecked); + moduleEnabledRef.current = isChecked; enqueueConfigSave(alias, () => putAccountConfig(alias, info?.key, isChecked)).then((response) => { toaster.create({ type: 'success', title: '保存成功', description: response }); - }).catch((err: AxiosError) => { - onConfigUpdate?.(info.key, previousValue); - toaster.create({ type: 'error', title: '保存失败', description: (err.response?.data as string) || '网络错误' }); + }).catch(async (err: AxiosError) => { + onConfigUpdate?.(info.key, previousValue as ConfigValue); + moduleEnabledRef.current = previousValue; + toaster.create({ type: 'error', title: '保存失败', description: await getErrorDescription(err) }); }); }; @@ -75,10 +87,10 @@ export default function Module({ alias, areaKey, areaName, config, info, isOpen, onOpen(); postAccountAreaSingle(alias, info?.key).then(async (res) => { toaster.create({ type: 'success', title: '执行成功' }); - onClose(); await NiceModal.show(ResultInfoModal, { alias: alias, title: info?.name, resultInfo: res }); }).catch(async (err: AxiosError) => { - toaster.create({ type: 'error', title: '执行失败', description: await (err.response?.data as Blob).text() || "网络错误" }); + toaster.create({ type: 'error', title: '执行失败', description: await getErrorDescription(err) }); + }).finally(() => { onClose(); }); } @@ -88,11 +100,11 @@ export default function Module({ alias, areaKey, areaName, config, info, isOpen, toaster.create({ type: 'info', title: `正在获取${info?.name}的结果` }); onOpen(); getAccountAreaSingleResultList(alias, info?.key).then(async (res) => { - onClose(); await NiceModal.show(ResultInfoModal, { alias: alias, title: info?.name, resultInfo: res }); }).catch(async (err: AxiosError) => { + toaster.create({ type: 'error', title: '获取结果失败', description: await getErrorDescription(err) }); + }).finally(() => { onClose(); - toaster.create({ type: 'error', title: '获取结果失败', description: await (err.response?.data as Blob).text() || "网络错误" }); }); } @@ -113,8 +125,13 @@ export default function Module({ alias, areaKey, areaName, config, info, isOpen, const targetAccounts = await NiceModal.show(ModuleSyncModal, { sourceAlias: alias, moduleName: info.name }); if (!Array.isArray(targetAccounts) || targetAccounts.length === 0) return; - const normalizedTargets = targetAccounts.filter((item): item is string => typeof item === 'string'); - if (normalizedTargets.length === 0) return; + const normalizedTargets = targetAccounts + .filter((item): item is string => typeof item === 'string') + .filter((item) => item !== alias); + if (normalizedTargets.length === 0) { + toaster.create({ type: 'warning', title: '没有可同步的目标账号' }); + return; + } onOpen(); try { @@ -168,6 +185,15 @@ export default function Module({ alias, areaKey, areaName, config, info, isOpen, } } + const handleHeaderClick = () => { + // 折叠前先失焦,让 text/int/time 的 onBlur 先保存并回写父级 + const ae = document.activeElement as HTMLElement | null; + if (ae && typeof ae.blur === 'function' && ae !== document.body) { + ae.blur(); + } + window.setTimeout(() => onToggleExpand(), 0); + }; + return ( - { - // 折叠/展开前先让当前输入失焦,触发 Config 的 onBlur 保存,避免草稿被卸载丢掉 - const ae = document.activeElement as HTMLElement | null; - if (ae && typeof ae.blur === 'function') ae.blur(); - // 等 blur 的同步 onChange/onBlur 入队后再切展开态 - window.setTimeout(() => onToggleExpand(), 0); - }} - > + e.stopPropagation()} mr={{ base: 1, md: 1 }}> - {/* 折叠时不挂 Config:条件渲染,兼容各版 Chakra */} + {/* 折叠卸载设置树省内存;Config 已回写 Area,再展开不会回到旧值 */} {isExpanded && ( @@ -257,7 +273,13 @@ export default function Module({ alias, areaKey, areaName, config, info, isOpen, 设置项 { info?.config_order.map((key) => ( - + )) } From 18a6b122b80206111278fefe0c276d84233a10fe Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:26:20 +0800 Subject: [PATCH 8/9] Update $account.tsx --- .../daily/_sidebar/account/$account.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/routes/daily/_sidebar/account/$account.tsx b/src/routes/daily/_sidebar/account/$account.tsx index 3157d62..d52baca 100644 --- a/src/routes/daily/_sidebar/account/$account.tsx +++ b/src/routes/daily/_sidebar/account/$account.tsx @@ -28,7 +28,6 @@ function AccountComponent() { const [favOnlyMap, setFavOnlyMap] = useState>({}); const [cleanLoading, setCleanLoading] = useState(false); - // 与一览一致:用清理结果的 status 驱动按钮旁徽章(不常驻死字) const [cleanStatus, setCleanStatus] = useState( () => (initialAccountInfo as any)?.daily_clean_time?.status || '', ); @@ -69,20 +68,27 @@ function AccountComponent() { } }; - // 导入配置成功:账号基础信息刷新 + 丢掉旧区服配置缓存(下次进区服拉新配置) const handleImportSuccess = async () => { clearAreaConfigCache(accountInfo?.alias || account); await refreshAccountData(); }; + // 仅切换账号时重置 Tab;刷新 accountInfo 不要把用户正在看的 Tab 打回初始 useEffect(() => { setActiveTab(initialTab); setDisplayName(localStorage.getItem(`autopcr_displayName_${account}`) || account); const st = (initialAccountInfo as any)?.daily_clean_time?.status; if (st) setCleanStatus(st); - }, [initialAccountInfo, initialTab, account]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [account]); + + useEffect(() => { + setDisplayName(localStorage.getItem(`autopcr_displayName_${account}`) || account); + const st = (initialAccountInfo as any)?.daily_clean_time?.status; + if (st) setCleanStatus(st); + }, [initialAccountInfo, account]); - // 离开本账号详情(回一览 / 换账号)时清缓存,把内存还回去 + // 离开本账号详情时清缓存 useEffect(() => { const alias = account; return () => { @@ -107,12 +113,11 @@ function AccountComponent() { localStorage.getItem(`autopcr_displayName_${a}`) || displayName || a; setCleanLoading(true); - setCleanStatus(''); // 清理中先清掉旧徽章,结束后再按真实 status 显示 + setCleanStatus(''); toaster.create({ type: 'info', title: `开始为${nameForUi}清理日常...` }); try { const res = await postAccountAreaDaily(a); - // 与一览相同数据源:daily_clean_time.status const st = (res as any)?.daily_clean_time?.status || (res as any)?.status || @@ -122,7 +127,6 @@ function AccountComponent() { sessionStorage.setItem('autopcr_need_refresh_dashboard', '1'); await refreshAccountData(); - // toaster 只保留原来就能弹的那套,不另造文案体系 if (st === '错误') { toaster.create({ type: 'error', title: `${nameForUi}清日常结束` }); } else if (st === '警告' || st === '中止') { @@ -226,7 +230,6 @@ function AccountComponent() { > 立刻清理 - {/* 与一览 statusMeta 一致:出现在清理按钮右边 */} {statusMeta && ( {statusMeta.icon} From 4156e62c58b5134cd70155d436da177ab7d06c59 Mon Sep 17 00:00:00 2001 From: sakurairsora <42933202+sakurairsora@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:27:19 +0800 Subject: [PATCH 9/9] Update DashBoard.tsx --- src/components/Account/DashBoard.tsx | 277 ++++++++++++++++----------- 1 file changed, 160 insertions(+), 117 deletions(-) diff --git a/src/components/Account/DashBoard.tsx b/src/components/Account/DashBoard.tsx index 5d9730c..b4eb43f 100644 --- a/src/components/Account/DashBoard.tsx +++ b/src/components/Account/DashBoard.tsx @@ -38,6 +38,27 @@ import { useDisclosure } from '@chakra-ui/react'; import ConfigSyncModal from './ConfigSyncModal'; import type { Candidate, ConfigType, ConfigValue, ModuleResponse } from '@interfaces/Module'; +async function getErrorDescription(err: unknown, fallback = '网络错误'): Promise { + const data = (err as { response?: { data?: unknown } })?.response?.data; + try { + if (data == null) { + if (err instanceof Error && err.message) return err.message; + return fallback; + } + if (typeof Blob !== 'undefined' && data instanceof Blob) { + const t = await data.text(); + return t || fallback; + } + if (typeof data === 'string') return data || fallback; + if (typeof data === 'object') { + try { return JSON.stringify(data); } catch { return fallback; } + } + return String(data); + } catch { + return fallback; + } +} + const handle: Map void> = new Map void>(); const DISPLAY_NAME_KEY = (alias: string) => `autopcr_displayName_${alias}`; @@ -209,41 +230,43 @@ export function DashBoard() { }; const handleCreateAccount = () => { - if (creatAccountSwitch.open) { - if (!alias || alias.trim() === '') { + if (!creatAccountSwitch.open) { + creatAccountSwitch.onOpen(); + return; + } + if (!alias || alias.trim() === '') { + toaster.create({ + type: 'warning', + title: '需输入名字,此次未创建', + }); + creatAccountSwitch.onClose(); + setAlias(''); + return; + } + + postAccount(alias) + .then((res) => { + toaster.create({ + type: 'success', + title: '创建账号成功', + description: res, + }); + creatAccountSwitch.onClose(); + setAlias(''); + freshAccountInfo.onToggle(); + }) + .catch(async (err: AxiosError) => { toaster.create({ type: 'error', title: '创建账号失败', - description: '账号昵称不能为空', + description: await getErrorDescription(err), }); - return; - } - - postAccount(alias) - .then((res) => { - toaster.create({ - type: 'success', - title: '创建账号成功', - description: res, - }); - creatAccountSwitch.onToggle(); - setAlias(''); - freshAccountInfo.onToggle(); - }) - .catch((err: AxiosError) => { - toaster.create({ - type: 'error', - title: '创建账号失败', - description: (err?.response?.data as string) || '网络错误', - }); - }); - } else { - creatAccountSwitch.onToggle(); - } + }); }; const handleAccountImport = (event: ChangeEvent) => { const file = event.target.files?.[0]; + event.target.value = ''; if (file) { postAccountImport(file) .then((res) => { @@ -480,9 +503,7 @@ export function DashBoard() { variant={creatAccountSwitch.open ? "solid" : "solid"} colorPalette={creatAccountSwitch.open ? "red" : "green"} onClick={() => { - if(creatAccountSwitch.open && !alias) creatAccountSwitch.onToggle(); - else if (creatAccountSwitch.open && alias) handleCreateAccount(); - else creatAccountSwitch.onToggle(); + handleCreateAccount(); }} > {creatAccountSwitch.open ? : } @@ -526,7 +547,7 @@ export function DashBoard() { - + 0 && selectedAccounts.length < (userInfo?.accounts?.length ?? 0)) @@ -665,6 +686,7 @@ function AccountInfo({ const [displayName, setDisplayName] = useState(() => getDisplayName(alias)); const [nameDraft, setNameDraft] = useState(displayName); const composingRef = useRef(false); + const nameInputRef = useRef(null); const clean = account.daily_clean_time; const cleanStatus = clean?.status || '未知'; @@ -740,7 +762,7 @@ function AccountInfo({ }; const handleDailyResult = () => { - toaster.create({ type: 'info', title: `正在获取${displayName || alias}的日常结果...` }); + toaster.create({ type: 'info', title: `正在获取${alias}的日常结果...` }); getAccountDailyResultList(alias) .then(async (res) => { toaster.create({ type: 'success', title: '获取日常结果成功' }); @@ -750,7 +772,7 @@ function AccountInfo({ toaster.create({ type: 'error', title: '获取日常结果失败', - description: (await (err?.response?.data as Blob).text()) || '网络错误', + description: await getErrorDescription(err), }); }); }; @@ -759,6 +781,15 @@ function AccountInfo({ void navigate({ to: `${DashBoardRoute.to || ''}${alias}` as any }); }; + + useEffect(() => { + if (isEditingName) { + // autoFocus 只在挂载时生效;编辑态切换时手动 focus + const t = window.setTimeout(() => nameInputRef.current?.focus(), 0); + return () => window.clearTimeout(t); + } + }, [isEditingName]); + const commitDisplayName = () => { const next = nameDraft.trim(); // 空名 / 等于真实 alias:恢复为 alias @@ -788,10 +819,10 @@ function AccountInfo({ // 始终同一 Input:可编辑区域与名字位置重合 const nameInput = ( { e.stopPropagation(); @@ -810,10 +841,25 @@ function AccountInfo({ onCompositionEnd={(e) => { composingRef.current = false; setNameDraft((e.target as HTMLInputElement).value); + // 组词中途点到别处:compositionend 时可能已失焦,补一次提交,避免卡在编辑态 + const el = e.target as HTMLInputElement; + window.setTimeout(() => { + if (document.activeElement !== el) { + commitDisplayName(); + } + }, 0); }} onBlur={() => { if (!isEditingName) return; - if (composingRef.current) return; + if (composingRef.current) { + // 组词中 blur:等 composition 结束后由上面的 timeout 处理;再兜底一次 + window.setTimeout(() => { + if (!composingRef.current) { + commitDisplayName(); + } + }, 0); + return; + } commitDisplayName(); }} onKeyDown={(e) => { @@ -825,7 +871,7 @@ function AccountInfo({ } }} fontWeight="bold" - maxW="8em" + maxW="12em" minW="4em" h="2em" px={isEditingName ? 2 : 0} @@ -1079,6 +1125,7 @@ function AccountInfo({ onClick={goDetail} title="进入详细设置" > + {/* ✅ 补回内层 Flex 容器;曾用名 minW 保底,标签不挤占省略空间 */} )} + {/* 标签单独一组 flexShrink=0,避免反噬曾用名 */} {defaultAccount === account.name && ( @@ -1144,11 +1192,12 @@ function AccountInfo({ isOpen={deleteConfirm.open} onClose={deleteConfirm.onClose} title="删除账号" - body={`确定删除账号${displayName || alias}吗?`} + body={`确定删除账号${alias}吗?`} onConfirm={handleDeleteAccount} > {' '} + {/* 热区略小、叉图形略大 */} + {/* 表格状态列:保持可进详情(仅卡片状态区做安全点击区) */} - - - + {/* 整卡 onClick 进详情;Header/状态/按钮区内控件自行 stopPropagation */} + + {/* 整 Header 随卡片进详情;仅选框/改名/删除 stopPropagation */} + + e.stopPropagation()} + flexShrink={0} + display="flex" + alignItems="center" + justifyContent="center" + title="选择账号" + cursor="default" + > + + + + e.stopPropagation()} - flexShrink={0} display="flex" alignItems="center" - justifyContent="center" - pl={0.5} - pr={1} - py={1} - minW="2rem" - minH="2rem" - title="选择账号" + lineHeight="1" + fontSize="lg" + flex="1 1 6em" + minW="4em" + maxW={displayName !== alias ? '52%' : '75%'} + overflow="hidden" + title="" cursor="default" > - + {nameInput} - - e.stopPropagation()} - display="flex" - alignItems="center" + {displayName !== alias && ( + - {nameInput} - - {displayName !== alias && ( - - {alias} - + {alias} + + )} + + + {defaultAccount === account.name && ( + + 默认 + + )} + {account.clan_forbid && ( + + 禁用 + )} - - {defaultAccount === account.name && ( - - 默认 - - )} - {account.clan_forbid && ( - - 禁用 - - )} - - {/* 空白热区:真正吃到 native title */} - e.stopPropagation()} flexShrink={0} - pr={0.5} title="删除账号" cursor="default" - display="flex" - alignItems="center" > { e.stopPropagation(); deleteConfirm.onOpen(); }} _hover={{ bg: 'red.subtle', color: 'red.fg' }} + css={{ + '& svg': { + width: '1.4em', + height: '1.4em', + strokeWidth: 2.5, + }, + }} > - +