右侧配件列表更新

This commit is contained in:
2026-06-24 00:01:39 +08:00
parent aba361158e
commit 83153a4300
3 changed files with 786 additions and 512 deletions
+512 -256
View File
@@ -1,289 +1,289 @@
import { useEffect, useMemo, useState } from "react" // import { useEffect, useMemo, useState } from "react"
import { FirearmApi } from "@/api" // import { FirearmApi } from "@/api"
import slotNames from "@/constant/slots.json" // import slotNames from "@/constant/slots.json"
import tuningNames from "@/constant/tunings.json" // import tuningNames from "@/constant/tunings.json"
import { Firearm, ModificationRequest } from "@/types" // import { Firearm, ModificationRequest } from "@/types"
import { AutoComplete, Button, Card, Form, Input, InputNumber, Select, Space, Tag, message } from "antd" // import { AutoComplete, Button, Card, Form, Input, InputNumber, Select, Space, Tag, message } from "antd"
import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons" // import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"
import AccessoryFormModal from "@/components/AccessoryFormModal" // import AccessoryFormModal from "@/components/AccessoryFormModal"
const slotOptions = slotNames.map((slotName) => ({ value: slotName })) // const slotOptions = slotNames.map((slotName) => ({ value: slotName }))
const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName })) // const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName }))
interface ModificationFormProps { // interface ModificationFormProps {
form: ReturnType<typeof Form.useForm<ModificationRequest>>[0] // form: ReturnType<typeof Form.useForm<ModificationRequest>>[0]
onFinish: (values: ModificationRequest) => void // onFinish: (values: ModificationRequest) => void
lockFirearmId?: number // lockFirearmId?: number
} // }
interface AccessoryType { // interface AccessoryType {
slotName: string // slotName: string
accessoryName: string // accessoryName: string
tunings: Array<{ tuningName: string; tuningValue: number }> // tunings: Array<{ tuningName: string; tuningValue: number }>
} // }
export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) { // export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) {
const [firearmOptions, setFirearmOptions] = useState<Array<{ value: number; label: string }>>([]) // const [firearmOptions, setFirearmOptions] = useState<Array<{ value: number; label: string }>>([])
const [firearmLoading, setFirearmLoading] = useState(false) // const [firearmLoading, setFirearmLoading] = useState(false)
// 配件弹窗相关状态
const [accessoryModalOpen, setAccessoryModalOpen] = useState(false)
const [editingAccessory, setEditingAccessory] = useState<AccessoryType | null>(null)
const [editingAccessoryIndex, setEditingAccessoryIndex] = useState<number | null>(null)
useEffect(() => { // // 配件弹窗相关状态
let active = true // const [accessoryModalOpen, setAccessoryModalOpen] = useState(false)
// const [editingAccessory, setEditingAccessory] = useState<AccessoryType | null>(null)
// const [editingAccessoryIndex, setEditingAccessoryIndex] = useState<number | null>(null)
async function loadAllFirearms() { // useEffect(() => {
setFirearmLoading(true) // let active = true
try {
const allFirearms: Firearm[] = []
let page = 0
let totalPages = 1
while (page < totalPages) { // async function loadAllFirearms() {
const paged = await FirearmApi.getFirearms({ // setFirearmLoading(true)
page, // try {
size: 100, // const allFirearms: Firearm[] = []
sortBy: "id", // let page = 0
direction: "ASC", // let totalPages = 1
})
allFirearms.push(...paged.items) // while (page < totalPages) {
totalPages = paged.totalPages // const paged = await FirearmApi.getFirearms({
page += 1 // page,
} // size: 100,
// sortBy: "id",
// direction: "ASC",
// })
if (!active) { // allFirearms.push(...paged.items)
return // totalPages = paged.totalPages
} // page += 1
// }
setFirearmOptions( // if (!active) {
allFirearms.map((firearm) => ({ // return
value: firearm.id, // }
label: `${firearm.name}`,
}))
)
} finally {
if (active) {
setFirearmLoading(false)
}
}
}
void loadAllFirearms() // setFirearmOptions(
// allFirearms.map((firearm) => ({
// value: firearm.id,
// label: `${firearm.name}`,
// }))
// )
// } finally {
// if (active) {
// setFirearmLoading(false)
// }
// }
// }
return () => { // void loadAllFirearms()
active = false
}
}, [])
const mergedFirearmOptions = useMemo(() => { // return () => {
if ( // active = false
lockFirearmId === undefined || // }
firearmOptions.some((option) => option.value === lockFirearmId) // }, [])
) {
return firearmOptions
}
return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions] // const mergedFirearmOptions = useMemo(() => {
}, [firearmOptions, lockFirearmId]) // if (
// lockFirearmId === undefined ||
// firearmOptions.some((option) => option.value === lockFirearmId)
// ) {
// return firearmOptions
// }
// 打开添加配件弹窗 // return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions]
const handleAddAccessory = () => { // }, [firearmOptions, lockFirearmId])
setEditingAccessory(null)
setEditingAccessoryIndex(null)
setAccessoryModalOpen(true)
}
// 打开编辑配件弹窗 // // 打开添加配件弹窗
const handleEditAccessory = (index: number, accessory: AccessoryType) => { // const handleAddAccessory = () => {
setEditingAccessory(accessory) // setEditingAccessory(null)
setEditingAccessoryIndex(index) // setEditingAccessoryIndex(null)
setAccessoryModalOpen(true) // setAccessoryModalOpen(true)
} // }
// 删除配件 // // 打开编辑配件弹窗
const handleDeleteAccessory = (remove: (index: number) => void, index: number) => { // const handleEditAccessory = (index: number, accessory: AccessoryType) => {
remove(index) // setEditingAccessory(accessory)
message.success("删除成功") // setEditingAccessoryIndex(index)
} // setAccessoryModalOpen(true)
// }
// 配件弹窗确认回调 // // 删除配件
const handleAccessoryModalOk = (values: AccessoryType) => { // const handleDeleteAccessory = (remove: (index: number) => void, index: number) => {
const accessories = form.getFieldValue("accessories") || [] // remove(index)
// message.success("删除成功")
if (editingAccessoryIndex !== null) { // }
// 编辑模式:更新指定索引的数据
const updatedAccessories = [...accessories]
updatedAccessories[editingAccessoryIndex] = values
form.setFieldsValue({ accessories: updatedAccessories })
message.success("修改成功")
} else {
// 新增模式:添加新数据
form.setFieldsValue({ accessories: [...accessories, values] })
message.success("添加成功")
}
setAccessoryModalOpen(false)
setEditingAccessory(null)
setEditingAccessoryIndex(null)
}
// 配件弹窗取消回调 // // 配件弹窗确认回调
const handleAccessoryModalCancel = () => { // const handleAccessoryModalOk = (values: AccessoryType) => {
setAccessoryModalOpen(false) // const accessories = form.getFieldValue("accessories") || []
setEditingAccessory(null)
setEditingAccessoryIndex(null)
}
return ( // if (editingAccessoryIndex !== null) {
<> // // 编辑模式:更新指定索引的数据
<Form<ModificationRequest> // const updatedAccessories = [...accessories]
form={form} // updatedAccessories[editingAccessoryIndex] = values
layout="vertical" // form.setFieldsValue({ accessories: updatedAccessories })
onFinish={() => { // message.success("修改成功")
const allValues = form.getFieldsValue(true); // } else {
console.log('完整数据:', allValues); // // 新增模式:添加新数据
onFinish(allValues); // form.setFieldsValue({ accessories: [...accessories, values] })
}} // message.success("添加成功")
requiredMark={false}> // }
<Form.Item<ModificationRequest>
name="firearmId"
label="武器"
rules={[{ required: true, message: "请输入武器" }]}>
<Select<number>
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())
},
}}
/>
</Form.Item>
<Form.Item<ModificationRequest> // setAccessoryModalOpen(false)
name="name" // setEditingAccessory(null)
label="改装名称" // setEditingAccessoryIndex(null)
rules={[{ required: true, message: "请输入改装名称" }]}> // }
<Input placeholder="请输入改装名称" />
</Form.Item>
<Form.Item<ModificationRequest> // // 配件弹窗取消回调
name="code" // const handleAccessoryModalCancel = () => {
label="改枪码" // setAccessoryModalOpen(false)
rules={[{ required: true, message: "请输入改枪码" }]}> // setEditingAccessory(null)
<Input placeholder="请输入改枪码" /> // setEditingAccessoryIndex(null)
</Form.Item> // }
<Form.Item<ModificationRequest> name="tags" label="标签"> // return (
<Select mode="tags" tokenSeparators={[",", " "]} placeholder="可选:输入后回车" /> // <>
</Form.Item> // <Form<ModificationRequest>
// form={form}
// layout="vertical"
// onFinish={() => {
// const allValues = form.getFieldsValue(true);
// console.log('完整数据:', allValues);
// onFinish(allValues);
// }}
// requiredMark={false}>
// <Form.Item<ModificationRequest>
// name="firearmId"
// label="武器"
// rules={[{ required: true, message: "请输入武器" }]}>
// <Select<number>
// 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())
// },
// }}
// />
// </Form.Item>
<Form.Item<ModificationRequest> name="author" label="作者"> // <Form.Item<ModificationRequest>
<Input placeholder="可选:请输入作者" /> // name="name"
</Form.Item> // label="改装名称"
// rules={[{ required: true, message: "请输入改装名称" }]}>
// <Input placeholder="请输入改装名称" />
// </Form.Item>
<Form.Item<ModificationRequest> name="videoUrl" label="视频链接"> // <Form.Item<ModificationRequest>
<Input placeholder="可选:请输入视频链接" /> // name="code"
</Form.Item> // label="改枪码"
// rules={[{ required: true, message: "请输入改枪码" }]}>
// <Input placeholder="请输入改枪码" />
// </Form.Item>
<Form.Item<ModificationRequest> name="note" label="备注"> // <Form.Item<ModificationRequest> name="tags" label="标签">
<Input.TextArea rows={3} placeholder="可选:补充说明" /> // <Select mode="tags" tokenSeparators={[",", " "]} placeholder="可选:输入后回车" />
</Form.Item> // </Form.Item>
{/* 改造后的配件列表 */} // <Form.Item<ModificationRequest> name="author" label="作者">
<div className="flex flex-col gap-4"> // <Input placeholder="可选:请输入作者" />
// </Form.Item>
<Form.Item<ModificationRequest> label="配件配置"> // <Form.Item<ModificationRequest> name="videoUrl" label="视频链接">
<Form.List name="accessories"> // <Input placeholder="可选:请输入视频链接" />
{(accessoryFields, { add, remove }) => ( // </Form.Item>
<div className="flex flex-col gap-3">
{/* 配件标签列表 */}
<div className="flex flex-wrap gap-2">
{accessoryFields.length === 0 ? (
<div className="text-gray-400 text-sm py-2"></div>
) : (
accessoryFields.map((accessoryField, index) => {
const accessory = form.getFieldValue(["accessories", index]) as AccessoryType
return (
<Tag
key={accessoryField.key}
closable
onClose={(e) => {
e.preventDefault()
handleDeleteAccessory(remove, accessoryField.name)
}}
closeIcon={<DeleteOutlined />}
className="px-3 py-1.5 text-sm border hover:shadow-sm transition-all"
style={{ marginRight: 8, marginBottom: 8 }}
>
<Space size={6}>
<span className="font-medium text-gray-700">
{accessory?.slotName || "?"}
</span>
<span className="text-gray-400">·</span>
<span className="text-gray-800">
{accessory?.accessoryName || "未命名"}
</span>
{accessory?.tunings && accessory.tunings.length > 0 && (
<span className="text-blue-500 text-xs bg-blue-50 px-1.5 py-0.5 rounded">
{accessory.tunings.length}
</span>
)}
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation()
const currentAccessory = form.getFieldValue(["accessories", index])
handleEditAccessory(index, currentAccessory)
}}
className="text-blue-500 hover:text-blue-700 p-0"
style={{ marginLeft: 4 }}
/>
</Space>
</Tag>
)
})
)}
</div>
{/* 添加按钮 */} // <Form.Item<ModificationRequest> name="note" label="备注">
<Button // <Input.TextArea rows={3} placeholder="可选:补充说明" />
type="dashed" // </Form.Item>
icon={<PlusOutlined />}
onClick={handleAddAccessory}
size="small"
className="w-full"
>
</Button>
</div>
)}
</Form.List>
</Form.Item>
</div>
</Form> // {/* 改造后的配件列表 */}
// <div className="flex flex-col gap-4">
{/* 配件编辑弹窗 */} // <Form.Item<ModificationRequest> label="配件配置">
<AccessoryFormModal // <Form.List name="accessories">
open={accessoryModalOpen} // {(accessoryFields, { add, remove }) => (
onCancel={handleAccessoryModalCancel} // <div className="flex flex-col gap-3">
onOk={handleAccessoryModalOk} // {/* 配件标签列表 */}
initialData={editingAccessory} // <div className="flex flex-wrap gap-2">
editingIndex={editingAccessoryIndex} // {accessoryFields.length === 0 ? (
/> // <div className="text-gray-400 text-sm py-2">暂无配件,点击下方按钮添加</div>
</> // ) : (
) // accessoryFields.map((accessoryField, index) => {
} // const accessory = form.getFieldValue(["accessories", index]) as AccessoryType
// return (
// <Tag
// key={accessoryField.key}
// closable
// onClose={(e) => {
// e.preventDefault()
// handleDeleteAccessory(remove, accessoryField.name)
// }}
// closeIcon={<DeleteOutlined />}
// className="px-3 py-1.5 text-sm border hover:shadow-sm transition-all "
// style={{ marginRight: 8, marginBottom: 8 }}
// >
// <Space size={6}>
// <span className="font-medium text-[#2ee59d]">
// {accessory?.slotName || "?"}
// </span>
// <span className="text-gray-400">·</span>
// <span className="text-[#2ee59d]">
// {accessory?.accessoryName || "未命名"}
// </span>
// {accessory?.tunings && accessory.tunings.length > 0 && (
// <span className="text-blue-500 text-xs bg-blue-50 px-1.5 py-0.5 rounded">
// {accessory.tunings.length}项精校
// </span>
// )}
// <Button
// type="link"
// size="small"
// icon={<EditOutlined />}
// onClick={(e) => {
// e.stopPropagation()
// const currentAccessory = form.getFieldValue(["accessories", index])
// handleEditAccessory(index, currentAccessory)
// }}
// className="text-blue-500 hover:text-blue-700 p-0"
// style={{ marginLeft: 4 }}
// />
// </Space>
// </Tag>
// )
// })
// )}
// </div>
// {/* 添加按钮 */}
// <Button
// type="dashed"
// icon={<PlusOutlined />}
// onClick={handleAddAccessory}
// size="small"
// className="w-full"
// >
// 添加配件
// </Button>
// </div>
// )}
// </Form.List>
// </Form.Item>
// </div>
// </Form>
// {/* 配件编辑弹窗 */}
// <AccessoryFormModal
// open={accessoryModalOpen}
// onCancel={handleAccessoryModalCancel}
// onOk={handleAccessoryModalOk}
// initialData={editingAccessory}
// editingIndex={editingAccessoryIndex}
// />
// </>
// )
// }
// import { useEffect, useMemo, useState } from "react" // import { useEffect, useMemo, useState } from "react"
// import { FirearmApi } from "@/api" // import { FirearmApi } from "@/api"
// import slotNames from "@/constant/slots.json" // import slotNames from "@/constant/slots.json"
@@ -500,4 +500,260 @@ export default function ModificationForm({ form, onFinish, lockFirearmId }: Modi
// </Form.List> // </Form.List>
// </Form> // </Form>
// ) // )
// } // }
import { useEffect, useMemo, useState } from "react";
import { FirearmApi } from "@/api";
import slotNames from "@/constant/slots.json";
import tuningNames from "@/constant/tunings.json";
import { Firearm, ModificationRequest } from "@/types";
import { AutoComplete, Button, Card, Form, Input, InputNumber, Select, Space } from "antd";
interface ModificationFormProps {
form: ReturnType<typeof Form.useForm<ModificationRequest>>[0];
onFinish: (values: ModificationRequest) => void;
lockFirearmId?: number;
}
const slotOptions = slotNames.map((slotName) => ({ value: slotName }));
const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName }));
export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) {
const [firearmOptions, setFirearmOptions] = useState<Array<{ value: number; label: string }>>([]);
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<ModificationRequest>
form={form}
layout="vertical"
onFinish={onFinish}
requiredMark={false}
className="max-w-full mx-auto"
>
{/* 强制不换行,左右并排 */}
<div className="flex flex-nowrap gap-6">
{/* 左侧:主要字段,固定宽度,控件宽度收窄 */}
<div className="flex-none w-[580px] space-y-4">
<Form.Item<ModificationRequest>
name="firearmId"
label="武器"
rules={[{ required: true, message: "请输入武器" }]}
>
<Select<number>
className="w-[580px]"
placeholder="请选择武器"
options={mergedFirearmOptions}
loading={firearmLoading}
disabled={lockFirearmId !== undefined}
showSearch={{
filterOption: (input, option) => {
const labelText = String(option?.label ?? "");
return labelText.toLowerCase().includes(input.toLowerCase());
},
}}
/>
</Form.Item>
<Form.Item<ModificationRequest>
name="name"
label="改装名称"
rules={[{ required: true, message: "请输入改装名称" }]}
>
<Input className="w-[580px]" placeholder="请输入改装名称" />
</Form.Item>
<Form.Item<ModificationRequest>
name="code"
label="改枪码"
rules={[{ required: true, message: "请输入改枪码" }]}
>
<Input className="w-[580px]" placeholder="请输入改枪码" />
</Form.Item>
<Form.Item<ModificationRequest> name="tags" label="标签">
<Select
mode="tags"
tokenSeparators={[",", " "]}
placeholder="可选:输入后回车"
className="w-[580px]"
/>
</Form.Item>
<Form.Item<ModificationRequest> name="author" label="作者">
<Input className="w-[580px]" placeholder="可选:请输入作者" />
</Form.Item>
<Form.Item<ModificationRequest> name="videoUrl" label="视频链接">
<Input className="w-[580px]" placeholder="可选:请输入视频链接" />
</Form.Item>
<Form.Item<ModificationRequest> name="note" label="备注">
<Input.TextArea className="w-[580px]" rows={3} placeholder="可选:补充说明" />
</Form.Item>
</div>
{/* 右侧:配件列表,固定宽度,卡片缩小,高度限制滚动 */}
<div className="flex-none w-[420px]">
<div className="max-h-[600px] overflow-y-auto pr-2 hide-scrollbar">
<Form.List name="accessories">
{(accessoryFields, { add: addAccessory, remove: removeAccessory }) => (
<div className="flex flex-col gap-4">
{accessoryFields.map((accessoryField) => (
<Card
key={accessoryField.key}
title={`配件 ${accessoryField.name + 1}`}
size="small"
style={{backgroundColor:'#1f1f1f'}}
extra={
<Button
danger
type="link"
size="small"
onClick={() => removeAccessory(accessoryField.name)}
>
</Button>
}
className="w-full [&_.ant-card-body]:!p-3" // 卡片内边距由默认24px缩小到12px
>
{/* 用容器控制表单项间距,由默认大间距缩小为4px */}
<div className="space-y-1">
<Form.Item
name={[accessoryField.name, "slotName"]}
label="槽位"
rules={[{ required: true, message: "请选择或输入槽位" }]}
className="!mb-1" // 表单项底部间距缩小
>
<AutoComplete options={slotOptions} placeholder="请选择或输入槽位" />
</Form.Item>
<Form.Item
name={[accessoryField.name, "accessoryName"]}
label="配件名称"
rules={[{ required: true, message: "请输入配件名称" }]}
className="!mb-1"
>
<Input placeholder="请输入配件名称" />
</Form.Item>
<Form.List name={[accessoryField.name, "tunings"]}>
{(tuningFields, { add: addTuning, remove: removeTuning }) => (
<div className="flex flex-col gap-2"> {/* 精校列表间距由3(12px)缩小为2(8px) */}
{tuningFields.map((tuningField) => (
<Space key={tuningField.key} align="start" className="w-full" size="small" wrap>
<Form.Item
name={[tuningField.name, "tuningName"]}
label="精校属性"
rules={[{ required: true, message: "请选择或输入精校属性" }]}
className="!mb-1"
>
<AutoComplete
options={tuningOptions}
placeholder="例如:后坐控制"
className="w-44" // 完全保留
/>
</Form.Item>
<Form.Item
name={[tuningField.name, "tuningValue"]}
label="精校值"
rules={[{ required: true, message: "请输入精校值" }]}
className="!mb-1"
>
<InputNumber className="w-32" placeholder="例如:0.35" /> {/* 完全保留 */}
</Form.Item>
<Button
type="link"
danger
className="mt-8" // 完全保留
onClick={() => removeTuning(tuningField.name)}
>
</Button>
</Space>
))}
<Button
type="dashed"
disabled={tuningFields.length >= 2}
onClick={() => addTuning({ tuningName: "", tuningValue: 0 })}
>
</Button>
</div>
)}
</Form.List>
</div>
</Card>
))}
{/* 添加配件按钮,保持在左侧(默认左对齐) */}
<Button
variant="solid"
color="lime"
className="self-start"
onClick={() => addAccessory({ slotName: "", accessoryName: "", tunings: [] })}
>
</Button>
</div>
)}
</Form.List>
</div>
</div>
</div>
</Form>
);
}
+18
View File
@@ -201,4 +201,22 @@ body {
.ant-collapse-body{ .ant-collapse-body{
background: #1e1e1e; background: #1e1e1e;
}
.ant-modal-container{
width: 620px;
}
.hide-scrollbar {
max-height: 500px; /* 限制高度,触发滚动 */
overflow-y: auto; /* 允许滚动 */
/* 隐藏滚动条(Firefox 和 IE */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* 隐藏滚动条(Chrome/Safari/Edge 新版) */
.hide-scrollbar::-webkit-scrollbar {
display: none;
} }
+256 -256
View File
@@ -172,118 +172,105 @@ export default function FirearmsPage() {
</div> </div>
</div> </div>
<div className="mb-6"> <div className="mb-6">
{loading?(<div className="flex justify-center items-center h-64"> {loading ? (<div className="flex justify-center items-center h-64">
<Spin size="large" tip="加载中..." /> <Spin size="large" tip="加载中..." />
</div>):( </div>) : (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{firearms.map((firearm) => ( {firearms.map((firearm) => (
<Col key={firearm.id} xs={24} md={24} lg={24}> <Col key={firearm.id} xs={24} md={24} lg={24}>
<Card <Card
className="hex-bg border-5px-#142c38" className="hex-bg border-5px-#142c38"
extra={ extra={
user ? ( user ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button type="link" size="small" onClick={() => setEditingFirearm(firearm)}> <Button type="link" size="small" onClick={() => setEditingFirearm(firearm)}>
</Button>
<Popconfirm
title="确认删除武器"
description={`确定要删除 ${firearm.name} 吗?该操作不可撤销。`}
okText="删除"
cancelText="取消"
okButtonProps={{ danger: true, loading: deletingId === firearm.id }}
onConfirm={() => handleDelete(firearm)}>
<Button type="link" danger size="small" loading={deletingId === firearm.id}>
</Button> </Button>
</Popconfirm> <Popconfirm
</div> title="确认删除武器"
) : null description={`确定要删除 ${firearm.name} 吗?该操作不可撤销。`}
} okText="删除"
variant="outlined" cancelText="取消"
style={{ okButtonProps={{ danger: true, loading: deletingId === firearm.id }}
height: '100%', display: 'flex', flexDirection: 'column' onConfirm={() => handleDelete(firearm)}>
}} <Button type="link" danger size="small" loading={deletingId === firearm.id}>
styles={{
root: { </Button>
border: '1px solid #313131' </Popconfirm>
}, </div>
header: { ) : null
borderBottom: '1px solid #303030',
},
body: {
flex: 1,
overflow: 'auto',
padding: '12px',
},
actions: {
flexShrink: 0,
background: '#1e1e1e',
display: 'flex'
} }
}} variant="outlined"
actions={[ style={{
<div> height: '100%', display: 'flex', flexDirection: 'column'
<Collapse }}
styles={{
root: {
border: '1px solid #313131'
},
header: {
borderBottom: '1px solid #303030',
},
body: {
flex: 1,
overflow: 'auto',
padding: '12px',
expandIcon={() => null} },
styles={ actions: {
{ flexShrink: 0,
root: { background: '#1e1e1e',
background: '#1e1e1e', display: 'flex'
width: '100%' }
}}
actions={[
<div>
<Collapse
expandIcon={() => null}
styles={
{
root: {
background: '#1e1e1e',
width: '100%'
}
} }
} }
} items={[{
items={[{ key: '1',
key: '1', label: (
label: ( <Button
<Button variant="outlined"
variant="outlined" styles={{
styles={{ root: {
root: { color: '#10E28C',
color: '#10E28C', border: '1px solid #10E28C',
border: '1px solid #10E28C', background: '#16343b96',
background: '#16343b96', width: '20%',
width: '20%', }
} }}
}} >
>
</Button>
</Button> ),
), children: <ModCodes firearmId={String(firearm.id)} />
children: <ModCodes firearmId={String(firearm.id)} /> }]}
}]} />
/> </div>,
</div>, ]}>
]}> <div className="flex flex-col gap-3">
<div className="flex flex-col gap-3"> <div className="lmr-container">
<div className="lmr-container"> <div className="lmr-left">
<div className="lmr-left"> <div style={{
<div style={{ display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', gap: '12px',
gap: '12px', width: '100%'
width: '100%'
}}>
<span style={{
display: 'inline-block',
backgroundColor: '#555555',
color: 'white',
padding: '4px 12px',
borderRadius: '4px',
fontSize: '14px',
fontWeight: '500',
letterSpacing: '0.5px'
}}> }}>
{firearm.name} <span style={{
</span>
<span className="flex items-center justify-between"
style={{
display: 'inline-block', display: 'inline-block',
backgroundColor: '#2d4f5796', backgroundColor: '#555555',
color: 'white', color: 'white',
padding: '4px 12px', padding: '4px 12px',
borderRadius: '4px', borderRadius: '4px',
@@ -291,171 +278,184 @@ export default function FirearmsPage() {
fontWeight: '500', fontWeight: '500',
letterSpacing: '0.5px' letterSpacing: '0.5px'
}}> }}>
{firearmTypeText[firearm.type]} {firearm.name}
</span>
</span></div> <span className="flex items-center justify-between"
</div> style={{
<div className="lmr-middle"> display: 'inline-block',
<div style={{ backgroundColor: '#2d4f5796',
color: 'white', color: 'white',
padding: '4px 12px', padding: '4px 12px',
borderRadius: '4px',
fontSize: '14px',
fontWeight: '500',
letterSpacing: '0.5px'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', // 最小宽度160px
gap: '12px',
}}>
{/* 武器输出等级 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{
padding: '4px 0px',
borderRadius: '4px', borderRadius: '4px',
color: '#10E28C', fontSize: '14px',
fontSize: '13px', fontWeight: '500',
fontWeight: 500, letterSpacing: '0.5px'
marginBottom: '6px', }}>
whiteSpace: 'nowrap', {firearmTypeText[firearm.type]}
</span></div>
</div>
<div className="lmr-middle">
<div style={{
color: 'white',
padding: '4px 12px',
borderRadius: '4px',
fontSize: '14px',
fontWeight: '500',
letterSpacing: '0.5px'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', // 最小宽度160px
gap: '12px',
}}>
{/* 武器输出等级 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center' textAlign: 'center'
}}> }}>
<div style={{
padding: '4px 0px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap',
textAlign: 'center'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{firearm.level}
</div>
</div> </div>
{/* 子弹口径 */}
<div style={{ <div style={{
fontSize: '16px', border: '2px solid #10E28C',
fontWeight: 600, backgroundColor: '#16343b96',
color: '#ffffff' padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}> }}>
{firearm.level} <div style={{
padding: '4px 12px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{firearm.calibre}
</div>
</div>
{/* 每秒甲伤 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{
padding: '4px 12px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{asDps(firearm.fireRate, firearm.armourDamage)}
</div>
</div>
{/* 每秒肉伤 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{
padding: '4px 12px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{asDps(firearm.fireRate, firearm.bodyDamage)}
</div>
</div> </div>
</div> </div>
{/* 子弹口径 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{
padding: '4px 12px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{firearm.calibre}
</div>
</div>
{/* 每秒甲伤 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{
padding: '4px 12px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{asDps(firearm.fireRate, firearm.armourDamage)}
</div>
</div>
{/* 每秒肉伤 */}
<div style={{
border: '2px solid #10E28C',
backgroundColor: '#16343b96',
padding: '12px 16px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{
padding: '4px 12px',
borderRadius: '4px',
color: '#10E28C',
fontSize: '13px',
fontWeight: 500,
marginBottom: '6px',
whiteSpace: 'nowrap'
}}>
</div>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#ffffff'
}}>
{asDps(firearm.fireRate, firearm.bodyDamage)}
</div>
</div>
</div> </div>
</div>
<div className="lmr-right">
<Typography.Paragraph
style={{ marginBottom: 0 }}
type="secondary"
ellipsis={{
rows: 3,
tooltip: firearm.review
? {
title: <div style={{ whiteSpace: "pre-line" }}>{firearm.review}</div>,
placement: "topLeft",
}
: false,
}}
className="whitespace-pre-line">
{firearm.review || "暂无描述"}
</Typography.Paragraph>
</div> </div>
</div> </div>
<div className="lmr-right">
<Typography.Paragraph
style={{ marginBottom: 0 }}
type="secondary"
ellipsis={{
rows: 3,
tooltip: firearm.review
? {
title: <div style={{ whiteSpace: "pre-line" }}>{firearm.review}</div>,
placement: "topLeft",
}
: false,
}}
className="whitespace-pre-line">
{firearm.review || "暂无描述"}
</Typography.Paragraph>
</div>
</div>
</div> </div>
</Card> </Card>
</Col> </Col>
))} ))}
{firearms.length === 0 && ( {firearms.length === 0 && (
<Col span={24}> <Col span={24}>
<Card> <Card>
<Typography.Text type="secondary"></Typography.Text> <Typography.Text type="secondary"></Typography.Text>
</Card> </Card>
</Col> </Col>
)} )}
</Row>)} </Row>)}
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Pagination <Pagination