feat: 新增allinssl暗色模式配置-黑金主题

This commit is contained in:
chudong
2025-12-05 18:08:12 +08:00
parent 0bb09ae6e5
commit 6e53bd522e
273 changed files with 21638 additions and 8823 deletions

View File

@@ -81,20 +81,24 @@ export class ApiClient {
...config,
};
const finalCfg = this.onRequest ? this.onRequest({ ...cfg }) || cfg : cfg;
const url = `${this.baseURL}${finalCfg.url.startsWith("/") ? "" : "/"}${
finalCfg.url
}`;
const whileList = ["/v1/order/cart/list", "/v1/contact/get_user_detail"];
// 白名单不进行登录状态判定
const isSkip = whileList.includes(finalCfg.url);
// 允许通过请求头 X-API-BASE 动态覆盖基础路径,默认保持现有逻辑
const requestedBase = finalCfg.headers?.["X-API-BASE"];
const urlBase =
typeof requestedBase === "string" && requestedBase.length > 0
? requestedBase
: this.baseURL;
const url = `${urlBase}${finalCfg.url.startsWith("/") ? "" : "/"}${finalCfg.url}`;
const whileList = ["/v1/order/cart/list", "/v1/contact/get_user_detail"];
// 白名单不进行登录状态判定
const isSkip = whileList.includes(finalCfg.url);
return new Promise((resolve, reject) => {
const method = (finalCfg.method || "POST").toUpperCase();
const isGet = method === "GET";
const payload = isGet
? finalCfg.data
: finalCfg.data
? JSON.stringify(finalCfg.data)
: undefined;
? JSON.stringify(finalCfg.data)
: undefined;
const contentType = isGet
? "application/x-www-form-urlencoded; charset=UTF-8"
: "application/json";
@@ -117,7 +121,8 @@ export class ApiClient {
(processedRes as any)?.msg === "身份失效";
const ok =
(processedRes as any)?.status === true &&
(processedRes as any)?.code === 0;
((processedRes as any)?.code === 0 ||
(processedRes as any)?.code === 200);
if (isLoginInvalid && !isSkip) {
setTimeout(() => {
location.href = "/login";

View File

@@ -4,8 +4,8 @@ import type { ApiResponse } from "../types/api";
import type {
DomainQueryCheckRequest,
DomainQueryCheckResponseData,
AiDomainQueryCheckRequest,
AiDomainQueryCheckResponseData
AiDomainQueryCheckRequest,
AiDomainQueryCheckResponseData,
} from "../types/api-types/domain-query-check";
import type {
ContactGetUserDetailRequest,
@@ -38,6 +38,10 @@ import type {
import type {
OrderPaymentStatusRequest,
OrderPaymentStatusResponseData,
AccountBalanceRequest,
AccountBalanceResponseData,
BalancePaymentRequest,
BalancePaymentResponseData,
} from "../types/api-types/order-payment-status";
import type {
OrderDetailRequest,
@@ -45,17 +49,27 @@ import type {
} from "../types/api-types/order-detail";
import type {
SeckillActivityInfoResponseData,
} from "../types/api-types/flashsale";
GetActivityInfoRequest,
GetActivityInfoResponseData,
DomainSearchParams,
DomainSearchData,
CreateOrderParams,
CreateOrderResponse,
SeckillStatusParams,
SeckillProcessingData,
PaymentStatusParams,
PaymentStatusData,
} from "../types/api-types/domain-flash";
// 落地页-域名查询
export function domainQueryCheck(
data: DomainQueryCheckRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<DomainQueryCheckResponseData>> {
return api.post<DomainQueryCheckResponseData>(
"/v1/domain/query/check",
data,
headers
headers,
);
}
// AI -域名查询
@@ -73,79 +87,79 @@ export function aiDomainQueryCheck(
// 获取实名信息模板列表
export function getContactUserDetail(
data: ContactGetUserDetailRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<ContactGetUserDetailResponseData>> {
return api.post<ContactGetUserDetailResponseData>(
"/v1/contact/get_user_detail",
data,
headers
headers,
);
}
// 购物车:获取列表
export function getOrderCartList(
data: OrderCartListRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderCartListResponseData>> {
return api.post<OrderCartListResponseData>(
"/v1/order/cart/list",
data,
headers
headers,
);
}
// 购物车:添加
export function addToCart(
data: OrderCartAddRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderCartAddResponseData>> {
return api.post<OrderCartAddResponseData>(
"/v1/order/cart/add",
data,
headers
headers,
);
}
// 购物车:更新
export function updateCart(
data: OrderCartUpdateRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderCartUpdateResponseData>> {
return api.post<OrderCartUpdateResponseData>(
"/v1/order/cart/update",
data,
headers
headers,
);
}
// 购物车:移除
export function removeFromCart(
data: OrderCartRemoveRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderCartRemoveResponseData>> {
return api.post<OrderCartRemoveResponseData>(
"/v1/order/cart/remove",
data,
headers
headers,
);
}
// 购物车:清空
export function clearCart(
data: OrderCartClearRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderCartClearResponseData>> {
return api.post<OrderCartClearResponseData>(
"/v1/order/cart/clear",
data,
headers
headers,
);
}
// 创建订单
export function createOrder(
data: OrderCreateRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderCreateResponseData>> {
return api.post<OrderCreateResponseData>("/v1/order/create", data, headers);
}
@@ -153,32 +167,106 @@ export function createOrder(
// 查询支付状态
export function queryPaymentStatus(
data: OrderPaymentStatusRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderPaymentStatusResponseData>> {
return api.post<OrderPaymentStatusResponseData>(
"/v1/order/payment/status",
data,
headers
headers,
);
}
// 获取指定订单的详细信息
export function getOrderDetail(
data: OrderDetailRequest,
headers?: Record<string, string>
headers?: Record<string, string>,
): Promise<ApiResponse<OrderDetailResponseData>> {
return api.post<OrderDetailResponseData>("/v1/order/detail", data, headers);
}
// 获取账户余额
export function getAccountBalance(
data: AccountBalanceRequest = {},
headers?: Record<string, string>,
): Promise<ApiResponse<AccountBalanceResponseData>> {
return api.post<AccountBalanceResponseData>(
"/v1/order/buy/get_buy",
data,
headers,
);
}
// 余额支付
export function payWithBalance(
data: BalancePaymentRequest,
headers?: Record<string, string>,
): Promise<ApiResponse<BalancePaymentResponseData>> {
return api.post<BalancePaymentResponseData>(
"/v1/order/buy/buy_payment",
data,
headers,
);
}
// 获取今日秒杀活动信息
export function getSeckillActivityInfo(): Promise<ApiResponse<SeckillActivityInfoResponseData>> {
return api.post<SeckillActivityInfoResponseData>("v1/user/flashsale/get_today_info",{});
export function getSeckillActivityInfo(): Promise<
ApiResponse<SeckillActivityInfoResponseData>
> {
return api.post<SeckillActivityInfoResponseData>(
"v1/user/flashsale/get_today_info",
{},
);
}
// 领取秒杀
export function grabSeckill(): Promise<ApiResponse> {
return api.post("v1/user/flashsale/grab_coupon", {});
}
// 获取活动信息(域名/SSL 板块)
export function getActivityInfo(
data: GetActivityInfoRequest = {},
): Promise<ApiResponse<GetActivityInfoResponseData>> {
return api.post<GetActivityInfoResponseData>("/activity_info", data, {
"X-API-BASE": "/newapi/activity/api",
});
}
// 域名检索(可注册状态)
export function searchDomain(
data: DomainSearchParams,
): Promise<ApiResponse<DomainSearchData>> {
return api.post<DomainSearchData>("/domain/search", data, {
"X-API-BASE": "/newapi/activity/api",
});
}
// 创建订单(普通/秒杀,返回联合类型)
export function createFlashOrder(
data: CreateOrderParams,
): Promise<ApiResponse<CreateOrderResponse>> {
return api.post<CreateOrderResponse>("/order/create", data, {
"X-API-BASE": "/newapi/activity/api",
});
}
// 查询秒杀任务状态(轮询)
export function getSeckillStatus(
data: SeckillStatusParams,
): Promise<ApiResponse<SeckillProcessingData>> {
return api.post<SeckillProcessingData>("/order/seckill", data, {
"X-API-BASE": "/newapi/activity/api",
});
}
// 查询订单支付状态(轮询)
export function getPaymentStatus(
data: PaymentStatusParams,
): Promise<ApiResponse<PaymentStatusData>> {
return api.post<PaymentStatusData>("/order/payment_status", data, {
"X-API-BASE": "/newapi/activity/api",
});
}
/**
* WHOIS查询API
* @param domain 域名
@@ -200,6 +288,14 @@ export const landingApi = {
queryPaymentStatus,
getOrderDetail,
queryWhois,
// 秒杀与活动相关
getSeckillActivityInfo,
grabSeckill,
getActivityInfo,
searchDomain,
createFlashOrder,
getSeckillStatus,
getPaymentStatus,
};
export default landingApi;

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,8 @@ import {
queryPaymentStatus as apiQueryPaymentStatus,
getContactUserDetail,
getOrderDetail as apiGetOrderDetail,
getAccountBalance,
payWithBalance,
} from "@api/landing";
import type {
@@ -65,7 +67,6 @@ import { Datum } from "../types/api-types/contact-get-user-detail";
import type { OrderCreateResponseData } from "../types/api-types/order-create";
import type { OrderDetailResponseData } from "../types/api-types/order-detail";
// ----------------------------
// 全局 Store函数式
// ----------------------------
@@ -106,10 +107,10 @@ type DomainStore = {
type CartStore = { list: Item[]; originalTotal: number; payableTotal: number };
type RealNameStore = { list: Datum[]; current: Datum | null };
const isDev = (): boolean => process.env.NODE_ENV === "development";
// 动态获取登录状态的函数
function getLoginStatus(): boolean {
// return true
return localStorage.getItem("isLogin") === "true";
return localStorage.getItem("isLogin") === "true" || isDev();
}
// 定义全局状态-域名
@@ -281,8 +282,8 @@ function renderDomainList(list: DomainItem[]) {
// 根据购物车内容标记搜索结果中的已选中(已加入购物车)项
const inCartDomainSet = new Set(
(cartState.list || []).map((it: any) =>
String(it.full_domain || it.domain_name || "")
)
String(it.full_domain || it.domain_name || ""),
),
);
console.log(list, "--");
const mapped = list.map((it, index) => {
@@ -308,9 +309,11 @@ function renderDomainList(list: DomainItem[]) {
canShowTags && hasOriginalPrice
? Math.round(
((Number(originalPrice) - Number(price)) / Number(originalPrice)) *
100
100,
)
: 0;
const isCnSuffix = canShowTags && it.suffix === "cn";
const isTopSuffix = canShowTags && it.suffix === "top";
return {
domain: fullDomain,
suffix: it.suffix,
@@ -341,17 +344,19 @@ function renderDomainList(list: DomainItem[]) {
domainState.searchMode === "ai" && Boolean((it as any)?.meaning), // 是否显示AI解释
// 多年价格(首年按优惠价,续费按续费价)
price3Years: formatPriceInteger(
Number(price) + Number(renewPriceDiscount) * 2
Number(price) + Number(renewPriceDiscount) * 2,
),
price5Years: formatPriceInteger(
Number(price) + Number(renewPriceDiscount) * 4
Number(price) + Number(renewPriceDiscount) * 4,
),
price10Years: formatPriceInteger(
Number(price) + Number(renewPriceDiscount) * 9
Number(price) + Number(renewPriceDiscount) * 9,
),
renewPrice3Years: formatPriceInteger(Number(renewPriceDiscount) * 3),
renewPrice5Years: formatPriceInteger(Number(renewPriceDiscount) * 5),
renewPrice10Years: formatPriceInteger(Number(renewPriceDiscount) * 10),
isCnSuffix,
isTopSuffix,
};
});
@@ -368,8 +373,8 @@ function appendDomainList(list: DomainItem[]) {
if (!$) return;
const inCartDomainSet = new Set(
(cartState.list || []).map((it: any) =>
String(it.full_domain || it.domain_name || "")
)
String(it.full_domain || it.domain_name || ""),
),
);
const mapped = list.map((it, index) => {
const price =
@@ -394,7 +399,7 @@ function appendDomainList(list: DomainItem[]) {
canShowTags && hasOriginalPrice
? Math.round(
((Number(originalPrice) - Number(price)) / Number(originalPrice)) *
100
100,
)
: 0;
return {
@@ -423,13 +428,13 @@ function appendDomainList(list: DomainItem[]) {
hasAiMeaning:
domainState.searchMode === "ai" && Boolean((it as any)?.meaning), // 是否显示AI解释
price3Years: formatPriceInteger(
Number(price) + Number(renewPriceDiscount) * 2
Number(price) + Number(renewPriceDiscount) * 2,
),
price5Years: formatPriceInteger(
Number(price) + Number(renewPriceDiscount) * 4
Number(price) + Number(renewPriceDiscount) * 4,
),
price10Years: formatPriceInteger(
Number(price) + Number(renewPriceDiscount) * 9
Number(price) + Number(renewPriceDiscount) * 9,
),
renewPrice3Years: formatPriceInteger(Number(renewPriceDiscount) * 3),
renewPrice5Years: formatPriceInteger(Number(renewPriceDiscount) * 5),
@@ -483,7 +488,7 @@ function renderCart(cart: CartStore) {
const mapped = items.map((item, index) => {
// 后端已按年限计算过汇总,这里价格用于单项展示,不乘年限
const price = Number(
item.total_price ?? item.price ?? item.domain_service_price ?? 0
item.total_price ?? item.price ?? item.domain_service_price ?? 0,
);
const original = Number(item.original_price ?? price);
return {
@@ -587,7 +592,7 @@ async function fetchDomainList(param: DomainQueryCheckRequest) {
if (domainState.searchMode === requestTargetMode) {
setHTML(
"#search-results",
`<div class="text-center py-8 text-red-500">${message}</div>`
`<div class="text-center py-8 text-red-500">${message}</div>`,
);
}
} finally {
@@ -615,15 +620,15 @@ async function fetchAiDomainList(params: {
try {
OverlayManager.showView?.("#search-results", { content: "AI推荐中..." });
// 使用AI推荐接口
const res = await aiDomainQueryCheck({
brand: params.brandName,
industry: params.industry,
// 注意AI接口可能不支持分页和推荐类型根据实际API调整
// p: params.p || 1,
// rows: params.rows || 20,
// recommend_type: params.recommend_type || -1
});
// 使用AI推荐接口
const res = await aiDomainQueryCheck({
brand: params.brandName,
industry: params.industry,
// 注意AI接口可能不支持分页和推荐类型根据实际API调整
// p: params.p || 1,
// rows: params.rows || 20,
// recommend_type: params.recommend_type || -1
});
const data = res.data as any;
const list: DomainItem[] = data || [];
@@ -688,7 +693,7 @@ function calculateSelectedTotals(items: any[]) {
it.originalPrice ??
it.price ??
it.domain_service_price ??
0
0,
);
return sum + Math.max(0, original);
}, 0);
@@ -727,7 +732,7 @@ async function buildPaymentModalContent(init: boolean = true) {
const idMasked = template?.id_number
? String(template.id_number).replace(
/(\d{6})\d{8}(\d{3}[0-9Xx])/,
"$1****$2"
"$1****$2",
)
: template?.id_number_masked || "";
const selectedTemplateName = idMasked
@@ -755,7 +760,7 @@ async function buildPaymentModalContent(init: boolean = true) {
totalItems: itemsWithFormatted.length,
cartItemsHtml: renderTemplateList(
TPL_PAYMENT_CART_ITEM,
itemsWithFormatted
itemsWithFormatted,
),
});
@@ -765,7 +770,8 @@ async function buildPaymentModalContent(init: boolean = true) {
payableTotal: Number(cartState.payableTotal ?? 0),
discount: Math.max(
0,
Number(cartState.originalTotal ?? 0) - Number(cartState.payableTotal ?? 0)
Number(cartState.originalTotal ?? 0) -
Number(cartState.payableTotal ?? 0),
),
};
@@ -825,7 +831,7 @@ async function showPaymentModal() {
$body.css("touch-action", prevTouchAction || "");
$body.css("overscroll-behavior", prevOverscroll || "");
$body.removeData(
"cartPaymentModalOverflow cartPaymentModalTouchAction cartPaymentModalOverscroll"
"cartPaymentModalOverflow cartPaymentModalTouchAction cartPaymentModalOverscroll",
);
},
});
@@ -849,15 +855,7 @@ function buildOrderItemsHtml(items: any[]) {
const mapped = (items || []).map((it: any) => {
const years = Math.max(1, Number(it.years || 1));
// 注意后端已给出总价total_price 或等效字段),避免再次乘以 years
const total = Number(
it.total_price != null
? it.total_price
: it.price != null
? it.price * years
: it.domain_service_price != null
? it.domain_service_price * years
: 0
);
const total = Number(it.price);
const domain = String(it.full_domain || it.domain_name || "");
const template = (realNameState.current as any) || {};
const templateName =
@@ -879,10 +877,10 @@ function buildOrderItemsHtml(items: any[]) {
/**
* 显示支付界面(不跳转外部支付页)
*/
function showPaymentInterface() {
async function showPaymentInterface() {
const $ = safe$();
const selectedItems = (cartState.list || []).filter(
(it: any) => (it.selected ?? 1) !== 0
(it: any) => (it.selected ?? 1) !== 0,
);
if (selectedItems.length === 0) {
NotificationManager.show?.({
@@ -896,7 +894,7 @@ function showPaymentInterface() {
calculateSelectedTotals(selectedItems);
const hasDiscount = discount > 0;
// 基础用户/余额信息(占位,实际可接入账户接口)
// 基础用户信息
const template = (realNameState.current as any) || {};
const phone =
template?.phone ||
@@ -906,9 +904,21 @@ function showPaymentInterface() {
"***";
const userName = template?.owner_name || template?.contact_person || "用户";
const userInitial = String(userName).trim().slice(0, 1) || "用";
const accountBalance = 0; // TODO: 可接后端账户余额接口
const remainingBalance = Math.max(0, accountBalance - payableTotal);
const insufficientBalance = payableTotal > accountBalance;
// 获取账户余额
let accountBalance = 0;
let insufficientBalance = false;
try {
const balanceRes = await getAccountBalance();
if (balanceRes?.data?.balance !== undefined) {
accountBalance = balanceRes.data.balance / 100;
insufficientBalance = accountBalance < payableTotal;
}
} catch (err) {
console.error("获取账户余额失败", err);
// 余额获取失败时,默认为余额不足
insufficientBalance = true;
}
const content = renderTemplate(TPL_PAYMENT_INTERFACE, {
orderItemsHtml: buildOrderItemsHtml(selectedItems),
@@ -917,10 +927,9 @@ function showPaymentInterface() {
formattedPayableTotal: formatPrice(payableTotal),
hasDiscount,
accountBalance: formatPrice(accountBalance),
remainingBalance: formatPrice(remainingBalance),
insufficientBalance,
userPhone: phone,
userInitial,
insufficientBalance,
isWechatSelected: selectedPaymentMethod === "wechat",
isAlipaySelected: selectedPaymentMethod === "alipay",
isBalanceSelected: selectedPaymentMethod === "balance",
@@ -954,17 +963,17 @@ function showPaymentInterface() {
// 初始显示二维码区域(微信)
if ($) {
if (selectedPaymentMethod === "balance") {
$("#qr-code-section").addClass("hidden");
$("#balance-payment-section").removeClass("hidden");
$("#qr-code-section").addClass("!hidden");
$("#balance-payment-section").removeClass("!hidden");
} else if (selectedPaymentMethod === "alipay") {
$("#balance-payment-section").addClass("hidden");
$("#qr-code-section").removeClass("hidden");
$("#balance-payment-section").addClass("!hidden");
$("#qr-code-section").removeClass("!hidden");
$("#qr-code-title").text("请使用支付宝扫码支付");
$("#qr-code-tip").text("请在手机上打开支付宝,扫描上方二维码完成支付");
generateQRCodeForSelectedMethod();
} else {
$("#balance-payment-section").addClass("hidden");
$("#qr-code-section").removeClass("hidden");
$("#balance-payment-section").addClass("!hidden");
$("#qr-code-section").removeClass("!hidden");
$("#qr-code-title").text("请使用微信扫码支付");
$("#qr-code-tip").text("请在手机上打开微信,扫描上方二维码完成支付");
generateQRCodeForSelectedMethod();
@@ -998,20 +1007,29 @@ function bindPaymentInterfaceEvents() {
selectedPaymentMethod = method;
if (method === "balance") {
$("#qr-code-section").addClass("hidden");
$("#balance-payment-section").removeClass("hidden");
// 切换到余额支付:隐藏二维码,停止轮询
$("#qr-code-section").addClass("!hidden");
$("#balance-payment-section").removeClass("!hidden");
if (paymentPollTimer) {
clearInterval(paymentPollTimer);
paymentPollTimer = null;
}
} else if (method === "wechat") {
$("#balance-payment-section").addClass("hidden");
$("#qr-code-section").removeClass("hidden");
// 切换到微信支付:显示二维码,启动轮询
$("#balance-payment-section").addClass("!hidden");
$("#qr-code-section").removeClass("!hidden");
$("#qr-code-title").text("请使用微信扫码支付");
$("#qr-code-tip").text("请在手机上打开微信,扫描上方二维码完成支付");
generateQRCodeForSelectedMethod();
startPaymentPolling();
} else if (method === "alipay") {
$("#balance-payment-section").addClass("hidden");
$("#qr-code-section").removeClass("hidden");
// 切换到支付宝支付:显示二维码,启动轮询
$("#balance-payment-section").addClass("!hidden");
$("#qr-code-section").removeClass("!hidden");
$("#qr-code-title").text("请使用支付宝扫码支付");
$("#qr-code-tip").text("请在手机上打开支付宝,扫描上方二维码完成支付");
generateQRCodeForSelectedMethod();
startPaymentPolling();
}
updateSegmentedSlider();
});
@@ -1030,8 +1048,38 @@ function bindPaymentInterfaceEvents() {
});
return;
}
// 此处可接余额扣款接口;当前仅进行状态轮询
startPaymentPolling();
const $btn = $(this);
const originalText = $btn.text();
try {
// 禁用按钮并显示加载状态
$btn.prop("disabled", true).text("支付中...");
// 调用余额支付接口
const res = await payWithBalance({
order_no: currentOrderNo,
});
if (res?.code === 0) {
// 支付成功,直接处理成功逻辑
handlePaymentSuccess();
} else {
// 支付失败,显示错误信息
NotificationManager.show?.({
type: "error",
message: res?.msg || "余额支付失败,请重试",
});
}
} catch (err: any) {
console.error("余额支付失败", err);
NotificationManager.show?.({
type: "error",
message: err?.message || "余额支付失败,请重试",
});
} finally {
// 恢复按钮状态
$btn.prop("disabled", false).text(originalText);
}
});
}
@@ -1062,19 +1110,19 @@ function updatePaymentSummary() {
const $ = safe$();
if (!$) return;
const selected = (cartState.list || []).filter(
(it: any) => (it.selected ?? 1) !== 0
(it: any) => (it.selected ?? 1) !== 0,
);
const { originalTotal, payableTotal, discount } =
calculateSelectedTotals(selected);
$("#cart-payment-modal .payment-section .line-through").text(
formatPrice(originalTotal)
formatPrice(originalTotal),
);
$("#cart-payment-modal .payment-section .text-green-600").text(
`-${formatPrice(discount)}`
`-${formatPrice(discount)}`,
);
$("#cart-payment-modal .payment-section .text-orange-500.font-bold").text(
formatPrice(payableTotal)
formatPrice(payableTotal),
);
$("#delete-selected-link").toggleClass("hidden", !selected.length);
@@ -1197,7 +1245,7 @@ function bindPaymentModalEvents() {
.off("click", "#confirm-payment-btn")
.on("click", "#confirm-payment-btn", async function () {
const selectedItems = (cartState.list || []).filter(
(it: any) => (it.selected ?? 1) !== 0
(it: any) => (it.selected ?? 1) !== 0,
);
if (selectedItems.length === 0) {
@@ -1252,7 +1300,7 @@ function bindPaymentModalEvents() {
}
// 展示站内支付界面
showPaymentInterface();
await showPaymentInterface();
});
// 年限选择(弹窗内)
@@ -1273,7 +1321,8 @@ function bindPaymentModalEvents() {
const options = [1, 2, 3, 5, 10];
const html = options
.map(
(y) => `<div class="select-option" data-value="${y}">${y}年</div>`
(y) =>
`<div class="select-option" data-value="${y}">${y}年</div>`,
)
.join("");
$menu.html(html);
@@ -1284,7 +1333,7 @@ function bindPaymentModalEvents() {
const index = Number($selector.data("index"));
const item = (cartState.list || [])[index] as any;
const currentYears = Number(
item?.years || $selector.data("value") || 1
item?.years || $selector.data("value") || 1,
);
$menu
.find(`.select-option[data-value="${currentYears}"]`)
@@ -1342,14 +1391,14 @@ function bindPaymentModalEvents() {
$menu.hide();
}
});
}
},
);
// 实名模板选择(弹窗内)
$(document)
.off(
"click",
".cart-payment-modal .real-name-template-selector .select-display"
".cart-payment-modal .real-name-template-selector .select-display",
)
.on(
"click",
@@ -1455,7 +1504,7 @@ function bindPaymentModalEvents() {
found.id_number_masked ||
found.id_number.replace(
/(\d{6})\d{8}(\d{3}[0-9Xx])/,
"$1****$2"
"$1****$2",
) ||
"";
$selector
@@ -1464,7 +1513,7 @@ function bindPaymentModalEvents() {
}
$menu.remove();
});
}
},
);
// 批量删除选中(弹窗内)
@@ -1473,7 +1522,7 @@ function bindPaymentModalEvents() {
.on("click", "#delete-selected-link", async function (e: any) {
e.preventDefault();
const selectedItems = (cartState.list || []).filter(
(it: any) => (it.selected ?? 1) !== 0
(it: any) => (it.selected ?? 1) !== 0,
);
if (selectedItems.length === 0) {
NotificationManager.show?.({
@@ -1571,7 +1620,7 @@ function generateQRCodeForSelectedMethod() {
($qr as any).qrcode({ text: qrText, width: 180, height: 180 });
} else {
$qr.append(
`<div class="w-180 h-180 flex items-center justify-center text-xs text-gray-500">二维码插件缺失,显示链接:${qrText}</div>`
`<div class="w-180 h-180 flex items-center justify-center text-xs text-gray-500">二维码插件缺失,显示链接:${qrText}</div>`,
);
}
$qrImage.append($qr);
@@ -1642,7 +1691,7 @@ async function showPaymentSuccess() {
years: it.years || 1,
templateName: realNameState.current?.template_name,
formattedPrice: formatPrice(
Number(it.total_amount || it.discount_price || it.one_price || 0)
Number(it.total_amount || it.discount_price || it.one_price || 0),
),
}));
@@ -1654,7 +1703,7 @@ async function showPaymentSuccess() {
: formatPrice(0);
const discountNumber = Math.max(
0,
Number(formattedOriginalTotal) - Number(formattedPayableTotal)
Number(formattedOriginalTotal) - Number(formattedPayableTotal),
);
const successHtml = renderTemplate("order-success-template", {
@@ -1668,8 +1717,8 @@ async function showPaymentSuccess() {
selectedPaymentMethod === "balance"
? "余额支付"
: selectedPaymentMethod === "alipay"
? "支付宝"
: "微信",
? "支付宝"
: "微信",
paymentTime: new Date().toLocaleString(),
transactionId: (currentOrderData as any)?.order_no || "",
});
@@ -1765,7 +1814,7 @@ async function fetchRealNameList() {
const first =
realNameState.list.find(
(t) =>
(t as any).template_status === "approved" || (t as any).status === 1
(t as any).template_status === "approved" || (t as any).status === 1,
) || null;
realNameState.current = first || realNameState.list[0] || null;
@@ -2212,7 +2261,7 @@ function bindEvents() {
const target = $(e.target);
if (
!target.closest(
"#mobile-cart-dropdown, #mobile-cart-toggle, #mobile-cart-bar"
"#mobile-cart-dropdown, #mobile-cart-toggle, #mobile-cart-bar",
).length &&
$("#mobile-cart-dropdown").hasClass("open")
) {
@@ -2270,7 +2319,7 @@ function bindEvents() {
$btn.prop("disabled", false);
OverlayManager.hideGlobal?.();
}
}
},
);
// 移动端下拉:年限选择
@@ -2365,7 +2414,7 @@ function bindEvents() {
$menu.remove();
}
});
}
},
);
const triggerSearch = () => {
@@ -2470,7 +2519,7 @@ function bindEvents() {
setTimeout(() => $input.removeClass("input-highlight"), 1500);
}
} catch {}
}
},
);
// 4) 筛选按钮:切换 recommend_type 并触发请求
@@ -2501,7 +2550,7 @@ function bindEvents() {
domainState.param.recommend_type = recommend;
updateUrlParam(
"recommend_type",
recommend === -1 ? undefined : String(recommend)
recommend === -1 ? undefined : String(recommend),
);
// 不修改 domain交由订阅在 param 变化时重新拉取
});
@@ -2525,7 +2574,7 @@ function bindEvents() {
// 从当前查询结果中查找后缀;若找不到则回退解析
const item = (domainState.list || []).find(
(it: any) => String((it as any).domain) === fullDomain
(it: any) => String((it as any).domain) === fullDomain,
) as any;
let suffix: string = (item && (item as any).suffix) || "";
if (!suffix) {
@@ -2559,7 +2608,7 @@ function bindEvents() {
type: "success",
message: "已加入购物车",
}),
1000
1000,
);
// 立即更新按钮文案与状态(无等待)
@@ -2918,9 +2967,9 @@ function bindEvents() {
// 跳转到独立的WHOIS查询页面
window.open(
`https://www.bt.cn/new/domain-whois.html?domain=${encodeURIComponent(
domain
domain,
)}`,
"_blank"
"_blank",
);
});
}
@@ -2964,7 +3013,7 @@ function renderMobileCartList(cart: CartStore) {
const rows = items.map((item: any, index: number) => {
const years = Number(item.years || 1);
const unit = Number(
item.total_price ?? item.price ?? item.domain_service_price ?? 0
item.total_price ?? item.price ?? item.domain_service_price ?? 0,
);
const domain = String(item.full_domain || item.domain_name || "");
return `
@@ -3011,14 +3060,14 @@ function updateMobileCartBar(cart: CartStore) {
$bar.removeClass("empty");
$empty.hide();
$(
".mobile-cart-summary, .mobile-cart-discount, #mobile-cart-toggle, #mobile-checkout-button"
".mobile-cart-summary, .mobile-cart-discount, #mobile-cart-toggle, #mobile-checkout-button",
).show();
} else {
// 空态:展示提示,仅显示底部条
$bar.addClass("empty");
$empty.show();
$(
".mobile-cart-summary, .mobile-cart-discount, #mobile-cart-toggle, #mobile-checkout-button"
".mobile-cart-summary, .mobile-cart-discount, #mobile-cart-toggle, #mobile-checkout-button",
).hide();
// 关闭下拉
$dropdown.removeClass("open").attr("aria-hidden", "true");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,63 +4,72 @@
* 使用建议:配合 `@types/template-data-map.d.ts` 的模块增强,
* - `renderTemplate(TPL_EMPTY_STATE, { icon: '...', text: '...' })` 将获得强类型校验
*/
import type { TemplateDataMap } from '../types/template-data-map'
import type { TemplateDataMap } from "../types/template-data-map";
/** 所有模板 ID 的联合类型 */
export type TemplateId = keyof TemplateDataMap
export type TemplateId = keyof TemplateDataMap;
// 空状态与列表项
export const TPL_EMPTY_STATE = 'empty-state-template' as const
export const TPL_SEARCH_RESULT_ITEM = 'search-result-item-template' as const
export const TPL_VIEW_MORE_BUTTON = 'view-more-button-template' as const
export const TPL_EMPTY_STATE = "empty-state-template" as const;
export const TPL_SEARCH_RESULT_ITEM = "search-result-item-template" as const;
export const TPL_VIEW_MORE_BUTTON = "view-more-button-template" as const;
// 购物车
export const TPL_CART_ITEM = 'cart-item-template' as const
export const TPL_CART_ITEM = "cart-item-template" as const;
// 模板选项
export const TPL_TEMPLATE_OPTION = 'template-option-template' as const
export const TPL_TEMPLATE_SELECT_OPTION = 'template-select-option-template' as const
export const TPL_TEMPLATE_OPTION = "template-option-template" as const;
export const TPL_TEMPLATE_SELECT_OPTION =
"template-select-option-template" as const;
// 模态框
export const TPL_MODAL_CONTAINER = 'modal-container-template' as const
export const TPL_MODAL_CONTENT = 'modal-content-template' as const
export const TPL_MODAL_BUTTON = 'modal-button-template' as const
export const TPL_MODAL_CONTAINER = "modal-container-template" as const;
export const TPL_MODAL_CONTENT = "modal-content-template" as const;
export const TPL_MODAL_BUTTON = "modal-button-template" as const;
// 通知
export const TPL_NOTIFICATION_CONTAINER = 'notification-container-template' as const
export const TPL_NOTIFICATION_CONTENT = 'notification-content-template' as const
export const TPL_NOTIFICATION_CONTAINER =
"notification-container-template" as const;
export const TPL_NOTIFICATION_CONTENT =
"notification-content-template" as const;
// 购买弹窗(域名单项)
export const TPL_BUY_MODAL_TEMPLATE_SELECTOR = 'buy-modal-template-selector-template' as const
export const TPL_BUY_MODAL_PRICE_INFO = 'buy-modal-price-info-template' as const
export const TPL_BUY_MODAL_WARNING = 'buy-modal-warning-template' as const
export const TPL_BUY_MODAL_CONTENT = 'buy-modal-content-template' as const
export const TPL_BUY_MODAL_TEMPLATE_SELECTOR =
"buy-modal-template-selector-template" as const;
export const TPL_BUY_MODAL_PRICE_INFO =
"buy-modal-price-info-template" as const;
export const TPL_BUY_MODAL_WARNING = "buy-modal-warning-template" as const;
export const TPL_BUY_MODAL_CONTENT = "buy-modal-content-template" as const;
// 支付弹窗 - 购物车项目与列表
export const TPL_PAYMENT_CART_ITEM = 'payment-cart-item-template' as const
export const TPL_PAYMENT_MODAL_CART_ITEMS = 'payment-modal-cart-items-template' as const
export const TPL_PAYMENT_MODAL_WARNING = 'payment-modal-warning-template' as const
export const TPL_PAYMENT_CART_ITEM = "payment-cart-item-template" as const;
export const TPL_PAYMENT_MODAL_CART_ITEMS =
"payment-modal-cart-items-template" as const;
export const TPL_PAYMENT_MODAL_WARNING =
"payment-modal-warning-template" as const;
// 支付方式/底部汇总
export const TPL_PAYMENT_METHOD_SELECTOR = 'payment-method-selector-template' as const
export const TPL_PAYMENT_MODAL_PAYMENT_SECTION = 'payment-modal-payment-section-template' as const
export const TPL_PAYMENT_MODAL_PAYMENT_SECTION =
"payment-modal-payment-section-template" as const;
// 支付弹窗完整内容
export const TPL_PAYMENT_MODAL_CONTENT = 'payment-modal-content-template' as const
export const TPL_PAYMENT_MODAL_CONTENT =
"payment-modal-content-template" as const;
// 支付界面
export const TPL_PAYMENT_INTERFACE = 'payment-interface-template' as const
export const TPL_PAYMENT_INTERFACE = "payment-interface-template" as const;
// 订单与支付结果
export const TPL_ORDER_ITEM = 'order-item-template' as const
export const TPL_ORDER_SUCCESS = 'order-success-template' as const
export const TPL_ORDER_ITEM = "order-item-template" as const;
export const TPL_ORDER_SUCCESS = "order-success-template" as const;
// 确认删除
export const TPL_CONFIRM_DELETE_MODAL = 'confirm-delete-modal-template' as const
export const TPL_CONFIRM_DELETE_MODAL =
"confirm-delete-modal-template" as const;
// 域名注册协议
export const TPL_DOMAIN_AGREEMENT_MODAL = 'domain-agreement-modal-template' as const
export const TPL_DOMAIN_AGREEMENT_MODAL =
"domain-agreement-modal-template" as const;
/** 模板常量字典,便于遍历或注入 */
export const TEMPLATES = {
@@ -82,7 +91,6 @@ export const TEMPLATES = {
TPL_PAYMENT_CART_ITEM,
TPL_PAYMENT_MODAL_CART_ITEMS,
TPL_PAYMENT_MODAL_WARNING,
TPL_PAYMENT_METHOD_SELECTOR,
TPL_PAYMENT_MODAL_PAYMENT_SECTION,
TPL_PAYMENT_MODAL_CONTENT,
TPL_PAYMENT_INTERFACE,
@@ -90,4 +98,4 @@ export const TEMPLATES = {
TPL_ORDER_SUCCESS,
TPL_CONFIRM_DELETE_MODAL,
TPL_DOMAIN_AGREEMENT_MODAL,
} as const
} as const;

View File

@@ -0,0 +1,185 @@
/* Consolidated domain flash types. Do not edit by hand unless synchronizing spec. */
// --- flashsale.d.ts ---
export type SeckillActivityInfoResponseData = {
// 是否可以抢券false表示不可抢
can_grab: boolean;
// 优惠券价格,单位:元
coupon_price: string;
// 当前系统时间
current_time: string;
// 活动描述
description: string;
// 活动结束时间
end_time: string;
// 抢券状态0=可抢1=已抢到未使用2=已使用3=活动未开始4=活动已结束5=已抢完
grab_status: number;
// 是否有活动true表示有活动
has_activity: boolean;
// 剩余优惠券数量
remaining_coupons: number;
// 活动开始时间
start_time: string;
// 状态文本描述
status_text: string;
// 总优惠券数量
total_coupons: number;
// 用户优惠券状态
user_coupon: {
// 0: 未领取1: 已领取2: 已使用
status: number;
};
};
// --- domain-flash-activity-info.d.ts ---
export interface GetActivityInfoRequest {
activity_id?: string;
}
export interface ActivityData {
type: number;
starttime: string;
endtime: string;
detail: ActivityDetail[];
discount_rate?: number;
discount_amount?: number;
}
export interface ActivityDetail {
/** 活动价 */
activity_price: string;
/** 购买按钮文案 */
buy_message: string;
/** 秒杀支付状态1 可参与 / 2 已参与过 / 3 售空 */
buy_status: number;
// 0.未开始 1.进行中 2.已结束 3.已暂停
status: number;
/** 周期 */
cycle: number;
/** 赠品信息 */
gifts: Gift[];
/** 活动产品id */
id: number;
/** 产品名称 */
name: string;
/** 产品名称2 */
product_name: string;
/** 数量 */
num: number;
/** 原价 */
original_price: string;
/** 活动id */
pid: number;
/** 产品类型1 官网产品 / 2 锐安信证书 / 3 宝塔证书 / 4 域名 */
product_class: number;
/** 产品id */
product_id: number;
/** 剩余库存null 表示无限制 */
remaining_stock: null;
/** 秒杀配置type=3 时存在) */
seckill: SeckillConfig | null;
seckill_daily: {
activity_id: number;
detail_id: number;
end_time: string;
id: number;
is_first_day: number;
remaining_stock: number;
seckill_date: string;
seckill_id: number;
sold_count: number;
start_time: string;
status: number;
total_stock: number;
};
/** 活动类型(同 ActivityData.type */
type: number;
/** 周期单位:'month' | 'year' | 'day' */
unit: string;
[property: string]: unknown;
}
export interface SeckillConfig {
activity_id: number;
daily_start_time: string;
detail_id: number;
id: number;
max_buy_count: number;
price: string;
seckill_end_date: string;
seckill_start_date: string;
status: number;
[property: string]: unknown;
}
export type GetActivityInfoResponseData = ActivityData[];
// --- domain-flash-search.d.ts ---
export interface DomainSearchParams {
domain: string;
}
export type DomainSearchData = Record<string, unknown>;
// --- domain-flash-order.d.ts ---
export interface CreateOrderParams {
detail_id: number;
domain?: string;
seckill_daily_id: number;
}
export interface PaymentUrls {
wechat: string;
alipay: string;
}
export interface NormalOrderData {
order_title: string;
order_no: string;
amount: number;
payment_type?: string;
expire_time: number | string;
payment_urls: PaymentUrls;
}
export interface SeckillOrderData {
task_id: string;
}
export type CreateOrderResponse = NormalOrderData | SeckillOrderData;
// --- domain-flash-seckill-status.d.ts ---
export interface SeckillStatusParams {
task_id: string;
}
export type SeckillOrderCreatedData = NormalOrderData & { success: boolean };
export interface SeckillProcessingData {
task_status?: "completed" | "processing" | "failed";
result: SeckillOrderCreatedData;
}
/* Auto-generated based on domainflash.md. Do not edit by hand. */
/** 支付状态查询入参 */
export interface PaymentStatusParams {
/** 订单号 */
order_no: string;
}
/** 支付状态返回status === 1 表示已支付) */
export interface PaymentStatusData {
/** 订单号 */
order_no: string;
/** 支付时间(字符串或时间戳) */
pay_time?: string | number;
/** 支付方式 */
pay_type?: string;
/** 支付状态1=已支付0=未支付 */
status: 1 | 0;
/** 交易号(可选) */
transaction_id?: string;
/** 其他字段(兼容不同后端) */
[key: string]: any;
}

View File

@@ -1,29 +0,0 @@
export type SeckillActivityInfoResponseData = {
// 是否可以抢券false表示不可抢
can_grab: boolean;
// 优惠券价格,单位:元
coupon_price: string;
// 当前系统时间
current_time: string;
// 活动描述
description: string;
// 活动结束时间
end_time: string;
// 抢券状态0=可抢1=已抢到未使用2=已使用3=活动未开始4=活动已结束5=已抢完
grab_status: number;
// 是否有活动true表示有活动
has_activity: boolean;
// 剩余优惠券数量
remaining_coupons: number;
// 活动开始时间
start_time: string;
// 状态文本描述
status_text: string;
// 总优惠券数量
total_coupons: number;
// 用户优惠券状态
user_coupon: {
// 0: 未领取1: 已领取2: 已使用
status: number;
};
};

View File

@@ -1,4 +1,5 @@
export * from './domain'
export * from './api'
export * from './utils'
export * from './template-data-map'
export * from "./domain";
export * from "./api";
export * from "./utils";
export * from "./template-data-map";
export * from "./api-types/domain-flash";

View File

@@ -8,44 +8,44 @@
// 单项结构声明
export interface OrderSuccessDomainItem {
domain: string
years: number | string
templateName: string
formattedPrice: string
domain: string;
years: number | string;
templateName: string;
formattedPrice: string;
}
export interface ConfirmDeleteItem {
domain: string
years: number | string
formattedPrice: string
domain: string;
years: number | string;
formattedPrice: string;
}
interface SearchResultItem {
domain: string
isLast: boolean
isRegistered: boolean
statusText: string
hasRecommended?: boolean
hasPopular: boolean
hasDiscount: boolean
whoisUrl: string
domain: string;
isLast: boolean;
isRegistered: boolean;
statusText: string;
hasRecommended?: boolean;
hasPopular: boolean;
hasDiscount: boolean;
whoisUrl: string;
// 价格
price: number
originalPrice: number
formattedPrice: string
formattedOriginalPrice: string
hasOriginalPrice: boolean
price: number;
originalPrice: number;
formattedPrice: string;
formattedOriginalPrice: string;
hasOriginalPrice: boolean;
// 多年价格展示
price3Years: string
price5Years: string
price10Years: string
renewPrice3Years: string
renewPrice5Years: string
renewPrice10Years: string
price3Years: string;
price5Years: string;
price10Years: string;
renewPrice3Years: string;
renewPrice5Years: string;
renewPrice10Years: string;
// 购物车状态
isInCart: boolean
isInCart: boolean;
// 角标
discountPercent?: number | string
discountPercent?: number | string;
}
/**
@@ -53,224 +53,221 @@ interface SearchResultItem {
*/
export interface TemplateDataMap {
// 空状态
'empty-state-template': {
icon: string
text: string
hint?: string
}
"empty-state-template": {
icon: string;
text: string;
hint?: string;
};
// 搜索结果项(域名)
'search-result-item-template': {
list: SearchResultItem[]
}
"search-result-item-template": {
list: SearchResultItem[];
};
// 查看更多按钮
'view-more-button-template': Record<string, never>
"view-more-button-template": Record<string, never>;
// 购物车项目
'cart-item-template': {
index: number
domain: string
years: number | string
formattedOriginalPrice: string
formattedPrice: string
}
"cart-item-template": {
index: number;
domain: string;
years: number | string;
formattedOriginalPrice: string;
formattedPrice: string;
};
// 实名模板选项(通用)
'template-option-template': {
id: string | number
name: string
desc?: string
formattedPrice?: string
}
"template-option-template": {
id: string | number;
name: string;
desc?: string;
formattedPrice?: string;
};
// 模态框容器与内容
'modal-container-template': {
id: string
className?: string
zIndex?: number | string
}
'modal-content-template': {
title?: string
closable?: boolean
content: string
hasButtons?: boolean
buttonsHtml?: string
sizeClass?: string
}
'modal-button-template': {
id?: string
typeClass?: string
className?: string
text: string
onClick?: string
}
"modal-container-template": {
id: string;
className?: string;
zIndex?: number | string;
};
"modal-content-template": {
title?: string;
closable?: boolean;
content: string;
hasButtons?: boolean;
buttonsHtml?: string;
sizeClass?: string;
};
"modal-button-template": {
id?: string;
typeClass?: string;
className?: string;
text: string;
onClick?: string;
};
// 通知
'notification-container-template': {
id: string
positionClass?: string
zIndex?: number | string
}
'notification-content-template': {
bgClass?: string
textClass?: string
iconClass?: string
iconColor?: string
title?: string
message: string
closable?: boolean
}
"notification-container-template": {
id: string;
positionClass?: string;
zIndex?: number | string;
};
"notification-content-template": {
bgClass?: string;
textClass?: string;
iconClass?: string;
iconColor?: string;
title?: string;
message: string;
closable?: boolean;
};
// 购买弹窗 - 模板选择与价格/提示
'buy-modal-template-selector-template': {
selectedTemplateName: string
}
'buy-modal-price-info-template': {
formattedOriginalPrice: string
hasDiscount?: boolean
formattedDiscount?: string
formattedPrice: string
}
'buy-modal-warning-template': Record<string, never>
'buy-modal-content-template': {
domain: string
templateSelectorHtml: string
priceInfoHtml: string
warningHtml: string
}
"buy-modal-template-selector-template": {
selectedTemplateName: string;
};
"buy-modal-price-info-template": {
formattedOriginalPrice: string;
hasDiscount?: boolean;
formattedDiscount?: string;
formattedPrice: string;
};
"buy-modal-warning-template": Record<string, never>;
"buy-modal-content-template": {
domain: string;
templateSelectorHtml: string;
priceInfoHtml: string;
warningHtml: string;
};
// 支付弹窗 - 购物车项与列表
'payment-cart-item-template': {
index: number
selected?: boolean
domain: string
years: number | string
formattedTotalPrice: string
formattedUnitPrice: string
}
'payment-modal-cart-items-template': {
totalItems: number
cartItemsHtml: string
}
'payment-modal-warning-template': Record<string, never>
// 支付方式选择
'payment-method-selector-template': {
isWechatSelected?: boolean
isAlipaySelected?: boolean
isBalanceSelected?: boolean
accountBalance?: string | number
insufficientBalance?: boolean
}
"payment-cart-item-template": {
index: number;
selected?: boolean;
domain: string;
years: number | string;
formattedTotalPrice: string;
formattedUnitPrice: string;
};
"payment-modal-cart-items-template": {
totalItems: number;
cartItemsHtml: string;
};
"payment-modal-warning-template": Record<string, never>;
// 支付弹窗底部汇总与模板选择
'payment-modal-payment-section-template': {
selectedTemplateId?: string | number
selectedTemplateName: string
formattedOriginalTotal: string
formattedDiscount?: string
formattedPayableTotal: string
}
"payment-modal-payment-section-template": {
selectedTemplateId?: string | number;
selectedTemplateName: string;
formattedOriginalTotal: string;
formattedDiscount?: string;
formattedPayableTotal: string;
};
// 支付弹窗整体内容
'payment-modal-content-template': {
warningHtml: string
cartItemsHtml: string
paymentSectionHtml: string
}
"payment-modal-content-template": {
warningHtml: string;
cartItemsHtml: string;
paymentSectionHtml: string;
};
// 支付界面(独立页)
'payment-interface-template': {
orderItemsHtml: string
formattedOriginalTotal: string
hasDiscount?: boolean
formattedDiscount?: string
formattedPayableTotal: string
isWechatSelected?: boolean
isAlipaySelected?: boolean
isBalanceSelected?: boolean
insufficientBalance?: boolean
}
"payment-interface-template": {
orderItemsHtml: string;
formattedOriginalTotal: string;
hasDiscount?: boolean;
formattedDiscount?: string;
formattedPayableTotal: string;
isWechatSelected?: boolean;
isAlipaySelected?: boolean;
isBalanceSelected?: boolean;
insufficientBalance?: boolean;
};
// 订单项目(支付界面左侧)
'order-item-template': {
domain: string
years: number | string
templateName: string
formattedTotalPrice: string
}
"order-item-template": {
domain: string;
years: number | string;
templateName: string;
formattedTotalPrice: string;
};
// 订单成功页
'order-success-template': {
orderNumber: string
domains: OrderSuccessDomainItem[]
formattedOriginalTotal: string
hasDiscount?: boolean
formattedDiscount?: string
formattedPayableTotal: string
paymentMethod: string
paymentTime: string
transactionId: string
}
"order-success-template": {
orderNumber: string;
domains: OrderSuccessDomainItem[];
formattedOriginalTotal: string;
hasDiscount?: boolean;
formattedDiscount?: string;
formattedPayableTotal: string;
paymentMethod: string;
paymentTime: string;
transactionId: string;
};
// 实名模板下拉选项
'template-select-option-template': {
id: string | number
name: string
desc?: string
selected?: boolean
}
"template-select-option-template": {
id: string | number;
name: string;
desc?: string;
selected?: boolean;
};
// 确认删除弹窗
'confirm-delete-modal-template': {
itemCount: number
items: ConfirmDeleteItem[]
}
"confirm-delete-modal-template": {
itemCount: number;
items: ConfirmDeleteItem[];
};
// 域名注册协议模态窗口
'domain-agreement-modal-template': Record<string, never>
"domain-agreement-modal-template": Record<string, never>;
// WHOIS查询模态窗口
'whois-modal-template': {
domain: string
isLoading?: boolean
hasError?: boolean
errorMessage?: string
hasData?: boolean
domainName?: string
status?: string
statusClass?: string
registrar?: string
creationDate?: string
expirationDate?: string
updatedDate?: string
registrant?: string
registrarName?: string
emails?: string
nameServers?: string[]
rawData?: string
}
"whois-modal-template": {
domain: string;
isLoading?: boolean;
hasError?: boolean;
errorMessage?: string;
hasData?: boolean;
domainName?: string;
status?: string;
statusClass?: string;
registrar?: string;
creationDate?: string;
expirationDate?: string;
updatedDate?: string;
registrant?: string;
registrarName?: string;
emails?: string;
nameServers?: string[];
rawData?: string;
};
}
// ---------------- 模块增强:为 @utils 提供模板 ID 智能提示与数据类型校验 ----------------
import type { RenderTemplateInstance } from '@utils'
import type { RenderTemplateInstance } from "@utils";
declare module '@utils' {
declare module "@utils" {
/** 根据模板 ID 精确定义 renderTemplate 的数据类型 */
// 非响应式
export function renderTemplate<K extends keyof TemplateDataMap>(templateId: K, data: TemplateDataMap[K]): string
export function renderTemplate<K extends keyof TemplateDataMap>(
templateId: K,
data: TemplateDataMap[K],
): string;
// 响应式
export function renderTemplate<K extends keyof TemplateDataMap, T extends TemplateDataMap[K]>(
export function renderTemplate<
K extends keyof TemplateDataMap,
T extends TemplateDataMap[K],
>(
templateId: K,
data: T,
options: { reactive: true }
): RenderTemplateInstance<T>
options: { reactive: true },
): RenderTemplateInstance<T>;
/** 模板列表渲染 */
export function renderTemplateList<K extends keyof TemplateDataMap>(
templateId: K,
dataArray: Array<TemplateDataMap[K]>
): string
dataArray: Array<TemplateDataMap[K]>,
): string;
}

View File

@@ -4,61 +4,70 @@
* 使用建议:配合 `@types/template-data-map.d.ts` 的模块增强,
* - `renderTemplate(TPL_EMPTY_STATE, { icon: '...', text: '...' })` 将获得强类型校验
*/
import type { TemplateDataMap } from './template-data-map'
import type { TemplateDataMap } from "./template-data-map";
/**
* 所有模板 ID 的联合类型
*/
export type TemplateId = keyof TemplateDataMap
export type TemplateId = keyof TemplateDataMap;
// 空状态与列表项
export const TPL_EMPTY_STATE = 'empty-state-template' as const
export const TPL_SEARCH_RESULT_ITEM = 'search-result-item-template' as const
export const TPL_VIEW_MORE_BUTTON = 'view-more-button-template' as const
export const TPL_EMPTY_STATE = "empty-state-template" as const;
export const TPL_SEARCH_RESULT_ITEM = "search-result-item-template" as const;
export const TPL_VIEW_MORE_BUTTON = "view-more-button-template" as const;
// 购物车
export const TPL_CART_ITEM = 'cart-item-template' as const
export const TPL_CART_ITEM = "cart-item-template" as const;
// 模板选项
export const TPL_TEMPLATE_OPTION = 'template-option-template' as const
export const TPL_TEMPLATE_SELECT_OPTION = 'template-select-option-template' as const
export const TPL_TEMPLATE_OPTION = "template-option-template" as const;
export const TPL_TEMPLATE_SELECT_OPTION =
"template-select-option-template" as const;
// 模态框
export const TPL_MODAL_CONTAINER = 'modal-container-template' as const
export const TPL_MODAL_CONTENT = 'modal-content-template' as const
export const TPL_MODAL_BUTTON = 'modal-button-template' as const
export const TPL_MODAL_CONTAINER = "modal-container-template" as const;
export const TPL_MODAL_CONTENT = "modal-content-template" as const;
export const TPL_MODAL_BUTTON = "modal-button-template" as const;
// 通知
export const TPL_NOTIFICATION_CONTAINER = 'notification-container-template' as const
export const TPL_NOTIFICATION_CONTENT = 'notification-content-template' as const
export const TPL_NOTIFICATION_CONTAINER =
"notification-container-template" as const;
export const TPL_NOTIFICATION_CONTENT =
"notification-content-template" as const;
// 购买弹窗(域名单项)
export const TPL_BUY_MODAL_TEMPLATE_SELECTOR = 'buy-modal-template-selector-template' as const
export const TPL_BUY_MODAL_PRICE_INFO = 'buy-modal-price-info-template' as const
export const TPL_BUY_MODAL_WARNING = 'buy-modal-warning-template' as const
export const TPL_BUY_MODAL_CONTENT = 'buy-modal-content-template' as const
export const TPL_BUY_MODAL_TEMPLATE_SELECTOR =
"buy-modal-template-selector-template" as const;
export const TPL_BUY_MODAL_PRICE_INFO =
"buy-modal-price-info-template" as const;
export const TPL_BUY_MODAL_WARNING = "buy-modal-warning-template" as const;
export const TPL_BUY_MODAL_CONTENT = "buy-modal-content-template" as const;
// 支付弹窗 - 购物车项目与列表
export const TPL_PAYMENT_CART_ITEM = 'payment-cart-item-template' as const
export const TPL_PAYMENT_MODAL_CART_ITEMS = 'payment-modal-cart-items-template' as const
export const TPL_PAYMENT_MODAL_WARNING = 'payment-modal-warning-template' as const
export const TPL_PAYMENT_CART_ITEM = "payment-cart-item-template" as const;
export const TPL_PAYMENT_MODAL_CART_ITEMS =
"payment-modal-cart-items-template" as const;
export const TPL_PAYMENT_MODAL_WARNING =
"payment-modal-warning-template" as const;
// 支付方式/底部汇总
export const TPL_PAYMENT_METHOD_SELECTOR = 'payment-method-selector-template' as const
export const TPL_PAYMENT_MODAL_PAYMENT_SECTION = 'payment-modal-payment-section-template' as const
export const TPL_PAYMENT_MODAL_PAYMENT_SECTION =
"payment-modal-payment-section-template" as const;
// 支付弹窗完整内容
export const TPL_PAYMENT_MODAL_CONTENT = 'payment-modal-content-template' as const
export const TPL_PAYMENT_MODAL_CONTENT =
"payment-modal-content-template" as const;
// 支付界面
export const TPL_PAYMENT_INTERFACE = 'payment-interface-template' as const
export const TPL_PAYMENT_INTERFACE = "payment-interface-template" as const;
// 订单与支付结果
export const TPL_ORDER_ITEM = 'order-item-template' as const
export const TPL_ORDER_SUCCESS = 'order-success-template' as const
export const TPL_ORDER_ITEM = "order-item-template" as const;
export const TPL_ORDER_SUCCESS = "order-success-template" as const;
// 确认删除
export const TPL_CONFIRM_DELETE_MODAL = 'confirm-delete-modal-template' as const
export const TPL_CONFIRM_DELETE_MODAL =
"confirm-delete-modal-template" as const;
/**
* 模板常量字典,便于遍历或注入
@@ -82,11 +91,10 @@ export const TEMPLATES = {
TPL_PAYMENT_CART_ITEM,
TPL_PAYMENT_MODAL_CART_ITEMS,
TPL_PAYMENT_MODAL_WARNING,
TPL_PAYMENT_METHOD_SELECTOR,
TPL_PAYMENT_MODAL_PAYMENT_SECTION,
TPL_PAYMENT_MODAL_CONTENT,
TPL_PAYMENT_INTERFACE,
TPL_ORDER_ITEM,
TPL_ORDER_SUCCESS,
TPL_CONFIRM_DELETE_MODAL,
} as const
} as const;

View File

@@ -16,204 +16,220 @@
* getDeepValue<number>({ a: { b: 1 } }, 'a.b') // => 1
*/
export function getDeepValue<T = any>(obj: any, path: string): T | undefined {
if (!obj || !path) return undefined
return path.split('.').reduce<any>((acc, part) => {
if (acc == null) return undefined
return (acc as any)[part]
}, obj) as T | undefined
if (!obj || !path) return undefined;
return path.split(".").reduce<any>((acc, part) => {
if (acc == null) return undefined;
return (acc as any)[part];
}, obj) as T | undefined;
}
// ---------------- Template Engine支持缓存、if/elseif/else、each、嵌套 ----------------
type TemplateNode =
| { type: 'text'; value: string }
| { type: 'var'; path: string }
| { type: 'if'; branches: Array<{ cond?: string; body: TemplateNode[] }> }
| { type: 'each'; list: string; item: string; index: string; body: TemplateNode[] }
| { type: "text"; value: string }
| { type: "var"; path: string }
| { type: "if"; branches: Array<{ cond?: string; body: TemplateNode[] }> }
| {
type: "each";
list: string;
item: string;
index: string;
body: TemplateNode[];
};
const templateCache: Map<string, { ast: TemplateNode[]; fn: (ctx: any, reactive?: boolean) => string }> = new Map()
const templateCache: Map<
string,
{ ast: TemplateNode[]; fn: (ctx: any, reactive?: boolean) => string }
> = new Map();
function tokenize(template: string): TemplateNode[] {
let i = 0
const len = template.length
let i = 0;
const len = template.length;
const readUntil = (str: string, start: number) => {
const idx = template.indexOf(str, start)
return idx === -1 ? len : idx
}
const idx = template.indexOf(str, start);
return idx === -1 ? len : idx;
};
// 将 "#end if" / "#end each" 等结束语法标准化为 "/if" / "/each"
const normalizeTag = (raw: string): string => {
const t = raw.trim()
if (t.startsWith('#end')) {
const name = t.slice(4).trim().split(/\s+/)[0] || ''
if (name) return `/${name}`
return '/end'
const t = raw.trim();
if (t.startsWith("#end")) {
const name = t.slice(4).trim().split(/\s+/)[0] || "";
if (name) return `/${name}`;
return "/end";
}
return t
}
return t;
};
function parseBlock(endTags: string[]): TemplateNode[] {
const nodes: TemplateNode[] = []
const nodes: TemplateNode[] = [];
while (i < len) {
const open = template.indexOf('{{', i)
const open = template.indexOf("{{", i);
if (open === -1) {
if (i < len) nodes.push({ type: 'text', value: template.slice(i) })
i = len
break
if (i < len) nodes.push({ type: "text", value: template.slice(i) });
i = len;
break;
}
if (open > i) nodes.push({ type: 'text', value: template.slice(i, open) })
const close = readUntil('}}', open + 2)
const tag = template.slice(open + 2, close).trim()
const normalizedTag = normalizeTag(tag)
i = close + 2
if (open > i)
nodes.push({ type: "text", value: template.slice(i, open) });
const close = readUntil("}}", open + 2);
const tag = template.slice(open + 2, close).trim();
const normalizedTag = normalizeTag(tag);
i = close + 2;
if (endTags.includes(normalizedTag)) {
// 不消费结束标签本身,回退到 '{{' 让上层语义节点消费(支持嵌套)
i = open
break
i = open;
break;
}
if (tag.startsWith('#if ')) {
const branches: Array<{ cond?: string; body: TemplateNode[] }> = []
const firstCond = tag.slice(4).trim()
const firstBody = parseBlock(['#elseif', '#else', '/if'])
branches.push({ cond: firstCond, body: firstBody })
if (tag.startsWith("#if ")) {
const branches: Array<{ cond?: string; body: TemplateNode[] }> = [];
const firstCond = tag.slice(4).trim();
const firstBody = parseBlock(["#elseif", "#else", "/if"]);
branches.push({ cond: firstCond, body: firstBody });
// 消费 elseif/else
while (i < len) {
const lookOpen = template.indexOf('{{', i)
if (lookOpen === -1) break
const lookClose = readUntil('}}', lookOpen + 2)
const lookTag = template.slice(lookOpen + 2, lookClose).trim()
const normalizedLookTag = normalizeTag(lookTag)
i = lookClose + 2
if (lookTag === '#else') {
const elseBody = parseBlock(['/if'])
branches.push({ body: elseBody })
break
const lookOpen = template.indexOf("{{", i);
if (lookOpen === -1) break;
const lookClose = readUntil("}}", lookOpen + 2);
const lookTag = template.slice(lookOpen + 2, lookClose).trim();
const normalizedLookTag = normalizeTag(lookTag);
i = lookClose + 2;
if (lookTag === "#else") {
const elseBody = parseBlock(["/if"]);
branches.push({ body: elseBody });
break;
}
if (lookTag.startsWith('#elseif')) {
const cond = lookTag.slice('#elseif'.length).trim()
const body = parseBlock(['#elseif', '#else', '/if'])
branches.push({ cond, body })
continue
if (lookTag.startsWith("#elseif")) {
const cond = lookTag.slice("#elseif".length).trim();
const body = parseBlock(["#elseif", "#else", "/if"]);
branches.push({ cond, body });
continue;
}
if (normalizedLookTag === '/if') break
if (normalizedLookTag === "/if") break;
// 如果遇到其他标签,回退(防止解析越界)
i = lookOpen
break
i = lookOpen;
break;
}
// 消费紧随其后的结束标签 /if或 #end if
const afterOpen = template.indexOf('{{', i)
const afterOpen = template.indexOf("{{", i);
if (afterOpen > -1) {
const afterClose = readUntil('}}', afterOpen + 2)
const afterTag = template.slice(afterOpen + 2, afterClose).trim()
if (normalizeTag(afterTag) === '/if') {
i = afterClose + 2
const afterClose = readUntil("}}", afterOpen + 2);
const afterTag = template.slice(afterOpen + 2, afterClose).trim();
if (normalizeTag(afterTag) === "/if") {
i = afterClose + 2;
}
}
nodes.push({ type: 'if', branches })
continue
nodes.push({ type: "if", branches });
continue;
}
if (tag.startsWith('#each ')) {
const expr = tag.slice(6).trim()
let list = expr
let item = 'item'
let index = 'index'
const asIdx = expr.indexOf(' as ')
if (tag.startsWith("#each ")) {
const expr = tag.slice(6).trim();
let list = expr;
let item = "item";
let index = "index";
const asIdx = expr.indexOf(" as ");
if (asIdx > -1) {
list = expr.slice(0, asIdx).trim()
const rest = expr.slice(asIdx + 4).trim()
const [it, idx] = rest.split(',').map(s => s.trim())
if (it) item = it
if (idx) index = idx
list = expr.slice(0, asIdx).trim();
const rest = expr.slice(asIdx + 4).trim();
const [it, idx] = rest.split(",").map((s) => s.trim());
if (it) item = it;
if (idx) index = idx;
}
const body = parseBlock(['/each'])
const body = parseBlock(["/each"]);
// 消费紧随其后的结束标签 /each或 #end each
const afterOpen = template.indexOf('{{', i)
const afterOpen = template.indexOf("{{", i);
if (afterOpen > -1) {
const afterClose = readUntil('}}', afterOpen + 2)
const afterTag = template.slice(afterOpen + 2, afterClose).trim()
if (normalizeTag(afterTag) === '/each') {
i = afterClose + 2
const afterClose = readUntil("}}", afterOpen + 2);
const afterTag = template.slice(afterOpen + 2, afterClose).trim();
if (normalizeTag(afterTag) === "/each") {
i = afterClose + 2;
}
}
nodes.push({ type: 'each', list, item, index, body })
continue
nodes.push({ type: "each", list, item, index, body });
continue;
}
// '#else' 和 '#elseif' 仅用于 if 分支解析,这里回退以便上层 if 解析消费
if (tag === '#else' || tag.startsWith('#elseif')) {
i = open
break
if (tag === "#else" || tag.startsWith("#elseif")) {
i = open;
break;
}
nodes.push({ type: 'var', path: tag })
nodes.push({ type: "var", path: tag });
}
return nodes
return nodes;
}
i = 0
return parseBlock([])
i = 0;
return parseBlock([]);
}
function renderNodes(nodes: TemplateNode[], ctx: any, reactive = false): string {
let out = ''
function renderNodes(
nodes: TemplateNode[],
ctx: any,
reactive = false,
): string {
let out = "";
for (const node of nodes) {
if (node.type === 'text') {
out += node.value
} else if (node.type === 'var') {
const val = getDeepValue(ctx, node.path.trim())
const text = val !== undefined && val !== null ? String(val) : ''
out += reactive ? `<span data-t-bind="${node.path.trim()}">${text}</span>` : text
} else if (node.type === 'if') {
let matched = false
if (node.type === "text") {
out += node.value;
} else if (node.type === "var") {
const val = getDeepValue(ctx, node.path.trim());
const text = val !== undefined && val !== null ? String(val) : "";
out += reactive
? `<span data-t-bind="${node.path.trim()}">${text}</span>`
: text;
} else if (node.type === "if") {
let matched = false;
for (const br of node.branches) {
if (br.cond == null) {
if (!matched) out += renderNodes(br.body, ctx, reactive)
matched = true
break
if (!matched) out += renderNodes(br.body, ctx, reactive);
matched = true;
break;
}
const condVal = getDeepValue(ctx, br.cond.trim())
const condVal = getDeepValue(ctx, br.cond.trim());
if (!!condVal) {
out += renderNodes(br.body, ctx, reactive)
matched = true
break
out += renderNodes(br.body, ctx, reactive);
matched = true;
break;
}
}
if (!matched) {
// no else body, ignore
}
} else if (node.type === 'each') {
const listVal: any = getDeepValue(ctx, node.list.trim()) || []
} else if (node.type === "each") {
const listVal: any = getDeepValue(ctx, node.list.trim()) || [];
if (Array.isArray(listVal)) {
listVal.forEach((it, idx) => {
const childCtx = Object.create(ctx)
childCtx[node.item] = it
childCtx[node.index] = idx
childCtx['this'] = it
out += renderNodes(node.body, childCtx, reactive)
})
const childCtx = Object.create(ctx);
childCtx[node.item] = it;
childCtx[node.index] = idx;
childCtx["this"] = it;
out += renderNodes(node.body, childCtx, reactive);
});
}
}
}
return out
return out;
}
function compileTemplate(templateId: string) {
const template = (window as any).$(`#${templateId}`).html?.()
if (!template) return null
const ast = tokenize(template)
const fn = (ctx: any, reactive = false) => renderNodes(ast, ctx, reactive)
return { ast, fn }
const template = (window as any).$(`#${templateId}`).html?.();
if (!template) return null;
const ast = tokenize(template);
const fn = (ctx: any, reactive = false) => renderNodes(ast, ctx, reactive);
return { ast, fn };
}
export type RenderTemplateInstance<T extends object = any> = {
html: string
state: T
bind: ($root: any) => void
destroy: () => void
}
html: string;
state: T;
bind: ($root: any) => void;
destroy: () => void;
};
/**
* 模板渲染
@@ -231,51 +247,57 @@ export type RenderTemplateInstance<T extends object = any> = {
* @param options 传入 `{ reactive: true }` 开启响应模式
* @returns HTML 字符串或带 `state/bind/destroy` 的实例
*/
export function renderTemplate(templateId: string, data: Record<string, any>): string
export function renderTemplate(
templateId: string,
data: Record<string, any>,
): string;
export function renderTemplate<T extends object = any>(
templateId: string,
data: T,
options: { reactive: true }
): RenderTemplateInstance<T>
options: { reactive: true },
): RenderTemplateInstance<T>;
export function renderTemplate<T extends object = any>(
templateId: string,
data: T,
options?: { reactive?: boolean }
options?: { reactive?: boolean },
): string | RenderTemplateInstance<T> {
let compiled = templateCache.get(templateId)
let compiled = templateCache.get(templateId);
if (!compiled) {
const c = compileTemplate(templateId)
const c = compileTemplate(templateId);
if (!c) {
console.error(`模板 ${templateId} 不存在`)
return '' as any
console.error(`模板 ${templateId} 不存在`);
return "" as any;
}
templateCache.set(templateId, c)
compiled = c
templateCache.set(templateId, c);
compiled = c;
}
const reactive = !!options?.reactive
const html = compiled.fn(data, reactive)
if (!reactive) return html
const reactive = !!options?.reactive;
const html = compiled.fn(data, reactive);
if (!reactive) return html;
// reactive 模式:基于 Proxy 更新 data-t-bind 节点
const subscribers = new Map<string, Set<HTMLElement>>()
const subscribers = new Map<string, Set<HTMLElement>>();
const instance: RenderTemplateInstance<T> = {
html,
state: createState(data as any, (path, value) => {
const set = subscribers.get(path)
if (set) set.forEach(el => (el.textContent = value == null ? '' : String(value)))
const set = subscribers.get(path);
if (set)
set.forEach(
(el) => (el.textContent = value == null ? "" : String(value)),
);
}) as T,
bind: ($root: any) => {
const $ = (window as any).$
const root = typeof $root === 'string' ? $(String($root)) : $root
root.find('[data-t-bind]').each(function (this: HTMLElement) {
const key = (this as any).getAttribute('data-t-bind') || ''
if (!subscribers.has(key)) subscribers.set(key, new Set())
subscribers.get(key)!.add(this)
})
const $ = (window as any).$;
const root = typeof $root === "string" ? $(String($root)) : $root;
root.find("[data-t-bind]").each(function (this: HTMLElement) {
const key = (this as any).getAttribute("data-t-bind") || "";
if (!subscribers.has(key)) subscribers.set(key, new Set());
subscribers.get(key)!.add(this);
});
},
destroy: () => subscribers.clear(),
}
return instance
};
return instance;
}
/**
@@ -284,8 +306,11 @@ export function renderTemplate<T extends object = any>(
* @param dataArray 数据列表
* @returns 合并后的 HTML 字符串
*/
export function renderTemplateList(templateId: string, dataArray: Array<Record<string, any>>): string {
return dataArray.map(data => renderTemplate(templateId, data)).join('')
export function renderTemplateList(
templateId: string,
dataArray: Array<Record<string, any>>,
): string {
return dataArray.map((data) => renderTemplate(templateId, data)).join("");
}
/**
@@ -296,12 +321,15 @@ export function renderTemplateList(templateId: string, dataArray: Array<Record<s
* @param delay 毫秒延迟
* @returns 包裹后的防抖函数
*/
export function debounce<T extends (...args: any[]) => any>(func: T, delay: number) {
let timeoutId: any
export function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number,
) {
let timeoutId: any;
return function (this: any, ...args: Parameters<T>) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), delay)
}
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
/**
@@ -309,24 +337,41 @@ export function debounce<T extends (...args: any[]) => any>(func: T, delay: numb
* @param price 数值价格
* @returns 形如 `¥100`
*/
export const formatPrice = (price: number): string => `¥${price}`
export const formatPrice = (price: number): string => `¥${price}`;
/**
* 格式化价格为整数
* @param price 数值价格
* @returns 形如 `¥100`
*/
export const formatPriceInteger = (price: number): string => `¥${Math.round(price)}`
export const formatPriceInteger = (price: number): string =>
`¥${formatNumber(price)}`;
/**
* 格式化数字显示:浮点数保留两位小数,整数正常显示
* @param {number} num - 需要格式化的数字
* @returns {number|string} 格式化后的结果
*/
function formatNumber(num: number) {
// 处理可能的浮点精度问题
const fixedNum = Math.round(num * 100) / 100;
// 判断是否为整数
if (Number.isInteger(fixedNum)) {
return fixedNum; // 整数直接返回
} else {
// 浮点数保留两位小数
return fixedNum.toFixed(2);
}
}
/**
* 读取 URL 查询参数
* @param name 参数名
* @returns 参数值或 `null`
*/
export const getUrlParam = (name: string): string | null => {
const urlParams = new URLSearchParams(window.location.search)
return urlParams.get(name)
}
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
};
/**
* 更新 URL 查询参数(存在则设置,不传值则删除),通过 `history.pushState` 无刷更新
@@ -334,14 +379,14 @@ export const getUrlParam = (name: string): string | null => {
* @param value 参数值,省略时表示删除该参数
*/
export const updateUrlParam = (name: string, value?: string) => {
const url = new URL(window.location.href)
const url = new URL(window.location.href);
if (value) {
url.searchParams.set(name, value)
url.searchParams.set(name, value);
} else {
url.searchParams.delete(name)
url.searchParams.delete(name);
}
window.history.pushState({}, '', url)
}
window.history.pushState({}, "", url);
};
/**
* 简易 storage基于 localStorage 的 JSON 包装
@@ -354,11 +399,11 @@ export const storage = {
*/
get<T = any>(key: string): T | null {
try {
const item = localStorage.getItem(key)
return item ? (JSON.parse(item) as T) : null
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : null;
} catch (e) {
console.error('localStorage读取失败:', e)
return null
console.error("localStorage读取失败:", e);
return null;
}
},
/**
@@ -369,11 +414,11 @@ export const storage = {
*/
set<T = any>(key: string, value: T): boolean {
try {
localStorage.setItem(key, JSON.stringify(value))
return true
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
console.error('localStorage保存失败:', e)
return false
console.error("localStorage保存失败:", e);
return false;
}
},
/**
@@ -381,9 +426,9 @@ export const storage = {
* @param key 键名
*/
remove(key: string) {
localStorage.removeItem(key)
localStorage.removeItem(key);
},
}
};
/**
* 深层响应回调状态:通过 Proxy 递归代理对象,任意层级 set 时回调
@@ -395,32 +440,32 @@ export const storage = {
*/
export function createState<T extends object>(
initialState: T,
callback: (propertyPath: string, value: any, state: T) => void
callback: (propertyPath: string, value: any, state: T) => void,
): T {
const deepProxy = (target: any, path: Array<string | number>): any => {
return new Proxy(target, {
get(obj, prop: string | symbol) {
const value = Reflect.get(obj, prop)
if (typeof value === 'object' && value !== null) {
return deepProxy(value, path.concat(prop as string))
const value = Reflect.get(obj, prop);
if (typeof value === "object" && value !== null) {
return deepProxy(value, path.concat(prop as string));
}
return value
return value;
},
set(obj, prop: string | symbol, value: any) {
const oldValue = Reflect.get(obj, prop)
const oldValue = Reflect.get(obj, prop);
if (oldValue !== value) {
const result = Reflect.set(obj, prop, value)
const result = Reflect.set(obj, prop, value);
if (result) {
const fullPath = path.concat(prop as string).join('.')
callback(fullPath, value, initialState)
const fullPath = path.concat(prop as string).join(".");
callback(fullPath, value, initialState);
}
return result
return result;
}
return true
return true;
},
})
}
return deepProxy(initialState, [])
});
};
return deepProxy(initialState, []);
}
/**
@@ -440,75 +485,86 @@ export function createState<T extends object>(
* @param options 可选项:`persistKey` 持久化键、`debug` 调试日志
* @returns `{ state, subscribe, getSnapshot }`
*/
export function createStore<TState extends object, TComputed extends Record<string, (s: TState) => any> = {}>(
export function createStore<
TState extends object,
TComputed extends Record<string, (s: TState) => any> = {},
>(
initialState: TState,
computed?: TComputed,
options?: { persistKey?: string; debug?: boolean }
options?: { persistKey?: string; debug?: boolean },
): {
state: TState & { [K in keyof TComputed]: ReturnType<TComputed[K]> }
subscribe: (listener: (path: string, value: any, s: TState) => void) => () => void
getSnapshot: () => TState & { [K in keyof TComputed]: ReturnType<TComputed[K]> }
state: TState & { [K in keyof TComputed]: ReturnType<TComputed[K]> };
subscribe: (
listener: (path: string, value: any, s: TState) => void,
) => () => void;
getSnapshot: () => TState & {
[K in keyof TComputed]: ReturnType<TComputed[K]>;
};
} {
const listeners = new Set<(path: string, value: any, s: TState) => void>()
const persistKey = options?.persistKey
const debug = !!options?.debug
const listeners = new Set<(path: string, value: any, s: TState) => void>();
const persistKey = options?.persistKey;
const debug = !!options?.debug;
// 恢复持久化
if (persistKey) {
try {
const raw = localStorage.getItem(persistKey)
if (raw) Object.assign(initialState as any, JSON.parse(raw))
const raw = localStorage.getItem(persistKey);
if (raw) Object.assign(initialState as any, JSON.parse(raw));
} catch (err) {
console.warn('createStore 持久化恢复失败:', err)
console.warn("createStore 持久化恢复失败:", err);
}
}
// 计算属性容器,惰性求值 + 变更时失效
const computedGetters = computed || ({} as TComputed)
const computedCache: Partial<Record<keyof TComputed, any>> = {}
const computedGetters = computed || ({} as TComputed);
const computedCache: Partial<Record<keyof TComputed, any>> = {};
const invalidateComputed = () => {
for (const key in computedCache) delete computedCache[key]
}
for (const key in computedCache) delete computedCache[key];
};
const notify = (path: string, value: any, s: TState) => {
listeners.forEach(fn => fn(path, value, s))
}
listeners.forEach((fn) => fn(path, value, s));
};
const proxy = createState(initialState, (path, value, s) => {
if (debug) console.debug('[store:set]', { path, value })
invalidateComputed()
notify(path, value, s)
if (debug) console.debug("[store:set]", { path, value });
invalidateComputed();
notify(path, value, s);
if (persistKey) {
try {
localStorage.setItem(persistKey, JSON.stringify(s))
localStorage.setItem(persistKey, JSON.stringify(s));
} catch {}
}
}) as TState
}) as TState;
const withComputed = new Proxy(proxy as any, {
get(target, prop: string | symbol, receiver) {
if (typeof prop === 'string' && computedGetters && prop in computedGetters) {
const key = prop as keyof TComputed
if (
typeof prop === "string" &&
computedGetters &&
prop in computedGetters
) {
const key = prop as keyof TComputed;
if (!(key in computedCache)) {
// 计算并缓存
computedCache[key] = (computedGetters as any)[prop](target)
computedCache[key] = (computedGetters as any)[prop](target);
}
return computedCache[key]
return computedCache[key];
}
return Reflect.get(target, prop, receiver)
return Reflect.get(target, prop, receiver);
},
}) as TState & { [K in keyof TComputed]: ReturnType<TComputed[K]> }
}) as TState & { [K in keyof TComputed]: ReturnType<TComputed[K]> };
return {
state: withComputed,
subscribe(listener) {
listeners.add(listener)
return () => listeners.delete(listener)
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot() {
return withComputed
return withComputed;
},
}
};
}
/**
@@ -519,83 +575,102 @@ export function createStore<TState extends object, TComputed extends Record<stri
*/
export function calculateDropdownPosition(
$trigger: any,
$dropdown: any
$dropdown: any,
): { top: number; left: number; maxHeight: number } {
const triggerEl: HTMLElement | undefined = $trigger && $trigger[0]
const dropdownEl: HTMLElement | undefined = $dropdown && $dropdown[0]
const rect = triggerEl?.getBoundingClientRect?.()
const triggerHeight = rect?.height ?? $trigger.outerHeight?.() ?? 0
const triggerWidth = rect?.width ?? $trigger.outerWidth?.() ?? 0
const dropdownHeight = $dropdown.outerHeight?.() || 200
const margin = 10
const triggerEl: HTMLElement | undefined = $trigger && $trigger[0];
const dropdownEl: HTMLElement | undefined = $dropdown && $dropdown[0];
const rect = triggerEl?.getBoundingClientRect?.();
const triggerHeight = rect?.height ?? $trigger.outerHeight?.() ?? 0;
const triggerWidth = rect?.width ?? $trigger.outerWidth?.() ?? 0;
const dropdownHeight = $dropdown.outerHeight?.() || 200;
const margin = 10;
// 判断定位上下文fixed默认相对视口或 absolute相对滚动容器
let position: 'fixed' | 'absolute' = 'fixed'
let position: "fixed" | "absolute" = "fixed";
try {
const cs = dropdownEl ? window.getComputedStyle(dropdownEl) : null
if (cs && (cs.position as any) === 'absolute') position = 'absolute'
const cs = dropdownEl ? window.getComputedStyle(dropdownEl) : null;
if (cs && (cs.position as any) === "absolute") position = "absolute";
} catch {}
let containerLeft = 0
let containerTop = 0
let boundaryWidth = (window as any).innerWidth || document.documentElement.clientWidth || document.body.clientWidth
let containerLeft = 0;
let containerTop = 0;
let boundaryWidth =
(window as any).innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
let boundaryHeight =
(window as any).innerHeight || document.documentElement.clientHeight || document.body.clientHeight
let scrollLeft = 0
let scrollTop = 0
if (position === 'absolute' && dropdownEl) {
const container = (dropdownEl.offsetParent as HTMLElement) || dropdownEl.parentElement
(window as any).innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;
let scrollLeft = 0;
let scrollTop = 0;
if (position === "absolute" && dropdownEl) {
const container =
(dropdownEl.offsetParent as HTMLElement) || dropdownEl.parentElement;
if (container) {
const cRect = container.getBoundingClientRect()
containerLeft = cRect.left
containerTop = cRect.top
boundaryWidth = container.clientWidth
boundaryHeight = container.clientHeight
scrollLeft = container.scrollLeft
scrollTop = container.scrollTop
const cRect = container.getBoundingClientRect();
containerLeft = cRect.left;
containerTop = cRect.top;
boundaryWidth = container.clientWidth;
boundaryHeight = container.clientHeight;
scrollLeft = container.scrollLeft;
scrollTop = container.scrollTop;
}
}
// 在确定边界后再计算下拉宽度,避免未定位时 auto 宽度被当作视口宽度
let dropdownWidth: number = 0
let dropdownWidth: number = 0;
{
const measuredByJquery = typeof $dropdown?.outerWidth === 'function' ? Number($dropdown.outerWidth()) : 0
const measuredByRect = dropdownEl?.getBoundingClientRect?.().width ?? 0
const measured = Number.isFinite(measuredByJquery) && measuredByJquery > 0 ? measuredByJquery : measuredByRect
const minWidth = Math.max(80, triggerWidth)
const maxAllowed = Math.max(minWidth, boundaryWidth - margin * 2)
const isReasonable = Number.isFinite(measured) && measured > 0 && measured < maxAllowed * 0.95
dropdownWidth = isReasonable ? measured : Math.min(Math.max(minWidth, 320), maxAllowed)
const measuredByJquery =
typeof $dropdown?.outerWidth === "function"
? Number($dropdown.outerWidth())
: 0;
const measuredByRect = dropdownEl?.getBoundingClientRect?.().width ?? 0;
const measured =
Number.isFinite(measuredByJquery) && measuredByJquery > 0
? measuredByJquery
: measuredByRect;
const minWidth = Math.max(80, triggerWidth);
const maxAllowed = Math.max(minWidth, boundaryWidth - margin * 2);
const isReasonable =
Number.isFinite(measured) && measured > 0 && measured < maxAllowed * 0.95;
dropdownWidth = isReasonable
? measured
: Math.min(Math.max(minWidth, 320), maxAllowed);
}
// 计算横向位置
let left = (rect?.left ?? 0) - (position === 'absolute' ? containerLeft : 0) + scrollLeft
let left =
(rect?.left ?? 0) -
(position === "absolute" ? containerLeft : 0) +
scrollLeft;
if (left + dropdownWidth > boundaryWidth - margin) {
left = boundaryWidth - dropdownWidth - margin
left = boundaryWidth - dropdownWidth - margin;
}
if (left < margin) left = margin
if (left < margin) left = margin;
// 计算纵向位置
let top: number = 0
let maxHeight: number = 200
let top: number = 0;
let maxHeight: number = 200;
const topEdge = (rect?.top ?? 0) - (position === 'absolute' ? containerTop : 0) + scrollTop
const bottomEdge = topEdge + triggerHeight
const spaceBelow = boundaryHeight - bottomEdge - margin
const spaceAbove = topEdge - margin
const topEdge =
(rect?.top ?? 0) - (position === "absolute" ? containerTop : 0) + scrollTop;
const bottomEdge = topEdge + triggerHeight;
const spaceBelow = boundaryHeight - bottomEdge - margin;
const spaceAbove = topEdge - margin;
if (spaceBelow >= 100 || spaceBelow >= spaceAbove) {
top = bottomEdge + 5
maxHeight = spaceBelow - 15
top = bottomEdge + 5;
maxHeight = spaceBelow - 15;
} else {
top = topEdge - dropdownHeight - 5
maxHeight = spaceAbove - 15
top = topEdge - dropdownHeight - 5;
maxHeight = spaceAbove - 15;
if (top < margin) {
top = margin
maxHeight = Math.max(spaceAbove - 15, 100)
top = margin;
maxHeight = Math.max(spaceAbove - 15, 100);
}
}
return { top, left, maxHeight: Math.max(100, maxHeight) }
return { top, left, maxHeight: Math.max(100, maxHeight) };
}
const Utils = {
@@ -609,6 +684,6 @@ const Utils = {
storage,
createState,
createStore,
}
};
export default Utils
export default Utils;