vhall-mini-sdk 0.2.0 → 0.3.0

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 (31) hide show
  1. package/README.md +106 -11
  2. package/lib/components/announcement/announ-banner/index.js +40 -0
  3. package/lib/components/announcement/announ-banner/index.json +4 -0
  4. package/lib/components/announcement/announ-banner/index.wxml +13 -0
  5. package/lib/components/announcement/announ-banner/index.wxss +1 -0
  6. package/lib/components/announcement/announ-icon/index.js +69 -0
  7. package/lib/components/announcement/announ-icon/index.json +3 -0
  8. package/lib/components/announcement/announ-icon/index.wxml +10 -0
  9. package/lib/components/announcement/announ-icon/index.wxss +1 -0
  10. package/lib/components/announcement/announ-popup/index.js +44 -0
  11. package/lib/components/announcement/announ-popup/index.json +7 -0
  12. package/lib/components/announcement/announ-popup/index.wxml +39 -0
  13. package/lib/components/announcement/announ-popup/index.wxss +1 -0
  14. package/lib/components/announcement/index.js +228 -0
  15. package/lib/components/announcement/index.json +9 -0
  16. package/lib/components/announcement/index.wxml +6 -0
  17. package/lib/components/announcement/index.wxss +0 -0
  18. package/lib/components/chat/chat-item/chat-item.js +5 -5
  19. package/lib/components/live-landscape/live-landscape.js +7 -1
  20. package/lib/components/live-landscape/live-landscape.json +2 -0
  21. package/lib/components/live-landscape/live-landscape.wxml +7 -4
  22. package/lib/components/live-portrait/live-portrait.js +10 -2
  23. package/lib/components/live-portrait/live-portrait.json +3 -1
  24. package/lib/components/live-portrait/live-portrait.wxml +8 -4
  25. package/lib/components/mall-reward/index.js +47 -29
  26. package/lib/components/product/product-card/product-card.js +5 -6
  27. package/lib/components/product/product-detail/product-detail.js +6 -6
  28. package/lib/components/product/product-list/product-list.js +2 -3
  29. package/lib/sdk/main/index.js +2 -2
  30. package/lib/sdk/plugins/MallRewardServer.js +1 -1
  31. package/package.json +6 -10
package/README.md CHANGED
@@ -8,43 +8,138 @@
8
8
 
9
9
  ## 快速开始
10
10
 
11
- 1. 安装sdk组件
11
+ ### 1. 安装 SDK 组件
12
12
 
13
13
  ```bash
14
- # npm安装
14
+ # npm 安装
15
15
  npm install vhall-mini-sdk
16
16
 
17
- # yarn安装
17
+ # yarn 安装
18
18
  yarn add vhall-mini-sdk
19
19
  ```
20
20
 
21
- uni-app工程安装完成后,会在当前工程目录下自动生成 wxcomponents/vhall-mini-sdk 目录。
21
+ > **uni-app 开发注意**:安装完成后,会在当前工程目录下自动生成 `wxcomponents/vhall-mini-sdk` 目录。
22
22
 
23
- 2. 构建npm(原生微信小程序开发接入需要此步骤)
23
+ ### 2. 构建 npm(仅原生微信小程序需要)
24
24
 
25
- 微信开发者工具的菜单栏找到 “工具→构建npm” ,点击执行生成 “miniprogram_npm/vhall-mini-sdk“ 目录.
25
+ 在微信开发者工具菜单栏点击 **工具 → 构建 npm**,执行后生成 `miniprogram_npm/vhall-mini-sdk` 目录。
26
26
 
27
- 3. SDK引入
27
+ ### 3. 引入 SDK
28
28
 
29
- (1) 原生微信小程序开发
29
+ #### (1) 原生微信小程序开发
30
+
31
+ 支持 `require` 和 `import` 两种方式引入:
30
32
 
