This commit is contained in:
dzc committed 2026-07-15 20:20:05 +08:00
1 parent 8ad36b1d57
commit cf96d48a38
174 files changed
+2156 -2008

No files matched your search

+2 -2
View File
@@ -10,8 +10,8 @@ SHOPRO_VERSION=v2.4.1
SHOPRO_API_PATH=/app-api
# 后端接口 - 测试环境(通过 process.env.NODE_ENV == development)
SHOPRO_DEV_BASE_URL = https://api.htysapp.com
# SHOPRO_DEV_BASE_URL=https://baac9a029ea84f36ba28c023fed4fc5f.hd.takin.cc
# SHOPRO_DEV_BASE_URL = https://api.htysapp.com
SHOPRO_DEV_BASE_URL=https://baac9a029ea84f36ba28c023fed4fc5f.hd.takin.cc
# 后端接口 - 正式环境(通过 process.env.NODE_ENV != development)
SHOPRO_BASE_URL=https://api.htysapp.com
+301 -401
View File
@@ -1,456 +1,356 @@
<!DOCTYPE html>
<html style="font-size:50px">
<head>
<html style="font-size: 50px">
<head>
<title>荷塘月色 - app下载</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=no">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta name="description" content="高端私密真实可靠的聊天交友APP。平台通过人工审核和会员制度,保证用户质量。快来邂逅附近的女神吧!">
<meta name="keywords" content="荷塘月色,聊天,交友">
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta
name="description"
content="高端私密真实可靠的聊天交友APP。平台通过人工审核和会员制度,保证用户质量。快来邂逅附近的女神吧!"
/>
<meta name="keywords" content="荷塘月色,聊天,交友" />
<script src="./device-fingerprint.js"></script>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/vconsole@3.4.0/dist/vconsole.min.js"></script> -->
<!-- <script src="https://res.sharetrace.com/sharetrace.min.js"></script> -->
<style type="text/css">
a,
abbr,
acronym,
address,
applet,
article,
aside,
audio,
b,
big,
blockquote,
body,
canvas,
caption,
center,
cite,
code,
dd,
del,
details,
dfn,
div,
dl,
dt,
em,
embed,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
header,
html,
i,
iframe,
img,
input,
ins,
kbd,
label,
legend,
li,
mark,
menu,
nav,
object,
ol,
output,
p,
pre,
q,
ruby,
s,
samp,
section,
small,
span,
strike,
strong,
sub,
summary,
sup,
table,
tbody,
td,
tfoot,
th,
thead,
time,
tr,
tt,
u,
ul,
var,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: 400;
vertical-align: baseline;
}
a,
abbr,
acronym,
address,
applet,
article,
aside,
audio,
b,
big,
blockquote,
body,
canvas,
caption,
center,
cite,
code,
dd,
del,
details,
dfn,
div,
dl,
dt,
em,
embed,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
header,
html,
i,
iframe,
img,
input,
ins,
kbd,
label,
legend,
li,
mark,
menu,
nav,
object,
ol,
output,
p,
pre,
q,
ruby,
s,
samp,
section,
small,
span,
strike,
strong,
sub,
summary,
sup,
table,
tbody,
td,
tfoot,
th,
thead,
time,
tr,
tt,
u,
ul,
var,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: 400;
vertical-align: baseline;
}
</style>
<style type="text/css">
#txt-title{
color: white;
}
@media (min-width: 765px) {
.pc {
display: block
}
#txt-title {
color: white;
}
.msite {
display: none
}
.msite-footer {
display: none !important;
}
@keyframes myfirst {
0% {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%;
}
@media (max-width: 764px) {
.pc {
display: none;
font-size: 14px
}
.msite {
display: block;
font-size: .24rem
}
.msite-footer {
display: block;
font-size: .24rem
}
50% {
background: url(bg-image.png) 100% no-repeat;
background-size: auto 100%;
}
@keyframes myfirst {
0% {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%
}
to {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%;
}
}
50% {
background: url(bg-image.png) 100% no-repeat;
background-size: auto 100%
}
to {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%
}
@-webkit-keyframes myfirst {
0% {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%;
}
@-webkit-keyframes myfirst {
0% {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%
}
50% {
background: url(bg-image.png) 100% no-repeat;
background-size: auto 100%
}
to {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%
}
50% {
background: url(bg-image.png) 100% no-repeat;
background-size: auto 100%;
}
.msite {
display: block;
font-size: .24rem;
to {
background: url(bg-image.png) 0 no-repeat;
background-size: auto 100%;
}
}
.container {
position: fixed;
width: 100%;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: url(./bg-image.png) 100% no-repeat;
background-size: auto 100%;
text-align: center;
-webkit-animation: myfirst 50s linear infinite;
animation: myfirst 50s linear infinite;
}
.msite {
display: block;
font-size: 0.24rem;
}
.top-box {
text-align: center;
padding-top: 1.7rem;
}
.container {
position: fixed;
width: 100%;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: url(./bg-image.png) 100% no-repeat;
background-size: auto 100%;
text-align: center;
-webkit-animation: myfirst 50s linear infinite;
animation: myfirst 50s linear infinite;
}
.logo-ico {
width: 1.4rem;
height: auto;
border-radius: 10px;
}
.top-box {
text-align: center;
padding-top: 1.7rem;
}
.sub-title {
font-size: .64rem;
font-weight: 700;
line-height: .5rem;
letter-spacing: 3px;
margin-top: .2rem;
color: white;
}
.logo-ico {
width: 1.4rem;
height: auto;
border-radius: 10px;
}
.top-box p {
font-size: .36rem;
font-family: PingFang-SC-Bold, PingFang-SC;
font-weight: 700;
color: #fff;
line-height: .5rem;
letter-spacing: 3px;
margin-top: .2rem;
}
.sub-title {
font-size: 0.64rem;
font-weight: 700;
line-height: 0.5rem;
letter-spacing: 3px;
margin-top: 0.2rem;
color: white;
}
.top-box .sagn-ico {
width: 5.89rem;
height: auto;
margin-top: .49rem;
}
.top-box p {
font-size: 0.36rem;
font-family: PingFang-SC-Bold, PingFang-SC;
font-weight: 700;
color: #fff;
line-height: 0.5rem;
letter-spacing: 3px;
margin-top: 0.2rem;
}
.download-btn {
position: relative;
bottom: -52px;
display: flex;
justify-content: center;
}
.top-box .sagn-ico {
width: 5.89rem;
height: auto;
margin-top: 0.49rem;
}
.download-btn-real {
margin: 0 auto;
width: 5.6rem;
height: .9rem;
background: linear-gradient(-89deg, #EC6978, #F2AC66);
box-shadow: 0.02rem 0.02rem 0.3rem 0 rgb(255 155 116 / 46%);
line-height: .9rem;
border-radius: .45rem;
border: 1px solid #faa36a;
text-align: center;
font-size: .32rem;
font-family: PingFang-SC-Bold, PingFang-SC;
font-weight: 700;
color: #fff;
letter-spacing: 4px;
text-shadow: 1px 1px 1px rgb(213 86 29 / 55%);
}
.download-btn {
position: relative;
bottom: -52px;
display: flex;
justify-content: center;
}
.footer {
width: 100%;
color: #fff;
text-align: center;
position: fixed;
bottom: .21rem;
line-height: .4rem;
}
.download-btn-real {
margin: 0 auto;
width: 5.6rem;
height: 0.9rem;
background: linear-gradient(-89deg, #ec6978, #f2ac66);
box-shadow: 0.02rem 0.02rem 0.3rem 0 rgb(255 155 116 / 46%);
line-height: 0.9rem;
border-radius: 0.45rem;
border: 1px solid #faa36a;
text-align: center;
font-size: 0.32rem;
font-family: PingFang-SC-Bold, PingFang-SC;
font-weight: 700;
color: #fff;
letter-spacing: 4px;
text-shadow: 1px 1px 1px rgb(213 86 29 / 55%);
}
.footer-container p {
margin-bottom: 0;
}
.footer {
width: 100%;
color: #fff;
text-align: center;
position: fixed;
bottom: 0.21rem;
line-height: 0.4rem;
}
.footer-container p {
margin-bottom: 0;
}
</style>
</head>
</head>
<body>
<body>
<div class="msite container">
<div class="top-box">
<img class="logo-ico" src="./logo.png" alt="logo">
<p id='txt-title'>荷塘月色</p>
<div class="sub-title">同城交友平台</div>
</div>
<div class="download-btn" id="downloadButton" style="visibility: visible;" onclick="download()">
<div class="download-btn-real">下载安装</div>
</div>
<div id="footer" class="msite footer msite-footer">
<div class="footer-container">
<p>更新时间:&nbsp;2025年12月12日</p>
</div>
<div class="top-box">
<img class="logo-ico" src="./logo.png" alt="logo" />
<p id="txt-title">荷塘月色</p>
<div class="sub-title">同城交友平台</div>
</div>
<div class="download-btn" onclick="download()">
<div class="download-btn-real">下载安装</div>
</div>
<div class="footer">
<div class="footer-container">
<p>更新时间:&nbsp;2025年12月12日</p>
</div>
</div>
</div>
</div>
</body>
</body>
<script type="text/javascript">
// var vConsole = new VConsole();
var app = ''
var param = ''
var device = ''
var isInitSuccess = false;
<script type="text/javascript">
const IOS_DOWNLOAD_URL =
'https://apps.apple.com/cn/app/%E8%8D%B7%E5%A1%98%E5%8A%A9%E6%89%8B/id6764264313';
const ANDROID_DOWNLOAD_URL = 'http://download.htysapp.com/荷塘月色.apk';
function isWeiXin() {
var ua = window.navigator.userAgent.toLowerCase();
if (ua.indexOf('micromessenger') !== -1) {
return true;
} else {
return false;
}
return /MicroMessenger/i.test(navigator.userAgent);
}
function judgeClient() {
client = '';
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
//判断iPhone|iPad|iPod|iOS
client = 'ios';
} else if (/(Android)/i.test(navigator.userAgent)) {
//判断Android
client = 'android';
} else {
client = 'pc';
}
return client;
}
// 点击下载安装包
function _download() {
client = judgeClient()
if (client == 'android') {
const ua = navigator.userAgent
var src = 'http://download.htysapp.com/荷塘月色.apk';
var form = document.createElement('form');
form.action = src;
document.getElementsByTagName('body')[0].appendChild(form);
form.submit()
return
}
if (client == 'ios') {
// window.open("https://apps.apple.com/cn/app/id6444849074", "_self")
alert("暂不支持")
}
const userAgent = navigator.userAgent;
// iPadOS 13+ 使用桌面模式时 UA 类似 macOS,需要结合触摸点数判断。
const isIOS =
/iPhone|iPad|iPod/i.test(userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isIOS) {
return 'ios';
}
if (/Android/i.test(userAgent)) {
return 'android';
}
return 'pc';
}
// 根据当前设备跳转到对应的应用下载地址。
function download() {
client = judgeClient()
if (isWeiXin() && client != 'ios') {
alert('请点击右上角按钮选择从浏览器打开此页面。');
return;
}
if (isInitSuccess && client == 'ios') {
// ShareTrace.download();
alert("暂不支持")
} else {
//如果初始化失败建议自己指定下载地址,虽然不能获取安装参数但是不会影响用户的下载
_download()
}
}
const client = judgeClient();
//获取url中的参数
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
//构造一个含有目标参数的正则表达式对象
var r = window.location.search.substr(1).match(reg);
//匹配目标参数
if (r != null)
return unescape(r[2]);
return null;
//返回参数值
}
if (client === 'ios') {
window.location.href = IOS_DOWNLOAD_URL;
return;
}
function initShareSdk() {
key = '1e778edaea34f45b'
// 微信内置浏览器通常会拦截 Android APK 下载。
if (client === 'android' && isWeiXin()) {
alert('请点击右上角按钮选择从浏览器打开此页面。');
return;
}
client = judgeClient()
if (client == 'ios') {
key = '193c5fe0456ed92a'
}
ShareTrace.init({
appkey: key,
param: param,
success: function () {
console.log("init success...")
isInitSuccess = true;
if (!app || client == 'ios') {
download();
// window.open("https://a.app.qq.com/o/simple.jsp?pkgname=com.xxdd.imsearch", "_self")
}
},
error: function (msg) {
console.log(msg)
}
});
}
if (client === 'android') {
window.location.href = ANDROID_DOWNLOAD_URL;
return;
}
function generateRandomNumber() {
const options = [2, 3, 5, 6];
const randomIndex = Math.floor(Math.random() * options.length);
const randomNumber = options[randomIndex];
return randomNumber;
alert('请使用手机浏览器下载安装。');
}
function getApiUrl(app) {
if (app == 'yyq') {
return 'https://api.newroad.top/api/v1/publicShare/setKey'
}
return 'https://api.xiaoquanapp.top/api/v1/publicShare/setKey'
if (app === 'yyq') {
return 'https://api.newroad.top/api/v1/publicShare/setKey';
}
return 'https://api.xiaoquanapp.top/api/v1/publicShare/setKey';
}
async function sendUid(uid) {
const deviceFingerprint = DeviceFingerprint.init({});
// 等待依赖加载完成
await new Promise((resolve) => setTimeout(resolve, 500));
const fingerprints = await deviceFingerprint.getWebGLFingerprint();
console.log(fingerprints);
$.ajax({
url: getApiUrl(app),
type: 'POST',
data: {
key: fingerprints,
value: uid
}
// 记录设备指纹与分享用户的对应关系,用于渠道归因。
async function sendUid(app, uid) {
const deviceFingerprint = DeviceFingerprint.init({});
// 等待指纹依赖完成初始化。
await new Promise((resolve) => setTimeout(resolve, 500));
const fingerprints = await deviceFingerprint.getWebGLFingerprint();
$.ajax({
url: getApiUrl(app),
type: 'POST',
data: {
key: fingerprints,
value: uid,
},
});
}
(function () {
const urlParams = new URLSearchParams(window.location.search);
const app = urlParams.get('app') || '';
const uid = urlParams.get('uid');
if (uid) {
sendUid(app, uid).catch(function (error) {
console.error('设备指纹上报失败:', error);
});
}
(async function ($) {
var tmp = getUrlParam('app')
if (tmp != null) {
app = tmp;
}
var d = getUrlParam('device')
if (d != null) {
device = d;
}
var uid = getUrlParam('uid')
if (uid != null) {
param = 'uid=' + uid;
}
document.title = '荷塘月色-高端约会'
document.getElementById('txt-title').innerHTML = '荷塘月色'
initShareSdk();
sendUid(uid);
}
)(jQuery);
</script>
<script>
}
})();
</script>
<script>
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?8841fecf6f4a06974ef8797421c60dc0";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
}
)();
</script>
</html>
var hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?8841fecf6f4a06974ef8797421c60dc0';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</html>
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -58,7 +58,7 @@
"lodash-es": "^4.17.21",
"luch-request": "^3.0.8",
"pinia": "^2.0.33",
"pinia-plugin-persist-uni": "^1.2.0",
"pinia-plugin-persistedstate": "3.2.3",
"pinyin-pro": "^3.23.1",
"quill": "^1.3.7",
"quill-image-resize-mp": "^3.0.1",
+7 -29
View File
@@ -77,9 +77,9 @@ importers:
pinia:
specifier: ^2.0.33
version: 2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5))
pinia-plugin-persist-uni:
specifier: ^1.2.0
version: 1.3.2(pinia@2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5)))(vue@3.5.30(typescript@4.9.5))
pinia-plugin-persistedstate:
specifier: 3.2.3
version: 3.2.3(pinia@2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5)))
pinyin-pro:
specifier: ^3.23.1
version: 3.28.0
@@ -3266,15 +3266,10 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pinia-plugin-persist-uni@1.3.2:
resolution: {integrity: sha512-Z47MWlAI3dnm9xENwodYyT6B3TMDuwFK7MHHymwQyrTchuhG2Yu26w7bYlIap7df0fCwNCmAo0n7n1RK3Ute+w==}
pinia-plugin-persistedstate@3.2.3:
resolution: {integrity: sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==}
peerDependencies:
'@vue/composition-api': ^1.0.0
pinia: '>2.0.0'
vue: ^2.0.0 || >=3.0.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
pinia: ^2.0.0
pinia@2.3.1:
resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
@@ -3968,17 +3963,6 @@ packages:
terser:
optional: true
vue-demi@0.12.5:
resolution: {integrity: sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
@@ -8435,11 +8419,9 @@ snapshots:
pify@2.3.0: {}
pinia-plugin-persist-uni@1.3.2(pinia@2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5)))(vue@3.5.30(typescript@4.9.5)):
pinia-plugin-persistedstate@3.2.3(pinia@2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5))):
dependencies:
pinia: 2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5))
vue: 3.5.30(typescript@4.9.5)
vue-demi: 0.12.5(vue@3.5.30(typescript@4.9.5))
pinia@2.3.1(typescript@4.9.5)(vue@3.5.30(typescript@4.9.5)):
dependencies:
@@ -9207,10 +9189,6 @@ snapshots:
sass: 1.78.0
terser: 5.46.1
vue-demi@0.12.5(vue@3.5.30(typescript@4.9.5)):
dependencies:
vue: 3.5.30(typescript@4.9.5)
vue-demi@0.14.10(vue@3.5.30(typescript@4.9.5)):
dependencies:
vue: 3.5.30(typescript@4.9.5)
-1
View File
@@ -536,7 +536,6 @@ export default {
);
// #endif
// #ifndef MP-WEIXIN
this.$mountStore();
this.closeSplashscreen(1000); // 延迟1s,避免用户看到页面跳转
let loginInfo = uni.getStorageSync('loginInfo');
if (!loginInfo || !loginInfo.refreshToken) {
+1 -1
View File
@@ -546,4 +546,4 @@ export default defineStore('chatStore', {
return null;
}
}
});
});
+17 -11
View File
@@ -5,27 +5,27 @@ import { TERMINAL_TYPE } from '../common/enums.js'
export default defineStore('friendStore', {
state: () => {
return {
friendMap: new Map(),
friendMap: {},
timer: null
}
},
actions: {
setFriends(friends) {
this.friendMap.clear();
friends.forEach(f => this.friendMap.set(f.id, f))
this.friendMap = {};
friends.forEach(f => this.friendMap[f.id] = f)
},
updateFriend(friend) {
let f = this.findFriend(friend.id);
friend.online = f.online;
friend.onlineWeb = f.onlineWeb;
friend.onlineApp = f.onlineApp;
this.friendMap.set(friend.id, friend)
this.friendMap[friend.id] = friend
},
removeFriend(id) {
this.friendMap.get(id).deleted = true;
this.friendMap[id].deleted = true;
},
addFriend(friend) {
this.friendMap.set(friend.id, friend)
this.friendMap[friend.id] = friend
},
setOnlineStatus(onlineTerminals) {
this.friends.forEach((f) => {
@@ -59,10 +59,10 @@ export default defineStore('friendStore', {
}, 30000)
},
setDnd(id, isDnd) {
this.friendMap.get(id).isDnd = isDnd;
this.friendMap[id].isDnd = isDnd;
},
clear() {
this.friendMap.clear();
this.friendMap = {};
clearTimeout(this.timer);
this.timer = null;
},
@@ -83,14 +83,20 @@ export default defineStore('friendStore', {
},
getters: {
friends: (state) => {
return Array.from(state.friendMap.values());
return Object.values(state.friendMap);
},
findFriend: (state) => (userId) => {
return state.friendMap.get(userId);
return state.friendMap[userId];
},
isFriend: (state) => (userId) => {
let f = state.findFriend(userId)
return f && !f.deleted
}
},
persist: {
paths: ['friendMap'],
afterRestore: ({ store }) => {
store.timer = null;
}
}
})
})
+13 -12
View File
@@ -4,28 +4,28 @@ import http from '@/common/request';
export default defineStore('groupStore', {
state: () => {
return {
groupMap: new Map()
groupMap: {}
}
},
actions: {
setGroups(groups) {
this.groupMap.clear();
groups.forEach(g => this.groupMap.set(g.id, g))
this.groupMap = {};
groups.forEach(g => this.groupMap[g.id] = g)
},
addGroup(group) {
this.groupMap.set(group.id, group);
this.groupMap[group.id] = group;
},
removeGroup(id) {
this.groupMap.get(id).quit = true;
this.groupMap[id].quit = true;
},
updateGroup(group) {
this.groupMap.set(group.id, group);
this.groupMap[group.id] = group;
},
setDnd(id, isDnd) {
this.groupMap.get(id).isDnd = isDnd;
this.groupMap[id].isDnd = isDnd;
},
clear() {
this.groupMap.clear();
this.groupMap = {};
},
loadGroup() {
return new Promise((resolve, reject) => {
@@ -43,14 +43,15 @@ export default defineStore('groupStore', {
},
getters: {
groups: (state) => {
return Array.from(state.groupMap.values());
return Object.values(state.groupMap);
},
findGroup: (state) => (id) => {
return state.groupMap.get(id);
return state.groupMap[id];
},
isGroup: (state) => (id) => {
let group = state.findGroup(id);
return group && !group.quit
}
}
})
},
persist: true
})
+1
View File
@@ -30,4 +30,5 @@ export default defineStore('userStore', {
});
},
},
persist: true,
});
+5 -31
View File
@@ -34,37 +34,11 @@ const emoTextList = [
'享受',
'鼻涕',
'快乐',
'雀跃',
'微笑',
'贪婪',
'红心',
'粉心',
'星星',
'大火',
'眼睛',
'音符',
'叹号',
'问号',
'绿叶',
'燃烧',
'喇叭',
'警告',
'信封',
'房子',
'礼物',
'点赞',
'举手',
'拍手',
'点头',
'摇头',
'偷瞄',
'庆祝',
'疾跑',
'打滚',
'惊吓',
'起跳',
'雀跃', '微笑',
'贪婪', '红心', '粉心', '星星', '大火', '眼睛', '音符', "叹号", "问号", "绿叶",
"燃烧", "喇叭", "警告", "信封", "房子", "礼物", "点赞", "举手", "拍手", "点头",
"摇头", "偷瞄", "庆祝", "疾跑", "打滚", "惊吓", "起跳"
];
const regex = /\#[\u4E00-\u9FA5]{1,3}\;/gi;
let containEmoji = (content) => {
@@ -88,7 +62,7 @@ let transform = (content, extClass) => {
let textToPath = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
return EMO_URL + idx + '.png';
return EMO_URL + idx + '.gif';
};
export default {
+88 -23
View File
@@ -1,7 +1,9 @@
import CHAT_ENV from '@/chat-env.js';
// 请求队列
let requestList = [];
const TOKEN_REFRESH_THRESHOLD_SECONDS = 5 * 60;
// 等待同一次 token 刷新结果的请求队列
let refreshWaitList = [];
// 是否正在刷新中
let isRefreshToken = false;
@@ -20,36 +22,41 @@ const request = (options) => {
timeout: options.timeout || 3000,
async success(res) {
if (res.data.code == 200) {
const currentLoginInfo = uni.getStorageSync('loginInfo');
if (isAccessTokenExpiringSoon(currentLoginInfo?.accessToken)) {
try {
await refreshAccessToken(currentLoginInfo);
} catch (error) {
console.log('刷新token失败', error);
getApp().$vm.exit();
return reject(error);
}
}
return resolve(res.data.data);
} else if (res.data.code == 400) {
console.log('code 400');
getApp().$vm.exit();
} else if (res.data.code == 401) {
console.log('token失效,尝试重新获取');
if (isRefreshToken) {
// 正在刷新token,把其他请求存起来
requestList.push(() => {
resolve(request(options));
});
return;
const currentLoginInfo = uni.getStorageSync('loginInfo');
// 当前请求使用的是旧 token,直接用已刷新的 token 重试。
if (
currentLoginInfo?.accessToken &&
currentLoginInfo.accessToken !== header.accessToken
) {
return resolve(request(options));
}
isRefreshToken = true;
// 发送请求, 进行刷新token操作, 获取新的token
const res = await reqRefreshToken(loginInfo);
if (!res || res.data.code != 200) {
requestList = [];
isRefreshToken = false;
console.log('刷新token失败');
try {
await refreshAccessToken(currentLoginInfo);
// 重新发送刚才的请求
return resolve(request(options));
} catch (error) {
console.log('刷新token失败', error);
getApp().$vm.exit();
return;
return reject(error);
}
let newInfo = res.data.data;
uni.setStorageSync('loginInfo', newInfo);
requestList.forEach((cb) => cb());
requestList = [];
isRefreshToken = false;
// 重新发送刚才的请求
return resolve(request(options));
} else {
uni.showToast({
icon: 'none',
@@ -71,6 +78,64 @@ const request = (options) => {
});
};
const isAccessTokenExpiringSoon = (accessToken) => {
if (!accessToken) {
return false;
}
try {
const payload = accessToken.split('.')[1];
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
const bytes = new Uint8Array(uni.base64ToArrayBuffer(paddedBase64));
let decodedPayload = '';
bytes.forEach((byte) => {
decodedPayload += String.fromCharCode(byte);
});
const expMatch = decodedPayload.match(/"exp"\s*:\s*(\d+)/);
if (!expMatch) {
return false;
}
const remainingSeconds = Number(expMatch[1]) - Math.floor(Date.now() / 1000);
console.log("remainingSeconds:", remainingSeconds);
return remainingSeconds <= TOKEN_REFRESH_THRESHOLD_SECONDS;
} catch (error) {
console.warn('解析 accessToken 过期时间失败', error);
return false;
}
};
const refreshAccessToken = (loginInfo) => {
if (!loginInfo?.refreshToken) {
return Promise.reject(new Error('缺少 refreshToken'));
}
if (isRefreshToken) {
return new Promise((resolve, reject) => {
refreshWaitList.push({ resolve, reject });
});
}
isRefreshToken = true;
return reqRefreshToken(loginInfo)
.then((res) => {
if (!res || res.data.code != 200) {
throw res?.data || new Error('刷新token失败');
}
const newInfo = res.data.data;
uni.setStorageSync('loginInfo', newInfo);
refreshWaitList.forEach(({ resolve }) => resolve(newInfo));
return newInfo;
})
.catch((error) => {
refreshWaitList.forEach(({ reject }) => reject(error));
throw error;
})
.finally(() => {
refreshWaitList = [];
isRefreshToken = false;
});
};
const reqRefreshToken = (loginInfo) => {
return new Promise(function (resolve, reject) {
uni.request({
+99 -106
View File
@@ -1,13 +1,6 @@
<template>
<view class="head-image none-pointer-events" @click="showUserInfo($event)" :title="name">
<image
class="avatar-image"
v-if="url"
:src="url"
:style="avatarImageStyle"
lazy-load="true"
mode="aspectFill"
/>
<image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true" mode="aspectFill" />
<view class="avatar-text" v-if="!url" :style="avatarTextStyle">
{{ name?.substring(0, 1).toUpperCase() }}
</view>
@@ -16,117 +9,117 @@
</template>
<script>
export default {
name: 'head-image',
data() {
return {
colors: ['#5daa31', '#c7515a', '#e03697', '#85029b', '#c9b455', '#326eb6'],
};
export default {
name: 'head-image',
data() {
return {
colors: ['#5daa31', '#c7515a', '#e03697', '#85029b', '#c9b455', '#326eb6'],
};
},
props: {
id: {
type: Number,
},
props: {
id: {
type: Number,
},
size: {
type: [Number, String],
default: 'default',
},
url: {
type: String,
},
name: {
type: String,
default: null,
},
radius: {
type: String,
default: '50%',
},
online: {
type: Boolean,
default: false,
},
size: {
type: [Number, String],
default: 'default',
},
methods: {
showUserInfo(e) {
if (this.id && this.id > 0) {
uni.navigateTo({
url: '/pages/message/user-info?id=' + this.id,
});
}
},
url: {
type: String,
},
computed: {
_size() {
if (typeof this.size === 'number') {
return this.size;
} else if (typeof this.size === 'string') {
return {
default: 96,
small: 84,
smaller: 72,
mini: 60,
minier: 48,
lage: 108,
lager: 120,
}[this.size];
}
},
avatarImageStyle() {
return `width:${this._size}rpx;
name: {
type: String,
default: null,
},
radius: {
type: String,
default: '50%',
},
online: {
type: Boolean,
default: false,
},
},
methods: {
showUserInfo(e) {
if (this.id && this.id > 0) {
uni.navigateTo({
url: '/pages/message/user-info?id=' + this.id,
});
}
},
},
computed: {
_size() {
if (typeof this.size === 'number') {
return this.size;
} else if (typeof this.size === 'string') {
return {
default: 96,
small: 84,
smaller: 72,
mini: 60,
minier: 48,
lage: 108,
lager: 120,
}[this.size];
}
},
avatarImageStyle() {
return `width:${this._size}rpx;
height:${this._size}rpx;`;
},
avatarTextStyle() {
return `width: ${this._size}rpx;
},
avatarTextStyle() {
return `width: ${this._size}rpx;
height:${this._size}rpx;
background: linear-gradient(145deg,#ffffff20 25%,#00000060),${this.textColor};
font-size:${this._size * 0.45}rpx;
border-radius: ${this.radius};
`;
},
textColor() {
if (!this.name) {
return '#fff';
}
let hash = 0;
for (var i = 0; i < this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
return this.colors[hash % this.colors.length];
},
},
};
textColor() {
if (!this.name) {
return '#fff';
}
let hash = 0;
for (var i = 0; i < this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
return this.colors[hash % this.colors.length];
},
},
};
</script>
<style scoped lang="scss">
.head-image {
.head-image {
position: relative;
cursor: pointer;
.avatar-image {
position: relative;
cursor: pointer;
.avatar-image {
position: relative;
overflow: hidden;
border-radius: 50%;
vertical-align: bottom;
}
.avatar-text {
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.online {
position: absolute;
right: -10%;
bottom: 0;
width: 24rpx;
height: 24rpx;
background: limegreen;
border-radius: 50%;
border: 6rpx solid white;
}
overflow: hidden;
border-radius: 50%;
vertical-align: bottom;
}
.avatar-text {
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.online {
position: absolute;
right: -10%;
bottom: 0;
width: 24rpx;
height: 24rpx;
background: limegreen;
border-radius: 50%;
border: 6rpx solid white;
}
}
</style>
+12 -10
View File
@@ -23,8 +23,8 @@ import { initH5HostShell } from '@/sheep/platform/h5-shell';
window.Quill = Quill;
window.ImageResize = { default: ImageResize };
// 调试器
import VConsole from 'vconsole';
new VConsole();
// import VConsole from 'vconsole';
// new VConsole();
initH5HostShell();
patchH5Navigation();
// #endif
@@ -48,7 +48,7 @@ import switchBar from '@/components/bar/switch-bar';
export function createApp() {
const app = createSSRApp(App);
setupPinia(app);
const pinia = setupPinia(app);
// 聊天common模块挂载为全局属性
app.config.globalProperties.$http = request;
@@ -67,14 +67,16 @@ export function createApp() {
app.component('btn-bar', btnBar);
app.component('switch-bar', switchBar);
// $mountStore挂载为全局属性,等app初始化时再挂载chat-store到全局属性,方便页面中直接this使用
app.config.globalProperties.$mountStore = () => {
app.config.globalProperties.chatStore = useChatStore();
app.config.globalProperties.friendStore = useFriendStore();
app.config.globalProperties.groupStore = useGroupStore();
app.config.globalProperties.configStore = useConfigStore();
app.config.globalProperties.userStore = useUserStore();
// 在页面首次渲染前挂载聊天仓库,避免刷新页面时全局 store 尚未可用。
const mountStores = () => {
app.config.globalProperties.chatStore = useChatStore(pinia);
app.config.globalProperties.friendStore = useFriendStore(pinia);
app.config.globalProperties.groupStore = useGroupStore(pinia);
app.config.globalProperties.configStore = useConfigStore(pinia);
app.config.globalProperties.userStore = useUserStore(pinia);
};
app.config.globalProperties.$mountStore = mountStores;
mountStores();
return {
app,
+105 -120
View File
@@ -6,13 +6,9 @@
<view>
<view class="commont-from-wrap">
<!-- 评价商品 -->
<s-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.payPrice"
:num="item.count"
/>
<s-goods-item :img="item.picUrl" :title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')" :price="item.payPrice"
:num="item.count" />
</view>
<view class="form-item">
@@ -27,23 +23,12 @@
</view>
<!-- 评价 -->
<view class="area-box">
<uni-easyinput
:inputBorder="false"
type="textarea"
maxlength="120"
autoHeight
v-model="state.commentList[index].content"
placeholder="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~"
/>
<uni-easyinput :inputBorder="false" type="textarea" maxlength="120" autoHeight
v-model="state.commentList[index].content" placeholder="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~" />
<view class="img-box">
<s-uploader
v-model:url="state.commentList[index].images"
fileMediatype="image"
limit="9"
mode="grid"
<s-uploader v-model:url="state.commentList[index].images" fileMediatype="image" limit="9" mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
@success="(payload) => uploadSuccess(payload, index)"
/>
@success="(payload) => uploadSuccess(payload, index)" />
</view>
</view>
<view class="checkbox-container">
@@ -69,122 +54,122 @@
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
orderInfo: {},
commentList: [],
id: null,
});
const state = reactive({
orderInfo: {},
commentList: [],
id: null,
});
/**
* 切换是否匿名
*
* @param commentIndex 当前评论下标
* @param event 复选框事件
*/
function toggleAnonymous(commentIndex, event) {
state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
/**
* 切换是否匿名
*
* @param commentIndex 当前评论下标
* @param event 复选框事件
*/
function toggleAnonymous(commentIndex, event) {
state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
}
/**
* 发布评论
*
* @returns {Promise<void>}
*/
async function onSubmit() {
// 顺序提交评论
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
// 都评论好,返回
sheep.$router.back();
}
/**
* 发布评论
*
* @returns {Promise<void>}
*/
async function onSubmit() {
// 顺序提交评论
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
// 都评论好,返回
sheep.$router.back();
/**
* 图片添加到表单
*
* @param payload 上传成功后的回调数据
* @param commentIndex 当前评论的下标
*/
function uploadSuccess(payload, commentIndex) {
state.commentList[commentIndex].picUrls = payload.tempFilePaths;
}
onLoad(async (options) => {
if (!options.id) {
sheep.$helper.toast(`缺少订单信息,请检查`);
return;
}
state.id = options.id;
/**
* 图片添加到表单
*
* @param payload 上传成功后的回调数据
* @param commentIndex 当前评论的下标
*/
function uploadSuccess(payload, commentIndex) {
state.commentList[commentIndex].picUrls = payload.tempFilePaths;
const { code, data } = await OrderApi.getOrderDetail(state.id);
if (code !== 0) {
sheep.$helper.toast('无待评价订单');
return;
}
onLoad(async (options) => {
if (!options.id) {
sheep.$helper.toast(`缺少订单信息,请检查`);
return;
}
state.id = options.id;
const { code, data } = await OrderApi.getOrderDetail(state.id);
if (code !== 0) {
sheep.$helper.toast('无待评价订单');
return;
}
// 处理评论
data.items.forEach((item) => {
state.commentList.push({
anonymous: false,
orderItemId: item.id,
descriptionScores: 5,
benefitScores: 5,
content: '',
picUrls: [],
});
// 处理评论
data.items.forEach((item) => {
state.commentList.push({
anonymous: false,
orderItemId: item.id,
descriptionScores: 5,
benefitScores: 5,
content: '',
picUrls: [],
});
state.orderInfo = data;
});
state.orderInfo = data;
});
</script>
<style lang="scss" scoped>
// 评价商品
.goods-card {
margin: 10rpx 0;
padding: 20rpx;
background: #fff;
// 评价商品
.goods-card {
margin: 10rpx 0;
padding: 20rpx;
background: #fff;
}
// 评论,选择图片
.form-item {
background: #fff;
.star-box {
height: 100rpx;
padding: 0 25rpx;
}
// 评论,选择图片
.form-item {
background: #fff;
.star-box {
height: 100rpx;
padding: 0 25rpx;
}
.star-title {
font-weight: 600;
}
.star-title {
font-weight: 600;
}
}
.area-box {
width: 690rpx;
min-height: 306rpx;
background: rgba(249, 250, 251, 1);
border-radius: 20rpx;
padding: 28rpx;
margin: auto;
.area-box {
width: 690rpx;
min-height: 306rpx;
background: rgba(249, 250, 251, 1);
border-radius: 20rpx;
padding: 28rpx;
margin: auto;
.img-box {
margin-top: 20rpx;
}
.img-box {
margin-top: 20rpx;
}
}
.checkbox-container {
padding: 10rpx;
}
.checkbox-container {
padding: 10rpx;
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
</style>
+29 -5
View File
@@ -1,5 +1,5 @@
<template>
<view class="relative h-screen w-screen overflow-hidden">
<view class="login-page fixed inset-0 w-screen overflow-hidden" :style="pageStyle">
<!-- <image class="absolute inset-0 h-full w-full" src="@/static/login-bg.png" mode="aspectFill" /> -->
<view class="relative z-10 flex flex-col items-center px-40rpx">
<view class="mt-300rpx mb-200rpx text-center">
@@ -11,16 +11,19 @@
<view class="relative flex w-full items-center">
<!-- #ifdef MP-WEIXIN -->
<input class="h-100rpx w-full rounded-50rpx bg-color-light px-40rpx text-32rpx text-white" type="number"
placeholder="请输入账号" placeholder-style="color: rgba(255, 255, 255, 0.6)" v-model="phone" maxlength="11" />
placeholder="请输入账号" placeholder-style="color: rgba(255, 255, 255, 0.6)" v-model="phone" maxlength="11"
:adjust-position="false" />
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<input class="h-100rpx w-full rounded-50rpx bg-color-light px-40rpx text-32rpx text-white" type="number"
placeholder="请输入手机号" placeholder-style="color: rgba(255, 255, 255, 0.6)" v-model="phone" maxlength="11" />
placeholder="请输入手机号" placeholder-style="color: rgba(255, 255, 255, 0.6)" v-model="phone" maxlength="11"
:adjust-position="false" />
<!-- #endif -->
</view>
<view class="relative flex w-full items-center">
<input class="h-100rpx w-full rounded-50rpx bg-color-light px-40rpx text-32rpx text-white" type="number"
placeholder="验证码" placeholder-style="color: rgba(255, 255, 255, 0.6)" v-model="smsCode" maxlength="4" />
placeholder="验证码" placeholder-style="color: rgba(255, 255, 255, 0.6)" v-model="smsCode" maxlength="4"
:adjust-position="false" />
<view class="absolute right-20rpx text-12px flex-row-center z-200">
<wd-button @click.stop.prevent="handleGetCode" :disabled="isTiming" size="medium">
<text>获取验证码</text><text v-if="isTiming"> ({{ countDown }}s)</text>
@@ -75,7 +78,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import AuthUtil from '@/sheep/api/member/auth';
@@ -91,8 +94,20 @@ const isTiming = ref(false);
const countDown = ref(60);
const isAgree = ref(false);
const nodes = ref(''); // 同意协议的文本
const loginPageHeight = ref('100vh');
const pageStyle = computed(() => ({ height: loginPageHeight.value }));
// h5端进入输入态时锁定页面高度,避免键盘把整页顶起
const syncLoginPageHeight = () => {
// #ifdef H5
if (typeof window !== 'undefined') {
loginPageHeight.value = `${window.innerHeight}px`;
}
// #endif
};
onShow(() => {
syncLoginPageHeight();
// #ifndef MP-WEIXIN
if (!phone.value) {
const lastLoginPhone = uni.getStorageSync('last_login_phone');
@@ -187,3 +202,12 @@ async function alert(type, title) {
});
}
</script>
<style lang="scss">
.login-page {
// #ifdef H5
height: 100vh;
min-height: 100vh;
// #endif
}
</style>
+51 -12
View File
@@ -3,12 +3,14 @@
<wd-config-provider theme="dark">
<wd-navbar fixed safeAreaInsetTop :bordered="false">
<template #left>
<navigator open-type="navigateBack" hover-class="navigator-hover">
<uni-icons type="back" size="30" color="#D29E52"></uni-icons>
</navigator>
<view class="flex items-center" :style="navUserInfoStyle">
<image class="size-70rpx bg-#333 rounded-full mr-1" :src="avatar" mode="aspectFill" />
<text class="truncate text-sm">{{ title }}</text>
<view class="flex items-center">
<navigator open-type="navigateBack" hover-class="navigator-hover">
<uni-icons type="back" size="30" color="#D29E52"></uni-icons>
</navigator>
<view class="flex items-center" :style="navUserInfoStyle">
<image class="size-70rpx bg-#333 rounded-full mr-1" :src="avatar" mode="aspectFill" />
<text class="truncate text-sm">{{ title }}</text>
</view>
</view>
</template>
<template #right>
@@ -182,6 +184,9 @@
import CHAT_ENV from '@/chat-env.js';
import RecentApi from '@/sheep/api/member/recent';
import sheep from '@/sheep';
// #ifdef H5
import { getH5StatusBarHeight } from '@/sheep/platform/h5-shell';
// #endif
import WarningCar from "./components/warning-car.vue"
import UserInfoCar from "./components/user-info-car.vue"
@@ -208,6 +213,7 @@ export default {
screenHeight: 1000, // 屏幕高度
windowHeight: 1000, // 窗口高度
initHeight: 1000, // h5初始高度
h5StatusBarHeight: 0, // h5套壳状态栏高度
atUserIds: [],
showMinIdx: 0, // 下标小于showMinIdx的消息不显示,否则可能很卡
reqQueue: [], // 请求队列
@@ -933,14 +939,15 @@ export default {
});
},
async ensureChatStoreReady() {
if (!this.userStore.userInfo?.id) {
await this.userStore.loadUser();
this.$mountStore?.();
if (!this.userStore?.userInfo?.id) {
await this.userStore?.loadUser?.();
}
if (!this.chatStore.curChats?.length) {
await this.chatStore.loadChat();
if (!this.chatStore?.curChats?.length) {
await this.chatStore?.loadChat?.();
}
if (
this.chatStore.refreshChats &&
this.chatStore?.refreshChats &&
this.chatStore.curChats?.length !== this.chatStore.chats.length
) {
this.chatStore.refreshChats();
@@ -1024,7 +1031,11 @@ export default {
let sysInfo = uni.getSystemInfoSync();
let h = this.windowHeight;
// 减去标题栏高度
h -= 50;
h -= 44;
// #ifdef H5
// h5套壳场景下,页面内容还需要减去宿主状态栏高度
h -= this.h5StatusBarHeight;
// #endif
// 减去键盘高度
if (this.isShowKeyBoard || this.chatTabBox != 'none') {
// ios app的键盘高度不准,需要减去屏幕和窗口差
@@ -1059,6 +1070,21 @@ export default {
}
// #endif
},
syncH5StatusBarHeight() {
// #ifdef H5
this.h5StatusBarHeight = getH5StatusBarHeight();
// #endif
},
handleH5ShellStatusBarChange(event) {
// #ifdef H5
const nextHeight = Number.parseFloat(event?.detail?.height);
this.h5StatusBarHeight = Number.isFinite(nextHeight)
? Math.max(nextHeight, 0)
: getH5StatusBarHeight();
this.windowHeight = uni.getSystemInfoSync().windowHeight;
this.reCalChatMainHeight();
// #endif
},
listenKeyBoard() {
// #ifdef H5
if (navigator.platform == 'Win32') {
@@ -1414,10 +1440,17 @@ export default {
this.windowHeight = uni.getSystemInfoSync().windowHeight;
this.screenHeight = uni.getSystemInfoSync().screenHeight;
// #ifdef H5
this.syncH5StatusBarHeight();
// #endif
this.reCalChatMainHeight();
this.$nextTick(() => {
// 上面获取的windowHeight可能不准,重新计算一次聊天窗口高度
this.windowHeight = uni.getSystemInfoSync().windowHeight;
// #ifdef H5
this.syncH5StatusBarHeight();
window.addEventListener('app-shell-status-bar-change', this.handleH5ShellStatusBarChange);
// #endif
this.reCalChatMainHeight();
this.scrollToBottom();
// #ifdef H5
@@ -1437,6 +1470,9 @@ export default {
},
onUnload() {
this.unListenKeyboard();
// #ifdef H5
window.removeEventListener('app-shell-status-bar-change', this.handleH5ShellStatusBarChange);
// #endif
},
};
</script>
@@ -1474,6 +1510,9 @@ export default {
// #ifndef APP-PLUS
top: $im-nav-bar-height;
// #endif
// #ifdef H5
top: calc($im-nav-bar-height + var(--status-bar-height, 0px));
// #endif
// #ifdef APP-PLUS
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif
+21 -18
View File
@@ -16,7 +16,7 @@
<wd-textarea v-model="params.description" :maxlength="1000" clearable show-word-limit />
<wd-tabs v-model="activeTab">
<wd-tab title="发图文">
<view class="px-4 py-8">
<view class="px-4 py-8 flex-row-center">
<s-uploader v-model:url="params.coverPic" fileMediatype="image" limit="6" mode="grid" :imageStyles="{
width: '230rpx',
height: '230rpx',
@@ -24,6 +24,19 @@
}" />
</view>
</wd-tab>
<wd-tab title="发视频">
<view class="px-4 py-8 flex-row-center">
<s-uploader v-model:url="params.coverVideo" fileMediatype="video" limit="6" mode="grid" :imageStyles="{
width: '230rpx',
height: '230rpx',
border: { width: '4rpx', color: '#D29E52', radius: '40rpx' },
}">
<view class="flex-row-center size-full">
<image src="/static/video-icon.png" class="size-100rpx"></image>
</view>
</s-uploader>
</view>
</wd-tab>
<wd-tab v-if="false" title="发视频">
<view class="px-4 py-8">
<wd-upload v-model:file-list="params.coverVideo" :action="baseUrl + apiPath + '/infra/file/upload'"
@@ -31,16 +44,6 @@
</view>
</wd-tab>
</wd-tabs>
<!-- <wd-col-picker
label="选择地址"
title="选择地址"
align-right
v-model="addressValue"
:columns="area"
:column-change="columnChange"
@confirm="areaPickerConfirm"
/> -->
</scroll-view>
<wd-toast />
</wd-config-provider>
@@ -72,10 +75,8 @@ const params = reactive({
issueCity: '',
issueDistrict: '',
});
const coverPic = computed(() => params.coverPic?.join(',') || []);
const coverVideo = computed(() => {
return params.coverVideo.map((item: any) => JSON.parse(item.response).data);
});
const coverPic = computed(() => params.coverPic?.join(',') || '');
const coverVideo = computed(() => params.coverVideo?.join(',') || '');
const area = ref<any[]>([
colPickerData.map((item) => {
return {
@@ -123,8 +124,10 @@ const header = {
// 发布朋友圈
const publishMoments = async () => {
if (!params.description) return uni.showToast({ title: '请填写内容', icon: 'error' });
if (!params.coverPic || !params.coverPic.length)
if (activeTab.value === 0 && (!params.coverPic || !params.coverPic.length))
return uni.showToast({ title: '请上传图片', icon: 'error' });
if (activeTab.value === 1 && (!params.coverVideo || !params.coverVideo.length))
return uni.showToast({ title: '请上传视频', icon: 'error' });
// 获取经纬度位置信息
uni.getLocation({
@@ -134,8 +137,8 @@ const publishMoments = async () => {
const { code, msg } = await RecentApi.createMoment({
...params,
coverPic: coverPic.value,
coverVideo: coverVideo.value.join(','),
coverPic: activeTab.value === 0 ? coverPic.value : '',
coverVideo: activeTab.value === 1 ? coverVideo.value : '',
});
if (code !== 0)
return toast.error({
+2
View File
@@ -15,6 +15,8 @@
<view class="text-base mx-4 my-2">图片</view>
<view class="px-4">
<s-uploader v-model:url="params.pic" fileMediatype="image" limit="1" mode="grid" :imageStyles="{
width: '230rpx',
height: '230rpx',
border: { width: '6rpx', color: '#D29E52' },
}" />
</view>
+17 -8
View File
@@ -108,6 +108,7 @@ import sheep from '@/sheep';
// @ts-ignore
import PayOrderApi from '@/sheep/api/pay/order';
import { jumpToWechatMiniProgram, wechatNavBack } from '@/sheep/util/wechat-tool';
import appConfig from '@/sheep/config';
const messageBox = useMessage();
@@ -199,16 +200,24 @@ const handleRecharge = (item: any) => {
msg: `确认使用${payMethodItem.title}${fen2yuanSimple(item.payPrice)}元获取${fen2yuanSimple(item.bonusPrice + item.payPrice)}砖石?`,
})
.then(async () => {
// #ifdef MP-WEIXIN
if (payMethod.value === 'wechat') {
disposeWechatPay(item.id);
if (appConfig.buildTarget === 'H5') {
if (payMethod.value === 'wechat') {
disposeWechatPay(item.id);
} else {
disposeAlipayPay(item.id);
}
} else {
disposeAlipayPay(item.id);
// #ifdef MP-WEIXIN
if (payMethod.value === 'wechat') {
disposeWechatPay(item.id);
} else {
disposeAlipayPay(item.id);
}
// #endif
// #ifndef MP-WEIXIN
jumpToWechatPage(item.id);
// #endif
}
// #endif
// #ifndef MP-WEIXIN
jumpToWechatPage(item.id);
// #endif
})
.catch(() => { });
};
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<view class="p-4 pb-0">
<view class="p-x-4">
<view class="flex items-center justify-between text-color-theme mb-1">
<text>我的相册</text>
<navigator class="flex-row-center" url="/pages/profile/upload-album" open-type="navigate"
+1 -1
View File
@@ -143,4 +143,4 @@ const goSelectAddress = () => {
max-width: 70% !important;
}
}
</style>
</style>
-2
View File
@@ -19,7 +19,6 @@
<wd-input v-model="searchText" placeholder="搜索好友" prefix-icon="search" clearable :maxlength="10"
:focus="true" />
</view>
<view v-if="!pageReady || loading" class="chat-loading">
<loading :size="50" :mask="false">
<view>{{ pageReady ? '消息接收中...' : '正在初始化...' }}</view>
@@ -33,7 +32,6 @@
<view class="chat-tip" v-else-if="visibleChats.length == 0">
<wd-status-tip image="message" tip="点击私聊,然后开始精彩的对话吧" />
</view>
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
<view v-for="(chat, index) in visibleChats" :key="index">
<!-- <long-press-menu v-if="isShowChat(chat)" :items="menu.items" @select="onSelectMenu($event, index)"> -->
+20 -15
View File
@@ -12,7 +12,7 @@
</template>
</wd-navbar>
<scroll-view class="h-[calc(100vh-144rpx)]" scroll-y>
<scroll-view style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" scroll-y>
<!-- 个人信息 -->
<navigator url="/pages/profile/userinfo" open-type="navigate" hover-class="navigator-hover">
<view class="flex items-center p-6">
@@ -101,12 +101,15 @@
</view>
</view>
</view>
<view class="mt-4 mx-1">
<wd-cell-group>
<wd-cell title="修改资料" title-width="110px" icon="user" icon-size="24" is-link to="/pages/profile/userinfo" />
</wd-cell-group>
</view>
<!-- 用户相册 -->
<user-ablum :ablumList="ablumList" />
<!-- 菜单 -->
<view class="mt-4 mx-1">
<view class="mx-1">
<wd-cell-group>
<wd-cell v-for="(item, index) in MenuCellList" :key="index" :title="item.title" title-width="110px"
:value="item.value" icon-size="24" :icon="item.icon" is-link :to="item.to" @click="item.onClick">
@@ -173,6 +176,7 @@ import { formatDate } from '@/sheep/helper/utils';
// @ts-ignore
import { fen2yuanSimple } from '@/sheep/hooks/useGoods';
import { useMessage } from '@/uni_modules/wot-design-uni';
import { saveImageToPhotosAlbum } from "@/sheep/util/file-tool"
const { ablumList } = useAblum();
@@ -253,10 +257,8 @@ onShow(async () => {
sheep.$store('user').updateUserData();
const { data: totalData } = await RecentApi.recentTotal();
recentTotal.value = totalData ?? {};
const { data: issueTotalData } = await RecentApi.recentIssueTotal();
recentTotal.value.selfRecentTotal = issueTotalData?.selfRecentTotal || 0;
getMemberSetting()
});
@@ -287,16 +289,19 @@ const subOfficial = () => {
confirmButtonText: "保存图片",
})
.then(async () => {
uni.saveImageToPhotosAlbum({
filePath: "http://download.htysapp.com/gzh.jpg",
success: () => {
let imagePath = "http://download.htysapp.com/gzh.jpg";
// #ifdef H5
imagePath = "/static/gzh/gzh.jpg";
// #endif
saveImageToPhotosAlbum(imagePath,
() => {
uni.showToast({ title: '保存成功', icon: 'success' });
},
fail: (err) => {
(err) => {
uni.showToast({ title: '保存失败', icon: 'none' });
console.error('|保存海报|,意外的错误:', err);
},
});
console.error('|保存图片|,意外的错误:', err);
}
)
})
};
@@ -312,7 +317,7 @@ const MenuCellList = ref([
// { title: '我的收益', value: '', icon: 'wallet', to: '/pages/wechat-tabbar/index' },
{ title: '访客记录', value: '', icon: 'browse', to: '/pages/profile/view-record' },
// { title: '认证中心', value: '', icon: 'discount-filled', to: '/pages/profile/verify-status' },
{ title: '联系方式', value: '', icon: 'user', to: '/pages/profile/set-wechat' },
{ title: '联系方式', value: '', icon: 'chat', to: '/pages/profile/set-wechat' },
// { title: '我的动态', value: '', icon: 'star', to: '/pages/profile/my-moment' },
{ title: '我的评价', value: '', icon: 'view-module', to: '', onClick: lookMyEvaluation },
{ title: '问题反馈', value: '', icon: 'backtop-rectangle', to: '', onClick: goFeedback },
@@ -339,4 +344,4 @@ const onTapMenu = (item: any) => {
}
</script>
<style lang="scss" scope></style>
<style lang="scss" scope></style>
+6 -7
View File
@@ -11,7 +11,6 @@
</template>
</wd-navbar>
<scroll-view style="height: calc(100dvh - 44px - var(--status-bar-height, 0px));" scroll-y>
<view style="background: red; height: var(--status-bar-height);"></view>
<!-- 用户信息 -->
<view class="mx-20rpx mt-30rpx bg-color-dark rounded-12rpx flex justify-between items-center px-6 py-4">
<view class="flex items-center">
@@ -120,6 +119,7 @@ import TradeConfigApi from '@/sheep/api/trade/config.js';
import { useMessage } from '@/uni_modules/wot-design-uni';
import useBindWechat from "@/sheep/hooks/use-bind-wechat"
import { jumpToWechatMiniProgram } from '@/sheep/util/wechat-tool';
import { saveImageToPhotosAlbum } from "@/sheep/util/file-tool"
const message = useMessage();
const jumpWechatMessage = useMessage('jump-wechat');
@@ -290,16 +290,15 @@ const savePoster = () => {
uni.showToast({ title: '海报不存在', icon: 'none' });
return;
}
uni.saveImageToPhotosAlbum({
filePath: state.posterPic,
success: () => {
saveImageToPhotosAlbum(state.posterPic,
() => {
uni.showToast({ title: '保存成功', icon: 'success' });
},
fail: (err) => {
(err) => {
uni.showToast({ title: '保存失败', icon: 'none' });
console.error('|保存海报|,意外的错误:', err);
},
});
}
)
};
const doLogin = () => {
+22 -20
View File
@@ -7,22 +7,24 @@
</navigator>
</template>
</wd-navbar>
<view class="mx-20rpx mt-30rpx bg-color-dark rounded-12rpx px-6 py-4">
<image class="mt-30rpx" :src="state.posterPic" mode="widthFix" />
<view class="mt-30rpx flex justify-between items-center text-xs">
<view
class="flex-1 text-center px-2 py-8rpx border-1px border-solid rounded-full border-color-theme active_cl_bg-color-theme"
@tap="copyLink">
复制链接
</view>
<view class="w-30rpx" />
<view
class="flex-1 text-center px-2 py-8rpx border-1px border-solid rounded-full border-color-theme-dark bg-color-theme-dark active_cl_bg-color-theme"
@tap="savePoster">
保存海报
<scroll-view style="height: calc(100dvh - 44px - var(--status-bar-height, 0px));" scroll-y>
<view class="mx-20rpx mt-30rpx bg-color-dark rounded-12rpx px-6 py-4">
<image class="mt-30rpx" :src="state.posterPic" mode="widthFix" />
<view class="mt-30rpx flex justify-between items-center text-xs">
<view
class="flex-1 text-center px-2 py-8rpx border-1px border-solid rounded-full border-color-theme active_cl_bg-color-theme"
@tap="copyLink">
复制链接
</view>
<view class="w-30rpx" />
<view
class="flex-1 text-center px-2 py-8rpx border-1px border-solid rounded-full border-color-theme-dark bg-color-theme-dark active_cl_bg-color-theme"
@tap="savePoster">
保存海报
</view>
</view>
</view>
</view>
</scroll-view>
</wd-config-provider>
</template>
@@ -32,6 +34,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { saveImageToPhotosAlbum } from "@/sheep/util/file-tool"
const state = reactive({
posterPic: ''
@@ -80,16 +83,15 @@ const savePoster = () => {
uni.showToast({ title: '海报不存在', icon: 'none' });
return;
}
uni.saveImageToPhotosAlbum({
filePath: state.posterPic,
success: () => {
saveImageToPhotosAlbum(state.posterPic,
() => {
uni.showToast({ title: '保存成功', icon: 'success' });
},
fail: (err) => {
(err) => {
uni.showToast({ title: '保存失败', icon: 'none' });
console.error('|保存海报|,意外的错误:', err);
},
});
}
)
};
</script>
@@ -2,21 +2,12 @@
<template>
<su-popup :show="show" round="10" @close="onClosePoster" type="center" class="popup-box">
<view class="ss-flex-col ss-col-center ss-row-center">
<image
v-if="!!painterImageUrl"
class="poster-img"
:src="painterImageUrl"
:style="{
height: poster.css.height+ 'px',
width: poster.css.width + 'px',
}"
:show-menu-by-longpress="true"
/>
<image v-if="!!painterImageUrl" class="poster-img" :src="painterImageUrl" :style="{
height: poster.css.height + 'px',
width: poster.css.width + 'px',
}" :show-menu-by-longpress="true" />
</view>
<view
class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
v-if="!!painterImageUrl"
>
<view class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center" v-if="!!painterImageUrl">
<button class="cancel-btn ss-reset-button" @tap="onClosePoster">取消</button>
<button class="save-btn ss-reset-button ui-BG-Main" @tap="onSavePoster">
{{
@@ -27,142 +18,135 @@
</button>
</view>
<!-- 海报画板:默认隐藏只用来生成海报。生成方式为主动调用 -->
<l-painter
isCanvasToTempFilePath
pathType="url"
@success="setPainterImageUrl"
hidden
ref="painterRef"
/>
<l-painter isCanvasToTempFilePath pathType="url" @success="setPainterImageUrl" hidden ref="painterRef" />
</su-popup>
</template>
<script setup>
/**
* 海报生成和展示
* 提示:小程序码默认跳转首页,由首页进行 spm 参数解析后跳转到对应的分享页面
* @description 用于生成分享海报,如:分享商品海报。
* @tutorial https://ext.dcloud.net.cn/plugin?id=2389
* @property {Boolean} show 弹出层控制
* @property {Object} shareInfo 分享信息
*/
import { reactive, ref, unref } from 'vue';
import sheep from '@/sheep';
import { getPosterData } from '@/sheep/components/s-share-modal/canvas-poster/poster';
/**
* 海报生成和展示
* 提示:小程序码默认跳转首页,由首页进行 spm 参数解析后跳转到对应的分享页面
* @description 用于生成分享海报,如:分享商品海报。
* @tutorial https://ext.dcloud.net.cn/plugin?id=2389
* @property {Boolean} show 弹出层控制
* @property {Object} shareInfo 分享信息
*/
import { reactive, ref, unref } from 'vue';
import sheep from '@/sheep';
import { getPosterData } from '@/sheep/components/s-share-modal/canvas-poster/poster';
const props = defineProps({
show: {
type: Boolean,
default: false,
const props = defineProps({
show: {
type: Boolean,
default: false,
},
shareInfo: {
type: Object,
default: () => {
},
shareInfo: {
type: Object,
default: () => {
},
},
});
},
});
const poster = reactive({
css: {
// 根节点若无尺寸,自动获取父级节点
width: sheep.$platform.device.windowWidth * 0.9,
height: 600,
},
views: [],
});
const poster = reactive({
css: {
// 根节点若无尺寸,自动获取父级节点
width: sheep.$platform.device.windowWidth * 0.9,
height: 600,
},
views: [],
});
const emits = defineEmits(['success', 'close']);
const emits = defineEmits(['success', 'close']);
const onClosePoster = () => {
emits('close');
};
const onClosePoster = () => {
emits('close');
};
const painterRef = ref(); // 海报画板
const painterImageUrl = ref(); // 海报 url
// 渲染海报
const renderPoster = async () => {
await painterRef.value.render(unref(poster));
};
// 获得生成的图片
const setPainterImageUrl = (path) => {
painterImageUrl.value = path;
};
// 保存海报图片
const onSavePoster = () => {
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
sheep.$helper.toast('请长按图片保存');
return;
}
// 非H5 保存到相册
uni.saveImageToPhotosAlbum({
filePath: painterImageUrl.value,
success: (res) => {
onClosePoster();
sheep.$helper.toast('保存成功');
},
fail: (err) => {
sheep.$helper.toast('保存失败');
console.log('图片保存失败:', err);
},
});
};
// 获得海报数据
async function getPoster() {
painterImageUrl.value = undefined
poster.views = await getPosterData({
width: poster.css.width,
shareInfo: props.shareInfo,
});
await renderPoster();
const painterRef = ref(); // 海报画板
const painterImageUrl = ref(); // 海报 url
// 渲染海报
const renderPoster = async () => {
await painterRef.value.render(unref(poster));
};
// 获得生成的图片
const setPainterImageUrl = (path) => {
painterImageUrl.value = path;
};
// 保存海报图片
const onSavePoster = () => {
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
sheep.$helper.toast('请长按图片保存');
return;
}
defineExpose({
getPoster,
// 非H5 保存到相册
uni.saveImageToPhotosAlbum({
filePath: painterImageUrl.value,
success: (res) => {
onClosePoster();
sheep.$helper.toast('保存成功');
},
fail: (err) => {
sheep.$helper.toast('保存失败');
console.log('图片保存失败:', err);
},
});
};
// 获得海报数据
async function getPoster() {
painterImageUrl.value = undefined
poster.views = await getPosterData({
width: poster.css.width,
shareInfo: props.shareInfo,
});
await renderPoster();
}
defineExpose({
getPoster,
});
</script>
<style lang="scss" scoped>
.popup-box {
position: relative;
.popup-box {
position: relative;
}
.poster-title {
color: #999;
}
// 分享海报
.poster-btn-box {
width: 600rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -80rpx;
.cancel-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
background: $white;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
.poster-title {
color: #999;
}
// 分享海报
.poster-btn-box {
width: 600rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -80rpx;
.cancel-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
background: $white;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
.save-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
}
}
.poster-img {
border-radius: 20rpx;
.save-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
}
}
.poster-img {
border-radius: 20rpx;
}
</style>
@@ -1,7 +1,6 @@
'use strict';
import FileApi from '@/sheep/api/infra/file';
const ERR_MSG_OK = 'chooseAndUploadFile:ok';
const ERR_MSG_FAIL = 'chooseAndUploadFile:fail';
function chooseImage(opts) {
@@ -30,8 +29,55 @@ function chooseImage(opts) {
}
function chooseVideo(opts) {
const { camera, compressed, maxDuration, sourceType = ['album', 'camera'], extension } = opts;
const {
count,
camera,
compressed,
maxDuration,
sourceType = ['album', 'camera'],
extension,
} = opts;
return new Promise((resolve, reject) => {
// 微信小程序的 chooseMedia 会直接返回视频缩略图。
// #ifdef MP-WEIXIN
uni.chooseMedia({
count,
mediaType: ['video'],
sourceType,
sizeType: compressed ? ['compressed'] : ['original'],
maxDuration,
camera,
success(res) {
resolve(
normalizeChooseAndUploadFileRes(
{
errMsg: 'chooseVideo:ok',
tempFiles: res.tempFiles.map((item) => ({
name: item.name || '',
path: item.tempFilePath,
thumbTempFilePath: item.thumbTempFilePath || '',
size: item.size,
type: item.type || '',
width: item.width,
height: item.height,
duration: item.duration,
fileType: 'video',
cloudPath: '',
})),
},
'video',
),
);
},
fail(res) {
reject({
errMsg: res.errMsg.replace('chooseMedia:fail', ERR_MSG_FAIL),
});
},
});
// #endif
// #ifndef MP-WEIXIN
uni.chooseVideo({
camera,
compressed,
@@ -39,7 +85,7 @@ function chooseVideo(opts) {
sourceType,
extension,
success(res) {
const { tempFilePath, duration, size, height, width } = res;
const { tempFilePath, thumbTempFilePath, duration, size, height, width } = res;
resolve(
normalizeChooseAndUploadFileRes(
{
@@ -49,6 +95,7 @@ function chooseVideo(opts) {
{
name: (res.tempFile && res.tempFile.name) || '',
path: tempFilePath,
thumbTempFilePath: thumbTempFilePath || '',
size,
type: (res.tempFile && res.tempFile.type) || '',
width,
@@ -69,6 +116,7 @@ function chooseVideo(opts) {
});
},
});
// #endif
});
}
File diff suppressed because it is too large. Load diff
+276 -293
View File
@@ -7,15 +7,9 @@
<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle">
<!-- ,'is-list-card':showType === 'list-card' -->
<view
class="uni-file-picker__lists-box"
v-for="(item, index) in list"
:key="index"
:class="{
'files-border': index !== 0 && styles.dividline,
}"
:style="index !== 0 && styles.dividline && borderLineStyle"
>
<view class="uni-file-picker__lists-box" v-for="(item, index) in list" :key="index" :class="{
'files-border': index !== 0 && styles.dividline,
}" :style="index !== 0 && styles.dividline && borderLineStyle">
<view class="uni-file-picker__item">
<!-- :class="{'is-text-image':showType === 'list'}" -->
<!-- <view class="files__image is-text-image">
@@ -27,22 +21,11 @@
<view class="icon-del rotate"></view>
</view>
</view>
<view
v-if="(item.progress && item.progress !== 100) || item.progress === 0"
class="file-picker__progress"
>
<progress
class="file-picker__progress-item"
:percent="item.progress === -1 ? 0 : item.progress"
stroke-width="4"
:backgroundColor="item.errMsg ? '#ff5a5f' : '#EBEBEB'"
/>
<view v-if="(item.progress && item.progress !== 100) || item.progress === 0" class="file-picker__progress">
<progress class="file-picker__progress-item" :percent="item.progress === -1 ? 0 : item.progress"
stroke-width="4" :backgroundColor="item.errMsg ? '#ff5a5f' : '#EBEBEB'" />
</view>
<view
v-if="item.status === 'error'"
class="file-picker__mask"
@click.stop="uploadFiles(item, index)"
>
<view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="uploadFiles(item, index)">
点击重试
</view>
</view>
@@ -51,285 +34,285 @@
</template>
<script>
export default {
name: 'uploadFile',
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
type: Array,
default() {
return [];
},
},
delIcon: {
type: Boolean,
default: true,
},
limit: {
type: [Number, String],
default: 9,
},
showType: {
type: String,
default: '',
},
listStyles: {
type: Object,
default() {
return {
// 是否显示边框
border: true,
// 是否显示分隔线
dividline: true,
// 线条样式
borderStyle: {},
};
},
},
readonly: {
type: Boolean,
default: false,
export default {
name: 'uploadFile',
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
type: Array,
default() {
return [];
},
},
computed: {
list() {
let files = [];
this.filesList.forEach((v) => {
files.push(v);
});
return files;
},
styles() {
let styles = {
delIcon: {
type: Boolean,
default: true,
},
limit: {
type: [Number, String],
default: 9,
},
showType: {
type: String,
default: '',
},
listStyles: {
type: Object,
default() {
return {
// 是否显示边框
border: true,
// 是否显示分隔线
dividline: true,
'border-style': {},
// 线条样式
borderStyle: {},
};
return Object.assign(styles, this.listStyles);
},
borderStyle() {
let { borderStyle, border } = this.styles;
let obj = {};
if (!border) {
obj.border = 'none';
} else {
let width = (borderStyle && borderStyle.width) || 1;
width = this.value2px(width);
let radius = (borderStyle && borderStyle.radius) || 5;
radius = this.value2px(radius);
obj = {
'border-width': width,
'border-style': (borderStyle && borderStyle.style) || 'solid',
'border-color': (borderStyle && borderStyle.color) || '#eee',
'border-radius': radius,
};
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
borderLineStyle() {
let obj = {};
let { borderStyle } = this.styles;
if (borderStyle && borderStyle.color) {
obj['border-color'] = borderStyle.color;
}
if (borderStyle && borderStyle.width) {
let width = (borderStyle && borderStyle.width) || 1;
let style = (borderStyle && borderStyle.style) || 0;
if (typeof width === 'number') {
width += 'px';
} else {
width = width.indexOf('px') ? width : width + 'px';
}
obj['border-width'] = width;
if (typeof style === 'number') {
style += 'px';
} else {
style = style.indexOf('px') ? style : style + 'px';
}
obj['border-top-style'] = style;
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
},
methods: {
uploadFiles(item, index) {
this.$emit('uploadFiles', {
item,
index,
});
},
choose() {
this.$emit('choose');
},
delFile(index) {
this.$emit('delFile', index);
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
} else {
value = value.indexOf('px') !== -1 ? value : value + 'px';
}
return value;
},
readonly: {
type: Boolean,
default: false,
},
};
},
computed: {
list() {
let files = [];
this.filesList.forEach((v) => {
files.push(v);
});
return files;
},
styles() {
let styles = {
border: true,
dividline: true,
'border-style': {},
};
return Object.assign(styles, this.listStyles);
},
borderStyle() {
let { borderStyle, border } = this.styles;
let obj = {};
if (!border) {
obj.border = 'none';
} else {
let width = (borderStyle && borderStyle.width) || 1;
width = this.value2px(width);
let radius = (borderStyle && borderStyle.radius) || 5;
radius = this.value2px(radius);
obj = {
'border-width': width,
'border-style': (borderStyle && borderStyle.style) || 'solid',
'border-color': (borderStyle && borderStyle.color) || '#eee',
'border-radius': radius,
};
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
borderLineStyle() {
let obj = {};
let { borderStyle } = this.styles;
if (borderStyle && borderStyle.color) {
obj['border-color'] = borderStyle.color;
}
if (borderStyle && borderStyle.width) {
let width = (borderStyle && borderStyle.width) || 1;
let style = (borderStyle && borderStyle.style) || 0;
if (typeof width === 'number') {
width += 'px';
} else {
width = width.indexOf('px') ? width : width + 'px';
}
obj['border-width'] = width;
if (typeof style === 'number') {
style += 'px';
} else {
style = style.indexOf('px') ? style : style + 'px';
}
obj['border-top-style'] = style;
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
},
methods: {
uploadFiles(item, index) {
this.$emit('uploadFiles', {
item,
index,
});
},
choose() {
this.$emit('choose');
},
delFile(index) {
this.$emit('delFile', index);
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
} else {
value = value.indexOf('px') !== -1 ? value : value + 'px';
}
return value;
},
},
};
</script>
<style lang="scss">
.uni-file-picker__files {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: flex-start;
}
.files-button {
// border: 1px red solid;
}
.uni-file-picker__lists {
position: relative;
margin-top: 5px;
overflow: hidden;
}
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
color: #fff;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.4);
}
.uni-file-picker__lists-box {
position: relative;
}
.uni-file-picker__item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
padding: 8px 10px;
padding-right: 5px;
padding-left: 10px;
}
.files-border {
border-top: 1px #eee solid;
}
.files__name {
flex: 1;
font-size: 14px;
color: #666;
margin-right: 25px;
/* #ifndef APP-NVUE */
word-break: break-all;
word-wrap: break-word;
/* #endif */
}
.icon-files {
/* #ifndef APP-NVUE */
position: static;
background-color: initial;
/* #endif */
}
// .icon-files .icon-del {
// background-color: #333;
// width: 12px;
// height: 1px;
// }
.is-list-card {
border: 1px #eee solid;
margin-bottom: 5px;
border-radius: 5px;
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
padding: 5px;
}
.files__image {
width: 40px;
height: 40px;
margin-right: 10px;
}
.header-image {
width: 100%;
height: 100%;
}
.is-text-box {
border: 1px #eee solid;
border-radius: 5px;
}
.is-text-image {
width: 25px;
height: 25px;
margin-left: 5px;
}
.rotate {
position: absolute;
transform: rotate(90deg);
}
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
margin: auto 0;
/* #endif */
align-items: center;
justify-content: center;
position: absolute;
top: 0px;
bottom: 0;
right: 5px;
height: 26px;
width: 26px;
// border-radius: 50%;
// background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
transform: rotate(-45deg);
}
.icon-del {
width: 15px;
height: 1px;
background-color: #333;
// border-radius: 1px;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.uni-file-picker__files {
max-width: 375px;
}
}
.uni-file-picker__files {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: flex-start;
}
.files-button {
// border: 1px red solid;
}
.uni-file-picker__lists {
position: relative;
margin-top: 5px;
overflow: hidden;
}
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
color: #fff;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.4);
}
.uni-file-picker__lists-box {
position: relative;
}
.uni-file-picker__item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
padding: 8px 10px;
padding-right: 5px;
padding-left: 10px;
}
.files-border {
border-top: 1px #eee solid;
}
.files__name {
flex: 1;
font-size: 14px;
color: #666;
margin-right: 25px;
/* #ifndef APP-NVUE */
word-break: break-all;
word-wrap: break-word;
/* #endif */
}
.icon-files {
/* #ifndef APP-NVUE */
position: static;
background-color: initial;
/* #endif */
}
// .icon-files .icon-del {
// background-color: #333;
// width: 12px;
// height: 1px;
// }
.is-list-card {
border: 1px #eee solid;
margin-bottom: 5px;
border-radius: 5px;
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
padding: 5px;
}
.files__image {
width: 40px;
height: 40px;
margin-right: 10px;
}
.header-image {
width: 100%;
height: 100%;
}
.is-text-box {
border: 1px #eee solid;
border-radius: 5px;
}
.is-text-image {
width: 25px;
height: 25px;
margin-left: 5px;
}
.rotate {
position: absolute;
transform: rotate(90deg);
}
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
margin: auto 0;
/* #endif */
align-items: center;
justify-content: center;
position: absolute;
top: 0px;
bottom: 0;
right: 5px;
height: 26px;
width: 26px;
// border-radius: 50%;
// background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
transform: rotate(-45deg);
}
.icon-del {
width: 15px;
height: 1px;
background-color: #333;
// border-radius: 1px;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.uni-file-picker__files {
max-width: 375px;
}
}
/* #endif */
</style>
@@ -1,13 +1,28 @@
<template>
<view class="uni-file-picker__container">
<view class="file-picker__box" v-for="(url, index) in list" :key="index" :style="boxStyle">
<view class="file-picker__box" v-for="(item, index) in list" :key="index" :style="boxStyle">
<view class="file-picker__box-content" :style="borderStyle">
<image
v-if="fileMediatype === 'image' || getVideoThumb(item)"
class="file-image"
:src="getImageUrl(url)"
:src="getImageUrl(fileMediatype === 'video' ? getVideoThumb(item) : getFileUrl(item))"
mode="aspectFill"
@click.stop="previewImage(url, index)"
@click.stop="previewFile(item, index)"
></image>
<!-- #ifndef APP-PLUS -->
<video v-else class="file-video" :src="getFileUrl(item)" object-fit="cover" :controls="false"
:autoplay="false" :show-center-play-btn="false" :show-fullscreen-btn="false" :show-play-btn="false"
:show-loading="false" :show-progress="false" :enable-progress-gesture="false"
:initial-time="0.1" :muted="true"
@click.stop="previewFile(item, index)"></video>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<dom-video v-else class="file-video" :src="getFileUrl(item)" object-fit="cover" :controls="false"
:autoplay="false"></dom-video>
<!-- #endif -->
<view v-if="fileMediatype === 'video'" class="video-play" @click.stop="previewFile(item, index)">
<view class="video-play__triangle"></view>
</view>
<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
<view class="icon-del"></view>
<view class="icon-del rotate"></view>
@@ -26,12 +41,23 @@
</view>
</view>
</view>
<!-- #ifndef MP-WEIXIN -->
<wd-video-preview v-if="fileMediatype === 'video'" ref="videoPreview"></wd-video-preview>
<!-- #endif -->
</template>
<script>
import sheep from '@/sheep';
// #ifdef APP-PLUS
import domVideo from '@/sheep/ui/su-video/components/dom-video.vue';
// #endif
export default {
name: 'uploadImage',
components: {
// #ifdef APP-PLUS
domVideo,
// #endif
},
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
@@ -70,6 +96,10 @@
type: Boolean,
default: false,
},
fileMediatype: {
type: String,
default: 'image',
},
},
computed: {
list() {
@@ -148,8 +178,17 @@
},
},
methods: {
getFileUrl(file) {
if (typeof file === 'string') return file;
return file.url || file.path || '';
},
getVideoThumb(file) {
if (!file || typeof file === 'string') return '';
return file.thumb || file.thumbTempFilePath || (file.video && file.video.thumb) || '';
},
getImageUrl(url) {
if ('blob:http:' === url.substr(0, 10)) {
if (!url) return '';
if (/^(blob:|data:|wxfile:|file:|https?:|_doc\/|_downloads\/|\/(tmp|var|private|storage)\/)/.test(url)) {
return url;
} else {
return sheep.$url.cdn(url);
@@ -171,7 +210,7 @@
}
if (this.disablePreview) return;
this.list.forEach((i) => {
urls.push(this.getImageUrl(i));
urls.push(this.getImageUrl(this.getFileUrl(i)));
});
uni.previewImage({
@@ -179,6 +218,28 @@
current: index,
});
},
previewFile(file, index) {
if (this.fileMediatype === 'image') {
this.previewImage(this.getFileUrl(file), index);
return;
}
const videoUrl = this.getFileUrl(file);
if (!videoUrl) return;
// #ifdef MP-WEIXIN
uni.previewMedia({
current: 0,
sources: [{ url: videoUrl, type: 'video', poster: this.getVideoThumb(file) }],
});
// #endif
// #ifndef MP-WEIXIN
this.$refs.videoPreview?.open({
url: videoUrl,
poster: this.getVideoThumb(file),
title: file.name || '视频',
});
// #endif
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
@@ -260,6 +321,36 @@
height: 100%;
}
.file-video {
width: 100%;
height: 100%;
}
.video-play {
position: absolute;
top: 50%;
left: 50%;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
height: 56rpx;
border: 4rpx solid #fff;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.35);
transform: translate(-50%, -50%);
}
.video-play__triangle {
width: 0;
height: 0;
margin-left: 4rpx;
border-top: 12rpx solid transparent;
border-bottom: 12rpx solid transparent;
border-left: 18rpx solid #fff;
}
.is-add {
/* #ifndef APP-NVUE */
display: flex;
+6 -1
View File
@@ -94,7 +94,12 @@ export const get_file_data = async (files, type = 'image') => {
size: files.size, //单位是字节
image: {},
path: files.path,
video: {},
video: {
width: files.width,
height: files.height,
duration: files.duration,
thumb: files.thumbTempFilePath || files.thumb || '',
},
};
if (type === 'image') {
const imageinfo = await get_file_info(files.path);
+16 -3
View File
@@ -18,13 +18,13 @@
<text class="py-4rpx px-2 rounded-full text-22rpx bg-color-theme-dark whitespace-nowrap">
{{
member.appRefreshDate
? formatDate(member.appRefreshDate, 'MM-DD HH:mm前') + ' 在线'
? formatRelativeTime(member.appRefreshDate) + ' 在线'
: '离线'
}}
</text>
</view>
<view v-for="(item, index) in data.memberTagList" :key="index">
<view v-if="['1'].includes(item.tagBizType)"
<view v-if="tagVisibleTypeList.includes(item.tagBizType)"
class="mt-2 inline-flex px-1 py-6rpx text-18rpx c-black rounded-4rpx bg-color-theme-dark mr-2">
<wd-icon name="discount-filled" size="10px" color="#000" />
<text>{{ item.tagTitle }}</text>
@@ -61,7 +61,8 @@ import { computed } from 'vue';
// @ts-ignore
import { getGender } from '@/sheep/util/index';
// @ts-ignore
import { formatDate } from '@/sheep/util/index';
import sheep from '@/sheep';
import { formatDate, formatRelativeTime } from '@/sheep/util/index';
const props = withDefaults(defineProps<{ data: any; backdropFilter?: boolean }>(), {
backdropFilter: false,
@@ -70,6 +71,18 @@ const emit = defineEmits(['click']);
const member = computed(() => props.data.member || {});
const userInfo = computed(() => sheep.$store('user').userInfo);
// 只有男性用户才显示VIP标签
const tagVisibleTypeList = computed(() => {
const list = ['1'];
const sex = userInfo.value?.sex ?? null;
if (sex == 1) {
list.push('4');
}
return list;
});
const filterStyle = computed(() => {
return props.backdropFilter ? { backdropFilter: 'blur(10px)' } : {};
});
+54 -55
View File
@@ -1,7 +1,10 @@
const H5_STATUS_BAR_STORAGE_KEY = '__friendship_h5_status_bar_height__';
const H5_STATUS_BAR_QUERY_KEYS = ['statusBarHeight', 'safeTop', 'safeAreaTop', 'appStatusBarHeight'];
const H5_MIN_STATUS_BAR_HEIGHT = 22;
const H5_MAX_STATUS_BAR_HEIGHT = 200;
let currentStatusBarHeight = H5_MIN_STATUS_BAR_HEIGHT;
let statusBarStyleObserver = null;
// 将 URL 参数、宿主全局变量和 postMessage 消息中的高度统一转换为数字。
function normalizeStatusBarHeight(value) {
if (value === undefined || value === null || value === '') {
return null;
@@ -11,7 +14,13 @@ function normalizeStatusBarHeight(value) {
if (!Number.isFinite(height)) {
return null;
}
return Math.max(0, Math.min(height, 200));
return Math.max(0, Math.min(height, H5_MAX_STATUS_BAR_HEIGHT));
}
// 宿主未提供高度或提供的高度过小时,统一使用 H5 最小状态栏高度。
function resolveStatusBarHeight(value) {
const height = normalizeStatusBarHeight(value);
return height === null ? H5_MIN_STATUS_BAR_HEIGHT : Math.max(height, H5_MIN_STATUS_BAR_HEIGHT);
}
function parseMessageData(data) {
@@ -28,6 +37,7 @@ function parseMessageData(data) {
return data;
}
// URL 参数优先级最高,同时兼容普通查询参数和 hash 查询参数。
function readStatusBarHeightFromQuery() {
const searchParamsList = [];
searchParamsList.push(new URLSearchParams(window.location.search));
@@ -48,6 +58,7 @@ function readStatusBarHeightFromQuery() {
return null;
}
// 宿主可以在 H5 应用初始化前将状态栏高度注入 window。
function readStatusBarHeightFromWindow() {
const candidates = [
window.__APP_STATUS_BAR_HEIGHT__,
@@ -64,76 +75,65 @@ function readStatusBarHeightFromWindow() {
return null;
}
function readStatusBarHeightFromStorage() {
try {
return normalizeStatusBarHeight(window.localStorage.getItem(H5_STATUS_BAR_STORAGE_KEY));
} catch (error) {
return null;
}
}
function readNativeStatusBarHeight() {
if (typeof window === 'undefined') {
// 读取页面当前实际使用的 --status-bar-height。
export function getH5StatusBarHeight() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return 0;
}
const rootStyle = window.getComputedStyle(document.documentElement);
return normalizeStatusBarHeight(rootStyle.getPropertyValue('--native-status-bar-height')) ?? 0;
}
function ensureMinShellStatusBarHeight(height) {
if (height === null || height <= 0) {
return 0;
}
return Math.max(height, H5_MIN_STATUS_BAR_HEIGHT);
const statusBarHeight = window
.getComputedStyle(document.documentElement)
.getPropertyValue('--status-bar-height')
.replace(/px$/i, '')
.trim();
return resolveStatusBarHeight(statusBarHeight);
}
// 依次从 URL、宿主全局变量中获取初始高度,均未提供时使用默认最小高度。
export function getH5HostStatusBarHeight() {
if (typeof window === 'undefined') {
return 0;
}
return ensureMinShellStatusBarHeight(
readStatusBarHeightFromQuery() ??
readStatusBarHeightFromWindow() ??
readStatusBarHeightFromStorage() ??
0,
);
return resolveStatusBarHeight(readStatusBarHeightFromQuery() ?? readStatusBarHeightFromWindow());
}
// 只在 html 上设置一个状态栏变量,body 和其他子元素会自动继承。
export function applyH5HostStatusBarHeight(height) {
if (typeof window === 'undefined') {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return 0;
}
const safeTop = ensureMinShellStatusBarHeight(normalizeStatusBarHeight(height));
const nativeSafeTop = readNativeStatusBarHeight();
const finalSafeTop = Math.max(nativeSafeTop, safeTop);
const safeTopValue = `${safeTop}px`;
const finalSafeTopValue = `${finalSafeTop}px`;
const rootStyle = document.documentElement.style;
const bodyStyle = document.body?.style;
rootStyle.setProperty('--app-shell-status-bar-height', safeTopValue);
rootStyle.setProperty('--status-bar-height', finalSafeTopValue);
bodyStyle?.setProperty('--app-shell-status-bar-height', safeTopValue);
bodyStyle?.setProperty('--status-bar-height', finalSafeTopValue);
window.__APP_STATUS_BAR_HEIGHT__ = safeTop;
try {
window.localStorage.setItem(H5_STATUS_BAR_STORAGE_KEY, String(safeTop));
} catch (error) {
// ignore
}
const statusBarHeight = resolveStatusBarHeight(height);
currentStatusBarHeight = statusBarHeight;
document.documentElement.style.setProperty('--status-bar-height', `${statusBarHeight}px`, 'important');
// 通知使用数值高度参与 JavaScript 布局计算的页面重新计算。
window.dispatchEvent(
new CustomEvent('app-shell-status-bar-change', {
detail: { height: finalSafeTop, appHeight: safeTop, nativeHeight: nativeSafeTop },
detail: { height: statusBarHeight },
}),
);
return finalSafeTop;
return statusBarHeight;
}
// uni-app 重写 html 样式后,如果状态栏高度小于 22px,则立即恢复当前有效高度。
function observeStatusBarHeight() {
if (statusBarStyleObserver || typeof MutationObserver === 'undefined') {
return;
}
statusBarStyleObserver = new MutationObserver(() => {
const statusBarHeight = normalizeStatusBarHeight(
document.documentElement.style.getPropertyValue('--status-bar-height'),
);
if (statusBarHeight === null || statusBarHeight < H5_MIN_STATUS_BAR_HEIGHT) {
applyH5HostStatusBarHeight(currentStatusBarHeight);
}
});
statusBarStyleObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style'],
});
}
// 解析宿主运行期间通过 postMessage 发送的状态栏高度。
function resolveMessageStatusBarHeight(message) {
const data = parseMessageData(message?.data);
if (!data) {
@@ -149,6 +149,7 @@ function resolveMessageStatusBarHeight(message) {
);
}
// 初始化状态栏高度,并支持宿主通过全局方法或 postMessage 动态更新。
export function initH5HostShell() {
if (typeof window === 'undefined') {
return 0;
@@ -160,6 +161,7 @@ export function initH5HostShell() {
}
window.__FRIENDSHIP_H5_SHELL_READY__ = true;
observeStatusBarHeight();
window.setAppShellStatusBarHeight = (nextHeight) => applyH5HostStatusBarHeight(nextHeight);
window.addEventListener('message', (message) => {
const nextHeight = resolveMessageStatusBarHeight(message);
@@ -167,9 +169,6 @@ export function initH5HostShell() {
applyH5HostStatusBarHeight(nextHeight);
}
});
window.addEventListener('load', () => {
applyH5HostStatusBarHeight(getH5HostStatusBarHeight());
});
return safeTop;
}
-9
View File
@@ -36,32 +36,26 @@ export function patchH5Navigation() {
}
window.__UNI_H5_NAV_PATCHED__ = true;
syncCurrentH5Route();
const rawNavigateTo = uni.navigateTo?.bind(uni);
const rawRedirectTo = uni.redirectTo?.bind(uni);
const rawSwitchTab = uni.switchTab?.bind(uni);
const rawReLaunch = uni.reLaunch?.bind(uni);
uni.navigateTo = (options = {}) => {
saveH5RouteSnapshot(options.url);
return rawNavigateTo(options);
};
uni.redirectTo = (options = {}) => {
saveH5RouteSnapshot(options.url);
return rawRedirectTo(options);
};
uni.switchTab = (options = {}) => {
saveH5RouteSnapshot(options.url);
return rawSwitchTab(options);
};
uni.reLaunch = (options = {}) => {
saveH5RouteSnapshot(options.url);
return rawReLaunch(options);
};
uni.navigateBack = (options = {}) => {
const fallbackRoute =
sessionStorage.getItem(H5_PREV_ROUTE_KEY) || H5_DEFAULT_ROUTE;
@@ -71,18 +65,15 @@ export function patchH5Navigation() {
options.success && options.success();
return Promise.resolve();
}
if (fallbackRoute && fallbackRoute !== getCurrentH5Route()) {
window.location.assign(fallbackRoute);
options.success && options.success();
return Promise.resolve();
}
window.location.assign(H5_DEFAULT_ROUTE);
options.success && options.success();
return Promise.resolve();
};
window.addEventListener('pageshow', syncCurrentH5Route);
window.addEventListener('popstate', syncCurrentH5Route);
}
+1 -6
View File
@@ -115,12 +115,7 @@ const app = defineStore({
},
},
persist: {
enabled: true,
strategies: [
{
key: 'app-store',
},
],
key: 'app-store',
},
});
+1 -6
View File
@@ -109,12 +109,7 @@ const cart = defineStore({
},
},
persist: {
enabled: true,
strategies: [
{
key: 'cart-store',
},
],
key: 'cart-store',
},
});
+16 -3
View File
@@ -1,5 +1,18 @@
import { createPinia } from 'pinia';
import piniaPersist from 'pinia-plugin-persist-uni';
import { createPersistedState } from 'pinia-plugin-persistedstate';
export const uniStorage = {
getItem(key) {
const value = uni.getStorageSync(key);
if (value === undefined || value === null || value === '') {
return null;
}
return typeof value === 'string' ? value : JSON.stringify(value);
},
setItem(key, value) {
uni.setStorageSync(key, value);
},
};
// 自动注入所有pinia模块
const files = import.meta.glob('./*.js', { eager: true });
@@ -10,9 +23,9 @@ Object.keys(files).forEach((key) => {
export const setupPinia = (app) => {
const pinia = createPinia();
pinia.use(piniaPersist);
pinia.use(createPersistedState({ storage: uniStorage }));
app.use(pinia);
return pinia;
};
export default (name) => {
+2 -7
View File
@@ -16,13 +16,8 @@ const modal = defineStore({
}
}),
persist: {
enabled: true,
strategies: [
{
key: 'modal-store',
paths: ['lastTimer', 'advHistory'],
},
],
key: 'modal-store',
paths: ['lastTimer', 'advHistory'],
},
});
+1 -6
View File
@@ -20,12 +20,7 @@ const sys = defineStore({
},
},
persist: {
enabled: true,
strategies: [
{
key: 'sys-store',
},
],
key: 'sys-store',
},
});
+1 -6
View File
@@ -163,12 +163,7 @@ const user = defineStore({
},
},
persist: {
enabled: true,
strategies: [
{
key: 'user-store',
},
],
key: 'user-store',
},
});
+55
View File
@@ -0,0 +1,55 @@
/**
* 保存图片到相册
* @param {string} filePath 图片地址
* @param {Function} successCallback 保存成功回调
* @param {Function} errorCallback 保存失败回调
*/
export const saveImageToPhotosAlbum = (
filePath: string,
successCallback?: (res: any) => void,
errorCallback?: (error: any) => void,
) => {
if (!filePath) {
const error = new Error('图片地址不能为空');
errorCallback?.(error);
return;
}
// #ifdef H5
const saveImageForH5 = async () => {
let objectUrl = '';
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`图片下载失败: ${response.status}`);
}
const blob = await response.blob();
objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
const fileName = filePath.split('/').pop()?.split('?')[0] || `image-${Date.now()}.jpg`;
link.href = objectUrl;
link.download = decodeURIComponent(fileName);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
successCallback?.({ errMsg: 'saveImageToPhotosAlbum:ok' });
} catch (error) {
errorCallback?.(error);
} finally {
if (objectUrl) setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}
};
saveImageForH5();
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath,
success: (res) => successCallback?.(res),
fail: (error) => errorCallback?.(error),
});
// #endif
};
+21
View File
@@ -163,3 +163,24 @@ export const getGender = (genderType) => {
return '';
}
};
/**
* 将时间转换为相对当前时间的文字描述
* @param {dayjs.ConfigType} date 需要转换的时间
* @returns {string} 几分钟前、几小时前、几天前或近期
*/
export const formatRelativeTime = (date) => {
if (!date) return '';
const targetTime = dayjs(date);
if (!targetTime.isValid()) return '';
const now = dayjs();
const minutes = now.diff(targetTime, 'minute');
// 未来时间和不足5分钟都按近期处理。
if (minutes < 5) return '近期';
if (minutes < 60) return `${minutes}分钟前`;
const hours = now.diff(targetTime, 'hour');
if (hours < 24) return `${hours}小时前`;
const days = now.diff(targetTime, 'day');
if (days < 7) return `${days}天前`;
return '近期';
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff. Show more