Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2025-02-22

- 添加了关卡筛选器的联想输入功能 [@guansss](https://github.com/guansss)
- 添加了用户个人页面 [@guansss](https://github.com/guansss)
- 修复了在作业详情里无法正常使用鼠标回退键的问题 [@guansss](https://github.com/guansss)
- 优化了作业列表和作业详情界面 [@guansss](https://github.com/guansss)

## 2025-02-17

- 修复了编辑作业时无法修改干员名称的问题 [@Gemini2035](https://github.com/Gemini2035)
Expand Down
1 change: 0 additions & 1 deletion src/apis/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const useLevels = ({ suspense }: { suspense?: boolean } = {}) => {
}

if (stageIds.has(level.stageId)) {
console.warn('Duplicate level removed:', level.stageId, level.name)
return false
}

Expand Down
18 changes: 8 additions & 10 deletions src/apis/operation-set.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useAtomValue } from 'jotai'
import { noop } from 'lodash-es'
import {
CopilotSetPageRes,
Expand All @@ -9,7 +8,6 @@ import {
import useSWR from 'swr'
import useSWRInfinite from 'swr/infinite'

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

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

export interface UseOperationSetsParams {
keyword?: string
byMyself?: boolean
creatorId?: string

disabled?: boolean
suspense?: boolean
}

export function useOperationSets({
keyword,
byMyself,
creatorId,
disabled,
suspense,
}: UseOperationSetsParams) {
const { userId } = useAtomValue(authAtom)

const {
data: pages,
error,
Expand All @@ -53,14 +49,13 @@ export function useOperationSets({
limit: 50,
page: pageIndex + 1,
keyword,
creatorId: byMyself ? userId : undefined,
creatorId,
} satisfies CopilotSetQuery,
byMyself,
]
},
async ([, req, byMyself]) => {
async ([, req]) => {
const res = await new OperationSetApi({
sendToken: byMyself ? 'always' : 'optional', // 如果有 token 会用来获取自己的作业集
sendToken: 'optional', // 如果有 token 即可获取到私有的作业集
requireData: true,
}).querySets({ copilotSetQuery: req })
return res.data
Expand All @@ -72,10 +67,12 @@ export function useOperationSets({
)

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

return {
operationSets,
total,
error,
setSize,
isValidating,
Expand Down Expand Up @@ -130,6 +127,7 @@ export function useOperationSetSearch({
if (id) {
return {
operationSets: [operationSet],
total: operationSet ? 1 : 0,
isReachingEnd: true,
setSize: noop,

Expand Down
13 changes: 7 additions & 6 deletions src/apis/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface UseOperationsParams {
levelKeyword?: string
operator?: string
operationIds?: number[]
byMyself?: boolean
uploaderId?: string

disabled?: boolean
suspense?: boolean
Expand All @@ -34,7 +34,7 @@ export function useOperations({
levelKeyword,
operator,
operationIds,
byMyself,
uploaderId,
disabled,
suspense,
revalidateFirstPage,
Expand Down Expand Up @@ -84,20 +84,19 @@ export function useOperations({
orderBy,
desc: descending,
copilotIds: operationIds,
uploaderId: byMyself ? 'me' : undefined,
uploaderId,
} satisfies QueriesCopilotRequest,
]
},
async ([, req]) => {
// 如果指定了 id 列表,但是列表为空,就直接返回空数据。不然要是直接传空列表,就相当于没有这个参数,
// 会导致后端返回所有数据
if (req.copilotIds?.length === 0) {
return { data: [], hasNext: false }
return { data: [], hasNext: false, total: 0 }
}

const res = await new OperationApi({
sendToken:
'uploaderId' in req && req.uploaderId === 'me' ? 'always' : 'never',
sendToken: 'optional',
requireData: true,
}).queriesCopilot(req)

Expand All @@ -122,6 +121,7 @@ export function useOperations({
)

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

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

Expand All @@ -135,6 +135,7 @@ export function useOperations({
return {
error,
operations,
total,
setSize,
isValidating,
isReachingEnd,
Expand Down
32 changes: 32 additions & 0 deletions src/apis/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import useSWR from 'swr'

import { NotFoundError } from '../utils/error'
import { UserApi } from '../utils/maa-copilot-client'

export function useUserInfo({
userId,
suspense,
}: {
userId?: string
suspense?: boolean
}) {
return useSWR(
userId ? ['user', userId] : null,
async ([, userId]) => {
const res = await new UserApi({
sendToken: 'never',
requireData: true,
}).getUserInfo({
userId,
})

// FIXME: 严谨一点!!!
if (res.data.userName === '未知用户:(') {
throw new NotFoundError()
}

return res.data
},
{ suspense },
)
}
13 changes: 5 additions & 8 deletions src/components/AccountManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,11 @@ const AccountMenu: FC = () => {
/>
)}

{isSM && (
<MenuItem
shouldDismissPopover={false}
icon="user"
text={authState.username}
/>
)}

<MenuItem
icon="person"
text={(isSM ? authState.username + ' - ' : '') + '个人主页'}
href={`/profile/${authState.userId}`}
/>
<MenuItem
shouldDismissPopover={false}
icon="edit"
Expand Down
137 changes: 137 additions & 0 deletions src/components/LevelSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { MenuDivider, MenuItem } from '@blueprintjs/core'

import clsx from 'clsx'
import Fuse from 'fuse.js'
import { FC, Fragment, useMemo } from 'react'

import { useLevels } from '../apis/level'
import { createCustomLevel, isHardMode } from '../models/level'
import { Level } from '../models/operation'
import { Suggest } from './Suggest'

interface LevelSelectProps {
className?: string
value: string
onChange: (level: string) => void
}

export const LevelSelect: FC<LevelSelectProps> = ({
className,
value,
onChange,
}) => {
const { data } = useLevels()
const levels = useMemo(
() =>
data
// to simplify the list, we only show levels in normal mode
.filter((level) => !isHardMode(level.stageId))
.sort((a, b) => a.levelId.localeCompare(b.levelId)),
[data],
)

const fuse = useMemo(
() =>
new Fuse(levels, {
keys: ['name', 'catTwo', 'catThree', 'stageId'],
threshold: 0.3,
}),
[levels],
)

// value 可以由用户输入,所以可以是任何值,只有用 stageId 才能匹配到唯一的关卡
const selectedLevel = useMemo(
() => levels.find((el) => el.stageId === value) ?? null,
[levels, value],
)

const search = (query: string) => {
// 如果 query 和当前关卡完全匹配(也就是唯一对应),就显示同类关卡
if (selectedLevel && selectedLevel.stageId === query) {
let similarLevels: Level[]
let headerName: string

if (selectedLevel.catOne === '剿灭作战') {
headerName = selectedLevel.catOne
similarLevels = levels.filter(
(el) => el.catOne === selectedLevel.catOne,
)
} else if (
selectedLevel.stageId.includes('rune') ||
selectedLevel.stageId.includes('crisis')
) {
// 危机合约分类非常混乱,直接全塞到一起
headerName = '危机合约'
similarLevels = levels.filter(
(el) => el.stageId.includes('rune') || el.stageId.includes('crisis'),
)
} else if (selectedLevel.catTwo) {
headerName = selectedLevel.catTwo
similarLevels = levels.filter(
(el) => el.catTwo === selectedLevel.catTwo,
)
} else {
// catTwo 为空的时候用 levelId 来分类
headerName = '相关关卡'
const levelIdPrefix = selectedLevel.levelId
.split('/')
.slice(0, -1)
.join('/')
similarLevels = levelIdPrefix
? levels.filter((el) => el.levelId.startsWith(levelIdPrefix))
: []
}

if (similarLevels.length > 1) {
const header = createCustomLevel(headerName)
header.stageId = 'header'
return [header, ...similarLevels]
}
}

return query ? fuse.search(query).map((el) => el.item) : levels
}

return (
<Suggest<Level>
updateQueryOnSelect
items={levels}
itemListPredicate={search}
onReset={() => onChange('')}
className={clsx(className, selectedLevel && '[&_input]:italic')}
itemRenderer={(item, { handleClick, handleFocus, modifiers }) =>
item.stageId === 'header' ? (
<Fragment key="header">
<div className="ml-2 text-zinc-500 text-xs">{item.name}</div>
<MenuDivider />
</Fragment>
) : (
<MenuItem
key={item.stageId}
text={`${item.catThree} ${item.name}`}
onClick={handleClick}
onFocus={handleFocus}
selected={modifiers.active}
disabled={modifiers.disabled}
/>
)
}
selectedItem={selectedLevel}
onItemSelect={(level) => onChange(level.stageId)}
inputValueRenderer={(item) => item.stageId}
noResults={<MenuItem disabled text="没有可选的关卡" />}
inputProps={{
placeholder: '关卡名、关卡类型、关卡编号',
leftIcon: 'area-of-interest',
large: true,
size: 64,
onBlur: (e) => {
// 失焦时直接把 query 提交上去,用于处理关卡未匹配的情况
if (value !== e.target.value) {
onChange(e.target.value)
}
},
}}
/>
)
}
Loading