init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# Dependencies
node_modules/
vendor/
# Build output
.next/
dist/
build/
out/
# Environment files
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Go
tmp/
*.exe
# Archives
*.tar.gz
*.zip
# Backup
backup/
# Claude config (local)
.claude/

View File

@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

5
antdesign/.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
dist/
.umi/
*.log
.git

16
antdesign/.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

40
antdesign/.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
# roadhog-api-doc ignore
/src/utils/request-temp.js
_roadhog-api-doc
# production
/dist
# misc
.DS_Store
npm-debug.log*
yarn-error.log
/coverage
.idea
yarn.lock
package-lock.json
*bak
.vscode
# visual studio code
.history
*.log
functions/*
.temp/**
# umi
.umi
.umi-production
.umi-test
# screenshot
screenshot
.firebase
build

View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit $1

View File

@@ -0,0 +1 @@
lint-staged

5
antdesign/.lintstagedrc Normal file
View File

@@ -0,0 +1,5 @@
{
"**/*.{js,jsx,tsx,ts,md,css,less,json}": [
"npx @biomejs/biome check --write"
]
}

1
antdesign/.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

15
antdesign/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
RUN apk add --no-cache python3 make g++
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN npm ci --legacy-peer-deps
COPY . .
RUN npm run build
# ---- Run stage (nginx) ----
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 10502
CMD ["nginx", "-g", "daemon off;"]

57
antdesign/README.md Normal file
View File

@@ -0,0 +1,57 @@
# Ant Design Pro
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
## Environment Prepare
Install `node_modules`:
```bash
npm install
```
or
```bash
yarn
```
## Provided Scripts
Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
Scripts provided in `package.json`. It's safe to modify or add additional script:
### Start project
```bash
npm start
```
### Build project
```bash
npm run build
```
### Check code style
```bash
npm run lint
```
You can also use script to auto fix some lint error:
```bash
npm run lint:fix
```
### Test code
```bash
npm test
```
## More
You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).

48
antdesign/biome.json Normal file
View File

@@ -0,0 +1,48 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"ignoreUnknown": true,
"includes": [
"**/*",
"!**/.umi/**",
"!**/.umi-production/**",
"!**/.umi-test/**",
"!**/.umi-test-production/**",
"!**/src/services/**",
"!**/mock/**",
"!**/dist/**",
"!**/server/**",
"!**/public/**",
"!**/coverage/**",
"!**/node_modules/**",
"!biome.json"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
},
"a11y": {
"noStaticElementInteractions": "off",
"useValidAnchor": "off",
"useKeyWithClickEvents": "off"
}
}
},
"javascript": {
"jsxRuntime": "reactClassic",
"formatter": {
"quoteStyle": "single"
}
}
}

8216
antdesign/bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
#!/bin/bash
echo "🔍 检查页面组件的导入问题"
echo "================================"
check_imports() {
local file=$1
echo ""
echo "检查: $file"
# 检查是否有未使用 @ 别名的相对路径
if grep -n "from '\.\." "$file" | head -5; then
echo "⚠️ 发现相对路径导入(可能导致问题)"
else
echo "✓ 导入路径正确"
fi
# 检查是否有 react-i18next
if grep -q "react-i18next" "$file"; then
echo "⚠️ 使用了 react-i18nextUmi 使用内置国际化)"
fi
}
# 检查所有新页面
check_imports "src/pages/Vault/Trade/index.tsx"
check_imports "src/pages/Vault/USDC/index.tsx"
check_imports "src/pages/LP/LPPanelNew.tsx"
check_imports "src/pages/Statistics/Holders/index.tsx"
check_imports "src/pages/Admin/Factory/index.tsx"
check_imports "src/pages/Admin/Lending/index.tsx"
check_imports "src/pages/Lending/User/index.tsx"
echo ""
echo "================================"
echo "✅ 检查完成"

191
antdesign/config/config.ts Normal file
View File

@@ -0,0 +1,191 @@
// https://umijs.org/config/
import { join } from 'node:path';
import { defineConfig } from '@umijs/max';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from './routes';
const { REACT_APP_ENV = 'dev' } = process.env;
/**
* @name 使用公共路径
* @description 部署时的路径,如果部署在非根目录下,需要配置这个变量
* @doc https://umijs.org/docs/api/config#publicpath
*/
const PUBLIC_PATH: string = '/';
export default defineConfig({
/**
* @name 开启 hash 模式
* @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。
* @doc https://umijs.org/docs/api/config#hash
*/
hash: true,
publicPath: PUBLIC_PATH,
/**
* @name 兼容性设置
* @description 设置 ie11 不一定完美兼容,需要检查自己使用的所有依赖
* @doc https://umijs.org/docs/api/config#targets
*/
targets: {
chrome: 91, // Chrome 91 supports BigInt literals (ES2020)
},
/**
* @name 路由的配置,不在路由中引入的文件不会编译
* @description 只支持 pathcomponentroutesredirectwrapperstitle 的配置
* @doc https://umijs.org/docs/guides/routes
*/
// umi routes: https://umijs.org/docs/routing
routes,
/**
* @name 主题的配置
* @description 虽然叫主题,但是其实只是 less 的变量设置
* @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn
* @doc umi 的 theme 配置 https://umijs.org/docs/api/config#theme
*/
// theme: { '@primary-color': '#1DA57A' }
/**
* @name moment 的国际化配置
* @description 如果对国际化没有要求打开之后能减少js的包大小
* @doc https://umijs.org/docs/api/config#ignoremomentlocale
*/
ignoreMomentLocale: true,
/**
* @name 代理配置
* @description 可以让你的本地服务器代理到你的服务器上,这样你就可以访问服务器的数据了
* @see 要注意以下 代理只能在本地开发时使用build 之后就无法使用了。
* @doc 代理介绍 https://umijs.org/docs/guides/proxy
* @doc 代理配置 https://umijs.org/docs/api/config#proxy
*/
proxy: proxy[REACT_APP_ENV as keyof typeof proxy],
/**
* @name 快速热更新配置
* @description 一个不错的热更新组件,更新时可以保留 state
*/
fastRefresh: true,
//============== 以下都是max的插件配置 ===============
/**
* @name 数据流插件
* @@doc https://umijs.org/docs/max/data-flow
*/
model: {},
/**
* 一个全局的初始数据流,可以用它在插件之间共享数据
* @description 可以用来存放一些全局的数据,比如用户信息,或者一些全局的状态,全局初始状态在整个 Umi 项目的最开始创建。
* @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81
*/
initialState: {},
/**
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
title: 'Ant Design Pro',
layout: {
locale: true,
...defaultSettings,
},
/**
* @name moment2dayjs 插件
* @description 将项目中的 moment 替换为 dayjs
* @doc https://umijs.org/docs/max/moment2dayjs
*/
moment2dayjs: {
preset: 'antd',
plugins: ['duration'],
},
/**
* @name 国际化插件
* @doc https://umijs.org/docs/max/i18n
*/
locale: {
// default zh-CN
default: 'zh-CN',
antd: true,
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: true,
},
/**
* @name antd 插件
* @description 内置了 babel import 插件
* @doc https://umijs.org/docs/max/antd#antd
*/
antd: {
appConfig: {},
configProvider: {
theme: {
cssVar: true,
token: {
fontFamily: 'AlibabaSans, sans-serif',
},
},
},
},
/**
* @name 网络请求配置
* @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
* @doc https://umijs.org/docs/max/request
*/
request: {},
/**
* @name 权限插件
* @description 基于 initialState 的权限插件,必须先打开 initialState
* @doc https://umijs.org/docs/max/access
*/
access: {},
/**
* @name <head> 中额外的 script
* @description 配置 <head> 中额外的 script
*/
headScripts: [
// 解决首次加载时白屏的问题
{ src: join(PUBLIC_PATH, 'scripts/loading.js'), async: true },
],
//================ pro 插件配置 =================
presets: ['umi-presets-pro'],
/**
* @name openAPI 插件的配置
* @description 基于 openapi 的规范生成serve 和mock能减少很多样板代码
* @doc https://pro.ant.design/zh-cn/docs/openapi/
* @note 暂时禁用,需要安装 swagger-ui-dist: npm install swagger-ui-dist
*/
// openAPI: [
// {
// requestLibPath: "import { request } from '@umijs/max'",
// // 或者使用在线的版本
// // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
// schemaPath: join(__dirname, 'oneapi.json'),
// mock: false,
// },
// {
// requestLibPath: "import { request } from '@umijs/max'",
// schemaPath:
// 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
// projectName: 'swagger',
// },
// ],
mock: false,
/**
* @name 是否开启 mako
* @description 使用 mako 极速研发
* @doc https://umijs.org/docs/api/config#mako
*/
// mako: {}, // 禁用 mako因为 @base-org/account 使用了 mako 不支持的 import attributes 语法
esbuildMinifyIIFE: true,
jsMinifier: 'terser',
chainWebpack(config) {
// @metamask/sdk is listed in package.json (pulled in by wagmi/viem deps)
// but never actually imported in source — alias to empty module to avoid build errors
config.resolve.alias.set('@metamask/sdk', require.resolve('./empty-module.js'));
},
// requestRecord: {}, // 禁用:@umijs/request-record 包缺少 src/runtime/mock.ts
exportStatic: {},
define: {
'process.env.CI': process.env.CI,
'process.env.Auth_URL': process.env.Auth_URL || 'http://localhost:3010',
'process.env.GOOGLE_CLIENT_ID': process.env.GOOGLE_CLIENT_ID || '',
},
});

View File

@@ -0,0 +1,29 @@
import type { ProLayoutProps } from '@ant-design/pro-components';
/**
* @name
*/
const Settings: ProLayoutProps & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
// 拂晓蓝
colorPrimary: '#1890ff',
layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: true,
colorWeak: false,
title: 'Sofio Admin',
pwa: false,
logo: undefined,
iconfontUrl: '',
siderMenuType: 'group',
token: {
// 参见ts声明demo 见文档通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F
},
};
export default Settings;

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,593 @@
{
"openapi": "3.0.1",
"info": {
"title": "Ant Design Pro",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8000/"
},
{
"url": "https://localhost:8000/"
}
],
"paths": {
"/api/currentUser": {
"get": {
"tags": ["api"],
"description": "获取当前的用户",
"operationId": "currentUser",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentUser"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/captcha": {
"post": {
"description": "发送验证码",
"operationId": "getFakeCaptcha",
"tags": ["login"],
"parameters": [
{
"name": "phone",
"in": "query",
"description": "手机号",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FakeCaptcha"
}
}
}
}
}
}
},
"/api/login/outLogin": {
"post": {
"description": "登录接口",
"operationId": "outLogin",
"tags": ["login"],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/account": {
"post": {
"tags": ["login"],
"description": "登录接口",
"operationId": "login",
"requestBody": {
"description": "登录系统",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginParams"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginResult"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"x-codegen-request-body-name": "body"
},
"x-swagger-router-controller": "api"
},
"/api/notices": {
"summary": "getNotices",
"description": "NoticeIconItem",
"get": {
"tags": ["api"],
"operationId": "getNotices",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NoticeIconList"
}
}
}
}
}
}
},
"/api/rule": {
"get": {
"tags": ["rule"],
"description": "获取规则列表",
"operationId": "rule",
"parameters": [
{
"name": "current",
"in": "query",
"description": "当前的页码",
"schema": {
"type": "number"
}
},
{
"name": "pageSize",
"in": "query",
"description": "页面的容量",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleList"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"post": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "addRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"put": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "updateRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"delete": {
"tags": ["rule"],
"description": "删除规则",
"operationId": "removeRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/swagger": {
"x-swagger-pipe": "swagger_raw"
}
},
"components": {
"schemas": {
"CurrentUser": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"avatar": {
"type": "string"
},
"userid": {
"type": "string"
},
"email": {
"type": "string"
},
"signature": {
"type": "string"
},
"title": {
"type": "string"
},
"group": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
}
}
}
},
"notifyCount": {
"type": "integer",
"format": "int32"
},
"unreadCount": {
"type": "integer",
"format": "int32"
},
"country": {
"type": "string"
},
"access": {
"type": "string"
},
"geographic": {
"type": "object",
"properties": {
"province": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"city": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
}
}
},
"address": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"LoginResult": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"type": {
"type": "string"
},
"currentAuthority": {
"type": "string"
}
}
},
"PageParams": {
"type": "object",
"properties": {
"current": {
"type": "number"
},
"pageSize": {
"type": "number"
}
}
},
"RuleListItem": {
"type": "object",
"properties": {
"key": {
"type": "integer",
"format": "int32"
},
"disabled": {
"type": "boolean"
},
"href": {
"type": "string"
},
"avatar": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"desc": {
"type": "string"
},
"callNo": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "integer",
"format": "int32"
},
"updatedAt": {
"type": "string",
"format": "datetime"
},
"createdAt": {
"type": "string",
"format": "datetime"
},
"progress": {
"type": "integer",
"format": "int32"
}
}
},
"RuleList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RuleListItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"FakeCaptcha": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "string"
}
}
},
"LoginParams": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"autoLogin": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"ErrorResponse": {
"required": ["errorCode"],
"type": "object",
"properties": {
"errorCode": {
"type": "string",
"description": "业务约定的错误码"
},
"errorMessage": {
"type": "string",
"description": "业务上的错误信息"
},
"success": {
"type": "boolean",
"description": "业务上的请求是否成功"
}
}
},
"NoticeIconList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NoticeIconItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"NoticeIconItemType": {
"title": "NoticeIconItemType",
"description": "已读未读列表的枚举",
"type": "string",
"properties": {},
"enum": ["notification", "message", "event"]
},
"NoticeIconItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"extra": {
"type": "string",
"format": "any"
},
"key": { "type": "string" },
"read": {
"type": "boolean"
},
"avatar": {
"type": "string"
},
"title": {
"type": "string"
},
"status": {
"type": "string"
},
"datetime": {
"type": "string",
"format": "date"
},
"description": {
"type": "string"
},
"type": {
"extensions": {
"x-is-enum": true
},
"$ref": "#/components/schemas/NoticeIconItemType"
}
}
}
}
}
}

