初始化商家app代码

This commit is contained in:
丁杰 2026-03-24 11:45:13 +08:00
commit 1aa732ad02
7795 changed files with 1571629 additions and 0 deletions

299
App.vue Normal file
View File

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

27
Info.plist Normal file
View File

@ -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
README.md Normal file
View File

32
androidPrivacy.json Normal file
View File

@ -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"
}
}
}

34
api/index.js Normal file
View File

@ -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('')
}
}
// 其他接口...

5
app.miniapp.json Normal file
View File

@ -0,0 +1,5 @@
{
"adapteByMiniprogram": {
"userName": "gh_89c92e18b70b"
}
}

View File

@ -0,0 +1,11 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "wx5b1c0c8d01ded35d",
"paths": ["*"]
}
]
}
}

View File

@ -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 // 1MBMD5
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>

View File

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

111
components/MyTabBar.vue Normal file
View File

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

View File

@ -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. xy
// 211170 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>

View File

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

View File

@ -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);
// filefile
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>

View File

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

View File

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

View File

@ -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=1limit=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 {
// $nextTickDOM
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>

View File

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

View File

@ -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: "",
},
// Iconrpx
iconWidth: {
type: Number,
default: 96,
},
// Iconrpx
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>

View File

@ -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">&#xe679;</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>

View File

@ -0,0 +1,2 @@
## 1.0.02024-10-10
1.0.0

View File

@ -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) { // 0id
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;
},
// IDsequence
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>

View File

@ -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"
}
}
}
}
}

View File

@ -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>
```

View File

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

View File

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

View File

@ -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]; //
//endmove
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]; //
//endmove
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) {
//isOktrueisIndexGo
//endmove
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>

View File

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

View File

@ -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, '&nbsp;');
// textareaviewtextarea<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
// 100px100px
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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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, //01
},
},
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>

View File

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

View File

@ -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
},
// nvuevue
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) {
// // 101
// 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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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; // 240
//
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>

View File

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

View File

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

View File

@ -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, //01
startUpload: false, //
down_type: null, //12
apkUpdateContent: "", //
updateType: "", //12
versionsFlag: 1, //123
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>

View File

@ -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({
// // yourRegionBucket1yourRegionoss-cn-hangzhou
// region: "oss-cn-changzhou",
// authorizationV4: true,
// // STS访AccessKey IDAccessKey Secret
// accessKeyId: "LTAI5tGsiEK2Zuat4bJSHXs9",
// accessKeySecret: "x9ToLlfdcgg2sGnyfVpQWGVLfEdG3E",
// // STSSecurityToken
// 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 {
// // ObjectObjectBucket
// // exampleobject.txtexampledir/exampleobject.txtBucketBucket
// // datafileBlobOSS 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>

View File

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

View File

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

View File

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

View File

@ -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 | 设置水印数量 |

View File

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

20
index.html Normal file
View File

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

43
main.js Normal file
View File

@ -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";

245
manifest.json Normal file
View File

@ -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 */

242
mixins/mixins.js Normal file
View File

@ -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
},
};

View File

@ -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"}}]}

View File

@ -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"}}

View File

@ -0,0 +1,5 @@
{
"VIVO_APPKEY": "aa27f4e4839fd1345bf57025778df558 ",
"VIVO_APPID": "105916679",
"HONOR_APPID":"104505186"
}

View File

@ -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.

View File

@ -0,0 +1,3 @@
{
"businessID":"47959"
}

1
node_modules/.bin/mime generated vendored Symbolic link
View File

@ -0,0 +1 @@
../mime/cli.js

1
node_modules/.bin/mkdirp generated vendored Symbolic link
View File

@ -0,0 +1 @@
../mkdirp/bin/cmd.js

1
node_modules/.bin/nanoid generated vendored Symbolic link
View File

@ -0,0 +1 @@
../nanoid/bin/nanoid.cjs

1
node_modules/.bin/os-name generated vendored Symbolic link
View File

@ -0,0 +1 @@
../os-name/cli.js

1
node_modules/.bin/osx-release generated vendored Symbolic link
View File

@ -0,0 +1 @@
../osx-release/cli.js

1
node_modules/.bin/parser generated vendored Symbolic link
View File

@ -0,0 +1 @@
../@babel/parser/bin/babel-parser.js

1
node_modules/.bin/semver generated vendored Symbolic link
View File

@ -0,0 +1 @@
../semver/bin/semver

1413
node_modules/.package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

22
node_modules/@babel/helper-string-parser/LICENSE generated vendored Normal file
View File

@ -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.

19
node_modules/@babel/helper-string-parser/README.md generated vendored Normal file
View File

@ -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
```

295
node_modules/@babel/helper-string-parser/lib/index.js generated vendored Normal file
View File

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

31
node_modules/@babel/helper-string-parser/package.json generated vendored Normal file
View File

@ -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"
}

View File

@ -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.

View File

@ -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
```

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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":[]}

View File

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

View File

@ -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":[]}

View File

@ -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"
}

1073
node_modules/@babel/parser/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

19
node_modules/@babel/parser/LICENSE generated vendored Normal file
View File

@ -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.

19
node_modules/@babel/parser/README.md generated vendored Normal file
View File

@ -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
```

15
node_modules/@babel/parser/bin/babel-parser.js generated vendored Executable file
View File

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