diff --git a/src/components/modification-create-modal/index.tsx b/src/components/modification-create-modal/index.tsx new file mode 100644 index 0000000..905aef5 --- /dev/null +++ b/src/components/modification-create-modal/index.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from "react" +import { App, Form, Modal } from "antd" +import { ModificationApi } from "@/api" +import ModificationForm from "@/components/modification-form" +import { Modification, ModificationRequest } from "@/types" + +interface ModificationCreateModalProps { + open: boolean + defaultFirearmId?: number + lockedFirearmId?: number + onCancel: () => void + onSuccess: (modification: Modification) => void +} + +function normalizeRequest(values: ModificationRequest): ModificationRequest { + return { + firearmId: values.firearmId, + name: values.name.trim(), + code: values.code.trim(), + tags: values.tags?.map((tag) => tag.trim()).filter(Boolean) || [], + note: values.note?.trim() || undefined, + author: values.author?.trim() || undefined, + videoUrl: values.videoUrl?.trim() || undefined, + accessories: (values.accessories || []).map((accessory) => ({ + slotName: accessory.slotName.trim(), + accessoryName: accessory.accessoryName.trim(), + tunings: (accessory.tunings || []).map((tuning) => ({ + tuningName: tuning.tuningName.trim(), + tuningValue: tuning.tuningValue, + })), + })), + } +} + +export default function ModificationCreateModal({ + open, + defaultFirearmId, + lockedFirearmId, + onCancel, + onSuccess, +}: ModificationCreateModalProps) { + const { message } = App.useApp() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!open) { + return + } + + form.setFieldsValue({ + firearmId: lockedFirearmId ?? defaultFirearmId, + accessories: [{ slotName: "", accessoryName: "", tunings: [] }], + tags: [], + }) + }, [open, defaultFirearmId, lockedFirearmId, form]) + + async function onFinish(values: ModificationRequest) { + setLoading(true) + try { + const modification = await ModificationApi.addModification( + normalizeRequest({ + ...values, + firearmId: lockedFirearmId ?? values.firearmId, + }) + ) + message.success("改枪码创建成功") + form.resetFields() + onSuccess(modification) + } catch { + message.error("改枪码创建失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( + form.submit()} + okText="创建" + cancelText="取消" + confirmLoading={loading} + width={820} + destroyOnHidden + afterOpenChange={(visible) => { + if (!visible) { + form.resetFields() + } + }} + > + + + ) +} + + + diff --git a/src/components/modification-edit-modal/index.tsx b/src/components/modification-edit-modal/index.tsx new file mode 100644 index 0000000..6ba8c63 --- /dev/null +++ b/src/components/modification-edit-modal/index.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from "react" +import { App, Form, Modal } from "antd" +import { ModificationApi } from "@/api" +import ModificationForm from "@/components/modification-form" +import { Modification, ModificationRequest } from "@/types" + +interface ModificationEditModalProps { + open: boolean + modification: Modification | null + lockedFirearmId?: number + onCancel: () => void + onSuccess: (modification: Modification) => void +} + +function normalizeRequest(values: ModificationRequest): ModificationRequest { + return { + firearmId: values.firearmId, + name: values.name.trim(), + code: values.code.trim(), + tags: values.tags?.map((tag) => tag.trim()).filter(Boolean) || [], + note: values.note?.trim() || undefined, + author: values.author?.trim() || undefined, + videoUrl: values.videoUrl?.trim() || undefined, + accessories: (values.accessories || []).map((accessory) => ({ + slotName: accessory.slotName.trim(), + accessoryName: accessory.accessoryName.trim(), + tunings: (accessory.tunings || []).map((tuning) => ({ + tuningName: tuning.tuningName.trim(), + tuningValue: tuning.tuningValue, + })), + })), + } +} + +export default function ModificationEditModal({ + open, + modification, + lockedFirearmId, + onCancel, + onSuccess, +}: ModificationEditModalProps) { + const { message } = App.useApp() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!open || !modification) { + return + } + + const { id: _id, ...editableValues } = modification + form.setFieldsValue({ + ...editableValues, + firearmId: lockedFirearmId ?? editableValues.firearmId, + tags: editableValues.tags || [], + accessories: editableValues.accessories || [], + }) + }, [open, modification, lockedFirearmId, form]) + + async function onFinish(values: ModificationRequest) { + if (!modification) { + return + } + + setLoading(true) + try { + const updated = await ModificationApi.editModification( + modification.id, + normalizeRequest({ + ...values, + firearmId: lockedFirearmId ?? values.firearmId, + }) + ) + message.success("改枪码更新成功") + onSuccess(updated) + } catch { + message.error("改枪码更新失败,请稍后重试") + } finally { + setLoading(false) + } + } + + return ( + form.submit()} + okText="保存" + cancelText="取消" + confirmLoading={loading} + width={820} + destroyOnHidden + > + + + ) +} + + diff --git a/src/components/modification-form/index.tsx b/src/components/modification-form/index.tsx new file mode 100644 index 0000000..2c0cdd1 --- /dev/null +++ b/src/components/modification-form/index.tsx @@ -0,0 +1,209 @@ +import { useEffect, useMemo, useState } from "react" +import { FirearmApi } from "@/api" +import slotNames from "@/constant/slots.json" +import { Firearm, ModificationRequest } from "@/types" +import { AutoComplete, Button, Card, Form, Input, InputNumber, Select, Space } from "antd" + +interface ModificationFormProps { + form: ReturnType>[0] + onFinish: (values: ModificationRequest) => void + lockFirearmId?: number +} + +const slotOptions = slotNames.map((slotName) => ({ value: slotName })) + +export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) { + const [firearmOptions, setFirearmOptions] = useState>([]) + const [firearmLoading, setFirearmLoading] = useState(false) + + useEffect(() => { + let active = true + + async function loadAllFirearms() { + setFirearmLoading(true) + try { + const allFirearms: Firearm[] = [] + let page = 0 + let totalPages = 1 + + while (page < totalPages) { + const paged = await FirearmApi.getFirearms({ + page, + size: 100, + sortBy: "id", + direction: "ASC", + }) + + allFirearms.push(...paged.items) + totalPages = paged.totalPages + page += 1 + } + + if (!active) { + return + } + + setFirearmOptions( + allFirearms.map((firearm) => ({ + value: firearm.id, + label: `${firearm.name}`, + })) + ) + } finally { + if (active) { + setFirearmLoading(false) + } + } + } + + void loadAllFirearms() + + return () => { + active = false + } + }, []) + + const mergedFirearmOptions = useMemo(() => { + if (lockFirearmId === undefined || firearmOptions.some((option) => option.value === lockFirearmId)) { + return firearmOptions + } + + return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions] + }, [firearmOptions, lockFirearmId]) + + return ( + + form={form} + layout="vertical" + onFinish={onFinish} + requiredMark={false}> + + name="firearmId" + label="武器" + rules={[{ required: true, message: "请输入武器" }]}> + + className="w-full" + placeholder="请选择武器" + options={mergedFirearmOptions} + loading={firearmLoading} + disabled={lockFirearmId !== undefined} + showSearch={{ + filterOption: (input, option) => { + const labelText = String(option?.label ?? "") + return labelText.toLowerCase().includes(input.toLowerCase()) + }, + }} + /> + + + + name="name" + label="改装名称" + rules={[{ required: true, message: "请输入改装名称" }]}> + + + + + name="code" + label="改枪码" + rules={[{ required: true, message: "请输入改枪码" }]}> + + + + name="tags" label="标签"> + + + + name="videoUrl" label="视频链接"> + + + + name="note" label="备注"> + + + + + {(accessoryFields, { add: addAccessory, remove: removeAccessory }) => ( +
+ {accessoryFields.map((accessoryField) => ( + removeAccessory(accessoryField.name)}> + 删除配件 + + }> + + + + + + + + + + {(tuningFields, { add: addTuning, remove: removeTuning }) => ( +
+ {tuningFields.map((tuningField) => ( + + + + + + + + + + ))} + +
+ )} +
+
+ ))} + +
+ )} +
+ + ) +} + + + diff --git a/src/constant/slots.json b/src/constant/slots.json new file mode 100644 index 0000000..a45a22b --- /dev/null +++ b/src/constant/slots.json @@ -0,0 +1,22 @@ +[ + "枪口", + "左导轨", + "右导轨", + "枪管", + "左贴片", + "右贴片", + "上导轨", + "上贴片", + "下导轨", + "瞄准镜", + "战术设备", + "增高座瞄具", + "侧瞄具", + "枪托", + "枪托套件", + "后握把", + "前握把", + "导轨脚架", + "弹匣座", + "弹匣" +] diff --git a/src/page/mod-codes/index.tsx b/src/page/mod-codes/index.tsx index fa122b4..4695d7e 100644 --- a/src/page/mod-codes/index.tsx +++ b/src/page/mod-codes/index.tsx @@ -1,7 +1,9 @@ -import { useEffect, useMemo, useState } from "react" +import { App, Button, Card, Col, Pagination, Popconfirm, Row, Select, Space, Tag, Typography } from "antd" +import { useCallback, useEffect, useMemo, useState } from "react" import { Link, useSearchParams } from "react-router-dom" -import { Button, Card, Col, Pagination, Row, Select, Space, Tag, Typography } from "antd" import { ModificationApi, TagApi } from "@/api" +import ModificationCreateModal from "@/components/modification-create-modal" +import ModificationEditModal from "@/components/modification-edit-modal" import { useAppSelector } from "@/store" import { Modification } from "@/types" @@ -9,24 +11,36 @@ const pageSize = 12 export default function ModCodesPage() { const user = useAppSelector((state) => state.auth.user) + const { message } = App.useApp() const [searchParams] = useSearchParams() const firearmId = useMemo(() => searchParams.get("firearmId") || undefined, [searchParams]) + const parsedFirearmId = useMemo(() => { + if (!firearmId) { + return undefined + } + + const value = Number(firearmId) + return Number.isFinite(value) ? value : undefined + }, [firearmId]) const [page, setPage] = useState(1) const [modifications, setModifications] = useState([]) const [tagOptions, setTagOptions] = useState([]) const [selectedTags, setSelectedTags] = useState([]) const [total, setTotal] = useState(0) + const [deletingId, setDeletingId] = useState(null) + const [createModalOpen, setCreateModalOpen] = useState(false) + const [editingModification, setEditingModification] = useState(null) useEffect(() => { - const _firearmId = firearmId ? +firearmId : (void 0) + const _firearmId = firearmId ? +firearmId : void 0 TagApi.getTags(_firearmId).then((tags) => { setTagOptions(tags) }) - }, []) + }, [firearmId]) - useEffect(() => { - ModificationApi.getModifications({ + const loadModifications = useCallback(() => { + return ModificationApi.getModifications({ page: page - 1, size: pageSize, sortBy: "id", @@ -39,6 +53,31 @@ export default function ModCodesPage() { }) }, [page, firearmId, selectedTags]) + useEffect(() => { + loadModifications() + }, [loadModifications]) + + async function handleDelete(modification: Modification) { + if (!user) { + return + } + + setDeletingId(modification.id) + try { + await ModificationApi.removeModification(modification.id) + message.success("改枪码删除成功") + if (modifications.length === 1 && page > 1) { + setPage(page - 1) + } else { + loadModifications() + } + } catch { + message.error("改枪码删除失败,请稍后重试") + } finally { + setDeletingId(null) + } + } + useEffect(() => { setPage(1) }, [firearmId]) @@ -82,21 +121,39 @@ export default function ModCodesPage() { )} - {user && } + {user && ( + + )}
{modifications.map((modification) => ( - + - 编辑 - +
+ + handleDelete(modification)} + > + + +
) : null } variant="outlined" @@ -124,14 +181,45 @@ export default function ModCodesPage() { {modification.author || "未知"} - {modification.tags.length > 0 && ( + {(modification.tags?.length || 0) > 0 && (
- {modification.tags.map((tag) => ( + {(modification.tags || []).map((tag) => ( {tag} ))}
)} +
+ 配件配置: + {(modification.accessories?.length || 0) > 0 ? ( +
+
+ {(modification.accessories || []).map((accessory, accessoryIndex) => ( +
+
+ {accessory.slotName || "未填写槽位"} + {accessory.accessoryName || "未填写配件"} +
+ {(accessory.tunings?.length || 0) > 0 ? ( +
+ {accessory.tunings.map((tuning, tuningIndex) => ( + + {tuning.tuningName || "未命名"}: {tuning.tuningValue ?? "-"} + + ))} +
+ ) : null} +
+ ))} +
+
+ ) : ( + + 暂无配件信息 + + )} +
+
+ + setCreateModalOpen(false)} + onSuccess={() => { + setCreateModalOpen(false) + loadModifications() + }} + /> + + setEditingModification(null)} + onSuccess={() => { + setEditingModification(null) + loadModifications() + }} + /> ) } diff --git a/src/types/modification.ts b/src/types/modification.ts index f1286b9..20b6e58 100644 --- a/src/types/modification.ts +++ b/src/types/modification.ts @@ -1,12 +1,24 @@ +export interface Tuning { + tuningName: string + tuningValue: number +} + +export interface Accessory { + slotName: string + accessoryName: string + tunings: Tuning[] +} + export interface Modification { id: number firearmId: number name: string code: string - tags: string[] - note: string - author: string - videoUrl: string + tags?: string[] + note?: string + author?: string + videoUrl?: string, + accessories: Accessory[] } export interface ModificationRequest extends Omit {}