初始化商家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