uview-plus 3.8.32 → 3.8.39

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.
package/README.md CHANGED
@@ -86,3 +86,5 @@ uview-plus现已推出免费可视化设计,可以方便的进行页面可视
86
86
 
87
87
  ## 版权信息
88
88
  uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uview-plus应用到您的产品中。
89
+
90
+ Root 组件基于上游项目 `uni-ku/root` 二次开发,已按 MIT 要求保留其原始授权文件:`libs/root/LICENSE`。
package/changelog.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## 3.8.39(2026-05-27)
2
+ feat: 新增 tabbar 风格体系与动态图标表现
3
+
4
+ - 为 tabbar 增加多种内置风格、轻量选中动画与双态图标支持。
5
+ - 扩展示例页覆盖普通场景、NVUE 场景及中间按钮组合用法。
6
+
7
+ ## 3.8.38(2026-05-25)
8
+ add: uni-ku/root上游组建的协议原文
9
+
10
+ ## 3.8.37(2026-05-22)
11
+ feat: 日历组件新增今天按钮并支持快速回到当月
12
+
13
+ 在日历头部增加今天按钮与禁用态,点击后可跳转到今天所在月份;为今天日期增加独立高亮样式且不覆盖用户当前已选日期。同时补充 showToday 配置、类型声明与多语言文案键,保证不同语言和模式下行为一致。
14
+
15
+ ## 3.8.36(2026-05-20)
16
+ fix: 修复 u-parse 链接点击在 iOS 端可能闪退的问题
17
+
18
+ - 在 u-parse 的两个链接点击入口统一增加 href 规范化与外链判断,避免空值或非法值传入 plus.runtime.openWeb/openURL。
19
+ - 同时将原生外链打开封装为受保护调用并添加异常兜底,降低 APP-PLUS 与 APP-HARMONY 下点击链接导致崩溃的风险。
20
+
21
+ ## 3.8.35(2026-05-19)
22
+ fix: 修复up-input在confirm阶段可能丢失扫码内容
23
+
24
+ - 问题背景:issue #1011 反馈外接扫码枪场景下,up-input 的 confirm 回调偶发丢码,连续0更明显。
25
+ - 原因分析:组件 onConfirm 直接返回 innerValue,当扫码输入与回车触发节奏接近时,confirm 可能早于最后一次 input 同步,导致取到旧值。
26
+ - 改动说明:onConfirm 优先使用 event.detail.value,若平台未提供该字段再回退到 innerValue,兼容原有行为并降低扫码场景丢码风险。
27
+ ## 3.8.34(2026-05-18)
28
+ fix: 仅在 TabBar 页面同步 TabBar 样式
29
+
30
+ - 在 libs/theme/runtime.js 新增 isTabBarPage 与 trySetTabBarStyle,统一封装 TabBar 页面判断与安全调用。
31
+ - 在 runtime/theme/common mixin 的原生主题同步路径中,全部改为调用 trySetTabBarStyle,不再直接调用 uni.setTabBarStyle。
32
+
33
+ ## 3.8.33(2026-05-17)
34
+ fix: 修复主题变量桥接与 nvue 按钮取色问题
35
+
36
+ - 调整主题 bridge 的导出位置,避免 legacy light 变量在样式注入后覆盖用户自定义 CSS 变量。
37
+ - 修复 up-button 在 nvue 下的主题取色与 info/plain 样式问题,补充运行时取色兜底,避免按钮颜色错误或主题方法缺失时报错。
38
+ - 同步更新示例接入、版本号与变更记录,保持主题改造后的行为与文档一致。
39
+
1
40
  ## 3.8.32(2026-05-14)
2
41
  feat: 新增二维码静区配置
3
42
 
@@ -2088,3 +2127,7 @@ fix: #261u-input在直接修改v-model的绑定值时,每隔一次会无法出
2088
2127
  增加插件市场地址
2089
2128
  ## 3.0.0(2022-07-09)
2090
2129
  # uview-plus(vue3)初步发布
