feat: 电子药箱小程序 - 4Tab药品管理

- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡
- 药品百科: 搜索 + 分类筛选 + 20种药品静态数据
- 惠教中心: 4条药品使用指南课程
- 我的: 家庭成员信息管理
- 自定义TabBar + navigation-bar组件
- SVG药品分类插图
This commit is contained in:
陈誉午
2026-07-29 11:20:52 +08:00
commit 994b267f5c
683 changed files with 43849 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
miniprogram_npm/
.DS_Store
*.log
+74
View File
@@ -0,0 +1,74 @@
{
"pages": [
"pages/tab-bar/medicine-box/index",
"pages/tab-bar/medicine-box/add/index",
"pages/tab-bar/drug-wiki/index",
"pages/tab-bar/education/index",
"pages/tab-bar/profile/index",
"pages/tab-bar/profile/edit/index",
"pages/drug-guide/index",
"pages/login/login",
"pages/live/video/video",
"pages/live/registration/registration",
"pages/live/red-history/red-history",
"pages/live/report/report",
"pages/common/red/red",
"pages/live/room/room",
"pages/live/h5Room/index",
"pages/live/wxroom/index"
],
"subPackages": [
{
"root": "package-live",
"pages": [
"wxroom/index",
"login/login",
"red-history/red-history"
]
}
],
"tabBar": {
"custom": true,
"list": [
{
"pagePath": "pages/tab-bar/medicine-box/index",
"text": "电子药箱"
},
{
"pagePath": "pages/tab-bar/drug-wiki/index",
"text": "药品百科"
},
{
"pagePath": "pages/tab-bar/education/index",
"text": "惠教中心"
},
{
"pagePath": "pages/tab-bar/profile/index",
"text": "我的"
}
]
},
"window": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
},
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true,
"defaultContentBox": true,
"tagNameStyleIsolation": "legacy",
"disableABTest": true,
"sdkVersionBegin": "3.0.0",
"sdkVersionEnd": "15.255.255"
}
},
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents",
"requiredPrivateInfos": [
"chooseAddress"
],
"useExtendedLib": {
"weui": true
}
}
+17
View File
@@ -0,0 +1,17 @@
/**app.wxss**/
@import "./variables";
@import './miniprogram_npm/@vant/weapp/common/index.wxss';
.container {
background-color: #f5f5f5;
padding: $primary-gap;
font-size: $defualt-font-size;
min-height: 88vh;
height: 100%;
width: 702rpx;
}
.h5{
display: flex;
flex-direction: column;
}
+100
View File
@@ -0,0 +1,100 @@
// app.ts
import { startSSE, stopSSE } from './utils/server-sent-events'
import { checkUpdate } from './utils/updateManager'
// @ts-ignore
const systemInfo = wx.getWindowInfo();
export { };
App<IAppOption>({
globalData: {
isLogin: false, // 当前是否登录
wasLogin: false, // 之前是否登录过,用来判断是不是 登录状态失效
safeBottom: systemInfo.safeArea.bottom - systemInfo.safeArea.height,
safeTop: systemInfo.safeArea.top * 2,
store: {},
joinedArray: [], //加入的频道,加入了就不会重复加入
sseStore: {
chat: [],
system: [],
product: []
},
isModalShowing: false, //防止有多个登录弹窗打开
},
async onLaunch(props: any) {
wx.removeStorageSync("state");
if (props.query.state) {
wx.setStorageSync("state", props.query.state)
} else {
wx.removeStorageSync("state")
}
// 检查小程序版本更新
checkUpdate();
this.checkLoginState();
startSSE();
},
onHide() {
stopSSE();
},
checkLoginState() {
const openId = wx.getStorageSync("openId");
const unionId = wx.getStorageSync("unionId");
const userId = wx.getStorageSync("userId");
const ok = openId && unionId && userId;
if (!ok) {
// 清理
wx.removeStorageSync("mbuId");
wx.removeStorageSync("userName");
wx.removeStorageSync("openId");
wx.removeStorageSync("unionId");
wx.removeStorageSync("addressId");
wx.removeStorageSync("defualtAddress");
}
// 保存上一次的登录状态,用来判断是否从 已登录 变成 未登录
const prev = this.globalData.isLogin;
this.globalData.isLogin = !!ok;
this.globalData.wasLogin = prev;
return this.globalData.isLogin;
},
login(value: string) {
if (!this.checkLoginState()) {
wx.reLaunch({ url: `/pages/login/login?back=${value}` });
return false;
}
return true;
},
redirectLogin() {
if (!this.checkLoginState()) {
//已有modal在展示,就不展示了
if (this.globalData.isModalShowing) {
return false;
}
this.globalData.isModalShowing = true;
const content = this.globalData.wasLogin
? "登录状态失效,请重新登录!"
: "请登录获取完整服务!";
wx.showModal({
title: "登录提示",
content,
confirmText: "去登录",
success: (options) => {
if (options.confirm) {
wx.navigateTo({ url: "/pages/login/login" });
}
},
complete: () => {
this.globalData.isModalShowing = false;
}
});
return false;
}
return true;
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756456020247" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18415" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 64a448 448 0 1 0 448 448A448 448 0 0 0 512 64z m0 832a384 384 0 1 1 384-384 384 384 0 0 1-384 384z" p-id="18416" fill="#ffffff"></path><path d="M544 320h-64v160h-160v64h160v160h64v-160h160v-64h-160v-160z" p-id="18417" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 585 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765961452071" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5300" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M896 108.032h-52.224c-16.896 0-30.72 13.824-30.72 30.72s13.824 30.72 30.72 30.72H896c16.896 0 30.72 13.824 30.72 30.72v651.776c0 16.896-13.824 30.72-30.72 30.72H129.536c-16.896 0-30.72-13.824-30.72-30.72V200.192c0-16.896 13.824-30.72 30.72-30.72h79.36c16.896 0 30.72-13.824 30.72-30.72s-13.824-30.72-30.72-30.72H129.536c-50.688 0-92.16 41.472-92.16 92.16v651.776c0 50.688 41.472 92.16 92.16 92.16H896c50.688 0 92.16-41.472 92.16-92.16V200.192c0-50.688-41.472-92.16-92.16-92.16z" fill="#FA6905" p-id="5301"></path><path d="M408.576 169.472H640c16.896 0 30.72-13.824 30.72-30.72s-13.824-30.72-30.72-30.72H408.576c-16.896 0-30.72 13.824-30.72 30.72s13.824 30.72 30.72 30.72zM308.736 225.28c16.896 0 30.72-13.824 30.72-30.72V55.296c0-16.896-13.824-30.72-30.72-30.72s-30.72 13.824-30.72 30.72V194.56c0 16.896 13.824 30.72 30.72 30.72zM741.376 225.28c16.896 0 30.72-13.824 30.72-30.72V63.488c0-16.896-13.824-30.72-30.72-30.72s-30.72 13.824-30.72 30.72V194.56c0 16.896 13.824 30.72 30.72 30.72zM728.064 385.024L457.728 655.36l-123.904-123.904c-11.776-11.776-31.232-11.776-43.52 0-11.776 11.776-11.776 31.232 0 43.52l145.408 145.408c6.144 6.144 13.824 9.216 21.504 9.216 3.072 0 6.144-0.512 8.704-1.536 4.608-1.536 9.216-3.584 12.8-7.68l291.84-291.84c11.776-11.776 11.776-31.232 0-43.52-11.264-12.288-30.72-12.288-42.496 0z" fill="#FA6905" p-id="5302"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745572414914" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2594" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 0a512 512 0 1 1 0 1024A512 512 0 0 1 512 0z m0 192a32 32 0 0 0-32 32v334.848a64 64 0 0 0 28.48 53.248l151.168 100.8a32 32 0 1 0 35.456-53.248L572.48 577.856a64 64 0 0 1-28.48-53.248V224A32 32 0 0 0 512 192z" fill="#1296db" p-id="2595"></path></svg>

After

Width:  |  Height:  |  Size: 586 B

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 200 200">
<path fill="#fff"
d="M75,17.2v48.4c0,5.2-4.2,9.4-9.4,9.4H17.2c-2.6,0-4.7-2.1-4.7-4.7v-15.6c0-2.6,2.1-4.7,4.7-4.7h32.8V17.2c0-2.6,2.1-4.7,4.7-4.7h15.6c2.6,0,4.7,2.1,4.7,4.7Z"/>
<path fill="#fff"
d="M187.5,70.3v-15.6c0-2.6-2.1-4.7-4.7-4.7h-32.8V17.2c0-2.6-2.1-4.7-4.7-4.7h-15.6c-2.6,0-4.7,2.1-4.7,4.7v48.4c0,5.2,4.2,9.4,9.4,9.4h48.4c2.6,0,4.7-2.1,4.7-4.7Z"/>
<path fill="#fff"
d="M129.7,187.5h15.6c2.6,0,4.7-2.1,4.7-4.7v-32.8h32.8c2.6,0,4.7-2.1,4.7-4.7v-15.6c0-2.6-2.1-4.7-4.7-4.7h-48.4c-5.2,0-9.4,4.2-9.4,9.4v48.4c0,2.6,2.1,4.7,4.7,4.7Z"/>
<path fill="#fff"
d="M12.5,129.7v15.6c0,2.6,2.1,4.7,4.7,4.7h32.8v32.8c0,2.6,2.1,4.7,4.7,4.7h15.6c2.6,0,4.7-2.1,4.7-4.7v-48.4c0-5.2-4.2-9.4-9.4-9.4H17.2c-2.6,0-4.7,2.1-4.7,4.7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 896 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744786271399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4083" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M816.49 909H211.21c-1.1 0-2-0.9-2-2v-68.18c0-1.1 0.9-2 2-2h605.28c1.1 0 2 0.9 2 2V907c0 1.1-0.9 2-2 2z" fill="#FFAA22" p-id="4084"></path><path d="M910.24 316.23c-27.11 0-49.1 22.52-49.1 50.31 0 7.28 1.58 14.16 4.3 20.4l-176.13 80.21-147.2-258.57c14.56-8.73 24.46-24.74 24.46-43.28 0-27.79-21.98-50.31-49.1-50.31s-49.1 22.52-49.1 50.31c0 17.99 9.29 33.66 23.15 42.55l-158.16 259.3-176.13-80.21c2.71-6.25 4.3-13.12 4.3-20.4 0-27.78-21.98-50.31-49.1-50.31s-49.1 22.52-49.1 50.31c0 27.78 21.98 50.31 49.1 50.31 3.99 0 7.82-0.62 11.53-1.54l86.65 366.28h601.43l86.65-366.28c3.71 0.92 7.54 1.54 11.53 1.54 27.12 0 49.1-22.52 49.1-50.31 0.01-27.78-21.97-50.31-49.08-50.31z" fill="#FFD68D" p-id="4085"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757059529894" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5109" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M914.285714 146.285714c20.114286 0 36.571429 16.457143 36.571429 36.571429s-16.457143 36.571429-36.571429 36.571428H877.714286v731.428572a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V219.428571H109.714286a36.644571 36.644571 0 0 1-36.571429-36.571428c0-20.114286 16.457143-36.571429 36.571429-36.571429h804.571428zM804.571429 950.857143V219.428571H219.428571v731.428572h585.142858z m-402.285715-73.142857a36.644571 36.644571 0 0 1-36.571428-36.571429v-512c0-20.114286 16.457143-36.571429 36.571428-36.571428s36.571429 16.457143 36.571429 36.571428v512c0 20.114286-16.457143 36.571429-36.571429 36.571429z m219.428572 0a36.644571 36.644571 0 0 1-36.571429-36.571429v-512c0-20.114286 16.457143-36.571429 36.571429-36.571428s36.571429 16.457143 36.571428 36.571428v512c0 20.114286-16.457143 36.571429-36.571428 36.571429z m-292.571429-804.571429A36.644571 36.644571 0 0 1 292.571429 36.571429c0-20.114286 16.457143-36.571429 36.571428-36.571429h365.714286c20.114286 0 36.571429 16.457143 36.571428 36.571429s-16.457143 36.571429-36.571428 36.571428h-365.714286z" fill="#d81e06" fill-opacity=".7" p-id="5110"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756453781220" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15676" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M358.165868 554.624796c-0.533143 0.680499-1.066285 1.391696-1.303692 2.251274l-41.104163 150.700257c-2.400676 8.772804 0.059352 18.226107 6.550183 24.892947 4.860704 4.742001 11.261485 7.350408 18.077727 7.350408 2.252297 0 4.504594-0.267083 6.727215-0.860601l149.630902-40.808428c0.23843 0 0.357134 0.207731 0.534166 0.207731 1.718131 0 3.408633-0.62217 4.683672-1.927909l400.113747-400.054395c11.883655-11.897981 18.404162-28.109198 18.404162-45.74281 0-19.989263-8.476045-39.963177-23.324218-54.767348l-37.786605-37.844933c-14.81645-14.848173-34.822087-23.338544-54.797024-23.338544-17.631566 0-33.842783 6.520507-45.75816 18.388812L358.758362 553.232077C358.344946 553.615816 358.462626 554.179658 358.165868 554.624796M862.924953 257.19778l-39.742143 39.71349-64.428382-65.451688 39.180348-39.179324c6.193049-6.222725 18.194384-5.318122 25.308409 1.822508l37.813211 37.845956c3.943822 3.941775 6.195096 9.18622 6.195096 14.372336C867.223862 250.574942 865.710392 254.42769 862.924953 257.19778M429.322487 560.907896l288.712541-288.728914 64.459081 65.49569L494.314711 625.838721 429.322487 560.907896zM376.718409 677.970032l20.863167-76.580143 55.656601 55.657624L376.718409 677.970032z" fill="#FA6905" p-id="15677"></path><path d="M888.265084 415.735539c-15.144932 0-27.562752 12.313443-27.620058 27.665083l0 372.98283c0 19.559475-15.885805 35.444257-35.475979 35.444257L194.220958 851.827709c-19.559475 0-35.504632-15.883759-35.504632-35.444257L158.716326 207.602222c0-19.575848 15.945157-35.474956 35.504632-35.474956l406.367171 0c15.231913 0 27.592428-12.371772 27.592428-27.606755 0-15.202237-12.360516-27.590382-27.592428-27.590382L190.013123 116.930129c-47.684022 0-86.49291 38.779212-86.49291 86.49291L103.520213 820.59231c0 47.713698 38.808888 86.47756 86.49291 86.47756l639.334083 0c47.715745 0 86.509283-38.763862 86.509283-86.47756L915.856489 443.222567C915.794068 428.048983 903.408993 415.735539 888.265084 415.735539" fill="#FA6905" p-id="15678"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756971012126" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9026" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M855.6 427.2H168.5c-12.7 0-24.4 6.9-30.6 18L4.4 684.7C1.5 689.9 0 695.8 0 701.8v287.1c0 19.4 15.7 35.1 35.1 35.1H989c19.4 0 35.1-15.7 35.1-35.1V701.8c0-6-1.5-11.8-4.4-17.1L886.2 445.2c-6.2-11.1-17.9-18-30.6-18zM673.4 695.6c-16.5 0-30.8 11.5-34.3 27.7-12.7 58.5-64.8 102.3-127.2 102.3s-114.5-43.8-127.2-102.3c-3.5-16.1-17.8-27.7-34.3-27.7H119c-26.4 0-43.3-28-31.1-51.4l81.7-155.8c6.1-11.6 18-18.8 31.1-18.8h622.4c13 0 25 7.2 31.1 18.8l81.7 155.8c12.2 23.4-4.7 51.4-31.1 51.4H673.4zM819.9 209.5c-1-1.8-2.1-3.7-3.2-5.5-9.8-16.6-31.1-22.2-47.8-12.6L648.5 261c-17 9.8-22.7 31.6-12.6 48.4 0.9 1.4 1.7 2.9 2.5 4.4 9.5 17 31.2 22.8 48 13L807 257.3c16.7-9.7 22.4-31 12.9-47.8zM375.4 261.1L255 191.6c-16.7-9.6-38-4-47.8 12.6-1.1 1.8-2.1 3.6-3.2 5.5-9.5 16.8-3.8 38.1 12.9 47.8L337.3 327c16.9 9.7 38.6 4 48-13.1 0.8-1.5 1.7-2.9 2.5-4.4 10.2-16.8 4.5-38.6-12.4-48.4zM512 239.3h2.5c19.5 0.3 35.5-15.5 35.5-35.1v-139c0-19.3-15.6-34.9-34.8-35.1h-6.4C489.6 30.3 474 46 474 65.2v139c0 19.5 15.9 35.4 35.5 35.1h2.5z" p-id="9027" fill="#AEADB2"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744770628716" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7645" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M64 360V112c0-26.6 21.4-48 48-48h248c13.2 0 24 10.8 24 24v80c0 13.2-10.8 24-24 24H192v168c0 13.2-10.8 24-24 24H88c-13.2 0-24-10.8-24-24zM640 88v80c0 13.2 10.8 24 24 24h168v168c0 13.2 10.8 24 24 24h80c13.2 0 24-10.8 24-24V112c0-26.6-21.4-48-48-48H664c-13.2 0-24 10.8-24 24z m296 552h-80c-13.2 0-24 10.8-24 24v168h-168c-13.2 0-24 10.8-24 24v80c0 13.2 10.8 24 24 24h248c26.6 0 48-21.4 48-48V664c0-13.2-10.8-24-24-24zM384 936v-80c0-13.2-10.8-24-24-24H192v-168c0-13.2-10.8-24-24-24H88c-13.2 0-24 10.8-24 24v248c0 26.6 21.4 48 48 48h248c13.2 0 24-10.8 24-24z" p-id="7646" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 927 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745572629047" class="icon" viewBox="0 0 1030 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5496" xmlns:xlink="http://www.w3.org/1999/xlink" width="201.171875" height="200"><path d="M187.362681 903.986178a509.339566 509.339566 0 1 1 650.404075-783.972356 509.339566 509.339566 0 1 1-650.404075 783.972356z m607.174466-731.943263a441.691642 441.691642 0 1 0-563.985268 679.91417 441.696693 441.696693 0 1 0 564.005473-679.91417z" fill="#d81e06" p-id="5497"></path><path d="M146.793121 166.529852l772.771452 641.110569-41.283822 49.762039-772.771452-641.111579z" fill="#d81e06" p-id="5498"></path></svg>

After

Width:  |  Height:  |  Size: 759 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756454072489" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16732" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 97.52381c175.055238 0 316.952381 140.312381 316.952381 313.392761 0 3.657143-0.073143 7.314286-0.219429 11.044572-3.900952 103.570286-61.805714 220.038095-156.842666 338.139428a1352.289524 1352.289524 0 0 1-148.382476 156.306286L512 926.47619l-11.50781-10.069333-13.824-12.531809a1352.289524 1352.289524 0 0 1-134.582857-143.774477c-95.012571-118.101333-152.917333-234.569143-156.818285-338.163809A292.08381 292.08381 0 0 1 195.047619 410.940952c0-170.715429 138.044952-309.540571 309.784381-313.295238L512 97.52381z m0 69.095619c-137.97181 0-249.027048 109.83619-249.027048 244.297142 0 2.755048 0.048762 5.558857 0.146286 8.387048 3.145143 82.724571 51.541333 185.173333 141.55581 297.008762a1271.271619 1271.271619 0 0 0 91.91619 102.058667l15.408762 15.11619 15.408762-15.11619a1271.271619 1271.271619 0 0 0 91.91619-102.058667c90.014476-111.835429 138.410667-214.308571 141.55581-297.008762 0.097524-2.82819 0.146286-5.607619 0.146286-8.387048 0-134.460952-111.055238-244.297143-249.027048-244.297142z m0 80.627809c87.527619 0 158.47619 72.167619 158.47619 161.231238 0 89.039238-70.948571 161.231238-158.47619 161.231238s-158.47619-72.167619-158.47619-161.231238c0-89.039238 70.948571-161.231238 158.47619-161.231238z m0 69.095619c-50.005333 0-90.550857 41.252571-90.550857 92.135619 0 50.883048 40.545524 92.135619 90.550857 92.135619s90.550857-41.252571 90.550857-92.135619c0-50.883048-40.545524-92.135619-90.550857-92.135619z" p-id="16733" fill="#FA6905"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765961791175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7321" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M817.52802 103.262852l102.085026 0c22.63144 0 40.97318 18.431791 40.97318 41.052998l0 163.135474c0 22.672372-18.361183 41.052998-40.97318 41.052998l-817.273728 0c-22.63144 0-40.97318-18.430768-40.97318-41.052998l0-163.135474c0-22.671349 18.371416-41.052998 40.97318-41.052998l102.085026 0 0 61.310368-61.370743 0c-11.116175 0-20.377096 9.060353-20.377096 20.247136l0 82.127486c0 11.026124 9.120728 20.246113 20.377096 20.246113l735.845161 0c11.116175 0 20.377096-9.060353 20.377096-20.246113l0-82.127486c0-11.026124-9.120728-20.247136-20.377096-20.247136l-61.370743 0L817.52802 103.262852zM633.596917 103.262852 388.355447 103.262852l0 61.310368 245.24147 0L633.596917 103.262852zM163.550425 389.379265l-61.211107 0c-22.601764 0-40.97318 18.222013-40.97318 40.693818l0 490.843144c0 22.292726 18.34174 40.694841 40.97318 40.694841l817.273728 0c22.611997 0 40.97318-18.222013 40.97318-40.694841L960.586227 430.073082c0-22.302959-18.34174-40.693818-40.97318-40.693818l-61.210084 0 0 61.310368 20.4958 0c11.256368 0 20.377096 8.980535 20.377096 20.487613l0 408.647097c0 11.296277-9.260921 20.476357-20.377096 20.476357L143.053602 900.3007c-11.256368 0-20.377096-8.979512-20.377096-20.476357l0-408.647097c0-11.316743 9.260921-20.487613 20.377096-20.487613l20.496823 0L163.550425 389.379265zM694.907285 92.984764c0-16.894785 13.612019-30.594809 30.655184-30.594809 16.924461 0 30.655184 13.730723 30.655184 30.594809l0 143.177933c0 16.893762-13.610996 30.594809-30.655184 30.594809-16.923438 0-30.655184-13.731746-30.655184-30.594809L694.907285 92.984764zM265.734712 92.984764c0-16.894785 13.612019-30.594809 30.655184-30.594809 16.944927 0 30.655184 13.730723 30.655184 30.594809l0 143.177933c0 16.893762-13.610996 30.594809-30.655184 30.594809-16.944927 0-30.655184-13.731746-30.655184-30.594809L265.734712 92.984764zM644.514571 695.612854c-12.593829 39.556924-28.321022 75.240635-47.201022 106.993826l102.503559 0 0 22.932292L323.033719 825.538973l0-22.932292 248.195755 0c18.281365-30.275537 34.766828-68.196195 49.455365-113.738437L644.514571 695.612854zM397.235698 695.15339c12.573363 23.091928 24.568559 51.710732 35.96512 85.879949l-22.033829 5.38873c-9.001001-28.180829-20.995173-56.202023-35.964096-84.062558L397.235698 695.15339zM501.097184 685.71339c11.675924 29.078269 21.255093 59.793828 28.758997 92.165096l-22.013363 5.38873c-8.702196-33.249265-18.441001-63.384609-29.237905-90.369194L501.097184 685.71339zM395.440819 644.799562l231.531214 0 0 21.595854L395.440819 666.395416 395.440819 644.799562zM372.050086 495.075539c-11.69639 23.381523-25.186635 44.50461-40.47483 63.385633l-20.676925-13.032828c28.180829-34.466999 48.697095-72.825632 61.590753-115.096365l23.829731 4.949732c-5.988388 16.784268-10.638291 29.218462-13.930267 37.321l124.097366 0 0 22.472827-71.050196 0c9.001001 19.487843 15.446807 35.824927 19.338441 49.015344l-22.033829 8.082072c-5.987365-19.178805-13.192464-38.207184-21.574365-57.098439L372.050086 495.074515zM561.788451 495.075539c-8.102538 16.485463-17.083074 31.773657-26.982538 45.861514l-20.236903-13.490246c20.996197-28.76923 36.56273-61.141522 46.761-97.114828l23.370267 4.949732c-3.591805 11.685133-7.942903 24.128537-13.012362 37.321l135.752824 0 0 22.472827-77.316923 0c10.777461 18.890232 19.320021 36.422538 25.625634 52.609195l-22.471804 8.540514c-7.205099-18.878976-16.804734-39.265282-28.779464-61.149708L561.788451 495.075539zM518.620279 544.990369c57.259098 42.25129 122.282021 75.380828 195.128119 99.370195l-11.695367 21.115923c-75.240635-25.786293-138.906653-59.196217-191.07685-100.269681-48.856731 38.380122-112.542192 71.649853-191.096293 99.830683l-11.6749-21.595854c77.616752-26.364461 142.67856-59.19417 195.128119-98.451266L518.620279 544.990369z" fill="#FA6905" p-id="7322"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M429.354667 172.970667L87.68 751.146667A96 96 0 0 0 170.325333 896h683.349334a96 96 0 0 0 82.645333-144.853333L594.645333 172.970667a96 96 0 0 0-165.290666 0z m98.944 21.290666a32 32 0 0 1 11.264 11.264l341.632 578.176a32 32 0 0 1-27.52 48.298667H170.325333a32 32 0 0 1-27.52-48.298667l341.632-578.176a32 32 0 0 1 39.936-13.226666l3.925334 1.962666z" /><path fill="#ffffff" d="M512 352c16.426667 0 32.426667 12.672 32 32l-4.48 222.165333c0 17.706667-9.813333 28.757333-27.52 28.757334-16.426667 0-26.154667-11.050667-26.666667-28.757334-0.341333-11.776-2.133333-85.845333-5.333333-222.165333a32 32 0 0 1 32-32z" /><path fill="#ffffff" d="M512 757.333333a42.666667 42.666667 0 1 0 0-85.333333 42.666667 42.666667 0 0 0 0 85.333333z" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744769960542" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5093" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M350 148h-56c-8.8 0-16 6.5-16 14.6v698.9c0 8 7.2 14.6 16 14.6h56c8.8 0 16-6.5 16-14.6V162.6c0-8.1-7.2-14.6-16-14.6zM730 148h-56c-8.8 0-16 6.5-16 14.6v698.9c0 8 7.2 14.6 16 14.6h56c8.8 0 16-6.5 16-14.6V162.6c0-8.1-7.2-14.6-16-14.6z" p-id="5094" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 605 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745301431888" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7055" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M868.251 829.599c0 55-45 100-100 100H255.745c-55 0-100-45-100-100V194.401c0-55 45-100 100-100h512.506c55 0 100 45 100 100v635.198z" fill="#E7402E" p-id="7056"></path><path d="M511.998 466.089c-96.093 0-173.991 77.899-173.991 173.991s77.899 173.991 173.991 173.991 173.991-77.899 173.991-173.991-77.898-173.991-173.991-173.991z m1 259.487l-85.496-85.496 85.496-85.496 85.496 85.496-85.496 85.496z" fill="#F4DD50" p-id="7057"></path><path d="M340.103 880.595h-59.177c-40.901 0-74.177-42.621-74.177-95.008v-80.008c0-8.284 6.716-15 15-15 8.284 0 15 6.716 15 15v80.008c0 35.238 20.23 65.008 44.177 65.008h59.177c8.284 0 15 6.716 15 15s-6.716 15-15 15zM464.596 880.595h-44.997c-8.284 0-15-6.716-15-15s6.716-15 15-15h44.997c8.284 0 15 6.716 15 15s-6.715 15-15 15z" fill="#FFFFFF" p-id="7058"></path><path d="M768.25 944.599H255.745c-63.411 0-115-51.589-115-115V194.401c0-63.411 51.589-115 115-115H768.25c63.411 0 115 51.589 115 115v252.063c0 8.284-6.716 15-15 15-8.284 0-15-6.716-15-15V194.401c0-46.869-38.131-85-85-85H255.745c-46.869 0-85 38.131-85 85v635.198c0 46.869 38.131 85 85 85H768.25c46.869 0 85-38.131 85-85V696.951c0-8.284 6.716-15 15-15 8.284 0 15 6.716 15 15v132.648c0.001 63.411-51.588 115-115 115z" fill="#3D100B" p-id="7059"></path><path d="M868.251 641.455c-8.284 0-15-6.716-15-15v-86.996c0-8.284 6.716-15 15-15s15 6.716 15 15v86.996c0 8.284-6.716 15-15 15zM549 422.096c-79.635 0-155.779-7.503-226.316-22.301-67.737-14.21-127.949-35.052-174.127-60.273-7.271-3.971-9.945-13.084-5.975-20.354 3.971-7.27 13.084-9.945 20.355-5.975C253.398 362.6 397.72 392.096 549 392.096c156.316 0 222.174-29.81 312.063-78.906 7.271-3.97 16.384-1.295 20.355 5.975 3.971 7.271 1.296 16.383-5.974 20.354C784.117 389.4 712.056 422.096 549 422.096z" fill="#3D100B" p-id="7060"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744769898908" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3722" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 213.333333l0-170.666667-213.333333 213.333333 213.333333 213.333333 0-170.666667c141.44 0 256 114.56 256 256s-114.56 256-256 256-256-114.56-256-256l-85.333333 0c0 188.586667 152.746667 341.333333 341.333333 341.333333s341.333333-152.746667 341.333333-341.333333-152.746667-341.333333-341.333333-341.333333z" fill="#ffffff" p-id="3723"></path></svg>

After

Width:  |  Height:  |  Size: 686 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757486916852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5122" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357 357 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a352 352 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357 357 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64z m-23.424 64H446.72l-36.352 113.088-24.512 11.968a294 294 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293 293 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294 294 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288 288 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293 293 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a288 288 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256" p-id="5123" fill="#FA6905"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744873435594" class="icon" viewBox="0 0 1246 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3679" xmlns:xlink="http://www.w3.org/1999/xlink" width="243.359375" height="200"><path d="M575.85566552 16.30191287c-12.76660889-1.07826041-33.03791287 0.30191287-45.24382674 12.07652179 0 0-299.79965247 270.64347823-312.52313068 282.63373959H117.29288234C60.87827345 311.01217425 12.48592591 355.09147821 12.48592591 409.34956534v196.67478219c0 54.17182575 48.47860889 98.2511307 104.80695643 98.25113068h100.79582575c12.80973959 12.20591288 312.65252177 282.63373959 312.6525218 282.63373961 13.24104357 12.76660889 32.43408712 13.15478218 45.20069602 12.07652177 25.87826041-2.37217425 49.38434753-26.0507822 49.38434754-45.24382672 0.21565247-2.15652177 0.43130397-889.95339111 0-892.10991287 0-18.67547821-23.59234753-42.95791287-49.47060793-45.24382575V16.30191287zM1188.2215784 354.57391288a61.89217425 61.89217425 0 1 0-87.55478317-87.5547822l-153.11304357 153.15617329-153.11304356-153.11304356a61.89217425 61.89217425 0 0 0-87.55478219 87.46852177l153.15617426 153.15617425-153.11304357 153.11304357a61.89217425 61.89217425 0 1 0 87.4685218 87.5547822l153.15617326-153.11304261 153.11304356 153.11304261a61.89217425 61.89217425 0 0 0 87.55478318-87.5547822l-153.11304358-153.11304357 153.11304358-153.11304355z" p-id="3680" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745572584235" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4073" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M511.978705 1024C229.689806 1024 0 794.310194 0 511.978705 0 229.689806 229.689806 0 511.978705 0s511.978705 229.689806 511.978705 511.978705c0.04259 282.331489-229.647215 512.021295-511.978705 512.021295z m0-945.505968c-239.017094 0-433.484673 194.467579-433.484673 433.442082 0 239.017094 194.467579 433.527264 433.484673 433.527264 239.017094 0 433.484673-194.467579 433.484673-433.527264 0-238.974504-194.424989-433.442083-433.484673-433.442082z" fill="#1296db" p-id="4074"></path><path d="M774.377906 373.645219a42.803311 42.803311 0 0 0-60.691261 0l-269.085888 269.724743-134.841076-134.628124a42.590359 42.590359 0 1 0-60.222768 60.265357l165.122822 164.86728a42.590359 42.590359 0 0 0 30.537287 12.393794c11.116084 0.085181 22.232167-4.046084 30.79283-12.564156l298.430645-299.154681a43.186624 43.186624 0 0 0-0.042591-60.904213z" fill="#1296db" p-id="4075"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785141972553" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6593" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M337.387283 341.82659c-17.757225 0-35.514451 11.83815-35.514451 29.595375s17.757225 29.595376 35.514451 29.595376 29.595376-11.83815 29.595376-29.595376c0-18.49711-11.83815-29.595376-29.595376-29.595375zM577.849711 513.479769c-11.83815 0-22.936416 12.578035-22.936416 23.6763 0 12.578035 11.83815 23.676301 22.936416 23.676301 17.757225 0 29.595376-11.83815 29.595376-23.676301s-11.83815-23.676301-29.595376-23.6763zM501.641618 401.017341c17.757225 0 29.595376-12.578035 29.595376-29.595376 0-17.757225-11.83815-29.595376-29.595376-29.595375s-35.514451 11.83815-35.51445 29.595375 17.757225 29.595376 35.51445 29.595376zM706.589595 513.479769c-11.83815 0-22.936416 12.578035-22.936416 23.6763 0 12.578035 11.83815 23.676301 22.936416 23.676301 17.757225 0 29.595376-11.83815 29.595376-23.676301s-11.83815-23.676301-29.595376-23.6763z" fill="#28C445" p-id="6594"></path><path d="M510.520231 2.959538C228.624277 2.959538 0 231.583815 0 513.479769s228.624277 510.520231 510.520231 510.520231 510.520231-228.624277 510.520231-510.520231-228.624277-510.520231-510.520231-510.520231zM413.595376 644.439306c-29.595376 0-53.271676-5.919075-81.387284-12.578034l-81.387283 41.433526 22.936416-71.768786c-58.450867-41.433526-93.965318-95.445087-93.965317-159.815029 0-113.202312 105.803468-201.988439 233.803468-201.98844 114.682081 0 216.046243 71.028902 236.023121 166.473989-7.398844-0.739884-14.797688-1.479769-22.196532-1.479769-110.982659 1.479769-198.289017 85.086705-198.289017 188.67052 0 17.017341 2.959538 33.294798 7.398844 49.572255-7.398844 0.739884-15.537572 1.479769-22.936416 1.479768z m346.265896 82.867052l17.757225 59.190752-63.630058-35.514451c-22.936416 5.919075-46.612717 11.83815-70.289017 11.83815-111.722543 0-199.768786-76.947977-199.768786-172.393063-0.739884-94.705202 87.306358-171.653179 198.289017-171.65318 105.803468 0 199.028902 77.687861 199.028902 172.393064 0 53.271676-34.774566 100.624277-81.387283 136.138728z" fill="#28C445" p-id="6595"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"popup": "/components/popup/popup"
}
}
+217
View File
@@ -0,0 +1,217 @@
/* components/banner/banner.wxss */
.banner-wrap {
width: 100%;
position: absolute;
top: 7%;
left: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
padding: 30rpx;
box-sizing: border-box;
}
.banner__avatar {
border-radius: 50%;
margin-right: 20rpx;
}
.banner__card {
border-radius: 25rpx;
padding: 18rpx;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.5);
margin-bottom: 20rpx;
display: inline-block;
}
.banner__icon {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.banner__avatar {
width: 80rpx;
height: 80rpx;
}
.banner-id {
font-size: 21rpx;
}
.banner-box {
display: flex;
align-items: center;
}
.banner__title {
max-width: 250rpx;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.banner__count {
color: #fff;
margin-top: 10rpx;
}
.popup-header {
text-align: center;
font-weight: 700;
color: #000;
font-size: 32rpx;
}
.popup-text {
color: #000;
font-size: 28rpx;
}
.report-options {
width: 100%;
margin: 24rpx 0;
}
.options-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 24rpx;
}
.option-item {
display: flex;
align-items: center;
margin-right: 30rpx;
margin-bottom: 20rpx;
}
.option-item radio {
margin-right: 10rpx;
}
.other-input {
margin: 24rpx 0;
}
.other-input-field {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
box-sizing: border-box;
}
.upload-section {
margin-bottom: 24rpx;
}
.upload-title {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.image-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
overflow: hidden;
}
.uploaded-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.delete-image {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 32rpx;
height: 32rpx;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
}
.upload-btn {
width: 160rpx;
height: 160rpx;
border: 2rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8f8f8;
}
.upload-icon {
font-size: 48rpx;
color: #999;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 24rpx;
color: #999;
}
.description-section {
margin-bottom: 32rpx;
}
.description-input {
width: 100%;
height: 160rpx;
padding: 16rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
box-sizing: border-box;
margin-bottom: 8rpx;
}
.char-count {
font-size: 24rpx;
color: #999;
text-align: right;
display: block;
}
.submit-section {
padding: 0 24rpx;
}
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #ff4d4f;
color: #fff;
font-size: 32rpx;
border-radius: 44rpx;
border: none;
}
.submit-btn:active {
background: #ff7875;
}
.submit-btn[disabled] {
background: #ccc;
color: #999;
opacity: 0.7;
}
+281
View File
@@ -0,0 +1,281 @@
import { request } from "../../utils/request";
// components/banner/banner.ts
interface BannerData {
avatarUrl: string;
nickName: string;
userId: string;
showPopup: boolean;
reportOptions: Array<{
id: number;
text: string;
checked: boolean;
}>;
showOtherInput: boolean;
otherReason: string;
uploadedImages: string[];
description: string;
state: number;
isSubmitEnabled: boolean;
}
interface BannerProperties {
roomInfo: {
iconUrl?: string;
name?: string;
mumId?: string;
state: number;
};
}
interface BannerComponent extends WechatMiniprogram.Component.TrivialInstance {
data: BannerData;
properties: BannerProperties;
}
const state: Record<number, string> = {
1: "直播中",
2: "直播结束",
};
Component({
/**
* 组件的属性列表
*/
properties: {
roomInfo: {
type: Object,
value: {
iconUrl: "",
nickname: "",
mumId: "",
state: null
},
observer: function (
this: BannerComponent,
newVal: BannerProperties["roomInfo"]
) {
this.updateRoomInfo(newVal);
},
},
watchNum:{
type:Number,
data:0
}
},
/**
* 组件的初始数据
*/
data: {
avatarUrl: "",
nickName: "",
userId: "",
state: "",
showPopup: false,
reportOptions: [
{ id: 1, text: "价格虚假", checked: false },
{ id: 2, text: "品牌虚假", checked: false },
{ id: 3, text: "色情低俗", checked: false },
{ id: 4, text: "虚假活动", checked: false },
{ id: 5, text: "不良行为", checked: false },
{ id: 6, text: "商品信息虚假", checked: false },
{ id: 7, text: "出售违禁品", checked: false },
{ id: 8, text: "其他", checked: false },
],
showOtherInput: false,
otherReason: "",
uploadedImages: [],
description: "",
isSubmitEnabled: false,
},
lifetimes: {
attached(this: BannerComponent) {
this.updateRoomInfo(this.properties.roomInfo);
},
},
/**
* 组件的方法列表
*/
methods: {
updateRoomInfo(
this: BannerComponent,
roomInfo?: BannerProperties["roomInfo"]
) {
const info = roomInfo || this.properties.roomInfo;
this.setData({
avatarUrl: info?.iconUrl || "",
nickName: info?.name || "",
userId: info?.mumId || "",
state: state[info?.state ?? 2],
});
},
handOpen(this: WechatMiniprogram.Component.TrivialInstance) {
this.setData({
showPopup: true,
isSubmitEnabled: false,
});
},
closePopup(this: WechatMiniprogram.Component.TrivialInstance) {
this.setData({
showPopup: false,
});
},
onOptionChange(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
const { value } = e.detail;
const reportOptions = this.data.reportOptions.map((item: any) => ({
...item,
checked: value.includes(item.id.toString()),
}));
const showOtherInput =
reportOptions.find((item: any) => item.id === 8)?.checked || false;
this.setData({
reportOptions,
showOtherInput,
}, () => {
this.updateSubmitEnabled();
});
},
onOtherInput(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
this.setData({
otherReason: e.detail.value,
}, () => {
this.updateSubmitEnabled();
});
},
deleteImage(
this: WechatMiniprogram.Component.TrivialInstance,
e: WechatMiniprogram.TouchEvent
) {
const { index } = e.currentTarget.dataset;
wx.showModal({
title: "提示",
content: "确定要删除这张图片吗?",
success: (res) => {
if (res.confirm) {
const uploadedImages = [...this.data.uploadedImages];
uploadedImages.splice(index, 1);
this.setData({ uploadedImages });
}
},
});
},
chooseImage(this: WechatMiniprogram.Component.TrivialInstance) {
wx.chooseImage({
count: 3 - this.data.uploadedImages.length,
success: (res) => {
this.setData({
uploadedImages: [...this.data.uploadedImages, ...res.tempFilePaths],
});
},
});
},
previewImage(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
const { src } = e.currentTarget.dataset;
wx.previewImage({
current: src,
urls: this.data.uploadedImages,
});
},
onDescriptionInput(
this: WechatMiniprogram.Component.TrivialInstance,
e: any
) {
this.setData({
description: e.detail.value,
}, () => {
this.updateSubmitEnabled();
});
},
updateSubmitEnabled(this: BannerComponent) {
const selectedOptions = this.data.reportOptions.filter(
(item: any) => item.checked
);
const hasSelectedOptions = selectedOptions.length > 0;
const isOtherSelected = selectedOptions.some((item: any) => item.id === 8);
const otherReasonValid = !isOtherSelected || (this.data.otherReason && this.data.otherReason.trim().length > 0);
const hasDescription = this.data.description && this.data.description.trim().length > 0;
this.setData({
isSubmitEnabled: hasSelectedOptions && otherReasonValid && hasDescription,
});
},
async submitReport(this: BannerComponent) {
if (!this.data.isSubmitEnabled) {
return;
}
const selectedOptions = this.data.reportOptions.filter(
(item: any) => item.checked
);
if (selectedOptions.length === 0) {
wx.showToast({
title: "请选择举报原因",
icon: "none",
});
return;
}
if (
selectedOptions.some((item: any) => item.id === 8) &&
!this.data.otherReason
) {
wx.showToast({
title: "请填写其他举报原因",
icon: "none",
});
return;
}
// 提交举报信息
const reportData = {
reasons: selectedOptions,
otherReason: this.data.otherReason,
images: this.data.uploadedImages,
description: this.data.description,
userId: this.data.userId,
roomId: wx.getStorageSync("joinedGroupId"),
};
console.log("举报信息:", reportData);
try {
await request({
options: {
url: `/app/video-live/feedback`,
data: reportData,
},
});
wx.showToast({
title: "举报成功",
icon: "success",
});
// 提交成功后关闭弹窗
this.setData({
showPopup: false,
reportOptions: this.data.reportOptions.map((item: any) => ({
...item,
checked: false,
})),
showOtherInput: false,
otherReason: "",
uploadedImages: [],
description: "",
isSubmitEnabled: false,
});
} catch (error) {
this.handleError(error);
}
},
},
});
+83
View File
@@ -0,0 +1,83 @@
<!--components/banner/banner.wxml-->
<view class="banner-wrap">
<view>
<view class="banner__card banner-box">
<image class="banner__avatar" src="{{avatarUrl}}" mode="aspectFill" />
<view>
<view class="banner__title text-overflow">{{nickName}}</view>
<view class="banner-box banner__count">
<image class="banner__icon" src="https://img.alicdn.com/imgextra/i1/O1CN01GC0HVZ1kWz8bvu0KX_!!6000000004692-2-tps-36-36.png" mode="widthFix" />
<text>{{watchNum}}</text>
</view>
</view>
</view>
<view class="banner__card">{{state}}</view>
</view>
<view>
<view class="banner__card banner-box" bind:tap="handOpen">
<image class="banner__icon" src="/assets/img/p.svg" mode="widthFix"/>
<text>投诉</text>
</view>
<view class="banner-id">用户ID:{{userId}}</view>
</view>
</view>
<popup visible="{{showPopup}}" bind:close="closePopup">
<view slot="header" class="popup-header">
<text>举报本场直播</text>
</view>
<view class="popup-content">
<radio-group class="report-options" bindchange="onOptionChange">
<view class="options-row">
<view class="option-item" wx:for="{{reportOptions}}" wx:for-item="item" wx:key="id">
<radio value="{{item.id}}" checked="{{item.checked}}"/>
<text class="popup-text">{{item.text}}</text>
</view>
</view>
</radio-group>
<view class="other-input" wx:if="{{showOtherInput}}">
<input
class="other-input-field popup-text"
type="text"
placeholder="请输入其他举报原因"
value="{{otherReason}}"
bindinput="onOtherInput"
/>
</view>
<view class="upload-section">
<view class="upload-title popup-text">上传图片证据(最多3张)</view>
<view class="image-list">
<block wx:for="{{uploadedImages}}" wx:key="*this">
<view class="image-wrapper">
<image class="uploaded-image" src="{{item}}" mode="aspectFill" bindtap="previewImage" data-src="{{item}}"/>
<view class="delete-image" catchtap="deleteImage" data-index="{{index}}">×</view>
</view>
</block>
<view class="upload-btn" wx:if="{{uploadedImages.length < 3}}" bindtap="chooseImage">
<view class="upload-icon">+</view>
<text class="upload-text">上传图片</text>
</view>
</view>
</view>
<view class="description-section">
<textarea
class="description-input popup-text"
placeholder="请补充说明(选填)"
value="{{description}}"
bindinput="onDescriptionInput"
maxlength="200"
/>
<text class="char-count popup-text">{{description.length}}/200</text>
</view>
<view class="submit-section">
<button class="submit-btn" disabled="{{!isSubmitEnabled}}" bindtap="submitReport">提交举报</button>
</view>
</view>
</popup>
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1,32 @@
/* components/collapse/collapse.wxss */
.collapse {
border-radius: 16rpx;
overflow: hidden;
}
.collapse-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
font-weight: 700;
}
.arrow {
transition: transform 0.3s;
}
.arrow.opened {
transform: rotate(180deg);
}
.collapse-content {
overflow: hidden;
transition: max-height 0.3s ease;
}
.content-inner {
padding: 24rpx;
color: #333;
background: #ffddcb;
}
@@ -0,0 +1,64 @@
// components/collapse/collapse.ts
export { }
Component({
properties: {
title: {
type: String,
value: ''
},
titleStyle: {
type: String,
value: ''
},
defaultOpen: {
type: Boolean,
value: false
},
disableOpen: {
type: Boolean,
value: false
},
offsetHeight: {
type: Number,
value: 0
}
},
data: {
open: false,
contentHeight: 0
},
lifetimes: {
ready(this: WechatMiniprogram.Component.TrivialInstance) {
this.setData({ open: this.data.defaultOpen })
this.calcHeight()
}
},
methods: {
toggle(this: WechatMiniprogram.Component.TrivialInstance) {
if (this.data.disableOpen) return;
const open = !this.data.open;
this.setData({ open }, () => {
if (open) {
// 等展开后再算高度
this.calcHeight();
}
});
},
calcHeight(this: WechatMiniprogram.Component.TrivialInstance) {
const query = this.createSelectorQuery()
query.select('#content').boundingClientRect()
query.exec(res => {
if (res && res[0]) {
this.setData({
contentHeight: res[0].height
})
}
})
}
}
})
@@ -0,0 +1,17 @@
<view class="collapse">
<!-- header -->
<view class="collapse-header" bind:tap="toggle">
<view class="title">
<view class="title-text" style="{{ titleStyle }}">
{{ title }}
</view>
</view>
<view class="arrow {{ open ? 'open' : '' }}">▼</view>
</view>
<view class="collapse-content" style="max-height: {{ open ? (contentHeight+offsetHeight) + 'px' : '0rpx' }}">
<view class="content-inner" id="content">
<slot />
</view>
</view>
</view>
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+83
View File
@@ -0,0 +1,83 @@
/* components/empty/empty.wxss */
@import "../../variables";
.empty {
width: 100%;
position: relative;
box-sizing: border-box;
.content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
position: relative;
z-index: 1;
}
.icon {
width: 200rpx;
height: 200rpx;
image {
width: 100%;
height: 100%;
}
}
.title {
color: #333333;
font-size: 32rpx;
line-height: 44rpx;
margin-top: 28rpx;
}
.description {
color: #999999;
font-size: 28rpx;
line-height: 40rpx;
margin-top: 8rpx;
text-align: center;
}
.action {
height: 54rpx;
line-height: 54rpx;
border-radius: 46rpx;
min-width: 140rpx;
padding: 0 24rpx;
text-align: center;
color: #ffffff;
font-size: 28rpx;
margin-top: 108rpx;
background: linear-gradient(139deg, #ff9a33 0%, #ff7200 100%);
}
}
.empty-legacy {
height: 60vh;
color: $gray-color;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.empty-scene {
min-height: 1220rpx;
padding-top: 400rpx;
overflow: hidden;
background: #f5f5f5;
.scene-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 982rpx;
border-bottom-left-radius: 28rpx;
border-bottom-right-radius: 28rpx;
// background: linear-gradient(180deg, #ffd5af 0%, rgba(255, 255, 255, 0.2) 100%);
}
}
+135
View File
@@ -0,0 +1,135 @@
// components/empty/empty.ts
type SceneConfig = {
title: string;
description: string;
icon: string;
buttonText?: string;
action?: string;
imageStyle?: string;
};
const SCENE_MAP: Record<string, SceneConfig> = {
loadFail: {
title: "加载失败",
description: "请点击刷新重新加载试试~",
icon: "./assets/load-failed.png",
buttonText: "刷新",
action: "refresh",
imageStyle: "width: 328rpx; height: 392rpx;"
},
noDynamic: {
title: "暂无动态",
description: "这个地方空空如也~",
icon: "./assets/no-dynamic.png",
imageStyle: "width: 260rpx; height: 378rpx;"
},
noMessage: {
title: "暂无消息",
description: "暂无新的消息",
icon: "./assets/no-message.png",
imageStyle: "width: 344rpx; height: 376rpx;"
},
noAddress: {
title: "暂无地址",
description: "快去添加一个吧~",
icon: "./assets/no-address.png",
buttonText: "添加地址",
action: "addAddress",
imageStyle: "width: 264rpx; height: 392rpx;"
},
noArchive: {
title: "暂无档案信息",
description: "去添加宝贝的成长档案吧~",
icon: "./assets/no-archive.png",
imageStyle: "width: 388rpx; height: 408rpx;"
}
};
const DEFAULT_TITLE = "什么都没搜到";
const DEFAULT_ICON = "../../assets/img/empty.svg";
Component({
properties: {
scene: {
type: String,
value: "",
observer: "syncScene"
},
title: {
type: String,
value: DEFAULT_TITLE,
observer: "syncScene"
},
description: {
type: String,
value: "",
observer: "syncScene"
},
icon: {
type: String,
value: DEFAULT_ICON,
observer: "syncScene"
},
buttonText: {
type: String,
value: "",
observer: "syncScene"
},
emptyStyle: {
type: String,
value: ""
},
imageStyle: {
type: String,
value: "",
observer: "syncScene"
}
},
data: {
sceneMode: false,
resolvedTitle: "",
resolvedDescription: "",
resolvedIcon: "",
resolvedButtonText: "",
resolvedAction: "",
resolvedImageStyle: ""
},
lifetimes: {
attached() {
(this as any).syncScene();
}
},
methods: {
syncScene() {
const that = this as any;
const scene = (that.data.scene || "").trim();
const sceneConfig = scene ? SCENE_MAP[scene] : undefined;
const title = that.data.title || "";
const description = that.data.description || "";
const icon = that.data.icon || "";
const buttonText = that.data.buttonText || "";
const imageStyle = that.data.imageStyle || "";
that.setData({
sceneMode: !!sceneConfig,
resolvedTitle: sceneConfig && title === DEFAULT_TITLE ? sceneConfig.title : (title || sceneConfig?.title || DEFAULT_TITLE),
resolvedDescription: sceneConfig && !description ? sceneConfig.description : description,
resolvedIcon: sceneConfig && icon === DEFAULT_ICON ? sceneConfig.icon : (icon || sceneConfig?.icon || DEFAULT_ICON),
resolvedButtonText: sceneConfig && !buttonText ? (sceneConfig.buttonText || "") : buttonText,
resolvedAction: sceneConfig?.action || "",
resolvedImageStyle: sceneConfig && !imageStyle ? (sceneConfig.imageStyle || "") : imageStyle
});
},
handleAction() {
const that = this as any;
that.triggerEvent("action", {
scene: that.data.scene,
action: that.data.resolvedAction
});
}
}
});
+12
View File
@@ -0,0 +1,12 @@
<!--components/empty/empty.wxml-->
<view class="empty {{sceneMode ? 'empty-scene' : 'empty-legacy'}}" style="{{emptyStyle}}">
<view wx:if="{{sceneMode}}" class="scene-bg"></view>
<view class="content">
<view class="icon" style="{{resolvedImageStyle}}">
<image src="{{resolvedIcon}}" mode="aspectFit"></image>
</view>
<view class="title">{{resolvedTitle}}</view>
<view class="description" wx:if="{{resolvedDescription}}">{{resolvedDescription}}</view>
<view class="action" wx:if="{{resolvedButtonText}}" bind:tap="handleAction">{{resolvedButtonText}}</view>
</view>
</view>
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+10
View File
@@ -0,0 +1,10 @@
/* components/end/end.wxss */
.end {
display: flex;
justify-content: center;
align-items: center;
cursor: none;
color: #999;
margin: 0;
font-size: 26rpx;
}
+24
View File
@@ -0,0 +1,24 @@
// components/end/end.ts
Component({
/**
* 组件的属性列表
*/
properties: {
},
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
}
})
+2
View File
@@ -0,0 +1,2 @@
<!--components/end/end.wxml-->
<text class="end">—— · ——</text>
@@ -0,0 +1,9 @@
{
"component": true,
"usingComponents": {
"player": "/components/player/player",
"banner": "/components/banner/banner",
"comment": "/components/static/comment/comment",
"product-popup": "/components/static/product-popup/product-popup"
}
}
@@ -0,0 +1,94 @@
/* components/liveroom/liveroom.wxss */
.liveroom-wrap {
position: relative;
width: 100%;
height: 100%;
color: #fcfcfd;
background-image: linear-gradient(90deg, #2f1d17 0%, #1e2326 100%);
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.countdown {
margin: 30rpx 0;
}
.countdown-text {
color: white;
font-size: 28rpx;
}
.countdown-time {
color: white;
font-size: 36rpx;
font-weight: bold;
}
.enter-room-btn {
background: #ff6b6b;
width: 400rpx;
height: 80rpx;
line-height: 80rpx;
color: #fff;
font-size: 32rpx;
border-radius: 40rpx;
border: none;
}
.live-ended {
position: fixed;
top: 35%;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
}
.ended-content {
border-radius: 16rpx;
padding: 48rpx;
text-align: center;
width: 600rpx;
}
.ended-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 24rpx;
}
.ended-text {
font-size: 36rpx;
color: #fff;
font-weight: 700;
display: block;
margin-bottom: 16rpx;
}
.ended-time {
font-size: 28rpx;
color: #999;
display: block;
margin-bottom: 48rpx;
}
.replay-btn {
width: 400rpx;
height: 80rpx;
line-height: 80rpx;
background: #ff6b35;
color: #fff;
font-size: 32rpx;
border-radius: 40rpx;
border: none;
}
.replay-btn:active {
background: #ff8555;
}
+216
View File
@@ -0,0 +1,216 @@
import { request } from "../../utils/request";
import { appId } from "../../env";
import {
join,
leave,
addCustomEventListener,
removeCustomEventListener,
} from "../../utils/server-sent-events";
interface LiveroomData {
isPlayback: boolean;
pluginVisible: boolean;
roomInfo: any;
pullUrlInfo: any;
vodInfo: any;
watchNum: number;
productNum: number;
chatEnhancementApproved: boolean;
}
interface LiveroomProperties {
joinedGroupId: string;
}
interface LiveroomComponent
extends WechatMiniprogram.Component.TrivialInstance {
data: LiveroomData;
properties: LiveroomProperties;
_listener?: (data: any) => void;
onLiveMessage: (data: any) => void;
init: () => Promise<void>;
cleanup: () => void;
handleError: (error?: unknown) => void;
setChatEnhancementApproval: (approved: boolean) => void;
}
const statusId = "roomId-1";
// components/liveroom/liveroom.ts
Component({
/**
* 组件的属性列表
*/
properties: {
joinedGroupId: {
type: String,
value: "",
observer: function (
this: LiveroomComponent,
newVal: string,
oldVal: string,
) {
if (newVal && newVal !== oldVal) {
this.init();
}
},
},
},
lifetimes: {
attached(this: LiveroomComponent) {},
detached(this: LiveroomComponent) {
this.cleanup();
},
},
pageLifetimes: {
show(this: LiveroomComponent) {
this._listener = this.onLiveMessage.bind(this);
addCustomEventListener("pushMsg", this._listener);
join(statusId);
},
hide() {
// @ts-ignore
removeCustomEventListener("pushMsg", this._listener);
leave(statusId);
},
},
/**
* 组件的初始数据
*/
data: {
isPlayback: false,
pluginVisible: true,
roomInfo: null,
watchNum: 0,
productNum: 0,
durationInterval: null,
},
/**
* 组件的方法列表
*/
methods: {
onLiveMessage(data: any) {
console.log("收到通知:", data);
const that = this as any;
if (data.type == "watchNum") {
that.setData({ watchNum: data.count });
}
if (data.type == "productNum") {
that.setData({ productNum: data.count });
}
},
updateIsPlayback(this: any, bool: boolean) {
this.isPlayback = bool;
},
controlstoggle(this: LiveroomComponent, e: WechatMiniprogram.CustomEvent) {
const { state } = e.detail;
if (state && this.data.roomInfo) {
this.setData({
roomInfo: {
...this.data.roomInfo,
state,
},
});
}
},
async init(this: LiveroomComponent) {
const app = getApp<IAppOption>();
if (!app.globalData.isLogin) return;
const roomId = this.properties.joinedGroupId;
const params = {
roomId: roomId,
appId: appId,
openId: wx.getStorageSync("openId"),
};
try {
const data = await request({
options: {
url: `/app/video-live/details`,
data: params,
method: "GET",
},
});
const bool = this.compareTime(data.data.onlineDatetime);
console.log(bool, "12313");
this.setData({
roomInfo: data.data,
pullUrlInfo: data.data,
vodInfo: data.data,
bool,
});
} catch (error) {
this.handleError(error);
}
},
handleError(this: LiveroomComponent) {
wx.showToast({
title: "获取直播信息失败",
icon: "none",
duration: 2000,
});
},
cleanup(this: LiveroomComponent) {
this.setData({
isPlayback: false,
pluginVisible: true,
roomInfo: null,
watchNum: 0,
productNum: 0,
chatEnhancementApproved: false,
});
},
compareTime(targetTimeStr: string) {
const targetTime = new Date(targetTimeStr);
const currentTime = new Date();
return targetTime > currentTime;
},
startDurationTimer(this: LiveroomComponent) {
this.durationInterval = setInterval(() => {
const now = new Date().getTime();
const startTime = new Date(this.data.roomInfo.onlineDatetime).getTime();
if (now < startTime) {
const remainingTime = Math.floor((startTime - now) / 1000);
const days = Math.floor(remainingTime / (24 * 3600));
const hours = Math.floor((remainingTime % (24 * 3600)) / 3600);
const minutes = Math.floor((remainingTime % 3600) / 60);
const seconds = remainingTime % 60;
this.setData({
isLiveStarted: false,
countdown:
days > 0
? `${days}${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
: `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
});
} else {
const duration = Math.floor((now - startTime) / 1000);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = duration % 60;
this.setData({
isLiveStarted: true,
duration: `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
});
}
}, 1000);
},
stopDurationTimer(this: LiveroomComponent) {
if (this.durationInterval) {
clearInterval(this.durationInterval);
this.durationInterval = null;
}
},
},
});
@@ -0,0 +1,39 @@
<!--components/liveroom/liveroom.wxml-->
<view class="liveroom-wrap">
<banner room-info="{{roomInfo}}" watchNum="{{watchNum}}"/>
<!-- 直播未开始提示
<view class="live-ended" wx:if="{{bool}}" >
<view class="ended-content">
<text class="ended-text">直播未开始</text>
<view class="countdown">
<text class="countdown-text">距离开始还有:</text>
<text class="countdown-time">{{roomInfo.onlineDatetime}}</text>
</view>
<button class="enter-room-btn" bind:tap="enterRoom">进入房间</button>
</view>
</view> -->
<!-- 直播结束提示 -->
<view class="live-ended" wx:if="{{roomInfo.state == 2}}">
<view class="ended-content">
<text class="ended-text">直播已结束</text>
<text class="ended-time">结束时间:{{roomInfo.offlineDatetime}}</text>
<button class="replay-btn">观看回放</button>
</view>
</view>
<banner room-info="{{roomInfo}}"/>
<player
is-playback="{{isPlayback}}"
is-living="{{isLiving}}"
pull-url-info="{{pullUrlInfo}}"
vod-info="{{vodInfo}}"
bind:controlstoggle="controlstoggle"
/>
<comment wx:if="{{roomInfo.chat}}"/>
<product-popup joinedGroupId="{{joinedGroupId}}" productNum="{{productNum}}"/>
</view>
+33
View File
@@ -0,0 +1,33 @@
export type LoginRes = {
data: {
code?: number;
appId: string;
id: number;
mpId: number;
openId: number;
mpUserId: number;
unionId: string;
nickName: string;
avatarUrl: string;
msg?: string,
}
statusCode: number,
msg?: string,
}
export type userInfoRes = {
openId: string,
mbuId: number,
nickName: string,
gender: number,
city: string,
province: string,
country: string,
avatarUrl: string,
unionId: string,
watermark: {
appid: string,
timestamp: string
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+48
View File
@@ -0,0 +1,48 @@
/* components/login/login.wxss */
@import "../../variables";
.loginContainer {
position: absolute;
top: 0;
left: 0;
z-index: 999;
width: 750rpx;
height: 100%;
background-color: rgba($color: #000000, $alpha: .3);
backdrop-filter: blur(12rpx);
display: flex;
justify-content: center;
align-items: center;
.contain {
width: 100%;
height: 25vh;
background-color: #ffffff;
border-radius: 20rpx 20rpx 0 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: fixed;
bottom: 0;
left: 0;
text {
font-size: 35rpx;
width: 80%;
line-height: 50rpx;
margin-bottom: 50rpx;
}
}
.login {
width: 60%;
height: 80rpx;
background: $primary-color-blue;
color: white;
font-size: 40rpx;
font-weight: 800;
line-height: 80rpx;
text-align: center;
border-radius: $primary-border-radius-20;
}
}
+73
View File
@@ -0,0 +1,73 @@
// components/login/login.ts
import { BASE_URL, appId } from "../../env";
import { LoginRes } from "./login.d";
import { wxGetUserInfo, wxLogin, wxRequest } from "../../utils/wx-api";
interface LoginComponent {
data: {
mbuId: number;
userName: string;
};
setData: (data: Partial<{ mbuId: number; userName: string }>) => void;
triggerEvent: (name: string, detail?: any) => void;
handleLogin: () => Promise<void>;
}
const app = getApp<IAppOption>();
Component({
data: {
mbuId: wx.getStorageSync("mbuId"),
userName: wx.getStorageSync("userName"),
},
methods: {
async handleLogin(this: LoginComponent) {
try {
wx.showLoading({ title: "登录中" });
const loginRes = await wxLogin();
const infoRes = await wxGetUserInfo();
if (!loginRes.code) throw new Error(loginRes.errMsg);
const loginResult = await wxRequest<LoginRes>({
url: `${BASE_URL}/auth/app-login?appId=${appId}&code=${loginRes.code}`,
method: "POST",
data: {
...infoRes.userInfo,
},
});
if (
loginResult?.statusCode !== 200 ||
(loginResult.data.code && loginResult.data.code !== 200)
) {
throw new Error(loginResult?.msg || "登录失败");
}
const { mpUserId, id, openId, unionId, nickName } = loginResult.data;
wx.setStorageSync("openId", openId);
wx.setStorageSync("userId", mpUserId);
wx.setStorageSync("mbuId", id);
wx.setStorageSync("unionId", unionId);
wx.setStorageSync("userName", nickName);
wx.setStorageSync("avatarUrl", infoRes.userInfo.avatarUrl);
this.setData({ mbuId: id, userName: nickName });
app.globalData.isLogin = true;
this.triggerEvent("loginSuccess", {
userName: nickName,
avatarUrl: infoRes.userInfo.avatarUrl,
});
console.log("登录成功");
} catch (err: any) {
console.log("登录失败:", err);
app.globalData.isLogin = false;
wx.showToast({
title: `登录失败:${err.message || err.errMsg}`,
icon: "none",
});
} finally {
wx.hideLoading();
}
},
},
});
+7
View File
@@ -0,0 +1,7 @@
<!--components/login/login.wxml-->
<view class="loginContainer" wx:if="{{!userName||!mbuId}}" catch:touchmove="emptyEvent">
<view class="contain">
<text>观看请点击学习课程,如不想观看请点击右上角圆圈“○”,退出小程序</text>
<view class="login" bind:tap="handleLogin">学习课程</view>
</view>
</view>
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+109
View File
@@ -0,0 +1,109 @@
/* 容器样式 */
.quiz-container {
padding: 40rpx 30rpx 230rpx;
box-sizing: border-box;
}
/* 题目卡片 */
.question-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
/* 题目标题 */
.question-title {
margin-bottom: 30rpx;
word-wrap: break-word;
white-space: normal;
}
.tag {
display: inline;
font-size: 28rpx;
margin-right: 16rpx;
border-radius: 6rpx;
font-size: 36rpx;
color: #04befe
}
.question-text {
font-size: 32rpx;
color: #333;
flex: 1;
width: 100%;
margin-top: 8rpx;
}
/* 选项区域 */
.options {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 选项项 */
.option-item {
display: flex;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
background: #f8f8f8;
transition: all 0.3s ease;
}
.option-item:active {
transform: scale(0.98);
}
/* 选项文本 */
.option-item text {
margin-left: 16rpx;
font-size: 28rpx;
color: #333;
flex: 1;
}
/* 正确答案样式 */
.option-item.correct {
background: #f6ffed;
border: 2rpx solid #b7eb8f;
color: #52c41a;
}
.option-item.correct text {
color: #52c41a;
font-weight: 500;
}
/* 错误答案样式 */
.option-item.wrong {
background: #fff2f0;
border: 2rpx solid #ffccc7;
color: #ff4d4f;
}
.option-item.wrong text {
color: #ff4d4f;
font-weight: 500;
}
/* 选中状态样式 - 使用微信小程序兼容的方式 */
.option-item radio:checked + text,
.option-item checkbox:checked + text {
color: #1890ff;
font-weight: 500;
}
/* 由于微信小程序不支持:has()选择器,我们通过JS动态添加class来实现选中状态样式 */
.option-item.selected {
background: #e6f7ff;
// border: 2rpx solid #91d5ff;
}
.option-item.selected text {
color: #1890ff;
font-weight: 500;
}
+216
View File
@@ -0,0 +1,216 @@
// 定义接口类型
interface Question {
id: number;
type: number;
question: string;
option: Record<string, any>;
}
interface AnswerItem {
queId: string;
ans: string[];
suc?: boolean;
}
interface QuizComponent {
properties: {
questions: Question[];
answerList: AnswerItem[];
};
data: {
selectedAnswers: Record<number, string[]>;
renderQuestions: Array<
Question & { options: Array<{ key: string; value: string }> }
>;
};
setData: (data: Partial<Record<string, any>>, callback?: () => void) => void;
triggerEvent: (name: string, data?: any) => void;
isSelected: (qid: string | number, key: string) => boolean;
getSelectedAnswer: (qid: string | number, index: number) => string;
isCorrect: (qid: string, key: string) => boolean;
isWrong: (qid: string, key: string) => boolean;
handleRadioChange: (e: WechatMiniprogram.RadioGroupChange) => void;
handleCheckboxChange: (e: WechatMiniprogram.CheckboxGroupChange) => void;
getFormattedAnswers: () => Array<{
questionId: number;
answers: string[];
questionType: number;
}>;
getAllAnswers: () => Array<{
questionId: number;
answers: string[];
questionType: number;
}>;
isAllAnswered: () => boolean;
getProgress: () => { answered: number; total: number; percentage: number };
}
// 定义常量
const QUESTION_TYPE = {
SINGLE: 1,
MULTIPLE: 2,
};
Component({
properties: {
questions: {
type: Array,
value: [],
},
answerList: {
type: Array,
value: [],
},
},
observers: {
"questions, answerList": function (this: QuizComponent) {
const renderQuestions = (this.properties.questions || []).map(
(q: Question) => ({
...q,
options: Object.keys(q.option || {}).map((k) => ({
key: k,
value: q.option[k],
})),
})
);
const answerList = Array.isArray(this.properties.answerList)
? this.properties.answerList
: [];
const selectedAnswers = answerList.reduce((acc, item) => {
if (item && item.queId && item.ans) {
acc[Number(item.queId)] = Array.isArray(item.ans)
? item.ans
: [item.ans];
}
return acc;
}, {} as Record<number, string[]>);
this.setData({
renderQuestions,
selectedAnswers,
});
},
},
data: {
selectedAnswers: {} as Record<number, string[]>,
renderQuestions: [] as Array<
Question & { options: Array<{ key: string; value: string }> }
>,
},
methods: {
isSelected(this: QuizComponent, qid: string | number, key: string) {
const selected = this.data.selectedAnswers[Number(qid)];
return Array.isArray(selected) && selected.includes(key);
},
getSelectedAnswer(this: QuizComponent, qid: string | number, index: number) {
const selected = this.data.selectedAnswers[Number(qid)];
return Array.isArray(selected) ? selected[index] : "";
},
isCorrect(this: QuizComponent, qid: string, key: string) {
const answerList = Array.isArray(this.properties.answerList)
? this.properties.answerList
: [];
return answerList.some((item: AnswerItem) => {
const answers = Array.isArray(item.ans) ? item.ans : [];
return String(item.queId) === String(qid) && answers.includes(key) && item.suc;
});
},
isWrong(this: QuizComponent, qid: string, key: string) {
const answerList = Array.isArray(this.properties.answerList)
? this.properties.answerList
: [];
return answerList.some((item: AnswerItem) => {
const answers = Array.isArray(item.ans) ? item.ans : [];
return String(item.queId) === String(qid) && answers.includes(key) && !item.suc;
});
},
// 处理单选变化 { queId: q.id, ans: selectedAnswers }
handleRadioChange(
this: QuizComponent,
e: WechatMiniprogram.RadioGroupChange
) {
const { questionId } = e.currentTarget.dataset;
const value = e.detail.value;
if (!questionId) return;
const numericQuestionId = Number(questionId);
const newAnswers = value ? [value] : [];
this.setData({
[`selectedAnswers.${numericQuestionId}`]: newAnswers,
});
this.triggerEvent("change", {
queId: numericQuestionId,
ans: newAnswers,
});
},
// 处理多选变化
handleCheckboxChange(
this: QuizComponent,
e: WechatMiniprogram.CheckboxGroupChange
) {
const { questionId } = e.currentTarget.dataset;
const values = Array.isArray(e.detail.value) ? e.detail.value : [];
if (!questionId) return;
const numericQuestionId = Number(questionId);
this.setData({
[`selectedAnswers.${numericQuestionId}`]: values,
});
this.triggerEvent("change", {
queId: numericQuestionId,
ans: values,
});
},
getFormattedAnswers(this: QuizComponent) {
const answers = [];
for (const [questionId, selectedOptions] of Object.entries(
this.data.selectedAnswers
)) {
if (selectedOptions && selectedOptions.length > 0) {
answers.push({
queId: Number(questionId),
ans: selectedOptions,
});
}
}
return answers;
},
getAllAnswers(this: QuizComponent) {
return this.getFormattedAnswers();
},
isAllAnswered(this: QuizComponent) {
const totalQuestions = this.properties.questions.length;
const answeredQuestions = Object.values(this.data.selectedAnswers).filter(
(answers) => answers && answers.length > 0
).length;
return totalQuestions === answeredQuestions;
},
getProgress(this: QuizComponent) {
const totalQuestions = this.data.renderQuestions.length;
const answeredQuestions = Object.values(this.data.selectedAnswers).filter(
(answers) => answers && answers.length > 0
).length;
return {
answered: answeredQuestions,
total: totalQuestions,
percentage:
totalQuestions > 0
? Math.round((answeredQuestions / totalQuestions) * 100)
: 0,
};
},
},
});
@@ -0,0 +1,50 @@
<!--components/my-quiz/my-quiz.wxml-->
<view class="quiz-container">
<block wx:for="{{renderQuestions}}" wx:key="id" wx:for-item="q" wx:for-index="qi">
<view class="question-card">
<view class="question-title">
<text class="tag">{{qi + 1}}.</text>
<text class="tag" wx:if="{{q.type == 2}}">[ 多选题 ]</text>
<text class="tag" wx:else>[ 单选题 ]</text>
{{q.question}}
</view>
<view class="options">
<!-- 单选题使用 radio-group -->
<radio-group
wx:if="{{q.type == 1}}"
bindchange="handleRadioChange"
data-question-id="{{q.id}}"
value="{{getSelectedAnswer(q.id, 0)}}"
>
<block wx:for="{{q.options}}" wx:key="key" wx:for-item="op" wx:if="{{op.value}}">
<label class="option-item {{isCorrect(q.id, op.key) ? 'correct' : isWrong(q.id, op.key) ? 'wrong' : ''}} {{getSelectedAnswer(q.id, 0) === op.key ? 'selected' : ''}}">
<radio
value="{{op.key}}"
checked="{{getSelectedAnswer(q.id, 0) === op.key}}"
/>
<text>{{op.key}}. {{op.value}}</text>
</label>
</block>
</radio-group>
<!-- 多选题使用 checkbox-group -->
<checkbox-group
wx:if="{{q.type == 2}}"
bindchange="handleCheckboxChange"
data-question-id="{{q.id}}"
value="{{selectedAnswers[q.id] || []}}"
>
<block wx:for="{{q.options}}" wx:key="key" wx:for-item="op" wx:if="{{op.value}}">
<label class="option-item {{isCorrect(q.id, op.key) ? 'correct' : isWrong(q.id, op.key) ? 'wrong' : ''}} {{isSelected(q.id, op.key) ? 'selected' : ''}}">
<checkbox
value="{{op.key}}"
checked="{{isSelected(q.id, op.key)}}"
/>
<text>{{op.key}}. {{op.value}}</text>
</label>
</block>
</checkbox-group>
</view>
</view>
</block>
</view>
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1,83 @@
/* components/my-video.wxss */
.live-content {
max-width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
z-index: 1000;
}
.video {
width: 100%;
min-height: 400rpx; /* 200px -> 400rpx */
background-color: #eeeeee;
}
.screen {
.video {
height: 100%;
position: absolute;
z-index: -1;
}
}
.tools {
position: absolute;
display: flex;
width: 100%;
height: 100%;
bottom: 0;
justify-content: space-between;
align-items: flex-end;
color: white;
transition: opacity 0.3s ease;
}
.video-poster {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
z-index: 2;
.play-button {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.7);
.play-icon {
font-size: 50rpx;
color:white;
margin-left: 6rpx;
margin-bottom: 3rpx;
}
}
}
.poster-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.cover {
position: absolute;
bottom: 0;
z-index: 998;
height: 20%;
width: 70%;
left: 10%;
background-color: rgba(255, 255, 255, 0.007);
}
+262
View File
@@ -0,0 +1,262 @@
// components/my-video/index.ts
import { AES_KEY, AES_IV } from "../../env";
import { request } from "../../utils/request";
import { aesEncrypt } from "../../utils/crypto";
interface VideoData {
screen: boolean;
playing: boolean;
currentTime: number;
isReady: boolean;
sendDurationTimer: number | null;
lastUpdateTime: number;
isConnected: boolean; // 新增网络连接状态字段
}
interface MyVideoComponent {
data: VideoData;
videoContext: WechatMiniprogram.VideoContext | null;
pauseVideoHandler?: () => void;
resumeVideoHandler?: () => void;
setData: (d: Partial<VideoData>, cb?: () => void) => void;
initNetworkListener: () => void;
startSendProgress: () => void;
clearSendProgressTimer: () => void;
sendProgress: () => Promise<void>;
}
Component({
properties: {
src: String,
backgroundUrl: String,
},
data: {
screen: false,
playing: false,
currentTime: 0,
isReady: false,
isConnected: false,
sendDurationTimer: 0,
lastUpdateTime: 0,
} as VideoData,
lifetimes: {
attached(this: MyVideoComponent) {
this.initNetworkListener();
// 获取 videoContext 并初始化
this.videoContext = wx.createVideoContext("videoPlayer", this as any);
const pauseVideo = () => {
if (this.data.playing) {
wx.setStorageSync("interruptedTime", this.data.currentTime);
this.videoContext?.pause();
this.setData({ playing: false });
this.clearSendProgressTimer();
}
};
const resumeVideo = () => {
if (!this.videoContext) {
this.videoContext = wx.createVideoContext("videoPlayer", this as any);
}
this.videoContext.pause();
};
this.pauseVideoHandler = pauseVideo;
this.resumeVideoHandler = resumeVideo;
wx.onAudioInterruptionBegin(this.pauseVideoHandler);
wx.onAudioInterruptionEnd(this.resumeVideoHandler);
wx.onAppHide(this.pauseVideoHandler);
wx.onAppShow(this.resumeVideoHandler);
},
detached(this: MyVideoComponent) {
if (this.pauseVideoHandler) {
wx.offAudioInterruptionBegin(this.pauseVideoHandler);
wx.offAppHide(this.pauseVideoHandler);
}
if (this.resumeVideoHandler) {
wx.offAudioInterruptionEnd(this.resumeVideoHandler);
wx.offAppShow(this.resumeVideoHandler);
}
this.clearSendProgressTimer();
this.videoContext = null;
},
},
methods: {
initNetworkListener(this: MyVideoComponent) {
// 首先获取当前网络状态
wx.getNetworkType({
success: (res) => {
this.setData({
isConnected: res.networkType !== "none",
});
},
});
wx.onNetworkStatusChange(
(res: WechatMiniprogram.OnNetworkStatusChangeCallbackResult) => {
this.setData({
isConnected: res.isConnected,
});
if (!res.isConnected && this.data.playing) {
this.videoContext?.pause();
this.setData({ playing: false });
wx.showToast({
title: "网络连接已断开",
icon: "none",
duration: 2000,
});
}
if (res.isConnected) {
wx.showToast({
title: "网络已连接",
icon: "success",
duration: 1500,
});
}
}
);
},
handleLoadedMetadata(this: MyVideoComponent) {
this.setData({ isReady: true });
},
handlePosterTap(this: MyVideoComponent) {
if (!this.videoContext || !this.data.isReady) return;
if (!this.data.isConnected) {
wx.showToast({
title: "网络连接异常,请检查网络设置",
icon: "none",
duration: 2000,
});
return;
}
const newPlaying = !this.data.playing;
const resumeTime =
(wx.getStorageSync("interruptedTime") as number) ||
this.data.currentTime ||
(wx.getStorageSync("currentTime") as number) ||
0;
if (newPlaying) {
if (resumeTime > 0) {
this.videoContext.seek(resumeTime);
}
this.videoContext.play();
if (!this.data.sendDurationTimer) {
this.startSendProgress();
}
} else {
this.videoContext.pause();
this.clearSendProgressTimer();
}
this.setData({ playing: newPlaying });
wx.removeStorageSync("interruptedTime");
},
handlePause(this: MyVideoComponent) {
wx.setStorageSync("interruptedTime", this.data.currentTime);
this.setData({ playing: false });
this.clearSendProgressTimer();
},
handleTimeUpdate(
this: MyVideoComponent,
e: WechatMiniprogram.VideoTimeUpdate
) {
const now = Date.now();
const currentTime = Math.floor(e.detail.currentTime ?? 0);
if (now - this.data.lastUpdateTime >= 1000) {
this.setData({ currentTime });
(this.data as VideoData).lastUpdateTime = now;
wx.setStorageSync("currentTime", currentTime);
}
},
handleEnded(this: MyVideoComponent) {
this.sendProgress();
wx.setStorageSync("ended", true);
this.setData({ playing: false, currentTime: 0 });
wx.removeStorageSync("currentTime");
wx.removeStorageSync("interruptedTime");
this.clearSendProgressTimer();
},
handleFullscreenChange(this: MyVideoComponent) {
const newScreen = !this.data.screen;
if (newScreen) {
this.videoContext?.requestFullScreen({ direction: 90 });
} else {
this.videoContext?.exitFullScreen();
}
this.setData({ screen: newScreen });
},
handleError(this: MyVideoComponent, e: WechatMiniprogram.VideoError) {
console.error("播放错误:", e.detail.errMsg);
this.setData({ playing: false });
},
startSendProgress(this: MyVideoComponent) {
this.clearSendProgressTimer();
const timer = setInterval(() => {
this.sendProgress();
}, 15000) as unknown as number;
this.setData({ sendDurationTimer: timer });
},
clearSendProgressTimer(this: MyVideoComponent) {
const t = this.data.sendDurationTimer;
if (t) {
clearInterval(t);
this.setData({ sendDurationTimer: 0 });
}
},
async sendProgress(this: MyVideoComponent) {
try {
const openId = wx.getStorageSync("openId");
const code = wx.getStorageSync("code");
const state = wx.getStorageSync("state");
const seeId = wx.getStorageSync("seeId");
const duration = this.data.currentTime + 1;
if (code) {
const params = {
openId,
code,
duration
};
await request({
options: {
url: `/app/video-watch/inspect`,
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
},
isLoading: false,
});
} else {
const params = {
openId,
state,
seeId,
duration
};
await request({
options: {
url: `/app/watch/inspect`,
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
},
isLoading: false,
});
}
} catch (err) {
console.error("上报观看进度失败:", err);
}
},
},
});
@@ -0,0 +1,33 @@
<!--components/my-video.wxml-->
<view class="live-content">
<video id="videoPlayer"
class="video"
danmu-list="{{danmuList}}"
enable-danmu="{{false}}"
show-fullscreen-btn
show-center-play-btn='{{false}}'
enable-progress-gesture="{{false}}"
show-play-btn="{{true}}"
controls="{{true}}"
object-fit="contain"
picture-in-picture-mode="{{[]}}"
show-background-playback-button="{{false}}"
src="{{src}}"
bindended="handleEnded"
binderror="handleError"
bindpause="handlePause"
bindtimeupdate="handleTimeUpdate"
bindfullscreenchange="handleFullscreenChange"
bindloadedmetadata="handleLoadedMetadata"
>
<cover-view class="cover"></cover-view>
<image wx:if="{{!playing}}" src="{{backgroundUrl}}" mode="aspectFill" class="poster-image"></image>
<view class="video-poster" wx:if="{{!playing}}" catchtap="handlePosterTap">
<view class="play-button">
<view class="play-icon">
</view>
</view>
</view>
</video>
</view>
@@ -0,0 +1,5 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
}
@@ -0,0 +1,97 @@
.weui-navigation-bar {
--weui-FG-0:rgba(0,0,0,.9);
--height: 44px;
--left: 16px;
}
.weui-navigation-bar .android {
--height: 48px;
}
.weui-navigation-bar {
overflow: hidden;
color: var(--weui-FG-0);
flex: none;
}
.weui-navigation-bar__inner {
position: relative;
top: 0;
left: 0;
height: calc(var(--height) + env(safe-area-inset-top));
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: env(safe-area-inset-top);
width: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__left {
position: relative;
padding-left: var(--left);
padding-top: 16rpx; //默认位置偏高
display: flex;
flex-direction: row;
align-items: flex-start;
height: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__btn_goback_wrapper {
padding: 11px 18px 11px 16px;
margin: -11px -18px -11px -16px;
}
.weui-navigation-bar__btn_goback_wrapper.weui-active {
opacity: 0.5;
}
.weui-navigation-bar__btn_goback {
font-size: 12px;
width: 12px;
height: 24px;
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--weui-FG-0);
}
.weui-navigation-bar__center {
font-size: 17px;
text-align: center;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-weight: bold;
flex: 1;
height: 100%;
}
.weui-navigation-bar__loading {
margin-right: 4px;
align-items: center;
}
.weui-loading {
font-size: 16px;
width: 16px;
height: 16px;
display: block;
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
background-size: 100%;
margin-left: 0;
animation: loading linear infinite 1s;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,143 @@
interface NavigationBarComponent {
data: {
displayStyle: string;
ios?: boolean;
innerPaddingRight?: string;
leftWidth?: string;
safeAreaTop?: string;
delta?: any;
animated?: any;
};
setData: (
data: Partial<NavigationBarComponent["data"]>,
callback?: () => void
) => void;
triggerEvent: <DetailType = any>(
name: string,
detail?: DetailType,
options?: WechatMiniprogram.Component.TriggerEventOption
) => void;
_showChange: (show: boolean) => void;
back: () => void;
handleBack: () => void;
}
Component({
options: {
multipleSlots: true, // 在组件定义时的选项中启用多slot支持
},
/**
* 组件的属性列表
*/
properties: {
extClass: {
type: String,
value: "",
},
title: {
type: String,
value: "",
},
background: {
type: String,
value: "",
},
color: {
type: String,
value: "",
},
back: {
type: Boolean,
value: true,
},
loading: {
type: Boolean,
value: false,
},
homeButton: {
type: Boolean,
value: false,
},
animated: {
// 显示隐藏的时候opacity动画效果
type: Boolean,
value: true,
},
show: {
// 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在
type: Boolean,
value: true,
observer: "_showChange",
},
// back为true的时候,返回的页面深度
delta: {
type: Number,
value: 1,
},
},
/**
* 组件的初始数据
*/
data: {
displayStyle: "",
},
lifetimes: {
attached(this: NavigationBarComponent) {
const rect = wx.getMenuButtonBoundingClientRect();
wx.getSystemInfo({
success: (res) => {
const isAndroid = res.platform === "android";
const isDevtools = res.platform === "devtools";
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${res.windowWidth - rect.left
}px`,
leftWidth: `width: ${res.windowWidth - rect.left}px`,
safeAreaTop:
isDevtools || isAndroid
? `height: calc(var(--height) + ${res.safeArea.top
}px); padding-top: ${res.safeArea.top || 24}px`
: ``,
});
},
});
},
},
/**
* 组件的方法列表
*/
methods: {
_showChange(this: NavigationBarComponent, show: boolean) {
const animated = this.data.animated;
let displayStyle = "";
if (animated) {
displayStyle = `opacity: ${show ? "1" : "0"};transition:opacity 0.5s;`;
} else {
displayStyle = `display: ${show ? "" : "none"}`;
}
this.setData({
displayStyle,
});
},
back(this: NavigationBarComponent) {
this.triggerEvent("handleBack", { delta: 1 });
const data = this.data;
const pages = getCurrentPages();
//通过链接进入,或只有一个栈时,点击返回是没有用的,直接返回首页
if (pages.length <= 1) {
wx.switchTab({ url: "/pages/tab-bar/medicine-box/index" });
return;
}
if (data.delta) {
wx.navigateBack({
delta: data.delta,
});
} else {
wx.switchTab({ url: "/pages/tab-bar/medicine-box/index" });
}
this.triggerEvent("back", { delta: data.delta }, {});
},
},
});
@@ -0,0 +1,64 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
</block>
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
</block>
</block>
<block wx:else>
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
</block>
<block wx:else>
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>
@@ -0,0 +1,6 @@
{
"component": true,
"pageOrientation": "portrait",
"usingComponents": {
}
}
+14
View File
@@ -0,0 +1,14 @@
/* components/player.wxss */
.player-object {
width: 100%;
height: 100%;
}
.player-video {
width: 100%;
height: 100%;
}
.video {
width: 100%;
height: 100vh;
overflow: hidden;
}
+206
View File
@@ -0,0 +1,206 @@
interface PlayerProperties {
isPlayback: boolean;
isLiving: boolean;
pullUrlInfo: {
rtmpUrl: string;
rtmpOriaacUrl: string;
hlsOriaacUrl: string;
hlsUrl: string;
};
vodInfo: {
state: number;
pullUrl: string;
};
}
interface PlayerData {
liveUrl: string;
vodUrl: string;
isPlaying: boolean;
showVideo: boolean;
fullScreen: boolean;
height: number;
retryCount: number;
maxRetryCount: number;
}
interface PlayerComponent extends WechatMiniprogram.Component.TrivialInstance {
data: PlayerData;
properties: PlayerProperties;
videoContext: WechatMiniprogram.VideoContext | null;
}
Component({
properties: {
isPlayback: {
type: Boolean,
value: false,
},
isLiving: {
type: Boolean,
value: false,
},
pullUrlInfo: {
type: Object,
value: {
rtmpUrl: "",
rtmpOriaacUrl: "",
hlsOriaacUrl: "",
hlsUrl: "",
},
},
vodInfo: {
type: Object,
value: {
state: 0,
pullUrl: "",
},
},
},
data: {
liveUrl: "",
vodUrl: "",
isPlaying: false,
fullScreen: false,
height: 0,
showVideo: true,
retryCount: 0,
maxRetryCount: 3,
},
lifetimes: {
attached(this: PlayerComponent) {
this.initPlayer();
},
detached(this: PlayerComponent) {
this.cleanup();
},
},
observers: {
"isLiving, pullUrlInfo": function (
this: PlayerComponent,
isLiving: boolean,
pullUrlInfo: PlayerProperties["pullUrlInfo"]
) {
if (isLiving && pullUrlInfo.rtmpUrl) {
this.setData({ liveUrl: pullUrlInfo.rtmpUrl });
} else {
this.setData({ liveUrl: "" });
}
},
vodInfo: function (
this: PlayerComponent,
data: PlayerProperties["vodInfo"]
) {
if (data && data.pullUrl) {
this.setData({
vodUrl: data.pullUrl,
isPlaying: true,
showVideo: true,
});
this.playVideo();
}
},
},
methods: {
playVideo(this: PlayerComponent) {
if (!this.videoContext || !this.data.showVideo) return;
setTimeout(() => {
try {
this.videoContext?.play();
} catch (error) {
this.handlePlayError();
}
}, 500);
},
handlePlayError(this: PlayerComponent) {
const { retryCount, maxRetryCount } = this.data;
if (retryCount < maxRetryCount) {
this.setData({ retryCount: retryCount + 1 });
setTimeout(() => {
this.playVideo();
}, 2000);
} else {
this.setData({
showVideo: false,
retryCount: 0,
});
wx.showToast({
title: "播放失败,请检查网络连接",
icon: "none",
});
}
},
initPlayer(this: PlayerComponent) {
try {
const sysInfo = wx.getSystemInfoSync();
this.setData({ height: sysInfo.screenHeight });
this.videoContext = wx.createVideoContext("vodVideo", this);
} catch (err) {
console.error("初始化播放器失败:", err);
wx.showToast({ title: "播放器初始化失败", icon: "none" });
}
},
onLiveStateChange(
this: PlayerComponent,
event: WechatMiniprogram.CustomEvent
) {
const code = event.detail.code;
const stateMap: Record<number, string> = {
2001: "已连接",
2002: "已断开",
2003: "缓冲中",
2004: "播放开始",
2005: "播放结束",
2006: "加载中",
};
console.log("直播状态:", stateMap[code] || `未知(${code})`);
},
onLiveError(this: PlayerComponent, event: WechatMiniprogram.CustomEvent) {
const errCode = event.detail.errCode;
const errorMap: Record<number, string> = {
10001: "系统错误",
10002: "网络错误",
10003: "解码错误",
10004: "直播流不存在",
};
const msg = errorMap[errCode] || `直播错误: ${event.detail.errMsg}`;
wx.showToast({ title: msg, icon: "none" });
},
onVideoPlay(this: PlayerComponent) {
this.setData({ isPlaying: true, retryCount: 0, showVideo: true });
},
onVideoPause(this: PlayerComponent) {
this.setData({ isPlaying: false });
},
onVideoEnded(this: PlayerComponent) {
this.setData({ isPlaying: false });
},
onVideoError(this: PlayerComponent, err: WechatMiniprogram.CustomEvent) {
if (err) {
this.setData({ isPlaying: false, showVideo: false });
this.triggerEvent("controlstoggle", { state: 2 });
}
},
cleanup(this: PlayerComponent) {
this.videoContext = null;
this.setData({
liveUrl: "",
vodUrl: "",
isPlaying: false,
fullScreen: false,
showVideo: true,
retryCount: 0,
});
},
},
});
+38
View File
@@ -0,0 +1,38 @@
<!--components/player.wxml-->
<view class="player-object">
<video
wx:if="{{showVideo && vodUrl}}"
id="vodVideo"
src="{{vodUrl}}"
autoplay="{{true}}"
show-fullscreen-btn="{{false}}"
show-play-btn="{{false}}"
show-center-play-btn="{{false}}"
class="video"
object-fit="contain"
direction="0"
bindplay="onVideoPlay"
bindpause="onVideoPause"
bindended="onVideoEnded"
binderror="onVideoError"
></video>
<live-player
wx:if="{{liveUrl}}"
live-url="{{liveUrl}}"
vod-url="{{vodUrl}}"
is-playback="{{isPlayback}}"
room-title="{{roomInfo.title}}"
viewer-count="{{roomInfo.viewerCount}}"
is-living="{{roomInfo.isLiving}}"
object-fit="contain"
mode="live"
class="player-video"
bindstatechange="onLiveStateChange"
binderror="onLiveError"
bindplay="onVideoPlay"
bindpause="onVideoPause"
bindended="onVideoEnded"
/>
</view>
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+44
View File
@@ -0,0 +1,44 @@
@import "../../variables";
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 999;
opacity: 0;
transition: opacity 0.5s ease;
overscroll-behavior: none;
}
.mask.show {
opacity: 1;
}
.mask.hide {
opacity: 0;
}
.content {
width: 100%;
max-height: 90%;
background: #fff;
// background: linear-gradient(180deg, #edf0f7 0%, #e7ebf4 100%);
// box-shadow: 0 28rpx 70rpx rgba(11, 18, 35, 0.18);
border-top-left-radius: $primary-gap;
border-top-right-radius: $primary-border-radius;
padding: $primary-gap;
transform: translateY(100%);
transition: transform 0.5s ease;
}
.content.show {
transform: translateY(0);
}
.content.hide {
transform: translateY(100%);
}
+47
View File
@@ -0,0 +1,47 @@
// @ts-nocheck
const systemInfo = wx.getWindowInfo();
Component({
properties: {
visible: {
type: Boolean,
value: false,
observer(newVal:any) {
if (newVal) {
//先挂载再执行动画
this.setData({ showSelf: true }, () => {
setTimeout(() => {
this.setData({
popupAnimate: 'show',
maskAnimate: 'show'
})
}, 20)
})
} else {
//先执行动画,不见了再卸载
this.setData({
popupAnimate: 'hide',
maskAnimate: 'hide'
})
// 等动画结束再卸载
setTimeout(() => {
this.setData({ showSelf: false })
}, 500) //和transition一致
}
}
}
},
data: {
showSelf: false,
safeBottom: systemInfo.safeArea.bottom - systemInfo.safeArea.height, //底部的安全距离,避免被返回键挡住了
popupAnimate: '',
maskAnimate: ''
},
methods: {
handleClose() {
this.triggerEvent('close');
},
emptyEvent() {
}
}
})
+7
View File
@@ -0,0 +1,7 @@
<!-- 下半弹窗 -->
<view wx:if="{{showSelf}}" class="mask {{maskAnimate}}" bind:tap="handleClose" catchtouchmove="emptyEvent" >
<view class="content {{popupAnimate}}" style="padding-bottom: {{safeBottom}}rpx;" catchtap="emptyEvent" catchtouchmove="emptyEvent">
<slot name="header"></slot>
<slot></slot>
</view>
</view>
@@ -0,0 +1,7 @@
{
"component": true,
"usingComponents": {
"popup": "/components/popup/popup",
"tag":"/components/tag/tag"
}
}
@@ -0,0 +1,159 @@
/* components/buy-record/buy-record.wxss */
@import "variables";
.order {
margin-top: $primary-gap;
border-radius: $primary-border-radius;
background-color: white;
overflow: hidden;
.order-header {
border-bottom: 2rpx solid rgba(0, 0, 0, 0.1);
line-height: $primary-gap;
height: $primary-gap;
padding: $primary-gap;
text-align: end;
}
.order-footer {
border-top: 2rpx solid rgba(0, 0, 0, 0.1);
// height: $primary-gap;
padding: $primary-gap;
display: flex;
flex-direction: column;
gap: $primary-gap/2;
.order-price {
display: flex;
justify-content: space-between;
}
.order-total-price {
font-size: 32rpx;
font-weight: 700;
}
.order-trade-amount {
color: $primary-color;
font-size: 38rpx;
font-weight: 700;
}
.orderNo {
// font-size: $large-font-size;
}
.payTime {
// font-size: $large-font-size;
}
}
.item+.item {
border-top: 2rpx solid rgba(0, 0, 0, 0.1);
}
.item {
width: 702rpx;
background-color: white;
height: 300rpx;
.info {
display: flex;
justify-content: space-between;
padding: $primary-gap;
padding-bottom: 0;
height: 200rpx;
.left {
border-radius: $primary-border-radius;
overflow: hidden;
min-width: 200rpx;
width: 200rpx;
height: 200rpx;
margin-right: $primary-gap;
image {
width: 100%;
height: 100%;
}
}
.right {
display: flex;
flex-direction: column;
justify-content: space-around;
width: 100%;
.spu {
display: flex;
justify-content: space-between;
width: 100%;
font-size: $large-font-size;
font-weight: 700;
.spuName {
max-width: 300rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.sku {
display: flex;
justify-content: space-between;
width: 100%;
color: $gray-color;
.skuName {
max-width: 300rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.history {
width: 100%;
border-radius: $primary-border-radius;
text-align: end;
color: $primary-color;
}
}
}
}
}
.order:first-child {
margin-top: 0rpx;
}
.footer {
display: flex;
flex-direction: row;
height: 40rpx;
justify-content: space-between;
.totalPrice {
font-size: $large-font-size;
font-weight: 700;
margin-right: $primary-gap;
margin-top: $primary-gap/2;
}
}
// .popup-header {
// display: flex;
// align-items: center;
// font-weight: 700;
// font-size: $large-font-size;
// margin-bottom: $primary-gap * 2;
// }
@@ -0,0 +1,82 @@
import { Orders } from '../../../pages/tab-bar/user/user.d'
export const OrderStateMap: Record<number, any> = {
10: { id: 10, name: "未付款", color: "#ff0056" },
20: { id: 20, name: "已驳回", color: "#ff0056" },
30: { id: 30, name: "待发货", color: "#FA6905" },
40: { id: 40, name: "待审核", color: "#ff7500" },
50: { id: 50, name: "发货中", color: "#1a64d3" },
60: { id: 60, name: "已签收", color: "#00bc12" },
70: { id: 70, name: "售后中", color: "#00b1dd" },
80: { id: 80, name: "全退款", color: "#ff0000" },
90: { id: 90, name: "部分退款", color: "#d90000" },
100: { id: 100, name: "已取消", color: "#ffba00" },
};
export const DeliveryStateMap: Record<number, any> = {
10: { id: 10, name: "待确认", color: "#ff4c3e" },
15: { id: 15, name: "定时发货", color: "#ff4c3e" },
20: { id: 20, name: "待发送ERP", color: "gold" },
30: { id: 30, name: "仓库待发货", color: "#ff7500" },
40: { id: 40, name: "仓库已发货", color: "#FABD14" },
50: { id: 50, name: "物流已揽收", color: "#057748" },
60: { id: 60, name: "客户签收", color: "#00bc12" },
70: { id: 70, name: "客户半签", color: "#52c41a" },
80: { id: 80, name: "客户拒收", color: "#ff0000" },
100: { id: 100, name: "已取消", color: "#ffba00" },
110: { id: 110, name: "售后中", color: "#00B4F4" },
120: { id: 120, name: "售后完成", color: "#156AA8" },
};
//不能查看物流信息
Component({
properties: {
order: {
type: Object,
value: {} as Orders,
},
isDetail: {
type: Boolean,
value: false
}
},
data: {
item: [] as any[],
state: {},
orderTime: "",
payTime: "",
showHistoryPopup: false,
deliveryState: null
},
observers: {
'order': function (order: Orders) {
if (order && order.good) {
// @ts-ignore
this.setData({
item: order.good,
state: OrderStateMap[order.state],
orderTime: order.orderTime.replace("T", " "),
payTime: order.payTime ? order.payTime.replace("T", " ") : ""
})
}
if (order && order.deliveryState) {
// @ts-ignore
this.setData({
deliveryState: DeliveryStateMap[order.deliveryState]
})
}
}
},
methods: {
// openPopup() {
// this.setData({ showHistoryPopup: true })
// },
// closePopup() {
// this.setData({ showHistoryPopup: false })
// }
handleDetail(this: WechatMiniprogram.Component.TrivialInstance) {
if (this.data.isDetail) return;
wx.navigateTo({ url: `/pages/common/order-detail/order-detail?id=${this.data.order.orderNo}` })
}
}
})
export { }
@@ -0,0 +1,68 @@
<!--components/buy-record/buy-record.wxml-->
<view class="order">
<view class="order-header">订单创建时间 {{orderTime}}</view>
<view class="item" wx:for="{{item}}" wx:key="skuName" bind:tap="handleDetail">
<view class="info">
<view class="left">
<image src="{{item.picUrl}}" mode="aspectFill" />
</view>
<view class="right">
<view class="spu">
<view class="spuName">{{item.spuName}}</view>
<view class="price">{{item.price?'¥'+item.price / 100:"暂无报价"}}</view>
</view>
<view class="sku">
<view class="skuName">{{item.skuName}}</view>
<view class="count">x{{item.count}}</view>
</view>
<tag class="history" color="{{state.color}}" propStyle="margin-right: 0rpx;">{{state.name}}</tag>
<view class="history" style="color:{{deliveryState.color}}">{{deliveryState.name}}</view>
<!-- <view class="history">
查看物流
</view> -->
<view>
</view>
</view>
</view>
<view class="footer">
<view></view>
<view class="totalPrice">
应付款:¥{{item.totalPrice / 100}}
</view>
</view>
</view>
<view class="order-footer">
<view class="order-price">
<view class="order-total-price">订单总金额 ¥{{order.totalAmount/100}}</view>
<view class="order-total-price">
优惠金额:¥{{order.discountAmount/100 || 0.00}}
</view>
</view>
<view class="order-price">
<view class="order-trade-amount">
实付金额 ¥{{order.advancePrice/100 || 0.00}}
</view>
<view class="order-trade-amount" >
{{state>=70?'退款金额':'支付金额'}}
¥{{order.tradeAmount/100}}
</view>
</view>
<view class="orderNo" wx:if="{{order.orderNo}}">订单号 {{order.orderNo}}</view>
<view class="orderNo" wx:if="{{order.transactionId}}">微信订单号 {{order.transactionId}}</view>
<view class="payTime" wx:if="{{order.payTime}}">订单支付时间 {{payTime||'未付款'}}</view>
<view class="payTime" wx:if="{{order.sendTime}}">订单发货时间 {{sendTime||'未付款'}}</view>
</view>
</view>
<!-- <popup visible="{{showHistoryPopup}}" bind:close="closePopup">
<view slot="header" class="popup-header">
<text>物流信息</text>
</view>
<view>
这里显示物流的信息
</view>
</popup> -->
@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"van-stepper": "@vant/weapp/stepper/index"
}
}
@@ -0,0 +1,101 @@
/* components/cart-item/cart-item.wxss */
@import "variables";
.item {
background-color: white;
// background-color: red;
height: 300rpx;
margin-bottom: $primary-gap;
border-radius: $primary-border-radius;
display: flex;
justify-content: space-between;
padding: $primary-gap;
.left {
height: 300rpx;
display: flex;
justify-content: space-between;
flex-direction: column;
.pic {
border-radius: $primary-border-radius;
overflow: hidden;
min-width: 200rpx;
width: 200rpx;
height: 200rpx;
margin-right: $primary-gap;
image {
width: 100%;
height: 100%;
}
}
.delete {
height: 100rpx;
line-height: 120rpx;
margin-left: $primary-gap/2;
.delete-button {
color: $danger-color;
font-size: $large-font-size;
}
}
}
.right {
height: 200rpx;
display: flex;
flex-direction: column;
justify-content: space-around;
height: 300rpx;
max-width: 440rpx;
.name {
font-size: 40rpx;
font-weight: 700;
max-width: 500rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.price {
font-size: $large-font-size;
font-weight: 700;
text-align: end;
color: $primary-color;
}
.count {
display: flex;
align-items: center;
width: 100%;
text-align: end;
justify-content: flex-end;
float: right;
.stepper {
margin-right: $primary-gap/2;
}
.unit {
font-size: $large-font-size;
text-align: end;
}
}
.stock {
color: $gray-color;
text-align: end;
}
.total-price {
font-size: $large-font-size;
font-weight: 700;
text-align: end;
color: $primary-color;
}
}
}
@@ -0,0 +1,66 @@
// @ts-nocheck
// components/cart-item/cart-item.ts
export { }
const app = getApp<IAppOption>();
Component({
/**
* 组件的属性列表
*/
properties: {
item: {},
shoppingCart: {},
},
/**
* 组件的初始数据
*/
data: {
totalPrice: 0,
showStock: app.globalData.showStock
},
/**
* 组件的方法列表
*/
methods: {
calcTotalPrice() {
if (!this.data.shoppingCart || !this.data.shoppingCart.data) return 0;
const total = this.data.shoppingCart.data.reduce((sum: number, item: any) => {
return sum + item.price * item.count;
}, 0);
this.setData({ totalPrice: total });
},
handleCountChange(e: any) {
this.setData({ item: { ...this.data.item, count: e.detail } })
const target = this.data.shoppingCart.data.find(
(item: any) => item.skuId == this.data.item.skuId
)
if (target) target.count = e.detail;
let storage = wx.getStorageSync("shoppingCart") || [];
storage = storage.map((cart: any) => {
if (cart.mbuId == this.data.shoppingCart.mbuId) {
return this.data.shoppingCart;
}
return cart;
});
wx.setStorageSync("shoppingCart", storage);
this.calcTotalPrice();
this.triggerEvent('handleCountChange', {
totalPrice: this.data.totalPrice
})
},
handleDelete() {
this.triggerEvent('delete', {
skuId: this.data.item.skuId
})
}
},
lifetimes: {
created: function () {
}
}
})
@@ -0,0 +1,30 @@
<!--components/cart-item/cart-item.wxml-->
<view class="item">
<view class="left">
<view class="pic">
<image src="{{item.picUrl}}"></image>
</view>
<view class="delete">
<view bind:tap="handleDelete" class="delete-button">
删除
</view>
</view>
</view>
<view class="right">
<view class="name">{{item.name}}</view>
<view class="price">¥ {{item.price/100}} </view>
<!-- 这里用计数器 -->
<view class="count">
<!-- max="{{showStock?checkItem ? checkItem.stock : skus[0].stock :99999}}" -->
<van-stepper integer min="1" bind:change="handleCountChange" max="{{showStock?item.stock:99999}}" value="{{item.count}}"
class="stepper"></van-stepper>
<view class="unit">{{item.unit}}</view>
</view>
<view class="total-price">总金额:¥ {{item.price*item.count/100}}</view>
<view class="stock" wx:if="{{showStock}}">库存:{{item.stock}}</view>
</view>
</view>
@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"guide-popup": "/components/static/guide-popup/guide-popup"
}
}
@@ -0,0 +1,439 @@
@import "variables";
.comment {
padding: 0rpx $primary-gap;
position: fixed;
left: 0;
right: 0;
bottom: 100rpx;
z-index: 9;
overflow: visible;
.top-notice-layer {
position: absolute;
left: $primary-gap;
right: 190rpx;
top: -86rpx;
height: 64rpx;
overflow: hidden;
pointer-events: none;
}
.top-notice-pill {
height: 64rpx;
width: 100%;
padding: 0 20rpx;
border-radius: 999rpx;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 12rpx;
color: #f6fbff;
background: linear-gradient(
90deg,
rgba(12, 24, 52, 0.92) 0%,
rgba(30, 78, 132, 0.78) 100%
);
border: 1rpx solid rgba(117, 207, 255, 0.24);
box-shadow: 0 14rpx 36rpx rgba(0, 0, 0, 0.22);
animation: top-notice-slide 3.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.top-notice-tag {
flex-shrink: 0;
height: 36rpx;
padding: 0 16rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #62f0ff 0%, #3cb6ff 100%);
color: #07253d;
font-size: 20rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.top-notice-text {
flex: 1;
min-width: 0;
font-size: 24rpx;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comment-container {
width: 470rpx;
max-width: calc(100% - 260rpx);
overflow: hidden;
.list-wrapper {
will-change: transform;
.msg-item {
line-height: 50rpx;
border-radius: 30rpx;
display: inline-block;
max-width: 100%;
min-height: 50rpx;
margin-top: $primary-gap / 2;
font-weight: 700;
background: rgba(0, 0, 0, 0.5);
word-break: break-all;
padding: 0rpx $primary-gap;
&.is-system {
background: rgba(30, 65, 117, 0.48);
border: 1rpx solid rgba(104, 196, 255, 0.18);
}
.msg-item-name {
color: #76dbfd;
margin-right: $primary-gap;
}
.msg-item-text {
color: white;
}
}
}
}
.unread-reminder {
position: absolute;
left: 50%;
bottom: 116rpx;
transform: translateX(-50%);
min-width: 180rpx;
height: 56rpx;
padding: 0 22rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
box-sizing: border-box;
color: #ffffff;
background: rgba(14, 24, 44, 0.9);
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.18);
animation: unread-reminder-pop 0.24s ease-out;
}
.unread-reminder-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: #ff6a88;
box-shadow: 0 0 0 8rpx rgba(255, 106, 136, 0.12);
}
.unread-reminder-text {
font-size: 24rpx;
line-height: 1;
}
.input-shell {
margin-top: $primary-gap * 1.5;
width: calc(100% - 260rpx);
max-width: 470rpx;
height: 90rpx;
padding: 0rpx $primary-gap;
border-radius: 999rpx;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.56);
display: flex;
align-items: center;
transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
&.is-enhanced {
padding-left: 18rpx;
padding-right: 10rpx;
background: linear-gradient(
135deg,
rgba(12, 19, 30, 0.88) 0%,
rgba(20, 36, 59, 0.84) 100%
);
border: 1rpx solid rgba(135, 195, 255, 0.16);
box-shadow: 0 18rpx 40rpx rgba(0, 0, 0, 0.18);
}
&.is-focused {
transform: translateY(-2rpx);
box-shadow: 0 18rpx 42rpx rgba(48, 137, 255, 0.18);
}
}
.input-leading {
width: 28rpx;
height: 28rpx;
margin-right: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.input-leading-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: linear-gradient(135deg, #5dfff0 0%, #4ab7ff 100%);
box-shadow: 0 0 0 10rpx rgba(73, 207, 255, 0.1);
animation: input-pulse 1.8s ease-in-out infinite;
}
.msg-input {
flex: 1;
min-width: 0;
height: 100%;
line-height: 90rpx;
color: white;
background: transparent;
box-sizing: border-box;
}
.input-send {
min-width: 108rpx;
height: 68rpx;
padding: 0 24rpx;
border-radius: 999rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.64);
font-size: 24rpx;
font-weight: 600;
letter-spacing: 2rpx;
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
&.is-ready {
background: linear-gradient(135deg, #53d6ff 0%, #2f8cff 100%);
color: #ffffff;
transform: translateY(-1rpx);
box-shadow: 0 10rpx 26rpx rgba(47, 140, 255, 0.26);
}
}
.action-bar {
position: absolute;
right: $primary-gap;
bottom: 7rpx;
display: flex;
align-items: center;
gap: 14rpx;
}
.action-btn {
width: 76rpx;
height: 76rpx;
padding: 0;
border-radius: 50%;
background: rgba(17, 17, 17, 0.58);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 28rpx rgba(0, 0, 0, 0.14);
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
box-sizing: border-box;
&.action-btn-hover {
transform: scale(0.94);
background: rgba(17, 17, 17, 0.7);
}
}
.action-share {
border: none;
&::after {
border: none;
}
}
.action-btn-heart {
background: rgba(255, 61, 104, 0.18);
border: 2rpx solid rgba(255, 108, 143, 0.45);
&.is-active {
transform: scale(1.08);
box-shadow: 0 12rpx 30rpx rgba(255, 61, 104, 0.24);
}
}
.action-icon {
width: 38rpx;
height: 38rpx;
}
.action-icon-heart {
width: 40rpx;
height: 40rpx;
}
.heart-layer {
position: absolute;
right: $primary-gap + 12rpx;
bottom: 84rpx;
width: 160rpx;
height: 340rpx;
pointer-events: none;
overflow: visible;
}
.heart-particle {
position: absolute;
opacity: 0;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
}
.heart-glyph {
display: none;
line-height: 1;
text-shadow: 0 8rpx 20rpx rgba(255, 77, 109, 0.2);
}
.heart-fly-icon {
width: 36rpx;
height: 36rpx;
display: block;
}
.pulse-small {
animation: heart-pop 0.72s ease-out;
}
.pulse-medium {
animation: heart-pop 0.84s ease-out;
}
.pulse-large {
animation: heart-pop 0.96s ease-out;
}
.path-left {
animation-name: float-heart-left;
}
.path-center {
animation-name: float-heart-center;
}
.path-right {
animation-name: float-heart-right;
}
}
@keyframes input-pulse {
0%,
100% {
transform: scale(0.9);
opacity: 0.82;
}
50% {
transform: scale(1.08);
opacity: 1;
}
}
@keyframes unread-reminder-pop {
0% {
opacity: 0;
transform: translateX(-50%) translateY(12rpx);
}
100% {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@keyframes top-notice-slide {
0% {
opacity: 0;
transform: translate3d(120%, 0, 0) scale(0.96);
}
12% {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1);
}
86% {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1);
}
100% {
opacity: 0;
transform: translate3d(-16%, 0, 0) scale(0.98);
}
}
@keyframes heart-pop {
0% {
transform: scale(0.6);
}
45% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
@keyframes float-heart-left {
0% {
opacity: 0;
transform: translate3d(0, 0, 0) scale(0.45);
}
18% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate3d(-48rpx, -240rpx, 0) scale(1.18);
}
}
@keyframes float-heart-center {
0% {
opacity: 0;
transform: translate3d(0, 0, 0) scale(0.45);
}
18% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate3d(8rpx, -260rpx, 0) scale(1.24);
}
}
@keyframes float-heart-right {
0% {
opacity: 0;
transform: translate3d(0, 0, 0) scale(0.45);
}
18% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate3d(54rpx, -228rpx, 0) scale(1.12);
}
}
@@ -0,0 +1,681 @@
import { request } from "../../../utils/request";
import { shopId } from "../../../env";
import {
join,
leave,
addCustomEventListener,
removeCustomEventListener,
} from "../../../utils/server-sent-events";
type CommentMessage = {
id: string;
name: string;
msg: string;
kind?: "system" | "chat";
};
type CommentFeatureState = {
approved: boolean;
unreadReminder: boolean;
optimizedInput: boolean;
topTicker: boolean;
};
interface CommentProperties {
enhancementApproved: boolean;
avatarUrl: string;
}
interface CommentData {
list: CommentMessage[];
displayList: CommentMessage[];
scrollTop: number;
isAtBottom: boolean;
maxVisible: number;
itemHeight: number;
MAX_HISTORY: number;
joined: boolean;
listener: boolean;
lastMsgKey: string;
inputValue: string;
canSubmit: boolean;
inputFocused: boolean;
inputPlaceholderStyle: string;
showGuidePopup: boolean;
guideTitle: string;
guideSubtitle: string;
guideImages: string[];
heartParticles: any[];
heartButtonActive: boolean;
unreadMessageCount: number;
topNoticeText: string;
topNoticeVisible: boolean;
featureState: CommentFeatureState;
}
interface CommentInstance extends WechatMiniprogram.Component.TrivialInstance {
data: CommentData;
properties: CommentProperties;
_sseListener?: (payload: any) => void;
_heartButtonTimer?: number | null;
_heartTimers?: number[];
_topNoticeTimer?: number | null;
_topNoticeQueue?: string[];
safeLeave: () => Promise<void>;
bindSseListener: () => void;
unbindSseListener: () => void;
appendMsgFromSse: (
payload: { name: string; msg: string },
options?: { kind?: "system" | "chat"; skipTopNotice?: boolean },
) => void;
handleSubmit: (e: any) => Promise<void>;
appendMsgText: (text: string) => void;
createHeartParticle: (index: number) => any;
removeHeartParticle: (id: string) => void;
clearHeartEffects: () => void;
safeVibrate: () => void;
}
const ITEM_HEIGHT = 60;
const MAX_VISIBLE = 5;
const MAX_HISTORY = 50;
const SSE_CHANNEL = "12";
const SYSTEM_NOTICE = "欢迎来到直播间,请文明发言,禁止发布违规信息。";
const DEFAULT_GUIDE_SUBTITLE = "个人中心";
const DEFAULT_GUIDE_IMAGES = Object.freeze(["/assets/img/600.jpg"]);
const DEFAULT_PLACEHOLDER_STYLE = "color:white;font-size:32rpx";
const ENHANCED_PLACEHOLDER_STYLE =
"color:rgba(255,255,255,0.62);font-size:28rpx";
const TOP_NOTICE_DURATION = 3400;
const REVIEW_READY_FEATURES = Object.freeze({
unreadReminder: true,
optimizedInput: true,
topTicker: true,
});
function getChatChannelName() {
return `${shopId}-${SSE_CHANNEL}`;
}
function createMessage(
name: string,
msg: string,
kind: "system" | "chat" = "chat",
): CommentMessage {
return {
id: `msg_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
name,
msg,
kind,
};
}
function buildWelcomeMessage() {
return createMessage("系统", SYSTEM_NOTICE, "system");
}
function buildFeatureState(approved: boolean): CommentFeatureState {
return {
approved,
unreadReminder: approved && REVIEW_READY_FEATURES.unreadReminder,
optimizedInput: approved && REVIEW_READY_FEATURES.optimizedInput,
topTicker: approved && REVIEW_READY_FEATURES.topTicker,
};
}
function sanitizeInput(value: unknown): string {
return String(value ?? "")
.replace(/\s+/g, " ")
.trim();
}
function buildGuideImages(avatarUrl: unknown): string[] {
const normalizedAvatarUrl = sanitizeInput(avatarUrl);
return normalizedAvatarUrl
? [normalizedAvatarUrl]
: [...DEFAULT_GUIDE_IMAGES];
}
function syncGuideImages(instance: CommentInstance, avatarUrl: unknown) {
const nextGuideImages = buildGuideImages(avatarUrl);
const currentGuideImages = instance.data.guideImages || [];
const unchanged =
currentGuideImages.length === nextGuideImages.length &&
currentGuideImages.every((item, index) => item === nextGuideImages[index]);
if (unchanged) return;
instance.setData({
guideImages: nextGuideImages,
});
}
function trimMessages(messages: CommentMessage[], maxHistory: number) {
if (messages.length <= maxHistory) return messages;
const pinnedMessage = messages[0]?.kind === "system" ? messages[0] : null;
if (!pinnedMessage) {
return messages.slice(-maxHistory);
}
const normalMessages = messages.slice(1);
return [pinnedMessage, ...normalMessages.slice(-(maxHistory - 1))];
}
function formatTopNoticeText(name: string, msg: string) {
const safeName = sanitizeInput(name) || "新消息";
const preview = sanitizeInput(msg);
const clippedPreview =
preview.length > 18 ? `${preview.slice(0, 18)}...` : preview;
return clippedPreview
? `${safeName}${clippedPreview}`
: `${safeName} 发来了一条新消息`;
}
function clearTopNotice(instance: CommentInstance) {
if (instance._topNoticeTimer) {
clearTimeout(instance._topNoticeTimer);
instance._topNoticeTimer = null;
}
instance._topNoticeQueue = [];
if (instance.data.topNoticeVisible || instance.data.topNoticeText) {
instance.setData({
topNoticeVisible: false,
topNoticeText: "",
});
}
}
function playTopNoticeQueue(instance: CommentInstance) {
if (!instance.data.featureState.topTicker || instance.data.topNoticeVisible) {
return;
}
instance._topNoticeQueue = instance._topNoticeQueue || [];
const nextNotice = instance._topNoticeQueue.shift();
if (!nextNotice) return;
instance.setData(
{
topNoticeText: nextNotice,
topNoticeVisible: false,
},
() => {
wx.nextTick(() => {
instance.setData({ topNoticeVisible: true });
instance._topNoticeTimer = setTimeout(() => {
instance.setData({ topNoticeVisible: false }, () => {
instance._topNoticeTimer = null;
playTopNoticeQueue(instance);
});
}, TOP_NOTICE_DURATION);
});
},
);
}
function enqueueTopNotice(instance: CommentInstance, text: string) {
if (!instance.data.featureState.topTicker || !text) return;
instance._topNoticeQueue = instance._topNoticeQueue || [];
instance._topNoticeQueue.push(text);
if (!instance.data.topNoticeVisible) {
playTopNoticeQueue(instance);
}
}
function clearUnreadReminder(instance: CommentInstance) {
if (!instance.data.unreadMessageCount) return;
instance.setData({ unreadMessageCount: 0 });
}
function scrollToLatest(instance: CommentInstance, clearUnread = false) {
if (clearUnread) {
clearUnreadReminder(instance);
}
instance.setData({ scrollTop: Date.now() });
}
function syncFeatureApproval(instance: CommentInstance, approved: boolean) {
const justEnabledTopTicker =
!instance.data.featureState.topTicker &&
approved &&
REVIEW_READY_FEATURES.topTicker;
const featureState = buildFeatureState(approved);
instance.setData(
{
featureState,
inputPlaceholderStyle: featureState.optimizedInput
? ENHANCED_PLACEHOLDER_STYLE
: DEFAULT_PLACEHOLDER_STYLE,
inputFocused: featureState.optimizedInput
? instance.data.inputFocused
: false,
unreadMessageCount: featureState.unreadReminder
? instance.data.unreadMessageCount
: 0,
topNoticeText: featureState.topTicker ? instance.data.topNoticeText : "",
topNoticeVisible: featureState.topTicker
? instance.data.topNoticeVisible
: false,
},
() => {
if (justEnabledTopTicker) {
enqueueTopNotice(
instance,
formatTopNoticeText("直播提醒", SYSTEM_NOTICE),
);
}
},
);
if (!featureState.topTicker) {
clearTopNotice(instance);
}
}
Component({
properties: {
enhancementApproved: {
type: Boolean,
value: false,
observer(this: CommentInstance, nextValue: boolean) {
syncFeatureApproval(this, !!nextValue);
},
},
avatarUrl: {
type: String,
value: "",
observer(this: CommentInstance, nextValue: string) {
syncGuideImages(this, nextValue);
},
},
},
data: {
list: [] as CommentMessage[],
displayList: [] as CommentMessage[],
scrollTop: 999999,
isAtBottom: true,
maxVisible: MAX_VISIBLE,
itemHeight: ITEM_HEIGHT,
MAX_HISTORY,
joined: false,
listener: false,
lastMsgKey: "",
inputValue: "",
canSubmit: false,
inputFocused: false,
inputPlaceholderStyle: DEFAULT_PLACEHOLDER_STYLE,
showGuidePopup: false,
guideTitle: "",
guideSubtitle: DEFAULT_GUIDE_SUBTITLE,
guideImages: [...DEFAULT_GUIDE_IMAGES],
heartParticles: [] as any[],
heartButtonActive: false,
unreadMessageCount: 0,
topNoticeText: "",
topNoticeVisible: false,
featureState: buildFeatureState(false),
} as CommentData,
lifetimes: {
attached(this: CommentInstance) {
const initialMessages = [buildWelcomeMessage()];
this._topNoticeQueue = [];
this.setData(
{
list: initialMessages,
displayList: initialMessages,
},
() => {
syncGuideImages(this, this.properties.avatarUrl);
syncFeatureApproval(this, !!this.properties.enhancementApproved);
this.bindSseListener();
setTimeout(() => {
scrollToLatest(this);
}, 50);
},
);
},
detached(this: CommentInstance) {
this.unbindSseListener();
this.safeLeave();
this.clearHeartEffects();
clearTopNotice(this);
},
},
pageLifetimes: {
show(this: CommentInstance) {
this.bindSseListener();
wx.nextTick(async () => {
if (this.data.joined) return;
try {
await join(getChatChannelName());
this.setData({ joined: true });
} catch (error) {
console.warn("SSE join failed", error);
}
});
},
hide(this: CommentInstance) {
this.unbindSseListener();
this.safeLeave();
this.clearHeartEffects();
clearTopNotice(this);
},
},
methods: {
async safeLeave(this: CommentInstance) {
if (!this.data.joined) return;
this.setData({ joined: false });
try {
await leave(getChatChannelName());
} catch (error) {
console.warn("SSE leave failed", error);
}
},
bindSseListener(this: CommentInstance) {
if (this.data.listener) return;
this.setData({ listener: true });
this._sseListener = (payload: any) => {
if (!payload) return;
const { name, msg } = payload;
if (!msg && msg !== "") return;
const key = `${name ?? ""}:${msg}`;
if (key === this.data.lastMsgKey) return;
this.setData({ lastMsgKey: key });
this.appendMsgFromSse({
name: name ?? "用户",
msg,
});
};
addCustomEventListener("pushMsg", this._sseListener);
},
unbindSseListener(this: CommentInstance) {
if (!this.data.listener) return;
this.setData({ listener: false });
if (!this._sseListener) return;
removeCustomEventListener("pushMsg", this._sseListener);
this._sseListener = undefined;
},
onScroll(this: CommentInstance, e: any) {
const { scrollTop, scrollHeight } = e.detail;
wx.createSelectorQuery()
.in(this)
.select("#scrollView")
.boundingClientRect((rect) => {
const clientHeight = rect?.height || 0;
const isBottom = scrollTop + clientHeight >= scrollHeight - 20;
if (isBottom !== this.data.isAtBottom) {
this.setData({ isAtBottom: isBottom });
}
if (isBottom) {
clearUnreadReminder(this);
}
})
.exec();
},
appendMsgFromSse(
this: CommentInstance,
payload: { name: string; msg: string },
options: { kind?: "system" | "chat"; skipTopNotice?: boolean } = {},
) {
const messageName =
sanitizeInput(payload.name) ||
(options.kind === "system" ? "系统" : "用户");
const messageText = String(payload.msg ?? "");
const nextMessage = createMessage(
messageName,
messageText,
options.kind || "chat",
);
const nextList = trimMessages(
[...this.data.list, nextMessage],
this.data.MAX_HISTORY,
);
const shouldAutoScroll = this.data.isAtBottom;
const shouldShowUnreadReminder =
this.data.featureState.unreadReminder &&
!shouldAutoScroll &&
(options.kind || "chat") !== "system";
this.setData(
{
list: nextList,
displayList: nextList,
unreadMessageCount: shouldShowUnreadReminder
? this.data.unreadMessageCount + 1
: this.data.unreadMessageCount,
},
() => {
if (shouldAutoScroll) {
scrollToLatest(this);
}
},
);
if (this.data.featureState.topTicker && !options.skipTopNotice) {
enqueueTopNotice(this, formatTopNoticeText(messageName, messageText));
}
},
handleInput(this: CommentInstance, e: any) {
const value = e.detail.value || "";
this.setData({
inputValue: value,
canSubmit: !!sanitizeInput(value),
});
},
handleInputFocus(this: CommentInstance) {
if (!this.data.featureState.optimizedInput) return;
this.setData({ inputFocused: true });
},
handleInputBlur(this: CommentInstance) {
if (!this.data.inputFocused) return;
this.setData({ inputFocused: false });
},
submitCurrentInput(this: CommentInstance) {
if (!this.data.canSubmit) return;
this.handleSubmit({
detail: {
value: this.data.inputValue,
},
});
},
handleUnreadReminderTap(this: CommentInstance) {
scrollToLatest(this, true);
},
openGuidePopup(this: CommentInstance) {
this.setData({
showGuidePopup: true,
guideTitle: wx.getStorageSync("userName") || "当前用户",
guideSubtitle: DEFAULT_GUIDE_SUBTITLE,
});
},
closeGuidePopup(this: CommentInstance) {
this.setData({
showGuidePopup: false,
});
},
async handleSubmit(this: CommentInstance, e: any) {
const value = sanitizeInput(e?.detail?.value ?? this.data.inputValue);
if (!value) {
wx.showToast({
title: "发送内容不能为空",
icon: "none",
duration: 2000,
});
return;
}
try {
await request({
options: {
url: "/app/mini-app/push-chat-msg",
data: {
shopId,
id: Number(SSE_CHANNEL),
name: wx.getStorageSync("userName") || "微信用户",
msg: value,
},
},
isLoading: false,
});
} catch (error) {
console.log(error, "push chat message failed");
this.appendMsgText(value);
} finally {
this.setData({
inputValue: "",
canSubmit: false,
});
}
},
appendMsgText(this: CommentInstance, text: string) {
this.appendMsgFromSse({
name: wx.getStorageSync("userName") || "微信用户",
msg: text,
});
},
handleHeartTap(this: CommentInstance) {
const particles = Array.from({ length: 6 }, (_, index) =>
this.createHeartParticle(index),
);
this.setData({
heartParticles: [...this.data.heartParticles, ...particles],
heartButtonActive: true,
});
this.safeVibrate();
if (this._heartButtonTimer) {
clearTimeout(this._heartButtonTimer);
}
this._heartButtonTimer = setTimeout(() => {
this.setData({ heartButtonActive: false });
}, 220);
this._heartTimers = this._heartTimers || [];
particles.forEach((particle: any) => {
const timer = setTimeout(
() => {
this.removeHeartParticle(particle.id);
},
particle.duration + particle.delay + 260,
);
this._heartTimers?.push(timer);
});
},
createHeartParticle(index: number) {
const palette = ["#ff4d6d", "#ff6b81", "#ff8399", "#ffb3c1"];
const pathClasses = ["path-left", "path-center", "path-right"];
const pulseClasses = ["pulse-small", "pulse-medium", "pulse-large"];
return {
id: `heart_${Date.now()}_${index}_${Math.floor(Math.random() * 1000)}`,
right: 16 + Math.floor(Math.random() * 28),
bottom: 10 + Math.floor(Math.random() * 18),
size: 34 + Math.floor(Math.random() * 10),
color:
palette[
(index + Math.floor(Math.random() * palette.length)) %
palette.length
],
duration: 920 + Math.floor(Math.random() * 320),
delay: index * 40,
rotate: -16 + Math.floor(Math.random() * 32),
pathClass:
pathClasses[
(index + Math.floor(Math.random() * pathClasses.length)) %
pathClasses.length
],
pulseClass: pulseClasses[index % pulseClasses.length],
};
},
removeHeartParticle(this: CommentInstance, id: string) {
this.setData({
heartParticles: this.data.heartParticles.filter(
(item: any) => item.id !== id,
),
});
},
clearHeartEffects(this: CommentInstance) {
if (this._heartButtonTimer) {
clearTimeout(this._heartButtonTimer);
this._heartButtonTimer = null;
}
if (Array.isArray(this._heartTimers)) {
this._heartTimers.forEach((timer) => clearTimeout(timer));
this._heartTimers = [];
}
this.setData({
heartParticles: [],
heartButtonActive: false,
});
},
safeVibrate() {
if (typeof wx.vibrateShort !== "function") return;
try {
wx.vibrateShort();
} catch (error) {
console.warn("vibrateShort failed", error);
}
},
},
});
@@ -0,0 +1,103 @@
<view class="comment">
<view wx:if="{{featureState.topTicker && topNoticeVisible}}" class="top-notice-layer">
<view class="top-notice-pill">
<text class="top-notice-tag">LIVE</text>
<text class="top-notice-text">{{topNoticeText}}</text>
</view>
</view>
<scroll-view
scroll-y="true"
class="comment-container"
id="scrollView"
style="height: {{maxVisible * itemHeight + 24 * (maxVisible - 1)}}rpx"
bindscroll="onScroll"
scroll-with-animation="true"
scroll-top="{{scrollTop}}"
>
<view id="list" class="list-wrapper">
<view wx:for="{{displayList}}" wx:key="id">
<view class="msg-item {{item.kind === 'system' ? 'is-system' : ''}}">
<text class="msg-item-name" wx:if="{{item.name}}">{{item.name}}</text>
<text class="msg-item-text" wx:if="{{item.msg || item.msg === ''}}">{{item.msg}}</text>
</view>
</view>
</view>
</scroll-view>
<view
wx:if="{{featureState.unreadReminder && unreadMessageCount > 0}}"
class="unread-reminder"
bindtap="handleUnreadReminderTap">
<view class="unread-reminder-dot"></view>
<text class="unread-reminder-text">{{unreadMessageCount}} 条新消息</text>
</view>
<view class="input-shell {{featureState.optimizedInput ? 'is-enhanced' : ''}} {{inputFocused ? 'is-focused' : ''}}">
<view wx:if="{{featureState.optimizedInput}}" class="input-leading">
<view class="input-leading-dot"></view>
</view>
<input
value="{{inputValue}}"
confirm-type="send"
bindinput="handleInput"
bindconfirm="handleSubmit"
bindfocus="handleInputFocus"
bindblur="handleInputBlur"
class="msg-input"
cursor-spacing="18"
placeholder="说点什么吧~"
placeholder-style="{{inputPlaceholderStyle}}"
/>
<view
wx:if="{{featureState.optimizedInput}}"
class="input-send {{canSubmit ? 'is-ready' : ''}}"
bindtap="submitCurrentInput"
>
发送
</view>
</view>
<view class="action-bar">
<view class="action-btn" hover-class="action-btn-hover" bindtap="openGuidePopup">
<image class="action-icon" src="./icons/gallery.svg" mode="aspectFit" />
</view>
<button class="action-btn action-share" hover-class="action-btn-hover" open-type="share">
<image class="action-icon" src="./icons/share.svg" mode="aspectFit" />
</button>
<view
class="action-btn action-btn-heart {{heartButtonActive ? 'is-active' : ''}}"
hover-class="action-btn-hover"
bindtap="handleHeartTap"
>
<image class="action-icon action-icon-heart" src="./icons/heart.svg" mode="aspectFit" />
</view>
</view>
<view class="heart-layer">
<view
wx:for="{{heartParticles}}"
wx:key="id"
class="heart-particle {{item.pathClass}}"
style="right: {{item.right}}rpx; bottom: {{item.bottom}}rpx; animation-duration: {{item.duration}}ms; animation-delay: {{item.delay}}ms;"
>
<image class="heart-fly-icon {{item.pulseClass}}" src="./icons/heart.svg" mode="aspectFit" />
<text
class="heart-glyph {{item.pulseClass}}"
style="font-size: {{item.size}}rpx; color: {{item.color}}; transform: rotate({{item.rotate}}deg);"
>❤</text>
</view>
</view>
<guide-popup
show="{{showGuidePopup}}"
title="{{guideTitle}}"
subtitle="{{guideSubtitle}}"
images="{{guideImages}}"
bind:close="closeGuidePopup"
/>
</view>
@@ -0,0 +1,5 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="22" width="62" height="52" rx="12" stroke="#FFFFFF" stroke-width="6"/>
<circle cx="63" cy="38" r="6" fill="#FFFFFF"/>
<path d="M27 62L41 48L52 58L61 49L69 58V66H27V62Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

Some files were not shown because too many files have changed in this diff Show More