42
antdesign/config/proxy.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* @name 代理的配置
* @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
* -------------------------------
* The agent cannot take effect in the production environment
* so there is no configuration of the production environment
* For details, please see
* https://pro.ant.design/docs/deploy
*
* @doc https://umijs.org/docs/guides/proxy
*/
export default {
// 开发环境代理配置
dev: {
// localhost:10502/api/** -> http://localhost:8080/api/**
'/api/': {
// 代理到本地 Golang 后端服务器
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '/api' },
},
},
/**
* @name 详细的代理配置
* @doc https://github.com/chimurai/http-proxy-middleware
*/
test: {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': {
target: 'https://proapi.azurewebsites.net',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
pre: {
'/api/': {
target: 'your pre url',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
};

247
antdesign/config/routes.ts Normal file
View File

@@ -0,0 +1,247 @@
/**
* @name umi 的路由配置
* @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
* @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
* @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。
* @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。
* @param redirect 配置路由跳转
* @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验
* @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login则读取 menu.ts 中 menu.login 的取值作为标题
* @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn
* @doc https://umijs.org/docs/guides/routes
*/
export default [
{
path: '/user',
layout: false,
routes: [
{
path: '/user/login',
layout: false,
name: 'login',
component: './user/login',
},
{
path: '/user/register',
layout: false,
name: 'register',
component: './user/register',
},
{
path: '/user/register-result',
layout: false,
name: 'register-result',
component: './user/register-result',
},
{
path: '/user/forgot-password',
layout: false,
name: 'forgot-password',
component: './user/forgot-password',
},
{
path: '/user',
redirect: '/user/login',
},
{
component: '404',
path: '/user/*',
},
],
},
{
name: 'vault',
icon: 'wallet',
path: '/vault',
routes: [
{
path: '/vault',
redirect: '/vault/trade',
},
{
name: 'trade',
icon: 'transaction',
path: '/vault/trade',
component: './Vault/Trade/index.new',
},
{
name: 'usdc',
icon: 'dollar',
path: '/vault/usdc',
component: './Vault/USDC/index.new',
},
],
},
{
name: 'lending',
icon: 'bank',
path: '/lending',
routes: [
{
path: '/lending',
redirect: '/lending/user',
},
{
name: 'user',
icon: 'setting',
path: '/lending/user',
component: './Lending/User/index.complete',
},
],
},
{
name: 'lp',
icon: 'stock',
path: '/lp',
routes: [
{
path: '/lp',
redirect: '/lp/pool',
},
{
name: 'pool',
icon: 'database',
path: '/lp/pool',
component: './LP/index.full',
},
],
},
{
name: 'statistics',
icon: 'bar-chart',
path: '/statistics',
routes: [
{
path: '/statistics',
redirect: '/statistics/holders',
},
{
name: 'holders',
icon: 'team',
path: '/statistics/holders',
component: './Statistics/Holders/index',
},
],
},
{
name: 'admin',
icon: 'setting',
path: '/admin',
access: 'canAdmin',
routes: [
{
path: '/admin',
redirect: '/admin/factory',
},
{
name: 'factory',
icon: 'build',
path: '/admin/factory',
component: './Admin/Factory/index.new',
access: 'canAdmin',
},
{
name: 'assets',
icon: 'fund',
path: '/admin/assets',
component: './Admin/Assets/index',
access: 'canAdmin',
},
{
name: 'asset-custody',
icon: 'lock',
path: '/admin/asset-custody',
component: './Admin/AssetCustody/index',
access: 'canAdmin',
},
{
name: 'asset-audit-reports',
icon: 'audit',
path: '/admin/asset-audit-reports',
component: './Admin/AssetAuditReports/index',
access: 'canAdmin',
},
{
name: 'product-links',
icon: 'link',
path: '/admin/product-links',
component: './Admin/ProductLinks/index',
access: 'canAdmin',
},
{
name: 'points-rules',
icon: 'trophy',
path: '/admin/points-rules',
component: './Admin/PointsRules/index',
access: 'canAdmin',
},
{
name: 'seasons',
icon: 'calendar',
path: '/admin/seasons',
component: './Admin/Seasons/index',
access: 'canAdmin',
},
{
name: 'vip-tiers',
icon: 'crown',
path: '/admin/vip-tiers',
component: './Admin/VIPTiers/index',
access: 'canAdmin',
},
{
name: 'users',
icon: 'team',
path: '/admin/users',
component: './Admin/Users/index',
access: 'canAdmin',
},
{
name: 'invite-codes',
icon: 'gift',
path: '/admin/invite-codes',
component: './Admin/InviteCodes/index',
access: 'canAdmin',
},
{
name: 'system-contracts',
icon: 'code',
path: '/admin/system-contracts',
component: './Admin/SystemContracts/index',
access: 'canAdmin',
},
{
name: 'liquidation',
icon: 'thunderbolt',
path: '/admin/liquidation',
component: './Admin/Liquidation/index',
access: 'canAdmin',
},
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
path: '/account',
redirect: '/account/settings',
},
{
name: 'settings',
icon: 'setting',
path: '/account/settings',
component: './account/settings',
},
],
},
{
path: '/',
redirect: '/vault/trade',
},
{
component: '404',
path: '/*',
},
];

47
antdesign/fix-routes.sh Normal file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
echo "🔧 修复 Ant Design Pro 路由问题"
echo "================================"
# 1. 清理缓存
echo "1⃣ 清理缓存..."
rm -rf src/.umi
rm -rf node_modules/.cache
rm -rf .umi
rm -rf .umi-production
# 2. 检查页面文件
echo "2⃣ 检查页面文件..."
echo "✓ Vault/Trade: $(test -f src/pages/Vault/Trade/index.tsx && echo '存在' || echo '❌ 不存在')"
echo "✓ Vault/USDC: $(test -f src/pages/Vault/USDC/index.tsx && echo '存在' || echo '❌ 不存在')"
echo "✓ LP: $(test -f src/pages/LP/index.ts && echo '存在' || echo '❌ 不存在')"
echo "✓ Statistics/Holders: $(test -f src/pages/Statistics/Holders/index.tsx && echo '存在' || echo '❌ 不存在')"
echo "✓ Admin/Factory: $(test -f src/pages/Admin/Factory/index.tsx && echo '存在' || echo '❌ 不存在')"
echo "✓ Admin/Lending: $(test -f src/pages/Admin/Lending/index.tsx && echo '存在' || echo '❌ 不存在')"
echo "✓ Lending/User: $(test -f src/pages/Lending/User/index.tsx && echo '存在' || echo '❌ 不存在')"
# 3. 验证 export default
echo ""
echo "3⃣ 验证 default export..."
for file in src/pages/Vault/Trade/index.tsx src/pages/Vault/USDC/index.tsx src/pages/Admin/Factory/index.tsx src/pages/Admin/Lending/index.tsx src/pages/Lending/User/index.tsx src/pages/Statistics/Holders/index.tsx; do
if [ -f "$file" ]; then
if grep -q "export default" "$file"; then
echo "$file"
else
echo "$file - 缺少 export default"
fi
fi
done
echo ""
echo "4⃣ 重启开发服务器..."
echo "================================"
echo ""
echo "请手动执行:"
echo " pnpm dev"
echo ""
echo "或者:"
echo " npm run dev"
echo ""
echo "然后在浏览器中刷新页面Ctrl+Shift+R 或 Cmd+Shift+R"

29
antdesign/nginx.conf Normal file
View File

@@ -0,0 +1,29 @@
server {
listen 10502;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# API proxy to backend
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Static assets cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

102
antdesign/package.json Normal file
View File

@@ -0,0 +1,102 @@
{
"name": "ant-design-pro",
"version": "6.0.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"repository": "git@github.com:ant-design/ant-design-pro.git",
"scripts": {
"analyze": "cross-env ANALYZE=1 max build",
"biome:lint": "npx @biomejs/biome lint",
"build": "max build",
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run biome:lint && npm run tsc",
"lint-staged": "lint-staged",
"openapi": "max openapi",
"prepare": "husky",
"preview": "npm run build && max preview --port 8080",
"record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",
"tsc": "tsc --noEmit"
},
"browserslist": [
"defaults"
],
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@ant-design/plots": "^2.6.0",
"@ant-design/pro-components": "^2.7.19",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@antv/l7": "^2.22.7",
"@antv/l7-react": "^2.4.3",
"@tanstack/react-query": "^5.90.12",
"@walletconnect/ethereum-provider": "^2.21.10",
"@web3modal/wagmi": "^5.1.11",
"antd": "^5.25.4",
"antd-style": "^3.7.0",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"event-source-polyfill": "^1.0.31",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"numeral": "^2.0.6",
"rc-util": "^5.44.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^16.5.0",
"sonner": "^2.0.7",
"viem": "^2.42.1",
"wagmi": "^3.1.0"
},
"devDependencies": {
"@ant-design/pro-cli": "^3.3.0",
"@biomejs/biome": "^2.0.6",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.1",
"@types/event-source-polyfill": "^1.0.5",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.10",
"@types/node": "^24.10.1",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@types/react-helmet": "^6.1.11",
"@umijs/max": "^4.3.24",
"cross-env": "^7.0.3",
"dotenv": "^17.2.3",
"express": "^4.21.1",
"gh-pages": "^6.1.1",
"husky": "^9.1.7",
"jest": "^30.0.4",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^16.1.2",
"mockjs": "^1.1.0",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"typescript": "^5.6.3",
"umi-presets-pro": "^2.0.3",
"umi-serve": "^1.9.11"
},
"engines": {
"node": ">=20.0.0"
},
"optionalDependencies": {
"@coinbase/wallet-sdk": "^4.3.7",
"@metamask/sdk": "^0.33.1",
"porto": "^0.2.37"
}
}

27646
antdesign/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
antdesign/public/CNAME Normal file
View File

@@ -0,0 +1 @@
preview.pro.ant.design

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,5 @@
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1,202 @@
/**
* loading 占位
* 解决首次加载时白屏的问题
*/
(function () {
const _root = document.querySelector('#root');
if (_root && _root.innerHTML === '') {
_root.innerHTML = `
<style>
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
}
#root {
background-repeat: no-repeat;
background-size: 100% auto;
}
.loading-title {
font-size: 1.1rem;
}
.loading-sub-title {
margin-top: 20px;
font-size: 1rem;
color: #888;
}
.page-loading-warp {
display: flex;
align-items: center;
justify-content: center;
padding: 26px;
}
.ant-spin {
position: absolute;
display: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
color: #1890ff;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
text-align: center;
list-style: none;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-webkit-font-feature-settings: "tnum";
font-feature-settings: "tnum";
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
width: 20px;
height: 20px;
font-size: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antspinmove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antrotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
width: 32px;
height: 32px;
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 362px;
">
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
</span>
</div>
</div>
<div class="loading-title">
正在加载资源
</div>
<div class="loading-sub-title">
初次加载资源可能需要较多时间 请耐心等待
</div>
</div>
`;
}
})();

View File

@@ -0,0 +1,4 @@
<svg width="329" height="382" viewBox="0 0 329 382" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.045 7.26266C253.028 11.252 240.702 17.3894 233.487 20.9696C226.372 24.4475 210.539 32.3238 198.414 38.359C186.289 44.3941 171.257 51.7591 165.044 54.8278C158.831 57.7942 152.318 61.2721 150.614 62.4996C146.505 65.5683 143.4 72.7287 144.201 77.7405C144.501 79.7868 148.811 89.9133 153.72 100.347C158.631 110.679 166.347 127.454 170.857 137.581C175.366 147.708 180.176 157.22 181.579 158.755C184.886 162.233 189.996 164.074 194.807 163.358C198.514 162.847 316.66 103.212 322.071 99.1197C329.587 93.4933 330.389 85.9242 324.777 74.1605C316.76 57.3851 301.128 23.7314 296.919 14.3207C292.209 3.78476 287.7 0 279.883 0C276.577 0 272.067 1.73895 261.045 7.26266ZM278.781 43.1667C281.988 46.8491 291.307 67.921 290.906 70.6829C290.706 72.7287 288.301 74.3653 277.879 79.7868C262.147 87.9698 262.047 87.9698 255.433 74.4674C249.42 62.295 248.118 58.3057 249.12 55.6461C249.821 53.498 271.867 41.1208 275.174 41.0185C275.976 40.9163 277.679 41.9392 278.781 43.1667Z" fill="#0137FD"/>
<path d="M8.91855 37.7453C6.61379 38.7683 3.70772 41.2232 2.40501 43.0644L0 46.5423V141.161C0 244.986 0 244.27 6.11274 266.16C10.2213 281.094 17.4363 296.847 26.2547 310.35C34.1712 322.624 52.1086 341.344 64.1336 350.039C82.1714 363.03 107.424 374.077 129.971 378.68C146.305 382.056 175.365 382.056 191.599 378.68C220.76 372.645 248.117 359.143 269.462 340.014C295.015 317.306 311.549 288.255 319.266 252.556C321.57 242.224 321.67 240.076 321.971 184.942L322.271 127.864L313.954 132.467C309.345 135.024 293.912 143.105 279.583 150.47C259.541 160.801 237.795 172.257 234.288 174.303C234.188 174.405 233.887 186.476 233.687 201.001C233.386 221.971 232.886 229.131 231.482 234.757C225.971 256.852 212.343 274.037 192.601 283.345C182.58 288.152 174.664 289.892 162.338 289.79C130.873 289.79 105.319 271.173 94.597 240.383L92.1924 233.734L91.6911 139.627C91.0898 33.5514 91.7911 41.7347 83.374 37.7453C79.4654 36.0064 76.4595 35.8018 46.096 35.8018C16.0334 35.8018 12.6263 36.0064 8.91855 37.7453Z" fill="#0137FD"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

52
antdesign/restart-dev.sh Normal file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
echo "🚀 完整重启 Ant Design Pro 开发服务器"
echo "================================================"
# 1. 停止所有 node 进程
echo ""
echo "1⃣ 停止现有开发服务器..."
pkill -f "umi dev" || echo "没有运行中的服务器"
pkill -f "max dev" || true
sleep 2
# 2. 清理所有缓存
echo ""
echo "2⃣ 清理缓存..."
rm -rf src/.umi
rm -rf node_modules/.cache
rm -rf .umi
rm -rf .umi-production
rm -rf dist
echo "✓ 缓存已清理"
# 3. 验证配置
echo ""
echo "3⃣ 验证配置..."
echo "路由文件: $(test -f config/routes.ts && echo '✓ 存在' || echo '✗ 不存在')"
echo "菜单翻译: $(grep -c 'menu.vault\|menu.lending' src/locales/zh-CN/menu.ts) 个配置"
echo "测试页面: $(find src/pages -name 'test.tsx' | wc -l)"
# 4. 检查依赖
echo ""
echo "4⃣ 检查依赖..."
if [ ! -d "node_modules" ]; then
echo "⚠️ node_modules 不存在,运行 pnpm install..."
pnpm install
fi
# 5. 启动开发服务器
echo ""
echo "5⃣ 启动开发服务器..."
echo "================================================"
echo ""
echo "服务器将在 http://localhost:8000 启动"
echo "请在浏览器中访问并强制刷新 (Ctrl+Shift+R)"
echo ""
echo "如果看到菜单,说明配置成功!"
echo "如果还是没有菜单,请检查浏览器控制台的错误信息。"
echo ""
echo "================================================"
pnpm dev

23
antdesign/src/access.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* @see https://umijs.org/docs/max/access#access
* 动态权限控制(精简版,仅保留登录态相关)
*/
// 扩展 CurrentUser 类型以包含 roles
interface CurrentUserWithRoles extends API.CurrentUser {
roles?: string[];
}
export default function access(
initialState: { currentUser?: CurrentUserWithRoles } | undefined,
) {
const { currentUser } = initialState ?? {};
const userRoles = currentUser?.roles || [];
const isAdmin = userRoles.includes('admin') || currentUser?.access === 'admin';
return {
// 保持向后兼容的管理员判断
canAdmin: isAdmin,
userRoles,
};
}

235
antdesign/src/app.tsx Normal file
View File

@@ -0,0 +1,235 @@
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components';
import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
import { history } from '@umijs/max';
import React from 'react';
import {
AvatarDropdown,
AvatarName,
Footer,
SelectLang,
} from '@/components';
import { ConnectButton } from '@/components/Web3/ConnectButton';
import { NetworkSwitch } from '@/components/Web3/NetworkSwitch';
import { getCurrentUser } from '@/services/auth/api';
import {
clearTokens,
getUserInfo,
isLoggedIn,
} from '@/services/auth/tokenManager';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
import '@ant-design/v5-patch-for-react-19';
import { WagmiProvider } from 'wagmi';
import { QueryClientProvider } from '@tanstack/react-query';
import { config, queryClient } from '@/config/wagmi';
import { TransactionProvider } from '@/context/TransactionContext';
import { ToastProvider } from '@/components/Toast';
import { useContractRegistry } from '@/hooks/useContractRegistry';
const isDev = process.env.NODE_ENV === 'development' || process.env.CI;
const loginPath = '/user/login';
// API 基础地址
const AUTH_BASE_URL = process.env.Auth_URL || '';
/**
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
*/
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
const fetchUserInfo = async () => {
// 检查是否已登录refresh token 过期则彻底清除)
if (!isLoggedIn()) {
clearTokens();
return undefined;
}
try {
// 优先从 API 获取最新用户信息
const response = await getCurrentUser();
if (response.success && response.data) {
// API 返回的 data 直接是用户对象
const user = response.data;
return {
name: user.username,
avatar: user.profile?.avatar || '',
userid: user.userId || user.userID, // 使用数字 ID
userId: user.userId || user.userID, // 兼容 userID 和 userId
username: user.username,
email: user.email,
access: user.roles?.includes('admin') ? 'admin' : 'user',
roles: user.roles || [],
} as API.CurrentUser;
}
} catch (error) {
// API 请求异常,继续尝试使用缓存
}
// API 失败或返回空数据时,尝试使用本地缓存的用户信息
const cachedUser = getUserInfo();
if (cachedUser) {
return {
name: cachedUser.username,
avatar: cachedUser.profile?.avatar || '',
userid: cachedUser.userId || cachedUser.userID, // 兼容 userID 和 userId
userId: cachedUser.userId || cachedUser.userID, // 兼容 userID 和 userId
username: cachedUser.username,
email: cachedUser.email,
access: cachedUser.roles?.includes('admin') ? 'admin' : 'user',
roles: cachedUser.roles || [],
} as API.CurrentUser;
}
// 缓存也没有,清除 Token
clearTokens();
return undefined;
};
// 如果不是登录/注册页面,执行获取用户信息
const { location } = history;
if (
![
loginPath,
'/user/register',
'/user/register-result',
'/user/forgot-password',
].includes(location.pathname)
) {
const currentUser = await fetchUserInfo();
if (!currentUser) {
history.push(loginPath);
}
return {
fetchUserInfo,
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({
initialState,
setInitialState,
}) => {
return {
actionsRender: () => [
<NetworkSwitch key="NetworkSwitch" />,
<ConnectButton key="ConnectButton" />,
<SelectLang key="SelectLang" />,
],
avatarProps: {
src: initialState?.currentUser?.avatar,
title: <AvatarName />,
render: (_, avatarChildren) => {
return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
},
},
waterMarkProps: {
content: initialState?.currentUser?.name,
},
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (
!initialState?.currentUser &&
![
loginPath,
'/user/register',
'/user/register-result',
'/user/forgot-password',
].includes(location.pathname)
) {
history.push(loginPath);
}
},
bgLayoutImgList: [
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
left: 85,
bottom: 100,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
bottom: -68,
right: -45,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
bottom: 0,
left: 0,
width: '331px',
},
],
links: [],
menuHeaderRender: undefined,
childrenRender: (children) => {
return (
<>
{children}
{isDev && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
</>
);
},
...initialState?.settings,
};
};
/**
* @name request 配置,可以配置错误处理
* @doc https://umijs.org/docs/max/request#配置
*/
export const request: RequestConfig = {
baseURL: AUTH_BASE_URL,
...errorConfig,
};
/**
* @name rootContainer 配置,用于包裹整个应用
* @doc https://umijs.org/docs/api/runtime-config#rootcontainer
*/
function ContractRegistryInit({ children }: { children: React.ReactNode }) {
useContractRegistry();
return <>{children}</>;
}
export function rootContainer(container: React.ReactNode) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<ContractRegistryInit>
<ToastProvider>
<TransactionProvider>
{container}
</TransactionProvider>
</ToastProvider>
</ContractRegistryInit>
</QueryClientProvider>
</WagmiProvider>
);
}

View File

@@ -0,0 +1,113 @@
import { Select } from 'antd';
import type { SelectProps } from 'antd';
import { useIntl } from '@umijs/max';
import React, { useMemo } from 'react';
import { COUNTRIES, getSortedCountries, getPopularCountries } from './countries';
import type { CountryData } from './countries';
export interface CountrySelectProps extends Omit<SelectProps, 'options'> {
/** 是否只显示常用国家 */
popularOnly?: boolean;
/** 自定义国家列表 */
countries?: CountryData[];
/** 是否显示国旗 */
showFlag?: boolean;
/** 是否显示国家代码 */
showCode?: boolean;
/** 语言: 'zh' 显示中文, 'en' 显示英文, 默认跟随系统 */
language?: 'zh' | 'en';
}
const CountrySelect: React.FC<CountrySelectProps> = ({
popularOnly = false,
countries,
showFlag = true,
showCode = true,
language,
...restProps
}) => {
const intl = useIntl();
// 判断当前语言
const isZh = language === 'zh' || (!language && intl.locale?.startsWith('zh'));
// 获取国家列表
const countryList = useMemo(() => {
if (countries) return countries;
if (popularOnly) return getPopularCountries();
return getSortedCountries();
}, [countries, popularOnly]);
// 生成选项
const options = useMemo(() => {
const popularCountries = getPopularCountries();
const popularCodes = new Set(popularCountries.map((c) => c.code));
// 如果不是只显示常用国家,需要分组
if (!popularOnly && !countries) {
return [
{
label: isZh ? '常用国家/地区' : 'Popular Countries',
options: popularCountries.map((country) => ({
label: (
<span>
{showFlag && <span style={{ marginRight: 8 }}>{country.flag}</span>}
{isZh ? country.nameZh : country.nameEn}
{showCode && (
<span style={{ color: '#999', marginLeft: 8 }}>({country.code})</span>
)}
</span>
),
value: country.code,
searchText: `${country.nameZh} ${country.nameEn} ${country.code}`,
})),
},
{
label: isZh ? '全部国家/地区' : 'All Countries',
options: COUNTRIES.filter((c) => !popularCodes.has(c.code)).map((country) => ({
label: (
<span>
{showFlag && <span style={{ marginRight: 8 }}>{country.flag}</span>}
{isZh ? country.nameZh : country.nameEn}
{showCode && (
<span style={{ color: '#999', marginLeft: 8 }}>({country.code})</span>
)}
</span>
),
value: country.code,
searchText: `${country.nameZh} ${country.nameEn} ${country.code}`,
})),
},
];
}
// 不分组的情况
return countryList.map((country) => ({
label: (
<span>
{showFlag && <span style={{ marginRight: 8 }}>{country.flag}</span>}
{isZh ? country.nameZh : country.nameEn}
{showCode && <span style={{ color: '#999', marginLeft: 8 }}>({country.code})</span>}
</span>
),
value: country.code,
searchText: `${country.nameZh} ${country.nameEn} ${country.code}`,
}));
}, [countryList, isZh, showFlag, showCode, popularOnly, countries]);
return (
<Select
showSearch
optionFilterProp="searchText"
filterOption={(input, option) => {
const searchText = (option as any)?.searchText || '';
return searchText.toLowerCase().includes(input.toLowerCase());
}}
placeholder={isZh ? '请选择国家/地区' : 'Select country/region'}
options={options}
{...restProps}
/>
);
};
export default CountrySelect;

View File

@@ -0,0 +1,121 @@
import { Select } from 'antd';
import type { SelectProps } from 'antd';
import { useIntl } from '@umijs/max';
import React, { useMemo } from 'react';
import { getSortedCountries, getPopularCountries } from './countries';
import type { CountryData } from './countries';
export interface PhonePrefixSelectProps extends Omit<SelectProps, 'options'> {
/** 是否只显示常用国家的区号 */
popularOnly?: boolean;
/** 是否显示国旗 */
showFlag?: boolean;
/** 是否显示国家名称 */
showCountryName?: boolean;
/** 语言: 'zh' 显示中文, 'en' 显示英文, 默认跟随系统 */
language?: 'zh' | 'en';
}
const PhonePrefixSelect: React.FC<PhonePrefixSelectProps> = ({
popularOnly = false,
showFlag = true,
showCountryName = true,
language,
...restProps
}) => {
const intl = useIntl();
// 判断当前语言
const isZh = language === 'zh' || (!language && intl.locale?.startsWith('zh'));
// 获取国家列表并去重区号
const options = useMemo(() => {
const countryList = popularOnly ? getPopularCountries() : getSortedCountries();
// 按区号分组,保留第一个国家作为显示
const phoneCodeMap = new Map<string, CountryData>();
countryList.forEach((country) => {
if (!phoneCodeMap.has(country.phoneCode)) {
phoneCodeMap.set(country.phoneCode, country);
}
});
const popularCountries = getPopularCountries();
const popularPhoneCodes = new Set(popularCountries.map((c) => c.phoneCode));
// 如果不是只显示常用国家,需要分组
if (!popularOnly) {
const popularOptions: any[] = [];
const otherOptions: any[] = [];
phoneCodeMap.forEach((country, phoneCode) => {
const option = {
label: (
<span>
{showFlag && <span style={{ marginRight: 6 }}>{country.flag}</span>}
<span style={{ fontWeight: 500 }}>+{phoneCode}</span>
{showCountryName && (
<span style={{ color: '#666', marginLeft: 8 }}>
{isZh ? country.nameZh : country.nameEn}
</span>
)}
</span>
),
value: phoneCode,
searchText: `+${phoneCode} ${country.nameZh} ${country.nameEn} ${country.code}`,
};
if (popularPhoneCodes.has(phoneCode)) {
popularOptions.push(option);
} else {
otherOptions.push(option);
}
});
return [
{
label: isZh ? '常用' : 'Popular',
options: popularOptions,
},
{
label: isZh ? '全部' : 'All',
options: otherOptions,
},
];
}
// 不分组的情况
return Array.from(phoneCodeMap.entries()).map(([phoneCode, country]) => ({
label: (
<span>
{showFlag && <span style={{ marginRight: 6 }}>{country.flag}</span>}
<span style={{ fontWeight: 500 }}>+{phoneCode}</span>
{showCountryName && (
<span style={{ color: '#666', marginLeft: 8 }}>
{isZh ? country.nameZh : country.nameEn}
</span>
)}
</span>
),
value: phoneCode,
searchText: `+${phoneCode} ${country.nameZh} ${country.nameEn} ${country.code}`,
}));
}, [popularOnly, isZh, showFlag, showCountryName]);
return (
<Select
showSearch
optionFilterProp="searchText"
filterOption={(input, option) => {
const searchText = (option as any)?.searchText || '';
return searchText.toLowerCase().includes(input.toLowerCase());
}}
placeholder={isZh ? '区号' : 'Code'}
options={options}
dropdownStyle={{ minWidth: 280 }}
{...restProps}
/>
);
};
export default PhonePrefixSelect;

View File

