初始化商家app代码
This commit is contained in:
commit
1aa732ad02
|
|
@ -0,0 +1,299 @@
|
||||||
|
<script>
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
import {
|
||||||
|
isNotificationsEnabled,
|
||||||
|
permissions,
|
||||||
|
registPush
|
||||||
|
} from '@/utils/AS-NotifyTheAuthority.js'
|
||||||
|
import {
|
||||||
|
permissionListener
|
||||||
|
} from './utils/permissionTips.js'
|
||||||
|
// import { appUpdateListener } from '@/common/utils/appUpdateTips.js'
|
||||||
|
import locationService from './utils/locationService.js'
|
||||||
|
// import uploadApp from './utils/uploadApp.js'
|
||||||
|
// #endif
|
||||||
|
import request from './utils/request'
|
||||||
|
import {
|
||||||
|
setVersion
|
||||||
|
} from './utils/version.js'
|
||||||
|
//初始化一键登录插件
|
||||||
|
const aLiSDKModule = uni.requireNativePlugin('AliCloud-NirvanaPns');
|
||||||
|
export default {
|
||||||
|
globalData: {
|
||||||
|
artisanType: null,
|
||||||
|
latitude: '38.366667',
|
||||||
|
longitude: '117.333333',
|
||||||
|
address: '黄骅市',
|
||||||
|
servers_region: `河北省-沧州市-黄骅市`,
|
||||||
|
deviceid: '',
|
||||||
|
addressRes: {
|
||||||
|
"type": "gcj02",
|
||||||
|
"altitude": 0,
|
||||||
|
"latitude": 38.372266,
|
||||||
|
"longitude": 117.332895,
|
||||||
|
"speed": 0,
|
||||||
|
"accuracy": 30,
|
||||||
|
"adcode": "130983",
|
||||||
|
"cCode": "130900",
|
||||||
|
"pcode": "130000",
|
||||||
|
"address": {
|
||||||
|
"country": "中国",
|
||||||
|
"province": "河北省",
|
||||||
|
"city": "沧州市",
|
||||||
|
"district": "黄骅市",
|
||||||
|
"street": "渤海西路",
|
||||||
|
"streetNum": "35号",
|
||||||
|
// "poiName": "请开启位置权限",
|
||||||
|
"poiName": "黄骅市人民政府",
|
||||||
|
"cityCode": "0317",
|
||||||
|
"adcode": "130983",
|
||||||
|
"cCode": "130900",
|
||||||
|
"pcode": "130000",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLaunch: async function() {
|
||||||
|
// plus.android.requestPermissions(['POST_NOTIFICATIONS'], function(e) {
|
||||||
|
// console.log('1111111111' + e.deniedAlways.length);
|
||||||
|
// if (e.deniedAlways.length > 0) { //权限被永久拒绝
|
||||||
|
// // 弹出提示框解释为何需要定位权限,引导用户打开设置页面开启
|
||||||
|
// console.log('Always Denied!!! ' + e.deniedAlways.toString());
|
||||||
|
// }
|
||||||
|
// if (e.deniedPresent.length > 0) { //权限被临时拒绝
|
||||||
|
// // 弹出提示框解释为何需要定位权限,可再次调用plus.android.requestPermissions申请权限
|
||||||
|
// console.log('Present Denied!!! ' + e.deniedPresent.toString());
|
||||||
|
// }
|
||||||
|
// if (e.granted.length > 0) { //权限被允许
|
||||||
|
// //调用依赖获取定位权限的代码
|
||||||
|
// console.log('Granted!!! ' + e.granted.toString());
|
||||||
|
// }
|
||||||
|
// }, function(e) {
|
||||||
|
// console.log('Request Permissions error:' + JSON.stringify(e));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (uni.getStorageSync('accessToken')) {
|
||||||
|
registPush()
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
//跟新底部消息数量
|
||||||
|
this.messageUpudateNum()
|
||||||
|
|
||||||
|
//隐藏原生Tabar
|
||||||
|
uni.hideTabBar({
|
||||||
|
animation: true
|
||||||
|
});
|
||||||
|
//缓存手机版本号
|
||||||
|
// await setVersion()
|
||||||
|
|
||||||
|
//获取访问路由
|
||||||
|
// 发起 GET 请求
|
||||||
|
let version = uni.getStorageSync('setVersion')
|
||||||
|
let baseUrl
|
||||||
|
await uni.request({
|
||||||
|
url: 'https://app.mrrweb.com.cn/api/openPuc/getApiUrl', // 接口地址
|
||||||
|
method: 'GET', // 请求方法(默认 GET)
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json', // 请求头
|
||||||
|
'VersionCode': version.VersionCode,
|
||||||
|
'DeviceBrand': version.DeviceBrand,
|
||||||
|
'DeviceType': version.DeviceType
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
|
||||||
|
if (res.data.code == 200) {
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
uni.setStorageSync('baseUrl', res.data.data.url)
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
wx.setStorageSync('baseUrl', res.data.data.url)
|
||||||
|
// #endif
|
||||||
|
this.$store.commit("setLocation", true)
|
||||||
|
}
|
||||||
|
console.log('请求成功', res.data);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.log('请求失败', err);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
console.log('请求完成(无论成功失败都会执行)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
//禁止手机横屏
|
||||||
|
plus.screen.lockOrientation("portrait-primary")
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
//权限提示监听
|
||||||
|
let systemInfo = uni.getSystemInfoSync();
|
||||||
|
let platform = systemInfo.platform;
|
||||||
|
if (platform === 'android') {
|
||||||
|
// console.log('安卓用户')
|
||||||
|
permissionListener()
|
||||||
|
} else if (platform === 'ios') {
|
||||||
|
// console.log('苹果用户')
|
||||||
|
} else {
|
||||||
|
// console.log('平台未知')
|
||||||
|
}
|
||||||
|
|
||||||
|
//一键登录密钥设置
|
||||||
|
let sdkInfo = '';
|
||||||
|
//设置秘钥
|
||||||
|
if (platform == 'android') {
|
||||||
|
//开启SDK日志打印
|
||||||
|
aLiSDKModule.setLoggerEnable(true);
|
||||||
|
aLiSDKModule.expandAuthPageCheckedScope(true);
|
||||||
|
//禁用物理返回键
|
||||||
|
//aLiSDKModule.closeAuthPageReturnBack(true);
|
||||||
|
//开启区分界面返回及物理返回功能,自动控制后续返回事件
|
||||||
|
aLiSDKModule.userControlAuthPageCancel();
|
||||||
|
//是否跟随系统深色模式
|
||||||
|
aLiSDKModule.setAuthPageUseDayLight(false);
|
||||||
|
sdkInfo =
|
||||||
|
'VXQhWWtyx6hqQIh6W8oMB2Kdzarg+2TSlQOvRb8fCUA1ygRWUKLsYkTuncn1VB1EK5gVvwoYZNXPF26x853cRzOfYW+lLKDNjMX7QZFgift8remIOFof9wjwyZFuHW2gBQAef6O6vxXeobInLVmJ/6sPqpJylk0ipYe+KBkEhKwiaNZudEHGrTFMQ2PvaNNjnnIWTB6WQJRTljArPvbgVT4FjXTwY8BJlFBR84CGE4k6HRRs1/ov1cBbON3zz6xE9DUlJhS43eEYVRYosJxrfDHfgz+++CfjinSGOX1m474=';
|
||||||
|
} else if (platform == 'ios') {
|
||||||
|
sdkInfo =
|
||||||
|
'+4L59pm2xfA84cMIyUc4//tTAznhbtCGZ5bXa3Ax8UUTCH3IwxX0F0o8gVw7eanDoI3XVvriXMpQA3wcnAEbBD75JswXKAG0+ekz2cVUAgpHLGU3rDkakqTyRC9LIS4aHsJ/i9yv94tEAifXkxlIzdmZbT6dyYAexeJL6dLd6olEnFuKaaSRRl0VBtvSC5HQKlYqRZQ82ljIpAWCqaDG74lvsKfdxKU0wQPBQPzO15nJQOSfLIQpRsBm8STvkmeR'
|
||||||
|
}
|
||||||
|
aLiSDKModule.setAuthSDKInfo(sdkInfo)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// await uploadApp.uploadApp('onLaunch');
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
|
||||||
|
console.log('App Launch')
|
||||||
|
this.globalData.deviceId = uni.getDeviceInfo().deviceId;
|
||||||
|
// this.globalData.deviceid = 'AC6F605124DFA87DCE40D5C062C94C3B';
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
if (uni.getStorageSync('refreshToken')) {
|
||||||
|
let endTime = uni.getStorageSync('refresh_token_expries');
|
||||||
|
endTime = new Date(endTime)
|
||||||
|
// 当前系统时间
|
||||||
|
var nowTime = new Date();
|
||||||
|
var residueTime = (endTime.getTime() - nowTime.getTime() / 1000);
|
||||||
|
// 4小时 14400
|
||||||
|
if (residueTime < 4 * 60 * 60) {
|
||||||
|
console.log('----------------------------')
|
||||||
|
|
||||||
|
uni.removeStorageSync('accessToken')
|
||||||
|
uni.removeStorageSync('refreshToken')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (wx.getStorageSync('refreshToken')) {
|
||||||
|
let endTime = wx.getStorageSync('refresh_token_expries');
|
||||||
|
endTime = new Date(endTime)
|
||||||
|
// 当前系统时间
|
||||||
|
var nowTime = new Date();
|
||||||
|
var residueTime = (endTime.getTime() - nowTime.getTime() / 1000);
|
||||||
|
// 4小时 14400
|
||||||
|
if (residueTime < 4 * 60 * 60) {
|
||||||
|
console.log('----------------------------')
|
||||||
|
|
||||||
|
wx.removeStorageSync('accessToken')
|
||||||
|
wx.removeStorageSync('refreshToken')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
//隐藏原生Tabar
|
||||||
|
uni.hideTabBar()
|
||||||
|
console.log('App Show')
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
console.log('App Hide')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 验证登陆的全局方法
|
||||||
|
verifyLogin() {
|
||||||
|
if (!vuex.state.userInfo.token) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '您需要登陆后才能进行此操作',
|
||||||
|
confirmColor: '#F1AC66',
|
||||||
|
success(e) {
|
||||||
|
if (e.confirm) {
|
||||||
|
// uni.oprPresentLogin()
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/blogPopup/blogPopup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 区/县adcode转换为6位市adcode(后两位补0)
|
||||||
|
* @param {string|number} districtAdcode - 6位区/县编码(字符串或数字类型)
|
||||||
|
* @returns {string|null} 6位市编码,无效编码返回null
|
||||||
|
*/
|
||||||
|
districtAdcodeTo6DigitCityAdcode(districtAdcode) {
|
||||||
|
// 1. 统一转换为字符串并去除空格
|
||||||
|
const adcodeStr = String(districtAdcode).trim();
|
||||||
|
|
||||||
|
// 2. 验证编码有效性(6位数字)
|
||||||
|
const adcodeReg = /^\d{6}$/;
|
||||||
|
if (!adcodeReg.test(adcodeStr)) {
|
||||||
|
console.warn('无效的区/县编码,必须是6位数字');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 截取前4位 + 拼接"00" → 6位市编码
|
||||||
|
const cityAdcode4Digit = adcodeStr.slice(0, 4);
|
||||||
|
const cityAdcode6Digit = cityAdcode4Digit + '00';
|
||||||
|
|
||||||
|
// 4. 可选验证:确保结果为6位数字
|
||||||
|
if (!/^\d{6}$/.test(cityAdcode6Digit)) {
|
||||||
|
console.warn('编码转换异常');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cityAdcode6Digit;
|
||||||
|
},
|
||||||
|
// 更新全局定位
|
||||||
|
updateAddress(item) {
|
||||||
|
console.log('item', item)
|
||||||
|
let location = item.location.split(',');
|
||||||
|
this.globalData.latitude = location[1];
|
||||||
|
this.globalData.longitude = location[0];
|
||||||
|
this.globalData.address = item.name;
|
||||||
|
this.globalData.servers_region = `${item.pname}-${item.cityname}-${item.adname}`;
|
||||||
|
this.globalData.pcode = item.pcode;
|
||||||
|
this.globalData.cCode = this.districtAdcodeTo6DigitCityAdcode(item.adcode);
|
||||||
|
this.globalData.adcode = item.adcode;
|
||||||
|
//存储用户定位
|
||||||
|
this.globalData.addressRes.latitude = location[1];
|
||||||
|
this.globalData.addressRes.longitude = location[0];
|
||||||
|
this.globalData.addressRes.address.province = item.pname;
|
||||||
|
this.globalData.addressRes.address.city = item.cityname;
|
||||||
|
this.globalData.addressRes.address.district = item.adname;
|
||||||
|
this.globalData.addressRes.address.street = item.address;
|
||||||
|
this.globalData.addressRes.address.poiName = item.name;
|
||||||
|
this.globalData.addressRes.address.streetNum = '';
|
||||||
|
this.globalData.addressRes.address.cityCode = '';
|
||||||
|
this.globalData.addressRes.address.pcode = item.pcode;
|
||||||
|
this.globalData.addressRes.address.cCode = this.districtAdcodeTo6DigitCityAdcode(item.adcode);
|
||||||
|
this.globalData.addressRes.address.adcode = item.adcode;
|
||||||
|
this.globalData.addressRes.pcode = item.pcode;
|
||||||
|
this.globalData.addressRes.cCode = this.districtAdcodeTo6DigitCityAdcode(item.adcode);
|
||||||
|
this.globalData.addressRes.adcode = item.adcode;
|
||||||
|
console.log('globalData', this.globalData)
|
||||||
|
uni.setStorageSync('userAdrees', this.globalData)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/*每个页面公共css */
|
||||||
|
@import '@/static/css/common.css';
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>WeChat</key>
|
||||||
|
<dict>
|
||||||
|
<key>appid</key>
|
||||||
|
<string>wx5b1c0c8d01ded35d</string>
|
||||||
|
<key>universalLink</key>
|
||||||
|
<string>https://app.mrrweb.com.cn/</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>WeChat</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>wx5b1c0c8d01ded35d</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"version" : "1",
|
||||||
|
"prompt" : "template",
|
||||||
|
"title" : "美融融平台服务协议及隐私政策",
|
||||||
|
"message" : "您注册为美融融用户的过程中,需要完成我们的注册流程并且以点击的形式在线签署以下协议, 请您务必仔细阅读、充分理解协议中的条款内容后点击同意: <a href=\"https://www.mrrweb.com.cn/junit/system/yhxy\">《美融融平台服务协议》</a>和<a href=\"https://www.mrrweb.com.cn/junit/system/yszc\">《隐私政策》</a>",
|
||||||
|
"buttonAccept" : "同意",
|
||||||
|
"buttonRefuse" : "不同意",
|
||||||
|
"hrefLoader" : "system",
|
||||||
|
"backToExit" : "false",
|
||||||
|
"disagreeMode" : {
|
||||||
|
"support" : false,
|
||||||
|
"loadNativePlugins" : false,
|
||||||
|
"visitorEntry" : false,
|
||||||
|
"showAlways" : false
|
||||||
|
},
|
||||||
|
"styles" : {
|
||||||
|
"backgroundColor" : "#ffffff",
|
||||||
|
"borderRadius" : "5px",
|
||||||
|
"title" : {
|
||||||
|
"color" : "#000000"
|
||||||
|
},
|
||||||
|
"buttonAccept" : {
|
||||||
|
"color" : "#f00"
|
||||||
|
},
|
||||||
|
"buttonRefuse" : {
|
||||||
|
"color" : "#000"
|
||||||
|
},
|
||||||
|
"buttonVisitor" : {
|
||||||
|
"color" : "#00ffff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 上传相关接口
|
||||||
|
export const uploadApi = {
|
||||||
|
// 上传图片
|
||||||
|
uploadPhoto(filePath) {
|
||||||
|
return request.upload('/user/uploadphoto', filePath, 'file')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上传视频
|
||||||
|
uploadVideo(filePath) {
|
||||||
|
return request.upload('/user/uploadvideo', filePath, 'video')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户相关接口
|
||||||
|
export const userApi = {
|
||||||
|
// 登录
|
||||||
|
login(data) {
|
||||||
|
return request.post('', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
register(data) {
|
||||||
|
return request.post('', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
getUserInfo() {
|
||||||
|
return request.get('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他接口...
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"adapteByMiniprogram": {
|
||||||
|
"userName": "gh_89c92e18b70b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"apps": [],
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appID": "wx5b1c0c8d01ded35d",
|
||||||
|
"paths": ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,414 @@
|
||||||
|
<template>
|
||||||
|
<view class="chunk-upload">
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<button @click="chooseVideo" :disabled="uploading">
|
||||||
|
{{ uploading ? `上传中...${progress}%` : '选择视频上传' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<progress v-if="uploading" :percent="progress" show-info stroke-width="3" />
|
||||||
|
|
||||||
|
<!-- 上传状态 -->
|
||||||
|
<view class="status" v-if="status">
|
||||||
|
{{ status }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
<script>
|
||||||
|
// 引入 spark-md5 计算文件哈希
|
||||||
|
import SparkMD5 from 'spark-md5'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChunkUpload',
|
||||||
|
props: {
|
||||||
|
// 上传接口地址
|
||||||
|
uploadUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// 分片大小 (默认2MB)
|
||||||
|
chunkSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 2 * 1024 * 1024
|
||||||
|
},
|
||||||
|
// 允许的视频类型
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: 'video/*'
|
||||||
|
},
|
||||||
|
// 最大文件大小 (MB)
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uploading: false,
|
||||||
|
progress: 0,
|
||||||
|
status: '',
|
||||||
|
file: null,
|
||||||
|
fileMd5: '',
|
||||||
|
cancelToken: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
methods: {
|
||||||
|
// 选择视频文件
|
||||||
|
async chooseVideo() {
|
||||||
|
if (this.uploading) {
|
||||||
|
uni.showToast({ title: '正在上传中', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.getFile()
|
||||||
|
if (!res) return
|
||||||
|
|
||||||
|
this.file = res
|
||||||
|
const sizeMB = res.size / 1024 / 1024
|
||||||
|
|
||||||
|
if (sizeMB > this.maxSize) {
|
||||||
|
uni.showToast({ title: `文件不能超过${this.maxSize}MB`, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: '计算文件MD5...', mask: true })
|
||||||
|
this.fileMd5 = await this.calculateFileMD5(res)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
this.startUpload()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择文件出错:', error)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '选择文件失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取文件 (兼容各平台)
|
||||||
|
getFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// #ifdef H5
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = this.accept
|
||||||
|
input.onchange = (e) => {
|
||||||
|
resolve(e.target.files[0])
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.chooseMedia({
|
||||||
|
count: 1,
|
||||||
|
mediaType: ['video'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
resolve(res.tempFiles[0])
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算文件MD5 (分块计算)
|
||||||
|
calculateFileMD5(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const chunkSize = 1 * 1024 * 1024 // 1MB分块计算MD5
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize)
|
||||||
|
const spark = new SparkMD5.ArrayBuffer()
|
||||||
|
let currentChunk = 0
|
||||||
|
|
||||||
|
const loadNext = () => {
|
||||||
|
const start = currentChunk * chunkSize
|
||||||
|
const end = Math.min(start + chunkSize, file.size)
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
spark.append(e.target.result)
|
||||||
|
currentChunk++
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext()
|
||||||
|
} else {
|
||||||
|
resolve(spark.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file.slice(start, end))
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
const fileReader = new plus.io.FileReader()
|
||||||
|
fileReader.onload = (e) => {
|
||||||
|
spark.append(e.target.result)
|
||||||
|
currentChunk++
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext()
|
||||||
|
} else {
|
||||||
|
resolve(spark.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileReader.readAsArrayBuffer(file.slice(start, end))
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-TOUTIAO
|
||||||
|
const fs = uni.getFileSystemManager()
|
||||||
|
fs.readFile({
|
||||||
|
filePath: file.path,
|
||||||
|
position: start,
|
||||||
|
length: end - start,
|
||||||
|
arrayBuffer: true,
|
||||||
|
success: (res) => {
|
||||||
|
spark.append(res.data)
|
||||||
|
currentChunk++
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext()
|
||||||
|
} else {
|
||||||
|
resolve(spark.end())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error("读取文件失败:", err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNext()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 开始分片上传
|
||||||
|
async startUpload() {
|
||||||
|
if (!this.file) return
|
||||||
|
|
||||||
|
this.uploading = true
|
||||||
|
this.progress = 0
|
||||||
|
this.status = '准备上传...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检查文件是否已上传过
|
||||||
|
const checkRes = await this.checkFile()
|
||||||
|
|
||||||
|
if (checkRes.uploaded) {
|
||||||
|
this.status = '文件已存在,秒传成功'
|
||||||
|
this.progress = 100
|
||||||
|
this.$emit('success', checkRes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 开始分片上传
|
||||||
|
const uploadedChunks = checkRes.uploadedChunks || []
|
||||||
|
const chunks = Math.ceil(this.file.size / this.chunkSize)
|
||||||
|
let uploadedSize = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks; i++) {
|
||||||
|
if (uploadedChunks.includes(i)) {
|
||||||
|
uploadedSize += this.getChunkSize(i)
|
||||||
|
this.updateProgress(uploadedSize)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传当前分片
|
||||||
|
await this.uploadChunk(i)
|
||||||
|
uploadedSize += this.getChunkSize(i)
|
||||||
|
this.updateProgress(uploadedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 合并分片
|
||||||
|
this.status = '正在合并文件...'
|
||||||
|
const mergeRes = await this.mergeChunks()
|
||||||
|
|
||||||
|
this.status = '上传成功'
|
||||||
|
this.$emit('success', mergeRes)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传出错:', error)
|
||||||
|
this.status = '上传失败: ' + (error.message || error.errMsg || '未知错误')
|
||||||
|
this.$emit('fail', error)
|
||||||
|
} finally {
|
||||||
|
this.uploading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查文件状态
|
||||||
|
checkFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
url: `${this.uploadUrl}/check`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
fileMd5: this.fileMd5,
|
||||||
|
fileName: this.file.name,
|
||||||
|
fileSize: this.file.size,
|
||||||
|
chunkSize: this.chunkSize
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
resolve(res.data.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error(res.data.msg))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上传分片
|
||||||
|
uploadChunk(chunkIndex) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const start = chunkIndex * this.chunkSize
|
||||||
|
const end = Math.min(start + this.chunkSize, this.file.size)
|
||||||
|
const chunkBlob = this.file.slice(start, end)
|
||||||
|
|
||||||
|
// #ifdef H5 || APP-PLUS
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', chunkBlob)
|
||||||
|
formData.append('chunkIndex', chunkIndex)
|
||||||
|
formData.append('fileMd5', this.fileMd5)
|
||||||
|
formData.append('chunkSize', this.chunkSize)
|
||||||
|
formData.append('totalChunks', Math.ceil(this.file.size / this.chunkSize))
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
this.cancelToken = xhr
|
||||||
|
|
||||||
|
xhr.open('POST', `${this.uploadUrl}/upload`, true)
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
// 计算当前分片的上传进度
|
||||||
|
const chunkProgress = Math.round((e.loaded / e.total) * 100)
|
||||||
|
const baseProgress = (chunkIndex / Math.ceil(this.file.size / this.chunkSize)) * 100
|
||||||
|
this.progress = Math.min(baseProgress + (chunkProgress / Math.ceil(this.file.size / this.chunkSize)), 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const res = JSON.parse(xhr.responseText)
|
||||||
|
if (res.code === 0) {
|
||||||
|
resolve(res.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error(res.msg))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`上传失败: ${xhr.status}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
reject(new Error('上传出错'))
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(formData)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-TOUTIAO
|
||||||
|
uni.uploadFile({
|
||||||
|
url: `${this.uploadUrl}/upload`,
|
||||||
|
filePath: this.file.path,
|
||||||
|
name: 'file',
|
||||||
|
formData: {
|
||||||
|
chunkIndex,
|
||||||
|
fileMd5: this.fileMd5,
|
||||||
|
chunkSize: this.chunkSize,
|
||||||
|
totalChunks: Math.ceil(this.file.size / this.chunkSize))
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
if (data.code === 0) {
|
||||||
|
resolve(data.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error(data.msg))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 合并分片
|
||||||
|
mergeChunks() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
url: `${this.uploadUrl}/merge`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
fileMd5: this.fileMd5,
|
||||||
|
fileName: this.file.name,
|
||||||
|
fileSize: this.file.size,
|
||||||
|
chunkSize: this.chunkSize
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
resolve(res.data.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error(res.data.msg))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前分片大小
|
||||||
|
getChunkSize(chunkIndex) {
|
||||||
|
const start = chunkIndex * this.chunkSize
|
||||||
|
const end = Math.min(start + this.chunkSize, this.file.size)
|
||||||
|
return end - start
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
updateProgress(uploadedSize) {
|
||||||
|
this.progress = Math.round((uploadedSize / this.file.size) * 100)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消上传
|
||||||
|
cancelUpload() {
|
||||||
|
if (this.cancelToken) {
|
||||||
|
// #ifdef H5 || APP-PLUS
|
||||||
|
this.cancelToken.abort()
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-TOUTIAO
|
||||||
|
// 小程序端无法直接取消上传,需要标记取消状态
|
||||||
|
// #endif
|
||||||
|
this.cancelToken = null
|
||||||
|
}
|
||||||
|
this.uploading = false
|
||||||
|
this.status = '上传已取消'
|
||||||
|
this.$emit('cancel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chunk-upload {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
color: #666;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
<template>
|
||||||
|
<transition name="popup-fade">
|
||||||
|
<div class="global-popup" v-show="visible">
|
||||||
|
<!-- 遮罩层 -->
|
||||||
|
<div class="popup-mask" @click="maskClose && handleClose"></div>
|
||||||
|
<!-- 弹窗主体 -->
|
||||||
|
<div class="popup-main" :style="{ width: width + 'px' }">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="popup-title" v-if="title">{{ title }}</div>
|
||||||
|
<!-- 自定义内容插槽 -->
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<!-- 默认内容 -->
|
||||||
|
<div class="popup-content" v-if="!$slots.content">{{ content }}</div>
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<div class="popup-footer" v-if="showBtn">
|
||||||
|
<button class="btn cancel" @click="handleCancel" v-if="showCancelBtn">
|
||||||
|
{{ cancelText }}
|
||||||
|
</button>
|
||||||
|
<button class="btn confirm" @click="handleConfirm">
|
||||||
|
{{ confirmText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "GlobalPopup",
|
||||||
|
props: {
|
||||||
|
// 是否显示弹窗
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 弹窗标题
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// 弹窗内容
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// 弹窗宽度(px)
|
||||||
|
width: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
// 是否显示按钮
|
||||||
|
showBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 是否显示取消按钮
|
||||||
|
showCancelBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 取消按钮文字
|
||||||
|
cancelText: {
|
||||||
|
type: String,
|
||||||
|
default: "取消",
|
||||||
|
},
|
||||||
|
// 确认按钮文字
|
||||||
|
confirmText: {
|
||||||
|
type: String,
|
||||||
|
default: "确认",
|
||||||
|
},
|
||||||
|
// 点击遮罩是否关闭
|
||||||
|
maskClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 通用关闭方法
|
||||||
|
handleClose() {
|
||||||
|
this.$emit("update:visible", false);
|
||||||
|
},
|
||||||
|
// 取消按钮回调
|
||||||
|
handleCancel() {
|
||||||
|
this.handleClose();
|
||||||
|
this.$emit("cancel");
|
||||||
|
},
|
||||||
|
// 确认按钮回调
|
||||||
|
handleConfirm() {
|
||||||
|
this.handleClose();
|
||||||
|
this.$emit("confirm");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Vue2 过渡动画类名 */
|
||||||
|
.popup-fade-enter,
|
||||||
|
.popup-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.popup-fade-enter-active,
|
||||||
|
.popup-fade-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗容器:固定定位+居中 */
|
||||||
|
.global-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999; /* 高层级保证不被覆盖 */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层 */
|
||||||
|
.popup-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗主体 */
|
||||||
|
.popup-main {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题样式 */
|
||||||
|
.popup-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容样式 */
|
||||||
|
.popup-content {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮容器 */
|
||||||
|
.popup-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
.cancel {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.confirm {
|
||||||
|
background: #007aff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<!-- 固定在底部,适配安全区 -->
|
||||||
|
<view class="custom-tabbar" :style="{ paddingBottom: safeBottom + 'px' }">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in tabList"
|
||||||
|
:key="index"
|
||||||
|
class="tab-item"
|
||||||
|
@click="handleSwitch(index)"
|
||||||
|
>
|
||||||
|
<!-- 图标 -->
|
||||||
|
<image
|
||||||
|
:src="currentIndex === index ? item.selectedIcon : item.icon"
|
||||||
|
class="tab-icon"
|
||||||
|
/>
|
||||||
|
<!-- 文字 -->
|
||||||
|
<text class="tab-text" :class="{ active: currentIndex === index }">{{
|
||||||
|
item.text
|
||||||
|
}}</text>
|
||||||
|
<!-- 徽标/小红点 -->
|
||||||
|
<view
|
||||||
|
v-if="item.badge"
|
||||||
|
class="tab-badge"
|
||||||
|
:style="{ backgroundColor: item.badgeColor || '#ff4d4f' }"
|
||||||
|
>
|
||||||
|
{{ item.badge === true ? "" : item.badge }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// Tab 配置:[{ text, icon, selectedIcon, path, badge }]
|
||||||
|
tabList: { type: Array, required: true },
|
||||||
|
// 当前激活索引
|
||||||
|
currentIndex: { type: Number, default: 0 },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
safeBottom: 0, // iOS 底部安全距离
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 计算安全距离
|
||||||
|
uni.getSystemInfo({
|
||||||
|
success: (res) => {
|
||||||
|
this.safeBottom = res.safeAreaInsets.bottom;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSwitch(index) {
|
||||||
|
if (index === this.currentIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 跳转 Tab 页面(必须用 switchTab)
|
||||||
|
// uni.redirectTo({ url: this.tabList[index].path , animationType: 'none' });
|
||||||
|
// 通知父组件更新激活态(或通过 Vuex 管理)
|
||||||
|
this.$emit("change", index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.custom-tabbar {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 120rpx;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
// border-top: 1px solid #eee;
|
||||||
|
box-shadow: 0px -8rpx 24rpx 0px rgba(81,118,171,0.2);
|
||||||
|
}
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
.tab-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
}
|
||||||
|
.tab-text {
|
||||||
|
font-size:24rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.tab-text.active {
|
||||||
|
color: #E8101E; /* 选中颜色 */
|
||||||
|
}
|
||||||
|
.tab-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 20rpx;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
min-width: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,459 @@
|
||||||
|
<template>
|
||||||
|
<view class="ShareCanvas">
|
||||||
|
<view class="canvas">
|
||||||
|
<canvas canvas-id="shareCanvas" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ShareCanvas',
|
||||||
|
methods: {
|
||||||
|
setRadius(ctx, r, x, y, width, height) {
|
||||||
|
ctx.beginPath();
|
||||||
|
// 依次绘制四个圆角的弧
|
||||||
|
ctx.moveTo(x + r, y);
|
||||||
|
ctx.arcTo(x + width, y, x + width, y + height, r);
|
||||||
|
ctx.arcTo(x + width, y + height, x, y + height, r);
|
||||||
|
ctx.arcTo(x, y + height, x, y, r);
|
||||||
|
ctx.arcTo(x, y, x + width, y, r);
|
||||||
|
ctx.closePath();
|
||||||
|
},
|
||||||
|
// 订单分享
|
||||||
|
setOrderCanvas(info) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
console.log('订单分享-info', info);
|
||||||
|
try {
|
||||||
|
const ctx = uni.createCanvasContext('shareCanvas', this)
|
||||||
|
// 绘制背景图
|
||||||
|
// ctx.setFillStyle('#19C161')
|
||||||
|
// ctx.fillRect(0, 0, 211, 170) // 保证宽高比是 5:4
|
||||||
|
const bgImageUrl = "../../static/images/shop/pintuan/shareBj.png";
|
||||||
|
try {
|
||||||
|
// 2. 异步获取图片信息(利用已封装的getImge方法,确保图片加载完成)
|
||||||
|
const { path: bgImagePath } = await this.getImge(bgImageUrl);
|
||||||
|
// 3. 绘制背景图:参数依次为「图片路径、x坐标、y坐标、宽度、高度」
|
||||||
|
// 宽度211、高度170 与Canvas尺寸一致,确保背景铺满
|
||||||
|
ctx.drawImage(bgImagePath, 0, 0, 211, 170);
|
||||||
|
} catch (bgError) {
|
||||||
|
console.error("背景图加载/绘制失败:", bgError);
|
||||||
|
// 可选:背景图失败时降级用原纯色填充,避免画布空白
|
||||||
|
ctx.setFillStyle('#19C161');
|
||||||
|
ctx.fillRect(0, 0, 211, 170);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制文本信息
|
||||||
|
// ctx.setFontSize(21);
|
||||||
|
// ctx.setTextAlign('left')
|
||||||
|
// ctx.setFillStyle('#FFFFFF')
|
||||||
|
// ctx.fillText('我买好啦!', 9, 32)
|
||||||
|
|
||||||
|
// 浅绿色背景
|
||||||
|
// this.setRadius(ctx, 10, 106, 11, 97, 27) // 加圆角
|
||||||
|
// ctx.setFillStyle('#EFF9F1')
|
||||||
|
// ctx.fill()
|
||||||
|
|
||||||
|
// ctx.fillRect(106, 11, 97, 27) // x, y, width, height
|
||||||
|
|
||||||
|
// ctx.setFontSize(14);
|
||||||
|
// ctx.setTextAlign('center')
|
||||||
|
// ctx.setFillStyle('#19C161')
|
||||||
|
// ctx.fillText('跟团号:' + info.followNum, 155, 30)
|
||||||
|
|
||||||
|
this.setRadius(ctx, 3, 9, 49, 194, 106)
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fill()
|
||||||
|
// ctx.fillRect(9, 49, 194, 106) // 不设置圆角的时候这么画有背景色的矩形
|
||||||
|
|
||||||
|
// 画商品图
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 5, 17, 59, 85, 85)
|
||||||
|
ctx.clip();//画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.orderCartInfos[0].productImg)
|
||||||
|
ctx.drawImage(path, 17, 59, 85, 85)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
if (info.teamLeaderUser && info.teamLeaderUser.avatar) {
|
||||||
|
// 团长头像
|
||||||
|
// ctx.save();
|
||||||
|
// this.setRadius(ctx, 5, 110, 60, 25, 25)
|
||||||
|
// ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
// try {
|
||||||
|
// const { path } = await this.getImge(info.teamLeaderUser.avatar)
|
||||||
|
// ctx.drawImage(path, 110, 60, 25, 25)
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error(error);
|
||||||
|
// }
|
||||||
|
// ctx.restore();
|
||||||
|
|
||||||
|
// // ctx.setFillStyle(grd)
|
||||||
|
// // // ctx.fillRect(230/2, 218/2, 149/2, 53/2) // x, y, width, height
|
||||||
|
// // ctx.fill()
|
||||||
|
|
||||||
|
// ctx.setFontSize(12);
|
||||||
|
// ctx.setTextAlign('center')
|
||||||
|
// ctx.setFillStyle('#FFFFFF')
|
||||||
|
// ctx.fillText('立即成团 >', 152, 135)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.teamLeaderUser && info.teamLeaderUser.nickname) {
|
||||||
|
// 团长昵称
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('left')
|
||||||
|
ctx.setFillStyle('rgba(253, 218, 226, 1)')
|
||||||
|
ctx.fillText(info.teamLeaderUser.nickname.length > 4 ? info.teamLeaderUser.nickname.slice(0, 4) + '...' : info.teamLeaderUser.nickname, 140, 76)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.setFontSize(14);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FB7415')
|
||||||
|
ctx.fillText(`¥${info.orderCartInfos[0].unitPrice}`, 152, 105)
|
||||||
|
|
||||||
|
this.setRadius(ctx, 10, 115, 118, 75, 26)
|
||||||
|
|
||||||
|
const grd = ctx.createLinearGradient(115, 118, 115, 144)
|
||||||
|
grd.addColorStop(0, '#FDAC2F')
|
||||||
|
grd.addColorStop(0.5, '#FDA72C')
|
||||||
|
grd.addColorStop(1, '#FB5615')
|
||||||
|
|
||||||
|
// 橙色按钮背景
|
||||||
|
ctx.setFillStyle(grd)
|
||||||
|
// ctx.fillRect(230/2, 218/2, 149/2, 53/2) // x, y, width, height
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillText('立即成团 >', 152, 135)
|
||||||
|
|
||||||
|
ctx.draw(false, (() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvasId: 'shareCanvas',
|
||||||
|
success: (res) => {
|
||||||
|
return resolve(res.tempFilePath)
|
||||||
|
},
|
||||||
|
fail: function (error) {
|
||||||
|
console.log('fail----fail', error);
|
||||||
|
//TODO
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
}, this)
|
||||||
|
}, 500);
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.log('画图失败error', error);
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 商品分享
|
||||||
|
setGoodsShareCanvas(info) {
|
||||||
|
console.log('商品分享--info', info);
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const ctx = uni.createCanvasContext('shareCanvas', this)
|
||||||
|
// 绘制背景图
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillRect(0, 0, 211, 170)
|
||||||
|
|
||||||
|
// 团长头像
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 5, 0, 0, 30, 30)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.avatar)
|
||||||
|
ctx.drawImage(path, 0, 0, 30, 30)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// 团长昵称
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('left')
|
||||||
|
ctx.setFillStyle('#96999B')
|
||||||
|
ctx.fillText(info.nickname.length > 11 ? info.nickname.slice(0, 11) + `${info.pinkId ? '...的团' : '...'}` : info.nickname + '的团', 35, 18)
|
||||||
|
|
||||||
|
// 商品1图
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 3, 0, 35, 211, 211)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.image)
|
||||||
|
ctx.drawImage(path, 0, 35, 211, 211)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// 绿色背景
|
||||||
|
ctx.setFillStyle('#19C161')
|
||||||
|
ctx.fillRect(0, 130, 131, 40) // x, y, width, height
|
||||||
|
|
||||||
|
ctx.setFontSize(16);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillText(`¥${info.pinkId ? info.pinkPrice : info.price}起`, 65, 130 + 18)
|
||||||
|
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillText(`¥${info.otPrice}`, 65, 130 + 34)
|
||||||
|
|
||||||
|
// 划线
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.setLineWidth(1);
|
||||||
|
ctx.setStrokeStyle('#FFFFFF')
|
||||||
|
ctx.moveTo(40, 130 + 30)
|
||||||
|
ctx.lineTo(90, 130 + 30)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 深绿色背景
|
||||||
|
ctx.setFillStyle('#19AF5C')
|
||||||
|
ctx.fillRect(131, 130, 211 - 131, 40)
|
||||||
|
|
||||||
|
if (info.pinkId) {
|
||||||
|
// 立即跟团按钮
|
||||||
|
ctx.setFontSize(16);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillText(`立即跟团`, 131 + (211 - 131) / 2, 130 + 26)
|
||||||
|
} else {
|
||||||
|
// 已团数量
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillText(`已团 ${this.getSales(info.sales)} 件`, 131 + (211 - 131) / 2, 130 + 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.draw(false, (() => {
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvasId: 'shareCanvas',
|
||||||
|
success: (res) => {
|
||||||
|
return resolve(res.tempFilePath)
|
||||||
|
},
|
||||||
|
fail: function (error) {
|
||||||
|
console.log('fail----fail', error);
|
||||||
|
//TODO
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
}, this)
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.log('画图失败error', error);
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 团分享
|
||||||
|
setGroupShareCanvas(info) {
|
||||||
|
console.log('团分享-info', info);
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const ctx = uni.createCanvasContext('shareCanvas', this)
|
||||||
|
// 绘制背景图
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillRect(0, 0, 211, 170)
|
||||||
|
|
||||||
|
if (info.productImageList.length == 1) {
|
||||||
|
// 团长头像
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 5, 0, 0, 30, 30)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.avatar)
|
||||||
|
ctx.drawImage(path, 0, 0, 30, 30)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// 团长昵称
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('left')
|
||||||
|
ctx.setFillStyle('#96999B')
|
||||||
|
ctx.fillText((info.nickname.length > 11 ? info.nickname.slice(0, 11) + '...' : info.nickname) + '的团', 35, 18)
|
||||||
|
|
||||||
|
// 商品1图
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 3, 0, 35, 211, 211)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.productImageList[0])
|
||||||
|
ctx.drawImage(path, 0, 35, 211, 211)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.productImageList.length >= 2) {
|
||||||
|
// 团长头像
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 5, 0, 0, 42, 42)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.avatar)
|
||||||
|
ctx.drawImage(path, 0, 0, 42, 42)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// 团长昵称
|
||||||
|
ctx.setFontSize(12);
|
||||||
|
ctx.setTextAlign('left')
|
||||||
|
ctx.setFillStyle('#96999B')
|
||||||
|
ctx.fillText((info.nickname.length > 10 ? info.nickname.slice(0, 10) + '...' : info.nickname) + '的团', 47, 25)
|
||||||
|
|
||||||
|
// 商品1图
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 3, 0, 51, 69, 69)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.productImageList[0])
|
||||||
|
ctx.drawImage(path, 0, 51, 69, 69)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// 商品2图
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 3, 69 + 2, 51, 69, 69)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.productImageList[1])
|
||||||
|
ctx.drawImage(path, 69 + 2, 51, 69, 69)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
if (info.productImageList.length >= 3) {
|
||||||
|
// 商品3图
|
||||||
|
ctx.save();
|
||||||
|
this.setRadius(ctx, 3, 69 * 2 + 4, 51, 69, 69)
|
||||||
|
ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
|
||||||
|
try {
|
||||||
|
const { path } = await this.getImge(info.productImageList[2])
|
||||||
|
ctx.drawImage(path, 69 * 2 + 4, 51, 69, 69)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绿色背景
|
||||||
|
ctx.setFillStyle('#19C161')
|
||||||
|
ctx.fillRect(0, 128, 211, 42) // x, y, width, height
|
||||||
|
|
||||||
|
ctx.setFontSize(16);
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
ctx.setFillStyle('#FFFFFF')
|
||||||
|
ctx.fillText(`立即跟团`, 211 / 2, 128 + 26)
|
||||||
|
|
||||||
|
ctx.draw(false, (() => {
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvasId: 'shareCanvas',
|
||||||
|
success: (res) => {
|
||||||
|
return resolve(res.tempFilePath)
|
||||||
|
},
|
||||||
|
fail: function (error) {
|
||||||
|
console.log('fail----fail', error);
|
||||||
|
//TODO
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
}, this)
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.log('画图失败error', error);
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 设置圆角矩形
|
||||||
|
*
|
||||||
|
* @param ctx 绘图上下文
|
||||||
|
* @param cornerRadius 圆角半径
|
||||||
|
* @param width 矩形宽度
|
||||||
|
* @param height 矩形高度
|
||||||
|
* @param x 矩形左上角的 x 坐标
|
||||||
|
* @param y 矩形左上角的 y 坐标
|
||||||
|
* @returns 无返回值
|
||||||
|
*/
|
||||||
|
setRadius(ctx, cornerRadius, x, y, width, height) {
|
||||||
|
// 开始绘制路径
|
||||||
|
ctx.beginPath();
|
||||||
|
// 绘制最左侧的圆角
|
||||||
|
ctx.arc(x + cornerRadius, y + cornerRadius, cornerRadius, Math.PI, Math.PI * 1.5);
|
||||||
|
// 绘制顶部边缘
|
||||||
|
ctx.moveTo(x + cornerRadius, y);
|
||||||
|
ctx.lineTo(x + width - cornerRadius, y);
|
||||||
|
ctx.lineTo(x + width, y + cornerRadius);
|
||||||
|
// 绘制最右侧的圆角
|
||||||
|
ctx.arc(x + width - cornerRadius, y + cornerRadius, cornerRadius, Math.PI * 1.5, Math.PI * 2);
|
||||||
|
// 绘制右侧边缘
|
||||||
|
ctx.lineTo(x + width, y + height - cornerRadius);
|
||||||
|
ctx.lineTo(x + width - cornerRadius, y + height);
|
||||||
|
// 绘制最下侧的圆角
|
||||||
|
ctx.arc(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0, Math.PI * 0.5);
|
||||||
|
// 绘制底部边缘
|
||||||
|
ctx.lineTo(x + cornerRadius, y + height);
|
||||||
|
ctx.lineTo(x, y + height - cornerRadius);
|
||||||
|
// 绘制最左侧的圆角
|
||||||
|
ctx.arc(x + cornerRadius, y + height - cornerRadius, cornerRadius, Math.PI * 0.5, Math.PI);
|
||||||
|
// 绘制左侧边缘
|
||||||
|
ctx.lineTo(x, y + cornerRadius);
|
||||||
|
ctx.lineTo(x + cornerRadius, y);
|
||||||
|
// 闭合路径
|
||||||
|
ctx.closePath();
|
||||||
|
},
|
||||||
|
// 获取图片地址
|
||||||
|
getImge(path) {
|
||||||
|
// 利用promise异步转同步,否则可能显示不了~
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.getImageInfo({
|
||||||
|
src: path,
|
||||||
|
success: function (res) {
|
||||||
|
if (res && res.path) {
|
||||||
|
resolve(res)
|
||||||
|
} else {
|
||||||
|
reject(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: function (res) {
|
||||||
|
reject(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getSales(sales) {
|
||||||
|
return sales >= 10000 ? sales / 10000 + 'w+' : sales
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 隐藏画布
|
||||||
|
.ShareCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: -200px;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
.canvas canvas {
|
||||||
|
width: 211px;
|
||||||
|
height: 170px; // +16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<view class="agree-radio" :style="{width:radioSize,height:radioSize}">
|
||||||
|
<image v-if="!isAgree"
|
||||||
|
:src="noImg"
|
||||||
|
class="agree-btn"
|
||||||
|
mode="aspectFit"
|
||||||
|
@click="changeAgree(true)" :style="{width:radioSize,height:radioSize}"></image>
|
||||||
|
<image v-else
|
||||||
|
:src="yesImg"
|
||||||
|
class="agree-btn"
|
||||||
|
mode="aspectFit"
|
||||||
|
@click="changeAgree(false)" :style="{width:radioSize,height:radioSize}"></image>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name:"AgreeRadio",
|
||||||
|
props: {
|
||||||
|
// 是否显示返回按钮
|
||||||
|
isAgree: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
yesImg :{
|
||||||
|
type: String,
|
||||||
|
default: "/static/images/agree_y.png"
|
||||||
|
},
|
||||||
|
noImg :{
|
||||||
|
type: String,
|
||||||
|
default: "/static/images/agree_n.png"
|
||||||
|
},
|
||||||
|
radioSize:{
|
||||||
|
type: String,
|
||||||
|
default: "32rpx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeAgree(value) {
|
||||||
|
this.$emit('change-agree', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.agree-radio {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
.agree-btn {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,542 @@
|
||||||
|
<template>
|
||||||
|
<view class="ali-oss-uploader">
|
||||||
|
<view class="preview-container" :style="'width:'+width">
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width">
|
||||||
|
<view
|
||||||
|
v-for="(file, index) in fileList"
|
||||||
|
:key="index"
|
||||||
|
class="preview-item"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:src="file"
|
||||||
|
class="preview-image"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="handlePreview(index)"
|
||||||
|
/>
|
||||||
|
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete(index)">
|
||||||
|
×
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<view class="preview-item">
|
||||||
|
<view
|
||||||
|
class="upload-btn"
|
||||||
|
@click="handleUpload"
|
||||||
|
:style="btnStyle"
|
||||||
|
v-if="fileList.length < maxCount"
|
||||||
|
>
|
||||||
|
<slot name="button">
|
||||||
|
<text class="plus-icon">+</text>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 视频预览区域 -->
|
||||||
|
<view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'">
|
||||||
|
<view
|
||||||
|
class="preview-item"
|
||||||
|
v-if="oneFile"
|
||||||
|
>
|
||||||
|
<!-- <video
|
||||||
|
:src="oneFile"
|
||||||
|
class="preview-image"
|
||||||
|
@click="handlePreview(index)"
|
||||||
|
/> -->
|
||||||
|
<image class="preview-image" :src="oneFile + '?x-oss-process=video/snapshot,t_0,f_jpg'" mode="aspectFill"></image>
|
||||||
|
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete()">
|
||||||
|
×
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="preview-item">
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<view
|
||||||
|
class="upload-btn"
|
||||||
|
@click="handleUpload"
|
||||||
|
:style="btnStyle"
|
||||||
|
v-if="!oneFile"
|
||||||
|
>
|
||||||
|
<slot name="button">
|
||||||
|
<text class="plus-icon">+</text>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 提示文字 -->
|
||||||
|
<!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '../../utils/request'
|
||||||
|
import permissionUtils from '../../utils/per'
|
||||||
|
import locationService from '../../utils/locationService';
|
||||||
|
export default {
|
||||||
|
name: 'AliOssUploader',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
// 已上传文件列表
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
// 单个上传文件
|
||||||
|
fileString: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 按钮文字
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
default: '上传图片'
|
||||||
|
},
|
||||||
|
// 提示文字
|
||||||
|
tips: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 最大上传数量
|
||||||
|
maxCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 3
|
||||||
|
},
|
||||||
|
// 单个文件大小限制(MB)
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
},
|
||||||
|
// 是否显示预览
|
||||||
|
showPreview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否可删除
|
||||||
|
deletable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 按钮自定义样式
|
||||||
|
btnStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
// 文件类型
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: 'image/*'
|
||||||
|
},
|
||||||
|
// 当前用户id
|
||||||
|
userId: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
kind: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fileList: [],
|
||||||
|
oneFile: '',
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.fileList = [...newVal]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fileString: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.oneFile = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
// 显示权限说明弹窗
|
||||||
|
showPermissionDialog(title, content) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
uni.showModal({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
confirmText: '去开启',
|
||||||
|
success(res) {
|
||||||
|
resolve(res.confirm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 触发上传
|
||||||
|
async handleUpload() {
|
||||||
|
// 判断
|
||||||
|
try {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
if(systemInfo.platform === 'ios') {
|
||||||
|
this.openCamera();
|
||||||
|
}else {
|
||||||
|
let xcpermission = '';
|
||||||
|
let xjpermission = '';
|
||||||
|
if(systemInfo.platform === 'ios') {
|
||||||
|
xcpermission = 'photo_library',
|
||||||
|
xjpermission = 'camera'
|
||||||
|
}else {
|
||||||
|
xcpermission = `android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE`
|
||||||
|
xjpermission = 'android.permission.CAMERA'
|
||||||
|
}
|
||||||
|
const xjfirstRequest = !plus.storage.getItem(`perm_${xjpermission}`)
|
||||||
|
const xcfirstRequest = !plus.storage.getItem(`perm_${xcpermission}`)
|
||||||
|
if(xcfirstRequest || xjfirstRequest) {
|
||||||
|
// this.isShowPer = true;
|
||||||
|
console.log('ckeckisShowPer',true)
|
||||||
|
this.$emit('ckeckisShowPer',true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 检查权限
|
||||||
|
// const { granted } = await permissionUtils.checkPermission('photo_library', '需要相册权限用于上传照片');
|
||||||
|
this.$emit('ckecknowQer','xc')
|
||||||
|
const xc = await permissionUtils.checkPermission('photo_library', '需要相册权限用于上传照片');
|
||||||
|
this.$emit('ckecknowQer','xj')
|
||||||
|
const xj = await permissionUtils.checkPermission('camera', '需要相机权限用于拍摄照片');
|
||||||
|
this.$emit('ckeckisShowPer',false)
|
||||||
|
this.$emit('ckecknowQer','')
|
||||||
|
if (xc.granted && xj.granted) {
|
||||||
|
this.openCamera();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// this.isShowPer = false;
|
||||||
|
// 2. 显示权限说明弹窗
|
||||||
|
const confirm = await this.showPermissionDialog(
|
||||||
|
'相机相册权限申请',
|
||||||
|
'我们需要访问您的相机以及相册以完成更换头像功能'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
// 3. 请求权限
|
||||||
|
this.$emit('ckeckisShowPer',true)
|
||||||
|
this.$emit('ckecknowQer','xc')
|
||||||
|
const result1 = await permissionUtils.requestPermission('photo_library', '我们需要访问您的相册以上传图片/视频');
|
||||||
|
this.$emit('ckecknowQer','xj')
|
||||||
|
const result2 = await permissionUtils.requestPermission('camera', '我们需要访问您的相机以上传图片/视频');
|
||||||
|
if (result1 && result2) {
|
||||||
|
this.openCamera();
|
||||||
|
} else {
|
||||||
|
locationService.openAppSettings();
|
||||||
|
// uni.showToast({ title: '相机权限被拒绝', icon: 'none' });
|
||||||
|
}
|
||||||
|
this.$emit('ckeckisShowPer',false)
|
||||||
|
this.$emit('ckecknowQer','')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传出错:', error)
|
||||||
|
this.$emit('error', error)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openCamera() {
|
||||||
|
if (this.loading) return
|
||||||
|
if (this.fileList.length >= this.maxCount) {
|
||||||
|
return uni.showToast({
|
||||||
|
title: `最多上传${this.maxCount}个文件`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
let res;
|
||||||
|
let tempFiles;
|
||||||
|
if(this.accept == 'image/*') {
|
||||||
|
const res = await uni.chooseImage({
|
||||||
|
count: this.maxCount - this.fileList.length,
|
||||||
|
sizeType: ['original', 'compressed'],
|
||||||
|
sourceType: ['album', 'camera']
|
||||||
|
})
|
||||||
|
const tempFiles = res.tempFiles
|
||||||
|
for (const file of tempFiles) {
|
||||||
|
await this.uploadFile(file)
|
||||||
|
}
|
||||||
|
}else if(this.accept == 'video/*') {
|
||||||
|
const res = await uni.chooseVideo({
|
||||||
|
count: 1,
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
compressed: false
|
||||||
|
})
|
||||||
|
// #ifdef H5
|
||||||
|
const file = res.tempFile;
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
|
const file = res;
|
||||||
|
// #endif
|
||||||
|
await this.uploadFile(file)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传出错:', error)
|
||||||
|
this.$emit('error', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 上传文件到OSS
|
||||||
|
async uploadFile(file) {
|
||||||
|
// 检查文件大小
|
||||||
|
if (file.size > this.maxSize * 1024 * 1024) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `文件大小不能超过${this.maxSize}MB`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return Promise.reject(new Error('文件大小超出限制'))
|
||||||
|
}
|
||||||
|
let date = new Date().getTime()
|
||||||
|
let imageType;
|
||||||
|
if(this.accept == 'image/*') {
|
||||||
|
imageType = file.path.split('.')
|
||||||
|
}else if(this.accept == 'video/*') {
|
||||||
|
// #ifdef H5
|
||||||
|
imageType = file.name.split('.')
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
|
imageType = file.tempFilePath.split('.')
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
console.log('imageType',imageType)
|
||||||
|
|
||||||
|
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
|
||||||
|
let name = `${this.userId}_${artisan}_${date}.${imageType[imageType.length - 1]}`
|
||||||
|
console.log('name',name)
|
||||||
|
try {
|
||||||
|
// 1. 获取OSS上传凭证
|
||||||
|
const ossConfig = await this.getOssConfig()
|
||||||
|
// 2. 上传文件
|
||||||
|
const uploadRes = await this.uploadToOss(file, ossConfig, name)
|
||||||
|
// 3. 添加到文件列表
|
||||||
|
const fileUrl = `${ossConfig.host}/${ossConfig.dir}${name}`
|
||||||
|
if(this.accept == 'image/*') {
|
||||||
|
const newFile = [`${fileUrl}`]
|
||||||
|
this.fileList = this.fileList.concat(newFile)
|
||||||
|
this.$emit('input', this.fileList)
|
||||||
|
this.$emit('change', this.fileList)
|
||||||
|
this.$emit('success', newFile)
|
||||||
|
return newFile
|
||||||
|
}else if(this.accept == 'video/*') {
|
||||||
|
const newFile = fileUrl
|
||||||
|
this.oneFile = fileUrl
|
||||||
|
this.$emit('input', this.oneFile)
|
||||||
|
this.$emit('change', this.oneFile)
|
||||||
|
this.$emit('success', newFile)
|
||||||
|
return newFile
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error)
|
||||||
|
this.$emit('error', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取OSS配置
|
||||||
|
async getOssConfig() {
|
||||||
|
try {
|
||||||
|
const res = await request.post('/user/getalioss',{type: this.type,kind: this.kind})
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取OSS配置失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上传到OSS
|
||||||
|
uploadToOss(file, ossConfig, name) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// #ifdef H5
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('name',name);
|
||||||
|
formData.append('policy', ossConfig.policy);
|
||||||
|
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
||||||
|
formData.append('success_action_status', '200');
|
||||||
|
formData.append('signature', ossConfig.signature);
|
||||||
|
formData.append('key', ossConfig.dir + name);
|
||||||
|
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
|
||||||
|
formData.append('file', file);
|
||||||
|
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
|
||||||
|
resolve(res)
|
||||||
|
}).catch(()=>{
|
||||||
|
reject(new Error(`上传失败: ${res.statusCode}`))
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
|
let formData = {};
|
||||||
|
formData.name = name;
|
||||||
|
formData.policy = ossConfig.policy;
|
||||||
|
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||||
|
formData.success_action_status = '200';
|
||||||
|
formData.signature = ossConfig.signature;
|
||||||
|
formData.key = ossConfig.dir + name;
|
||||||
|
formData.file = file;
|
||||||
|
let path;
|
||||||
|
if(this.accept == 'image/*') {
|
||||||
|
path = file.path
|
||||||
|
}else if(this.accept == 'video/*') {
|
||||||
|
// #ifdef H5
|
||||||
|
path = file.name
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
|
path = file.tempFilePath
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
// 使用 uni.uploadFile 上传文件(支持多平台)
|
||||||
|
uni.uploadFile({
|
||||||
|
url: ossConfig.host,
|
||||||
|
filePath: path, // 手机端文件路径
|
||||||
|
name: 'file',
|
||||||
|
formData: {
|
||||||
|
key: ossConfig.dir + name, // 文件名
|
||||||
|
policy: ossConfig.policy, // 后台获取超时时间
|
||||||
|
OSSAccessKeyId: ossConfig.ossAccessKeyId, // 后台获取临时ID
|
||||||
|
success_action_status: 200, // 让服务端返回200,不然,默认会返回204
|
||||||
|
signature: ossConfig.signature // 后台获取签名
|
||||||
|
},
|
||||||
|
success(res) {
|
||||||
|
resolve(`${ossConfig.host}/${ossConfig.dir}${name}`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// 预览图片
|
||||||
|
handlePreview(index) {
|
||||||
|
uni.previewImage({
|
||||||
|
current: index,
|
||||||
|
urls: this.fileList.map(item => item.url)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
handleDelete(index) {
|
||||||
|
// uni.showModal({
|
||||||
|
// title: '提示',
|
||||||
|
// content: '确定删除该文件吗?',
|
||||||
|
// success: (res) => {
|
||||||
|
// if (res.confirm) {
|
||||||
|
if(this.accept == 'image/*') {
|
||||||
|
const deletedFile = this.fileList[index]
|
||||||
|
this.fileList.splice(index, 1)
|
||||||
|
this.$emit('input', this.fileList)
|
||||||
|
this.$emit('change', this.fileList)
|
||||||
|
this.$emit('delete', deletedFile, index)
|
||||||
|
}else if(this.accept == 'video/*') {
|
||||||
|
const deletedFile = this.oneFile
|
||||||
|
this.oneFile = ''
|
||||||
|
this.$emit('input', this.oneFile)
|
||||||
|
this.$emit('change', this.oneFile)
|
||||||
|
this.$emit('delete', deletedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* .ali-oss-uploader {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.upload-btn {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* margin-top: 20rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
position: relative;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: -10rpx;
|
||||||
|
right: -10rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
<template>
|
||||||
|
<view class="circle-progress" :style="{ width: size + 'rpx', height: size + 'rpx' }">
|
||||||
|
<!-- 背景圆环 -->
|
||||||
|
<canvas
|
||||||
|
:type="canvasType"
|
||||||
|
class="progress-canvas"
|
||||||
|
:style="{ width: size + 'rpx', height: size + 'rpx' }"
|
||||||
|
:id="canvasId"
|
||||||
|
:canvas-id="canvasId"
|
||||||
|
@ready="onCanvasReady"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
|
<!-- 进度文字 -->
|
||||||
|
<view class="progress-text" v-if="showText">
|
||||||
|
<slot>
|
||||||
|
<text class="text-value">{{ progress }}%</text>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CircleProgress',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
// 进度值(0-100)
|
||||||
|
progress: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
validator: value => value >= 0 && value <= 100
|
||||||
|
},
|
||||||
|
// 圆环大小
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
|
},
|
||||||
|
// 圆环宽度
|
||||||
|
strokeWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
// 圆环颜色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#E8101E'
|
||||||
|
},
|
||||||
|
// 背景圆环颜色
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#F5F5F5'
|
||||||
|
},
|
||||||
|
// 是否显示进度文字
|
||||||
|
showText: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示动画
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 动画时长(毫秒)
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvasId: 'circleProgress_' + Math.random().toString(36).substr(2, 9),
|
||||||
|
ctx: null,
|
||||||
|
canvas: null,
|
||||||
|
animationFrame: null,
|
||||||
|
currentProgress: 0,
|
||||||
|
canvasType: '2d'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
progress: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (this.animation) {
|
||||||
|
this.animateProgress(newVal)
|
||||||
|
} else {
|
||||||
|
this.currentProgress = newVal
|
||||||
|
this.drawProgress()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// #ifdef H5
|
||||||
|
this.canvasType = ''
|
||||||
|
// #endif
|
||||||
|
this.initCanvas()
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.animationFrame) {
|
||||||
|
cancelAnimationFrame(this.animationFrame)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// 初始化画布
|
||||||
|
async initCanvas() {
|
||||||
|
const query = uni.createSelectorQuery().in(this)
|
||||||
|
query.select(`#${this.canvasId}`)
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec((res) => {
|
||||||
|
if (!res[0]) return
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
this.canvas = res[0].node || res[0].canvas
|
||||||
|
if (!this.canvas) {
|
||||||
|
this.canvas = document.getElementById(this.canvasId)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
this.canvas = res[0].node
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
this.ctx = this.canvas.getContext('2d')
|
||||||
|
if (!this.ctx) return
|
||||||
|
|
||||||
|
// 设置画布大小
|
||||||
|
const dpr = uni.getSystemInfoSync().pixelRatio
|
||||||
|
this.canvas.width = this.size * dpr
|
||||||
|
this.canvas.height = this.size * dpr
|
||||||
|
this.ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
|
// 设置初始进度
|
||||||
|
this.currentProgress = this.progress
|
||||||
|
// 绘制初始进度
|
||||||
|
this.drawProgress()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 绘制进度
|
||||||
|
drawProgress() {
|
||||||
|
if (!this.ctx) return
|
||||||
|
|
||||||
|
const center = this.size / 2
|
||||||
|
const radius = (this.size - this.strokeWidth) / 2
|
||||||
|
|
||||||
|
// 清空画布
|
||||||
|
this.ctx.clearRect(0, 0, this.size, this.size)
|
||||||
|
|
||||||
|
// 绘制背景圆环
|
||||||
|
this.ctx.beginPath()
|
||||||
|
this.ctx.arc(center, center, radius, 0, Math.PI * 2)
|
||||||
|
this.ctx.strokeStyle = this.backgroundColor
|
||||||
|
this.ctx.lineWidth = this.strokeWidth
|
||||||
|
this.ctx.lineCap = 'round'
|
||||||
|
this.ctx.stroke()
|
||||||
|
|
||||||
|
// 绘制进度圆环
|
||||||
|
if (this.currentProgress > 0) {
|
||||||
|
const endAngle = (this.currentProgress / 100) * Math.PI * 2
|
||||||
|
|
||||||
|
// 创建渐变色
|
||||||
|
const gradient = this.ctx.createLinearGradient(0, 0, this.size, 0)
|
||||||
|
gradient.addColorStop(0, this.color)
|
||||||
|
gradient.addColorStop(1, this.color)
|
||||||
|
|
||||||
|
this.ctx.beginPath()
|
||||||
|
this.ctx.arc(center, center, radius, -Math.PI / 2, endAngle - Math.PI / 2)
|
||||||
|
this.ctx.strokeStyle = gradient
|
||||||
|
this.ctx.lineWidth = this.strokeWidth
|
||||||
|
this.ctx.lineCap = 'round'
|
||||||
|
this.ctx.stroke()
|
||||||
|
|
||||||
|
// 添加发光效果
|
||||||
|
this.ctx.shadowColor = this.color
|
||||||
|
this.ctx.shadowBlur = 10
|
||||||
|
this.ctx.stroke()
|
||||||
|
this.ctx.shadowBlur = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
animateProgress(targetProgress) {
|
||||||
|
if (this.animationFrame) {
|
||||||
|
cancelAnimationFrame(this.animationFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startProgress = this.currentProgress
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
// 使用缓动函数
|
||||||
|
const easeOutQuart = (x) => {
|
||||||
|
return 1 - Math.pow(1 - x, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const currentTime = Date.now()
|
||||||
|
const elapsed = currentTime - startTime
|
||||||
|
|
||||||
|
if (elapsed >= this.duration) {
|
||||||
|
this.currentProgress = targetProgress
|
||||||
|
this.drawProgress()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = easeOutQuart(elapsed / this.duration)
|
||||||
|
this.currentProgress = startProgress + (targetProgress - startProgress) * progress
|
||||||
|
this.drawProgress()
|
||||||
|
|
||||||
|
this.animationFrame = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.circle-progress {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* H5适配 */
|
||||||
|
/* #ifdef H5 */
|
||||||
|
.progress-canvas {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
/* APP适配 */
|
||||||
|
/* #ifdef APP-PLUS */
|
||||||
|
.progress-canvas {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
/* 小程序适配 */
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
.progress-canvas {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,488 @@
|
||||||
|
<template>
|
||||||
|
<view class="common-filter">
|
||||||
|
<!-- 筛选触发按钮(服务时间 + 主筛选) -->
|
||||||
|
<view class="filter-btns-wrap">
|
||||||
|
<!-- 服务时间按钮 -->
|
||||||
|
<view class="filter-trigger time-trigger" @click="toggleTimePopup">
|
||||||
|
<text class="filter-text" :class="{ active: timePopupShow }">
|
||||||
|
{{ timeBtnText }}
|
||||||
|
</text>
|
||||||
|
<image
|
||||||
|
:src="!timePopupShow ? sqIcon : zkIcon"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="time-icon"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 原筛选按钮 -->
|
||||||
|
<view class="filter-trigger main-filter-trigger time-trigger" @click="toggleMainPopup">
|
||||||
|
<text class="filter-text" :class="{ active: mainPopupShow }">
|
||||||
|
{{ filterBtnText }}
|
||||||
|
</text>
|
||||||
|
<image
|
||||||
|
:src="!mainPopupShow ? sqIcon : zkIcon"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="time-icon"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<filterPopup
|
||||||
|
ref="filterPopupRef"
|
||||||
|
v-model="mainPopupShow"
|
||||||
|
:filterList="filterList"
|
||||||
|
@reset="resetFilter"
|
||||||
|
@sure="$emit('search')"
|
||||||
|
>
|
||||||
|
<filterCard
|
||||||
|
v-for="info in filterList"
|
||||||
|
:info="info"
|
||||||
|
v-model="value"
|
||||||
|
@selectDetail="(value) => selectDetail(info.type, value)"
|
||||||
|
:isSingle="true"
|
||||||
|
:infoDetailList="info"
|
||||||
|
>
|
||||||
|
<view class="price-input" v-if="info.type == 'price'">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:value="value.start_price"
|
||||||
|
@input="(e) => selectDetail('start_price', [e.detail.value])"
|
||||||
|
placeholder="最低价"
|
||||||
|
class="price-input-field"
|
||||||
|
/>
|
||||||
|
<text class="price-separator"></text>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:value="value.end_price"
|
||||||
|
@input="(e) => selectDetail('end_price', [e.detail.value])"
|
||||||
|
placeholder="最高价"
|
||||||
|
class="price-input-field"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</filterCard>
|
||||||
|
</filterPopup>
|
||||||
|
<view class="time-filter-popup" :class="{ show: timePopupShow }">
|
||||||
|
<view class="time-filter-content">
|
||||||
|
<view
|
||||||
|
class="time-option"
|
||||||
|
v-for="(item, index) in timeOptions"
|
||||||
|
:key="index"
|
||||||
|
:class="{ active: selectedTimeOption === index }"
|
||||||
|
@click="changeTime(index)"
|
||||||
|
>
|
||||||
|
<text class="time-text">{{ item.text }}</text>
|
||||||
|
<!-- <text class="time-discount" v-if="item.discount">{{ item.discount }}</text> -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import filterPopup from "@/components/filter-popup/filter-popup.vue";
|
||||||
|
import filterCard from "@/components/filterCard/filterCard.vue";
|
||||||
|
export default {
|
||||||
|
name: "CommonFilter",
|
||||||
|
props: {
|
||||||
|
// 服务时间按钮文本
|
||||||
|
timeBtnText: {
|
||||||
|
type: String,
|
||||||
|
default: "服务时间",
|
||||||
|
},
|
||||||
|
// 服务时间展开/收起图标
|
||||||
|
sqIcon: {
|
||||||
|
type: String,
|
||||||
|
default:
|
||||||
|
"https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/sq_icon.png",
|
||||||
|
},
|
||||||
|
zkIcon: {
|
||||||
|
type: String,
|
||||||
|
default:
|
||||||
|
"https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/zk_icon.png",
|
||||||
|
},
|
||||||
|
// 主筛选按钮文本
|
||||||
|
filterBtnText: {
|
||||||
|
type: String,
|
||||||
|
default: "筛选",
|
||||||
|
},
|
||||||
|
// 主筛选图标
|
||||||
|
filterIcon: {
|
||||||
|
type: String,
|
||||||
|
default:
|
||||||
|
"https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/filter_icon.png",
|
||||||
|
},
|
||||||
|
filterDetailsLists: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "价格",
|
||||||
|
type: "price",
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "0-10元",
|
||||||
|
value: "0-10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "10-30元",
|
||||||
|
value: "10-30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "30-50元",
|
||||||
|
value: "30-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "50-100元",
|
||||||
|
value: "50-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "100-300元",
|
||||||
|
value: "100-300",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "300+",
|
||||||
|
value: "300-",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "排序",
|
||||||
|
type: "ordersort",
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: " 销量最高",
|
||||||
|
value: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "价格最高",
|
||||||
|
value: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "价格最低",
|
||||||
|
value: "3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "服务类型",
|
||||||
|
type: "server_kind",
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: " 到家",
|
||||||
|
value: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "到店",
|
||||||
|
value: "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { filterPopup, filterCard },
|
||||||
|
computed: {
|
||||||
|
filterList() {
|
||||||
|
return this.filterDetailsLists.map((item) => {
|
||||||
|
if (item.type == "price") {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
defaultValue: [
|
||||||
|
`${this.value.start_price || ""}-${this.value.end_price || ""}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
defaultValue: [this.value[item.type]],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timePopupShow: false, // 服务时间弹框显示状态(当前模板未实现弹框内容)
|
||||||
|
mainPopupShow: false, // 主筛选弹框显示状态(当前模板未实现弹框内容)
|
||||||
|
timeOptions: [
|
||||||
|
{ text: "今日可约" },
|
||||||
|
{ text: "近两日可约", discount: "64.31%" },
|
||||||
|
{ text: "近三日可约" },
|
||||||
|
],
|
||||||
|
selectedTimeOption: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeTime(index) {
|
||||||
|
this.selectedTimeOption = index;
|
||||||
|
this.selectDetail("date", [this.getDate(index)]);
|
||||||
|
this.$emit("search");
|
||||||
|
this.timePopupShow = false;
|
||||||
|
},
|
||||||
|
getDate(index) {
|
||||||
|
//今天的时间
|
||||||
|
var day1 = new Date();
|
||||||
|
day1.setTime(day1.getTime());
|
||||||
|
var m1 = day1.getMonth() + 1;
|
||||||
|
if (m1 <= 9) {
|
||||||
|
m1 = "0" + m1;
|
||||||
|
}
|
||||||
|
var d1 = day1.getDate();
|
||||||
|
if (d1 <= 9) {
|
||||||
|
d1 = "0" + d1;
|
||||||
|
}
|
||||||
|
var s1 = day1.getFullYear() + "-" + m1 + "-" + d1;
|
||||||
|
|
||||||
|
//明天的时间
|
||||||
|
var day2 = new Date();
|
||||||
|
day2.setTime(day2.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
var m2 = day2.getMonth() + 1;
|
||||||
|
if (m2 <= 9) {
|
||||||
|
m2 = "0" + m2;
|
||||||
|
}
|
||||||
|
var d2 = day2.getDate();
|
||||||
|
if (d2 <= 9) {
|
||||||
|
d2 = "0" + d2;
|
||||||
|
}
|
||||||
|
var s2 = day2.getFullYear() + "-" + m2 + "-" + d2;
|
||||||
|
|
||||||
|
//后天的时间
|
||||||
|
var day3 = new Date();
|
||||||
|
day3.setTime(day3.getTime() + 24 * 60 * 60 * 1000 * 2);
|
||||||
|
var m3 = day3.getMonth() + 1;
|
||||||
|
if (m3 <= 9) {
|
||||||
|
m3 = "0" + m3;
|
||||||
|
}
|
||||||
|
var d3 = day3.getDate();
|
||||||
|
if (d3 <= 9) {
|
||||||
|
d3 = "0" + d3;
|
||||||
|
}
|
||||||
|
var s3 = day3.getFullYear() + "-" + m3 + "-" + d3;
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
return [`${s1}`];
|
||||||
|
} else if (index == 1) {
|
||||||
|
return [`${s1}`, `${s2}`];
|
||||||
|
} else if (index == 2) {
|
||||||
|
return [`${s1}`, `${s2}`, `${s3}`];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
const resetObj = {};
|
||||||
|
this.filterDetailsLists.forEach((it) => {
|
||||||
|
if (it.type === "price") {
|
||||||
|
resetObj.start_price = undefined;
|
||||||
|
resetObj.end_price = undefined;
|
||||||
|
} else {
|
||||||
|
resetObj[it.type] = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$emit("input", {
|
||||||
|
...this.value,
|
||||||
|
...resetObj,
|
||||||
|
});
|
||||||
|
this.$emit("search");
|
||||||
|
},
|
||||||
|
// 切换服务时间弹框(弹框内容需后续补充)
|
||||||
|
toggleTimePopup() {
|
||||||
|
this.timePopupShow = !this.timePopupShow;
|
||||||
|
if (this.timePopupShow) this.mainPopupShow = false; // 互斥显示
|
||||||
|
},
|
||||||
|
// 切换主筛选弹框(弹框内容需后续补充)
|
||||||
|
toggleMainPopup() {
|
||||||
|
if (this.$refs.filterPopupRef) {
|
||||||
|
if (!this.mainPopupShow) {
|
||||||
|
this.timePopupShow = false;
|
||||||
|
this.mainPopupShow = true;
|
||||||
|
} else {
|
||||||
|
this.timePopupShow = false; // 互斥显示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectDetail(type, value) {
|
||||||
|
console.log(type, value);
|
||||||
|
if (type == "price") {
|
||||||
|
if (value[0]) {
|
||||||
|
let priceList = value[0].split("-");
|
||||||
|
this.$emit("input", {
|
||||||
|
...this.value,
|
||||||
|
start_price: priceList[0],
|
||||||
|
end_price: priceList[1],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$emit("input", {
|
||||||
|
...this.value,
|
||||||
|
start_price: undefined,
|
||||||
|
end_price: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$emit("input", {
|
||||||
|
...this.value,
|
||||||
|
[type]: value[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.common-filter {
|
||||||
|
position: relative;
|
||||||
|
z-index: 900;
|
||||||
|
background-color: #fff;
|
||||||
|
height: 102rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* 服务时间弹框样式 */
|
||||||
|
.time-filter-popup {
|
||||||
|
width: 750rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
visibility: hidden;
|
||||||
|
border-radius: 0 0 20rpx 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 102rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-filter-popup.show {
|
||||||
|
opacity: 1;
|
||||||
|
height: 320rpx;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-filter-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-option:not(:last-child)::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 32rpx;
|
||||||
|
right: 32rpx;
|
||||||
|
bottom: 0;
|
||||||
|
height: 1rpx;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-option.active {
|
||||||
|
background-color: #fff5f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-option.active .time-text {
|
||||||
|
color: #E8101E;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-discount {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background-color: #fff5f6;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 筛选按钮容器:横向排列两个按钮,控制间距和内边距 */
|
||||||
|
.filter-btns-wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 基础筛选按钮样式:统一按钮的flex布局、对齐方式和行高 */
|
||||||
|
.filter-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
line-height: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮文本样式:默认灰色,选中时高亮 */
|
||||||
|
.filter-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
color: #E8101E; /* 选中态主题色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务时间按钮专属:控制下拉图标尺寸 */
|
||||||
|
.time-trigger .time-icon {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主筛选按钮专属:控制筛选图标尺寸 */
|
||||||
|
.main-filter-trigger .filter-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
//价格区间样式
|
||||||
|
.price-input {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 370rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #f3f6f8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
|
||||||
|
.price-input-field {
|
||||||
|
width: 166rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 50rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-separator {
|
||||||
|
height: 3rpx;
|
||||||
|
width: 38rpx;
|
||||||
|
background-color: #dddddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
<template>
|
||||||
|
<view class="common-list">
|
||||||
|
<!-- 头部插槽:父组件可插入独立的Tab、筛选组件等 -->
|
||||||
|
<slot name="header"></slot>
|
||||||
|
|
||||||
|
<!-- 核心列表区域(scroll-view 实现刷新加载) -->
|
||||||
|
<scroll-view ref="listScrollRef" :scroll-top="scrollTop" class="list-scroll" scroll-y
|
||||||
|
:style="{ height: listScrollHeight }" :refresher-enabled="refresherEnabled" :refresher-triggered="isRefreshing"
|
||||||
|
@refresherrefresh="handleRefresh" @scrolltolower="handleLoadMore" :show-scrollbar="false"
|
||||||
|
:refresher-threshold="20" lower-threshold="130">
|
||||||
|
<!-- 滑动区域头部插槽:父组件可插入独立的Tab、筛选组件等 -->
|
||||||
|
<slot name="headerScroll"></slot>
|
||||||
|
<!-- 列表项插槽:父组件自定义列表内容 -->
|
||||||
|
<view class="list-container" v-if="listData.length">
|
||||||
|
<slot name="listData" :list="listData"></slot>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空数据插槽:父组件自定义空状态 -->
|
||||||
|
<slot name="empty" v-else></slot>
|
||||||
|
|
||||||
|
<!-- 加载提示 -->
|
||||||
|
<view class="load-tips" v-if="listData.length">
|
||||||
|
<text v-if="hasMore && !isLoading">{{ loadingText }}</text>
|
||||||
|
<text v-else>{{ !hasMore ? noMoreText : "已加载完成" }}</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CommonList",
|
||||||
|
props: {
|
||||||
|
//是否开启下拉刷新
|
||||||
|
refresherEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 必需:列表数据请求地址
|
||||||
|
apiUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// 初始查询参数(如分页、筛选条件等,由父组件传入)
|
||||||
|
initialQuery: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
// 列表滚动高度(父组件根据布局动态计算)
|
||||||
|
listScrollHeight: {
|
||||||
|
type: String,
|
||||||
|
default: "calc(100vh - 200rpx)",
|
||||||
|
},
|
||||||
|
// 自定义提示文本(可选)
|
||||||
|
loadingText: {
|
||||||
|
type: String,
|
||||||
|
default: "加载中...",
|
||||||
|
},
|
||||||
|
noMoreText: {
|
||||||
|
type: String,
|
||||||
|
default: "~暂时只有这些了~",
|
||||||
|
},
|
||||||
|
// 分页配置(可选,默认page=1,limit=10)
|
||||||
|
pageConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
pageKey: "page", // 接口分页参数名(如“page”)
|
||||||
|
limitKey: "limit", // 接口每页条数参数名(如“limit”)
|
||||||
|
page: 1, // 初始页码
|
||||||
|
limit: 10, // 每页条数
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// 接口数据结构映射(适配不同接口返回格式)
|
||||||
|
responseMap: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
list: "data", // 列表数据在响应中的字段名
|
||||||
|
total: "count", // 总条数在响应中的字段名
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const {
|
||||||
|
pageKey,
|
||||||
|
limitKey,
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
} = this.pageConfig;
|
||||||
|
return {
|
||||||
|
queryData: {
|
||||||
|
[pageKey]: page,
|
||||||
|
[limitKey]: limit,
|
||||||
|
...this.initialQuery,
|
||||||
|
},
|
||||||
|
scrollTop: 0,
|
||||||
|
listData: [], // 列表数据
|
||||||
|
isRefreshing: false, // 下拉刷新状态(控制动画)
|
||||||
|
isLoading: false, // 加载中状态(防止重复请求)
|
||||||
|
hasMore: true, // 是否有更多数据
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 初始化加载列表
|
||||||
|
// this.fetchListData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 核心:请求列表数据 */
|
||||||
|
async fetchListData() {
|
||||||
|
// 防止重复请求
|
||||||
|
if (this.isLoading) return;
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await request.post(this.apiUrl, this.queryData);
|
||||||
|
const {
|
||||||
|
list: listField,
|
||||||
|
total: totalField
|
||||||
|
} = this.responseMap;
|
||||||
|
// 从响应中提取列表数据和总条数(适配不同接口格式)
|
||||||
|
|
||||||
|
let list = res[listField] || [];
|
||||||
|
let total = res[totalField] || 0;
|
||||||
|
if (list.list) {
|
||||||
|
total = list[totalField] || 0;
|
||||||
|
list = list.list || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页逻辑:第一页重置,后续页追加
|
||||||
|
if (this.queryData[this.pageConfig.pageKey] === 1) {
|
||||||
|
this.scrollToTop();
|
||||||
|
this.listData = list;
|
||||||
|
} else {
|
||||||
|
this.listData = [...this.listData, ...list];
|
||||||
|
}
|
||||||
|
// 判断是否有更多数据(当前列表长度 >= 总条数则无更多)
|
||||||
|
this.hasMore = this.listData.length < total;
|
||||||
|
|
||||||
|
// 向父组件传递加载成功事件
|
||||||
|
this.$emit("load-success", {
|
||||||
|
list: this.listData,
|
||||||
|
total,
|
||||||
|
currentPage: this.queryData[this.pageConfig.pageKey],
|
||||||
|
});
|
||||||
|
this.$emit("load-success-all", res);
|
||||||
|
} catch (err) {
|
||||||
|
this.$emit("load-fail", err);
|
||||||
|
console.error("列表请求失败:", err);
|
||||||
|
} finally {
|
||||||
|
// 关键修复:用$nextTick确保状态更新同步到DOM
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isRefreshing = false; // 结束下拉刷新动画
|
||||||
|
this.isLoading = false; // 结束加载状态
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 下拉刷新:重置页码为1,重新加载 */
|
||||||
|
handleRefresh() {
|
||||||
|
// 关键修复:明确开启刷新状态(与scroll-view双向绑定同步)
|
||||||
|
this.isRefreshing = true;
|
||||||
|
// 重置页码为1
|
||||||
|
this.queryData[this.pageConfig.pageKey] = 1;
|
||||||
|
// 触发数据请求
|
||||||
|
this.fetchListData();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 上拉加载更多:页码+1,追加数据 */
|
||||||
|
handleLoadMore() {
|
||||||
|
// 无更多数据/正在加载/正在刷新时,不触发加载
|
||||||
|
if (!this.hasMore || this.isLoading || this.isRefreshing) return;
|
||||||
|
this.queryData[this.pageConfig.pageKey] += 1;
|
||||||
|
this.fetchListData();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 对外暴露:手动刷新列表(父组件可通过ref调用) */
|
||||||
|
manualRefresh(newQuery = {}) {
|
||||||
|
this.queryData = {
|
||||||
|
...this.queryData,
|
||||||
|
[this.pageConfig.pageKey]: 1, // 重置为第一页
|
||||||
|
...newQuery,
|
||||||
|
};
|
||||||
|
this.fetchListData();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 对外暴露:清空列表数据 */
|
||||||
|
clearList() {
|
||||||
|
this.listData = [];
|
||||||
|
this.queryData[this.pageConfig.pageKey] = 1;
|
||||||
|
this.hasMore = true;
|
||||||
|
},
|
||||||
|
/** 工具方法:滚动到顶部(复用逻辑) */
|
||||||
|
scrollToTop() {
|
||||||
|
if (this.scrollTop == 0) {
|
||||||
|
this.scrollTop = 0.1;
|
||||||
|
} else {
|
||||||
|
this.scrollTop = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.common-list {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
padding-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表滚动容器 */
|
||||||
|
.list-scroll {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表容器 */
|
||||||
|
.list-container {
|
||||||
|
padding: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载提示 */
|
||||||
|
.load-tips {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #cecece;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
<template>
|
||||||
|
<!-- 根元素绑定CSS变量,关联props中的颜色 -->
|
||||||
|
<view
|
||||||
|
class="service-first-tab"
|
||||||
|
:style="[
|
||||||
|
{ backgroundColor: bgColor },
|
||||||
|
{ '--active-color': activeColor }, // 激活色变量
|
||||||
|
{ '--inactive-color': inactiveColor }, // 非激活色变量
|
||||||
|
{ '--tab-padding-top': tabPaddingTop}, //
|
||||||
|
{ '--tab-padding-bottom': tabPaddingBottom} //
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<scroll-view
|
||||||
|
scroll-x
|
||||||
|
class="tab-scroll"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
@touchmove.prevent.stop
|
||||||
|
>
|
||||||
|
<view class="tab-container">
|
||||||
|
<view
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
:class="['tab-item', { active: activeId == tab.id }]"
|
||||||
|
:style="{ paddingTop: tabPaddingTop,paddingBottom: tabPaddingBottom }"
|
||||||
|
@click="handleTabClick(tab)"
|
||||||
|
>
|
||||||
|
<text class="tab-text">{{ tab.title }}</text>
|
||||||
|
<view class="tab-active-indicator" v-if="activeId == tab.id"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ServiceFirstTab",
|
||||||
|
props: {
|
||||||
|
tabs: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
activeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
tabWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 140,
|
||||||
|
},
|
||||||
|
// 已定义的激活色props(无需修改)
|
||||||
|
activeColor: {
|
||||||
|
type: String,
|
||||||
|
default: "#E8101E",
|
||||||
|
},
|
||||||
|
// 已定义的非激活色props(无需修改)
|
||||||
|
inactiveColor: {
|
||||||
|
type: String,
|
||||||
|
default: "#333",
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 28,
|
||||||
|
},
|
||||||
|
indicatorHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 6,
|
||||||
|
},
|
||||||
|
bgColor: {
|
||||||
|
type: String,
|
||||||
|
default: "#fafafa",
|
||||||
|
},
|
||||||
|
//itempading
|
||||||
|
tabPaddingTop: {
|
||||||
|
type: String,
|
||||||
|
default: "30rpx", // 默认值就是复合写法,验证生效
|
||||||
|
},
|
||||||
|
tabPaddingBottom: {
|
||||||
|
type: String,
|
||||||
|
default: "14rpx", // 默认值就是复合写法,验证生效
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTabClick(tab) {
|
||||||
|
if (this.activeId === tab.id) return;
|
||||||
|
this.$emit("tab-click", tab.id, tab.title, tab);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.service-first-tab {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 反圆角 */
|
||||||
|
/* .service-first-tab::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 40rpx;
|
||||||
|
background: #fafafa;
|
||||||
|
border-top-left-radius: 30rpx;
|
||||||
|
border-top-right-radius: 30rpx;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.tab-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
gap: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
/* padding: 0 20rpx; */
|
||||||
|
/* height: 88rpx; */
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 非激活色:使用CSS变量 --inactive-color */
|
||||||
|
.tab-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: var(--inactive-color);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激活色:使用CSS变量 --active-color */
|
||||||
|
.tab-item.active .tab-text {
|
||||||
|
color: var(--active-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下划线颜色:与激活色一致,使用 --active-color */
|
||||||
|
.tab-active-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 6rpx; /* 可关联 indicatorHeight props,见延伸优化 */
|
||||||
|
background-color: var(--active-color);
|
||||||
|
border-radius: 10rpx;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<view class="service-second-icon-tab">
|
||||||
|
<scroll-view
|
||||||
|
scroll-x
|
||||||
|
class="icon-scroll"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
@touchmove.prevent.stop
|
||||||
|
>
|
||||||
|
<view class="icon-container">
|
||||||
|
<view
|
||||||
|
v-for="(icon, index) in icons"
|
||||||
|
:key="icon.id"
|
||||||
|
:class="['icon-item', { active: activeId == icon.id }]"
|
||||||
|
@click="handleIconClick(icon)"
|
||||||
|
>
|
||||||
|
<view class="icon-img-wrap">
|
||||||
|
<image
|
||||||
|
:src="icon.photo || defaultIcon"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="icon-img"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
<text class="icon-text">{{ icon.title }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ServiceSecondIconTab",
|
||||||
|
props: {
|
||||||
|
icons: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 当前选中的Icon ID
|
||||||
|
activeId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// Icon宽度(rpx)
|
||||||
|
iconWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 96,
|
||||||
|
},
|
||||||
|
// Icon高度(rpx)
|
||||||
|
iconHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 96,
|
||||||
|
},
|
||||||
|
// 文字大小(rpx)
|
||||||
|
textSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 24,
|
||||||
|
},
|
||||||
|
// 激活状态的颜色
|
||||||
|
activeColor: {
|
||||||
|
type: String,
|
||||||
|
default: "#E8101E",
|
||||||
|
},
|
||||||
|
// 非激活状态的颜色
|
||||||
|
inactiveColor: {
|
||||||
|
type: String,
|
||||||
|
default: "#3d3d3d",
|
||||||
|
},
|
||||||
|
// 图标默认图片(当没有提供photo时使用)
|
||||||
|
defaultIcon: {
|
||||||
|
type: String,
|
||||||
|
default:
|
||||||
|
"https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/default-icon.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 处理Icon点击
|
||||||
|
handleIconClick(icon) {
|
||||||
|
// 触发事件,向父组件传递选中的Icon信息
|
||||||
|
this.$emit("icon-click", icon.id, icon.title, icon);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.service-second-icon-tab {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-scroll {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
gap: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 110rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-img-wrap {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item.active .icon-img-wrap::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border: 2rpx solid #E8101E;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3d3d3d;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item.active .icon-text {
|
||||||
|
color: #E8101E;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<view class="custom-navbar" :style="{paddingTop:`${statusBarHeight}rpx`,background:backgroundColor}">
|
||||||
|
<view class="navbar-content"
|
||||||
|
:style="{'border-bottom':borderBottom}">
|
||||||
|
<view class="left-area">
|
||||||
|
<view class="back-btn" @click="goBack" v-if="showBack">
|
||||||
|
<!-- <text class="iconfont icon-back"></text> -->
|
||||||
|
<image :src="leftImg" mode="aspectFit" class="back-icon"></image>
|
||||||
|
</view>
|
||||||
|
<slot name="left"></slot>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="title" :style="'color:' + titleColor + ';'">{{ title }}</text>
|
||||||
|
|
||||||
|
<view class="right-area">
|
||||||
|
<slot name="right">
|
||||||
|
<image :src="headleSrc"
|
||||||
|
v-if="showHeadle"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="iconfont icon-headle"
|
||||||
|
@click="onHeadleClick"></image>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="header-top" :style="{ height: `${statusBarHeight + 88}rpx` }"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CustomNavbar',
|
||||||
|
props: {
|
||||||
|
// 标题
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 是否显示返回按钮
|
||||||
|
showBack: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示用户图标
|
||||||
|
showHeadle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
headleSrc: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 背景色
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#ffffff'
|
||||||
|
},
|
||||||
|
// 标题颜色
|
||||||
|
titleColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#333333'
|
||||||
|
},
|
||||||
|
// 下划线
|
||||||
|
borderBottom: {
|
||||||
|
type: String,
|
||||||
|
default: '1rpx solid rgba(0, 0, 0, 0.05)'
|
||||||
|
},
|
||||||
|
nowStyle: {
|
||||||
|
type: String,
|
||||||
|
default: '1rpx solid rgba(0, 0, 0, 0.05)'
|
||||||
|
},
|
||||||
|
leftImg:{
|
||||||
|
type: String,
|
||||||
|
default: '/static/images/back.png'
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
// 获取状态栏高度
|
||||||
|
const systemInfo = await uni.getSystemInfoSync()
|
||||||
|
let screenWidth = systemInfo.screenWidth;
|
||||||
|
this.statusBarHeight = this.pxToRpx(systemInfo.statusBarHeight,screenWidth);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
pxToRpx(px,screenWidth) {
|
||||||
|
if (typeof px !== 'number') {
|
||||||
|
throw new Error('请传入数字类型的px值');
|
||||||
|
}
|
||||||
|
return px * (750 / screenWidth);
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$emit('back')
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onHeadleClick() {
|
||||||
|
this.$emit('onHeadleClick')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.header-top {
|
||||||
|
height: 88rpx;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.navbar-content {
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 30rpx 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-area, .icon-headle {
|
||||||
|
width: 46rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.left-area, .back-icon {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
/* padding: 20rpx;
|
||||||
|
margin-left: -20rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
/* position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%); */
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小程序适配 */
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
.custom-navbar {
|
||||||
|
padding-top: calc(var(--status-bar-height) + constant(safe-area-inset-top));
|
||||||
|
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
## 1.0.0(2024-10-10)
|
||||||
|
1.0.0
|
||||||
|
|
@ -0,0 +1,394 @@
|
||||||
|
<template>
|
||||||
|
<view class="lottery-grid" :style="[gridStyle]">
|
||||||
|
<view v-for="(item, index) in gridItems" :key="index" :class="['grid-item', { 'prize-item': !item.isCenter }]">
|
||||||
|
<template v-if="!item.isCenter">
|
||||||
|
<slot :item="item" :index="index">
|
||||||
|
<view class="grid-item-content" :style="[getItemStyle(item, index)]">
|
||||||
|
<image class="grid-item-content-bg" v-if="gridItemBg" :src="gridItemBg"></image>
|
||||||
|
<image class="grid-item-content-img" :src="item.simple_img" mode="widthFix"></image>
|
||||||
|
<view class="grid-item-content-text">{{ item.show_text }}</view>
|
||||||
|
</view>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<view class="start-btn" :style="[startButtonStyle]" @click="!isRunning ? $emit('centerButtonClick') : null">
|
||||||
|
<slot name="centerButton" v-if="$slots && $slots.centerButton"></slot>
|
||||||
|
<view class="start-btn-content" v-else>
|
||||||
|
{{ isRunning ? '抽奖中...' : '开始' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
rows: {
|
||||||
|
type: Number,
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
cols: {
|
||||||
|
type: Number,
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
minSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
maxSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
accelerationRate: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
gridItemBg: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
//
|
||||||
|
prizes: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 新增 prizeItemStyle prop
|
||||||
|
prizeItemStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
borderRadius: '10rpx',
|
||||||
|
fontWeight: '500',
|
||||||
|
fontSize: '18rpx',
|
||||||
|
color: '#CB1316',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// 新增 activeItemStyle prop
|
||||||
|
activeItemStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
backgroundColor: '#FF622B',
|
||||||
|
color: '#FFF',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRunning: false, // 抽奖是否正在进行
|
||||||
|
currentIndex: -1, // 当前高亮的格子索引
|
||||||
|
targetIndex: -1, // 最终停止的目标索引
|
||||||
|
speed: 50, // 初始转动速度(毫秒)
|
||||||
|
rounds: 0, // 经完成的轮数
|
||||||
|
minRounds: 3, // 最小旋转轮数
|
||||||
|
totalSteps: 0, // 总步数(用于控制停止)
|
||||||
|
gridItems: [], // 存储所有格子信息的数组
|
||||||
|
sequence: [], // 存储边缘格子索引的数组(按顺时针顺序)
|
||||||
|
timerId: null, // 新增:定时器ID,用于清除未执行的定时器
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
gridStyle() {
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: `repeat(${this.cols}, 1fr)`,
|
||||||
|
gridTemplateRows: `repeat(${this.rows}, 1fr)`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
startButtonStyle() {
|
||||||
|
return {
|
||||||
|
gridArea: `2 / 2 / ${this.rows} / ${this.cols}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.validatePrizes();
|
||||||
|
this.initializeGrid();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// 新增:组件销毁前清除定时器,避免内存泄漏
|
||||||
|
this.clearLotteryState();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
validatePrizes() {
|
||||||
|
const prizeSlots = (this.rows * this.cols) - (this.rows - 2) * (this.cols - 2); // 减去中心格子
|
||||||
|
if (this.prizes.length !== prizeSlots) {
|
||||||
|
throw new Error(
|
||||||
|
`奖品数量不匹配。需要 ${prizeSlots} 个奖品,但提供了 ${this.prizes.length} 个。`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 新增:验证每个奖品必须有唯一的id
|
||||||
|
const prizeIds = this.prizes.map((item, idx) => {
|
||||||
|
if (!item.id && item.id !== 0) { // 兼容0作为合法id
|
||||||
|
throw new Error(`第 ${idx + 1} 个奖品缺少必填的id字段`);
|
||||||
|
}
|
||||||
|
return item.id;
|
||||||
|
});
|
||||||
|
// 验证id唯一性
|
||||||
|
const uniqueIds = [...new Set(prizeIds)];
|
||||||
|
if (uniqueIds.length !== prizeIds.length) {
|
||||||
|
throw new Error('奖品列表中存在重复的id,请检查');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeGrid() {
|
||||||
|
// 初始化网格项目和序列数组
|
||||||
|
this.gridItems = [];
|
||||||
|
this.sequence = [];
|
||||||
|
let prizeIndex = 0;
|
||||||
|
|
||||||
|
// 遍历行和列,创建网格项目
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
if (this.isCenter(i, j)) {
|
||||||
|
// 如果是中心格,添加一个中心项目
|
||||||
|
this.gridItems.push({
|
||||||
|
isCenter: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 否则,添加一个奖品项目
|
||||||
|
this.gridItems.push({
|
||||||
|
...this.prizes[prizeIndex],
|
||||||
|
isCenter: false,
|
||||||
|
});
|
||||||
|
prizeIndex++;
|
||||||
|
|
||||||
|
// 如果是边缘格,将其索引添加到序列中
|
||||||
|
if (i === 0 || i === this.rows - 1 || j === 0 || j === this.cols - 1) {
|
||||||
|
this.sequence.push(i * this.cols + j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整序列顺序
|
||||||
|
this.adjustSequence();
|
||||||
|
},
|
||||||
|
|
||||||
|
isCenter(row, col) {
|
||||||
|
// 判断给定的行列是否为中心格
|
||||||
|
return row > 0 && row < this.rows - 1 && col > 0 && col < this.cols - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
adjustSequence() {
|
||||||
|
// 调整序列顺序,使其按照顺时针方向排列
|
||||||
|
const newSequence = [];
|
||||||
|
// 上边
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
newSequence.push(j);
|
||||||
|
}
|
||||||
|
// 右边
|
||||||
|
for (let i = 1; i < this.rows - 1; i++) {
|
||||||
|
newSequence.push(i * this.cols + this.cols - 1);
|
||||||
|
}
|
||||||
|
// 下边
|
||||||
|
for (let j = this.cols - 1; j >= 0; j--) {
|
||||||
|
newSequence.push((this.rows - 1) * this.cols + j);
|
||||||
|
}
|
||||||
|
// 左边
|
||||||
|
for (let i = this.rows - 2; i > 0; i--) {
|
||||||
|
newSequence.push(i * this.cols);
|
||||||
|
}
|
||||||
|
this.sequence = newSequence;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增:根据奖品ID查找其在sequence中的索引
|
||||||
|
getTargetIndexById(prizeId) {
|
||||||
|
const targetSeqIndex = this.sequence.findIndex(seqItem => {
|
||||||
|
const gridItem = this.gridItems[seqItem];
|
||||||
|
return gridItem.id === prizeId;
|
||||||
|
});
|
||||||
|
if (targetSeqIndex === -1) {
|
||||||
|
throw new Error(`未找到id为「${prizeId}」的奖品,请检查奖品列表`);
|
||||||
|
}
|
||||||
|
return targetSeqIndex;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始抽奖(修改:参数改为奖品ID)
|
||||||
|
* @param {string|number} prizeId - 中奖奖品的唯一ID
|
||||||
|
*/
|
||||||
|
startLottery(prizeId) {
|
||||||
|
// 开始抽奖
|
||||||
|
if (this.isRunning) return;
|
||||||
|
// 初始化抽奖参数
|
||||||
|
this.isRunning = true;
|
||||||
|
this.speed = 50;
|
||||||
|
this.rounds = 0;
|
||||||
|
// 核心修改:通过ID获取目标索引
|
||||||
|
this.targetIndex = this.getTargetIndexById(prizeId);
|
||||||
|
// 计算总步数
|
||||||
|
this.totalSteps = this.minRounds * this.sequence.length + this.targetIndex + 1;
|
||||||
|
// 开始运行抽奖
|
||||||
|
this.runLottery();
|
||||||
|
},
|
||||||
|
|
||||||
|
runLottery() {
|
||||||
|
// 先清除之前的定时器(防止多个定时器叠加)
|
||||||
|
if (this.timerId) {
|
||||||
|
clearTimeout(this.timerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行抽奖过程
|
||||||
|
this.currentIndex++;
|
||||||
|
// 如果到达序列末尾,重置索引并增加轮数
|
||||||
|
if (this.currentIndex >= this.sequence.length) {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.rounds++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前步数
|
||||||
|
const currentStep = this.rounds * this.sequence.length + this.currentIndex;
|
||||||
|
|
||||||
|
// 如果达到总步数,结束抽奖
|
||||||
|
if (currentStep >= this.totalSteps - 1) {
|
||||||
|
this.currentIndex = this.targetIndex;
|
||||||
|
this.isRunning = false;
|
||||||
|
// 触发抽奖结束事件
|
||||||
|
this.$emit('lottery-end', this.gridItems[this.sequence[this.currentIndex]]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改速度计算逻辑
|
||||||
|
const progress = currentStep / this.totalSteps;
|
||||||
|
this.speed = this.calculateSpeed(progress);
|
||||||
|
|
||||||
|
// 保存定时器ID
|
||||||
|
this.timerId = setTimeout(() => this.runLottery(), this.speed);
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateSpeed(progress) {
|
||||||
|
// 使用父组件传入的参数计算速度
|
||||||
|
return (
|
||||||
|
this.minSpeed +
|
||||||
|
(this.maxSpeed - this.minSpeed) * Math.pow(progress, this.accelerationRate)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
isActive(index) {
|
||||||
|
// 判断给定索引是否为当前活动项
|
||||||
|
if (!this.isRunning && this.currentIndex === -1) return false;
|
||||||
|
return this.sequence[this.currentIndex] === index;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除 getItemStyle 方法,因为我们现在直接在模板中处理样式
|
||||||
|
getItemStyle(item, index) {
|
||||||
|
if (item.isCenter) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (this.isActive(index)) {
|
||||||
|
return {
|
||||||
|
...this.prizeItemStyle,
|
||||||
|
...this.activeItemStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.prizeItemStyle;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增:清除抽奖状态(重置所有抽中/运行状态)
|
||||||
|
* @param {boolean} emitEvent - 是否触发状态清除事件,默认true
|
||||||
|
*/
|
||||||
|
clearLotteryState(emitEvent = true) {
|
||||||
|
// 1. 清除定时器(防止抽奖过程中清除后仍继续执行)
|
||||||
|
if (this.timerId) {
|
||||||
|
clearTimeout(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 重置核心状态变量
|
||||||
|
this.isRunning = false;
|
||||||
|
this.currentIndex = -1; // 清除高亮索引
|
||||||
|
this.targetIndex = -1;
|
||||||
|
this.speed = this.minSpeed;
|
||||||
|
this.rounds = 0;
|
||||||
|
this.totalSteps = 0;
|
||||||
|
|
||||||
|
// 3. 可选:触发状态清除事件,通知父组件
|
||||||
|
if (emitEvent) {
|
||||||
|
this.$emit('lottery-state-cleared');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lottery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 10rpx;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
/* padding: 4rpx;
|
||||||
|
width: 141rpx;
|
||||||
|
height: 141rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #FEE9CE;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 4rpx solid #FEDEBE;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item-content-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item-content-img {
|
||||||
|
width: 56%;
|
||||||
|
/* height: 50%; */
|
||||||
|
position: relative;
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item-content-text {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-item {
|
||||||
|
/* 移除默认样式,由父组件控制 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"id": "dengbo-lotteryGrid",
|
||||||
|
"displayName": "uniapp抽奖-九宫格",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "uniapp抽奖-九宫格",
|
||||||
|
"keywords": [
|
||||||
|
"抽奖",
|
||||||
|
"九宫格",
|
||||||
|
"宫格"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "无",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y",
|
||||||
|
"alipay": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "u",
|
||||||
|
"app-uvue": "u"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "u",
|
||||||
|
"微信浏览器(Android)": "u",
|
||||||
|
"QQ浏览器(Android)": "u"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "u",
|
||||||
|
"Edge": "u",
|
||||||
|
"Firefox": "u",
|
||||||
|
"Safari": "u"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "u",
|
||||||
|
"百度": "u",
|
||||||
|
"字节跳动": "u",
|
||||||
|
"QQ": "u",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
# dengbo-lotteryGrid
|
||||||
|
|
||||||
|
dengbo-lotteryGrid 是一个用于创建抽奖格子游戏的 uniapp 组件。它提供了一个可自定义的抽奖网格,支持不同的行列数、奖品设置和样式定制。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 支持自定义行数和列数
|
||||||
|
- 可配置奖品信息和图片
|
||||||
|
- 可自定义抽奖速度和加速度
|
||||||
|
- 支持自定义网格项和激活项样式
|
||||||
|
- 提供开始抽奖和抽奖结束事件
|
||||||
|
|
||||||
|
## 属性
|
||||||
|
|
||||||
|
| 属性名 | 类型 | 默认值 | 描述 |
|
||||||
|
|--------|------|--------|------|
|
||||||
|
| rows | Number | 3 | 网格的行数 |
|
||||||
|
| cols | Number | 3 | 网格的列数 |
|
||||||
|
| minSpeed | Number | 50 | 最小转动速度(毫秒) |
|
||||||
|
| maxSpeed | Number | 300 | 最大转动速度(毫秒) |
|
||||||
|
| accelerationRate | Number | 2 | 加速度率 |
|
||||||
|
| gridItemBg | String | '' | 网格项背景图片 |
|
||||||
|
| prizes | Array | 必填 | 奖品数组,每个奖品对象包含 name 和 image 属性 |
|
||||||
|
| prizeItemStyle | Object | {} | 奖品项的自定义样式 |
|
||||||
|
| activeItemStyle | Object | { backgroundColor: '#feca57' } | 激活项的自定义样式 |
|
||||||
|
|
||||||
|
## 事件
|
||||||
|
|
||||||
|
| 事件名 | 参数 | 描述 |
|
||||||
|
|--------|------|------|
|
||||||
|
| centerButtonClick | 无 | 点击中心按钮时触发 |
|
||||||
|
| lottery-end | prize | 抽奖结束时触发,返回中奖奖品对象 |
|
||||||
|
|
||||||
|
## 插槽
|
||||||
|
|
||||||
|
| 插槽名 | 描述 |
|
||||||
|
|--------|------|
|
||||||
|
| default | 自定义奖品项内容 |
|
||||||
|
| centerButton | 自定义中心按钮内容 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 确保提供的奖品数量与网格大小匹配(rows * cols - (this.rows-2)*(this.cols-2))
|
||||||
|
- 使用 `startLottery` 方法时,需要传入最终中奖的索引
|
||||||
|
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
```
|
||||||
|
<template>
|
||||||
|
<view class="content">
|
||||||
|
<view class="lottery">
|
||||||
|
<image src="/static/lk.png" style="width: 100%; height: 100%"></image>
|
||||||
|
<view style="
|
||||||
|
width: 500rpx;
|
||||||
|
height: 500rpx;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -45%);
|
||||||
|
">
|
||||||
|
<dengbo-lotteryGrid ref="lottery" @lottery-end="onLotteryEnd" :prizes="prizes"
|
||||||
|
:prizeItemStyle="prizeItemStyle" @centerButtonClick="startLottery" gridItemBg="/static/lb.png">
|
||||||
|
<template v-slot:centerButton>
|
||||||
|
<view style="width: 100%; height: 100%; position: relative">
|
||||||
|
<image src="/static/lj.png" style="width: 100%; height: 100%"></image>
|
||||||
|
<view style="
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 20%);
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #fee6aa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
">
|
||||||
|
<view style="white-space: nowrap">10积分/次</view>
|
||||||
|
<view style="white-space: nowrap">剩余9999积分</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</dengbo-lotteryGrid>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view style="margin-top: -64rpx">
|
||||||
|
<image @click="startLottery" src="/static/lan.png"
|
||||||
|
style="width: 520rpx; height: 192rpx; position: relative; z-index: 9">
|
||||||
|
</image>
|
||||||
|
</view>
|
||||||
|
<view style="margin-top: -96rpx">
|
||||||
|
<image src="/static/lt.png" style="width: 670rpx; height: 180rpx"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
prizeItemStyle: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
fontWeight: '400',
|
||||||
|
fontSize: '18rpx',
|
||||||
|
color: '#EE4F00',
|
||||||
|
},
|
||||||
|
prizes: [{
|
||||||
|
id: 1,
|
||||||
|
name: '一等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '二等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '三等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: '四等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: '五等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: '六等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: '七等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: '八等奖',
|
||||||
|
image: '/static/lj.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
startLottery() {
|
||||||
|
// // 选择一个奖品索引(这里使用随机选择作为示例)
|
||||||
|
let selectedPrizeIndex = Math.floor(Math.random() * this.prizes.length);
|
||||||
|
// // 开始抽奖
|
||||||
|
// this.isLotteryRunning = true;
|
||||||
|
this.$refs.lottery.startLottery(selectedPrizeIndex);
|
||||||
|
},
|
||||||
|
onLotteryEnd(prize) {
|
||||||
|
console.log(prize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lottery {
|
||||||
|
margin-top: 100rpx;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 688rpx;
|
||||||
|
height: 740rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<uni-popup
|
||||||
|
ref="popup"
|
||||||
|
type="bottom"
|
||||||
|
@change ="change"
|
||||||
|
border-radius="20rpx"
|
||||||
|
style="z-index: 10000"
|
||||||
|
>
|
||||||
|
<view class="filter-popup">
|
||||||
|
<view class="filter-popup__header"> 全部筛选 </view>
|
||||||
|
<view class="filter-popup__content">
|
||||||
|
<view class="filter-popup__content__left">
|
||||||
|
<view
|
||||||
|
class="filter-popup__content__left__item"
|
||||||
|
v-for="(item, index) in filterList"
|
||||||
|
:class="{ active: selectTabId === item.id }"
|
||||||
|
@click="handleTap(item.id)"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="filter-popup__content__left__item__point"
|
||||||
|
v-show="filterListSelectLists[item.id]"
|
||||||
|
></view>
|
||||||
|
{{ item.title }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="filter-popup__content__right"
|
||||||
|
:scroll-into-view="targetId"
|
||||||
|
scroll-with-animation
|
||||||
|
>
|
||||||
|
<slot> </slot>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
<view class="filter-popup__footer">
|
||||||
|
<view class="filter-popup__footer__sure filter-popup__footer__btn"
|
||||||
|
@click="sure"
|
||||||
|
>确定</view
|
||||||
|
>
|
||||||
|
<view class="filter-popup__footer__reset filter-popup__footer__btn"
|
||||||
|
@click="reset"
|
||||||
|
>重置</view
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
distanceList: [], // 右侧各部分距离顶部的距离
|
||||||
|
targetId: "", // 当前滚动到的目标ID
|
||||||
|
selectTabId: null, // 当前选中的tab id
|
||||||
|
start_price: null,
|
||||||
|
end_price: null,
|
||||||
|
// Filter details
|
||||||
|
filterDetailsLists: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "营业时间",
|
||||||
|
details: [
|
||||||
|
{ id: 1, name: "24小时营业" },
|
||||||
|
{ id: 2, name: "9-12时" },
|
||||||
|
{ id: 3, name: "12-14时" },
|
||||||
|
{ id: 4, name: "14-18时" },
|
||||||
|
{ id: 5, name: "18-24时" },
|
||||||
|
{ id: 6, name: "0-9时" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "价格",
|
||||||
|
details: [
|
||||||
|
{ id: 1, name: "0-10元" },
|
||||||
|
{ id: 2, name: "10-30元" },
|
||||||
|
{ id: 3, name: "30-50元" },
|
||||||
|
{ id: 4, name: "50-100元" },
|
||||||
|
{ id: 5, name: "100-300元" },
|
||||||
|
{ id: 6, name: "300+" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "排序",
|
||||||
|
details: [
|
||||||
|
{ id: 1, name: "价格最低" },
|
||||||
|
{ id: 2, name: "价格最高" },
|
||||||
|
{ id: 3, name: "销量最高" },
|
||||||
|
{ id: 4, name: "评分最高" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// Filter list select status
|
||||||
|
filterListSelectLists: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
filterList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "营业时间",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "价格",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "排序",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
mounted() {},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleTap(id) {
|
||||||
|
this.targetId = `filter_${id}`;
|
||||||
|
this.selectTabId = id;
|
||||||
|
// this.filterListId = id;
|
||||||
|
// this.$emit("filterChange", id);
|
||||||
|
},
|
||||||
|
getDistanceToTop() {
|
||||||
|
//获取右侧各部分距离顶部的距离
|
||||||
|
let that = this;
|
||||||
|
let selectorQuery = uni.createSelectorQuery().in(this);
|
||||||
|
selectorQuery
|
||||||
|
.selectAll(".filter-popup__content__right__items")
|
||||||
|
.boundingClientRect(function (rects) {
|
||||||
|
rects.forEach(function (rect) {
|
||||||
|
that.distanceList.push(rect.top);
|
||||||
|
});
|
||||||
|
console.log("that.distanceList", that.distanceList);
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
},
|
||||||
|
open() {
|
||||||
|
this.$refs.popup.open("bottom");
|
||||||
|
// this.getDistanceToTop();
|
||||||
|
},
|
||||||
|
change(e){
|
||||||
|
|
||||||
|
this.$emit("change",e);
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.$emit("reset");
|
||||||
|
},
|
||||||
|
sure() {
|
||||||
|
this.$emit("sure");
|
||||||
|
this.$refs.popup.close();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$refs.popup.close();
|
||||||
|
},
|
||||||
|
// Open the time selection popup
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.filter-popup {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx 20rpx 0 0;
|
||||||
|
&__header {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 50rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
padding-top: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
padding-top: 50rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
&__left {
|
||||||
|
margin-right: 10rpx;
|
||||||
|
width: 160rpx;
|
||||||
|
&__item {
|
||||||
|
height: 86rpx;
|
||||||
|
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
background: #f3f6f8;
|
||||||
|
border-radius: 0rpx 10rpx 10rpx 0rpx;
|
||||||
|
padding-left: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
&__point {
|
||||||
|
background-color: #E8101E;
|
||||||
|
height: 6rpx;
|
||||||
|
width: 6rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 3rpx;
|
||||||
|
position: absolute;
|
||||||
|
left: 21rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #E8101E;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__right {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
height: 50vh;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
// &__items {
|
||||||
|
// display: flex;
|
||||||
|
// flex-wrap: wrap;
|
||||||
|
// gap: 10rpx;
|
||||||
|
// margin-bottom: 40rpx;
|
||||||
|
// &__title {
|
||||||
|
// width: 100%;
|
||||||
|
// font-weight: 500;
|
||||||
|
// font-size: 26rpx;
|
||||||
|
// color: #333333;
|
||||||
|
// line-height: 37rpx;
|
||||||
|
// text-align: left;
|
||||||
|
// font-style: normal;
|
||||||
|
// margin-bottom: 14rpx;
|
||||||
|
// }
|
||||||
|
// &__item {
|
||||||
|
// width: 180rpx;
|
||||||
|
// height: 80rpx;
|
||||||
|
// background: #f3f6f8;
|
||||||
|
// border-radius: 10rpx;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
// }
|
||||||
|
// .price-input {
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
// align-items: center;
|
||||||
|
// width: 370rpx;
|
||||||
|
// height: 80rpx;
|
||||||
|
// background: #f3f6f8;
|
||||||
|
// border-radius: 10rpx;
|
||||||
|
// .price-input-field {
|
||||||
|
// width: 166rpx;
|
||||||
|
// height: 80rpx;
|
||||||
|
// line-height: 50rpx;
|
||||||
|
// text-align: center;
|
||||||
|
// border-radius: 26rpx;
|
||||||
|
// font-size: 24rpx;
|
||||||
|
// color: #3d3d3d;
|
||||||
|
// }
|
||||||
|
// .price-separator {
|
||||||
|
// height: 3rpx;
|
||||||
|
// width: 38rpx;
|
||||||
|
// background-color: #dddddd;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__footer {
|
||||||
|
height: 164rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0rpx 4rpx 8rpx 0rpx rgba(0, 0, 0, 0.25);
|
||||||
|
padding: 16rpx 30rpx 0 30rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
&__btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 340rpx;
|
||||||
|
height: 82rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
&__sure {
|
||||||
|
border-radius: 41rpx;
|
||||||
|
border: 2rpx solid #E8101E;
|
||||||
|
}
|
||||||
|
&__reset {
|
||||||
|
background: #E8101E;
|
||||||
|
border-radius: 41rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<view class="filter-popup__content__right__items" :id="`filter_${info.id}`">
|
||||||
|
<view class="filter-popup__content__right__items__title">{{
|
||||||
|
info.title
|
||||||
|
}}</view>
|
||||||
|
<view
|
||||||
|
v-for="(detailItem, index) in info.details"
|
||||||
|
:key="detailItem.id"
|
||||||
|
class="filter-popup__content__right__items__item"
|
||||||
|
:class="{ active: detailLists.includes(detailItem.value) }"
|
||||||
|
@click="selectDetail(detailItem)"
|
||||||
|
>
|
||||||
|
{{ detailItem.name }}
|
||||||
|
</view>
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* selectDetail 块被点击
|
||||||
|
* isSingle 是否单选
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "filterCard",
|
||||||
|
props: {
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
//是否单选
|
||||||
|
isSingle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 选中的详情列表
|
||||||
|
infoDetailList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
detailLists: [], // 选中的详情列表
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
infoDetailList: {
|
||||||
|
handler(newVal, oldVal) {
|
||||||
|
// 当父组件传入的 infoDetailList 变化时,执行此处逻辑
|
||||||
|
if(newVal.value=[]){
|
||||||
|
this.detailLists = newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
deep: true, // 若 infoDetailList 是复杂数组/对象(含嵌套),需要深度监听
|
||||||
|
immediate: true // 初始化时立即执行一次(获取初始值)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectDetail(detail) {
|
||||||
|
if (this.detailLists.includes(detail.value)) {
|
||||||
|
//如果已经存在删除存在的字段
|
||||||
|
this.detailLists = this.detailLists.filter(
|
||||||
|
(item) => item !== detail.value
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (this.isSingle) {
|
||||||
|
//如果是单选,清空之前的选择
|
||||||
|
this.detailLists = [];
|
||||||
|
}
|
||||||
|
this.detailLists = [...this.detailLists, detail.value];
|
||||||
|
}
|
||||||
|
this.$emit("selectDetail", this.detailLists);
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.detailLists = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.filter-popup__content__right__items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
&__title {
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
&__item {
|
||||||
|
width: 178rpx;
|
||||||
|
height: 78rpx;
|
||||||
|
background: #f3f6f8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1rpx solid #f3f6f8;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
background: rgba(252, 67, 124, 0.1);
|
||||||
|
color: #E8101E;
|
||||||
|
border: 1rpx solid #E8101E;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,826 @@
|
||||||
|
<template>
|
||||||
|
<view class="hao-bigbox" :style="{'top':statusBarHeight+'px','height':`calc(100% - ${statusBarHeight}px)`}">
|
||||||
|
<!-- 搜索 -->
|
||||||
|
<view class="hao-search" v-if="isSearch">
|
||||||
|
<view class="hao-searchCent">
|
||||||
|
<image src="/static/images/search_1.png" class="hao-searchImg"></image>
|
||||||
|
<input type="text" v-model="inputKey" @input="searchInput" placeholder="请输入你的服务地址"
|
||||||
|
class="hao-searchInput" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 搜索内容 -->
|
||||||
|
<view v-if="isSearch && searchList.length > 0" class="hao-searchScroll">
|
||||||
|
<scroll-view scroll-y style="width:100%;height:100%;">
|
||||||
|
<view>
|
||||||
|
<view class="hao-secondEach" v-for="(item,index) in searchList" :key="item[idValue]"
|
||||||
|
@tap="selectIndex(item)">
|
||||||
|
<view class="hao-avatar" v-if="isAvatar">
|
||||||
|
<image :src="item[avatarValue]" class="hao-avatarImg"></image>
|
||||||
|
</view>
|
||||||
|
<view class="hao-text">
|
||||||
|
<view class="hao-name">{{item[nameValue]}}</view>
|
||||||
|
<view class="hao-fu" v-if="isFu">{{item[fuValue]}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 右侧索引 -->
|
||||||
|
<view class="hao-letters" v-if="isLetters&&searchList.length <= 0"
|
||||||
|
:style="{'top':isSearch?'120rpx':'0rpx','height':isSearch?'calc(100% - 120rpx)':'100%'}">
|
||||||
|
<image src="/static/haoIndexListImg/top.png" class="hao-letterTop" v-if="isTop" @tap="goTop"></image>
|
||||||
|
<view v-for="(item,index) in letters" :key="item"
|
||||||
|
:style="{'height':indexEachHH+'rpx','background-color':(currentCeiling==index&&isIndexAc)?indexAcColor:''}"
|
||||||
|
class="hao-lettersEach" :class="(currentCeiling==index&&isIndexAc)?'hao-lettersEachAc':''"
|
||||||
|
@touchmove="handleTouchMove" @touchend="handleTouchEnd(item,index)"
|
||||||
|
@tap="handleLetterClick(item,index)">
|
||||||
|
{{item}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提示当前索引 -->
|
||||||
|
<view class="hao-toastIndex" v-if="isIndexToast && toastIndex">{{toastIndex}}</view>
|
||||||
|
|
||||||
|
<!-- 吸顶 -->
|
||||||
|
|
||||||
|
<view class="ceiling"
|
||||||
|
v-if="currentCeiling !== null&&isCeil&&listInfo.length>0&&searchList.length <= 0&&isOkCurrent"
|
||||||
|
:style="{'top':isSearch?'120rpx':'0rpx'}">{{listInfo[currentCeiling].id.split('-')[1]}}</view>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<scroll-view scroll-y class="hao-scroll" @scroll="listenScroll" :scroll-top="scrollTop"
|
||||||
|
:scroll-into-view="scrollEachId" v-show="searchList.length <= 0"
|
||||||
|
:style="{'top':isSearch?'120rpx':'0rpx','height':isSearch?'calc(100% - 120rpx)':'100%'}">
|
||||||
|
|
||||||
|
<view>
|
||||||
|
<view class="hao-modu" v-if="isCurrentCity || isHotCity">
|
||||||
|
<!-- <view class="hao-current" v-if="isCurrentCity">
|
||||||
|
<view class="hao-moduTitle">当前定位城市</view>
|
||||||
|
<view class="hao-currentBox">
|
||||||
|
<view class="hao-currentLs">
|
||||||
|
<image src="/static/haoIndexListImg/posi.png" class="hao-currentImg"></image>
|
||||||
|
<view class="hao-currentCity" @tap="selectIndex(currentCity)">{{currentCity[nameValue] || '暂无'}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view> -->
|
||||||
|
<view class="hao-hotCity" v-if="isHotCity && hotCity.length>0">
|
||||||
|
<view class="hao-moduTitle">热门城市</view>
|
||||||
|
<view class="hao-hotCityBox">
|
||||||
|
<view v-for="(item,index) in hotCity" :key="item[idValue]" @tap="selectIndex(item)"
|
||||||
|
class="hao-hotEachCity">{{item[nameValue]}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-for="(item,index) in cityList" :key="index" class="hao-each">
|
||||||
|
<view class="hao-eachTitle" :id="'hao-'+index">{{index}}</view>
|
||||||
|
<view class="hao-second">
|
||||||
|
<view v-for="(ite,ind) in item" :key="ite[idValue]" class="hao-secondEach"
|
||||||
|
@tap="selectIndex(ite)">
|
||||||
|
<view class="hao-avatar" v-if="isAvatar">
|
||||||
|
<image :src="ite[avatarValue]" class="hao-avatarImg"></image>
|
||||||
|
</view>
|
||||||
|
<view class="hao-text">
|
||||||
|
<view class="hao-name">{{ite[nameValue]}}</view>
|
||||||
|
<view class="hao-fu" v-if="isFu">{{ite[fuValue]}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ChinaCitys from '../../../static/data/newChinaCitys.json'
|
||||||
|
export default {
|
||||||
|
name: 'hao-indexList',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputKey: '', //搜索关键字
|
||||||
|
searchTimeout: null, //搜索防抖
|
||||||
|
searchList: [], //搜索内容
|
||||||
|
scrollEachId: 'all', //跳转到对应列表子项的id
|
||||||
|
toastIndex: '', //当前索引提示内容
|
||||||
|
indexEachHH: 36, //索引项的高度,单位rpx
|
||||||
|
afterConverHH: 0, //索引项的高度转换成PX值
|
||||||
|
indexFirstTT: 0, //索引第一个子元素顶部位置,单位px
|
||||||
|
indexLastBB: 0, //索引最后一个子元素底部位置,单位px
|
||||||
|
isOk: true, //索引滑动节流
|
||||||
|
isIndexGo: false, //在滑动索引时是否让列表跳转
|
||||||
|
currentCeiling: null, //当前吸顶项和当前索引
|
||||||
|
listInfo: [], //列表分类项位置信息
|
||||||
|
scrollInfo: {}, //scroll的位置信息
|
||||||
|
scrollTop: 0.01, //跳转至scroll某个位置
|
||||||
|
isOkCurrent: false, //当列表不长时,吸顶就不显示
|
||||||
|
statusBarHeight: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 获取状态栏高度并将单位转为rpx
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
const screenWidth = systemInfo.screenWidth
|
||||||
|
this.statusBarHeight = (screenWidth * (systemInfo.statusBarHeight * 2 + 88)) / 750;
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
this.statusBarHeight = 44;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
//右侧索引
|
||||||
|
letters: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
//是否显示右侧索引
|
||||||
|
isLetters: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//是否显示索引提示
|
||||||
|
isIndexToast: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//id对应的值名
|
||||||
|
idValue: {
|
||||||
|
type: String,
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
//名字对应的值名
|
||||||
|
nameValue: {
|
||||||
|
type: String,
|
||||||
|
default: 'name'
|
||||||
|
},
|
||||||
|
//当前城市
|
||||||
|
currentCity: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
//是否显示当前城市
|
||||||
|
isCurrentCity: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//热门城市
|
||||||
|
hotCity: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
//是否显示热门城市
|
||||||
|
isHotCity: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//是否显示搜索
|
||||||
|
isSearch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//城市列表
|
||||||
|
cityList: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
//是否显示头像
|
||||||
|
isAvatar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
//头像对应的值名
|
||||||
|
avatarValue: {
|
||||||
|
type: String,
|
||||||
|
default: 'avatar'
|
||||||
|
},
|
||||||
|
//是否显示副标题
|
||||||
|
isFu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
//副标题对应的值名
|
||||||
|
fuValue: {
|
||||||
|
type: String,
|
||||||
|
default: 'fu'
|
||||||
|
},
|
||||||
|
//是否吸顶
|
||||||
|
isCeil: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//是否显示当前索引背景色
|
||||||
|
isIndexAc: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//当前索引背景色
|
||||||
|
indexAcColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#5594f9'
|
||||||
|
},
|
||||||
|
//是否显示回顶部图标
|
||||||
|
isTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.afterConverHH = uni.upx2px(this.indexEachHH); //将索引项的高度值转成px
|
||||||
|
this.getLocation();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
//回顶部
|
||||||
|
goTop() {
|
||||||
|
this.scrollTop = 0;
|
||||||
|
//防止第二次点击不跳
|
||||||
|
setTimeout(() => {
|
||||||
|
this.scrollTop = 0.01;
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
//搜索输入
|
||||||
|
searchInput() {
|
||||||
|
if (this.searchTimeout) {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
}
|
||||||
|
if (!this.inputKey) {
|
||||||
|
this.searchList = [];
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//查询匹配的数据
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
// let data = this.cityList;
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
// let list = Object.values(data).flat();
|
||||||
|
// list.map(r => {
|
||||||
|
// if (r[this.nameValue].indexOf(this.inputKey) !== -1) {
|
||||||
|
// result.push(r)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
ChinaCitys.districts[0].districts.forEach(item => {
|
||||||
|
if (item.name == '台湾省') {
|
||||||
|
if (item.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': '台湾',
|
||||||
|
'adcode': item.adcode,
|
||||||
|
"center": item.center,
|
||||||
|
"citycode": item.citycode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (item.name == '香港特别行政区') {
|
||||||
|
if (item.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': '香港',
|
||||||
|
'adcode': item.adcode,
|
||||||
|
"center": item.center,
|
||||||
|
"citycode": item.citycode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (item.name == '澳门特别行政区') {
|
||||||
|
if (item.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': '澳门',
|
||||||
|
'adcode': item.adcode,
|
||||||
|
"center": item.center,
|
||||||
|
"citycode": item.citycode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (item.name == '重庆市') {
|
||||||
|
if (item.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': '重庆',
|
||||||
|
'adcode': item.adcode,
|
||||||
|
"center": item.center,
|
||||||
|
"citycode": item.citycode,
|
||||||
|
})
|
||||||
|
item.districts.forEach(item2 => {
|
||||||
|
console.log(item2)
|
||||||
|
if (item2.name == '重庆郊县') {
|
||||||
|
item2.districts.forEach(item3 => {
|
||||||
|
result.push({
|
||||||
|
'name': `${item3.name},${item2.name},${item.name}`,
|
||||||
|
'adcode': item3.adcode,
|
||||||
|
"center": item3.center,
|
||||||
|
"citycode": item3.citycode,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (item.name == '天津市') {
|
||||||
|
if (item.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': '天津',
|
||||||
|
'adcode': item.adcode,
|
||||||
|
"center": item.center,
|
||||||
|
"citycode": item.citycode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.districts.forEach(item2 => {
|
||||||
|
if (item2.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': `${item2.name},${item.name}`,
|
||||||
|
'adcode': item2.adcode,
|
||||||
|
"center": item2.center,
|
||||||
|
"citycode": item2.citycode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
item2.districts.forEach(item3 => {
|
||||||
|
if (item3.name.startsWith(this.inputKey)) {
|
||||||
|
result.push({
|
||||||
|
'name': `${item3.name},${item2.name},${item.name}`,
|
||||||
|
'adcode': item3.adcode,
|
||||||
|
"center": item3.center,
|
||||||
|
"citycode": item3.citycode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
console.log(result)
|
||||||
|
this.searchList = result;
|
||||||
|
}, 800)
|
||||||
|
},
|
||||||
|
//获取节点信息
|
||||||
|
getLocation() {
|
||||||
|
const that = this;
|
||||||
|
//获取每项索引的位置信息
|
||||||
|
uni.createSelectorQuery()
|
||||||
|
.in(this)
|
||||||
|
.selectAll('.hao-lettersEach')
|
||||||
|
.boundingClientRect(rect => {
|
||||||
|
that.indexFirstTT = Math.round(rect[0].top);
|
||||||
|
that.indexLastBB = Math.round(rect[rect.length - 1].bottom);
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
//获取scroll位置信息
|
||||||
|
uni.createSelectorQuery()
|
||||||
|
.in(this)
|
||||||
|
.select('.hao-scroll')
|
||||||
|
.boundingClientRect(rect => {
|
||||||
|
that.scrollInfo = rect;
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
//获取列表每个分类项位置信息
|
||||||
|
uni.createSelectorQuery()
|
||||||
|
.in(this)
|
||||||
|
.selectAll('.hao-eachTitle')
|
||||||
|
.boundingClientRect(rect => {
|
||||||
|
that.listInfo = rect;
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
},
|
||||||
|
//监听滚动
|
||||||
|
listenScroll(e) {
|
||||||
|
let top = this.scrollInfo.top || e.target.offsetTop; //scroll顶部的位置
|
||||||
|
let scrollTop = e.detail.scrollTop; //scroll滚动了多少
|
||||||
|
let allTop = top + scrollTop;
|
||||||
|
|
||||||
|
if (this.currentCeiling !== null) {
|
||||||
|
//大于最后一个
|
||||||
|
if (allTop >= this.listInfo[this.listInfo.length - 1].top) {
|
||||||
|
this.currentCeiling = this.listInfo.length - 1;
|
||||||
|
return
|
||||||
|
} else if (allTop > this.listInfo[this.currentCeiling].top && allTop > this.listInfo[this
|
||||||
|
.currentCeiling + 1].top) {
|
||||||
|
//当前位置远大于上一个的位置,通过遍历查询在哪
|
||||||
|
//查询当前所在的位置
|
||||||
|
let aa = this.listInfo.slice(this.currentCeiling + 1, this.listInfo.length); //截取后半段遍历
|
||||||
|
let bb = true;
|
||||||
|
aa.some((r, v) => {
|
||||||
|
if (allTop < r.top && bb) {
|
||||||
|
this.currentCeiling += v;
|
||||||
|
bb = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//当前位置小于之前
|
||||||
|
if (allTop < this.listInfo[this.currentCeiling].top) {
|
||||||
|
//小于第一个就隐藏
|
||||||
|
if (allTop < this.listInfo[0].top) {
|
||||||
|
this.currentCeiling = null;
|
||||||
|
} else {
|
||||||
|
let aa = this.listInfo.slice(0, this.currentCeiling).reverse(); //截取前半段遍历并倒序
|
||||||
|
let bb = true;
|
||||||
|
aa.some((r, v) => {
|
||||||
|
if (allTop >= r.top && bb) {
|
||||||
|
this.currentCeiling = this.currentCeiling - v - 1;
|
||||||
|
bb = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//在第一个与第二个之间
|
||||||
|
if (this.listInfo.length > 0 && allTop >= this.listInfo[0].top && allTop < this.listInfo[1].top) {
|
||||||
|
this.currentCeiling = 0;
|
||||||
|
//当列表够长时才让吸顶显示
|
||||||
|
if (!this.isOkCurrent) {
|
||||||
|
this.isOkCurrent = true;
|
||||||
|
}
|
||||||
|
} else if (this.listInfo.length > 0 && allTop > this.listInfo[0].top && allTop > this.listInfo[1]
|
||||||
|
.top) {
|
||||||
|
let bb = true;
|
||||||
|
this.listInfo.some((r, v) => {
|
||||||
|
if (allTop < r.top && bb) {
|
||||||
|
this.currentCeiling = v;
|
||||||
|
bb = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//当列表够长时才让吸顶显示
|
||||||
|
if (!this.isOkCurrent) {
|
||||||
|
this.isOkCurrent = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.currentCeiling = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//选取
|
||||||
|
selectIndex(e) {
|
||||||
|
if (!e || e.target) return;
|
||||||
|
console.log('选取', e)
|
||||||
|
this.$emit('haoTap', e);
|
||||||
|
},
|
||||||
|
//点击索引跳对应项
|
||||||
|
handleLetterClick(e, index) {
|
||||||
|
this.scrollEachId = 'hao-' + e;
|
||||||
|
this.currentCeiling = index;
|
||||||
|
},
|
||||||
|
//手指在索引滑动中
|
||||||
|
handleTouchMove(e) {
|
||||||
|
if (this.isOk) {
|
||||||
|
//节流
|
||||||
|
this.isOk = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
//当前滑动到的位置
|
||||||
|
if (e.touches[0]) {
|
||||||
|
let nowY = Math.round(e.touches[0].pageY); //四舍五入
|
||||||
|
//在索引区域内
|
||||||
|
if (nowY >= this.indexFirstTT && nowY <= this.indexLastBB) {
|
||||||
|
let aa = nowY - this.indexFirstTT;
|
||||||
|
let bb = aa / this.afterConverHH;
|
||||||
|
let cc = parseInt(bb);
|
||||||
|
if (bb > cc) {
|
||||||
|
this.toastIndex = this.letters[cc]; //提示索引
|
||||||
|
//当end方法比move方法先执行时
|
||||||
|
if (this.isIndexGo) {
|
||||||
|
this.isIndexGo = false;
|
||||||
|
this.currentCeiling = cc;
|
||||||
|
this.scrollEachId = 'hao-' + this.letters[cc]; //跳到索引指定位置
|
||||||
|
setTimeout(() => {
|
||||||
|
this.toastIndex = ''; //关闭提示
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toastIndex = this.letters[cc - 1]; //提示索引
|
||||||
|
//当end方法比move方法先执行时
|
||||||
|
if (this.isIndexGo) {
|
||||||
|
this.isIndexGo = false;
|
||||||
|
this.currentCeiling = cc - 1;
|
||||||
|
this.scrollEachId = 'hao-' + this.letters[cc - 1]; //跳到索引指定位置
|
||||||
|
setTimeout(() => {
|
||||||
|
this.toastIndex = ''; //关闭提示
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//超出范围就为空
|
||||||
|
this.toastIndex = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isOk = true;
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//手指在索引滑动结束
|
||||||
|
handleTouchEnd(e, index) {
|
||||||
|
//有索引值才跳转
|
||||||
|
if (this.toastIndex) {
|
||||||
|
//isOk为true代表滑动真正结束,否则让isIndexGo来决定最后跳转
|
||||||
|
//由于滑动方法处做了节流,当快速滑动时end会比move先执行
|
||||||
|
if (this.isOk) {
|
||||||
|
this.scrollEachId = 'hao-' + this.toastIndex; //跳到索引指定位置
|
||||||
|
this.currentCeiling = index;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.toastIndex = '';
|
||||||
|
}, 700)
|
||||||
|
} else {
|
||||||
|
this.isIndexGo = true; //让滑动处来执行跳转
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hao-bigbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fafafa;
|
||||||
|
|
||||||
|
/* 搜索 */
|
||||||
|
.hao-search {
|
||||||
|
width: 100%;
|
||||||
|
height: 120rpx;
|
||||||
|
padding-top: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.hao-searchCent {
|
||||||
|
width: 690rpx;
|
||||||
|
height: 68rpx;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 2rpx solid #E8101E;
|
||||||
|
background-color: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 68rpx;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.hao-searchImg {
|
||||||
|
width: 29rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-searchInput {
|
||||||
|
flex: 1;
|
||||||
|
height: 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
|
||||||
|
::v-deep .uni-input-placeholder {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #CCCCCC;
|
||||||
|
line-height: 34rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*搜索结果*/
|
||||||
|
.hao-searchScroll {
|
||||||
|
position: absolute;
|
||||||
|
top: 120rpx;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 120rpx);
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*右侧索引*/
|
||||||
|
.hao-letters {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 60rpx;
|
||||||
|
z-index: 6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.hao-letterTop {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-lettersEach {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #333;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-lettersEachAc {
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*当前索引提示内容*/
|
||||||
|
.hao-toastIndex {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
line-height: 160rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
z-index: 6;
|
||||||
|
background-color: #ddd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 当前与热门 */
|
||||||
|
.hao-modu {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0rpx 0rpx 12rpx 0rpx rgba(0, 0, 0, 0.05);
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 19rpx;
|
||||||
|
|
||||||
|
.hao-current {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-hotCity {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-moduTitle {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-currentBox {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.hao-currentLs {
|
||||||
|
border-radius: 10rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 2rpx solid #eee;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.hao-currentImg {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-currentCity {
|
||||||
|
color: #333;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-hotCityBox {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 22rpx;
|
||||||
|
row-gap: 20rpx;
|
||||||
|
|
||||||
|
.hao-hotEachCity {
|
||||||
|
width: 156rpx;
|
||||||
|
height: 58rpx;
|
||||||
|
background: #EFEFEF;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据列表 */
|
||||||
|
.hao-scroll {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
background-color: #fafafa;
|
||||||
|
|
||||||
|
.hao-each {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.hao-eachTitle {
|
||||||
|
display: inline-table;
|
||||||
|
width: 19rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
padding: 20rpx 0rpx;
|
||||||
|
margin: 0rpx 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 690rpx;
|
||||||
|
border-bottom: 1px solid #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-second {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*吸顶*/
|
||||||
|
.ceiling {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 10rpx 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*搜索结果与列表的每一项*/
|
||||||
|
.hao-secondEach {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 2rpx solid #f4f4f4;
|
||||||
|
|
||||||
|
.hao-avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
|
||||||
|
.hao-avatarImg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.hao-name {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 42rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-fu {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hao-secondEach:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"id": "hao-indexList",
|
||||||
|
"name": "hao-indexList 城市选择,通讯录,地址索引列表等",
|
||||||
|
"displayName": "hao-indexList 城市选择,通讯录,地址索引列表等",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"description": "城市选择,地址选取,通讯录等索引列表 ,支持搜索功能,可自定义显示字段,功能高度自由选择组合,能满足多种场景",
|
||||||
|
"keywords": [
|
||||||
|
"城市选择",
|
||||||
|
"地址",
|
||||||
|
"通讯录",
|
||||||
|
"索引列表"
|
||||||
|
],
|
||||||
|
"dcloudext": {
|
||||||
|
"category": [
|
||||||
|
"前端组件",
|
||||||
|
"通用组件"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 813 B |
Binary file not shown.
|
After Width: | Height: | Size: 609 B |
Binary file not shown.
|
After Width: | Height: | Size: 410 B |
|
|
@ -0,0 +1,297 @@
|
||||||
|
<template>
|
||||||
|
<view class="highlight-box">
|
||||||
|
<template v-if="type === 'textarea'">
|
||||||
|
<view v-if="value"
|
||||||
|
class="textarea-outer"
|
||||||
|
ref="textareaOuter"
|
||||||
|
>
|
||||||
|
<view ref="outerInner" class="outer-inner"
|
||||||
|
v-html="highlightHtml(value)">
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<textarea
|
||||||
|
ref="textareaBox"
|
||||||
|
maxlength="1000"
|
||||||
|
auto-height
|
||||||
|
:autoSize="true"
|
||||||
|
@blur="blur"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
v-model:value="value">
|
||||||
|
</textarea>
|
||||||
|
</template>
|
||||||
|
<template v-if="type === 'input'">
|
||||||
|
<view v-if="value"
|
||||||
|
class="input-outer"
|
||||||
|
v-html="highlightHtml(value)">
|
||||||
|
</view>
|
||||||
|
<a-input type="text" class="ant-input"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
v-model:value.trim="value"/>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script >
|
||||||
|
export default {
|
||||||
|
name: 'HighlightTextarea',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '请输入'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
highlightKey: {
|
||||||
|
type: Array,
|
||||||
|
require: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: 'textarea'
|
||||||
|
},
|
||||||
|
maxHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 220
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value (v) {
|
||||||
|
// this.value = this.text.replace(/(^\s*)|(\s*$)/g, '').replace(/<br \/>|<br\/>|<br>/g, '\n');
|
||||||
|
this.$emit("input", v);
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler(v) {
|
||||||
|
console.log(v, 'modelValue 3333')
|
||||||
|
|
||||||
|
this.value = v ? this.modelValue : ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler(v) {
|
||||||
|
// this.editorConfig.readOnly = v
|
||||||
|
// this.$nextTick(() => {
|
||||||
|
// if (this.editor) v ? this.editor.disable() : this.editor.enable();
|
||||||
|
// })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// this.value = this.text.replace(/(^\s*)|(\s*$)/g, '').replace(/<br \/>|<br\/>|<br>/g, '\n');
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// this.scrollMousewheel();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setValue(newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
// 触发所有必要的事件
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
this.$emit('update:modelValue', newValue);
|
||||||
|
this.$emit('change', newValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightHtml(str) {
|
||||||
|
if ((!str || !this.highlightKey || this.highlightKey.length === 0) && this.type !== 'textarea') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
let rebuild = str;
|
||||||
|
if (this.type === 'textarea') {
|
||||||
|
rebuild = rebuild.replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
||||||
|
// textarea有滚动条时,view底部不能和textarea重合,故加一个<br/>
|
||||||
|
// const wrap = this.$refs.textareaBox;
|
||||||
|
// if (wrap && wrap.scrollHeight > this.maxHeight) {
|
||||||
|
// rebuild = rebuild + '<br/>';
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
if (this.highlightKey.filter(item => ~str.indexOf(item)).length) {
|
||||||
|
let regStr = '';
|
||||||
|
let regExp = null;
|
||||||
|
this.highlightKey.forEach(list => {
|
||||||
|
regStr = this.escapeString(list);
|
||||||
|
regExp = new RegExp(regStr, 'g');
|
||||||
|
rebuild = rebuild.replace(regExp,`<span style="color:red">${list}</span>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rebuild;
|
||||||
|
},
|
||||||
|
blur(){
|
||||||
|
this.$emit("blur")
|
||||||
|
},
|
||||||
|
syncScrollTop() {
|
||||||
|
const wrap = this.$refs.textareaBox;
|
||||||
|
const outerWrap = this.$refs.textareaOuter;
|
||||||
|
const outerInner = this.$refs.outerInner;
|
||||||
|
if (wrap.scrollHeight > this.maxHeight && outerInner.scrollHeight !== wrap.scrollHeight) {
|
||||||
|
outerInner.style.height = `${wrap.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
if (wrap.scrollTop !== outerWrap.scrollTop) {
|
||||||
|
outerWrap.scrollTop = wrap.scrollTop;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// scrollMousewheel() {
|
||||||
|
// if (this.type === 'input') {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// this.$nextTick(() => {
|
||||||
|
// this.eventHandler('add');
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// 处理字符串中可能对正则有影响的字符
|
||||||
|
escapeString(value) {
|
||||||
|
const characterss = ['(', ')', '[', ']', '{', '}', '^', '$', '|', '?', '*', '+', '.'];
|
||||||
|
let str = value.replace(new RegExp('\\\\', 'g'), '\\\\');
|
||||||
|
characterss.forEach(function (characters) {
|
||||||
|
let r = new RegExp('\\' + characters, 'g');
|
||||||
|
str = str.replace(r, '\\' + characters);
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
eventHandler(type) {
|
||||||
|
const wrap = this.$refs.textareaBox;
|
||||||
|
if (wrap) {
|
||||||
|
let mousewheelevt = (/Firefox/i.test(navigator.userAgent))
|
||||||
|
? 'DOMMouseScroll' : 'mousewheel';
|
||||||
|
wrap[`${type}EventListener`](mousewheelevt, this.syncScrollTop);
|
||||||
|
wrap[`${type}EventListener`]('scroll', this.syncScrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
// this.eventHandler('remove');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@width: 100%; // 500px
|
||||||
|
.highlight-box {
|
||||||
|
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
//font-size: 12px;
|
||||||
|
width: @width;
|
||||||
|
//position: relative;
|
||||||
|
//color: #333333;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
.textarea-outer,
|
||||||
|
.input-outer {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: @width;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
// border: 1px solid transparent;
|
||||||
|
border-top: 0;
|
||||||
|
// 鼠标事件失效 ie6-10不支持
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: text;
|
||||||
|
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
span {
|
||||||
|
color: red; // #F27C49
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
border-color: #4C84FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.textarea-outer {
|
||||||
|
overflow-y: auto;
|
||||||
|
//line-height: 20px;
|
||||||
|
word-break: break-all;
|
||||||
|
.outer-inner {
|
||||||
|
// padding: 5px 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: @width;
|
||||||
|
//line-height: 20px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
.input-outer,
|
||||||
|
input {
|
||||||
|
width: @width;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.input-outer {
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
textarea,
|
||||||
|
input {
|
||||||
|
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
|
||||||
|
//font-size: 12px;
|
||||||
|
// position: relative;
|
||||||
|
// z-index: 2;
|
||||||
|
// 光标的颜色
|
||||||
|
//color: #333333;
|
||||||
|
// 文本颜色
|
||||||
|
text-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
// color: transparent;
|
||||||
|
// color: #333333;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
min-height: 300rpx;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 2px;
|
||||||
|
// border: 1px solid #d9d9d9;
|
||||||
|
padding: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
//&::placeholder {
|
||||||
|
// -webkit-text-fill-color: #d5d5d5;
|
||||||
|
//}
|
||||||
|
//&:hover {
|
||||||
|
// border-color: #36cfc9;
|
||||||
|
//}
|
||||||
|
//&:focus {
|
||||||
|
// border-color:#36cfc9;
|
||||||
|
// box-shadow: 0 0 0 2px #DBE4FF;
|
||||||
|
// outline: none;
|
||||||
|
//}
|
||||||
|
.textarea-placeholder{
|
||||||
|
-webkit-text-fill-color: #B8B8B8;
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #B8B8B8;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
<template>
|
||||||
|
<view class="hot-card" :style="{backgroundImage:`url(${bg})`}">
|
||||||
|
<view class="hot-card-footer">
|
||||||
|
<view class="hot-card-footer__left">
|
||||||
|
<image
|
||||||
|
mode="widthFix"
|
||||||
|
class="hot-card-footer__left__icon"
|
||||||
|
:src="titleImg"
|
||||||
|
></image>
|
||||||
|
<!-- <view class="hot-card-footer__left__text1">特价团</view> -->
|
||||||
|
<view class="hot-card-footer__left__text2">活动火热进行中</view>
|
||||||
|
</view>
|
||||||
|
<view class="hot-card-footer__right" @click="$emit('toMore')">
|
||||||
|
更多
|
||||||
|
<image
|
||||||
|
class="hot-card-footer__right__icon"
|
||||||
|
src="@/static/images/shop/pintuan/right_gray.png"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="hot-card-body">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in info"
|
||||||
|
:key="index"
|
||||||
|
class="hot-card-body-item"
|
||||||
|
@click="$emit('goDetail', item.server_id)"
|
||||||
|
>
|
||||||
|
<view class="hot-card-body-item-box">
|
||||||
|
<image
|
||||||
|
:mode="`aspectFill`"
|
||||||
|
:src="item.photo[0]"
|
||||||
|
class="hot-card-body-item-box-img"
|
||||||
|
></image>
|
||||||
|
<view class="hot-card-body-item-box-text" :style="{backgroundImage:`url(${lsBg})`}" :class="{'long-font-szie':getValueLength(formatPriceNumber(item.server_price - item.price)) >=7}">
|
||||||
|
<image
|
||||||
|
:mode="`aspectFill`"
|
||||||
|
src="@/static/images/shop/pintuan/jiantou.png"
|
||||||
|
class="hot-card-body-item-box-text-img"
|
||||||
|
></image>
|
||||||
|
<text> 立省<text class="num_medium_text">{{ formatPriceNumber(item.server_price - item.price) }}</text>元</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="hot-card-body-item-price num_medium_text"
|
||||||
|
>¥ <text style="font-size: 36rpx">{{ item.price }}</text></view
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "hotCard",
|
||||||
|
props: {
|
||||||
|
info: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
bg:{
|
||||||
|
type: String,
|
||||||
|
default:"/static/images/shop/pintuan/bj3.png",
|
||||||
|
},
|
||||||
|
titleImg:{
|
||||||
|
type: String,
|
||||||
|
default:"/static/images/shop/pintuan/tjt.png",
|
||||||
|
},
|
||||||
|
lsBg:{
|
||||||
|
type: String,
|
||||||
|
default:"static/images/shop/pintuan/bottomBj.png",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hot-card {
|
||||||
|
width: 702rpx;
|
||||||
|
background-position: top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
margin: 0 24rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15rpx;
|
||||||
|
&__icon {
|
||||||
|
width: 108rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
}
|
||||||
|
&__text1 {
|
||||||
|
font-family: "shuHeiTi", sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 44rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
&__text2 {
|
||||||
|
padding: 0 12rpx;
|
||||||
|
background: linear-gradient(339deg, #fff4f8 0%, #fef2f6 100%);
|
||||||
|
border-radius: 27rpx;
|
||||||
|
border: 1rpx solid #fba9bb;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&__icon {
|
||||||
|
width: 10rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hot-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 24rpx;
|
||||||
|
.hot-card-body-item {
|
||||||
|
width: 198rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.hot-card-body-item-box {
|
||||||
|
height: 244rpx;
|
||||||
|
width: 198rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
.hot-card-body-item-box-img {
|
||||||
|
border-radius: 24rpx;
|
||||||
|
height: 244rpx;
|
||||||
|
width: 198rpx;
|
||||||
|
}
|
||||||
|
.hot-card-body-item-box-text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
font-style: normal;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-position: top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 5rpx 0;
|
||||||
|
.hot-card-body-item-box-text-img {
|
||||||
|
width: 18rpx;
|
||||||
|
height: 25rpx;
|
||||||
|
margin-right: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.long-font-szie{
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hot-card-body-item-price {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<template>
|
||||||
|
<view class="code-box">
|
||||||
|
<view
|
||||||
|
v-for="(item, idx) in codeLength"
|
||||||
|
:key="idx"
|
||||||
|
:class="['code-cell', idx === currentIndex ? 'active' : '', code[idx] ? 'filled' : '']">
|
||||||
|
<text v-if="code[idx]">{{ code[idx] }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 隐藏真实输入框 -->
|
||||||
|
<input
|
||||||
|
class="real-input"
|
||||||
|
type="number"
|
||||||
|
:maxlength="codeLength"
|
||||||
|
v-model="inputValue"
|
||||||
|
:focus="true"
|
||||||
|
@input="onInput"
|
||||||
|
@focus="onFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
cursor-spacing="100"
|
||||||
|
ref="realInput" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name:"input-box",
|
||||||
|
props: {
|
||||||
|
codeLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputValue: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
code() {
|
||||||
|
return this.inputValue.split('').slice(0, this.codeLength)
|
||||||
|
},
|
||||||
|
currentIndex() {
|
||||||
|
return this.inputValue.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onInput(e) {
|
||||||
|
let val = e.detail.value.replace(/[^0-9]/g, '').slice(0, this.codeLength)
|
||||||
|
this.inputValue = val
|
||||||
|
this.$emit('input', val)
|
||||||
|
if (val.length === this.codeLength) {
|
||||||
|
this.$emit('confirm', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFocus() {
|
||||||
|
this.isFocus = true
|
||||||
|
this.inputValue = ''
|
||||||
|
},
|
||||||
|
onBlur() {
|
||||||
|
this.isFocus = false
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
if (this.inputValue.length === this.codeLength) {
|
||||||
|
this.$emit('confirm', this.inputValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.code-box {
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.code-cell {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border: 2rpx solid #eee;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #ff4d8d;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 72rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #ff4d8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filled {
|
||||||
|
border-color: #ff4d8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<view class="noData">
|
||||||
|
<image src="/static/images/shop/noData.png" class="noData-img" mode="widthFix"></image>
|
||||||
|
<text class="noData-text">暂无数据哦~</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.noData{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.noData-img{
|
||||||
|
margin-top: 100rpx;
|
||||||
|
width: 348rpx;
|
||||||
|
height: 348rpx;
|
||||||
|
}
|
||||||
|
.noData-text{
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<view class="numberValue">
|
||||||
|
<view class="fuHao" @click="calculate(-1)">-</view>
|
||||||
|
<view class="numberValue-card">
|
||||||
|
<input type="number" :value="value" @input="changeInput" class="numberValue-input" placeholder="请输入比例" />
|
||||||
|
<text class="percentage">%</text>
|
||||||
|
</view>
|
||||||
|
<view class="fuHao" @click="calculate(1)">+</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "NumberBox",
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [Number, String],
|
||||||
|
},
|
||||||
|
maxValue:{
|
||||||
|
type: Number,
|
||||||
|
default: 30,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
calculate(value) {
|
||||||
|
let num = Number(this.value) + value;
|
||||||
|
if (num > this.maxValue) {
|
||||||
|
if(num<this.maxValue +1){
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('input', this.maxValue);
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: `不能大于${this.maxValue}%`,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} else if (num < 0) {
|
||||||
|
if(num>-1){
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('input', 0);
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: `不能小于0%`,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('input', num);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeInput(event) {
|
||||||
|
let value = event.detail.value;
|
||||||
|
this.$emit('input', value);
|
||||||
|
// 将输入值转为数字(处理可能的字符串输入)
|
||||||
|
let num = Number(value);
|
||||||
|
// 处理非数字的情况(如输入非数字字符时,默认设为0)
|
||||||
|
if (isNaN(num)) {
|
||||||
|
num = 0;
|
||||||
|
}
|
||||||
|
// 边界值处理
|
||||||
|
if (num > this.maxValue) {
|
||||||
|
num = this.maxValue;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('input', num);
|
||||||
|
});
|
||||||
|
} else if (num < 0) {
|
||||||
|
num = 0;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('input', num);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.numberValue {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.numberValue-card {
|
||||||
|
position: relative;
|
||||||
|
width: 140rpx;
|
||||||
|
padding-right: 40rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
background: #F6F6F6;
|
||||||
|
|
||||||
|
.numberValue-input {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
background: #F6F6F6;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
text-align: center !important;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentage {
|
||||||
|
position: absolute;
|
||||||
|
right: 9rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuHao {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 34rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 48rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.uni-input-placeholder{
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #D1CCCC;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
<template>
|
||||||
|
<view class="popup-picker" v-if="show">
|
||||||
|
<!-- 遮罩层 -->
|
||||||
|
<view class="popup-mask" v-if="show" :class="{ show: show }" @click="handleClose"></view>
|
||||||
|
|
||||||
|
<!-- 选择器内容 -->
|
||||||
|
<view class="popup-content" :class="{ show: show }">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="cancel" @click="handleClose">取消</text>
|
||||||
|
<text class="title">{{title}}</text>
|
||||||
|
<text class="confirm" @click="handleConfirm">确定</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 选择器 -->
|
||||||
|
<picker-view
|
||||||
|
:value="currentValue"
|
||||||
|
@change="handleChange"
|
||||||
|
class="picker-view"
|
||||||
|
>
|
||||||
|
<picker-view-column v-for="(column, index) in columns" :key="index">
|
||||||
|
<view
|
||||||
|
class="picker-item"
|
||||||
|
v-for="(item, itemIndex) in column"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
{{item[labelKey] || item.title}}
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
</picker-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'PopupPicker',
|
||||||
|
props: {
|
||||||
|
// 是否显示
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 标题
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '请选择'
|
||||||
|
},
|
||||||
|
// 选项数据
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
// 当前选中的值
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
// 选项的标签字段
|
||||||
|
labelKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'label'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentValue: this.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(val) {
|
||||||
|
this.currentValue = val
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 处理选择变化
|
||||||
|
handleChange(e) {
|
||||||
|
this.currentValue = e.detail.value;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理关闭
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('close')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.show = false
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理确认
|
||||||
|
handleConfirm() {
|
||||||
|
const selectedItems = this.currentValue.map((value, index) => {
|
||||||
|
const column = this.columns[index]
|
||||||
|
return column[value]
|
||||||
|
})
|
||||||
|
this.$emit('confirm', {
|
||||||
|
value: this.currentValue,
|
||||||
|
items: selectedItems
|
||||||
|
})
|
||||||
|
this.handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popup-picker {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层 */
|
||||||
|
.popup-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-mask.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区 */
|
||||||
|
.popup-content {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content.show {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部 */
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 88rpx;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #4080FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选择器 */
|
||||||
|
.picker-view {
|
||||||
|
width: 100%;
|
||||||
|
height: 480rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
line-height: 68rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平台适配 */
|
||||||
|
/* #ifdef H5 */
|
||||||
|
.popup-content {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
.popup-content {
|
||||||
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="scroll-container"
|
||||||
|
@touchstart="handleTouchStart"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
|
@touchend="handleTouchEnd"
|
||||||
|
:style="{ transform: `translateY(${offsetY}px)` }"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ScrollContainer',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
startY: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
isScrolling: false,
|
||||||
|
canScroll: true // 控制是否允许滑动
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTouchStart(e) {
|
||||||
|
// 检查触摸点是否在可滑动区域(页面顶部或底部)
|
||||||
|
const touchY = e.touches[0].clientY;
|
||||||
|
const windowHeight = uni.getSystemInfoSync().windowHeight;
|
||||||
|
|
||||||
|
// 只在页面顶部100px或底部100px区域内允许滑动
|
||||||
|
this.canScroll = touchY < 100 || touchY > windowHeight - 100;
|
||||||
|
|
||||||
|
if (this.canScroll) {
|
||||||
|
this.startY = e.touches[0].clientY;
|
||||||
|
this.isScrolling = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleTouchMove(e) {
|
||||||
|
if (!this.isScrolling || !this.canScroll) return;
|
||||||
|
|
||||||
|
const currentY = e.touches[0].clientY;
|
||||||
|
const deltaY = currentY - this.startY;
|
||||||
|
const maxScroll = 100;
|
||||||
|
|
||||||
|
// 应用阻力效果
|
||||||
|
this.offsetY += deltaY * 0.5;
|
||||||
|
|
||||||
|
// 限制滚动范围
|
||||||
|
if (Math.abs(this.offsetY) > maxScroll) {
|
||||||
|
this.offsetY = this.offsetY > 0 ? maxScroll : -maxScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startY = currentY;
|
||||||
|
|
||||||
|
// 阻止默认行为,避免与内部滚动冲突
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
handleTouchEnd() {
|
||||||
|
this.isScrolling = false;
|
||||||
|
this.canScroll = true;
|
||||||
|
// 使用动画回弹
|
||||||
|
this.offsetY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scroll-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
position: relative;
|
||||||
|
/* 允许穿透点击 */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
<template>
|
||||||
|
<view class="search">
|
||||||
|
<image class="back" src="/static/images/back.png" mode="aspectFill" @click="goBack" v-if="showBack"></image>
|
||||||
|
<view class="search-right" :style="{ 'width': width,'height':height,'border':border}">
|
||||||
|
<input type="text" class="search-inp" :placeholder="placeholder" :value="value" @input="onInput"
|
||||||
|
@confirm="onConfirm" confirm-type="search" placeholder-style="color: #999999;" />
|
||||||
|
<image class="search-icon" src="/static/images/search.png" @click.stop="onSearch"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'search-box',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请输入搜索内容'
|
||||||
|
},
|
||||||
|
showBack: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
searchStyle() {
|
||||||
|
// #ifdef APP-PLUS || H5
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// #ifdef APP-PLUS || H5
|
||||||
|
// 获取状态栏高度
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
this.statusBarHeight = systemInfo.statusBarHeight
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
uni.navigateBack()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN || H5
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
onSearch(e) {
|
||||||
|
// App阻止冒泡
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
e.stopPropagation()
|
||||||
|
// #endif
|
||||||
|
this.$emit('search')
|
||||||
|
},
|
||||||
|
onInput(e) {
|
||||||
|
// #ifdef APP-PLUS || H5
|
||||||
|
this.$emit('input', e.detail.value || e.target.value)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
this.$emit('input', e.detail.value)
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
onConfirm(e) {
|
||||||
|
// #ifdef APP-PLUS || H5
|
||||||
|
this.$emit('confirm', e.detail.value || e.target.value)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
this.$emit('confirm', e.detail.value)
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search {
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
/* #ifdef H5 */
|
||||||
|
cursor: pointer;
|
||||||
|
/* #endif */
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-right {
|
||||||
|
width: 660rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
position: relative;
|
||||||
|
/* #ifdef H5 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-inp {
|
||||||
|
flex: 1;
|
||||||
|
height: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #000000;
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0 70rpx 0 48rpx;
|
||||||
|
/* #ifdef H5 */
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
position: absolute;
|
||||||
|
right: 40rpx;
|
||||||
|
top: 16rpx;
|
||||||
|
/* #ifdef H5 */
|
||||||
|
cursor: pointer;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #ifdef H5 */
|
||||||
|
.search-inp::-webkit-input-placeholder {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-inp::-moz-placeholder {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-inp:-ms-input-placeholder {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<template>
|
||||||
|
<view class="selectTime">
|
||||||
|
<view class="selectTime-time" :class="{'selectTime-time-y': startTime}" @click="openPicker(startTime, 0)">
|
||||||
|
{{ startTime || '开始时间' }}
|
||||||
|
</view>
|
||||||
|
<view class="selectTime-time" :class="{'selectTime-time-y': endTime}" @click="openPicker(endTime, 1)">
|
||||||
|
{{ endTime || '结束时间' }}
|
||||||
|
</view>
|
||||||
|
<timeDatePicker ref="timePopup" @confirmTime="confirmTime" v-model="timeValue" :timeIndex="timeIndex"
|
||||||
|
:timeList="value" :type="type"></timeDatePicker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import timeDatePicker from 'components/time-picker/time-date-picker.vue';
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
startTime: null,
|
||||||
|
endTime: null, // 修复拼写错误:endTiem → endTime
|
||||||
|
timeValue: null,
|
||||||
|
timeIndex: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
timeDatePicker,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [] // 确保默认值是数组
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'datetimerange', // 身份证有效期默认用日期范围
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听父组件传入的value变化,同步到子组件
|
||||||
|
value: {
|
||||||
|
immediate: true, // 初始化时立即执行
|
||||||
|
handler(newVal) {
|
||||||
|
if (Array.isArray(newVal) && newVal.length === 2) {
|
||||||
|
this.startTime = newVal[0];
|
||||||
|
this.endTime = newVal[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openPicker(time, index) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.timeIndex = index;
|
||||||
|
this.timeValue = time;
|
||||||
|
this.$refs.timePopup.openTimePopup();
|
||||||
|
},
|
||||||
|
confirmTime(e) {
|
||||||
|
// 更新对应的值
|
||||||
|
if (this.timeIndex === 0) {
|
||||||
|
this.startTime = e;
|
||||||
|
} else {
|
||||||
|
this.endTime = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保数组格式正确
|
||||||
|
const timeArry = [this.startTime || '', this.endTime || ''];
|
||||||
|
// 触发父组件更新
|
||||||
|
this.$emit("input", timeArry);
|
||||||
|
// 额外触发change事件,方便父组件处理
|
||||||
|
this.$emit("change", timeArry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.selectTime {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
column-gap: 22rpx;
|
||||||
|
|
||||||
|
.selectTime-time {
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #CACCCC;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
height: 72rpx;
|
||||||
|
border: 1rpx solid #E5E5E5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
transition: all 0.3s; // 增加过渡效果
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectTime-time-y {
|
||||||
|
color: #333;
|
||||||
|
border-color: #409EFF; // 选中时改变边框颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用状态样式
|
||||||
|
&.disabled {
|
||||||
|
.selectTime-time {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- <view class="selectTime">
|
||||||
|
<view class="select-title">开始日期</view>
|
||||||
|
<view class="select-title">结束日期</view>
|
||||||
|
</view> -->
|
||||||
|
<view class="selectTime">
|
||||||
|
<view class="selectTime-time" :class="{'selectTime-time-y': startTime}" @click="openPicker(startTime, 0)">
|
||||||
|
<view class="selectTime-time-left">
|
||||||
|
{{ startTime || '开始时间' }}
|
||||||
|
<image src="/static/images/recruit/close.png" v-if="startTime" class="clearing" @tap.stop="clear(0)"></image>
|
||||||
|
</view>
|
||||||
|
<view class="selectTime-time-right">
|
||||||
|
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/fd0058af-7e38-4442-a7d3-6a65768ce0da" mode="aspectFill"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="selectTime-time" :class="{'selectTime-time-y': endTime}" @click="openPicker(endTime, 1)">
|
||||||
|
|
||||||
|
<view class="selectTime-time-left">
|
||||||
|
{{ endTime || '结束时间' }}
|
||||||
|
<image src="/static/images/recruit/close.png" v-if="endTime" class="clearing" @tap.stop="clear(1)"></image>
|
||||||
|
</view>
|
||||||
|
<view class="selectTime-time-right">
|
||||||
|
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/fd0058af-7e38-4442-a7d3-6a65768ce0da" mode="aspectFill"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<timeDatePicker ref="timePopup" @confirmTime="confirmTime" v-model="timeValue" :timeIndex="timeIndex"
|
||||||
|
:timeList="value" :type="type" :endDistanceYear="endDistanceYear"></timeDatePicker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import timeDatePicker from 'components/time-picker/time-date-picker.vue';
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
startTime: null,
|
||||||
|
endTime: null, // 修复拼写错误:endTiem → endTime
|
||||||
|
timeValue: null,
|
||||||
|
timeIndex: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
timeDatePicker,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [] // 确保默认值是数组
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'daterange', // 身份证有效期默认用日期范围
|
||||||
|
},
|
||||||
|
idCardtype: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
endDistanceYear:{
|
||||||
|
type: Number,
|
||||||
|
default: 0,//结束时间,与当前时间点相距的时间
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听父组件传入的value变化,同步到子组件
|
||||||
|
value: {
|
||||||
|
immediate: true, // 初始化时立即执行
|
||||||
|
handler(newVal) {
|
||||||
|
if (Array.isArray(newVal) && newVal.length === 2) {
|
||||||
|
this.startTime = newVal[0];
|
||||||
|
this.endTime = newVal[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
idCardtype:{
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal ==1) {
|
||||||
|
// 确保数组格式正确
|
||||||
|
this.endTime = ""
|
||||||
|
const timeArry = [this.startTime || '', ''];
|
||||||
|
// 触发父组件更新
|
||||||
|
this.$emit("input", timeArry);
|
||||||
|
// 额外触发change事件,方便父组件处理
|
||||||
|
this.$emit("change", timeArry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openPicker(time, index) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
if(!this.idCardtype) return;
|
||||||
|
if(index==1 && this.idCardtype==1) return;
|
||||||
|
this.timeIndex = index;
|
||||||
|
this.timeValue = time;
|
||||||
|
this.$refs.timePopup.openTimePopup();
|
||||||
|
},
|
||||||
|
confirmTime(e) {
|
||||||
|
|
||||||
|
// 更新对应的值
|
||||||
|
if (this.timeIndex === 0) {
|
||||||
|
this.startTime = e;
|
||||||
|
} else {
|
||||||
|
this.endTime = e;
|
||||||
|
}
|
||||||
|
// 确保数组格式正确
|
||||||
|
const timeArry = [this.startTime || '', this.endTime || ''];
|
||||||
|
// 触发父组件更新
|
||||||
|
this.$emit("input", timeArry);
|
||||||
|
// 额外触发change事件,方便父组件处理
|
||||||
|
this.$emit("change", timeArry);
|
||||||
|
},
|
||||||
|
clear(index){
|
||||||
|
// 更新对应的值
|
||||||
|
if (index === 0) {
|
||||||
|
this.startTime = "";
|
||||||
|
} else {
|
||||||
|
this.endTime = "e";
|
||||||
|
}
|
||||||
|
// 确保数组格式正确
|
||||||
|
const timeArry = [this.startTime || '', this.endTime || ''];
|
||||||
|
// 触发父组件更新
|
||||||
|
this.$emit("input", timeArry);
|
||||||
|
// 额外触发change事件,方便父组件处理
|
||||||
|
this.$emit("change", timeArry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.selectTime {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
column-gap: 26rpx;
|
||||||
|
|
||||||
|
.selectTime-time {
|
||||||
|
width: 328rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 2rpx solid #E9E9E9;
|
||||||
|
display: flex;
|
||||||
|
transition: all 0.3s; // 增加过渡效果
|
||||||
|
background: #F8F8F8 ;
|
||||||
|
.selectTime-time-left{
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999999;
|
||||||
|
line-height: 40rpx;
|
||||||
|
justify-content: center;
|
||||||
|
font-style: normal;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 19rpx;
|
||||||
|
position: relative;
|
||||||
|
.clearing{
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
position: absolute;
|
||||||
|
right: 8rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.selectTime-time-right{
|
||||||
|
width: 59rpx;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 2rpx solid #E9E9E9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
image{
|
||||||
|
width: 35rpx;
|
||||||
|
height: 33rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectTime-time-y {
|
||||||
|
color: #333;
|
||||||
|
border-color: #409EFF; // 选中时改变边框颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用状态样式
|
||||||
|
&.disabled {
|
||||||
|
.selectTime-time {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-title{
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
width: 317rpx;
|
||||||
|
padding-left: 14rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,451 @@
|
||||||
|
<template>
|
||||||
|
<uni-popup ref="popup" type="bottom" border-radius="20rpx">
|
||||||
|
<view class="time-popup">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">请选择服务时间</text>
|
||||||
|
<text class="popup-close" @click="closeTime">取消</text>
|
||||||
|
</view>
|
||||||
|
<view class="time-picker-content">
|
||||||
|
<!-- 日期选择 -->
|
||||||
|
<view class="time-picker-content__datas">
|
||||||
|
<view
|
||||||
|
class="time-picker-content__datas__date__item"
|
||||||
|
v-for="(date, index) in dateList"
|
||||||
|
:key="index"
|
||||||
|
@click="selectDate(index)"
|
||||||
|
>
|
||||||
|
<view class="time-picker-content__datas__date__item__box">
|
||||||
|
<image
|
||||||
|
src="/static/images/dataBox.png"
|
||||||
|
class="time-picker-content__datas__date__item__box__img"
|
||||||
|
v-if="selectedDateIndex == index"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
class="time-picker-content__datas__date__item__text"
|
||||||
|
:class="selectedDateIndex === index ? 'active' : ''"
|
||||||
|
>{{ date }}</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 时间选择 -->
|
||||||
|
<scroll-view scroll-y="true" style="height: 600rpx; width: 690rpx">
|
||||||
|
<view class="time-picker-content__time__list" v-if="timeOptions[selectedDateIndex] && timeOptions[selectedDateIndex].length">
|
||||||
|
<view
|
||||||
|
class="time-picker-content__time__list__item"
|
||||||
|
v-for="(time, index) in timeOptions[selectedDateIndex]"
|
||||||
|
:key="index"
|
||||||
|
:class="[getItemClass(time.state || 0), {
|
||||||
|
firstActive: selectedTimeIndex == index,
|
||||||
|
lastActive: selectedTimeIndex + serviceTimeNum - 1 == index,
|
||||||
|
isActive: index >= selectedTimeIndex &&
|
||||||
|
index <= selectedTimeIndex + serviceTimeNum - 1 &&
|
||||||
|
sureTimeDataIndex === selectedDateIndex &&
|
||||||
|
selectedTimeIndex !== null
|
||||||
|
}]"
|
||||||
|
@click="selectTime(time, index)"
|
||||||
|
>
|
||||||
|
<text class="time-picker-content__time__list__item__text">{{
|
||||||
|
time.show_time
|
||||||
|
}}</text>
|
||||||
|
<!-- <text class="time-picker-content__time__list__item__state">{{
|
||||||
|
time.stateName || getStateText(time.state)
|
||||||
|
}}</text> -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="time-picker-content__time__sure">
|
||||||
|
<view class="time-picker-content__time__sure__box" @click="sureTime">
|
||||||
|
<text class="time-picker-content__time__sure__box__text">确认</text>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* sureTime 确认时间触发事件
|
||||||
|
* openTime 打开时间选择弹框
|
||||||
|
* closeTime 关闭时间选择弹框
|
||||||
|
* selectDate 选择日期
|
||||||
|
* selectTime 选择时间
|
||||||
|
* getItemClass 获取列表项的class
|
||||||
|
* handelGetTimeArr 获取时间段,
|
||||||
|
*/
|
||||||
|
import request from "@/utils/request";
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isFirstOPen: true,
|
||||||
|
sureTimeObj: {}, // 确认时间对象
|
||||||
|
dateList: ["今天", "明天", "后天"],
|
||||||
|
selectedTimeIndex: "", // 选中的时间索引
|
||||||
|
serviceTimeNum: 1, // 服务时间段数量
|
||||||
|
sureTimeDataIndex: null, // 确认的时间索引
|
||||||
|
selectedDateIndex: 0, // 选中的日期索引
|
||||||
|
startTimeObj: {
|
||||||
|
serviceTimeNum: "", // 服务时间段数量
|
||||||
|
sureTimeDataIndex: 0, // 确认的时间索引
|
||||||
|
selectedDateIndex: 0, // 选中的日期索引
|
||||||
|
}, // 开始时间对象
|
||||||
|
// 状态映射配置
|
||||||
|
stateConfig: {
|
||||||
|
1: {
|
||||||
|
class: "state-available",
|
||||||
|
text: "可约",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
class: "state-full",
|
||||||
|
text: "约满",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
class: "state-rest",
|
||||||
|
text: "休息",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
class: "state-locked",
|
||||||
|
text: "锁定",
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
class: "state-unavailable",
|
||||||
|
text: "不可用",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// 时间选项数组
|
||||||
|
timeOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 初始时间索引
|
||||||
|
firstTimeIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
// 初始日期索引
|
||||||
|
firstDataIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
// 服务信息
|
||||||
|
service: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
publish_user_id: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
server_type: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
service: {
|
||||||
|
handler(newValue) {
|
||||||
|
this.serviceTimeNum = Math.max(1, Math.ceil((newValue.server_time || 0) / 10 + 1));
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {},
|
||||||
|
mounted() {},
|
||||||
|
methods: {
|
||||||
|
openTime() {
|
||||||
|
console.log("打开时间选择弹框", this.selectedDateIndex);
|
||||||
|
this.isFirstOPen = false;
|
||||||
|
|
||||||
|
this.startTimeObj = {
|
||||||
|
selectedTimeIndex: this.selectedTimeIndex,
|
||||||
|
sureTimeDataIndex: this.sureTimeDataIndex,
|
||||||
|
selectedDateIndex: this.selectedDateIndex,
|
||||||
|
};
|
||||||
|
this.$refs.popup.open("bottom");
|
||||||
|
},
|
||||||
|
// 获取状态文本
|
||||||
|
getStateText(state) {
|
||||||
|
return this.stateConfig[state]?.text || "未知";
|
||||||
|
},
|
||||||
|
// 列表项的class获取方法
|
||||||
|
getItemClass(state) {
|
||||||
|
const _state = state || 0;
|
||||||
|
return this.stateConfig[_state]?.class || "state-unknown";
|
||||||
|
},
|
||||||
|
// 选择日期
|
||||||
|
selectDate(index) {
|
||||||
|
this.selectedDateIndex = index;
|
||||||
|
},
|
||||||
|
selectTime(time, index) {
|
||||||
|
if (time.state === 1) {
|
||||||
|
// 可约状态
|
||||||
|
let parms = {
|
||||||
|
user_syr_id: this.publish_user_id || this.service.publish_user_id,
|
||||||
|
reserve_time: this.timeOptions[this.selectedDateIndex][index].time,
|
||||||
|
server_id: this.service.id,
|
||||||
|
server_type: this.server_type,
|
||||||
|
};
|
||||||
|
|
||||||
|
request
|
||||||
|
.post("/user/reserve/timeCheck", parms)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 200) {
|
||||||
|
this.selectedTimeIndex = index;
|
||||||
|
this.sureTimeDataIndex = this.selectedDateIndex; // 更新确认时间索引
|
||||||
|
|
||||||
|
this.sureTimeObj = {
|
||||||
|
selectedDateIndex: this.selectedDateIndex,
|
||||||
|
selectedTimeIndex: this.selectedTimeIndex,
|
||||||
|
time: this.timeOptions[this.selectedDateIndex][
|
||||||
|
this.selectedTimeIndex
|
||||||
|
],
|
||||||
|
res: res,
|
||||||
|
};
|
||||||
|
this.$emit("selectTime", this.sureTimeObj);
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: "当前时间不可预约",
|
||||||
|
icon: "none",
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("报错", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sureTime() {
|
||||||
|
this.$refs.popup.close();
|
||||||
|
this.$emit("sureTime", this.sureTimeObj);
|
||||||
|
},
|
||||||
|
handelGetTimeArr(time = null) {
|
||||||
|
// 获取时间段
|
||||||
|
let date = {
|
||||||
|
selectedDateIndex: this.selectedDateIndex,
|
||||||
|
selectedTimeIndex: this.selectedTimeIndex,
|
||||||
|
time: this.timeOptions[this.selectedDateIndex][this.selectedTimeIndex],
|
||||||
|
};
|
||||||
|
this.$emit("getTimeArr", date);
|
||||||
|
},
|
||||||
|
closeTime() {
|
||||||
|
// 关闭时间选择弹框
|
||||||
|
this.selectedTimeIndex = this.startTimeObj.selectedTimeIndex;
|
||||||
|
this.sureTimeDataIndex = this.startTimeObj.sureTimeDataIndex;
|
||||||
|
this.selectedDateIndex = this.startTimeObj.selectedDateIndex;
|
||||||
|
this.$refs.popup.close();
|
||||||
|
this.$emit("closeTime");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式,nvue不支持scoped和less,需使用普通css */
|
||||||
|
/* 时间选择弹框 */
|
||||||
|
.time-popup {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx 30rpx 36rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content {
|
||||||
|
padding: 32rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 750rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__datas {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 690rpx;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__datas__date__item {
|
||||||
|
width: 214rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__datas__date__item__text {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__datas__date__item__text.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__datas__date__item__box {
|
||||||
|
width: 214rpx;
|
||||||
|
height: 74rpx;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.time-picker-content__datas__date__item__box__img {
|
||||||
|
width: 214rpx;
|
||||||
|
height: 74rpx;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__time__list {
|
||||||
|
width: 690rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__time__list__item {
|
||||||
|
width: 114rpx;
|
||||||
|
height: 66rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__time__list__item__text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 66rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__time__list__item__state {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态样式 */
|
||||||
|
.state-available {
|
||||||
|
color: #3c3638;
|
||||||
|
/* background: #f7f8fa; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-full {
|
||||||
|
color: #3c3638;
|
||||||
|
background: #e4e9f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-rest {
|
||||||
|
color: #cdcdcd;
|
||||||
|
background: #f7f8f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-locked {
|
||||||
|
color: #3c3638;
|
||||||
|
background: #e4e9f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-unavailable {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #e4e9f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-unknown {
|
||||||
|
color: #999999;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isActive {
|
||||||
|
background: rgba(252, 67, 124, 0.08);
|
||||||
|
color: #e8101e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isActive .time-picker-content__time__list__item__text {
|
||||||
|
color: #e8101e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中样式前后弧度 */
|
||||||
|
.firstActive {
|
||||||
|
border-radius: 51rpx 0rpx 0rpx 51rpx;
|
||||||
|
}
|
||||||
|
.lastActive {
|
||||||
|
border-radius: 0rpx 51rpx 51rpx 0rpx}
|
||||||
|
|
||||||
|
.time-picker-content__time__sure {
|
||||||
|
padding: 39rpx 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 690rpx;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content__time__sure__box {
|
||||||
|
width: 648rpx;
|
||||||
|
height: 94rpx;
|
||||||
|
background: #E8101E;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.time-picker-content__time__sure__box__text {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,511 @@
|
||||||
|
<template>
|
||||||
|
<view class="time-popup">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">选择服务</text>
|
||||||
|
</view>
|
||||||
|
<view class="time-picker-content">
|
||||||
|
<!-- 日期选择 -->
|
||||||
|
<view class="time-picker-content__datas">
|
||||||
|
<view class="time-picker-content__datas__date__item" v-for="(date, index) in dateList" :key="index"
|
||||||
|
@click="selectDate(index)">
|
||||||
|
<text class="time-picker-content__datas__date__item__text"
|
||||||
|
:class="{ active: selectedDateIndex === index }">{{ date }}</text>
|
||||||
|
<image v-if="selectedDateIndex === index" src="/static/images/dataBox.png"
|
||||||
|
class="time-picker-content__datas__date__item__box" mode="aspectFit"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 时间选择 -->
|
||||||
|
<scroll-view class="time-picker-content__time__list" scroll-y>
|
||||||
|
<view class="time-picker-content__time__list__item"
|
||||||
|
v-for="(time, index) in timeOptions[selectedDateIndex]" :key="index" :class="[
|
||||||
|
getItemClass(time.state),
|
||||||
|
{
|
||||||
|
isActive:
|
||||||
|
activeList.includes(index) &&
|
||||||
|
sureTimeDataIndex == selectedDateIndex,
|
||||||
|
},
|
||||||
|
]" @click="selectTime(time, index)">
|
||||||
|
<view class="time-picker-content__time__list__item__text">{{
|
||||||
|
time.show_time
|
||||||
|
}}</view>
|
||||||
|
<view class="time-picker-content__time__list__item__state">{{
|
||||||
|
time.stateName
|
||||||
|
}}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<!-- <view class="time-picker-content__time__sure">
|
||||||
|
<view class="time-picker-content__time__sure__box" @click="sureTime"
|
||||||
|
>确认</view
|
||||||
|
>
|
||||||
|
</view> -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* sureTime 确认时间触发事件
|
||||||
|
* openTime 打开时间选择弹框
|
||||||
|
* closeTime 关闭时间选择弹框
|
||||||
|
* selectDate 选择日期
|
||||||
|
* selectTime 选择时间
|
||||||
|
* getItemClass 获取列表项的class
|
||||||
|
* handelGetTimeArr 获取时间段,
|
||||||
|
*/
|
||||||
|
import request from "@/utils/request";
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isFirstOPen: true,
|
||||||
|
sureTimeObj: {}, // 确认时间对象
|
||||||
|
dateList: ["今天", "明天", "后天"],
|
||||||
|
startTimeIndex: null, // 开始时间索引
|
||||||
|
endTimeIndex: null, //结束时间
|
||||||
|
activeList: [],
|
||||||
|
sureTimeDataIndex: 0, // 确认的时间索引
|
||||||
|
selectedDateIndex: 0, // 选中的日期索引
|
||||||
|
// 状态映射配置
|
||||||
|
stateConfig: {
|
||||||
|
1: {
|
||||||
|
class: "state-available",
|
||||||
|
text: "可约",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
class: "state-full",
|
||||||
|
text: "约满",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
class: "state-rest",
|
||||||
|
text: "休息",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
class: "state-locked",
|
||||||
|
text: "锁定",
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
class: "state-unavailable",
|
||||||
|
text: "不可用",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// 时间选项数组
|
||||||
|
timeOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 服务信息
|
||||||
|
service: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
identity: {
|
||||||
|
type: Number,
|
||||||
|
default: 0, //0为商家,1为手艺人
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
console.log("选择时间", this.service);
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {
|
||||||
|
// 列表项的class获取方法
|
||||||
|
getItemClass(state) {
|
||||||
|
return this.stateConfig[state]?.class || "state-unknown";
|
||||||
|
},
|
||||||
|
// 选择日期
|
||||||
|
selectDate(index) {
|
||||||
|
this.selectedDateIndex = index;
|
||||||
|
},
|
||||||
|
// 生成两个数字之间的数组(包含边界值)
|
||||||
|
createRangeArray(start, end) {
|
||||||
|
// 确定步长,正数表示递增,负数表示递减
|
||||||
|
let step = start <= end ? 1 : -1;
|
||||||
|
let length = Math.abs(end - start) + 1;
|
||||||
|
|
||||||
|
// 创建数组并填充值
|
||||||
|
return Array.from({
|
||||||
|
length
|
||||||
|
}, (_, i) => start + i * step);
|
||||||
|
},
|
||||||
|
selectTime(time, index) {
|
||||||
|
let that = this;
|
||||||
|
//如果和记录日期不同重新记录
|
||||||
|
if (this.sureTimeDataIndex != this.selectedDateIndex) {
|
||||||
|
that.startTimeIndex = null;
|
||||||
|
that.endTimeIndex = null;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
let startTimeIndex = this.startTimeIndex;
|
||||||
|
let endTimeIndex = this.endTimeIndex;
|
||||||
|
if (time.state === 1) {
|
||||||
|
// 可约状态
|
||||||
|
if(startTimeIndex == endTimeIndex && endTimeIndex == index){
|
||||||
|
this.startTimeIndex = null;
|
||||||
|
this.endTimeIndex = null;
|
||||||
|
this.storageStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (startTimeIndex == null || startTimeIndex != endTimeIndex) {
|
||||||
|
this.startTimeIndex = index;
|
||||||
|
this.endTimeIndex = index;
|
||||||
|
this.storageStatus();
|
||||||
|
that.handelGetTimeArr();
|
||||||
|
uni.showToast({
|
||||||
|
title: `您已选择${this.timeOptions[this.selectedDateIndex][this.startTimeIndex].show_time},烦请再选一个时间,完成「开始 / 结束」的时间段设置`,
|
||||||
|
icon: "none",
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// if (endTimeIndex == index) {
|
||||||
|
// this.endTimeIndex = startTimeIndex;
|
||||||
|
// this.storageStatus();
|
||||||
|
// that.handelGetTimeArr();
|
||||||
|
// return;
|
||||||
|
// } else if (startTimeIndex == index) {
|
||||||
|
// this.startTimeIndex = endTimeIndex;
|
||||||
|
// this.storageStatus();
|
||||||
|
// that.handelGetTimeArr();
|
||||||
|
// return;
|
||||||
|
// } else if (startTimeIndex > index) {
|
||||||
|
// startTimeIndex = index;
|
||||||
|
// } else if (endTimeIndex < index) {
|
||||||
|
// endTimeIndex = index;
|
||||||
|
// } else {
|
||||||
|
// let leftNum = index - startTimeIndex;
|
||||||
|
// let rightNum = endTimeIndex - index;
|
||||||
|
// //如果靠近开始时间
|
||||||
|
// if (leftNum < rightNum) {
|
||||||
|
// startTimeIndex = index;
|
||||||
|
// } else {
|
||||||
|
// endTimeIndex = index;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
if (startTimeIndex > index) {
|
||||||
|
startTimeIndex = index;
|
||||||
|
} else{
|
||||||
|
endTimeIndex = index;
|
||||||
|
}
|
||||||
|
// let len = this.timeOptions[this.sureTimeDataIndex].length
|
||||||
|
if (endTimeIndex - startTimeIndex < 3) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "预约时间最少需要30分钟,请调整您的结束时间选择~",
|
||||||
|
icon: "none",
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let found = this.timeOptions[this.selectedDateIndex].slice(startTimeIndex,endTimeIndex).findIndex(item=>item.state !=1)
|
||||||
|
console.log(22222,found,this.timeOptions[this.selectedDateIndex].slice(startTimeIndex,endTimeIndex))
|
||||||
|
if(found!=-1){
|
||||||
|
uni.showToast({
|
||||||
|
title: "您选择的时间段内部分时间不可用,请调整后重新选择~",
|
||||||
|
icon: "none",
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let parms;
|
||||||
|
if (this.identity == 0) {
|
||||||
|
parms = {
|
||||||
|
user_syr_id: this.service.user_syr.id,
|
||||||
|
reserve_time: this.timeOptions[this.selectedDateIndex][startTimeIndex]
|
||||||
|
.time,
|
||||||
|
end_time: this.timeOptions[this.selectedDateIndex][endTimeIndex].time,
|
||||||
|
};
|
||||||
|
} else if (this.identity == 1) {
|
||||||
|
parms = {
|
||||||
|
work_seat_id: this.service.work_seat.id,
|
||||||
|
user_sj_id: this.service.user_sj.id,
|
||||||
|
reserve_time: this.timeOptions[this.selectedDateIndex][startTimeIndex]
|
||||||
|
.time,
|
||||||
|
end_time: this.timeOptions[this.selectedDateIndex][endTimeIndex].time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.url) {
|
||||||
|
request
|
||||||
|
.post(this.url, parms)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.code == 200) {
|
||||||
|
that.startTimeIndex = startTimeIndex;
|
||||||
|
that.endTimeIndex = endTimeIndex;
|
||||||
|
this.storageStatus();
|
||||||
|
that.handelGetTimeArr();
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res.msg,
|
||||||
|
icon: "none",
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '网络繁忙,稍后再试',
|
||||||
|
icon: "none",
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
console.log("报错", err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
that.startTimeIndex = startTimeIndex;
|
||||||
|
that.endTimeIndex = endTimeIndex;
|
||||||
|
this.storageStatus();
|
||||||
|
that.handelGetTimeArr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//存储状态
|
||||||
|
storageStatus() {
|
||||||
|
this.sureTimeDataIndex = this.selectedDateIndex;
|
||||||
|
if(this.startTimeIndex==null){
|
||||||
|
this.activeList = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.activeList = this.createRangeArray(
|
||||||
|
this.startTimeIndex,
|
||||||
|
this.endTimeIndex
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handelGetTimeArr() {
|
||||||
|
//获取时间段
|
||||||
|
let startTime =
|
||||||
|
this.timeOptions[this.selectedDateIndex][
|
||||||
|
this.startTimeIndex
|
||||||
|
].time.split(" ");
|
||||||
|
let data = {
|
||||||
|
reserve_start_time: this.timeOptions[this.selectedDateIndex][this.startTimeIndex].time,
|
||||||
|
reserve_end_time: this.timeOptions[this.selectedDateIndex][this.endTimeIndex].time,
|
||||||
|
startTime: this.timeOptions[this.selectedDateIndex][this.startTimeIndex]
|
||||||
|
.show_time,
|
||||||
|
endTime: this.timeOptions[this.selectedDateIndex][this.endTimeIndex].show_time,
|
||||||
|
date: startTime[0],
|
||||||
|
num: this.activeList.length - 1,
|
||||||
|
};
|
||||||
|
this.$emit("changeTime", data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 时间选择弹框 */
|
||||||
|
.time-popup {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx 30rpx 36rpx 30rpx;
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-content {
|
||||||
|
padding: 0rpx 30rpx 30rpx 30rpx;
|
||||||
|
|
||||||
|
&__datas {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&__date__item {
|
||||||
|
width: 214rpx;
|
||||||
|
height: 67rpx;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
position: relative;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: transparent;
|
||||||
|
background-position: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__box {
|
||||||
|
width: 214rpx;
|
||||||
|
height: 74rpx;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time__list {
|
||||||
|
flex: 1;
|
||||||
|
height: 400rpx;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni-scroll-view .uni-scroll-view::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .uni-scroll-view-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
-webkit-column-gap: 21rpx;
|
||||||
|
column-gap: 21rpx;
|
||||||
|
row-gap: 30rpx;
|
||||||
|
max-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 156rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #3c3638;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.__text {}
|
||||||
|
|
||||||
|
.__state {}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-available {
|
||||||
|
color: #3c3638;
|
||||||
|
background: #f7f8fa;
|
||||||
|
/* 可约 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-full {
|
||||||
|
color: #3c3638;
|
||||||
|
background: #e4e9f0;
|
||||||
|
/* 约满 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-rest {
|
||||||
|
color: #cdcdcd;
|
||||||
|
background: #f7f8f9;
|
||||||
|
/* 休息 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-locked {
|
||||||
|
color: #3c3638;
|
||||||
|
background: #e4e9f0;
|
||||||
|
/* 锁定 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-unavailable {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #e4e9f0;
|
||||||
|
/* 不可用 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-unknown {
|
||||||
|
/* 未知状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.isActive {
|
||||||
|
background: rgba(252, 67, 124, 0.08);
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 1rpx solid #E8101E;
|
||||||
|
color: #E8101E;
|
||||||
|
/* 选中状态 */
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item.active {
|
||||||
|
background-color: #fff0f0;
|
||||||
|
color: #ff3e3e;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item.noselected {
|
||||||
|
/* background-color: #FFF0F0; */
|
||||||
|
color: #999999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time__sure {
|
||||||
|
padding: 39rpx 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__box {
|
||||||
|
width: 648rpx;
|
||||||
|
height: 94rpx;
|
||||||
|
background: linear-gradient(140deg, #fa4e9a 0%, #E8101E 100%);
|
||||||
|
border-radius: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
<template>
|
||||||
|
<view class="service-code-modal" v-if="show" :style="{bottom:`${popBotom}px`}">
|
||||||
|
<view class="modal-mask" @touchmove.stop.prevent></view>
|
||||||
|
<view class="modal-content">
|
||||||
|
<view class="modal-title">开始服务</view>
|
||||||
|
<view class="modal-subtitle">请输入服务码</view>
|
||||||
|
<view class="code-box">
|
||||||
|
<view
|
||||||
|
v-for="(item, idx) in codeLength"
|
||||||
|
:key="idx"
|
||||||
|
:class="['code-cell', idx === currentIndex ? 'active' : '', code[idx] ? 'filled' : '']">
|
||||||
|
<text v-if="code[idx]">{{ code[idx] }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 隐藏真实输入框 -->
|
||||||
|
<input
|
||||||
|
:adjust-position="false"
|
||||||
|
class="real-input"
|
||||||
|
type="number"
|
||||||
|
:maxlength="codeLength"
|
||||||
|
v-model="inputValue"
|
||||||
|
:focus="true"
|
||||||
|
@input="onInput"
|
||||||
|
@focus="onFocus"
|
||||||
|
@keyboardheightchange="onkeyboardheightchange"
|
||||||
|
@blur="onBlur"
|
||||||
|
cursor-spacing="100"
|
||||||
|
ref="realInput" />
|
||||||
|
</view>
|
||||||
|
<button class="confirm-btn" :disabled="code.length < codeLength" @click="onConfirm">确定</button>
|
||||||
|
<view class="close-btn" @click="close">×</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'serviceCodeInput',
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
codeLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputValue: '',
|
||||||
|
isFocus: true,
|
||||||
|
keyboardHeight: 0,
|
||||||
|
popBotom:0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
code() {
|
||||||
|
return this.inputValue.split('').slice(0, this.codeLength)
|
||||||
|
},
|
||||||
|
currentIndex() {
|
||||||
|
return this.inputValue.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// show(val) {
|
||||||
|
// if (val) {
|
||||||
|
// this.$refs.realInput && this.$refs.realInput.focus
|
||||||
|
// } else {
|
||||||
|
// this.inputValue = ''
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// uni.onKeyboardHeightChange((res)=>{
|
||||||
|
// this.keyboardHeight = res.height;
|
||||||
|
// })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onInput(e) {
|
||||||
|
let val = e.detail.value.replace(/[^0-9]/g, '').slice(0, this.codeLength)
|
||||||
|
this.inputValue = val
|
||||||
|
this.$emit('input', val)
|
||||||
|
if (val.length === this.codeLength) {
|
||||||
|
this.$emit('finish', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFocus(e) {
|
||||||
|
this.isFocus = true
|
||||||
|
this.inputValue = ''
|
||||||
|
},
|
||||||
|
onkeyboardheightchange(e){
|
||||||
|
this.popBotom = e.target.height || 0
|
||||||
|
},
|
||||||
|
onBlur() {
|
||||||
|
this.isFocus = false
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
if (this.inputValue.length === this.codeLength) {
|
||||||
|
this.$emit('confirm', this.inputValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.service-code-modal {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999999;
|
||||||
|
|
||||||
|
.modal-mask {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
padding: 56rpx 40rpx 40rpx 40rpx;
|
||||||
|
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08);
|
||||||
|
min-height: 340rpx;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: modalUp 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #222;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subtitle {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #888;
|
||||||
|
margin: 24rpx 0 40rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.code-cell {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border: 2rpx solid #eee;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #ff4d8d;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 72rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #ff4d8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filled {
|
||||||
|
border-color: #ff4d8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background: #ff4d8d;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 88rpx;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #ffd1e3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 32rpx;
|
||||||
|
top: 32rpx;
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #bbb;
|
||||||
|
z-index: 10;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
<template>
|
||||||
|
<view class="service-grid">
|
||||||
|
<view class="service-item" v-for="(item, index) in services" :style="'width: '+width" :key="services.id"
|
||||||
|
@click="onServiceClick(item)">
|
||||||
|
<image :src="item.photo[0]" v-if="item.photo && item.photo.length != 0" mode="aspectFill"
|
||||||
|
class="service-image" lazy-load :style="'width: '+width"></image>
|
||||||
|
<text class="service-tag">{{item.server_kind == 1 ? '到家' : item.server_kind == 2 ? '到店' : ''}}</text>
|
||||||
|
<view class="service-info">
|
||||||
|
<view class="service-title">
|
||||||
|
<image class="service-title-type" mode="widthFix" src="@/static/images/shop/pintuan/group.png"
|
||||||
|
v-if="item.team_buy && Object.keys(item.team_buy).length != 0"></image>
|
||||||
|
<text class="service-title-text">
|
||||||
|
{{ item.title }}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<view class="service-row">
|
||||||
|
<view class="service-price" v-if="item.team_buy && Object.keys(item.team_buy).length != 0">
|
||||||
|
<text class="price-symbol">拼团价:¥</text>
|
||||||
|
<text class="price-value" :style="{'font-size': `${formatPriceSize(item.team_buy.price, 36, 5)}rpx`}">{{ item.team_buy.price }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-price" v-else>
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ item.server_price }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="service-sales">销量{{item.sales_num}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-shop" v-if="item.server_kind == 2">
|
||||||
|
<text class="shop-text text-overflow-1">{{item.shop_name}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ServiceGrid',
|
||||||
|
props: {
|
||||||
|
// 服务列表数据
|
||||||
|
services: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
// 是否显示多图布局
|
||||||
|
showMultiImages: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 是否显示找相似按钮
|
||||||
|
showSimilar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 上一页是nvue页面还是vue页面
|
||||||
|
pageType: {
|
||||||
|
type: String,
|
||||||
|
default: 'vue'
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '334rpx'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onServiceClick(service) {
|
||||||
|
if (this.pageType == 'nvue') {
|
||||||
|
console.log(this.pageType)
|
||||||
|
this.$emit('serviceclick', service)
|
||||||
|
} else {
|
||||||
|
this.$emit('service-click', service)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
formatPriceNumber(num) {
|
||||||
|
num = Number(num)
|
||||||
|
let formatted;
|
||||||
|
formatted = num.toFixed(2);
|
||||||
|
// 根据数字大小确定保留的小数位数
|
||||||
|
// if (num >= 100) {
|
||||||
|
// // 大于等于100时,不保留小数
|
||||||
|
// formatted = Math.floor(num).toString();
|
||||||
|
// } else if (num >= 10) {
|
||||||
|
// // 大于等于10时,保留1位小数
|
||||||
|
// formatted = num.toFixed(1);
|
||||||
|
// } else {
|
||||||
|
// // 其他情况,保留两位小数
|
||||||
|
// formatted = num.toFixed(2);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 处理小数最后一位是0的情况
|
||||||
|
// 如果包含小数点,则检查并移除末尾的0和可能的小数点
|
||||||
|
// if (formatted.includes(".")) {
|
||||||
|
// formatted = formatted.replace(/0+$/, "").replace(/\.$/, "");
|
||||||
|
// }
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
},
|
||||||
|
formatPriceSize(o_num, o_fontSize, type = 1) {
|
||||||
|
let fontSize = o_fontSize;
|
||||||
|
const len = this.formatPriceNumber(o_num).length;
|
||||||
|
//首页拼团立省价格
|
||||||
|
if (len > 7 && type == 1) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
//分享图立省价格
|
||||||
|
if (len > 7 && type == 2) {
|
||||||
|
fontSize -= 3;
|
||||||
|
}
|
||||||
|
//分享图拼团价格
|
||||||
|
if (type === 3) {
|
||||||
|
if (len > 4) {
|
||||||
|
fontSize -= 10;
|
||||||
|
}
|
||||||
|
if (len > 5) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
if (len > 6) {
|
||||||
|
fontSize -= 6;
|
||||||
|
}
|
||||||
|
if (len > 7) {
|
||||||
|
fontSize -= 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//分享图原价
|
||||||
|
if (type === 4) {
|
||||||
|
if (len > 5) {
|
||||||
|
fontSize -= 2;
|
||||||
|
}
|
||||||
|
if (len > 6) {
|
||||||
|
fontSize -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//首页商家
|
||||||
|
if (type === 5) {
|
||||||
|
if (len > 6) {
|
||||||
|
fontSize -= 6;
|
||||||
|
}
|
||||||
|
if (len > 7) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
if (len > 8) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontSize;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.service-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
/* grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 34rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
width: 334rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
box-shadow: 0 0 12rpx 0 rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-image {
|
||||||
|
width: 334rpx;
|
||||||
|
height: 334rpx;
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 0rpx;
|
||||||
|
left: 0rpx;
|
||||||
|
background-color: #E8101E;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
border-radius: 0 0 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.service-title-type {
|
||||||
|
width: 64rpx;
|
||||||
|
/* 图片与文字之间的间距 */
|
||||||
|
margin-right: 13rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
/* 显示的行数 */
|
||||||
|
lines: 1;
|
||||||
|
/* NVUE下要用这个属性,来让文字超出隐藏变省略号 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.service-row {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-sales {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-price {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3D3D3D;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
.service-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
background-color: #F8F8F8;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-images {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 320rpx;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 320rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #FF94B4;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
border-radius: 20rpx 0 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-similar {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-overflow {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* APP适配 */
|
||||||
|
/* #ifdef APP-PLUS */
|
||||||
|
.service-item {
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- 店铺列表 -->
|
||||||
|
<!-- @scrolltolower="loadMore"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="isRefreshing"
|
||||||
|
@refresherrefresh="onRefresh" -->
|
||||||
|
<scroll-view scroll-y class="shop-list">
|
||||||
|
<view class="shop-item" v-for="(shop, index) in shopList" @click.stop="goToArtisan($event,shop.id)"
|
||||||
|
:key="shop.id">
|
||||||
|
<!-- 店铺信息 -->
|
||||||
|
<view class="shop-header">
|
||||||
|
<view class="shop-title">
|
||||||
|
<view class="shop-title__left">
|
||||||
|
<image :src="shop.head_photo" class="head-photo" mode="aspectFill"></image>
|
||||||
|
<text class="title-text">{{shop.name}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="shop-distance" v-if="shop.distance>=0">
|
||||||
|
<text class="shop-distance__left">距您</text>
|
||||||
|
<text class="shop-distance__right">{{shop.distance.toFixed(2)}}km</text>
|
||||||
|
</view>
|
||||||
|
<!-- <image v-if="shop.verified" src="/static/images/verified.png" class="verified-icon" mode="aspectFit"></image> -->
|
||||||
|
</view>
|
||||||
|
<view class="shop-info">
|
||||||
|
<view class="shop-info-left">
|
||||||
|
<!-- <view class="rating">
|
||||||
|
<view class="rating-stars">
|
||||||
|
<image v-for="i in 5" :key="i" src="/static/images/star_y.png" class="star-icon" mode="aspectFit"></image>
|
||||||
|
</view>
|
||||||
|
<text class="rating-score">{{shop.rating}}</text>
|
||||||
|
</view> -->
|
||||||
|
<!-- <text class="divider">|</text> -->
|
||||||
|
<text class="shop-tags" v-for="(tag,i) in shop.servers_kill_arr">
|
||||||
|
{{tag}}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 服务列表 -->
|
||||||
|
<scroll-view scroll-x="true">
|
||||||
|
<view class="service-list">
|
||||||
|
<view class="service-item" v-for="(item, index) in shop.servers_list" :key="index"
|
||||||
|
@click.stop="onServiceClick($event,item)">
|
||||||
|
<image :src="item.photo[0]" mode="aspectFill" class="service-image" lazy-load></image>
|
||||||
|
<view class="service-info">
|
||||||
|
<view class="service-title">
|
||||||
|
<image
|
||||||
|
class="service-title-type"
|
||||||
|
mode="widthFix"
|
||||||
|
src="@/static/images/shop/pintuan/group.png"
|
||||||
|
v-if="item.team_buy && Object.keys(item.team_buy).length != 0"
|
||||||
|
></image>
|
||||||
|
<text class="service-title-text">
|
||||||
|
{{ item.title }}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<view class="service-price" v-if=" item.team_buy && Object.keys(item.team_buy).length != 0">
|
||||||
|
<text class="price-text">拼团价:</text>
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value" :style="{
|
||||||
|
'font-size': `${formatPriceSize(item.server_price, 30, 5)}rpx`,
|
||||||
|
}">{{ item.team_buy.price }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-price" v-else>
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ item.server_price }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "shop-list",
|
||||||
|
props: {
|
||||||
|
// 服务列表数据
|
||||||
|
shopList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
// 是否显示多图布局
|
||||||
|
isRefreshing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
// 手艺人或商家
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onServiceClick(e, service) {
|
||||||
|
console.log('service', service)
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
// #endif
|
||||||
|
this.$emit('service-click', service)
|
||||||
|
},
|
||||||
|
goToArtisan(e, id) {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
// #endif
|
||||||
|
this.$emit('go-artisan', id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.shop-item {
|
||||||
|
width: 750rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-header {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-title {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-photo {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
max-width: 400rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
/* 显示的行数 */
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-left {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-score {
|
||||||
|
color: #FF94B4;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-stars {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-icon {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
margin-right: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
color: #EEEEEE;
|
||||||
|
margin: 0 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-tags {
|
||||||
|
color: #999;
|
||||||
|
padding-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-distance {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999999;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
position: relative;
|
||||||
|
width: 180rpx;
|
||||||
|
/* height: 304rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-image {
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
background-color: #000000;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .service-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 0rpx;
|
||||||
|
left: 0rpx;
|
||||||
|
background-color: #FF94B4;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
border-radius: 20rpx 0 20rpx 0;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 28rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
/* 关键:使用flex布局让子元素同行显示 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.service-title-type {
|
||||||
|
width: 50rpx;
|
||||||
|
/* 图片与文字之间的间距 */
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title-text {
|
||||||
|
/* 关键:移除固定宽度,使用flex自动分配剩余空间 */
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-price {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-text{
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 33rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* H5适配 */
|
||||||
|
/* #ifdef H5 */
|
||||||
|
.nearby-page {
|
||||||
|
/* padding-top: 88rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
/* APP适配 */
|
||||||
|
/* #ifdef APP-PLUS */
|
||||||
|
.shop-item {
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
/* 小程序适配 */
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
/* .nearby-page {
|
||||||
|
padding-top: calc(88rpx + constant(safe-area-inset-top));
|
||||||
|
padding-top: calc(88rpx + env(safe-area-inset-top));
|
||||||
|
} */
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
<template>
|
||||||
|
<view class="step-tab">
|
||||||
|
<view class="step-container">
|
||||||
|
<!-- 步骤1: 入驻协议 -->
|
||||||
|
<view class="step-item" :class="getStepClass(0)">
|
||||||
|
<view class="step-icon">
|
||||||
|
<image
|
||||||
|
v-if="currentStep === 0"
|
||||||
|
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/55a51333-318f-4ae4-8402-aca265bd499d"
|
||||||
|
class="step-img active-circle"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<view v-else-if="currentStep > 0" class="step-done">1</view>
|
||||||
|
<view v-else class="step-number">1</view>
|
||||||
|
</view>
|
||||||
|
<text class="step-text" :class="{ active: currentStep === 0 }">入驻协议</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 第一个连接线 -->
|
||||||
|
<view class="line-segment line-1">
|
||||||
|
<view class="line-progress" :class="{ active: line1Active, deactivating: line1Deactivating }"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 步骤2: 根据身份显示不同信息 -->
|
||||||
|
<view class="step-item" :class="getStepClass(1)">
|
||||||
|
<view class="step-icon">
|
||||||
|
<image
|
||||||
|
v-if="currentStep === 1"
|
||||||
|
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/55a51333-318f-4ae4-8402-aca265bd499d"
|
||||||
|
class="step-img active-circle"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<view v-else-if="currentStep > 1" class="step-done">2</view>
|
||||||
|
<view v-else class="step-number">2</view>
|
||||||
|
</view>
|
||||||
|
<text class="step-text" :class="{ active: currentStep === 1 }">
|
||||||
|
{{ identity === 1 ? '个人信息' : '资质信息' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 第二个连接线 -->
|
||||||
|
<view class="line-segment line-2">
|
||||||
|
<view class="line-progress" :class="{ active: line2Active, deactivating: line2Deactivating }"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 步骤3: 根据身份显示不同信息 -->
|
||||||
|
<view class="step-item" :class="getStepClass(2)">
|
||||||
|
<view class="step-icon">
|
||||||
|
<image
|
||||||
|
v-if="currentStep === 2"
|
||||||
|
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/55a51333-318f-4ae4-8402-aca265bd499d"
|
||||||
|
class="step-img active-circle"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<view v-else-if="currentStep > 2" class="step-done">3</view>
|
||||||
|
<view v-else class="step-number">3</view>
|
||||||
|
</view>
|
||||||
|
<text class="step-text" :class="{ active: currentStep === 2 }">
|
||||||
|
{{ identity === 1 ? '资质信息' : '门店信息' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "StepTab",
|
||||||
|
props: {
|
||||||
|
// 当前步骤索引 (0, 1, 2)
|
||||||
|
currentStep: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
// 身份类型:1-手艺人,2-商家
|
||||||
|
identity: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
prevStep: 0, // 记录上一步骤
|
||||||
|
line1Active: false, // 第一条线激活状态
|
||||||
|
line2Active: false, // 第二条线激活状态
|
||||||
|
line1Deactivating: false, // 第一条线正在取消激活
|
||||||
|
line2Deactivating: false // 第二条线正在取消激活
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听currentStep变化,处理线条动画
|
||||||
|
currentStep(newVal, oldVal) {
|
||||||
|
this.prevStep = oldVal;
|
||||||
|
|
||||||
|
// 处理前进动画
|
||||||
|
if (newVal > oldVal) {
|
||||||
|
if (newVal >= 1) {
|
||||||
|
this.line1Active = true;
|
||||||
|
this.line1Deactivating = false;
|
||||||
|
}
|
||||||
|
if (newVal >= 2) {
|
||||||
|
this.line2Active = true;
|
||||||
|
this.line2Deactivating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理返回动画
|
||||||
|
else if (newVal < oldVal) {
|
||||||
|
if (newVal < 2) {
|
||||||
|
this.line2Deactivating = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.line2Active = false;
|
||||||
|
this.line2Deactivating = false;
|
||||||
|
}, 300); // 与动画持续时间匹配
|
||||||
|
}
|
||||||
|
if (newVal < 1) {
|
||||||
|
this.line1Deactivating = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.line1Active = false;
|
||||||
|
this.line1Deactivating = false;
|
||||||
|
}, 300); // 与动画持续时间匹配
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始化线条状态
|
||||||
|
this.line1Active = this.currentStep >= 1;
|
||||||
|
this.line2Active = this.currentStep >= 2;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 获取步骤的CSS类名
|
||||||
|
getStepClass(stepIndex) {
|
||||||
|
if (stepIndex === this.currentStep) {
|
||||||
|
return 'active';
|
||||||
|
}
|
||||||
|
else if (stepIndex < this.currentStep) {
|
||||||
|
return 'completed';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.step-tab {
|
||||||
|
height: 98rpx;
|
||||||
|
position: relative;
|
||||||
|
padding: 31rpx 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5rpx 60rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-icon {
|
||||||
|
width: 50rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-img.active-circle {
|
||||||
|
width: 50rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 50rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #F4F6FA;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999999;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-done {
|
||||||
|
width: 50rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #FAD0D3;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: inset 0 0 4rpx rgba(232, 16, 30, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-text.active {
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-item.completed .step-text {
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-segment {
|
||||||
|
position: relative;
|
||||||
|
height: 8rpx;
|
||||||
|
background-color: #F4F6FA;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
top: -25rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 126rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-1 {
|
||||||
|
margin-left: 20rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-2 {
|
||||||
|
margin-left: 20rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-progress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: #F4F6FA;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激活状态的线条 - 前进动画 */
|
||||||
|
.line-progress.active {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #E8101E;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 取消激活状态的线条 - 返回动画 */
|
||||||
|
.line-progress.deactivating {
|
||||||
|
width: 0%;
|
||||||
|
background-color: #E8101E;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
transform-origin: right center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.step-container {
|
||||||
|
/* padding: 5rpx 40rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-segment {
|
||||||
|
/* width: 100rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .line-1,
|
||||||
|
.line-2 {
|
||||||
|
margin-left: 15rpx;
|
||||||
|
margin-right: 15rpx;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<uv-tabbar fixed :value="selected" @change="change1" activeColor="#ee0a24" :placeholder="false"
|
||||||
|
:safeAreaInsetBottom="false" :customStyle="{height:`110rpx`}">
|
||||||
|
<uv-tabbar-item v-for="(item,index) in $store.state.tabbarList" :key="index" :badge="getNum(index)" :customStyle="{height:`110rpx`}">
|
||||||
|
<template v-slot:active-icon>
|
||||||
|
<image class="icon" :src="item.selectedIconPath"></image>
|
||||||
|
</template>
|
||||||
|
<template v-slot:inactive-icon>
|
||||||
|
<image class="icon" :src="item.iconPath"></image>
|
||||||
|
</template>
|
||||||
|
</uv-tabbar-item>
|
||||||
|
</uv-tabbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "@/utils/request";
|
||||||
|
export default {
|
||||||
|
name: "tab-bar",
|
||||||
|
props: {
|
||||||
|
selected: {
|
||||||
|
type: [Number, String]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tabbarList: [{
|
||||||
|
"iconPath": "/static/images/tabbar/new_home_gray.png",
|
||||||
|
"text": "首页",
|
||||||
|
"selectedIconPath": "/static/images/tabbar/new_home.png",
|
||||||
|
"pagePath": "/pages/home/home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iconPath": "/static/images/tabbar/new_order_gray.png",
|
||||||
|
"text": "订单",
|
||||||
|
"selectedIconPath": "/static/images/tabbar/new_order.png",
|
||||||
|
"pagePath": "/pages/order/userorder-list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iconPath": "/static/images/tabbar/new_message_gray.png",
|
||||||
|
"text": "消息",
|
||||||
|
"selectedIconPath": "/static/images/tabbar/new_message.png",
|
||||||
|
"pagePath": "/pages/message/message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iconPath": "/static/images/tabbar/new_my_gray.png",
|
||||||
|
"text": "我的",
|
||||||
|
"selectedIconPath": "/static/images/tabbar/new_my.png",
|
||||||
|
"pagePath": "/pages/my/my"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed:{
|
||||||
|
messageNum(){
|
||||||
|
return this.$store.state.messageNum || 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
if (!this.$store.state.tabbarList.length) {
|
||||||
|
let isnNetwork = uni.getStorageSync("isnNetwork");
|
||||||
|
if (isnNetwork) {
|
||||||
|
await this.getButtonImg()
|
||||||
|
} else {
|
||||||
|
uni.getNetworkType({
|
||||||
|
success: (res) => {
|
||||||
|
console.log(res.networkType);
|
||||||
|
if (res.networkType == "none") {
|
||||||
|
|
||||||
|
this.$store.commit("setTabbarList", this.tabbarList)
|
||||||
|
} else {
|
||||||
|
// 页面加载逻辑
|
||||||
|
uni.setStorageSync("isnNetwork", 1);
|
||||||
|
this.getButtonImg()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getNum(index){
|
||||||
|
if(index==2){
|
||||||
|
return this.messageNum
|
||||||
|
}else{
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//获取头部背景图片
|
||||||
|
async getButtonImg() {
|
||||||
|
let res = await request.post("/user/poster/getButtonImg", {});
|
||||||
|
if (res.code == 200) {
|
||||||
|
// if (res.data?.photo) {
|
||||||
|
// this.topBgImage = res.data?.photo + '?time=666'
|
||||||
|
// }
|
||||||
|
this.tabbarList.forEach((item, index) => {
|
||||||
|
if (res.data[index].iconPath) {
|
||||||
|
item.iconPath = res.data[index].iconPath
|
||||||
|
}
|
||||||
|
if (res.data[index].selectedIconPath) {
|
||||||
|
item.selectedIconPath = res.data[index].selectedIconPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.$store.commit("setTabbarList", this.tabbarList)
|
||||||
|
},
|
||||||
|
change1(val) {
|
||||||
|
// 因为在pages中配置了需要跳转的页面为 tabbar页面
|
||||||
|
// 所以不能使用navigateTo 只能使用下面这个方法跳转。
|
||||||
|
uni.switchTab({
|
||||||
|
url: this.tabbarList[val].pagePath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon {
|
||||||
|
height: 98rpx;
|
||||||
|
width: 98rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
<template>
|
||||||
|
<uni-popup ref="popup" type="bottom" border-radius="20rpx 20rpx 0 0" background-color="#fff" style="z-index: 20000">
|
||||||
|
<!-- 时间选择滚轮 -->
|
||||||
|
<view class="region-body">
|
||||||
|
<image class="region-body__close" @click="close"
|
||||||
|
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/693c54b5-f5a5-4aa0-8d7b-6123250f9839">
|
||||||
|
</image>
|
||||||
|
<view class="region-body__header"> 请选择时间段 </view>
|
||||||
|
<picker-view v-model="pickerValue" @change="handleTimeChange" class="picker-scroll"
|
||||||
|
:indicator-style="indicatorStyle" immediate-change>
|
||||||
|
<picker-view-column v-if="pickerShowList[0]">
|
||||||
|
<view class="picker-item" v-for="(year, index) in years" :key="index">
|
||||||
|
{{ year }}年
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column v-if="pickerShowList[1]">
|
||||||
|
<view class="picker-item" v-for="(month, index) in months" :key="index">
|
||||||
|
{{ month }}月
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column v-if="pickerShowList[2]">
|
||||||
|
<view class="picker-item" v-for="(date, index) in dates[pickerValue[0]][pickerValue[1]]"
|
||||||
|
:key="index">
|
||||||
|
{{ date }}日
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column v-if="pickerShowList[3]">
|
||||||
|
<view class="picker-item" v-for="(hour, index) in hours" :key="index">
|
||||||
|
{{ hour }}时
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column v-if="pickerShowList[4]">
|
||||||
|
<view class="picker-item" v-for="(minute, index) in minutes" :key="index">
|
||||||
|
{{ minute }}分
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
</picker-view>
|
||||||
|
<view class="region-body__btn" :class="{'noSure':timeCompareComputed}" @tap="confirmTime">确认
|
||||||
|
{{ `(${formatTime})` }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sureTime: null,
|
||||||
|
default_times: "",
|
||||||
|
business_time: "",
|
||||||
|
indicatorStyle: "height: 68rpx;",
|
||||||
|
pickerValue: [0, 0, 0, 0, 0],
|
||||||
|
pickerShowList: [1, 1, 1, 1, 1],
|
||||||
|
years: [],
|
||||||
|
months: Array.from({
|
||||||
|
length: 12
|
||||||
|
}, (_, i) => i + 1),
|
||||||
|
dates: [],
|
||||||
|
hours: Array.from({
|
||||||
|
length: 24
|
||||||
|
}, (_, i) => i),
|
||||||
|
minutes: Array.from({
|
||||||
|
length: 60
|
||||||
|
}, (_, i) => i),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: String,
|
||||||
|
timeIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
timeList: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'datetimerange', //datetimerange:年月日时分;daterange:年月日;yearMonth年月;
|
||||||
|
},
|
||||||
|
startYear:{
|
||||||
|
type: Number,
|
||||||
|
default: 1980,
|
||||||
|
},
|
||||||
|
endDistanceYear:{
|
||||||
|
type: Number,
|
||||||
|
default: 20,//结束时间,与当前时间点相距的时间
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
//初始化滑块显示行数
|
||||||
|
if (this.type == 'datetimerange') {
|
||||||
|
this.pickerShowList = [1, 1, 1, 1, 1]
|
||||||
|
this.pickerValue = [0, 0, 0, 0, 0]
|
||||||
|
} else if (this.type == 'daterange') {
|
||||||
|
this.pickerShowList = [1, 1, 1]
|
||||||
|
this.pickerValue = [0, 0, 0]
|
||||||
|
} else if (this.type == 'yearMonth') {
|
||||||
|
this.pickerShowList = [1, 1]
|
||||||
|
this.pickerValue = [0, 0]
|
||||||
|
}
|
||||||
|
//初始化时间数组
|
||||||
|
this.setTimesList();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
//获取当月最大天数
|
||||||
|
formatTime() {
|
||||||
|
if (this.type == 'datetimerange') {
|
||||||
|
// 将“/”替换为“-”,并保持时间补零逻辑
|
||||||
|
this.sureTime = `${this.years[this.pickerValue[0]]}-${this.months[
|
||||||
|
this.pickerValue[1]
|
||||||
|
]
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${this.dates[this.pickerValue[0]][
|
||||||
|
this.pickerValue[1]
|
||||||
|
][this.pickerValue[2]]
|
||||||
|
?.toString()
|
||||||
|
.padStart(2, "0")} ${this.hours[this.pickerValue[3]]
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:${this.minutes[this.pickerValue[4]]
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:00`;
|
||||||
|
} else if (this.type == 'daterange') {
|
||||||
|
this.sureTime = `${this.years[this.pickerValue[0]]}-${this.months[
|
||||||
|
this.pickerValue[1]
|
||||||
|
]
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${this.dates[this.pickerValue[0]][
|
||||||
|
this.pickerValue[1]
|
||||||
|
][this.pickerValue[2]]
|
||||||
|
?.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
} else if (this.type == 'yearMonth') {
|
||||||
|
this.sureTime = `${this.years[this.pickerValue[0]]}-${this.months[
|
||||||
|
this.pickerValue[1]
|
||||||
|
]
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
return this.sureTime;
|
||||||
|
},
|
||||||
|
timeCompareComputed() {
|
||||||
|
if ((this.timeIndex == 0 && this.timeList[1]) || (this.timeIndex == 1 && this.timeList[0]) && this
|
||||||
|
.formatTime) {
|
||||||
|
if (this.timeIndex == 0) {
|
||||||
|
// 开始时间
|
||||||
|
if (!this.timeCompare(this.formatTime, this.timeList[1])) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 结束时间
|
||||||
|
if (!this.timeCompare(this.timeList[0], this.formatTime)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.setPickerValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setTimesList() {
|
||||||
|
// 初始化时间数组
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const startYear = this.startYear; // 开始年份
|
||||||
|
const endYear = currentYear + this.endDistanceYear; // 结束年份
|
||||||
|
const yearCount = endYear - startYear + 1; // 总年数
|
||||||
|
|
||||||
|
for (let i = 0; i < yearCount; i++) {
|
||||||
|
const year = startYear + i;
|
||||||
|
this.years.push(year);
|
||||||
|
this.dates[i] = [];
|
||||||
|
for (let n = 1; n <= 12; n++) {
|
||||||
|
let num = this.getMonthDay(year, n); // 使用当前循环的年份计算天数
|
||||||
|
this.dates[i][n - 1] = Array.from({
|
||||||
|
length: num
|
||||||
|
},
|
||||||
|
(_, idx) => idx + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无value时设置默认当前时间
|
||||||
|
if (!this.value) {
|
||||||
|
this.setDefaultPickerValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增:设置默认当前时间的索引
|
||||||
|
setDefaultPickerValue() {
|
||||||
|
const now = new Date();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const currentMonth = now.getMonth() + 1; // 转换为1-12
|
||||||
|
const currentDate = now.getDate();
|
||||||
|
const currentHour = now.getHours();
|
||||||
|
const currentMinute = now.getMinutes();
|
||||||
|
|
||||||
|
// 计算各维度索引
|
||||||
|
const yearIndex = this.years.findIndex(y => y === currentYear);
|
||||||
|
const monthIndex = this.months.findIndex(m => m === currentMonth);
|
||||||
|
const dayIndex = this.dates[yearIndex]?.[monthIndex]?.findIndex(d => d === currentDate) ?? 0;
|
||||||
|
const hourIndex = this.hours.findIndex(h => h === currentHour);
|
||||||
|
const minuteIndex = this.minutes.findIndex(m => m === currentMinute);
|
||||||
|
|
||||||
|
this.pickerValue = [yearIndex, monthIndex, dayIndex, hourIndex, minuteIndex];
|
||||||
|
},
|
||||||
|
|
||||||
|
setPickerValue() {
|
||||||
|
if (this.value) {
|
||||||
|
// 统一将输入的日期字符串中的“-”替换为“/”,兼容解析
|
||||||
|
const normalizedValue = this.value.replace(/-/g, '/');
|
||||||
|
|
||||||
|
if (this.type == 'datetimerange') {
|
||||||
|
// 解析时间字符串 '2024/05/27 11:31:00'
|
||||||
|
const [datePart, timePart] = normalizedValue.split(" ");
|
||||||
|
const [year, month, day] = datePart.split("/").map(Number);
|
||||||
|
// 时间部分用 : 分隔,取前两位(小时和分钟),忽略秒数
|
||||||
|
const [hour, minute] = timePart.split(":").map(Number);
|
||||||
|
|
||||||
|
// 计算各部分在数组中的索引
|
||||||
|
const yearIndex = this.years.findIndex((y) => y === year);
|
||||||
|
const monthIndex = this.months.findIndex((m) => m === month);
|
||||||
|
const dayIndex = this.dates[yearIndex]?.[monthIndex]?.findIndex((d) => d === day) ?? 0;
|
||||||
|
const hourIndex = this.hours.findIndex((h) => h === hour);
|
||||||
|
const minuteIndex = this.minutes.findIndex((m) => m === minute);
|
||||||
|
|
||||||
|
// 设置pickerValue
|
||||||
|
this.pickerValue = [
|
||||||
|
yearIndex,
|
||||||
|
monthIndex,
|
||||||
|
dayIndex,
|
||||||
|
hourIndex,
|
||||||
|
minuteIndex,
|
||||||
|
];
|
||||||
|
} else if (this.type == 'daterange') {
|
||||||
|
// 解析时间字符串 '2024/05/27'
|
||||||
|
const [year, month, day] = normalizedValue.split("/").map(Number);
|
||||||
|
|
||||||
|
// 计算各部分在数组中的索引
|
||||||
|
const yearIndex = this.years.findIndex((y) => y === year);
|
||||||
|
const monthIndex = this.months.findIndex((m) => m === month);
|
||||||
|
const dayIndex = this.dates[yearIndex]?.[monthIndex]?.findIndex((d) => d === day) ?? 0;
|
||||||
|
|
||||||
|
// 设置pickerValue
|
||||||
|
this.pickerValue = [
|
||||||
|
yearIndex,
|
||||||
|
monthIndex,
|
||||||
|
dayIndex,
|
||||||
|
];
|
||||||
|
} else if (this.type == 'yearMonth') {
|
||||||
|
// 解析时间字符串 '2024/05'
|
||||||
|
const [year, month] = normalizedValue.split("/").map(Number);
|
||||||
|
|
||||||
|
// 计算各部分在数组中的索引
|
||||||
|
const yearIndex = this.years.findIndex((y) => y === year);
|
||||||
|
const monthIndex = this.months.findIndex((m) => m === month);
|
||||||
|
|
||||||
|
// 设置pickerValue
|
||||||
|
this.pickerValue = [
|
||||||
|
yearIndex,
|
||||||
|
monthIndex
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getMonthDay(year, month) {
|
||||||
|
let days = new Date(year, month, 0).getDate();
|
||||||
|
return days;
|
||||||
|
},
|
||||||
|
// 处理时间变化
|
||||||
|
handleTimeChange(e) {
|
||||||
|
this.pickerValue = e.detail.value;
|
||||||
|
},
|
||||||
|
openTimePopup() {
|
||||||
|
this.$refs.popup.open();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$refs.popup.close();
|
||||||
|
},
|
||||||
|
// 确认选择
|
||||||
|
confirmTime() {
|
||||||
|
if ((this.timeIndex == 0 && this.timeList[1]) || (this.timeIndex == 1 && this.timeList[0])) {
|
||||||
|
if (this.timeIndex == 0) {
|
||||||
|
// 开始时间
|
||||||
|
if (!this.timeCompare(this.formatTime, this.timeList[1])) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "开始时间不能大于结束时间",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 结束时间
|
||||||
|
if (!this.timeCompare(this.timeList[0], this.formatTime)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "结束时间不能小于开始时间",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$emit("input", this.sureTime);
|
||||||
|
this.$emit("confirmTime", this.sureTime);
|
||||||
|
this.$refs.popup.close();
|
||||||
|
},
|
||||||
|
//比较开始时间结束时间大小
|
||||||
|
timeCompare(dateStr1, dateStr2) {
|
||||||
|
if (this.timeIndex >= 0) {
|
||||||
|
// 处理日期格式:将“-”替换为“/”,纯日期补充为当天0点
|
||||||
|
const formatDate = (str) => {
|
||||||
|
// 先将所有“-”替换为“/”,兼容iOS解析
|
||||||
|
const normalized = str.replace(/-/g, '/');
|
||||||
|
// 判断是否包含时间部分(是否有空格)
|
||||||
|
if (!normalized.includes(' ')) {
|
||||||
|
return normalized + ' 00:00:00'; // 纯日期补全为当天0点
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 格式化并转换为时间戳
|
||||||
|
const time1 = new Date(formatDate(dateStr1)).getTime();
|
||||||
|
const time2 = new Date(formatDate(dateStr2)).getTime();
|
||||||
|
// 时间戳比较(避免字符串比较的潜在问题)
|
||||||
|
return time2 > time1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('日期格式错误:', error);
|
||||||
|
return false; // 格式错误时返回默认值
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 样式部分不变 */
|
||||||
|
.region-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx 20rpx 0 0;
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
width: 22rpx;
|
||||||
|
height: 22rpx;
|
||||||
|
position: absolute;
|
||||||
|
top: 44rpx;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: 40rpx 0;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 45rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 690rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #E8101E;
|
||||||
|
border-radius: 38rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 39rpx;
|
||||||
|
text-align: right;
|
||||||
|
font-style: normal;
|
||||||
|
margin: 0 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noSure {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-scroll {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-scroll {
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
height: 68rpx;
|
||||||
|
line-height: 68rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-item.checkbox-item {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
<template>
|
||||||
|
<uni-popup
|
||||||
|
ref="popup"
|
||||||
|
type="bottom"
|
||||||
|
border-radius="20rpx"
|
||||||
|
style="z-index: 20000"
|
||||||
|
>
|
||||||
|
<!-- 时间选择滚轮 -->
|
||||||
|
<view class="region-body">
|
||||||
|
<image class="region-body__close" @click="close" src="/static/images/shop/close.png"></image>
|
||||||
|
<view class="region-body__header"> 请选择时间段 </view>
|
||||||
|
<picker-view
|
||||||
|
:value="pickerValue"
|
||||||
|
@change="handleTimeChange"
|
||||||
|
class="picker-scroll"
|
||||||
|
:indicator-style="indicatorStyle"
|
||||||
|
>
|
||||||
|
<picker-view-column>
|
||||||
|
<view class="picker-item" v-for="(hour, index) in hours" :key="index">
|
||||||
|
{{ hour }}时
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column>
|
||||||
|
<view
|
||||||
|
class="picker-item"
|
||||||
|
v-for="(minute, index) in minutes"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ minute }}分
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column>
|
||||||
|
<view class="picker-item"> - </view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column>
|
||||||
|
<view class="picker-item" v-for="(hour, index) in hours" :key="index">
|
||||||
|
{{ hour }}时
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
<picker-view-column>
|
||||||
|
<view
|
||||||
|
class="picker-item"
|
||||||
|
v-for="(minute, index) in minutes"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ minute }}分
|
||||||
|
</view>
|
||||||
|
</picker-view-column>
|
||||||
|
</picker-view>
|
||||||
|
<view class="region-body__btn" @tap="confirmTime"
|
||||||
|
>确认
|
||||||
|
{{
|
||||||
|
`(${this.formatTime(this.startTime)}-${this.formatTime(
|
||||||
|
this.endTime
|
||||||
|
)})`
|
||||||
|
}}</view
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
default_times: "",
|
||||||
|
business_time: "",
|
||||||
|
indicatorStyle: "height: 68rpx;",
|
||||||
|
pickerValue: [8, 0, 0, 18, 0],
|
||||||
|
// 小时数组直接用数字字符串,确保parseInt转换无歧义
|
||||||
|
hours: Array.from({ length: 24 }, (_, i) => `${i}`),
|
||||||
|
// 分钟数组使用数字字符串,避免格式问题
|
||||||
|
minutes: ["0", "10", "20", "30", "40", "50", "59"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
startTime() {
|
||||||
|
return {
|
||||||
|
hour: this.hours[this.pickerValue[0]],
|
||||||
|
minute: this.minutes[this.pickerValue[1]],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
endTime() {
|
||||||
|
return {
|
||||||
|
hour: this.hours[this.pickerValue[3]],
|
||||||
|
minute: this.minutes[this.pickerValue[4]],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 处理时间变化,确保开始时间小于结束时间
|
||||||
|
handleTimeChange(e) {
|
||||||
|
const values = e.detail.value;
|
||||||
|
// 转换为数值计算(避免字符串比较问题)
|
||||||
|
const startHour = parseInt(this.hours[values[0]], 10);
|
||||||
|
const startMinute = parseInt(this.minutes[values[1]], 10);
|
||||||
|
const endHour = parseInt(this.hours[values[3]], 10);
|
||||||
|
const endMinute = parseInt(this.minutes[values[4]], 10);
|
||||||
|
|
||||||
|
// 计算总分钟数进行比较
|
||||||
|
const startTotalMinutes = startHour * 60 + startMinute;
|
||||||
|
const endTotalMinutes = endHour * 60 + endMinute;
|
||||||
|
|
||||||
|
if (startTotalMinutes >= endTotalMinutes) {
|
||||||
|
// 自动调整结束时间为开始时间后1小时(处理跨天情况)
|
||||||
|
let newEndHour = startHour + 1;
|
||||||
|
if (newEndHour >= 24) newEndHour = 0; // 超过24点则重置为0点
|
||||||
|
// 找到新结束小时在数组中的索引
|
||||||
|
const newEndHourIndex = this.hours.findIndex(h => parseInt(h, 10) === newEndHour);
|
||||||
|
// 保持分钟与开始时间一致
|
||||||
|
values[3] = newEndHourIndex;
|
||||||
|
values[4] = values[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pickerValue = values;
|
||||||
|
},
|
||||||
|
openTimePopup() {
|
||||||
|
this.$refs.popup.open();
|
||||||
|
},
|
||||||
|
// 格式化时间显示(保持":""分隔,iOS兼容)
|
||||||
|
formatTime(time) {
|
||||||
|
// 确保分钟为两位数显示(可选,根据需求调整)
|
||||||
|
const minute = time.minute.padStart(2, '0');
|
||||||
|
return `${time.hour}:${minute}`;
|
||||||
|
},
|
||||||
|
close(){
|
||||||
|
this.$refs.popup.close();
|
||||||
|
},
|
||||||
|
// 确认选择,发射标准化的时间字符串
|
||||||
|
confirmTime() {
|
||||||
|
const startTimeStr = this.formatTime(this.startTime);
|
||||||
|
const endTimeStr = this.formatTime(this.endTime);
|
||||||
|
this.$emit("confirmTime", `${startTimeStr}-${endTimeStr}`);
|
||||||
|
this.$refs.popup.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 样式部分不变 */
|
||||||
|
.region-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx 20rpx 0 0;
|
||||||
|
&__close{
|
||||||
|
width: 22rpx;
|
||||||
|
height: 22rpx;
|
||||||
|
position: absolute;
|
||||||
|
top: 44rpx;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
&__header {
|
||||||
|
padding: 40rpx 0;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 45rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
&__btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 690rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #E8101E;
|
||||||
|
border-radius: 38rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 39rpx;
|
||||||
|
text-align: right;
|
||||||
|
font-style: normal;
|
||||||
|
margin: 0 30rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-scroll {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.picker-scroll {
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
.picker-item {
|
||||||
|
height: 68rpx;
|
||||||
|
line-height: 68rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-item.checkbox-item {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<view class="tips-popup" v-if="show">
|
||||||
|
<view class="mask" @click="handleClose"></view>
|
||||||
|
<view class="popup-content">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">提现规则</text>
|
||||||
|
<view class="close-btn" @click="handleClose">
|
||||||
|
<image src="/static/images/close.png" mode="aspectFit" class="close-icon"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="popup-body">
|
||||||
|
<view class="rule-item">
|
||||||
|
<text class="rule-text">发起提现申请,后台审核通过,线下进行打款</text>
|
||||||
|
</view>
|
||||||
|
<view class="rule-item">
|
||||||
|
<text class="rule-text">请您确认绑定的支付宝、微信号的准确性。</text>
|
||||||
|
</view>
|
||||||
|
<view class="rule-item">
|
||||||
|
<text class="rule-text">后台将按照您绑定的提现账户进行进行转账。</text>
|
||||||
|
</view>
|
||||||
|
<view class="rule-item">
|
||||||
|
<text class="rule-text">如因提交的信息有误导致的转账失败或转错账户,将由用户自行承担,平台不承担任何责任。</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'tips-popup',
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('close-popup', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tips-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 600rpx;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
position: relative;
|
||||||
|
height: 112rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body {
|
||||||
|
padding: 32rpx 40rpx 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<template>
|
||||||
|
<view class="tips-popup" v-if="show">
|
||||||
|
<view class="mask"></view>
|
||||||
|
<view class="popup-content">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">{{title}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="popup-body">
|
||||||
|
<view class="rule-item">
|
||||||
|
<view class="rule-text"
|
||||||
|
> <slot> </slot></view
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="btn_box">
|
||||||
|
<view @click="goAdd" class="btn">{{ sureText }}</view>
|
||||||
|
<view @click="handleClose" class="btn">{{ closeText }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "tips-popup",
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
sureText:{
|
||||||
|
type: String,
|
||||||
|
default: '知道了',
|
||||||
|
},
|
||||||
|
closeText:{
|
||||||
|
type: String,
|
||||||
|
default: '取消',
|
||||||
|
},
|
||||||
|
title:{
|
||||||
|
type: String,
|
||||||
|
default: '温馨提示',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.$emit("closePopup", false);
|
||||||
|
},
|
||||||
|
goAdd() {
|
||||||
|
this.$emit("closePopup", false);
|
||||||
|
this.$emit("okBtn", false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tips-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
background-image: url("/static/images/icons/tip_bg.png");
|
||||||
|
background-position: top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 570rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 60rpx 47rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 34rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 48rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body {
|
||||||
|
padding: 36rpx 0rpx 50rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.btn_box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 72rpx;
|
||||||
|
.btn {
|
||||||
|
width: 200rpx;
|
||||||
|
border-radius: 72rpx;
|
||||||
|
border: 2rpx solid #979797;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,437 @@
|
||||||
|
<template>
|
||||||
|
<uni-popup
|
||||||
|
ref="popup"
|
||||||
|
type="bottom"
|
||||||
|
border-radius="20rpx"
|
||||||
|
style="z-index: 99999"
|
||||||
|
:is-mask-click="false"
|
||||||
|
>
|
||||||
|
<view class="upload-app">
|
||||||
|
<image
|
||||||
|
src="/static/images/upload/bj.png"
|
||||||
|
class="upload-app__bj"
|
||||||
|
mode="aspectFit"
|
||||||
|
></image>
|
||||||
|
<view class="upload-app__main">
|
||||||
|
<view class="upload-app__main__title">发现新版本</view>
|
||||||
|
<view class="upload-app__main__version">
|
||||||
|
<text class="upload-app__main__version__text"
|
||||||
|
>v{{ newVersion }}已发布,推荐更新</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="upload-app__main__content__title"
|
||||||
|
v-if="this.apkUpdateContent"
|
||||||
|
>更新内容</view
|
||||||
|
>
|
||||||
|
<view class="upload-app__main__contents">
|
||||||
|
<rich-text
|
||||||
|
:nodes="this.apkUpdateContent"
|
||||||
|
v-if="this.apkUpdateContent"
|
||||||
|
></rich-text>
|
||||||
|
<!-- <view class="upload-app__main__contents__content"
|
||||||
|
>1.修复多处稳定性问题,提升了应用的流程</view
|
||||||
|
>
|
||||||
|
<view class="upload-app__main__contents__content"
|
||||||
|
>2.修复多处稳定性问题,提升了应用的流程</view
|
||||||
|
> -->
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="upload-app__main__buttons"
|
||||||
|
v-if="this.is_force == 0 && !this.startUpload"
|
||||||
|
>
|
||||||
|
<view class="upload-app__main__buttons__Later">
|
||||||
|
<text
|
||||||
|
class="upload-app__main__buttons__Later__text"
|
||||||
|
@click="Updatelater"
|
||||||
|
>稍后更新</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view class="upload-app__main__buttons__immediately">
|
||||||
|
<text
|
||||||
|
class="upload-app__main__buttons__immediately__text"
|
||||||
|
@click="UpdateNow"
|
||||||
|
>立即更新</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="upload-app__main__buttons"
|
||||||
|
v-else-if="this.is_force == 1 && !this.startUpload"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="upload-app__main__buttons__immediately Mandatory"
|
||||||
|
@click="UpdateNow"
|
||||||
|
>
|
||||||
|
<text class="upload-app__main__buttons__immediately__text"
|
||||||
|
>立即更新</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="upload-app__main__Progress" v-if="this.startUpload">
|
||||||
|
<view class="upload-app__main__Progress__bar">
|
||||||
|
<view
|
||||||
|
class="upload-app__main__Progress__bar__active"
|
||||||
|
:style="{ width: 495 * (updateProgress / 100) + 'rpx' }"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view class="upload-app__main__Progress__bar__text">
|
||||||
|
{{ updateProgress }}%
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="upload-app__main__Progress__hint">
|
||||||
|
新版本正在努力更新中,请耐心等待
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from "../../utils/request";
|
||||||
|
import uniPopup from "@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue";
|
||||||
|
export default {
|
||||||
|
name: "uploadAppVue",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
version: null, //现在版本
|
||||||
|
updateProgress: 0, //下载进度
|
||||||
|
newVersion: null, //最新版本
|
||||||
|
updateAPKPath: "", //更新地址
|
||||||
|
is_force: 0, //是否强更,0为否1为是
|
||||||
|
startUpload: false, //是否开启跟新
|
||||||
|
down_type: null, //1是跳转,2是下载
|
||||||
|
apkUpdateContent: "", //更新内容说明
|
||||||
|
|
||||||
|
updateType: "", //1资源包更新2版本更新
|
||||||
|
versionsFlag: 1, //1需要更新弹窗2已是最新版本弹窗3正在下载
|
||||||
|
latestVersionInfo: "", //已是最新版本弹窗内容
|
||||||
|
|
||||||
|
restartFlag: false, // 是否需要重启APP
|
||||||
|
show: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isHome: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {},
|
||||||
|
mounted() {
|
||||||
|
if (this.isHome) {
|
||||||
|
this.uploadApp();
|
||||||
|
}
|
||||||
|
// this.$refs.popup.open("center");
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
Updatelater() {
|
||||||
|
let timestamp = Date.now();
|
||||||
|
uni.setStorageSync("updatelaterTime", timestamp);
|
||||||
|
this.$refs.popup.close();
|
||||||
|
// uni.showTabBar();
|
||||||
|
},
|
||||||
|
UpdateNow() {
|
||||||
|
if (this.down_type == 1) {
|
||||||
|
plus.runtime.openURL(this.updateAPKPath);
|
||||||
|
} else if (this.down_type == 2) {
|
||||||
|
this.startUpload = true;
|
||||||
|
this.uploadAppDowlond();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// app内跟新下载
|
||||||
|
uploadAppDowlond() {
|
||||||
|
const downloadTask = uni.downloadFile({
|
||||||
|
url: this.updateAPKPath,
|
||||||
|
success: (downloadResult) => {
|
||||||
|
if (downloadResult.statusCode === 200) {
|
||||||
|
plus.runtime.install(
|
||||||
|
downloadResult.tempFilePath,
|
||||||
|
{
|
||||||
|
force: false,
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
// 下载资源成功,重启应用
|
||||||
|
plus.runtime.restart();
|
||||||
|
},
|
||||||
|
function (e) {
|
||||||
|
// 下载资源失败
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadTask.onProgressUpdate((res) => {
|
||||||
|
this.updateProgress = res.progress;
|
||||||
|
console.log("下载进度" + res.progress);
|
||||||
|
console.log("已经下载的数据长度" + res.totalBytesWritten);
|
||||||
|
console.log("预期需要下载的数据总长度" + res.totalBytesExpectedToWrite);
|
||||||
|
|
||||||
|
// 满足测试条件,取消下载任务。
|
||||||
|
// if (res.progress > 50) {
|
||||||
|
// downloadTask.abort();
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async uploadApp(type) {
|
||||||
|
// uploadAppDowlond()
|
||||||
|
try {
|
||||||
|
await this.getVersionNum();
|
||||||
|
if (!this.version) return;
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
await this.getUpdateInfo();
|
||||||
|
let timestamp = Date.now();
|
||||||
|
let updatelaterTime = uni.getStorageSync("updatelaterTime") || 0;
|
||||||
|
if (
|
||||||
|
Math.abs(timestamp - updatelaterTime) < 86400000 &&
|
||||||
|
this.is_force != 1 &&
|
||||||
|
type != "setting"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newVersion) {
|
||||||
|
if (type == "setting") {
|
||||||
|
uni.showToast({
|
||||||
|
title: "当前已是最新版本",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.hideTabBar();
|
||||||
|
this.$refs.popup.open("center");
|
||||||
|
}
|
||||||
|
// if (this.compareVersion(this.version, this.newVersion)) {
|
||||||
|
// uni.hideTabBar();
|
||||||
|
// this.$refs.popup.open("center");
|
||||||
|
// } else {
|
||||||
|
// uni.showToast({
|
||||||
|
// title: "当前已是最新版本",
|
||||||
|
// icon: "none",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// #endif
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查更新失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getVersionNum() {
|
||||||
|
// 获取当前app的版本
|
||||||
|
const systemInfo = uni.getSystemInfoSync();
|
||||||
|
// 应用程序版本号
|
||||||
|
// 条件编译,只在APP渲染
|
||||||
|
// #ifdef APP
|
||||||
|
this.version = systemInfo.appWgtVersion;
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
this.version = systemInfo.appVersion;
|
||||||
|
console.log(systemInfo.appVersion, "版本号");
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const accountInfo = uni.getAccountInfoSync();
|
||||||
|
this.version = accountInfo.miniProgram.version; // 小程序 版本号
|
||||||
|
console.log(accountInfo.miniProgram.version, "小程序版本号");
|
||||||
|
// #endif
|
||||||
|
return this.version;
|
||||||
|
},
|
||||||
|
async getUpdateInfo() {
|
||||||
|
const response = await request.post("/user/getappversion");
|
||||||
|
this.newVersion = response.data?.version;
|
||||||
|
this.updateAPKPath = response.data?.url;
|
||||||
|
this.is_force = response.data?.is_force;
|
||||||
|
this.down_type = response.data?.down_type;
|
||||||
|
this.apkUpdateContent = response.data?.explain;
|
||||||
|
},
|
||||||
|
compareVersion(current, latest) {
|
||||||
|
const curParts = current.split(".").map(Number);
|
||||||
|
const latParts = latest.split(".").map(Number);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(curParts.length, latParts.length); i++) {
|
||||||
|
const cur = curParts[i] || 0;
|
||||||
|
const lat = latParts[i] || 0;
|
||||||
|
if (cur < lat) return true;
|
||||||
|
if (cur > lat) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.popupShow {
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-app {
|
||||||
|
width: 568rpx;
|
||||||
|
background-color: #ffff;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
|
||||||
|
&__bj {
|
||||||
|
width: 568rpx;
|
||||||
|
height: 317rpx;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: -23rpx;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 46rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 65rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
padding: 40rpx 38rpx 14rpx 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__version {
|
||||||
|
margin-left: 38rpx;
|
||||||
|
width: auto;
|
||||||
|
display: inline;
|
||||||
|
// height: 33rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
background: linear-gradient(136deg, #fed7f5 0%, #fbdce5 100%);
|
||||||
|
border-radius: 0rpx 10rpx 0rpx 10rpx;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #E8101E;
|
||||||
|
line-height: 25rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content__title {
|
||||||
|
padding-left: 39rpx;
|
||||||
|
padding-top: 38rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__contents {
|
||||||
|
padding: 20rpx 40rpx 40rpx 40rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 14rpx;
|
||||||
|
|
||||||
|
::v-deep p {
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5e5e60;
|
||||||
|
line-height: 37rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
padding: 0rpx 40rpx 50rpx 40rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__Later {
|
||||||
|
width: 236rpx;
|
||||||
|
height: 78rpx;
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-radius: 39rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__immediately {
|
||||||
|
width: 236rpx;
|
||||||
|
height: 78rpx;
|
||||||
|
background: #E8101E;
|
||||||
|
border-radius: 39rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__immediately.Mandatory {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Progress {
|
||||||
|
padding: 10rpx 40rpx 40rpx 40rpx;
|
||||||
|
|
||||||
|
&__bar {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
width: 495rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
background: #f0f1f3;
|
||||||
|
border-radius: 39rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__active {
|
||||||
|
height: 28rpx;
|
||||||
|
background: linear-gradient(270deg, #fe78b3 0%, #E8101E 100%);
|
||||||
|
border-radius: 39rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333333;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #a5a6ad;
|
||||||
|
line-height: 28rpx;
|
||||||
|
text-align: left;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
<template>
|
||||||
|
<view class="upload-content" :style="'width:'+width">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in photo"
|
||||||
|
:key="index"
|
||||||
|
class="image-preview"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:src="item"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="preview-image"
|
||||||
|
@click="previewImage(photo,index)"
|
||||||
|
></image>
|
||||||
|
<view class="delete-btn" @click="deleteImage(index)">×</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="photo.length < maxNum"
|
||||||
|
class="upload-btn"
|
||||||
|
@click="chooseImage"
|
||||||
|
>
|
||||||
|
<text class="plus-icon">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.18.0.min.js"></script>
|
||||||
|
<script>
|
||||||
|
import request from '../../utils/request';
|
||||||
|
export default {
|
||||||
|
name:"upload-img",
|
||||||
|
props: {
|
||||||
|
photo: {
|
||||||
|
type: Array,
|
||||||
|
default: []
|
||||||
|
},
|
||||||
|
maxNum: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imagesList: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
chooseImage() {
|
||||||
|
let that = this;
|
||||||
|
uni.chooseImage({
|
||||||
|
count: that.maxNum - that.photo.length,
|
||||||
|
sizeType: ['original', 'compressed'],
|
||||||
|
sourceType: ['album'],
|
||||||
|
success: (res) => {
|
||||||
|
// 处理文件大小限制
|
||||||
|
const validFiles = res.tempFiles.filter(file => {
|
||||||
|
const isValidSize = file.size < 10 * 1024 * 1024 // 10MB限制
|
||||||
|
if (!isValidSize) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '图片大小不能超过10MB',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return isValidSize
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加到媒体列表
|
||||||
|
const newImages = validFiles.map(file => ({
|
||||||
|
type: 'image',
|
||||||
|
url: file.path,
|
||||||
|
size: file.size
|
||||||
|
}))
|
||||||
|
console.log('newImages',newImages)
|
||||||
|
that.imagesList = newImages;
|
||||||
|
that.uploadPhoto();
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 上传图片接口
|
||||||
|
async uploadPhoto() {
|
||||||
|
let that = this;
|
||||||
|
// 刷新token
|
||||||
|
const newToken = await request.post('/user/refreshtoken',{refresh_token: uni.getStorageSync('refreshToken'),deviceid: getApp().globalData.deviceid})
|
||||||
|
// 2. 保存到本地
|
||||||
|
uni.setStorageSync('accessToken',newToken.access_token)
|
||||||
|
uni.setStorageSync('refreshToken',newToken.refresh_token)
|
||||||
|
|
||||||
|
const promises = that.imagesList.map(item => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.uploadImage('/user/uploadphoto',item.url,{type: item.type}).then(res=>{
|
||||||
|
console.log('res==========',res)
|
||||||
|
resolve(res)
|
||||||
|
}).catch(err=>{
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// const client = new OSS({
|
||||||
|
// // yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。
|
||||||
|
// region: "oss-cn-changzhou",
|
||||||
|
// authorizationV4: true,
|
||||||
|
// // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
||||||
|
// accessKeyId: "LTAI5tGsiEK2Zuat4bJSHXs9",
|
||||||
|
// accessKeySecret: "x9ToLlfdcgg2sGnyfVpQWGVLfEdG3E",
|
||||||
|
// // 从STS服务获取的安全令牌(SecurityToken)。
|
||||||
|
// stsToken: "yourSecurityToken",
|
||||||
|
// // 填写Bucket名称。
|
||||||
|
// bucket: "mrrplus",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // 从输入框获取file对象,例如<input type="file" id="file" />。
|
||||||
|
// let data;
|
||||||
|
// // 创建并填写Blob数据。
|
||||||
|
// //const data = new Blob(['Hello OSS']);
|
||||||
|
// // 创建并填写OSS Buffer内容。
|
||||||
|
// //const data = new OSS.Buffer(['Hello OSS']);
|
||||||
|
|
||||||
|
// const upload = document.getElementById("upload");
|
||||||
|
|
||||||
|
// async function putObject(data) {
|
||||||
|
// try {
|
||||||
|
// // 填写Object完整路径。Object完整路径中不能包含Bucket名称。
|
||||||
|
// // 您可以通过自定义文件名(例如exampleobject.txt)或文件完整路径(例如exampledir/exampleobject.txt)的形式实现将数据上传到当前Bucket或Bucket中的指定目录。
|
||||||
|
// // data对象可以自定义为file对象、Blob数据或者OSS Buffer。
|
||||||
|
// const options = {
|
||||||
|
// meta: { temp: "demo" },
|
||||||
|
// mime: "json",
|
||||||
|
// headers: { "Content-Type": "text/plain" },
|
||||||
|
// };
|
||||||
|
// const result = await client.put("examplefile.txt", data, options);
|
||||||
|
// console.log(result);
|
||||||
|
// } catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// upload.addEventListener("click", () => {
|
||||||
|
// const data = file.files[0];
|
||||||
|
// putObject(data);
|
||||||
|
// });
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(result => {
|
||||||
|
var arr = [];
|
||||||
|
result.forEach((element,i)=>{
|
||||||
|
arr[i] = element.path;
|
||||||
|
})
|
||||||
|
// that.photo = that.photo.concat(arr);
|
||||||
|
that.$emit('addImg',arr)
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '上传成功'
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log('上传结果err',err)
|
||||||
|
uni.showToast({
|
||||||
|
title: '上传失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 删除图片
|
||||||
|
deleteImage(index) {
|
||||||
|
this.$emit('deleteImage',index)
|
||||||
|
},
|
||||||
|
previewImage(urls, current) {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: urls,
|
||||||
|
current: current
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-content {
|
||||||
|
/* width: 750rpx; */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.image-preview,
|
||||||
|
.upload-btn {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-video,
|
||||||
|
.preview-image {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
border: 2rpx dashed #DDDDDD;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -20rpx;
|
||||||
|
right: -20rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,421 @@
|
||||||
|
<template>
|
||||||
|
<!-- 上传短视频 -->
|
||||||
|
<view class="upload-content">
|
||||||
|
<view class="progress-box" v-if="showProgress">
|
||||||
|
<text class="progress-text">{{progress}}%</text>
|
||||||
|
<text class="progress-lable">上传中</text>
|
||||||
|
<progress :percent="progress"></progress>
|
||||||
|
</view>
|
||||||
|
<view v-if="video" class="video-preview">
|
||||||
|
<video
|
||||||
|
:src="'https://app.mrrweb.com.cn' + video"
|
||||||
|
ref="videoPlayer"
|
||||||
|
v-if="video"
|
||||||
|
class="preview-video"
|
||||||
|
:controls="false"
|
||||||
|
object-fit="cover"
|
||||||
|
:autoplay="true"
|
||||||
|
:muted="true"
|
||||||
|
:initial-time="1"
|
||||||
|
:show-center-play-btn="false"
|
||||||
|
:show-loading="false"
|
||||||
|
:enable-progress-gesture="false"
|
||||||
|
:show-fullscreen-btn="false"
|
||||||
|
:show-play-btn="false"
|
||||||
|
@play="onPlay"
|
||||||
|
></video>
|
||||||
|
<!-- 备用提示 -->
|
||||||
|
<view v-if="showFallback" class="video-fallback">
|
||||||
|
<text>视频加载失败,请尝试刷新或更换视频源</text>
|
||||||
|
</view>
|
||||||
|
<view class="delete-btn" @click="deleteVideo">×</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="upload-btn" @click="chooseVideo">
|
||||||
|
<text class="plus-icon">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '../../utils/request';
|
||||||
|
import sparkMD5 from 'spark-md5'; // 引入MD5计算库
|
||||||
|
export default {
|
||||||
|
name:"upload-video",
|
||||||
|
props: {
|
||||||
|
video: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
videoNum: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
autoplay: false,
|
||||||
|
showProgress: false,
|
||||||
|
progress: 0,
|
||||||
|
showFallback: false,
|
||||||
|
finalVideoUrl: '',
|
||||||
|
platformAutoplay: true,
|
||||||
|
autoplayMuted: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// watch: {
|
||||||
|
// video: {
|
||||||
|
// handler(newValue) {
|
||||||
|
// this.video = newValue
|
||||||
|
// },
|
||||||
|
// deep: true
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
methods: {
|
||||||
|
// 获取文件 (兼容各平台)
|
||||||
|
getFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// #ifdef H5
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = this.accept
|
||||||
|
input.onchange = (e) => {
|
||||||
|
// resolve(e.target.files[0])
|
||||||
|
resolve(e)
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.chooseVideo({
|
||||||
|
count: 1,
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
compressed: false,
|
||||||
|
maxDuration: 180,
|
||||||
|
camera: 'back',
|
||||||
|
success: (res) => {
|
||||||
|
// resolve(res.tempFiles[0])
|
||||||
|
resolve(res)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 上传视频区域
|
||||||
|
async chooseVideo() {
|
||||||
|
// 选择文件(添加类型校验)
|
||||||
|
// const res = await uni.chooseVideo({
|
||||||
|
// sourceType: ['album', 'camera'],
|
||||||
|
// compressed: false,
|
||||||
|
// maxDuration: 180,
|
||||||
|
// camera: 'back'
|
||||||
|
// });
|
||||||
|
const res = await this.getFile();
|
||||||
|
console.log(res,';;;;;;;;;;;;;;;;;;;;;;;;;;;')
|
||||||
|
this.showProgress = true;
|
||||||
|
let size = res.size;
|
||||||
|
if(size > 5*1024*1024) {
|
||||||
|
// 统一获取文件列表
|
||||||
|
const files = res.tempFile || res;
|
||||||
|
const file = res.tempFile || res;
|
||||||
|
console.log('file',file)
|
||||||
|
// if (!files || files.length === 0) {
|
||||||
|
// uni.showToast({ title: '文件选择取消', icon: 'none' });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
uni.showToast({ title: '视频选择取消', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress: (res) => {
|
||||||
|
console.log(`分片${index}进度: ${res.progress}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过 tempFiles 获取文件(UniApp规范)
|
||||||
|
// const file = files[0];
|
||||||
|
|
||||||
|
|
||||||
|
// 计算文件MD5(优化大文件计算)
|
||||||
|
const md5 = await this.calculateFileMD5(file);
|
||||||
|
|
||||||
|
// 分片大小(2MB)
|
||||||
|
const chunkSize = 1 * 1024 * 1024;
|
||||||
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
|
// 分片上传
|
||||||
|
for (let index = 0; index < totalChunks; index++) {
|
||||||
|
const start = index * chunkSize;
|
||||||
|
const end = Math.min(file.size, start + chunkSize);
|
||||||
|
// #ifdef H5
|
||||||
|
// 浏览器环境使用标准的 slice 方法
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
// 上传分片
|
||||||
|
await this.uploadChunk(chunk, index, totalChunks, md5);
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
// 非浏览器环境使用 Uniapp API
|
||||||
|
const fs = uni.getFileSystemManager();
|
||||||
|
const arrayBuffer = fs.readFileSync({
|
||||||
|
filePath: file,
|
||||||
|
position: start,
|
||||||
|
length: end - start
|
||||||
|
});
|
||||||
|
// 上传分片
|
||||||
|
await this.uploadChunk(arrayBuffer, index, totalChunks, md5);
|
||||||
|
// #endif
|
||||||
|
// 上传分片
|
||||||
|
// await this.uploadChunk(chunk, index, totalChunks, md5);
|
||||||
|
this.progress = parseInt((index+1)/totalChunks*100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并请求
|
||||||
|
await this.mergeChunks(md5, file.name);
|
||||||
|
}else {
|
||||||
|
let item = {
|
||||||
|
filePath: res.tempFilePath,
|
||||||
|
type: 'video'
|
||||||
|
}
|
||||||
|
this.uploadVideo(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploadVideo(item) {
|
||||||
|
request.uploadVideo('/user/uploadvideo',item.filePath,'video',{type: item.type}).then(res=>{
|
||||||
|
this.$emit('addVideo',res.path)
|
||||||
|
// this.initVideo();
|
||||||
|
this.showProgress = false;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 计算文件MD5(分块计算)
|
||||||
|
calculateFileMD5(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const chunkSize = 1 * 1024 * 1024;
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize);
|
||||||
|
const spark = new sparkMD5.ArrayBuffer();
|
||||||
|
let currentChunk = 0;
|
||||||
|
|
||||||
|
const loadNext = () => {
|
||||||
|
const start = currentChunk * chunkSize;
|
||||||
|
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
|
||||||
|
// #ifdef H5
|
||||||
|
const reader = new FileReader();
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
const reader = uni.getfilesystem;
|
||||||
|
// #endif
|
||||||
|
reader.onload = (e) => {
|
||||||
|
spark.append(e.target.result);
|
||||||
|
currentChunk++;
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext();
|
||||||
|
} else {
|
||||||
|
resolve(spark.end());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file.slice(start, end));
|
||||||
|
};
|
||||||
|
loadNext();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadChunk(chunk, index, totalChunks, md5) {
|
||||||
|
let that = this;
|
||||||
|
try {
|
||||||
|
// 获取ArrayBuffer
|
||||||
|
const arrayBuffer = await new Promise((resolve) => {
|
||||||
|
// #ifdef H5
|
||||||
|
const reader = new FileReader();
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
const reader = uni.getfilesystem;
|
||||||
|
// #endif
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.readAsArrayBuffer(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 平台差异化处理
|
||||||
|
let tempFilePath = '';
|
||||||
|
|
||||||
|
// 微信小程序/App端写入临时文件
|
||||||
|
// #ifdef MP-WEIXIN || APP-PLUS
|
||||||
|
const fs = uni.getFileSystemManager();
|
||||||
|
tempFilePath = `${getTempDir()}${md5}_${Date.now()}_${index}.tmp`;
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
data: arrayBuffer,
|
||||||
|
encoding: 'binary',
|
||||||
|
success: resolve,
|
||||||
|
fail: (err) => reject(`文件写入失败: ${JSON.stringify(err)}`)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// H5端特殊处理
|
||||||
|
// #ifdef H5
|
||||||
|
const blob = new Blob([arrayBuffer], { type: chunk.type });
|
||||||
|
const file = new File([blob], `${md5}_${index}.bin`, {
|
||||||
|
type: chunk.type,
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
tempFilePath = file;
|
||||||
|
// #endif
|
||||||
|
const uploadTask = await request.uploadTask('/user/uploadvideoother',tempFilePath,'file',{chunkIndex: index,totalChunks: totalChunks,md5: md5});
|
||||||
|
|
||||||
|
// 上传后清理临时文件(非H5)
|
||||||
|
// #ifdef MP-WEIXIN || APP-PLUS
|
||||||
|
fs.unlink({ filePath: tempFilePath });
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return uploadTask;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('分片上传失败:', error);
|
||||||
|
throw new Error(`分片${index}上传失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 合并请求
|
||||||
|
mergeChunks(md5, fileName) {
|
||||||
|
let that = this;
|
||||||
|
request.post('/user/uploadvideomerge',{md5: md5,filename: fileName}).then(res=>{
|
||||||
|
that.$emit('addVideo',res.path)
|
||||||
|
// that.initVideo();
|
||||||
|
that.showProgress = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
// 上传视频结束区域
|
||||||
|
// 删除视频
|
||||||
|
deleteVideo() {
|
||||||
|
this.$emit('deleteVideo')
|
||||||
|
},
|
||||||
|
initVideo() {
|
||||||
|
// 平台自动播放处理
|
||||||
|
this.platformAutoplay = this.autoplay;
|
||||||
|
|
||||||
|
// iOS自动播放必须静音
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (plus.os.name === 'iOS' && this.autoplay) {
|
||||||
|
this.autoplayMuted = true;
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 路径处理
|
||||||
|
this.processVideoUrl();
|
||||||
|
},
|
||||||
|
processVideoUrl() {
|
||||||
|
if (!this.video) return;
|
||||||
|
|
||||||
|
// 处理APP本地路径
|
||||||
|
if (this.video.startsWith('file://') || this.video.startsWith('/storage')) {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
this.finalVideoUrl = plus.io.convertLocalFileSystemURL(this.video);
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN || H5
|
||||||
|
this.finalVideoUrl = this.video;
|
||||||
|
// #endif
|
||||||
|
} else {
|
||||||
|
this.finalVideoUrl = this.video;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPlay() {
|
||||||
|
console.log('视频开始播放');
|
||||||
|
this.platformAutoplay = true;
|
||||||
|
this.showFallback = false;
|
||||||
|
},
|
||||||
|
onError(e) {
|
||||||
|
console.error('视频播放错误:', e);
|
||||||
|
this.showFallback = true;
|
||||||
|
|
||||||
|
// 尝试备用方案
|
||||||
|
this.tryFallbackSource();
|
||||||
|
},
|
||||||
|
tryFallbackSource() {
|
||||||
|
// 可以在这里实现备用视频源逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.progress-box {
|
||||||
|
width: 204rpx;
|
||||||
|
height: 204rpx;
|
||||||
|
background-color: rgba(0 ,0 ,0 , 0.5);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.progress-text {
|
||||||
|
font-size: 42rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.progress-lable {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.progress-box progress {
|
||||||
|
/* width: 204rpx; */
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.video-preview,
|
||||||
|
.upload-btn {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-video {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
.upload-btn {
|
||||||
|
border: 2rpx dashed #DDDDDD;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -20rpx;
|
||||||
|
right: -20rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
<template>
|
||||||
|
<view class="upload-section">
|
||||||
|
<!-- 上传类型选择 -->
|
||||||
|
<view class="upload-header">
|
||||||
|
<text class="section-title">上传照片/视频</text>
|
||||||
|
<text class="upload-tip">(最多可上传9张图片或1个视频)</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 媒体列表 -->
|
||||||
|
<view class="media-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in mediaList"
|
||||||
|
:key="index"
|
||||||
|
class="media-item"
|
||||||
|
>
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<image
|
||||||
|
v-if="item.type === 'image'"
|
||||||
|
:src="item.url"
|
||||||
|
class="media-preview"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
<!-- 视频预览 -->
|
||||||
|
<video
|
||||||
|
v-if="item.type === 'video'"
|
||||||
|
:src="item.url"
|
||||||
|
class="media-preview"
|
||||||
|
:poster="item.poster"
|
||||||
|
></video>
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<view class="delete-btn" @click="deleteMedia(index)">
|
||||||
|
<text class="delete-icon">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<view
|
||||||
|
v-if="showUploadBtn"
|
||||||
|
class="upload-btn"
|
||||||
|
@click="showActionSheet"
|
||||||
|
>
|
||||||
|
<text class="plus-icon">+</text>
|
||||||
|
<text class="upload-text">上传照片/视频</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'media-upload',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mediaList: [],
|
||||||
|
maxCount: 9, // 最大图片数量
|
||||||
|
maxVideo: 1, // 最大视频数量
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showUploadBtn() {
|
||||||
|
// 如果有视频,不显示上传按钮
|
||||||
|
if (this.mediaList.some(item => item.type === 'video')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 如果图片数量达到最大值,不显示上传按钮
|
||||||
|
if (this.mediaList.length >= this.maxCount) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showActionSheet() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['拍摄', '从相册选择'],
|
||||||
|
success: (res) => {
|
||||||
|
if (res.tapIndex === 0) {
|
||||||
|
this.takePhoto()
|
||||||
|
} else {
|
||||||
|
this.chooseMedia()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 拍摄照片或视频
|
||||||
|
takePhoto() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['拍照', '录视频'],
|
||||||
|
success: (res) => {
|
||||||
|
if (res.tapIndex === 0) {
|
||||||
|
// 拍照
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sourceType: ['camera'],
|
||||||
|
success: (res) => {
|
||||||
|
this.handleImageSuccess(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 录视频
|
||||||
|
uni.chooseVideo({
|
||||||
|
sourceType: ['camera'],
|
||||||
|
maxDuration: 60,
|
||||||
|
camera: 'back',
|
||||||
|
success: (res) => {
|
||||||
|
this.handleVideoSuccess(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从相册选择
|
||||||
|
chooseMedia() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['选择图片', '选择视频'],
|
||||||
|
success: (res) => {
|
||||||
|
if (res.tapIndex === 0) {
|
||||||
|
// 选择图片
|
||||||
|
uni.chooseImage({
|
||||||
|
count: this.maxCount - this.mediaList.length,
|
||||||
|
sourceType: ['album'],
|
||||||
|
success: (res) => {
|
||||||
|
this.handleImageSuccess(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 选择视频
|
||||||
|
uni.chooseVideo({
|
||||||
|
sourceType: ['album'],
|
||||||
|
maxDuration: 60,
|
||||||
|
success: (res) => {
|
||||||
|
this.handleVideoSuccess(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理图片选择结果
|
||||||
|
handleImageSuccess(res) {
|
||||||
|
const newImages = res.tempFilePaths.map(path => ({
|
||||||
|
type: 'image',
|
||||||
|
url: path
|
||||||
|
}))
|
||||||
|
this.mediaList = [...this.mediaList, ...newImages]
|
||||||
|
this.$emit('change', this.mediaList)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理视频选择结果
|
||||||
|
handleVideoSuccess(res) {
|
||||||
|
this.mediaList = [{
|
||||||
|
type: 'video',
|
||||||
|
url: res.tempFilePath,
|
||||||
|
poster: res.thumbTempFilePath
|
||||||
|
}]
|
||||||
|
this.$emit('change', this.mediaList)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除媒体文件
|
||||||
|
deleteMedia(index) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要删除这个文件吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
this.mediaList.splice(index, 1)
|
||||||
|
this.$emit('change', this.mediaList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload-section {
|
||||||
|
padding: 30rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-header {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-list {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-item {
|
||||||
|
width: 220rpx;
|
||||||
|
height: 220rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-preview {
|
||||||
|
width: 220rpx;
|
||||||
|
height: 220rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background-color: #f5f6fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: -16rpx;
|
||||||
|
top: -16rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
width: 220rpx;
|
||||||
|
height: 220rpx;
|
||||||
|
background-color: #f5f6fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* #ifdef H5 */
|
||||||
|
cursor: pointer;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
<template>
|
||||||
|
<view class="video-player-container">
|
||||||
|
<video
|
||||||
|
:id="videoId"
|
||||||
|
:src="'https://app.mrrweb.com.cn'+src"
|
||||||
|
class="video-cont"
|
||||||
|
:autoplay="autoplay"
|
||||||
|
:controls="controls"
|
||||||
|
:muted="isMuted"
|
||||||
|
:show-fullscreen-btn="showFullscreenBtn"
|
||||||
|
:show-play-btn="showPlayBtn"
|
||||||
|
:show-progress="showProgress"
|
||||||
|
:loop="loop"
|
||||||
|
:object-fit="objectFit"
|
||||||
|
@play="onPlay"
|
||||||
|
@pause="onPause"
|
||||||
|
@ended="onEnded"
|
||||||
|
@error="onError"
|
||||||
|
@timeupdate="onTimeUpdate"
|
||||||
|
@fullscreenchange="onFullscreenChange">
|
||||||
|
</video>
|
||||||
|
<view class="video-controls">
|
||||||
|
<!-- <view class="control-btn" @click="togglePlay">
|
||||||
|
<image :src="isPlaying ? '/static/images/pause.png' : '/static/images/play.png'" class="control-icon"></image>
|
||||||
|
</view> -->
|
||||||
|
<view class="control-btn" @click="toggleMute">
|
||||||
|
<image :src="isMuted ? '/static/images/mute.png' : '/static/images/unmute.png'" class="control-icon"></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'VideoPlayer',
|
||||||
|
props: {
|
||||||
|
// 视频源
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
videoStyle: {
|
||||||
|
type: String,
|
||||||
|
default: 'width: 200rpx;height: 200rpx;'
|
||||||
|
},
|
||||||
|
// 视频封面
|
||||||
|
poster: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 是否显示控制栏
|
||||||
|
controls: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 是否自动播放
|
||||||
|
autoplay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否循环播放
|
||||||
|
loop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 是否静音
|
||||||
|
muted: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示全屏按钮
|
||||||
|
showFullscreenBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示播放按钮
|
||||||
|
showPlayBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示进度条
|
||||||
|
showProgress: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 视频填充模式
|
||||||
|
objectFit: {
|
||||||
|
type: String,
|
||||||
|
default: 'cover',
|
||||||
|
validator: value => ['contain', 'fill', 'cover'].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
videoId: `video-${Date.now()}`,
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 0,
|
||||||
|
isPlaying: false,
|
||||||
|
isFullscreen: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 播放事件
|
||||||
|
onPlay(e) {
|
||||||
|
this.isPlaying = true
|
||||||
|
this.$emit('play', e)
|
||||||
|
},
|
||||||
|
// 暂停事件
|
||||||
|
onPause(e) {
|
||||||
|
this.isPlaying = false
|
||||||
|
this.$emit('pause', e)
|
||||||
|
},
|
||||||
|
// 播放结束事件
|
||||||
|
onEnded(e) {
|
||||||
|
this.isPlaying = false
|
||||||
|
this.$emit('ended', e)
|
||||||
|
},
|
||||||
|
// 错误事件
|
||||||
|
onError(e) {
|
||||||
|
this.$emit('error', e)
|
||||||
|
},
|
||||||
|
// 播放进度更新事件
|
||||||
|
onTimeUpdate(e) {
|
||||||
|
this.currentTime = e.detail.currentTime
|
||||||
|
this.duration = e.detail.duration
|
||||||
|
this.$emit('timeupdate', e)
|
||||||
|
},
|
||||||
|
// 全屏状态改变事件
|
||||||
|
onFullscreenChange(e) {
|
||||||
|
this.isFullscreen = e.detail.fullScreen
|
||||||
|
this.$emit('fullscreenchange', e)
|
||||||
|
},
|
||||||
|
// 播放方法
|
||||||
|
play() {
|
||||||
|
const videoContext = uni.createVideoContext(this.videoId)
|
||||||
|
videoContext.play()
|
||||||
|
},
|
||||||
|
// 暂停方法
|
||||||
|
pause() {
|
||||||
|
const videoContext = uni.createVideoContext(this.videoId)
|
||||||
|
videoContext.pause()
|
||||||
|
},
|
||||||
|
// 跳转到指定时间
|
||||||
|
seek(time) {
|
||||||
|
const videoContext = uni.createVideoContext(this.videoId)
|
||||||
|
videoContext.seek(time)
|
||||||
|
},
|
||||||
|
// 进入全屏
|
||||||
|
requestFullScreen() {
|
||||||
|
const videoContext = uni.createVideoContext(this.videoId)
|
||||||
|
videoContext.requestFullScreen()
|
||||||
|
},
|
||||||
|
// 退出全屏
|
||||||
|
exitFullScreen() {
|
||||||
|
const videoContext = uni.createVideoContext(this.videoId)
|
||||||
|
videoContext.exitFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
video-player-container {
|
||||||
|
width: 750rpx;
|
||||||
|
height: 750rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.video-cont {
|
||||||
|
width: 750rpx;
|
||||||
|
height: 750rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-controls {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 30rpx;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
## 自定义水印添加
|
||||||
|
***
|
||||||
|
一款声明版权立场的组件
|
||||||
|
该组件参考了 [315500033@qq.com](https://ext.dcloud.net.cn/plugin?id=362) 大神的组件 修改后符合自己的编码习惯 让水印有更多的展示方式
|
||||||
|
|
||||||
|
### 使用方式
|
||||||
|
#### 在 ``script`` 中引入组件:
|
||||||
|
```javascript
|
||||||
|
import wmWatermark from '../../components/wm-watermark.vue';
|
||||||
|
export default {
|
||||||
|
components: {wmWatermark}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 在 ``template`` 中使用组件:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<wm-watermark text="这是水印信息"></wm-watermark>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **属性说明**
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
| :---: | :---: | :---: | :--------: |
|
||||||
|
| text | String | 无 | 设置水印文字 注:(文字优先图片) |
|
||||||
|
| imgUrl | String | 无 | 设置水印图片链接 注:(文字优先图片) |
|
||||||
|
| opacity | Number,String | 0.6 | 设置显示透明度 |
|
||||||
|
| num | Number | 20 | 设置水印数量 |
|
||||||
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<!-- 水印 -->
|
||||||
|
<view class="watermark">
|
||||||
|
<block v-for="(item,index) in num" :key="index">
|
||||||
|
<view class="watermark-text" v-if="text != ''" :style="{opacity:opacity}">{{text}}</view>
|
||||||
|
<image class="watermark-img" :src="imgUrl" mode="aspectFill" v-if="imgUrl !='' && text ==''" :style="{opacity:opacity}"></image>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name:'wm-watermark',
|
||||||
|
props:{
|
||||||
|
text:{ //设置水印文字
|
||||||
|
type:String,
|
||||||
|
default:''
|
||||||
|
},
|
||||||
|
imgUrl:{ //设置水印图片
|
||||||
|
type:String,
|
||||||
|
default:''
|
||||||
|
},
|
||||||
|
opacity:{ //设置透明度
|
||||||
|
type:[Number,String],
|
||||||
|
default:0.6
|
||||||
|
},
|
||||||
|
num:{ //设置水印数量
|
||||||
|
type:Number,
|
||||||
|
default:8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.watermark{
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top:0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999999;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
.watermark-text{
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 32upx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.watermark-img{
|
||||||
|
width: 375upx;
|
||||||
|
height: 375upx;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import App from './App'
|
||||||
|
import request from './utils/request'
|
||||||
|
import store from './store/index.js'
|
||||||
|
import uploadAppVue from './components/upload-app/upload-app.vue'
|
||||||
|
import noData from 'components/noData/noData.vue'
|
||||||
|
import shareMixin from "./mixins/mixins";
|
||||||
|
import uvUI from '@/uni_modules/uv-ui-tools'
|
||||||
|
import { oprPresentLogin } from '@/utils/loginGlobal.js';
|
||||||
|
|
||||||
|
|
||||||
|
// 挂载到uni全局对象(核心:跨端通用)
|
||||||
|
uni.oprPresentLogin = oprPresentLogin;
|
||||||
|
|
||||||
|
Vue.prototype.$store = store
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './uni.promisify.adaptor'
|
||||||
|
Vue.mixin(shareMixin);
|
||||||
|
Vue.component('upload-app-vue', uploadAppVue);
|
||||||
|
Vue.component('noData', noData);
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
store,
|
||||||
|
uvUI,
|
||||||
|
...App
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
Vue.prototype.$baseUrl = "https://app.mrrweb.com.cn";
|
||||||
|
Vue.prototype.$imgBaseUrl = "https://www.mrrweb.com.cn";
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
{
|
||||||
|
"name" : "美融融plus",
|
||||||
|
"appid" : "__UNI__DA7CE05",
|
||||||
|
"description" : "同城便捷到家美业“技术外卖”服务、同城预约到店服务平台美融融为同城爱美人群提供更优质的更全面的服务。",
|
||||||
|
"versionName" : "1.2.20",
|
||||||
|
"versionCode" : 1220,
|
||||||
|
"transformPx" : false,
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"video" : {
|
||||||
|
"hardwareAcceleration" : "auto",
|
||||||
|
"decode" : {
|
||||||
|
"h264" : "hardware",
|
||||||
|
"hevc" : "software"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {
|
||||||
|
"Maps" : {},
|
||||||
|
"Geolocation" : {},
|
||||||
|
"Share" : {},
|
||||||
|
"Payment" : {},
|
||||||
|
"VideoPlayer" : {},
|
||||||
|
"OAuth" : {},
|
||||||
|
"Camera" : {}
|
||||||
|
},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>"
|
||||||
|
],
|
||||||
|
"minSdkVersion" : 21,
|
||||||
|
"targetSdkVersion" : 30
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {
|
||||||
|
"urlschemewhitelist" : [ "iosmap" ],
|
||||||
|
"dSYMs" : false,
|
||||||
|
"plistcmds" : [
|
||||||
|
"Set :NSMicrophoneUsageDescription 获取语音权限以进行语音输入",
|
||||||
|
"Set :NSPhotoLibraryUsageDescription 从相册选择图片作为用户头像",
|
||||||
|
"Set :NSCameraUsageDescription 用户使用相机拍摄图片作为自定义头像",
|
||||||
|
"Set :NSPhotoLibraryAddUsageDescription 保存图片到相册"
|
||||||
|
],
|
||||||
|
"privacyDescription" : {
|
||||||
|
"NSLocationWhenInUseUsageDescription" : "美融融商家请求您的地理位置,以便您使用该功能获取您附近的手艺人以及店铺下的服务内容以及添加地址,是否允许?",
|
||||||
|
"NSLocationAlwaysAndWhenInUseUsageDescription" : "美融融商家请求您的地理位置,以便您使用该功能获取您附近的手艺人以及店铺下的服务内容以及添加地址,是否允许?",
|
||||||
|
"NSPhotoLibraryUsageDescription" : "美融融商家请求您的相册权限,使用相册照片功能,用于上传头像或者评价上传图片与发布动态,是否允许?",
|
||||||
|
"NSPhotoLibraryAddUsageDescription" : "美融融商家请求您的相册权限,使用相册照片功能,用于上传头像或者评价上传图片与发布动态,是否允许?",
|
||||||
|
"NSCameraUsageDescription" : "美融融商家请求您的摄像头权限,使用拍照功能,用于上传头像或者上传入驻资质,是否允许?",
|
||||||
|
"NSMicrophoneUsageDescription" : "美融融商家请求您的麦克风权限,使用录像功能,用于上传个人短视频更新个人资料,是否允许?",
|
||||||
|
"NSLocationAlwaysUsageDescription" : "美融融商家请求您的地理位置,以便您使用该功能获取您附近的手艺人以及店铺下的服务内容以及添加地址,是否允许?",
|
||||||
|
"NSCalendarsUsageDescription" : "该应用需要获取你的日历,以便为您提供学习记录,更好的体验",
|
||||||
|
"NSRemindersUsageDescription" : "该应用需要获取你的提醒事项,以便为您提供学习记录,更好的体验",
|
||||||
|
"NSContactsUsageDescription" : "美融融商家需要获取您的通讯录,以便您联系商家或手艺人,是否允许?"
|
||||||
|
},
|
||||||
|
"capabilities" : {
|
||||||
|
"entitlements" : {
|
||||||
|
"com.apple.developer.associated-domains" : "applinks:app.mrrweb.com.cn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {
|
||||||
|
"maps" : {
|
||||||
|
"amap" : {
|
||||||
|
"name" : "amapgDZVQ1L0",
|
||||||
|
"appkey_ios" : "3c7285f894266dc8b9ee30297c2d708e",
|
||||||
|
"appkey_android" : "3150b3d08eb0875c5c660f7ff3c08ebd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"payment" : {
|
||||||
|
"weixin" : {
|
||||||
|
"__platform__" : [ "ios", "android" ],
|
||||||
|
"appid" : "wx2381a791a52a402d",
|
||||||
|
"UniversalLinks" : "https://app.mrrweb.com.cn/"
|
||||||
|
},
|
||||||
|
"appleiap" : {},
|
||||||
|
"alipay" : {
|
||||||
|
"__platform__" : [ "ios", "android" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"share" : {
|
||||||
|
"weixin" : {
|
||||||
|
"appid" : "wx2381a791a52a402d",
|
||||||
|
"UniversalLinks" : "https://app.mrrweb.com.cn/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geolocation" : {
|
||||||
|
"amap" : {
|
||||||
|
"name" : "amapgDZVQ1L0",
|
||||||
|
"__platform__" : [ "ios", "android" ],
|
||||||
|
"appkey_ios" : "3c7285f894266dc8b9ee30297c2d708e",
|
||||||
|
"appkey_android" : "3150b3d08eb0875c5c660f7ff3c08ebd"
|
||||||
|
},
|
||||||
|
"system" : {
|
||||||
|
"__platform__" : [ "ios", "android" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth" : {
|
||||||
|
"weixin" : {
|
||||||
|
"appid" : "wx2381a791a52a402d",
|
||||||
|
"UniversalLinks" : "https://app.mrrweb.com.cn/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons" : {
|
||||||
|
"android" : {
|
||||||
|
"hdpi" : "unpackage/res/icons/72x72.png",
|
||||||
|
"xhdpi" : "unpackage/res/icons/96x96.png",
|
||||||
|
"xxhdpi" : "unpackage/res/icons/144x144.png",
|
||||||
|
"xxxhdpi" : "unpackage/res/icons/192x192.png"
|
||||||
|
},
|
||||||
|
"ios" : {
|
||||||
|
"appstore" : "unpackage/res/icons/1024x1024.png",
|
||||||
|
"ipad" : {
|
||||||
|
"app" : "unpackage/res/icons/76x76.png",
|
||||||
|
"app@2x" : "unpackage/res/icons/152x152.png",
|
||||||
|
"notification" : "unpackage/res/icons/20x20.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"proapp@2x" : "unpackage/res/icons/167x167.png",
|
||||||
|
"settings" : "unpackage/res/icons/29x29.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"spotlight" : "unpackage/res/icons/40x40.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png"
|
||||||
|
},
|
||||||
|
"iphone" : {
|
||||||
|
"app@2x" : "unpackage/res/icons/120x120.png",
|
||||||
|
"app@3x" : "unpackage/res/icons/180x180.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"notification@3x" : "unpackage/res/icons/60x60.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"settings@3x" : "unpackage/res/icons/87x87.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png",
|
||||||
|
"spotlight@3x" : "unpackage/res/icons/120x120.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splashscreen" : {
|
||||||
|
"useOriginalMsgbox" : true,
|
||||||
|
"androidStyle" : "common"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nativePlugins" : {
|
||||||
|
"AliCloud-NirvanaPns" : {
|
||||||
|
"__plugin_info__" : {
|
||||||
|
"name" : "阿里云号码认证SDK",
|
||||||
|
"description" : "阿里云号码认证SDK,包含一键登录和本机号码校验两个功能。",
|
||||||
|
"platforms" : "Android,iOS",
|
||||||
|
"url" : "https://ext.dcloud.net.cn/plugin?id=4297",
|
||||||
|
"android_package_name" : "cn.mrrweb",
|
||||||
|
"ios_bundle_id" : "cn.mrrweb",
|
||||||
|
"android_package_name" : "cn.mrrweb",
|
||||||
|
"ios_bundle_id" : "cn.mrrweb",
|
||||||
|
"isCloud" : true,
|
||||||
|
"bought" : 1,
|
||||||
|
"pid" : "4297",
|
||||||
|
"parameters" : {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp" : {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "wxb486b10a60d2c78d",
|
||||||
|
"privacy" : {
|
||||||
|
"requiredPrivateInfos" : [ "getBackgroundFetchData" ]
|
||||||
|
},
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false,
|
||||||
|
"minified" : true
|
||||||
|
},
|
||||||
|
"usingComponents" : true,
|
||||||
|
"optimization" : {
|
||||||
|
"subPackages" : true
|
||||||
|
},
|
||||||
|
"permission" : {
|
||||||
|
"scope.userLocation" : {
|
||||||
|
"desc" : "美融融商家需获取位置,展示附近手艺人、店铺及添加地址"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requiredPrivateInfos" : [
|
||||||
|
"getLocation" // 声明 getLocation 接口
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "2",
|
||||||
|
"_spaceID" : "mp-908e3a8e-a3ae-4a70-aaa7-545c9d98f662",
|
||||||
|
"h5" : {
|
||||||
|
"devServer" : {
|
||||||
|
"port" : ""
|
||||||
|
},
|
||||||
|
"sdkConfigs" : {
|
||||||
|
"maps" : {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* SDK配置 */
|
||||||
|
|
||||||
|
/* ios打包配置 */
|
||||||
|
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
// mixins/share.js
|
||||||
|
import request from '@/utils/request'
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
formatPriceNumber(num) {
|
||||||
|
num = Number(num)
|
||||||
|
let formatted;
|
||||||
|
formatted = num.toFixed(2);
|
||||||
|
// 根据数字大小确定保留的小数位数
|
||||||
|
// if (num >= 100) {
|
||||||
|
// // 大于等于100时,不保留小数
|
||||||
|
// formatted = Math.floor(num).toString();
|
||||||
|
// } else if (num >= 10) {
|
||||||
|
// // 大于等于10时,保留1位小数
|
||||||
|
// formatted = num.toFixed(1);
|
||||||
|
// } else {
|
||||||
|
// // 其他情况,保留两位小数
|
||||||
|
// formatted = num.toFixed(2);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 处理小数最后一位是0的情况
|
||||||
|
// 如果包含小数点,则检查并移除末尾的0和可能的小数点
|
||||||
|
// if (formatted.includes(".")) {
|
||||||
|
// formatted = formatted.replace(/0+$/, "").replace(/\.$/, "");
|
||||||
|
// }
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
},
|
||||||
|
getValueLength(value) {
|
||||||
|
// 处理null和undefined
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理字符串和数组(直接使用length属性)
|
||||||
|
if (typeof value === 'string' || Array.isArray(value)) {
|
||||||
|
return value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对象(返回可枚举自有属性的数量)
|
||||||
|
if (Object.prototype.toString.call(value) === '[object Object]') {
|
||||||
|
return Object.keys(value).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理其他类型(数字、布尔值、函数等无长度概念的类型)
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
formatPriceSize(o_num, o_fontSize, type = 1) {
|
||||||
|
let fontSize = o_fontSize;
|
||||||
|
const len = this.formatPriceNumber(o_num).length;
|
||||||
|
//首页拼团立省价格
|
||||||
|
if (len > 7 && type == 1) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
//分享图立省价格
|
||||||
|
if (len > 7 && type == 2) {
|
||||||
|
fontSize -= 3;
|
||||||
|
}
|
||||||
|
//分享图拼团价格
|
||||||
|
if (type === 3) {
|
||||||
|
if (len > 4) {
|
||||||
|
fontSize -= 10;
|
||||||
|
}
|
||||||
|
if (len > 5) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
if (len > 6) {
|
||||||
|
fontSize -= 6;
|
||||||
|
}
|
||||||
|
if (len > 7) {
|
||||||
|
fontSize -= 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//分享图原价
|
||||||
|
if (type === 4) {
|
||||||
|
if (len > 5) {
|
||||||
|
fontSize -= 2;
|
||||||
|
}
|
||||||
|
if (len > 6) {
|
||||||
|
fontSize -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//首页商家
|
||||||
|
if (type === 5) {
|
||||||
|
if (len > 6) {
|
||||||
|
fontSize -= 6;
|
||||||
|
}
|
||||||
|
if (len > 7) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
if (len > 8) {
|
||||||
|
fontSize -= 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontSize;
|
||||||
|
},
|
||||||
|
//px转rpx
|
||||||
|
pxToRpx(px, screenWidth) {
|
||||||
|
|
||||||
|
if (typeof px !== 'number') {
|
||||||
|
throw new Error('请传入数字类型的px值');
|
||||||
|
}
|
||||||
|
return px * (750 / screenWidth);
|
||||||
|
},
|
||||||
|
//获取rpx值的statusBarHeight
|
||||||
|
getRpxStatusBarHeight() {
|
||||||
|
let statusBarHeight = this.$store.state.systemInfo.statusBarHeight
|
||||||
|
let screenWidth = this.$store.state.systemInfo.screenWidth
|
||||||
|
return this.pxToRpx(statusBarHeight, screenWidth)
|
||||||
|
},
|
||||||
|
//消息数量跟新
|
||||||
|
messageUpudateNum() {
|
||||||
|
request.post("/user/push/messageUnreadNum").then(res => {
|
||||||
|
if( res.code==200){
|
||||||
|
this.$store.commit("setMessageNum", res.data.count)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图片路径(通过条件编译区分环境:仅App缓存,非App直接返回网络URL)
|
||||||
|
* @param {string | string[]} urls - 单个图片URL或URL数组
|
||||||
|
* @returns {Promise<string | string[]>} - 单个URL返回字符串,数组返回对应路径数组
|
||||||
|
*/
|
||||||
|
async getImagePath(urls) {
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// 条件编译:非App环境(小程序/H5/快应用等)
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
console.log('【编译期判断】当前非App环境,直接返回网络图片路径');
|
||||||
|
// 格式化返回值:过滤空值,保持入参格式(数组/字符串)
|
||||||
|
if (Array.isArray(urls)) {
|
||||||
|
return urls.filter(url => typeof url === 'string' && url.trim());
|
||||||
|
}
|
||||||
|
return typeof urls === 'string' && urls.trim() ? urls.trim() : '';
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// 条件编译:仅App环境(5+App)
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
// 参数校验
|
||||||
|
if (!urls) {
|
||||||
|
console.error('getImagePath:请传入有效的URL(字符串/数组)');
|
||||||
|
return Array.isArray(urls) ? [] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 统一转为数组处理
|
||||||
|
const isBatch = Array.isArray(urls);
|
||||||
|
const urlList = isBatch ?
|
||||||
|
urls.filter(url => typeof url === 'string' && url.trim()) : [urls.trim()];
|
||||||
|
|
||||||
|
if (urlList.length === 0) {
|
||||||
|
return isBatch ? [] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历处理每个URL(优先读缓存,无缓存则下载)
|
||||||
|
const resultList = [];
|
||||||
|
for (const url of urlList) {
|
||||||
|
try {
|
||||||
|
// 生成唯一缓存Key
|
||||||
|
const cacheKey = this._getImageCacheKey(url);
|
||||||
|
// 读取本地缓存
|
||||||
|
const cachedPath = uni.getStorageSync(cacheKey);
|
||||||
|
|
||||||
|
if (cachedPath) {
|
||||||
|
resultList.push(cachedPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无缓存则下载并保存
|
||||||
|
await this._downloadAndCacheImage(url);
|
||||||
|
// 再次读取缓存(兜底:防止保存失败)
|
||||||
|
const newCachedPath = uni.getStorageSync(cacheKey);
|
||||||
|
resultList.push(newCachedPath || url);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`图片${url}缓存失败,使用网络路径:`, error.message);
|
||||||
|
resultList.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按入参格式返回(数组/字符串)
|
||||||
|
return isBatch ? resultList : resultList[0];
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// 以下方法仅App环境编译,非App环境会被剔除
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
/**
|
||||||
|
* 内部方法:下载图片并缓存到本地(仅App环境)
|
||||||
|
* @param {string} url - 图片URL
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async _downloadAndCacheImage(url) {
|
||||||
|
// 下载文件
|
||||||
|
const downloadRes = await new Promise((resolve, reject) => {
|
||||||
|
uni.downloadFile({
|
||||||
|
url,
|
||||||
|
success: resolve,
|
||||||
|
fail: (err) => reject(new Error(`下载失败:${err.errMsg}`))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 校验下载状态
|
||||||
|
if (downloadRes.statusCode !== 200) {
|
||||||
|
throw new Error(`下载状态异常:状态码${downloadRes.statusCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地
|
||||||
|
const saveRes = await new Promise((resolve, reject) => {
|
||||||
|
uni.saveFile({
|
||||||
|
tempFilePath: downloadRes.tempFilePath,
|
||||||
|
success: resolve,
|
||||||
|
fail: (err) => reject(new Error(`保存失败:${err.errMsg}`))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储缓存Key与本地路径的映射
|
||||||
|
const cacheKey = this._getImageCacheKey(url);
|
||||||
|
uni.setStorageSync(cacheKey, saveRes.savedFilePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部方法:生成URL对应的唯一缓存Key(仅App环境)
|
||||||
|
* @param {string} url - 图片URL
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_getImageCacheKey(url) {
|
||||||
|
// App环境优先用原生MD5,稳定性更高
|
||||||
|
if (plus.runtime.MD5) {
|
||||||
|
return `img_cache_${plus.runtime.MD5(url)}`;
|
||||||
|
}
|
||||||
|
// 兜底:URL编码作为Key
|
||||||
|
return `img_cache_${encodeURIComponent(url).replace(/[^\w]/g, '_')}`;
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"agcgw":{"url":"connect-drcn.dbankcloud.cn","backurl":"connect-drcn.hispace.hicloud.com","websocketurl":"connect-ws-drcn.hispace.dbankcloud.cn","websocketbackurl":"connect-ws-drcn.hispace.dbankcloud.com"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"260086000058238908","product_id":"461323198430018699","client_id":"1707180678149323456","client_secret":"2FCF8113D06D9C43DDDD4D291632F3769D4B991BBBF142448955B982673F3BF6","project_id":"461323198430018699","app_id":"114445451","api_key":"DgEDAP73r+rgXVMxFcwnVKN09OMmc1zg7Je7hSyx1o+hZpcOBR7cBZC40l/ehJBUzOuUGblIPOhhJOVPMHBdX1L1SFnEMcUjAEtgWA==","package_name":"cn.mrrweb"},"oauth_client":{"client_id":"114445451","client_type":1},"app_info":{"app_id":"114445451","package_name":"cn.mrrweb"},"service":{"analytics":{"collector_url":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drcn.ai.dbankcloud.com,ml-api-drcn.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drcn.cloud.huawei.com"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"CN","configuration_version":"3.0","appInfos":[{"package_name":"cn.mrrweb","client":{"app_id":"114445451"},"app_info":{"package_name":"cn.mrrweb","app_id":"114445451"},"oauth_client":{"client_type":1,"client_id":"114445451"}}]}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":"1.0.1","cn.mrrweb":{"manifestPlaceholders":{"VIVO_APPKEY":"aa27f4e4839fd1345bf57025778df558","VIVO_APPID":"105916679","HONOR_APPID":"104505186"},"oppoPushBussinessId":"46175","oppoPushAppKey":"b30fc37dd1f54108bb464e963dc427df","oppoPushAppSecret":"7cfff81389cf426c92e249d89f41ddb0","huaweiPushBussinessId":"46176","huaweiBadgeClassName":"","honorPushBussinessId":"46177","honorBadgeClassName":"","xiaomiPushBussinessId":"46191","xiaomiPushAppId":"2882303761520423236","xiaomiPushAppKey":"5772042339236","vivoPushBussinessId":"46192"}}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"VIVO_APPKEY": "aa27f4e4839fd1345bf57025778df558 ",
|
||||||
|
"VIVO_APPID": "105916679",
|
||||||
|
"HONOR_APPID":"104505186"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"developer_id":"110000064495",
|
||||||
|
"app_id":"104505186",
|
||||||
|
"package_name":"cn.mrrweb",
|
||||||
|
"version":"1.0"
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"businessID":"47959"
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
../mime/cli.js
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../mkdirp/bin/cmd.js
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../nanoid/bin/nanoid.cjs
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../os-name/cli.js
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../osx-release/cli.js
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../@babel/parser/bin/babel-parser.js
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../semver/bin/semver
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# @babel/helper-string-parser
|
||||||
|
|
||||||
|
> A utility package to parse strings
|
||||||
|
|
||||||
|
See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save @babel/helper-string-parser
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/helper-string-parser
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.readCodePoint = readCodePoint;
|
||||||
|
exports.readInt = readInt;
|
||||||
|
exports.readStringContents = readStringContents;
|
||||||
|
var _isDigit = function isDigit(code) {
|
||||||
|
return code >= 48 && code <= 57;
|
||||||
|
};
|
||||||
|
const forbiddenNumericSeparatorSiblings = {
|
||||||
|
decBinOct: new Set([46, 66, 69, 79, 95, 98, 101, 111]),
|
||||||
|
hex: new Set([46, 88, 95, 120])
|
||||||
|
};
|
||||||
|
const isAllowedNumericSeparatorSibling = {
|
||||||
|
bin: ch => ch === 48 || ch === 49,
|
||||||
|
oct: ch => ch >= 48 && ch <= 55,
|
||||||
|
dec: ch => ch >= 48 && ch <= 57,
|
||||||
|
hex: ch => ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102
|
||||||
|
};
|
||||||
|
function readStringContents(type, input, pos, lineStart, curLine, errors) {
|
||||||
|
const initialPos = pos;
|
||||||
|
const initialLineStart = lineStart;
|
||||||
|
const initialCurLine = curLine;
|
||||||
|
let out = "";
|
||||||
|
let firstInvalidLoc = null;
|
||||||
|
let chunkStart = pos;
|
||||||
|
const {
|
||||||
|
length
|
||||||
|
} = input;
|
||||||
|
for (;;) {
|
||||||
|
if (pos >= length) {
|
||||||
|
errors.unterminated(initialPos, initialLineStart, initialCurLine);
|
||||||
|
out += input.slice(chunkStart, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const ch = input.charCodeAt(pos);
|
||||||
|
if (isStringEnd(type, ch, input, pos)) {
|
||||||
|
out += input.slice(chunkStart, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ch === 92) {
|
||||||
|
out += input.slice(chunkStart, pos);
|
||||||
|
const res = readEscapedChar(input, pos, lineStart, curLine, type === "template", errors);
|
||||||
|
if (res.ch === null && !firstInvalidLoc) {
|
||||||
|
firstInvalidLoc = {
|
||||||
|
pos,
|
||||||
|
lineStart,
|
||||||
|
curLine
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
out += res.ch;
|
||||||
|
}
|
||||||
|
({
|
||||||
|
pos,
|
||||||
|
lineStart,
|
||||||
|
curLine
|
||||||
|
} = res);
|
||||||
|
chunkStart = pos;
|
||||||
|
} else if (ch === 8232 || ch === 8233) {
|
||||||
|
++pos;
|
||||||
|
++curLine;
|
||||||
|
lineStart = pos;
|
||||||
|
} else if (ch === 10 || ch === 13) {
|
||||||
|
if (type === "template") {
|
||||||
|
out += input.slice(chunkStart, pos) + "\n";
|
||||||
|
++pos;
|
||||||
|
if (ch === 13 && input.charCodeAt(pos) === 10) {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
++curLine;
|
||||||
|
chunkStart = lineStart = pos;
|
||||||
|
} else {
|
||||||
|
errors.unterminated(initialPos, initialLineStart, initialCurLine);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pos,
|
||||||
|
str: out,
|
||||||
|
firstInvalidLoc,
|
||||||
|
lineStart,
|
||||||
|
curLine,
|
||||||
|
containsInvalid: !!firstInvalidLoc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function isStringEnd(type, ch, input, pos) {
|
||||||
|
if (type === "template") {
|
||||||
|
return ch === 96 || ch === 36 && input.charCodeAt(pos + 1) === 123;
|
||||||
|
}
|
||||||
|
return ch === (type === "double" ? 34 : 39);
|
||||||
|
}
|
||||||
|
function readEscapedChar(input, pos, lineStart, curLine, inTemplate, errors) {
|
||||||
|
const throwOnInvalid = !inTemplate;
|
||||||
|
pos++;
|
||||||
|
const res = ch => ({
|
||||||
|
pos,
|
||||||
|
ch,
|
||||||
|
lineStart,
|
||||||
|
curLine
|
||||||
|
});
|
||||||
|
const ch = input.charCodeAt(pos++);
|
||||||
|
switch (ch) {
|
||||||
|
case 110:
|
||||||
|
return res("\n");
|
||||||
|
case 114:
|
||||||
|
return res("\r");
|
||||||
|
case 120:
|
||||||
|
{
|
||||||
|
let code;
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readHexChar(input, pos, lineStart, curLine, 2, false, throwOnInvalid, errors));
|
||||||
|
return res(code === null ? null : String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
case 117:
|
||||||
|
{
|
||||||
|
let code;
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors));
|
||||||
|
return res(code === null ? null : String.fromCodePoint(code));
|
||||||
|
}
|
||||||
|
case 116:
|
||||||
|
return res("\t");
|
||||||
|
case 98:
|
||||||
|
return res("\b");
|
||||||
|
case 118:
|
||||||
|
return res("\u000b");
|
||||||
|
case 102:
|
||||||
|
return res("\f");
|
||||||
|
case 13:
|
||||||
|
if (input.charCodeAt(pos) === 10) {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
case 10:
|
||||||
|
lineStart = pos;
|
||||||
|
++curLine;
|
||||||
|
case 8232:
|
||||||
|
case 8233:
|
||||||
|
return res("");
|
||||||
|
case 56:
|
||||||
|
case 57:
|
||||||
|
if (inTemplate) {
|
||||||
|
return res(null);
|
||||||
|
} else {
|
||||||
|
errors.strictNumericEscape(pos - 1, lineStart, curLine);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if (ch >= 48 && ch <= 55) {
|
||||||
|
const startPos = pos - 1;
|
||||||
|
const match = /^[0-7]+/.exec(input.slice(startPos, pos + 2));
|
||||||
|
let octalStr = match[0];
|
||||||
|
let octal = parseInt(octalStr, 8);
|
||||||
|
if (octal > 255) {
|
||||||
|
octalStr = octalStr.slice(0, -1);
|
||||||
|
octal = parseInt(octalStr, 8);
|
||||||
|
}
|
||||||
|
pos += octalStr.length - 1;
|
||||||
|
const next = input.charCodeAt(pos);
|
||||||
|
if (octalStr !== "0" || next === 56 || next === 57) {
|
||||||
|
if (inTemplate) {
|
||||||
|
return res(null);
|
||||||
|
} else {
|
||||||
|
errors.strictNumericEscape(startPos, lineStart, curLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res(String.fromCharCode(octal));
|
||||||
|
}
|
||||||
|
return res(String.fromCharCode(ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readHexChar(input, pos, lineStart, curLine, len, forceLen, throwOnInvalid, errors) {
|
||||||
|
const initialPos = pos;
|
||||||
|
let n;
|
||||||
|
({
|
||||||
|
n,
|
||||||
|
pos
|
||||||
|
} = readInt(input, pos, lineStart, curLine, 16, len, forceLen, false, errors, !throwOnInvalid));
|
||||||
|
if (n === null) {
|
||||||
|
if (throwOnInvalid) {
|
||||||
|
errors.invalidEscapeSequence(initialPos, lineStart, curLine);
|
||||||
|
} else {
|
||||||
|
pos = initialPos - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: n,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function readInt(input, pos, lineStart, curLine, radix, len, forceLen, allowNumSeparator, errors, bailOnError) {
|
||||||
|
const start = pos;
|
||||||
|
const forbiddenSiblings = radix === 16 ? forbiddenNumericSeparatorSiblings.hex : forbiddenNumericSeparatorSiblings.decBinOct;
|
||||||
|
const isAllowedSibling = radix === 16 ? isAllowedNumericSeparatorSibling.hex : radix === 10 ? isAllowedNumericSeparatorSibling.dec : radix === 8 ? isAllowedNumericSeparatorSibling.oct : isAllowedNumericSeparatorSibling.bin;
|
||||||
|
let invalid = false;
|
||||||
|
let total = 0;
|
||||||
|
for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
|
||||||
|
const code = input.charCodeAt(pos);
|
||||||
|
let val;
|
||||||
|
if (code === 95 && allowNumSeparator !== "bail") {
|
||||||
|
const prev = input.charCodeAt(pos - 1);
|
||||||
|
const next = input.charCodeAt(pos + 1);
|
||||||
|
if (!allowNumSeparator) {
|
||||||
|
if (bailOnError) return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine);
|
||||||
|
} else if (Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev) || forbiddenSiblings.has(next)) {
|
||||||
|
if (bailOnError) return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
errors.unexpectedNumericSeparator(pos, lineStart, curLine);
|
||||||
|
}
|
||||||
|
++pos;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (code >= 97) {
|
||||||
|
val = code - 97 + 10;
|
||||||
|
} else if (code >= 65) {
|
||||||
|
val = code - 65 + 10;
|
||||||
|
} else if (_isDigit(code)) {
|
||||||
|
val = code - 48;
|
||||||
|
} else {
|
||||||
|
val = Infinity;
|
||||||
|
}
|
||||||
|
if (val >= radix) {
|
||||||
|
if (val <= 9 && bailOnError) {
|
||||||
|
return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
} else if (val <= 9 && errors.invalidDigit(pos, lineStart, curLine, radix)) {
|
||||||
|
val = 0;
|
||||||
|
} else if (forceLen) {
|
||||||
|
val = 0;
|
||||||
|
invalid = true;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++pos;
|
||||||
|
total = total * radix + val;
|
||||||
|
}
|
||||||
|
if (pos === start || len != null && pos - start !== len || invalid) {
|
||||||
|
return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
n: total,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors) {
|
||||||
|
const ch = input.charCodeAt(pos);
|
||||||
|
let code;
|
||||||
|
if (ch === 123) {
|
||||||
|
++pos;
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readHexChar(input, pos, lineStart, curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, errors));
|
||||||
|
++pos;
|
||||||
|
if (code !== null && code > 0x10ffff) {
|
||||||
|
if (throwOnInvalid) {
|
||||||
|
errors.invalidCodePoint(pos, lineStart, curLine);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readHexChar(input, pos, lineStart, curLine, 4, false, throwOnInvalid, errors));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "@babel/helper-string-parser",
|
||||||
|
"version": "7.27.1",
|
||||||
|
"description": "A utility package to parse strings",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/babel/babel.git",
|
||||||
|
"directory": "packages/babel-helper-string-parser"
|
||||||
|
},
|
||||||
|
"homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"charcodes": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"author": "The Babel Team (https://babel.dev/team)",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# @babel/helper-validator-identifier
|
||||||
|
|
||||||
|
> Validate identifier/keywords name
|
||||||
|
|
||||||
|
See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save @babel/helper-validator-identifier
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/helper-validator-identifier
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.isIdentifierChar = isIdentifierChar;
|
||||||
|
exports.isIdentifierName = isIdentifierName;
|
||||||
|
exports.isIdentifierStart = isIdentifierStart;
|
||||||
|
let nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u0870-\u0887\u0889-\u088e\u08a0-\u08c9\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c5d\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cdd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u1711\u171f-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4c\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c8a\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7cd\ua7d0\ua7d1\ua7d3\ua7d5-\ua7dc\ua7f2-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";
|
||||||
|
let nonASCIIidentifierChars = "\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0897-\u089f\u08ca-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3c\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0cf3\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ece\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u180f-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1ace\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\u30fb\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f\uff65";
|
||||||
|
const nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
|
||||||
|
const nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
|
||||||
|
nonASCIIidentifierStartChars = nonASCIIidentifierChars = null;
|
||||||
|
const astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 2, 60, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 328, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 129, 74, 6, 0, 67, 12, 65, 1, 2, 0, 29, 6135, 9, 1237, 42, 9, 8936, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 496, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4153, 7, 221, 3, 5761, 15, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 4191];
|
||||||
|
const astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 80, 3, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 343, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 330, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 726, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];
|
||||||
|
function isInAstralSet(code, set) {
|
||||||
|
let pos = 0x10000;
|
||||||
|
for (let i = 0, length = set.length; i < length; i += 2) {
|
||||||
|
pos += set[i];
|
||||||
|
if (pos > code) return false;
|
||||||
|
pos += set[i + 1];
|
||||||
|
if (pos >= code) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function isIdentifierStart(code) {
|
||||||
|
if (code < 65) return code === 36;
|
||||||
|
if (code <= 90) return true;
|
||||||
|
if (code < 97) return code === 95;
|
||||||
|
if (code <= 122) return true;
|
||||||
|
if (code <= 0xffff) {
|
||||||
|
return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
return isInAstralSet(code, astralIdentifierStartCodes);
|
||||||
|
}
|
||||||
|
function isIdentifierChar(code) {
|
||||||
|
if (code < 48) return code === 36;
|
||||||
|
if (code < 58) return true;
|
||||||
|
if (code < 65) return false;
|
||||||
|
if (code <= 90) return true;
|
||||||
|
if (code < 97) return code === 95;
|
||||||
|
if (code <= 122) return true;
|
||||||
|
if (code <= 0xffff) {
|
||||||
|
return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes);
|
||||||
|
}
|
||||||
|
function isIdentifierName(name) {
|
||||||
|
let isFirst = true;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
let cp = name.charCodeAt(i);
|
||||||
|
if ((cp & 0xfc00) === 0xd800 && i + 1 < name.length) {
|
||||||
|
const trail = name.charCodeAt(++i);
|
||||||
|
if ((trail & 0xfc00) === 0xdc00) {
|
||||||
|
cp = 0x10000 + ((cp & 0x3ff) << 10) + (trail & 0x3ff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isFirst) {
|
||||||
|
isFirst = false;
|
||||||
|
if (!isIdentifierStart(cp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!isIdentifierChar(cp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !isFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=identifier.js.map
|
||||||
1
node_modules/@babel/helper-validator-identifier/lib/identifier.js.map
generated
vendored
Normal file
1
node_modules/@babel/helper-validator-identifier/lib/identifier.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,57 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isIdentifierChar", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _identifier.isIdentifierChar;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isIdentifierName", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _identifier.isIdentifierName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isIdentifierStart", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _identifier.isIdentifierStart;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isKeyword", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isKeyword;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isStrictBindOnlyReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isStrictBindOnlyReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isStrictBindReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isStrictBindReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isStrictReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isStrictReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var _identifier = require("./identifier.js");
|
||||||
|
var _keyword = require("./keyword.js");
|
||||||
|
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"names":["_identifier","require","_keyword"],"sources":["../src/index.ts"],"sourcesContent":["export {\n isIdentifierName,\n isIdentifierChar,\n isIdentifierStart,\n} from \"./identifier.ts\";\nexport {\n isReservedWord,\n isStrictBindOnlyReservedWord,\n isStrictBindReservedWord,\n isStrictReservedWord,\n isKeyword,\n} from \"./keyword.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,WAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA","ignoreList":[]}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.isKeyword = isKeyword;
|
||||||
|
exports.isReservedWord = isReservedWord;
|
||||||
|
exports.isStrictBindOnlyReservedWord = isStrictBindOnlyReservedWord;
|
||||||
|
exports.isStrictBindReservedWord = isStrictBindReservedWord;
|
||||||
|
exports.isStrictReservedWord = isStrictReservedWord;
|
||||||
|
const reservedWords = {
|
||||||
|
keyword: ["break", "case", "catch", "continue", "debugger", "default", "do", "else", "finally", "for", "function", "if", "return", "switch", "throw", "try", "var", "const", "while", "with", "new", "this", "super", "class", "extends", "export", "import", "null", "true", "false", "in", "instanceof", "typeof", "void", "delete"],
|
||||||
|
strict: ["implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"],
|
||||||
|
strictBind: ["eval", "arguments"]
|
||||||
|
};
|
||||||
|
const keywords = new Set(reservedWords.keyword);
|
||||||
|
const reservedWordsStrictSet = new Set(reservedWords.strict);
|
||||||
|
const reservedWordsStrictBindSet = new Set(reservedWords.strictBind);
|
||||||
|
function isReservedWord(word, inModule) {
|
||||||
|
return inModule && word === "await" || word === "enum";
|
||||||
|
}
|
||||||
|
function isStrictReservedWord(word, inModule) {
|
||||||
|
return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);
|
||||||
|
}
|
||||||
|
function isStrictBindOnlyReservedWord(word) {
|
||||||
|
return reservedWordsStrictBindSet.has(word);
|
||||||
|
}
|
||||||
|
function isStrictBindReservedWord(word, inModule) {
|
||||||
|
return isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word);
|
||||||
|
}
|
||||||
|
function isKeyword(word) {
|
||||||
|
return keywords.has(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=keyword.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"names":["reservedWords","keyword","strict","strictBind","keywords","Set","reservedWordsStrictSet","reservedWordsStrictBindSet","isReservedWord","word","inModule","isStrictReservedWord","has","isStrictBindOnlyReservedWord","isStrictBindReservedWord","isKeyword"],"sources":["../src/keyword.ts"],"sourcesContent":["const reservedWords = {\n keyword: [\n \"break\",\n \"case\",\n \"catch\",\n \"continue\",\n \"debugger\",\n \"default\",\n \"do\",\n \"else\",\n \"finally\",\n \"for\",\n \"function\",\n \"if\",\n \"return\",\n \"switch\",\n \"throw\",\n \"try\",\n \"var\",\n \"const\",\n \"while\",\n \"with\",\n \"new\",\n \"this\",\n \"super\",\n \"class\",\n \"extends\",\n \"export\",\n \"import\",\n \"null\",\n \"true\",\n \"false\",\n \"in\",\n \"instanceof\",\n \"typeof\",\n \"void\",\n \"delete\",\n ],\n strict: [\n \"implements\",\n \"interface\",\n \"let\",\n \"package\",\n \"private\",\n \"protected\",\n \"public\",\n \"static\",\n \"yield\",\n ],\n strictBind: [\"eval\", \"arguments\"],\n};\nconst keywords = new Set(reservedWords.keyword);\nconst reservedWordsStrictSet = new Set(reservedWords.strict);\nconst reservedWordsStrictBindSet = new Set(reservedWords.strictBind);\n\n/**\n * Checks if word is a reserved word in non-strict mode\n */\nexport function isReservedWord(word: string, inModule: boolean): boolean {\n return (inModule && word === \"await\") || word === \"enum\";\n}\n\n/**\n * Checks if word is a reserved word in non-binding strict mode\n *\n * Includes non-strict reserved words\n */\nexport function isStrictReservedWord(word: string, inModule: boolean): boolean {\n return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode, but it is allowed as\n * a normal identifier.\n */\nexport function isStrictBindOnlyReservedWord(word: string): boolean {\n return reservedWordsStrictBindSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode\n *\n * Includes non-strict reserved words and non-binding strict reserved words\n */\nexport function isStrictBindReservedWord(\n word: string,\n inModule: boolean,\n): boolean {\n return (\n isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word)\n );\n}\n\nexport function isKeyword(word: string): boolean {\n return keywords.has(word);\n}\n"],"mappings":";;;;;;;;;;AAAA,MAAMA,aAAa,GAAG;EACpBC,OAAO,EAAE,CACP,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,UAAU,EACV,SAAS,EACT,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EACL,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,KAAK,EACL,MAAM,EACN,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,MAAM,EACN,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,QAAQ,CACT;EACDC,MAAM,EAAE,CACN,YAAY,EACZ,WAAW,EACX,KAAK,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,OAAO,CACR;EACDC,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW;AAClC,CAAC;AACD,MAAMC,QAAQ,GAAG,IAAIC,GAAG,CAACL,aAAa,CAACC,OAAO,CAAC;AAC/C,MAAMK,sBAAsB,GAAG,IAAID,GAAG,CAACL,aAAa,CAACE,MAAM,CAAC;AAC5D,MAAMK,0BAA0B,GAAG,IAAIF,GAAG,CAACL,aAAa,CAACG,UAAU,CAAC;AAK7D,SAASK,cAAcA,CAACC,IAAY,EAAEC,QAAiB,EAAW;EACvE,OAAQA,QAAQ,IAAID,IAAI,KAAK,OAAO,IAAKA,IAAI,KAAK,MAAM;AAC1D;AAOO,SAASE,oBAAoBA,CAACF,IAAY,EAAEC,QAAiB,EAAW;EAC7E,OAAOF,cAAc,CAACC,IAAI,EAAEC,QAAQ,CAAC,IAAIJ,sBAAsB,CAACM,GAAG,CAACH,IAAI,CAAC;AAC3E;AAMO,SAASI,4BAA4BA,CAACJ,IAAY,EAAW;EAClE,OAAOF,0BAA0B,CAACK,GAAG,CAACH,IAAI,CAAC;AAC7C;AAOO,SAASK,wBAAwBA,CACtCL,IAAY,EACZC,QAAiB,EACR;EACT,OACEC,oBAAoB,CAACF,IAAI,EAAEC,QAAQ,CAAC,IAAIG,4BAA4B,CAACJ,IAAI,CAAC;AAE9E;AAEO,SAASM,SAASA,CAACN,IAAY,EAAW;EAC/C,OAAOL,QAAQ,CAACQ,GAAG,CAACH,IAAI,CAAC;AAC3B","ignoreList":[]}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "@babel/helper-validator-identifier",
|
||||||
|
"version": "7.27.1",
|
||||||
|
"description": "Validate identifier/keywords name",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/babel/babel.git",
|
||||||
|
"directory": "packages/babel-helper-validator-identifier"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@unicode/unicode-16.0.0": "^1.0.0",
|
||||||
|
"charcodes": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"author": "The Babel Team (https://babel.dev/team)",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# @babel/parser
|
||||||
|
|
||||||
|
> A JavaScript parser
|
||||||
|
|
||||||
|
See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save-dev @babel/parser
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/parser --dev
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable no-var, unicorn/prefer-node-protocol */
|
||||||
|
|
||||||
|
var parser = require("..");
|
||||||
|
var fs = require("fs");
|
||||||
|
|
||||||
|
var filename = process.argv[2];
|
||||||
|
if (!filename) {
|
||||||
|
console.error("no filename specified");
|
||||||
|
} else {
|
||||||
|
var file = fs.readFileSync(filename, "utf8");
|
||||||
|
var ast = parser.parse(file);
|
||||||
|
|
||||||
|
console.log(JSON.stringify(ast, null, " "));
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue