产品列表 水滴静态-游戏/ 套餐/线路列表

This commit is contained in:
Eamon
2026-10-14 18:19:39 +08:00
parent 1fc625ede8
commit b5076d7ca7
7 changed files with 852 additions and 295 deletions

View File

@@ -116,6 +116,11 @@ export default function ChannelsPage() {
const [page, setPage] = useState(1)
const [selection, setSelection] = useState<Record<string, boolean>>({})
const selectedAccounts = useMemo(
() => data.filter(item => selection[String(item.Id)]),
[data, selection],
)
const selectedProduct = useMemo(
() =>
products.find(item => String(item.Product.Id) === query.productId) ??
@@ -207,7 +212,10 @@ export default function ChannelsPage() {
<SelectContent>
<SelectItem value="0">全部产品</SelectItem>
{products.map(item => (
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
<SelectItem
key={item.Product.Id}
value={String(item.Product.Id)}
>
{item.Product.Name}
</SelectItem>
))}
@@ -256,7 +264,25 @@ export default function ChannelsPage() {
<div className="flex flex-wrap items-center gap-4 pt-2 p-4">
<Button
className="bg-red-600 hover:bg-red-700 text-white"
onClick={() => toast.info("续费功能开发中,敬请期待")}
onClick={() => {
if (selectedAccounts.length === 0) {
toast.error("请先勾选需要续费的账号")
return
}
const productIds = new Set(
selectedAccounts.map(item => item.ProductId),
)
if (productIds.size > 1) {
toast.error("所选账号必须为同一种产品")
return
}
const accounts = selectedAccounts
.map(item => item.Account)
.join(",")
const productId = selectedAccounts[0].ProductId
// 续费走后端老页面(/product/rebuyindex → /product/rebuy),与 juipnet 行为一致
window.location.href = `/product/rebuyindex?accounts=${encodeURIComponent(accounts)}&productId=${productId}`
}}
>
续费
</Button>

84
src/api/shuidi.ts Normal file
View File

@@ -0,0 +1,84 @@
import type { ApiResponse } from "@/lib/api"
import type {
ShuidiGame,
ShuidiPackageList,
ShuidiRegion,
} from "@/lib/models/shuidi"
import { phpFetch, phpGet, phpPost } from "./http"
// ==================== 水滴静态/固态(jinyou) ====================
// - 游戏列表:POST /jinyou/api/gameList(不带 cookie)
// - 套餐列表:POST /jinyou/Jinyoujt/getPackage?type=1(1:静态 2:固态),需带 cookie
// - 线路列表:GET /jinyou/api/regionSurplusOuts?product_id=&is_unshar=&game_id=(不带 cookie)
type JinyouResult<T> = {
code: number
msg?: string
data?: T
}
export async function fetchShuidiGameList(): Promise<
ApiResponse<ShuidiGame[]>
> {
const res = await phpFetch<JinyouResult<ShuidiGame[]>>(
"POST",
"/jinyou/api/gameList",
)
if (!res.success) return res
const data = res.data?.data
return {
success: true,
data: Array.isArray(data) ? data : [],
}
}
export async function fetchShuidiPackages(
type: 1 | 2 = 1,
): Promise<ApiResponse<ShuidiPackageList>> {
const res = await phpPost<JinyouResult<ShuidiPackageList>>(
`/jinyou/Jinyoujt/getPackage?type=${type}`,
{ cookie: document.cookie },
)
if (!res.success) return res
const data = res.data?.data
if (!data || typeof data !== "object") {
return { success: true, data: { game: {}, line: {} } }
}
return {
success: true,
data: {
game: data.game ?? {},
line: data.line ?? {},
},
}
}
export async function fetchShuidiRegions(params: {
product_id: number
is_unshar: 0 | 1
game_id: number | ""
}): Promise<ApiResponse<ShuidiRegion>> {
const query = new URLSearchParams({
product_id: String(params.product_id),
is_unshar: String(params.is_unshar),
game_id: String(params.game_id),
})
const res = await phpGet<JinyouResult<{ region?: ShuidiRegion }>>(
`/jinyou/api/regionSurplusOuts?${query.toString()}`,
)
if (!res.success) return res
const region = res.data?.data?.region
if (!region || typeof region !== "object") {
return {
success: true,
data: { province_surplus_outs: {} },
}
}
return {
success: true,
data: {
surplus_out_total: region.surplus_out_total,
province_surplus_outs: region.province_surplus_outs ?? {},
},
}
}

View File

@@ -247,6 +247,8 @@ export default function BuyPage() {
PackageId: packageId,
Pwd: pwd.trim(),
ConnectCount: displayConnectCount,
OrderType: 1,
Price: product.price,
CouponAmount: 0,
CouponId: 0,
OPayType: oPayType,

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,10 @@ export type CreateOrderRequest = {
PackageId: number
Pwd: string
ConnectCount: number
// 订单类型:1新开 2批量新开 3续费 4批量续费 5退款
OrderType: number
// 套餐单价(后端按 单价×连接数×账号数 计算总价)
Price: number
// 优惠券减免金额
CouponAmount: number
// 优惠券ID
@@ -133,11 +137,13 @@ export const DEFAULT_FILTERS: OrderFilters = {
eTime: "",
}
// 与后端 Hncore.Pass.Vpn.Domain.OrderType 枚举保持一致
export const ORDER_TYPE_LABELS: Record<number, string> = {
1: "新开",
2: "续费",
3: "批量新开",
2: "批量新开",
3: "续费",
4: "批量续费",
5: "退款",
}
export const PACKAGE_OPTIONS = [

75
src/lib/models/shuidi.ts Normal file
View File

@@ -0,0 +1,75 @@
// ==================== 水滴静态:游戏列表 ====================
export type ShuidiGame = {
id: number
head?: number
name: string
}
// ==================== 水滴静态:套餐列表 ====================
export type ShuidiPackage = {
Id: string
TenantId: string
ProductId: string
Name: string
PackageType: string
Title: string | null
Image: string
Status: string
Profile: string
Price: string
LinePrice: string
DayPrice: string
MinPrice: string
DayCount: string
OriginKey: string
OriginName: string
CreateTime: string
UpdateTime: string
DeleteTag: string
IsTest: string
package_name: string
}
// 带宽档位(test / 2M / 4M / 10M / 20M)-> 该档位下的套餐(天卡/周卡/月卡...)
export type ShuidiPackageGroup = Record<string, ShuidiPackage[]>
export type ShuidiPackageList = {
game: ShuidiPackageGroup
line: ShuidiPackageGroup
}
// ==================== 水滴静态:线路列表 ====================
export type ShuidiLineLevel = {
total: number
line_level_en_name: string
province: number | string
}
export type ShuidiCity = {
total: number
discount?: number
isp_class: number
name: string
province: string
rid: string
selected_num: number
city?: string | number
ping?: number
}
export type ShuidiProvince = {
total: number
province_count: number
ping: number
line_levels: Record<string, ShuidiLineLevel>
en_name: string
citys: ShuidiCity[]
}
export type ShuidiRegion = {
surplus_out_total?: number
province_surplus_outs: Record<string, ShuidiProvince>
}

View File

@@ -25,11 +25,28 @@ export default defineConfig({
target: "http://192.168.3.6:5000",
bypass(req) {
if (req.headers?.accept?.includes("text/html")) {
return "/index.html"
// 仅前端 SPA 路由回退 index.html;续费等后端老页面(rebuyindex/rebuy)
// 需要直达后端渲染
const pathname = new URL(req.url ?? "", "http://localhost").pathname
const spaPaths = [
"/product",
"/product/buy",
"/product/buy-static",
"/product/http",
"/product/routeros",
]
if (spaPaths.includes(pathname)) {
return "/index.html"
}
}
},
},
"/script": "https://php-api.juip.com",
"/jinyou": {
target: "https://php-api.juip.com",
changeOrigin: true,
secure: false,
},
"/http": {
target: "https://php-api.juip.com",
changeOrigin: true,