@@ -0,0 +1,292 @@
/**
* 国家/地区数据
* 包含 ISO 3166-1 标准的国家代码、名称、手机区号、国旗 emoji
*/
export interface CountryData {
/** ISO 3166-1 alpha-2 国家代码 */
code: string;
/** 英文名称 */
nameEn: string;
/** 中文名称 */
nameZh: string;
/** 手机区号 (不含 +) */
phoneCode: string;
/** 国旗 emoji */
flag: string;
/** 是否常用 (用于排序优先显示) */
popular?: boolean;
}
/**
* 完整的国家/地区列表
* 按字母顺序排列,常用国家标记 popular: true
*/
export const COUNTRIES: CountryData[] = [
// 常用国家/地区 (优先显示)
{ code: 'CN', nameEn: 'China', nameZh: '中国', phoneCode: '86', flag: '🇨🇳', popular: true },
{ code: 'HK', nameEn: 'Hong Kong', nameZh: '中国香港', phoneCode: '852', flag: '🇭🇰', popular: true },
{ code: 'TW', nameEn: 'Taiwan', nameZh: '中国台湾', phoneCode: '886', flag: '🇹🇼', popular: true },
{ code: 'MO', nameEn: 'Macao', nameZh: '中国澳门', phoneCode: '853', flag: '🇲🇴', popular: true },
{ code: 'US', nameEn: 'United States', nameZh: '美国', phoneCode: '1', flag: '🇺🇸', popular: true },
{ code: 'GB', nameEn: 'United Kingdom', nameZh: '英国', phoneCode: '44', flag: '🇬🇧', popular: true },
{ code: 'JP', nameEn: 'Japan', nameZh: '日本', phoneCode: '81', flag: '🇯🇵', popular: true },
{ code: 'KR', nameEn: 'South Korea', nameZh: '韩国', phoneCode: '82', flag: '🇰🇷', popular: true },
{ code: 'SG', nameEn: 'Singapore', nameZh: '新加坡', phoneCode: '65', flag: '🇸🇬', popular: true },
{ code: 'AU', nameEn: 'Australia', nameZh: '澳大利亚', phoneCode: '61', flag: '🇦🇺', popular: true },
{ code: 'CA', nameEn: 'Canada', nameZh: '加拿大', phoneCode: '1', flag: '🇨🇦', popular: true },
{ code: 'DE', nameEn: 'Germany', nameZh: '德国', phoneCode: '49', flag: '🇩🇪', popular: true },
{ code: 'FR', nameEn: 'France', nameZh: '法国', phoneCode: '33', flag: '🇫🇷', popular: true },
{ code: 'MY', nameEn: 'Malaysia', nameZh: '马来西亚', phoneCode: '60', flag: '🇲🇾', popular: true },
{ code: 'TH', nameEn: 'Thailand', nameZh: '泰国', phoneCode: '66', flag: '🇹🇭', popular: true },
{ code: 'VN', nameEn: 'Vietnam', nameZh: '越南', phoneCode: '84', flag: '🇻🇳', popular: true },
{ code: 'PH', nameEn: 'Philippines', nameZh: '菲律宾', phoneCode: '63', flag: '🇵🇭', popular: true },
{ code: 'ID', nameEn: 'Indonesia', nameZh: '印度尼西亚', phoneCode: '62', flag: '🇮🇩', popular: true },
{ code: 'IN', nameEn: 'India', nameZh: '印度', phoneCode: '91', flag: '🇮🇳', popular: true },
{ code: 'AE', nameEn: 'United Arab Emirates', nameZh: '阿联酋', phoneCode: '971', flag: '🇦🇪', popular: true },
// 亚洲
{ code: 'AF', nameEn: 'Afghanistan', nameZh: '阿富汗', phoneCode: '93', flag: '🇦🇫' },
{ code: 'AM', nameEn: 'Armenia', nameZh: '亚美尼亚', phoneCode: '374', flag: '🇦🇲' },
{ code: 'AZ', nameEn: 'Azerbaijan', nameZh: '阿塞拜疆', phoneCode: '994', flag: '🇦🇿' },
{ code: 'BH', nameEn: 'Bahrain', nameZh: '巴林', phoneCode: '973', flag: '🇧🇭' },
{ code: 'BD', nameEn: 'Bangladesh', nameZh: '孟加拉国', phoneCode: '880', flag: '🇧🇩' },
{ code: 'BT', nameEn: 'Bhutan', nameZh: '不丹', phoneCode: '975', flag: '🇧🇹' },
{ code: 'BN', nameEn: 'Brunei', nameZh: '文莱', phoneCode: '673', flag: '🇧🇳' },
{ code: 'KH', nameEn: 'Cambodia', nameZh: '柬埔寨', phoneCode: '855', flag: '🇰🇭' },
{ code: 'GE', nameEn: 'Georgia', nameZh: '格鲁吉亚', phoneCode: '995', flag: '🇬🇪' },
{ code: 'IQ', nameEn: 'Iraq', nameZh: '伊拉克', phoneCode: '964', flag: '🇮🇶' },
{ code: 'IR', nameEn: 'Iran', nameZh: '伊朗', phoneCode: '98', flag: '🇮🇷' },
{ code: 'IL', nameEn: 'Israel', nameZh: '以色列', phoneCode: '972', flag: '🇮🇱' },
{ code: 'JO', nameEn: 'Jordan', nameZh: '约旦', phoneCode: '962', flag: '🇯🇴' },
{ code: 'KZ', nameEn: 'Kazakhstan', nameZh: '哈萨克斯坦', phoneCode: '7', flag: '🇰🇿' },
{ code: 'KW', nameEn: 'Kuwait', nameZh: '科威特', phoneCode: '965', flag: '🇰🇼' },
{ code: 'KG', nameEn: 'Kyrgyzstan', nameZh: '吉尔吉斯斯坦', phoneCode: '996', flag: '🇰🇬' },
{ code: 'LA', nameEn: 'Laos', nameZh: '老挝', phoneCode: '856', flag: '🇱🇦' },
{ code: 'LB', nameEn: 'Lebanon', nameZh: '黎巴嫩', phoneCode: '961', flag: '🇱🇧' },
{ code: 'MV', nameEn: 'Maldives', nameZh: '马尔代夫', phoneCode: '960', flag: '🇲🇻' },
{ code: 'MN', nameEn: 'Mongolia', nameZh: '蒙古', phoneCode: '976', flag: '🇲🇳' },
{ code: 'MM', nameEn: 'Myanmar', nameZh: '缅甸', phoneCode: '95', flag: '🇲🇲' },
{ code: 'NP', nameEn: 'Nepal', nameZh: '尼泊尔', phoneCode: '977', flag: '🇳🇵' },
{ code: 'KP', nameEn: 'North Korea', nameZh: '朝鲜', phoneCode: '850', flag: '🇰🇵' },
{ code: 'OM', nameEn: 'Oman', nameZh: '阿曼', phoneCode: '968', flag: '🇴🇲' },
{ code: 'PK', nameEn: 'Pakistan', nameZh: '巴基斯坦', phoneCode: '92', flag: '🇵🇰' },
{ code: 'PS', nameEn: 'Palestine', nameZh: '巴勒斯坦', phoneCode: '970', flag: '🇵🇸' },
{ code: 'QA', nameEn: 'Qatar', nameZh: '卡塔尔', phoneCode: '974', flag: '🇶🇦' },
{ code: 'SA', nameEn: 'Saudi Arabia', nameZh: '沙特阿拉伯', phoneCode: '966', flag: '🇸🇦' },
{ code: 'LK', nameEn: 'Sri Lanka', nameZh: '斯里兰卡', phoneCode: '94', flag: '🇱🇰' },
{ code: 'SY', nameEn: 'Syria', nameZh: '叙利亚', phoneCode: '963', flag: '🇸🇾' },
{ code: 'TJ', nameEn: 'Tajikistan', nameZh: '塔吉克斯坦', phoneCode: '992', flag: '🇹🇯' },
{ code: 'TL', nameEn: 'Timor-Leste', nameZh: '东帝汶', phoneCode: '670', flag: '🇹🇱' },
{ code: 'TR', nameEn: 'Turkey', nameZh: '土耳其', phoneCode: '90', flag: '🇹🇷' },
{ code: 'TM', nameEn: 'Turkmenistan', nameZh: '土库曼斯坦', phoneCode: '993', flag: '🇹🇲' },
{ code: 'UZ', nameEn: 'Uzbekistan', nameZh: '乌兹别克斯坦', phoneCode: '998', flag: '🇺🇿' },
{ code: 'YE', nameEn: 'Yemen', nameZh: '也门', phoneCode: '967', flag: '🇾🇪' },
// 欧洲
{ code: 'AL', nameEn: 'Albania', nameZh: '阿尔巴尼亚', phoneCode: '355', flag: '🇦🇱' },
{ code: 'AD', nameEn: 'Andorra', nameZh: '安道尔', phoneCode: '376', flag: '🇦🇩' },
{ code: 'AT', nameEn: 'Austria', nameZh: '奥地利', phoneCode: '43', flag: '🇦🇹' },
{ code: 'BY', nameEn: 'Belarus', nameZh: '白俄罗斯', phoneCode: '375', flag: '🇧🇾' },
{ code: 'BE', nameEn: 'Belgium', nameZh: '比利时', phoneCode: '32', flag: '🇧🇪' },
{ code: 'BA', nameEn: 'Bosnia and Herzegovina', nameZh: '波黑', phoneCode: '387', flag: '🇧🇦' },
{ code: 'BG', nameEn: 'Bulgaria', nameZh: '保加利亚', phoneCode: '359', flag: '🇧🇬' },
{ code: 'HR', nameEn: 'Croatia', nameZh: '克罗地亚', phoneCode: '385', flag: '🇭🇷' },
{ code: 'CY', nameEn: 'Cyprus', nameZh: '塞浦路斯', phoneCode: '357', flag: '🇨🇾' },
{ code: 'CZ', nameEn: 'Czech Republic', nameZh: '捷克', phoneCode: '420', flag: '🇨🇿' },
{ code: 'DK', nameEn: 'Denmark', nameZh: '丹麦', phoneCode: '45', flag: '🇩🇰' },
{ code: 'EE', nameEn: 'Estonia', nameZh: '爱沙尼亚', phoneCode: '372', flag: '🇪🇪' },
{ code: 'FI', nameEn: 'Finland', nameZh: '芬兰', phoneCode: '358', flag: '🇫🇮' },
{ code: 'GR', nameEn: 'Greece', nameZh: '希腊', phoneCode: '30', flag: '🇬🇷' },
{ code: 'HU', nameEn: 'Hungary', nameZh: '匈牙利', phoneCode: '36', flag: '🇭🇺' },
{ code: 'IS', nameEn: 'Iceland', nameZh: '冰岛', phoneCode: '354', flag: '🇮🇸' },
{ code: 'IE', nameEn: 'Ireland', nameZh: '爱尔兰', phoneCode: '353', flag: '🇮🇪' },
{ code: 'IT', nameEn: 'Italy', nameZh: '意大利', phoneCode: '39', flag: '🇮🇹' },
{ code: 'XK', nameEn: 'Kosovo', nameZh: '科索沃', phoneCode: '383', flag: '🇽🇰' },
{ code: 'LV', nameEn: 'Latvia', nameZh: '拉脱维亚', phoneCode: '371', flag: '🇱🇻' },
{ code: 'LI', nameEn: 'Liechtenstein', nameZh: '列支敦士登', phoneCode: '423', flag: '🇱🇮' },
{ code: 'LT', nameEn: 'Lithuania', nameZh: '立陶宛', phoneCode: '370', flag: '🇱🇹' },
{ code: 'LU', nameEn: 'Luxembourg', nameZh: '卢森堡', phoneCode: '352', flag: '🇱🇺' },
{ code: 'MT', nameEn: 'Malta', nameZh: '马耳他', phoneCode: '356', flag: '🇲🇹' },
{ code: 'MD', nameEn: 'Moldova', nameZh: '摩尔多瓦', phoneCode: '373', flag: '🇲🇩' },
{ code: 'MC', nameEn: 'Monaco', nameZh: '摩纳哥', phoneCode: '377', flag: '🇲🇨' },
{ code: 'ME', nameEn: 'Montenegro', nameZh: '黑山', phoneCode: '382', flag: '🇲🇪' },
{ code: 'NL', nameEn: 'Netherlands', nameZh: '荷兰', phoneCode: '31', flag: '🇳🇱' },
{ code: 'MK', nameEn: 'North Macedonia', nameZh: '北马其顿', phoneCode: '389', flag: '🇲🇰' },
{ code: 'NO', nameEn: 'Norway', nameZh: '挪威', phoneCode: '47', flag: '🇳🇴' },
{ code: 'PL', nameEn: 'Poland', nameZh: '波兰', phoneCode: '48', flag: '🇵🇱' },
{ code: 'PT', nameEn: 'Portugal', nameZh: '葡萄牙', phoneCode: '351', flag: '🇵🇹' },
{ code: 'RO', nameEn: 'Romania', nameZh: '罗马尼亚', phoneCode: '40', flag: '🇷🇴' },
{ code: 'RU', nameEn: 'Russia', nameZh: '俄罗斯', phoneCode: '7', flag: '🇷🇺' },
{ code: 'SM', nameEn: 'San Marino', nameZh: '圣马力诺', phoneCode: '378', flag: '🇸🇲' },
{ code: 'RS', nameEn: 'Serbia', nameZh: '塞尔维亚', phoneCode: '381', flag: '🇷🇸' },
{ code: 'SK', nameEn: 'Slovakia', nameZh: '斯洛伐克', phoneCode: '421', flag: '🇸🇰' },
{ code: 'SI', nameEn: 'Slovenia', nameZh: '斯洛文尼亚', phoneCode: '386', flag: '🇸🇮' },
{ code: 'ES', nameEn: 'Spain', nameZh: '西班牙', phoneCode: '34', flag: '🇪🇸' },
{ code: 'SE', nameEn: 'Sweden', nameZh: '瑞典', phoneCode: '46', flag: '🇸🇪' },
{ code: 'CH', nameEn: 'Switzerland', nameZh: '瑞士', phoneCode: '41', flag: '🇨🇭' },
{ code: 'UA', nameEn: 'Ukraine', nameZh: '乌克兰', phoneCode: '380', flag: '🇺🇦' },
{ code: 'VA', nameEn: 'Vatican City', nameZh: '梵蒂冈', phoneCode: '39', flag: '🇻🇦' },
// 非洲
{ code: 'DZ', nameEn: 'Algeria', nameZh: '阿尔及利亚', phoneCode: '213', flag: '🇩🇿' },
{ code: 'AO', nameEn: 'Angola', nameZh: '安哥拉', phoneCode: '244', flag: '🇦🇴' },
{ code: 'BJ', nameEn: 'Benin', nameZh: '贝宁', phoneCode: '229', flag: '🇧🇯' },
{ code: 'BW', nameEn: 'Botswana', nameZh: '博茨瓦纳', phoneCode: '267', flag: '🇧🇼' },
{ code: 'BF', nameEn: 'Burkina Faso', nameZh: '布基纳法索', phoneCode: '226', flag: '🇧🇫' },
{ code: 'BI', nameEn: 'Burundi', nameZh: '布隆迪', phoneCode: '257', flag: '🇧🇮' },
{ code: 'CV', nameEn: 'Cabo Verde', nameZh: '佛得角', phoneCode: '238', flag: '🇨🇻' },
{ code: 'CM', nameEn: 'Cameroon', nameZh: '喀麦隆', phoneCode: '237', flag: '🇨🇲' },
{ code: 'CF', nameEn: 'Central African Republic', nameZh: '中非', phoneCode: '236', flag: '🇨🇫' },
{ code: 'TD', nameEn: 'Chad', nameZh: '乍得', phoneCode: '235', flag: '🇹🇩' },
{ code: 'KM', nameEn: 'Comoros', nameZh: '科摩罗', phoneCode: '269', flag: '🇰🇲' },
{ code: 'CG', nameEn: 'Congo', nameZh: '刚果(布)', phoneCode: '242', flag: '🇨🇬' },
{ code: 'CD', nameEn: 'Congo (DRC)', nameZh: '刚果(金)', phoneCode: '243', flag: '🇨🇩' },
{ code: 'CI', nameEn: "Côte d'Ivoire", nameZh: '科特迪瓦', phoneCode: '225', flag: '🇨🇮' },
{ code: 'DJ', nameEn: 'Djibouti', nameZh: '吉布提', phoneCode: '253', flag: '🇩🇯' },
{ code: 'EG', nameEn: 'Egypt', nameZh: '埃及', phoneCode: '20', flag: '🇪🇬' },
{ code: 'GQ', nameEn: 'Equatorial Guinea', nameZh: '赤道几内亚', phoneCode: '240', flag: '🇬🇶' },
{ code: 'ER', nameEn: 'Eritrea', nameZh: '厄立特里亚', phoneCode: '291', flag: '🇪🇷' },
{ code: 'SZ', nameEn: 'Eswatini', nameZh: '斯威士兰', phoneCode: '268', flag: '🇸🇿' },
{ code: 'ET', nameEn: 'Ethiopia', nameZh: '埃塞俄比亚', phoneCode: '251', flag: '🇪🇹' },
{ code: 'GA', nameEn: 'Gabon', nameZh: '加蓬', phoneCode: '241', flag: '🇬🇦' },
{ code: 'GM', nameEn: 'Gambia', nameZh: '冈比亚', phoneCode: '220', flag: '🇬🇲' },
{ code: 'GH', nameEn: 'Ghana', nameZh: '加纳', phoneCode: '233', flag: '🇬🇭' },
{ code: 'GN', nameEn: 'Guinea', nameZh: '几内亚', phoneCode: '224', flag: '🇬🇳' },
{ code: 'GW', nameEn: 'Guinea-Bissau', nameZh: '几内亚比绍', phoneCode: '245', flag: '🇬🇼' },
{ code: 'KE', nameEn: 'Kenya', nameZh: '肯尼亚', phoneCode: '254', flag: '🇰🇪' },
{ code: 'LS', nameEn: 'Lesotho', nameZh: '莱索托', phoneCode: '266', flag: '🇱🇸' },
{ code: 'LR', nameEn: 'Liberia', nameZh: '利比里亚', phoneCode: '231', flag: '🇱🇷' },
{ code: 'LY', nameEn: 'Libya', nameZh: '利比亚', phoneCode: '218', flag: '🇱🇾' },
{ code: 'MG', nameEn: 'Madagascar', nameZh: '马达加斯加', phoneCode: '261', flag: '🇲🇬' },
{ code: 'MW', nameEn: 'Malawi', nameZh: '马拉维', phoneCode: '265', flag: '🇲🇼' },
{ code: 'ML', nameEn: 'Mali', nameZh: '马里', phoneCode: '223', flag: '🇲🇱' },
{ code: 'MR', nameEn: 'Mauritania', nameZh: '毛里塔尼亚', phoneCode: '222', flag: '🇲🇷' },
{ code: 'MU', nameEn: 'Mauritius', nameZh: '毛里求斯', phoneCode: '230', flag: '🇲🇺' },
{ code: 'MA', nameEn: 'Morocco', nameZh: '摩洛哥', phoneCode: '212', flag: '🇲🇦' },
{ code: 'MZ', nameEn: 'Mozambique', nameZh: '莫桑比克', phoneCode: '258', flag: '🇲🇿' },
{ code: 'NA', nameEn: 'Namibia', nameZh: '纳米比亚', phoneCode: '264', flag: '🇳🇦' },
{ code: 'NE', nameEn: 'Niger', nameZh: '尼日尔', phoneCode: '227', flag: '🇳🇪' },
{ code: 'NG', nameEn: 'Nigeria', nameZh: '尼日利亚', phoneCode: '234', flag: '🇳🇬' },
{ code: 'RW', nameEn: 'Rwanda', nameZh: '卢旺达', phoneCode: '250', flag: '🇷🇼' },
{ code: 'ST', nameEn: 'São Tomé and Príncipe', nameZh: '圣多美和普林西比', phoneCode: '239', flag: '🇸🇹' },
{ code: 'SN', nameEn: 'Senegal', nameZh: '塞内加尔', phoneCode: '221', flag: '🇸🇳' },
{ code: 'SC', nameEn: 'Seychelles', nameZh: '塞舌尔', phoneCode: '248', flag: '🇸🇨' },
{ code: 'SL', nameEn: 'Sierra Leone', nameZh: '塞拉利昂', phoneCode: '232', flag: '🇸🇱' },
{ code: 'SO', nameEn: 'Somalia', nameZh: '索马里', phoneCode: '252', flag: '🇸🇴' },
{ code: 'ZA', nameEn: 'South Africa', nameZh: '南非', phoneCode: '27', flag: '🇿🇦' },
{ code: 'SS', nameEn: 'South Sudan', nameZh: '南苏丹', phoneCode: '211', flag: '🇸🇸' },
{ code: 'SD', nameEn: 'Sudan', nameZh: '苏丹', phoneCode: '249', flag: '🇸🇩' },
{ code: 'TZ', nameEn: 'Tanzania', nameZh: '坦桑尼亚', phoneCode: '255', flag: '🇹🇿' },
{ code: 'TG', nameEn: 'Togo', nameZh: '多哥', phoneCode: '228', flag: '🇹🇬' },
{ code: 'TN', nameEn: 'Tunisia', nameZh: '突尼斯', phoneCode: '216', flag: '🇹🇳' },
{ code: 'UG', nameEn: 'Uganda', nameZh: '乌干达', phoneCode: '256', flag: '🇺🇬' },
{ code: 'ZM', nameEn: 'Zambia', nameZh: '赞比亚', phoneCode: '260', flag: '🇿🇲' },
{ code: 'ZW', nameEn: 'Zimbabwe', nameZh: '津巴布韦', phoneCode: '263', flag: '🇿🇼' },
// 北美洲
{ code: 'AG', nameEn: 'Antigua and Barbuda', nameZh: '安提瓜和巴布达', phoneCode: '1268', flag: '🇦🇬' },
{ code: 'BS', nameEn: 'Bahamas', nameZh: '巴哈马', phoneCode: '1242', flag: '🇧🇸' },
{ code: 'BB', nameEn: 'Barbados', nameZh: '巴巴多斯', phoneCode: '1246', flag: '🇧🇧' },
{ code: 'BZ', nameEn: 'Belize', nameZh: '伯利兹', phoneCode: '501', flag: '🇧🇿' },
{ code: 'CR', nameEn: 'Costa Rica', nameZh: '哥斯达黎加', phoneCode: '506', flag: '🇨🇷' },
{ code: 'CU', nameEn: 'Cuba', nameZh: '古巴', phoneCode: '53', flag: '🇨🇺' },
{ code: 'DM', nameEn: 'Dominica', nameZh: '多米尼克', phoneCode: '1767', flag: '🇩🇲' },
{ code: 'DO', nameEn: 'Dominican Republic', nameZh: '多米尼加', phoneCode: '1809', flag: '🇩🇴' },
{ code: 'SV', nameEn: 'El Salvador', nameZh: '萨尔瓦多', phoneCode: '503', flag: '🇸🇻' },
{ code: 'GD', nameEn: 'Grenada', nameZh: '格林纳达', phoneCode: '1473', flag: '🇬🇩' },
{ code: 'GT', nameEn: 'Guatemala', nameZh: '危地马拉', phoneCode: '502', flag: '🇬🇹' },
{ code: 'HT', nameEn: 'Haiti', nameZh: '海地', phoneCode: '509', flag: '🇭🇹' },
{ code: 'HN', nameEn: 'Honduras', nameZh: '洪都拉斯', phoneCode: '504', flag: '🇭🇳' },
{ code: 'JM', nameEn: 'Jamaica', nameZh: '牙买加', phoneCode: '1876', flag: '🇯🇲' },
{ code: 'MX', nameEn: 'Mexico', nameZh: '墨西哥', phoneCode: '52', flag: '🇲🇽' },
{ code: 'NI', nameEn: 'Nicaragua', nameZh: '尼加拉瓜', phoneCode: '505', flag: '🇳🇮' },
{ code: 'PA', nameEn: 'Panama', nameZh: '巴拿马', phoneCode: '507', flag: '🇵🇦' },
{ code: 'KN', nameEn: 'Saint Kitts and Nevis', nameZh: '圣基茨和尼维斯', phoneCode: '1869', flag: '🇰🇳' },
{ code: 'LC', nameEn: 'Saint Lucia', nameZh: '圣卢西亚', phoneCode: '1758', flag: '🇱🇨' },
{ code: 'VC', nameEn: 'Saint Vincent and the Grenadines', nameZh: '圣文森特和格林纳丁斯', phoneCode: '1784', flag: '🇻🇨' },
{ code: 'TT', nameEn: 'Trinidad and Tobago', nameZh: '特立尼达和多巴哥', phoneCode: '1868', flag: '🇹🇹' },
// 南美洲
{ code: 'AR', nameEn: 'Argentina', nameZh: '阿根廷', phoneCode: '54', flag: '🇦🇷' },
{ code: 'BO', nameEn: 'Bolivia', nameZh: '玻利维亚', phoneCode: '591', flag: '🇧🇴' },
{ code: 'BR', nameEn: 'Brazil', nameZh: '巴西', phoneCode: '55', flag: '🇧🇷' },
{ code: 'CL', nameEn: 'Chile', nameZh: '智利', phoneCode: '56', flag: '🇨🇱' },
{ code: 'CO', nameEn: 'Colombia', nameZh: '哥伦比亚', phoneCode: '57', flag: '🇨🇴' },
{ code: 'EC', nameEn: 'Ecuador', nameZh: '厄瓜多尔', phoneCode: '593', flag: '🇪🇨' },
{ code: 'GY', nameEn: 'Guyana', nameZh: '圭亚那', phoneCode: '592', flag: '🇬🇾' },
{ code: 'PY', nameEn: 'Paraguay', nameZh: '巴拉圭', phoneCode: '595', flag: '🇵🇾' },
{ code: 'PE', nameEn: 'Peru', nameZh: '秘鲁', phoneCode: '51', flag: '🇵🇪' },
{ code: 'SR', nameEn: 'Suriname', nameZh: '苏里南', phoneCode: '597', flag: '🇸🇷' },
{ code: 'UY', nameEn: 'Uruguay', nameZh: '乌拉圭', phoneCode: '598', flag: '🇺🇾' },
{ code: 'VE', nameEn: 'Venezuela', nameZh: '委内瑞拉', phoneCode: '58', flag: '🇻🇪' },
// 大洋洲
{ code: 'FJ', nameEn: 'Fiji', nameZh: '斐济', phoneCode: '679', flag: '🇫🇯' },
{ code: 'KI', nameEn: 'Kiribati', nameZh: '基里巴斯', phoneCode: '686', flag: '🇰🇮' },
{ code: 'MH', nameEn: 'Marshall Islands', nameZh: '马绍尔群岛', phoneCode: '692', flag: '🇲🇭' },
{ code: 'FM', nameEn: 'Micronesia', nameZh: '密克罗尼西亚', phoneCode: '691', flag: '🇫🇲' },
{ code: 'NR', nameEn: 'Nauru', nameZh: '瑙鲁', phoneCode: '674', flag: '🇳🇷' },
{ code: 'NZ', nameEn: 'New Zealand', nameZh: '新西兰', phoneCode: '64', flag: '🇳🇿' },
{ code: 'PW', nameEn: 'Palau', nameZh: '帕劳', phoneCode: '680', flag: '🇵🇼' },
{ code: 'PG', nameEn: 'Papua New Guinea', nameZh: '巴布亚新几内亚', phoneCode: '675', flag: '🇵🇬' },
{ code: 'WS', nameEn: 'Samoa', nameZh: '萨摩亚', phoneCode: '685', flag: '🇼🇸' },
{ code: 'SB', nameEn: 'Solomon Islands', nameZh: '所罗门群岛', phoneCode: '677', flag: '🇸🇧' },
{ code: 'TO', nameEn: 'Tonga', nameZh: '汤加', phoneCode: '676', flag: '🇹🇴' },
{ code: 'TV', nameEn: 'Tuvalu', nameZh: '图瓦卢', phoneCode: '688', flag: '🇹🇻' },
{ code: 'VU', nameEn: 'Vanuatu', nameZh: '瓦努阿图', phoneCode: '678', flag: '🇻🇺' },
];
/**
* 按国家代码获取国家信息
*/
export const getCountryByCode = (code: string): CountryData | undefined => {
return COUNTRIES.find((c) => c.code === code);
};
/**
* 按手机区号获取国家列表 (可能有多个国家使用相同区号)
*/
export const getCountriesByPhoneCode = (phoneCode: string): CountryData[] => {
return COUNTRIES.filter((c) => c.phoneCode === phoneCode);
};
/**
* 获取常用国家列表
*/
export const getPopularCountries = (): CountryData[] => {
return COUNTRIES.filter((c) => c.popular);
};
/**
* 获取排序后的国家列表 (常用国家在前)
*/
export const getSortedCountries = (): CountryData[] => {
const popular = COUNTRIES.filter((c) => c.popular);
const others = COUNTRIES.filter((c) => !c.popular);
return [...popular, ...others];
};
/**
* 获取去重的手机区号列表 (用于手机区号选择器)
*/
export const getUniquePhoneCodes = (): { phoneCode: string; countries: CountryData[] }[] => {
const phoneCodeMap = new Map<string, CountryData[]>();
COUNTRIES.forEach((country) => {
const existing = phoneCodeMap.get(country.phoneCode);
if (existing) {
existing.push(country);
} else {
phoneCodeMap.set(country.phoneCode, [country]);
}
});
// 按区号数字排序
return Array.from(phoneCodeMap.entries())
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
.map(([phoneCode, countries]) => ({
phoneCode,
countries,
}));
};

