@hlw-uni/mp-vue 2.1.51 → 2.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/app.d.ts +2 -20
  2. package/dist/composables/device/index.d.ts +5 -70
  3. package/dist/composables/index.d.ts +5 -14
  4. package/dist/composables/navigator/index.d.ts +15 -39
  5. package/dist/composables/request/client.d.ts +21 -0
  6. package/dist/composables/request/index.d.ts +6 -0
  7. package/dist/composables/request/service.d.ts +30 -0
  8. package/dist/composables/{http → request}/types.d.ts +0 -3
  9. package/dist/composables/share/index.d.ts +7 -66
  10. package/dist/composables/theme/appearance.d.ts +3 -2
  11. package/dist/composables/theme/index.d.ts +5 -7
  12. package/dist/composables/utils/index.d.ts +25 -32
  13. package/dist/hlw.d.ts +2 -4
  14. package/dist/index.d.ts +2 -3
  15. package/dist/index.js +485 -1269
  16. package/dist/index.mjs +484 -1268
  17. package/package.json +2 -1
  18. package/src/app.ts +3 -143
  19. package/src/components/hlw-ad/index.vue +2 -2
  20. package/src/components/hlw-page/index.vue +1 -1
  21. package/src/composables/device/index.ts +110 -83
  22. package/src/composables/index.ts +9 -39
  23. package/src/composables/navigator/index.ts +77 -77
  24. package/src/composables/request/client.ts +204 -0
  25. package/src/composables/request/index.ts +15 -0
  26. package/src/composables/request/service.ts +54 -0
  27. package/src/composables/{http → request}/types.ts +0 -4
  28. package/src/composables/share/index.ts +64 -168
  29. package/src/composables/theme/appearance.ts +3 -2
  30. package/src/composables/theme/index.ts +11 -28
  31. package/src/composables/theme/palette.ts +22 -4
  32. package/src/composables/utils/index.ts +131 -95
  33. package/src/hlw.ts +6 -14
  34. package/src/index.ts +2 -3
  35. package/dist/composables/_internal/unwrap.d.ts +0 -15
  36. package/dist/composables/ad/index.d.ts +0 -78
  37. package/dist/composables/algo/index.d.ts +0 -7
  38. package/dist/composables/algo/uuid.d.ts +0 -17
  39. package/dist/composables/color/index.d.ts +0 -8
  40. package/dist/composables/contact/index.d.ts +0 -32
  41. package/dist/composables/format/index.d.ts +0 -9
  42. package/dist/composables/http/client.d.ts +0 -66
  43. package/dist/composables/http/index.d.ts +0 -8
  44. package/dist/composables/loading/index.d.ts +0 -7
  45. package/dist/composables/page-meta/index.d.ts +0 -18
  46. package/dist/composables/storage/index.d.ts +0 -16
  47. package/dist/composables/validate/index.d.ts +0 -12
  48. package/dist/error.d.ts +0 -1
  49. package/src/composables/_internal/unwrap.ts +0 -19
  50. package/src/composables/ad/index.ts +0 -412
  51. package/src/composables/algo/index.ts +0 -7
  52. package/src/composables/algo/uuid.ts +0 -27
  53. package/src/composables/color/index.ts +0 -44
  54. package/src/composables/contact/index.ts +0 -92
  55. package/src/composables/format/index.ts +0 -48
  56. package/src/composables/http/client.ts +0 -237
  57. package/src/composables/http/index.ts +0 -8
  58. package/src/composables/http/useRequest.ts +0 -107
  59. package/src/composables/loading/index.ts +0 -23
  60. package/src/composables/page-meta/index.ts +0 -49
  61. package/src/composables/storage/index.ts +0 -76
  62. package/src/composables/validate/index.ts +0 -58
  63. package/src/error.ts +0 -5
  64. /package/dist/composables/{http → request}/adapters/alist.d.ts +0 -0
  65. /package/dist/composables/{http → request}/adapters/base.d.ts +0 -0
  66. /package/dist/composables/{http → request}/adapters/cos.d.ts +0 -0
  67. /package/dist/composables/{http → request}/adapters/index.d.ts +0 -0
  68. /package/dist/composables/{http → request}/adapters/oss.d.ts +0 -0
  69. /package/dist/composables/{http → request}/adapters/qiniu.d.ts +0 -0
  70. /package/src/composables/{http → request}/adapters/alist.ts +0 -0
  71. /package/src/composables/{http → request}/adapters/base.ts +0 -0
  72. /package/src/composables/{http → request}/adapters/cos.ts +0 -0
  73. /package/src/composables/{http → request}/adapters/index.ts +0 -0
  74. /package/src/composables/{http → request}/adapters/oss.ts +0 -0
  75. /package/src/composables/{http → request}/adapters/qiniu.ts +0 -0
