mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-12 01:30:09 +08:00
feat: 新增allinssl暗色模式配置-黑金主题
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
185
frontend/apps/domain-official/src/types/api-types/domain-flash.d.ts
vendored
Normal file
185
frontend/apps/domain-official/src/types/api-types/domain-flash.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user