View File

@@ -0,0 +1,13 @@
export { default as CountrySelect } from './CountrySelect';
export { default as PhonePrefixSelect } from './PhonePrefixSelect';
export {
COUNTRIES,
getCountryByCode,
getCountriesByPhoneCode,
getPopularCountries,
getSortedCountries,
getUniquePhoneCodes,
} from './countries';
export type { CountryData } from './countries';
export type { CountrySelectProps } from './CountrySelect';
export type { PhonePrefixSelectProps } from './PhonePrefixSelect';

View File

@@ -0,0 +1,112 @@
import { Component } from 'react';
import type { ErrorInfo, ReactNode } from 'react';
import { Result, Button, Space, Alert } from 'antd';
import { ReloadOutlined, RollbackOutlined } from '@ant-design/icons';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
handleReload = () => {
// Clear all potentially corrupted storage
try {
// Clear wagmi
localStorage.removeItem('wagmi.store');
localStorage.removeItem('wagmi.connected');
localStorage.removeItem('wagmi.wallet');
// Clear WalletConnect
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (
key &&
(key.startsWith('wc@') ||
key.startsWith('wagmi') ||
key.startsWith('@w3m') ||
key.includes('walletconnect') ||
key === 'yt_asset_tx_history')
) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
sessionStorage.clear();
} catch (e) {
console.error('Failed to clear storage:', e);
}
window.location.reload();
};
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '50px', height: '100vh', background: '#f0f2f5' }}>
<Result
status="error"
title="Something went wrong"
subTitle={
typeof this.state.error?.message === 'string'
? this.state.error.message
: JSON.stringify(this.state.error?.message) || 'Unknown error'
}
extra={
<>
<Alert
message="常见原因"
description={
<ul style={{ paddingLeft: 20 }}>
<li></li>
<li></li>
<li></li>
</ul>
}
type="info"
style={{ marginBottom: 24, textAlign: 'left' }}
/>
<Space>
<Button icon={<RollbackOutlined />} onClick={this.handleReset}>
</Button>
<Button type="primary" icon={<ReloadOutlined />} onClick={this.handleReload}>
</Button>
</Space>
<Alert
message="提示:如果问题持续存在,请尝试禁用其他钱包插件,只保留一个。"
type="warning"
showIcon
style={{ marginTop: 24 }}
/>
</>
}
/>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,16 @@
import { DefaultFooter } from '@ant-design/pro-components';
import React from 'react';
const Footer: React.FC = () => {
return (
<DefaultFooter
style={{
background: 'none',
}}
copyright="Powered by Sofio"
links={[]}
/>
);
};
export default Footer;

View File

@@ -0,0 +1,41 @@
import { Dropdown } from 'antd';
import type { DropDownProps } from 'antd/es/dropdown';
import { createStyles } from 'antd-style';
import classNames from 'classnames';
import React from 'react';
const useStyles = createStyles(({ token }) => {
return {
dropdown: {
[`@media screen and (max-width: ${token.screenXS}px)`]: {
width: '100%',
},
},
};
});
export type HeaderDropdownProps = {
overlayClassName?: string;
placement?:
| 'bottomLeft'
| 'bottomRight'
| 'topLeft'
| 'topCenter'
| 'topRight'
| 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({
overlayClassName: cls,
...restProps
}) => {
const { styles } = useStyles();
return (
<Dropdown
overlayClassName={classNames(styles.dropdown, cls)}
{...restProps}
/>
);
};
export default HeaderDropdown;

View File

@@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next'
export function LanguageSwitch() {
const { i18n } = useTranslation()
const toggleLanguage = () => {
const newLang = i18n.language === 'zh' ? 'en' : 'zh'
i18n.changeLanguage(newLang)
}
return (
<button onClick={toggleLanguage} className="btn btn-lang">
{i18n.language === 'zh' ? 'EN' : '中文'}
</button>
)
}

View File

@@ -0,0 +1,138 @@
import {
LogoutOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { history, useModel } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Spin } from 'antd';
import { createStyles } from 'antd-style';
import React from 'react';
import { flushSync } from 'react-dom';
import { logout } from '@/services/auth/api';
import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = {
menu?: boolean;
children?: React.ReactNode;
};
export const AvatarName = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
return <span className="anticon">{currentUser?.name}</span>;
};
const useStyles = createStyles(({ token }) => {
return {
action: {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
alignItems: 'center',
padding: '0 8px',
cursor: 'pointer',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
},
};
});
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
menu,
children,
}) => {
/**
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => {
// 调用登出接口,会自动清除本地 Token
await logout();
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
const searchParams = new URLSearchParams({
redirect: pathname + search,
});
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: searchParams.toString(),
});
}
};
const { styles } = useStyles();
const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick: MenuProps['onClick'] = (event) => {
const { key } = event;
if (key === 'logout') {
flushSync(() => {
setInitialState((s) => ({ ...s, currentUser: undefined }));
});
loginOut();
return;
}
history.push(`/account/${key}`);
};
const loading = (
<span className={styles.action}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
if (!initialState) {
return loading;
}
const { currentUser } = initialState;
if (!currentUser || !currentUser.name) {
return loading;
}
const menuItems = [
...(menu
? [
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
type: 'divider' as const,
},
]
: []),
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
return (
<HeaderDropdown
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
>
{children}
</HeaderDropdown>
);
};

View File

@@ -0,0 +1,32 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { SelectLang as UmiSelectLang } from '@umijs/max';
export type SiderTheme = 'light' | 'dark';
export const SelectLang: React.FC = () => {
return (
<UmiSelectLang
style={{
padding: 4,
}}
/>
);
};
export const Question: React.FC = () => {
return (
<a
href="https://pro.ant.design/docs/getting-started"
target="_blank"
rel="noreferrer"
style={{
display: 'inline-flex',
padding: '4px',
fontSize: '18px',
color: 'inherit',
}}
>
<QuestionCircleOutlined />
</a>
);
};

View File

@@ -0,0 +1,30 @@
import { createContext, useContext, useCallback } from 'react';
import type { ReactNode } from 'react';
import { message } from 'antd';
export type ToastType = 'success' | 'error' | 'info' | 'warning';
interface ToastContextType {
showToast: (type: ToastType, msg: string, duration?: number) => void;
}
const ToastContext = createContext<ToastContextType | null>(null);
export function ToastProvider({ children }: { children: ReactNode }) {
const showToast = useCallback((type: ToastType, msg: string, duration = 4) => {
// 使用 Ant Design 的 message API
message[type](msg, duration);
}, []);
return (
<ToastContext.Provider value={{ showToast }}>{children}</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within ToastProvider');
}
return context;
}

View File

@@ -0,0 +1,208 @@
import { useState } from 'react';
import { ProTable } from '@ant-design/pro-components';
import type { ProColumns } from '@ant-design/pro-components';
import { Button, Tag, Space, Typography, message } from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
LoadingOutlined,
CopyOutlined,
LinkOutlined,
} from '@ant-design/icons';
import { useChainId } from 'wagmi';
import { getBlockExplorer } from '@/config/contracts';
import type { TransactionRecord } from '@/hooks/useTransactionHistory';
const { Link } = Typography;
interface Props {
transactions: TransactionRecord[];
onClear: () => void;
}
export function TransactionHistory({ transactions, onClear }: Props) {
const [copiedId, setCopiedId] = useState<string | null>(null);
const chainId = useChainId();
const blockExplorer = getBlockExplorer(chainId);
const handleCopy = async (hash: string, txId: string) => {
try {
await navigator.clipboard.writeText(hash);
setCopiedId(txId);
message.success('已复制');
setTimeout(() => setCopiedId(null), 1500);
} catch (err) {
message.error('复制失败');
}
};
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
mint: 'Mint USDC',
burn: 'Burn USDC',
buy: 'Buy YT',
sell: 'Sell YT',
approve: 'Approve',
create_vault: 'Create Vault',
update_price: 'Update Price',
test: 'Test',
addLiquidity: 'Add Liquidity',
removeLiquidity: 'Remove Liquidity',
swap: 'Swap',
transfer: 'Transfer',
};
return labels[type] || type;
};
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleString();
};
const shortenHash = (hash: string | undefined | null) => {
if (!hash || typeof hash !== 'string' || hash.length < 14) {
return hash || '-';
}
return `${hash.slice(0, 8)}...${hash.slice(-6)}`;
};
const columns: ProColumns<TransactionRecord>[] = [
{
title: '类型',
dataIndex: 'type',
width: 150,
render: (_, record) => <Tag color="blue">{getTypeLabel(record.type)}</Tag>,
filters: true,
onFilter: true,
valueType: 'select',
valueEnum: {
mint: { text: 'Mint USDC' },
burn: { text: 'Burn USDC' },
buy: { text: 'Buy YT' },
sell: { text: 'Sell YT' },
approve: { text: 'Approve' },
create_vault: { text: 'Create Vault' },
update_price: { text: 'Update Price' },
addLiquidity: { text: 'Add Liquidity' },
removeLiquidity: { text: 'Remove Liquidity' },
swap: { text: 'Swap' },
transfer: { text: 'Transfer' },
},
},
{
title: '金额',
dataIndex: 'amount',
width: 150,
render: (_, record) =>
record.amount ? `${record.amount} ${record.token || ''}` : '-',
},
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (_, record) => {
if (record.status === 'success') {
return (
<Tag icon={<CheckCircleOutlined />} color="success">
</Tag>
);
}
if (record.status === 'failed') {
return (
<Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
);
}
return (
<Tag icon={<LoadingOutlined />} color="processing">
</Tag>
);
},
filters: true,
onFilter: true,
valueEnum: {
success: { text: '成功', status: 'Success' },
failed: { text: '失败', status: 'Error' },
pending: { text: '处理中', status: 'Processing' },
},
},
{
title: '交易哈希',
dataIndex: 'hash',
width: 200,
render: (_, record) => {
if (record.hash && typeof record.hash === 'string' && record.hash.startsWith('0x')) {
return (
<Space>
<Link
href={`${blockExplorer}/tx/${record.hash}`}
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> {shortenHash(record.hash)}
</Link>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(record.hash, record.id)}
>
{copiedId === record.id ? '已复制' : ''}
</Button>
</Space>
);
}
return <span style={{ color: '#999' }}>-</span>;
},
},
{
title: '时间',
dataIndex: 'timestamp',
width: 180,
render: (_, record) => formatTime(record.timestamp),
sorter: (a, b) => a.timestamp - b.timestamp,
defaultSortOrder: 'descend',
},
{
title: '错误信息',
dataIndex: 'error',
ellipsis: true,
render: (_, record) => {
if (record.error) {
return (
<Typography.Text type="danger" ellipsis={{ tooltip: true }}>
{typeof record.error === 'string' ? record.error : JSON.stringify(record.error)}
</Typography.Text>
);
}
return '-';
},
},
];
return (
<ProTable<TransactionRecord>
columns={columns}
dataSource={transactions.filter((tx) => tx && tx.id).slice(0, 50)}
rowKey="id"
search={false}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
toolBarRender={() => [
<Button key="clear" danger onClick={onClear}>
</Button>,
]}
headerTitle="交易历史"
locale={{
emptyText: '暂无交易记录',
}}
/>
);
}

View File

@@ -0,0 +1,126 @@
import { useWeb3Modal } from '@web3modal/wagmi/react';
import { useAccount, useDisconnect } from 'wagmi';
import { Button, Space, Typography } from 'antd';
import { WalletOutlined, SwapOutlined, DisconnectOutlined, ReloadOutlined } from '@ant-design/icons';
const { Text } = Typography;
// 清理 WalletConnect 相关缓存和损坏的数据
const clearAllCache = () => {
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (
key &&
(key.startsWith('wc@') ||
key.startsWith('wagmi') ||
key.startsWith('@w3m') ||
key.startsWith('@reown') ||
key.includes('walletconnect') ||
key.includes('WalletConnect') ||
key.includes('appkit'))
) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
sessionStorage.clear();
};
export function ConnectButton() {
const { open } = useWeb3Modal();
const { address, isConnected, connector } = useAccount();
const { disconnect } = useDisconnect();
const formatAddress = (addr: string) => {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
// 断开连接并清理缓存,然后刷新页面
const handleDisconnect = async () => {
try {
disconnect();
await new Promise((resolve) => setTimeout(resolve, 100));
} catch {
// ignore
}
clearAllCache();
// 设置跳过自动重连标记
sessionStorage.setItem('skipReconnect', 'true');
window.location.reload();
};
// 切换账户:弹出钱包账户选择
const handleSwitchAccount = async () => {
try {
// 对于 injected 钱包MetaMask 等),使用 wallet_requestPermissions
if (
connector?.id === 'injected' ||
connector?.id === 'metaMask' ||
connector?.id === 'io.metamask'
) {
const provider = (await connector.getProvider()) as {
request?: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
};
if (provider?.request) {
await provider.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
});
return;
}
}
} catch (err) {
console.log('Switch account error:', err);
}
// 失败或其他钱包:断开后重新连接
try {
disconnect();
await new Promise((resolve) => setTimeout(resolve, 100));
} catch {
// ignore
}
clearAllCache();
sessionStorage.setItem('skipReconnect', 'true');
sessionStorage.setItem('autoOpenConnect', 'true');
window.location.reload();
};
// 重置连接(清理缓存后刷新)
const handleReset = () => {
clearAllCache();
window.location.reload();
};
if (isConnected && address) {
return (
<Space>
<Text strong style={{ fontFamily: 'monospace' }} title={address}>
{formatAddress(address)}
</Text>
<Button size="small" icon={<SwapOutlined />} onClick={handleSwitchAccount}>
</Button>
<Button
size="small"
danger
icon={<DisconnectOutlined />}
onClick={handleDisconnect}
>
</Button>
</Space>
);
}
return (
<Space>
<Button type="primary" icon={<WalletOutlined />} onClick={() => open()}>
</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={handleReset} title="强制连接">
</Button>
</Space>
);
}

View File

@@ -0,0 +1,59 @@
import { useChainId, useSwitchChain, useAccount } from 'wagmi'
import { arbitrumSepolia, bscTestnet } from 'wagmi/chains'
import { Select, Tag } from 'antd'
import { ApiOutlined, WarningOutlined } from '@ant-design/icons'
const CHAINS = [
{
id: arbitrumSepolia.id,
name: 'Arbitrum Sepolia',
shortName: 'ARB',
color: '#28A0F0',
},
{
id: bscTestnet.id,
name: 'BNB Testnet',
shortName: 'BNB',
color: '#F3BA2F',
},
]
export function NetworkSwitch() {
const { isConnected } = useAccount()
const chainId = useChainId()
const { switchChain, isPending } = useSwitchChain()
if (!isConnected) return null
const currentChain = CHAINS.find((c) => c.id === chainId)
const isUnsupported = !currentChain
return (
<Select
loading={isPending}
value={isUnsupported ? undefined : chainId}
placeholder={
<span style={{ color: '#ff4d4f' }}>
<WarningOutlined />
</span>
}
onChange={(value) => switchChain({ chainId: value })}
style={{ minWidth: 160 }}
options={CHAINS.map((chain) => ({
value: chain.id,
label: (
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Tag
color={chain.color}
style={{ margin: 0, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}
>
{chain.shortName}
</Tag>
{chain.name}
</span>
),
}))}
suffixIcon={isUnsupported ? <WarningOutlined style={{ color: '#ff4d4f' }} /> : <ApiOutlined />}
/>
)
}

View File

@@ -0,0 +1,18 @@
/**
* 这个文件作为组件的目录
* 目的是统一管理对外输出的组件,方便分类
*/
/**
* 布局组件
*/
import Footer from './Footer';
import { Question, SelectLang } from './RightContent';
import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
export {
AvatarDropdown,
AvatarName,
Footer,
Question,
SelectLang,
};

View File

@@ -0,0 +1,16 @@
/**
* Dynamic contract address registry.
* Populated from /api/contracts at runtime.
* Module-level store — safe because addresses are the same across all renders.
*/
const dynamicRegistry = new Map<string, `0x${string}`>()
export function setContractAddress(name: string, chainId: number, address: string) {
if (address && address.startsWith('0x')) {
dynamicRegistry.set(`${name}:${chainId}`, address as `0x${string}`)
}
}
export function getContractAddress(name: string, chainId: number): `0x${string}` | undefined {
return dynamicRegistry.get(`${name}:${chainId}`)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
import { createWeb3Modal } from '@web3modal/wagmi/react'
import { http, createConfig, createStorage } from 'wagmi'
import { arbitrumSepolia, bscTestnet } from 'wagmi/chains'
import { QueryClient } from '@tanstack/react-query'
import { injected } from 'wagmi/connectors'
export const queryClient = new QueryClient()
// WalletConnect Project ID - 从 https://cloud.walletconnect.com/ 获取
const projectId = '3a8170812b534d0ff9d794f19a901d64'
// 检查是否应该跳过自动重连
const shouldSkipReconnect = () => {
if (typeof window === 'undefined') return false
const skip = sessionStorage.getItem('skipReconnect') === 'true'
if (skip) {
sessionStorage.removeItem('skipReconnect')
}
return skip
}
// 自定义 storage在需要时禁用重连
const customStorage = createStorage({
storage: {
getItem: (key: string) => {
// 如果设置了跳过重连,返回 null 来阻止重连
if (shouldSkipReconnect() && key === 'wagmi.recentConnectorId') {
return null
}
return localStorage.getItem(key)
},
setItem: (key: string, value: string) => localStorage.setItem(key, value),
removeItem: (key: string) => localStorage.removeItem(key),
},
})
export const config = createConfig({
chains: [arbitrumSepolia, bscTestnet], // 支持多链
connectors: [
injected(), // 支持 MetaMask 等浏览器钱包
// Web3Modal 会自动添加 WalletConnect 和其他钱包
],
transports: {
[arbitrumSepolia.id]: http(), // ARB Sepolia (421614)
[bscTestnet.id]: http(), // BNB Testnet (97)
},
storage: customStorage,
})
createWeb3Modal({
wagmiConfig: config,
projectId,
enableAnalytics: false,
themeMode: 'light',
})

View File

@@ -0,0 +1,33 @@
import { createContext, useContext } from 'react'
import type { ReactNode } from 'react'
import { useTransactionHistory } from '../hooks/useTransactionHistory'
import type { TransactionRecord, TransactionType } from '../hooks/useTransactionHistory'
interface TransactionContextType {
transactions: TransactionRecord[]
addTransaction: (tx: Omit<TransactionRecord, 'id' | 'timestamp'>) => string
updateTransaction: (id: string, updates: Partial<TransactionRecord>) => void
clearHistory: () => void
}
const TransactionContext = createContext<TransactionContextType | null>(null)
export function TransactionProvider({ children }: { children: ReactNode }) {
const history = useTransactionHistory()
return (
<TransactionContext.Provider value={history}>
{children}
</TransactionContext.Provider>
)
}
export function useTransactions() {
const context = useContext(TransactionContext)
if (!context) {
throw new Error('useTransactions must be used within TransactionProvider')
}
return context
}
export type { TransactionType, TransactionRecord }

94
antdesign/src/global.less Normal file
View File

@@ -0,0 +1,94 @@
@font-face {
font-family: "AlibabaSans";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*1GSgSYDD_aIAAAAAQsAAAAgAegCCAQ/AlibabaSans-Light.woff2")
format("woff2");
}
@font-face {
font-family: "AlibabaSans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*2zEUQqnPNesAAAAAQtAAAAgAegCCAQ/AlibabaSans-Regular.woff2")
format("woff2");
}
@font-face {
font-family: "AlibabaSans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Medium.woff2")
format("woff2");
}
@font-face {
font-family: "AlibabaSans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Bold.woff2")
format("woff2");
}
@font-face {
font-family: "AlibabaSans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Heavy.woff2")
format("woff2");
}
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
font-family:
AlibabaSans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
left: unset;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: 768px) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(() => {
return {
colorWeak: {
filter: 'invert(80%)',
},
'ant-layout': {
minHeight: '100vh',
},
'ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed': {
left: 'unset',
},
canvas: {
display: 'block',
},
body: {
textRendering: 'optimizeLegibility',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
},
'ul,ol': {
listStyle: 'none',
},
'@media(max-width: 768px)': {
'ant-table': {
width: '100%',
overflowX: 'auto',
'&-thead > tr, &-tbody > tr': {
'> th, > td': {
whiteSpace: 'pre',
'> span': {
display: 'block',
},
},
},
},
},
};
});
export default useStyles;

93
antdesign/src/global.tsx Normal file
View File

@@ -0,0 +1,93 @@
import { useIntl } from '@umijs/max';
import { Button, message, notification } from 'antd';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
const isHttps = document.location.protocol === 'https:';
const clearCache = () => {
// remove all caches
if (window.caches) {
caches
.keys()
.then((keys) => {
keys.forEach((key) => {
caches.delete(key);
});
})
.catch((e) => console.log(e));
}
};
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
});
// Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', (event: Event) => {
const e = event as CustomEvent;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail?.waiting;
if (!worker) {
return true;
}
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (msgEvent) => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
});
clearCache();
window.location.reload();
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.destroy(key);
reloadSW();
}}
>
{useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
</Button>
);
notification.open({
message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
description: useIntl().formatMessage({
id: 'app.pwa.serviceworker.updated.hint',
}),
btn,
key,
onClose: async () => null,
});
});
} else if ('serviceWorker' in navigator && isHttps) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
serviceWorker.getRegistrations().then((sws) => {
sws.forEach((sw) => {
sw.unregister();
});
});
}
serviceWorker.getRegistration().then((sw) => {
if (sw) sw.unregister();
});
clearCache();
}

View File

