右侧配件列表更新

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
+508 -252
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 [accessoryModalOpen, setAccessoryModalOpen] = useState(false)
const [editingAccessory, setEditingAccessory] = useState<AccessoryType | null>(null) // const [editingAccessory, setEditingAccessory] = useState<AccessoryType | null>(null)
const [editingAccessoryIndex, setEditingAccessoryIndex] = useState<number | null>(null) // const [editingAccessoryIndex, setEditingAccessoryIndex] = useState<number | null>(null)
useEffect(() => { // useEffect(() => {
let active = true // let active = true
async function loadAllFirearms() { // async function loadAllFirearms() {
setFirearmLoading(true) // setFirearmLoading(true)
try { // try {
const allFirearms: Firearm[] = [] // const allFirearms: Firearm[] = []
let page = 0 // let page = 0
let totalPages = 1 // let totalPages = 1
while (page < totalPages) { // while (page < totalPages) {
const paged = await FirearmApi.getFirearms({ // const paged = await FirearmApi.getFirearms({
page, // page,
size: 100, // size: 100,
sortBy: "id", // sortBy: "id",
direction: "ASC", // direction: "ASC",
}) // })
allFirearms.push(...paged.items) // allFirearms.push(...paged.items)
totalPages = paged.totalPages // totalPages = paged.totalPages
page += 1 // page += 1
} // }
if (!active) { // if (!active) {
return // return
} // }
setFirearmOptions( // setFirearmOptions(
allFirearms.map((firearm) => ({ // allFirearms.map((firearm) => ({
value: firearm.id, // value: firearm.id,
label: `${firearm.name}`, // label: `${firearm.name}`,
})) // }))
) // )
} finally { // } finally {
if (active) { // if (active) {
setFirearmLoading(false) // setFirearmLoading(false)
} // }
} // }
} // }
void loadAllFirearms() // void loadAllFirearms()
return () => { // return () => {
active = false // active = false
} // }
}, []) // }, [])
const mergedFirearmOptions = useMemo(() => { // const mergedFirearmOptions = useMemo(() => {
if ( // if (
lockFirearmId === undefined || // lockFirearmId === undefined ||
firearmOptions.some((option) => option.value === lockFirearmId) // firearmOptions.some((option) => option.value === lockFirearmId)
) { // ) {
return firearmOptions // return firearmOptions
} // }
return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions] // return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions]
}, [firearmOptions, lockFirearmId]) // }, [firearmOptions, lockFirearmId])
// 打开添加配件弹窗 // // 打开添加配件弹窗
const handleAddAccessory = () => { // const handleAddAccessory = () => {
setEditingAccessory(null) // setEditingAccessory(null)
setEditingAccessoryIndex(null) // setEditingAccessoryIndex(null)
setAccessoryModalOpen(true) // setAccessoryModalOpen(true)
} // }
// 打开编辑配件弹窗 // // 打开编辑配件弹窗
const handleEditAccessory = (index: number, accessory: AccessoryType) => { // const handleEditAccessory = (index: number, accessory: AccessoryType) => {
setEditingAccessory(accessory) // setEditingAccessory(accessory)
setEditingAccessoryIndex(index) // setEditingAccessoryIndex(index)
setAccessoryModalOpen(true) // setAccessoryModalOpen(true)
} // }
// 删除配件 // // 删除配件
const handleDeleteAccessory = (remove: (index: number) => void, index: number) => { // const handleDeleteAccessory = (remove: (index: number) => void, index: number) => {
remove(index) // remove(index)
message.success("删除成功") // message.success("删除成功")
} // }
// 配件弹窗确认回调 // // 配件弹窗确认回调
const handleAccessoryModalOk = (values: AccessoryType) => { // const handleAccessoryModalOk = (values: AccessoryType) => {
const accessories = form.getFieldValue("accessories") || [] // const accessories = form.getFieldValue("accessories") || []
if (editingAccessoryIndex !== null) { // if (editingAccessoryIndex !== null) {
// 编辑模式:更新指定索引的数据 // // 编辑模式:更新指定索引的数据
const updatedAccessories = [...accessories] // const updatedAccessories = [...accessories]
updatedAccessories[editingAccessoryIndex] = values // updatedAccessories[editingAccessoryIndex] = values
form.setFieldsValue({ accessories: updatedAccessories }) // form.setFieldsValue({ accessories: updatedAccessories })
message.success("修改成功") // message.success("修改成功")
} else { // } else {
// 新增模式:添加新数据 // // 新增模式:添加新数据
form.setFieldsValue({ accessories: [...accessories, values] }) // form.setFieldsValue({ accessories: [...accessories, values] })
message.success("添加成功") // message.success("添加成功")
} // }
setAccessoryModalOpen(false) // setAccessoryModalOpen(false)
setEditingAccessory(null) // setEditingAccessory(null)
setEditingAccessoryIndex(null) // setEditingAccessoryIndex(null)
} // }
// 配件弹窗取消回调 // // 配件弹窗取消回调
const handleAccessoryModalCancel = () => { // const handleAccessoryModalCancel = () => {
setAccessoryModalOpen(false) // setAccessoryModalOpen(false)
setEditingAccessory(null) // setEditingAccessory(null)
setEditingAccessoryIndex(null) // setEditingAccessoryIndex(null)
} // }
return ( // return (
<> // <>
<Form<ModificationRequest> // <Form<ModificationRequest>
form={form} // form={form}
layout="vertical" // layout="vertical"
onFinish={() => { // onFinish={() => {
const allValues = form.getFieldsValue(true); // const allValues = form.getFieldsValue(true);
console.log('完整数据:', allValues); // console.log('完整数据:', allValues);
onFinish(allValues); // onFinish(allValues);
}} // }}
requiredMark={false}> // requiredMark={false}>
<Form.Item<ModificationRequest> // <Form.Item<ModificationRequest>
name="firearmId" // name="firearmId"
label="武器" // label="武器"
rules={[{ required: true, message: "请输入武器" }]}> // rules={[{ required: true, message: "请输入武器" }]}>
<Select<number> // <Select<number>
className="w-full" // className="w-full"
placeholder="请选择武器" // placeholder="请选择武器"
options={mergedFirearmOptions} // options={mergedFirearmOptions}
loading={firearmLoading} // loading={firearmLoading}
disabled={lockFirearmId !== undefined} // disabled={lockFirearmId !== undefined}
showSearch={{ // showSearch={{
filterOption: (input, option) => { // filterOption: (input, option) => {
const labelText = String(option?.label ?? "") // const labelText = String(option?.label ?? "")
return labelText.toLowerCase().includes(input.toLowerCase()) // return labelText.toLowerCase().includes(input.toLowerCase())
}, // },
}} // }}
/> // />
</Form.Item> // </Form.Item>
<Form.Item<ModificationRequest> // <Form.Item<ModificationRequest>
name="name" // name="name"
label="改装名称" // label="改装名称"
rules={[{ required: true, message: "请输入改装名称" }]}> // rules={[{ required: true, message: "请输入改装名称" }]}>
<Input placeholder="请输入改装名称" /> // <Input placeholder="请输入改装名称" />
</Form.Item> // </Form.Item>
<Form.Item<ModificationRequest> // <Form.Item<ModificationRequest>
name="code" // name="code"
label="改枪码" // label="改枪码"
rules={[{ required: true, message: "请输入改枪码" }]}> // rules={[{ required: true, message: "请输入改枪码" }]}>
<Input placeholder="请输入改枪码" /> // <Input placeholder="请输入改枪码" />
</Form.Item> // </Form.Item>
<Form.Item<ModificationRequest> name="tags" label="标签"> // <Form.Item<ModificationRequest> name="tags" label="标签">
<Select mode="tags" tokenSeparators={[",", " "]} placeholder="可选:输入后回车" /> // <Select mode="tags" tokenSeparators={[",", " "]} placeholder="可选:输入后回车" />
</Form.Item> // </Form.Item>
<Form.Item<ModificationRequest> name="author" label="作者"> // <Form.Item<ModificationRequest> name="author" label="作者">
<Input placeholder="可选:请输入作者" /> // <Input placeholder="可选:请输入作者" />
</Form.Item> // </Form.Item>
<Form.Item<ModificationRequest> name="videoUrl" label="视频链接"> // <Form.Item<ModificationRequest> name="videoUrl" label="视频链接">
<Input placeholder="可选:请输入视频链接" /> // <Input placeholder="可选:请输入视频链接" />
</Form.Item> // </Form.Item>
<Form.Item<ModificationRequest> name="note" label="备注"> // <Form.Item<ModificationRequest> name="note" label="备注">
<Input.TextArea rows={3} placeholder="可选:补充说明" /> // <Input.TextArea rows={3} placeholder="可选:补充说明" />
</Form.Item> // </Form.Item>
{/* 改造后的配件列表 */} // {/* 改造后的配件列表 */}
<div className="flex flex-col gap-4"> // <div className="flex flex-col gap-4">
<Form.Item<ModificationRequest> label="配件配置"> // <Form.Item<ModificationRequest> label="配件配置">
<Form.List name="accessories"> // <Form.List name="accessories">
{(accessoryFields, { add, remove }) => ( // {(accessoryFields, { add, remove }) => (
<div className="flex flex-col gap-3"> // <div className="flex flex-col gap-3">
{/* 配件标签列表 */} // {/* 配件标签列表 */}
<div className="flex flex-wrap gap-2"> // <div className="flex flex-wrap gap-2">
{accessoryFields.length === 0 ? ( // {accessoryFields.length === 0 ? (
<div className="text-gray-400 text-sm py-2"></div> // <div className="text-gray-400 text-sm py-2">暂无配件,点击下方按钮添加</div>
) : ( // ) : (
accessoryFields.map((accessoryField, index) => { // accessoryFields.map((accessoryField, index) => {
const accessory = form.getFieldValue(["accessories", index]) as AccessoryType // const accessory = form.getFieldValue(["accessories", index]) as AccessoryType
return ( // return (
<Tag // <Tag
key={accessoryField.key} // key={accessoryField.key}
closable // closable
onClose={(e) => { // onClose={(e) => {
e.preventDefault() // e.preventDefault()
handleDeleteAccessory(remove, accessoryField.name) // handleDeleteAccessory(remove, accessoryField.name)
}} // }}
closeIcon={<DeleteOutlined />} // closeIcon={<DeleteOutlined />}
className="px-3 py-1.5 text-sm border hover:shadow-sm transition-all" // className="px-3 py-1.5 text-sm border hover:shadow-sm transition-all "
style={{ marginRight: 8, marginBottom: 8 }} // style={{ marginRight: 8, marginBottom: 8 }}
> // >
<Space size={6}> // <Space size={6}>
<span className="font-medium text-gray-700"> // <span className="font-medium text-[#2ee59d]">
{accessory?.slotName || "?"} // {accessory?.slotName || "?"}
</span> // </span>
<span className="text-gray-400">·</span> // <span className="text-gray-400">·</span>
<span className="text-gray-800"> // <span className="text-[#2ee59d]">
{accessory?.accessoryName || "未命名"} // {accessory?.accessoryName || "未命名"}
</span> // </span>
{accessory?.tunings && accessory.tunings.length > 0 && ( // {accessory?.tunings && accessory.tunings.length > 0 && (
<span className="text-blue-500 text-xs bg-blue-50 px-1.5 py-0.5 rounded"> // <span className="text-blue-500 text-xs bg-blue-50 px-1.5 py-0.5 rounded">
{accessory.tunings.length} // {accessory.tunings.length}项精校
</span> // </span>
)} // )}
<Button // <Button
type="link" // type="link"
size="small" // size="small"
icon={<EditOutlined />} // icon={<EditOutlined />}
onClick={(e) => { // onClick={(e) => {
e.stopPropagation() // e.stopPropagation()
const currentAccessory = form.getFieldValue(["accessories", index]) // const currentAccessory = form.getFieldValue(["accessories", index])
handleEditAccessory(index, currentAccessory) // handleEditAccessory(index, currentAccessory)
}} // }}
className="text-blue-500 hover:text-blue-700 p-0" // className="text-blue-500 hover:text-blue-700 p-0"
style={{ marginLeft: 4 }} // style={{ marginLeft: 4 }}
/> // />
</Space> // </Space>
</Tag> // </Tag>
) // )
}) // })
)} // )}
</div> // </div>
{/* 添加按钮 */} // {/* 添加按钮 */}
<Button // <Button
type="dashed" // type="dashed"
icon={<PlusOutlined />} // icon={<PlusOutlined />}
onClick={handleAddAccessory} // onClick={handleAddAccessory}
size="small" // size="small"
className="w-full" // className="w-full"
> // >
// 添加配件
</Button> // </Button>
</div> // </div>
)} // )}
</Form.List> // </Form.List>
</Form.Item> // </Form.Item>
</div> // </div>
</Form> // </Form>
{/* 配件编辑弹窗 */} // {/* 配件编辑弹窗 */}
<AccessoryFormModal // <AccessoryFormModal
open={accessoryModalOpen} // open={accessoryModalOpen}
onCancel={handleAccessoryModalCancel} // onCancel={handleAccessoryModalCancel}
onOk={handleAccessoryModalOk} // onOk={handleAccessoryModalOk}
initialData={editingAccessory} // initialData={editingAccessory}
editingIndex={editingAccessoryIndex} // 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"
@@ -501,3 +501,259 @@ export default function ModificationForm({ form, onFinish, lockFirearmId }: Modi
// </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
@@ -202,3 +202,21 @@ 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;
}
+2 -2
View File
@@ -172,9 +172,9 @@ 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}>