合并远程更新武器列表

This commit is contained in:
2026-05-25 14:23:02 +08:00
28 changed files with 851 additions and 547 deletions
+5 -5
View File
@@ -7,9 +7,9 @@ interface FirearmParams extends PageQueryParams {
}
/**
* 查询武器列表
* Fetch firearm list
*
* @param params 分页查询参数¬
* @param params Paged query parameters
*/
export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>> {
let uri = "/firearms"
@@ -28,9 +28,9 @@ export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>
}
/**
* 根据 ID 查询武器
* Fetch firearm by ID
*
* @param id 武器 ID
* @param id Firearm ID
*/
export async function getFirearm(id: number): Promise<Firearm> {
const { data } = await WebClient.get<Firearm>(`/firearms/${id}`)
@@ -38,7 +38,7 @@ export async function getFirearm(id: number): Promise<Firearm> {
}
/**
* 新建武器
* Create firearm
* @param request
*/
export async function addFirearm(request: AddFirearmRequest): Promise<Firearm> {
@@ -0,0 +1,17 @@
import React from "react"
interface MarkdownRendererProps {
/** HTML string processed by vite-plugin-markdown */
html: string
/** Optional custom class name */
className?: string
}
export default function MarkdownRenderer({ html, className = "" }: MarkdownRendererProps) {
return (
<article
className={`prose prose-slate max-w-none dark:prose-invert ${className}`}
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { ModificationApi, TagApi } from "@/api";
import { Modification } from "@/types";
import ModificationCreateModal from "@/components/modification-create-modal";
import ModificationEditModal from "@/components/modification-edit-modal";
import { useAppSelector } from "@/store/hooks";
import { useAppSelector } from "@/hooks/store";
const pageSize = 10; // 常量,不需要 useState
+6 -10
View File
@@ -1,23 +1,19 @@
[
"枪口",
"左导轨",
"右导轨",
"枪管",
"贴片",
"右贴片",
"上导轨",
"上贴片",
"下导轨",
"贴片",
"瞄准镜",
"战术设备",
"增高座瞄具",
"侧瞄具",
"枪托",
"托腮板",
"枪托套件",
"导轨脚架",
"前握把",
"后握把",
"导轨脚架",
"后握贴片",
"握把座",
"弹匣",
"弹匣座",
"托腮板"
"弹匣座"
]
+1 -2
View File
@@ -1,6 +1,5 @@
import { useDispatch, useSelector } from "react-redux"
import type { AppDispatch, RootState } from "./index"
import type { AppDispatch, RootState } from "@/store"
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
+4 -3
View File
@@ -1,6 +1,7 @@
@layer theme, base, antd, components, utilities;
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
html,
body {
@@ -111,7 +112,7 @@ body {
letter-spacing: '0.5px';
}
/* 公共列样式:平均分配宽度,带有视觉区分 */
.lmr-left,
.lmr-middle,
.lmr-right {
@@ -120,7 +121,7 @@ body {
height: 100%;
}
/* 为左右中区域添加微妙的背景色差异,便于区分 */
.lmr-left {
flex: 1;
background-color: '#555555';
@@ -167,7 +168,7 @@ body {
}
/* 响应式:小屏幕下自动转为垂直排列 */
@media (max-width: 768px) {
.lmr-container {
flex-direction: column;
+72 -115
View File
@@ -2,10 +2,16 @@ import { Outlet, Link, NavLink } from "react-router-dom"
import { useMemo } from "react"
import dayjs from "dayjs"
import { Dropdown } from "antd"
import {
FileTextOutlined,
GithubOutlined,
LockOutlined,
LoginOutlined,
} from "@ant-design/icons"
import { AuthApi } from "@/api"
import { useAppDispatch, useAppSelector } from "@/store/hooks"
import { useAppDispatch, useAppSelector } from "@/hooks/store"
import { clearCurrentUser } from "@/store/auth-slice"
import { useState } from 'react';
import { useState } from "react"
/**
* Main application component that serves as the root layout.
@@ -15,7 +21,7 @@ export default function HeroLayout() {
const today = useMemo(() => dayjs(), [])
const user = useAppSelector((state) => state.auth.user)
const dispatch = useAppDispatch()
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
async function handleLogout() {
try {
@@ -28,65 +34,31 @@ export default function HeroLayout() {
return (
<div className="bg-[#1b252a] ">
{/* Navigation Header */}
<header className="bg-white shadow-sm border-b bg-[url('/nav_bg.png')] bg-cover bg-center">
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 h-full">
<div className="flex justify-between items-center h-20">
<header className="bg-[#0b0f14] shadow-sm border-b">
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 h-full">
<div className="flex justify-between items-center h-20">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-900 text-white"></h1>
<h1 className="text-xl font-semibold text-white"></h1>
</div>
<nav className="flex h-full ">
<nav className="flex h-full">
<NavLink
to="/firearms"
className={({ isActive }) =>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${isActive ? 'active' : ''
} text-gray-500 hover:text-white whitespace-nowrap`
}
>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${
isActive ? "active" : ""
} text-gray-500 hover:text-white`
}>
</NavLink>
{/* <NavLink
to="/mod-codes"
className={({ isActive }) =>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${isActive ? 'active' : ''
} text-gray-500 hover:text-white whitespace-nowrap`
}
>
`nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 ${
isActive ? "active" : ""
} text-gray-500 hover:text-white`
}>
改枪码
</NavLink> */}
{/* {user ? (
<Dropdown
trigger={["hover"]}
menu={{
items: [
{
key: "logout",
label: "退出登录",
danger: true,
onClick: handleLogout,
},
],
}}
>
<span className="nav-item inline-flex items-center px-10 h-full text-base font-medium text-gray-500 hover:text-white cursor-pointer">
{user.username}
</span>
</Dropdown>
) : (
<Link
to="/login"
className="nav-item inline-flex items-center px-10 h-full text-base font-medium transition-all duration-200 text-gray-500 hover:text-white"
>
登录
</Link>
)}
<a
href="https://github.com/zihluwang/delta-force-firearm-modification-codes"
target="_blank"
rel="noopener noreferrer"
className="nav-item inline-flex items-center px-10 h-full text-lg font-medium transition-all duration-200 text-gray-500 hover:text-white"
>
GitHub
</a> */}
</nav>
</div>
</div>
@@ -102,71 +74,61 @@ export default function HeroLayout() {
{/* Footer */}
<footer className="bg-black border-t border-gray-800">
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 py-8">
<div className="flex flex-wrap justify-center items-center gap-x-6 gap-y-10 text-sm">
<div
className="relative"
onMouseEnter={() => setIsDropdownOpen(true)}
onMouseLeave={() => setIsDropdownOpen(false)}
>
<button
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200 focus:outline-none"
aria-label="GitHub 仓库"
>
<img
src="/github-logo.png"
alt=""
className="w-5 h-5 opacity-80"
/>
<span>GitHub</span>
<svg
className={`w-3 h-3 transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isDropdownOpen && (
<div className="absolute top-full left-2/3 -translate-x-1/2 w-18 bg-gray-950 border border-gray-700 rounded-lg shadow-xl py-1 z-20 opacity-60">
<a
href="https://github.com/zihluwang/delta-force-firearm-modification-codes"
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors"
>
</a>
<a
href="https://github.com/zihluwang/backend-repo"
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors"
>
</a>
</div>
)}
</div>
className="relative"
onMouseEnter={() => setIsDropdownOpen(true)}
onMouseLeave={() => setIsDropdownOpen(false)}>
<button
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200 focus:outline-none"
aria-label="GitHub 仓库">
<GithubOutlined className="text-base opacity-80" />
<span>GitHub</span>
<svg
className={`w-3 h-3 transition-transform duration-200 ${isDropdownOpen ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isDropdownOpen && (
<div className="absolute top-full left-2/3 -translate-x-1/2 w-18 bg-gray-950 border border-gray-700 rounded-lg shadow-xl py-1 z-20 opacity-60">
<a
href="https://github.com/zihluwang/delta-force-guide-web"
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
Web
</a>
<a
href="https://github.com/zihluwang/delta-force-guide-server"
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
Server
</a>
</div>
)}
</div>
<span className="text-gray-700 select-none"></span>
<Link
to="/"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200"
>
<img src="/footer-1.png" alt="" className="w-8 w-8 opacity-80" />
to="/legal?tab=eula"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
<FileTextOutlined className="text-lg opacity-80" />
EULA
</Link>
<span className="text-gray-700 select-none"></span>
<Link
to="/"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200"
>
<img src="/footer-2.png" alt="" className="w-8 w-8 opacity-80" />
to="/legal?tab=privacy"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
<LockOutlined className="text-lg opacity-80" />
</Link>
<span className="text-gray-700 select-none"></span>
@@ -182,8 +144,7 @@ export default function HeroLayout() {
onClick: handleLogout,
},
],
}}
>
}}>
<span className="nav-item inline-flex items-center px-10 h-full text-base font-medium text-gray-500 hover:text-white cursor-pointer">
{user.username}
</span>
@@ -191,9 +152,8 @@ export default function HeroLayout() {
) : (
<Link
to="/login"
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200"
>
<img src="/footer-3.png" alt="" className="w-8 w-8 opacity-80" />
className="flex items-center gap-1.5 text-gray-400 hover:text-white transition-colors duration-200">
<LoginOutlined className="text-lg opacity-80" />
</Link>
)}
@@ -201,11 +161,8 @@ export default function HeroLayout() {
<div className="border-t border-gray-800 my-6" />
<div className="text-center text-xs text-gray-500">
<p>
© 2024-{today.year()} Zihlu Wang OnixByte使 React TypeScript
</p>
<p>© 2024-{today.year()} Zihlu Wang OnixByte使 React TypeScript </p>
</div>
</div>
</footer>
</div>
+6
View File
@@ -0,0 +1,6 @@
declare module "*.md" {
const attributes: Record<string, any>
const html: string
const toc: { level: string; content: string; slug: string }[]
export { attributes, html, toc }
}
+1 -1
View File
@@ -4,7 +4,7 @@ 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 "@/store/hooks"
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';
+34
View File
@@ -0,0 +1,34 @@
import { Tabs } from "antd"
import { useSearchParams } from "react-router-dom"
import MarkdownRenderer from "@/components/markdown-renderer"
import { html as EulaHtml } from "@/docs/EULA.md"
import { html as PrivacyHtml } from "@/docs/PrivacyPolicy.md"
const tabKeys = new Set(["eula", "privacy"])
export default function LegalPage() {
const [searchParams, setSearchParams] = useSearchParams()
const rawTab = searchParams.get("tab")
const activeTab = rawTab && tabKeys.has(rawTab) ? rawTab : "eula"
return (
<div className="mx-auto max-w-4xl">
<Tabs
activeKey={activeTab}
onChange={(key) => setSearchParams({ tab: key })}
items={[
{
key: "eula",
label: "最终用户许可协议",
children: <MarkdownRenderer html={EulaHtml} />,
},
{
key: "privacy",
label: "隐私政策",
children: <MarkdownRenderer html={PrivacyHtml} />,
},
]}
/>
</div>
)
}
+5 -5
View File
@@ -2,7 +2,7 @@ import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { App, Button, Card, Form, Input, Typography } from "antd"
import { AuthApi } from "@/api"
import { useAppDispatch } from "@/store/hooks"
import { useAppDispatch } from "@/hooks/store"
import { setCurrentUser } from "@/store/auth-slice"
import { LoginRequest } from "@/types"
@@ -29,11 +29,11 @@ export default function LoginPage() {
return (
<div className="min-h-screen bg-gray-100 px-4 py-10 sm:py-16">
<div className="mx-auto max-w-md">
<Card bordered={false} className="shadow-sm">
<Typography.Title level={3} className="!mb-2 text-center">
<Card variant="borderless" className="shadow-sm">
<Typography.Title level={3} className="mb-2! text-center">
</Typography.Title>
<Typography.Paragraph className="!mb-6 text-center !text-gray-500">
<Typography.Paragraph className="mb-6! text-center text-gray-500!">
使
</Typography.Paragraph>
@@ -54,7 +54,7 @@ export default function LoginPage() {
<Input.Password autoComplete="current-password" placeholder="请输入密码" />
</Form.Item>
<Form.Item className="!mb-0">
<Form.Item className="mb-0!">
<Button type="primary" htmlType="submit" loading={loading} block>
</Button>
+2 -2
View File
@@ -16,7 +16,7 @@ import { Link, useSearchParams } from "react-router-dom"
import { ModificationApi, TagApi } from "@/api"
import ModificationCreateModal from "@/components/modification-create-modal"
import ModificationEditModal from "@/components/modification-edit-modal"
import { useAppSelector } from "@/store/hooks"
import { useAppSelector } from "@/hooks/store"
import { Modification } from "@/types"
const pageSize = 10
@@ -56,7 +56,7 @@ export default function ModCodesPage() {
page: page - 1,
size: pageSize,
sortBy: "id",
direction: "ASC",
direction: "DESC",
firearmId,
tags: selectedTags,
}).then((pagedData) => {
+4
View File
@@ -40,6 +40,10 @@ const router = createBrowserRouter(
path: "mod-codes",
lazy: lazy(() => import("@/page/mod-codes")),
},
{
path: "legal",
lazy: lazy(() => import("@/page/legal"))
}
],
},
{