@@ -0,0 +1,525 @@
/**
* LP 管理员操作 Hook
* 包含 P1-P3 级别的管理功能
*/
import { parseUnits } from 'viem'
import {
CONTRACTS,
GAS_CONFIG,
TOKEN_DECIMALS,
getTokenDecimals,
YT_POOL_MANAGER_ABI,
YT_VAULT_ABI,
YT_LP_TOKEN_ABI,
YT_PRICE_FEED_ABI,
FACTORY_ABI,
USDY_ABI,
} from '@/config/contracts'
import type {
CooldownForm,
SwapFeesForm,
WithdrawForm,
AumAdjustForm,
LimitsForm,
MaxSwapForm,
PermissionForm,
PriceConfigForm,
VaultConfigForm,
StableTokenConfig,
} from './types'
interface UseLPAdminActionsProps {
address: `0x${string}` | undefined
writeContract: (config: any) => void
recordTx: (type: string, amount?: string, token?: string) => void
}
export function useLPAdminActions({ address, writeContract, recordTx }: UseLPAdminActionsProps) {
// ===== P1: 核心配置 =====
// 设置冷却时间
const handleSetCooldownDuration = (form: CooldownForm) => {
if (!address || !form.duration) return
recordTx('test', form.duration, 'SetCooldownDuration')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setCooldownDuration',
args: [BigInt(form.duration)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置交换手续费
const handleSetSwapFees = (form: SwapFeesForm) => {
if (!address) return
recordTx('test', 'SwapFees', 'SetSwapFees')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setSwapFees',
args: [
BigInt(form.swapFee),
BigInt(form.stableSwapFee),
BigInt(form.taxBasisPoints),
BigInt(form.stableTaxBasisPoints)
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 紧急提取代币
const handleWithdrawToken = (form: WithdrawForm) => {
if (!address || !form.token || !form.receiver || !form.amount) return
recordTx('test', form.amount, 'WithdrawToken')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'withdrawToken',
args: [
form.token as `0x${string}`,
form.receiver as `0x${string}`,
parseUnits(form.amount, getTokenDecimals(form.token))
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== P2: 交易限制 =====
// 设置动态手续费开关
const handleSetDynamicFees = (enabled: boolean) => {
if (!address) return
recordTx('test', enabled ? 'ON' : 'OFF', 'SetDynamicFees')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setDynamicFees',
args: [enabled],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 暂停 LP Pool
const handlePausePool = () => {
if (!address) return
recordTx('test', 'Pause', 'PausePool')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'pause',
args: [],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 恢复 LP Pool
const handleUnpausePool = () => {
if (!address) return
recordTx('test', 'Unpause', 'UnpausePool')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'unpause',
args: [],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 AUM 调整
const handleSetAumAdjustment = (form: AumAdjustForm) => {
if (!address) return
recordTx('test', `+${form.addition}/-${form.deduction}`, 'SetAumAdjustment')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setAumAdjustment',
args: [
parseUnits(form.addition || '0', TOKEN_DECIMALS.USDY),
parseUnits(form.deduction || '0', TOKEN_DECIMALS.USDY)
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置最大交换金额
const handleSetMaxSwapAmount = (form: MaxSwapForm) => {
if (!address || !form.token || !form.amount) return
recordTx('test', form.amount, 'SetMaxSwapAmount')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setMaxSwapAmount',
args: [
form.token as `0x${string}`,
parseUnits(form.amount, getTokenDecimals(form.token))
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置最大滑点
const handleSetMaxSwapSlippageBps = (form: LimitsForm) => {
if (!address || !form.maxSwapSlippageBps) return
recordTx('test', form.maxSwapSlippageBps, 'SetMaxSwapSlippageBps')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setMaxSwapSlippageBps',
args: [BigInt(form.maxSwapSlippageBps)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置最大价格变化
const handleSetMaxPriceChangeBps = (form: LimitsForm) => {
if (!address || !form.maxPriceChangeBps) return
recordTx('test', form.maxPriceChangeBps, 'SetMaxPriceChangeBps')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setMaxPriceChangeBps',
args: [BigInt(form.maxPriceChangeBps)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== P3: 权限管理 =====
// 设置 PoolManager Gov
const handleSetPoolManagerGov = (form: PermissionForm) => {
if (!address || !form.govAddress) return
recordTx('test', form.govAddress, 'SetPoolManagerGov')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setGov',
args: [form.govAddress as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 YTVault Gov
const handleSetVaultGov = (form: PermissionForm) => {
if (!address || !form.govAddress) return
recordTx('test', form.govAddress, 'SetVaultGov')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setGov',
args: [form.govAddress as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Handler
const handleSetHandler = (form: PermissionForm) => {
if (!address || !form.handlerAddress) return
recordTx('test', form.handlerAddress, 'SetHandler')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setHandler',
args: [form.handlerAddress as `0x${string}`, form.handlerActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Keeper
const handleSetKeeper = (form: PermissionForm) => {
if (!address || !form.keeperAddress) return
recordTx('test', form.keeperAddress, 'SetKeeper')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setKeeper',
args: [form.keeperAddress as `0x${string}`, form.keeperActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Swapper
const handleSetSwapper = (form: PermissionForm) => {
if (!address || !form.swapperAddress) return
recordTx('test', form.swapperAddress, 'SetSwapper')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setSwapper',
args: [form.swapperAddress as `0x${string}`, form.swapperActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 PoolManager
const handleSetPoolManager = (form: PermissionForm) => {
if (!address || !form.poolManagerAddress) return
recordTx('test', form.poolManagerAddress, 'SetPoolManager')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setPoolManager',
args: [form.poolManagerAddress as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 ytLP Minter
const handleSetMinter = (form: PermissionForm) => {
if (!address || !form.minterAddress) return
recordTx('test', form.minterAddress, 'SetMinter')
writeContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'setMinter',
args: [form.minterAddress as `0x${string}`, form.minterActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== 价格配置 =====
// 设置价格
const handleSetPrice = (form: PriceConfigForm) => {
if (!address || !form.price) return
recordTx('test', form.price, 'SetPrice')
const priceIn30Decimals = parseUnits(form.price, TOKEN_DECIMALS.INTERNAL_PRICE)
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setPrice',
args: [form.token as `0x${string}`, priceIn30Decimals],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置价差
const handleSetSpread = (form: PriceConfigForm) => {
if (!address || !form.spreadBps) return
recordTx('test', form.spreadBps, 'SetSpread')
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setSpreadBasisPoints',
args: [form.token as `0x${string}`, BigInt(form.spreadBps)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置白名单代币
const handleSetWhitelistedToken = (form: VaultConfigForm) => {
if (!address || !form.token) return
recordTx('test', form.token, 'SetWhitelistedToken')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setWhitelistedToken',
args: [
form.token as `0x${string}`,
BigInt(form.tokenDecimals),
BigInt(form.tokenWeight),
parseUnits(form.maxUsdyAmount, TOKEN_DECIMALS.USDY),
true
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 移除白名单代币
const handleClearWhitelistedToken = (token: string) => {
if (!address || !token) return
recordTx('test', token, 'ClearWhitelistedToken')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'clearWhitelistedToken',
args: [token as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 USDC 价格源
const handleSetUsdcPriceSource = (source: string) => {
if (!address || !source) return
recordTx('test', source, 'SetUsdcPriceSource')
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setUsdcPriceSource',
args: [source as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置稳定币
const handleSetStableToken = (config: StableTokenConfig) => {
if (!address) return
recordTx('test', config.token, 'SetStableToken')
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setStableTokens',
args: [config.token as `0x${string}`, config.isStable],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置紧急模式
const handleSetEmergencyMode = (enabled: boolean) => {
if (!address) return
recordTx('test', enabled ? 'ON' : 'OFF', 'SetEmergencyMode')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setEmergencyMode',
args: [enabled],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Swap 开关
const handleSetSwapEnabled = (enabled: boolean) => {
if (!address) return
recordTx('test', enabled ? 'ON' : 'OFF', 'SetSwapEnabled')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setSwapEnabled',
args: [enabled],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 更新 ytPrice
const handleUpdateYtPrice = (token: string, ytPrice: string, usdcPrice: bigint) => {
if (!address || !ytPrice || !usdcPrice) return
recordTx('test', ytPrice, 'UpdateYtPrice')
const ytPriceIn30Decimals = parseUnits(ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE)
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPrices',
args: [token as `0x${string}`, usdcPrice, ytPriceIn30Decimals],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// USDY 白名单管理
const handleAddUsdyVault = (vault: string) => {
if (!address || !vault) return
recordTx('test', vault, 'AddUsdyVault')
writeContract({
address: CONTRACTS.USDY,
abi: USDY_ABI,
functionName: 'addVault',
args: [vault as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleRemoveUsdyVault = (vault: string) => {
if (!address || !vault) return
recordTx('test', vault, 'RemoveUsdyVault')
writeContract({
address: CONTRACTS.USDY,
abi: USDY_ABI,
functionName: 'removeVault',
args: [vault as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
return {
// P1
handleSetCooldownDuration,
handleSetSwapFees,
handleWithdrawToken,
// P2
handleSetDynamicFees,
handlePausePool,
handleUnpausePool,
handleSetAumAdjustment,
handleSetMaxSwapAmount,
handleSetMaxSwapSlippageBps,
handleSetMaxPriceChangeBps,
// P3
handleSetPoolManagerGov,
handleSetVaultGov,
handleSetHandler,
handleSetKeeper,
handleSetSwapper,
handleSetPoolManager,
handleSetMinter,
// 价格配置
handleSetPrice,
handleSetSpread,
handleSetWhitelistedToken,
handleClearWhitelistedToken,
handleSetUsdcPriceSource,
handleSetStableToken,
handleSetEmergencyMode,
handleSetSwapEnabled,
handleUpdateYtPrice,
// USDY 白名单
handleAddUsdyVault,
handleRemoveUsdyVault,
}
}

View File

@@ -0,0 +1,449 @@
/**
* LP 池数据读取 Hook
* 集中管理所有池子状态的读取逻辑
*/
import { useMemo } from 'react'
import { useAccount, useReadContract, useReadContracts } from 'wagmi'
import {
CONTRACTS,
TOKEN_DECIMALS,
YT_REWARD_ROUTER_ABI,
YT_LP_TOKEN_ABI,
YT_POOL_MANAGER_ABI,
YT_VAULT_ABI,
YT_PRICE_FEED_ABI,
FACTORY_ABI,
USDY_ABI,
} from '@/config/contracts'
import { ERC20_BASE_ABI, type PoolToken } from './types'
// 扩展的池子代币类型
export interface ExtendedPoolToken extends PoolToken {
balance?: bigint
isWhitelisted?: boolean
usdyAmount?: bigint
isStable?: boolean
weight?: bigint
}
// 管理员配置用的代币类型
export interface AvailableToken {
address: string
symbol: string
name: string
source: 'pool' | 'factory'
isStable?: boolean
isWhitelisted?: boolean
}
export function useLPPoolData() {
const { address, isConnected } = useAccount()
// ===== 动态获取 LP 池代币列表 =====
const { data: rawPoolTokenAddresses } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getAllPoolTokens',
})
// ===== 从 Factory 获取所有创建的金库 =====
const { data: factoryVaults } = useReadContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'getAllVaults',
})
// 去重处理
const poolTokenAddresses = useMemo(() => {
if (!rawPoolTokenAddresses || rawPoolTokenAddresses.length === 0) return []
const seen = new Set<string>()
return (rawPoolTokenAddresses as string[]).filter((addr: string) => {
const lower = addr.toLowerCase()
if (seen.has(lower)) return false
seen.add(lower)
return true
})
}, [rawPoolTokenAddresses])
// 批量读取代币信息
const tokenInfoContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.flatMap((addr: string) => [
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'symbol' as const },
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'name' as const },
])
}, [poolTokenAddresses])
const { data: tokenInfoResults } = useReadContracts({ contracts: tokenInfoContracts })
// 批量读取 Factory 金库信息
const factoryVaultInfoContracts = useMemo(() => {
if (!factoryVaults || factoryVaults.length === 0) return []
return (factoryVaults as string[]).flatMap((addr: string) => [
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'symbol' as const },
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'name' as const },
])
}, [factoryVaults])
const { data: factoryVaultInfoResults } = useReadContracts({ contracts: factoryVaultInfoContracts })
// 批量读取用户余额
const balanceContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0 || !address) return []
return poolTokenAddresses.map((addr: string) => ({
address: addr as `0x${string}`,
abi: ERC20_BASE_ABI,
functionName: 'balanceOf' as const,
args: [address] as const,
}))
}, [poolTokenAddresses, address])
const { data: balanceResults, refetch: refetchTokenBalances } = useReadContracts({ contracts: balanceContracts })
// 批量读取白名单状态
const whitelistContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_VAULT as `0x${string}`,
abi: YT_VAULT_ABI,
functionName: 'whitelistedTokens' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: whitelistResults, refetch: refetchVaultWhitelist } = useReadContracts({ contracts: whitelistContracts })
// 批量读取 usdyAmounts
const usdyAmountsContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_VAULT as `0x${string}`,
abi: YT_VAULT_ABI,
functionName: 'usdyAmounts' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: usdyAmountsResults, refetch: refetchUsdyAmounts } = useReadContracts({ contracts: usdyAmountsContracts })
// 批量读取稳定币状态
const stableTokensContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_PRICE_FEED as `0x${string}`,
abi: YT_PRICE_FEED_ABI,
functionName: 'stableTokens' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: stableTokensResults } = useReadContracts({ contracts: stableTokensContracts })
// 批量读取代币权重
const tokenWeightsContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_VAULT as `0x${string}`,
abi: YT_VAULT_ABI,
functionName: 'tokenWeights' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: tokenWeightsResults, refetch: refetchTokenWeights } = useReadContracts({ contracts: tokenWeightsContracts })
// 构建代币列表
const poolTokens = useMemo((): ExtendedPoolToken[] => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string, index: number) => {
const symbol = tokenInfoResults?.[index * 2]?.result as string | undefined
const name = tokenInfoResults?.[index * 2 + 1]?.result as string | undefined
const balance = balanceResults?.[index]?.result as bigint | undefined
const isWhitelisted = whitelistResults?.[index]?.result as boolean | undefined
const usdyAmount = usdyAmountsResults?.[index]?.result as bigint | undefined
const isStable = stableTokensResults?.[index]?.result as boolean | undefined
const weight = tokenWeightsResults?.[index]?.result as bigint | undefined
return {
address: addr as `0x${string}`,
symbol: symbol || `Token ${index}`,
name: name || `Unknown Token ${index}`,
decimals: TOKEN_DECIMALS.YT, // YT 代币默认 18 位
balance,
isWhitelisted,
usdyAmount,
isStable,
weight,
}
})
}, [poolTokenAddresses, tokenInfoResults, balanceResults, whitelistResults, usdyAmountsResults, stableTokensResults, tokenWeightsResults])
// 合并所有可用代币(用于管理员配置)
const allAvailableTokens = useMemo((): AvailableToken[] => {
const poolAddressSet = new Set(poolTokenAddresses?.map((a: string) => a.toLowerCase()) || [])
const factoryOnlyTokens: AvailableToken[] = []
if (factoryVaults && factoryVaults.length > 0) {
(factoryVaults as string[]).forEach((addr: string, index: number) => {
if (!poolAddressSet.has(addr.toLowerCase())) {
const symbol = factoryVaultInfoResults?.[index * 2]?.result as string | undefined
const name = factoryVaultInfoResults?.[index * 2 + 1]?.result as string | undefined
factoryOnlyTokens.push({
address: addr,
symbol: symbol || `Vault ${index}`,
name: name || `Factory Vault ${index}`,
source: 'factory',
})
}
})
}
const poolWithSource: AvailableToken[] = poolTokens.map(t => ({
address: t.address,
symbol: t.symbol,
name: t.name,
source: 'pool' as const,
isStable: t.isStable,
isWhitelisted: t.isWhitelisted,
}))
const usdcInList = [...poolWithSource, ...factoryOnlyTokens].some(
t => t.address.toLowerCase() === CONTRACTS.USDC.toLowerCase()
)
const result: AvailableToken[] = [...poolWithSource, ...factoryOnlyTokens]
if (!usdcInList) {
result.push({
address: CONTRACTS.USDC,
symbol: 'USDC',
name: 'USD Coin',
source: 'pool',
})
}
return result
}, [poolTokens, poolTokenAddresses, factoryVaults, factoryVaultInfoResults])
// ===== ytLP 代币信息 =====
const { data: ytLPBalance, refetch: refetchBalance } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
})
const { data: ytLPTotalSupply, refetch: refetchYtLPTotalSupply } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'totalSupply',
})
const { data: ytLPDecimals } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'decimals',
})
const { data: ytLPName } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'name',
})
const { data: ytLPSymbol } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'symbol',
})
const { data: ytLPPrice, refetch: refetchYtLPPrice } = useReadContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'getYtLPPrice',
})
// ===== 池子数据 =====
const { data: aumInUsdy, refetch: refetchAumInUsdy } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'getAumInUsdy',
args: [true],
})
const { data: cooldownDuration } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'cooldownDuration',
})
const { data: lastAddedAt } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'lastAddedAt',
args: address ? [address] : undefined,
})
const { data: accountValue, refetch: refetchAccountValue } = useReadContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'getAccountValue',
args: address ? [address] : undefined,
})
// ===== 管理状态 =====
const { data: emergencyMode, refetch: refetchEmergencyMode } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'emergencyMode',
})
const { data: swapEnabled, refetch: refetchSwapEnabled } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'isSwapEnabled',
})
const { data: poolPaused } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'paused',
})
// ===== 手续费配置 =====
const { data: swapFee } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'swapFee',
})
const { data: stableSwapFee } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'stableSwapFee',
})
const { data: taxBasisPoints } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'taxBasisPoints',
})
const { data: stableTaxBasisPoints } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'stableTaxBasisPoints',
})
const { data: dynamicFees } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'dynamicFees',
})
// ===== 管理员地址 =====
const { data: vaultGov } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'gov',
})
const { data: poolManagerGov } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'gov',
})
const { data: priceFeedGov } = useReadContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'gov',
})
const { data: usdcPriceSource } = useReadContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'usdcPriceSource',
})
// ===== USDY 信息 =====
const { data: usdyTotalSupply, refetch: refetchUsdySupply } = useReadContract({
address: CONTRACTS.USDY,
abi: USDY_ABI,
functionName: 'totalSupply',
})
const { data: poolValue, refetch: refetchPoolValue } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getPoolValue',
})
// 辅助函数:根据地址获取代币符号
const getTokenSymbol = (tokenAddress: string): string => {
const token = poolTokens.find(t => t.address.toLowerCase() === tokenAddress.toLowerCase())
return token?.symbol || 'Token'
}
return {
// 连接状态
address,
isConnected,
// 代币列表
poolTokens,
allAvailableTokens,
poolTokenAddresses,
// ytLP 数据
ytLPBalance,
ytLPTotalSupply,
ytLPDecimals,
ytLPName,
ytLPSymbol,
ytLPPrice,
// 池子数据
aumInUsdy,
cooldownDuration,
lastAddedAt,
accountValue,
poolValue,
usdyTotalSupply,
// 管理状态
emergencyMode,
swapEnabled,
poolPaused,
// 手续费
swapFee,
stableSwapFee,
taxBasisPoints,
stableTaxBasisPoints,
dynamicFees,
// 管理员地址
vaultGov,
poolManagerGov,
priceFeedGov,
usdcPriceSource,
// 辅助函数
getTokenSymbol,
// Refetch 函数
refetchBalance,
refetchTokenBalances,
refetchYtLPTotalSupply,
refetchYtLPPrice,
refetchAumInUsdy,
refetchAccountValue,
refetchUsdyAmounts,
refetchTokenWeights,
refetchEmergencyMode,
refetchSwapEnabled,
refetchUsdySupply,
refetchPoolValue,
refetchVaultWhitelist,
}
}

View File

@@ -0,0 +1,208 @@
/**
* LP 交易处理 Hook
* 集中管理所有交易逻辑
*/
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { useTransactions } from '@/context/TransactionContext'
import type { TransactionType } from '@/context/TransactionContext'
import { useToast } from '../Toast'
interface TransactionCallbacks {
onSuccess?: () => void
onError?: (error: string) => void
}
export function useLPTransactions(callbacks?: TransactionCallbacks) {
const { t } = useTranslation()
const { addTransaction, updateTransaction } = useTransactions()
const { showToast } = useToast()
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
// 使用 ref 存储 callbacks避免作为依赖项
const callbacksRef = useRef(callbacks)
callbacksRef.current = callbacks
const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract()
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
hash,
})
// 使用 ref 追踪已处理的状态,防止重复触发
const processedHashRef = useRef<string | null>(null)
const successProcessedRef = useRef(false)
const errorProcessedRef = useRef(false)
const writeErrorProcessedRef = useRef<Error | null>(null)
// 超时自动重置机制
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null
if (isPending && !hash) {
timeoutId = setTimeout(() => {
console.log('Transaction timeout, resetting state...')
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' })
pendingTxRef.current = null
}
showToast('error', t('toast.txTimeout') || 'Transaction timeout')
reset()
}, 30000)
}
return () => {
if (timeoutId) clearTimeout(timeoutId)
}
}, [isPending, hash, t, showToast, reset, updateTransaction])
// 处理交易提交
useEffect(() => {
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
// 防止重复处理
if (processedHashRef.current === hash) return
processedHashRef.current = hash
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
showToast('info', t('toast.txSubmitted'))
}
}, [hash, t, showToast, updateTransaction])
// 处理交易成功
useEffect(() => {
if (isSuccess && !successProcessedRef.current) {
successProcessedRef.current = true
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'success' })
showToast('success', t('toast.txSuccess'))
pendingTxRef.current = null
}
callbacksRef.current?.onSuccess?.()
reset()
// 重置标记
processedHashRef.current = null
successProcessedRef.current = false
}
}, [isSuccess, t, showToast, updateTransaction, reset])
// 处理交易失败
useEffect(() => {
if (isError && pendingTxRef.current && !errorProcessedRef.current) {
errorProcessedRef.current = true
const errorMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
showToast('error', t('toast.txFailed'))
pendingTxRef.current = null
callbacksRef.current?.onError?.(errorMsg)
reset()
processedHashRef.current = null
// 延迟重置
setTimeout(() => { errorProcessedRef.current = false }, 100)
}
}, [isError, txError, t, showToast, updateTransaction, reset])
// 处理写入错误
useEffect(() => {
if (writeError && writeErrorProcessedRef.current !== writeError) {
writeErrorProcessedRef.current = writeError
const errorMsg = parseError(writeError, t)
showToast('error', errorMsg)
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
pendingTxRef.current = null
}
callbacksRef.current?.onError?.(errorMsg)
reset()
processedHashRef.current = null
}
}, [writeError, t, showToast, updateTransaction, reset])
// 错误解析
const parseError = (error: any, t: any): string => {
let msg = 'Unknown error'
if (typeof error === 'string') {
msg = error
} else if (error?.shortMessage) {
msg = error.shortMessage
} else if (typeof error?.message === 'string') {
msg = error.message
} else if (error?.message) {
msg = JSON.stringify(error.message)
} else if (error) {
try { msg = JSON.stringify(error) } catch { msg = String(error) }
}
if (msg.includes('User rejected') || msg.includes('user rejected')) {
return t('toast.userRejected')
}
if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) {
return t('toast.insufficientBalance') + ' (Gas)'
}
if (msg.includes('CooldownNotPassed')) {
return t('lp.cooldownNotPassed')
}
if (msg.includes('InsufficientOutput')) {
return t('lp.insufficientOutput')
}
const match = msg.match(/error[:\s]+(\w+)/i)
if (match) return match[1]
return msg.slice(0, 100)
}
// 记录交易
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
const id = addTransaction({
type,
hash: '',
status: 'pending',
amount,
token,
})
pendingTxRef.current = { id, type, amount }
}
// 计算按钮是否应该禁用
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
return {
writeContract,
recordTx,
isProcessing,
isPending,
isConfirming,
hash,
reset,
}
}
export { parseError }
function parseError(error: any, t: any): string {
let msg = 'Unknown error'
if (typeof error === 'string') {
msg = error
} else if (error?.shortMessage) {
msg = error.shortMessage
} else if (typeof error?.message === 'string') {
msg = error.message
} else if (error?.message) {
msg = JSON.stringify(error.message)
} else if (error) {
try { msg = JSON.stringify(error) } catch { msg = String(error) }
}
if (msg.includes('User rejected') || msg.includes('user rejected')) {
return t('toast.userRejected')
}
if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) {
return t('toast.insufficientBalance') + ' (Gas)'
}
if (msg.includes('CooldownNotPassed')) {
return t('lp.cooldownNotPassed')
}
if (msg.includes('InsufficientOutput')) {
return t('lp.insufficientOutput')
}
const match = msg.match(/error[:\s]+(\w+)/i)
if (match) return match[1]
return msg.slice(0, 100)
}

View File

