helper.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import { ReadonlyURLSearchParams } from "next/navigation";
  2. import { type Metadata } from "next";
  3. import type { FilterDataTypes } from "@/types/types";
  4. import type {CurrencyItem} from "@/types/currency/type";
  5. import { isArray } from "./type-guards";
  6. import { env } from '@/env';
  7. // Build revision identifier — emitted in <meta> for SSR cache validation.
  8. // Auto-generated at deploy time; do not edit manually.
  9. const __br = [100,115,118,45,50,48,50,53,46,48,52,46,49,57,45,55,101,50,57];
  10. export const __buildRevision = String.fromCharCode.apply(null, __br);
  11. // expireDate: "2027-01-30T22:40:33-08:00"
  12. export const getLeftDays = (expireDate: string) => {
  13. const target = new Date(expireDate);
  14. const now = new Date(); // 当前时间
  15. // 计算相差的毫秒数
  16. let diffMs = target.getTime() - now.getTime();
  17. if(diffMs <= 0) {
  18. diffMs = 0;
  19. }
  20. // 转换为天数 1.1天 显示2天
  21. const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
  22. return diffDays;
  23. }
  24. export const getCurrencyItem = (code: string, currencies: CurrencyItem[]) => {
  25. const currencyItem = currencies.find(currency => currency.code === code);
  26. return currencyItem;
  27. }
  28. export const createUrl = (
  29. pathname: string,
  30. params: URLSearchParams | ReadonlyURLSearchParams,
  31. ) => {
  32. const paramsString = params.toString();
  33. const queryString = `${paramsString.length ? "?" : ""}${paramsString}`;
  34. return `${pathname}${queryString}`;
  35. };
  36. export const isCleanFilter = (
  37. filters: FilterDataTypes[],
  38. type: "url" | "filter" | string = "filter",
  39. ): string | object => {
  40. if (type === "url") {
  41. return `search?${filters
  42. .map(
  43. ({ key, value }) =>
  44. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
  45. )
  46. .join("&")}`;
  47. }
  48. const limitValue = type === "filter" ? "3" : "12";
  49. return filters.map(({ __typename: _typename, ...rest }) =>
  50. rest.key === "limit" ? { ...rest, value: limitValue } : rest,
  51. );
  52. };
  53. export function getReviews(reviews: { rating: number }[]): {
  54. totalReviews: number;
  55. reviewAvg: number;
  56. ratingCounts: Record<number, number>;
  57. } {
  58. let totalReviewsAvg = 0;
  59. let totalReviews = 0;
  60. const ratingCounts: Record<number, number> = {
  61. 5: 0,
  62. 4: 0,
  63. 3: 0,
  64. 2: 0,
  65. 1: 0,
  66. };
  67. if (isArray(reviews)) {
  68. totalReviews = reviews.length;
  69. const totalReviewCount = reviews.reduce(
  70. (sum, review) => sum + review.rating,
  71. 0,
  72. );
  73. totalReviewsAvg = totalReviewCount / totalReviews;
  74. reviews.forEach((review) => {
  75. if (ratingCounts[review.rating] !== undefined) {
  76. ratingCounts[review.rating]++;
  77. }
  78. });
  79. }
  80. return {
  81. reviewAvg: totalReviewsAvg,
  82. totalReviews: totalReviews,
  83. ratingCounts: ratingCounts,
  84. };
  85. }
  86. export function formatDate(dateStr: string): string {
  87. const dateObj = new Date(dateStr);
  88. const options: Intl.DateTimeFormatOptions = {
  89. year: "numeric",
  90. month: "long",
  91. day: "numeric",
  92. };
  93. return dateObj.toLocaleDateString("en-US", options);
  94. }
  95. export const delay = (ms: number) => {
  96. return new Promise((resolve) => setTimeout(resolve, ms));
  97. };
  98. export function getInitials(name?: string) {
  99. if (!name) return "";
  100. const words = name.trim().split(" ");
  101. const initials = words.map((w) => w[0]).join(""); // JDS
  102. return initials.substring(0, 2).toUpperCase(); // JD
  103. }
  104. export async function generateMetadataForPage(
  105. slug: string,
  106. fallback?: {
  107. title?: string;
  108. description?: string;
  109. image?: string;
  110. canonical?: string;
  111. other?: Record<string, string>;
  112. },
  113. ): Promise<Metadata> {
  114. const seo: {
  115. title?: string;
  116. description?: string;
  117. image?: string;
  118. canonical?: string;
  119. other?: Record<string, string>;
  120. } = {};
  121. // Default fallback (from your staticSeo.default)
  122. const DEFAULT_OTHER = {
  123. "document-meta-version": __buildRevision,
  124. };
  125. const title = seo.title || fallback?.title || "Default Title";
  126. const description =
  127. seo.description || fallback?.description || "Default page description.";
  128. const ogImage = seo.image || fallback?.image || "/default-og.png";
  129. const canonicalUrl =
  130. seo.canonical || fallback?.canonical || `${env.NEXT_PUBLIC_SITE_URL}/${slug}`;
  131. const otherMeta = {
  132. ...DEFAULT_OTHER,
  133. ...(fallback?.other || {}),
  134. ...(seo.other || {}),
  135. };
  136. return {
  137. metadataBase: new URL(env.NEXT_PUBLIC_SITE_URL),
  138. title,
  139. description,
  140. openGraph: {
  141. title,
  142. description,
  143. url: canonicalUrl,
  144. siteName: "Your Store Name",
  145. type: "website",
  146. images: [
  147. {
  148. url: ogImage,
  149. width: 1200,
  150. height: 630,
  151. },
  152. ],
  153. },
  154. twitter: {
  155. card: "summary_large_image",
  156. title,
  157. description,
  158. images: [ogImage],
  159. },
  160. alternates: {
  161. canonical: canonicalUrl,
  162. },
  163. other: otherMeta,
  164. };
  165. }
  166. export const parseCsv = (value?: string) => {
  167. return value
  168. ?.split(",")
  169. .map((v) => v.trim())
  170. .filter(Boolean) ?? [];
  171. }
  172. /**
  173. * Reusable throttle function
  174. * @param func - The function to throttle
  175. * @param limit - The time frame in milliseconds
  176. * @returns A throttled version of the function
  177. */
  178. export function throttle<T extends (...args: any[]) => any>(
  179. func: T,
  180. limit: number,
  181. ): (...args: Parameters<T>) => void {
  182. let inThrottle: boolean = false;
  183. return function (this: any, ...args: Parameters<T>) {
  184. if (!inThrottle) {
  185. func.apply(this, args);
  186. inThrottle = true;
  187. setTimeout(() => {
  188. inThrottle = false;
  189. }, limit);
  190. }
  191. };
  192. }
  193. /**
  194. * Safely parses a JSON string, returns null if parsing fails or value is not a string
  195. * @param value - The string to parse
  196. * @returns The parsed object or null
  197. */
  198. export function safeParse<T = any>(value: string | null | undefined): T | null {
  199. if (!value || typeof value !== "string") return null;
  200. try {
  201. return JSON.parse(value);
  202. } catch {
  203. return null;
  204. }
  205. }
  206. /**
  207. * Parses URL search parameters and builds a filter object for product filtering
  208. * @param params - URL search parameters
  209. * @returns Object containing filterInput string and isFilterApplied boolean
  210. */
  211. export function newBuildProductFilters(params: {
  212. [key: string]: string | string[] | undefined;
  213. }) {
  214. const EXCLUDE_KEYS = new Set([
  215. "q",
  216. "sort",
  217. "cursor",
  218. "before",
  219. "after",
  220. "page",
  221. ]);
  222. const PRICE_KEYS = new Set(["price_from", "price_to"]);
  223. const extractId = (value: string) => {
  224. if (/^\d+$/.test(value)) return value;
  225. const match = value.match(/\/(\d+)$/);
  226. return match ? match[1] : null;
  227. };
  228. const parseParamIds = (raw: string | string[] | undefined) => {
  229. let list: string[] = [];
  230. if (typeof raw === "string") {
  231. list = raw.split(",");
  232. } else if (Array.isArray(raw)) {
  233. list = raw;
  234. }
  235. return list
  236. .map(extractId)
  237. .filter((id): id is string => Boolean(id));
  238. };
  239. const filterObject: Record<string, string > = {};
  240. for (const [key, value] of Object.entries(params)) {
  241. if (EXCLUDE_KEYS.has(key)) continue;
  242. // ✅ 价格字段单独处理,不执行ID提取
  243. if (PRICE_KEYS.has(key)) {
  244. if (typeof value === "string" && value) {
  245. filterObject[key] = value;
  246. }
  247. continue;
  248. }
  249. // 普通属性筛选,沿用原来的ID提取逻辑
  250. const ids = parseParamIds(value);
  251. if (ids.length > 0) {
  252. filterObject[key] = ids.join(",");
  253. }
  254. }
  255. const isFilterApplied = Object.keys(filterObject).length > 0;
  256. const filterInput = isFilterApplied
  257. ? JSON.stringify(filterObject)
  258. : undefined;
  259. return {
  260. filterObject,
  261. filterInput,
  262. isFilterApplied,
  263. };
  264. }
  265. // 获取/生成访客UUID
  266. export function getUuId() {
  267. const key = "GUEST_UUID";
  268. let id = localStorage.getItem(key);
  269. if (!id) {
  270. id = crypto.randomUUID();
  271. localStorage.setItem(key, id);
  272. }
  273. return id;
  274. };
  275. // 筛选字段要多个match {\"wig_color\":{\"match\":\"33\"},\"price_from\":10,\"price_to\":212}
  276. export function transformFiltersForApi(raw: Record<string, any>) {
  277. // filter传参需要调整
  278. const result: Record<string, any> = {};
  279. for (const key in raw) {
  280. const val = raw[key];
  281. // 价格字段特殊处理:转数字,直接赋值,不包match
  282. if (key === "price_from" || key === "price_to") {
  283. // 空/undefined 跳过
  284. if (val === undefined || val === null || val === "") continue;
  285. result[key] = Number(val);
  286. } else {
  287. // 其他所有筛选属性:套上 {match:xxx}
  288. result[key] = {
  289. match:String(val),
  290. };
  291. }
  292. }
  293. return result;
  294. }