2130
+ ## 3.8.39
2131
+
2132
+ - feat: tabbar 新增多种风格预设、动态图标切换与轻量选中动画
2133
+ - improvment: tabbar 补充高颜值本地图标示例并优化发布型底栏视觉
@@ -117,6 +117,7 @@ import { mixin } from '../../libs/mixin/mixin';
117
117
  import { props } from "./props";
118
118
  import { addStyle } from '../../libs/function/index';
119
119
  import { throttle } from '../../libs/function/throttle';
120
+ import { getThemeVar } from '../../libs/theme/runtime.js';
120
121
  /**
121
122
  * button 按钮
122
123
  * @description Button 按钮
@@ -194,8 +195,43 @@ export default {
194
195
  isDarkTheme() {
195
196
  return this.$u.theme && this.$u.theme.mode === 'dark';
196
197
  },
198
+ nvueMainColor() {
199
+ return this.resolveNvueColor(this.$u.color.mainColor, this.isDarkTheme ? '#f5f5f5' : '#303133');
200
+ },
201
+ nvueBorderColor() {
202
+ return this.resolveNvueColor(this.$u.color.borderColor, this.isDarkTheme ? '#3a3a3c' : '#dadbde');
203
+ },
197
204
  themeTypeColor() {
198
- return this.$u.color[this.type] || this.$u.color.info;
205
+ const fallbackMap = {
206
+ primary: '#3c9cff',
207
+ success: '#5ac725',
208
+ warning: '#f9ae3d',
209
+ error: '#f56c6c',
210
+ info: '#909399',
211
+ };
212
+ const type = fallbackMap[this.type] ? this.type : 'info';
213
+ return this.resolveNvueColor(
214
+ this.upThemeVar(`--up-${type}`, fallbackMap[type]),
215
+ fallbackMap[type]
216
+ );
217
+ },
218
+ nvueInfoBackgroundColor() {
219
+ return this.upThemeVar(
220
+ '--up-button-info-background-color',
221
+ this.upThemeVar(
222
+ '--up-card-bg-color',
223
+ this.isDarkTheme ? '#1c1c1e' : '#ffffff'
224
+ )
225
+ );
226
+ },
227
+ nvuePlainBackgroundColor() {
228
+ return this.upThemeVar(
229
+ '--up-button-plain-background-color',
230
+ this.upThemeVar(
231
+ '--up-card-bg-color',
232
+ this.isDarkTheme ? '#1c1c1e' : '#ffffff'
233
+ )
234
+ );
199
235
  },
200
236
  loadingColor() {
201
237
  if (this.plain) {
@@ -212,13 +248,11 @@ export default {
212
248
  iconColorCom() {
213
249
  // 如果是镂空状态,设置了color就用color值,否则使用主题颜色,
214
250
  // up-icon的color能接受一个主题颜色的值
215
- if (this.iconColor) return this.iconColor;
251
+ if (this.iconColor) return this.iconColor;
216
252
  if (this.plain) {
217
253
  return this.color ? this.color : this.type;
218
254
  } else {
219
- return this.type === "info"
220
- ? (this.isDarkTheme ? "#f3f4f6" : "#000000")
221
- : "#ffffff";
255
+ return this.type === "info" ? this.nvueMainColor : "#ffffff";
222
256
  }
223
257
  },
224
258
  baseColor() {
@@ -249,26 +283,28 @@ export default {
249
283
  }
250
284
  return style;
251
285
  }
286
+ // #ifdef APP-NVUE
252
287
  const typeColor = this.themeTypeColor;
253
288
  if (this.plain) {
254
- style.color = this.type === 'info' ? this.$u.color.mainColor : typeColor;
255
- style.backgroundColor = this.isDarkTheme ? '#1c1c1e' : '#ffffff';
256
- style.borderColor = this.type === 'info' ? this.$u.color.borderColor : typeColor;
289
+ style.color = this.type === 'info' ? this.nvueMainColor : typeColor;
290
+ style.backgroundColor = this.nvuePlainBackgroundColor;
291
+ style.borderColor = this.type === 'info' ? this.nvueBorderColor : typeColor;
257
292
  style.borderWidth = this.hairline ? '0.5px' : '1px';
258
293
  style.borderStyle = 'solid';
259
294
  } else {
260
295
  style.borderWidth = this.hairline ? '0.5px' : '1px';
261
296
  style.borderStyle = 'solid';
262
297
  if (this.type === 'info') {
263
- style.color = this.$u.color.mainColor;
264
- style.backgroundColor = this.isDarkTheme ? '#2c2c2e' : '#ffffff';
265
- style.borderColor = this.$u.color.borderColor;
298
+ style.color = this.nvueMainColor;
299
+ style.backgroundColor = this.nvueInfoBackgroundColor;
300
+ style.borderColor = this.nvueBorderColor;
266
301
  } else {
267
302
  style.color = '#ffffff';
268
303
  style.backgroundColor = typeColor;
269
304
  style.borderColor = typeColor;
270
305
  }
271
306
  }
307
+ // #endif
272
308
  return style;
273
309
  },
274
310
  // nvue版本按钮的字体不会继承父组件的颜色,需要对每一个text组件进行单独的设置
@@ -276,7 +312,7 @@ export default {
276
312
  let style = {};
277
313
  // 针对自定义了color颜色的情况,镂空状态下,就是用自定义的颜色
278
314
  if (this.type === "info") {
279
- style.color = this.$u.color.mainColor;
315
+ style.color = this.nvueMainColor;
280
316
  }
281
317
  if (this.color) {
282
318
  style.color = this.plain ? this.color : "white";
@@ -301,6 +337,14 @@ export default {
301
337
  'error', 'opensetting', 'launchapp', 'agreeprivacyauthorization'],
302
338
  methods: {
303
339
  addStyle,
340
+ resolveNvueColor(colorValue, fallbackColor) {
341
+ if (typeof colorValue !== 'string') return fallbackColor;
342
+ if (colorValue.indexOf('var(') > -1) return fallbackColor;
343
+ return colorValue;
344
+ },
345
+ upThemeVar(varName, fallbackColor) {
346
+ return getThemeVar(varName, fallbackColor, this.$u);
347
+ },
304
348
  clickHandler(e: any) {
305
349
  // 非禁止并且非加载中,才能点击
306
350
  if (!this.disabled && !this.loading) {
@@ -362,9 +406,9 @@ $u-button-mini-height: 22px !default;
362
406
  $u-button-mini-font-size: 10px !default;
363
407
  $u-button-mini-min-width: 50px !default;
364
408
  $u-button-disabled-opacity: 0.5 !default;
365
- $u-button-info-color: #323233 !default;
366
- $u-button-info-background-color: #fff !default;
367
- $u-button-info-border-color: #ebedf0 !default;
409
+ $u-button-info-color: $u-main-color !default;
410
+ $u-button-info-background-color: var(--up-button-info-background-color, var(--up-card-bg-color, #fff)) !default;
411
+ $u-button-info-border-color: $u-border-color !default;
368
412
  $u-button-info-border-width: 1px !default;
369
413
  $u-button-info-border-style: solid !default;
370
414
  $u-button-success-color: #fff !default;
@@ -397,7 +441,7 @@ $u-button-square-border-top-left-radius: 3px !default;
397
441
  $u-button-square-border-bottom-left-radius: 3px !default;
398
442
  $u-button-square-border-bottom-right-radius: 3px !default;
399
443
  $u-button-icon-min-width: 1em !default;
400
- $u-button-plain-background-color: #fff !default;
444
+ $u-button-plain-background-color: var(--up-button-plain-background-color, var(--up-card-bg-color, #fff)) !default;
401
445
  $u-button-hairline-border-width: 0.5px !default;
402
446
 
403
447
  .u-button {
@@ -38,9 +38,10 @@ export default {
38
38
  showRangePrompt: true,
39
39
  allowSameDay: false,
40
40
  round: 0,
41
- monthNum: 3,
42
- monthSwitch: false,
43
- weekText: [t("up.week.one"), t("up.week.two"), t("up.week.three"), t("up.week.four"), t("up.week.five"), t("up.week.six"), t("up.week.seven")],
41
+ monthNum: 3,
42
+ monthSwitch: false,
43
+ showToday: true,
44
+ weekText: [t("up.week.one"), t("up.week.two"), t("up.week.three"), t("up.week.four"), t("up.week.five"), t("up.week.six"), t("up.week.seven")],
44
45
  forbidDays: [],
45
46
  forbidDaysToast: t("up.calendar.disabled"),
46
47
  monthFormat: '',
@@ -4,10 +4,10 @@
4
4
  class="u-calendar-header__title"
5
5
  v-if="showTitle"
6
6
  >{{ title }}</text>
7
- <view
8
- class="u-calendar-header__subtitle-wrap"
9
- v-if="showSubtitle"
10
- >
7
+ <view
8
+ class="u-calendar-header__subtitle-wrap"
9
+ v-if="showSubtitle"
10
+ >
11
11
  <text
12
12
  v-if="showSwitch"
13
13
  class="u-calendar-header__switch"
@@ -27,13 +27,19 @@
27
27
  :class="{ 'u-calendar-header__switch--disabled': nextDisabled }"
28
28
  @click="next"
29
29
  >›</text>
30
- <text
31
- v-if="showSwitch"
32
- class="u-calendar-header__switch"
33
- :class="{ 'u-calendar-header__switch--disabled': nextYearDisabled }"
34
- @click="nextYear"
35
- >»</text>
36
- </view>
30
+ <text
31
+ v-if="showSwitch"
32
+ class="u-calendar-header__switch"
33
+ :class="{ 'u-calendar-header__switch--disabled': nextYearDisabled }"
34
+ @click="nextYear"
35
+ >»</text>
36
+ <text
37
+ v-if="showToday"
38
+ class="u-calendar-header__today"
39
+ :class="{ 'u-calendar-header__today--disabled': todayDisabled }"
40
+ @click="today"
41
+ >{{ todayText }}</text>
42
+ </view>
37
43
  <view class="u-calendar-header__weekdays">
38
44
  <text class="u-calendar-header__weekdays__weekday">{{ weekText[0] }}</text>
39
45
  <text class="u-calendar-header__weekdays__weekday">{{ weekText[1] }}</text>
@@ -100,12 +106,27 @@
100
106
  type: Boolean,
101
107
  default: false
102
108
  },
103
- // 下一年按钮是否禁用
104
- nextYearDisabled: {
105
- type: Boolean,
106
- default: false
107
- }
108
- },
109
+ // 下一年按钮是否禁用
110
+ nextYearDisabled: {
111
+ type: Boolean,
112
+ default: false
113
+ },
114
+ // 是否显示今天按钮
115
+ showToday: {
116
+ type: Boolean,
117
+ default: true
118
+ },
119
+ // 今天按钮文案
120
+ todayText: {
121
+ type: String,
122
+ default: ''
123
+ },
124
+ // 今天按钮是否禁用
125
+ todayDisabled: {
126
+ type: Boolean,
127
+ default: false
128
+ }
129
+ },
109
130
  data() {
110
131
  return {
111
132
 
@@ -127,13 +148,18 @@
127
148
  this.$emit('prevYear')
128
149
  }
129
150
  },
130
- nextYear() {
131
- if (!this.nextYearDisabled) {
132
- this.$emit('nextYear')
133
- }
134
- }
135
- },
136
- }
151
+ nextYear() {
152
+ if (!this.nextYearDisabled) {
153
+ this.$emit('nextYear')
154
+ }
155
+ },
156
+ today() {
157
+ if (!this.todayDisabled) {
158
+ this.$emit('today')
159
+ }
160
+ }
161
+ },
162
+ }
137
163
  </script>
138
164
 
139
165
  <style lang="scss" scoped>
@@ -182,17 +208,33 @@
182
208
  }
183
209
  }
184
210
 
185
- &__weekdays {
186
- @include flex;
187
- justify-content: space-between;
211
+ &__weekdays {
212
+ @include flex;
213
+ justify-content: space-between;
188
214
 
189
215
  &__weekday {
190
216
  font-size: 13px;
191
217
  color: var(--up-main-color, $u-main-color);
192
218
  line-height: 30px;
193
219
  flex: 1;
194
- text-align: center;
195
- }
196
- }
197
- }
198
- </style>
220
+ text-align: center;
221
+ }
222
+ }
223
+
224
+ &__today {
225
+ margin-left: 4px;
226
+ height: 28px;
227
+ line-height: 28px;
228
+ padding: 0 10px;
229
+ border-radius: 14px;
230
+ font-size: 13px;
231
+ color: var(--up-primary, $u-primary);
232
+ border: 1px solid var(--up-primary, $u-primary);
233
+ flex-shrink: 0;
234
+
235
+ &--disabled {
236
+ opacity: 0.35;
237
+ }
238
+ }
239
+ }
240
+ </style>
@@ -135,6 +135,11 @@
135
135
  forbidDaysToast: {
136
136
  type: String,
137
137
  default: ''
138
+ },
139
+ // 今天日期,用于独立高亮
140
+ todayDate: {
141
+ type: String,
142
+ default: ''
138
143
  }
139
144
  },
140
145
  data() {
@@ -193,6 +198,10 @@
193
198
  if (this.selected.some(item => this.dateSame(item, date))) {
194
199
  style.backgroundColor = this.color
195
200
  }
201
+ if (this.todayDate && this.dateSame(date, this.todayDate) && !this.selected.some(item => this.dateSame(item, date))) {
202
+ style.border = `1px solid ${this.color}`
203
+ style.boxSizing = 'border-box'
204
+ }
196
205
  if (this.mode === 'single') {
197
206
  if (date === this.selected[0]) {
198
207
  // 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等
@@ -148,6 +148,11 @@ export const props = defineMixin({
148
148
  type: Boolean,
149
149
  default: () => defProps.calendar.monthSwitch
150
150
  },
151
+ // 是否显示今天按钮
152
+ showToday: {
153
+ type: Boolean,
154
+ default: () => defProps.calendar.showToday
155
+ },
151
156
  // 星期文案
152
157
  weekText: {
153
158
  type: Array,
@@ -16,6 +16,9 @@
16
16
  :showTitle="showTitle"
17
17
  :weekText="weekText"
18
18
  :showSwitch="monthSwitch"
19
+ :showToday="showToday"
20
+ :todayText="todayText"
21
+ :todayDisabled="todayDisabled"
19
22
  :prevDisabled="switchPrevDisabled"
20
23
  :nextDisabled="switchNextDisabled"
21
24
  :prevYearDisabled="switchPrevYearDisabled"
@@ -24,6 +27,7 @@
24
27
  @next="nextMonth"
25
28
  @prevYear="prevYear"
26
29
  @nextYear="nextYear"
30
+ @today="jumpToToday"
27
31
  ></uHeader>
28
32
  <scroll-view
29
33
  v-if="!monthSwitch"
@@ -56,6 +60,7 @@
56
60
  :forbidDays="forbidDays"
57
61
  :forbidDaysToast="forbidDaysToast"
58
62
  :monthFormat="monthFormat"
63
+ :todayDate="todayDate"
59
64
  ref="month"
60
65
  @monthSelected="monthSelected"
61
66
  @updateMonthTop="onUpdateMonthTop"
@@ -88,6 +93,7 @@
88
93
  :forbidDays="forbidDays"
89
94
  :forbidDaysToast="forbidDaysToast"
90
95
  :monthFormat="monthFormat"
96
+ :todayDate="todayDate"
91
97
  ref="month"
92
98
  @monthSelected="monthSelected"
93
99
  @updateMonthTop="onUpdateMonthTop"
@@ -121,6 +127,7 @@ import { mpMixin } from '../../libs/mixin/mpMixin.js'
121
127
  import { mixin } from '../../libs/mixin/mixin.js'
122
128
  import { addUnit, getPx, range, error, padZero } from '../../libs/function/index';
123
129
  import test from '../../libs/function/test';
130
+ import { t } from '../../libs/i18n'
124
131
  /**
125
132
  * Calendar 日历
126
133
  * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
@@ -224,6 +231,28 @@ export default {
224
231
  ? Number(this.minDate)
225
232
  : this.minDate
226
233
  },
234
+ todayDate() {
235
+ return dayjs().format('YYYY-MM-DD')
236
+ },
237
+ todayText() {
238
+ return t('up.calendar.today')
239
+ },
240
+ todayDisabled() {
241
+ const today = dayjs(this.todayDate)
242
+ const minDate = this.innerMinDate
243
+ ? dayjs(this.innerMinDate)
244
+ : null
245
+ const maxDate = this.innerMaxDate
246
+ ? dayjs(this.innerMaxDate)
247
+ : null
248
+ if (minDate && today.isBefore(minDate, 'day')) {
249
+ return true
250
+ }
251
+ if (maxDate && today.isAfter(maxDate, 'day')) {
252
+ return true
253
+ }
254
+ return false
255
+ },
227
256
  // 多个条件的变化,会引起选中日期的变化,这里统一管理监听
228
257
  selectedChange() {
229
258
  return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
@@ -449,6 +478,22 @@ export default {
449
478
  this.monthIndex += 12
450
479
  }
451
480
  },
481
+ jumpToToday() {
482
+ if (this.todayDisabled) {
483
+ return
484
+ }
485
+ const targetMonth = dayjs(this.todayDate).format('YYYY-MM')
486
+ if (this.monthSwitch) {
487
+ const todayMonthIndex = this.months.findIndex(({ year, month }) => {
488
+ return `${year}-${padZero(month)}` === targetMonth
489
+ })
490
+ if (todayMonthIndex !== -1) {
491
+ this.monthIndex = todayMonthIndex
492
+ }
493
+ return
494
+ }
495
+ this.scrollIntoDefaultMonth(targetMonth)
496
+ },
452
497
  // 滚动到默认设置的月份
453
498
  scrollIntoDefaultMonth(selected) {
454
499
  // 查询默认日期在可选列表的下标
@@ -462,6 +507,7 @@ export default {
462
507
  if (_index !== -1) {
463
508
  // #ifndef MP-WEIXIN
464
509
  this.$nextTick(() => {
510
+ this.scrollIntoView = ''
465
511
  this.scrollIntoView = `month-${_index}`
466
512
  this.scrollIntoViewScroll = this.scrollIntoView
467
513
  })
@@ -324,7 +324,9 @@ export default {
324
324
  },
325
325
  // 点击完成按钮时触发
326
326
  onConfirm(event) {
327
- this.$emit("confirm", this.innerValue);
327
+ const detail = event && event.detail ? event.detail : {};
328
+ const confirmValue = typeof detail.value !== "undefined" ? detail.value : this.innerValue;
329
+ this.$emit("confirm", confirmValue);
328
330
  },
329
331
  // 键盘高度发生变化的时候触发此事件
330
332
  // 兼容性:微信小程序2.7.0+、App 3.1.0+
@@ -185,6 +185,53 @@ export default {
185
185
  // #ifdef MP-WEIXIN
186
186
  toJSON () { return this },
187
187
  // #endif
188
+ /**
189
+ * @description 规范化链接地址
190
+ * @param {any} href
191
+ * @return {String}
192
+ */
193
+ normalizeHref (href) {
194
+ return typeof href === 'string' ? href.trim() : ''
195
+ },
196
+
197
+ /**
198
+ * @description 判断是否为外部链接
199
+ * @param {String} href
200
+ * @return {Boolean}
201
+ */
202
+ isExternalLink (href) {
203
+ return href.split('?')[0].includes('://')
204
+ },
205
+
206
+ /**
207
+ * @description 打开外部链接
208
+ * @param {String} href
209
+ */
210
+ openExternalLink (href) {
211
+ // #ifdef H5
212
+ window.open(href)
213
+ // #endif
214
+ // #ifdef MP
215
+ uni.setClipboardData({
216
+ data: href,
217
+ success: () =>
218
+ uni.showToast({
219
+ title: '链接已复制'
220
+ })
221
+ })
222
+ // #endif
223
+ // #ifdef APP-PLUS
224
+ try {
225
+ plus.runtime.openWeb(href)
226
+ } catch (e) { }
227
+ // #endif
228
+ // #ifdef APP-HARMONY
229
+ try {
230
+ plus.runtime.openURL(href)
231
+ } catch (e) { }
232
+ // #endif
233
+ },
234
+
188
235
  /**
189
236
  * @description 播放视频事件
190
237
  * @param {Event} e
@@ -341,7 +388,7 @@ export default {
341
388
  linkTap (e) {
342
389
  const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
343
390
  const attrs = node.attrs || e
344
- const href = attrs.href
391
+ const href = this.normalizeHref(attrs.href)
345
392
  this.root.$emit('linktap', Object.assign({
346
393
  innerText: this.root.getText(node.children || []) // 链接内的文本内容
347
394
  }, attrs))
@@ -349,27 +396,10 @@ export default {
349
396
  if (href[0] === '#') {
350
397
  // 跳转锚点
351
398
  this.root.navigateTo(href.substring(1)).catch(() => { })
352
- } else if (href.split('?')[0].includes('://')) {
399
+ } else if (this.isExternalLink(href)) {
353
400
  // 复制外部链接
354
401
  if (this.root.copyLink) {
355
- // #ifdef H5
356
- window.open(href)
357
- // #endif
358
- // #ifdef MP
359
- uni.setClipboardData({
360
- data: href,
361
- success: () =>
362
- uni.showToast({
363
- title: '链接已复制'
364
- })
365
- })
366
- // #endif
367
- // #ifdef APP-PLUS
368
- plus.runtime.openWeb(href)
369
- // #endif
370
- // #ifdef APP-HARMONY
371
- plus.runtime.openURL(href)
372
- // #endif
402
+ this.openExternalLink(href)
373
403
  }
374
404
  } else {
375
405
  // 跳转页面
@@ -130,6 +130,41 @@ export default {
130
130
  this._hook('onDetached')
131
131
  },
132
132
  methods: {
133
+ /**
134
+ * @description 规范化链接地址
135
+ * @param {any} href
136
+ * @return {String}
137
+ */
138
+ normalizeHref(href) {
139
+ return typeof href === 'string' ? href.trim() : ''
140
+ },
141
+
142
+ /**
143
+ * @description 判断是否为外部链接
144
+ * @param {String} href
145
+ * @return {Boolean}
146
+ */
147
+ isExternalLink(href) {
148
+ return href.split('?')[0].includes('://')
149
+ },
150
+
151
+ /**
152
+ * @description 打开外部链接
153
+ * @param {String} href
154
+ */
155
+ openExternalLink(href) {
156
+ // #ifdef APP-PLUS
157
+ try {
158
+ plus.runtime.openWeb(href)
159
+ } catch (e) { }
160
+ // #endif
161
+ // #ifdef APP-HARMONY
162
+ try {
163
+ plus.runtime.openURL(href)
164
+ } catch (e) { }
165
+ // #endif
166
+ },
167
+
133
168
  /**
134
169
  * @description 将锚点跳转的范围限定在一个 scroll-view 内
135
170
  * @param {Object} page scroll-view 所在页面的示例
@@ -420,7 +455,7 @@ export default {
420
455
  break
421
456
  // 链接点击
422
457
  case 'onLinkTap': {
423
- const href = message.attrs.href
458
+ const href = this.normalizeHref(message.attrs.href)
424
459
  this.$emit('linktap', message.attrs)
425
460
  if (href) {
426
461
  // 锚点跳转
@@ -430,15 +465,10 @@ export default {
430
465
  offset: message.offset
431
466
  })
432
467
  }
433
- } else if (href.includes('://')) {
468
+ } else if (this.isExternalLink(href)) {
434
469
  // 打开外链
435
470
  if (this.copyLink) {
436
- // #ifdef APP-PLUS
437
- plus.runtime.openWeb(href)
438
- // #endif
439
- // #ifdef APP-HARMONY
440
- plus.runtime.openURL(href)
441
- // #endif
471
+ this.openExternalLink(href)
442
472
  }
443
473
  } else {
444
474
  uni.navigateTo({