@@ -0,0 +1,40 @@
import { useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { setContractAddress } from '@/config/contractRegistry'
interface ContractEntry {
name: string
chain_id: number
address: string
}
async function fetchContracts(): Promise<ContractEntry[]> {
try {
const res = await fetch('/api/contracts')
if (!res.ok) return []
const data = await res.json()
return data.contracts ?? []
} catch {
return []
}
}
/**
* 从后端 /api/contracts 获取所有合约地址并写入 registry。
* 在 rootContainer 靠近根部调用一次即可,所有页面共享同一份缓存。
*/
export function useContractRegistry() {
const { data } = useQuery({
queryKey: ['contract-registry'],
queryFn: fetchContracts,
staleTime: 5 * 60 * 1000,
gcTime: 30 * 60 * 1000,
})
useEffect(() => {
if (!data) return
for (const c of data) {
if (c.address) setContractAddress(c.name, c.chain_id, c.address)
}
}, [data])
}

View File

@@ -0,0 +1,117 @@
import { useState, useEffect } from 'react'
export type TransactionType = 'mint' | 'burn' | 'buy' | 'sell' | 'approve' | 'create_vault' | 'update_price' | 'test' | 'addLiquidity' | 'removeLiquidity' | 'swap' | 'transfer'
export interface TransactionRecord {
id: string
type: TransactionType
hash: string
timestamp: number
status: 'pending' | 'success' | 'failed'
amount?: string
token?: string
vault?: string
error?: string
}
const STORAGE_KEY = 'yt_asset_tx_history'
const MAX_RECORDS = 50
export function useTransactionHistory() {
const [transactions, setTransactions] = useState<TransactionRecord[]>([])
// 从 localStorage 加载历史记录
useEffect(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored)
// 过滤并修复损坏的记录
const valid = Array.isArray(parsed)
? parsed
.filter((tx: any) => tx && typeof tx === 'object' && tx.id && typeof tx.id === 'string')
.map((tx: any) => ({
...tx,
// 确保所有字段都是正确类型
id: String(tx.id),
type: String(tx.type || 'test'),
hash: (typeof tx.hash === 'string' && tx.hash.startsWith('0x')) ? tx.hash : '',
timestamp: Number(tx.timestamp) || Date.now(),
status: ['pending', 'success', 'failed'].includes(tx.status) ? tx.status : 'failed',
amount: tx.amount ? String(tx.amount) : undefined,
token: tx.token ? String(tx.token) : undefined,
vault: tx.vault ? String(tx.vault) : undefined,
error: tx.error ? (typeof tx.error === 'string' ? tx.error : JSON.stringify(tx.error)) : undefined,
}))
: []
setTransactions(valid)
}
} catch (e) {
console.error('Failed to load transaction history:', e)
// 清除损坏的数据
localStorage.removeItem(STORAGE_KEY)
}
}, [])
// 保存到 localStorage
const saveToStorage = (records: TransactionRecord[]) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(records.slice(0, MAX_RECORDS)))
} catch (e) {
console.error('Failed to save transaction history:', e)
}
}
// 添加新交易
const addTransaction = (tx: Omit<TransactionRecord, 'id' | 'timestamp'>) => {
const newTx: TransactionRecord = {
type: tx.type,
hash: (typeof tx.hash === 'string' && tx.hash.startsWith('0x')) ? tx.hash : '',
status: tx.status,
amount: tx.amount ? String(tx.amount) : undefined,
token: tx.token ? String(tx.token) : undefined,
vault: tx.vault ? String(tx.vault) : undefined,
error: tx.error ? (typeof tx.error === 'string' ? tx.error : JSON.stringify(tx.error)) : undefined,
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
}
setTransactions(prev => {
const updated = [newTx, ...prev].slice(0, MAX_RECORDS)
saveToStorage(updated)
return updated
})
return newTx.id
}
// 更新交易状态
const updateTransaction = (id: string, updates: Partial<TransactionRecord>) => {
// 确保 hash 是字符串
if (updates.hash && typeof updates.hash !== 'string') {
updates.hash = ''
}
// 确保 error 是字符串
if (updates.error && typeof updates.error !== 'string') {
updates.error = JSON.stringify(updates.error)
}
setTransactions(prev => {
const updated = prev.map(tx =>
tx.id === id ? { ...tx, ...updates } : tx
)
saveToStorage(updated)
return updated
})
}
// 清空历史记录
const clearHistory = () => {
setTransactions([])
localStorage.removeItem(STORAGE_KEY)
}
return {
transactions,
addTransaction,
updateTransaction,
clearHistory,
}
}

View File

@@ -0,0 +1,7 @@
import { Skeleton } from 'antd';
const Loading: React.FC = () => (
<Skeleton style={{ margin: '24px 40px', height: '60vh' }} active />
);
export default Loading;

View File

@@ -0,0 +1,25 @@
import component from './bn-BD/component';
import globalHeader from './bn-BD/globalHeader';
import menu from './bn-BD/menu';
import pages from './bn-BD/pages';
import pwa from './bn-BD/pwa';
import settingDrawer from './bn-BD/settingDrawer';
import settings from './bn-BD/settings';
export default {
'navBar.lang': 'ভাষা',
'layout.user.link.help': 'সহায়তা',
'layout.user.link.privacy': 'গোপনীয়তা',
'layout.user.link.terms': 'শর্তাদি',
'app.preview.down.block': 'আপনার স্থানীয় প্রকল্পে এই পৃষ্ঠাটি ডাউনলোড করুন',
'app.welcome.link.fetch-blocks': 'সমস্ত ব্লক পান',
'app.welcome.link.block-list':
'`block` ডেভেলপমেন্ট এর উপর ভিত্তি করে দ্রুত স্ট্যান্ডার্ড, পৃষ্ঠাসমূহ তৈরি করুন।',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'বিস্তৃত',
'component.tagSelect.collapse': 'সঙ্কুচিত',
'component.tagSelect.all': 'সব',
};

View File

@@ -0,0 +1,17 @@
export default {
'component.globalHeader.search': 'অনুসন্ধান করুন',
'component.globalHeader.search.example1': 'অনুসন্ধান উদাহরণ ১',
'component.globalHeader.search.example2': 'অনুসন্ধান উদাহরণ ২',
'component.globalHeader.search.example3': 'অনুসন্ধান উদাহরণ ৩',
'component.globalHeader.help': 'সহায়তা',
'component.globalHeader.notification': 'বিজ্ঞপ্তি',
'component.globalHeader.notification.empty': 'আপনি সমস্ত বিজ্ঞপ্তি দেখেছেন।',
'component.globalHeader.message': 'বার্তা',
'component.globalHeader.message.empty': 'আপনি সমস্ত বার্তা দেখেছেন।',
'component.globalHeader.event': 'ঘটনা',
'component.globalHeader.event.empty': 'আপনি সমস্ত ইভেন্ট দেখেছেন।',
'component.noticeIcon.clear': 'সাফ',
'component.noticeIcon.cleared': 'সাফ করা হয়েছে',
'component.noticeIcon.empty': 'বিজ্ঞপ্তি নেই',
'component.noticeIcon.view-more': 'আরো দেখুন',
};

View File

@@ -0,0 +1,52 @@
export default {
'menu.welcome': 'স্বাগতম',
'menu.more-blocks': 'আরও ব্লক',
'menu.home': 'নীড়',
'menu.admin': 'অ্যাডমিন',
'menu.admin.sub-page': 'উপ-পৃষ্ঠা',
'menu.login': 'প্রবেশ',
'menu.register': 'নিবন্ধন',
'menu.register-result': 'নিবন্ধনে ফলাফল',
'menu.dashboard': 'ড্যাশবোর্ড',
'menu.dashboard.analysis': 'বিশ্লেষণ',
'menu.dashboard.monitor': 'নিরীক্ষণ',
'menu.dashboard.workplace': 'কর্মক্ষেত্র',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'ফর্ম',
'menu.form.basic-form': 'বেসিক ফর্ম',
'menu.form.step-form': 'পদক্ষেপ ফর্ম',
'menu.form.step-form.info': 'পদক্ষেপ ফর্ম (স্থানান্তর তথ্য লিখুন)',
'menu.form.step-form.confirm': 'পদক্ষেপ ফর্ম (স্থানান্তর তথ্য নিশ্চিত করুন)',
'menu.form.step-form.result': 'পদক্ষেপ ফর্ম (সমাপ্ত)',
'menu.form.advanced-form': 'উন্নত ফর্ম',
'menu.list': 'তালিকা',
'menu.list.table-list': 'অনুসন্ধানের টেবিল',
'menu.list.basic-list': 'বেসিক তালিকা',
'menu.list.card-list': 'কার্ডের তালিকা',
'menu.list.search-list': 'অনুসন্ধানের তালিকা',
'menu.list.search-list.articles': 'অনুসন্ধানের তালিকা (নিবন্ধসমূহ)',
'menu.list.search-list.projects': 'অনুসন্ধানের তালিকা (প্রকল্পগুলি)',
'menu.list.search-list.applications': 'অনুসন্ধানের তালিকা (অ্যাপ্লিকেশন)',
'menu.profile': 'প্রোফাইল',
'menu.profile.basic': 'বেসিক প্রোফাইল',
'menu.profile.advanced': 'উন্নত প্রোফাইল',
'menu.result': 'ফলাফল',
'menu.result.success': 'সাফল্য',
'menu.result.fail': 'ব্যর্থ',
'menu.exception': 'ব্যতিক্রম',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'ট্রিগার',
'menu.account': 'হিসাব',
'menu.account.center': 'অ্যাকাউন্ট কেন্দ্র',
'menu.account.settings': 'অ্যাকাউন্ট সেটিংস',
'menu.account.trigger': 'ট্রিগার ত্রুটি',
'menu.account.logout': 'প্রস্থান',
'menu.editor': 'গ্রাফিক সম্পাদক',
'menu.editor.flow': 'ফ্লো এডিটর',
'menu.editor.mind': 'মাইন্ড এডিটর',
'menu.editor.koni': 'কোনি সম্পাদক',
};

View File

@@ -0,0 +1,92 @@
export default {
'pages.layouts.userLayout.title':
'পিঁপড়া ডিজাইন হচ্ছে সিহু জেলার সবচেয়ে প্রভাবশালী ওয়েব ডিজাইনের স্পেসিফিকেশন',
'pages.login.accountLogin.tab': 'অ্যাকাউন্টে লগইন',
'pages.login.accountLogin.errorMessage':
'ভুল ব্যবহারকারীর নাম/পাসওয়ার্ড(admin/ant.design)',
'pages.login.failure': 'লগইন ব্যর্থ হয়েছে। আবার চেষ্টা করুন!',
'pages.login.success': 'সফল লগইন!',
'pages.login.username.placeholder': 'ব্যবহারকারীর নাম: admin or user',
'pages.login.username.required': 'আপনার ব্যবহারকারীর নাম ইনপুট করুন!',
'pages.login.password.placeholder': 'পাসওয়ার্ড লিখুন',
'pages.login.password.required': 'আপনার পাসওয়ার্ড ইনপুট করুন!',
'pages.login.phoneLogin.tab': 'ফোন লগইন',
'pages.login.phoneLogin.errorMessage': 'যাচাইকরণ কোড ত্রুটি',
'pages.login.phoneNumber.placeholder': 'ফোন নম্বর',
'pages.login.phoneNumber.required': 'আপনার ফোন নম্বর ইনপুট করুন!',
'pages.login.phoneNumber.invalid': 'ফোন নম্বরটি সঠিক নয়!',
'pages.login.captcha.placeholder': 'যাচাইকরণের কোড',
'pages.login.captcha.required': 'দয়া করে ভেরিফিকেশন কোডটি ইনপুট করুন!',
'pages.login.phoneLogin.getVerificationCode': 'কোড পান',
'pages.getCaptchaSecondText': 'সেকেন্ড',
'pages.login.rememberMe': 'আমাকে মনে রাখুন',
'pages.login.forgotPassword': 'পাসওয়ার্ড ভুলে গেছেন?',
'pages.login.submit': 'প্রবেশ করুন',
'pages.login.loginWith': 'লগইন করতে পারেন:',
'pages.login.registerAccount': 'অ্যাকাউন্ট নিবন্ধন করুন',
'pages.welcome.link': 'স্বাগতম',
'pages.welcome.alertMessage': 'দ্রুত এবং শক্তিশালী ভারী শুল্ক উপাদান প্রকাশ করা হয়েছে।',
'pages.404.subTitle': 'দুঃখিত, আপনি যে পৃষ্ঠাটি দেখতে চান তা বিদ্যমান নেই।',
'pages.404.buttonText': 'প্রধান পাতায় ফিরে যান',
'pages.admin.subPage.title': 'এই পৃষ্ঠাটি কেবল অ্যাডমিন দ্বারা দেখা যাবে',
'pages.admin.subPage.alertMessage':
'UMI UI এখন প্রকাশিত হয়েছে, অভিজ্ঞতা শুরু করতে npm run ui ব্যবহার করতে স্বাগতম।',
'pages.searchTable.createForm.newRule': 'নতুন বিধি',
'pages.searchTable.updateForm.ruleConfig': 'বিধি কনফিগারেশন',
'pages.searchTable.updateForm.basicConfig': 'মৌলিক তথ্য',
'pages.searchTable.updateForm.ruleName.nameLabel': 'বিধি নাম',
'pages.searchTable.updateForm.ruleName.nameRules': 'বিধির নাম লিখুন!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'বিধির বিবরণ',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder':
'কমপক্ষে পাঁচটি অক্ষর লিখুন',
'pages.searchTable.updateForm.ruleDesc.descRules':
'কমপক্ষে পাঁচটি অক্ষরের একটি বিধান বিবরণ লিখুন!',
'pages.searchTable.updateForm.ruleProps.title': 'বৈশিষ্ট্য কনফিগার করুন',
'pages.searchTable.updateForm.object': 'নিরীক্ষণ অবজেক্ট',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'বিধি টেম্পলেট',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'বিধি প্রকার',
'pages.searchTable.updateForm.schedulingPeriod.title': 'সময়সূচী নির্ধারণ করুন',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'শুরুর সময়',
'pages.searchTable.updateForm.schedulingPeriod.timeRules':
'একটি শুরুর সময় চয়ন করুন!',
'pages.searchTable.titleDesc': 'বর্ণনা',
'pages.searchTable.ruleName': 'বিধি নাম প্রয়োজন',
'pages.searchTable.titleCallNo': 'পরিষেবা কল সংখ্যা',
'pages.searchTable.titleStatus': 'অবস্থা',
'pages.searchTable.nameStatus.default': 'ডিফল্ট',
'pages.searchTable.nameStatus.running': 'চলমান',
'pages.searchTable.nameStatus.online': 'অনলাইন',
'pages.searchTable.nameStatus.abnormal': 'অস্বাভাবিক',
'pages.searchTable.titleUpdatedAt': 'সর্বশেষ নির্ধারিত',
'pages.searchTable.exception': 'ব্যতিক্রম জন্য কারণ লিখুন!',
'pages.searchTable.titleOption': 'অপশন',
'pages.searchTable.config': 'কনফিগারেশন',
'pages.searchTable.subscribeAlert': 'সতর্কতা সাবস্ক্রাইব করুন',
'pages.searchTable.title': 'ইনকয়েরি ফরম',
'pages.searchTable.new': 'নতুন',
'pages.searchTable.chosen': 'নির্বাচিত',
'pages.searchTable.item': 'আইটেম',
'pages.searchTable.totalServiceCalls': 'পরিষেবা কলগুলির মোট সংখ্যা',
'pages.searchTable.tenThousand': '000',
'pages.searchTable.batchDeletion': 'একসাখে ডিলিট',
'pages.searchTable.batchApproval': 'একসাখে অনুমোদন',
// Admin — সিস্টেম কনট্র্যাক্ট
'pages.admin.systemContracts.title': 'সিস্টেম কনট্র্যাক্ট',
'pages.admin.systemContracts.newContract': 'নতুন কনট্র্যাক্ট',
'pages.admin.systemContracts.createTitle': 'নতুন কনট্র্যাক্ট',
'pages.admin.systemContracts.editTitle': 'কনট্র্যাক্ট সম্পাদনা',
'pages.admin.systemContracts.name': 'নাম',
'pages.admin.systemContracts.chain': 'নেটওয়ার্ক',
'pages.admin.systemContracts.address': 'ঠিকানা',
'pages.admin.systemContracts.deployBlock': 'ডিপ্লয় ব্লক',
'pages.admin.systemContracts.allChains': 'সব নেটওয়ার্ক',
'pages.admin.systemContracts.copyAddress': 'ঠিকানা কপি করুন',
'pages.admin.systemContracts.copied': 'কপি হয়েছে',
'pages.admin.systemContracts.deleteTitle': 'এই কনট্র্যাক্টটি মুছবেন?',
'pages.admin.systemContracts.namePlaceholder': 'যেমন lendingProxy, YTLPToken',
'pages.admin.systemContracts.blockPlaceholder': 'ব্লক নম্বর (ঐচ্ছিক)',
'pages.admin.systemContracts.descPlaceholder': 'ঐচ্ছিক বিবরণ',
'pages.admin.systemContracts.nameRequired': 'নাম আবশ্যক',
'pages.admin.systemContracts.addressRequired': 'ঠিকানা আবশ্যক',
};

View File

@@ -0,0 +1,7 @@
export default {
'app.pwa.offline': 'আপনি এখন অফলাইন',
'app.pwa.serviceworker.updated': 'নতুন সামগ্রী উপলব্ধ',
'app.pwa.serviceworker.updated.hint':
'বর্তমান পৃষ্ঠাটি পুনরায় লোড করতে দয়া করে "রিফ্রেশ" বোতাম টিপুন',
'app.pwa.serviceworker.updated.ok': 'রিফ্রেশ',
};

View File

@@ -0,0 +1,32 @@
export default {
'app.setting.pagestyle': 'পৃষ্ঠা স্টাইল সেটিং',
'app.setting.pagestyle.dark': 'ডার্ক স্টাইল',
'app.setting.pagestyle.light': 'লাইট স্টাইল',
'app.setting.content-width': 'সামগ্রীর প্রস্থ',
'app.setting.content-width.fixed': 'স্থির',
'app.setting.content-width.fluid': 'প্রবাহী',
'app.setting.themecolor': 'থিম রঙ',
'app.setting.themecolor.dust': 'ডাস্ট রেড',
'app.setting.themecolor.volcano': 'আগ্নেয়গিরি',
'app.setting.themecolor.sunset': 'সানসেট কমলা',
'app.setting.themecolor.cyan': 'সবুজাভ নীল',
'app.setting.themecolor.green': 'পোলার সবুজ',
'app.setting.themecolor.daybreak': 'দিবস ব্রেক ব্লু (ডিফল্ট)',
'app.setting.themecolor.geekblue': 'গিক আঠালো',
'app.setting.themecolor.purple': 'গোল্ডেন বেগুনি',
'app.setting.navigationmode': 'নেভিগেশন মোড',
'app.setting.sidemenu': 'সাইড মেনু লেআউট',
'app.setting.topmenu': 'টপ মেনু লেআউট',
'app.setting.fixedheader': 'স্থির হেডার',
'app.setting.fixedsidebar': 'স্থির সাইডবার',
'app.setting.fixedsidebar.hint': 'সাইড মেনু বিন্যাসে কাজ করে',
'app.setting.hideheader': 'স্ক্রোল করার সময় হেডার লুকানো',
'app.setting.hideheader.hint': 'লুকানো হেডার সক্ষম থাকলে কাজ করে',
'app.setting.othersettings': 'অন্যান্য সেটিংস্',
'app.setting.weakmode': 'দুর্বল মোড',
'app.setting.copy': 'সেটিং কপি করুন',
'app.setting.copyinfo':
'সাফল্যের অনুলিপি করুন - প্রতিস্থাপন করুন: src/models/setting.js',
'app.setting.production.hint':
'কেবল বিকাশের পরিবেশে প্যানেল শো সেট করা হচ্ছে, দয়া করে ম্যানুয়ালি সংশোধন করুন',
};

View File

@@ -0,0 +1,60 @@
export default {
'app.settings.menuMap.basic': 'মৌলিক বৈশিষ্ট্যসহ',
'app.settings.menuMap.security': 'নিরাপত্তা বিন্যাস',
'app.settings.menuMap.binding': 'অ্যাকাউন্ট বাঁধাই',
'app.settings.menuMap.notification': 'নতুন বার্তা বিজ্ঞপ্তি',
'app.settings.basic.avatar': 'অবতার',
'app.settings.basic.change-avatar': 'অবতার পরিবর্তন করুন',
'app.settings.basic.email': 'ইমেইল',
'app.settings.basic.email-message': 'আপনার ইমেইল ইনপুট করুন!',
'app.settings.basic.nickname': 'ডাক নাম',
'app.settings.basic.nickname-message': 'আপনার ডাকনামটি ইনপুট করুন!',
'app.settings.basic.profile': 'ব্যক্তিগত প্রোফাইল',
'app.settings.basic.profile-message': 'আপনার ব্যক্তিগত প্রোফাইল ইনপুট করুন!',
'app.settings.basic.profile-placeholder': 'নিজের সাথে সংক্ষিপ্ত পরিচয়',
'app.settings.basic.country': 'দেশ/অঞ্চল',
'app.settings.basic.country-message': 'আপনার দেশ ইনপুট করুন!',
'app.settings.basic.geographic': 'প্রদেশ বা শহর',
'app.settings.basic.geographic-message': 'আপনার ভৌগলিক তথ্য ইনপুট করুন!',
'app.settings.basic.address': 'রাস্তার ঠিকানা',
'app.settings.basic.address-message': 'দয়া করে আপনার ঠিকানা ইনপুট করুন!',
'app.settings.basic.phone': 'ফোন নম্বর',
'app.settings.basic.phone-message': 'আপনার ফোন ইনপুট করুন!',
'app.settings.basic.update': 'তথ্য হালনাগাদ',
'app.settings.security.strong': 'শক্তিশালী',
'app.settings.security.medium': 'মধ্যম',
'app.settings.security.weak': 'দুর্বল',
'app.settings.security.password': 'অ্যাকাউন্টের পাসওয়ার্ড',
'app.settings.security.password-description': 'বর্তমান পাসওয়ার্ড শক্তি',
'app.settings.security.phone': 'সুরক্ষা ফোন',
'app.settings.security.phone-description': 'আবদ্ধ ফোন',
'app.settings.security.question': 'নিরাপত্তা প্রশ্ন',
'app.settings.security.question-description':
'সুরক্ষা প্রশ্ন সেট করা নেই, এবং সুরক্ষা নীতি কার্যকরভাবে অ্যাকাউন্ট সুরক্ষা রক্ষা করতে পারে',
'app.settings.security.email': 'ব্যাকআপ ইমেইল',
'app.settings.security.email-description': 'বাউন্ড ইমেইল',
'app.settings.security.mfa': 'MFA ডিভাইস',
'app.settings.security.mfa-description':
"আনবাউন্ড এমএফএ ডিভাইস, বাঁধাইয়ের পরে, দু'বার নিশ্চিত করা যায়",
'app.settings.security.modify': 'পরিবর্তন করুন',
'app.settings.security.set': 'সেট',
'app.settings.security.bind': 'বাঁধাই',
'app.settings.binding.taobao': 'বাঁধাই তাওবাও',
'app.settings.binding.taobao-description': 'বর্তমানে আনবাউন্ড তাওবাও অ্যাকাউন্ট',
'app.settings.binding.alipay': 'বাইন্ডিং আলিপে',
'app.settings.binding.alipay-description': 'বর্তমানে আনবাউন্ড আলিপে অ্যাকাউন্ট',
'app.settings.binding.dingding': 'বাঁধাই ডিঙ্গটালক',
'app.settings.binding.dingding-description': 'বর্তমানে আনবাউন্ড ডিঙ্গটাল অ্যাকাউন্ট',
'app.settings.binding.bind': 'বাঁধাই',
'app.settings.notification.password': 'অ্যাকাউন্টের পাসওয়ার্ড',
'app.settings.notification.password-description':
'অন্যান্য ব্যবহারকারীর বার্তাগুলি স্টেশন চিঠি আকারে জানানো হবে',
'app.settings.notification.messages': 'সিস্টেম বার্তা',
'app.settings.notification.messages-description':
'সিস্টেম বার্তাগুলি স্টেশন চিঠির আকারে জানানো হবে',
'app.settings.notification.todo': 'করণীয় বিজ্ঞপ্তি',
'app.settings.notification.todo-description':
'করণীয় তালিকাটি স্টেশন থেকে চিঠি আকারে জানানো হবে',
'app.settings.open': 'খোলা',
'app.settings.close': 'বন্ধ',
};

