diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8e0113..e3223b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ +## 2025-05-25 + +- 添加干员和职业的英文名称 [@Constrat](https://github.com/Constrat) +- 修复了作业列表里的自定义关卡显示不正常的问题 [@Constrat](https://github.com/Constrat) +- 修复了部分文字的翻译显示不正常的问题 [@guansss](https://github.com/guansss) +- 添加编辑器v2 [@guansss](https://github.com/guansss) +- 优化首页搜索逻辑 [@ChingCdesu](https://github.com/ChingCdesu) [@Aliothmoon](https://github.com/Aliothmoon) [@dragove](https://github.com/dragove) + ## 2025-05-11 -- 添加 i18n 及英文翻译 [@Constrat](https://github.com/Constrat) [@guansss](https://github.com/guansss) +- 添加国际化及英文翻译 [@Constrat](https://github.com/Constrat) [@guansss](https://github.com/guansss) ## 2025-05-02 diff --git a/package.json b/package.json index fdcca45b..2e3d060b 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "@blueprintjs/core": "^4.15.1", "@blueprintjs/popover2": "^1.12.1", "@blueprintjs/select": "^4.8.18", - "@dnd-kit/core": "^6.0.5", - "@dnd-kit/sortable": "^7.0.1", - "@dnd-kit/utilities": "^3.2.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@iconify/icons-simple-icons": "^1.2.22", "@iconify/react": "^3.2.2", "@sentry/react": "^7.27.0", @@ -40,7 +40,10 @@ "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "fuse.js": "^6.6.2", + "immer": "^10.1.1", "jotai": "^2.7.0", + "jotai-devtools": "^0.11.0", + "jotai-immer": "^0.4.1", "linkify-react": "^3.0.4", "linkifyjs": "^3.0.5", "lodash-es": "^4.17.21", @@ -54,6 +57,7 @@ "react-hook-form": "^7.33.1", "react-markdown": "^8.0.5", "react-rating": "^2.0.5", + "react-resizable-panels": "^2.1.8", "react-rnd": "^10.4.1", "react-router-dom": "6", "react-spring": "^9.4.5", @@ -62,9 +66,10 @@ "remark-gfm": "^3.0.1", "snakecase-keys": "^5.4.4", "swr": "^2.2.5", - "type-fest": "^4.10.2", + "type-fest": "^4.40.1", "unfetch": "^4.2.0", - "vite-tsconfig-paths": "^3.5.0" + "vite-tsconfig-paths": "^3.5.0", + "zod": "^4.0.0-beta.20250424T163858" }, "devDependencies": { "@hookform/devtools": "^4.1.1", @@ -75,7 +80,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@typescript-eslint/parser": "^7.0.1", - "@vitejs/plugin-react": "^1.3.0", + "@vitejs/plugin-react": "^4.4.0", "autoprefixer": "^10.4.7", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", diff --git a/scripts/shared.ts b/scripts/shared.ts index c860042f..ccaa6aa8 100644 --- a/scripts/shared.ts +++ b/scripts/shared.ts @@ -1,10 +1,10 @@ import { access } from 'fs/promises' -import { uniq, uniqBy } from 'lodash-es' +import { capitalize, uniq, uniqBy } from 'lodash-es' import fetch from 'node-fetch' import { pinyin } from 'pinyin' import simplebig from 'simplebig' -type Profession = { id: string; name: string } +type Profession = { id: string; name: string; name_en?: string } type Professions = (Profession & { sub: Profession[] })[] export async function fileExists(file: string) { @@ -48,10 +48,14 @@ function transformOperatorName(name: string) { } } -const CHARACTER_TABLE_JSON_URL = +const CHARACTER_TABLE_JSON_URL_CN = 'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/character_table.json' -const UNIEQUIP_TABLE_JSON_URL = +const UNIEQUIP_TABLE_JSON_URL_CN = 'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/uniequip_table.json' +const CHARACTER_TABLE_JSON_URL_EN = + 'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData_YoStar/main/en_US/gamedata/excel/character_table.json' +const UNIEQUIP_TABLE_JSON_URL_EN = + 'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData_YoStar/main/en_US/gamedata/excel/uniequip_table.json' const CHARACTER_BLOCKLIST = [ 'char_512_aprot', // 暮落(集成战略):It's just not gonna be there. @@ -74,51 +78,85 @@ async function json(url: string) { } export async function getOperators() { - const [charTable, uniequipTable] = await Promise.all([ - json(CHARACTER_TABLE_JSON_URL), - json(UNIEQUIP_TABLE_JSON_URL), - ]) + const [charTableCN, uniequipTableCN, charTableEN, uniequipTableEN] = + await Promise.all([ + json(CHARACTER_TABLE_JSON_URL_CN), + json(UNIEQUIP_TABLE_JSON_URL_CN), + json(CHARACTER_TABLE_JSON_URL_EN), + json(UNIEQUIP_TABLE_JSON_URL_EN), + ]) - const { subProfDict } = uniequipTable + const { subProfDict: subProfDictCN, equipDict } = uniequipTableCN + const { subProfDict: subProfDictEN } = uniequipTableEN + const equipsByOperatorId = Object.values(equipDict).reduce( + (acc: Record, equip: any) => { + acc[equip.charId] ||= [] + acc[equip.charId].push(equip) + return acc + }, + {}, + ) - const opIds = Object.keys(charTable) + const opIds = Object.keys(charTableCN) const professions: Professions = [] const result = uniqBy( opIds.flatMap((id) => { - const op = charTable[id] + const op = charTableCN[id] + const enName = charTableEN[id]?.name || op.appellation || op.name + if (['TRAP'].includes(op.profession)) return [] if (!['TOKEN'].includes(op.profession)) { const prof = professions.find((p) => p.id === op.profession) if (!prof) { + const enSubProfName = + subProfDictEN?.[op.subProfessionId]?.subProfessionName || + capitalize(op.subProfessionId) + professions.push({ id: op.profession, name: PROFESSION_NAMES[op.profession], + name_en: + op.profession.charAt(0) + op.profession.slice(1).toLowerCase(), sub: [ { id: op.subProfessionId, - name: subProfDict[op.subProfessionId].subProfessionName, + name: subProfDictCN[op.subProfessionId].subProfessionName, + name_en: enSubProfName, }, ], }) } else if (!prof.sub.find((p) => p.id === op.subProfessionId)) { + const enSubProfName = + subProfDictEN?.[op.subProfessionId]?.subProfessionName || + capitalize(op.subProfessionId) + prof.sub.push({ id: op.subProfessionId, - name: subProfDict[op.subProfessionId].subProfessionName, + name: subProfDictCN[op.subProfessionId].subProfessionName, + name_en: enSubProfName, }) } } + const modules = equipsByOperatorId[id] + ?.sort((a, b) => a.charEquipOrder - b.charEquipOrder) + .map(({ typeName1, typeName2 }) => { + return typeName1 === 'ORIGINAL' ? '' : typeName2 + }) + .map((m) => (m === 'A' ? 'α' : m === 'D' ? 'Δ' : m)) return [ { id: id, prof: op.profession, subProf: op.subProfessionId, + name_en: enName, ...transformOperatorName(op.name), rarity: op.subProfessionId === 'notchar1' ? 0 : Number(op.rarity?.split('TIER_').join('') || 0), alt_name: op.appellation, + modules, }, ] }), diff --git a/src/apis/announcement.ts b/src/apis/announcement.ts index 7a930602..799cce23 100644 --- a/src/apis/announcement.ts +++ b/src/apis/announcement.ts @@ -7,7 +7,7 @@ const isMock = process.env.NODE_ENV === 'development' const announcementURL = isMock ? mockFile - : 'https://ota.maa.plus/MaaAssistantArknights/api/announcements/copilot.md' + : 'https://api.maa.plus/MaaAssistantArknights/api/announcements/copilot.md' export const announcementBaseURL = isMock ? location.href diff --git a/src/apis/operation.ts b/src/apis/operation.ts index d6d5b4f5..a1b56d9c 100644 --- a/src/apis/operation.ts +++ b/src/apis/operation.ts @@ -4,7 +4,7 @@ import { CopilotInfoStatusEnum, QueriesCopilotRequest, } from 'maa-copilot-client' -import useSWR from 'swr' +import useSWR, { SWRConfiguration } from 'swr' import useSWRInfinite from 'swr/infinite' import { toCopilotOperation } from 'models/converter' @@ -161,16 +161,15 @@ export function useRefreshOperations() { return () => refresh((key) => key.includes('operations')) } -interface UseOperationParams { +interface UseOperationParams extends SWRConfiguration { id?: number - suspense?: boolean } -export function useOperation({ id, suspense }: UseOperationParams) { +export function useOperation({ id, ...config }: UseOperationParams) { return useSWR( id ? ['operation', id] : null, () => getOperation({ id: id! }), - { suspense }, + config, ) } @@ -196,7 +195,8 @@ export async function createOperation(req: { content: string status: CopilotInfoStatusEnum }) { - await new OperationApi().uploadCopilot({ copilotCUDRequest: req }) + return (await new OperationApi().uploadCopilot({ copilotCUDRequest: req })) + .data } export async function updateOperation(req: { diff --git a/src/assets/icons/elite_0.png b/src/assets/icons/elite_0.png new file mode 100644 index 00000000..ceceb779 Binary files /dev/null and b/src/assets/icons/elite_0.png differ diff --git a/src/assets/icons/elite_1.png b/src/assets/icons/elite_1.png new file mode 100644 index 00000000..be3f1a32 Binary files /dev/null and b/src/assets/icons/elite_1.png differ diff --git a/src/assets/icons/elite_2.png b/src/assets/icons/elite_2.png new file mode 100644 index 00000000..fa1a6dcd Binary files /dev/null and b/src/assets/icons/elite_2.png differ diff --git a/src/components/ActionCard.tsx b/src/components/ActionCard.tsx index a68ef708..1baa554b 100644 --- a/src/components/ActionCard.tsx +++ b/src/components/ActionCard.tsx @@ -113,7 +113,9 @@ export const ActionCard: FC = ({ {action.preDelay ? formatDuration(action.preDelay) : '-'} - {action.rearDelay ? formatDuration(action.rearDelay) : '-'} + {action.rearDelay || action.postDelay + ? formatDuration(action.rearDelay || action.postDelay!) + : '-'} diff --git a/src/components/MasteryIcon.tsx b/src/components/MasteryIcon.tsx new file mode 100644 index 00000000..47bab714 --- /dev/null +++ b/src/components/MasteryIcon.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react' + +interface MasteryIconProps extends React.SVGProps { + mastery: number + mainClassName?: string + subClassName?: string +} + +export const MasteryIcon: FC = ({ + mastery, + mainClassName = 'fill-current', + subClassName = 'fill-gray-300 dark:fill-gray-600', + ...props +}) => { + return ( + + = 1 ? mainClassName : subClassName} + cx="50" + cy="27" + r="22" + /> + = 2 ? mainClassName : subClassName} + cx="75" + cy="70" + r="22" + /> + = 3 ? mainClassName : subClassName} + cx="25" + cy="70" + r="22" + /> + + ) +} diff --git a/src/components/OperationCard.tsx b/src/components/OperationCard.tsx index d269435f..dcd99abc 100644 --- a/src/components/OperationCard.tsx +++ b/src/components/OperationCard.tsx @@ -2,6 +2,7 @@ import { Button, Card, Elevation, H4, H5, Icon, Tag } from '@blueprintjs/core' import { Tooltip2 } from '@blueprintjs/popover2' import clsx from 'clsx' +import { useAtomValue } from 'jotai' import { CopilotInfoStatusEnum } from 'maa-copilot-client' import { copyShortCode, handleLazyDownloadJSON } from 'services/operation' @@ -11,8 +12,9 @@ import { OperationRating } from 'components/viewer/OperationRating' import { OpDifficulty, Operation } from 'models/operation' import { useLevels } from '../apis/level' -import { useTranslation } from '../i18n/i18n' +import { languageAtom, useTranslation } from '../i18n/i18n' import { createCustomLevel, findLevelByStageName } from '../models/level' +import { getLocalizedOperatorName } from '../models/operator' import { Paragraphs } from './Paragraphs' import { ReLinkDiv } from './ReLinkDiv' import { UserName } from './UserName' @@ -245,13 +247,14 @@ export const OperationCard = ({ operation }: { operation: Operation }) => { const OperatorTags = ({ operation }: { operation: Operation }) => { const t = useTranslation() + const language = useAtomValue(languageAtom) const { opers, groups } = operation.parsedContent return opers?.length || groups?.length ? (
{opers?.map(({ name, skill }, index) => ( - {`${name} ${skill ?? 1}`} + {`${getLocalizedOperatorName(name, language)} ${skill ?? 1}`} ))} {groups?.map(({ name, opers }, index) => ( @@ -261,7 +264,10 @@ const OperatorTags = ({ operation }: { operation: Operation }) => { placement="top" content={ opers - ?.map(({ name, skill }) => `${name} ${skill ?? 1}`) + ?.map( + ({ name, skill }) => + `${getLocalizedOperatorName(name, language)} ${skill ?? 1}`, + ) .join(', ') || t.components.OperationCard.no_operators } > diff --git a/src/components/OperatorFilter.tsx b/src/components/OperatorFilter.tsx index a0627113..71be153a 100644 --- a/src/components/OperatorFilter.tsx +++ b/src/components/OperatorFilter.tsx @@ -9,11 +9,11 @@ import { } from '@blueprintjs/core' import clsx from 'clsx' -import { getDefaultStore, useAtom } from 'jotai' +import { getDefaultStore, useAtom, useAtomValue } from 'jotai' import { compact } from 'lodash-es' import { FC, useEffect, useMemo, useState } from 'react' -import { useTranslation } from '../i18n/i18n' +import { languageAtom, useTranslation } from '../i18n/i18n' import { OPERATORS } from '../models/operator' import { DEFAULT_OPERATOR_FILTER, @@ -53,6 +53,7 @@ export const OperatorFilter: FC = ({ onChange, }) => { const t = useTranslation() + const language = useAtomValue(languageAtom) const [savedFilter, setSavedFilter] = useAtom(operatorFilterAtom) const [dialogOpen, setDialogOpen] = useState(false) const [editingFilter, setEditingFilter] = useState(filter) @@ -137,7 +138,7 @@ export const OperatorFilter: FC = ({ !filter.enabled && 'opacity-30', )} > - {includedOperators.map(({ id, name, rarity }) => ( + {includedOperators.map(({ id, name, name_en, rarity }) => (
= ({ id={id} rarity={rarity} /> -  {name}  +  {language === 'en' ? name_en : name} +  
))} - {excludedOperators.map(({ id, name, rarity }) => ( + {excludedOperators.map(({ id, name, name_en, rarity }) => (
= ({ id={id} rarity={rarity} /> -  {name}  {/* 两边加空格让删除线更显眼一些 */} +  {language === 'en' ? name_en : name} +   {/* 两边加空格让删除线更显眼一些 */}
))} diff --git a/src/components/OperatorSelect.tsx b/src/components/OperatorSelect.tsx index 36e4f821..9fbfac85 100644 --- a/src/components/OperatorSelect.tsx +++ b/src/components/OperatorSelect.tsx @@ -4,10 +4,11 @@ import { MultiSelect2 } from '@blueprintjs/select' import clsx from 'clsx' import Fuse from 'fuse.js' +import { useAtomValue } from 'jotai' import { compact } from 'lodash-es' import { FC, useMemo } from 'react' -import { useTranslation } from '../i18n/i18n' +import { languageAtom, useTranslation } from '../i18n/i18n' import { OPERATORS } from '../models/operator' import { useDebouncedQuery } from '../utils/useDebouncedQuery' import { OperatorAvatar } from './editor/operator/EditorOperator' @@ -26,13 +27,14 @@ export const OperatorSelect: FC = ({ onChange, }) => { const t = useTranslation() + const language = useAtomValue(languageAtom) const { query, trimmedDebouncedQuery, updateQuery, onOptionMouseDown } = useDebouncedQuery() const fuse = useMemo( () => new Fuse(OPERATORS, { - keys: ['name', 'alias', 'alt_name'], + keys: ['name', 'name_en', 'alias', 'alt_name'], threshold: 0.3, }), [], @@ -82,7 +84,7 @@ export const OperatorSelect: FC = ({ id={item.id} rarity={item.rarity} /> - {item.name} + {language === 'en' ? item.name_en : item.name}
} onClick={handleClick} @@ -129,7 +131,7 @@ export const OperatorSelect: FC = ({ id={item.id} rarity={item.rarity} /> - {item.name} + {language === 'en' ? item.name_en : item.name} )} popoverProps={{ diff --git a/src/components/Select.tsx b/src/components/Select.tsx index a9276217..1bcbc4af 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -1,4 +1,5 @@ -import { Button, ButtonProps, Label } from '@blueprintjs/core' +import { Button, ButtonProps, Classes, Label } from '@blueprintjs/core' +import { Classes as Popover2Classes } from '@blueprintjs/popover2' import { QueryList, Select2, @@ -27,8 +28,9 @@ export const Select = ({ canReset ??= selectedItem !== undefined return ( -