31
33
  ```js
34
+ // require方式
32
35
  const VhallMiniSdk = require('vhall-mini-sdk/sdk/main/index.js');
33
36
  const CustomMenuServer = require('vhall-mini-sdk/sdk/plugins/CustomMenuServer.js');
34
37
  const ChatServer = require('vhall-mini-sdk/sdk/plugins/ChatServer.js');
35
38
  const ProductServer = require('vhall-mini-sdk/sdk/plugins/ProductServer.js');
36
39
  const InteractServer = require('vhall-mini-sdk/sdk/plugins/InteractServer.js');
40
+ const LotteryServer = require('vhall-mini-sdk/sdk/plugins/LotteryServer.js');
41
+ const MallRewardServer = require('vhall-mini-sdk/sdk/plugins/MallRewardServer.js');
42
+ const InviteCardServer = require('vhall-mini-sdk/sdk/plugins/InviteCardServer.js');
43
+
44
+ // import方式
45
+ import VhallMiniSdk from 'vhall-mini-sdk/sdk/main/index.js';
46
+ import ChatServer from 'vhall-mini-sdk/sdk/plugins/ChatServer.js';
47
+ import CustomMenuServer from 'vhall-mini-sdk/sdk/plugins/CustomMenuServer.js';
48
+ import InteractServer from 'vhall-mini-sdk/sdk/plugins/InteractServer.js';
49
+ import InviteCardServer from 'vhall-mini-sdk/sdk/plugins/InviteCardServer.js';
50
+ import LotteryServer from 'vhall-mini-sdk/sdk/plugins/LotteryServer.js';
51
+ import MallRewardServer from 'vhall-mini-sdk/sdk/plugins/MallRewardServer.js';
52
+ import ProductServer from 'vhall-mini-sdk/sdk/plugins/ProductServer.js';
37
53
  ```
38
54
 
39
- (2) uni-app小程序开发
55
+ #### (2) uni-app 小程序开发
56
+
57
+ > **⚠️ 重点:必须用 `require` 引入,不能用 `import`**
58
+ >
59
+ > `wxcomponents` 目录专门存放“原生小程序组件/库”,uni-app 为保证这些原生代码的纯净性与兼容性,会将其排除在编译流程之外。因此**不能用 Web 标准的 `import` 读取**(其 `export default` 未被编译成可执行代码),必须降级使用小程序标准的 `require` 加载。
40
60
 
41
61
  ```js
42
- import { ref, onMounted } from 'vue';
62
+ import { ref, onMounted, onUnmounted } from 'vue';
63
+ // wxcomponents目录是专门用来存放“原生小程序组件/库”的,uni-app为了保证这些原生代码的纯净性和兼容性,特意把它们排除在编译流程之外。
64
+ // 所以,我们不能用 Web 标准的 import 去读取它(因为 export default 没被转换成任何可执行代码),你必须降级使用小程序标准的 require 来加载它。
43
65
  const VhallMiniSdk = require('../../wxcomponents/vhall-mini-sdk/sdk/main/index.js');
66
+ // 下面的Server插件可按需加载
44
67
  const CustomMenuServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/CustomMenuServer.js');
45
68
  const ChatServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/ChatServer.js');
46
69
  const ProductServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/ProductServer.js');
47
70
  const InteractServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/InteractServer.js');
71
+ const LotteryServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/LotteryServer.js');
72
+ const MallRewardServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/MallRewardServer.js');
73
+ const InviteCardServer = require('../../wxcomponents/vhall-mini-sdk/sdk/plugins/InviteCardServer.js');
74
+
75
+ // 布尔值用ref包裹,.value修改值
76
+ const sdkReady = ref(false);
77
+ // SDK实例不需要响应式,普通变量即可
78
+ let sdkInstance = null;
79
+ let unListener =null;
80
+ async function initSdk() {
81
+ const options = {
82
+ app_key: '',
83
+ email: '',
84
+ sign: '',
85
+ signedat: ,
86
+ username: '',
87
+ webinar_id: '',
88
+ head: 'https://cnstatic01.e.vhall.com/common-static/images/default_avatar.png',
89
+ modules: [
90
+ CustomMenuServer,
91
+ ChatServer,
92
+ ProductServer,
93
+ InteractServer,
94
+ LotteryServer,
95
+ MallRewardServer,
96
+ InviteCardServer,
97
+ ],
98
+ };
99
+
100
+ try {
101
+ // 创建实例
102
+ sdkInstance = new VhallMiniSdk(options);
103
+
104
+ // 事件监听
105
+ unListener = sdkInstance.on({
106
+ [VhallMiniSdk.EVENT.NET_STATE_CHANGE]: (state) => {
107
+ console.log('网络状态变化', state);
108
+ },
109
+ [VhallMiniSdk.EVENT.SDK_READY]: () => {
110
+ console.log('initLiveRoom is ready');
111
+ },
112
+ [VhallMiniSdk.EVENT.INIT_FAILED]: (error) => {
113
+ console.error('initLiveRoom is failed', error);
114
+ },
115
+ [VhallMiniSdk.EVENT.ERROR]: (err) => {
116
+ console.error('VhallMiniSdk error:', JSON.stringify(err));
117
+ },
118
+ });
119
+
120
+ // 初始化直播间
121
+ await sdkInstance.init();
122
+
123
+ if (sdkInstance.isReady()) {
124
+ // 关键:ref变量修改必须加 .value
125
+ sdkReady.value = true;
126
+ }
127
+ } catch (error) {
128
+ console.warn('VhallMiniSdk init failed', error);
129
+ }
130
+ }
131
+
132
+ onMounted(() => {
133
+ initSdk();
134
+ });
135
+
136
+ onUnmounted(() => {
137
+ unListener?.();
138
+ sdkInstance?.destroy();
139
+ sdkInstance = null;
140
+ });
48
141
  ```