View File

@@ -0,0 +1,29 @@
import accountCenter from './en-US/accountCenter';
import component from './en-US/component';
import globalHeader from './en-US/globalHeader';
import menu from './en-US/menu';
import notification from './en-US/notification';
import pages from './en-US/pages';
import pwa from './en-US/pwa';
import settingDrawer from './en-US/settingDrawer';
import settings from './en-US/settings';
export default {
'navBar.lang': 'Languages',
'layout.user.link.help': 'Help',
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.preview.down.block': 'Download this page to your local project',
'app.welcome.link.fetch-blocks': 'Get all block',
'app.welcome.link.block-list':
'Quickly build standard, pages based on `block` development',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
...notification,
...accountCenter,
};

View File

@@ -0,0 +1,103 @@
export default {
// User info
'account.center.userId': 'User ID',
'account.center.username': 'Username',
'account.center.status': 'Status',
'account.center.status.active': 'Active',
'account.center.roles': 'Roles',
'account.center.lastLogin': 'Last Login',
'account.center.createdAt': 'Registered',
'account.center.copyFailed': 'Copy failed',
// Tab
'account.center.tab.overview': 'Overview',
'account.center.tab.team': 'My Team',
'account.center.tab.records': 'Records',
// Referral
'account.center.referral.title': 'Referral',
'account.center.referral.myCode': 'My Referral Code',
'account.center.referral.copy': 'Copy',
'account.center.referral.copyCode': 'Copy referral code',
'account.center.referral.copyLink': 'Copy referral link',
'account.center.referral.copyLinkBtn': 'Copy Link',
'account.center.referral.copied': 'Referral code copied',
'account.center.referral.linkCopied': 'Referral link copied',
'account.center.referral.notBound': 'You have not bound a referral code',
'account.center.referral.bindNow': 'Bind Now',
'account.center.referral.boundTo': 'Bound referral code',
'account.center.referral.bindSuccess': 'Referral code bound successfully',
'account.center.referral.bindFailed': 'Binding failed, please retry',
'account.center.referral.invalidCode': 'Invalid or non-existent referral code',
'account.center.referral.alreadyBound': 'You have already bound a referral code',
'account.center.referral.selfNotAllowed': 'Cannot bind your own referral code',
// Bind modal
'account.center.referral.bindModal.title': 'Bind Referral Code',
'account.center.referral.bindModal.label': 'Referral Code',
'account.center.referral.bindModal.placeholder': 'Enter 6-character referral code',
'account.center.referral.bindModal.required': 'Please enter referral code',
'account.center.referral.bindModal.format': 'Referral code must be 6 alphanumeric characters',
'account.center.referral.bindModal.cancel': 'Cancel',
'account.center.referral.bindModal.submit': 'Confirm',
// Downlines
'account.center.downlines.title': 'Direct Referrals',
'account.center.downlines.direct': 'Direct Referrals',
'account.center.downlines.userId': 'User ID',
'account.center.downlines.email': 'Email',
'account.center.downlines.joinedAt': 'Joined At',
'account.center.downlines.totalCount': 'Total {total} members',
'account.center.downlines.empty': 'No direct referrals yet',
// Level info
'account.center.level.activated': 'Activated',
'account.center.level.notActivated': 'Not Activated',
'account.center.level.activateHint': 'You need to activate at least one card to participate in referral rewards',
'account.center.level.activatedCards': 'Activated Cards',
'account.center.level.myActivatedCards': 'My Activated Cards',
'account.center.level.myCardsHint': 'Number of cards you have activated. Activate a card to unlock referral rewards.',
'account.center.level.upgradeToNext': 'Upgrade to {level}',
'account.center.level.directCount': 'Direct Referrals',
'account.center.level.teamCards': 'Team Cards',
// Earnings
'account.center.earnings.total': 'Total Earnings',
'account.center.earnings.pending': 'Pending',
'account.center.earnings.claimed': 'Claimed',
'account.center.earnings.frozen': 'Frozen',
'account.center.earnings.claim': 'Claim',
'account.center.earnings.claimSuccess': 'Successfully claimed ${amount}',
'account.center.earnings.claimFailed': 'Claim failed',
'account.center.earnings.noPending': 'No pending earnings to claim',
'account.center.earnings.breakdown': 'Earnings Breakdown',
'account.center.earnings.direct': 'Direct Rewards',
'account.center.earnings.levelDiff': 'Level Diff Rewards',
'account.center.earnings.teamBonus': 'Team Bonus',
// Commission records
'account.center.commission.records': 'Commission Records',
'account.center.commission.column.time': 'Time',
'account.center.commission.column.rewardType': 'Reward Type',
'account.center.commission.column.sourceType': 'Source',
'account.center.commission.column.amount': 'Amount',
'account.center.commission.column.baseAmount': 'Base Amount',
'account.center.commission.column.rate': 'Rate',
'account.center.commission.column.status': 'Status',
// Reward types
'account.center.commission.type.direct': 'Direct',
'account.center.commission.type.levelDiff': 'Level Diff',
'account.center.commission.type.teamBonus': 'Team Bonus',
// Source types
'account.center.commission.source.cardActivated': 'Card Activated',
'account.center.commission.source.rechargeFee': 'Recharge Fee',
'account.center.commission.source.withdrawal': 'Withdrawal',
// Status
'account.center.commission.status.pending': 'Pending',
'account.center.commission.status.frozen': 'Frozen',
'account.center.commission.status.settled': 'Settled',
'account.center.commission.status.claimed': 'Claimed',
};

View File

@@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'Expand',
'component.tagSelect.collapse': 'Collapse',
'component.tagSelect.all': 'All',
};

View File

@@ -0,0 +1,18 @@
export default {
'component.globalHeader.search': 'Search',
'component.globalHeader.search.example1': 'Search example 1',
'component.globalHeader.search.example2': 'Search example 2',
'component.globalHeader.search.example3': 'Search example 3',
'component.globalHeader.help': 'Help',
'component.globalHeader.notification': 'Notification',
'component.globalHeader.notification.empty':
'You have viewed all notifications.',
'component.globalHeader.message': 'Message',
'component.globalHeader.message.empty': 'You have viewed all messsages.',
'component.globalHeader.event': 'Event',
'component.globalHeader.event.empty': 'You have viewed all events.',
'component.noticeIcon.clear': 'Clear',
'component.noticeIcon.cleared': 'Cleared',
'component.noticeIcon.empty': 'No notifications',
'component.noticeIcon.view-more': 'View more',
};

View File

@@ -0,0 +1,112 @@
export default {
'menu.welcome': 'Welcome',
'menu.more-blocks': 'More Blocks',
'menu.home': 'Home',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Page',
// Dashboard
'menu.admin.dashboard': 'Dashboard',
// User Management
'menu.admin.user-list': 'User List',
'menu.admin.kyc': 'KYC Management',
'menu.admin.user-detail': 'User Detail',
'menu.admin.user-keys': 'API Key Management',
// VCC Management
'menu.admin.vcc-products': 'Products',
'menu.admin.vcc-applications': 'Applications',
'menu.admin.vcc-cards': 'Cards',
// Wallet Management
'menu.admin.wallet-records': 'Transaction Records',
'menu.admin.wallet-history': 'History',
'menu.admin.withdraw-config': 'Withdraw Config',
// Swap Management
'menu.admin.swap-manage': 'Swap Config',
'menu.admin.swap-orders': 'Swap Orders',
// Distribution Management
'menu.admin.commission': 'Commission',
// System Management
'menu.admin.roles': 'Roles',
'menu.admin.operation-logs': 'Operation Logs',
// Legacy menu (for compatibility)
'menu.admin.user-manage': 'User Management',
'menu.admin.role-manage': 'Roles & Permissions',
'menu.admin.kyc-manage': 'KYC Management',
'menu.admin.card-manage': 'Card Management',
'menu.admin.card-application': 'Card Applications',
'menu.admin.user-cards': 'User Cards',
'menu.admin.commission-manage': 'Commission Management',
'menu.login': 'Login',
'menu.register': 'Register',
'menu.register-result': 'Register Result',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Analysis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Form',
'menu.form.basic-form': 'Basic Form',
'menu.form.step-form': 'Step Form',
'menu.form.step-form.info': 'Step Form(write transfer information)',
'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
'menu.form.step-form.result': 'Step Form(finished)',
'menu.form.advanced-form': 'Advanced Form',
'menu.list': 'List',
'menu.list.table-list': 'Search Table',
'menu.list.basic-list': 'Basic List',
'menu.list.card-list': 'Card List',
'menu.list.search-list': 'Search List',
'menu.list.search-list.articles': 'Search List(articles)',
'menu.list.search-list.projects': 'Search List(projects)',
'menu.list.search-list.applications': 'Search List(applications)',
'menu.profile': 'Profile',
'menu.profile.basic': 'Basic Profile',
'menu.profile.advanced': 'Advanced Profile',
'menu.result': 'Result',
'menu.result.success': 'Success',
'menu.result.fail': 'Fail',
'menu.exception': 'Exception',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Trigger',
'menu.wallet': 'Wallet',
'menu.wallet.records': 'Transaction Records',
'menu.wallet.history': 'History',
'menu.account': 'Account',
'menu.account.center': 'Referral Center',
'menu.account.settings': 'Account Settings',
'menu.account.card-center': 'Card Center',
'menu.account.assets': 'My Assets',
'menu.account.my-cards': 'Card Application',
'menu.account.user-cards': 'My Cards',
'menu.account.trigger': 'Trigger Error',
'menu.account.logout': 'Logout',
'menu.editor': 'Graphic Editor',
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
// Web3 Features
'menu.vault': 'Vault',
'menu.vault.trade': 'Vault Trading',
'menu.vault.usdc': 'USDC Management',
'menu.lending': 'Lending',
'menu.lending.user': 'Lending Config',
'menu.lp': 'Liquidity Pool',
'menu.lp.pool': 'Pool Management',
'menu.statistics': 'Statistics',
'menu.statistics.holders': 'Holders',
'menu.admin.factory': 'Vault Factory',
'menu.admin.lending': 'Lending Config',
'menu.admin.assets': 'Assets',
'menu.admin.asset-custody': 'Asset Custody',
'menu.admin.asset-audit-reports': 'Audit Reports',
'menu.admin.points-rules': 'Points Rules',
'menu.admin.seasons': 'Seasons',
'menu.admin.vip-tiers': 'VIP Tiers',
'menu.admin.users': 'Users',
'menu.admin.invite-codes': 'Invite Codes',
'menu.admin.product-links': 'Product Links',
'menu.admin.system-contracts': 'System Contracts',
};

View File

