@hlw-uni/mp-vue 2.1.13 → 2.1.15

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.
@@ -35,6 +35,11 @@ export interface AdAdapter {
35
35
  isAuth?: () => boolean;
36
36
  /** 是否 VIP;配合 config.vip_no_ad=1 时屏蔽展示型广告(reward 不受影响) */
37
37
  isVip?: () => boolean;
38
+ /**
39
+ * 用户级强制屏蔽:返回 true 直接屏蔽展示型广告(reward 不受影响),
40
+ * 优先级高于 mp.vip_no_ad + isVip 的联合判断;不传 = 不强制。
41
+ */
42
+ userNoAd?: () => boolean;
38
43
  }
39
44
  /** 激励视频关闭回调返回 */
40
45
  export interface AdCloseResult {
package/dist/index.js CHANGED
@@ -658,9 +658,11 @@ var __publicField = (obj, key, value) => {
658
658
  const store = useAdStore();
659
659
  const { loaded } = pinia.storeToRefs(store);
660
660
  const config2 = vue.computed(() => {
661
- var _a;
661
+ var _a, _b;
662
662
  const raw = store.config;
663
- if (raw.vip_no_ad === 1 && ((_a = adapter$1 == null ? void 0 : adapter$1.isVip) == null ? void 0 : _a.call(adapter$1))) {
663
+ const userForce = ((_a = adapter$1 == null ? void 0 : adapter$1.userNoAd) == null ? void 0 : _a.call(adapter$1)) === true;
664
+ const vipHide = raw.vip_no_ad === 1 && ((_b = adapter$1 == null ? void 0 : adapter$1.isVip) == null ? void 0 : _b.call(adapter$1)) === true;
665
+ if (userForce || vipHide) {
664
666
  return { ...EMPTY, reward_unit_id: raw.reward_unit_id, vip_no_ad: 1 };
665
667
  }
666
668
  return raw;
@@ -1815,6 +1817,14 @@ var __publicField = (obj, key, value) => {
1815
1817
  _interceptorCleanup.forEach((dispose) => dispose());
1816
1818
  _interceptorCleanup = [];
1817
1819
  const offRequest = http.onRequest((config2) => {
1820
+ const method = (config2.method ?? "GET").toUpperCase();
1821
+ if (method === "GET" && config2.data && typeof config2.data === "object") {
1822
+ const qs = Object.entries(config2.data).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
1823
+ if (qs) {
1824
+ config2.url = config2.url + (config2.url.includes("?") ? "&" : "?") + qs;
1825
+ }
1826
+ config2.data = void 0;
1827
+ }
1818
1828
  const device = useDevice();
1819
1829
  if (device.value) {
1820
1830
  const d = device.value;
package/dist/index.mjs CHANGED
@@ -657,9 +657,11 @@ function useAd() {
657
657
  const store = useAdStore();
658
658
  const { loaded } = storeToRefs(store);
659
659
  const config2 = computed(() => {
660
- var _a;
660
+ var _a, _b;
661
661
  const raw = store.config;
662
- if (raw.vip_no_ad === 1 && ((_a = adapter$1 == null ? void 0 : adapter$1.isVip) == null ? void 0 : _a.call(adapter$1))) {
662
+ const userForce = ((_a = adapter$1 == null ? void 0 : adapter$1.userNoAd) == null ? void 0 : _a.call(adapter$1)) === true;
663
+ const vipHide = raw.vip_no_ad === 1 && ((_b = adapter$1 == null ? void 0 : adapter$1.isVip) == null ? void 0 : _b.call(adapter$1)) === true;
664
+ if (userForce || vipHide) {
663
665
  return { ...EMPTY, reward_unit_id: raw.reward_unit_id, vip_no_ad: 1 };
664
666
  }
665
667
  return raw;
@@ -1814,6 +1816,14 @@ function setupInterceptors(options = {}) {
1814
1816
  _interceptorCleanup.forEach((dispose) => dispose());
1815
1817
  _interceptorCleanup = [];
1816
1818
  const offRequest = http.onRequest((config2) => {
1819
+ const method = (config2.method ?? "GET").toUpperCase();
1820
+ if (method === "GET" && config2.data && typeof config2.data === "object") {
1821
+ const qs = Object.entries(config2.data).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
1822
+ if (qs) {
1823
+ config2.url = config2.url + (config2.url.includes("?") ? "&" : "?") + qs;
1824
+ }
1825
+ config2.data = void 0;
1826
+ }
1817
1827
  const device = useDevice();
1818
1828
  if (device.value) {
1819
1829
  const d = device.value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hlw-uni/mp-vue",
3
- "version": "2.1.13",
3
+ "version": "2.1.15",
4
4
  "description": "hlw-uni 小程序运行时 — Vue 组件 + composables + theme + http + 工具集(合并自原 mp-core)",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/app.ts CHANGED
@@ -102,6 +102,21 @@ export function setupInterceptors(options: InterceptorOptions & { sigSecret?: st
102
102
  * 请求拦截:注入设备信息、签名和 token。
103
103
  */
104
104
  const offRequest = http.onRequest((config: RequestConfig) => {
105
+ // GET 请求:把 cfg.data 合并到 url query,并清空 cfg.data。
106
+ // 否则 uni.request 会在 sig 之后再把 data 自动拼到 URL,导致后端 raw query
107
+ // 多了未参与签名的字段(如 id/cate_id),sig 校验必然失败。
108
+ const method = (config.method ?? 'GET').toUpperCase();
109
+ if (method === 'GET' && config.data && typeof config.data === 'object') {
110
+ const qs = Object.entries(config.data as Record<string, unknown>)
111
+ .filter(([, v]) => v !== undefined && v !== null)
112
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
113
+ .join('&');
114
+ if (qs) {
115
+ config.url = config.url + (config.url.includes('?') ? '&' : '?') + qs;
116
+ }
117
+ config.data = undefined;
118
+ }
119
+
105
120
  const device = useDevice();
106
121
  if (device.value) {
107
122
  const d = device.value;
@@ -3,35 +3,21 @@
3
3
  class="hlw-card"
4
4
  :class="[
5
5
  `hlw-card--radius-${radius}`,
6
- border ? 'hlw-card--bordered' : '',
6
+ ...borderClasses,
7
7
  ]"
8
+ :style="rootStyle"
8
9
  >
9
- <!-- 头部 -->
10
+ <!-- 头部 — 用 #header slot 自定义;常规场景请用 <hlw-card-header> -->
10
11
  <view v-if="hasHeader" class="hlw-card-header">
11
- <slot name="header">
12
- <view class="hlw-card-header-inner">
13
- <!-- 头部左侧 -->
14
- <view class="hlw-card-header-left">
15
- <slot name="header-left">
16
- <text v-if="title" class="hlw-card-title">{{ title }}</text>
17
- </slot>
18
- </view>
19
- <!-- 头部右侧 -->
20
- <view v-if="$slots['header-right'] || extra" class="hlw-card-header-right">
21
- <slot name="header-right">
22
- <text v-if="extra" class="hlw-card-extra">{{ extra }}</text>
23
- </slot>
24
- </view>
25
- </view>
26
- </slot>
12
+ <slot name="header" />
27
13
  </view>
28
14
 
29
- <!-- 头部虚线分隔 -->
30
- <view v-if="showDivider" class="hlw-card-divider"></view>
15
+ <!-- 头部虚线分隔(有 #header slot 且 divider != false 时显示) -->
16
+ <view v-if="showDivider" class="hlw-card-divider" />
31
17
 
32
18
  <!-- 内容区 -->
33
19
  <view class="hlw-card-body" :class="{ 'hlw-card-body--padded': padding }">
34
- <slot></slot>
20
+ <slot />
35
21
  </view>
36
22
 
37
23
  <!-- 底部 -->
@@ -39,10 +25,10 @@
39
25
  <slot name="footer">
40
26
  <view class="hlw-card-footer-inner">
41
27
  <view class="hlw-card-footer-left">
42
- <slot name="footer-left"></slot>
28
+ <slot name="footer-left" />
43
29
  </view>
44
30
  <view v-if="$slots['footer-right']" class="hlw-card-footer-right">
45
- <slot name="footer-right"></slot>
31
+ <slot name="footer-right" />
46
32
  </view>
47
33
  </view>
48
34
  </slot>
@@ -56,68 +42,106 @@ import { computed, useSlots } from "vue";
56
42
  /**
57
43
  * hlw-card 卡片容器
58
44
  *
59
- * @example 基础用法
45
+ * 头部用法已迁出:常规标题 + 副标题请用 <hlw-card-header>,写在 default slot 里
46
+ * (记得 :padding="false" 避免 body padding 把 header 顶出来)。
47
+ *
48
+ * @example 标准头部 + body
60
49
  * ```vue
61
- * <hlw-card title="标题" extra="更多">
62
- * <text>内容</text>
50
+ * <hlw-card :padding="false">
51
+ * <hlw-card-header title="标题" icon="i-fa6-solid-star" extra="副标题" />
52
+ * <view style="padding: 24rpx 28rpx">content</view>
63
53
  * </hlw-card>
64
54
  * ```
65
55
  *
66
- * @example 自定义头部左右
56
+ * @example 完全自定义头部
67
57
  * ```vue
68
58
  * <hlw-card>
69
- * <template #header-left>
70
- * <text>自定义左侧</text>
71
- * </template>
72
- * <template #header-right>
73
- * <button>操作</button>
74
- * </template>
59
+ * <template #header>...</template>
75
60
  * <text>内容</text>
76
61
  * </hlw-card>
77
62
  * ```
78
63
  *
79
64
  * @example 自定义底部
80
65
  * ```vue
81
- * <hlw-card title="标题">
66
+ * <hlw-card>
82
67
  * <text>内容</text>
83
- * <template #footer-left>
84
- * <text>左侧说明</text>
85
- * </template>
86
- * <template #footer-right>
87
- * <button>确认</button>
88
- * </template>
68
+ * <template #footer-left><text>左侧说明</text></template>
69
+ * <template #footer-right><button>确认</button></template>
89
70
  * </hlw-card>
90
71
  * ```
91
72
  */
73
+ type BorderValue = boolean | string | string[];
74
+
92
75
  interface Props {
93
- /** 卡片标题 */
94
- title?: string;
95
- /** 头部右侧文字(无 header-right slot 时显示) */
96
- extra?: string;
97
- /** 是否显示边框,默认 true */
98
- border?: boolean;
76
+ /**
77
+ * 边框,支持三种形式:
78
+ * - `true`(默认)/`false` —— 四边全开 / 全关
79
+ * - 字符串:"t r b l" 或 "top right bottom left",空格分隔,如 `"t b"` = 仅上下
80
+ * - 数组:`['t','l']` 同上
81
+ */
82
+ border?: BorderValue;
83
+ /** 边框颜色,任意 CSS color:`#f00`、`rgb(...)`、`var(--xxx)`;默认走主题 var(--border-color) */
84
+ borderColor?: string;
85
+ /** 边框线型:solid(默认)/ dashed / dotted / double */
86
+ borderStyle?: "solid" | "dashed" | "dotted" | "double";
87
+ /** 边框宽度,CSS 长度值,默认 `1rpx` */
88
+ borderWidth?: string;
99
89
  /** 圆角大小,对应 CSS 变量体系 */
100
90
  radius?: "none" | "sm" | "md" | "lg" | "xl";
101
- /** 头部与内容之间是否显示虚线分隔,有头部时默认 true */
91
+ /** 头部与内容之间是否显示虚线分隔,有 #header slot 时默认 true */
102
92
  divider?: boolean;
103
93
  /** body 是否有内边距,默认 true */
104
94
  padding?: boolean;
105
95
  }
106
96
 
107
97
  const props = withDefaults(defineProps<Props>(), {
108
- title: "",
109
- extra: "",
110
98
  border: true,
99
+ borderColor: "",
100
+ borderStyle: "solid",
101
+ borderWidth: "",
111
102
  radius: "xl",
112
103
  divider: undefined,
113
104
  padding: true,
114
105
  });
115
106
 
107
+ const rootStyle = computed<Record<string, string>>(() => {
108
+ const s: Record<string, string> = {};
109
+ if (props.borderColor) s["--card-border-color"] = props.borderColor;
110
+ if (props.borderStyle && props.borderStyle !== "solid") s["--card-border-style"] = props.borderStyle;
111
+ if (props.borderWidth) s["--card-border-width"] = props.borderWidth;
112
+ return s;
113
+ });
114
+
115
+ const SIDE_MAP: Record<string, string> = {
116
+ t: "top", top: "top",
117
+ r: "right", right: "right",
118
+ b: "bottom", bottom: "bottom",
119
+ l: "left", left: "left",
120
+ };
121
+
122
+ const borderClasses = computed<string[]>(() => {
123
+ if (props.border === false) return [];
124
+ if (props.border === true) return ["hlw-card--bordered"];
125
+
126
+ const sides = Array.isArray(props.border)
127
+ ? props.border
128
+ : String(props.border).trim().split(/\s+/).filter(Boolean);
129
+
130
+ const seen = new Set<string>();
131
+ const classes: string[] = [];
132
+ for (const s of sides) {
133
+ const side = SIDE_MAP[s.toLowerCase()];
134
+ if (side && !seen.has(side)) {
135
+ seen.add(side);
136
+ classes.push(`hlw-card--border-${side}`);
137
+ }
138
+ }
139
+ return classes.length === 4 ? ["hlw-card--bordered"] : classes;
140
+ });
141
+
116
142
  const slots = useSlots();
117
143
 
118
- const hasHeader = computed(
119
- () => !!(props.title || props.extra || slots.header || slots["header-left"] || slots["header-right"]),
120
- );
144
+ const hasHeader = computed(() => !!slots.header);
121
145
 
122
146
  const hasFooter = computed(
123
147
  () => !!(slots.footer || slots["footer-left"] || slots["footer-right"]),
@@ -142,46 +166,46 @@ const showDivider = computed(() => {
142
166
  &--radius-lg { border-radius: var(--radius-lg, 24rpx); }
143
167
  &--radius-xl { border-radius: var(--radius-xl, 32rpx); }
144
168
 
145
- /* 边框 */
169
+ /* 边框 — width / style / color 全部走 CSS 变量,未设置时回落 */
146
170
  &--bordered {
147
- border: 1rpx solid var(--border-color, #e2e8f0);
171
+ border:
172
+ var(--card-border-width, 1rpx)
173
+ var(--card-border-style, solid)
174
+ var(--card-border-color, var(--border-color, #e2e8f0));
175
+ }
176
+
177
+ /* 边框 — 单边 */
178
+ &--border-top {
179
+ border-top:
180
+ var(--card-border-width, 1rpx)
181
+ var(--card-border-style, solid)
182
+ var(--card-border-color, var(--border-color, #e2e8f0));
183
+ }
184
+ &--border-right {
185
+ border-right:
186
+ var(--card-border-width, 1rpx)
187
+ var(--card-border-style, solid)
188
+ var(--card-border-color, var(--border-color, #e2e8f0));
189
+ }
190
+ &--border-bottom {
191
+ border-bottom:
192
+ var(--card-border-width, 1rpx)
193
+ var(--card-border-style, solid)
194
+ var(--card-border-color, var(--border-color, #e2e8f0));
195
+ }
196
+ &--border-left {
197
+ border-left:
198
+ var(--card-border-width, 1rpx)
199
+ var(--card-border-style, solid)
200
+ var(--card-border-color, var(--border-color, #e2e8f0));
148
201
  }
149
202
  }
150
203
 
151
- /* 头部 */
204
+ /* 头部 wrapper(#header slot) */
152
205
  .hlw-card-header {
153
206
  width: 100%;
154
207
  }
155
208
 
156
- .hlw-card-header-inner {
157
- display: flex;
158
- align-items: center;
159
- justify-content: space-between;
160
- padding: 24rpx 28rpx;
161
- }
162
-
163
- .hlw-card-header-left {
164
- flex: 1;
165
- min-width: 0;
166
- }
167
-
168
- .hlw-card-header-right {
169
- flex-shrink: 0;
170
- margin-left: 16rpx;
171
- }
172
-
173
- .hlw-card-title {
174
- font-size: var(--font-sm, 24rpx);
175
- font-weight: 700;
176
- color: #1e293b;
177
- letter-spacing: 0.02em;
178
- }
179
-
180
- .hlw-card-extra {
181
- font-size: var(--font-xs, 20rpx);
182
- color: #94a3b8;
183
- }
184
-
185
209
  /* 虚线分隔 */
186
210
  .hlw-card-divider {
187
211
  width: 100%;
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <view class="hlw-card-header">
3
+ <view class="hlw-card-header__left">
4
+ <slot name="left">
5
+ <text v-if="icon" class="hlw-card-header__icon" :class="icon" />
6
+ <text v-if="title" class="hlw-card-header__title">{{ title }}</text>
7
+ </slot>
8
+ </view>
9
+ <view v-if="hasRight" class="hlw-card-header__right">
10
+ <slot name="right">
11
+ <text v-if="extra" class="hlw-card-header__extra">{{ extra }}</text>
12
+ </slot>
13
+ </view>
14
+ </view>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed, useSlots } from "vue";
19
+
20
+ /**
21
+ * hlw-card-header — 卡片头部独立组件
22
+ *
23
+ * 三种用法:
24
+ *
25
+ * @example A. 直接当 <hlw-card> 的 default slot 子元素(注意把 hlw-card 的 padding 关掉,否则会双重 padding)
26
+ * ```vue
27
+ * <hlw-card :padding="false">
28
+ * <hlw-card-header title="标题" icon="i-fa6-solid-heart-pulse text-rose-500" extra="副标题" />
29
+ * <view style="padding: 24rpx 28rpx">body</view>
30
+ * </hlw-card>
31
+ * ```
32
+ *
33
+ * @example B. 放进 <hlw-card> 的 #header slot(保留 body 默认 padding)
34
+ * ```vue
35
+ * <hlw-card>
36
+ * <template #header>
37
+ * <hlw-card-header title="标题" icon="i-fa6-solid-star" extra="副标题" />
38
+ * </template>
39
+ * body
40
+ * </hlw-card>
41
+ * ```
42
+ *
43
+ * @example C. 完全独立(不必嵌在 hlw-card 里)
44
+ * ```vue
45
+ * <hlw-card-header title="独立标题">
46
+ * <template #right><button>操作</button></template>
47
+ * </hlw-card-header>
48
+ * ```
49
+ */
50
+ interface Props {
51
+ /** 标题文字 */
52
+ title?: string;
53
+ /** 图标 class(iconify 或自定义),如 `"i-fa6-solid-heart-pulse text-rose-500"` */
54
+ icon?: string;
55
+ /** 右侧附加文字(无 right slot 时显示) */
56
+ extra?: string;
57
+ }
58
+
59
+ const props = withDefaults(defineProps<Props>(), {
60
+ title: "",
61
+ icon: "",
62
+ extra: "",
63
+ });
64
+
65
+ const slots = useSlots();
66
+
67
+ const hasRight = computed(() => !!(slots.right || props.extra));
68
+
69
+ defineOptions({
70
+ name: "HlwCardHeader",
71
+ });
72
+ </script>
73
+
74
+ <style lang="scss" scoped>
75
+ .hlw-card-header {
76
+ width: 100%;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: space-between;
80
+ padding: 24rpx 28rpx;
81
+ box-sizing: border-box;
82
+ }
83
+
84
+ .hlw-card-header__left {
85
+ flex: 1;
86
+ min-width: 0;
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 12rpx;
90
+ }
91
+
92
+ .hlw-card-header__right {
93
+ flex-shrink: 0;
94
+ margin-left: 16rpx;
95
+ }
96
+
97
+ .hlw-card-header__icon {
98
+ font-size: var(--font-base, 28rpx);
99
+ }
100
+
101
+ .hlw-card-header__title {
102
+ font-size: var(--font-sm, 24rpx);
103
+ font-weight: 700;
104
+ color: var(--text-primary, #1e293b);
105
+ letter-spacing: 0.02em;
106
+ }
107
+
108
+ .hlw-card-header__extra {
109
+ font-size: var(--font-xs, 20rpx);
110
+ color: var(--text-subtle, #94a3b8);
111
+ }
112
+ </style>
@@ -13,6 +13,8 @@
13
13
 
14
14
  <scroll-view
15
15
  class="hlw-page-content"
16
+ :class="bodyClass"
17
+ :style="bodyStyle"
16
18
  :scroll-y="true"
17
19
  :enable-flex="true"
18
20
  :enhanced="true"
@@ -39,16 +41,25 @@ defineOptions({
39
41
  inheritAttrs: false,
40
42
  });
41
43
 
44
+ type ClassValue = string | Record<string, boolean> | Array<string | Record<string, boolean>>;
45
+ type StyleValue = string | Record<string, string | number>;
46
+
42
47
  interface Props {
43
48
  title?: string;
44
49
  isBack?: boolean;
45
50
  bgClass?: string;
51
+ /** 透传到 scroll-view 内容区的 class,常用于直接挂 .container */
52
+ bodyClass?: ClassValue;
53
+ /** 透传到 scroll-view 内容区的 style */
54
+ bodyStyle?: StyleValue;
46
55
  }
47
56
 
48
57
  const props = withDefaults(defineProps<Props>(), {
49
58
  title: "",
50
59
  isBack: false,
51
60
  bgClass: "",
61
+ bodyClass: "",
62
+ bodyStyle: "",
52
63
  });
53
64
  </script>
54
65
 
@@ -87,6 +87,11 @@ export interface AdAdapter {
87
87
  isAuth?: () => boolean;
88
88
  /** 是否 VIP;配合 config.vip_no_ad=1 时屏蔽展示型广告(reward 不受影响) */
89
89
  isVip?: () => boolean;
90
+ /**
91
+ * 用户级强制屏蔽:返回 true 直接屏蔽展示型广告(reward 不受影响),
92
+ * 优先级高于 mp.vip_no_ad + isVip 的联合判断;不传 = 不强制。
93
+ */
94
+ userNoAd?: () => boolean;
90
95
  }
91
96
 
92
97
  /** 激励视频关闭回调返回 */
@@ -147,10 +152,16 @@ export function useAd() {
147
152
  const store = useAdStore();
148
153
  const { loaded } = storeToRefs(store);
149
154
 
150
- /** 真实生效的广告配置:VIP + vip_no_ad=1 命中时屏蔽展示型 unit_id(reward 保留) */
155
+ /**
156
+ * 真实生效的广告配置;以下任一命中即屏蔽展示型 unit_id(reward 保留):
157
+ * 1. adapter.userNoAd() === true —— 用户级强制屏蔽,优先级最高
158
+ * 2. raw.vip_no_ad === 1 && adapter.isVip() —— 小程序级 VIP 隐藏开关
159
+ */
151
160
  const config = computed<AdConfig>(() => {
152
161
  const raw = store.config;
153
- if (raw.vip_no_ad === 1 && adapter?.isVip?.()) {
162
+ const userForce = adapter?.userNoAd?.() === true;
163
+ const vipHide = raw.vip_no_ad === 1 && adapter?.isVip?.() === true;
164
+ if (userForce || vipHide) {
154
165
  return { ...EMPTY, reward_unit_id: raw.reward_unit_id, vip_no_ad: 1 };
155
166
  }
156
167
  return raw;