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
+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

@@ -0,0 +1,9 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M48 80C46.1 80 44.3 79.3 42.8 78L22.3 59.2C10.2 48.1 9.1 29.4 19.8 17.9C30.4 6.5 48 5.8 59.4 16.3L62 18.7L64.6 16.3C76 5.8 93.6 6.5 104.2 17.9C114.9 29.4 113.8 48.1 101.7 59.2L81.2 78C79.7 79.3 77.9 80 76 80C74.1 80 72.3 79.3 70.8 78L62 69.9L53.2 78C51.7 79.3 49.9 80 48 80Z" fill="url(#paint0_linear_420_7)" transform="translate(-14 0)"/>
<defs>
<linearGradient id="paint0_linear_420_7" x1="48" y1="10" x2="48" y2="80" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF8AA1"/>
<stop offset="1" stop-color="#FF325C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 685 B

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