Files
delta-force-guide-web/src/page/firearms/index.tsx
T

438 lines
16 KiB
TypeScript

import { useCallback, useEffect, useState } from "react"
import { Link } from "react-router-dom"
import { FirearmApi } from "@/api"
import FirearmCreateModal from "@/components/firearm-create-modal"
import FirearmEditModal from "@/components/firearm-edit-modal"
import ModCodes from "@/components/mod-codes"
import { useAppSelector } from "@/hooks/store"
import { Firearm, FirearmType } from "@/types"
import { Button, Card, Col, Pagination, Popconfirm, Row, Select, Tag, Typography, App } from "antd"
import { ConfigProvider, theme } from 'antd';
import type { CollapseProps } from 'antd';
import { Collapse } from 'antd';
const firearmTypeText: Record<FirearmType, string> = {
RIFLE: "步枪",
SUB_MACHINE_GUN: "冲锋枪",
SHOTGUN: "霰弹枪",
LIGHT_MACHINE_GUN: "轻机枪",
DESIGNATED_MARKSMAN_RIFLE: "射手步枪",
SNIPER_RIFLE: "狙击步枪",
PISTOL: "手枪",
SPECIAL: "特殊",
}
const darkTheme = {
algorithm: theme.darkAlgorithm,
// token: { colorPrimary: '#00b96b' },
};
const allTypeValue = "ALL"
type FirearmTypeFilter = FirearmType | typeof allTypeValue
function asDps(fireRate: number, damage: number) {
return ((fireRate / 60) * damage).toFixed(2)
}
export default function FirearmsPage() {
const user = useAppSelector((state) => state.auth.user)
const { message } = App.useApp()
const [page, setPage] = useState<number>(1)
const [typeFilter, setTypeFilter] = useState<FirearmTypeFilter>(allTypeValue)
const [firearms, setFirearms] = useState<Firearm[]>([])
const [total, setTotal] = useState<number>(0)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [editingFirearm, setEditingFirearm] = useState<Firearm | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(null)
const loadFirearms = useCallback(async () => {
const pagedData = await FirearmApi.getFirearms({
page: page - 1,
size: 12,
sortBy: "id",
direction: "ASC",
type: typeFilter === allTypeValue ? undefined : typeFilter,
})
setFirearms(pagedData.items)
setTotal(pagedData.totalElements)
}, [page, typeFilter])
const [expandedId, setExpandedId] = useState<number | null>(null);
useEffect(() => {
void loadFirearms()
}, [loadFirearms])
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
}, [page]);
async function handleDelete(firearm: Firearm) {
setDeletingId(firearm.id)
try {
await FirearmApi.removeFirearm(firearm.id)
message.success("武器删除成功")
if (firearms.length === 1 && page > 1) {
setPage(page - 1)
} else {
void loadFirearms()
}
} catch {
message.error("武器删除失败,请稍后重试")
} finally {
setDeletingId(null)
}
}
return (
<>
<ConfigProvider theme={darkTheme}>
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
{user && (
<Button type="primary" onClick={() => setCreateModalOpen(true)}>
</Button>
)}
</div>
<Select<FirearmTypeFilter>
className="w-full sm:w-64"
value={typeFilter}
options={[
{ value: allTypeValue, label: "全部类型" },
...Object.entries(firearmTypeText).map(([value, label]) => ({
value,
label,
})),
]}
onChange={(nextType) => {
setPage(1)
setTypeFilter(nextType)
}}
/>
</div>
<div className="mb-6">
<Row gutter={[16, 16]}>
{firearms.map((firearm) => (
<Col key={firearm.id} xs={24} md={24} lg={24}>
<Card
className="hex-bg border-5px-#142c38"
extra={
user ? (
<div className="flex items-center gap-1">
<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>
</Popconfirm>
</div>
) : null
}
variant="outlined"
style={{
height: '100%', display: 'flex', flexDirection: 'column'
}}
styles={{
root: {
border: '1px solid #313131'
},
header: {
borderBottom: '1px solid #303030',
},
body: {
flex: 1,
overflow: 'auto',
padding: '12px',
},
actions: {
flexShrink: 0,
background: '#1e1e1e',
display: 'flex'
}
}}
actions={[
<div>
<Collapse
expandIcon={() => null}
styles={
{
root: {
background: '#1e1e1e',
}
}
}
items={[{
key: '1',
label: (
<Button
variant="outlined"
styles={{
root: {
color: '#10E28C',
border: '1px solid #10E28C',
background: '#16343b96',
width: '20%',
}
}}
>
</Button>
),
children: <ModCodes firearmId={String(firearm.id)} />
}]}
/>
</div>,
]}>
<div className="flex flex-col gap-3">
<div className="lmr-container">
<div className="lmr-left">
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
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>
<span className="flex items-center justify-between"
style={{
display: 'inline-block',
backgroundColor: '#2d4f5796',
color: 'white',
padding: '4px 12px',
borderRadius: '4px',
fontSize: '14px',
fontWeight: '500',
letterSpacing: '0.5px'
}}>
{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'
}}>
<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 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>
</Card>
</Col>
))}
{firearms.length === 0 && (
<Col span={24}>
<Card>
<Typography.Text type="secondary"></Typography.Text>
</Card>
</Col>
)}
</Row>
</div>
<div className="flex justify-end">
<Pagination
align="end"
current={page}
pageSize={12}
total={total}
onChange={(nextPage) => {
setPage(nextPage)
}}
showSizeChanger={false}
/>
</div>
<FirearmCreateModal
open={createModalOpen}
onCancel={() => setCreateModalOpen(false)}
onSuccess={() => {
setCreateModalOpen(false)
void loadFirearms()
}}
/>
<FirearmEditModal
open={!!editingFirearm}
firearm={editingFirearm}
onCancel={() => setEditingFirearm(null)}
onSuccess={() => {
setEditingFirearm(null)
void loadFirearms()
}}
/>
</ConfigProvider>
</>
)
}