diff --git a/src/components/Account/Area.tsx b/src/components/Account/Area.tsx index b2311c8..76d2bab 100644 --- a/src/components/Account/Area.tsx +++ b/src/components/Account/Area.tsx @@ -1,12 +1,13 @@ import { Box, Flex, IconButton, Popover, Stack, useDisclosure } from '@chakra-ui/react'; -import { useEffect, useState, useMemo } from 'react'; +import { useCallback, 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'; +import { toaster } from '../../components/ui/toaster'; interface AreaProps { alias: string; @@ -20,70 +21,121 @@ 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; + + const hit = getCachedAreaConfig(alias, key); + if (hit) { + 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 })); + toaster.create({ + type: 'error', + title: '加载配置失败', + description: '请检查网络后重新进入该区服', + }); + } + }); + return () => { isMounted = false; }; }, [alias, key]); - const handleConfigUpdate = (configKey: string, value: ConfigValue) => { + const handleConfigUpdate = useCallback((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 }, }; + 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; @@ -145,7 +197,7 @@ export default function Area({ alias, keys: key, areaName, showOnlyFav = false } 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; } diff --git a/src/components/Account/DashBoard.tsx b/src/components/Account/DashBoard.tsx index 8e11eaf..b4eb43f 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'; @@ -39,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}`; @@ -109,16 +129,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 +199,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); @@ -189,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: '账号昵称不能为空', - }); - 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) || '网络错误', - }); + description: await getErrorDescription(err), }); - } else { - creatAccountSwitch.onToggle(); - } + }); }; const handleAccountImport = (event: ChangeEvent) => { const file = event.target.files?.[0]; + event.target.value = ''; if (file) { postAccountImport(file) .then((res) => { @@ -323,7 +366,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 已回写 Area,再展开不会回到旧值 */} + {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(); }} /> )} - + ) } diff --git a/src/routes/daily/_sidebar/account/$account.tsx b/src/routes/daily/_sidebar/account/$account.tsx index 49aed99..d52baca 100644 --- a/src/routes/daily/_sidebar/account/$account.tsx +++ b/src/routes/daily/_sidebar/account/$account.tsx @@ -3,12 +3,12 @@ import { useEffect, useMemo, useState } from 'react'; import { FiActivity, FiCheck, FiStar, FiTarget, FiUserX } from 'react-icons/fi'; import { AccountResponse } from '@interfaces/Account'; -import Area from '@components/Account/Area'; +import Area, { clearAreaConfigCache } from '@components/Account/Area'; import ConfigImportExport from '@components/Account/ConfigImportExport.tsx'; import Info from '@components/Account/Info'; import { createFileRoute } from '@tanstack/react-router'; import { getAccount, postAccountAreaDaily } from '@api/Account'; -import { toaster } from '@components/ui/toaster'; +import { toaster } from '../../../../components/ui/toaster'; export const Route = createFileRoute('/daily/_sidebar/account/$account')({ component: AccountComponent, @@ -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,12 +68,33 @@ 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 () => { + clearAreaConfigCache(alias); + }; + }, [account]); const currentArea = activeTab !== '0' && accountInfo?.area @@ -93,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 || @@ -108,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 === '中止') { @@ -131,8 +149,6 @@ function AccountComponent() { return (