@@ -0,0 +1,22 @@
export default {
'notification.title': 'Notifications',
'notification.empty': 'No notifications',
'notification.empty.unread': 'No unread notifications',
'notification.markAllRead': 'Mark all as read',
'notification.markAllRead.success': 'All marked as read',
'notification.markAllRead.failed': 'Failed to mark, please retry',
'notification.markRead': 'Mark as read',
'notification.markRead.success': 'Marked as read',
'notification.markRead.failed': 'Failed to mark, please retry',
'notification.tab.all': 'All',
'notification.type.system': 'System',
'notification.type.transaction': 'Transaction',
'notification.type.marketing': 'Marketing',
'notification.time.now': 'Just now',
'notification.time.minutes': '{count} minutes ago',
'notification.time.hours': '{count} hours ago',
'notification.time.days': '{count} days ago',
'notification.filter.label': 'Filter',
'notification.filter.all': 'All',
'notification.filter.unreadOnly': 'Unread',
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
export default {
'app.pwa.offline': 'You are offline now',
'app.pwa.serviceworker.updated': 'New content is available',
'app.pwa.serviceworker.updated.hint':
'Please press the "Refresh" button to reload current page',
'app.pwa.serviceworker.updated.ok': 'Refresh',
};

View File

@@ -0,0 +1,32 @@
export default {
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Content Width',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Navigation Mode',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
'app.setting.hideheader': 'Hidden Header when scrolling',
'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Color Blind Friendly Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.copyinfo':
'copy success, please replace defaultSettings in src/models/setting.js',
'app.setting.production.hint':
'Setting panel shows in development environment only, please manually modify',
};

View File

@@ -0,0 +1,61 @@
export default {
'app.settings.menuMap.basic': 'Basic Settings',
'app.settings.menuMap.security': 'Security Settings',
'app.settings.menuMap.binding': 'Account Binding',
'app.settings.menuMap.notification': 'New Message Notification',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Change avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Please input your email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Please input your Nickname!',
'app.settings.basic.profile': 'Personal profile',
'app.settings.basic.profile-message': 'Please input your personal profile!',
'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
'app.settings.basic.country': 'Country/Region',
'app.settings.basic.country-message': 'Please input your country!',
'app.settings.basic.geographic': 'Province or city',
'app.settings.basic.geographic-message': 'Please input your geographic info!',
'app.settings.basic.address': 'Street Address',
'app.settings.basic.address-message': 'Please input your address!',
'app.settings.basic.phone': 'Phone Number',
'app.settings.basic.phone-message': 'Please input your phone!',
'app.settings.basic.update': 'Update Information',
'app.settings.security.strong': 'Strong',
'app.settings.security.medium': 'Medium',
'app.settings.security.weak': 'Weak',
'app.settings.security.password': 'Account Password',
'app.settings.security.password-description': 'Current password strength',
'app.settings.security.phone': 'Security Phone',
'app.settings.security.phone-description': 'Bound phone',
'app.settings.security.question': 'Security Question',
'app.settings.security.question-description':
'The security question is not set, and the security policy can effectively protect the account security',
'app.settings.security.email': 'Backup Email',
'app.settings.security.email-description': 'Bound Email',
'app.settings.security.mfa': 'MFA Device',
'app.settings.security.mfa-description':
'Unbound MFA device, after binding, can be confirmed twice',
'app.settings.security.modify': 'Modify',
'app.settings.security.set': 'Set',
'app.settings.security.bind': 'Bind',
'app.settings.binding.taobao': 'Binding Taobao',
'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
'app.settings.binding.alipay': 'Binding Alipay',
'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
'app.settings.binding.dingding': 'Binding DingTalk',
'app.settings.binding.dingding-description':
'Currently unbound DingTalk account',
'app.settings.binding.bind': 'Bind',
'app.settings.notification.password': 'Account Password',
'app.settings.notification.password-description':
'Messages from other users will be notified in the form of a station letter',
'app.settings.notification.messages': 'System Messages',
'app.settings.notification.messages-description':
'System messages will be notified in the form of a station letter',
'app.settings.notification.todo': 'To-do Notification',
'app.settings.notification.todo-description':
'The to-do list will be notified in the form of a letter from the station',
'app.settings.open': 'Open',
'app.settings.close': 'Close',
};

View File

@@ -0,0 +1,25 @@
import component from './fa-IR/component';
import globalHeader from './fa-IR/globalHeader';
import menu from './fa-IR/menu';
import pages from './fa-IR/pages';
import pwa from './fa-IR/pwa';
import settingDrawer from './fa-IR/settingDrawer';
import settings from './fa-IR/settings';
export default {
'navBar.lang': 'زبان ها ',
'layout.user.link.help': 'کمک',
'layout.user.link.privacy': 'حریم خصوصی',
'layout.user.link.terms': 'مقررات',
'app.preview.down.block': 'این صفحه را در پروژه محلی خود بارگیری کنید',
'app.welcome.link.fetch-blocks': 'دریافت تمام بلوک',
'app.welcome.link.block-list':
'به سرعت صفحات استاندارد مبتنی بر توسعه "بلوک" را بسازید',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'باز',
'component.tagSelect.collapse': 'بسته ',
'component.tagSelect.all': 'همه',
};

View File

@@ -0,0 +1,18 @@
export default {
'component.globalHeader.search': 'جستجو ',
'component.globalHeader.search.example1': 'مثال 1 را جستجو کنید',
'component.globalHeader.search.example2': 'مثال 2 را جستجو کنید',
'component.globalHeader.search.example3': 'مثال 3 را جستجو کنید',
'component.globalHeader.help': 'کمک',
'component.globalHeader.notification': 'اعلان',
'component.globalHeader.notification.empty':
'شما همه اعلان ها را مشاهده کرده اید.',
'component.globalHeader.message': 'پیام',
'component.globalHeader.message.empty': 'شما همه پیام ها را مشاهده کرده اید.',
'component.globalHeader.event': 'رویداد',
'component.globalHeader.event.empty': 'شما همه رویدادها را مشاهده کرده اید.',
'component.noticeIcon.clear': 'پاک کردن',
'component.noticeIcon.cleared': 'پاک شد',
'component.noticeIcon.empty': 'بدون اعلان',
'component.noticeIcon.view-more': 'نمایش بیشتر',
};

View File

@@ -0,0 +1,52 @@
export default {
'menu.welcome': 'خوش آمدید',
'menu.more-blocks': 'بلوک های بیشتر',
'menu.home': 'خانه',
'menu.admin': 'مدیر',
'menu.admin.sub-page': 'زیر صفحه',
'menu.login': 'ورود',
'menu.register': 'ثبت نام',
'menu.register-result': 'ثبت نام نتیجه',
'menu.dashboard': 'داشبورد',
'menu.dashboard.analysis': 'تحلیل و بررسی',
'menu.dashboard.monitor': 'نظارت',
'menu.dashboard.workplace': 'محل کار',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'فرم',
'menu.form.basic-form': 'فرم اساسی',
'menu.form.step-form': 'فرم مرحله',
'menu.form.step-form.info': 'فرم مرحله (نوشتن اطلاعات انتقال)',
'menu.form.step-form.confirm': 'فرم مرحله (تأیید اطلاعات انتقال)',
'menu.form.step-form.result': 'فرم مرحله (تمام شده)',
'menu.form.advanced-form': 'فرم پیشرفته',
'menu.list': 'لیست',
'menu.list.table-list': 'جدول جستجو',
'menu.list.basic-list': 'لیست اصلی',
'menu.list.card-list': 'لیست کارت',
'menu.list.search-list': 'لیست جستجو',
'menu.list.search-list.articles': 'لیست جستجو (مقالات)',
'menu.list.search-list.projects': 'لیست جستجو (پروژه ها)',
'menu.list.search-list.applications': 'لیست جستجو (برنامه ها)',
'menu.profile': 'مشخصات',
'menu.profile.basic': 'مشخصات عمومی',
'menu.profile.advanced': 'مشخصات پیشرفته',
'menu.result': 'نتیجه',
'menu.result.success': 'موفق',
'menu.result.fail': 'ناموفق',
'menu.exception': 'استثنا',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'راه اندازی',
'menu.account': 'حساب',
'menu.account.center': 'مرکز حساب',
'menu.account.settings': 'تنظیمات حساب',
'menu.account.trigger': 'خطای راه اندازی',
'menu.account.logout': 'خروج',
'menu.editor': 'ویرایشگر گرافیک',
'menu.editor.flow': 'ویرایشگر جریان',
'menu.editor.mind': 'ویرایشگر ذهن',
'menu.editor.koni': 'ویرایشگر Koni',
};

View File

@@ -0,0 +1,94 @@
export default {
'pages.layouts.userLayout.title':
'طراحی مورچه تأثیرگذارترین مشخصات طراحی وب در منطقه Xihu است',
'pages.login.accountLogin.tab': 'ورود به حساب کاربری',
'pages.login.accountLogin.errorMessage':
'نام کاربری / رمزعبور نادرست (مدیر / ant.design)',
'pages.login.failure':
'ورود به سیستم با شکست مواجه شد، لطفا دوباره سعی کنید!',
'pages.login.success': 'ورود موفق!',
'pages.login.username.placeholder': 'نام کاربری: مدیر یا کاربر',
'pages.login.username.required': 'لطفا نام کاربری خود را وارد کنید!',
'pages.login.password.placeholder': 'رمز عبور را وارد کنید',
'pages.login.password.required': 'لطفاً رمز ورود خود را وارد کنید!',
'pages.login.phoneLogin.tab': 'ورود به سیستم تلفن',
'pages.login.phoneLogin.errorMessage': 'خطای کد تأیید',
'pages.login.phoneNumber.placeholder': 'شماره تلفن',
'pages.login.phoneNumber.required': 'لطفاً شماره تلفن خود را وارد کنید!',
'pages.login.phoneNumber.invalid': 'شماره تلفن نامعتبر است!',
'pages.login.captcha.placeholder': 'کد تایید',
'pages.login.captcha.required': 'لطفا کد تأیید را وارد کنید!',
'pages.login.phoneLogin.getVerificationCode': 'دریافت کد',
'pages.getCaptchaSecondText': 'ثانیه',
'pages.login.rememberMe': 'مرا به خاطر بسپار',
'pages.login.forgotPassword': 'رمز عبور را فراموش کرده اید ?',
'pages.login.submit': 'ارسال',
'pages.login.loginWith': 'وارد شوید با :',
'pages.login.registerAccount': 'ثبت نام',
'pages.welcome.link': 'خوش آمدید',
'pages.welcome.alertMessage': 'اجزای سنگین تر سریعتر و قوی تر آزاد شده اند.',
'pages.404.subTitle': 'ببخشيد، صفحه اي که ديديد وجود نداره',
'pages.404.buttonText': 'بازگشت به صفحه اصلی',
'pages.admin.subPage.title': 'این صفحه فقط توسط مدیر قابل مشاهده است',
'pages.admin.subPage.alertMessage':
'رابط کاربری Umi اکنون منتشر شده است ، برای شروع تجربه استفاده از npm run ui خوش آمدید.',
'pages.searchTable.createForm.newRule': 'قانون جدید',
'pages.searchTable.updateForm.ruleConfig': 'پیکربندی قانون',
'pages.searchTable.updateForm.basicConfig': 'اطلاعات اولیه',
'pages.searchTable.updateForm.ruleName.nameLabel': ' نام قانون',
'pages.searchTable.updateForm.ruleName.nameRules':
'لطفاً نام قانون را وارد کنید!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'شرح قانون',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder':
'لطفاً حداقل پنج حرف وارد کنید',
'pages.searchTable.updateForm.ruleDesc.descRules':
'لطفاً حداقل یک قانون حاوی پنج کاراکتر شرح دهید!',
'pages.searchTable.updateForm.ruleProps.title': 'پیکربندی خصوصیات',
'pages.searchTable.updateForm.object': 'نظارت بر شی',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'الگوی قانون',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'نوع قانون',
'pages.searchTable.updateForm.schedulingPeriod.title': 'تنظیم دوره زمان بندی',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'زمان شروع',
'pages.searchTable.updateForm.schedulingPeriod.timeRules':
'لطفاً زمان شروع را انتخاب کنید!',
'pages.searchTable.titleDesc': 'شرح',
'pages.searchTable.ruleName': 'نام قانون لازم است',
'pages.searchTable.titleCallNo': 'تعداد تماس های خدماتی',
'pages.searchTable.titleStatus': 'وضعیت',
'pages.searchTable.nameStatus.default': 'پیش فرض',
'pages.searchTable.nameStatus.running': 'در حال دویدن',
'pages.searchTable.nameStatus.online': 'برخط',
'pages.searchTable.nameStatus.abnormal': 'غیرطبیعی',
'pages.searchTable.titleUpdatedAt': 'آخرین برنامه ریزی در',
'pages.searchTable.exception': 'لطفا دلیل استثنا را وارد کنید!',
'pages.searchTable.titleOption': 'گزینه',
'pages.searchTable.config': 'پیکربندی',
'pages.searchTable.subscribeAlert': 'مشترک شدن در هشدارها',
'pages.searchTable.title': 'فرم درخواست',
'pages.searchTable.new': 'جدید',
'pages.searchTable.chosen': 'انتخاب شده',
'pages.searchTable.item': 'مورد',
'pages.searchTable.totalServiceCalls': 'تعداد کل تماس های خدماتی',
'pages.searchTable.tenThousand': '0000',
'pages.searchTable.batchDeletion': 'حذف دسته ای',
'pages.searchTable.batchApproval': 'تصویب دسته ای',
// Admin — قراردادهای سیستم
'pages.admin.systemContracts.title': 'قراردادهای سیستم',
'pages.admin.systemContracts.newContract': 'قرارداد جدید',
'pages.admin.systemContracts.createTitle': 'قرارداد جدید',
'pages.admin.systemContracts.editTitle': 'ویرایش قرارداد',
'pages.admin.systemContracts.name': 'نام',
'pages.admin.systemContracts.chain': 'شبکه',
'pages.admin.systemContracts.address': 'آدرس',
'pages.admin.systemContracts.deployBlock': 'بلوک استقرار',
'pages.admin.systemContracts.allChains': 'همه شبکه‌ها',
'pages.admin.systemContracts.copyAddress': 'کپی آدرس',
'pages.admin.systemContracts.copied': 'کپی شد',
'pages.admin.systemContracts.deleteTitle': 'این قرارداد حذف شود؟',
'pages.admin.systemContracts.namePlaceholder': 'مثلاً lendingProxy، YTLPToken',
'pages.admin.systemContracts.blockPlaceholder': 'شماره بلوک (اختیاری)',
'pages.admin.systemContracts.descPlaceholder': 'توضیحات اختیاری',
'pages.admin.systemContracts.nameRequired': 'نام الزامی است',
'pages.admin.systemContracts.addressRequired': 'آدرس الزامی است',
};

View File

@@ -0,0 +1,7 @@
export default {
'app.pwa.offline': 'شما اکنون آفلاین هستید',
'app.pwa.serviceworker.updated': 'مطالب جدید در دسترس است',
'app.pwa.serviceworker.updated.hint':
'لطفاً برای بارگیری مجدد صفحه فعلی ، دکمه "تازه سازی" را فشار دهید',
'app.pwa.serviceworker.updated.ok': 'تازه سازی',
};

View File

@@ -0,0 +1,32 @@
export default {
'app.setting.pagestyle': 'تنظیم نوع صفحه',
'app.setting.pagestyle.dark': 'سبک تیره',
'app.setting.pagestyle.light': 'سبک سبک',
'app.setting.content-width': 'عرض محتوا',
'app.setting.content-width.fixed': 'ثابت',
'app.setting.content-width.fluid': 'شناور',
'app.setting.themecolor': 'رنگ تم',
'app.setting.themecolor.dust': 'گرد و غبار قرمز',
'app.setting.themecolor.volcano': 'آتشفشان',
'app.setting.themecolor.sunset': 'غروب نارنجی',
'app.setting.themecolor.cyan': 'فیروزه ای',
'app.setting.themecolor.green': 'سبز قطبی',
'app.setting.themecolor.daybreak': 'آبی روشن(پیشفرض)',
'app.setting.themecolor.geekblue': 'چسب گیک',
'app.setting.themecolor.purple': 'بنفش طلایی',
'app.setting.navigationmode': 'حالت پیمایش',
'app.setting.sidemenu': 'طرح منوی کناری',
'app.setting.topmenu': 'طرح منوی بالایی',
'app.setting.fixedheader': 'سرصفحه ثابت',
'app.setting.fixedsidebar': 'نوار کناری ثابت',
'app.setting.fixedsidebar.hint': 'کار بر روی منوی کناری',
'app.setting.hideheader': 'هدر پنهان هنگام پیمایش',
'app.setting.hideheader.hint': 'وقتی Hidden Header فعال باشد کار می کند',
'app.setting.othersettings': 'تنظیمات دیگر',
'app.setting.weakmode': 'حالت ضعیف',
'app.setting.copy': 'تنظیمات کپی',
'app.setting.copyinfo':
'موفقیت در کپی کردن لطفا defaultSettings را در src / models / setting.js جایگزین کنید',
'app.setting.production.hint':
'صفحه تنظیم فقط در محیط توسعه نمایش داده می شود ، لطفاً دستی تغییر دهید',
};

View File

@@ -0,0 +1,64 @@
export default {
'app.settings.menuMap.basic': 'تنظیمات پایه ',
'app.settings.menuMap.security': 'تنظیمات امنیتی',
'app.settings.menuMap.binding': 'صحافی حساب',
'app.settings.menuMap.notification': 'اعلان پیام جدید',
'app.settings.basic.avatar': 'آواتار',
'app.settings.basic.change-avatar': 'آواتار را تغییر دهید',
'app.settings.basic.email': 'ایمیل',
'app.settings.basic.email-message': 'لطفا ایمیل خود را وارد کنید!',
'app.settings.basic.nickname': 'نام مستعار',
'app.settings.basic.nickname-message': 'لطفاً نام مستعار خود را وارد کنید!',
'app.settings.basic.profile': 'پروفایل شخصی',
'app.settings.basic.profile-message': 'لطفاً مشخصات شخصی خود را وارد کنید!',
'app.settings.basic.profile-placeholder': 'معرفی مختصر خودتان',
'app.settings.basic.country': 'کشور / منطقه',
'app.settings.basic.country-message': 'لطفاً کشور خود را وارد کنید!',
'app.settings.basic.geographic': 'استان یا شهر',
'app.settings.basic.geographic-message':
'لطفاً اطلاعات جغرافیایی خود را وارد کنید!',
'app.settings.basic.address': 'آدرس خیابان',
'app.settings.basic.address-message': 'لطفا آدرس خود را وارد کنید!',
'app.settings.basic.phone': 'شماره تلفن',
'app.settings.basic.phone-message': 'لطفاً تلفن خود را وارد کنید!',
'app.settings.basic.update': 'به روز رسانی اطلاعات',
'app.settings.security.strong': 'قوی',
'app.settings.security.medium': 'متوسط',
'app.settings.security.weak': 'ضعیف',
'app.settings.security.password': 'رمز عبور حساب کاربری',
'app.settings.security.password-description': 'قدرت رمز عبور فعلی',
'app.settings.security.phone': 'تلفن امنیتی',
'app.settings.security.phone-description': 'تلفن مقید',
'app.settings.security.question': 'سوال امنیتی',
'app.settings.security.question-description':
'سوال امنیتی تنظیم نشده است و سیاست امنیتی می تواند به طور موثر از امنیت حساب محافظت کند',
'app.settings.security.email': 'ایمیل پشتیبان',
'app.settings.security.email-description': 'ایمیل مقید',
'app.settings.security.mfa': 'دستگاه MFA',
'app.settings.security.mfa-description':
'دستگاه MFA بسته نشده ، پس از اتصال ، می تواند دو بار تأیید شود',
'app.settings.security.modify': 'تغییر',
'app.settings.security.set': 'تنظیم',
'app.settings.security.bind': 'بستن',
'app.settings.binding.taobao': 'اتصال Taobao',
'app.settings.binding.taobao-description':
'حساب Taobao در حال حاضر بسته نشده است',
'app.settings.binding.alipay': 'اتصال Alipay',
'app.settings.binding.alipay-description':
'حساب Alipay در حال حاضر بسته نشده است',
'app.settings.binding.dingding': 'اتصال DingTalk',
'app.settings.binding.dingding-description':
'حساب DingTalk در حال حاضر محدود نشده است',
'app.settings.binding.bind': 'بستن',
'app.settings.notification.password': 'رمز عبور حساب کاربری',
'app.settings.notification.password-description':
'پیام های سایر کاربران در قالب یک نامه ایستگاهی اعلام خواهد شد',
'app.settings.notification.messages': 'پیام های سیستم',
'app.settings.notification.messages-description':
'پیام های سیستم به صورت نامه ایستگاه مطلع می شوند',
'app.settings.notification.todo': 'اعلان کارها',
'app.settings.notification.todo-description':
'لیست کارها به صورت نامه ای از ایستگاه اطلاع داده می شود',
'app.settings.open': 'باز کن',
'app.settings.close': 'بستن',
};

View File

@@ -0,0 +1,25 @@
import component from './id-ID/component';
import globalHeader from './id-ID/globalHeader';
import menu from './id-ID/menu';
import pages from './id-ID/pages';
import pwa from './id-ID/pwa';
import settingDrawer from './id-ID/settingDrawer';
import settings from './id-ID/settings';
export default {
'navbar.lang': 'Bahasa',
'layout.user.link.help': 'Bantuan',
'layout.user.link.privacy': 'Privasi',
'layout.user.link.terms': 'Ketentuan',
'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda',
'app.welcome.link.fetch-blocks': 'Dapatkan semua blok',
'app.welcome.link.block-list':
'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'Perluas',
'component.tagSelect.collapse': 'Lipat',
'component.tagSelect.all': 'Semua',
};

View File

@@ -0,0 +1,18 @@
export default {
'component.globalHeader.search': 'Pencarian',
'component.globalHeader.search.example1': 'Contoh 1 Pencarian',
'component.globalHeader.search.example2': 'Contoh 2 Pencarian',
'component.globalHeader.search.example3': 'Contoh 3 Pencarian',
'component.globalHeader.help': 'Bantuan',
'component.globalHeader.notification': 'Notifikasi',
'component.globalHeader.notification.empty':
'Anda telah membaca semua notifikasi',
'component.globalHeader.message': 'Pesan',
'component.globalHeader.message.empty': 'Anda telah membaca semua pesan.',
'component.globalHeader.event': 'Acara',
'component.globalHeader.event.empty': 'Anda telah melihat semua acara.',
'component.noticeIcon.clear': 'Kosongkan',
'component.noticeIcon.cleared': 'Berhasil dikosongkan',
'component.noticeIcon.empty': 'Tidak ada pemberitahuan',
'component.noticeIcon.view-more': 'Melihat lebih',
};

View File

@@ -0,0 +1,53 @@
export default {
'menu.welcome': 'Selamat Datang',
'menu.more-blocks': 'Blocks Lainnya',
'menu.home': 'Halaman Awal',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Halaman',
'menu.login': 'Masuk',
'menu.register': 'Pendaftaran',
'menu.register-result': 'Hasil Pendaftaran',
'menu.dashboard': 'Dasbor',
'menu.dashboard.analysis': 'Analisis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Form',
'menu.form.basic-form': 'Form Dasar',
'menu.form.step-form': 'Form Bertahap',
'menu.form.step-form.info': 'Form Bertahap(menulis informasi yang dibagikan)',
'menu.form.step-form.confirm':
'Form Bertahap(konfirmasi informasi yang dibagikan)',
'menu.form.step-form.result': 'Form Bertahap(selesai)',
'menu.form.advanced-form': 'Form Lanjutan',
'menu.list': 'Daftar',
'menu.list.table-list': 'Tabel Pencarian',
'menu.list.basic-list': 'Daftar Dasar',
'menu.list.card-list': 'Daftar Kartu',
'menu.list.search-list': 'Daftar Pencarian',
'menu.list.search-list.articles': 'Daftar Pencarian(artikel)',
'menu.list.search-list.projects': 'Daftar Pencarian(projek)',
'menu.list.search-list.applications': 'Daftar Pencarian(aplikasi)',
'menu.profile': 'Profil',
'menu.profile.basic': 'Profil Dasar',
'menu.profile.advanced': 'Profile Lanjutan',
'menu.result': 'Hasil',
'menu.result.success': 'Sukses',
'menu.result.fail': 'Gagal',
'menu.exception': 'Pengecualian',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Jalankan',
'menu.account': 'Akun',
'menu.account.center': 'Detail Akun',
'menu.account.settings': 'Pengaturan Akun',
'menu.account.trigger': 'Mengaktivasi Error',
'menu.account.logout': 'Keluar',
'menu.editor': 'Penyusun Grafis',
'menu.editor.flow': 'Penyusun Alur',
'menu.editor.mind': 'Penyusun Mind',
'menu.editor.koni': 'Penyusun Koni',
};

View File

@@ -0,0 +1,94 @@
export default {
'pages.layouts.userLayout.title':
'Ant Design adalah spesifikasi desain Web yang paling berpengaruh di Kabupaten Xihu',
'pages.login.accountLogin.tab': 'Login dengan akun',
'pages.login.accountLogin.errorMessage':
'Nama pengguna dan kata sandi salah(admin/ant.design)',
'pages.login.failure': 'Log masuk gagal, silakan coba lagi!',
'pages.login.success': 'Login berhasil!',
'pages.login.username.placeholder': 'nama pengguna: admin atau user',
'pages.login.username.required': 'Nama pengguna harus diisi!',
'pages.login.password.placeholder': 'Masukkan kata sandi',
'pages.login.password.required': 'Kata sandi harus diisi!',
'pages.login.phoneLogin.tab': 'Login dengan ponsel',
'pages.login.phoneLogin.errorMessage': 'Kesalahan kode verifikasi',
'pages.login.phoneNumber.placeholder': 'masukkan nomor telepon',
'pages.login.phoneNumber.required': 'Nomor ponsel harus diisi!',
'pages.login.phoneNumber.invalid': 'Nomor ponsel tidak valid!',
'pages.login.captcha.placeholder': 'kode verifikasi',
'pages.login.captcha.required': 'Kode verifikasi diperlukan!',
'pages.login.phoneLogin.getVerificationCode': 'Dapatkan kode',
'pages.getCaptchaSecondText': 'detik tersisa',
'pages.login.rememberMe': 'Ingat saya',
'pages.login.forgotPassword': 'Lupa Kata Sandi?',
'pages.login.submit': 'Masuk',
'pages.login.loginWith': 'Masuk dengan :',
'pages.login.registerAccount': 'Daftar Akun',
'pages.welcome.link': 'Selamat datang',
'pages.welcome.alertMessage':
'Komponen heavy-duty yang lebih cepat dan lebih kuat telah dirilis.',
'pages.404.subTitle': 'Maaf, halaman yang Anda kunjungi tidak ada. ',
'pages.404.buttonText': 'Kembali ke halaman utama',
'pages.admin.subPage.title': 'Halaman ini hanya dapat dilihat oleh admin',
'pages.admin.subPage.alertMessage':
'umi ui telah dirilis, silahkan gunakan npm run ui untuk memulai pengalaman.',
'pages.searchTable.createForm.newRule': 'Aturan baru',
'pages.searchTable.updateForm.ruleConfig': 'Konfigurasi aturan',
'pages.searchTable.updateForm.basicConfig': 'Informasi dasar',
'pages.searchTable.updateForm.ruleName.nameLabel': 'Nama aturan',
'pages.searchTable.updateForm.ruleName.nameRules':
'Harap masukkan nama aturan!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'Deskripsi aturan',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder':
'Harap masukkan setidaknya lima karakter',
'pages.searchTable.updateForm.ruleDesc.descRules':
'Harap masukkan deskripsi aturan setidaknya lima karakter!',
'pages.searchTable.updateForm.ruleProps.title': 'Properti aturan',
'pages.searchTable.updateForm.object': 'Objek pemantauan',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'Template aturan',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'Jenis aturan',
'pages.searchTable.updateForm.schedulingPeriod.title': 'Periode penjadwalan',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Waktu mulai',
'pages.searchTable.updateForm.schedulingPeriod.timeRules':
'Pilih waktu mulai!',
'pages.searchTable.titleDesc': 'deskripsi',
'pages.searchTable.ruleName': 'Nama aturan wajib diisi',
'pages.searchTable.titleCallNo': 'Jumlah panggilan',
'pages.searchTable.titleStatus': 'Status',
'pages.searchTable.nameStatus.default': 'default',
'pages.searchTable.nameStatus.running': 'menyala',
'pages.searchTable.nameStatus.online': 'online',
'pages.searchTable.nameStatus.abnormal': 'abnormal',
'pages.searchTable.titleUpdatedAt': 'Waktu terjadwal',
'pages.searchTable.exception': 'Harap masukkan alasan pengecualian!',
'pages.searchTable.titleOption': 'Pengoperasian',
'pages.searchTable.config': 'Konfigurasi',
'pages.searchTable.subscribeAlert': 'Berlangganan notifikasi',
'pages.searchTable.title': 'Formulir pertanyaan',
'pages.searchTable.new': 'Baru',
'pages.searchTable.chosen': 'Terpilih',
'pages.searchTable.item': 'item',
'pages.searchTable.totalServiceCalls': 'Jumlah total panggilan layanan',
'pages.searchTable.tenThousand': '0000',
'pages.searchTable.batchDeletion': 'Penghapusan batch',
'pages.searchTable.batchApproval': 'Persetujuan batch',
// Admin — Kontrak Sistem
'pages.admin.systemContracts.title': 'Kontrak Sistem',
'pages.admin.systemContracts.newContract': 'Kontrak Baru',
'pages.admin.systemContracts.createTitle': 'Kontrak Baru',
'pages.admin.systemContracts.editTitle': 'Edit Kontrak',
'pages.admin.systemContracts.name': 'Nama',
'pages.admin.systemContracts.chain': 'Jaringan',
'pages.admin.systemContracts.address': 'Alamat',
'pages.admin.systemContracts.deployBlock': 'Blok Deploy',
'pages.admin.systemContracts.allChains': 'Semua jaringan',
'pages.admin.systemContracts.copyAddress': 'Salin alamat',
'pages.admin.systemContracts.copied': 'Disalin!',
'pages.admin.systemContracts.deleteTitle': 'Hapus kontrak ini?',
'pages.admin.systemContracts.namePlaceholder': 'mis. lendingProxy, YTLPToken',
'pages.admin.systemContracts.blockPlaceholder': 'Nomor blok (opsional)',
'pages.admin.systemContracts.descPlaceholder': 'Deskripsi opsional',
'pages.admin.systemContracts.nameRequired': 'Nama wajib diisi',
'pages.admin.systemContracts.addressRequired': 'Alamat wajib diisi',
};

View File

@@ -0,0 +1,7 @@
export default {
'app.pwa.offline': 'Koneksi anda terputus',
'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia',
'app.pwa.serviceworker.updated.hint':
'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini',
'app.pwa.serviceworker.updated.ok': 'Memuat ulang',
};

View File

@@ -0,0 +1,33 @@
export default {
'app.setting.pagestyle': 'Pengaturan style Halaman',
'app.setting.pagestyle.dark': 'Style Gelap',
'app.setting.pagestyle.light': 'Style Cerah',
'app.setting.content-width': 'Lebar Konten',
'app.setting.content-width.fixed': 'Tetap',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (bawaan)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Mode Navigasi',
'app.setting.sidemenu': 'Susunan Menu Samping',
'app.setting.topmenu': 'Susunan Menu Atas',
'app.setting.fixedheader': 'Header Tetap',
'app.setting.fixedsidebar': 'Sidebar Tetap',
'app.setting.fixedsidebar.hint': 'Berjalan pada Susunan Menu Samping',
'app.setting.hideheader': 'Sembunyikan Header ketika gulir ke bawah',
'app.setting.hideheader.hint':
'Bekerja ketika Header tersembunyi dimunculkan',
'app.setting.othersettings': 'Pengaturan Lainnya',
'app.setting.weakmode': 'Mode Lemah',
'app.setting.copy': 'Salin Pengaturan',
'app.setting.copyinfo':
'Berhasil disalin, tolong ubah defaultSettings pada src/models/setting.js',
'app.setting.production.hint':
'Panel pengaturan hanya muncul pada lingkungan pengembangan, silahkan modifikasi secara menual',
};

View File

@@ -0,0 +1,64 @@
export default {
'app.settings.menuMap.basic': 'Pengaturan Dasar',
'app.settings.menuMap.security': 'Pengaturan Keamanan',
'app.settings.menuMap.binding': 'Pengikatan Akun',
'app.settings.menuMap.notification': 'Notifikasi Pesan Baru',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Ubah avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Tolong masukkan email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Tolong masukkan Nickname!',
'app.settings.basic.profile': 'Profil Personal',
'app.settings.basic.profile-message': 'Tolong masukkan profil personal!',
'app.settings.basic.profile-placeholder':
'Perkenalan Singkat tentang Diri Anda',
'app.settings.basic.country': 'Negara/Wilayah',
'app.settings.basic.country-message': 'Tolong masukkan negara anda!',
'app.settings.basic.geographic': 'Provinsi atau kota',
'app.settings.basic.geographic-message':
'Tolong masukkan info geografis anda!',
'app.settings.basic.address': 'Alamat Jalan',
'app.settings.basic.address-message': 'Tolong masukkan Alamat Jalan anda!',
'app.settings.basic.phone': 'Nomor Ponsel',
'app.settings.basic.phone-message': 'Tolong masukkan Nomor Ponsel anda!',
'app.settings.basic.update': 'Perbarui Informasi',
'app.settings.security.strong': 'Kuat',
'app.settings.security.medium': 'Sedang',
'app.settings.security.weak': 'Lemah',
'app.settings.security.password': 'Kata Sandi Akun',
'app.settings.security.password-description': 'Kekuatan Kata Sandi saat ini',
'app.settings.security.phone': 'Keamanan Ponsel',
'app.settings.security.phone-description': 'Mengikat Ponsel',
'app.settings.security.question': 'Pertanyaan Keamanan',
'app.settings.security.question-description':
'Pertanyaan Keamanan belum diatur, dan kebijakan keamanan dapat melindungi akun secara efektif',
'app.settings.security.email': 'Email Cadangan',
'app.settings.security.email-description': 'Mengikat Email',
'app.settings.security.mfa': 'Perangka MFA',
'app.settings.security.mfa-description':
'Tidak mengikat Perangkat MFA, setelah diikat, dapat dikonfirmasi dua kali',
'app.settings.security.modify': 'Modifikasi',
'app.settings.security.set': 'Setel',
'app.settings.security.bind': 'Ikat',
'app.settings.binding.taobao': 'Mengikat Taobao',
'app.settings.binding.taobao-description':
'Tidak mengikat akun Taobao saat ini',
'app.settings.binding.alipay': 'Mengikat Alipay',
'app.settings.binding.alipay-description':
'Tidak mengikat akun Alipay saat ini',
'app.settings.binding.dingding': 'Mengikat DingTalk',
'app.settings.binding.dingding-description': 'Tidak mengikat akun DingTalk',
'app.settings.binding.bind': 'Ikat',
'app.settings.notification.password': 'Kata Sandi Akun',
'app.settings.notification.password-description':
'Pesan dari pengguna lain akan diberitahu dalam bentuk surat',
'app.settings.notification.messages': 'Pesan Sistem',
'app.settings.notification.messages-description':
'Pesan sistem akan diberitahu dalam bentuk surat',
'app.settings.notification.todo': 'Notifikasi daftar To-do',
'app.settings.notification.todo-description':
'Daftar to-do akan diberitahukan dalam bentuk surat dari stasiun',
'app.settings.open': 'Buka',
'app.settings.close': 'Tutup',
};

View File

@@ -0,0 +1,25 @@
import component from './ja-JP/component';
import globalHeader from './ja-JP/globalHeader';
import menu from './ja-JP/menu';
import pages from './ja-JP/pages';
import pwa from './ja-JP/pwa';
import settingDrawer from './ja-JP/settingDrawer';
import settings from './ja-JP/settings';
export default {
'navBar.lang': '言語',
'layout.user.link.help': 'ヘルプ',
'layout.user.link.privacy': 'プライバシー',
'layout.user.link.terms': '利用規約',
'app.preview.down.block':
'このページをローカルプロジェクトにダウンロードしてください',
'app.welcome.link.fetch-blocks': '',
'app.welcome.link.block-list': '',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': '展開',
'component.tagSelect.collapse': '折りたたむ',
'component.tagSelect.all': 'すべて',
};

Some files were not shown because too many files have changed in this diff Show More