补充个人中心下的页面

This commit is contained in:
Eamon
2026-08-31 14:49:28 +08:00
parent b1f99b51e2
commit 3072b97bcf
59 changed files with 4122 additions and 970 deletions

View File

@@ -19,6 +19,20 @@ type ApiResponse<T = undefined> =
data: T
// 响应头(key 统一小写),用于登录等场景从 header 取数据
headers?: Record<string, string>
// .NET 分页接口(ApiResultPaged)携带的总条数
totalCount?: number
}
export { API_BASE_URL, type ApiResponse, CLIENT_ID, CLIENT_SECRET }
// 服务端分页结果
type PageResult<T> = {
total: number
list: T[]
}
export {
API_BASE_URL,
type ApiResponse,
CLIENT_ID,
CLIENT_SECRET,
type PageResult,
}

View File

@@ -0,0 +1,51 @@
import { useMemo, useState } from "react"
import {
DEFAULT_FILTERS,
matchOrders,
type OrderFilters,
type OrderItem,
} from "@/lib/models/order"
// 订单列表通用筛选/分页逻辑(orders / refundOrders 页面共用)
export function useOrderFilters(list: OrderItem[]) {
const [filters, setFilters] = useState<OrderFilters>(DEFAULT_FILTERS)
const [appliedFilters, setAppliedFilters] =
useState<OrderFilters>(DEFAULT_FILTERS)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const filtered = useMemo(
() => matchOrders(list, appliedFilters),
[list, appliedFilters],
)
const paged = useMemo(
() => filtered.slice((page - 1) * pageSize, page * pageSize),
[filtered, page, pageSize],
)
const patch = (partial: Partial<OrderFilters>) =>
setFilters({ ...filters, ...partial })
const handleSearch = () => {
setAppliedFilters(filters)
setPage(1)
}
const handleSizeChange = (size: number) => {
setPageSize(size)
setPage(1)
}
return {
filters,
patch,
handleSearch,
page,
setPage,
pageSize,
handleSizeChange,
filtered,
paged,
}
}

2
src/lib/models/auth.ts Normal file
View File

@@ -0,0 +1,2 @@
// 短信验证码用途
export type SMSPurpose = "User_Code" | "FindUser_Code" | "User_Login"

10
src/lib/models/cashOut.ts Normal file
View File

@@ -0,0 +1,10 @@
export type CashOutItem = {
id: number
username: string
money: number
reason: string
alipay_account: string
remark: string
status: string
create_time: string
}

28
src/lib/models/coupon.ts Normal file
View File

@@ -0,0 +1,28 @@
// 优惠券(.NET /api/sells/v1/coupon/*,对应 CouponEntity)
export type CouponItem = {
Id: number
Name: string
Remark: string
CouponType: number
TotalCount?: number
GrantCount?: number
UsedCount?: number
GetLimitCount?: number
UseRange: number
AllowMinAmount: number
CouponValue: number
DateRule: number
ValidDay: number
StartDate: string
EndDate: string
CreateDate: string
IsOverlay: number
IsOpen: number
Disabled?: number
}
export type CouponQueryParams = {
PageIndex?: number
PageSize?: number
KeyWord?: string
}

View File

@@ -0,0 +1,40 @@
export type DashboardBalance = {
balance: number
hCoin: number
}
export type AccountInfo = {
phone: string
qq: string
wechat: string
email: string
taobao: string
}
export type RecentOrderItem = {
date: string
product: string
plan: string
price: number
connections: number
account: string
orderAmount: number
paidAmount: number
}
export type ConsumptionStats = {
today: number
month: number
todayRefund: number
year: number
todayRecharge: number
monthRecharge: number
monthRefund: number
}
export type IpAccountStats = {
using: number
total: number
expired: number
yearConsumption: number
}

View File

@@ -0,0 +1,6 @@
export type HttpRechargeItem = {
create_time: string
ju_money: number
pay_type: string
pay_money: number
}

35
src/lib/models/line.ts Normal file
View File

