feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
miniprogram_npm/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 50 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {
|
||||||
|
"popup": "/components/popup/popup"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// components/end/end.ts
|
||||||
|
Component({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件的属性列表
|
||||||
|
*/
|
||||||
|
properties: {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件的初始数据
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件的方法列表
|
||||||
|
*/
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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": {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -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%);
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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 |