Browse Source

顶部功能实现

main
mm 7 months ago
parent
commit
51d2962a95
  1. 2
      package.json
  2. 79
      pnpm-lock.yaml
  3. 105
      src/api/client.ts
  4. 7
      src/api/services/index.ts
  5. 44
      src/api/services/transactionService.ts
  6. 58
      src/api/services/userService.ts
  7. 39
      src/api/types.ts
  8. 1
      src/app/home/assets/components/AssetListCard.tsx
  9. 1
      src/app/home/assets/components/ResourceCard.tsx
  10. 35
      src/app/home/assets/components/TransferCard.tsx
  11. 57
      src/app/home/assets/components/VoteCard.tsx
  12. 162
      src/app/home/assets/components/WalletCard.tsx
  13. 14
      src/app/home/assets/page.tsx
  14. 43
      src/app/home/components/HeaderMenu/api.ts
  15. 206
      src/app/home/components/HeaderMenu/groupManager.tsx
  16. 261
      src/app/home/components/HeaderMenu/index.tsx
  17. 2
      src/app/home/layout.tsx
  18. 31
      src/store/userAtoms.ts

2
package.json

@ -14,6 +14,8 @@
"@ant-design/nextjs-registry": "^1.0.2",
"@ant-design/pro-components": "^2.8.2",
"antd": "^5.22.6",
"axios": "^1.7.9",
"jotai": "^2.11.0",
"next": "15.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"

79
pnpm-lock.yaml

@ -23,6 +23,12 @@ importers:
antd:
specifier: ^5.22.6
version: 5.22.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
axios:
specifier: ^1.7.9
version: 1.7.9
jotai:
specifier: ^2.11.0
version: 2.11.0(@types/react@19.0.2)(react@19.0.0)
next:
specifier: 15.1.2
version: 15.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -822,6 +828,9 @@ packages:
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@ -830,6 +839,9 @@ packages:
resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==}
engines: {node: '>=4'}
axios@1.7.9:
resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@ -922,6 +934,10 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@ -1011,6 +1027,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@ -1265,6 +1285,15 @@ packages:
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@ -1272,6 +1301,10 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -1531,6 +1564,18 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
jotai@2.11.0:
resolution: {integrity: sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=17.0.0'
react: '>=17.0.0'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -1923,6 +1968,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
prr@1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
@ -3603,12 +3651,22 @@ snapshots:
ast-types-flow@0.0.8: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.0.0
axe-core@4.10.2: {}
axios@1.7.9:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {}
balanced-match@1.0.2: {}
@ -3711,6 +3769,10 @@ snapshots:
color-string: 1.9.1
optional: true
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@2.20.3: {}
commander@4.1.1: {}
@ -3794,6 +3856,8 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
delayed-stream@1.0.0: {}
dequal@2.0.3: {}
detect-libc@2.0.3:
@ -4184,6 +4248,8 @@ snapshots:
flatted@3.3.2: {}
follow-redirects@1.15.9: {}
for-each@0.3.3:
dependencies:
is-callable: 1.2.7
@ -4193,6 +4259,12 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
fsevents@2.3.3:
optional: true
@ -4459,6 +4531,11 @@ snapshots:
jiti@1.21.7: {}
jotai@2.11.0(@types/react@19.0.2)(react@19.0.0):
optionalDependencies:
'@types/react': 19.0.2
react: 19.0.0
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@ -4818,6 +4895,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
proxy-from-env@1.1.0: {}
prr@1.0.1:
optional: true

105
src/api/client.ts

@ -0,0 +1,105 @@
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
/**
* API客户端类
* axios的HTTP请求方法
*/
export class ApiClient {
private client: AxiosInstance;
// constructor(baseURL: string = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com') {
constructor(baseURL: string = " http://192.168.31.65:6600") {
this.client = axios.create({
baseURL,
timeout: 10000, // 请求超时时间:10秒
headers: {
"Content-Type": "application/json",
},
});
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
// 在这里添加认证token
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
this.client.interceptors.response.use(
(response) => response,
(error) => {
// 在这里处理常见错误
if (error.response?.status === 401) {
// 处理未授权访问
console.error("未授权访问");
}
return Promise.reject(error);
},
);
}
/**
* GET请求
* @param url
* @param config
* @returns
*/
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(url, config);
return response.data;
}
/**
* POST请求
* @param url
* @param data
* @param config
* @returns
*/
async post<T>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response.data;
}
/**
* PUT请求
* @param url
* @param data
* @param config
* @returns
*/
async put<T>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
const response = await this.client.put<T>(url, data, config);
return response.data;
}
/**
* DELETE请求
* @param url
* @param config
* @returns
*/
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response.data;
}
}
// 导出API客户端实例
export const apiClient = new ApiClient();

