Skip to content

Commit af04b30

Browse files
Merge pull request #380 from MaaAssistantArknights/dev
## 2025-02-22 - 添加了关卡筛选器的联想输入功能 [@guansss](https://github.com/guansss) - 添加了用户个人页面 [@guansss](https://github.com/guansss) - 修复了在作业详情里无法正常使用鼠标回退键的问题 [@guansss](https://github.com/guansss) - 优化了作业列表和作业详情界面 [@guansss](https://github.com/guansss)
2 parents 40c57d8 + 7d09188 commit af04b30

26 files changed

+717
-226
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2025-02-22
2+
3+
- 添加了关卡筛选器的联想输入功能 [@guansss](https://github.com/guansss)
4+
- 添加了用户个人页面 [@guansss](https://github.com/guansss)
5+
- 修复了在作业详情里无法正常使用鼠标回退键的问题 [@guansss](https://github.com/guansss)
6+
- 优化了作业列表和作业详情界面 [@guansss](https://github.com/guansss)
7+
18
## 2025-02-17
29

310
- 修复了编辑作业时无法修改干员名称的问题 [@Gemini2035](https://github.com/Gemini2035)

src/apis/level.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export const useLevels = ({ suspense }: { suspense?: boolean } = {}) => {
2828
}
2929

3030
if (stageIds.has(level.stageId)) {
31-
console.warn('Duplicate level removed:', level.stageId, level.name)
3231
return false
3332
}
3433

src/apis/operation-set.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useAtomValue } from 'jotai'
21
import { noop } from 'lodash-es'
32
import {
43
CopilotSetPageRes,
@@ -9,7 +8,6 @@ import {
98
import useSWR from 'swr'
109
import useSWRInfinite from 'swr/infinite'
1110

12-
import { authAtom } from 'store/auth'
1311
import { OperationSetApi } from 'utils/maa-copilot-client'
1412
import { useSWRRefresh } from 'utils/swr'
1513

@@ -19,20 +17,18 @@ export type OrderBy = 'views' | 'hot' | 'id'
1917

2018
export interface UseOperationSetsParams {
2119
keyword?: string
22-
byMyself?: boolean
20+
creatorId?: string
2321

2422
disabled?: boolean
2523
suspense?: boolean
2624
}
2725

2826
export function useOperationSets({
2927
keyword,
30-
byMyself,
28+
creatorId,
3129
disabled,
3230
suspense,
3331
}: UseOperationSetsParams) {
34-
const { userId } = useAtomValue(authAtom)
35-
3632
const {
3733
data: pages,
3834
error,
@@ -53,14 +49,13 @@ export function useOperationSets({
5349
limit: 50,
5450
page: pageIndex + 1,
5551
keyword,
56-
creatorId: byMyself ? userId : undefined,
52+
creatorId,
5753
} satisfies CopilotSetQuery,
58-
byMyself,
5954
]
6055
},
61-
async ([, req, byMyself]) => {
56+
async ([, req]) => {
6257
const res = await new OperationSetApi({
63-
sendToken: byMyself ? 'always' : 'optional', // 如果有 token 会用来获取自己的作业集
58+
sendToken: 'optional', // 如果有 token 即可获取到私有的作业集
6459
requireData: true,
6560
}).querySets({ copilotSetQuery: req })
6661
return res.data
@@ -72,10 +67,12 @@ export function useOperationSets({
7267
)
7368

7469
const isReachingEnd = !!pages?.some((page) => !page.hasNext)
70+
const total = pages?.[0]?.total ?? 0
7571
const operationSets = pages?.map((page) => page.data).flat()
7672

7773
return {
7874
operationSets,
75+
total,
7976
error,
8077
setSize,
8178
isValidating,
@@ -130,6 +127,7 @@ export function useOperationSetSearch({
130127
if (id) {
131128
return {
132129
operationSets: [operationSet],
130+
total: operationSet ? 1 : 0,
133131
isReachingEnd: true,
134132
setSize: noop,
135133

src/apis/operation.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface UseOperationsParams {
1919
levelKeyword?: string
2020
operator?: string
2121
operationIds?: number[]
22-
byMyself?: boolean
22+
uploaderId?: string
2323

2424
disabled?: boolean
2525
suspense?: boolean
@@ -34,7 +34,7 @@ export function useOperations({
3434
levelKeyword,
3535
operator,
3636
operationIds,
37-
byMyself,
37+
uploaderId,
3838
disabled,
3939
suspense,
4040
revalidateFirstPage,
@@ -84,20 +84,19 @@ export function useOperations({
8484
orderBy,
8585
desc: descending,
8686
copilotIds: operationIds,
87-
uploaderId: byMyself ? 'me' : undefined,
87+
uploaderId,
8888
} satisfies QueriesCopilotRequest,
8989
]
9090
},
9191
async ([, req]) => {
9292
// 如果指定了 id 列表,但是列表为空,就直接返回空数据。不然要是直接传空列表,就相当于没有这个参数,
9393
// 会导致后端返回所有数据
9494
if (req.copilotIds?.length === 0) {
95-
return { data: [], hasNext: false }
95+
return { data: [], hasNext: false, total: 0 }
9696
}
9797

9898
const res = await new OperationApi({
99-
sendToken:
100-
'uploaderId' in req && req.uploaderId === 'me' ? 'always' : 'never',
99+
sendToken: 'optional',
101100
requireData: true,
102101
}).queriesCopilot(req)
103102

@@ -122,6 +121,7 @@ export function useOperations({
122121
)
123122

124123
const isReachingEnd = !!pages?.some((page) => !page.hasNext)
124+
const total = pages?.[0]?.total ?? 0
125125

126126
const _operations = pages?.map((page) => page.data).flat() ?? []
127127

@@ -135,6 +135,7 @@ export function useOperations({
135135
return {
136136
error,
137137
operations,
138+
total,
138139
setSize,
139140
isValidating,
140141
isReachingEnd,

src/apis/user.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import useSWR from 'swr'
2+
3+
import { NotFoundError } from '../utils/error'
4+
import { UserApi } from '../utils/maa-copilot-client'
5+
6+
export function useUserInfo({
7+
userId,
8+
suspense,
9+
}: {
10+
userId?: string
11+
suspense?: boolean
12+
}) {
13+
return useSWR(
14+
userId ? ['user', userId] : null,
15+
async ([, userId]) => {
16+
const res = await new UserApi({
17+
sendToken: 'never',
18+
requireData: true,
19+
}).getUserInfo({
20+
userId,
21+
})
22+
23+
// FIXME: 严谨一点!!!
24+
if (res.data.userName === '未知用户:(') {
25+
throw new NotFoundError()
26+
}
27+
28+
return res.data
29+
},
30+
{ suspense },
31+
)
32+
}

src/components/AccountManager.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,11 @@ const AccountMenu: FC = () => {
7373
/>
7474
)}
7575

76-
{isSM && (
77-
<MenuItem
78-
shouldDismissPopover={false}
79-
icon="user"
80-
text={authState.username}
81-
/>
82-
)}
83-
76+
<MenuItem
77+
icon="person"
78+
text={(isSM ? authState.username + ' - ' : '') + '个人主页'}
79+
href={`/profile/${authState.userId}`}
80+
/>
8481
<MenuItem
8582
shouldDismissPopover={false}
8683
icon="edit"

src/components/LevelSelect.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { MenuDivider, MenuItem } from '@blueprintjs/core'
2+
3+
import clsx from 'clsx'
4+
import Fuse from 'fuse.js'
5+
import { FC, Fragment, useMemo } from 'react'
6+
7+
import { useLevels } from '../apis/level'
8+
import { createCustomLevel, isHardMode } from '../models/level'
9+
import { Level } from '../models/operation'
10+
import { Suggest } from './Suggest'
11+
12+
interface LevelSelectProps {
13+
className?: string
14+
value: string
15+
onChange: (level: string) => void
16+
}
17+
18+
export const LevelSelect: FC<LevelSelectProps> = ({
19+
className,
20+
value,
21+
onChange,
22+
}) => {
23+
const { data } = useLevels()
24+
const levels = useMemo(
25+
() =>
26+
data
27+
// to simplify the list, we only show levels in normal mode
28+
.filter((level) => !isHardMode(level.stageId))
29+
.sort((a, b) => a.levelId.localeCompare(b.levelId)),
30+
[data],
31+
)
32+
33+
const fuse = useMemo(
34+
() =>
35+
new Fuse(levels, {
36+
keys: ['name', 'catTwo', 'catThree', 'stageId'],
37+
threshold: 0.3,
38+
}),
39+
[levels],
40+
)
41+
42+
// value 可以由用户输入,所以可以是任何值,只有用 stageId 才能匹配到唯一的关卡
43+
const selectedLevel = useMemo(
44+
() => levels.find((el) => el.stageId === value) ?? null,
45+
[levels, value],
46+
)
47+
48+
const search = (query: string) => {
49+
// 如果 query 和当前关卡完全匹配(也就是唯一对应),就显示同类关卡
50+
if (selectedLevel && selectedLevel.stageId === query) {
51+
let similarLevels: Level[]
52+
let headerName: string
53+
54+
if (selectedLevel.catOne === '剿灭作战') {
55+
headerName = selectedLevel.catOne
56+
similarLevels = levels.filter(
57+
(el) => el.catOne === selectedLevel.catOne,
58+
)
59+
} else if (
60+
selectedLevel.stageId.includes('rune') ||
61+
selectedLevel.stageId.includes('crisis')
62+
) {
63+
// 危机合约分类非常混乱,直接全塞到一起
64+
headerName = '危机合约'
65+
similarLevels = levels.filter(
66+
(el) => el.stageId.includes('rune') || el.stageId.includes('crisis'),
67+
)
68+
} else if (selectedLevel.catTwo) {
69+
headerName = selectedLevel.catTwo
70+
similarLevels = levels.filter(
71+
(el) => el.catTwo === selectedLevel.catTwo,
72+
)
73+
} else {
74+
// catTwo 为空的时候用 levelId 来分类
75+
headerName = '相关关卡'
76+
const levelIdPrefix = selectedLevel.levelId
77+
.split('/')
78+
.slice(0, -1)
79+
.join('/')
80+
similarLevels = levelIdPrefix
81+
? levels.filter((el) => el.levelId.startsWith(levelIdPrefix))
82+
: []
83+
}
84+
85+
if (similarLevels.length > 1) {
86+
const header = createCustomLevel(headerName)
87+
header.stageId = 'header'
88+
return [header, ...similarLevels]
89+
}
90+
}
91+
92+
return query ? fuse.search(query).map((el) => el.item) : levels
93+
}
94+
95+
return (
96+
<Suggest<Level>
97+
updateQueryOnSelect
98+
items={levels}
99+
itemListPredicate={search}
100+
onReset={() => onChange('')}
101+
className={clsx(className, selectedLevel && '[&_input]:italic')}
102+
itemRenderer={(item, { handleClick, handleFocus, modifiers }) =>
103+
item.stageId === 'header' ? (
104+
<Fragment key="header">
105+
<div className="ml-2 text-zinc-500 text-xs">{item.name}</div>
106+
<MenuDivider />
107+
</Fragment>
108+
) : (
109+
<MenuItem
110+
key={item.stageId}
111+
text={`${item.catThree} ${item.name}`}
112+
onClick={handleClick}
113+
onFocus={handleFocus}
114+
selected={modifiers.active}
115+
disabled={modifiers.disabled}
116+
/>
117+
)
118+
}
119+
selectedItem={selectedLevel}
120+
onItemSelect={(level) => onChange(level.stageId)}
121+
inputValueRenderer={(item) => item.stageId}
122+
noResults={<MenuItem disabled text="没有可选的关卡" />}
123+
inputProps={{
124+
placeholder: '关卡名、关卡类型、关卡编号',
125+
leftIcon: 'area-of-interest',
126+
large: true,
127+
size: 64,
128+
onBlur: (e) => {
129+
// 失焦时直接把 query 提交上去,用于处理关卡未匹配的情况
130+
if (value !== e.target.value) {
131+
onChange(e.target.value)
132+
}
133+
},
134+
}}
135+
/>
136+
)
137+
}

0 commit comments

Comments
 (0)