diff --git a/package-lock.json b/package-lock.json index c219063..ca0e7ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@ant-design/icons": "^6.2.2", + "@anthropic-ai/claude-code": "^2.1.139", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-virtual": "^3.13.24", @@ -131,6 +132,133 @@ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@anthropic-ai/claude-code": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.139.tgz", + "integrity": "sha512-I8SZmO4844GYXGALBZc+NPuFr38yA9OlsoMPMzU1Txxk3uCHnlOFA1FlcUayZ/Z8j7gv/O9LPB+WiIcp9Ynlaw==", + "hasInstallScript": true, + "license": "SEE LICENSE IN README.md", + "bin": { + "claude": "bin/claude.exe" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-code-darwin-arm64": "2.1.139", + "@anthropic-ai/claude-code-darwin-x64": "2.1.139", + "@anthropic-ai/claude-code-linux-arm64": "2.1.139", + "@anthropic-ai/claude-code-linux-arm64-musl": "2.1.139", + "@anthropic-ai/claude-code-linux-x64": "2.1.139", + "@anthropic-ai/claude-code-linux-x64-musl": "2.1.139", + "@anthropic-ai/claude-code-win32-arm64": "2.1.139", + "@anthropic-ai/claude-code-win32-x64": "2.1.139" + } + }, + "node_modules/@anthropic-ai/claude-code-darwin-arm64": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-arm64/-/claude-code-darwin-arm64-2.1.139.tgz", + "integrity": "sha512-2OnJCrL/LO8GgfgKBrmTwLhiC9S/UJbbEpeP5oz44InbhREhFU4rFuRc49pmDyUmT5yje8dNJ00ZVXiXnOOOVw==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-code-darwin-x64": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-x64/-/claude-code-darwin-x64-2.1.139.tgz", + "integrity": "sha512-7Wt45d4e0V6SZHq5TgDA+5Jm1uBztRQ1wzqbPe5atmf8S12RLK0d2r5h50YK1HsUqQvdvRmZBnvSvz2QFrFjBQ==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-arm64": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64/-/claude-code-linux-arm64-2.1.139.tgz", + "integrity": "sha512-xE3s+MSxm1n9Ywq5+L83eBsv+bJNy77MEUE8au49+bwD8/LQP07gO6YP9PFTbpwMvUe4aQKwKXG16uGj1ABvHg==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-arm64-musl": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64-musl/-/claude-code-linux-arm64-musl-2.1.139.tgz", + "integrity": "sha512-HAU7n0ZMLmYQkKGuUyzp+W0RXtgtEZ+o0hgPwxt/wA3ox2HpOaqwTXgPEcprrGVTWImMXwt/YxNwqSGsPn7JVA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-x64": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.139.tgz", + "integrity": "sha512-BwfSmFhas8MkOtYweY23gHPJTasHzklDSBGqWaae/ObTmk7swiz6NYSaP7DEbrWa1WSl3PTgduFq9/fJVhYNGQ==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-linux-x64-musl": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64-musl/-/claude-code-linux-x64-musl-2.1.139.tgz", + "integrity": "sha512-Y+weBppaTayuHOR2PuYE7p9aY1R6wPo2v2qB2tkWgvnazUDxlEgSdUxM/zMNeW27UHFmcFizt/ILW71VrAYEHg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-code-win32-arm64": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-arm64/-/claude-code-win32-arm64-2.1.139.tgz", + "integrity": "sha512-kx0ApoeuSieGDivd21EJu0nvdXH4ZVQTpQjVEimPtHXe7U4mracKJh7l/hxTQ2cKru5wVMqv1JZzfcTv7xRJ4w==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-code-win32-x64": { + "version": "2.1.139", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-x64/-/claude-code-win32-x64-2.1.139.tgz", + "integrity": "sha512-DfKgp68zhWWNGpeqCRKGvuYAdQ83aoa20nnVDuTt2C0dCsY+Fmw/Fjc83vdDSQScrpFtpkANwZp5j0O+/qbgsw==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", diff --git a/package.json b/package.json index 5807689..8db5692 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@ant-design/icons": "^6.2.2", + "@anthropic-ai/claude-code": "^2.1.139", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-virtual": "^3.13.24", diff --git a/src/api/daily-password-api.ts b/src/api/daily-password-api.ts new file mode 100644 index 0000000..74ebdad --- /dev/null +++ b/src/api/daily-password-api.ts @@ -0,0 +1,9 @@ +import { WebClient } from "@/shared/web-client" +import { DailyPasswordResponse, PasswordItem } from "@/types/DailyPasswordResponse" + + +export async function getDailyPassword(): Promise { + const { data } = await WebClient.get(`/daily-passwords`) + const allPasswords:PasswordItem[]=data.data.passwords + return allPasswords +} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index edb4561..0975cc3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,3 +2,4 @@ export * as FirearmApi from "./firearm-api" export * as ModificationApi from "./modification-api" export * as TagApi from "./tag-api" export * as AuthApi from "./auth-api" +export * as DailyPasswordApi from "./daily-password-api" diff --git a/src/components/AccessoryFormModal/index.tsx b/src/components/AccessoryFormModal/index.tsx new file mode 100644 index 0000000..269ddb9 --- /dev/null +++ b/src/components/AccessoryFormModal/index.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from "react" +import { Button, Form, Input, InputNumber, Modal, AutoComplete, Space } from "antd" +import slotNames from "@/constant/slots.json" +import tuningNames from "@/constant/tunings.json" + +interface AccessoryType { + slotName: string + accessoryName: string + tunings: Array<{ tuningName: string; tuningValue: number }> +} + +interface AccessoryFormModalProps { + open: boolean + onCancel: () => void + onOk: (values: AccessoryType) => void + initialData?: AccessoryType | null + editingIndex?: number | null +} + +const slotOptions = slotNames.map((slotName) => ({ value: slotName })) +const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName })) + +export default function AccessoryFormModal({ + open, + onCancel, + onOk, + initialData, + editingIndex +}: AccessoryFormModalProps) { + const [form] = Form.useForm() + + // 重置表单或回填数据 + useEffect(() => { + if (open) { + if (initialData) { + // 编辑模式:回填数据 + form.setFieldsValue(initialData) + } else { + // 新增模式:重置表单 + form.resetFields() + } + } + }, [open, initialData, form]) + + const handleOk = async () => { + try { + const values = await form.validateFields() + onOk(values) + form.resetFields() + } catch (error) { + console.log("表单校验失败:", error) + } + } + + const handleCancel = () => { + form.resetFields() + onCancel() + } + + return ( + +
+ + + + + + + + + + {(tuningFields, { add: addTuning, remove: removeTuning }) => ( +
+
精校配置
+ {tuningFields.map((tuningField) => ( + + + + + + + + + + ))} + +
+ )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/DisplayBoard/DisplayBoard.css b/src/components/DisplayBoard/DisplayBoard.css new file mode 100644 index 0000000..f9ca98d --- /dev/null +++ b/src/components/DisplayBoard/DisplayBoard.css @@ -0,0 +1,100 @@ +/* DisplayBoard.css - 荧光绿主题 + 紧凑导航栏样式 */ + +.display-board { + /* 横向排列,紧凑高度 */ + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + height: 48px; /* 严格控制高度,适合导航栏 */ + padding: 0 20px; + background-color: #ffffff00; + overflow-x: auto; /* 窄屏时允许滚动,保证所有项目可见 */ + white-space: nowrap; + scrollbar-width: thin; +} + +/* 自定义滚动条(可选,提升美观) */ +.display-board::-webkit-scrollbar { + height: 4px; +} +.display-board::-webkit-scrollbar-track { + background: #f0f0f0; +} +.display-board::-webkit-scrollbar-thumb { + background: #10e28c; + border-radius: 4px; +} + +/* 每个内容卡片 - 轻量胶囊风格 */ +.display-item { + border-radius: 2px; + display: inline-flex; + align-items: center; + gap: 8px; + background-color: #0b0f149d; + padding: 4px 14px; + transition: all 0.2s ease; + box-shadow: 0 1px 2px rgba(0,0,0,0.02); +} + +.loading-placeholder{ + color: #10e28c; +} + +/* 名字样式 - 清晰易读 */ +.item-name { + font-size: 13px; + font-weight: 500; + color: #a8a8a8; + letter-spacing: 0.3px; +} + +/* 计算器风格数字板 —— 黑底、荧光绿字、等宽字体、内陷光感 */ +.calculator-digit { + background: #0c0f0c; + border-radius: 6px; + min-width: 62px; + text-align: center; + + font-size: 16px; + font-weight: 700; + letter-spacing: 1.5px; + color: #10e28c; + text-shadow: 0 0 3px #10e28c, 0 0 1px #73ff50; + box-shadow: inset 0 1px 4px #10e28c, 0 0 0 1px #73ff5046; + transition: all 0.1s linear; + display: grid; + height: 100%; +} + +.calculator-digit div{ + height: 100%; +} + +/* 模拟计算器屏幕的额外“发光感” */ +.calculator-digit:hover { + text-shadow: 0 0 5px #10e28c; + box-shadow: inset 0 1px 4px rgba(0,0,0,0.8), 0 0 2px #10e28c; +} + +/* 响应式微调:当屏幕宽度小于500px时适当缩小间距和字体 */ +@media (max-width: 560px) { + .display-board { + gap: 8px; + padding: 0 12px; + } + .display-item { + padding: 3px 10px; + gap: 5px; + } + .item-name { + font-size: 11px; + } + .calculator-digit { + font-size: 13px; + min-width: 54px; + padding: 2px 6px; + letter-spacing: 1px; + } +} \ No newline at end of file diff --git a/src/components/DisplayBoard/index.tsx b/src/components/DisplayBoard/index.tsx new file mode 100644 index 0000000..753e036 --- /dev/null +++ b/src/components/DisplayBoard/index.tsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect } from 'react'; +import './DisplayBoard.css'; +import { DailyPasswordApi } from '@/api'; +import { DailyPasswordData,PasswordItem } from '@/types/DailyPasswordResponse'; + +// 单条数据接口 +interface DisplayItem { + mapName: string; + password: number; +} + +// 组件 Props 接口(支持动态获取 + 静态传入) +interface DisplayBoardProps { + /** 可选:静态数据,若同时提供 fetchData,则静态数据仅作为初始值 */ + items?: PasswordItem[]; + /** 可选:异步获取数据的函数,返回值应为 DisplayItem[] */ + fetchData?: () => Promise; + /** 可选:自动刷新间隔(毫秒),默认不自动刷新 */ + refreshInterval?: number; + /** 可选:是否强制要求数组长度为 5(默认 true) */ + enforceLength?: boolean; +} + +/** + * 头部导航栏显示框组件 + * - 支持静态数据传入 + * - 支持动态数据获取(通过 fetchData 属性) + * - 自动处理加载状态(显示简短占位符) + * - 紧凑设计(高 48px),荧光绿主题,计算器风格密码 + */ +const DisplayBoard: React.FC = ({ + items: staticItems, + fetchData, + refreshInterval, + enforceLength = true, +}) => { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(!!fetchData); + + const defaultItems: PasswordItem[] = [ + { mapName: 'Alex', password: '1234' }, + { mapName: 'Jamie', password: '5678' }, + { mapName: 'Taylor', password: '9012'}, + { mapName: 'Jordan', password: '3456' }, + { mapName: 'Casey', password: '7890' }, + ]; + + // 确保数组长度为 5(若 enforceLength 为 true) + const ensureLength = (data: PasswordItem[]): PasswordItem[] => { + if (!enforceLength) return data; + if (data.length === 5) return data; + if (data.length < 5) { + // 不足则用默认数据补齐,实际项目中可自定义补齐逻辑 + return [...data, ...defaultItems.slice(data.length, 5)]; + } + // 超过5个则截取前5个 + return data.slice(0, 5); + }; + + // 加载数据的函数 + const loadData = async () => { + if (DailyPasswordApi.getDailyPassword) { + setLoading(true); + try { + const dynamicData = await DailyPasswordApi.getDailyPassword(); + const finalData = ensureLength(dynamicData); + setItems(finalData); + } catch (error) { + console.error('动态获取密码数据失败', error); + // 出错时使用静态数据或默认数据兜底 + if (staticItems && staticItems.length > 0) { + setItems(ensureLength(staticItems)); + } else { + setItems(defaultItems); + } + } finally { + setLoading(false); + } + } else if (staticItems) { + // 仅静态数据模式 + setItems(ensureLength(staticItems)); + } else { + // 无任何数据源,使用默认 + setItems(defaultItems); + } + }; + + // 初次加载 & fetchData 变化时重新拉取 + useEffect(() => { + loadData(); + }, [fetchData]); // 注意:staticItems 变化不会自动触发重新拉取,如需可自行添加依赖 + + // 定时刷新逻辑 + useEffect(() => { + if (!fetchData || !refreshInterval || refreshInterval <= 0) return; + const timer = setInterval(() => { + loadData(); + }, refreshInterval); + return () => clearInterval(timer); + }, [fetchData, refreshInterval]); + + // 辅助:格式化密码为四位数字(计算器风格) + const formatPassword = (pwd: string | number): string => { + const str = String(pwd).replace(/\D/g, ''); + if (str.length >= 4) return str.slice(0, 4); + return str.padStart(4, '0'); + }; + + // 加载状态时显示简洁的占位符(保持高度不变,避免布局抖动) + if (loading && items.length === 0) { + return ( +
+
加载密码...
+
+ ); + } + + return ( +
+ {items.map((item, index) => ( +
+ {item.mapName} +
+
+ {formatPassword(item.password)} +
+
+
+ ))} +
+ ); +}; + +export default DisplayBoard; \ No newline at end of file diff --git a/src/components/mod-codes/index.tsx b/src/components/mod-codes/index.tsx index 823f687..39fe7b8 100644 --- a/src/components/mod-codes/index.tsx +++ b/src/components/mod-codes/index.tsx @@ -1,6 +1,6 @@ // ModCodes.tsx import { useEffect, useState, useCallback, useMemo } from "react"; -import { Card, Col, Pagination, Row, Tag, Typography, Button, Popconfirm, Space, Select, App } from "antd"; +import { Card, Col, Pagination, Row, Tag, Typography, Button, Popconfirm, Space, Select, App, message } from "antd"; import { Link } from "react-router-dom"; import { ModificationApi, TagApi } from "@/api"; import { Modification } from "@/types"; @@ -47,9 +47,10 @@ export default function ModCodes({ firearmId }: ModCodesProps) { page: page - 1, size: pageSize, sortBy: "id", - direction: "ASC", + direction: "DESC", firearmId: numericId, // 使用数字类型 tags: selectedTags, + }); setModifications(pagedData.items); setTotal(pagedData.totalElements); @@ -92,14 +93,14 @@ export default function ModCodes({ firearmId }: ModCodesProps) { if (!parsedFirearmId) { return 无效的武器 ID; } -const tagColors = [ - '#e28010', // 青绿 - '#0EA5E9', // 天蓝 - '#8B5CF6', // 紫色 - '#F59E0B', // 琥珀 - '#EF4444', // 红色 - '#EC4899', // 粉红 -]; + const tagColors = [ + '#e28010', // 青绿 + '#0EA5E9', // 天蓝 + '#8B5CF6', // 紫色 + '#F59E0B', // 琥珀 + '#EF4444', // 红色 + '#EC4899', // 粉红 + ]; return ( @@ -183,11 +184,11 @@ const tagColors = [ 改枪码: {modification.code} @@ -195,7 +196,14 @@ const tagColors = [ @@ -204,9 +212,9 @@ const tagColors = [ {/* 作者 */}
+ color: '#B59728', + fontWeight: 800 + }}> 作者: {modification.author || "未知"} @@ -214,7 +222,7 @@ const tagColors = [ {/* 标签列表 */} {(modification.tags?.length || 0) > 0 && (
- {(modification.tags || []).map((tag,idx) => ( + {(modification.tags || []).map((tag, idx) => ( 配件配置:
{(modification.accessories?.length || 0) > 0 ? (
-
+
{(modification.accessories || []).map((accessory, accessoryIndex) => (
({ value: slotName })) +const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName })) interface ModificationFormProps { form: ReturnType>[0] @@ -11,12 +16,20 @@ interface ModificationFormProps { lockFirearmId?: number } -const slotOptions = slotNames.map((slotName) => ({ value: slotName })) -const tuningOptions = tuningNames.map((tuningName) => ({ value: tuningName })) +interface AccessoryType { + slotName: string + accessoryName: string + tunings: Array<{ tuningName: string; tuningValue: number }> +} export default function ModificationForm({ form, onFinish, lockFirearmId }: ModificationFormProps) { const [firearmOptions, setFirearmOptions] = useState>([]) const [firearmLoading, setFirearmLoading] = useState(false) + + // 配件弹窗相关状态 + const [accessoryModalOpen, setAccessoryModalOpen] = useState(false) + const [editingAccessory, setEditingAccessory] = useState(null) + const [editingAccessoryIndex, setEditingAccessoryIndex] = useState(null) useEffect(() => { let active = true @@ -76,142 +89,415 @@ export default function ModificationForm({ form, onFinish, lockFirearmId }: Modi return [{ value: lockFirearmId, label: `武器 ID: ${lockFirearmId}` }, ...firearmOptions] }, [firearmOptions, lockFirearmId]) + // 打开添加配件弹窗 + const handleAddAccessory = () => { + setEditingAccessory(null) + setEditingAccessoryIndex(null) + setAccessoryModalOpen(true) + } + + // 打开编辑配件弹窗 + const handleEditAccessory = (index: number, accessory: AccessoryType) => { + setEditingAccessory(accessory) + setEditingAccessoryIndex(index) + setAccessoryModalOpen(true) + } + + // 删除配件 + const handleDeleteAccessory = (remove: (index: number) => void, index: number) => { + remove(index) + message.success("删除成功") + } + + // 配件弹窗确认回调 + const handleAccessoryModalOk = (values: AccessoryType) => { + const accessories = form.getFieldValue("accessories") || [] + + 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 = () => { + setAccessoryModalOpen(false) + setEditingAccessory(null) + setEditingAccessoryIndex(null) + } + 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()) - }, - }} - /> - + <> + + form={form} + layout="vertical" + onFinish={() => { + const allValues = form.getFieldsValue(true); + console.log('完整数据:', allValues); + onFinish(allValues); + }} + 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="name" + label="改装名称" + rules={[{ required: true, message: "请输入改装名称" }]}> + + - - name="code" - label="改枪码" - rules={[{ required: true, message: "请输入改枪码" }]}> - - + + name="code" + label="改枪码" + rules={[{ required: true, message: "请输入改枪码" }]}> + + - name="tags" label="标签"> - + - name="author" label="作者"> - - + name="author" label="作者"> + + - name="videoUrl" label="视频链接"> - - + name="videoUrl" label="视频链接"> + + - name="note" label="备注"> - - + name="note" label="备注"> + + - - {(accessoryFields, { add: addAccessory, remove: removeAccessory }) => ( -
- {accessoryFields.map((accessoryField) => ( - removeAccessory(accessoryField.name)}> - 删除配件 - - }> - - - + {/* 改造后的配件列表 */} +
- - - - - - {(tuningFields, { add: addTuning, remove: removeTuning }) => ( -
- {tuningFields.map((tuningField) => ( - - - label="配件配置"> + + {(accessoryFields, { add, remove }) => ( +
+ {/* 配件标签列表 */} +
+ {accessoryFields.length === 0 ? ( +
暂无配件,点击下方按钮添加
+ ) : ( + accessoryFields.map((accessoryField, index) => { + const accessory = form.getFieldValue(["accessories", index]) as AccessoryType + return ( + { + e.preventDefault() + handleDeleteAccessory(remove, accessoryField.name) + }} + closeIcon={} + className="px-3 py-1.5 text-sm border hover:shadow-sm transition-all" + style={{ marginRight: 8, marginBottom: 8 }} + > + + + {accessory?.slotName || "?"} + + · + + {accessory?.accessoryName || "未命名"} + + {accessory?.tunings && accessory.tunings.length > 0 && ( + + {accessory.tunings.length}项精校 + + )} + - - ))} - -
+ + + ) + }) )} - - - ))} - -
- )} -
- +
+ + {/* 添加按钮 */} + +
+ )} + + +
+ + + + {/* 配件编辑弹窗 */} + + ) } +// 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>[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>([]) +// 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) => ( +// +// +// +// +// +// +// +// +// +// ))} +// +//
+// )} +//
+//
+// ))} +// +//
+// )} +//
+// +// ) +// } \ No newline at end of file diff --git a/src/layout/hero-layout/index.tsx b/src/layout/hero-layout/index.tsx index d635675..c222751 100644 --- a/src/layout/hero-layout/index.tsx +++ b/src/layout/hero-layout/index.tsx @@ -1,5 +1,5 @@ -import { Outlet, Link, NavLink } from "react-router-dom" -import { useMemo } from "react" +import { Outlet, Link, NavLink, useNavigate } from "react-router-dom" +import { useEffect, useMemo } from "react" import dayjs from "dayjs" import { Dropdown } from "antd" import { @@ -12,17 +12,31 @@ import { AuthApi } from "@/api" import { useAppDispatch, useAppSelector } from "@/hooks/store" import { clearCurrentUser } from "@/store/auth-slice" import { useState } from "react" - +import { clearAutoLogout, scheduleAutoLogout } from "@/utils/autoLogout"; +import DisplayBoard from '@/components/DisplayBoard'; /** * Main application component that serves as the root layout. * Uses React Router's Outlet to render child routes. */ + + + export default function HeroLayout() { + + + + const today = useMemo(() => dayjs(), []) const user = useAppSelector((state) => state.auth.user) const dispatch = useAppDispatch() const [isDropdownOpen, setIsDropdownOpen] = useState(false) + // 统一登出函数:清除定时器、清除 Redux、跳转登录页 + const performLogout = () => { + clearAutoLogout(); + dispatch(clearCurrentUser()); + }; + async function handleLogout() { try { await AuthApi.logout() @@ -31,8 +45,22 @@ export default function HeroLayout() { } } + useEffect(() => { + if (user?.expiration) { + const localDate = new Date(user.expiration.replace(' ', 'T')); + const targetTimestamp = localDate.getTime(); + if (targetTimestamp > Date.now()) { + scheduleAutoLogout(targetTimestamp, performLogout); + } else { + // 已过期立即登出 + performLogout(); + } + } else { + clearAutoLogout(); + } + }, [user]); return ( -
+
{/* Navigation Header */}
@@ -40,12 +68,14 @@ export default function HeroLayout() {

《三角洲》指南

+
+ +
+ {/* Main Content Area */} -
+
@@ -145,7 +176,7 @@ export default function HeroLayout() { }, ], }}> - + {user.username} @@ -161,7 +192,7 @@ export default function HeroLayout() {
-

© 2024-{today.year()} Zihlu Wang 和 OnixByte。使用 React 与 TypeScript 构建。

+

© 2024-{today.year()} OnixByte。

diff --git a/src/page/firearms/index.tsx b/src/page/firearms/index.tsx index 28884b9..89c897a 100644 --- a/src/page/firearms/index.tsx +++ b/src/page/firearms/index.tsx @@ -1,17 +1,15 @@ -import { useCallback, useEffect, useState } from "react" -import { Link } from "react-router-dom" +import { useCallback, useEffect, useState, useMemo } from "react" 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 { Button, Card, Col, Pagination, Popconfirm, Row, Select, Spin, Typography, App, Input, AutoComplete } from "antd" import { ConfigProvider, theme } from 'antd'; -import type { CollapseProps } from 'antd'; +import type { AutoCompleteProps } from 'antd'; import { Collapse } from 'antd'; - const firearmTypeText: Record = { RIFLE: "步枪", SUB_MACHINE_GUN: "冲锋枪", @@ -23,12 +21,8 @@ const firearmTypeText: Record = { SPECIAL: "特殊", } - - const darkTheme = { algorithm: theme.darkAlgorithm, - - // token: { colorPrimary: '#00b96b' }, }; const allTypeValue = "ALL" @@ -45,36 +39,76 @@ export default function FirearmsPage() { const [typeFilter, setTypeFilter] = useState(allTypeValue) const [firearms, setFirearms] = useState([]) const [total, setTotal] = useState(0) - const [createModalOpen, setCreateModalOpen] = useState(false) const [editingFirearm, setEditingFirearm] = useState(null) const [deletingId, setDeletingId] = useState(null) + const [allOptions, setAllOptions] = useState([]); + const [filteredOptions, setFilteredOptions] = useState([]); + const [loading, setLoading] = useState(false); const loadFirearms = useCallback(async () => { - const pagedData = await FirearmApi.getFirearms({ - page: page - 1, - size: 12, + setLoading(true); + const start = Date.now(); + + try { + const pagedData = await FirearmApi.getFirearms({ + page: page - 1, + size: 8, + sortBy: "id", + direction: "ASC", + type: typeFilter === allTypeValue ? undefined : typeFilter, + }); + setFirearms(pagedData.items); + setTotal(pagedData.totalElements); + } catch (error) { + // 可在此处理错误,比如 message.error + } finally { + const elapsed = Date.now() - start; + if (elapsed < 500) { + await new Promise(resolve => setTimeout(resolve, 500 - elapsed)); + } + setLoading(false); + } + }, [page, typeFilter]); + + const GetFirearmName = useCallback(async () => { + const Firearms = await FirearmApi.getFirearms({ + page: 0, // 从第一页开始 + size: 100, // 足够大的值,或者根据实际总数调整 sortBy: "id", direction: "ASC", - type: typeFilter === allTypeValue ? undefined : typeFilter, - }) - setFirearms(pagedData.items) - setTotal(pagedData.totalElements) - }, [page, typeFilter]) - - const [expandedId, setExpandedId] = useState(null); - - + type: undefined, // 不按类型过滤,获取全部 + }); + const formatted = Firearms.items.map(item => ({ + label: item.name, + value: item.name, // 用名称作为 value + data: item, + })); + setAllOptions(formatted); + setFilteredOptions(formatted); + }, []); + const handleSearch = (searchText: string) => { + if (!searchText) { + setFilteredOptions(allOptions); + void loadFirearms() + return; + } + const lower = searchText.toLowerCase(); + const filtered = allOptions?.filter(opt => + opt?.label?.toString().toLowerCase().includes(lower) + ); + setFilteredOptions(filtered); + }; useEffect(() => { void loadFirearms() - }, [loadFirearms]) + void GetFirearmName() + }, [loadFirearms, GetFirearmName]) useEffect(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }, [page]); - async function handleDelete(firearm: Firearm) { setDeletingId(firearm.id) try { @@ -102,23 +136,45 @@ export default function FirearmsPage() { )}
- - 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) - }} - /> +
+ + 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) + }} + /> + { + const selected = option.data as Firearm; + console.log('选中 ID:', selected.id); + console.log('选中名称:', selected.name); + const pagedData = await FirearmApi.getFirearm(selected.id) + const newArray = []; + newArray.push(pagedData) + setFirearms(newArray) + setTotal(1) + }} + allowClear + /> +
+ {loading?(
+ +
):( {firearms.map((firearm) => ( @@ -171,11 +227,13 @@ export default function FirearmsPage() { actions={[
null} styles={ { root: { background: '#1e1e1e', + width: '100%' } } } @@ -397,13 +455,13 @@ export default function FirearmsPage() { )} - + )}
{ setPage(nextPage) diff --git a/src/page/login/index.tsx b/src/page/login/index.tsx index c077fe5..740d731 100644 --- a/src/page/login/index.tsx +++ b/src/page/login/index.tsx @@ -5,6 +5,7 @@ import { AuthApi } from "@/api" import { useAppDispatch } from "@/hooks/store" import { setCurrentUser } from "@/store/auth-slice" import { LoginRequest } from "@/types" +import dayjs from "dayjs"; export default function LoginPage() { const navigate = useNavigate() @@ -16,7 +17,9 @@ export default function LoginPage() { setLoading(true) try { const user = await AuthApi.login(values) - dispatch(setCurrentUser(user)) + const expireTimestamp = dayjs(user.expiration).valueOf(); + const authUser = { ...user, expireAt: expireTimestamp }; + dispatch(setCurrentUser(authUser)) message.success(`欢迎回来,${user.username}`) navigate("/firearms") } catch { diff --git a/src/store/auth-slice.ts b/src/store/auth-slice.ts index 71dc980..698c712 100644 --- a/src/store/auth-slice.ts +++ b/src/store/auth-slice.ts @@ -1,6 +1,11 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit" import { User } from "@/types" +export interface AuthUser extends User { + expireAt: number; // 转换 expiration 得到的时间戳 +} + + interface AuthState { user: User | null } @@ -15,9 +20,11 @@ const authSlice = createSlice({ reducers: { setCurrentUser(state, action: PayloadAction) { state.user = action.payload + localStorage.setItem('auth_user', JSON.stringify(action.payload)); }, clearCurrentUser(state) { state.user = null + localStorage.removeItem('auth_user'); }, }, }) diff --git a/src/types/DailyPasswordResponse.ts b/src/types/DailyPasswordResponse.ts new file mode 100644 index 0000000..5755571 --- /dev/null +++ b/src/types/DailyPasswordResponse.ts @@ -0,0 +1,25 @@ +export interface PasswordItem { + mapName: string; // 地图名称,如 "零号大坝" + password: string; // 四位数字密码,如 "6460" +} + +// data 字段的类型 +export interface DailyPasswordData { + updateDate: string; // 如 "06月14日每日密码已更新" + totalCount: number; // 总数,这里固定为 5 + passwords: PasswordItem[]; // 密码数组 + source: string; // 如 "三角洲行动每日密码" + lastUpdated: string; // 最后更新时间字符串,如 "2026-06-14 16:54:46" + timestamp: number; // Unix 时间戳(秒),如 1781427286 +} + +// 根返回值的类型 +export interface DailyPasswordResponse { + status: 'success' | 'error'; // 可根据实际调整字面量,或直接用 string + message: string; // 如 "每日密码获取成功" + data: DailyPasswordData; + metadata: { + version: string; // 如 "1.0" + author: string; // 如 "tmini.net" + }; +} \ No newline at end of file diff --git a/src/types/auth.ts b/src/types/auth.ts index 34c2b14..cc77d72 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -7,5 +7,5 @@ export interface User { id: number username: string email: string + expiration: string } - diff --git a/src/types/common.ts b/src/types/common.ts index 9f98731..d3a8b48 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,6 +1,7 @@ export type Direction = "ASC" | "DESC" export interface Page { + map(arg0: (item: any) => { label: any; value: any; data: any }): unknown items: T[] page: number size: number diff --git a/src/utils/autoLogout.ts b/src/utils/autoLogout.ts new file mode 100644 index 0000000..5fd9d4a --- /dev/null +++ b/src/utils/autoLogout.ts @@ -0,0 +1,20 @@ +let logoutTimer: ReturnType | null = null; + +export const scheduleAutoLogout = (expireTimestamp: number, onLogout: () => void) => { + if (logoutTimer) clearTimeout(logoutTimer); + const delay = expireTimestamp - Date.now(); + if (delay <= 0) { + onLogout(); + return; + } + logoutTimer = setTimeout(() => { + onLogout(); + }, delay); +}; + +export const clearAutoLogout = () => { + if (logoutTimer) { + clearTimeout(logoutTimer); + logoutTimer = null; + } +}; \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 629a4ba..aab76ae 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -44,6 +44,15 @@ export default defineConfig({ }, }, }, + server: { + proxy: { + "/api": { + target: "http://dfguide.onixbyte.cn/dev-api", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)),