7
src/api/services/index.ts

@ -0,0 +1,7 @@
/**
*
* 便
*/
export * from './userService';
export * from './transactionService';

44
src/api/services/transactionService.ts

@ -0,0 +1,44 @@
import { apiClient } from '../client';
import { ApiResponse, Transaction, CreateTransactionRequest } from '../types';
/**
* - API请求
*/
export class TransactionService {
// 交易API的基础路径
private static readonly BASE_PATH = '/transactions';
/**
*
* @param data
* @returns
*/
static async createTransaction(data: CreateTransactionRequest): Promise<ApiResponse<Transaction>> {
return apiClient.post<ApiResponse<Transaction>>(this.BASE_PATH, data);
}
/**
* ID的交易详情
* @param id ID
* @returns
*/
static async getTransaction(id: string): Promise<ApiResponse<Transaction>> {
return apiClient.get<ApiResponse<Transaction>>(`${this.BASE_PATH}/${id}`);
}
/**
*
* @param params
* @param params.page
* @param params.limit
* @param params.status
* @returns
*/
static async listTransactions(params?: {
page?: number;
limit?: number;
status?: Transaction['status'];
}): Promise<ApiResponse<Transaction[]>> {
return apiClient.get<ApiResponse<Transaction[]>>(this.BASE_PATH, { params });
}
}

58
src/api/services/userService.ts

@ -0,0 +1,58 @@
import { apiClient } from '../client';
import { ApiResponse, LoginRequest, LoginResponse, User } from '../types';
/**
* - API请求
*/
export class UserService {
// 用户API的基础路径
private static readonly BASE_PATH = '/users';
/**
*
* @param credentials
* @returns token和用户信息
*/
static async login(credentials: LoginRequest): Promise<ApiResponse<LoginResponse>> {
return apiClient.post<ApiResponse<LoginResponse>>(`${this.BASE_PATH}/login`, credentials);
}
/**
*
* @returns
*/
static async getCurrentUser(): Promise<ApiResponse<User>> {
return apiClient.get<ApiResponse<User>>(`${this.BASE_PATH}/me`);
}
/**
*
* @param userId ID
* @param data
* @returns
*/
static async updateProfile(userId: string, data: Partial<User>): Promise<ApiResponse<User>> {
return apiClient.put<ApiResponse<User>>(`${this.BASE_PATH}/${userId}`, data);
}
}
// 使用示例:
/*
try {
// 尝试用户登录
const response = await UserService.login({
email: 'user@example.com',
password: 'password123'
});
if (response.success) {
const { token, user } = response.data;
// 将token存储在localStorage中
localStorage.setItem('token', token);
// 处理用户数据
console.log('登录用户:', user);
}
} catch (error) {
console.error('登录失败:', error);
}
*/

39
src/api/types.ts

@ -0,0 +1,39 @@
// Common API response wrapper
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
// User related types
export interface User {
id: string;
username: string;
email: string;
createdAt: string;
}
export interface LoginRequest {
email: string;
password: string;
}
export interface LoginResponse {
token: string;
user: User;
}
// Transaction related types
export interface Transaction {
id: string;
amount: number;
currency: string;
status: 'pending' | 'completed' | 'failed';
timestamp: string;
}
export interface CreateTransactionRequest {
amount: number;
currency: string;
recipientAddress: string;
}

1
src/app/home/assets/components/AssetListCard.tsx

