Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.eslintrc.js
do not edit these files
generated
8 changes: 4 additions & 4 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:react/jsx-runtime',
"plugin:react-hooks/recommended"
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
Expand All @@ -25,8 +25,8 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',

project: './tsconfig.json',
tsconfigRootDir: './',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
plugins: ['react', '@typescript-eslint', 'prettier', 'import'],
rules: {
Expand All @@ -36,7 +36,7 @@ module.exports = {
'react/self-closing-comp': 'error',
'no-unused-vars': 'off',
eqeqeq: 'error',
"react-hooks/exhaustive-deps": "error",
'react-hooks/exhaustive-deps': 'error',
},
settings: {
react: {
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ dist-ssr
*.sw?

build/
public/locales/*.json
src/i18n/generated
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2025-05-11

- 添加 i18n 及英文翻译 [@Constrat](https://github.com/Constrat) [@guansss](https://github.com/guansss)

## 2025-05-02

- 添加用户名重复校验 [@dragove](https://github.com/dragove)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# maa-copilot-frontend
# zoot-plus-frontend

MAA 作业站前端
ZOOT Plus 前端

## 文档

- ~~后端接口文档~~ (暂无,请参考 [maa-copilot-client](https://github.com/MaaAssistantArknights/maa-copilot-client-ts) 的 TS 类型,或者从后端 [Actions](https://github.com/MaaAssistantArknights/MaaBackendCenter/actions/workflows/openapi.yml) 的 Artifacts 里下载最新的 OpenAPI 文档)
- 作业格式:[MAA 战斗流程协议](https://maa.plus/docs/zh-cn/protocol/copilot-schema.html)
- ~~后端接口文档~~ (暂无,请参考 [zoot-plus-client](https://github.com/ZOOT-Plus/zoot-plus-client-ts) 的 TS 类型,或者从后端 [Actions](https://github.com/ZOOT-Plus/ZootPlusBackend/actions/workflows/openapi.yml) 的 Artifacts 里下载最新的 OpenAPI 文档)
- 作业格式:[战斗流程协议](https://maa.plus/docs/zh-cn/protocol/copilot-schema.html)

更新 maa-copilot-client 时,需要在 [Tags](https://github.com/MaaAssistantArknights/maa-copilot-client-ts/tags) 中复制版本号,然后替换掉 `package.json` 中的 `maa-copilot-client` 版本号,再运行 `yarn` 安装依赖
更新 zoot-plus-client 时,需要在 [Tags](https://github.com/ZOOT-Plus/zoot-plus-client-ts/tags) 中复制版本号,然后替换掉 `package.json` 中的 `maa-copilot-client` 版本号,再运行 `yarn` 安装依赖

## 开发流程

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"linkifyjs": "^3.0.5",
"lodash-es": "^4.17.21",
"maa-copilot-client": "https://github.com/MaaAssistantArknights/maa-copilot-client-ts.git#0.1.0-SNAPSHOT.824.f8ad839",
"mitt": "^3.0.1",
"normalize.css": "^8.0.1",
"prettier": "^3.2.5",
"react": "^18.0.0",
Expand Down
87 changes: 87 additions & 0 deletions scripts/generate-translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { isObject } from 'lodash'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { inspect } from 'node:util'
import { Plugin } from 'vite'

const translationsFile = fileURLToPath(
new URL('../src/i18n/translations.json', import.meta.url),
)
const outputDir = fileURLToPath(
new URL('../src/i18n/generated', import.meta.url),
)

export function generateTranslations(): Plugin {
splitTranslations()

return {
name: 'generate-translations',
apply: 'serve',
configureServer(server) {
server.watcher.on('change', (filePath) => {
if (filePath === translationsFile) {
try {
splitTranslations()
} catch (e) {
console.error('Failed to generate translations:', e)
}
}
})
},
}
}

function splitTranslations() {
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true })
}

const translationsJson = readFileSync(translationsFile, 'utf-8')
const languages = Object.keys(
JSON.parse(translationsJson).essentials.language,
)
const essentials: Record<string, Record<string, unknown>> = {}

for (const language of languages) {
const languageTranslations = JSON.parse(translationsJson, (key, value) => {
if (
isObject(value) &&
Object.keys(value).some((key) => languages.includes(key))
) {
return value[language] || '__NOT_TRANSLATED__'
}
return value
})

essentials[language] = languageTranslations.essentials
delete languageTranslations.essentials

writeTsFile(`${language}.ts`, languageTranslations)
}

writeTsFile(`essentials.ts`, essentials)

console.log(`Translations generated for ${languages.join(', ')}`)
}

function writeTsFile(filename: string, jsonObject: Record<string, unknown>) {
const filePath = join(outputDir, filename)

const literalObject = inspect(jsonObject, {
depth: 100,
maxStringLength: Infinity,
breakLength: Infinity,
sorted: false,
compact: false,
})

const content = `// This file is auto-generated by generate-translations.ts, do not edit it directly.
export default ${literalObject} as const`

if (existsSync(filePath) && readFileSync(filePath, 'utf-8') === content) {
// skip writing to avoid unnecessary hot reloads
return
}
writeFileSync(filePath, content, 'utf-8')
}
25 changes: 13 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { authAtom } from 'store/auth'
import { TokenManager } from 'utils/token-manager'

import { GlobalErrorBoundary } from './components/GlobalErrorBoundary'
import { I18NProvider } from './i18n/I18NProvider'
import { FCC } from './types'

// jotai 在没有 Provider 时会使用默认的 store
Expand All @@ -14,18 +15,18 @@ TokenManager.setAuthSetter((v) => getDefaultStore().set(authAtom, v))

export const App: FCC = ({ children }) => {
return (
<>
<SWRConfig
value={{
focusThrottleInterval: 1000 * 60,
errorRetryInterval: 1000 * 3,
errorRetryCount: 3,
}}
>
<GlobalErrorBoundary>
<SWRConfig
value={{
focusThrottleInterval: 1000 * 60,
errorRetryInterval: 1000 * 3,
errorRetryCount: 3,
}}
>
<GlobalErrorBoundary>
<I18NProvider>
<BrowserRouter>{children}</BrowserRouter>
</GlobalErrorBoundary>
</SWRConfig>
</>
</I18NProvider>
</GlobalErrorBoundary>
</SWRConfig>
)
}
3 changes: 2 additions & 1 deletion src/apis/announcement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import useSWR from 'swr'

import { i18n } from '../i18n/i18n'
import mockFile from './mock/announcements.md?url'

const isMock = process.env.NODE_ENV === 'development'
Expand All @@ -20,7 +21,7 @@ export function useAnnouncement() {
.then((res) => res.text())
.catch((e) => {
if ((e as Error).message === 'Failed to fetch') {
throw new Error('网络错误')
throw new Error(i18n.apis.announcement.network_error)
}

throw e
Expand Down
4 changes: 3 additions & 1 deletion src/apis/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import useSWRInfinite from 'swr/infinite'

import { CommentApi } from 'utils/maa-copilot-client'

import { i18n } from '../i18n/i18n'
import { CommentRating } from '../models/comment'
import { Operation } from '../models/operation'

Expand Down Expand Up @@ -35,7 +36,7 @@ export function useComments({
}

if (!isFinite(+operationId)) {
throw new Error('operationId is not a valid number')
throw new Error(i18n.apis.comment.invalid_operation_id)
}

return [
Expand Down Expand Up @@ -86,6 +87,7 @@ export async function sendComment(req: {
copilotId: req.operationId,
fromCommentId: req.fromCommentId,
notification: false,
commentStatus: 'ENABLED',
},
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/apis/mock/announcements.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!--
<!--
可以写注释,会过滤掉
TODO: 支持 HTML 标签
-->
Expand Down
37 changes: 24 additions & 13 deletions src/components/AccountManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { LoginPanel } from 'components/account/LoginPanel'
import { authAtom } from 'store/auth'
import { useCurrentSize } from 'utils/useCurrenSize'

import { useTranslation } from '../i18n/i18n'
import {
GlobalErrorBoundary,
withGlobalErrorBoundary,
Expand All @@ -30,6 +31,7 @@ import { EditDialog } from './account/EditDialog'
import { RegisterPanel } from './account/RegisterPanel'

const AccountMenu: FC = () => {
const t = useTranslation()
const [authState, setAuthState] = useAtom(authAtom)
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false)
const [editDialogOpen, setEditDialogOpen] = useState(false)
Expand All @@ -39,24 +41,24 @@ const AccountMenu: FC = () => {
setAuthState({})
AppToaster.show({
intent: 'success',
message: '已退出登录',
message: t.components.AccountManager.logout_success,
})
}

return (
<>
<Alert
isOpen={logoutDialogOpen}
cancelButtonText="取消"
confirmButtonText="退出登录"
cancelButtonText={t.components.AccountManager.cancel}
confirmButtonText={t.components.AccountManager.logout}
icon="log-out"
intent="danger"
canOutsideClickCancel
onCancel={() => setLogoutDialogOpen(false)}
onConfirm={handleLogout}
>
<H4>退出登录</H4>
<p>确定要退出登录吗?</p>
<H4>{t.components.AccountManager.logout}</H4>
<p>{t.components.AccountManager.logout_confirm}</p>
</Alert>

<EditDialog
Expand All @@ -69,19 +71,22 @@ const AccountMenu: FC = () => {
<MenuItem
disabled
icon="warning-sign"
text="账号未激活,请在退出登录后,以重置密码的方式激活"
text={t.components.AccountManager.account_not_activated}
/>
)}

<MenuItem
icon="person"
text={(isSM ? authState.username + ' - ' : '') + '个人主页'}
text={
(isSM ? authState.username + ' - ' : '') +
t.components.AccountManager.profile
}
href={`/profile/${authState.userId}`}
/>
<MenuItem
shouldDismissPopover={false}
icon="edit"
text="修改信息..."
text={t.components.AccountManager.edit_info}
onClick={() => setEditDialogOpen(true)}
/>
<MenuDivider />
Expand All @@ -90,7 +95,7 @@ const AccountMenu: FC = () => {
shouldDismissPopover={false}
intent="danger"
icon="log-out"
text="退出登录"
text={t.components.AccountManager.logout}
onClick={() => setLogoutDialogOpen(true)}
/>
</Menu>
Expand All @@ -102,11 +107,12 @@ export const AccountAuthDialog: ComponentType<{
open?: boolean
onClose?: () => void
}> = withGlobalErrorBoundary(({ open, onClose }) => {
const t = useTranslation()
const [activeTab, setActiveTab] = useState<TabId>('login')

return (
<Dialog
title="PRTS Plus 账户"
title={t.components.AccountManager.maa_account}
icon="user"
isOpen={open}
onClose={onClose}
Expand All @@ -127,7 +133,9 @@ export const AccountAuthDialog: ComponentType<{
title={
<div>
<Icon icon="person" />
<span className="ml-1">登录</span>
<span className="ml-1">
{t.components.AccountManager.login}
</span>
</div>
}
panel={
Expand All @@ -142,7 +150,9 @@ export const AccountAuthDialog: ComponentType<{
title={
<div>
<Icon icon="new-person" />
<span className="ml-1">注册</span>
<span className="ml-1">
{t.components.AccountManager.register}
</span>
</div>
}
panel={<RegisterPanel onComplete={() => setActiveTab('login')} />}
Expand All @@ -155,6 +165,7 @@ export const AccountAuthDialog: ComponentType<{
})

export const AccountManager: ComponentType = withGlobalErrorBoundary(() => {
const t = useTranslation()
const [open, setOpen] = useState(false)
const [authState] = useAtom(authAtom)
const { isSM } = useCurrentSize()
Expand All @@ -173,7 +184,7 @@ export const AccountManager: ComponentType = withGlobalErrorBoundary(() => {
</Popover2>
) : (
<Button className="ml-auto" icon="user" onClick={() => setOpen(true)}>
{!isSM && '登录 / 注册'}
{!isSM && t.components.AccountManager.login_register}
</Button>
)}
</>
Expand Down
Loading