@@ -1,12 +0,0 @@
1
- /**
2
- * useValidate - 校验工具 composable
3
- */
4
- export declare function useValidate(): {
5
- phone: (value: string) => boolean;
6
- email: (value: string) => boolean;
7
- url: (value: string) => boolean;
8
- idCard: (value: string) => boolean;
9
- carNumber: (value: string) => boolean;
10
- password: (value: string) => boolean;
11
- empty: (value: unknown) => boolean;
12
- };
package/dist/error.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function getErrorMessage(error: unknown): string;
@@ -1,19 +0,0 @@
1
- /**
2
- * Adapter 返回值解包:兼容业务方传「已解包 T」或「ThinkAdmin envelope { code, data }」。
3
- *
4
- * 业务方写法:
5
- * setConfigAd({ getConfig: getAdConfig }) // 直接传 envelope-returning 函数
6
- * setConfigAd({ getConfig: async () => myUnwrapped }) // 也支持已解包
7
- *
8
- * 鸭子类型识别:raw 是对象且有 number 类型的 code 字段 → 当 envelope 处理。
9
- */
10
- export type AdapterPayload<T> = T | null | { code: number; data?: T; info?: string };
11
-
12
- export function unwrapPayload<T>(raw: AdapterPayload<T>): T | null {
13
- if (raw == null) return null;
14
- if (typeof raw === "object" && "code" in raw && typeof (raw as { code: unknown }).code === "number") {
15
- const env = raw as { code: number; data?: T };
16
- return env.code === 1 && env.data ? env.data : null;
17
- }
18
- return raw as T;
19
- }
@@ -1,412 +0,0 @@
1
- /**
2
- * useAd —— 完整的广告业务封装
3
- *
4
- * 内置了:
5
- * - 6 个 unit_id 配置(store 缓存,调 getConfig 拉一次)
6
- * - 激励视频:广告加载 loading → onShown 自动关闭 → 中途关闭挽留 modal → 看完调业务方的 claim 发奖
7
- * - 插屏:一行调用
8
- * - 底层 SDK 适配(onLoad / offClose / show 兜底 / 实例缓存)
9
- *
10
- * 业务侧使用方式:
11
- *
12
- * 1. 在 App.vue 注入「全局」回调(拿配置 + 校验登录):
13
- *
14
- * import { setConfigAd } from "@hlw-uni/mp-vue";
15
- * import { getAdConfig } from "@/api/ad";
16
- * import { useUserStore } from "@/store";
17
- *
18
- * setConfigAd({
19
- * getConfig: async () => {
20
- * const res = await getAdConfig();
21
- * return res.code === 1 && res.data ? res.data : null;
22
- * },
23
- * isAuth: () => !!useUserStore().token,
24
- * });
25
- *
26
- * 2. 任意页面拉配置 / 用 unit_id 渲染:
27
- *
28
- * const { config, loadConfig, showPopup } = useAd();
29
- * await loadConfig();
30
- * <hlw-ad type="banner" :unit-id="config.banner" />
31
- *
32
- * 3. 看广告领奖励(业务方按场景传不同的 claim 接口):
33
- *
34
- * const ok = await showReward(async () => {
35
- * const res = await claimAdReward();
36
- * return res.code === 1
37
- * ? { ok: true, reward: res.data?.reward }
38
- * : { ok: false, msg: res.info };
39
- * });
40
- *
41
- * // 不传 claim 就是纯展示,看完即返回 true
42
- * const ok = await showReward();
43
- */
44
-
45
- import { defineStore, storeToRefs } from "pinia";
46
- import { ref, computed } from "vue";
47
- import { useMsg, type HlwMsg } from "../msg";
48
- import { unwrapPayload, type AdapterPayload } from "../_internal/unwrap";
49
-
50
- /** showPopup 默认延迟(ms):避免一进页面就弹打扰用户 */
51
- const DEFAULT_POPUP_DELAY_MS = 3000;
52
-
53
- /** 6 种广告类型 */
54
- export type AdType = "banner" | "grid" | "custom" | "video" | "reward" | "popup";
55
-
56
- /** 广告配置 —— 字段名跟后端表列名对齐(plugin_qz_mp.{type}_unit_id) */
57
- export interface AdConfig {
58
- banner_unit_id: string;
59
- grid_unit_id: string;
60
- custom_unit_id: string;
61
- video_unit_id: string;
62
- reward_unit_id: string;
63
- popup_unit_id: string;
64
- /** VIP 屏蔽展示型广告(reward 除外):0=否,1=屏蔽 banner/grid/custom/video/popup */
65
- vip_no_ad?: 0 | 1;
66
- }
67
-
68
- /** 广告错误对象(onError 回调参数) */
69
- export interface AdError {
70
- errCode: number;
71
- errMsg: string;
72
- }
73
-
74
- /**
75
- * 业务回调注入接口 —— setConfigAd 时由项目提供。
76
- *
77
- * getConfig 支持两种返回:
78
- * - 已解包:直接返回 AdConfig 或 null
79
- * - ThinkAdmin envelope:返回 { code, data } 对象,库自动按 code===1 解包
80
- *
81
- * 业务方可以直接传 envelope-returning 的接口函数引用:
82
- * setConfigAd({ getConfig: getAdConfig })
83
- */
84
- export interface AdAdapter {
85
- getConfig: () => Promise<AdapterPayload<AdConfig>>;
86
- /** 是否已登录;不传 = 不校验(showReward 调用前会问一次) */
87
- isAuth?: () => boolean;
88
- /** 是否 VIP;配合 config.vip_no_ad=1 时屏蔽展示型广告(reward 不受影响) */
89
- isVip?: () => boolean;
90
- /**
91
- * 用户级强制屏蔽:返回 true 直接屏蔽展示型广告(reward 不受影响),
92
- * 优先级高于 mp.vip_no_ad + isVip 的联合判断;不传 = 不强制。
93
- */
94
- userNoAd?: () => boolean;
95
- }
96
-
97
- /** 激励视频关闭回调返回 */
98
- export interface AdCloseResult {
99
- /** 用户是否完整观看 */
100
- isEnded: boolean;
101
- }
102
-
103
- const EMPTY: AdConfig = {
104
- banner_unit_id: "",
105
- grid_unit_id: "",
106
- custom_unit_id: "",
107
- video_unit_id: "",
108
- reward_unit_id: "",
109
- popup_unit_id: "",
110
- vip_no_ad: 0,
111
- };
112
-
113
- let adapter: AdAdapter | null = null;
114
-
115
- /** 进行中的 loadConfig promise;并发调用时复用,避免冷启动多发请求 */
116
- let pending: Promise<void> | null = null;
117
-
118
- /**
119
- * 注入业务回调,应用启动时调用一次。
120
- * 不调用也不会崩,但 loadConfig / showReward 会无效。
121
- */
122
- export function setConfigAd(a: AdAdapter): void {
123
- adapter = a;
124
- }
125
-
126
- const useAdStore = defineStore("hlw_ad", () => {
127
- const config = ref<AdConfig>({ ...EMPTY });
128
- const loaded = ref(false);
129
- return { config, loaded };
130
- });
131
-
132
- /* ============================================================
133
- * 内部状态:消息单例 + 广告实例缓存
134
- * ============================================================ */
135
-
136
- let _msg: HlwMsg | null = null;
137
- const msg = (): HlwMsg => (_msg ??= useMsg());
138
-
139
- interface RewardedEntry {
140
- ad: any;
141
- /** 同一 unit_id 是否在 load 中,避免并发 */
142
- loading: boolean;
143
- }
144
- const rewardedCache = new Map<string, RewardedEntry>();
145
- const interstitialCache = new Map<string, any>();
146
-
147
- /* ============================================================
148
- * 公共 composable
149
- * ============================================================ */
150
-
151
- export function useAd() {
152
- const store = useAdStore();
153
- const { loaded } = storeToRefs(store);
154
-
155
- /**
156
- * 真实生效的广告配置;以下任一命中即屏蔽展示型 unit_id(reward 保留):
157
- * 1. adapter.userNoAd() === true —— 用户级强制屏蔽,优先级最高
158
- * 2. raw.vip_no_ad === 1 && adapter.isVip() —— 小程序级 VIP 隐藏开关
159
- */
160
- const config = computed<AdConfig>(() => {
161
- const raw = store.config;
162
- const userForce = adapter?.userNoAd?.() === true;
163
- const vipHide = raw.vip_no_ad === 1 && adapter?.isVip?.() === true;
164
- if (userForce || vipHide) {
165
- return { ...EMPTY, reward_unit_id: raw.reward_unit_id, vip_no_ad: 1 };
166
- }
167
- return raw;
168
- });
169
-
170
- /**
171
- * 拉取广告配置(小程序冷启动后调一次即可)。
172
- * 并发调用复用同一次请求;`force=true` 跳过 loaded/pending 短路重新拉。
173
- */
174
- async function loadConfig(force = false): Promise<void> {
175
- if (loaded.value && !force) return;
176
- if (pending && !force) return pending;
177
- if (!adapter?.getConfig) {
178
- console.warn("[useAd] adapter.getConfig 未注入;先调用 setConfigAd()");
179
- return;
180
- }
181
- const fn = adapter.getConfig;
182
- const flight = (async () => {
183
- try {
184
- const cfg = unwrapPayload(await fn());
185
- if (cfg) {
186
- store.config = cfg;
187
- store.loaded = true;
188
- }
189
- } catch (e) {
190
- console.warn("[useAd] load config failed", e);
191
- } finally {
192
- if (pending === flight) pending = null;
193
- }
194
- })();
195
- pending = flight;
196
- return flight;
197
- }
198
-
199
- /** 取指定类型的 unit_id(hlw-ad 组件 / 业务直接调时用),自动应用 VIP 屏蔽 */
200
- function getUnitId(type: AdType): string {
201
- return (config.value[`${type}_unit_id` as keyof AdConfig] as string) || "";
202
- }
203
-
204
- /**
205
- * 显示激励视频 —— 播一次、关闭后调 onClose(带 isEnded),其它都交给业务。
206
- *
207
- * showReward(({ isEnded }) => {
208
- * if (!isEnded) confirm().then(goon => goon && tapReward());
209
- * else claimAdReward().then(...);
210
- * });
211
- *
212
- * @param onClose 关闭回调;不传则只播。retry / toast / 发奖全在业务侧自己写。
213
- */
214
- async function showReward(onClose?: (res: AdCloseResult) => void | Promise<void>): Promise<void> {
215
- const unitId = getUnitId("reward");
216
- if (!unitId) {
217
- msg().toast("激励广告未配置");
218
- return;
219
- }
220
- if (adapter?.isAuth && !adapter.isAuth()) {
221
- msg().toast("请先登录");
222
- return;
223
- }
224
- const closeRes = await playRewardedOnce(unitId);
225
- if (onClose) await onClose(closeRes);
226
- }
227
-
228
- /**
229
- * 显示插屏广告
230
- * @returns true=展示成功;false=配置缺失 / show 失败(如近期已展示过)
231
- */
232
- /**
233
- * 显示插屏广告,默认延迟 3000ms 避免一进页面就弹打扰;传 0 立即弹。
234
- * @returns true=展示成功;false=配置缺失 / show 失败(如近期已展示过)
235
- *
236
- * 实现注意:用 setTimeout + .then() 串接而非 async/await,保持 WX 插屏
237
- * ad.show() 的调用栈跟 setTimeout 回调一致 —— 小程序引擎对插屏的合法
238
- * 触发时机敏感,async/await 多插的 microtask 会被判为"非合法触发"导致
239
- * 静默不弹。
240
- */
241
- function showPopup(delayMs: number = DEFAULT_POPUP_DELAY_MS): Promise<boolean> {
242
- return new Promise<boolean>((resolve) => {
243
- setTimeout(() => {
244
- loadConfig().then(() => {
245
- const unitId = getUnitId("popup");
246
- if (!unitId) {
247
- resolve(false);
248
- return;
249
- }
250
- showInterstitialAd(unitId).then(resolve);
251
- });
252
- }, delayMs);
253
- });
254
- }
255
-
256
- return {
257
- // 状态
258
- config,
259
- loaded,
260
- // 方法
261
- loadConfig,
262
- getUnitId,
263
- showReward,
264
- showPopup,
265
- confirm: confirmModal,
266
- };
267
- }
268
-
269
- /* ============================================================
270
- * 内部 helper:业务流程层
271
- * ============================================================ */
272
-
273
- /** 播放一次激励视频,期间显示加载提示,广告 UI 弹出后自动 hide */
274
- async function playRewardedOnce(unitId: string): Promise<AdCloseResult> {
275
- msg().showLoading("广告加载中");
276
- let hidden = false;
277
- const hide = () => {
278
- if (hidden) return;
279
- hidden = true;
280
- msg().hideLoading();
281
- };
282
- try {
283
- const isEnded = await showRewardedAd(unitId, { onShown: hide });
284
- return { isEnded };
285
- } finally {
286
- // 兜底:如果 onShown 没触发(show 直接 reject 等),最终也得关 loading
287
- hide();
288
- }
289
- }
290
-
291
- /**
292
- * 激励视频中途关闭挽留弹窗 —— 通过 useAd().confirm 暴露给业务方:
293
- *
294
- * const { showReward, confirm } = useAd();
295
- * showReward(async ({ isEnded }) => {
296
- * if (!isEnded) return { ok: false, retry: await confirm() };
297
- * const r = await claimAdReward();
298
- * return r.code === 1 ? { ok: true, reward: r.data?.reward } : { ok: false, msg: r.info };
299
- * });
300
- *
301
- * @returns true=用户选「继续观看」 / false=放弃
302
- */
303
- function confirmModal(): Promise<boolean> {
304
- return new Promise((resolve) => {
305
- uni.showModal({
306
- title: "提示",
307
- content: "看完广告才可以领取奖励哦,要继续观看吗?",
308
- confirmText: "继续观看",
309
- cancelText: "放弃",
310
- success: (r) => resolve(!!r.confirm),
311
- fail: () => resolve(false),
312
- });
313
- });
314
- }
315
-
316
- /* ============================================================
317
- * 内部 helper:底层 SDK 适配
318
- * ============================================================ */
319
-
320
- /**
321
- * 底层激励视频包装:onClose / onError 解绑 + load + show 兜底
322
- *
323
- * @param hooks.onShown 广告 UI 已渲染时回调(业务用来关 loading)
324
- */
325
- function showRewardedAd(unitId: string, hooks?: { onShown?: () => void }): Promise<boolean> {
326
- return new Promise((resolve) => {
327
- let entry = rewardedCache.get(unitId);
328
- if (!entry) {
329
- const ad: any = uni.createRewardedVideoAd({ adUnitId: unitId });
330
- if (!ad) { resolve(false); return; }
331
- ad.onError?.((err: AdError) => {
332
- console.warn(`[useAd] reward onError (${unitId})`, err);
333
- });
334
- entry = { ad, loading: false };
335
- rewardedCache.set(unitId, entry);
336
- }
337
- const { ad } = entry;
338
-
339
- let closeHandler: ((res: { isEnded: boolean }) => void) | null = null;
340
- let errorHandler: ((err: AdError) => void) | null = null;
341
- const cleanup = () => {
342
- if (closeHandler) { ad.offClose?.(closeHandler); closeHandler = null; }
343
- if (errorHandler) { ad.offError?.(errorHandler); errorHandler = null; }
344
- };
345
- closeHandler = (res) => {
346
- cleanup();
347
- resolve(!!(res && res.isEnded));
348
- };
349
- errorHandler = (err) => {
350
- console.warn(`[useAd] reward show error (${unitId})`, err);
351
- cleanup();
352
- resolve(false);
353
- };
354
- ad.onClose(closeHandler);
355
- ad.onError(errorHandler);
356
-
357
- const doShow = () =>
358
- ad.show()
359
- .then(() => {
360
- try { hooks?.onShown?.(); } catch (e) { console.warn("[useAd] onShown error", e); }
361
- })
362
- .catch(() => {
363
- cleanup();
364
- resolve(false);
365
- });
366
-
367
- if (entry.loading) {
368
- Promise.resolve().then(doShow);
369
- } else {
370
- entry.loading = true;
371
- ad.load()
372
- .then(doShow)
373
- .catch(doShow)
374
- .finally(() => { if (entry) entry.loading = false; });
375
- }
376
- });
377
- }
378
-
379
- /** 底层插屏广告包装:实例按 unitId 缓存复用,show 失败 console.warn 不重试(频控/网络等失败重试也救不了) */
380
- function showInterstitialAd(unitId: string): Promise<boolean> {
381
- return new Promise((resolve) => {
382
- let ad: any = interstitialCache.get(unitId);
383
- if (!ad) {
384
- // 老基础库可能没有这个 API(对应官方 if (wx.createInterstitialAd))
385
- if (typeof uni.createInterstitialAd !== "function") {
386
- console.warn("[useAd] 当前基础库不支持插屏广告");
387
- resolve(false);
388
- return;
389
- }
390
- ad = uni.createInterstitialAd({ adUnitId: unitId });
391
- if (!ad) { resolve(false); return; }
392
- ad.onError?.((err: AdError) => {
393
- console.warn(`[useAd] popup onError (${unitId})`, err);
394
- });
395
- interstitialCache.set(unitId, ad);
396
- }
397
- ad.show()
398
- .then(() => resolve(true))
399
- .catch((err: any) => {
400
- console.warn(`[useAd] popup show error (${unitId})`, err);
401
- resolve(false);
402
- });
403
- });
404
- }
405
-
406
- /** 销毁全部广告实例并清空缓存(业务一般不用,hot reload 时调) */
407
- export function destroyAds(): void {
408
- rewardedCache.forEach((e) => e.ad?.destroy?.());
409
- rewardedCache.clear();
410
- interstitialCache.forEach((ad) => ad?.destroy?.());
411
- interstitialCache.clear();
412
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * 算法集:把通用算法包成 useXxx composable,业务侧统一从这里取。
3
- *
4
- * 已有:
5
- * - useUuid RFC 4122 v4,32 字符 hex
6
- */
7
- export { useUuid } from "./uuid";
@@ -1,27 +0,0 @@
1
- /**
2
- * UUID v4 生成器(RFC 4122 v4 算法),返回 32 字符 hex(无连字符)。
3
- *
4
- * 用途:当 nonce、idempotency key、临时唯一标识用。
5
- *
6
- * 算法:
7
- * 1. 生成 16 字节随机源
8
- * 2. byte 6 高 4 位置成 0100 标版本号 v4
9
- * 3. byte 8 高 2 位置成 10 标变体(RFC 4122)
10
- * 4. 16 字节转 hex 拼成 32 字符串
11
- *
12
- * 注意:随机源用 Math.random(小程序兼容兜底),加密强度有限,
13
- * 不能用于密钥派生 / token 签发,只能挡重放。
14
- */
15
- export function useUuid() {
16
- function v4(): string {
17
- const bytes: number[] = [];
18
- for (let i = 0; i < 16; i++) {
19
- bytes.push(Math.floor(Math.random() * 256));
20
- }
21
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
22
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
23
- return bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
24
- }
25
-
26
- return { v4 };
27
- }
@@ -1,44 +0,0 @@
1
- const HEX_RE = /^#[0-9a-fA-F]{6}$/;
2
-
3
- /**
4
- * 解析 6 位十六进制颜色值,返回 RGB 数组。
5
- */
6
- function parseHex(hex: string): [number, number, number] {
7
- if (!HEX_RE.test(hex)) throw new Error(`Invalid hex color: ${hex}`);
8
- return [
9
- parseInt(hex.slice(1, 3), 16),
10
- parseInt(hex.slice(3, 5), 16),
11
- parseInt(hex.slice(5, 7), 16),
12
- ];
13
- }
14
-
15
- /**
16
- * 颜色处理工具。
17
- */
18
- export function useColor() {
19
- /**
20
- * 将 CSS 变量对象拼接成内联 style 字符串。
21
- */
22
- function varsToStyle(vars: Record<string, string>): string {
23
- return Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";") + ";";
24
- }
25
-
26
- /**
27
- * 将十六进制颜色转为 rgba 字符串。
28
- */
29
- function hexToRgba(hex: string, alpha: number): string {
30
- const [r, g, b] = parseHex(hex);
31
- return `rgba(${r},${g},${b},${alpha})`;
32
- }
33
-
34
- /**
35
- * 将十六进制颜色按比例压暗。
36
- */
37
- function darkenHex(hex: string, amount = 0.15): string {
38
- const [r, g, b] = parseHex(hex);
39
- const d = (c: number) => Math.max(0, Math.round(c * (1 - amount)));
40
- return `#${d(r).toString(16).padStart(2, "0")}${d(g).toString(16).padStart(2, "0")}${d(b).toString(16).padStart(2, "0")}`;
41
- }
42
-
43
- return { varsToStyle, hexToRgba, darkenHex };
44
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * useContact —— 客服按钮配置(business-level cache)
3
- *
4
- * 后端返回 <button open-type="contact"> 的 4 个微信原生属性,模块级缓存共享给所有客服按钮。
5
- *
6
- * 使用方式(业务侧):
7
- *
8
- * 1. App.vue / bootstrap 注入回调:
9
- *
10
- * setConfigContact({
11
- * getConfig: async () => {
12
- * const res = await getContactConfig();
13
- * return res.code === 1 ? res.data : null;
14
- * },
15
- * });
16
- *
17
- * 2. 任意按钮组件:
18
- *
19
- * const contact = useContact();
20
- * <hlw-button open-type="contact" v-bind="contact">联系客服</hlw-button>
21
- */
22
- import { computed, ref } from "vue";
23
- import { unwrapPayload, type AdapterPayload } from "../_internal/unwrap";
24
-
25
- /** 后端返回的客服配置(按 button open-type=contact 标准属性命名) */
26
- export interface ContactConfig {
27
- send_message_title: string;
28
- send_message_path: string;
29
- send_message_img: string;
30
- show_message_card: boolean;
31
- }
32
-
33
- /**
34
- * Adapter 注入接口 —— getConfig 支持「已解包」或「ThinkAdmin envelope」两种返回。
35
- * 业务方可以直接传 envelope-returning 接口:setConfigContact({ getConfig: getContactConfig })
36
- */
37
- export interface ContactAdapter {
38
- getConfig: () => Promise<AdapterPayload<ContactConfig>>;
39
- }
40
-
41
- /** v-bind 到 button 的 camelCase props(微信原生属性约定) */
42
- export interface ContactBindProps {
43
- sendMessageTitle: string;
44
- sendMessagePath: string;
45
- sendMessageImg: string;
46
- showMessageCard: boolean;
47
- }
48
-
49
- let adapter: ContactAdapter | null = null;
50
- const config = ref<ContactConfig | null>(null);
51
- let pending: Promise<void> | null = null;
52
-
53
- /**
54
- * 注入业务回调(应用启动时调用一次;不调用则 useContact 始终返回空字段)。
55
- */
56
- export function setConfigContact(a: ContactAdapter): void {
57
- adapter = a;
58
- }
59
-
60
- function loadConfig(): Promise<void> {
61
- if (config.value) return Promise.resolve();
62
- if (pending) return pending;
63
- if (!adapter?.getConfig) {
64
- console.warn("[useContact] adapter.getConfig 未注入;先调用 setConfigContact()");
65
- return Promise.resolve();
66
- }
67
- pending = adapter.getConfig()
68
- .then((raw) => {
69
- const cfg = unwrapPayload(raw);
70
- if (cfg) config.value = cfg;
71
- })
72
- .catch((e) => {
73
- console.warn("[useContact] load config failed", e);
74
- })
75
- .finally(() => {
76
- pending = null;
77
- });
78
- return pending;
79
- }
80
-
81
- /**
82
- * 返回 v-bind 友好的客服配置 computed(首次调用会异步拉一次配置)。
83
- */
84
- export function useContact() {
85
- void loadConfig();
86
- return computed<ContactBindProps>(() => ({
87
- sendMessageTitle: config.value?.send_message_title || "",
88
- sendMessagePath: config.value?.send_message_path || "",
89
- sendMessageImg: config.value?.send_message_img || "",
90
- showMessageCard: config.value?.show_message_card ?? false,
91
- }));
92
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * useFormat — 格式化工具 composable
3
- */
4
- export function useFormat() {
5
- /**
6
- * 按指定模板格式化日期时间。
7
- */
8
- function date(date: Date | number | string, format = "YYYY-MM-DD HH:mm:ss"): string {
9
- const d = new Date(date);
10
- if (isNaN(d.getTime())) return "";
11
-
12
- const year = d.getFullYear();
13
- const month = String(d.getMonth() + 1).padStart(2, "0");
14
- const day = String(d.getDate()).padStart(2, "0");
15
- const hours = String(d.getHours()).padStart(2, "0");
16
- const minutes = String(d.getMinutes()).padStart(2, "0");
17
- const seconds = String(d.getSeconds()).padStart(2, "0");
18
-
19
- return format.replace("YYYY", String(year)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes).replace("ss", seconds);
20
- }
21
-
22
- /**
23
- * 将字节数格式化为更易读的文件大小。
24
- */
25
- function fileSize(bytes: number): string {
26
- if (bytes === 0) return "0 B";
27
- const k = 1024;
28
- const sizes = ["B", "KB", "MB", "GB", "TB"];
29
- const i = Math.floor(Math.log(bytes) / Math.log(k));
30
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
31
- }
32
-
33
- /**
34
- * 对手机号中间四位进行脱敏。
35
- */
36
- function phone(value: string): string {
37
- return value.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
38
- }
39
-
40
- /**
41
- * 按指定精度与千分位格式输出金额。
42
- */
43
- function money(amount: number, decimals = 2, decPoint = ".", thousandsSep = ","): string {
44
- return amount.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep);
45
- }
46
-
47
- return { date, fileSize, phone, money };
48
- }