49
142
 
50
- 4. 更多接入支持,参考文档: https://vhall.apifox.cn/dev/sdk/wmp/quick-start
143
+ ### 4. 更多接入支持
144
+
145
+ 参考文档:<https://vhall.apifox.cn/dev/sdk/wmp/quick-start>
@@ -0,0 +1,40 @@
1
+ Component({
2
+ properties: {
3
+ text: {
4
+ type: String,
5
+ value: '',
6
+ },
7
+ show: {
8
+ type: Boolean,
9
+ value: false,
10
+ },
11
+ static: {
12
+ type: Boolean,
13
+ value: false,
14
+ },
15
+ // 直播间模式:0=竖屏,1=横屏(样式区分)
16
+ mode: {
17
+ type: Number,
18
+ value: 0,
19
+ },
20
+ },
21
+
22
+ data: {
23
+ duration: 6, // 滚动动画时长(秒),按文字长度动态计算
24
+ },
25
+
26
+ observers: {
27
+ // 文字越长滚动时长越大,保持匀速观感
28
+ text(val) {
29
+ const len = (val || '').length;
30
+ const duration = Math.max(4, Math.ceil(len * 0.2));
31
+ this.setData({ duration });
32
+ },
33
+ },
34
+
35
+ methods: {
36
+ onClose() {
37
+ this.triggerEvent('close');
38
+ },
39
+ },
40
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "styleIsolation": "shared"
4
+ }
@@ -0,0 +1,13 @@
1
+ <view wx:if="{{show }}" class="announ-banner {{static ? 'announ-banner--static' : ''}} mode-{{mode}}">
2
+ <view class="announ-banner-icon">
3
+ <view class="announ-banner-icon__box"></view>
4
+ <view class="announ-banner-icon__body"></view>
5
+ <view class="announ-banner-icon__wave announ-banner-icon__wave--top"></view>
6
+ <view class="announ-banner-icon__wave announ-banner-icon__wave--mid"></view>
7
+ <view class="announ-banner-icon__wave announ-banner-icon__wave--bottom"></view>
8
+ </view>
9
+ <view class="announ-banner-text-wrap">
10
+ <view class="announ-banner-text" style="animation-duration:{{duration}}s">{{text}}</view>
11
+ </view>
12
+ <view class="announ-banner-close" catchtap="onClose">&#10005;</view>
13
+ </view>
@@ -0,0 +1 @@
1
+ .announ-banner{align-items:center;background:rgba(0,0,0,.5);border-radius:40rpx;box-sizing:border-box;color:#fff;display:flex;font-size:24rpx;left:12rpx;padding:0 12rpx;position:fixed;right:12rpx;top:180rpx;z-index:99}.announ-banner-icon{color:#fff;flex-shrink:0;height:32rpx;margin-right:16rpx;position:relative;width:34rpx}.mode-1 .announ-banner-icon{color:#fb3a32}.announ-banner-icon__box{background:currentColor;border-bottom-left-radius:4rpx;border-top-left-radius:4rpx;height:14rpx;left:3rpx;position:absolute;top:9rpx;width:7rpx;z-index:2}.announ-banner-icon__body{background:currentColor;border-radius:4rpx 6rpx 6rpx 4rpx;-webkit-clip-path:polygon(0 20%,0 80%,78% 100%,78% 0);clip-path:polygon(0 20%,0 80%,78% 100%,78% 0);height:28rpx;left:8rpx;position:absolute;top:2rpx;width:15rpx}.announ-banner-icon__wave{background:currentColor;border-radius:4rpx;position:absolute}.announ-banner-icon__wave--top{height:4rpx;right:4rpx;top:5rpx;transform:rotate(-32deg);width:9rpx}.announ-banner-icon__wave--mid{height:4rpx;right:1rpx;top:14rpx;width:11rpx}.announ-banner-icon__wave--bottom{bottom:5rpx;height:4rpx;right:4rpx;transform:rotate(32deg);width:9rpx}.announ-banner-text-wrap{align-items:center;display:flex;flex:1;height:100%;overflow:hidden;padding:6rpx 0}.announ-banner-text{animation:announ-banner-scroll linear infinite;display:inline-block;padding-left:100%;white-space:nowrap}.announ-banner--static .announ-banner-text{animation:none;padding-left:0}.announ-banner-close{flex-shrink:0;font-size:32rpx;margin-left:16rpx}.announ-banner.mode-1{background:linear-gradient(59.32deg,#ffebe9,#edecff 107.06%);border-radius:0;color:#262626;left:0;max-width:none;padding:16rpx 12rpx;right:0;top:calc(56.25vw + 85rpx);transform:none;width:100%}@keyframes announ-banner-scroll{0%{transform:translateX(0)}to{transform:translateX(-100%)}}
@@ -0,0 +1,69 @@
1
+ Component({
2
+ properties: {
3
+ // 是否显示公告图标
4
+ showIcon: {
5
+ type: Boolean,
6
+ value: false,
7
+ },
8
+ // 未读角标数字
9
+ badge: {
10
+ type: Number,
11
+ value: 0,
12
+ },
13
+ // 直播间模式:0=竖屏,1=横屏(样式区分)
14
+ mode: {
15
+ type: Number,
16
+ value: 0,
17
+ },
18
+ },
19
+ data: {
20
+ x: 20,
21
+ y: 40,
22
+ startX: 0,
23
+ startY: 0,
24
+ hasMoved: false,
25
+ },
26
+ lifetimes: {
27
+ attached() {
28
+ // 初始定位:竖屏在左上角;横屏 70rpx≈35px,在右下角抽奖历史图标上方
29
+ try {
30
+ const info = wx.getSystemInfoSync();
31
+ const iconSize = this.data.mode === 1 ? 35 : 42;
32
+ if (this.data.mode === 1) {
33
+ // 横屏:距右 40rpx≈20px;历史图标 bottom:200rpx + 高 70rpx + 间隔 36rpx ≈ 306rpx
34
+ const rightGap = 20;
35
+ const bottomGap = 160;
36
+ const x = Math.max(0, info.windowWidth - iconSize - rightGap);
37
+ const y = Math.max(0, info.windowHeight - iconSize - bottomGap);
38
+ this.setData({ x, y });
39
+ } else {
40
+ // 竖屏:左上角,留 12rpx≈6px 边距
41
+ const x = 6;
42
+ const y = 40;
43
+ this.setData({ x, y });
44
+ }
45
+ } catch (e) {
46
+ // 获取失败则保持默认左上角
47
+ }
48
+ },
49
+ },
50
+ methods: {
51
+ onTouchStart(e) {
52
+ this.setData({ hasMoved: false });
53
+ },
54
+
55
+ onTouchEnd(e) {
56
+ this.data.hasMoved = false;
57
+ },
58
+
59
+ onDragChange(e) {
60
+ const { x, y } = e.detail;
61
+ this.setData({ x, y, hasMoved: true });
62
+ },
63
+
64
+ onTapIcon() {
65
+ if (this.data.hasMoved) return;
66
+ this.triggerEvent('onTapIcon');
67
+ },
68
+ },
69
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "component": true
3
+ }
@@ -0,0 +1,10 @@
1
+ <movable-area class="drag-area">
2
+ <movable-view wx:if="{{showIcon}}" class="drag-view" direction="all" x="{{x}}" y="{{y}}" damping="{{40}}" out-of-bounds="{{false}}" bindchange="onDragChange" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd">
3
+ <view class="announ-icon mode-{{mode}}" bindtap="onTapIcon">
4
+ <image wx:if="{{mode === 1}}" class="announ-icon-img" src="https://cnstatic01.e.vhall.com/common-static/images/mp/announ_icon_2.png" mode="aspectFit" />
5
+ <image wx:else class="announ-icon-img" src="https://cnstatic01.e.vhall.com/common-static/images/mp/announ_icon_1.png" mode="aspectFit" />
6
+ <text wx:if="{{mode !== 1}}" class="announ-icon-text">公告</text>
7
+ <view wx:if="{{badge > 0}}" class="announ-icon-badge">{{badge > 99 ? '99+' : badge}}</view>
8
+ </view>
9
+ </movable-view>
10
+ </movable-area>
@@ -0,0 +1 @@
1
+ .drag-area{height:100vh;left:0;pointer-events:none;position:fixed;top:0;width:100vw;z-index:100}.drag-view{height:96rpx;pointer-events:auto;top:10rpx;width:96rpx}.announ-icon{align-items:center;background:rgba(0,0,0,.4);border-radius:50%;display:flex;flex-direction:column;height:84rpx;justify-content:center;position:relative;width:84rpx}.announ-icon.mode-1{height:70rpx;width:70rpx}.announ-icon-img{height:64rpx;width:64rpx}.mode-1 .announ-icon-img{height:70rpx;width:70rpx}.announ-icon-text{color:#fff;font-size:24rpx;margin-top:-20rpx}.announ-icon-badge{align-items:center;background:#ff3b30;border:2rpx solid #fff;border-radius:16rpx;box-sizing:border-box;color:#fff;display:flex;font-size:20rpx;font-weight:700;height:32rpx;justify-content:center;min-width:32rpx;padding:0 8rpx;position:absolute;right:-8rpx;top:-8rpx}
@@ -0,0 +1,44 @@
1
+ Component({
2
+ properties: {
3
+ show: { type: Boolean, value: false },
4
+ // 公告信息(store.announcement)
5
+ info: { type: Object, value: null },
6
+ },
7
+
8
+ data: {
9
+ list: [],
10
+ },
11
+
12
+ observers: {
13
+ info: function (info) {
14
+ const rawList = info?.list || [];
15
+ this.setData({
16
+ list: rawList.map((item) => ({
17
+ ...item,
18
+ _text: this.getContentText(item.content),
19
+ _time: this.formatTime(item.created_at),
20
+ })),
21
+ });
22
+ },
23
+ },
24
+
25
+ methods: {
26
+ getContentText(content) {
27
+ if (!content) return '';
28
+ if (typeof content === 'string') return content;
29
+ return content.content || content.text || '';
30
+ },
31
+
32
+ formatTime(time) {
33
+ if (!time) return '';
34
+ if (typeof time !== 'string') return '';
35
+ // 将 "2026-08-07 16:47:13" 截断为 "08-07 16:47"
36
+ const matched = time.match(/\d{4}-(\d{2}-\d{2})\s+(\d{2}:\d{2}):\d{2}/);
37
+ return matched ? `${matched[1]} ${matched[2]}` : time;
38
+ },
39
+
40
+ onClose() {
41
+ this.triggerEvent('close');
42
+ },
43
+ },
44
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "component": true,
3
+ "styleIsolation": "shared",
4
+ "usingComponents": {
5
+ "vh-popup": "../../../pubComponents/popup/popup"
6
+ }
7
+ }
@@ -0,0 +1,39 @@
1
+ <vh-popup
2
+ wx:if="{{show}}"
3
+ visible="{{show}}"
4
+ showClose="{{true}}"
5
+ mode="bottom"
6
+ hasMask="{{true}}"
7
+ maskClosable="{{true}}"
8
+ bgColor="#f7f7f7"
9
+ bind:close="onClose"
10
+ >
11
+ <view class="announ-popup">
12
+ <view class="announ-popup-header">
13
+ <text class="announ-popup-star left">✦</text>
14
+ <text class="announ-popup-title">公告</text>
15
+ <text class="announ-popup-star right">✦</text>
16
+ </view>
17
+ <scroll-view class="announ-popup-list" scroll-y>
18
+ <view class="announ-items">
19
+ <view
20
+ wx:for="{{list}}"
21
+ wx:key="id"
22
+ class="announ-item {{index === list.length - 1 ? 'last' : ''}} {{index === 0 ? 'first' : ''}}"
23
+ >
24
+ <view class="announ-item-timeline">
25
+ <view class="announ-item-line announ-item-line-top"></view>
26
+ <view class="announ-item-dot-outer">
27
+ <view class="announ-item-dot-inner"></view>
28
+ </view>
29
+ <view class="announ-item-line announ-item-line-bottom"></view>
30
+ </view>
31
+ <view class="announ-item-content">
32
+ <view class="announ-item-text">{{item._text}}</view>
33
+ <view class="announ-item-time">{{item._time}}</view>
34
+ </view>
35
+ </view>
36
+ </view>
37
+ </scroll-view>
38
+ </view>
39
+ </vh-popup>
@@ -0,0 +1 @@
1
+ .popup-body{display:flex}.announ-popup{background:#f7f7f7;box-sizing:border-box;display:flex;flex-direction:column;height:50vh;width:100%}.announ-popup-header{align-items:center;display:flex;justify-content:center;margin-bottom:32rpx;position:relative}.announ-popup-title{color:#1a1a1a;font-size:36rpx;font-weight:600;position:relative}.announ-popup-title:after{background:#ff4d4f;border-radius:4rpx;bottom:-4rpx;content:"";height:8rpx;left:50%;opacity:.6;position:absolute;transform:translateX(-50%);width:60%}.announ-popup-star{color:#ffcd00;font-size:24rpx;margin:0 12rpx}.announ-popup-star.left{transform:rotate(-15deg)}.announ-popup-star.right{transform:rotate(15deg)}.announ-popup-list{box-sizing:border-box;flex:1;min-height:0;overflow:hidden}.announ-popup-items{position:relative}.announ-popup-line{border-left:1rpx dashed #bfbfbf;left:50%;position:absolute;transform:translateX(-50%);width:0}.announ-item{display:flex;padding-bottom:32rpx;padding-left:48rpx;position:relative}.announ-item-timeline{border-left:1rpx dashed #bfbfbf;bottom:0;left:16rpx;position:absolute;top:0;width:24rpx}.announ-item-dot-outer{align-items:center;background:#fff;border:3rpx solid #ccc;border-radius:50%;box-sizing:border-box;display:flex;flex-shrink:0;height:28rpx;justify-content:center;left:-14rpx;position:absolute;top:0;width:28rpx;z-index:1}.announ-item-dot-inner{background:#ccc;border-radius:50%;box-sizing:border-box;height:12rpx;width:12rpx}.announ-item.first .announ-item-dot-outer{background:#fff;border-color:#fb2626}.announ-item.first .announ-item-dot-inner{background:#fb2626}.announ-item.last{padding-bottom:0}.announ-item.last .announ-item-timeline{border:none}.announ-item-content{background:#fff;border-radius:12rpx;flex:1;padding:20rpx 24rpx}.announ-item-text{color:#1a1a1a;font-size:28rpx;line-height:1.5;margin-bottom:12rpx;word-break:break-all}.announ-item-time{color:#999;font-size:24rpx}
@@ -0,0 +1,228 @@
1
+ Component({
2
+ properties: {
3
+ sdkInstance: {
4
+ type: Object,
5
+ value: null,
6
+ },
7
+ // 直播间模式:0=竖屏,1=横屏(样式区分)
8
+ mode: {
9
+ type: Number,
10
+ value: 0,
11
+ },
12
+ },
13
+
14
+ data: {
15
+ announInfo: null, // 公告信息
16
+ showIcon: false, // 是否显示公告小图标
17
+ showPopup: false, // 是否显示公告弹窗
18
+ showBanner: false, // 是否显示公告banner
19
+ bannerText: '', // 公告banner文本
20
+ isStatic: false, // 公告banner内容是否静止不滚动(display_type==1)
21
+ curNoticeId: '', // 当前展示的公告 id
22
+ badge: 0, // 公告未读角标数字(收到推送 +1,打开弹窗清零)
23
+ },
24
+
25
+ observers: {
26
+ sdkInstance: function sdkInstance(_sdkInstance) {
27
+ if (!_sdkInstance) return;
28
+ this.sdkInstance = _sdkInstance;
29
+
30
+ this.unsub = this.sdkInstance.store.get({
31
+ announcement: (val) => {
32
+ // 初始化 / 配置刷新时,按公告数据统一处理展示逻辑
33
+ const watchInitData = this.sdkInstance.store.get('watchInitData');
34
+ const webinarType = watchInitData?.webinar?.type;
35
+ // 不是直播也不是回放,不处理
36
+ if (webinarType != 1 && webinarType != 5) return;
37
+
38
+ this.applyAnnouncement(val);
39
+ },
40
+ });
41
+
42
+ this.initEventListener();
43
+ },
44
+ },
45
+
46
+ lifetimes: {
47
+ detached() {
48
+ this.unsub && this.unsub();
49
+ this.unlistener?.();
50
+ this.clearTimer();
51
+ },
52
+ },
53
+
54
+ methods: {
55
+ initEventListener() {
56
+ this.unlistener?.();
57
+ const EVENT = this.sdkInstance.EVENT;
58
+ const obj = {};
59
+ // 推送公告:把消息字段转成与接口列表一致的格式,插入到列表首位(去重同 id),保留已有公告
60
+ obj[EVENT.ROOM_ANNOUNCEMENT] = (msg) => {
61
+ const data = msg?.data || msg;
62
+ if (!data) return;
63
+ // display_type: 0
64
+ // duration: 600
65
+ // event_type: "room_announcement"
66
+ // id: "364"
67
+ // push_time: "2026-08-10 16:39:55"
68
+ // record_display: 1
69
+ // room_announcement_text: "zxcfzxcxzc"
70
+ // room_id: "lss_1d63a879"
71
+ // room_join_id: "101643"
72
+ // set_type: 1
73
+ // type: "room_announcement"
74
+
75
+ const item = {
76
+ id: data.id,
77
+ content: data.room_announcement_text ? { content: data.room_announcement_text } : data.content || '',
78
+ duration: data.duration,
79
+ display_type: data.display_type,
80
+ create_at: data.push_time,
81
+ record_display: data.record_display,
82
+ set_type: data.set_type
83
+ };
84
+ const prev = this.sdkInstance.store.get('announcement') || {};
85
+ const list = (prev.list || []).filter((it) => it.id !== item.id);
86
+ list.unshift(item);
87
+
88
+ this.sdkInstance.store.set('announcement', { list, total: list.length });
89
+ // 收到推送公告,未读角标 +1
90
+ this.setData({ badge: this.data.badge + 1 });
91
+ };
92
+
93
+ // 公告删除:从列表中移除该公告,store 变化会自动触发 applyAnnouncement 重新渲染并关闭 banner
94
+ obj[EVENT.DEL_ROOM_ANNOUNCEMENT] = (msg) => {
95
+ const delId = msg?.data?.id ?? msg?.id;
96
+ if (!delId) return;
97
+ const prev = this.sdkInstance.store.get('announcement') || {};
98
+ const list = (prev.list || []).filter((it) => it.id !== delId);
99
+ this.sdkInstance.store.set('announcement', { list, total: list.length });
100
+ };
101
+
102
+ // 直播结束:自动关闭公告
103
+ obj[EVENT.LIVE_END] = () => {
104
+ this.closeAnnouncement();
105
+ };
106
+
107
+ this.unlistener = this.sdkInstance.on(obj);
108
+ },
109
+
110
+ /**
111
+ * 根据公告数据统一处理展示逻辑(store / common-config 回调)
112
+ * - 列表为空:关闭 banner
113
+ * - 否则取第一条公告 item 交给统一调度
114
+ */
115
+ applyAnnouncement(announ) {
116
+ this.setData({ announInfo: announ });
117
+
118
+ const list = announ?.list || [];
119
+ const total = announ?.total || 0;
120
+
121
+ if (total < 1) {
122
+ this.clearTimer();
123
+ this.setData({
124
+ showIcon: false,
125
+ showBanner: false,
126
+ isStatic: false,
127
+ curNoticeId: '',
128
+ bannerText: '',
129
+ });
130
+ return;
131
+ }
132
+
133
+ // 取第一条公告 item 交给统一调度
134
+ this.scheduleBanner(list[0] || {});
135
+ },
136
+
137
+ /**
138
+ * 统一调度 banner 展示与定时关闭
139
+ * - content: {content: "sdfdsfsdfsdfdsfdsfdsf"}
140
+ * - created_at: "2026-08-10 16:22:52" // 公告创建四时间
141
+ * - display_type:0 // 0 滚动, 1:不滚动
142
+ * - duration: 300 剩余时长,0:永久展示 ; >0 按剩余时长展示,超时自动关闭
143
+ * - id: "363" //公告id
144
+ * - set_type: 1
145
+ */
146
+ scheduleBanner(item = {}) {
147
+ const id = item.id;
148
+ const duration = Number(item.duration) || 0;
149
+ const displayType = Number(item.display_type);
150
+ const createAt = item.created_at;
151
+ const text = typeof item.content === 'string' ? item.content : item.content?.content || item.content?.text || '';
152
+
153
+ this.clearTimer();
154
+
155
+ this.setData({
156
+ showIcon: true,
157
+ isStatic: displayType === 1,
158
+ curNoticeId: id,
159
+ bannerText: text,
160
+ });
161
+
162
+ // duration < 1:永久展示
163
+ if (duration < 1) {
164
+ this.setData({ showBanner: true });
165
+ return;
166
+ }
167
+
168
+ // 计算剩余展示时长
169
+ const now = Date.now();
170
+ const createTs = this.parseTime(createAt);
171
+ // pushTime 解析失败则视为从当前时刻开始计时,避免 NaN 导致不展示
172
+ const remain = isNaN(createTs) ? duration * 1000 : duration * 1000 - (now - createTs);
173
+
174
+ if (remain > 0) {
175
+ this.setData({ showBanner: true });
176
+ this.announTimer = setTimeout(() => {
177
+ this.setData({ showBanner: false });
178
+ this.announTimer = null;
179
+ }, remain);
180
+ } else {
181
+ this.setData({ showBanner: false });
182
+ }
183
+ },
184
+
185
+ // 解析时间字符串为时间戳(兼容 iOS:将 "2026-08-07 16:47:13" 转为 "2026/08/07 16:47:13")
186
+ parseTime(str) {
187
+ if (!str) return NaN;
188
+ const s = String(str).replace(/-/g, '/');
189
+ const t = new Date(s).getTime();
190
+ return isNaN(t) ? NaN : t;
191
+ },
192
+
193
+ // 清除展示定时器
194
+ clearTimer() {
195
+ if (this.announTimer) {
196
+ clearTimeout(this.announTimer);
197
+ this.announTimer = null;
198
+ }
199
+ },
200
+
201
+ // 被动关闭(删除/直播结束):关闭当前 banner
202
+ closeAnnouncement() {
203
+ this.clearTimer();
204
+ this.setData({ showBanner: false });
205
+ },
206
+
207
+ // 用户手动关闭当前 banner:记录该公告 id,同一条不再自动弹(新公告仍展示)
208
+ handleBannerClose() {
209
+ this.clearTimer();
210
+ this.setData({ showBanner: false });
211
+ },
212
+
213
+ // 点击公告图标:打开弹窗并清零未读角标
214
+ handleIconClick() {
215
+ this.setData({ showPopup: true, badge: 0 });
216
+ },
217
+
218
+ // 关闭公告弹窗
219
+ handlePopupClose() {
220
+ this.setData({ showPopup: false });
221
+ },
222
+
223
+ // 关闭顶部滚动条(用户手动关闭)
224
+ handleBannerClose() {
225
+ this.closeAnnouncement();
226
+ },
227
+ },
228
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "component": true,
3
+ "styleIsolation": "shared",
4
+ "usingComponents": {
5
+ "announ-icon": "./announ-icon/index",
6
+ "announ-popup": "./announ-popup/index",
7
+ "announ-banner": "./announ-banner/index"
8
+ }
9
+ }
@@ -0,0 +1,6 @@
1
+ <!-- 公告顶部滚动条 -->
2
+ <announ-banner show="{{showBanner}}" text="{{bannerText}}" static="{{isStatic}}" mode="{{mode}}" bind:close="handleBannerClose" />
3
+ <!-- 公告浮动小图标 -->
4
+ <announ-icon wx:if="{{showIcon}}" showIcon="{{showIcon}}" badge="{{badge}}" mode="{{mode}}" bind:onTapIcon="handleIconClick" />
5
+ <!-- 公告弹窗 -->
6
+ <announ-popup show="{{showPopup}}" info="{{announInfo}}" bind:close="handlePopupClose" />
File without changes