@ -63,6 +63,7 @@ const AssetListCard: React.FC = () => {
return (
<Card
style={{ minHeight: "240px" }}
title={
<Space>
<UnorderedListOutlined />

1
src/app/home/assets/components/ResourceCard.tsx

@ -6,6 +6,7 @@ import Link from "antd/lib/typography/Link";
const ResourceCard: React.FC = () => {
return (
<Card
style={{ minHeight: "240px" }}
title={
<div
style={{

35
src/app/home/assets/components/TransferCard.tsx

@ -11,6 +11,7 @@ import Link from "antd/lib/typography/Link";
const TransferCard: React.FC = () => {
return (
<Card
style={{ minHeight: "240px" }}
title={
<>
<SwapOutlined />
@ -18,22 +19,24 @@ const TransferCard: React.FC = () => {
}
hoverable
>
<Row justify="space-between">
<div>
<Switch></Switch>
<Text> &lt;$0.5 </Text>
<Tooltip
title={
"开启选项后,价值小于 $0.5 的转账将被隐藏。如果未进行日期筛选,则仅显示近 180 天的记录;如果进行日期筛选,则仅显示最多 180 天的记录。"
}
>
<QuestionCircleOutlined />
</Tooltip>
</div>
<Link>
<FilterOutlined />
</Link>
</Row>
<div style={{ height: 100 }}>
<Row justify="space-between">
<div>
<Switch></Switch>
<Text> &lt;$0.5 </Text>
<Tooltip
title={
"开启选项后,价值小于 $0.5 的转账将被隐藏。如果未进行日期筛选,则仅显示近 180 天的记录;如果进行日期筛选,则仅显示最多 180 天的记录。"
}
>
<QuestionCircleOutlined />
</Tooltip>
</div>
<Link>
<FilterOutlined />
</Link>
</Row>
</div>
</Card>
);
};

57
src/app/home/assets/components/VoteCard.tsx

@ -1,5 +1,5 @@
import React from "react";
import { Card, Progress, Row, Space, Tooltip, Typography } from "antd";
import { Button, Card, Progress, Row, Space, Tooltip, Typography } from "antd";
import { AppstoreOutlined, QuestionCircleOutlined } from "@ant-design/icons";
import Link from "antd/lib/typography/Link";
@ -8,6 +8,7 @@ const { Title } = Typography;
const VoteCard: React.FC = () => {
return (
<Card
style={{ minHeight: "240px" }}
title={
<div
style={{
@ -24,32 +25,34 @@ const VoteCard: React.FC = () => {
}
hoverable
>
<Row justify="space-between">
<div>
<Tooltip
title={
"投票权 (TP) 用来给 TRON SR 投票,用户可以通过质押 TRX 来获得投票权。"
}
>
<QuestionCircleOutlined />
</Tooltip>
</div>
<div>0.0</div>
</Row>
<Progress percent={30} size="small" />
<Row justify="space-between">
<div>APY</div>
<div>0.0%</div>
</Row>
<Row justify="space-between">
<div></div>
<div>0TRX</div>
</Row>
<Space>
<button></button>
<button></button>
</Space>
<div style={{ height: 100 }}>
<Row justify="space-between">
<div>
<Tooltip
title={
"投票权 (TP) 用来给 TRON SR 投票,用户可以通过质押 TRX 来获得投票权。"
}
>
<QuestionCircleOutlined />
</Tooltip>
</div>
<div>0.0</div>
</Row>
<Progress percent={30} size="small" />
<Row justify="space-between">
<div>APY</div>
<div>0.0%</div>
</Row>
<Row justify="space-between">
<div></div>
<div>0TRX</div>
</Row>
<Space>
<Button></Button>
<Button></Button>
</Space>
</div>
</Card>
);
};

162
src/app/home/assets/components/WalletCard.tsx

@ -1,28 +1,150 @@
import React from "react";
import { Card, Space, Typography } from "antd";
import { WalletOutlined } from "@ant-design/icons";
import React, { useState } from "react";
import {
Card,
Space,
Typography,
Modal,
Input,
Select,
Button,
QRCode,
message,
} from "antd";
import {
WalletOutlined,
QrcodeOutlined,
TransactionOutlined,
} from "@ant-design/icons";
const { Title } = Typography;
const { TextArea } = Input;
const WalletCard: React.FC = () => {
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false);
const [receiverAddress, setReceiverAddress] = useState("");
const [selectedToken, setSelectedToken] = useState("TRX");
const [transferNote, setTransferNote] = useState("");
// 示例钱包地址
const walletAddress = "TRx1234567890abcdefghijklmnopqrstuvwxyz";
const handleTransfer = () => {
if (!receiverAddress) {
message.error("请输入接收账户地址");
return;
}
// 这里添加转账逻辑
message.success("转账请求已提交");
setIsTransferModalOpen(false);
// 重置表单
setReceiverAddress("");
setSelectedToken("TRX");
setTransferNote("");
};
const handleSaveQRCode = () => {
message.success("二维码已保存");
};
return (
<Card
title={
<>
<WalletOutlined />
</>
}
hoverable
>
<div className="text-center">
<Title level={3}>0.00 TRX</Title>
<p></p>
</div>
<Space>
<button></button>
<button></button>
</Space>
</Card>
<>
<Card
title={
<>
<WalletOutlined />
</>
}
hoverable
style={{ minHeight: "240px" }}
>
<div className="text-center">
<Title level={3}>0.00 TRX</Title>
<p></p>
</div>
<Space>
<Button
type="primary"
icon={<TransactionOutlined />}
onClick={() => setIsTransferModalOpen(true)}
>
</Button>
<Button
icon={<QrcodeOutlined />}
onClick={() => setIsReceiveModalOpen(true)}
>
</Button>
</Space>
</Card>
{/* 转账弹窗 */}
<Modal
title="转账"
open={isTransferModalOpen}
onOk={handleTransfer}
onCancel={() => setIsTransferModalOpen(false)}
centered
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<p></p>
<Input
placeholder="请输入接收账户地址"
value={receiverAddress}
onChange={(e) => setReceiverAddress(e.target.value)}
/>
</div>
<div>
<p></p>
<Select
style={{ width: "100%" }}
value={selectedToken}
onChange={setSelectedToken}
options={[
{ value: "TRX", label: "TRX" },
{ value: "USDT", label: "USDT" },
]}
/>
</div>
<div>
<p></p>
<TextArea
rows={4}
placeholder="请输入转账备注"
value={transferNote}
onChange={(e) => setTransferNote(e.target.value)}
/>
</div>
</Space>
</Modal>
{/* 收款弹窗 */}
<Modal
title="收款"
open={isReceiveModalOpen}
onCancel={() => setIsReceiveModalOpen(false)}
footer={[
<Button key="save" type="primary" onClick={handleSaveQRCode}>
</Button>,
]}
centered
>
<Space
direction="vertical"
style={{ width: "100%", alignItems: "center" }}
size="large"
>
<div style={{ wordBreak: "break-all", textAlign: "center" }}>
<p></p>
<p>{walletAddress}</p>
</div>
<QRCode value={walletAddress} size={200} />
</Space>
</Modal>
</>
);
};

14
src/app/home/assets/page.tsx

@ -32,21 +32,21 @@ const AssetsPage: React.FC = () => {
</div>
<div className="p-6">
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={8}>
<Col xs={24} sm={12} lg={12}>
<WalletCard />
</Col>
<Col xs={24} sm={12} lg={8}>
<Col xs={24} sm={12} lg={12}>
<ResourceCard />
</Col>
<Col xs={24} sm={12} lg={8}>
<Col xs={24} sm={12} lg={12}>
<VoteCard />
</Col>
<Col xs={24} sm={12} lg={16}>
<AssetListCard />
</Col>
<Col xs={24} sm={12} lg={8}>
<Col xs={24} sm={12} lg={12}>
<TransferCard />
</Col>
<Col xs={24} sm={24} lg={24}>
<AssetListCard />
</Col>
</Row>
</div>
</>

43
src/app/home/components/HeaderMenu/api.ts

@ -0,0 +1,43 @@
import { apiClient } from "@/api/client";
export class Api {
private static readonly BASE_PATH = "/transactions";
//http://192.168.31.65:6600/api/v1/group/add
static async addGroupArr(data: any) {
return apiClient.post("/api/v1/group/add", data);
}
//http://192.168.31.65:6600/api/v1/group/list
static async getGroupArr() {
return apiClient.get("/api/v1/group/list");
}
//http://192.168.31.65:6600/api/v1/group/update
static async updateGroupArr(data: any) {
return apiClient.put("/api/v1/group/update", data);
}
//http://192.168.31.65:6600/api/v1/group/{id}
static async deleteGroupArr(id: string) {
return apiClient.delete(`/api/v1/group/${id}`);
}
//http://192.168.31.65:6600/api/v1/address/add
static async addAddressArr(data: any) {
return apiClient.post("/api/v1/address/add", data);
}
//http://192.168.31.65:6600/api/v1/address/list
static async getAddressArr() {
return apiClient.get("/api/v1/address/list");
}
//http://192.168.31.65:6600/api/v1/address/update
static async updateAddressArr(data: any) {
return apiClient.put("/api/v1/address/update", data);
}
//http://192.168.31.65:6600/api/v1/address/{id}
static async deleteAddressArr(id: string) {
return apiClient.delete(`/api/v1/address/${id}`);
}
}

206
src/app/home/components/HeaderMenu/groupManager.tsx

@ -0,0 +1,206 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button, Form, Modal, Space, Table, Typography } from "antd";
import { BetaSchemaForm } from "@ant-design/pro-components";
import { Api } from "./api";
const { Link } = Typography;
interface GroupManagerProps {
modalOpen: boolean;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
dataSource: any[];
setDataSource: React.Dispatch<React.SetStateAction<any[]>>;
getGroupArr: Function;
}
const GroupManager: React.FC<GroupManagerProps> = ({
modalOpen,
setModalOpen,
dataSource,
setDataSource,
getGroupArr,
}) => {
const [createOpen, setCreate] = useState(false);
const [editorOpen, setEditorOpen] = useState(false);
const [createForm] = Form.useForm(); // 只取 Form 实例
const [editForm] = Form.useForm(); // 只取 Form 实例
const onCancel = () => {
setModalOpen(false);
};
const openCreateForm = () => {
createForm.resetFields();
setCreate(true);
};
const openEditForm = (record: any) => {
editForm.setFieldsValue(record);
setEditorOpen(true);
};
const closeCreateForm = () => {
setCreate(false);
};
const closeEditForm = () => {
setEditorOpen(false);
};
//============================================通信====================================================
const handleAdd = async () => {
const reqData = createForm.getFieldsValue();
await Api.addGroupArr(reqData);
getGroupArr();
closeCreateForm();
};
const subDelect = async (id: string) => {
await Api.deleteGroupArr(id);
getGroupArr();
};
const subEdit = async () => {
const reqData = editForm.getFieldsValue();
await Api.updateGroupArr(reqData);
getGroupArr();
closeEditForm();
};
//===================================================================================================
const columns = [
{
title: "ID",
dataIndex: "id",
key: "id",
formItemProps: {
hidden: true,
},
},
{
title: "分组名称",
dataIndex: "name",
key: "name",
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
},
{
title: "排序",
dataIndex: "sort",
key: "sort",
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
},
{
title: "操作",
hideInForm: true,
render: (_: any, record: any) => {
return (
<Space>
<a onClick={() => openEditForm(record)}></a>
<a onClick={() => subDelect(record.id)}></a>
</Space>
);
},
},
];
return (
<>
<Modal
title="分组管理"
open={modalOpen}
onCancel={onCancel}
footer={null}
width={1000}
height={1000}
>
<Button
onClick={() => {
openCreateForm();
}}
type="primary"
style={{ marginBottom: 16 }}
>
</Button>
<Table
columns={columns as []}
dataSource={dataSource}
bordered
scroll={{ x: 600, y: 600 }}
/>
</Modal>
<Modal
title="添加"
open={createOpen}
onCancel={closeCreateForm}
footer={null}
>
<BetaSchemaForm
form={createForm}
layoutType="Form"
onFinish={handleAdd}
columns={columns as any[]}
submitter={{
render: (props) => {
return [
<Button
key="submit"
type="primary"
onClick={() => props.submit()}
>
</Button>,
<Button
key="cancel"
onClick={closeCreateForm}
style={{ marginLeft: 8 }}
>
</Button>,
];
},
}}
/>
</Modal>
<Modal
title="编辑"
open={editorOpen}
onCancel={closeEditForm}
footer={null}
>
<BetaSchemaForm
form={editForm}
layoutType="Form"
onFinish={subEdit}
columns={columns as any[]}
submitter={{
render: (props) => {
return [
<Button
key="submit"
type="primary"
onClick={() => props.submit()}
>
</Button>,
<Button
key="cancel"
onClick={closeEditForm}
style={{ marginLeft: 8 }}
>
</Button>,
];
},
}}
/>
</Modal>
</>
);
};
export default GroupManager;

261
src/app/home/components/HeaderMenu/index.tsx

@ -1,33 +1,43 @@
"use client";
import React, { useState } from "react";
import { Button, Modal, Space, Table, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { Button, Form, Modal, Select, Space, Table, Typography } from "antd";
import { BetaSchemaForm } from "@ant-design/pro-components";
const { Link } = Typography;
import { Api } from "./api";
import GroupManager from "./groupManager";
import { useAtom } from "jotai";
import { userInfoAtom } from "@/store/userAtoms";
const { Link, Text } = Typography;
const HeaderMenu: React.FC = () => {
const [userInfo, setUserInfo] = useAtom(userInfoAtom);
const [groupArr, setGroupArr] = useState<any[]>([]);
const [dataSource, setDataSource] = useState([]);
const [modalOpen, setModalOpen] = useState(false);
const [formModalOpen, setFormModalOpen] = useState(false);
const [dataSource, setDataSource] = useState([
{
key: '1',
name: "John Brown",
age: 32,
address: "New York No. 1 Lake Park",
},
{
key: '2',
name: "Jim Green",
age: 42,
address: "London No. 1 Lake Park",
},
{
key: '3',
name: "Joe Black",
age: 32,
address: "Sydney No. 1 Lake Park",
},
]);
const [groupMgrOpen, setGroupMgrOpen] = useState(false);
const [createOpen, setCreate] = useState(false);
const [editorOpen, setEditorOpen] = useState(false);
const [createForm] = Form.useForm(); // 只取 Form 实例
const [editForm] = Form.useForm(); // 只取 Form 实例
const openCreateForm = () => {
createForm.resetFields();
setCreate(true);
};
const openEditForm = (record: any) => {
editForm.setFieldsValue(record);
setEditorOpen(true);
};
const closeCreateForm = () => {
setCreate(false);
};
const closeEditForm = () => {
setEditorOpen(false);
};
const onShow = () => {
setModalOpen(true);
};
@ -35,46 +45,140 @@ const HeaderMenu: React.FC = () => {
setModalOpen(false);
};
const subDelect = (record: any) => {};
const subEdit = (record: any) => {};
//============================================通信====================================================
const handleAdd = () => {
setFormModalOpen(true);
const handleAdd = async () => {
const reqData = createForm.getFieldsValue();
await Api.addAddressArr(reqData);
getAddressArr();
closeCreateForm();
};
const subDelect = async (record: any) => {
await Api.deleteAddressArr(record.id);
getAddressArr();
};
const subEdit = async () => {
const reqData = editForm.getFieldsValue();
await Api.updateAddressArr(reqData);
getAddressArr();
closeEditForm();
};
const handleFormSubmit = async (values: any) => {
const newKey = (dataSource.length + 1).toString();
setDataSource([...dataSource, { ...values, key: newKey }]);
setFormModalOpen(false);
const getAddressArr = async () => {
const arr = await Api.getAddressArr();
setDataSource((arr as any)?.data.data || []);
};
const handleFormCancel = () => {
setFormModalOpen(false);
const getGroupArr = async () => {
const groupArr = await Api.getGroupArr();
setGroupArr((groupArr as any)?.data.data || []);
};
useEffect(() => {
if (modalOpen) {
getGroupArr();
getAddressArr();
}
}, [modalOpen]);
//===================================================================================================
const columns = [
{
title: "姓名",
title: "ID",
dataIndex: "id",
key: "id",
formItemProps: {
hidden: true,
},
},
{
title: "分组",
dataIndex: "group_id",
key: "group_id",
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
render: (group_id: string, record: any) => {
let foundItem: any;
if (groupArr && Array.isArray(groupArr)) {
foundItem = groupArr.find((item) => item.id === record.group_id);
}
return <Text>{foundItem?.name}</Text>;
},
renderFormItem: (item: any, _: any, dform: any) => {
let op: any[] = [];
groupArr.forEach((item) => {
op.push({ value: item.id, label: item.name });
});
return (
<Select
defaultValue=""
style={{ width: 120 }}
onChange={(value: any) => {
dform.setFieldValue("group_id", value);
}}
options={op}
></Select>
);
},
},
{
title: "名称",
dataIndex: "name",
key: "name",
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
render: (name: string) => {
return (
<Link
onClick={() => {
setUserInfo({
address: name,
});
setModalOpen(false);
}}
>
{name}
</Link>
);
},
},
{
title: "年龄",
dataIndex: "age",
key: "age",
title: "密钥",
dataIndex: "key",
key: "key",
valueType: "textarea",
hideInEdit: true,
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
},
{
title: "地址",
dataIndex: "address",
key: "address",
hideInEdit: true,
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
},
{
title: "排序",
dataIndex: "sort",
key: "sort",
formItemProps: {
rules: [{ required: true, message: "必填项" }],
},
},
{
title: "操作",
key: "action",
hideInForm: true,
render: (_: any, record: any) => {
return (
<Space>
<a onClick={() => subEdit(record)}></a>
<a onClick={() => openEditForm(record)}></a>
<a onClick={() => subDelect(record)}></a>
</Space>
);
@ -84,18 +188,34 @@ const HeaderMenu: React.FC = () => {
return (
<>
<Link onClick={onShow}>JGAJKGASJKAGSJKAGJKSG</Link>
<Link onClick={onShow} copyable>
{userInfo?.address}
</Link>
<Modal
title="弹窗标题"
open={modalOpen}
onCancel={onCancel}
footer={null}
width={1000}
width={1400}
height={1000}
>
<Button onClick={handleAdd} type="primary" style={{ marginBottom: 16 }}>
</Button>
<Space>
<Button
onClick={openCreateForm}
type="primary"
style={{ marginBottom: 16 }}
>
</Button>
<Button
onClick={() => {
setGroupMgrOpen(true);
}}
style={{ marginBottom: 16 }}
>
</Button>
</Space>
<Table
columns={columns as []}
dataSource={dataSource}
@ -105,15 +225,49 @@ const HeaderMenu: React.FC = () => {
</Modal>
<Modal
title="添加新内容"
open={formModalOpen}
onCancel={handleFormCancel}
title="添加"
open={createOpen}
onCancel={closeCreateForm}
footer={null}
>
<BetaSchemaForm
form={createForm}
layoutType="Form"
onFinish={handleAdd}
columns={columns as any[]}
submitter={{
render: (props) => {
return [
<Button
key="submit"
type="primary"
onClick={() => props.submit()}
>
</Button>,
<Button
key="cancel"
onClick={closeCreateForm}
style={{ marginLeft: 8 }}
>
</Button>,
];
},
}}
/>
</Modal>
<Modal
title="编辑"
open={editorOpen}
onCancel={closeEditForm}
footer={null}
>
<BetaSchemaForm
form={editForm}
layoutType="Form"
onFinish={handleFormSubmit}
columns={columns}
onFinish={subEdit}
columns={columns.filter((item: any) => !item.hideInEdit) as any[]}
submitter={{
render: (props) => {
return [
@ -126,7 +280,7 @@ const HeaderMenu: React.FC = () => {
</Button>,
<Button
key="cancel"
onClick={handleFormCancel}
onClick={closeEditForm}
style={{ marginLeft: 8 }}
>
@ -136,6 +290,13 @@ const HeaderMenu: React.FC = () => {
}}
/>
</Modal>
<GroupManager
modalOpen={groupMgrOpen}
setModalOpen={setGroupMgrOpen}
dataSource={groupArr}
setDataSource={setGroupArr}
getGroupArr={getGroupArr}
></GroupManager>
</>
);
};

2
src/app/home/layout.tsx

@ -18,7 +18,7 @@ const RootLayout = ({ children }: React.PropsWithChildren) => (
<Layout
style={{ minWidth: 100, maxWidth: 1800, minHeight: 100, height: "100vh" }}
>
<Header>
<Header style={{ backgroundColor: "#491d1d" }}>
<HeaderMenu></HeaderMenu>
</Header>
<Layout>

31
src/store/userAtoms.ts

@ -0,0 +1,31 @@
import { atom } from "jotai";
interface UserInfo {
address: string;
}
// 创建初始用户信息
const initialUserInfo: UserInfo = {
address: "AAAAAAAAAAAAAAAAAAA",
};
// 用户信息 atom
export const userInfoAtom = atom<UserInfo>(initialUserInfo);
// 登录状态 atom
export const isLoggedInAtom = atom<boolean>(false);
// 派生的 atom,用于更新用户信息
export const userInfoActionsAtom = atom(
(get) => get(userInfoAtom),
(get, set, updates: Partial<UserInfo>) => {
const currentInfo = get(userInfoAtom);
set(userInfoAtom, { ...currentInfo, ...updates });
},
);
// 清除用户信息的 atom
export const clearUserInfoAtom = atom(null, (get, set) => {
set(userInfoAtom, initialUserInfo);
set(isLoggedInAtom, false);
});
Loading…
Cancel
Save