Helper.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import { CASH_BASE } from '@scripts/constants/App';
  2. import { sys } from 'cc';
  3. import { ANDROID, IOS } from 'cc/env';
  4. const STORAGE_ENCRYPTION_HEAD = 'a6072637';
  5. const STORAGE_ENCRYPTION_PASSWORD = 'xbd4jl';
  6. const STORAGE_ENCRYPTION_ENABLED = true;
  7. /**
  8. * 将数字格式化为财务展示格式:使用千位分隔符并保留两位小数。
  9. * 例如:1234567.8 -> 1,234,567.80。
  10. */
  11. export function formatCurrency(value: number | string, decimal = 0): string {
  12. if (typeof value === 'string') value = Number(value);
  13. if (!Number.isFinite(value)) value = 0;
  14. return value.toLocaleString('en-US', {
  15. minimumFractionDigits: decimal,
  16. maximumFractionDigits: decimal,
  17. useGrouping: true,
  18. });
  19. }
  20. export function formatSCNum(value: number | string) {
  21. return formatCurrency(value, 2);
  22. }
  23. export function formatGCNum(value: number | string) {
  24. return formatCurrency(value, 0);
  25. }
  26. export function formatCash(value: number) {
  27. value = Math.floor(value / CASH_BASE);
  28. return formatCurrency(value, 2);
  29. }
  30. const SHORT_MONTH_NAMES = [
  31. 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
  32. 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
  33. ];
  34. /** Converts `YYYY-MM-DD HH:mm:ss.SSS` into `Mon D, HH:mm`. */
  35. export function formatShortDateTime(value: string): string {
  36. const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})/);
  37. if (!match) return '';
  38. const month = Number(match[2]);
  39. const day = Number(match[3]);
  40. const hour = Number(match[4]);
  41. const minute = Number(match[5]);
  42. if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59) return '';
  43. return `${SHORT_MONTH_NAMES[month - 1]} ${day}, ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
  44. }
  45. /**
  46. * 商品货币显示
  47. * 小于 10,000 时展示完整数值;小于 1,000,000 时向下取整为 K;
  48. * 其余数值向下截取到 0.01M,并固定展示两位小数。
  49. * @param value 商品数量
  50. */
  51. export function formatGoodsNum(value: number): string {
  52. if (!Number.isFinite(value)) value = 0;
  53. if (value < 10_000) {
  54. let decimal = value % 1 > 0 ? 2 : 0;
  55. return value.toLocaleString('en-US', {
  56. minimumFractionDigits: decimal,
  57. maximumFractionDigits: 20,
  58. useGrouping: true,
  59. });
  60. }
  61. if (value < 1_000_000) {
  62. return `${Math.floor(value / 1_000)}K`;
  63. }
  64. return `${(Math.floor(value / 10_000) / 100).toFixed(2)}M`;
  65. }
  66. /**
  67. * 读取本地数据,并兼容 game_web_us2 写入的加密格式。
  68. */
  69. export function localStorageGet<T = string>(key: string, defaultValue: T = '' as T): string | T {
  70. let value = sys.localStorage.getItem(key);
  71. if (value === null) return defaultValue;
  72. if (value.startsWith(STORAGE_ENCRYPTION_HEAD)) {
  73. try {
  74. value = decrypt(value.substring(STORAGE_ENCRYPTION_HEAD.length));
  75. } catch {
  76. // 与原项目保持一致:解密失败时返回空字符串。
  77. value = '';
  78. }
  79. }
  80. return value;
  81. }
  82. /**
  83. * 加密并保存本地数据。空字符串不加密,行为与 game_web_us2 保持一致。
  84. */
  85. export function localStorageSet(key: string, value: unknown): void {
  86. let storageValue = value;
  87. if (STORAGE_ENCRYPTION_ENABLED && storageValue !== null && storageValue !== '') {
  88. const plainValue = typeof storageValue === 'string' ? storageValue : String(storageValue);
  89. storageValue = plainValue;
  90. try {
  91. storageValue = STORAGE_ENCRYPTION_HEAD + encrypt(plainValue);
  92. } catch {
  93. // 加密失败时沿用原值保存,兼容原项目行为。
  94. }
  95. }
  96. sys.localStorage.setItem(key, String(storageValue));
  97. }
  98. export function localStorageRemove(key: string): void {
  99. sys.localStorage.removeItem(key);
  100. }
  101. function encrypt(value: string, password = STORAGE_ENCRYPTION_PASSWORD): string {
  102. if (value === '') return '';
  103. const source = encodeURIComponent(value);
  104. const encodedPassword = encodeURIComponent(password || STORAGE_ENCRYPTION_PASSWORD);
  105. if (encodedPassword.length === 0) return '';
  106. let randomSeed = '';
  107. for (let index = 0; index < encodedPassword.length; index += 1) {
  108. randomSeed += encodedPassword.charCodeAt(index).toString();
  109. }
  110. const seedPosition = Math.floor(randomSeed.length / 5);
  111. const multiplier = Number.parseInt(
  112. randomSeed.charAt(seedPosition)
  113. + randomSeed.charAt(seedPosition * 2)
  114. + randomSeed.charAt(seedPosition * 3)
  115. + randomSeed.charAt(seedPosition * 4)
  116. + randomSeed.charAt(seedPosition * 5),
  117. 10,
  118. );
  119. const increment = Math.ceil(encodedPassword.length / 2);
  120. const modulus = Math.pow(2, 31) - 1;
  121. if (multiplier < 2) return '';
  122. let salt = Math.round(Math.random() * 1_000_000_000) % 100_000_000;
  123. randomSeed += salt;
  124. randomSeed = reduceSeed(randomSeed);
  125. let pseudoRandom = (multiplier * Number(randomSeed) + increment) % modulus;
  126. let encrypted = '';
  127. for (let index = 0; index < source.length; index += 1) {
  128. const encryptedChar = source.charCodeAt(index) ^ Math.floor((pseudoRandom / modulus) * 255);
  129. encrypted += encryptedChar < 16
  130. ? `0${encryptedChar.toString(16)}`
  131. : encryptedChar.toString(16);
  132. pseudoRandom = (multiplier * pseudoRandom + increment) % modulus;
  133. }
  134. let saltHex = salt.toString(16);
  135. while (saltHex.length < 8) saltHex = `0${saltHex}`;
  136. return encrypted + saltHex;
  137. }
  138. function decrypt(value: string, password = STORAGE_ENCRYPTION_PASSWORD): string {
  139. if (value === '') return '';
  140. const encodedPassword = encodeURIComponent(password || STORAGE_ENCRYPTION_PASSWORD);
  141. if (value.length < 8 || encodedPassword.length === 0) return '';
  142. let randomSeed = '';
  143. for (let index = 0; index < encodedPassword.length; index += 1) {
  144. randomSeed += encodedPassword.charCodeAt(index).toString();
  145. }
  146. const seedPosition = Math.floor(randomSeed.length / 5);
  147. const multiplier = Number.parseInt(
  148. randomSeed.charAt(seedPosition)
  149. + randomSeed.charAt(seedPosition * 2)
  150. + randomSeed.charAt(seedPosition * 3)
  151. + randomSeed.charAt(seedPosition * 4)
  152. + randomSeed.charAt(seedPosition * 5),
  153. 10,
  154. );
  155. const increment = Math.round(encodedPassword.length / 2);
  156. const modulus = Math.pow(2, 31) - 1;
  157. const salt = Number.parseInt(value.substring(value.length - 8), 16);
  158. const encrypted = value.substring(0, value.length - 8);
  159. randomSeed += salt;
  160. randomSeed = reduceSeed(randomSeed);
  161. let pseudoRandom = (multiplier * Number(randomSeed) + increment) % modulus;
  162. let decrypted = '';
  163. for (let index = 0; index < encrypted.length; index += 2) {
  164. const decryptedChar = Number.parseInt(encrypted.substring(index, index + 2), 16)
  165. ^ Math.floor((pseudoRandom / modulus) * 255);
  166. decrypted += String.fromCharCode(decryptedChar);
  167. pseudoRandom = (multiplier * pseudoRandom + increment) % modulus;
  168. }
  169. return decodeURIComponent(decrypted);
  170. }
  171. function reduceSeed(seed: string): string {
  172. let result = seed;
  173. while (result.length > 10) {
  174. let remaining = result;
  175. let total = 0;
  176. while (remaining.length > 0) {
  177. total += Number.parseInt(remaining.substring(0, 10), 10);
  178. remaining = remaining.substring(10);
  179. }
  180. result = total.toString();
  181. }
  182. return result;
  183. }
  184. /**
  185. * 进入平台判定:0=普通浏览器 1=iOS壳包 2=Android壳包 3=PWA(桌面图标/TWA 进入)
  186. * 用于按来源差异化展示(如提现页官网引流提示:桌面图标进入的玩家不显示)。
  187. * 壳包判定优先于 PWA;TWA 判定内联实现,避免 helpers→UAUtils 循环依赖。
  188. */
  189. export function getEnterPlatform(): 0 | 1 | 2 | 3 {
  190. if (ANDROID) return 2;
  191. if (IOS) return 1;
  192. return 0;
  193. }
  194. export function createUUID() {
  195. const chars: string[] = [];
  196. const hexDigits = '0123456789abcdef';
  197. for (let index = 0; index < 36; index += 1) {
  198. chars[index] = hexDigits.charAt(
  199. Math.floor(Math.random() * 16),
  200. );
  201. }
  202. // UUID version 4:0100
  203. chars[14] = '4';
  204. // RFC variant:10xx
  205. const variant = (Number.parseInt(chars[19], 16) & 0x3) | 0x8;
  206. chars[19] = hexDigits.charAt(variant);
  207. chars[8] = '-';
  208. chars[13] = '-';
  209. chars[18] = '-';
  210. chars[23] = '-';
  211. return chars.join('');
  212. }
  213. export function getUrlParams(url: string) {
  214. const queryIndex = url.indexOf('?');
  215. const params = {};
  216. if (queryIndex >= 0) {
  217. url.slice(queryIndex + 1).split('&').forEach((entry) => {
  218. if (!entry) return;
  219. const separatorIndex = entry.indexOf('=');
  220. const rawKey = separatorIndex >= 0 ? entry.slice(0, separatorIndex) : entry;
  221. const rawValue = separatorIndex >= 0 ? entry.slice(separatorIndex + 1) : '';
  222. try {
  223. const key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
  224. params[key] = decodeURIComponent(rawValue.replace(/\+/g, ' '));
  225. } catch {
  226. // Preserve malformed values instead of crashing the native callback.
  227. params[rawKey] = rawValue;
  228. }
  229. });
  230. }
  231. return params;
  232. }
  233. export function waitForSeconds(duration: number) {
  234. return new Promise((resolve, reject) => {
  235. setTimeout(resolve, duration * 1000)
  236. })
  237. }
  238. export function formatMaskContent(content: string, prefix: number, suffix: number) {
  239. const startIdx = Math.min(prefix, content.length);
  240. const endIdx = content.length - suffix;
  241. const first = content.substring(0, startIdx);
  242. const last = content.substring(endIdx, content.length)
  243. return `${first}***${last}`
  244. }
  245. export function formatMaskNickName(nickName: string) {
  246. return formatMaskContent(nickName, 2, 1);
  247. }
  248. export function formatMaskEmail(email: string) {
  249. if (!email || email.length === 0) return '';
  250. let index = email.indexOf('@');
  251. if (index === -1) return formatMaskContent(email, 2, 3);
  252. return formatMaskContent(email, 2, email.length - index);
  253. }
  254. export function checkIsEmail(str: string): boolean {
  255. const s = str.trim();
  256. if (!s) return false;
  257. const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  258. return reg.test(s);
  259. }
  260. export function safeFloor(num: number, decimal = 2) {
  261. let unit = Math.pow(10, decimal);
  262. return Math.floor(num * unit) / unit
  263. }
  264. export function formatMailTimeToMMDDYYYY(timeStr: string): string {
  265. const date = new Date(timeStr);
  266. if (isNaN(date.getTime())) {
  267. return ""; // 无效时间返回空
  268. }
  269. const month = String(date.getMonth() + 1).padStart(2, "0");
  270. const day = String(date.getDate()).padStart(2, "0");
  271. const year = date.getFullYear();
  272. return `${month}-${day}-${year}`;
  273. }
  274. export function parseMailRewardStr(str: string) {
  275. const result = [];
  276. const itemStrList = str.split(";").filter(Boolean);
  277. for (const itemStr of itemStrList) {
  278. const parts = itemStr.split(",");
  279. if (parts.length !== 2) continue;
  280. const type = parts[0]; // 直接字符串,不转数字!
  281. const rawRewardStr = parts[1];
  282. const rawReward = Number(rawRewardStr);
  283. if (isNaN(rawReward)) continue;
  284. if (rawReward == 0) continue;
  285. result.push({
  286. type,
  287. reward: rawReward / 100,
  288. rawReward: rawRewardStr
  289. });
  290. }
  291. return result;
  292. }