@@ -0,0 +1,35 @@
export type LineItem = {
city: string | number
daikuan: string | number
name: string | number
nasname: string | number
online: string | number
supply: string | number
}
export type LineData = {
count: number | string
use_count: number | string
data: LineItem[]
}
export type LineSearchResult = {
data: LineItem[]
}
// IP线路表产品选项
export type ProductOption = {
id: number
name: string
l2tp: string
sstp: string
}
export const PRODUCTS: ProductOption[] = [
{ id: 29, name: "极狐IP", l2tp: "8899", sstp: "4430" },
{ id: 3, name: "极光IP", l2tp: "1234", sstp: "4430" },
{ id: 27, name: "麒麟IP", l2tp: "123", sstp: "4432" },
{ id: 26, name: "猎豹IP", l2tp: "1234", sstp: "4430" },
{ id: 28, name: "水滴独享IP", l2tp: "123", sstp: "4432" },
{ id: 22, name: "火狐静态IP", l2tp: "888888", sstp: "5908" },
]

View File

@@ -16,6 +16,58 @@ export type OrderItem = {
UpdateTime: string
}
// ==================== 下单接口(php-api /product/CreateOrder) ====================
export type CreateOrderRequest = {
PackageId: number
OrderType: number
Account: string
Pwd: string
ConnectCount: number
MinPostfix?: number
MaxPostfix?: number
CouponId?: number
UseAccountAmount?: number
OPayType: number
PayChannel?: number
Price?: number
}
export type CreateTestAccountRequest = {
ProductId: number
PackageId: number
Account: string
Pwd: string
}
export type OrderInfo = {
OrderNo: string
OtherPayAmount: number
CreateTime: string
PayType: number
}
export type CreateOrderData = {
OrderInfo: OrderInfo
PayData: string
}
// ==================== 管理端订单查询(.NET /api/course/v1/order/*) ====================
export type OrderQueryParams = {
PageIndex?: number
PageSize?: number
KeyWord?: string
OrderTypes?: number[]
BTime?: string
ETime?: string
ProductId?: number
PackageId?: number
Channel?: string
ProductIds?: number[]
PackageNames?: string[]
}
export type OrderFilters = {
orderType: string
productId: string
@@ -65,3 +117,61 @@ export function cutAccount(accounts: string, max = 15): string {
export function totalConnections(item: OrderItem): number {
return item.ConnectCount * item.AccountCount
}
// ==================== 表格展示辅助 ====================
export function formatMoney(value: number): string {
return value.toFixed(2)
}
export function formatDate(value: string): string {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
return `${year}.${month}.${day}`
}
// 前端筛选(后端筛选接口就绪后可移除)
export function matchOrders(
list: OrderItem[],
filters: OrderFilters,
): OrderItem[] {
const keyword = filters.keyword.trim()
return list.filter(order => {
if (
filters.orderType !== "0" &&
order.OrderType !== Number(filters.orderType)
) {
return false
}
if (
filters.productId !== "0" &&
order.ProductId !== Number(filters.productId)
) {
return false
}
if (
filters.packageName !== "0" &&
order.PackageName !== filters.packageName
) {
return false
}
if (
keyword &&
!order.OrderNo.includes(keyword) &&
!order.Accounts.includes(keyword)
) {
return false
}
if (filters.bTime && order.CreateTime.slice(0, 10) < filters.bTime) {
return false
}
if (filters.eTime && order.CreateTime.slice(0, 10) > filters.eTime) {
return false
}
return true
})
}

279
src/lib/models/package.ts Normal file
View File

@@ -0,0 +1,279 @@
// 短效/长效套餐相关模型(与 php-api.juip.com 的 /http/user/* 接口对应)
// 短效套餐(已开通)
export type DxPackage = {
id: number
name: string
planType: number
remainingAmount: number | string
durationType: string
durationType2?: number
expireTime: string
isExpired?: boolean | number | string
ipAmount?: number
}
// 短效购买记录
export type DxOrder = {
order_no: string
order_type: number | string
package_type: string
durationType: string
money: number | string
create_time: string
}
// 长效套餐(已开通)
export type CxPackage = {
id: number
isAbroad: number | string
isAbroad2?: number
shareType: number | string
ipAmount: number
expireTime: string
isRelayed?: number
}
// 长效购买记录
export type CxOrder = {
order_no: string
order_type: number | string
game_type: string
ip_count: number
money: number | string
create_time: string
}
// 已开通长效 IP
export type LangIpItem = {
id: number
packId?: number
isRelayed: number
isAbroad: number
ip: string
realIp: string
httpPort: number
socks5Port: number
area: string
bandwidth: number
lineType: number | string
lineType2?: number
shareType: number | string
shareType2?: number
state: number | string
username: string
password: string
expireTime: string
isExpired: boolean | number | string
gameId?: number
isp?: number
}
// 长效 IP 切换记录
export type SwitchRecord = {
oldIp: string
oldArea: string
newIp: string
newArea: string
reason: string
createTime: string
}
// 服务端分页信息
export type Pager = {
currentPage: number
pageSize?: number
totalCount?: number
totalPage?: number
}
// 长效 IP 列表查询结果
export type LangIpListResult = {
items: LangIpItem[]
pager: Pager
}
// 长效 IP 切换记录查询结果
export type SwitchListResult = {
items: SwitchRecord[]
pager: Pager
}
// 下单/续费/切换通用返回
export type OrderActionResult = {
code: number
msg?: string
}
// 批量提取的长效 IP
export type LangIpExtract = {
ip: string
httpPort: number
socks5Port: number
username: string
password: string
}
// 短效提取用户凭证(get_user_token)
export type UserApiToken = {
uid: number | string
token: string
}
// 续费/切换下单数据
export type RebuyData = {
title?: string
packId?: number
packType?: number
periodType: number
periodAmount: number
durationType?: number
ipAmount: number
ids?: string
isRelayed?: number
isAbroad?: number
shareType?: number
lineType?: number
bandwidth?: number
}
// 套餐订单信息(order_type: 2-短效无限量 3-短效包天 5-长效套餐 6-单IP续费 7-切换IP)
export type PackageOrderInfo = {
order_type: number
money: number
pay_type?: number
data: RebuyData
}
// 长效 IP 切换节点/入口下单数据
export type ChangeIpData = {
title?: string
id: number
isAbroad: number
cityCode: number
isp: number
switchPop: 0 | 1
data: RebuyData
}
// 短效 API 链接生成参数
export type ApiLinkConfig = {
pack?: number
num: number
gt?: number
port: number
type: number
lb: number
clb?: string
distinct: number
isp: number
aw: number
at: number
sp: number
csp?: string
pt?: number
time?: number
u?: number | string
t?: string
}
// 短效套餐类型
export const SHORT_PLAN_DAILY = 21 // 短效包天
export const SHORT_PLAN_VOLUME = 22 // 短效包量
export const SHORT_PLAN_UNLIMITED = 23 // 短效无限量
export const LONG_PLAN = 31 // 长效套餐
// 套餐周期选项
export const PERIOD_OPTIONS = [
{ value: 1, label: "按天", unit: "天" },
{ value: 7, label: "按周", unit: "周" },
{ value: 30, label: "按月", unit: "月" },
{ value: 90, label: "按季", unit: "季" },
] as const
// 长效订单类型
export const LONG_ORDER_TYPE_LABELS: Record<number, string> = {
5: "长效游戏",
6: "单IP续费",
7: "切换IP",
}
// 短效订单类型
export const SHORT_ORDER_TYPE_LABELS: Record<number, string> = {
2: "短效无限量",
3: "短效包天",
4: "短效包量",
}
// 长效 IP 状态
export const IP_STATE_LABELS: Record<number, string> = {
1: "正常",
0: "异常",
9: "封禁",
}
// ==================== 字段格式化 ====================
// 后端的 isExpired 可能返回 true/1/"1",统一转布尔
export function parseExpired(
value: boolean | number | string | undefined,
): boolean {
return value === true || value === 1 || value === "1"
}
export function formatExpired(
value: boolean | number | string | undefined,
): string {
return parseExpired(value) ? "已过期" : "未过期"
}
export function formatAbroad(value: number | string | undefined): string {
return value === 1 || value === "1" ? "海外" : "国内"
}
export function formatIpState(value: number | string): string {
const num = Number(value)
return IP_STATE_LABELS[num] ?? String(value)
}
export function formatOrderTypeLabel(
labels: Record<number, string>,
value: number | string,
): string {
return labels[Number(value)] ?? String(value)
}
// 提取地区选项
export const REGION_OPTIONS = [
{ value: "110000", label: "北京市" },
{ value: "120000", label: "天津市" },
{ value: "130000", label: "河北省" },
{ value: "140000", label: "山西省" },
{ value: "210000", label: "辽宁省" },
{ value: "220000", label: "吉林省" },
{ value: "310000", label: "上海市" },
{ value: "320000", label: "江苏省" },
{ value: "330000", label: "浙江省" },
{ value: "340000", label: "安徽省" },
{ value: "350000", label: "福建省" },
{ value: "360000", label: "江西省" },
{ value: "370000", label: "山东省" },
{ value: "410000", label: "河南省" },
{ value: "420000", label: "湖北省" },
{ value: "430000", label: "湖南省" },
{ value: "440000", label: "广东省" },
{ value: "460000", label: "海南省" },
{ value: "500000", label: "重庆市" },
{ value: "510000", label: "四川省" },
{ value: "520000", label: "贵州省" },
{ value: "530000", label: "云南省" },
{ value: "610000", label: "陕西省" },
{ value: "620000", label: "甘肃省" },
{ value: "630000", label: "青海省" },
{ value: "230000", label: "黑龙江省" },
{ value: "540000", label: "西藏自治区" },
{ value: "150000", label: "内蒙古自治区" },
{ value: "450000", label: "广西壮族自治区" },
{ value: "640000", label: "宁夏回族自治区" },
{ value: "650000", label: "新疆维吾尔自治区" },
] as const

View File

@@ -0,0 +1,11 @@
export type RosOrderItem = {
id: number
username: string
product_name: string
real_price: number
order_no: string
user: string
phone: string
address: string
create_time: string
}

16
src/lib/models/user.ts Normal file
View File

@@ -0,0 +1,16 @@
// 用户(.NET /api/baseinfo/v1/user/*,对应 UserEntity)
export type UserItem = {
Id: number
LoginCode: string
Name: string
Phone: string
Enabled: number
PhotoUrl?: string
LastLoginDate?: string
}
export type UserQueryParams = {
PageIndex?: number
PageSize?: number
KeyWord?: string
}

View File

@@ -0,0 +1,6 @@
export type WhiteIpItem = {
id: number
ip: string
createTime: string
isLocked: string
}

View File

@@ -8,3 +8,49 @@ export type Children = { children: ReactNode }
export function cn(...inputs: ClassNameValue[]) {
return twMerge(inputs)
}
// 以文本文件形式下载内容
export function downloadTextFile(filename: string, content: string) {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = filename
anchor.style.display = "none"
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(url)
}
// 导出 CSV(带 BOM,避免 Excel 打开中文乱码)
export function downloadCsv(
filename: string,
header: string[],
rows: (string | number)[][],
) {
const lines = [header, ...rows].map(line =>
line.map(v => `"${String(v).replaceAll('"', '""')}"`).join(","),
)
const csv = lines.join("\r\n")
const blob = new Blob([`\uFEFF${csv}`], {
type: "text/csv;charset=utf-8",
})
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename.endsWith(".csv") ? filename : `${filename}.csv`
a.click()
URL.revokeObjectURL(url)
}
// 服务端未返回总条数时,按当前页数据量估算
export function estimateTotal(
pagerTotal: number | undefined,
page: number,
pageSize: number,
itemCount: number,
): number {
if (pagerTotal && pagerTotal > 0) return pagerTotal
return itemCount > 0 ? (page - 1) * pageSize + itemCount : 0
}