项目初始化

This commit is contained in:
jerry
2025-01-21 01:46:34 +08:00
parent 364021b042
commit 48153e7761
962 changed files with 172070 additions and 0 deletions

27
.env Normal file
View File

@@ -0,0 +1,27 @@
# 版本号
SHOPRO_VERSION = v1.8.3
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development
SHOPRO_BASE_URL = http://localhost:48080
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development
SHOPRO_DEV_BASE_URL = http://127.0.0.1:48080
### SHOPRO_DEV_BASE_URL = https://peiwan.mfzt.site
# 后端接口前缀(一般不建议调整)
SHOPRO_API_PATH = /app-api
# 后端 websocket 接口前缀
SHOPRO_WEBSOCKET_PATH = /infra/ws
# 开发环境运行端口
SHOPRO_DEV_PORT = 3000
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
SHOPRO_STATIC_URL = https://file.sheepjs.com
# 是否开启直播 1 开启直播 | 0 关闭直播 (小程序官方后台未审核开通直播权限时请勿开启)
SHOPRO_MPLIVE_ON = 0
# 租户ID 默认 1
SHOPRO_TENANT_ID = 1

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
unpackage/*
node_modules/*
.idea/*
deploy.sh
.hbuilderx/
.vscode/
**/.DS_Store
yarn.lock
package-lock.json
*.keystore
pnpm-lock.yaml

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
/unpackage/*
/node_modules/**
/uni_modules/**
/public/*
**/*.svg
**/*.sh

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"printWidth": 100,
"semi": true,
"vueIndentScriptAndStyle": true,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"htmlWhitespaceSensitivity": "strict",
"endOfLine": "auto"
}

129
App.vue Normal file
View File

@@ -0,0 +1,129 @@
<script setup>
import { onLaunch, onShow, onHide, onUnload, onError } from '@dcloudio/uni-app';
import { ShoproInit } from './sheep';
import { useWebSocket } from '@/sheep/hooks/useWebSocket';
import { WebSocketMessageTypeConstants } from '@/sheep/util/const';
onLaunch(() => {
// 隐藏原生导航栏 使用自定义底部导航
//uni.hideTabBar();
// 加载Shopro底层依赖
ShoproInit();
});
onError((err) => {
console.log('AppOnError:', err);
});
onShow((options) => {
// #ifdef APP-PLUS
// 获取urlSchemes参数
const args = plus.runtime.arguments;
if (args) {
}
// 获取剪贴板
uni.getClipboardData({
success: (res) => { },
});
// #endif
// #ifdef MP-WEIXIN
// 确认收货回调结果
console.log(options,'options');
// #endif
// 禁止截屏
if (wx.setVisualEffectOnCapture) {
wx.setVisualEffectOnCapture({
visualEffect: 'hidden',
complete: function(res) {}
})
}
if (wx.setVisualEffectOnCapture) {
let that = this
wx.onUserCaptureScreen(function (res) {
uni.showToast({
icon: 'none',
title: '禁止截屏!',
position: 'bottom'
})
that.flushed()
})
}
console.log('App Show')
});
onHide(() => {
if (wx.setVisualEffectOnCapture) {
wx.setVisualEffectOnCapture({
visualEffect: 'none',
complete: function(res) {}
})
}
});
onUnload(() => {
if (wx.setVisualEffectOnCapture) {
wx.setVisualEffectOnCapture({
visualEffect: 'none',
complete: function(res) {}
})
}
});
//======================= 聊天工具相关 end =======================
/* const { options } = useWebSocket({
// 连接成功
onConnected: async () => {
console.log('连接成功');
},
// 收到消息
onMessage: async (data) => {
console.log('接收消息');
const type = data.type;
if (!type) {
console.error('未知的消息类型:' + data.value);
return;
}
if (type == WebSocketMessageTypeConstants.IM_MESSAGE_READ) {
console.log('刷新消息');
setTimeout(function(){
uni.$emit(WebSocketMessageTypeConstants.IM_MESSAGE_READ, data);
},1000);
return;
}
if (type == WebSocketMessageTypeConstants.IM_MESSAGE_NEWS) {
uni.$emit(WebSocketMessageTypeConstants.IM_MESSAGE_NEWS, data);
return;
}
// 2.2 消息类型KEFU_MESSAGE_TYPE
if (type == WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
console.log('刷新消息');
// 刷新消息列表
//await messageListRef.value.refreshMessageList(JSON.parse(data.content));
return;
}
// 2.3 消息类型KEFU_MESSAGE_ADMIN_READ
if (type == WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
console.log('管理员已读消息');
}
},
}); */
</script>
<style lang="scss">
@import '@/sheep/scss/index.scss';
@import "@/static/css/iconfont.css";
@import "@/uni_modules/vk-uview-ui/index.scss";
::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
color: transparent !important;
display: none;
}
</style>

3
androidPrivacy.json Normal file
View File

@@ -0,0 +1,3 @@
{
"prompt" : "template"
}

View File

@@ -0,0 +1,89 @@
<!-- z-paging自定义的点击返回顶部view -->
<template>
<view class="back-to-top">
<view class="container">
<u-icon name="fanhuidingbu" color="#555666" size="50" custom-prefix="iconfont"></u-icon>
</view>
</view>
</template>
<script>
export default {
props: {
// 当前加载了几页
current: {
type: Number,
default: function() {
return 0;
},
},
// 一共有多少页可以加载
total: {
type: Number,
default: function() {
return 0;
},
},
},
data() {
return {
// 0 返回返回顶部图片 1显示current/total
type: 0,
// 当前定时器
timeout: null
};
},
methods: {
}
}
</script>
<style scoped>
.back-to-top {
/* 这里请设置100%填充满返回顶部按钮 */
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: white;
border-radius: 50%;
background-color: white;
box-shadow: 0 0 20rpx #cccccc;
background-color: white;
}
.container {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
}
.count-text {
font-size: 24rpx;
color: #555555;
font-weight: bold;
}
.line {
width: 55rpx;
height: 1rpx;
background-color: #dedede;
}
.back-img {
width: 50rpx;
height: 50rpx;
}
</style>

View File

@@ -0,0 +1,831 @@
<template>
<view class="htz-image-upload-list">
<view class="htz-image-upload-Item" v-for="(item,index) in uploadLists" :key="index">
<view class="htz-image-upload-Item-video" v-if="isVideo(item)">
<!-- #ifndef APP-PLUS -->
<video :disabled="false" :controls="false" :src="getFileUrl(item)">
<cover-view class="htz-image-upload-Item-video-fixed" @tap.stop="previewVideo(getFileUrl(item))">
</cover-view>
<cover-view class="htz-image-upload-Item-del-cover" v-if="remove && previewVideoSrc==''" @tap.stop="imgDel(index)">×</cover-view>
</video>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view class="htz-image-upload-Item-video-fixed" @tap.stop="previewVideo(getFileUrl(item))"></view>
<image v-if="dataType==1 && item.cover" class="htz-image-upload-Item-video-app-poster" mode="widthFix" :src="item.cover"></image>
<image v-else class="htz-image-upload-Item-video-app-poster" mode="widthFix" :src="appVideoPoster"></image>
<!-- #endif -->
</view>
<image v-else :src="getFileUrl(item)" @tap.stop="imgPreview(getFileUrl(item))"></image>
<!-- #ifdef APP-PLUS -->
<view class="htz-image-upload-Item-del" v-if="remove" @tap.stop="imgDel(index)">×</view>
<!-- #endif -->
</view>
<view class="htz-image-upload-Item htz-image-upload-Item-add" v-if="uploadLists.length<max && add" @click="chooseFile">
+
</view>
<view class="preview-full" v-if="previewVideoSrc!=''">
<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
<cover-view class="preview-full-close" @click="previewVideoClose"> ×
</cover-view>
</video>
</view>
<!-- -->
</view>
</template>
<style>
.ceshi {
width: 100%;
height: 100%;
position: relative;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #FFFFFF;
color: #2C405A;
opacity: 0.5;
z-index: 100;
}
</style>
<script>
export default {
name: 'htz-image-upload',
props: {
max: { //展示图片最大值
type: Number,
default: 1,
},
chooseNum: { //选择图片数
type: Number,
default: 9,
},
name: { //发到后台的文件参数名
type: String,
default: 'file',
},
dataType: { //v-model的数据结构类型
type: Number,
default: 0, // 0: ['http://xxxx.jpg','http://xxxx.jpg'] 1:[{type:0,url:'http://xxxx.jpg'}] type 0 图片 1 视频 url 文件地址 此类型是为了给没有文件后缀的状况使用的
},
remove: { //是否展示删除按钮
type: Boolean,
default: true,
},
add: { //是否展示添加按钮
type: Boolean,
default: true,
},
disabled: { //是否禁用
type: Boolean,
default: false,
},
sourceType: { //选择照片来源 【psH5就别费劲了设置了也没用。不是我说的官方文档就这样
type: Array,
default: () => ['album', 'camera'],
},
action: { //上传地址 如需使用uniCloud服务设置为uniCloud即可
type: String,
default: '',
},
headers: { //上传的请求头部
type: Object,
default: () => {},
},
formData: { //HTTP 请求中其他额外的 form data
type: Object,
default: () => {},
},
compress: { //是否需要压缩
type: Boolean,
default: true,
},
quality: { //压缩质量范围0100
type: Number,
default: 80,
},
// #ifndef VUE3
value: { //受控图片列表
type: Array,
default: () => [],
},
// #endif
// #ifdef VUE3
modelValue: { //受控图片列表
type: Array,
default: () => [],
},
// #endif
uploadSuccess: {
default: (res) => {
return {
success: false,
url: ''
}
},
},
mediaType: { //文件类型 image/video/all
type: String,
default: 'image',
},
maxDuration: { //拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。 (只针对拍摄视频有用)
type: Number,
default: 60,
},
camera: { //'front'、'back',默认'back'(只针对拍摄视频有用)
type: String,
default: 'back',
},
appVideoPoster: { //app端视频展示封面 只对app有效
type: String,
default: '/static/htz-image-upload/play.png',
},
},
data() {
return {
uploadLists: [],
mediaTypeData: ['image', 'video', 'all'],
previewVideoSrc: '',
}
},
mounted: function() {
this.$nextTick(function() {
// #ifndef VUE3
this.uploadLists = this.value;
// #endif
// #ifdef VUE3
this.uploadLists = this.modelValue;
// #endif
if (this.mediaTypeData.indexOf(this.mediaType) == -1) {
uni.showModal({
title: '提示',
content: 'mediaType参数不正确',
showCancel: false,
success: function(res) {
if (res.confirm) {
//console.log('用户点击确定');
} else if (res.cancel) {
//console.log('用户点击取消');
}
}
});
}
});
},
watch: {
// #ifndef VUE3
value(val, oldVal) {
//console.log('value',val, oldVal)
this.uploadLists = val;
},
// #endif
// #ifdef VUE3
modelValue(val, oldVal) {
//console.log('value',val, oldVal)
this.uploadLists = val;
},
// #endif
},
methods: {
isVideo(item) {
let isPass = false
if ((!/.(gif|jpg|jpeg|png|gif|jpg|png)$/i.test(item) && this.dataType == 0) || (this.dataType == 1 && item
.type == 1)) {
isPass = true
}
return isPass
},
getFileUrl(item) {
var url = item;
if (this.dataType == 1) {
url = item.url
}
//console.log('url', url)
return url
},
previewVideo(src) {
this.previewVideoSrc = src;
// this.previewVideoSrc =
// 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-fbd63a76-dc76-485c-b711-f79f2986daeb/ba804d82-860b-4d1a-a706-5a4c8ce137c3.mp4'
},
previewVideoClose() {
this.previewVideoSrc = ''
//console.log('previewVideoClose', this.previewVideoSrc)
},
imgDel(index) {
// this.uploadLists.splice(index, 1)
// this.$emit("input", this.uploadLists);
// this.$emit("imgDelete", this.uploadLists);
let delUrl = this.uploadLists[index]
this.uploadLists.splice(index, 1)
// #ifndef VUE3
this.$emit("input", this.uploadLists);
// #endif
// #ifdef VUE3
this.$emit("update:modelValue", this.uploadLists);
// #endif
this.$emit("imgDelete", {
del: delUrl,
tempFilePaths: this.uploadLists
});
},
imgPreview(index) {
var imgData = []
this.uploadLists.forEach(item => {
if (!this.isVideo(item)) {
imgData.push(this.getFileUrl(item))
}
})
//console.log('imgPreview', imgData)
uni.previewImage({
urls: imgData,
current: index,
loop: true,
});
},
chooseFile() {
if (this.disabled) {
return false;
}
switch (this.mediaTypeData.indexOf(this.mediaType)) {
case 1: //视频
this.videoAdd();
break;
case 2: //全部
uni.showActionSheet({
itemList: ['相册', '视频'],
success: (res) => {
if (res.tapIndex == 1) {
this.videoAdd();
} else if (res.tapIndex == 0) {
this.imgAdd();
}
},
fail: (res) => {
console.log(res.errMsg);
}
});
break;
default: //图片
this.imgAdd();
break;
}
//if(this.mediaType=='image'){
},
videoAdd() {
//console.log('videoAdd')
let nowNum = Math.abs(this.uploadLists.length - this.max);
let thisNum = (this.chooseNum > nowNum ? nowNum : this.chooseNum) //可选数量
uni.chooseVideo({
compressed: this.compress,
sourceType: this.sourceType,
camera: this.camera,
maxDuration: this.maxDuration,
success: (res) => {
// console.log('videoAdd', res)
// console.log(res.tempFilePath)
this.chooseSuccessMethod([res.tempFilePath], 1)
//this.imgUpload([res.tempFilePath]);
//console.log('tempFiles', res)
// if (this.action == '') { //未配置上传路径
// this.$emit("chooseSuccess", res.tempFilePaths);
// } else {
// if (this.compress && (res.tempFiles[0].size / 1024 > 1025)) { //设置了需要压缩 并且 文件大于1M进行压缩上传
// this.imgCompress(res.tempFilePaths);
// } else {
// this.imgUpload(res.tempFilePaths);
// }
// }
}
});
},
imgAdd() {
//console.log('imgAdd')
let nowNum = Math.abs(this.uploadLists.length - this.max);
let thisNum = (this.chooseNum > nowNum ? nowNum : this.chooseNum) //可选数量
//console.log('nowNum', nowNum)
//console.log('thisNum', thisNum)
// #ifdef APP-PLUS
if (this.sourceType.length > 1) {
uni.showActionSheet({
itemList: ['拍摄', '从手机相册选择'],
success: (res) => {
if (res.tapIndex == 1) {
this.appGallery(thisNum);
} else if (res.tapIndex == 0) {
this.appCamera();
}
},
fail: (res) => {
console.log(res.errMsg);
}
});
}
if (this.sourceType.length == 1 && this.sourceType.indexOf('album') > -1) {
this.appGallery(thisNum);
}
if (this.sourceType.length == 1 && this.sourceType.indexOf('camera') > -1) {
this.appCamera();
}
// #endif
//#ifndef APP-PLUS
uni.chooseImage({
count: thisNum,
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: this.sourceType,
success: (res) => {
this.chooseSuccessMethod(res.tempFilePaths, 0)
//console.log('tempFiles', res)
// if (this.action == '') { //未配置上传路径
// this.$emit("chooseSuccess", res.tempFilePaths);
// } else {
// if (this.compress && (res.tempFiles[0].size / 1024 > 1025)) { //设置了需要压缩 并且 文件大于1M进行压缩上传
// this.imgCompress(res.tempFilePaths);
// } else {
// this.imgUpload(res.tempFilePaths);
// }
// }
}
});
// #endif
},
appCamera() {
var cmr = plus.camera.getCamera();
var res = cmr.supportedImageResolutions[0];
var fmt = cmr.supportedImageFormats[0];
//console.log("Resolution: " + res + ", Format: " + fmt);
cmr.captureImage((path) => {
//alert("Capture image success: " + path);
this.chooseSuccessMethod([path], 0)
},
(error) => {
//alert("Capture image failed: " + error.message);
console.log("Capture image failed: " + error.message)
}, {
resolution: res,
format: fmt
}
);
},
appGallery(maxNum) {
plus.gallery.pick((res) => {
this.chooseSuccessMethod(res.files, 0)
}, function(e) {
//console.log("取消选择图片");
}, {
filter: "image",
multiple: true,
maximum: maxNum
});
},
chooseSuccessMethod(filePaths, type) {
if (this.action == '') { //未配置上传路径
this.$emit("chooseSuccess", filePaths, type); //filePaths 路径 type 0 为图片 1为视频
} else {
if (type == 1) {
this.imgUpload(filePaths, type);
} else {
if (this.compress) { //设置了需要压缩
this.imgCompress(filePaths, type);
} else {
this.imgUpload(filePaths, type);
}
}
}
},
imgCompress(tempFilePaths, type) { //type 0 为图片 1为视频
uni.showLoading({
title: '压缩中...'
});
let compressImgs = [];
let results = [];
tempFilePaths.forEach((item, index) => {
compressImgs.push(new Promise((resolve, reject) => {
// #ifndef H5
uni.compressImage({
src: item,
quality: this.quality,
success: res => {
//console.log('compressImage', res.tempFilePath)
results.push(res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (err) => {
//console.log(err.errMsg);
reject(err);
},
complete: () => {
//uni.hideLoading();
}
})
// #endif
// #ifdef H5
this.canvasDataURL(item, {
quality: this.quality / 100
}, (base64Codes) => {
//this.imgUpload(base64Codes);
results.push(base64Codes);
resolve(base64Codes);
})
// #endif
}))
})
Promise.all(compressImgs) //执行所有需请求的接口
.then((results) => {
uni.hideLoading();
//console.log('imgCompress', type)
this.imgUpload(results, type);
})
.catch((res, object) => {
uni.hideLoading();
});
},
imgUpload(tempFilePaths, type) { //type 0 为图片 1为视频
// if (this.action == '') {
// uni.showToast({
// title: '未配置上传地址',
// icon: 'none',
// duration: 2000
// });
// return false;
// }
if (this.action == 'uniCloud') {
this.uniCloudUpload(tempFilePaths, type)
return
}
uni.showLoading({
title: '上传中'
});
//console.log('imgUpload', tempFilePaths)
let uploadImgs = [];
tempFilePaths.forEach((item, index) => {
uploadImgs.push(new Promise((resolve, reject) => {
//console.log(index, item)
const uploadTask = uni.uploadFile({
url: this.action, //仅为示例,非真实的接口地址
filePath: item,
name: this.name,
fileType: 'image',
formData: this.formData,
header: this.headers,
success: (uploadFileRes) => {
//uni.hideLoading();
//console.log(typeof this.uploadSuccess)
//console.log('')
uploadFileRes.fileType = type
if (typeof this.uploadSuccess == 'function') {
let thisUploadSuccess = this.uploadSuccess(
uploadFileRes)
if (thisUploadSuccess.success) {
let keyName = '';
// #ifndef VUE3
keyName = 'value'
// #endif
// #ifdef VUE3
keyName = 'modelValue'
// #endif
if (this.dataType == 0) {
this[keyName].push(thisUploadSuccess.url)
} else {
this[keyName].push({
type: type,
url: thisUploadSuccess.url,
...thisUploadSuccess
})
}
//this.$emit("input", this.uploadLists);
// #ifndef VUE3
this.$emit("input", this.uploadLists);
// #endif
// #ifdef VUE3
this.$emit("update:modelValue", this.uploadLists);
// #endif
}
}
resolve(uploadFileRes);
this.$emit("uploadSuccess", uploadFileRes);
},
fail: (err) => {
console.log(err);
//uni.hideLoading();
reject(err);
this.$emit("uploadFail", err);
},
complete: () => {
//uni.hideLoading();
}
});
}))
})
Promise.all(uploadImgs) //执行所有需请求的接口
.then((results) => {
uni.hideLoading();
})
.catch((res, object) => {
uni.hideLoading();
this.$emit("uploadFail", res);
});
// uploadTask.onProgressUpdate((res) => {
// //console.log('',)
// uni.showLoading({
// title: '上传中' + res.progress + '%'
// });
// if (res.progress == 100) {
// uni.hideLoading();
// }
// });
},
uniCloudUpload(tempFilePaths, type) {
uni.showLoading({
title: '上传中'
});
console.log('uniCloudUpload', tempFilePaths);
let uploadImgs = [];
tempFilePaths.forEach((item, index) => {
uploadImgs.push(new Promise((resolve, reject) => {
uniCloud.uploadFile({
filePath: item,
cloudPath: this.guid() + '.' + this.getFileType(item, type),
success(uploadFileRes) {
if (uploadFileRes.success) {
resolve(uploadFileRes.fileID);
}
},
fail(err) {
console.log(err);
reject(err);
},
complete() {}
});
}))
})
Promise.all(uploadImgs) //执行所有需请求的接口
.then((results) => {
uni.hideLoading();
// console.log('then', results)
uniCloud.getTempFileURL({
fileList: results,
success: (res) => {
//console.log('success',res.fileList)
res.fileList.forEach(item => {
//console.log(item.tempFileURL)
//this.value.push(item.tempFileURL)
// #ifndef VUE3
this.value.push(item.tempFileURL)
this.$emit("input", this.value);
// #endif
// #ifdef VUE3
this.modelValue.push(item.tempFileURL)
this.$emit("update:modelValue", this.modelValue);
// #endif
})
},
fail() {},
complete() {}
});
})
.catch((res, object) => {
uni.hideLoading();
});
},
getFileType(path, type) { //手机端默认图片为jpg 视频为mp4
// #ifdef H5
var result = type == 0 ? 'jpg' : 'mp4';
// #endif
// #ifndef H5
var result = path.split('.').pop().toLowerCase();
// #ifdef MP
if (this.compress) { //微信小程序压缩完没有后缀
result = type == 0 ? 'jpg' : 'mp4';
}
// #endif
// #endif
return result;
},
guid() {
return 'xxxxxxxx-date-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
}).replace(/date/g, function(c) {
return Date.parse(new Date());
});
},
canvasDataURL(path, obj, callback) {
var img = new Image();
img.src = path;
img.onload = function() {
var that = this;
// 默认按比例压缩
var w = that.width,
h = that.height,
scale = w / h;
w = obj.width || w;
h = obj.height || (w / scale);
var quality = 0.8; // 默认图片质量为0.8
//生成canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
// 创建属性节点
var anw = document.createAttribute("width");
anw.nodeValue = w;
var anh = document.createAttribute("height");
anh.nodeValue = h;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
ctx.drawImage(that, 0, 0, w, h);
// 图像质量
if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
quality = obj.quality;
}
// quality值越小所绘制出的图像越模糊
var base64 = canvas.toDataURL('image/jpeg', quality);
// 回调函数返回base64的值
callback(base64);
}
},
}
}
</script>
<style>
.preview-full {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1002;
}
.preview-full video {
width: 100%;
height: 100%;
z-index: 1002;
}
.preview-full-close {
position: fixed;
right: 32rpx;
top: 25rpx;
width: 80rpx;
height: 80rpx;
line-height: 60rpx;
text-align: center;
z-index: 1003;
/* background-color: #808080; */
color: #fff;
font-size: 65rpx;
font-weight: bold;
text-shadow: 1px 2px 5px rgb(0 0 0);
}
/* .preview-full-close-before,
.preview-full-close-after {
position: absolute;
top: 50%;
left: 50%;
content: '';
height: 60rpx;
margin-top: -30rpx;
width: 6rpx;
margin-left: -3rpx;
background-color: #FFFFFF;
z-index: 20000;
}
.preview-full-close-before {
transform: rotate(45deg);
}
.preview-full-close-after {
transform: rotate(-45deg);
} */
.htz-image-upload-list {
display: flex;
flex-wrap: wrap;
}
.htz-image-upload-Item {
width: 160rpx;
height: 160rpx;
margin: 13rpx;
border-radius: 10rpx;
position: relative;
}
.htz-image-upload-Item image {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.htz-image-upload-Item-video {
width: 100%;
height: 100%;
border-radius: 10rpx;
position: relative;
}
.htz-image-upload-Item-video-fixed {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
border-radius: 10rpx;
z-index: 996;
}
.htz-image-upload-Item video {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.htz-image-upload-Item-add {
font-size: 105rpx;
/* line-height: 160rpx; */
text-align: center;
border: 1px dashed #d9d9d9;
color: #d9d9d9;
}
.htz-image-upload-Item-del {
background-color: var(--ui-BG-Main);
font-size: 24rpx;
position: absolute;
width: 35rpx;
height: 35rpx;
line-height: 35rpx;
text-align: center;
top: 0;
right: 0;
z-index: 997;
color: #fff;
border-top-right-radius: 10rpx;
}
.htz-image-upload-Item-del-cover {
background-color: var(--ui-BG-Main);
font-size: 24rpx;
position: absolute;
width: 35rpx;
height: 35rpx;
text-align: center;
top: 0;
right: 0;
color: #fff;
/* #ifdef APP-PLUS */
line-height: 25rpx;
/* #endif */
/* #ifndef APP-PLUS */
line-height: 35rpx;
/* #endif */
z-index: 997;
border-top-right-radius: 10rpx;
}
.htz-image-upload-Item-video-app-poster {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<su-popup
:show="modal.qrcode"
type="center"
backgroundColor="none"
round="0"
:showClose="false"
:isMaskClick="true"
@close="closeModal"
>
<view class="content">
<image class="img" :src="tradeConfig.qrcode" show-menu-by-longpress="true"></image>
<view class="tip">长按二维码 关注公众号</view>
<view class="desc">用于接收消息提醒</view>
<view class="close-btn" @click="getQrcode">点击验证</view>
</view>
</su-popup>
</template>
<script>
import sheep from '@/sheep';
import $store from '@/sheep/store';
import UserApi from '@/sheep/api/member/user';
export default {
components: {
},
data() {
return {
show: false,
};
},
computed: {
modal() {
return sheep.$store('modal');
},
tradeConfig() {
return sheep.$store('user').tradeConfig;
},
},
methods: {
open() {
this.show = true;
},
closeModal() {
$store('modal').$patch((state) => {
state.qrcode = false;
});
},
getQrcode() {
UserApi.getQrcode().then((res) => {
if (res.code === 0) {
if(res.data){
sheep.$helper.toast('验证通过');
this.closeModal();
}else{
sheep.$helper.toast('未关注公众号');
}
}
});
},
}
};
</script>
<style lang="scss" scoped>
.show {
background-color: rgba(0, 0, 0, 0.7);
position: fixed;
z-index: 999999;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.hide {
display: none;
}
.content {
padding: 40rpx;
background-color: #fff;
border-radius: 10rpx;
.img {
width: 500rpx;
height: 500rpx;
}
.tip {
display: flex;
justify-content: center;
padding-top: 40rpx;
padding-bottom: 20rpx;
font-size: 28rpx;
color: #666666;
}
.desc{
display: flex;
justify-content: center;
font-size: 28rpx;
font-weight: bold;
}
.close-btn {
display: flex;
justify-content: center;
background-color: var(--ui-BG-Main);
border-radius: 40px;
margin-top: 40rpx;
padding: 20rpx;
font-size: 24rpx;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,356 @@
<template>
<su-popup
:show="modal.search"
backgroundColor="#fff"
round="5"
:showClose="false"
:isMaskClick="true"
@close="closeModal"
>
<view class="s-form">
<view class="s-title">筛选</view>
<view class="s-box">
<view class="s-label">年龄</view>
<view class="age-box">
<view class="s-tag" @click="ageChange(aindex)" :class="ageIndex == aindex ? 'active' : ''" v-for="(age, aindex) in ageList" :key="aindex">{{age.text}}</view>
</view>
</view>
<view class="s-box">
<view class="s-label">性别</view>
<view class="age-box">
<view class="s-tag" @click="sexChange(index)" :class="sexIndex == index ? 'active' : ''" v-for="(sex, index) in sexList" :key="index">{{sex.text}}</view>
</view>
</view>
<view class="s-box">
<view class="s-label">是否同城会员特权</view>
<view class="age-box">
<view class="s-tag" @click="cityChange(cindex)" :class="cityIndex == cindex ? 'active' : ''" v-for="(city, cindex) in cityList" :key="cindex">{{city.text}}</view>
</view>
</view>
<view class="s-box">
<view class="s-label">城市会员特权</view>
<view class="age-box">
<view class="s-tag" @click="query.city = ''">全部</view>
<view class="s-tag" @click="cityShow = true" v-if="query.city">{{query.city}}</view>
<view class="s-tag" @click="cityShow = true" v-else>切换城市</view>
</view>
</view>
<!-- <view class="s-box">
<view class="s-label">距离</view>
<view class="age-box">
<view class="s-tag" @click="miChange(mindex)" :class="miIndex == mindex ? 'active' : ''" v-for="(mi, mindex) in miList" :key="mindex">{{mi.text}}</view>
</view>
</view> -->
<view class="btn-box" :class="(ageIndex < 0 && sexIndex < 0 && miIndex < 0) ? 'btn-disabled' : ''" @click="ok">确定</view>
</view>
</su-popup>
<su-region-picker :show="cityShow" @cancel="cityShow = false" @confirm="cityOk" />
</template>
<script>
import sheep from '@/sheep';
import $store from '@/sheep/store';
export default {
emits: ['query'],
components: {
},
data() {
return {
min: 18,
max: 18,
ageIndex: 0,
sexIndex: 0,
cityIndex: 0,
miIndex: -1,
ageList: [
{
text: '全部',
min: 0,
max: 100,
},
{
text: '18~23',
min: 18,
max: 23,
},
{
text: '23~28',
min: 23,
max: 28,
},
{
text: '28~33',
min: 28,
max: 33,
},
{
text: '33~38',
min: 33,
max: 38,
},
{
text: '38~43',
min: 38,
max: 43,
},
{
text: '43~48',
min: 43,
max: 48,
},
{
text: '48~53',
min: 48,
max: 53,
},
],
sexList: [
{
text: '全部',
value: '',
},
{
text: '男',
value: 2,
},
{
text: '女',
value: 1,
},
],
cityList: [
{
text: '否',
value: '1',
},
{
text: '是',
value: '0',
},
],
miList: [
{
text: '5KM',
value: 5000,
},
{
text: '10KM',
value: 10000,
},
{
text: '15KM',
value: 15000,
},
{
text: '20KM',
value: 20000,
},
{
text: '25KM',
value: 25000,
},
{
text: '30KM',
value: 30000,
},
{
text: '35KM',
value: 35000,
},
{
text: '40KM',
value: 40000,
},
],
search: false,
cityShow: false,
query: {
maxAge: 35,
minAge: 18,
sex: '',
isCity: 1,
city: '',
},
}
},
computed: {
modal() {
return sheep.$store('modal');
},
userInfo: {
get() {
return sheep.$store('user').userInfo;
},
},
},
methods: {
closeModal() {
$store('modal').$patch((state) => {
state.search = false;
});
},
ageChange(i) {
this.ageIndex = i;
},
sexChange(i) {
this.sexIndex = i;
},
cityChange(i) {
this.cityIndex = i;
},
miChange(i) {
this.miIndex = i;
},
ok() {
if(this.ageIndex < 0 && this.sexIndex < 0 && this.miIndex < 0){
return;
}
if(this.ageIndex > -1){
this.query.maxAge = this.ageList[this.ageIndex].max;
this.query.minAge = this.ageList[this.ageIndex].min;
}
if(this.sexIndex > -1){
this.query.sex = this.sexList[this.sexIndex].value;
}
if(this.cityIndex > -1){
this.query.isCity = this.cityList[this.cityIndex].value;
}
this.$emit('query', this.query);
this.closeModal();
},
handerChooseLocation () {
uni.chooseLocation({
success: (res) => {
this.updateLocation(res.latitude, res.longitude);
},
fail: function (err) {
console.log('取消按钮', err)
}
});
},
doCity() {
this.$u.route({
url: '/pages/index/user/city',
});
},
updateLocation(lat,lng) {
this.$u.put('/system/user/profile',{
lat: lat,
lon: lng,
}).then(res => {
this.doCity();
})
},
more() {
this.$u.route({
url: 'pages/index/user/menu',
});
},
open() {
this.search = true;
},
cancel() {
this.search = false;
},
minEnd() {
if(this.max < this.min){
this.max = this.min;
}
},
maxEnd() {
if(this.max < this.min){
this.min = this.max;
}
},
cityOk(e) {
this.query.city = e.city_name;
this.cityShow = false;
},
}
}
</script>
<style lang="scss" scoped>
.s-form {
padding: 20px;
}
.s-title {
font-size: 38rpx;
display: flex;
align-items: center;
font-weight: bold;
justify-content: center;
margin-bottom: 15px;
}
.s-label {
font-size: 30rpx;
margin-top: 10px;
margin-bottom: 10px;
color: black;
}
.s-age {
display: flex;
align-items: center;
margin: 30px 0;
}
.s-slider {
flex: 1;
}
.btn-box {
background-color: #283242;
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
border-radius: 20px;
font-size: 30rpx;
color: #fff;
margin-top: 20px;
}
.sex-btn {
background-color: #2979ff;
border-radius: 20px;
padding: 3px 10px;
color: #fff;
display: flex;
justify-content: center;
margin-left: 10px;
}
.more {
margin-bottom: 15px;
background-color: #19be6b;
}
.s-box {
display: flex;
flex-direction: column;
}
.age-box {
display: flex;
flex-wrap: wrap;
}
.s-tag {
border: 1px solid #ddd;
border-radius: 40px;
display: flex;
font-size: 24rpx;
align-items: center;
justify-content: center;
margin-right: 10px;
height: 25px;
width: 60px;
margin-bottom: 15px;
}
.active {
background-color: #283242;
color: #fff;
border: 1px solid #283242;
}
.btn-disabled {
background-color: #ddd;
}
</style>

View File

@@ -0,0 +1,201 @@
<template>
<view @touchmove.stop.prevent>
<view class="tui-actionsheet" :class="{'tui-actionsheet-show':show,'tui-actionsheet-radius':radius}"
:style="{zIndex:getZIndex}">
<view class="tui-actionsheet-tips" :style="{fontSize:size+'rpx',color:color}" v-if="tips">
{{tips}}
</view>
<view :class="[isCancel?'tui-operate-box':'']">
<block v-for="(item,index) in itemList" :key="index">
<view class="tui-actionsheet-btn tui-actionsheet-divider"
:class="{'tui-btn-last':!isCancel && index==itemList.length-1}"
hover-class="tui-actionsheet-hover" :hover-stay-time="150" :data-index="index"
:style="{color:item.color || '#2B2B2B'}" @tap="handleClickItem">{{item.text}}</view>
</block>
</view>
<view class="tui-actionsheet-btn tui-actionsheet-cancel" hover-class="tui-actionsheet-hover"
:hover-stay-time="150" v-if="isCancel" @tap="handleClickCancel">取消</view>
</view>
<view class="tui-actionsheet-mask" :class="{'tui-mask-show':show}" :style="{background:maskColor,zIndex:zIndex}"
@tap="handleClickMask"></view>
</view>
</template>
<script>
export default {
name: "tuiActionsheet",
emits: ['click', 'cancel'],
props: {
//显示操作菜单
show: {
type: Boolean,
default: false
},
//菜单按钮数组,自定义文本颜色,红色参考色:#e53a37
itemList: {
type: Array,
default: function() {
return [{
text: "确定",
color: "#2B2B2B"
}]
}
},
//点击遮罩 是否可关闭
maskClosable: {
type: Boolean,
default: true
},
//v2.1.0
maskColor: {
type: String,
default: "rgba(0, 0, 0, 0.6)"
},
//提示文字
tips: {
type: String,
default: ""
},
//提示文字颜色
color: {
type: String,
default: "#808080"
},
//提示文字大小 rpx
size: {
type: Number,
default: 26
},
//是否需要圆角
radius: {
type: Boolean,
default: true
},
//是否需要取消按钮
isCancel: {
type: Boolean,
default: true
},
zIndex: {
type: [Number, String],
default: 998
}
},
computed: {
getZIndex() {
return Number(this.zIndex) + 2
}
},
methods: {
handleClickMask() {
if (!this.maskClosable) return;
this.handleClickCancel();
},
handleClickItem(e) {
if (!this.show) return;
const index = Number(e.currentTarget.dataset.index);
this.$emit('click', {
index: index,
...this.itemList[index]
});
},
handleClickCancel() {
this.$emit('cancel');
}
}
}
</script>
<style scoped>
.tui-actionsheet {
width: 100%;
position: fixed;
left: 0;
right: 0;
bottom: 0;
visibility: hidden;
transform: translate3d(0, 100%, 0);
transform-origin: center;
transition: all 0.25s ease-in-out;
background-color: #F7F7F7;
min-height: 100rpx;
}
.tui-actionsheet-radius {
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
overflow: hidden;
}
.tui-actionsheet-show {
transform: translate3d(0, 0, 0);
visibility: visible;
}
.tui-actionsheet-tips {
width: 100%;
padding: 40rpx 60rpx;
box-sizing: border-box;
text-align: center;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.tui-operate-box {
padding-bottom: 12rpx;
}
.tui-actionsheet-btn {
width: 100%;
height: 100rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 34rpx;
position: relative;
}
.tui-btn-last {
padding-bottom: env(safe-area-inset-bottom);
}
.tui-actionsheet-divider::before {
content: '';
width: 100%;
border-top: 1rpx solid #E7E7E7;
position: absolute;
top: 0;
left: 0;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
.tui-actionsheet-cancel {
color: #1a1a1a;
padding-bottom: env(safe-area-inset-bottom);
}
.tui-actionsheet-hover {
background-color: #f7f7f9;
}
.tui-actionsheet-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.tui-mask-show {
opacity: 1;
visibility: visible;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<view :class="[dot ? 'tui-badge-dot' : 'tui-badge', !dot ? 'tui-badge-scale' : '']"
:style="{ top: top, right: right, position: absolute ? 'absolute' : 'static', transform: getStyle, margin: margin,background:getBackground,color:getColor }"
@tap="handleClick">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tuiBadge',
emits: ['click'],
props: {
//primary,warning,green,danger,whiteblackgray,white_red
type: {
type: String,
default: 'primary'
},
//是否是圆点
dot: {
type: Boolean,
default: false
},
margin: {
type: String,
default: '0'
},
//是否绝对定位
absolute: {
type: Boolean,
default: false
},
top: {
type: String,
default: '-8rpx'
},
right: {
type: String,
default: '0'
},
//缩放比例
scaleRatio: {
type: Number,
default: 1
},
//水平方向移动距离
translateX: {
type: String,
default: '0'
}
},
computed: {
getStyle() {
return `scale(${this.scaleRatio}) translateX(${this.translateX})`;
},
getBackground() {
const global = uni && uni.$tui && uni.$tui.color;
let color = {
'primary': (global && global.primary) || '#5677fc',
'green': (global && global.success) || '#07c160',
'warning': (global && global.warning) || '#ff7900',
'danger': (global && global.danger) || '#EB0909',
'white': '#fff',
'black': '#000',
'gray': '#ededed',
'red': (global && global.pink) || '#f74d54',
'pink': (global && global.pink) || '#f74d54',
'white_red': '#fff',
'white_primary': '#fff',
'white_green': '#fff',
'white_warning': '#fff',
'white_pink': '#fff'
} [this.type]
return color
},
getColor() {
const global = uni && uni.$tui && uni.$tui.color;
let color = {
'primary': '#fff',
'green': '#fff',
'warning': '#fff',
'danger': '#fff',
'white': '#333',
'black': '#fff',
'gray': '#999',
'red': '#fff',
'pink': (global && global.pink) || '#f74d54',
'white_red': (global && global.danger) || '#EB0909',
'white_primary': (global && global.primary) || '#5677fc',
'white_green': (global && global.success) || '#07c160',
'white_warning': (global && global.warning) || '#ff7900',
'white_pink': (global && global.pink) || '#f74d54',
} [this.type]
return color
}
},
methods: {
handleClick() {
this.$emit('click', {});
}
}
};
</script>
<style scoped>
.tui-badge-dot {
height: 8px;
width: 8px;
border-radius: 50%;
}
.tui-badge {
font-size: 24rpx;
line-height: 24rpx;
height: 36rpx;
min-width: 36rpx;
padding: 0 10rpx;
box-sizing: border-box;
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.tui-badge-scale {
transform-origin: center center;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<view @touchmove.stop.prevent>
<view class="tui-popup-class tui-bottom-popup"
:class="{ 'tui-popup-show': show, 'tui-popup-radius': radius,'tui-bp__safearea':isSafeArea }"
:style="{ background: backgroundColor, height: height ? height + 'rpx' : 'auto', zIndex: zIndex,transform:`translate3d(0, ${show?translateY:'100%'}, 0)`}">
<slot></slot>
</view>
<view class="tui-popup-mask" :class="[show ? 'tui-mask-show' : '']" :style="{ zIndex: maskZIndex }" v-if="mask"
@tap="handleClose"></view>
</view>
</template>
<script>
export default {
name: 'tuiBottomPopup',
emits: ['close'],
props: {
//是否需要mask
mask: {
type: Boolean,
default: true
},
//控制显示
show: {
type: Boolean,
default: false
},
//背景颜色
backgroundColor: {
type: String,
default: '#fff'
},
//高度 rpx
height: {
type: Number,
default: 0
},
//设置圆角
radius: {
type: Boolean,
default: true
},
zIndex: {
type: [Number, String],
default: 997
},
maskZIndex: {
type: [Number, String],
default: 996
},
//弹层显示时,垂直方向移动的距离
translateY: {
type: String,
default: '0'
},
//是否需要判断底部安全区域主要针对iphonex以上机型
isSafeArea: {
type: Boolean,
default: true
}
},
methods: {
handleClose() {
if (!this.show) {
return;
}
this.$emit('close', {});
}
}
};
</script>
<style scoped>
.tui-bottom-popup {
width: 100%;
position: fixed;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transform: translate3d(0, 100%, 0);
transform-origin: center;
transition: all 0.3s ease-in-out;
min-height: 20rpx;
}
.tui-bp__safearea {
padding-bottom: env(safe-area-inset-bottom);
}
.tui-popup-radius {
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
overflow: hidden;
}
.tui-popup-show {
opacity: 1;
/* transform: translate3d(0, 0, 0); */
}
.tui-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.tui-mask-show {
opacity: 1;
visibility: visible;
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<view :class="{ 'tui-flex-end': flexEnd }">
<view class="tui-popup-list" :class="{ 'tui-popup-show': show,'tui-z_index':show && position!='relative' }" :style="{ width: width, backgroundColor: backgroundColor, borderRadius: radius, color: color, position: position, left: left, right: right, bottom: bottom, top: top,transform:`translate(${translateX},${translateY})` }">
<view class="tui-triangle" :style="{
borderWidth: borderWidth,
borderColor: `transparent transparent ${backgroundColor} transparent`,
left: triangleLeft,
right: triangleRight,
top: triangleTop,
bottom: triangleBottom
}"
v-if="direction == 'top'"></view>
<view class="tui-triangle" :style="{
borderWidth: borderWidth,
borderColor: `${backgroundColor} transparent transparent transparent`,
left: triangleLeft,
right: triangleRight,
top: triangleTop,
bottom: triangleBottom
}"
v-if="direction == 'bottom'"></view>
<view class="tui-triangle" :style="{
borderWidth: borderWidth,
borderColor: `transparent ${backgroundColor} transparent transparent`,
left: triangleLeft,
right: triangleRight,
top: triangleTop,
bottom: triangleBottom
}"
v-if="direction == 'left'"></view>
<view class="tui-triangle" :style="{
borderWidth: borderWidth,
borderColor: `transparent transparent transparent ${backgroundColor}`,
left: triangleLeft,
right: triangleRight,
top: triangleTop,
bottom: triangleBottom
}"
v-if="direction == 'right'"></view>
<slot></slot>
</view>
<view @touchmove.stop.prevent="stop" class="tui-popup-mask" :class="{ 'tui-popup-show': show }" :style="{ backgroundColor: maskBgColor }"
v-if="mask" @tap="handleClose"></view>
</view>
</template>
<script>
export default {
name: 'tuiBubblePopup',
emits: ['close'],
props: {
//宽度
width: {
type: String,
default: '300rpx'
},
//popup圆角
radius: {
type: String,
default: '8rpx'
},
//popup 定位 left right top bottom值
left: {
type: String,
default: 'auto'
},
right: {
type: String,
default: 'auto'
},
top: {
type: String,
default: 'auto'
},
bottom: {
type: String,
default: 'auto'
},
translateX:{
type: String,
default: '0'
},
translateY:{
type: String,
default: '0'
},
//背景颜色
backgroundColor: {
type: String,
default: '#4c4c4c'
},
//字体颜色
color: {
type: String,
default: '#fff'
},
//三角border-width
borderWidth: {
type: String,
default: '12rpx'
},
//三角形方向 top left right bottom
direction: {
type: String,
default: 'top'
},
//定位 left right top bottom值
triangleLeft: {
type: String,
default: 'auto'
},
triangleRight: {
type: String,
default: 'auto'
},
triangleTop: {
type: String,
default: 'auto'
},
triangleBottom: {
type: String,
default: 'auto'
},
//定位 relative absolute fixed
position: {
type: String,
default: 'fixed'
},
//flex-end
flexEnd: {
type: Boolean,
default: false
},
//是否需要mask
mask: {
type: Boolean,
default: true
},
maskBgColor: {
type: String,
default: 'rgba(0, 0, 0, 0.4)'
},
//控制显示
show: {
type: Boolean,
default: false
}
},
methods: {
handleClose() {
if (!this.show) {
return;
}
this.$emit('close', {});
},
stop() {
return false;
}
}
};
</script>
<style scoped>
.tui-popup-list {
z-index: 1;
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.tui-flex-end {
width: 100%;
display: flex;
justify-content: flex-end;
}
.tui-triangle {
position: absolute;
width: 0;
height: 0;
border-style: solid;
z-index: 997;
}
.tui-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 995;
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.tui-popup-show {
opacity: 1;
visibility: visible;
}
.tui-z_index {
z-index: 996;
}
</style>

View File

@@ -0,0 +1,309 @@
<template>
<view class="tui-circular-container" :style="{ width: diam + 'px', height: (height || diam) + 'px' }">
<!-- #ifndef MP-ALIPAY -->
<canvas class="tui-circular-default" :canvas-id="defaultCanvasId" :id="defaultCanvasId"
:style="{ width: diam + 'px', height: (height || diam) + 'px' }"
v-if="defaultShow && defaultCanvasId"></canvas>
<canvas class="tui-circular-progress" :canvas-id="progressCanvasId" :id="progressCanvasId"
:style="{ width: diam + 'px', height: (height || diam) + 'px' }" v-if="progressCanvasId"></canvas>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<canvas class="tui-circular-default" :canvas-id="defaultCanvasId" :id="defaultCanvasId"
:style="{ width: diam*4 + 'px', height: (height || diam)*4 + 'px' }" v-if="defaultShow"></canvas>
<canvas class="tui-circular-progress" :canvas-id="progressCanvasId" :id="progressCanvasId"
:style="{ width: diam*4 + 'px', height: (height || diam)*4 + 'px' }"></canvas>
<!-- #endif -->
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tuiCircularProgress',
emits: ['change', 'end'],
props: {
/*
传值需使用rpx进行转换保证各终端兼容
px = rpx / 750 * wx.getSystemInfoSync().windowWidth
圆形进度条(画布)宽度,直径 [px]
*/
diam: {
type: Number,
default: 60
},
//圆形进度条(画布)高度默认取diam值[当画半弧时传值height有值时则取height]
height: {
type: Number,
default: 0
},
//进度条线条宽度[px]
lineWidth: {
type: Number,
default: 4
},
/*
线条的端点样式
butt向线条的每个末端添加平直的边缘
round 向线条的每个末端添加圆形线帽
square 向线条的每个末端添加正方形线帽
*/
lineCap: {
type: String,
default: 'round'
},
//圆环进度字体大小 [px]
fontSize: {
type: Number,
default: 12
},
//圆环进度字体颜色
fontColor: {
type: String,
default: ''
},
//是否显示进度文字
fontShow: {
type: Boolean,
default: true
},
/*
自定义显示文字[默认为空显示百分比fontShow=true时生效]
可以使用 slot自定义显示内容
*/
percentText: {
type: String,
default: ''
},
//是否显示默认(背景)进度条
defaultShow: {
type: Boolean,
default: true
},
//默认进度条颜色
defaultColor: {
type: String,
default: '#CCCCCC'
},
//进度条颜色
progressColor: {
type: String,
default: ''
},
//进度条渐变颜色[结合progressColor使用默认为空]
gradualColor: {
type: String,
default: ''
},
//起始弧度,单位弧度
sAngle: {
type: Number,
default: -Math.PI / 2
},
//指定弧度的方向是逆时针还是顺时针。默认是false即顺时针
counterclockwise: {
type: Boolean,
default: false
},
//进度百分比 [10% 传值 10]
percentage: {
type: Number,
default: 0
},
//进度百分比缩放倍数[使用半弧为100%时则可传2]
multiple: {
type: Number,
default: 1
},
//动画执行时间[单位毫秒低于50无动画]
duration: {
type: Number,
default: 800
},
//backwards: 动画从头播forwards动画从上次结束点接着播
activeMode: {
type: String,
default: 'backwards'
}
},
watch: {
percentage(val) {
this.initDraw();
}
},
data() {
// #ifndef MP-WEIXIN || MP-QQ
let cid = `id01_${Math.ceil(Math.random() * 10e5).toString(36)}`
let did = `id02_${Math.ceil(Math.random() * 10e5).toString(36)}`
// #endif
return {
// #ifdef MP-WEIXIN || MP-QQ
progressCanvasId: 'progressCanvasId',
defaultCanvasId: 'defaultCanvasId',
// #endif
// #ifndef MP-WEIXIN || MP-QQ
progressCanvasId: cid,
defaultCanvasId: did,
// #endif
progressContext: null,
linearGradient: null,
//起始百分比
startPercentage: 0
// dpi
//pixelRatio: uni.getSystemInfoSync().pixelRatio
};
},
mounted() {
this.$nextTick(() => {
setTimeout(() => {
this.initDraw(true);
}, 50)
})
},
methods: {
//初始化绘制
initDraw(init) {
let start = this.activeMode === 'backwards' ? 0 : this.startPercentage;
start = start > this.percentage ? 0 : start;
if (this.defaultShow && init) {
this.drawDefaultCircular();
}
this.drawProgressCircular(start);
},
//默认(背景)圆环
drawDefaultCircular() {
let ctx = uni.createCanvasContext(this.defaultCanvasId, this);
let lineWidth = Number(this.lineWidth)
// #ifdef MP-ALIPAY
lineWidth = lineWidth * 4
// #endif
ctx.setLineWidth(lineWidth);
ctx.setStrokeStyle(this.defaultColor);
//终止弧度
let eAngle = Math.PI * (this.height ? 1 : 2) + this.sAngle;
this.drawArc(ctx, eAngle);
},
//进度圆环
drawProgressCircular(startPercentage) {
let ctx = this.progressContext;
let gradient = this.linearGradient;
if (!ctx) {
ctx = uni.createCanvasContext(this.progressCanvasId, this);
//创建一个线性的渐变颜色 CanvasGradient对象
let diam = Number(this.diam)
// #ifdef MP-ALIPAY
diam = diam * 4
// #endif
const progressColor = this.progressColor || (uni && uni.$tui && uni.$tui.color.primary) || '#5677fc';
gradient = ctx.createLinearGradient(0, 0, diam, 0);
gradient.addColorStop('0', progressColor);
if (this.gradualColor) {
gradient.addColorStop('1', this.gradualColor);
}
// #ifdef APP-PLUS || MP
const res = uni.getSystemInfoSync();
if (!this.gradualColor && res.platform.toLocaleLowerCase() == 'android') {
gradient.addColorStop('1', progressColor);
}
// #endif
this.progressContext = ctx;
this.linearGradient = gradient;
}
let lineWidth = Number(this.lineWidth)
// #ifdef MP-ALIPAY
lineWidth = lineWidth * 4
// #endif
ctx.setLineWidth(lineWidth);
ctx.setStrokeStyle(gradient);
let time = this.percentage == 0 || this.duration < 50 ? 0 : this.duration / this.percentage;
if (this.percentage > 0) {
startPercentage = this.duration < 50 ? this.percentage - 1 : startPercentage;
startPercentage++;
}
if (this.fontShow) {
let fontSize = Number(this.fontSize)
// #ifdef MP-ALIPAY
fontSize = fontSize * 4
// #endif
ctx.setFontSize(fontSize);
const fontColor = this.fontColor || (uni && uni.$tui && uni.$tui.color.primary) || '#5677fc';
ctx.setFillStyle(fontColor);
ctx.setTextAlign('center');
ctx.setTextBaseline('middle');
let percentage = this.percentText;
if (!percentage) {
percentage = this.counterclockwise ? 100 - startPercentage * this.multiple : startPercentage * this
.multiple;
percentage = `${percentage}%`;
}
let radius = this.diam / 2;
// #ifdef MP-ALIPAY
radius = radius * 4
// #endif
ctx.fillText(percentage, radius, radius);
}
if (this.percentage == 0 || (this.counterclockwise && startPercentage == 100)) {
ctx.draw();
} else {
let eAngle = ((2 * Math.PI) / 100) * startPercentage + this.sAngle;
this.drawArc(ctx, eAngle);
}
setTimeout(() => {
this.startPercentage = startPercentage;
if (startPercentage >= this.percentage) {
this.$emit('end', {
canvasId: this.progressCanvasId,
percentage: startPercentage
});
} else {
this.drawProgressCircular(startPercentage);
}
this.$emit('change', {
percentage: startPercentage
});
}, time);
// #ifdef H5
// requestAnimationFrame(()=>{})
// #endif
},
//创建弧线
drawArc(ctx, eAngle) {
ctx.setLineCap(this.lineCap);
ctx.beginPath();
let radius = this.diam / 2; //x=y
let lineWidth = Number(this.lineWidth)
// #ifdef MP-ALIPAY
radius = radius * 4
lineWidth = lineWidth * 4
// #endif
ctx.arc(radius, radius, radius - lineWidth, this.sAngle, eAngle, this.counterclockwise);
ctx.stroke();
ctx.draw();
}
}
};
</script>
<style scoped>
.tui-circular-container,
.tui-circular-default {
position: relative;
}
/* #ifdef MP-ALIPAY */
.tui-circular-default,
.tui-circular-progress {
zoom: 0.25;
}
/* #endif */
.tui-circular-progress {
position: absolute !important;
left: 0;
top: 0;
z-index: 10;
}
</style>

View File

@@ -0,0 +1,3 @@
export default {
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,338 @@
<template>
<view class="tui-modal__container" :class="[show ? 'tui-modal-show' : '']" :style="{zIndex:zIndex}"
@touchmove.stop.prevent>
<view class="tui-modal-box"
:style="{ width: width, padding: padding, borderRadius: radius, backgroundColor: backgroundColor,zIndex:zIndex+1 }"
:class="[fadeIn || show ? 'tui-modal-normal' : 'tui-modal-scale', show ? 'tui-modal-show' : '']">
<view v-if="!custom">
<view class="tui-modal-title" v-if="title">{{ title }}</view>
<view class="tui-modal-content" :class="[title ? '' : 'tui-mtop']"
:style="{ color: color, fontSize: size + 'rpx' }">{{ content }}</view>
<view class="tui-modalBtn-box" :class="[button.length != 2 ? 'tui-flex-column' : '']">
<view v-for="(item, index) in button" :key="index" class="tui-modal-btn" :class="[
button.length != 2 ? 'tui-btn-width' : '',
button.length > 2 ? 'tui-mbtm' : '',
shape == 'circle' ? 'tui-circle-btn' : ''
]" :style="{background:getColor(item.type,true,item.plain),color:getTextColor(item.type,item.plain)}"
@tap="handleClick(index)">
{{ item.text || '确定' }}
<view class="tui-modal__border" :class="[shape == 'circle' ? 'tui-circle-border' : '']"
:style="{borderColor:getColor(item.type)}" v-if="item.plain"></view>
</view>
</view>
</view>
<view v-else>
<slot></slot>
</view>
</view>
<view v-if="isMask" class="tui-modal-mask" :class="[show ? 'tui-mask-show' : '']"
:style="{zIndex:maskZIndex,background:maskColor}" @tap="handleClickCancel"></view>
</view>
</template>
<script>
export default {
name: 'tuiModal',
emits: ['click', 'cancel'],
props: {
//是否显示
show: {
type: Boolean,
default: false
},
width: {
type: String,
default: '84%'
},
backgroundColor: {
type: String,
default: '#fff'
},
padding: {
type: String,
default: '40rpx 64rpx'
},
radius: {
type: String,
default: '24rpx'
},
//标题
title: {
type: String,
default: ''
},
//内容
content: {
type: String,
default: ''
},
//内容字体颜色
color: {
type: String,
default: '#999'
},
//内容字体大小 rpx
size: {
type: Number,
default: 28
},
//形状 circle, square
shape: {
type: String,
default: 'square'
},
button: {
type: Array,
default: function() {
return [{
text: '取消',
type: 'red',
plain: true //是否空心
},
{
text: '确定',
type: 'red',
plain: false
}
];
}
},
//点击遮罩 是否可关闭
maskClosable: {
type: Boolean,
default: true
},
//是否显示mask
isMask: {
type: Boolean,
default: true
},
maskColor: {
type: String,
default: 'rgba(0, 0, 0, 0.6)'
},
//淡入效果自定义弹框插入input输入框时传true
fadeIn: {
type: Boolean,
default: false
},
//自定义弹窗内容
custom: {
type: Boolean,
default: false
},
//容器z-index
zIndex: {
type: Number,
default: 9997
},
//mask z-index
maskZIndex: {
type: Number,
default: 9990
}
},
data() {
return {};
},
methods: {
getColor(type, isBg, plain) {
const global = uni && uni.$tui && uni.$tui.color;
let color = {
'primary': (global && global.primary) || '#5677fc',
'green': (global && global.success) || '#07c160',
'warning': (global && global.warning) || '#ff7900',
'danger': (global && global.danger) || '#EB0909',
'red': (global && global.danger) || '#EB0909',
'pink': (global && global.pink) || '#f74d54',
'white': isBg ? '#fff' : '#333',
'gray': isBg ? '#ededed' : '#999'
} [type || 'primary']
if (isBg && plain) {
color = 'transparent'
}
return color
},
getTextColor(type, plain) {
const global = uni && uni.$tui && uni.$tui.color;
let color = {
'primary': (global && global.primary) || '#5677fc',
'green': (global && global.success) || '#07c160',
'warning': (global && global.warning) || '#ff7900',
'danger': (global && global.danger) || '#EB0909',
'red': (global && global.danger) || '#EB0909',
'pink': (global && global.pink) || '#f74d54',
'white': '#333',
'gray': '#999'
} [type || 'primary']
if (!plain && type !== 'white' && type !== 'gray') {
color = '#fff'
}
return color
},
handleClick(index) {
if (!this.show) return;
this.$emit('click', {
index: Number(index)
});
},
handleClickCancel() {
if (!this.maskClosable) return;
this.$emit('cancel');
}
}
};
</script>
<style scoped>
.tui-modal__container {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
visibility: hidden;
}
.tui-modal-box {
position: relative;
opacity: 0;
visibility: hidden;
box-sizing: border-box;
transition: all 0.3s ease-in-out;
}
.tui-modal-scale {
transform: scale(0);
}
.tui-modal-normal {
transform: scale(1);
}
.tui-modal-show {
opacity: 1;
visibility: visible;
}
.tui-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.tui-mask-show {
visibility: visible;
opacity: 1;
}
.tui-modal-title {
text-align: center;
font-size: 34rpx;
color: #333;
padding-top: 20rpx;
font-weight: bold;
}
.tui-modal-content {
text-align: center;
color: #999;
font-size: 28rpx;
padding-top: 20rpx;
padding-bottom: 60rpx;
}
.tui-mtop {
margin-top: 30rpx;
}
.tui-mbtm {
margin-bottom: 30rpx;
}
.tui-modalBtn-box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.tui-flex-column {
flex-direction: column;
}
.tui-modal-btn {
width: 46%;
height: 68rpx;
line-height: 68rpx;
position: relative;
border-radius: 10rpx;
font-size: 26rpx;
overflow: visible;
margin-left: 0;
margin-right: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
border-width: 0;
}
.tui-modal-btn:active::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.15);
pointer-events: none;
}
.tui-modal-btn::after {
border-radius: 10rpx;
}
.tui-modal__border {
position: absolute;
width: 200%;
height: 200%;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
transform: scale(0.5, 0.5) translateZ(0);
border: 1px solid;
box-sizing: border-box;
left: 0;
top: 0;
z-index: 2;
border-radius: 20rpx;
pointer-events: none;
}
.tui-btn-width {
width: 80% !important;
}
.tui-circle-btn {
border-radius: 40rpx !important;
}
.tui-circle-btn::after {
border-radius: 40rpx !important;
}
.tui-circle-border {
border-radius: 80rpx !important;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<view class="tui-navigation-bar"
:class="{ 'tui-bar-line': opacity > 0.85 && splitLine, 'tui-navbar-fixed': isFixed, 'tui-backdrop__filter': backdropFilter && dropDownOpacity > 0 }"
:style="{ height: height + 'px', background: isOpacity? `rgba(${background},${opacity})`:background, opacity: dropDownOpacity, zIndex: isFixed ? zIndex : 'auto' }">
<view class="tui-status-bar" :style="{ height: statusBarHeight + 'px' }" v-if="isImmersive"></view>
<view class="tui-navigation_bar-title"
:style="{ opacity: transparent || opacity >= maxOpacity ? 1 : opacity, color: color, paddingTop: top - statusBarHeight + 'px' }"
v-if="title && !isCustom">
{{ title }}
</view>
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tuiNavigationBar',
emits: ['init', 'change'],
props: {
//NavigationBar标题
title: {
type: String,
default: ''
},
//NavigationBar标题颜色
color: {
type: String,
default: '#333'
},
//NavigationBar背景颜色,不支持rgb
backgroundColor: {
type: String,
default: '#fff'
},
//是否需要分割线
splitLine: {
type: Boolean,
default: false
},
//是否设置不透明度
isOpacity: {
type: Boolean,
default: true
},
//不透明度最大值 0-1
maxOpacity: {
type: [Number, String],
default: 1
},
//背景透明 【设置该属性则背景透明只出现内容isOpacity和maxOpacity失效】
transparent: {
type: Boolean,
default: false
},
//滚动条滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
/*
isOpacity 为true时生效
opacity=scrollTop /windowWidth * scrollRatio
*/
scrollRatio: {
type: [Number, String],
default: 0.3
},
//是否自定义header内容
isCustom: {
type: Boolean,
default: false
},
//是否沉浸式
isImmersive: {
type: Boolean,
default: true
},
isFixed: {
type: Boolean,
default: true
},
//是否开启高斯模糊效果[仅在支持的浏览器有效果]
backdropFilter: {
type: Boolean,
default: false
},
//下拉隐藏NavigationBar主要针对有回弹效果ios端
dropDownHide: {
type: Boolean,
default: false
},
//z-index设置
zIndex: {
type: [Number, String],
default: 996
}
},
watch: {
scrollTop(newValue, oldValue) {
if (this.isOpacity && !this.transparent) {
this.opacityChange();
}
},
backgroundColor(val) {
if (val) {
if (this.isOpacity) {
this.background = this.hexToRgb(val);
} else {
this.background = this.transparent ? 'rgba(0, 0, 0, 0)' : val
}
}
}
},
data() {
return {
width: 375, //header宽度
left: 375, //小程序端 左侧距胶囊按钮距离
height: 44, //header高度
top: 0,
scrollH: 1, //滚动总高度,计算opacity
opacity: 1, //0-1
statusBarHeight: 0, //状态栏高度
background: '255,255,255', //header背景色
dropDownOpacity: 1
};
},
created() {
this.dropDownOpacity = this.backdropFilter && 0;
this.opacity = this.isOpacity || this.transparent ? 0 : this.maxOpacity;
if (this.isOpacity) {
this.background = this.hexToRgb(this.backgroundColor);
} else {
this.background = this.transparent ? 'rgba(0, 0, 0, 0)' : this.backgroundColor
}
let obj = {};
// #ifdef MP-WEIXIN
obj = wx.getMenuButtonBoundingClientRect();
// #endif
// #ifdef MP-BAIDU
obj = swan.getMenuButtonBoundingClientRect();
// #endif
// #ifdef MP-ALIPAY
my.hideAddToDesktopMenu();
// #endif
uni.getSystemInfo({
success: res => {
this.statusBarHeight = res.statusBarHeight;
this.width = res.windowWidth;
this.left = obj.left || res.windowWidth;
if (this.isImmersive) {
this.height = obj.top ? obj.top + obj.height + 8 : res.statusBarHeight + 44;
}
this.scrollH = res.windowWidth * this.scrollRatio;
this.top = obj.top ? obj.top + (obj.height - 32) / 2 : res.statusBarHeight + 6;
this.$emit('init', {
width: this.width,
height: this.height,
left: this.left,
top: this.top,
statusBarHeight: this.statusBarHeight,
opacity: this.opacity,
windowHeight: res.windowHeight
});
}
});
},
methods: {
hexToRgb(hex) {
let rgb = '255,255,255';
if (hex && ~hex.indexOf('#')) {
if (hex.length === 4) {
let text = hex.substring(1, 4);
hex = '#' + text + text;
}
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result) {
rgb = `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}`;
}
}
return rgb;
},
opacityChange() {
if (this.dropDownHide) {
if (this.scrollTop < 0) {
if (this.dropDownOpacity > 0) {
this.dropDownOpacity = 1 - Math.abs(this.scrollTop) / 30;
}
} else {
this.dropDownOpacity = 1;
}
}
let scroll = this.scrollTop <= 1 ? 0 : this.scrollTop;
let opacity = scroll / this.scrollH;
if ((this.opacity >= this.maxOpacity && opacity >= this.maxOpacity) || (this.opacity == 0 && opacity ==
0)) {
return;
}
this.opacity = opacity > this.maxOpacity ? this.maxOpacity : opacity;
if (this.backdropFilter) {
this.dropDownOpacity = this.opacity >= this.maxOpacity ? 1 : this.opacity;
}
this.$emit('change', {
opacity: this.opacity
});
}
}
};
</script>
<style scoped>
.tui-navigation-bar {
width: 100%;
transition: opacity 0.4s;
}
.tui-backdrop__filter {
/* Safari for macOS & iOS */
-webkit-backdrop-filter: blur(15px);
/* Google Chrome */
backdrop-filter: blur(15px);
}
.tui-navbar-fixed {
position: fixed;
left: 0;
top: 0;
transform: translateZ(0);
}
.tui-status-bar {
width: 100%;
}
.tui-navigation_bar-title {
width: 100%;
font-size: 17px;
line-height: 17px;
/* #ifndef APP-PLUS */
font-weight: 500;
/* #endif */
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.tui-bar-line::after {
content: '';
position: absolute;
border-bottom: 1rpx solid #eaeef1;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
bottom: 0;
right: 0;
left: 0;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<view class="tui-numberbox">
<view class="tui-num__icon__box" :style="{background:iconBgColor,borderRadius:radius}" @tap="reduce"
:class="[disabled || min >= inputValue ? 'tui-disabled' : '']">
<text class="tui-numbox-icon tui-num__icon-reduce"
:style="{ color: iconColor, fontSize: iconSize + 'rpx',lineHeight:iconSize + 'rpx' }"></text>
</view>
<input type="number" v-model="inputValue" :disabled="disabled" @blur="blur" class="tui-num-input"
:style="{ color: color, fontSize: size + 'rpx', background: backgroundColor, height: height + 'rpx', minHeight: height + 'rpx', width: width + 'rpx' }" />
<view class="tui-num__icon__box" :style="{background:iconBgColor,borderRadius:radius}" @tap="plus"
:class="[disabled || inputValue >= max ? 'tui-disabled' : '']">
<text class="tui-numbox-icon tui-num__icon-plus"
:style="{ color: iconColor, fontSize: iconSize + 'rpx',lineHeight:iconSize + 'rpx' }"></text>
</view>
</view>
</template>
<script>
export default {
name: 'tuiNumberbox',
emits: ['change'],
props: {
value: {
type: [Number, String],
default: 1
},
//最小值
min: {
type: Number,
default: 1
},
//最大值
max: {
type: Number,
default: 99
},
//迈步大小 1 1.1 10...
step: {
type: Number,
default: 1
},
//是否禁用操作
disabled: {
type: Boolean,
default: false
},
iconBgColor: {
type: String,
default: 'transparent'
},
radius:{
type: String,
default: '50%'
},
//加减图标大小 rpx
iconSize: {
type: Number,
default: 22
},
iconColor: {
type: String,
default: '#666666'
},
//input 高度
height: {
type: Number,
default: 42
},
//input 宽度
width: {
type: Number,
default: 80
},
size: {
type: Number,
default: 28
},
//input 背景颜色
backgroundColor: {
type: String,
default: '#F5F5F5'
},
//input 字体颜色
color: {
type: String,
default: '#333'
},
//索引值,列表中使用
index: {
type: [Number, String],
default: 0
},
//自定义参数
custom: {
type: [Number, String],
default: 0
}
},
created() {
this.inputValue = +this.value;
},
data() {
return {
inputValue: 0
};
},
watch: {
value(val) {
this.inputValue = +val;
}
},
methods: {
getScale(val, step) {
let scale = 1;
let scaleVal = 1;
//浮点型
if (!Number.isInteger(step)) {
scale = Math.pow(10, (step + '').split('.')[1].length);
}
//浮点型
if (!Number.isInteger(val)) {
scaleVal = Math.pow(10, (val + '').split('.')[1].length);
}
return Math.max(scale, scaleVal);
},
calcNum: function(type) {
if (this.disabled || (this.inputValue == this.min && type === 'reduce') || (this.inputValue == this
.max && type === 'plus')) {
return;
}
const scale = this.getScale(Number(this.inputValue), Number(this.step));
let num = Number(this.inputValue) * scale;
let step = this.step * scale;
if (type === 'reduce') {
num -= step;
} else if (type === 'plus') {
num += step;
}
let value = Number((num / scale).toFixed(String(scale).length - 1));
if (value < this.min) {
value = this.min;
} else if (value > this.max) {
value = this.max;
}
this.handleChange(value, type);
},
plus: function() {
this.calcNum('plus');
},
reduce: function() {
this.calcNum('reduce');
},
blur: function(e) {
let value = e.detail.value;
if (value) {
if (~value.indexOf('.') && Number.isInteger(this.step) && Number.isInteger(Number(value))) {
value = value.split('.')[0];
}
value = Number(value);
if (value > this.max) {
value = this.max;
} else if (value < this.min) {
value = this.min;
}
} else {
value = this.min;
}
if ((value == this.value && value != this.inputValue) || !e.detail.value) {
this.inputValue = value;
}
this.handleChange(value, 'blur');
},
handleChange(value, type) {
if (this.disabled) return;
this.$emit('change', {
value: Number(value),
type: type,
index: this.index,
custom: this.custom
});
}
}
};
</script>
<style scoped>
@font-face {
font-family: 'numberbox';
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAASQAA0AAAAABtwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEdAAAABoAAAAciBpnRUdERUYAAARUAAAAHgAAAB4AKQALT1MvMgAAAZwAAABDAAAAVjxzSINjbWFwAAAB9AAAAEYAAAFK5zLpOGdhc3AAAARMAAAACAAAAAj//wADZ2x5ZgAAAkgAAACHAAAAnIfIEjxoZWFkAAABMAAAAC8AAAA2FZWEOWhoZWEAAAFgAAAAHAAAACQH3gOFaG10eAAAAeAAAAARAAAAEgwAAAFsb2NhAAACPAAAAAwAAAAMADAATm1heHAAAAF8AAAAHwAAACABEAAobmFtZQAAAtAAAAFJAAACiCnmEVVwb3N0AAAEHAAAAC0AAABV/+8iFXjaY2BkYGAA4gVmC5Tj+W2+MnCzMIDATWsFOQT9v5GFgbkeyOVgYAKJAgDrogf+AHjaY2BkYGBu+N/AEMPCAAJAkpEBFbAAAEcKAm142mNgZGBgYGWQYQDRDAxMQMwFhAwM/8F8BgALpAE5AHjaY2BkYWCcwMDKwMDUyXSGgYGhH0IzvmYwYuQAijKwMjNgBQFprikMDs9Yn01kbvjfwBDD3MDQABRmBMkBAOXpDHEAeNpjYYAAFghmZGAAAACdAA4AAAB42mNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZiesT6b+P8/AwOElvwnWQxVDwSMbAxwDiMTkGBiQAWMDMMeAABRZwszAAAAAAAAAAAAAAAwAE542iWKQQrCMBBF5xNpd0pQ7EIoTEnahSCTUNqdWz2A9TrieXKeXCc1qcPn/zfzh0BYv2pVH7oQgbvqdG5Yt/DTrNlPYz+wHvuuqhFSME4sFshTgKUsKfhH5lg8BSul3i5bS3mQdd0RIh2IjnvUrkXDd8zuhuFt86tY9fonIsSYgsXpB+cCGosAeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMGiTIxMjMwiWZmJQJRXVQoigTgjMd9QGIsgAFDsEBsAAAAAAAAB//8AAgABAAAADAAAABYAAAACAAEAAwAEAAEABAAAAAIAAAAAeNpjYGBgZACCq0vUOUD0TWsFORgNADPBBE4AAA==) format('woff');
font-weight: normal;
font-style: normal;
}
.tui-num__icon__box {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 12rpx;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.tui-numbox-icon {
font-family: 'numberbox' !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tui-num__icon-reduce:before {
content: '\e691';
}
.tui-num__icon-plus:before {
content: '\e605';
}
.tui-numberbox {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
.tui-num-input {
text-align: center;
margin: 0 6rpx;
font-weight: 400;
padding: 0;
border-width: 0;
}
/* #ifdef H5 */
::-webkit-inner-spin-button,
::-webkit-outer-spin-button{
-webkit-appearance: none;
margin: 0;
}
/* #endif */
.tui-disabled {
opacity: .5;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<view class="tui-sticky-class" :id="tui_Id">
<!--sticky 容器-->
<view :style="{height: stickyHeight,background:backgroundColor}" v-if="isFixed"></view>
<view :class="{'tui-sticky-fixed':isFixed}" :style="{top:isFixed?stickyTop+'px':'auto'}">
<slot name="header"></slot>
</view>
<!--sticky 容器-->
<!--内容-->
<slot name="content"></slot>
</view>
</template>
<script>
export default {
name: "tuiSticky",
emits: ['sticky', 'change'],
props: {
scrollTop: {
type: Number
},
//吸顶时与顶部的距离单位px
stickyTop: {
type: [Number, String]
// #ifndef H5
,
default: 0
// #endif
// #ifdef H5
,
default: 44
// #endif
},
//是否指定容器即内容放置插槽content内
container: {
type: Boolean,
default: false
},
//是否是原生自带header
isNativeHeader: {
type: Boolean,
default: true
},
//吸顶容器 高度 rpx
stickyHeight: {
type: String,
default: "auto"
},
//占位容器背景颜色
backgroundColor: {
type: String,
default: "transparent"
},
//是否重新计算[有异步加载时使用,设置大于0的数]
recalc: {
type: Number,
default: 0
},
//列表中的索引值
index: {
type: [Number, String],
default: 0
}
},
watch: {
scrollTop(newValue, oldValue) {
if (this.initialize != 0) {
this.updateScrollChange(() => {
this.updateStickyChange();
this.initialize = 0
});
} else {
this.updateStickyChange();
}
},
recalc(newValue, oldValue) {
this.updateScrollChange(() => {
this.updateStickyChange();
this.initialize = 0;
});
}
},
created() {
this.initialize = this.recalc
},
mounted() {
this.$nextTick(() => {
setTimeout(() => {
this.updateScrollChange();
}, 50)
})
},
data() {
const Id = `tui_${Math.ceil(Math.random() * 10e5).toString(36)}`
return {
timer: null,
top: 0,
height: 0,
isFixed: false,
initialize: 0, //重新初始化
tui_Id: Id
};
},
methods: {
updateStickyChange() {
const top = this.top;
const height = this.height;
const scrollTop = this.scrollTop
let stickyTop = this.stickyTop
// #ifdef H5
if (this.isNativeHeader) {
stickyTop = stickyTop - 44
stickyTop = stickyTop < 0 ? 0 : stickyTop
}
// #endif
if (this.container) {
this.isFixed = (scrollTop + stickyTop >= top && scrollTop + stickyTop < top + height) ? true : false
} else {
this.isFixed = scrollTop + stickyTop >= top ? true : false
}
//是否吸顶
this.$emit("sticky", {
isFixed: this.isFixed,
index: this.index
})
},
updateScrollChange(callback) {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
this.timer = setTimeout(() => {
const selectId = `#${this.tui_Id}`;
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select(selectId)
.fields({
size: true,
rect: true
}, res => {
if (res) {
this.top = res.top + (this.scrollTop || 0);
this.height = res.height;
callback && callback();
this.$emit("change", {
index: Number(this.index),
top: this.top
})
}
}).exec()
}, 0)
}
}
}
</script>
<style scoped>
.tui-sticky-fixed {
width: 100%;
position: fixed;
left: 0;
z-index: 888;
}
</style>

View File

@@ -0,0 +1,287 @@
<template>
<view class="tui-tabbar"
:class="{ 'tui-tabbar-fixed': isFixed, 'tui-unlined': unlined, 'tui-backdrop__filter': backdropFilter }"
:style="{ background: backgroundColor, zIndex: isFixed ? zIndex : 'auto' }">
<block v-for="(item, index) in tabBar" :key="index">
<view class="tui-tabbar-item" :class="{ 'tui-item-hump': item.hump }"
:style="{ backgroundColor: item.hump && !backdropFilter ? backgroundColor : 'none' }"
@tap="tabbarSwitch(index, item.hump, item.pagePath, item.verify)">
<view class="tui-icon-box" :class="{ 'tui-tabbar-hump': item.hump }">
<image :src="current == index ? item.selectedIconPath : item.iconPath"
:class="[item.hump ? '' : 'tui-tabbar-icon']" v-if="!item.name"></image>
<tui-icon :name="current===index?item.activeName:item.name" :customPrefix="item.customPrefix || ''"
:size="item.iconSize || iconSize" unit="rpx" :color="current == index?getActiveColor:color"
v-else></tui-icon>
<view :class="[item.isDot ? 'tui-badge-dot' : 'tui-badge']"
:style="{ color: badgeColor, backgroundColor: getBadgeBgColor }" v-if="item.num">
{{ item.isDot ? '' : item.num }}
</view>
</view>
<view class="tui-text-scale" :class="{ 'tui-text-hump': item.hump }"
:style="{ color: current == index ? getActiveColor : color }">{{ item.text }}</view>
</view>
</block>
<view :style="{ background: backgroundColor }" :class="{ 'tui-hump-box': hump }"
v-if="hump && !unlined && !backdropFilter"></view>
</view>
</template>
<script>
import tuiIcon from '@/components/thorui/tui-icon/tui-icon.vue'
export default {
name: 'tuiTabbar',
emits: ['click'],
components:{
tuiIcon
},
props: {
//当前索引
current: {
type: Number,
default: 0
},
//字体颜色
color: {
type: String,
default: '#666'
},
//字体选中颜色
selectedColor: {
type: String,
default: ''
},
//背景颜色
backgroundColor: {
type: String,
default: '#FFFFFF'
},
//是否需要中间凸起按钮
hump: {
type: Boolean,
default: false
},
iconSize: {
type: [Number, String],
default: 52
},
//固定在底部
isFixed: {
type: Boolean,
default: true
},
tabBar: {
type: Array,
default () {
return [];
}
},
//角标字体颜色
badgeColor: {
type: String,
default: '#fff'
},
//角标背景颜色
badgeBgColor: {
type: String,
default: ''
},
unlined: {
type: Boolean,
default: false
},
//是否开启高斯模糊效果[仅在支持的浏览器有效果]
backdropFilter: {
type: Boolean,
default: false
},
//z-index
zIndex: {
type: [Number, String],
default: 9999
}
},
data() {
return {};
},
computed:{
getActiveColor(){
return this.selectedColor || (uni && uni.$tui && uni.$tui.color.primary) || '#5677fc';
},
getBadgeBgColor(){
return this.badgeBgColor || (uni && uni.$tui && uni.$tui.color.pink) || '#f74d54';
}
},
methods: {
tabbarSwitch(index, hump, pagePath, verify) {
this.$emit('click', {
index: index,
hump: hump,
pagePath: pagePath,
verify: verify
});
}
}
};
</script>
<style scoped>
.tui-tabbar {
width: 100%;
height: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.tui-backdrop__filter {
/* Safari for macOS & iOS */
-webkit-backdrop-filter: blur(15px);
/* Google Chrome */
backdrop-filter: blur(15px);
}
.tui-tabbar-fixed {
position: fixed;
left: 0;
bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
box-sizing: content-box !important;
}
.tui-tabbar::before {
content: ' ';
width: 100%;
border-top: 1px solid #b2b2b2;
position: absolute;
top: -1rpx;
left: 0;
transform: scaleY(0.5) translateZ(0);
transform-origin: 0 0;
display: block;
z-index: 3;
}
.tui-tabbar-item {
height: 100%;
flex: 1;
display: flex;
text-align: center;
align-items: center;
flex-direction: column;
justify-content: space-between;
position: relative;
padding: 10rpx 0;
box-sizing: border-box;
z-index: 5;
}
.tui-icon-box {
position: relative;
}
.tui-item-hump {
height: 98rpx;
}
.tui-tabbar-icon {
width: 52rpx;
height: 52rpx;
display: block;
}
.tui-hump-box {
width: 120rpx;
height: 120rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -50rpx;
border-radius: 50%;
z-index: 4;
}
.tui-hump-box::after {
content: ' ';
height: 200%;
width: 200%;
border: 1px solid #b2b2b2;
position: absolute;
top: 0;
left: 0;
transform: scale(0.5) translateZ(0);
transform-origin: 0 0;
border-radius: 120rpx;
box-sizing: border-box;
display: block;
}
.tui-unlined::after {
height: 0 !important;
}
.tui-tabbar-hump {
width: 100rpx;
height: 100rpx;
position: absolute;
left: 50%;
-webkit-transform: translateX(-50%) rotate(0deg);
transform: translateX(-50%) rotate(0deg);
top: -40rpx;
-webkit-transition: all 0.2s linear;
transition: all 0.2s linear;
border-radius: 50%;
z-index: 5;
}
.tui-tabbar-hump image {
width: 100rpx;
height: 100rpx;
display: block;
}
.tui-hump-active {
-webkit-transform: translateX(-50%) rotate(135deg);
transform: translateX(-50%) rotate(135deg);
}
.tui-text-scale {
transform: scale(0.8);
font-size: 25rpx;
line-height: 28rpx;
transform-origin: center 100%;
}
.tui-text-hump {
position: absolute;
left: 50%;
bottom: 10rpx;
transform: scale(0.8) translateX(-50%);
transform-origin: 0 100%;
}
.tui-badge {
position: absolute;
font-size: 24rpx;
height: 32rpx;
min-width: 20rpx;
padding: 0 6rpx;
border-radius: 40rpx;
right: 0;
top: -5rpx;
transform: translateX(70%);
display: flex;
align-items: center;
justify-content: center;
}
.tui-badge-dot {
position: absolute;
height: 16rpx;
width: 16rpx;
border-radius: 50%;
right: -4rpx;
top: -4rpx;
}
</style>

View File

@@ -0,0 +1,346 @@
<template>
<view class="tui-tabs-view"
:class="[isFixed ? 'tui-tabs-fixed' : 'tui-tabs-relative', unlined ? 'tui-unlined' : '']" :style="{
width: tabsWidth + 'px',
height: height + 'rpx',
padding: `0 ${padding}rpx`,
background: backgroundColor,
top: isFixed ? top + 'px' : 'auto',
zIndex: isFixed ? zIndex : 'auto'
}" v-if="tabsWidth>0">
<view v-for="(item, index) in tabs" :key="index" class="tui-tabs-item"
:style="{ width: getItemWidth,height: height + 'rpx' }" @tap.stop="swichTabs(index)">
<view class="tui-tabs-title" :class="{'tui-tabs-disabled': item.disabled }" :style="{
color: currentTab == index ? getSelectedColor : color,
fontSize: size + 'rpx',
fontWeight: bold && currentTab == index ? 'bold' : 'normal',transform:`scale(${currentTab == index?scale:1})`
}">
{{ item[field] }} <text v-if="item[badgeField]">({{item[badgeField]}})</text>
<view :class="[item.isDot ? 'tui-badge__dot' : 'tui-tabs__badge']"
:style="{ color: badgeColor, backgroundColor: getBadgeBgColor }"
v-if="item.isDot">
{{ item.isDot ? '' : item[badgeField] }}
</view>
</view>
</view>
<view v-if="isSlider" class="tui-tabs-slider" :style="{
transform: 'translateX(' + scrollLeft + 'px)',
width: sliderWidth + 'rpx',
height: sliderHeight + 'rpx',
borderRadius: sliderRadius,
bottom: bottom,
background: getSliderBgColor,
marginBottom: bottom == '50%' ? '-' + sliderHeight / 2 + 'rpx' : 0
}"></view>
</view>
</template>
<script>
export default {
name: 'tuiTabs',
emits: ['change'],
props: {
//标签页
tabs: {
type: Array,
default () {
return [];
}
},
//显示文本字段名称
field: {
type: String,
default: 'name'
},
badgeField: {
type: String,
default: 'num'
},
//tabs宽度不传值则默认使用windowWidth单位px
width: {
type: [Number, String],
default: 0
},
//rpx
height: {
type: Number,
default: 80
},
//rpx 只对左右padding起作用上下为0
padding: {
type: Number,
default: 30
},
//背景色
backgroundColor: {
type: String,
default: '#FFFFFF'
},
//是否固定
isFixed: {
type: Boolean,
default: false
},
//px
top: {
type: Number,
// #ifndef H5
default: 0,
// #endif
// #ifdef H5
default: 44
// #endif
},
//是否去掉底部线条
unlined: {
type: Boolean,
default: false
},
//当前选项卡
currentTab: {
type: Number,
default: 0
},
isSlider: {
type: Boolean,
default: true
},
//滑块宽度
sliderWidth: {
type: Number,
default: 68
},
//滑块高度
sliderHeight: {
type: Number,
default: 6
},
//滑块背景颜色
sliderBgColor: {
type: String,
default: ''
},
sliderRadius: {
type: String,
default: '50rpx'
},
//滑块bottom
bottom: {
type: String,
default: '0'
},
//标签页宽度
itemWidth: {
type: String,
default: ''
},
//字体颜色
color: {
type: String,
default: '#666'
},
//选中后字体颜色
selectedColor: {
type: String,
default: ''
},
//字体大小
size: {
type: Number,
default: 28
},
//选中后 是否加粗 ,未选中则无效
bold: {
type: Boolean,
default: false
},
//2.3.0+
scale: {
type: [Number, String],
default: 1
},
//角标字体颜色
badgeColor: {
type: String,
default: '#fff'
},
//角标背景颜色
badgeBgColor: {
type: String,
default: ''
},
zIndex: {
type: [Number, String],
default: 996
}
},
watch: {
currentTab() {
this.checkCor();
},
tabs() {
this.checkCor();
},
width(val) {
this.tabsWidth = val;
this.checkCor();
}
},
computed: {
getItemWidth() {
let width = 100 / (this.tabs.length || 4) + '%'
return this.itemWidth ? this.itemWidth : width
},
getSliderBgColor() {
return this.sliderBgColor || (uni && uni.$tui && uni.$tui.color.primary) || '#5677fc';
},
getSelectedColor() {
return this.selectedColor || (uni && uni.$tui && uni.$tui.color.primary) || '#5677fc';
},
getBadgeBgColor() {
return this.badgeBgColor || (uni && uni.$tui && uni.$tui.color.pink) || '#f74d54';
}
},
created() {
// 高度自适应
setTimeout(() => {
uni.getSystemInfo({
success: res => {
this.winWidth = res.windowWidth;
this.tabsWidth = this.width == 0 ? this.winWidth : Number(this.width);
this.checkCor();
}
});
}, 0);
},
data() {
return {
winWidth: 0,
tabsWidth: 0,
scrollLeft: 0
};
},
methods: {
checkCor: function() {
let tabsNum = this.tabs.length;
let padding = uni.upx2px(Number(this.padding));
let width = this.tabsWidth - padding * 2;
let left = (width / tabsNum - uni.upx2px(Number(this.sliderWidth))) / 2 + padding;
let scrollLeft = left;
if (this.currentTab > 0) {
scrollLeft = scrollLeft + (width / tabsNum) * this.currentTab;
}
this.scrollLeft = scrollLeft;
},
// 点击标题切换当前页时改变样式
swichTabs: function(index) {
let item = this.tabs[index];
if (item && item.disabled) return;
if (this.currentTab == index) {
return false;
} else {
this.$emit('change', {
index: Number(index),
item: item
});
}
}
}
};
</script>
<style scoped>
.tui-tabs-view {
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
.tui-tabs-relative {
position: relative;
}
.tui-tabs-fixed {
position: fixed;
left: 0;
}
.tui-tabs-fixed::before,
.tui-tabs-relative::before {
content: '';
position: absolute;
border-bottom: 1rpx solid #eaeef1;
-webkit-transform: scaleY(0.5) translateZ(0);
transform: scaleY(0.5) translateZ(0);
transform-origin: 0 100%;
bottom: 0;
right: 0;
left: 0;
}
.tui-unlined::before {
border-bottom: 0 !important;
}
.tui-tabs-item {
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.tui-tabs-disabled {
opacity: 0.6;
}
.tui-tabs-title {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 3;
overflow: visible;
transition: all 0.15s ease-in-out;
}
.tui-tabs-slider {
position: absolute;
left: 0;
transition: all 0.3s ease-in-out;
z-index: 1;
transform-style: preserve-3d;
}
.tui-tabs__badge {
position: absolute;
font-size: 24rpx;
height: 32rpx;
min-width: 20rpx;
padding: 0 6rpx;
border-radius: 40rpx;
right: 0;
top: 0;
transform: translate(88%, -50%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
z-index: 4;
font-weight: normal !important;
}
.tui-badge__dot {
position: absolute;
height: 16rpx;
width: 16rpx;
border-radius: 50%;
right: -10rpx;
top: -10rpx;
z-index: 4;
}
</style>

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

9
jsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}

21
main.js Normal file
View File

@@ -0,0 +1,21 @@
import App from './App';
// 引入 uView UI
import uView from './uni_modules/vk-uview-ui';
import { createSSRApp } from 'vue';
import { setupPinia } from './sheep/store';
export function createApp() {
const app = createSSRApp(App);
app.use(uView);
setupPinia(app);
return {
app,
};
}

239
manifest.json Normal file
View File

@@ -0,0 +1,239 @@
{
"name": "晚趣语音",
"appid": "__UNI__458F814",
"description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
"versionName": "2.1.0",
"versionCode": 183,
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"nvueLaunchMode": "fast",
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"safearea": {
"bottom": {
"offset": "none"
}
},
"modules": {
"Payment": {},
"Share": {},
"VideoPlayer": {},
"OAuth": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.READ_SMS\"/>",
"<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
"<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
"<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
],
"minSdkVersion": 21,
"schemes": "shopro"
},
"ios": {
"urlschemewhitelist": [
"baidumap",
"iosamap"
],
"dSYMs": false,
"privacyDescription": {
"NSPhotoLibraryUsageDescription": "需要同意访问您的相册选取图片才能完善该条目",
"NSPhotoLibraryAddUsageDescription": "需要同意访问您的相册才能保存该图片",
"NSCameraUsageDescription": "需要同意访问您的摄像头拍摄照片才能完善该条目",
"NSUserTrackingUsageDescription": "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
},
"urltypes": "shopro",
"capabilities": {
"entitlements": {
"com.apple.developer.associated-domains": [
"applinks:shopro.sheepjs.com"
]
}
},
"idfa": true
},
"sdkConfigs": {
"speech": {
"ifly": {}
},
"ad": {},
"oauth": {
"apple": {},
"weixin": {
"appid": "wxae7a0c156da9383b",
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
}
},
"payment": {
"weixin": {
"__platform__": [
"ios",
"android"
],
"appid": "wxae7a0c156da9383b",
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
},
"alipay": {
"__platform__": [
"ios",
"android"
]
}
},
"share": {
"weixin": {
"appid": "wxae7a0c156da9383b",
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
}
}
},
"orientation": [
"portrait-primary"
],
"splashscreen": {
"androidStyle": "common",
"iosStyle": "common",
"useOriginalMsgbox": true
},
"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"
}
}
}
}
},
"quickapp": {},
"quickapp-native": {
"icon": "/static/logo.png",
"package": "com.example.demo",
"features": [
{
"name": "system.clipboard"
}
]
},
"quickapp-webview": {
"icon": "/static/logo.png",
"package": "com.example.demo",
"minPlatformVersion": 1070,
"versionName": "1.0.0",
"versionCode": 100
},
"mp-weixin": {
"appid": "wx2708b75e03e8440a",
"setting": {
"urlCheck": false,
"minified": true,
"postcss": true
},
"optimization": {
"subPackages": true
},
"plugins": {},
"lazyCodeLoading": "requiredComponents",
"usingComponents": {},
"permission": {},
"requiredPrivateInfos": [
"chooseAddress"
]
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"mp-jd": {
"usingComponents": true
},
"h5": {
"template": "index.html",
"router": {
"mode": "hash",
"base": "./"
},
"sdkConfigs": {
"maps": {}
},
"async": {
"timeout": 20000
},
"title": "晚趣语音",
"optimization": {
"treeShaking": {
"enable": true
}
}
},
"vueVersion": "3",
"_spaceID": "192b4892-5452-4e1d-9f09-eee1ece40639",
"locale": "zh-Hans",
"fallbackLocale": "zh-Hans"
}

103
package.json Normal file
View File

@@ -0,0 +1,103 @@
{
"id": "shopro",
"name": "shopro",
"displayName": "晚趣语音",
"version": "2.2.0",
"description": "晚趣语音一套代码同时发行到iOS、Android、H5、微信小程序多个平台请使用手机扫码快速体验强大功能",
"scripts": {
"prettier": "prettier --write \"{pages,sheep}/**/*.{js,json,tsx,css,less,scss,vue,html,md}\""
},
"repository": "https://github.com/sheepjs/shop.git",
"keywords": [
"商城",
"B2C",
"商城模板"
],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/sheepjs/shop/issues"
},
"homepage": "https://github.com/dcloudio/hello-uniapp#readme",
"dcloudext": {
"category": [
"前端页面模板",
"uni-app前端项目模板"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "u",
"vue3": "y"
}
}
}
},
"dependencies": {
"dayjs": "^1.11.7",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"luch-request": "^3.0.8",
"pinia": "^2.0.33",
"pinia-plugin-persist-uni": "^1.2.0",
"weixin-js-sdk": "^1.6.0"
},
"devDependencies": {
"prettier": "^2.8.7",
"vconsole": "^3.15.0"
}
}

625
pages.json Normal file
View File

@@ -0,0 +1,625 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^s-(.*)": "@/sheep/components/s-$1/s-$1.vue",
"^su-(.*)": "@/sheep/ui/su-$1/su-$1.vue"
}
},
"pages": [
{
"path": "pages/tabbar/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/index/login",
"style": {
"navigationBarTitleText": "登录"
}
}
],
"subPackages": [
{
"root": "pages/trend",
"pages": [
{
"path": "city/list",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "detail/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "my/list",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
}
]
},
{
"root": "pages/reward",
"pages": [
{
"path": "list",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
}
]
},
{
"root": "pages/worker",
"pages": [
{
"path": "levelList/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "workerList/index",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "workerList/set",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "blind/index",
"style": {
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/clerk",
"pages": [
{
"path": "apply/index",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "fans/list",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "apply/edit",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "detail/index",
"style": {
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/goods",
"pages": [
{
"path": "comment/worker/add",
"style": {
"navigationBarTitleText": "评价店员"
},
"meta": {
"auth": true
}
}
]
},
{
"root": "pages/order",
"pages": [
{
"path": "worker/confirm",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "blind/confirm",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "worker/aftersale/apply",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "my/aftersale/apply",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "worker/aftersale/list",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "blind/detail",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "worker/detail",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "my/detail",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "worker/list",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "blind/list",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "my/list",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "receive/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "my/index",
"style": {
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/user",
"pages": [{
"path": "info",
"style": {
"navigationBarTitleText": "我的信息"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户信息",
"group": "用户中心"
}
},
{
"path": "vip",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "detail/index",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{
"path": "wallet/money",
"style": {
"navigationBarTitleText": "我的余额"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户余额",
"group": "用户中心"
}
},
{
"path": "wallet/score",
"style": {
"navigationBarTitleText": "我的积分"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户积分",
"group": "用户中心"
}
}
]
},
{
"root": "pages/commission",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "分销"
},
"meta": {
"auth": true,
"sync": true,
"title": "分销中心",
"group": "分销商城"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的佣金"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户佣金",
"group": "分销中心"
}
},
{
"path": "order",
"style": {
"navigationBarTitleText": "分销订单"
},
"meta": {
"auth": true,
"sync": true,
"title": "分销订单",
"group": "分销商城"
}
},
{
"path": "team",
"style": {
"navigationBarTitleText": "我的团队"
},
"meta": {
"auth": true,
"sync": true,
"title": "我的团队",
"group": "分销商城"
}
}, {
"path": "promoter",
"style": {
"navigationBarTitleText": "推广人排行榜"
},
"meta": {
"auth": true,
"sync": true,
"title": "推广人排行榜",
"group": "分销商城"
}
}, {
"path": "commission-ranking",
"style": {
"navigationBarTitleText": "佣金排行榜"
},
"meta": {
"auth": true,
"sync": true,
"title": "佣金排行榜",
"group": "分销商城"
}
}, {
"path": "withdraw",
"style": {
"navigationBarTitleText": "申请提现"
},
"meta": {
"auth": true,
"sync": true,
"title": "申请提现",
"group": "分销商城"
}
}
]
},
{
"root": "pages/app",
"pages": [{
"path": "sign",
"style": {
"navigationBarTitleText": "签到中心"
},
"meta": {
"auth": true,
"sync": true,
"title": "签到中心",
"group": "应用"
}
}]
},
{
"root": "pages/public",
"pages": [{
"path": "setting",
"style": {
"navigationBarTitleText": "系统设置"
},
"meta": {
"auth": true,
"sync": true,
"title": "系统设置",
"group": "通用"
}
},
{
"path": "richtext",
"style": {
"navigationBarTitleText": "富文本"
},
"meta": {
"sync": true,
"title": "富文本",
"group": "通用"
}
},
{
"path": "faq",
"style": {
"navigationBarTitleText": "常见问题"
},
"meta": {
"sync": true,
"title": "常见问题",
"group": "通用"
}
},
{
"path": "error",
"style": {
"navigationBarTitleText": "错误页面"
}
},
{
"path": "webview",
"style": {
"navigationBarTitleText": ""
}
}
]
},
{
"root": "pages/coupon",
"pages": [{
"path": "list",
"style": {
"navigationBarTitleText": "领券中心"
},
"meta": {
"auth": true,
"sync": true,
"title": "领券中心",
"group": "优惠券"
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "优惠券"
},
"meta": {
"auth": false,
"sync": true,
"title": "优惠券详情",
"group": "优惠券"
}
}
]
},
{
"root": "pages/chat",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "客服"
},
"meta": {
"auth": true,
"sync": true,
"title": "客服",
"group": "客服"
}
}]
},
{
"root": "pages/im",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "聊天"
},
"meta": {
"auth": true,
"sync": true,
"title": "聊天",
"group": "聊天"
}
}]
},
{
"root": "pages/point",
"pages": [
{
"path": "recharge",
"style": {
"navigationBarTitleText": "充值积分"
},
"meta": {
"auth": true,
"sync": true,
"title": "充值积分",
"group": "支付"
}
},
{
"path": "recharge-log",
"style": {
"navigationBarTitleText": "充值记录"
},
"meta": {
"auth": true,
"sync": true,
"title": "充值记录",
"group": "支付"
}
},
{
"path": "result",
"style": {
"navigationBarTitleText": "支付结果"
},
"meta": {
"auth": true,
"sync": true,
"title": "支付结果",
"group": "支付"
}
}
]
},
{
"root": "pages/pay",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "收银台"
}
},
{
"path": "result",
"style": {
"navigationBarTitleText": "支付结果"
}
},
{
"path": "worker/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "point/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "vip/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "reward/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "worker/result",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "recharge",
"style": {
"navigationBarTitleText": "充值余额"
},
"meta": {
"auth": true,
"sync": true,
"title": "充值余额",
"group": "支付"
}
},
{
"path": "recharge-log",
"style": {
"navigationBarTitleText": "充值记录"
},
"meta": {
"auth": true,
"sync": true,
"title": "充值记录",
"group": "支付"
}
}
]
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "晚趣语音",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
}

451
pages/app/sign.vue Normal file
View File

@@ -0,0 +1,451 @@
<!-- 签到界面 -->
<template>
<s-layout title="签到有礼">
<s-empty v-if="state.loading" icon="/static/data-empty.png" text="签到活动还未开始" />
<view v-if="state.loading" />
<view class="sign-wrap" v-else-if="!state.loading">
<!-- 签到日历 -->
<view class="content-box calendar">
<view class="sign-everyday ss-flex ss-col-center ss-row-between ss-p-x-30">
<text class="sign-everyday-title">签到日历</text>
<view class="sign-num-box">
已连续签到 <text class="sign-num">{{ state.signInfo.continuousDay }}</text>
</view>
</view>
<view
class="list acea-row row-between-wrapper"
style="
padding: 0 30rpx;
height: 240rpx;
display: flex;
justify-content: space-between;
align-items: center;
"
>
<view class="item" v-for="(item, index) in state.signConfigList" :key="index">
<view
:class="
(index === state.signConfigList.length ? 'reward' : '') +
' ' +
(state.signInfo.continuousDay >= item.day ? 'rewardTxt' : '')
"
>
{{ item.day }}
</view>
<view
class="venus"
:class="
(index + 1 === state.signConfigList.length ? 'reward' : '') +
' ' +
(state.signInfo.continuousDay >= item.day ? 'venusSelect' : '')
"
>
</view>
<view class="num" :class="state.signInfo.continuousDay >= item.day ? 'on' : ''">
+ {{ item.point }}
</view>
</view>
</view>
<!-- 签到按钮 -->
<view class="myDateTable">
<view class="ss-flex ss-col-center ss-row-center sign-box ss-m-y-40">
<button
class="ss-reset-button sign-btn"
v-if="!state.signInfo.todaySignIn"
@tap="onSign"
>
签到
</button>
<button class="ss-reset-button already-btn" v-else disabled> 已签到 </button>
</view>
</view>
</view>
<!-- 签到说明 TODO @芋艿签到这里改成已累计签到改版接入 sheepjs -->
<view class="bg-white ss-m-t-16 ss-p-t-30 ss-p-b-60 ss-p-x-40">
<view class="activity-title ss-m-b-30">签到说明</view>
<view class="activity-des">1已累计签到{{state.signInfo.totalDay}}</view>
<view class="activity-des">
2据说连续签到第 {{ state.maxDay }} 天可获得超额积分一定要坚持签到哦~~~
</view>
</view>
</view>
<!-- 签到结果弹窗 -->
<su-popup :show="state.showModel" type="center" round="10" :isMaskClick="false">
<view class="model-box ss-flex-col">
<view class="ss-m-t-56 ss-flex-col ss-col-center">
<text class="cicon-check-round"></text>
<view class="score-title">
<text v-if="state.signResult.point">{{ state.signResult.point }} 积分 </text>
<text v-if="state.signResult.experience"> {{ state.signResult.experience }} 经验</text>
</view>
<view class="model-title ss-flex ss-col-center ss-m-t-22 ss-m-b-30">
已连续打卡 {{ state.signResult.day }}
</view>
</view>
<view class="model-bg ss-flex-col ss-col-center ss-row-right">
<view class="title ss-m-b-64">签到成功</view>
<view class="ss-m-b-40">
<button class="ss-reset-button confirm-btn" @tap="onConfirm">确认</button>
</view>
</view>
</view>
</su-popup>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onReady } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import SignInApi from '@/sheep/api/member/signin';
const headerBg = sheep.$url.css('/static/img/shop/app/sign.png');
const state = reactive({
loading: true,
signInfo: {}, // 签到信息
signConfigList: [], // 签到配置列表
maxDay: 0, // 最大的签到天数
showModel: false, // 签到弹框
signResult: {}, // 签到结果
});
// 发起签到
async function onSign() {
const { code, data } = await SignInApi.createSignInRecord();
if (code !== 0) {
return;
}
state.showModel = true;
state.signResult = data;
// 重新获得签到信息
await getSignInfo();
}
// 签到确认刷新页面
function onConfirm() {
state.showModel = false;
}
// 获得个人签到统计
async function getSignInfo() {
const { code, data } = await SignInApi.getSignInRecordSummary();
if (code !== 0) {
return;
}
state.signInfo = data;
state.loading = false;
}
// 获取签到配置
async function getSignConfigList() {
const { code, data } = await SignInApi.getSignInConfigList();
if (code !== 0) {
return;
}
state.signConfigList = data;
if (data.length > 0) {
state.maxDay = data[data.length - 1].day;
}
}
onReady(() => {
getSignInfo();
getSignConfigList();
});
// TODO 芋艿1css 需要优化例如说引入的图片2删除多余的样式
</script>
<style lang="scss" scoped>
.header-box {
border-top: 2rpx solid rgba(#dfdfdf, 0.5);
}
// 日历
.calendar {
background: #fff;
.sign-everyday {
height: 100rpx;
background: rgba(255, 255, 255, 1);
border: 2rpx solid rgba(223, 223, 223, 0.4);
.sign-everyday-title {
font-size: 32rpx;
color: rgba(51, 51, 51, 1);
font-weight: 500;
}
.sign-num-box {
font-size: 26rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
.sign-num {
font-size: 30rpx;
font-weight: 600;
color: #ff6000;
padding: 0 10rpx;
font-family: OPPOSANS;
}
}
}
// 年月日
.bar {
height: 100rpx;
.date {
font-size: 30rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #333333;
line-height: normal;
}
}
.cicon-back {
margin-top: 6rpx;
font-size: 30rpx;
color: #c4c4c4;
line-height: normal;
}
.cicon-forward {
margin-top: 6rpx;
font-size: 30rpx;
color: #c4c4c4;
line-height: normal;
}
// 星期
.week {
.week-item {
font-size: 24rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
flex: 1;
}
}
// 日历表
.myDateTable {
display: flex;
flex-wrap: wrap;
.dateCell {
width: calc(750rpx / 7);
height: 80rpx;
font-size: 26rpx;
font-weight: 400;
color: rgba(51, 51, 51, 1);
}
}
}
.is-sign {
width: 48rpx;
height: 48rpx;
position: relative;
.is-sign-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
line-height: normal;
}
.is-sign-image {
position: absolute;
left: 0;
top: 0;
width: 48rpx;
height: 48rpx;
}
}
.cell-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #333333;
line-height: normal;
}
.cicon-title {
position: absolute;
right: -10rpx;
top: -6rpx;
font-size: 20rpx;
color: red;
}
// 签到按钮
.sign-box {
height: 140rpx;
width: 100%;
.sign-btn {
width: 710rpx;
height: 80rpx;
border-radius: 35rpx;
font-size: 30rpx;
font-weight: 500;
box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
background: linear-gradient(90deg, #ff6000, #fe832a);
color: #fff;
}
.already-btn {
width: 710rpx;
height: 80rpx;
border-radius: 35rpx;
font-size: 30rpx;
font-weight: 500;
}
}
.model-box {
width: 520rpx;
// height: 590rpx;
background: linear-gradient(177deg, #ff6000 0%, #fe832a 100%);
// background: linear-gradient(177deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 10rpx;
.cicon-check-round {
font-size: 70rpx;
color: #fff;
}
.score-title {
font-size: 34rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #fcff00;
}
.model-title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
}
.model-bg {
width: 520rpx;
height: 344rpx;
background-size: 100% 100%;
background-image: v-bind(headerBg);
background-repeat: no-repeat;
border-radius: 0 0 10rpx 10rpx;
.title {
font-size: 34rpx;
font-weight: bold;
// color: var(--ui-BG-Main);
color: #ff6000;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #999999;
}
.cancel-btn {
width: 220rpx;
height: 70rpx;
border: 2rpx solid #ff6000;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ff6000;
line-height: normal;
margin-right: 10rpx;
}
.confirm-btn {
width: 220rpx;
height: 70rpx;
background: linear-gradient(90deg, #ff6000, #fe832a);
box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
line-height: normal;
}
}
}
//签到说明
.activity-title {
font-size: 32rpx;
font-weight: 500;
color: #333333;
line-height: normal;
}
.activity-des {
font-size: 26rpx;
font-weight: 500;
color: #666666;
line-height: 40rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.rewardTxt {
width: 74rpx;
height: 32rpx;
background-color: #f4b409;
border-radius: 16rpx;
font-size: 20rpx;
color: #a57d3f;
line-height: 32rpx;
}
.venusSelect {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QzkwRkI4NEFEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QzkwRkI4NDlEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkX00M0AAAfVSURBVHjaxFpdcBNVFD67STbppn9poU0LtSBYykBlgKrjqKOtihbq8ICKILyJwxM+6fjCk2/6pC+O4hs+6fgAbWFw1OID+ICMAwwKlRmKLW3Tpvlrskk3m13Pvdm0NNn//HA73+ymd3Pv+e4595xzT5bJ3DgJNWyMelUqNaD36a8N+9kaEeMR3yAERES952sxsbtya2lIbgQxqH72IT5EbEW8pZKuHkGlugyLyT3aBtW+qpJkKb/qgEeMIAYNnhlUn+Edz2NuokqtNVdTTbqVyhO0Q67qJMt2MnXbTq/cp+9+ZkyOYYFxN4EiLaF5SbokccyKkWRS1z4we4ZDfIE4hmhxvJLNu8HTtg85ekGRRcjOXwIp9pfT4aKIs4iP+f4zYrl78HPEqbLMJNAPXHBI/SQjSTd+PoD3LpCi15wMGVBlSiM+NfSihJ8Jjlt4Rheu5n7wtL+B93IJPO37aH8Z45+ohBdtKUtz7a+jJLK+/dN+BTX5p5MpWh6HF6XNE9iLwr9mSG6VZP65bPR6NVI1BwQZF3BtA+Bq2oG3Pt3HFCVnfUHaX6XQHCeXgVz8Nojz4+SDzTgo2yfIBV9G89utzi5DtRvDcnQ+JSciyd+rH+hdjb22tFOp5mreCUrocg0CPet5LATJvHbldWSiGllIzZpdeR2ZqFPtZTNJSIQeQGv3DucEbcrLmkRSXvP/cs4RZm6Ow/T1McffpyiSJTZ+jDfOZFDlOuARo5p9aKJ2IYkpCN+9AmLsIcSm/3Y0BkWpPCPRX9/nDVI1BTTAI0YRA9r99gWbv3MF90MGvG4WQrfHnRMslWcQMRL55SivbaJk064FjxhFDGj0gad5u22zkrMZiExcBQ7JcW4GlhfuQ3L+gSMTzc9fItcgYiTy8xHeTIOGmnMHtoEn2G971cP3MAUTk+B2MeBBkh4kGbp92ZEGyfxEDj1NLl56j9fbg4U9N6C179zNPZhK7cV7yRZkSYTI3atIjgUXywCLINf03L8gRGZtj0dA5CDyaMg5SPZk+OLhFZLMwtg7hTLDKGJAM09s6QGuY6+upxLTKcgkFmE5FUdFIYQEiHgv4VURl4BjgZon0SA9EeKKi1kZliU8Nnn84OabwIPw+huB8zdR+OqbwdcYMAwB4ux1yEYmtLp+I5WBdft/EApx8Cs9cu6mblyxXXTl9FpOFGDyjzGQMwlwIQlUEDAMagqvhBTVHl4ZplDezpsq+UdOTkMuIcByfBYElCWHmsgpLHQ/NwTe+gaTBH0XWkgGpPgDrfLHl4gTTOj8IVLdTqkF2aIslwF+2zCeGDjzIJ5OwtSVc6Cko9QMCUmWyZMiZJn8cI8E7Lwm11wJOTypBPuHoCG4yVrgz2VBmFBj69pGTvv1hVSN0XSxHE8LRUbaW9G01wdPvHgQQtcugJwMU5LFpIrWDjXMUGIsMNRR5DwcBPcMQV1L0NKchYHIkU2WkiVrTvgT6XEjyN/TY08R5KyAaWc6n3tagMvjhuCzQ+jlOmylVPRZjw/an9kPdYF1lucjIPLJmERoyP9j+8GflIIXPYW4XOKVJAmWZ27Y8nAMaq5t9yB4WjeumJ4hOaIErx/W978JXH2TbY9K5ZNKMhzC5dSjyTapQ5IyFxozvLJGz9Hp/Am+Y7utH8kan+yDhYVp8lVgdDWX33f+ji3gruNpnLPTxNl/8vKtbeTAeKDj0DmhuGShT3Jxkp4guGCv5cnT0QX0hgq4dOnBSshIxxehUbZJbu4OSJEpbXJvnxf0zoP6JMP/keVGTfZYEiCDQlttmXjE1hlTnJ3A+Ketuc53RwSz86AuSXFxitYzueBWUyGWE9E1pqiozoRhXdQJMLAaEyUMMdJyGp2Ux4Lm7iG5h5rkNhweFUpLFtonZANNTmOA3WzmFiGLGY3XlSel0DpOCzRs6gWXj4fkgwnIhKZIcKDhgsTLTCICfMs683gb1tbchiNjgt0TfYFkym7JQkyl8nkoixkMesiGzT1Q19q20t+0dTvwnU/A0iQSDYeoNinBQLOTE/2BjUcvCE5rMoJ2XcSYYBr3FOPmoHHLU+APdtJMpvg7bp8XAr19mKJ1weK9u5CJxUDpMt+HxfJ2HbsoVLzwa1aT4fw8dD3/Au43lv7YYjQF19gAHXv6UYMJa7UepQZFJzMT9fp9lJidorCvgbfkSRXbBB2UDRX5MdREnZYNwZECRcxQXLUnl8s5KPw6MNFsdBFzzdaaE8xGwrUx0eXZORrw3c1NNdEk0ZwUi2OQn7fvZBz9fEZKDjNzFLqn7dYApnVtNtKvecx5oxVfHCsmSt4ts/0rrxiO5NO6jvUWyC0guZgT+SPllu4Jzjr9AT0bjqKWQ1qH0RWQfvKcwzm+s6BB01X6RC1pHIf82w02NRmnsng7WjX28iJqLu5Ec4XXSE5XYg+S91A+UlHS/H2riXfq1n3N8mM2HKOOgutsodkNqZKIMxGQokvFw40jhnFMoZZ70CzyrpLd2S0kb00Oa5KMJDC8LAHL4QEmK4HGKYaSq+/bJFTyZ/GyX+VK3pzUStA1SRJrkTNZrWHG1e8IGuMZt5eqrUH9U8iwUbVci1w1BKnW65RWSVaVnBomAKoI3E9IQEEipX3jap9Q1hxmBElBocp/AmIYcQaRQSQQ36r/E8odvepOxoa5khfRT1pf+8q0/wUYAFU/P0XyeZQPAAAAAElFTkSuQmCC');
}
.venus {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDQ3N0E3RkJEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDQ3N0E3RkFEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnZClGIAAAT7SURBVHja7JtLcxtFEMdnn1rJUhIrTiS7KLsK/EXwNck3gCMcuUFx4QvACW48buHGMZxJPkiowpjC2LElcKzXSqul/6OdoOB9zezsCCimqmvXtdqd+U33dPf0rq2Tn54zg81KjrGuB75x8FbuddsQWIvkS5IxySA5b5no2DUE94TkKPk7IHmf5JDkUQJdW7MNw623o+Ra698KmAdnDNLeIJwRSHvDcLVDVnYynU771fnLl9eFcLZts+VymQf5iJ6pzfFYJeKgT/IZyTskXdWOPM9jjYbPLMticRyz2Sxk8/lc9XFDksckH1IcDKtq8FOSD6rMIuCCoPHXrBIk/qYDC0MlyO1kTBOSj6uuwXerwPn+63DrrdFo8OsV2ns6nEy3Chwg8lpFyK4OwNrgNGpSvxfFoDzP5etJR8PzsiYETmk+X5BjmpkBrHPGU109TeKqv5X3rT3QQ3ObaPDGRjIZXWZpol+b/cebUUA4iuHw938UoNbk9+zsjP16eqoV4JfjH1uqgLjxe10DiaKIDYZDNqfU7OrqSifjkzxIuwDubV2juLi8ZK7j8oT74uJSJ+BRHqQtC6cS/7A9wtrDvb7v84A9Go2UMyQZSFsWrmz6td4GgyF38a7r8lgGefHiQjnjkYG064ZDmjWktecRnEOCDa9DpjqdTrnUDSk2vJXgsHGdTKa0t5vx/V0Yhvy4WMx5hdf3VqYJDQqTxW+QdmECPM8h8flvPPRFR/QXBEEuKEw9Yz/5AyoDtBkei5zriyw4pGVFmoOHPD7+mQ/ccR1mWzZpyuIacx2Hgzl0FJkIjjBTHBeLiO6LuDZH4zFbRkt63oLt7+8XAmJcIhFP0eTn2C9CgzBTrPjUp7XbW6VSJGjx5OSEBhsz2wGkxc0R967LuukuSWKaFEwMnXJQSL/fZ51Op/QSuL5OdVjY7beFBq2sAlHZ/A8aOTg4YKenv/EBr8Nl5ZUO6jPJRGCg0dJie/d3WbPZlMpPMwpZsN0Y2sOVb7PcOzou22CGe3u7fB3J3Cd+udvvScG9soT0Kt13tAZj4UVRwHmWtZClcj+azX6vxx0GN8ECUFyHjnsEp+KlM8b3TBTKBCDqkA/SIFV20jCbO3duk8OIcgHF7G9ttbinVYFLcTBgeAAP+vc4mAkJVywLibARpZvOjWWA36rApYSI1+DSMhltkJPpRGIyJrXAZeWiWiCnk9kNU4RGl8m5MN1VLFzwWKobLm83kQtZbnMb8lgYJ2aIYH//3g73lEHQ5ECAXYUSu7QWZeCKqmoCcqSyPkQgd8lHbne3uSMRbWenSwNtU476BxtT9oJQOZ3OKKloq6SmmXBlyoZKb3nG4wnXyt1ul9261UkN9ggjvd497mDOz885KGN3pfvKg1OuixY15JCHh2/ymFjUms2AMqD9BFB/qwUQg5ZtrVY9b7H/LxtmZSCbaCr9KgGmpEeG6qpzM2tQBHudb5eKNLfKiUMzgAIyL6uRreHklB9qX4MDlQfLpHUV4AY6AB+rzl4ZyIqa+0aHiX6UlDTwMcK2CqQoEGmEE5+RfFK4N636vWjKh0Cp5ceS38k8JXko8yHQ7W7PXKBPBvYwGegNF4/q12g05mV7HXClqgs1ffEr+/LmaTIx0nCb+uI3U5M64Tadi5aBrBXORLKdB1k7nKndhID8GqUaErze/coEXJ1OJm9CY2bw3wr+FGAAoa6PIUihovYAAAAASUVORK5CYII=');
background-repeat: no-repeat;
background-size: 100% 100%;
width: 56rpx;
height: 56rpx;
margin: 10rpx 0;
}
.num {
font-size: 24rpx;
font-family: 'Guildford Pro';
}
.item {
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
height: 130rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.on {
background-color: #999 !important;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<s-goods-item
:title="goodsData.spuName"
:img="goodsData.picUrl"
:price="goodsData.price"
:skuText="goodsData.introduction"
priceColor="#FF3000"
:titleWidth="400"
/>
</template>
<script setup>
const props = defineProps({
goodsData: {
type: Object,
default: {},
},
});
</script>

View File

@@ -0,0 +1,102 @@
<template>
<view class="send-wrap ss-flex">
<view class="left ss-flex ss-flex-1">
<uni-easyinput
class="ss-flex-1 ss-p-l-22"
:inputBorder="false"
:clearable="false"
v-model="message"
placeholder="请输入你要咨询的问题"
></uni-easyinput>
</view>
<text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>
<text
v-if="!message"
class="sicon-edit"
:class="{ 'is-active': toolsMode === 'tools' }"
@tap.stop="onTools('tools')"
></text>
<button v-if="message" class="ss-reset-button send-btn" @tap="sendMessage">
发送
</button>
</view>
</template>
<script setup>
import { computed } from 'vue';
/**
* 消息发送组件
*/
const props = defineProps({
// 消息
modelValue: {
type: String,
default: '',
},
// 工具模式
toolsMode: {
type: String,
default: '',
},
});
const emits = defineEmits(['update:modelValue', 'onTools', 'sendMessage']);
const message = computed({
get() {
return props.modelValue;
},
set(newValue) {
emits(`update:modelValue`, newValue);
}
});
// 打开工具菜单
function onTools(mode) {
emits('onTools', mode);
}
// 发送消息
function sendMessage() {
emits('sendMessage');
}
</script>
<style scoped lang="scss">
.send-wrap {
padding: 18rpx 20rpx;
background: #fff;
.left {
height: 64rpx;
border-radius: 32rpx;
background: var(--ui-BG-1);
}
.bq {
font-size: 50rpx;
margin-left: 10rpx;
}
.sicon-edit {
font-size: 50rpx;
margin-left: 10rpx;
transform: rotate(0deg);
transition: all linear 0.2s;
&.is-active {
transform: rotate(45deg);
}
}
.send-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
font-size: 26rpx;
color: #fff;
margin-left: 11rpx;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<!-- 聊天虚拟列表 -->
<z-paging ref="pagingRef" v-model="messageList" use-chat-record-mode use-virtual-list
cell-height-mode="dynamic" default-page-size="20" :auto-clean-list-when-reload="false"
safe-area-inset-bottom bottom-bg-color="#f8f8f8" :back-to-top-style="backToTopStyle"
:auto-show-back-to-top="showNewMessageTip" @backToTopClick="onBackToTopClick"
@scrolltoupper="onScrollToUpper" @query="queryList">
<template #top>
<!-- 撑一下顶部导航 -->
<view style="height: 45px"></view>
</template>
<!-- style="transform: scaleY(-1)"必须写否则会导致列表倒置 -->
<!-- 注意不要直接在chat-item组件标签上设置style因为在微信小程序中是无效的请包一层view -->
<template #cell="{item,index}">
<view style="transform: scaleY(-1)">
<!-- 消息渲染 -->
<MessageListItem :message="item" :message-index="index" :message-list="messageList"></MessageListItem>
</view>
</template>
<!-- 底部聊天输入框 -->
<template #bottom>
<slot name="bottom"></slot>
</template>
<!-- 查看最新消息 -->
<template #backToTop>
<text>有新消息</text>
</template>
</z-paging>
</template>
<script setup>
import MessageListItem from '@/pages/chat/components/messageListItem.vue';
import { reactive, ref } from 'vue';
import KeFuApi from '@/sheep/api/promotion/kefu';
import { isEmpty } from '@/sheep/helper/utils';
const messageList = ref([]); // 消息列表
const showNewMessageTip = ref(false); // 显示有新消息提示
const backToTopStyle = reactive({
'width': '100px',
'background-color': '#fff',
'border-radius': '30px',
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
'display': 'flex',
'justifyContent': 'center',
'alignItems': 'center',
}); // 返回顶部样式
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
});
const pagingRef = ref(null); // 虚拟列表
const queryList = async (pageNo, pageSize) => {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
queryParams.pageNo = pageNo;
queryParams.pageSize = pageSize;
await getMessageList();
};
// 获得消息分页列表
const getMessageList = async () => {
const { data } = await KeFuApi.getKefuMessagePage(queryParams);
if (isEmpty(data.list)) {
return;
}
pagingRef.value.completeByTotal(data.list, data.total);
};
/** 刷新消息列表 */
const refreshMessageList = (message = undefined) => {
if (queryParams.pageNo != 1 && message !== undefined) {
showNewMessageTip.value = true;
// 追加数据
pagingRef.value.addChatRecordData([message], false);
return;
}
pagingRef.value.reload();
};
/** 滚动到最新消息 */
const onBackToTopClick = (event) => {
event(false); // 禁用默认操作
pagingRef.value.scrollToBottom();
};
/** 监听滚动到底部事件(因为 scroll 翻转了顶就是底) */
const onScrollToUpper = () => {
// 若已是第一页则不做处理
if (queryParams.pageNo === 1) {
return;
}
showNewMessageTip.value = false;
// 到底重置消息列表
refreshMessageList();
};
defineExpose({ getMessageList, refreshMessageList });
</script>

View File

@@ -0,0 +1,304 @@
<template>
<view class="chat-box">
<!-- 消息渲染 -->
<view class="message-item ss-flex-col scroll-item">
<view class="ss-flex ss-row-center ss-col-center">
<!-- 日期 -->
<view
v-if="
message.contentType !== KeFuMessageContentTypeEnum.SYSTEM &&
showTime(message, messageIndex)
"
class="date-message"
>
{{ formatDate(message.createTime) }}
</view>
<!-- 系统消息 -->
<view
v-if="message.contentType === KeFuMessageContentTypeEnum.SYSTEM"
class="system-message"
>
{{ message.content }}
</view>
</view>
<!-- 消息体渲染管理员消息和用户消息并左右展示 -->
<view
v-if="message.contentType !== KeFuMessageContentTypeEnum.SYSTEM"
class="ss-flex ss-col-top"
:class="[
message.senderType === UserTypeEnum.ADMIN
? `ss-row-left`
: message.senderType === UserTypeEnum.MEMBER
? `ss-row-right`
: '',
]"
>
<!-- 客服头像 -->
<image
v-show="message.senderType === UserTypeEnum.ADMIN"
class="chat-avatar ss-m-r-24"
:src="
sheep.$url.cdn(message.senderAvatar) ||
sheep.$url.static('/static/img/shop/chat/default.png')
"
mode="aspectFill"
></image>
<!-- 内容 -->
<template v-if="message.contentType === KeFuMessageContentTypeEnum.TEXT">
<view class="message-box" :class="{ admin: message.senderType === UserTypeEnum.ADMIN }">
<mp-html :content="replaceEmoji(message.content)" />
</view>
</template>
<template v-if="message.contentType === KeFuMessageContentTypeEnum.IMAGE">
<view
class="message-box"
:class="{ admin: message.senderType === UserTypeEnum.ADMIN }"
>
<su-image
class="message-img"
isPreview
:previewList="[sheep.$url.cdn(message.content)]"
:current="0"
:src="sheep.$url.cdn(message.content)"
:height="200"
:width="200"
mode="aspectFill"
></su-image>
</view>
</template>
<template v-if="message.contentType === KeFuMessageContentTypeEnum.PRODUCT">
<GoodsItem
:goodsData="getMessageContent(message)"
@tap="sheep.$router.go('/pages/goods/index', { id: getMessageContent(message).spuId })"
/>
</template>
<template v-if="message.contentType === KeFuMessageContentTypeEnum.ORDER">
<OrderItem
:orderData="getMessageContent(message)"
@tap="sheep.$router.go('/pages/order/my/detail', { id: getMessageContent(message).id })"
/>
</template>
<!-- user头像 -->
<image
v-if="message.senderType === UserTypeEnum.MEMBER"
class="chat-avatar ss-m-l-24"
:src="
sheep.$url.cdn(message.senderAvatar) ||
sheep.$url.static('/static/img/shop/chat/default.png')
"
mode="aspectFill"
>
</image>
</view>
</view>
</view>
</template>
<script setup>
import { computed, unref } from 'vue';
import dayjs from 'dayjs';
import { KeFuMessageContentTypeEnum, UserTypeEnum } from '@/pages/chat/util/constants';
import { emojiList } from '@/pages/chat/util/emoji';
import sheep from '@/sheep';
import { formatDate } from '@/sheep/util';
import GoodsItem from '@/pages/chat/components/goods.vue';
import OrderItem from '@/pages/chat/components/order.vue';
const props = defineProps({
// 消息
message: {
type: Object,
default: () => ({}),
},
// 消息索引
messageIndex: {
type: Number,
default: 0,
},
// 消息列表
messageList: {
type: Array,
default: () => [],
},
});
const getMessageContent = computed(() => (item) => JSON.parse(item.content)); // 解析消息内容
//======================= 工具 =======================
const showTime = computed(() => (item, index) => {
if (unref(props.messageList)[index + 1]) {
let dateString = dayjs(unref(props.messageList)[index + 1].createTime).fromNow();
return dateString !== dayjs(unref(item).createTime).fromNow();
}
return false;
});
// 处理表情
function replaceEmoji(data) {
let newData = data;
if (typeof newData !== 'object') {
let reg = /\[(.+?)]/g; // [] 中括号
let zhEmojiName = newData.match(reg);
if (zhEmojiName) {
zhEmojiName.forEach((item) => {
let emojiFile = selEmojiFile(item);
newData = newData.replace(
item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
'/static/img/chat/emoji/' + emojiFile,
)}"/>`,
);
});
}
}
return newData;
}
function selEmojiFile(name) {
for (let index in emojiList) {
if (emojiList[index].name === name) {
return emojiList[index].file;
}
}
return false;
}
</script>
<style scoped lang="scss">
.message-item {
margin-bottom: 33rpx;
}
.date-message,
.system-message {
width: fit-content;
border-radius: 12rpx;
padding: 8rpx 16rpx;
margin-bottom: 16rpx;
background-color: var(--ui-BG-3);
color: #999;
font-size: 24rpx;
}
.chat-avatar {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
}
.send-status {
color: #333;
height: 80rpx;
margin-right: 8rpx;
display: flex;
align-items: center;
.loading {
width: 32rpx;
height: 32rpx;
-webkit-animation: rotating 2s linear infinite;
animation: rotating 2s linear infinite;
@-webkit-keyframes rotating {
0% {
transform: rotateZ(0);
}
100% {
transform: rotateZ(360deg);
}
}
@keyframes rotating {
0% {
transform: rotateZ(0);
}
100% {
transform: rotateZ(360deg);
}
}
}
.warning {
width: 32rpx;
height: 32rpx;
color: #ff3000;
}
}
.message-box {
max-width: 50%;
font-size: 16px;
line-height: 20px;
white-space: normal;
word-break: break-all;
word-wrap: break-word;
padding: 20rpx;
border-radius: 10rpx;
color: #fff;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
&.admin {
background: #fff;
color: #333;
}
:deep() {
.imgred {
width: 100%;
}
.imgred,
img {
width: 100%;
}
}
}
:deep() {
.goods,
.order {
max-width: 500rpx;
}
}
.message-img {
width: 100px;
height: 100px;
border-radius: 6rpx;
}
.template-wrap {
// width: 100%;
padding: 20rpx 24rpx;
background: #fff;
border-radius: 10rpx;
.title {
font-size: 26rpx;
font-weight: 500;
color: #333;
margin-bottom: 29rpx;
}
.item {
font-size: 24rpx;
color: var(--ui-BG-Main);
margin-bottom: 16rpx;
&:last-of-type {
margin-bottom: 0;
}
}
}
.error-img {
width: 400rpx;
height: 400rpx;
}
.chat-box {
padding: 10px;
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<view class="bg-white order-list-card-box ss-r-10 ss-m-t-14 ss-m-20"
:key="orderData.id">
<view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
<view class="order-no">订单号{{ orderData.no }}</view>
<view class="order-state ss-font-26" :class="formatOrderColor(orderData)">
{{ formatOrderStatus(orderData) }}
</view>
</view>
<view class="border-bottom" v-for="item in orderData.items" :key="item.id">
<s-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
/>
</view>
<view class="pay-box ss-m-t-30 ss-p-b-30 ss-flex ss-row-right ss-p-r-20">
<view class="ss-flex ss-col-center">
<view class="discounts-title pay-color"> {{ orderData.productCount }} 件商品,总金额:</view>
<view class="discounts-money pay-color">
{{ fen2yuan(orderData.payPrice) }}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { fen2yuan, formatOrderColor, formatOrderStatus } from '@/sheep/hooks/useGoods';
const props = defineProps({
orderData: {
type: Object,
default: {},
},
});
</script>
<style lang="scss" scoped>
.order-list-card-box {
.order-card-header {
height: 80rpx;
.order-no {
font-size: 26rpx;
font-weight: 500;
margin-right: 5px;
}
.order-state {}
}
.pay-box {
.discounts-title {
font-size: 24rpx;
line-height: normal;
color: #999999;
}
.discounts-money {
font-size: 24rpx;
line-height: normal;
color: #999;
font-family: OPPOSANS;
}
.pay-color {
color: #333;
}
}
.order-card-footer {
height: 100rpx;
.more-item-box {
padding: 20rpx;
.more-item {
height: 60rpx;
.title {
font-size: 26rpx;
}
}
}
.more-btn {
color: $dark-9;
font-size: 24rpx;
}
.content {
width: 154rpx;
color: #333333;
font-size: 26rpx;
font-weight: 500;
}
}
}
.warning-color {
color: #faad14;
}
.danger-color {
color: #ff3000;
}
.success-color {
color: #52c41a;
}
.info-color {
color: #999999;
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<su-popup :show="show" showClose round="10" backgroundColor="#eee" @close="emits('close')">
<view class="select-popup">
<view class="title">
<span>{{ mode == 'goods' ? '我的浏览' : '我的订单' }}</span>
</view>
<scroll-view
class="scroll-box"
scroll-y="true"
:scroll-with-animation="true"
:show-scrollbar="false"
@scrolltolower="loadmore"
>
<view
class="item"
v-for="item in state.pagination.data"
:key="item.id"
@tap="emits('select', { type: mode, data: item })"
>
<template v-if="mode == 'goods'">
<GoodsItem :goodsData="item" />
</template>
<template v-if="mode == 'order'">
<OrderItem :orderData="item" />
</template>
</view>
<uni-load-more :status="state.loadStatus" :content-text="{ contentdown: '上拉加载更多' }" />
</scroll-view>
</view>
</su-popup>
</template>
<script setup>
import { reactive, watch } from 'vue';
import _ from 'lodash-es';
import GoodsItem from './goods.vue';
import OrderItem from './order.vue';
import OrderApi from '@/sheep/api/trade/order';
import SpuHistoryApi from '@/sheep/api/product/history';
const emits = defineEmits(['select', 'close']);
const props = defineProps({
mode: {
type: String,
default: 'goods',
},
show: {
type: Boolean,
default: false,
},
});
watch(
() => props.mode,
() => {
state.pagination.data = [];
if (props.mode) {
getList(state.pagination.page);
}
},
);
const state = reactive({
loadStatus: '',
pagination: {
data: [],
current_page: 1,
total: 1,
last_page: 1,
},
});
async function getList(page, list_rows = 5) {
state.loadStatus = 'loading';
const res =
props.mode == 'goods'
? await SpuHistoryApi.getBrowseHistoryPage({
page,
list_rows,
})
: await OrderApi.getOrderPage({
page,
list_rows,
});
let orderList = _.concat(state.pagination.data, res.data.list);
state.pagination = {
...res.data,
data: orderList,
};
if (state.pagination.current_page < state.pagination.last_page) {
state.loadStatus = 'more';
} else {
state.loadStatus = 'noMore';
}
}
function loadmore() {
if (state.loadStatus !== 'noMore') {
getList(state.pagination.current_page + 1);
}
}
</script>
<style lang="scss" scoped>
.select-popup {
max-height: 600rpx;
.title {
height: 100rpx;
line-height: 100rpx;
padding: 0 26rpx;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
span {
font-size: 32rpx;
position: relative;
&::after {
content: '';
display: block;
width: 100%;
height: 2px;
z-index: 1;
position: absolute;
left: 0;
bottom: -15px;
background: var(--ui-BG-Main);
pointer-events: none;
}
}
}
.scroll-box {
height: 500rpx;
}
.item {
background: #fff;
margin: 26rpx 26rpx 0;
border-radius: 20rpx;
:deep() {
.image {
width: 140rpx;
height: 140rpx;
}
}
}
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<su-popup
:show="showTools"
@close="handleClose"
>
<view class="ss-modal-box ss-flex-col">
<slot></slot>
<view class="content ss-flex ss-flex-1">
<template v-if="toolsMode === 'emoji'">
<swiper
class="emoji-swiper"
:indicator-dots="true"
circular
indicator-active-color="#7063D2"
indicator-color="rgba(235, 231, 255, 1)"
:autoplay="false"
:interval="3000"
:duration="1000"
>
<swiper-item v-for="emoji in emojiPage" :key="emoji">
<view class="ss-flex ss-flex-wrap">
<image
v-for="item in emoji" :key="item"
class="emoji-img"
:src="sheep.$url.cdn(`/static/img/chat/emoji/${item.file}`)"
@tap="onEmoji(item)"
>
</image>
</view>
</swiper-item>
</swiper>
</template>
<template v-else>
<view class="image">
<s-uploader
file-mediatype="image"
:imageStyles="{ width: 50, height: 50, border: false }"
@select="imageSelect({ type: 'image', data: $event })"
>
<image
class="icon"
:src="sheep.$url.static('/static/img/shop/chat/image.png')"
mode="aspectFill"
></image>
</s-uploader>
<view>图片</view>
</view>
<!-- <view class="goods" @tap="onShowSelect('goods')">
<image
class="icon"
:src="sheep.$url.static('/static/img/shop/chat/goods.png')"
mode="aspectFill"
></image>
<view>商品</view>
</view> -->
<view class="order" @tap="onShowSelect('order')">
<image
class="icon"
:src="sheep.$url.static('/static/img/shop/chat/order.png')"
mode="aspectFill"
></image>
<view>订单</view>
</view>
</template>
</view>
</view>
</su-popup>
</template>
<script setup>
/**
* 聊天工具
*/
import { emojiPage } from '@/pages/chat/util/emoji';
import sheep from '@/sheep';
const props = defineProps({
// 工具模式
toolsMode: {
type: String,
default: '',
},
// 控制工具菜单弹出
showTools: {
type: Boolean,
default: () => false,
},
});
const emits = defineEmits(['onEmoji', 'imageSelect', 'onShowSelect', 'close']);
// 关闭弹出工具菜单
function handleClose() {
emits('close');
}
// 选择表情
function onEmoji(emoji) {
emits('onEmoji', emoji);
}
// 选择图片
function imageSelect(val) {
emits('imageSelect', val);
}
// 选择商品或订单
function onShowSelect(mode) {
emits('onShowSelect', mode);
}
</script>
<style scoped lang="scss">
.content {
width: 100%;
align-content: space-around;
border-top: 1px solid #dfdfdf;
padding: 20rpx 0 0;
.emoji-swiper {
width: 100%;
height: 280rpx;
padding: 0 20rpx;
.emoji-img {
width: 50rpx;
height: 50rpx;
display: inline-block;
margin: 10rpx;
}
}
.image,
.goods,
.order {
width: 33.3%;
height: 280rpx;
text-align: center;
font-size: 24rpx;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.icon {
width: 50rpx;
height: 50rpx;
margin-bottom: 21rpx;
}
}
:deep() {
.uni-file-picker__container {
justify-content: center;
}
.file-picker__box {
display: none;
&:last-of-type {
display: flex;
}
}
}
}
</style>

187
pages/chat/index.vue Normal file
View File

@@ -0,0 +1,187 @@
<template>
<s-layout class="chat-wrap" :title="!isReconnecting ? '连接客服成功' : '会话重连中'" navbar="inner">
<!-- 覆盖头部导航栏背景颜色 -->
<div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
<!-- 聊天区域 -->
<MessageList ref="messageListRef">
<template #bottom>
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
</template>
</MessageList>
<!-- 聊天工具 -->
<tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose"
@on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
</tools-popup>
<!-- 商品订单选择 -->
<SelectPopup
:mode="chat.selectMode"
:show="chat.showSelect"
@select="onSelect"
@close="chat.showSelect = false"
/>
</s-layout>
</template>
<script setup>
import MessageList from '@/pages/chat/components/messageList.vue';
import { reactive, ref, toRefs } from 'vue';
import sheep from '@/sheep';
import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
import MessageInput from '@/pages/chat/components/messageInput.vue';
import SelectPopup from '@/pages/chat/components/select-popup.vue';
import { KeFuMessageContentTypeEnum, WebSocketMessageTypeConstants } from '@/pages/chat/util/constants';
import FileApi from '@/sheep/api/infra/file';
import KeFuApi from '@/sheep/api/promotion/kefu';
import { useWebSocket } from '@/sheep/hooks/useWebSocket';
const sys_navBar = sheep.$platform.navbar;
const chat = reactive({
msg: '',
scrollInto: '',
showTools: false,
toolsMode: '',
showSelect: false,
selectMode: '',
});
// 发送消息
async function onSendMessage() {
if (!chat.msg) return;
try {
const data = {
contentType: KeFuMessageContentTypeEnum.TEXT,
content: chat.msg,
};
await KeFuApi.sendKefuMessage(data);
await messageListRef.value.refreshMessageList();
chat.msg = '';
} finally {
chat.showTools = false;
}
}
const messageListRef = ref();
//======================= 聊天工具相关 start =======================
function handleToolsClose() {
chat.showTools = false;
chat.toolsMode = '';
}
function onEmoji(item) {
chat.msg += item.name;
}
// 点击工具栏开关
function onTools(mode) {
if (isReconnecting.value) {
sheep.$helper.toast('您已掉线!请返回重试');
return;
}
if (!chat.toolsMode || chat.toolsMode === mode) {
chat.showTools = !chat.showTools;
}
chat.toolsMode = mode;
if (!chat.showTools) {
chat.toolsMode = '';
}
}
function onShowSelect(mode) {
chat.showTools = false;
chat.showSelect = true;
chat.selectMode = mode;
}
async function onSelect({ type, data }) {
let msg;
switch (type) {
case 'image':
const res = await FileApi.uploadFile(data.tempFiles[0].path);
msg = {
contentType: KeFuMessageContentTypeEnum.IMAGE,
content: res.data,
};
break;
case 'goods':
msg = {
contentType: KeFuMessageContentTypeEnum.PRODUCT,
content: JSON.stringify(data),
};
break;
case 'order':
msg = {
contentType: KeFuMessageContentTypeEnum.ORDER,
content: JSON.stringify(data),
};
break;
}
if (msg) {
// 发送消息
// scrollBottom();
await KeFuApi.sendKefuMessage(msg);
await messageListRef.value.refreshMessageList();
chat.showTools = false;
chat.showSelect = false;
chat.selectMode = '';
}
}
//======================= 聊天工具相关 end =======================
const { options } = useWebSocket({
// 连接成功
onConnected: async () => {
},
// 收到消息
onMessage: async (data) => {
const type = data.type;
if (!type) {
console.error('未知的消息类型:' + data.value);
return;
}
// 2.2 消息类型KEFU_MESSAGE_TYPE
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
// 刷新消息列表
await messageListRef.value.refreshMessageList(JSON.parse(data.content));
return;
}
// 2.3 消息类型KEFU_MESSAGE_ADMIN_READ
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
console.log('管理员已读消息');
}
},
});
const isReconnecting = toRefs(options).isReconnecting; // 重连状态
</script>
<style scoped lang="scss">
.chat-wrap {
.page-bg {
width: 100%;
position: absolute;
top: 0;
left: 0;
background-color: var(--ui-BG-Main);
z-index: 1;
}
.status {
position: relative;
box-sizing: border-box;
z-index: 3;
height: 70rpx;
padding: 0 30rpx;
background: var(--ui-BG-Main-opacity-1);
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 400;
color: var(--ui-BG-Main);
}
}
</style>

View File

@@ -0,0 +1,21 @@
export const KeFuMessageContentTypeEnum = {
TEXT: 1, // 文本消息
IMAGE: 2, // 图片消息
VOICE: 3, // 语音消息
VIDEO: 4, // 视频消息
SYSTEM: 5, // 系统消息
// ========== 商城特殊消息 ==========
PRODUCT: 10,// 商品消息
ORDER: 11,// 订单消息"
};
export const UserTypeEnum = {
MEMBER: 1, // 会员 面向 c 端,普通用户
ADMIN: 2, // 管理员 面向 b 端,管理后台
};
// Promotion 的 WebSocket 消息类型枚举类
export const WebSocketMessageTypeConstants = {
KEFU_MESSAGE_TYPE: 'kefu_message_type', // 客服消息类型
IM_MESSAGE_READ: 'im_message_read_status_change', // IM消息已读
IM_MESSAGE_NEWS: 'im_message_news', // IM新消息
KEFU_MESSAGE_ADMIN_READ: 'kefu_message_read_status_change' // 客服消息管理员已读
}

58
pages/chat/util/emoji.js Normal file
View File

@@ -0,0 +1,58 @@
export const emojiList = [
{ name: '[笑掉牙]', file: 'xiaodiaoya.png' },
{ name: '[可爱]', file: 'keai.png' },
{ name: '[冷酷]', file: 'lengku.png' },
{ name: '[闭嘴]', file: 'bizui.png' },
{ name: '[生气]', file: 'shengqi.png' },
{ name: '[惊恐]', file: 'jingkong.png' },
{ name: '[瞌睡]', file: 'keshui.png' },
{ name: '[大笑]', file: 'daxiao.png' },
{ name: '[爱心]', file: 'aixin.png' },
{ name: '[坏笑]', file: 'huaixiao.png' },
{ name: '[飞吻]', file: 'feiwen.png' },
{ name: '[疑问]', file: 'yiwen.png' },
{ name: '[开心]', file: 'kaixin.png' },
{ name: '[发呆]', file: 'fadai.png' },
{ name: '[流泪]', file: 'liulei.png' },
{ name: '[汗颜]', file: 'hanyan.png' },
{ name: '[惊悚]', file: 'jingshu.png' },
{ name: '[困~]', file: 'kun.png' },
{ name: '[心碎]', file: 'xinsui.png' },
{ name: '[天使]', file: 'tianshi.png' },
{ name: '[晕]', file: 'yun.png' },
{ name: '[啊]', file: 'a.png' },
{ name: '[愤怒]', file: 'fennu.png' },
{ name: '[睡着]', file: 'shuizhuo.png' },
{ name: '[面无表情]', file: 'mianwubiaoqing.png' },
{ name: '[难过]', file: 'nanguo.png' },
{ name: '[犯困]', file: 'fankun.png' },
{ name: '[好吃]', file: 'haochi.png' },
{ name: '[呕吐]', file: 'outu.png' },
{ name: '[龇牙]', file: 'ziya.png' },
{ name: '[懵比]', file: 'mengbi.png' },
{ name: '[白眼]', file: 'baiyan.png' },
{ name: '[饿死]', file: 'esi.png' },
{ name: '[凶]', file: 'xiong.png' },
{ name: '[感冒]', file: 'ganmao.png' },
{ name: '[流汗]', file: 'liuhan.png' },
{ name: '[笑哭]', file: 'xiaoku.png' },
{ name: '[流口水]', file: 'liukoushui.png' },
{ name: '[尴尬]', file: 'ganga.png' },
{ name: '[惊讶]', file: 'jingya.png' },
{ name: '[大惊]', file: 'dajing.png' },
{ name: '[不好意思]', file: 'buhaoyisi.png' },
{ name: '[大闹]', file: 'danao.png' },
{ name: '[不可思议]', file: 'bukesiyi.png' },
{ name: '[爱你]', file: 'aini.png' },
{ name: '[红心]', file: 'hongxin.png' },
{ name: '[点赞]', file: 'dianzan.png' },
{ name: '[恶魔]', file: 'emo.png' },
];
export let emojiPage = {};
emojiList.forEach((item, index) => {
if (!emojiPage[Math.floor(index / 30) + 1]) {
emojiPage[Math.floor(index / 30) + 1] = [];
}
emojiPage[Math.floor(index / 30) + 1].push(item);
});

View File

@@ -0,0 +1,183 @@
<template>
<view>
<view class="form-item">
<view class="label">上传头像</view>
</view>
<view class="upload-box">
<!-- <button v-if="mp_is_new" class="avatar-box" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<u-avatar size="180" :src="avatarUrl"></u-avatar>
<view class="icon">
<u-icon name="camera" color="#fff" size="30"></u-icon>
</view>
</button> -->
<view class="avatar-box" @click="chooseImage">
<u-avatar size="180" :src="avatarUrl"></u-avatar>
<view class="icon">
<u-icon name="camera" color="#fff" size="30"></u-icon>
</view>
</view>
</view>
</view>
</template>
<script>
import FileApi from '@/sheep/api/infra/file';
export default {
components: {
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
mp_is_new: false,
}
},
created() {
// #ifdef MP-WEIXIN
const version = uni.getSystemInfoSync().SDKVersion;
if(this.compareVersion(version, '2.21.2') >= 0){
this.mp_is_new = true;
}
// #endif
},
computed: {
avatarUrl() {
return this.modelValue;
},
},
watch: {
},
methods: {
/**
* 小程序比较版本信息
* @param v1 当前版本
* @param v2 进行比较的版本
* @return boolen
*
*/
compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i])
const num2 = parseInt(v2[i])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
},
//选照片 or 拍照
chooseImage() {
uni.chooseImage({
count: 1, //默认9
sourceType: ['album', 'camera'],
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
success: (res) => {
for (let i = 0; i < res.tempFilePaths.length; i++) {
uni.getImageInfo({
src: res.tempFilePaths[i],
success: (image) => {
this.uploadImage(image.path);
}
});
}
}
});
},
uploadImage(path) {
FileApi.uploadFile(path).then((res) => {
this.$emit('update:modelValue', res.data);
});
},
// 微信头像获取
onChooseAvatar(e) {
const {
avatarUrl
} = e.detail
this.uploadImage(avatarUrl);
},
}
}
</script>
<style lang="scss" scoped>
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.upload-box {
padding: 15px;
display: flex;
justify-content: center;
align-items: center;
padding-top: 0;
.avatar-box {
display: flex;
justify-content: center;
align-items: center;
position: relative;
.icon {
position: absolute;
right: 0;
bottom: 5px;
background-color: var(--ui-BG-Main);
width: 50rpx;
height: 50rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 100%;
}
}
button{
padding: unset;
margin: unset;
border: unset;
position: relative;
line-height: unset;
background-color: unset;
font-size: unset;
color: unset;
border-radius: unset;
text-align: unset;
text-decoration: unset;
display: unset;
overflow: unset;
}
button::after{
border: none;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<view>
<view class="form-item">
<view class="label">所在城市</view>
<u-input @click="citySelect" input-align="right" type="select" placeholder="请选择所在的城市" v-model="city" />
</view>
<!-- 省市区弹窗 -->
<su-region-picker :show="show" @cancel="show = false" @confirm="cityOk" />
</view>
</template>
<script>
export default {
components: {
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
show: false,
params: {
province: true,
city: true,
area: false
},
}
},
created() {
},
computed: {
city() {
return this.modelValue;
},
},
watch: {
},
methods: {
citySelect() {
this.show = true;
},
cityOk(e) {
this.$emit('update:modelValue', e.city_name);
this.show = false;
},
}
}
</script>
<style lang="scss" scoped>
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<view>
<view class="form-item">
<view class="label">上传图片</view>
<view>{{imgList.length}}/{{number}}</view>
</view>
<view class="upload-box">
<shmily-drag-image :number="number" v-model="imgList"></shmily-drag-image>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
number: {
type: Number,
default: 6
},
modelValue: {
type: Array,
default: []
},
},
data() {
return {
imgList: [],
}
},
created() {
this.imgList = this.modelValue;
},
computed: {
},
watch: {
imgList: {
handler: function(newVal, oldVal) {
this.$emit('update:modelValue', newVal);
}
},
modelValue: {
handler: function(newVal, oldVal) {
this.imgList = newVal;
}
},
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.upload-box {
padding: 15px;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<view class="form-item">
<view class="label">性别</view>
<view class="radio-box">
<view @click="change(item)" class="text" :class="item.value == valueDom ? 'active' : ''" v-for="(item,index) in list">{{item.name}}</view>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
list: [
{
name: '男',
value: '0',
},
{
name: '女',
value: '1',
},
],
}
},
created() {
},
computed: {
valueDom() {
return this.modelValue;
},
},
watch: {
},
methods: {
change(e) {
this.$emit('update:modelValue', e.value);
},
}
}
</script>
<style lang="scss" scoped>
.form-item {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.radio-box {
display: flex;
justify-content: space-between;
align-items: center;
.text {
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #ececec;
color: #949494;
border-radius: 100%;
margin-left: 10px;
}
.active {
color: #fff;
background-color: #949494;
}
}
</style>

View File

@@ -0,0 +1,255 @@
<template>
<view class="form-bg">
<view class="form-item">
<view class="label">录音</view>
<view class="bubble-box">
<u-input @click="topBubble" input-align="right" type="select" :placeholder="voice.name" />
<tui-bubble-popup :show="show" :mask="false" position="absolute" direction="right" triangleRight="-22rpx" triangleTop="30rpx" @close="topBubble" :flexEnd="false">
<view @click="change(item)" class="tui-menu-item" v-for="(item,index) in list">{{item.name}}</view>
</tui-bubble-popup>
</view>
</view>
<view>
<view class="voice-box" v-if="voice.type == 'voice'">
<view v-if="voiceUrl" @click="playAudio" class="upload-btn-box">
<view class="icon">
<u-icon v-if="play" name="pause" color="#fff" size="70"></u-icon>
<u-icon v-else name="play-right-fill" color="#fff" size="70"></u-icon>
</view>
<view class="upload-btn" v-if="play">停止播放</view>
<view class="upload-btn" v-else>播放录音</view>
</view>
<view v-else>
<all-speech ref="speech" @okClick="voiceOk"></all-speech>
</view>
<view v-if="voiceUrl" @click="reloadBtn" class="reload-btn">
<u-icon name="reload" size="30"></u-icon>
<text class="text">重录</text>
</view>
</view>
<view class="voice-box" v-if="voice.type == 'upload'">
<view v-if="voiceUrl" @click="playAudio" class="upload-btn-box">
<view class="icon">
<u-icon v-if="play" name="pause" color="#fff" size="70"></u-icon>
<u-icon v-else name="play-right-fill" color="#fff" size="70"></u-icon>
</view>
<view class="upload-btn" v-if="play">停止播放</view>
<view class="upload-btn" v-else>播放录音</view>
</view>
<view v-else @click="chooseVoice" class="upload-btn-box">
<view class="icon">
<u-icon name="plus" color="#fff" size="70"></u-icon>
</view>
<view class="upload-btn">上传录音文件</view>
</view>
<view v-if="voiceUrl" @click="reloadBtn" class="reload-btn">
<u-icon name="reload" size="30"></u-icon>
<text class="text">重录</text>
</view>
</view>
</view>
</view>
</template>
<script>
import FileApi from '@/sheep/api/infra/file';
import tuiBubblePopup from "@/components/thorui/tui-bubble-popup/tui-bubble-popup.vue"
const audio = uni.createInnerAudioContext();
export default {
components: {
tuiBubblePopup,
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
voice: {
},
list: [
{
name: '直接录音',
type: 'voice',
},
],
current: 0,
voicePath: '',
show: false,
play: false,
}
},
created() {
this.voice = this.list[this.current];
// #ifndef MP-WEIXIN
var voiceType = {
name: '上传手机音频',
type: 'upload',
};
this.list.push(voiceType);
// #endif
},
computed: {
voiceUrl(){
return this.modelValue;
}
},
methods: {
topBubble() {
this.show = !this.show;
},
change(e) {
this.reloadBtn();
this.topBubble();
this.voice = e;
},
reloadBtn() {
this.play = false;
this.$emit('update:modelValue', "");
this.$emit('sec', "");
},
playAudio() {
if(this.play){
this.play = false;
audio.stop();
}else{
this.play = true;
//语音自然播放结束
audio.onEnded((res) => {
this.play = false;
});
audio.src = this.modelValue;
audio.play();
}
},
chooseVoice() {
var that = this;
uni.chooseFile({
count: 1, //默认100
extension:['.mp3','.mp4','.m4a'],
success: function (res) {
var fileSize = res.tempFiles[0].size;
if (fileSize > 1024 * 1024 * 10) { // 假设设置的文件大小限制为5MB
uni.showToast({
title: '文件大小限制为10MB',
icon: 'none'
});
return;
}
that.voicePath = res.tempFilePaths[0];
console.log(JSON.stringify(res.tempFilePaths));
that.uploadVoice(that.voicePath);
}
});
},
uploadVoice(path) {
FileApi.uploadFile(path).then((res) => {
this.$emit('update:modelValue', res.data);
});
},
voiceOk(e) {
this.voicePath = e.path;
this.uploadVoice(this.voicePath);
this.$emit('sec', e.sec);
},
}
}
</script>
<style lang="scss" scoped>
.form-bg {
padding: 0 15px;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.bubble-box {
position: relative;
.tui-menu-item {
width: 100%;
padding: 30rpx 20rpx;
text-align: center;
position: relative;
}
.tui-menu-item:after {
position: absolute;
box-sizing: border-box;
content: " ";
pointer-events: none;
top: 0%;
right: 10%;
bottom: 0%;
left: 10%;
border: 0 solid #ebedf0;
border-color: #646566;
border-bottom-width: 1px;
}
.tui-menu-item:last-child:after {
border-bottom-width: 0;
}
}
.voice-box {
height: 400rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.icon {
display: flex;
background-color: #3cc9a4;
border-radius: 100%;
width: 170rpx;
height: 170rpx;
justify-content: center;
align-items: center;
}
.upload-btn-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.upload-btn {
font-size: 34rpx;
color: #aaa;
margin-top: 10px;
}
.reload-btn {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 0px;
font-size: 30rpx;
color: #3cc9a4;
.text {
margin-left: 4px;
}
}
}
</style>

213
pages/clerk/apply/edit.vue Normal file
View File

@@ -0,0 +1,213 @@
<template>
<view class="page-app theme-light main-green font-1">
<su-navbar title="编辑资料" statusBar></su-navbar>
<view class="form-box">
<form-avatar v-model="form.avatar"></form-avatar>
</view>
<view class="form-box">
<view class="form-item">
<view class="label">昵称</view>
<u-input input-align="right" placeholder="请输入昵称" v-model="form.nickname" />
</view>
<form-sex v-model="form.sex"></form-sex>
<view class="form-item">
<view class="label">年龄</view>
<u-input input-align="right" placeholder="请输入年龄" type="number" v-model="form.age" />
</view>
<view class="form-item" v-if="isPass">
<view class="label">微信</view>
<u-input input-align="right" placeholder="请输入您的微信" v-model="form.weixin" />
</view>
<view class="form-item">
<view class="label">手机号</view>
<u-input input-align="right" placeholder="请输入手机号" type="number" v-model="form.mobile" />
</view>
<view class="form-item">
<view class="label">相关经验</view>
<u-input input-align="right" placeholder="是否有其它店铺的经验" v-model="form.experience" />
</view>
<view class="form-item">
<view class="label">自我介绍</view>
<u-input input-align="right" placeholder="请输入自我介绍" v-model="form.intro" />
</view>
<view class="form-item">
<view class="label">所在城市</view>
<u-input input-align="right" placeholder="请输入所在城市" v-model="form.city" />
</view>
</view>
<view class="form-box">
<form-image :number="6" v-model="imgList"></form-image>
</view>
<view class="form-box">
<form-voice @sec="toSec" v-model="form.sound"></form-voice>
</view>
<view class="submit-box">
<view class="sub-btn" @click="saveApply">提交申请</view>
</view>
<s-menu-tools />
<s-auth-modal />
</view>
</template>
<script>
import FormAvatar from '@/pages/clerk/apply/components/formAvatar.vue';
import FormSex from '@/pages/clerk/apply/components/formSex.vue';
import FormVoice from '@/pages/clerk/apply/components/formVoice.vue';
import FormImage from '@/pages/clerk/apply/components/formImage.vue';
import ClerkApi from '@/sheep/api/worker/clerk';
import sheep from '@/sheep';
export default {
components: {
FormAvatar,
FormSex,
FormVoice,
FormImage,
},
props: {
},
data() {
return {
form: {
id: 0,
avatar: '',
nickname: '',
sex: '',
age: '',
weixin: '',
mobile: '',
experience: '',
intro: '',
city: '',
albums: '',
sound: '',
soundTime: '',
},
imgList: [],
}
},
onLoad(options) {
this.form.id = options.id;
this.init();
},
computed: {
isPass() {
return sheep.$store('user').tradeConfig.weixinEnabled;
},
},
methods: {
init() {
ClerkApi.getClerkApply(this.form.id).then((res) => {
this.form = res.data;
if(this.form.albums){
this.imgList = this.form.albums.split(',');
}
});
},
saveApply() {
if(!this.form.avatar){
sheep.$helper.toast('请上传头像');
return;
}
if(!this.form.nickname){
sheep.$helper.toast('请输入昵称');
return;
}
if(!this.form.sex){
sheep.$helper.toast('请选择性别');
return;
}
if(!this.form.age){
sheep.$helper.toast('请输入年龄');
return;
}
if(this.isPass && !this.form.weixin){
sheep.$helper.toast('请输入正确的微信号');
return;
}
if(!this.form.mobile){
sheep.$helper.toast('请输入正确的手机号');
return;
}
if(!this.form.experience){
sheep.$helper.toast('请输入相关经验');
return;
}
if(!this.form.intro){
sheep.$helper.toast('请输入自我介绍');
return;
}
if(!this.form.city){
sheep.$helper.toast('请输入所在城市');
return;
}
if(this.imgList.length < 1){
sheep.$helper.toast('请上传图片');
return;
}
this.form.albums = this.imgList.join(',');
ClerkApi.updateClerkApply(this.form).then((res) => {
});
},
toSec(e) {
this.form.soundTime = e;
},
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
}
.form-box {
background-color: #fff;
margin: 15px;
border-radius: 10px;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.submit-box {
display: flex;
align-items: center;
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
padding: 0 15px;
z-index: 99;
.sub-btn {
background-color: var(--ui-BG-Main);
display: flex;
flex: 1;
justify-content: center;
align-items: center;
padding: 10px;
color: #fff;
border-radius: 40px;
font-size: 30rpx;
}
}
</style>

307
pages/clerk/apply/index.vue Normal file
View File

@@ -0,0 +1,307 @@
<template>
<view class="page-app theme-light main-green font-1">
<su-navbar title="达人申请" statusBar></su-navbar>
<!-- #ifdef MP -->
<view v-if="showSubscribeBtn" class="subscribe-box">
<u-icon name="bell-fill" color="var(--ui-BG-Main)" size="44"></u-icon>
<view class="info">获取实时审核结果</view>
<view class="sub-btn" @tap="subscribeMessage">立即订阅</view>
</view>
<!-- #endif -->
<view class="form-box">
<form-avatar v-model="form.avatar"></form-avatar>
</view>
<view class="form-box">
<view class="form-item">
<view class="label">昵称</view>
<u-input input-align="right" placeholder="请输入昵称" v-model="form.nickname" />
</view>
<form-sex v-model="form.sex"></form-sex>
<view class="form-item">
<view class="label">年龄</view>
<u-input input-align="right" placeholder="请输入年龄" type="number" v-model="form.age" />
</view>
<view class="form-item" v-if="isPass">
<view class="label">微信</view>
<u-input input-align="right" placeholder="请输入您的微信" v-model="form.weixin" />
</view>
<view class="form-item">
<view class="label">手机号</view>
<u-input input-align="right" placeholder="请输入手机号" type="number" v-model="form.mobile" />
</view>
<view class="form-item">
<view class="label">自我介绍</view>
<u-input input-align="right" placeholder="请输入自我介绍" v-model="form.intro" />
</view>
<view class="form-item">
<view class="label">所在城市</view>
<u-input input-align="right" placeholder="请输入所在城市" v-model="form.city" />
</view>
</view>
<view class="form-box">
<form-image :number="6" v-model="imgList"></form-image>
</view>
<view class="form-box">
<form-voice @sec="toSec" v-model="form.sound"></form-voice>
</view>
<view class="check-box" @click="changeCheck">
<u-icon size="44" v-if="check" name="checkmark-circle-fill" color="var(--ui-BG-Main)"></u-icon>
<u-icon size="44" v-else name="checkmark-circle" color="var(--ui-BG-Main)"></u-icon>
<text class="info">我已阅读并接受</text>
<text @tap.stop="toAggre()" class="sub-btn">达人申请协议</text>
</view>
<view class="submit-box">
<view class="sub-btn" @click="saveApply">提交申请</view>
</view>
<s-menu-tools />
<s-auth-modal />
<qrcode-modal />
</view>
</template>
<script>
import FormAvatar from '@/pages/clerk/apply/components/formAvatar.vue';
import FormSex from '@/pages/clerk/apply/components/formSex.vue';
import FormVoice from '@/pages/clerk/apply/components/formVoice.vue';
import FormImage from '@/pages/clerk/apply/components/formImage.vue';
import qrcodeModal from '@/components/qrcode-modal/qrcode-modal.vue';
import ClerkApi from '@/sheep/api/worker/clerk';
import test from '@/sheep/helper/test.js';
import { WxaSubscribeTemplate } from '@/sheep/util/const';
import sheep from '@/sheep';
export default {
components: {
FormAvatar,
FormSex,
FormVoice,
FormImage,
qrcodeModal,
},
props: {
},
data() {
return {
form: {
avatar: '',
nickname: '',
sex: '',
age: '',
weixin: '',
mobile: '',
experience: '',
intro: '',
city: '',
albums: '',
sound: '',
soundTime: '',
},
imgList: [],
showSubscribeBtn: false,
check: true,
}
},
onLoad() {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.autoSubscribeMessage();
// #endif
},
computed: {
isPass() {
return sheep.$store('user').tradeConfig.weixinEnabled;
},
},
methods: {
saveApply() {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.autoSubscribeMessage();
// #endif
if(!this.form.avatar){
sheep.$helper.toast('请上传头像');
return;
}
if(!this.form.nickname){
sheep.$helper.toast('请输入昵称');
return;
}
if(!this.form.sex){
sheep.$helper.toast('请选择性别');
return;
}
if(!this.form.age){
sheep.$helper.toast('请输入年龄');
return;
}
if(this.form.age < 18){
sheep.$helper.toast('未成年禁止申请');
return;
}
if(this.isPass && !this.form.weixin){
sheep.$helper.toast('请输入正确的微信号');
return;
}
if(!this.form.mobile || !test.mobile(this.form.mobile)){
sheep.$helper.toast('请输入正确的手机号');
return;
}
if(!this.form.intro){
sheep.$helper.toast('请输入自我介绍');
return;
}
if(!this.form.city){
sheep.$helper.toast('请输入所在城市');
return;
}
if(this.imgList.length < 1){
sheep.$helper.toast('请上传图片');
return;
}
if(this.imgList.length < 1){
sheep.$helper.toast('请上传图片');
return;
}
if(!this.check){
sheep.$helper.toast('未同意协议');
return;
}
this.form.albums = this.imgList.join(',');
ClerkApi.createClerkApply(this.form).then((res) => {
if(res.data){
sheep.$router.go('/pages/worker/levelList/index', {id: res.data});
}
});
},
toSec(e) {
this.form.soundTime = e;
},
subscribeMessage() {
const event = [WxaSubscribeTemplate.CLERK_APPLY_SUCCESS];
event.push(WxaSubscribeTemplate.CLERK_BLIND);
event.push(WxaSubscribeTemplate.CLERK_ORDER);
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
// 订阅后记录一下订阅状态
uni.removeStorageSync(WxaSubscribeTemplate.CLERK_APPLY_SUCCESS);
uni.setStorageSync(WxaSubscribeTemplate.CLERK_APPLY_SUCCESS, '已订阅');
// 隐藏订阅按钮
this.showSubscribeBtn = false;
});
},
async autoSubscribeMessage() {
// 1. 校验是否手动订阅过
const subscribeBtnStatus = uni.getStorageSync(WxaSubscribeTemplate.CLERK_APPLY_SUCCESS);
if (!subscribeBtnStatus) {
this.showSubscribeBtn = true;
}
// 2. 订阅消息
this.subscribeMessage();
},
changeCheck() {
if(this.check){
this.check = false;
}else{
this.check = true;
}
},
toAggre() {
sheep.$router.go('/pages/public/richtext', {title: '店员申请协议'})
},
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
}
.form-box {
background-color: #fff;
margin: 15px;
border-radius: 10px;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.submit-box {
display: flex;
align-items: center;
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
padding: 0 15px;
z-index: 99;
.sub-btn {
background-color: var(--ui-BG-Main);
display: flex;
flex: 1;
justify-content: center;
align-items: center;
padding: 10px;
color: #fff;
border-radius: 40px;
font-size: 30rpx;
}
}
.subscribe-box {
display: flex;
align-items: center;
padding: 10px;
padding-bottom: 0;
justify-content: center;
font-size: 28rpx;
.info {
margin: 0 10rpx;
}
.sub-btn {
color: var(--ui-BG-Main);
}
}
.check-box {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
.info{
margin: 0 10rpx;
}
.sub-btn {
color: var(--ui-BG-Main);
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<su-fixed alway :bgStyles="{ background: '#fff' }" :val="0" noNav opacity :placeholder="false">
<su-status-bar />
<view
class="ui-bar ss-flex ss-col-center ss-row-between ss-p-x-20"
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
>
<!-- -->
<view class="icon-box ss-flex">
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
<text :class="tabOpacityVal > 0.4 ? 'black' : ''" class="sicon-back" v-if="hasHistory" />
<text :class="tabOpacityVal > 0.4 ? 'black' : ''" class="sicon-home" v-else />
</view>
<view class="line"></view>
<view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
<text :class="tabOpacityVal > 0.4 ? 'black' : ''" class="sicon-more" />
</view>
</view>
<!-- -->
<view class="detail-tab-card ss-flex-1" :style="[{ opacity: tabOpacityVal }]">
<view class="tab-box ss-flex ss-col-center ss-row-around">
<view
class="tab-item ss-flex-1 ss-flex ss-row-center ss-col-center"
v-for="item in tabList"
:key="item.value"
@tap="onTab(item)"
>
<view class="tab-title" :class="currentTab === item.value ? 'cur-tab-title' : ''">
{{ item.label }}
</view>
<view v-show="currentTab === item.value" class="tab-line"></view>
</view>
</view>
</view>
<!-- #ifdef MP -->
<view :style="[capsuleStyle]"></view>
<!-- #endif -->
</view>
</su-fixed>
</template>
<script>
import sheep from '@/sheep';
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
export default {
components: {
},
props: {
title: {
type: String,
default: '',
},
//滚动条滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
},
data() {
return {
sys_navBar: sheep.$platform.navbar,
sys_statusBar: sheep.$platform.device.statusBarHeight,
hasHistory: sheep.$router.hasHistory(),
tabList: [
{
label: '价格',
value: 1,
},
{
label: '动态',
value: 2,
},
{
label: '评价',
value: 3,
},
],
capsuleStyle: {
width: sheep.$platform.capsule.width + 'px',
height: sheep.$platform.capsule.height + 'px',
},
}
},
computed: {
tabOpacityVal() {
return this.scrollTop > sheep.$platform.navbar ? 1 : this.scrollTop * 0.01;;
},
currentTab() {
return sheep.$store('sys').clerkTabIndex;
},
},
methods: {
onClickLeft() {
if (this.hasHistory) {
sheep.$router.back();
} else {
sheep.$router.go('/pages/tabbar/index');
}
},
onClickRight() {
showMenuTools();
},
onTab(e) {
this.$emit('onTab', e.value);
},
}
}
</script>
<style lang="scss" scoped>
.icon-box {
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
border-radius: 30rpx;
width: 134rpx;
height: 56rpx;
margin-left: 8rpx;
border: 1px solid rgba(#fff, 0.4);
.line {
width: 2rpx;
height: 24rpx;
background: #e5e5e7;
}
.sicon-back {
font-size: 32rpx;
color: #fff;
}
.sicon-home {
font-size: 32rpx;
color: #fff;
}
.sicon-more {
font-size: 32rpx;
color: #fff;
}
.black {
color: #000;
}
.icon-button {
width: 67rpx;
height: 56rpx;
&-left:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 30rpx 0px 0px 30rpx;
}
&-right:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 0px 30rpx 30rpx 0px;
}
}
}
.left-box {
position: relative;
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
.circle {
position: absolute;
left: 0;
top: 0;
width: 60rpx;
height: 60rpx;
background: rgba(#fff, 0.6);
border: 1rpx solid #ebebeb;
border-radius: 50%;
box-sizing: border-box;
z-index: -1;
}
}
.right {
position: relative;
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
.circle {
position: absolute;
left: 0;
top: 0;
width: 60rpx;
height: 60rpx;
background: rgba(#ffffff, 0.6);
border: 1rpx solid #ebebeb;
box-sizing: border-box;
border-radius: 50%;
z-index: -1;
}
}
.detail-tab-card {
width: 50%;
.tab-item {
height: 80rpx;
position: relative;
z-index: 11;
.tab-title {
font-size: 30rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10rpx;
background-color: var(--ui-BG-Main);
z-index: 12;
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<view>
<view class="option-box">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">服务类型</view>
</view>
<view class="span-box">
<view @click="changeGame(option)" class="span" :class="catId == option.id ? 'active': '' " v-for="(option,t) in optionList">{{option.name}}</view>
</view>
</view>
<view class="option-box" v-if="categoryList.length > 0">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">选项</view>
</view>
<view class="span-box">
<view @click="changeCategory(option)" class="span" :class="gameId == option.id ? 'active': '' " v-for="(option,t) in categoryList">{{option.name}}</view>
</view>
</view>
<view class="option-box" v-if="goodsList.length > 0">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">选项</view>
</view>
<view class="span-box">
<view @click="changeGoods(option)" class="span" :class="goodsId == option.id ? 'active': '' " v-for="(option,t) in goodsList">{{option.name}}</view>
</view>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
optionList: {
type: Array,
default: [],
},
modelValue: {
type: Object,
default: {}
},
},
data() {
return {
catId: -1,
gameId: -1,
goodsId: -1,
categoryList: [],
goodsList: [],
goodsList: [],
}
},
methods: {
changeGame(e) {
this.gameId = -1;
this.goodsId = -1;
this.catId = e.id;
if(e.categoryList){
this.goodsList = [];
this.categoryList = e.categoryList;
}else{
this.categoryList = [];
this.goodsList = e.goodsList;
}
var node = {
catId: this.catId,
gameId: this.gameId,
goodsId: this.goodsId,
price: 0,
}
this.$emit('update:modelValue', node);
},
changeCategory(e) {
this.goodsId = -1;
this.gameId = e.id;
this.goodsList = e.goodsList;
var node = {
catId: this.catId,
gameId: this.gameId,
goodsId: this.goodsId,
price: 0,
}
this.$emit('update:modelValue', node);
},
changeGoods(e) {
this.goodsId = e.id;
var node = {
catId: this.catId,
gameId: this.gameId,
goodsId: this.goodsId,
price: e.price,
}
this.$emit('update:modelValue', node);
},
}
}
</script>
<style lang="scss" scoped>
.option-box {
margin-bottom: 10px;
.tag-box {
display: flex;
align-items: center;
margin-bottom: 12px;
.name {
font-size: 28rpx;
color: #333;
margin-left: 5px;
}
}
.span-box {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
.span {
background-color: #f6f6f6;
padding: 14rpx 20rpx;
border-radius: 40px;
margin-right: 12px;
margin-bottom: 12px;
min-width: 140rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
color: #949494;
}
.active {
background-color: var(--ui-BG-Main);
color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,316 @@
<template>
<view v-show="showPop" class="gift-box">
<scroll-view style="height: 100%;" scroll-y>
<view class="top-box">
<view class="title">选择礼物</view>
<view class="btn-box" @click="close">
<u-icon name="close"></u-icon>
<view class="text">取消</view>
</view>
</view>
<view class="list-box">
<view v-for="(item,index) in dataList" class="gift" @click="sendGift(item)">
<view class="img-box">
<img class="img" :src="item.img"></img>
<view class="tag" v-if="item.giftType == 1">
<text>特效</text>
<text v-if="item.tag">·{{item.tag}}</text>
</view>
<view class="tag" v-if="item.giftType == 0 && item.tag">
<text>{{item.tag}}</text>
</view>
</view>
<view class="name">{{item.name}}</view>
<view class="price">{{ fen2yuan(item.money) }} 钻石</view>
</view>
</view>
</scroll-view>
</view>
<view class="svga-box" :class="giftFlag ? 'svga-show': 'svga-hide'">
<c-svga ref="cSvgaRef" :canvasId='canvasId' :src="src" :loops='0' :auto-play="false" @frame='onFrame' @finished='onFinished' @percentage='onPercentage' @loaded='onLoaded'></c-svga>
<view class="close-btn">
<view class="bottom-box">
<view class="title">{{gift.name}}</view>
<view class="price">{{ fen2yuan(gift.money) }} 钻石</view>
<view class="btn-box">
<view class="btn" @click="cannel">取消</view>
<view class="btn active" @click="ok">确定</view>
</view>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
export default {
components: {
},
props: {
modelValue: {
type: Object,
default: {}
},
showPop: {
type: Boolean,
default: false
},
dataList: {
type: Array,
default: []
},
},
emits: ["close", "update:modelValue"],
data() {
return {
giftFlag: false,
src: '',
canvasId: 'myCanvas2',
gift: {},
}
},
methods: {
close() {
this.$emit('close');
},
sendGift(e) {
this.gift = e;
if(e.giftType == 0){
// 普通礼物不播放
this.close();
this.$emit('update:modelValue', this.gift);
return;
}
this.src = e.pic;
this.giftFlag = true;
},
onFinished() {
this.giftFlag = false;
console.log('动画停止播放时回调');
},
onFrame(frame) {//动画播放至某帧后回调
// console.log(frame);
},
onPercentage(percentage) { //动画播放至某进度后回调
// console.log(percentage);
},
onLoaded() {
this.$refs.cSvgaRef.call('setContentMode', 'AspectFill');
console.log('加载完成');
this.$refs.cSvgaRef.call('startAnimation');
},
closeSvga() {
this.src = "";
this.$refs.cSvgaRef.call('stopAnimation');
this.giftFlag = false;
},
fen2yuan(price) {
var f = 0;
var p = (price / 100.0).toFixed(0);
var p1 = (price / 100.0).toFixed(1);
var p2 = (price / 100.0).toFixed(2);
if(p*100 == price){
f = 0;
}else if(p1*100 == price){
f = 1;
}else if(p2*100 == price){
f = 2;
}
return (price / 100.0).toFixed(f)
},
cannel() {
this.closeSvga();
},
ok() {
this.closeSvga();
this.close();
this.$emit('update:modelValue', this.gift);
},
}
}
</script>
<style lang="scss" scoped>
.gift-box {
background-color: #fff;
position: absolute;
top: 200rpx;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
padding: 15px;
border-top-right-radius: 20rpx;
border-top-left-radius: 20rpx;
padding-bottom: 0;
.top-box {
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
left: 0;
right: 0;
background-color: #fff;
top: 200rpx;
padding: 0 15px;
border-top-right-radius: 20rpx;
border-top-left-radius: 20rpx;
height: 110rpx;
z-index: 1;
.title {
font-size: 28rpx;
}
.btn-box {
background-color: var(--ui-BG-Main);
padding: 7px 15px;
font-size: 24rpx;
border-radius: 40px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
.text {
margin-left: 5px;
}
}
}
.list-box {
display: flex;
flex-wrap: wrap;
padding-top: 60rpx;
.gift {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 20rpx 0px;
.img-box {
position: relative;
.img{
max-width: 160rpx;
height: 160rpx;
}
.tag {
position: absolute;
top: 0;
right: 0;
font-size: 16rpx;
color: #fff;
background-color: var(--ui-BG-Main);
border-radius: 40px;
padding: 2rpx 8rpx;
}
}
.name {
font-size: 24rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 160rpx;
margin-top: 2px;
margin-bottom: 7px;
}
.price {
font-size: 20rpx;
color: var(--ui-BG-Main);
}
}
}
}
.svga-box {
position: fixed;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100%;
background-color: black;
.close-btn {
color: #fff;
position: absolute;
z-index: 999999999;
padding: 5px 10px;
left: 0;
right: 0;
bottom: 100rpx;
.bottom-box {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.title {
font-size: 28rpx;
margin-bottom: 5px;
}
.price {
font-size: 24rpx;
}
.btn-box {
display: flex;
align-items: center;
margin-top: 10px;
.btn {
border: 1px solid #fff;
font-size: 28rpx;
padding: 20rpx 110rpx;
border-radius: 40px;
margin: 15px;
color: #fff;
}
.active {
border: 1px solid var(--ui-BG-Main);
background-color: var(--ui-BG-Main);
}
}
}
}
}
.svga-hide {
/* #ifdef MP */
transform: translate(-100%, 0);
/* #endif */
/* #ifndef MP */
display: none;
/* #endif */
}
.svga-show {
/* #ifdef MP */
transform: translate(0, 0);
/* #endif */
/* #ifndef MP */
display: block;
/* #endif */
}
</style>

View File

@@ -0,0 +1,444 @@
<template>
<tui-bottom-popup :zIndex="1002" :maskZIndex="1001" :show="popupShow" @close="hiddenPopup">
<view class="order-box">
<view class="avatar-box">
<u-image width="140" height="140" border-radius="20" :src="clerk.avatar"></u-image>
<view class="close-span" @click="hiddenPopup">
<u-icon name="close-circle" color="#98a2a1" size="50"></u-icon>
</view>
</view>
<scroll-view style="height: 700rpx;" scroll-y>
<view class="page-box">
<view class="form-box">
<view class="input-box">
<view class="tag-box">
<view class="name">服务类型</view>
</view>
<view class="tab-span">
<!-- <view @click="changeTab(0)" class="btn" :class="form.rewardType == 0 ? 'active' : ''">赠送</view> -->
<view @click="changeTab(1)" class="btn" :class="form.rewardType == 1 ? 'active' : ''">赠礼</view>
</view>
</view>
<view class="input-box" v-if="form.rewardType == 0">
<view class="tag-box">
<view class="name">打赏金额</view>
</view>
<view class="input-span">
<u-input v-model="payMoney" type="number" :placeholder-style="`fontSize: 24rpx; color: #cbced5;`" placeholder="请输入赠送金额"></u-input>
</view>
</view>
<view class="input-box" v-if="form.rewardType == 1">
<view class="tag-box">
<view class="name">赠送礼物</view>
</view>
<view class="select-span" @click="openGift">
<view v-if="gift.id > 0">已选{{gift.name}}</view>
<view v-else>点击选择礼物</view>
<u-icon name="arrow-right"></u-icon>
</view>
</view>
<view class="input-box" v-if="gift.id > 0">
<view class="tag-box">
<view class="name">礼物数量</view>
</view>
<view class="step-span">
<img class="img" :src="gift.img"></img>
<tui-numberbox iconBgColor="var(--ui-BG-Main)" iconColor="#fff" backgroundColor="#fff" :min="1" :value="form.count" @change="change"></tui-numberbox>
</view>
</view>
<view class="input-box">
<view class="textarea-box">
<view class="name">心动留言</view>
</view>
<view class="textarea-span">
<u-input v-model="form.msg" type="textarea" :placeholder-style="`fontSize: 24rpx; color: #cbced5;`" placeholder="请输入心动留言"></u-input>
</view>
</view>
<view class="input-box" v-if="isPass">
<view class="tag-box">
<view class="name">您的微信号</view>
</view>
<view class="input-span">
<u-input v-model="form.weixin" :placeholder-style="`fontSize: 24rpx; color: #cbced5;`" placeholder="请输入正确的微信号"></u-input>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-box2" v-if="form.rewardType == 0">
<view class="btn-box">
<view class="pay-btn" @click="confirm">立即赠送</view>
</view>
</view>
<view class="bottom-box" v-if="form.rewardType == 1">
<view class="price-box">
<text>总价</text>
<text class="price">{{ fen2yuan(gift.money*form.count) }}</text>
<text>钻石</text>
</view>
<view class="btn-box">
<view class="pay-btn" @click="confirm">立即赠送</view>
</view>
</view>
</view>
</tui-bottom-popup>
<gift-list :showPop="giftFlag" :dataList="giftList" v-model="gift" @close="closeGift"></gift-list>
</template>
<script>
import tuiBottomPopup from "@/components/thorui/tui-bottom-popup/tui-bottom-popup.vue"
import tuiNumberbox from "@/components/thorui/tui-numberbox/tui-numberbox.vue"
import GiftList from '@/pages/clerk/detail/components/giftList.vue';
import RewardApi from '@/sheep/api/worker/reward';
import sheep from '@/sheep';
export default {
components: {
tuiBottomPopup,
tuiNumberbox,
GiftList,
},
props: {
},
data() {
return {
popupShow: false,
gift: {
id: -1,
money: 0,
},
giftFlag: false,
giftList: [],
payMoney: '',
form: {
rewardType: 1,
count: 1,
giftId: -1,
money: '',
weixin: '',
msg: '',
payMoney: '',
},
}
},
computed: {
clerk() {
return sheep.$store('sys').clerk;
},
userInfo() {
return sheep.$store('user').userInfo;
},
isPass() {
return sheep.$store('user').tradeConfig.weixinEnabled;
},
},
methods: {
//调用此方法显示弹层
showPopup() {
this.form.weixin = this.userInfo.weixin;
RewardApi.getGiftList().then(res => {
this.giftList = res.data;
this.popupShow = true
});
},
showGiftPopup(e) {
this.gift = e;
this.form.rewardType = 1;
this.form.weixin = this.userInfo.weixin;
RewardApi.getGiftList().then(res => {
this.giftList = res.data;
this.popupShow = true
});
},
hiddenPopup() {
this.popupShow = false
},
change(e) {
this.form.count = e.value
},
changeTab(e) {
this.form.rewardType = e;
this.gift = {
id: -1,
money: 0,
};
},
fen2yuan(price) {
var f = 0;
var p = (price / 100.0).toFixed(0);
var p1 = (price / 100.0).toFixed(1);
var p2 = (price / 100.0).toFixed(2);
if(p*100 == price){
f = 0;
}else if(p1*100 == price){
f = 1;
}else if(p2*100 == price){
f = 2;
}
return (price / 100.0).toFixed(f)
},
yuan2fen(price) {
return (price * 100.0).toFixed(0)
},
openGift() {
this.giftFlag = true;
},
closeGift() {
this.form.count = 1;
this.giftFlag = false;
},
confirm() {
if(this.isPass && !this.form.weixin){
sheep.$helper.toast('请输入正确的微信');
return;
}
if(this.form.rewardType == 0){
this.form.payMoney = this.yuan2fen(this.payMoney);
if(this.form.payMoney < 1){
sheep.$helper.toast('请输入打赏金额');
return;
}
}else{
this.form.giftId = this.gift.id;
if(this.form.giftId < 1){
sheep.$helper.toast('请选择打赏礼物');
return;
}
}
this.form.workerClerkId = this.clerk.id;
RewardApi.createRewardOrder(this.form).then(res => {
// 跳转到支付页面
this.$u.route({
url: 'pages/pay/reward/index',
params: {
id: res.data.payOrderId,
}
});
});
},
}
}
</script>
<style lang="scss" scoped>
.avatar-box {
padding: 10px 0;
display: flex;
justify-content: center;
align-items: center;
.close-span {
position: absolute;
right: 5px;
top: 5px;
width: 70rpx;
height: 70rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.page-box {
padding: 15px;
padding-bottom: 10px;
.form-box {
margin-bottom: 10px;
.tag-box {
display: flex;
align-items: center;
width: 180rpx;
.name {
font-size: 24rpx;
color: #333;
margin-left: 5px;
}
}
.textarea-box {
height: 20px;
display: flex;
width: 180rpx;
.name {
font-size: 24rpx;
color: #333;
margin-left: 5px;
}
}
.input-box {
display: flex;
margin-bottom: 20px;
align-items: center;
.input-span {
background-color: #f6f6f6;
margin-left: 15px;
display: flex;
flex: 1;
padding: 0 12px;
border-radius: 10rpx;
}
.select-span {
font-size: 24rpx;
background-color: #f6f6f6;
margin-left: 15px;
display: flex;
flex: 1;
padding: 12px 12px;
border-radius: 10rpx;
color: #aaaaaa;
justify-content: space-between;
}
.tab-span {
display: flex;
flex: 1;
padding: 0 12px;
align-items: center;
.btn {
background-color: #f6f6f6;
font-size: 24rpx;
padding: 7px 0;
border-radius: 40px;
min-width: 120rpx;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.active {
color: #fff;
background-color: var(--ui-BG-Main);
}
}
.textarea-span {
background-color: #f6f6f6;
margin-left: 15px;
flex: 1;
padding: 2px 12px;
border-radius: 10px;
}
.step-span {
display: flex;
justify-content: space-between;
flex: 1;
padding-left: 12px;
align-items: center;
.img {
width: 80rpx;
height: 80rpx;
border-radius: 100%;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
}
}
}
}
}
.bottom-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
box-shadow: 0 0 6px 0 #ccc;
.price-box {
color: #fb932c;
font-size: 28rpx;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
.price {
margin-right: 5px;
}
}
.btn-box {
width: 50%;
padding-left: 15px;
.pay-btn {
background-color: var(--ui-BG-Main);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-size: 28rpx;
height: 70rpx;
}
}
}
.bottom-box2 {
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
box-shadow: 0 0 6px 0 #ccc;
.price-box {
color: #fb932c;
font-size: 28rpx;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
.price {
margin-right: 5px;
}
}
.btn-box {
width: 50%;
.pay-btn {
background-color: var(--ui-BG-Main);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-size: 28rpx;
height: 70rpx;
}
}
}
</style>

View File

@@ -0,0 +1,263 @@
<!-- 虚拟列表演示(不使用内置列表)(vue) -->
<!-- 写法较简单在页面中对当前需要渲染的虚拟列表数据进行for循环在vue3中兼容性良好 -->
<!-- 在各平台兼容性请查阅https://z-paging.zxlee.cn/module/virtual-list.html -->
<template>
<view class="content">
<!-- 如果页面中的cell高度是固定不变的则不需要设置cell-height-mode如果页面中高度是动态改变的则设置cell-height-mode="dynamic" -->
<!-- 原先的v-model修改为@virtualListChange="virtualListChange"并赋值处理后的虚拟列表 -->
<z-paging ref="paging" :show-loading-more-no-more-view="showLoad" :show-default-loading-more-text="showLoad" use-virtual-list :force-close-inner-list="true" :paging-style="{ paddingTop: 0 + 'px', paddingBottom: paddingBottom + 'rpx' }" cell-height-mode="dynamic" @scroll="scroll" @virtualListChange="virtualListChange" @query="queryList">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中如果需要跟着滚动则不要设置slot="top" -->
<template #top>
<nav-bar :title="title" :scrollTop="scrollTop" @onTab="change" @initNav="initNav"></nav-bar>
</template>
<view>
<user-box :clerk="clerk" :clerkLevel="clerk.clerkLevel" :paddingTop="paddingTop"></user-box>
<sticky-box :clerk="clerk" @change="change"></sticky-box>
</view>
<!-- :id="`zp-id-${item.zp_index}`":key="item.zp_index" 必须写必须写 -->
<!-- 这里for循环的index不是数组中真实的index了请使用item.zp_index获取真实的index -->
<view class="data-box">
<star-list v-if="currentTab == 0" @openGift="openGift" :clerk="clerk" :virtualList="giftList"></star-list>
<price-list v-if="currentTab == 1" :clerk="clerk"></price-list>
<post-list v-if="currentTab == 2" :virtualList="virtualList"></post-list>
<rate-list v-if="currentTab == 3" :clerk="clerk" :virtualList="virtualList"></rate-list>
</view>
<template #bottom>
<view v-if="isGift" class="bottom-box">
<view class="order" @click="doOrder">立即下单</view>
<view class="gift" @click="doGift">赠礼</view>
</view>
<view v-else class="bottom-box">
<view class="order-btn" @click="doOrder">立即下单</view>
</view>
</template>
</z-paging>
<order-popup ref="orderPopup"></order-popup>
<gift-popup ref="giftPopup"></gift-popup>
</view>
</template>
<script>
import UserBox from '@/pages/clerk/detail/components/userBox.vue';
import StickyBox from '@/pages/clerk/detail/components/stickyBox.vue';
import StarList from '@/pages/clerk/detail/components/starList.vue';
import PriceList from '@/pages/clerk/detail/components/priceList.vue';
import PostList from '@/pages/clerk/detail/components/postList.vue';
import RateList from '@/pages/clerk/detail/components/rateList.vue';
import NavBar from '@/pages/clerk/detail/components/navBar.vue';
import OrderPopup from '@/pages/clerk/detail/components/orderPopup.vue';
import GiftPopup from '@/pages/clerk/detail/components/giftPopup.vue';
import UserApi from '@/sheep/api/member/user';
import ClerkApi from '@/sheep/api/worker/clerk';
import TrendApi from '@/sheep/api/worker/trend';
import RewardApi from '@/sheep/api/worker/reward';
import CommentApi from '@/sheep/api/product/comment';
import { showAuthModal } from '@/sheep/hooks/useModal';
import sheep from '@/sheep';
export default {
components: {
UserBox,
StickyBox,
StarList,
PriceList,
PostList,
RateList,
NavBar,
OrderPopup,
GiftPopup,
},
props: {
title: {
type: String,
default: '',
}
},
data() {
return {
// 虚拟列表数组,通过@virtualListChange监听获得最新数组
virtualList: [],
scrollTop: 0,
paddingTop: 0,
paddingBottom: 0,
height: 0,
clerk: {},
workerClerkId: 0,
giftList: [],
}
},
computed: {
currentTab() {
return sheep.$store('sys').clerkTabIndex;
},
showLoad() {
return sheep.$store('sys').clerkTabIndex > 1;
},
isLogin: {
get() {
return sheep.$store('user').isLogin;
},
},
userInfo: {
get() {
return sheep.$store('user').userInfo;
},
},
isGift() {
return sheep.$store('user').tradeConfig.giftEnabled;
},
},
methods: {
initNav(e) {
this.height = e.height;
this.paddingTop = this.height;
},
initData(options) {
this.workerClerkId = options.id;
ClerkApi.getClerk({
id: options.id,
userId: this.userInfo.id,
}).then(res => {
this.clerk = res.data;
sheep.$store('sys').setClerk(res.data);
});
RewardApi.getRewardGiftList({
workerClerkId: options.id,
}).then(res => {
this.giftList = res.data;
});
},
scroll(e) {
this.scrollTop = e.detail.scrollTop;
},
// 监听虚拟列表数组改变并赋值给virtualList进行重新渲染
virtualListChange(vList) {
this.virtualList = vList;
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
const params = {
pageNo: pageNo,
pageSize: pageSize,
workerClerkId: this.workerClerkId,
}
if(this.currentTab == 0){
}else if(this.currentTab == 1){
}else if(this.currentTab == 2){
TrendApi.getTrendPage(params).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
}else if(this.currentTab == 3){
CommentApi.getCommentPage(this.workerClerkId,pageNo,pageSize,0).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
}
},
change(e) {
sheep.$store('sys').setClerkTabIndex(e);
this.scrollTop = 0;
this.$refs.paging.reload();
},
doOrder() {
if(this.isLogin){
this.$refs.orderPopup.showPopup();
}else{
showAuthModal();
}
},
doGift() {
if(this.isLogin){
this.$refs.giftPopup.showPopup();
}else{
showAuthModal();
}
},
openGift(e) {
if(this.isLogin){
this.$refs.giftPopup.showGiftPopup(e);
}else{
showAuthModal();
}
},
}
}
</script>
<style lang="scss" scoped>
.data-box {
padding: 0 15px;
}
.bottom-box {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 30px;
box-shadow: 0 0 6px 0 #ccc;
.order {
background-color: var(--ui-BG-Main);
padding: 10px;
border-top-left-radius: 40px;
border-bottom-left-radius: 40px;
color: #fff;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
height: 88rpx;
font-size: 28rpx;
}
.order-btn {
background-color: var(--ui-BG-Main);
padding: 10px;
border-radius: 40px;
color: #fff;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 88rpx;
font-size: 28rpx;
}
.gift {
background-color: #eef3f2;
padding: 10px;
border-top-right-radius: 40px;
border-bottom-right-radius: 40px;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
height: 88rpx;
color: #aaa;
font-size: 28rpx;
}
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<view>
<tui-navigation-bar @init="initNavigation" @change="opacityChange" :scrollTop="scrollTop" backgroundColor="#fff" color="#333">
<detailNavbar @onTab="onTab" :scrollTop="scrollTop" />
</tui-navigation-bar>
</view>
</template>
<script>
import tuiNavigationBar from "@/components/thorui/tui-navigation-bar/tui-navigation-bar.vue";
import detailNavbar from '@/pages/clerk/detail/components/detail-navbar.vue';
export default {
components: {
tuiNavigationBar,
detailNavbar,
},
props: {
title: {
type: String,
default: '',
},
//滚动条滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
},
data() {
return {
top: 0, //标题图标距离顶部距离
opacity: 0,
height: 0,
}
},
methods: {
initNavigation(e) {
this.height = e.height;
this.opacity = e.opacity;
this.top = e.top;
this.$emit('initNav', e);
},
opacityChange(e) {
this.opacity = e.opacity;
},
onTab(e) {
this.$emit('onTab', e);
},
}
}
</script>
<style lang="scss" scoped>
.content-bpx {
color: #fff;
display: flex;
align-items: center;
height: 44px;
justify-content: center;
.left-box {
display: flex;
position: absolute;
left: 0;
}
.nickname {
position: absolute;
width: 100px;
display: flex;
justify-content: center;
align-items: center;
height: 44px;
font-size: 15px;
font-weight: bolder;
color: #1f2122;
}
.set-btn {
padding: 0 15px;
height: 44px;
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<tui-bottom-popup :zIndex="1002" :maskZIndex="1001" :show="popupShow" @close="hiddenPopup">
<view class="order-box">
<view class="avatar-box">
<u-image width="140" height="140" border-radius="20" :src="clerk.avatar"></u-image>
<view class="close-span" @click="hiddenPopup">
<u-icon name="close-circle" color="#98a2a1" size="50"></u-icon>
</view>
</view>
<scroll-view style="height: 700rpx;" scroll-y>
<view class="page-box">
<game-box v-model="game" :optionList="clerk.goodsList"></game-box>
<view class="form-box">
<!-- <view class="input-box">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">你的微信号</view>
</view>
<view class="input-span">
<u-input placeholder="请输入正确的微信号~"></u-input>
</view>
</view>
<view class="input-box">
<view class="textarea-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">其它备注</view>
</view>
<view class="textarea-span">
<u-input type="textarea" placeholder="请输入备注内容"></u-input>
</view>
</view> -->
<view class="input-box">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">购买数量</view>
</view>
<view class="step-span">
<tui-numberbox iconBgColor="var(--ui-BG-Main)" iconColor="#fff" backgroundColor="#fff" :min="1" :value="order.num" @change="change"></tui-numberbox>
</view>
</view>
<!-- <view class="input-box">
<radio-box style="width: 100%;" v-model="payMethod" :optionList="optionList"></radio-box>
</view> -->
</view>
</view>
</scroll-view>
<view class="bottom-box">
<view class="price-box">
<text>总价</text>
<text class="price">{{ fen2yuan(game.price*order.num) }}</text>
<text>钻石</text>
</view>
<view class="btn-box">
<view class="pay-btn" @click="confirm">立即下单</view>
</view>
</view>
</view>
</tui-bottom-popup>
</template>
<script>
import tuiBottomPopup from "@/components/thorui/tui-bottom-popup/tui-bottom-popup.vue"
import tuiNumberbox from "@/components/thorui/tui-numberbox/tui-numberbox.vue"
import RadioBox from '@/pages/clerk/detail/components/radioBox.vue';
import GameBox from '@/pages/clerk/detail/components/gameBox.vue';
import sheep from '@/sheep';
export default {
components: {
tuiBottomPopup,
tuiNumberbox,
RadioBox,
GameBox,
},
props: {
},
data() {
return {
popupShow: false,
optionList: [
{
name: '余额支付',
value: '0',
type: 'number',
number: '1000',
},
{
name: '微信支付',
value: '1',
type: 'icon',
},
],
payMethod: '0',
game: {
goodsId: -1,
price: 0,
},
order: {
num: 1,
goodsId: -1,
price: 0,
},
}
},
computed: {
clerk() {
return sheep.$store('sys').clerk;
},
},
methods: {
//调用此方法显示弹层
showPopup() {
this.popupShow = true
},
hiddenPopup() {
this.popupShow = false
},
change(e) {
this.order.num = e.value
},
// 选中某个单选框时由radio时触发
radioChange(e) {
// console.log(e);
},
// 选中任一radio时由radio-group触发
radioGroupChange(e) {
// console.log(e);
},
fen2yuan(price) {
return (price / 100.0).toFixed(0)
},
confirm() {
this.order.goodsId = this.game.goodsId,
this.order.price = this.game.price*this.order.num
if(this.order.goodsId < 0) {
sheep.$helper.toast('请选择商品');
return;
}
var data = {
"clerkId": this.clerk.id,
'items' : [{"skuId": this.order.goodsId, "count": this.order.num}]
}
this.$u.route({
url: 'pages/order/worker/confirm',
params: {
data: JSON.stringify(data),
}
});
},
}
}
</script>
<style lang="scss" scoped>
.avatar-box {
padding: 10px 0;
display: flex;
justify-content: center;
align-items: center;
.close-span {
position: absolute;
right: 5px;
top: 5px;
width: 70rpx;
height: 70rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.page-box {
padding: 15px;
padding-bottom: 10px;
.form-box {
margin-bottom: 10px;
.tag-box {
display: flex;
align-items: center;
width: 200rpx;
.name {
font-size: 28rpx;
color: #333;
margin-left: 5px;
}
}
.textarea-box {
height: 20px;
display: flex;
width: 200rpx;
.name {
font-size: 28rpx;
color: #333;
margin-left: 5px;
}
}
.input-box {
display: flex;
margin-bottom: 20px;
.input-span {
background-color: #f6f6f6;
margin-left: 15px;
display: flex;
flex: 1;
padding: 0 12px;
border-radius: 40px;
}
.textarea-span {
background-color: #f6f6f6;
margin-left: 15px;
flex: 1;
padding: 2px 12px;
border-radius: 10px;
}
.step-span {
display: flex;
justify-content: flex-end;
flex: 1;
align-items: center;
}
}
}
}
.bottom-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
box-shadow: 0 0 6px 0 #ccc;
.price-box {
color: #fb932c;
font-size: 28rpx;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
.price {
margin-right: 5px;
}
}
.btn-box {
width: 50%;
padding-left: 15px;
.pay-btn {
background-color: var(--ui-BG-Main);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-size: 28rpx;
height: 70rpx;
}
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<view class="photo-box">
<scroll-view scroll-x>
<view class="scroll-box">
<view class="img-box" @click="showImage(item)" v-for="(item,index) in dataList">
<u-image border-radius="20" width="170rpx" height="170rpx" :src="item"></u-image>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
components: {
},
props: {
dataList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
methods: {
showImage(src) {
uni.previewImage({
indicator: "none",
current: src,
urls: this.dataList,
});
},
}
}
</script>
<style lang="scss" scoped>
.photo-box {
.scroll-box {
display: flex;
}
.img-box {
margin-right: 15px;
}
}
</style>

View File

@@ -0,0 +1,342 @@
<template>
<view>
<view class="item" :id="`zp-id-${item.zp_index}`" :key="item.zp_index" v-for="(item,index) in orderList">
<view class="card">
<view class="right">
<view class="card-header">
<view class="box3">
<view class="box1">
<view class="tag-list">
<!-- <view class="tag">Ta爱玩王者荣耀</view> -->
<view class="tag">发布于 {{item.createTimeStr}}</view>
</view>
</view>
</view>
</view>
<view class="card-content">
<view class="text-box">
<rich-text :nodes="item.content"></rich-text>
</view>
</view>
<view class="image-box" v-if="item.fileType == 0">
<img-box :file="item.file"></img-box>
</view>
<view class="voice-box" v-if="item.fileType == 1">
<voice-play :sec="item.seconds" @tap.stop="playAudio(item)" :isPlay="item.id == playId"></voice-play>
</view>
<view class="video-box" v-if="item.fileType == 2">
<video-box :file="item.file"></video-box>
</view>
<view class="topic-list">
<view class="topic">
<u-icon name="map-fill" size="40" color="#3cc9a4"></u-icon>
<view class="tag-text">{{item.city}}</view>
<u-icon name="arrow-right" size="24" color="#3cc9a4"></u-icon>
</view>
</view>
<view class="card-footer">
<!-- <view class="toolbar">
<u-icon color="#f880ab" name="presentfill" size="38" custom-prefix="iconfont"></u-icon>
<view class="toolbar-text">3</view>
</view> -->
<!-- <view class="toolbar">
<u-icon name="pinglun" size="38" custom-prefix="iconfont"></u-icon>
<view class="toolbar-text">22</view>
</view> -->
<view class="toolbar" @click="thumb(item)">
<u-icon v-if="item.like" name="thumb-up-fill" color="var(--ui-BG-Main)" size="40"></u-icon>
<u-icon v-else name="thumb-up" size="40"></u-icon>
<view class="toolbar-text" v-if="item.likeNum > 0">{{item.likeNum}}</view>
<view class="text" v-else>点赞</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import ImgBox from '@/pages/tabbar/components/trend/imgBox.vue';
import VideoBox from '@/pages/tabbar/components/trend/videoBox.vue';
import VoicePlay from '@/pages/tabbar/components/trend/voicePlay.vue';
import sheep from '@/sheep';
import TrendApi from '@/sheep/api/worker/trend';
const audio = uni.createInnerAudioContext();
export default {
components: {
ImgBox,
VideoBox,
VoicePlay,
},
props: {
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
playId: null,
}
},
computed: {
orderList() {
this.virtualList.forEach((order) => order.createTimeStr = sheep.$helper.timeFormat(order.createTime, 'yyyy-mm-dd hh:MM'));
return this.virtualList;
},
},
methods: {
playAudio(e) {
if(this.playId == e.id){
this.playId = null;
audio.stop();
return;
}
this.playId = e.id;
//语音自然播放结束
audio.onEnded((res) => {
this.playId = null;
});
audio.src = e.file;
audio.play();
},
thumb(e) {
TrendApi.createTrendLike({
trendId: e.id,
}).then((res) => {
if(res) {
if(e.like){
e.like = false;
e.likeNum = e.likeNum-1;
sheep.$helper.toast('取消点赞');
}else{
e.like = true;
e.likeNum = e.likeNum+1;
sheep.$helper.toast('点赞成功');
}
}
});
},
fans(e) {
TrendApi.createClerkFans({
workerClerkId: e.workerClerkId,
}).then((res) => {
if(res){
if(e.fans){
sheep.$helper.toast('取消收藏');
e.fans = false;
}else{
sheep.$helper.toast('收藏成功');
e.fans = true;
}
}
});
},
detail(e) {
this.$u.route({
url: 'pages/clerk/detail/index',
params: {
id: e.workerClerkId,
}
});
},
}
}
</script>
<style lang="scss" scoped>
.item {
display: flex;
flex-direction: column;
}
.card {
padding: 30rpx 20rpx;
border-bottom: 1rpx solid #f5f5f5;
display: flex;
.right {
display: flex;
flex-direction: column;
flex: 1;
}
}
.avatar-img{
margin-right: 5px;
width: 110rpx;
height: 110rpx;
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 8rpx;
.img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}
.avatar{
margin-right: 5px;
width: 110rpx;
height: 110rpx;
position: relative;
display: flex;
justify-content: center;
align-items: center;
.img {
width: 75%;
height: 75%;
border-radius: 100%;
}
.gif {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
}
}
.card-header {
display: flex;
align-items: center;
.box1 {
.tag-list {
display: flex;
align-items: center;
font-size: 22rpx;
margin-top: 4rpx;
.tag::after {
content: '·';
padding: 0 10rpx; /* 添加一些间隔 */
}
}
.tag-list .tag:last-child::after {
content: unset;
}
}
.box2 {
display: flex;
align-items: center;
line-height: 40rpx;
.nickname {
font-size: 28rpx;
color: #333333;
}
.sex-box {
display: flex;
align-items: center;
justify-content: center;
background-color: #d06b8d1f;
border-radius: 20px;
font-size: 20rpx;
padding: 4rpx 10rpx;
color: #d06b8d;
line-height: 24rpx;
margin: 0 5px;
.age {
margin-left: 2px;
}
}
.nan {
background-color: #007aff1a;
color: #007aff;
}
}
.box3 {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
}
.fans {
width: 60rpx;
display: flex;
justify-content: flex-end;
height: 60rpx;
align-items: center;
color: var(--ui-BG-Main);
font-size: 24rpx;
padding-right: 3px;
}
}
.card-content {
margin-top: 20rpx;
margin-bottom: 10rpx;
.text-box {
font-size: 26rpx;
line-height: 180%;
white-space: pre-wrap;
letter-spacing: 2rpx;
}
}
.topic-list {
display: flex;
align-items: center;
margin-top: 10px;
.topic {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
margin-right: 20rpx;
color: var(--ui-BG-Main);
font-weight: bold;
.tag-text {
margin-left: 2px;
}
}
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
.toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
color: #666666;
width: 80rpx;
height: 40rpx;
flex: 1;
}
.toolbar-text {
font-size: 26rpx;
margin-left: 5px;
}
.text {
font-size: 24rpx;
margin-left: 5px;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<view class="price-box">
<table-list :tableList="clerk.goodsList"></table-list>
<view class="price-remark">
<u-parse :html="content"></u-parse>
</view>
</view>
</template>
<script>
import TableList from '@/pages/clerk/detail/components/tableList.vue';
export default {
components: {
TableList,
},
props: {
clerk: {
type: Object,
default: {},
},
},
data() {
return {
content: `
<p>露从今夜白,月是故乡明</p>
`
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.price-box {
padding: 30rpx 20rpx;
padding-top: 0px;
.price-remark {
padding: 5px 15px;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<view class="check-box">
<view class="check-option" @click="change(option)" v-for="(option,index) in optionList">
<view class="icon-box">
<view class="check-icon-span" v-if="option.value == valueDom">
<u-icon name="checkmark-circle-fill" color="var(--ui-BG-Main)" size="50"></u-icon>
</view>
<text class="check-icon" v-else></text>
<text class="check-label">{{option.name}}</text>
</view>
<view class="price-box" v-if="option.type == 'number'">
<text class="num">{{option.number}}</text>
<text>钻石</text>
</view>
<view v-if="option.type == 'icon'">
<u-icon name="weixin-fill" color="#39b54a" size="50"></u-icon>
</view>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
optionList: {
type: Array,
default: [],
},
modelValue: {
type: String,
default: ''
},
},
data() {
return {
}
},
computed: {
valueDom() {
return this.modelValue;
},
},
methods: {
change(e) {
this.$emit('update:modelValue', e.value);
},
}
}
</script>
<style lang="scss" scoped>
.check-box {
display: flex;
flex-direction: column;
flex: 1;
.check-option {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.icon-box {
display: flex;
align-items: center;
.check-icon {
width: 40rpx;
height: 40rpx;
border-radius: 100%;
border: 1px solid var(--ui-BG-Main);
display: flex;
justify-content: center;
align-items: center;
}
.check-icon-span {
width: 40rpx;
height: 44rpx;
display: flex;
justify-content: center;
align-items: center;
}
.check-label {
font-size: 28rpx;
color: var(--ui-BG-Main);
margin-left: 5px;
}
}
.price-box {
font-size: 26rpx;
color: var(--ui-BG-Main);
.num {
font-size: 40rpx;
margin-right: 2px;
}
}
}
.check-option:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<view :id="`zp-id-${item.zp_index}`" :key="item.zp_index" v-for="(item,index) in orderList" class="card-box">
<view class="avatar-box">
<u-avatar v-if="item.anonymous" size="80"></u-avatar>
<u-avatar v-else size="80" :src="item.userAvatar"></u-avatar>
<view class="nickname-box">
<view class="nickname">
<text class="text">{{item.userNickname}}</text>
<uni-rate :readonly="true" v-model="item.scores" size="14" />
</view>
<view class="time">{{item.createTimeStr}}</view>
</view>
</view>
<view class="spu-box">
<text class="tag">{{clerk.nickname}}</text>
<text class="tag">服务类型{{item.skuName}}</text>
</view>
<view class="content">
<rich-text :nodes="item.content"></rich-text>
</view>
<view class="img-box" v-if="item.picUrls && item.picUrls.length > 0">
<img-box :file="item.picUrls.join(',')"></img-box>
</view>
</view>
</template>
<script>
import ImgBox from '@/pages/tabbar/components/trend/imgBox.vue';
import sheep from '@/sheep';
export default {
components: {
ImgBox
},
props: {
clerk: {
type: Object,
default: {},
},
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
computed: {
orderList() {
this.virtualList.forEach((order) => order.createTimeStr = sheep.$helper.timeFormat(order.createTime, 'yyyy-mm-dd'));
this.virtualList.forEach(function(e){
if(e.skuProperties){
var skuNameList = [];
e.skuProperties.forEach(function(sku){
skuNameList.push(sku.valueName);
});
e.skuName = skuNameList.join('-');
}
});
return this.virtualList;
},
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.card-box {
padding: 30rpx 20rpx;
border-bottom: 1rpx solid #eeeeee;
}
.avatar-box {
display: flex;
align-items: center;
.nickname-box {
font-size: 24rpx;
margin-left: 10px;
.nickname {
letter-spacing: 0.0625rem;
display: flex;
align-items: center;
.text {
letter-spacing: 2rpx;
margin-right: 10px;
}
}
.time {
color: rgb(164, 164, 164);
font-size: 22rpx;
margin-top: 3px;
}
}
}
.spu-box {
font-size: 24rpx;
color: rgb(164, 164, 164);
margin: 10px 0;
letter-spacing: 2rpx;
.tag {
margin-right: 20rpx;
}
}
.content {
font-size: 26rpx;
line-height: 180%;
white-space: pre-wrap;
letter-spacing: 2rpx;
}
</style>

View File

@@ -0,0 +1,310 @@
<template>
<view>
<view v-if="isGift" class="star-box">
<view class="title-box">
<view class="title">礼物墙</view>
<view class="num">(已点亮{{starNum}}/{{total}})</view>
</view>
<view class="list-box">
<view @click="sendGift(item)" v-for="(item,index) in virtualList" :key="item.id" class="gift">
<view class="img-box">
<img class="img" :class="item.playStatus == 0 ? 'hui' : ''" :src="item.img"></img>
<view class="tag" v-if="item.giftType == 1">
<text>特效</text>
<text v-if="item.tag">·{{item.tag}}</text>
</view>
<view class="tag" v-if="item.giftType == 0 && item.tag">
<text>{{item.tag}}</text>
</view>
</view>
<view class="name">{{item.name}}</view>
<view class="price">x {{item.playNum}}</view>
<view class="btn" @tap.stop="openGift(item)" v-if="item.playStatus == 0">点亮</view>
<view class="btn" @tap.stop="openGift(item)" v-if="item.playStatus == 1">赠送</view>
<view class="btn" v-if="item.playStatus == 2">已下架</view>
</view>
</view>
</view>
<view class="svga-box" :class="giftFlag ? 'svga-show': 'svga-hide'">
<c-svga ref="cSvgaRef" :canvasId='canvasId' :src="src" :loops='0' :auto-play="false" @frame='onFrame' @finished='onFinished' @percentage='onPercentage' @loaded='onLoaded'></c-svga>
<view class="close-btn" @click="closeSvga">
<view class="bottom-box">
<view class="title">{{gift.name}}</view>
<view class="price">{{ fen2yuan(gift.money) }} 钻石</view>
<view class="btn-box">
<view class="btn" @click="cannel">取消</view>
<view v-if="gift.playStatus == 0" class="btn active" @click="ok">点亮</view>
<view v-if="gift.playStatus == 1" class="btn active" @click="ok">赠送</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
export default {
components: {
},
props: {
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
giftFlag: false,
src: '',
canvasId: 'myCanvas',
gift: {},
}
},
computed: {
starNum() {
var count = 0;
this.virtualList.forEach(e => {
if(e.playStatus > 0){
count ++;
}
});
return count;
},
total() {
return this.virtualList.length;
},
isGift() {
return sheep.$store('user').tradeConfig.giftEnabled;
},
},
methods: {
sendGift(e) {
this.gift = e;
if(e.giftType == 0){
// 普通礼物不播放
this.$emit('openGift', this.gift);
return;
}
this.src = e.pic;
this.giftFlag = true;
},
onFinished() {
this.giftFlag = false;
console.log('动画停止播放时回调');
},
onFrame(frame) {//动画播放至某帧后回调
// console.log(frame);
},
onPercentage(percentage) { //动画播放至某进度后回调
// console.log(percentage);
},
onLoaded() {
this.$refs.cSvgaRef.call('setContentMode', 'AspectFill');
console.log('加载完成');
this.$refs.cSvgaRef.call('startAnimation');
},
closeSvga() {
this.src = "";
this.$refs.cSvgaRef.call('stopAnimation');
this.giftFlag = false;
},
fen2yuan(price) {
var f = 0;
var p = (price / 100.0).toFixed(0);
var p1 = (price / 100.0).toFixed(1);
var p2 = (price / 100.0).toFixed(2);
if(p*100 == price){
f = 0;
}else if(p1*100 == price){
f = 1;
}else if(p2*100 == price){
f = 2;
}
return (price / 100.0).toFixed(f)
},
cannel() {
this.closeSvga();
},
ok() {
this.closeSvga();
this.$emit('openGift', this.gift);
},
openGift(e) {
this.$emit('openGift', e);
},
}
}
</script>
<style lang="scss" scoped>
.star-box {
padding: 30rpx 20rpx;
.title-box {
display: flex;
align-items: center;
.title {
font-size: 28rpx;
margin-right: 10px;
}
.num {
font-size: 24rpx;
color: var(--ui-BG-Main);
}
}
.list-box {
display: flex;
flex-wrap: wrap;
padding-top: 30rpx;
.gift {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 20rpx 0px;
.img-box {
position: relative;
.img{
max-width: 150rpx;
height: 150rpx;
}
.hui {
filter: grayscale(100%);
}
.tag {
position: absolute;
top: 0;
right: 0;
font-size: 16rpx;
color: #fff;
background-color: var(--ui-BG-Main);
border-radius: 40px;
padding: 2rpx 8rpx;
}
}
.name {
font-size: 24rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 150rpx;
margin-top: 2px;
margin-bottom: 7px;
}
.price {
font-size: 20rpx;
color: var(--ui-BG-Main);
margin-bottom: 7px;
}
.btn {
border: 1px solid var(--ui-BG-Main);
border-radius: 40px;
color: var(--ui-BG-Main);
padding: 8rpx 20rpx;
font-size: 24rpx;
}
}
}
}
.svga-box {
position: fixed;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100%;
background-color: black;
.close-btn {
color: #fff;
position: absolute;
z-index: 999999999;
padding: 5px 10px;
left: 0;
right: 0;
bottom: 100rpx;
.bottom-box {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.title {
font-size: 28rpx;
margin-bottom: 5px;
}
.price {
font-size: 24rpx;
}
.btn-box {
display: flex;
align-items: center;
margin-top: 10px;
.btn {
border: 1px solid #fff;
font-size: 28rpx;
padding: 20rpx 110rpx;
border-radius: 40px;
margin: 15px;
color: #fff;
}
.active {
border: 1px solid var(--ui-BG-Main);
background-color: var(--ui-BG-Main);
}
}
}
}
}
.svga-hide {
/* #ifdef MP */
transform: translate(-100%, 0);
/* #endif */
/* #ifndef MP */
display: none;
/* #endif */
}
.svga-show {
/* #ifdef MP */
transform: translate(0, 0);
/* #endif */
/* #ifndef MP */
display: block;
/* #endif */
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<view>
<view class="sticky-box">
<view>
<tui-tabs :tabs="tabList" badgeBgColor="var(--ui-BG-Main)" selectedColor="var(--ui-BG-Main)" sliderBgColor="var(--ui-BG-Main)" :currentTab="currentTab" @change="change"></tui-tabs>
</view>
</view>
</view>
</template>
<script>
import tuiTabs from "@/components/thorui/tui-tabs/tui-tabs.vue"
import sheep from '@/sheep';
export default {
components: {
tuiTabs,
},
props: {
clerk: {
type: Object,
default: {},
},
},
data() {
return {
}
},
computed: {
tabList() {
var tabs = [{
name: '礼物墙'
},{
name: '价格'
}, {
name: '动态',
num: this.clerk.trendNum,
}, {
name: '评价',
num: this.clerk.commentNum,
}];
return tabs;
},
currentTab() {
return sheep.$store('sys').clerkTabIndex;
},
},
created() {
},
methods: {
change(e) {
this.$emit('change', e.index);
},
}
}
</script>
<style lang="scss" scoped>
.sticky-box {
background-color: #fff;
padding: 5px 0;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view class="tn-table-class tn-table" :class="[tableClass]" :style="[tableStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'table-bd',
props: {
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 显示上边框
borderTop: {
type: Boolean,
default: true
},
// 显示右边框
borderRight: {
type: Boolean,
default: false
},
// 显示下边框
borderBottom: {
type: Boolean,
default: false
},
// 显示左边框
borderLeft: {
type: Boolean,
default: true
}
},
computed: {
parentData() {
return [this.borderWidth, this.borderColor]
},
tableClass() {
let clazz = ''
return clazz
},
tableStyle() {
let style = {}
if (this.borderWidth !== '') {
style.borderWidth = this.borderWidth + 'rpx'
}
if (this.borderColor) {
style.borderColor = this.borderColor
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderRight) {
style.borderRightStyle = 'solid'
}
if (this.borderTop) {
style.borderTopStyle = 'solid'
}
if (this.borderBottom) {
style.borderBottomStyle = 'solid'
}
return style
}
},
data() {
return {}
},
created() {
this.children = []
},
watch: {
parentData() {
// 更新子组件的数据
if (this.children.length) {
this.children.map((child) => {
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-table {
box-sizing: border-box;
width: 100%;
border-width: 1rpx;
border-style: none;
border-color: var(--ui-BG-Main);;
}
</style>

View File

@@ -0,0 +1,305 @@
<template>
<view class="tn-td-class tn-td" :class="[tdClass]" :style="[tdStyle]" @tap.stop="handleClick">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'table-td',
options: {
// 在微信小程序中将组件节点渲染为虚拟节点更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true
},
props: {
// 占整个表格的宽度跨度
// [1-24]
span: {
type: Number,
default: 24
},
// 宽度
// 优先级比span高
width: {
type: [String, Number],
default: ''
},
// 高度
height: {
type: [String, Number],
default: ''
},
// 字体加粗
bold: {
type: Boolean,
default: false
},
// 格内边距
padding: {
type: String,
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 左边框
borderLeft: {
type: Boolean,
default: false
},
// 下边框
borderBottom: {
type: Boolean,
default: false
},
// 右边框
borderRight: {
type: Boolean,
default: true
},
// 文字超出隐藏
ellipsis: {
type: Boolean,
default: false
},
// 文本对齐方式
// left center right
textAlign: {
type: String,
default: 'left'
},
// 排列方式
// left center right
alignItems: {
type: String,
default: 'left'
},
// 收缩表格
shrink: {
type: Boolean,
default: true
},
// 铺满剩余空间
grow: {
type: Boolean,
default: false
},
// 隐藏
hidden: {
type: Boolean,
default: false
},
// 固定列数据
fixed: {
type: Boolean,
default: false
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 列数
index: {
type: [String, Number],
default: 0
},
// keys
keys: {
type: [String, Number],
default: ''
}
},
computed: {
tdClass() {
let clazz = ''
clazz += `${this.ellipsis ? 'tn-td--ellipsis' : 'tn-td--normal'}`
/* if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fontColorClass) {
clazz += ` ${this.fontColorClass}`
} */
if (this.alignItems) {
clazz += ` tn-td--${this.alignItems}`
}
if (this.textAlign) {
clazz += ` tn-td__text--${this.textAlign}`
}
if (!this.shrink) {
clazz += ' tn-td--shrink'
}
if (this.grow) {
clazz += ' tn-td--grow'
}
if (this.hidden) {
clazz += ' tn-td--hidden'
}
return clazz
},
tdStyle() {
let style = {}
/* if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
} */
/* if (this.fontColorStyle) {
style.color = this.fontColorStyle
} */
/* if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
} */
style.width = this.getWidth()
if (this.height) {
style.height = this.height + 'rpx';
}
style.fontWeight = this.bold ? 'bold' : 'normal'
if (this.padding) {
style.padding = this.padding
}
if (this.borderWidth !== '' || this.parentData.borderWidthValue !== '') {
style.borderWidth = this.borderWidth !== '' ? this.borderWidth+'rpx' : this.parentData.borderWidthValue + 'rpx'
}
if (this.borderColor || this.parentData.borderColorValue) {
style.borderColor = this.borderColor || this.parentData.borderColorValue
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderRight) {
style.borderRightStyle = 'solid'
}
if (this.borderBottom) {
style.borderBottomStyle = 'solid'
}
if (this.fixed) {
style.zIndex = this.zIndex ? this.zIndex : 1000
}
return style
}
},
data() {
return {
parentData: {
borderColorValue: null,
borderWidthValue: null
}
}
},
created() {
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
// 获取表格宽度
getWidth() {
if (this.width) {
return this.width + 'rpx'
}
return [
'4.16666667%',
'8.33333333%',
'12.5%',
'16.66666667%',
'20.83333333%',
'25%',
'29.16666667%',
'33.33333333%',
'37.5%',
'41.66666667%',
'45.83333333%',
'50%',
'54.16666667%',
'58.33333333%',
'62.5%',
'66.66666667%',
'70.83333333%',
'75%',
'79.16666667%',
'83.33333333%',
'87.5%',
'91.66666667%',
'95.83333333%',
'100%'
][this.span - 1]
},
// 点击事件
handleClick() {
this.$emit('click', {
index: this.index,
key: this.keys
})
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-tr')
}
}
}
</script>
<style lang="scss" scoped>
.tn-td {
box-sizing: border-box;
position: relative;
word-break: break-all;
background-color: transparent;
height: auto;
padding: 12rpx;
border-width: 1rpx;
border-style: none;
border-color: var(--ui-BG-Main);
&--normal {
display: inline-flex;
align-items: center;
}
&--ellipsis {
display: inline-block;
overflow: hidden;
white-space: nowrap !important;
text-overflow: ellipsis;
}
&--shrink {
flex-shrink: 0;
}
&--grow {
flex-grow: 1;
}
&--left {
justify-content: flex-start;
}
&--center {
justify-content: center;
}
&--right {
justify-content: flex-end;
}
&__text {
&--left {
text-align: left;
}
&--center {
text-align: center;
}
&--right {
text-align: right;
}
}
&--hidden {
visibility: hidden;
}
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<view class="tn-tr-class tn-tr" :class="[trClass]" :style="[trStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'table-tr',
options: {
// 在微信小程序中将组件节点渲染为虚拟节点更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true
},
props: {
// 宽度
width: {
type: [String, Number],
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 左边框
borderLeft: {
type: Boolean,
default: false
},
// 上边框
borderTop: {
type: Boolean,
default: false
},
// 换行显示
wrap: {
type: Boolean,
default: false
},
// 固定表格
fixed: {
type: Boolean,
default: false
},
// left偏移值
left: {
type: [String, Number],
default: 0
},
// right偏移值
right: {
type: [String, Number],
default: 0
},
// top偏移值(自定义顶部导航栏时用到)
top: {
type: [String, Number],
default: 0
},
// 外边距
margin: {
type: String,
default: ''
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 行数索引
index: {
type: [String, Number],
default: 0
},
// 参数
params: {
type: String,
default: ''
}
},
computed: {
borderWidthValue() {
return this.borderWidth || this.parentData.borderWidth || ''
},
borderColorValue() {
return this.borderColor || this.parentData.borderColor || ''
},
trClass() {
let clazz = ''
/* if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fontColorClass) {
clazz += ` ${this.fontColorClass}`
} */
if (this.wrap) {
clazz += ' tn-tr--wrap'
}
if (this.fixed) {
clazz += ' tn-tr--fixed'
}
return clazz
},
trStyle() {
let style = {}
if (this.width) {
style.width = this.width + 'rpx'
}
/* if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
} */
if (this.borderWidth !== '' || this.parentData.borderWidth !== '') {
style.borderWidth = this.borderWidth !== '' ? this.borderWidth + 'rpx' : this.parentData.borderWidth + 'rpx'
}
if (this.borderColor || this.parentData.borderColor) {
style.borderColor = this.borderColor || this.parentData.borderColor
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderTop) {
style.borderTopStyle = 'solid'
}
if (this.fixed) {
style.left = this.left ? this.left + 'rpx' : 'auto'
style.right = this.right ? this.right + 'rpx' : 'auto'
style.top = this.top ? this.top + 'rpx' : 'auto'
}
if (this.margin) {
style.margin = this.margin
}
style.zIndex = this.zIndex ? this.zIndex : 1000
return style
}
},
data() {
return {
parentData: {
borderColor: null,
borderWidth: null
}
}
},
watch: {
parentData: {
handler() {
// 更新子组件的数据
if (this.children.length) {
this.children.map((child) => {
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
},
deep: true
}
},
created() {
this.children = []
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
handleClick() {
this.$emit('click', {
index: this.index,
params: this.params
})
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-table')
}
}
}
</script>
<style lang="scss" scoped>
.tn-tr {
width: 100%;
display: flex;
box-sizing: border-box;
background-color: #FFFFFF;
border-width: 1rpx;
border-style: none none solid none;
border-color: var(--ui-BG-Main);;
&--wrap {
flex-wrap: wrap;
}
&--fixed {
position: fixed;
}
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<view>
<table-bd style="width: 100%;">
<table-tr>
<table-td :span="18" height="80rpx" :fontSize="30" :bold="true" fontColor="#01BEFF" alignItems="center">服务项目</table-td>
<table-td :span="6" height="80rpx" :fontSize="30" :bold="true" fontColor="#01BEFF" alignItems="center">价格</table-td>
</table-tr>
<block v-for="(tb,t) in tableList">
<table-tr>
<table-td :span="6" alignItems="center">{{tb.name}}</table-td>
<block v-if="tb.goodsList">
<table-td :span="12" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.goodsList">
<table-tr :borderWidth="t2 == tb.goodsList.length-1 ? 0 : 1">
<table-td :span="24" :borderRight="false" alignItems="center">{{tb2.name}}</table-td>
</table-tr>
</block>
</table-td>
<table-td :span="6" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.goodsList">
<table-tr :borderWidth="t2 == tb.goodsList.length-1 ? 0 : 1">
<table-td :span="24" borderWidth="1rpx" :borderRight="false" alignItems="center">{{ fen2yuan(tb2.price) }}</table-td>
</table-tr>
</block>
</table-td>
</block>
<block v-if="tb.categoryList">
<table-td :span="12" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.categoryList">
<table-tr :borderWidth="t2 == tb.categoryList.length-1 ? 0 : 1">
<table-td :span="12" borderWidth="1rpx" alignItems="center">{{tb2.name}}</table-td>
<table-td :span="12" :ellipsis="true" :borderRight="false" padding="0">
<block v-for="(tb3,t3) in tb2.goodsList">
<table-tr :borderWidth="t3 == tb2.goodsList.length-1 ? 0 : 1">
<table-td :span="24" :borderRight="false" alignItems="center">{{tb3.name}}</table-td>
</table-tr>
</block>
</table-td>
</table-tr>
</block>
</table-td>
<table-td :span="6" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.categoryList">
<block v-for="(tb3,t3) in tb2.goodsList">
<table-tr :borderWidth="t2 == tb.categoryList.length-1 && t3 == tb2.goodsList.length-1 ? 0 : 1">
<table-td :span="24" borderWidth="1rpx" :borderRight="false" alignItems="center">{{ fen2yuan(tb3.price) }}</table-td>
</table-tr>
</block>
</block>
</table-td>
</block>
</table-tr>
</block>
</table-bd>
</view>
</template>
<script>
import TableBd from '@/pages/clerk/detail/components/table-bd.vue';
import TableTd from '@/pages/clerk/detail/components/table-td.vue';
import TableTr from '@/pages/clerk/detail/components/table-tr.vue';
export default {
components: {
TableBd,
TableTd,
TableTr,
},
props: {
tableList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
methods: {
fen2yuan(price) {
return (price / 100.0).toFixed(0)
},
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,270 @@
<template>
<view class="user-box" :style="`background-size: contain;background-image: url(${clerk.avatar});`">
<view class="info-box" :style="{ paddingTop: paddingTop + 20 + 'px'}">
<view class="avatar-box">
<view class="avatar-span">
<u-image width="100%" height="100%" shape="circle" :src="clerk.avatar"></u-image>
<view class="fans-btn" @click="fans(clerk)">
<u-icon v-if="clerk.isFans" color="var(--ui-BG-Main)" name="checkmark-circle-fill" size="46"></u-icon>
<u-icon v-else color="var(--ui-BG-Main)" name="plus-circle-fill" size="46"></u-icon>
</view>
</view>
<view class="right-box">
<view class="nickname-box">
<view class="nickname">{{clerk.nickname}}</view>
<view class="icon">
<u-icon name="wode_duanwei" size="36" custom-prefix="iconfont"></u-icon>
<text>{{clerkLevel.name}}</text>
</view>
</view>
<view class="online" v-if="clerk.onlineStatus">可接单</view>
<view class="online" v-else>休息中</view>
</view>
</view>
<view class="sex-box">
<view class="span" v-if="clerk.sex == 1">
<u-icon name="ziyuan2" color="#f898c5" size="34" custom-prefix="iconfont"></u-icon>
</view>
<view class="span" v-if="clerk.sex == 0">
<u-icon name="ziyuan3" color="#0081ff" size="34" custom-prefix="iconfont"></u-icon>
</view>
<view class="span">{{clerk.age}}</view>
<!-- <view class="span">{{clerk.city}}</view> -->
</view>
<view class="sign-box">{{clerk.intro}}</view>
<view class="voice-box" v-if="clerk.sound">
<view class="voice-span">
<voice-play :sec="clerk.soundTime" :src="clerk.sound"></voice-play>
</view>
<!-- <view class="span">全声线</view>
<view class="span">御姐音</view>
<view class="span">夹子音</view> -->
</view>
<view class="tag-list">
<view class="tag-box">
<span class="tag" v-for="(categoryName,index) in clerk.categoryNameList">{{categoryName}}</span>
</view>
</view>
<!-- <view class="tags-box">
<view class="span">搞笑</view>
<view class="span">风骚走位</view>
<view class="span">乖巧懂事🍑 极致体验</view>
</view> -->
<photo-box v-if="clerk.albums" :dataList="clerk.albums.split(',')"></photo-box>
</view>
</view>
</template>
<script>
import VoicePlay from '@/pages/clerk/detail/components/voicePlay.vue';
import PhotoBox from '@/pages/clerk/detail/components/photoBox.vue';
import sheep from '@/sheep';
import TrendApi from '@/sheep/api/worker/trend';
export default {
components: {
VoicePlay,
PhotoBox,
},
props: {
paddingTop: {
type: Number,
default: 0,
},
clerk: {
type: Object,
default: {},
},
clerkLevel: {
type: Object,
default: {},
},
},
data() {
return {
photo: 'http://cos.duopei.feiniaowangluo.com/34/clerk/1707236152609pJKyvtedS9.jpg,http://cos.duopei.feiniaowangluo.com/34/clerk/1720278125689P34fP9MU1c.jpg?imageView2/1/w/200/h/200',
}
},
methods: {
fans(e) {
TrendApi.createClerkFans({
workerClerkId: e.id,
}).then((res) => {
if(res){
if(e.isFans){
sheep.$helper.toast('取消关注');
e.isFans = false;
}else{
sheep.$helper.toast('关注成功');
e.isFans = true;
}
}
});
},
}
}
</script>
<style lang="scss" scoped>
.info-box {
backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 0.5);
padding: 15px;
.avatar-box {
display: flex;
align-items: center;
.avatar-span {
border-radius: 100%;
width: 140rpx;
height: 140rpx;
border: 2px solid #fff;
position: relative;
.fans-btn {
position: absolute;
right: 0;
bottom: 0;
}
}
.right-box {
color: #fff;
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
margin-left: 10px;
.nickname {
font-size: 34rpx;
font-weight: bold;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 320rpx;
}
.icon {
background-color: #ff5ebd;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-weight: bold;
color: #fff;
font-size: 22rpx;
line-height: 22rpx;
padding: 5px 5px;
margin-top: 10px;
}
.online {
border-bottom: 1px solid #fff;
font-size: 28rpx;
}
}
}
.sex-box {
color: #fff;
display: flex;
align-items: center;
line-height: 30px;
.span {
margin-right: 5px;
font-size: 26rpx;
}
}
.sign-box {
color: #cfcfcf;
font-size: 28rpx;
margin-top: 10px;
margin-bottom: 10px;
}
.voice-box {
display: flex;
flex-wrap: wrap;
align-items: center;
.voice-span {
margin-bottom: 10px;
margin-right: 10px;
}
.span {
padding: 10rpx 20rpx;
background-color: rgba(132, 167, 164, .81);
border-radius: 20px;
font-size: 24rpx;
margin-bottom: 10px;
margin-right: 10px;
color: #fff;
}
}
.tags-box {
display: flex;
flex-wrap: wrap;
align-items: center;
margin: 5px 0;
.span {
font-size: 20rpx;
color: #fff;
background-color: #000000;
border-radius: 999px;
padding: 3px 10px;
margin: 0 10px 10px 0;
}
}
.tag-list {
display: flex;
margin: 5px 0;
}
.tag-box {
display: inline-block;
align-items: center;
margin-bottom: 5px;
background-color: #eef2f26e;
border-radius: 3px;
color: #fff;
font-size: 12px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
padding: 5px 0;
.tag {
white-space: nowrap;
padding: 5px 5px;
position: relative;
}
.tag:after {
content: ' ';
border-left: 1px solid #fff;
display: inline-block;
height: 10px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
}
.tag:last-child:after{
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,183 @@
<template>
<view class="voice-btn" @click="playBtn" :class="play?'audioplaying':''">
<img class="play-btn" src="@/static/images/audio.png"></img>
<view class="audio-box">
<view class="audio1 wave1"></view>
<view class="audio2 wave2"></view>
<view class="audio3 wave3"></view>
<view class="audio2 wave2"></view>
<view class="audio1 wave1"></view>
<view class="audio2 wave2"></view>
<view class="audio3 wave3"></view>
<view class="audio2 wave2"></view>
<view class="audio1 wave1"></view>
<view class="audio2 wave2"></view>
<view class="audio3 wave3"></view>
<view class="audio2 wave2"></view>
</view>
<view>{{sec}}"</view>
</view>
</template>
<script>
const audio = uni.createInnerAudioContext();
export default {
emits: ['playAudio'],
props: {
src: {
type: String,
default: ''
},
sec: {
type: Number,
default: 0
},
},
data() {
return {
play: false,
};
},
methods: {
playBtn() {
if(this.play){
this.play = false;
audio.stop();
return;
}
this.play = true;
//语音自然播放结束
audio.onEnded((res) => {
this.play = false;
});
audio.src = this.src;
audio.play();
},
}
};
</script>
<style lang="scss" scoped>
view{
box-sizing: border-box;
}
.voice-btn {
background-color: #3cc9a4;
width: 140rpx;
border-radius: 20px;
padding: 10rpx 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 20rpx;
}
.play-btn {
width: 24rpx;
height: 24rpx;
}
.audio-box {
height: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.audio1{
margin-right: 2rpx;
width: 1px;
height: 33.3%;
border-radius: 50px;
background-color: #fff;
}
.audio2{
margin-right: 2rpx;
width: 1px;
height: 66.6%;
border-radius: 50px;
background-color: #fff;
}
.audio3{
margin-right: 2rpx;
width: 1px;
height: 100%;
border-radius: 50px;
background-color: #fff;
}
.audioplaying .wave1 {
animation: wave1 1s linear 0s infinite;
}
.audioplaying .wave2 {
animation: wave2 1s linear 0s infinite;
}
.audioplaying .wave3 {
animation: wave3 1s linear 0s infinite;
}
@keyframes wave1{
0% {
width: 1px;
height: 33%;
}
25% {
width: 1px;
height: 66%;
}
50% {
width: 1px;
height: 100%;
}
75% {
width: 1px;
height: 66%;
}
100% {
width: 1px;
height: 33%;
}
}
@keyframes wave2{
0% {
width: 1px;
height: 66%;
}
25% {
width: 1px;
height: 33%;
}
50% {
width: 1px;
height: 66%;
}
75% {
width: 1px;
height: 100%;
}
100% {
width: 1px;
height: 66%;
}
}
@keyframes wave3{
0% {
width: 1px;
height: 100%;
}
50% {
width: 1px;
height: 33%;
}
100% {
width: 1px;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<view class="container page-app theme-light main-green font-1">
<layout ref="skill" title="选人下单">
</layout>
<s-auth-modal />
<s-menu-tools />
</view>
</template>
<script>
import layout from '@/pages/clerk/detail/components/layout.vue';
import sheep from '@/sheep';
export default {
components: {
layout,
},
data() {
return {
}
},
// 分享小程序
onShareAppMessage(res) {
return {
title: this.shareInfo.title,
imageUrl: this.shareInfo.image,
};
},
onShareTimeline() {
return {
title: this.shareInfo.title,
imageUrl: this.shareInfo.image,
}
},
onLoad(options) {
this.$nextTick(() => {
this.$refs.skill.initData(options);
});
},
computed: {
shareInfo() {
return sheep.$platform.share.getShareInfo();
},
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.container {
background-color: #fff;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@@ -0,0 +1,82 @@
<!-- 虚拟列表演示(不使用内置列表)(vue) -->
<!-- 写法较简单在页面中对当前需要渲染的虚拟列表数据进行for循环在vue3中兼容性良好 -->
<!-- 在各平台兼容性请查阅https://z-paging.zxlee.cn/module/virtual-list.html -->
<template>
<view class="content">
<!-- 如果页面中的cell高度是固定不变的则不需要设置cell-height-mode如果页面中高度是动态改变的则设置cell-height-mode="dynamic" -->
<!-- 原先的v-model修改为@virtualListChange="virtualListChange"并赋值处理后的虚拟列表 -->
<z-paging ref="paging" :auto="true" use-virtual-list :force-close-inner-list="true" :paging-style="{ paddingTop: 0 + 'px', paddingBottom: paddingBottom + 'rpx' }" cell-height-mode="dynamic" @scroll="scroll" @virtualListChange="virtualListChange" @query="queryList">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中如果需要跟着滚动则不要设置slot="top" -->
<template #top>
<su-navbar :title="title" statusBar></su-navbar>
</template>
<!-- :id="`zp-id-${item.zp_index}`":key="item.zp_index" 必须写必须写 -->
<!-- 这里for循环的index不是数组中真实的index了请使用item.zp_index获取真实的index -->
<order-list :virtualList="virtualList"></order-list>
</z-paging>
</view>
</template>
<script>
import OrderList from '@/pages/clerk/fans/components/orderList.vue';
import ClerkApi from '@/sheep/api/worker/clerk';
import {
fen2yuan,
} from '@/sheep/hooks/useGoods';
export default {
components: {
OrderList,
},
props: {
title: {
type: String,
default: '我收藏的店员',
},
},
data() {
return {
// 虚拟列表数组,通过@virtualListChange监听获得最新数组
virtualList: [],
scrollTop: 0,
paddingTop: 0,
paddingBottom: 0,
height: 0,
}
},
methods: {
scroll(e) {
this.scrollTop = e.detail.scrollTop;
},
// 监听虚拟列表数组改变并赋值给virtualList进行重新渲染
virtualListChange(vList) {
this.virtualList = vList;
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
const params = {
pageNo: pageNo,
pageSize: pageSize,
}
ClerkApi.getClerkFansPage(params).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
},
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,212 @@
<template>
<view :id="`zp-id-${item.zp_index}`" :key="item.zp_index" v-for="(item,index) in orderList" class="order-card">
<view class="main-box" :class="index == orderList.length-1 ? '':'bt'">
<view @click="detail(item)">
<u-avatar mode="square" size="170" :src="item.avatar"></u-avatar>
</view>
<view class="right-box">
<view class="nickname-box">
<view class="nickname">{{item.nickname}}</view>
<view class="oline" v-if="item.onlineStatus">
<tui-badge :scaleRatio="0.8" type="green" dot></tui-badge>
<text class="text">在线</text>
</view>
<view class="oline un" v-else>
<tui-badge :scaleRatio="0.8" type="gray" dot></tui-badge>
<text class="text">休息中</text>
</view>
</view>
<view class="city">{{item.city}}</view>
<view class="sex-box">
<view class="sex-tag">
<text class="tag" v-if="item.sex == 1"></text>
<text class="tag" v-if="item.sex == 0"></text>
<text class="tag">{{item.age}}</text>
</view>
<view class="fans-box" @click="fans(item)">
<view class="fans">{{item.fansNum}}人收藏</view>
<u-icon v-if="item.fans" name="star-fill" size="36" color="var(--ui-BG-Main)"></u-icon>
<u-icon v-else name="star" size="36" color="#ddd"></u-icon>
</view>
</view>
<view class="info">{{item.intro}}</view>
</view>
</view>
</view>
</template>
<script>
import TuiBadge from "@/components/thorui/tui-badge/tui-badge.vue";
import TrendApi from '@/sheep/api/worker/trend';
import sheep from '@/sheep';
export default {
components: {
TuiBadge,
},
props: {
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
computed: {
orderList() {
return this.virtualList;
},
},
methods: {
fans(e) {
TrendApi.createClerkFans({
workerClerkId: e.workerClerkId,
}).then((res) => {
if(res){
if(e.fans){
sheep.$helper.toast('取消收藏');
e.fans = false;
}else{
sheep.$helper.toast('收藏成功');
e.fans = true;
}
}
});
},
detail(e) {
this.$u.route({
url: 'pages/clerk/detail/index',
params: {
id: e.workerClerkId,
}
});
},
}
}
</script>
<style lang="scss" scoped>
.order-card {
padding: 0 10px;
background-color: #fff;
display: flex;
flex-direction: column;
flex: 1;
.main-box {
display: flex;
padding: 10px 0;
.right-box {
display: flex;
justify-content: center;
flex: 1;
margin-left: 10px;
flex-direction: column;
.nickname-box {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.nickname {
font-size: 28rpx;
}
.oline {
display: flex;
align-items: center;
.text {
font-size: 24rpx;
margin-left: 10rpx;
}
}
.un {
color: rgb(100, 101, 102);
}
}
.city {
font-size: 24rpx;
margin-bottom: 10rpx;
}
.sex-box {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18rpx;
.sex-tag {
font-size: 24rpx;
.tag {
margin-right: 20rpx;
}
}
.fans-box {
display: flex;
align-items: center;
.fans {
font-size: 22rpx;
color: rgb(100, 101, 102);
margin-right: 10rpx;
}
}
}
.info {
font-size: 24rpx;
color: rgb(100, 101, 102);
line-height: 22rpx;
margin-bottom: 10rpx;
display: flex;
align-items: center;
}
}
}
.bt {
border-bottom: 1px solid #fbf0f0;
}
.bottom-box {
display: flex;
justify-content: flex-end;
.btn-box {
display: flex;
align-items: center;
}
.btn {
background-color: #ddd;
color: rgb(100, 101, 102);
font-size: 22rpx;
border-radius: 40px;
padding: 5px 10px;
margin-left: 10px;
min-width: 140rpx;
display: flex;
justify-content: center;
}
.active {
background-color: var(--ui-BG-Main);
color: #fff;
}
}
}
</style>

40
pages/clerk/fans/list.vue Normal file
View File

@@ -0,0 +1,40 @@
<template>
<view class="page-app theme-light main-green font-1">
<layout ref="order"></layout>
<s-menu-tools />
<s-auth-modal />
</view>
</template>
<script>
import layout from '@/pages/clerk/fans/components/layout.vue';
export default {
components: {
layout,
},
props: {
},
data() {
return {
}
},
onLoad(options) {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,125 @@
<!-- 分销账户展示基本统计信息 -->
<template>
<view class="account-card">
<view class="account-card-box">
<view class="ss-flex ss-row-between card-box-header">
<view class="ss-flex">
<view class="header-title ss-m-r-16">账户信息</view>
<button
class="ss-reset-button look-btn ss-flex"
@tap="state.showMoney = !state.showMoney"
>
<uni-icons
:type="state.showMoney ? 'eye-filled' : 'eye-slash-filled'"
color="#A57A55"
size="20"
/>
</button>
</view>
<view class="ss-flex" @tap="sheep.$router.go('/pages/commission/wallet')">
<view class="header-title ss-m-r-4">查看明细</view>
<text class="cicon-play-arrow" />
</view>
</view>
<!-- 收益 -->
<view class="card-content ss-flex">
<view class="ss-flex-1 ss-flex-col ss-col-center">
<view class="item-title">当前佣金()</view>
<view class="item-detail">
{{ state.showMoney ? fen2yuan(state.summary.brokeragePrice || 0) : '***' }}
</view>
</view>
<view class="ss-flex-1 ss-flex-col ss-col-center">
<view class="item-title">昨天的佣金()</view>
<view class="item-detail">
{{ state.showMoney ? fen2yuan(state.summary.yesterdayPrice || 0) : '***' }}
</view>
</view>
<view class="ss-flex-1 ss-flex-col ss-col-center">
<view class="item-title">累计已提()</view>
<view class="item-detail">
{{ state.showMoney ? fen2yuan(state.summary.withdrawPrice || 0) : '***' }}
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive, onMounted } from 'vue';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const userInfo = computed(() => sheep.$store('user').userInfo);
const state = reactive({
showMoney: false,
summary: {},
});
onMounted(async () => {
let { code, data } = await BrokerageApi.getBrokerageUserSummary();
if (code === 0) {
state.summary = data || {}
}
});
</script>
<style lang="scss" scoped>
.account-card {
width: 694rpx;
margin: 0 auto;
padding: 2rpx;
background: linear-gradient(180deg, #ffffff 0.88%, #fff9ec 100%);
border-radius: 12rpx;
z-index: 3;
position: relative;
.account-card-box {
background: #ffefd6;
.card-box-header {
padding: 0 30rpx;
height: 72rpx;
box-shadow: 0px 2px 6px #f2debe;
.header-title {
font-size: 24rpx;
font-weight: 500;
color: #a17545;
line-height: 30rpx;
}
.cicon-play-arrow {
color: #a17545;
font-size: 24rpx;
line-height: 30rpx;
}
}
.card-content {
height: 190rpx;
background: #fdfae9;
.item-title {
font-size: 24rpx;
font-weight: 500;
color: #cba67e;
line-height: 30rpx;
margin-bottom: 24rpx;
}
.item-detail {
font-size: 36rpx;
font-family: OPPOSANS;
font-weight: bold;
color: #692e04;
line-height: 30rpx;
}
}
}
}
</style>

View File

@@ -0,0 +1,160 @@
<!-- 提现方式的 select 组件 -->
<template>
<su-popup :show="show" class="ss-checkout-counter-wrap" @close="hideModal">
<view class="ss-modal-box bg-white ss-flex-col">
<view class="modal-header ss-flex-col ss-col-left">
<text class="modal-title ss-m-b-20">选择提现方式</text>
</view>
<view class="modal-content ss-flex-1 ss-p-b-100">
<radio-group @change="onChange">
<label
class="container-list ss-p-l-34 ss-p-r-24 ss-flex ss-col-center ss-row-center"
v-for="(item, index) in typeList"
:key="index"
>
<view class="container-icon ss-flex ss-m-r-20">
<image :src="sheep.$url.static(item.icon)" />
</view>
<view class="ss-flex-1">{{ item.title }}</view>
<radio
:value="item.value"
color="var(--ui-BG-Main)"
:checked="item.value === state.currentValue"
:disabled="!methods.includes(parseInt(item.value))"
/>
</label>
</radio-group>
</view>
<view class="modal-footer ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button save-btn" @tap="onConfirm">确定</button>
</view>
</view>
</su-popup>
</template>
<script setup>
import { reactive } from 'vue';
import sheep from '@/sheep';
const props = defineProps({
modelValue: {
type: Object,
default() {},
},
show: {
type: Boolean,
default: false,
},
methods: { // 开启的提现方式
type: Array,
default: [],
},
});
const emits = defineEmits(['update:modelValue', 'change', 'close']);
const state = reactive({
currentValue: '',
});
const typeList = [
{
icon: 'https://file.sheepjs.com/static/img/shop/pay/wallet.png', // TODO 芋艿:后续给个 icon
title: '钱包余额',
value: '1',
},
{
icon: '/static/img/shop/pay/bank.png',
title: '银行卡转账',
value: '2',
},
{
icon: '/static/img/shop/pay/wechat.png',
title: '微信零钱',
value: '3',
},
{
icon: '/static/img/shop/pay/alipay.png',
title: '支付宝账户',
value: '4',
}
];
function onChange(e) {
state.currentValue = e.detail.value;
}
const onConfirm = async () => {
if (state.currentValue === '') {
sheep.$helper.toast('请选择提现方式');
return;
}
// 赋值
emits('update:modelValue', {
type: state.currentValue
});
// 关闭弹窗
emits('close');
};
const hideModal = () => {
emits('close');
};
</script>
<style lang="scss" scoped>
.ss-modal-box {
border-radius: 30rpx 30rpx 0 0;
max-height: 1000rpx;
.modal-header {
position: relative;
padding: 60rpx 40rpx 40rpx;
.modal-title {
font-size: 32rpx;
font-weight: bold;
}
.close-icon {
position: absolute;
top: 10rpx;
right: 20rpx;
font-size: 46rpx;
opacity: 0.2;
}
}
.modal-content {
overflow-y: auto;
.container-list {
height: 96rpx;
border-bottom: 2rpx solid rgba(#dfdfdf, 0.5);
font-size: 28rpx;
font-weight: 500;
color: #333333;
.container-icon {
width: 36rpx;
height: 36rpx;
}
}
}
.modal-footer {
height: 120rpx;
.save-btn {
width: 710rpx;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
}
}
image {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,101 @@
<!-- 分销权限弹窗再没有权限时进行提示 -->
<template>
<su-popup
:show="state.show"
type="center"
round="10"
@close="state.show = false"
:isMaskClick="false"
maskBackgroundColor="rgba(0, 0, 0, 0.7)"
>
<view class="notice-box">
<view class="img-wrap">
<image
class="notice-img"
:src="sheep.$url.static('/static/img/shop/commission/forbidden.png')"
mode="aspectFill"
/>
</view>
<view class="notice-title"> 抱歉您没有分销权限 </view>
<view class="notice-detail"> 该功能暂不可用 </view>
<button
class="ss-reset-button notice-btn ui-Shadow-Main ui-BG-Main-Gradient"
@tap="sheep.$router.back()"
>
知道了
</button>
<button class="ss-reset-button back-btn" @tap="sheep.$router.back()"> 返回 </button>
</view>
</su-popup>
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { reactive } from 'vue';
import BrokerageApi from '@/sheep/api/trade/brokerage';
const state = reactive({
show: false,
});
onShow(async () => {
// 读取是否有分销权限
const { code, data } = await BrokerageApi.getBrokerageUser();
if (code === 0 && !data?.brokerageEnabled) {
state.show = true;
}
});
</script>
<style lang="scss" scoped>
.notice-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #fff;
width: 612rpx;
min-height: 658rpx;
background: #ffffff;
padding: 30rpx;
border-radius: 20rpx;
.img-wrap {
margin-bottom: 50rpx;
.notice-img {
width: 180rpx;
height: 170rpx;
}
}
.notice-title {
font-size: 35rpx;
font-weight: bold;
color: #333;
margin-bottom: 28rpx;
}
.notice-detail {
font-size: 28rpx;
font-weight: 400;
color: #999999;
line-height: 36rpx;
margin-bottom: 50rpx;
}
.notice-btn {
width: 492rpx;
line-height: 70rpx;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
margin-bottom: 10rpx;
}
.back-btn {
width: 492rpx;
line-height: 70rpx;
font-size: 28rpx;
font-weight: 500;
color: var(--ui-BG-Main-gradient);
background: none;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<!-- 分销商信息 -->
<template>
<!-- 用户资料 -->
<view class="user-card ss-flex ss-col-bottom">
<view class="card-top ss-flex ss-row-between">
<view class="ss-flex">
<view class="head-img-box">
<image class="head-img" :src="sheep.$url.cdn(userInfo.avatar)" mode="aspectFill"></image>
</view>
<view class="ss-flex-col">
<view class="user-name">{{ userInfo.nickname }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive } from 'vue';
const userInfo = computed(() => sheep.$store('user').userInfo);
const headerBg = sheep.$url.css('/static/img/shop/commission/background.png');
const state = reactive({
showMoney: false,
});
</script>
<style lang="scss" scoped>
// 用户资料卡片
.user-card {
width: 690rpx;
margin: -88rpx 20rpx 0 20rpx;
padding-top: 138rpx;
background: v-bind(headerBg) no-repeat;
background-size: 100% 100%;
.head-img-box {
margin-right: 20rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
position: relative;
background: #fce0ad;
.head-img {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.card-top {
box-sizing: border-box;
padding-bottom: 34rpx;
.user-name {
font-size: 32rpx;
font-weight: bold;
color: #692e04;
line-height: 30rpx;
margin-bottom: 20rpx;
}
.log-btn {
width: 84rpx;
height: 42rpx;
border: 2rpx solid rgba(#ffffff, 0.33);
border-radius: 21rpx;
font-size: 22rpx;
font-weight: 400;
color: #ffffff;
margin-bottom: 20rpx;
}
.look-btn {
color: #fff;
width: 40rpx;
height: 40rpx;
}
}
.user-info-box {
.tag-box {
background: #ff6000;
border-radius: 18rpx;
line-height: 36rpx;
.tag-img {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
margin-left: -2rpx;
}
.tag-title {
font-size: 24rpx;
padding: 0 10rpx;
font-weight: 500;
line-height: 36rpx;
color: #fff;
}
}
}
}
</style>

View File

@@ -0,0 +1,181 @@
<!-- 分销首页明细列表 -->
<template>
<view class="distribution-log-wrap">
<view class="header-box">
<image class="header-bg" :src="sheep.$url.static('/static/img/shop/commission/title2.png')" />
<view class="ss-flex header-title">
<view class="title">实时动态</view>
<text class="cicon-forward" />
</view>
</view>
<scroll-view
scroll-y="true"
@scrolltolower="loadmore"
class="scroll-box log-scroll"
scroll-with-animation="true"
>
<view v-if="state.pagination.list">
<view
class="log-item-box ss-flex ss-row-between"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="log-item-wrap">
<view class="log-item ss-flex ss-ellipsis-1 ss-col-center">
<view class="ss-flex ss-col-center">
<image
class="log-img"
:src="sheep.$url.static('/static/img/shop/avatar/notice.png')"
mode="aspectFill"
/>
</view>
<view class="log-text ss-ellipsis-1">
{{ item.title }} {{ fen2yuan(item.price) }}
</view>
</view>
</view>
<text class="log-time">{{ dayjs(item.createTime).fromNow() }}</text>
</view>
</view>
<!-- 加载更多 -->
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
color="#333333"
@tap="loadmore"
/>
</scroll-view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { reactive } from 'vue';
import _ from 'lodash-es';
import dayjs from 'dayjs';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../../sheep/hooks/useGoods';
const state = reactive({
loadStatus: '',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
});
async function getLog() {
state.loadStatus = 'loading';
const { code, data } = await BrokerageApi.getBrokerageRecordPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
getLog();
// 加载更多
function loadmore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getLog();
}
</script>
<style lang="scss" scoped>
.distribution-log-wrap {
width: 690rpx;
margin: 0 auto;
margin-bottom: 20rpx;
border-radius: 12rpx;
z-index: 3;
position: relative;
.header-box {
width: 690rpx;
height: 76rpx;
position: relative;
.header-bg {
width: 690rpx;
height: 76rpx;
}
.header-title {
position: absolute;
left: 20rpx;
top: 24rpx;
}
.title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
line-height: 30rpx;
}
.cicon-forward {
font-size: 30rpx;
font-weight: 400;
color: #ffffff;
line-height: 30rpx;
}
}
.log-scroll {
height: 600rpx;
background: #fdfae9;
padding: 10rpx 20rpx 0;
box-sizing: border-box;
border-radius: 0 0 12rpx 12rpx;
.log-item-box {
margin-bottom: 20rpx;
.log-time {
// margin-left: 30rpx;
text-align: right;
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 400;
color: #c4c4c4;
}
}
.loadmore-wrap {
// line-height: 80rpx;
}
.log-item {
// background: rgba(#ffffff, 0.2);
border-radius: 24rpx;
padding: 6rpx 20rpx 6rpx 12rpx;
.log-img {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 10rpx;
}
.log-text {
max-width: 480rpx;
font-size: 24rpx;
font-weight: 500;
color: #333333;
}
}
}
}
</style>

View File

@@ -0,0 +1,137 @@
<!-- 分销商菜单栏 -->
<template>
<view class="menu-box ss-flex-col">
<view class="header-box">
<image class="header-bg" :src="sheep.$url.static('/static/img/shop/commission/title1.png')" />
<view class="ss-flex header-title">
<view class="title">功能专区</view>
<text class="cicon-forward"></text>
</view>
</view>
<view class="menu-list ss-flex ss-flex-wrap">
<view v-for="(item, index) in state.menuList" :key="index" class="item-box ss-flex-col ss-col-center"
@tap="sheep.$router.go(item.path)">
<image class="menu-icon ss-m-b-10" :src="sheep.$url.static(item.img)" mode="aspectFill"></image>
<view>{{ item.title }}</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { reactive } from 'vue';
const state = reactive({
menuList: [{
img: '/static/img/shop/commission/commission_icon1.png',
title: '我的团队',
path: '/pages/commission/team',
},
{
img: '/static/img/shop/commission/commission_icon2.png',
title: '佣金明细',
path: '/pages/commission/wallet',
},
{
img: '/static/img/shop/commission/commission_icon3.png',
title: '分销订单',
path: '/pages/commission/order',
},
/* {
img: '/static/img/shop/commission/commission_icon4.png',
title: '推广商品',
path: '/pages/commission/goods',
}, */
// {
// img: '/static/img/shop/commission/commission_icon5.png',
// title: '我的资料',
// path: '/pages/commission/apply',
// isAgentFrom: true,
// },
// todo @芋艿:邀请海报需要登录后的个人数据
{
img: '/static/img/shop/commission/commission_icon7.png',
title: '邀请海报',
path: 'action:showShareModal',
},
/* {
img: '/static/img/shop/commission/commission_icon7.png',
title: '推广排行',
path: '/pages/commission/promoter',
},
{
img: '/static/img/shop/commission/commission_icon7.png',
title: '佣金排行',
path: '/pages/commission/commission-ranking',
} */
],
});
</script>
<style lang="scss" scoped>
.menu-box {
margin: 0 auto;
width: 690rpx;
margin-bottom: 20rpx;
margin-top: 20rpx;
border-radius: 12rpx;
z-index: 3;
position: relative;
}
.header-box {
width: 690rpx;
height: 76rpx;
position: relative;
.header-bg {
width: 690rpx;
height: 76rpx;
}
.header-title {
position: absolute;
left: 20rpx;
top: 24rpx;
}
.title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
line-height: 30rpx;
}
.cicon-forward {
font-size: 30rpx;
font-weight: 400;
color: #ffffff;
line-height: 30rpx;
}
}
.menu-list {
padding: 50rpx 0 10rpx 0;
background: #fdfae9;
border-radius: 0 0 12rpx 12rpx;
}
.item-box {
width: 25%;
margin-bottom: 40rpx;
}
.menu-icon {
width: 68rpx;
height: 68rpx;
background: #ffffff;
border-radius: 50%;
}
.menu-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
}
</style>

156
pages/commission/goods.vue Normal file
View File

@@ -0,0 +1,156 @@
<!-- 分销商品列表 -->
<template>
<s-layout title="推广商品" :onShareAppMessage="state.shareInfo">
<view class="goods-item ss-m-20" v-for="item in state.pagination.list" :key="item.id">
<s-goods-item
size="lg"
:img="item.picUrl"
:title="item.name"
:subTitle="item.introduction"
:price="item.price"
:originPrice="item.marketPrice"
priceColor="#333"
@tap="sheep.$router.go('/pages/goods/index', { id: item.id })"
>
<template #rightBottom>
<view class="ss-flex ss-row-between">
<view class="commission-num" v-if="item.brokerageMinPrice === undefined"
>预计佣金计算中</view
>
<view
class="commission-num"
v-else-if="item.brokerageMinPrice === item.brokerageMaxPrice"
>
预计佣金{{ fen2yuan(item.brokerageMinPrice) }}
</view>
<view class="commission-num" v-else>
预计佣金{{ fen2yuan(item.brokerageMinPrice) }} ~
{{ fen2yuan(item.brokerageMaxPrice) }}
</view>
<button
class="ss-reset-button share-btn ui-BG-Main-Gradient"
@tap.stop="onShareGoods(item)"
>
分享赚
</button>
</view>
</template>
</s-goods-item>
</view>
<s-empty
v-if="state.pagination.total === 0"
icon="/static/goods-empty.png"
text="暂无推广商品"
/>
<!-- 加载更多 -->
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import $share from '@/sheep/platform/share';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import { showShareModal } from '@/sheep/hooks/useModal';
import SpuApi from '@/sheep/api/product/spu';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
loadStatus: '',
shareInfo: {},
});
// TODO @puhui999【分享】接入
function onShareGoods(goodsInfo) {
state.shareInfo = $share.getShareInfo(
{
title: goodsInfo.title,
image: sheep.$url.cdn(goodsInfo.image),
desc: goodsInfo.subtitle,
params: {
page: '2',
query: goodsInfo.id,
},
},
{
type: 'goods', // 商品海报
title: goodsInfo.title, // 商品标题
image: sheep.$url.cdn(goodsInfo.image), // 商品主图
price: goodsInfo.price[0], // 商品价格
original_price: goodsInfo.original_price, // 商品原价
},
);
showShareModal();
}
async function getGoodsList() {
state.loadStatus = 'loading';
let { code, data } = await SpuApi.getSpuPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
// 补充分佣金额
data.list.forEach((item) => {
BrokerageApi.getProductBrokeragePrice(item.id).then((res) => {
item.brokerageMinPrice = res.data.brokerageMinPrice;
item.brokerageMaxPrice = res.data.brokerageMaxPrice;
});
});
}
onLoad(() => {
getGoodsList();
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGoodsList();
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.goods-item {
.commission-num {
font-size: 24rpx;
font-weight: 500;
color: $red;
}
.share-btn {
width: 120rpx;
height: 50rpx;
border-radius: 25rpx;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<!-- 分销中心 -->
<template>
<s-layout navbar="inner" class="index-wrap" title="分销中心" :bgStyle="bgStyle" :onShareAppMessage="shareInfo">
<!-- 分销商信息 -->
<commission-info />
<!-- 账户信息 -->
<account-info />
<!-- 菜单栏 -->
<commission-menu />
<!-- 分销记录 -->
<commission-log />
<!-- 权限弹窗 -->
<commission-auth />
</s-layout>
</template>
<script setup>
import { computed } from 'vue';
import { onPageScroll } from '@dcloudio/uni-app';
import commissionInfo from './components/commission-info.vue';
import accountInfo from './components/account-info.vue';
import commissionLog from './components/commission-log.vue';
import commissionMenu from './components/commission-menu.vue';
import commissionAuth from './components/commission-auth.vue';
import sheep from '@/sheep';
const shareInfo = computed(() => {
return sheep.$platform.share.getShareInfo({
params: {
page: '6',
},
}, {
type: 'user',
});
});
const bgStyle = {
backgroundColor: '#F7D598',
};
onPageScroll((e) => {
});
</script>
<style lang="scss" scoped>
:deep(.page-main) {
background-size: 100% 100% !important;
}
</style>

328
pages/commission/order.vue Normal file
View File

@@ -0,0 +1,328 @@
<!-- 分销 - 订单明细 -->
<template>
<s-layout title="分销订单" :class="state.scrollTop ? 'order-warp' : ''" navbar="inner">
<view
class="header-box"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88 + 22) + 'rpx',
paddingTop: Number(statusBarHeight + 108 + 22) + 'rpx',
},
]"
>
<!-- 团队数据总览 -->
<view class="team-data-box ss-flex ss-col-center ss-row-between" style="width: 100%">
<view class="data-card" style="width: 100%">
<view class="total-item" style="width: 100%">
<view class="item-title" style="text-align: center">累计推广订单</view>
<view class="total-num" style="text-align: center">
{{ state.totals }}
</view>
</view>
</view>
</view>
</view>
<!-- tab -->
<su-sticky bgColor="#fff">
<su-tabs
:list="tabMaps"
:scrollable="false"
:current="state.currentTab"
@change="onTabsChange"
>
</su-tabs>
</su-sticky>
<!-- 订单 -->
<view class="order-box">
<view class="order-item" v-for="item in state.pagination.list" :key="item">
<view class="order-header">
<view class="no-box ss-flex ss-col-center ss-row-between">
<text class="order-code">订单编号{{ item.bizId }}</text>
<text class="order-state">
{{ item.status === 0 ? '待结算' : item.status === 1 ? '已结算' : '已取消' }}
( 佣金 {{ fen2yuan(item.price) }} )
</text>
</view>
<view class="order-from ss-flex ss-col-center ss-row-between">
<view class="from-user ss-flex ss-col-center">
<text>{{ item.title }}</text>
</view>
<view class="order-time">
{{ sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}
</view>
</view>
</view>
</view>
<!-- 数据为空 -->
<s-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无订单" />
<!-- 加载更多 -->
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import { onPageScroll } from '@dcloudio/uni-app';
import { resetPagination } from '@/sheep/util';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
onPageScroll((e) => {
state.scrollTop = e.scrollTop <= 100;
});
const state = reactive({
totals: 0, // 累计推广订单(单)
scrollTop: false,
currentTab: 0,
loadStatus: '',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
});
const tabMaps = [
{
name: '全部',
value: 'all',
},
{
name: '待结算',
value: '0', // 待结算
},
{
name: '已结算',
value: '1', // 已结算
},
];
// 切换选项卡
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
getOrderList();
}
// 获取订单列表
async function getOrderList() {
state.loadStatus = 'loading';
let { code, data } = await BrokerageApi.getBrokerageRecordPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
bizType: 1, // 获得推广佣金
status: state.currentTab > 0 ? state.currentTab : undefined,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
if (state.currentTab === 0) {
state.totals = data.total;
}
}
onLoad(() => {
getOrderList();
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getOrderList();
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.header-box {
box-sizing: border-box;
padding: 0 20rpx 20rpx 20rpx;
width: 750rpx;
background: v-bind(headerBg) no-repeat,
linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
background-size: 750rpx 100%;
// 团队信息总览
.team-data-box {
.data-card {
width: 305rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 20rpx;
.total-item {
margin-bottom: 30rpx;
.item-title {
font-size: 24rpx;
font-weight: 500;
color: #999999;
line-height: normal;
margin-bottom: 20rpx;
}
.total-num {
font-size: 38rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
}
.category-num {
font-size: 26rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
}
}
// 直推
.direct-box {
margin-top: 20rpx;
.direct-item {
width: 340rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 20rpx;
box-sizing: border-box;
.item-title {
font-size: 22rpx;
font-weight: 500;
color: #999999;
margin-bottom: 6rpx;
}
.item-value {
font-size: 38rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
}
}
}
// 订单
.order-box {
.order-item {
background: #ffffff;
border-radius: 10rpx;
margin: 20rpx;
.order-footer {
padding: 20rpx;
font-size: 24rpx;
color: #999;
}
.order-header {
.no-box {
padding: 20rpx;
.order-code {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
.order-state {
font-size: 26rpx;
font-weight: 500;
color: var(--ui-BG-Main);
}
}
.order-from {
padding: 20rpx;
.from-user {
font-size: 24rpx;
font-weight: 400;
color: #666666;
.user-avatar {
width: 26rpx;
height: 26rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.user-name {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
}
.order-time {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
}
}
.commission-box {
.name {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
}
.commission-num {
font-size: 30rpx;
font-weight: 500;
color: $red;
font-family: OPPOSANS;
&::before {
content: '¥';
font-size: 22rpx;
}
}
.order-status {
line-height: 30rpx;
padding: 0 10rpx;
border-radius: 30rpx;
margin-left: 20rpx;
font-size: 24rpx;
color: var(--ui-BG-Main);
}
}
}
</style>

File diff suppressed because one or more lines are too long

585
pages/commission/team.vue Normal file
View File

@@ -0,0 +1,585 @@
<!-- 页面 TODO 芋艿该页面的实现代码需要优化包括 js css以及相关的样式设计 -->
<template>
<s-layout title="我的团队" :class="state.scrollTop ? 'team-wrap' : ''" :bgStyle="bgStyle" navbar="inner">
<view class="promoter-list">
<view
class="promoterHeader bg-color"
style="backgroundcolor: #e93323 !important; height: 218rpx; color: #fff"
>
<view class="headerCon acea-row row-between" style="padding: 28px 29px 0 29px">
<view>
<view class="name" style="color: #fff">推广人数</view>
<view>
<text class="num" style="color: #fff">
{{
state.summary.firstBrokerageUserCount + state.summary.secondBrokerageUserCount ||
0
}}
</text>
</view>
</view>
<view class="iconfont icon-tuandui" />
</view>
</view>
<view style="padding: 0 30rpx">
<view class="nav acea-row row-around l1">
<view :class="state.level == 1 ? 'item on' : 'item'" @click="setType(1)">
一级({{ state.summary.firstBrokerageUserCount || 0 }})
</view>
<view :class="state.level == 2 ? 'item on' : 'item'" @click="setType(2)">
二级({{ state.summary.secondBrokerageUserCount || 0 }})
</view>
</view>
<view
class="search acea-row row-between-wrapper"
style="display: flex; height: 100rpx; align-items: center"
>
<view class="input">
<input
placeholder="点击搜索会员名称"
v-model="state.nickname"
confirm-type="search"
name="search"
@confirm="submitForm"
/>
</view>
<image
src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/3de207ef9b45973cea9e050ae17a4e411769bb57cd7e4050381678c23776488b.png"
mode=""
style="width: 60rpx; height: 64rpx"
@click="submitForm"
/>
</view>
<view class="list">
<view class="sortNav acea-row row-middle" style="display: flex; align-items: center">
<view
class="sortItem"
@click="setSort('userCount', 'asc')"
v-if="sort === 'userCountDESC'"
>
团队排序
<!-- TODO 芋艿看看怎么从项目里拿出去 -->
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('userCount', 'desc')"
v-else-if="sort === 'userCountASC'"
>
团队排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('userCount', 'desc')" v-else>
团队排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('price', 'asc')" v-if="sort === 'priceDESC'">
金额排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('price', 'desc')"
v-else-if="sort === 'priceASC'"
>
金额排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('price', 'desc')" v-else>
金额排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('orderCount', 'asc')"
v-if="sort === 'orderCountDESC'"
>
订单排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('orderCount', 'desc')"
v-else-if="sort === 'orderCountASC'"
>
订单排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('orderCount', 'desc')" v-else>
订单排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
</view>
<block v-for="(item, index) in state.pagination.list" :key="index">
<view class="item acea-row row-between-wrapper" style="display: flex">
<view
class="picTxt acea-row row-between-wrapper"
style="display: flex; align-items: center"
>
<view class="pictrue">
<image :src="item.avatar" />
</view>
<view class="text">
<view class="name line1">{{ item.nickname }}</view>
<view>
加入时间:
{{ sheep.$helper.timeFormat(item.brokerageTime, 'yyyy-mm-dd hh:MM:ss') }}
</view>
</view>
</view>
<view
class="right"
style="
justify-content: center;
flex-direction: column;
display: flex;
margin-left: auto;
"
>
<view>
<text class="num font-color">{{ item.brokerageUserCount || 0 }} </text>
</view>
<view>
<text class="num">{{ item.orderCount || 0 }}</text
></view
>
<view>
<text class="num">{{ item.brokeragePrice || 0 }}</text
>
</view>
</view>
</view>
</block>
<block v-if="state.pagination.list.length === 0">
<s-empty icon="/static/data-empty.png" text="暂无推广人数"></s-empty>
</block>
</view>
</view>
</view>
<!-- <home></home> -->
<!-- <view class="header-box" :style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]">
<view v-if="userInfo.parent_user" class="referrer-box ss-flex ss-col-center">
推荐人
<image class="referrer-avatar ss-m-r-10" :src="sheep.$url.cdn(userInfo.parent_user.avatar)"
mode="aspectFill">
</image>
{{ userInfo.parent_user.nickname }}
</view>
<view class="team-data-box ss-flex ss-col-center ss-row-between">
<view class="data-card">
<view class="total-item">
<view class="item-title">团队总人数</view>
<view class="total-num">
{{ (state.summary.firstBrokerageUserCount+ state.summary.secondBrokerageUserCount)|| 0 }}
</view>
</view>
<view class="category-item ss-flex">
<view class="ss-flex-1">
<view class="item-title">一级成员</view>
<view class="category-num">{{ state.summary.firstBrokerageUserCount || 0 }}</view>
</view>
<view class="ss-flex-1">
<view class="item-title">二级成员</view>
<view class="category-num">{{ state.summary.secondBrokerageUserCount || 0 }}</view>
</view>
</view>
</view>
<view class="data-card">
<view class="total-item">
<view class="item-title">团队分销商人数</view>
<view class="total-num">{{ agentInfo?.child_agent_count_all || 0 }}</view>
</view>
<view class="category-item ss-flex">
<view class="ss-flex-1">
<view class="item-title">一级分销商</view>
<view class="category-num">{{ agentInfo?.child_agent_count_1 || 0 }}</view>
</view>
<view class="ss-flex-1">
<view class="item-title">二级分销商</view>
<view class="category-num">{{ agentInfo?.child_agent_count_2 || 0 }}</view>
</view>
</view>
</view>
</view>
</view>
<view class="list-box">
<uni-list :border="false">
<uni-list-chat v-for="item in state.pagination.data" :key="item.id" :avatar-circle="true"
:title="item.nickname" :avatar="sheep.$url.cdn(item.avatar)"
:note="filterUserNum(item.agent?.child_user_count_1)">
<view class="chat-custom-right">
<view v-if="item.avatar" class="tag-box ss-flex ss-col-center">
<image class="tag-img" :src="sheep.$url.cdn(item.avatar)" mode="aspectFill">
</image>
<text class="tag-title">{{ item.nickname }}</text>
</view>
<text
class="time-text">{{ sheep.$helper.timeFormat(item.brokerageTime, 'yyyy-mm-dd hh:MM:ss') }}</text>
</view>
</uni-list-chat>
</uni-list>
</view>
<s-empty v-if="state.pagination.total === 0" icon="/static/data-empty.png" text="暂无团队信息">
</s-empty> -->
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { computed, reactive, ref } from 'vue';
import _ from 'lodash-es';
import { onPageScroll } from '@dcloudio/uni-app';
import BrokerageApi from '@/sheep/api/trade/brokerage';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
// const agentInfo = computed(() => sheep.$store('user').agentInfo);
const userInfo = computed(() => sheep.$store('user').userInfo);
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
onPageScroll((e) => {
state.scrollTop = e.scrollTop <= 100;
});
let sort = ref();
const state = reactive({
summary: {},
pagination: {
pageNo: 1,
pageSize: 8,
list: [],
total: 0,
},
loadStatus: '',
// ↓ 新 ui 逻辑
level: 1,
nickname: ref(''),
sortKey: '',
isAsc: '',
});
function filterUserNum(num) {
if (_.isNil(num)) {
return '';
}
return `下级团队${num}`;
}
function submitForm() {
state.pagination.list = [];
getTeamList();
}
async function getTeamList() {
state.loadStatus = 'loading';
let { code, data } = await BrokerageApi.getBrokerageUserChildSummaryPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
level: state.level,
'sortingField.order': state.isAsc,
'sortingField.field': state.sortKey,
nickname: state.nickname,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
function setType(e) {
state.pagination.list = [];
state.level = e + '';
getTeamList();
}
function setSort(sortKey, isAsc) {
state.pagination.list = [];
sort = sortKey + isAsc.toUpperCase();
state.isAsc = isAsc;
state.sortKey = sortKey;
getTeamList();
}
onLoad(async () => {
await getTeamList();
// 概要数据
let { data } = await BrokerageApi.getBrokerageUserSummary();
state.summary = data;
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getTeamList();
}
const bgStyle = {
backgroundColor: 'var(--ui-BG-Main)',
};
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.l1 {
background-color: #fff;
height: 86rpx;
line-height: 86rpx;
font-size: 28rpx;
color: #282828;
border-bottom: 1rpx solid #eee;
border-top-left-radius: 14rpx;
border-top-right-radius: 14rpx;
display: flex;
justify-content: space-around;
}
.header-box {
box-sizing: border-box;
padding: 0 20rpx 20rpx 20rpx;
width: 750rpx;
z-index: 3;
position: relative;
background: v-bind(headerBg) no-repeat,
linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
background-size: 750rpx 100%;
// 团队信息总览
.team-data-box {
.data-card {
width: 305rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 20rpx;
.item-title {
font-size: 22rpx;
font-weight: 500;
color: #999999;
line-height: 30rpx;
margin-bottom: 10rpx;
}
.total-item {
margin-bottom: 30rpx;
}
.total-num {
font-size: 38rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
.category-num {
font-size: 26rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
}
}
}
.list-box {
z-index: 3;
position: relative;
}
.chat-custom-right {
.time-text {
font-size: 22rpx;
font-weight: 400;
color: #999999;
}
.tag-box {
background: rgba(0, 0, 0, 0.2);
border-radius: 21rpx;
line-height: 30rpx;
padding: 5rpx 10rpx;
width: 140rpx;
.tag-img {
width: 34rpx;
height: 34rpx;
margin-right: 6rpx;
border-radius: 50%;
}
.tag-title {
font-size: 18rpx;
font-weight: 500;
color: rgba(255, 255, 255, 1);
line-height: 20rpx;
}
}
}
// 推荐人
.referrer-box {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
padding: 20rpx;
.referrer-avatar {
width: 34rpx;
height: 34rpx;
border-radius: 50%;
}
}
.promoter-list .nav {
background-color: #fff;
height: 86rpx;
line-height: 86rpx;
font-size: 28rpx;
color: #282828;
border-bottom: 1rpx solid #eee;
border-top-left-radius: 14rpx;
border-top-right-radius: 14rpx;
margin-top: -30rpx;
}
.promoter-list .nav .item.on {
border-bottom: 5rpx solid;
// $theme-color
color: var(--ui-BG-Main);
// $theme-color
}
.promoter-list .search {
width: 100%;
background-color: #fff;
height: 100rpx;
padding: 0 24rpx;
box-sizing: border-box;
border-bottom-left-radius: 14rpx;
border-bottom-right-radius: 14rpx;
}
.promoter-list .search .input {
width: 592rpx;
height: 60rpx;
border-radius: 50rpx;
background-color: #f5f5f5;
text-align: center;
position: relative;
}
.promoter-list .search .input input {
height: 100%;
font-size: 26rpx;
width: 610rpx;
text-align: center;
}
.promoter-list .search .input .placeholder {
color: #bbb;
}
.promoter-list .search .input .iconfont {
position: absolute;
right: 28rpx;
color: #999;
font-size: 28rpx;
top: 50%;
transform: translateY(-50%);
}
.promoter-list .search .iconfont {
font-size: 32rpx;
color: #515151;
height: 60rpx;
line-height: 60rpx;
}
.promoter-list .list {
margin-top: 20rpx;
}
.promoter-list .list .sortNav {
background-color: #fff;
height: 76rpx;
border-bottom: 1rpx solid #eee;
color: #333;
font-size: 28rpx;
border-top-left-radius: 14rpx;
border-top-right-radius: 14rpx;
}
.promoter-list .list .sortNav .sortItem {
text-align: center;
flex: 1;
}
.promoter-list .list .sortNav .sortItem image {
width: 24rpx;
height: 24rpx;
margin-left: 6rpx;
vertical-align: -3rpx;
}
.promoter-list .list .item {
background-color: #fff;
border-bottom: 1rpx solid #eee;
height: 152rpx;
padding: 0 24rpx;
font-size: 24rpx;
color: #666;
}
.promoter-list .list .item .picTxt .pictrue {
width: 106rpx;
height: 106rpx;
border-radius: 50%;
}
.promoter-list .list .item .picTxt .pictrue image {
width: 100%;
height: 100%;
border-radius: 50%;
border: 3rpx solid #fff;
box-shadow: 0 0 10rpx #aaa;
box-sizing: border-box;
}
.promoter-list .list .item .picTxt .text {
// width: 304rpx;
font-size: 24rpx;
color: #666;
margin-left: 14rpx;
}
.promoter-list .list .item .picTxt .text .name {
font-size: 28rpx;
color: #333;
margin-bottom: 13rpx;
}
.promoter-list .list .item .right {
text-align: right;
font-size: 22rpx;
color: #333;
}
.promoter-list .list .item .right .num {
margin-right: 7rpx;
}
</style>

533
pages/commission/wallet.vue Normal file
View File

@@ -0,0 +1,533 @@
<!-- 分销 - 佣金明细 -->
<template>
<s-layout class="wallet-wrap" title="佣金">
<!-- 钱包卡片 -->
<view class="header-box ss-flex ss-row-center ss-col-center">
<view class="card-box ui-BG-Main ui-Shadow-Main">
<view class="card-head ss-flex ss-col-center">
<view class="card-title ss-m-r-10">当前佣金</view>
<view
@tap="state.showMoney = !state.showMoney"
class="ss-eye-icon"
:class="state.showMoney ? 'cicon-eye' : 'cicon-eye-off'"
/>
</view>
<view class="ss-flex ss-row-between ss-col-center ss-m-t-30">
<view class="money-num">{{
state.showMoney ? fen2yuan(state.summary.withdrawPrice || 0) : '*****'
}}</view>
<view class="ss-flex">
<view class="ss-m-r-20">
<button
class="ss-reset-button withdraw-btn"
@tap="sheep.$router.go('/pages/commission/withdraw')"
>
提现
</button>
</view>
<button class="ss-reset-button balance-btn ss-m-l-20" @tap="state.showModal = true">
转钻石
</button>
</view>
</view>
<view class="ss-flex">
<view class="loading-money">
<view class="loading-money-title">冻结佣金</view>
<view class="loading-money-num">
{{ state.showMoney ? fen2yuan(state.summary.frozenPrice || 0) : '*****' }}
</view>
</view>
<view class="loading-money ss-m-l-100">
<view class="loading-money-title">可提现佣金</view>
<view class="loading-money-num">
{{ state.showMoney ? fen2yuan(state.summary.brokeragePrice || 0) : '*****' }}
</view>
</view>
</view>
</view>
</view>
<su-sticky>
<!-- 统计 -->
<view class="filter-box ss-p-x-30 ss-flex ss-col-center ss-row-between">
<uni-datetime-picker
v-model="state.date"
type="daterange"
@change="onChangeTime"
:end="state.today"
>
<button class="ss-reset-button date-btn">
<text>{{ dateFilterText }}</text>
<text class="cicon-drop-down ss-seldate-icon" />
</button>
</uni-datetime-picker>
<view class="total-box">
<!-- TODO 芋艿这里暂时不考虑做 -->
<!-- <view class="ss-m-b-10">总收入{{ state.pagination.income.toFixed(2) }}</view> -->
<!-- <view>总支出{{ (-state.pagination.expense).toFixed(2) }}</view> -->
</view>
</view>
<su-tabs
:list="tabMaps"
@change="onChangeTab"
:scrollable="false"
:current="state.currentTab"
/>
</su-sticky>
<s-empty
v-if="state.pagination.total === 0"
icon="/static/data-empty.png"
text="暂无数据"
></s-empty>
<!-- 转余额弹框 -->
<su-popup
:show="state.showModal"
type="bottom"
round="20"
@close="state.showModal = false"
showClose
>
<view class="ss-p-x-20 ss-p-y-30">
<view class="model-title ss-m-b-30 ss-m-l-20">转余额</view>
<view class="model-subtitle ss-m-b-100 ss-m-l-20">将您的佣金转为钻石继续消费</view>
<view class="input-box ss-flex ss-col-center border-bottom ss-m-b-70 ss-m-x-20">
<view class="unit"></view>
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.price"
type="number"
placeholder="请输入金额"
/>
</view>
<button
class="ss-reset-button model-btn ui-BG-Main-Gradient ui-Shadow-Main"
@tap="onConfirm"
>
确定
</button>
</view>
</su-popup>
<!-- 钱包记录 -->
<view v-if="state.pagination.total > 0">
<view
class="wallet-list ss-flex border-bottom"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="list-content">
<view class="title-box ss-flex ss-row-between ss-m-b-20">
<text class="title ss-line-1">{{ item.description }}</text>
<view class="money">
<text v-if="item.price >= 0" class="add">+{{ fen2yuan(item.price) }}</text>
<text v-else class="minus">{{ fen2yuan(item.price) }}</text>
</view>
</view>
<view class="status-box">
<text class="time">{{
sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss')
}}</text>
<text class="status" v-if="item.price >= 0 && item.status == 0">待结算</text>
<text class="status" v-if="item.price >= 0 && item.status == 1">已结算</text>
</view>
</view>
</view>
</view>
<!-- <u-gap></u-gap> -->
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
/>
</s-layout>
</template>
<script setup>
import { computed, reactive } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import dayjs from 'dayjs';
import _ from 'lodash-es';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import { resetPagination } from '@/sheep/util';
const headerBg = sheep.$url.css('/static/img/shop/user/wallet_card_bg.png');
const state = reactive({
showMoney: false,
summary: {}, // 分销信息
today: '',
date: [],
currentTab: 0,
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
loadStatus: '',
price: undefined,
showModal: false,
});
const tabMaps = [
{
name: '分佣',
value: '1', // BrokerageRecordBizTypeEnum.ORDER
},
{
name: '提现',
value: '2', // BrokerageRecordBizTypeEnum.WITHDRAW
},
];
const dateFilterText = computed(() => {
if (state.date[0] === state.date[1]) {
return state.date[0];
} else {
return state.date.join('~');
}
});
async function getLogList() {
state.loadStatus = 'loading';
let { code, data } = await BrokerageApi.getBrokerageRecordPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
bizType: tabMaps[state.currentTab].value,
'createTime[0]': state.date[0] + ' 00:00:00',
'createTime[1]': state.date[1] + ' 23:59:59',
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
function onChangeTab(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
getLogList();
}
function onChangeTime(e) {
state.date[0] = e[0];
state.date[1] = e[e.length - 1];
resetPagination(state.pagination);
getLogList();
}
// 确认操作(转账到余额)
async function onConfirm() {
if (state.price <= 0) {
sheep.$helper.toast('请输入正确的金额');
return;
}
uni.showModal({
title: '提示',
content: '确认把您的佣金转入到余额钱包中?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await BrokerageApi.createBrokerageWithdraw({
type: 1, // 钱包
price: state.price * 100,
});
if (code === 0) {
state.showModal = false;
await getAgentInfo();
onChangeTab({
index: 1,
});
}
},
});
}
async function getAgentInfo() {
const { code, data } = await BrokerageApi.getBrokerageUserSummary();
if (code !== 0) {
return;
}
state.summary = data;
}
onLoad(async (options) => {
state.today = dayjs().format('YYYY-MM-DD');
state.date = [state.today, state.today];
if (options.type === 2) {
// 切换到“提现” tab 下
state.currentTab = 1;
}
getLogList();
getAgentInfo();
});
onReachBottom(() => {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getLogList();
});
</script>
<style lang="scss" scoped>
.status-box {
display: flex;
justify-content: space-between;
align-items: center;
.status {
font-size: 22rpx;
}
}
// 钱包
.header-box {
background-color: $white;
padding: 30rpx;
.card-box {
width: 100%;
min-height: 300rpx;
padding: 40rpx;
background-size: 100% 100%;
border-radius: 30rpx;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
&::after {
content: '';
display: block;
width: 100%;
height: 100%;
z-index: 2;
position: absolute;
top: 0;
left: 0;
background: v-bind(headerBg) no-repeat;
pointer-events: none;
}
.card-head {
color: $white;
font-size: 24rpx;
}
.ss-eye-icon {
font-size: 40rpx;
color: $white;
}
.money-num {
font-size: 40rpx;
line-height: normal;
font-weight: 500;
color: $white;
font-family: OPPOSANS;
}
.reduce-num {
font-size: 26rpx;
font-weight: 400;
color: $white;
}
.withdraw-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30px;
font-size: 24rpx;
font-weight: 500;
background-color: $white;
color: var(--ui-BG-Main);
}
.balance-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30px;
font-size: 24rpx;
font-weight: 500;
color: $white;
border: 1px solid $white;
}
}
}
.loading-money {
margin-top: 56rpx;
.loading-money-title {
font-size: 24rpx;
font-weight: 400;
color: #ffffff;
line-height: normal;
margin-bottom: 30rpx;
}
.loading-money-num {
font-size: 30rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #fefefe;
}
}
// 筛选
.filter-box {
height: 120rpx;
padding: 0 30rpx;
background-color: $bg-page;
.total-box {
font-size: 24rpx;
font-weight: 500;
color: $dark-9;
}
.date-btn {
background-color: $white;
line-height: 54rpx;
border-radius: 27rpx;
padding: 0 20rpx;
font-size: 24rpx;
font-weight: 500;
color: $dark-6;
.ss-seldate-icon {
font-size: 50rpx;
color: $dark-9;
}
}
}
// tab
.wallet-tab-card {
.tab-item {
height: 80rpx;
position: relative;
.tab-title {
font-size: 30rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 2rpx;
background-color: var(--ui-BG-Main);
}
}
}
// 钱包记录
.wallet-list {
padding: 30rpx;
background-color: #ffff;
.head-img {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
background: $gray-c;
}
.list-content {
justify-content: space-between;
align-items: flex-start;
flex: 1;
.title {
font-size: 28rpx;
color: $dark-3;
width: 400rpx;
}
.time {
color: $gray-c;
font-size: 22rpx;
}
}
.money {
font-size: 28rpx;
font-weight: bold;
font-family: OPPOSANS;
.add {
color: var(--ui-BG-Main);
}
.minus {
color: $dark-3;
}
}
}
.model-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.model-subtitle {
font-size: 26rpx;
color: #c2c7cf;
}
.model-btn {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
line-height: normal;
}
.input-box {
height: 100rpx;
.unit {
font-size: 48rpx;
color: #333;
font-weight: 500;
line-height: normal;
}
.uni-easyinput__placeholder-class {
font-size: 30rpx;
height: 40rpx;
line-height: normal;
}
}
</style>

View File

@@ -0,0 +1,467 @@
<!-- 分佣提现 -->
<template>
<s-layout title="申请提现" class="withdraw-wrap" navbar="inner">
<view class="page-bg"></view>
<view
class="wallet-num-box ss-flex ss-col-center ss-row-between"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88 + 22) + 'rpx',
paddingTop: Number(statusBarHeight + 88 + 22) + 'rpx',
},
]"
>
<view class="">
<view class="num-title">可提现金额</view>
<view class="wallet-num">{{ fen2yuan(state.brokerageInfo.brokeragePrice) }}</view>
</view>
<button
class="ss-reset-button log-btn"
@tap="sheep.$router.go('/pages/commission/wallet', { type: 2 })"
>
提现记录
</button>
</view>
<!-- 提现输入卡片-->
<view class="draw-card" v-if="isPass">
<view class="bank-box ss-flex ss-col-center ss-row-between ss-m-b-30">
<view class="name">提现至</view>
<view class="bank-list ss-flex ss-col-center" @tap="onAccountSelect(true)">
<view v-if="!state.accountInfo.type" class="empty-text">请选择提现方式</view>
<view v-if="state.accountInfo.type === '1'" class="empty-text">钱包余额</view>
<view v-if="state.accountInfo.type === '2'" class="empty-text">银行卡转账</view>
<view v-if="state.accountInfo.type === '3'" class="empty-text">微信零钱</view>
<view v-if="state.accountInfo.type === '4'" class="empty-text">支付宝账户</view>
<text class="cicon-forward" />
</view>
</view>
<!-- 提现金额 -->
<view class="card-title">提现金额</view>
<view class="input-box ss-flex ss-col-center border-bottom">
<view class="unit"></view>
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.price"
type="number"
placeholder="请输入提现金额"
/>
</view>
<!-- 提现账号 -->
<view class="card-title" v-show="['2', '3', '4'].includes(state.accountInfo.type)">
提现账号
</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="['2', '3', '4'].includes(state.accountInfo.type)"
>
<view class="unit" />
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.accountNo"
placeholder="请输入提现账号"
/>
</view>
<!-- 收款码 -->
<view class="card-title" v-show="['3', '4'].includes(state.accountInfo.type)">收款码</view>
<view
class="input-box ss-flex ss-col-center"
v-show="['3', '4'].includes(state.accountInfo.type)"
>
<view class="unit" />
<view class="upload-img">
<s-uploader
v-model:url="state.accountInfo.accountQrCodeUrl"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</view>
</view>
<!-- 持卡人姓名 -->
<view class="card-title" v-show="state.accountInfo.type === '2'">持卡人</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="state.accountInfo.type === '2'"
>
<view class="unit" />
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.name"
placeholder="请输入持卡人姓名"
/>
</view>
<!-- 提现银行 -->
<view class="card-title" v-show="state.accountInfo.type === '2'">提现银行</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="state.accountInfo.type === '2'"
>
<view class="unit" />
<!--银行改为下拉选择-->
<picker
@change="bankChange"
:value="state.bankListSelectedIndex"
:range="state.bankList"
range-key="label"
style="width: 100%"
>
<uni-easyinput
:inputBorder="false"
:value="state.accountInfo.bankName"
placeholder="请选择银行"
suffixIcon="right"
disabled
:styles="{ disableColor: '#fff', borderColor: '#fff', color: '#333!important' }"
/>
</picker>
</view>
<!-- 开户地址 -->
<view class="card-title" v-show="state.accountInfo.type === '2'">开户地址</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="state.accountInfo.type === '2'"
>
<view class="unit" />
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.bankAddress"
placeholder="请输入开户地址"
/>
</view>
<button class="ss-reset-button save-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onConfirm">
确认提现
</button>
</view>
<!-- 提现说明 -->
<view class="draw-notice">
<view class="title ss-m-b-30">提现说明</view>
<view class="draw-list"> 最低提现金额 {{ fen2yuan(state.minPrice) }} </view>
<view class="draw-list">
冻结佣金<text>{{ fen2yuan(state.brokerageInfo.frozenPrice) }}</text>
每笔佣金的冻结期为 {{ state.frozenDays }} 到期后可提现
</view>
</view>
<!-- 选择提现账户 -->
<account-type-select
:show="state.accountSelect"
@close="onAccountSelect(false)"
round="10"
v-model="state.accountInfo"
:methods="state.withdrawTypes"
/>
</s-layout>
</template>
<script setup>
import { computed, reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
import accountTypeSelect from './components/account-type-select.vue';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import TradeConfigApi from '@/sheep/api/trade/config';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import DictApi from '@/sheep/api/system/dict';
const isPass = computed(() => {
return sheep.$store('user').tradeConfig.weixinEnabled;
});
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const userStore = sheep.$store('user');
const userInfo = computed(() => userStore.userInfo);
const state = reactive({
accountInfo: {
// 提现表单
type: undefined,
accountNo: undefined,
accountQrCodeUrl: undefined,
name: undefined,
bankName: undefined,
bankAddress: undefined,
},
accountSelect: false,
brokerageInfo: {}, // 分销信息
frozenDays: 0, // 冻结天数
minPrice: 0, // 最低提现金额
withdrawTypes: [], // 提现方式
bankList: [], // 银行字典数据
bankListSelectedIndex: '', // 选中银行 bankList 的 index
});
// 打开提现方式的弹窗
const onAccountSelect = (e) => {
state.accountSelect = e;
};
// 提交提现
const onConfirm = async () => {
// 参数校验
//debugger;
if (
!state.accountInfo.price ||
state.accountInfo.price > state.brokerageInfo.price ||
state.accountInfo.price <= 0
) {
sheep.$helper.toast('请输入正确的提现金额');
return;
}
if (!state.accountInfo.type) {
sheep.$helper.toast('请选择提现方式');
return;
}
// 提交请求
let { code } = await BrokerageApi.createBrokerageWithdraw({
...state.accountInfo,
price: state.accountInfo.price * 100,
});
if (code !== 0) {
return;
}
// 提示
uni.showModal({
title: '操作成功',
content: '您的提现申请已成功提交',
cancelText: '继续提现',
confirmText: '查看记录',
success: (res) => {
if (res.confirm) {
sheep.$router.go('/pages/commission/wallet', { type: 2 });
return;
}
getBrokerageUser();
state.accountInfo = {};
},
});
};
// 获得分销配置
async function getWithdrawRules() {
let { code, data } = await TradeConfigApi.getTradeConfig();
if (code !== 0) {
return;
}
if (data) {
state.minPrice = data.brokerageWithdrawMinPrice || 0;
state.frozenDays = data.brokerageFrozenDays || 0;
state.withdrawTypes = data.brokerageWithdrawTypes;
}
}
// 获得分销信息
async function getBrokerageUser() {
const { data, code } = await BrokerageApi.getBrokerageUser();
if (code === 0) {
state.brokerageInfo = data;
}
}
// 获取提现银行配置字典
async function getDictDataListByType() {
let { code, data } = await DictApi.getDictDataListByType('brokerage_bank_name');
if (code !== 0) {
return;
}
if (data && data.length > 0) {
state.bankList = data;
}
}
// 银行选择
function bankChange(e) {
const value = e.detail.value;
state.bankListSelectedIndex = value;
state.accountInfo.bankName = state.bankList[value].label;
}
onBeforeMount(() => {
getWithdrawRules();
getBrokerageUser();
getDictDataListByType(); //获取银行字典数据
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-input-input {
font-family: OPPOSANS !important;
}
}
.wallet-num-box {
padding: 0 40rpx 80rpx;
background: var(--ui-BG-Main) v-bind(headerBg) center/750rpx 100% no-repeat;
border-radius: 0 0 5% 5%;
.num-title {
font-size: 26rpx;
font-weight: 500;
color: $white;
margin-bottom: 20rpx;
}
.wallet-num {
font-size: 60rpx;
font-weight: 500;
color: $white;
font-family: OPPOSANS;
}
.log-btn {
width: 170rpx;
height: 60rpx;
line-height: 60rpx;
border: 1rpx solid $white;
border-radius: 30rpx;
padding: 0;
font-size: 26rpx;
font-weight: 500;
color: $white;
}
}
// 提现输入卡片
.draw-card {
background-color: $white;
border-radius: 20rpx;
width: 690rpx;
min-height: 560rpx;
margin: -60rpx 30rpx 30rpx 30rpx;
padding: 30rpx;
position: relative;
z-index: 3;
box-sizing: border-box;
.card-title {
font-size: 30rpx;
font-weight: 500;
margin-bottom: 30rpx;
}
.bank-box {
.name {
font-size: 28rpx;
font-weight: 500;
}
.bank-list {
.empty-text {
font-size: 28rpx;
font-weight: 400;
color: $dark-9;
}
.cicon-forward {
color: $dark-9;
}
}
.input-box {
width: 624rpx;
height: 100rpx;
margin-bottom: 40rpx;
.unit {
font-size: 48rpx;
color: #333;
font-weight: 500;
}
.uni-easyinput__placeholder-class {
font-size: 30rpx;
height: 36rpx;
}
:deep(.uni-easyinput__content-input) {
font-size: 48rpx;
}
}
.save-btn {
width: 616rpx;
height: 86rpx;
line-height: 86rpx;
border-radius: 40rpx;
margin-top: 80rpx;
}
}
.bind-box {
.placeholder-text {
font-size: 26rpx;
color: $dark-9;
}
.add-btn {
width: 100rpx;
height: 50rpx;
border-radius: 25rpx;
line-height: 50rpx;
font-size: 22rpx;
color: var(--ui-BG-Main);
background-color: var(--ui-BG-Main-light);
}
}
.input-box {
width: 624rpx;
height: 100rpx;
margin-bottom: 40rpx;
.unit {
font-size: 48rpx;
color: #333;
font-weight: 500;
}
.uni-easyinput__placeholder-class {
font-size: 30rpx;
}
:deep(.uni-easyinput__content-input) {
font-size: 48rpx;
}
}
.save-btn {
width: 616rpx;
height: 86rpx;
line-height: 86rpx;
border-radius: 40rpx;
margin-top: 80rpx;
}
}
// 提现说明
.draw-notice {
width: 684rpx;
background: #ffffff;
border: 2rpx solid #fffaee;
border-radius: 20rpx;
margin: 20rpx 32rpx 0 32rpx;
padding: 30rpx;
box-sizing: border-box;
.title {
font-weight: 500;
color: #333333;
font-size: 30rpx;
}
.draw-list {
font-size: 24rpx;
color: #999999;
line-height: 46rpx;
}
}
</style>

390
pages/coupon/detail.vue Normal file
View File

@@ -0,0 +1,390 @@
<!-- 优惠券详情 -->
<template>
<s-layout title="优惠券详情">
<view class="bg-white">
<!-- 详情卡片 -->
<view class="detail-wrap ss-p-20">
<view class="detail-box">
<view class="tag-box ss-flex ss-col-center ss-row-center">
<image
class="tag-image"
:src="sheep.$url.static('/static/img/shop/app/coupon_icon.png')"
mode="aspectFit"
/>
</view>
<view class="top ss-flex-col ss-col-center">
<view class="title ss-m-t-50 ss-m-b-20 ss-m-x-20">{{ state.coupon.name }}</view>
<view class="subtitle ss-m-b-50">
{{ fen2yuan(state.coupon.usePrice) }}
{{
state.coupon.discountType === 1
? '减 ' + fen2yuan(state.coupon.discountPrice) + ' 元'
: '打 ' + state.coupon.discountPercent / 10.0 + ' 折'
}}
</view>
<button
class="ss-reset-button ss-m-b-30"
:class="
state.coupon.canTake || state.coupon.status === 1
? 'use-btn' // 优惠劵模版(可领取)、优惠劵(可使用)
: 'disable-btn'
"
:disabled="!state.coupon.canTake"
@click="getCoupon"
>
<text v-if="state.id > 0">{{ state.coupon.canTake ? '立即领取' : '已领取' }}</text>
<text v-else>
{{
state.coupon.status === 1
? '可使用'
: state.coupon.status === 2
? '已使用'
: '已过期'
}}
</text>
</button>
<view class="time ss-m-y-30" v-if="state.coupon.validityType === 2">
有效期领取后 {{ state.coupon.fixedEndTerm }} 天内可用
</view>
<view class="time ss-m-y-30" v-else>
有效期: {{ sheep.$helper.timeFormat(state.coupon.validStartTime, 'yyyy-mm-dd') }}
{{ sheep.$helper.timeFormat(state.coupon.validEndTime, 'yyyy-mm-dd') }}
</view>
<view class="coupon-line ss-m-t-14"></view>
</view>
<view class="bottom">
<view class="type ss-flex ss-col-center ss-row-between ss-p-x-30">
<view>优惠券类型</view>
<view>{{ state.coupon.discountType === 1 ? '满减券' : '折扣券' }}</view>
</view>
<uni-collapse>
<uni-collapse-item title="优惠券说明" v-if="state.coupon.description">
<view class="content ss-p-b-20">
<text class="des ss-p-l-30">{{ state.coupon.description }}</text>
</view>
</uni-collapse-item>
</uni-collapse>
</view>
</view>
</view>
<!-- 适用商品 -->
<view
class="all-user ss-flex ss-row-center ss-col-center"
v-if="state.coupon.productScope === 1"
>
全场通用
</view>
<su-sticky v-else bgColor="#fff">
<view class="goods-title ss-p-20">
{{ state.coupon.productScope === 2 ? '指定商品可用' : '指定分类可用' }}
</view>
<su-tabs
:scrollable="true"
:list="state.tabMaps"
@change="onTabsChange"
:current="state.currentTab"
v-if="state.coupon.productScope === 3"
/>
</su-sticky>
<!-- 指定商品 -->
<view v-if="state.coupon.productScope === 2">
<view v-for="(item, index) in state.pagination.list" :key="index">
<s-goods-column
class="ss-m-20"
size="lg"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
:goodsFields="{
title: { show: true },
subtitle: { show: true },
price: { show: true },
original_price: { show: true },
sales: { show: true },
stock: { show: false },
}"
/>
</view>
</view>
<!-- 指定分类 -->
<view v-if="state.coupon.productScope === 3">
<view v-for="(item, index) in state.pagination.list" :key="index">
<s-goods-column
class="ss-m-20"
size="lg"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
:goodsFields="{
title: { show: true },
subtitle: { show: true },
price: { show: true },
original_price: { show: true },
sales: { show: true },
stock: { show: false },
}"
></s-goods-column>
</view>
</view>
<uni-load-more
v-if="state.pagination.total > 0 && state.coupon.productScope === 3"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
<s-empty
v-if="state.coupon.productScope === 3 && state.pagination.total === 0"
paddingTop="0"
icon="/static/soldout-empty.png"
text="暂无商品"
/>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import CouponApi from '@/sheep/api/promotion/coupon';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import SpuApi from '@/sheep/api/product/spu';
import CategoryApi from '@/sheep/api/product/category';
import { resetPagination } from '@/sheep/util';
const state = reactive({
id: 0, // 优惠劵模版编号 templateId
couponId: 0, // 用户优惠劵编号 couponId
coupon: {}, // 优惠劵信息
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 1,
},
categoryId: 0, // 选中的商品分类编号
tabMaps: [], // 指定分类时,每个分类构成一个 tab
currentTab: 0, // 选中的 tabMaps 下标
loadStatus: '',
});
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
state.categoryId = e.value;
getGoodsListByCategory();
}
async function getGoodsListByCategory() {
state.loadStatus = 'loading';
const { code, data } = await SpuApi.getSpuPage({
categoryId: state.categoryId,
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 获得商品列表,指定商品范围
async function getGoodsListById() {
const { data, code } = await SpuApi.getSpuListByIds(state.coupon.productScopeValues.join(','));
if (code !== 0) {
return;
}
state.pagination.list = data;
}
// 获得分类列表
async function getCategoryList() {
const { data, code } = await CategoryApi.getCategoryListByIds(
state.coupon.productScopeValues.join(','),
);
if (code !== 0) {
return;
}
state.tabMaps = data.map((category) => ({ name: category.name, value: category.id }));
// 加载第一个分类的商品列表
if (state.tabMaps.length > 0) {
state.categoryId = state.tabMaps[0].value;
await getGoodsListByCategory();
}
}
// 领取优惠劵
async function getCoupon() {
const { code } = await CouponApi.takeCoupon(state.id);
if (code !== 0) {
return;
}
uni.showToast({
title: '领取成功',
});
setTimeout(() => {
getCouponContent();
}, 1000);
}
// 加载优惠劵信息
async function getCouponContent() {
const { code, data } =
state.id > 0
? await CouponApi.getCouponTemplate(state.id)
: await CouponApi.getCoupon(state.couponId);
if (code !== 0) {
return;
}
state.coupon = data;
// 不同指定范围,加载不同数据
if (state.coupon.productScope === 2) {
await getGoodsListById();
} else if (state.coupon.productScope === 3) {
await getCategoryList();
}
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGoodsListByCategory();
}
onLoad((options) => {
state.id = options.id;
state.couponId = options.couponId;
getCouponContent(state.id, state.couponId);
});
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.goods-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
.detail-wrap {
background: linear-gradient(
180deg,
var(--ui-BG-Main),
var(--ui-BG-Main-gradient),
var(--ui-BG-Main),
#fff
);
}
.detail-box {
// background-color: var(--ui-BG);
border-radius: 6rpx;
position: relative;
margin-top: 100rpx;
.tag-box {
width: 140rpx;
height: 140rpx;
background: var(--ui-BG);
border-radius: 50%;
position: absolute;
top: -70rpx;
left: 50%;
z-index: 6;
transform: translateX(-50%);
.tag-image {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
}
}
.top {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
-webkit-mask: radial-gradient(circle at 16rpx 100%, #0000 16rpx, red 0) -16rpx;
padding: 110rpx 0 0 0;
position: relative;
z-index: 5;
.title {
font-size: 40rpx;
color: #333;
font-weight: bold;
}
.subtitle {
font-size: 28rpx;
color: #333333;
}
.use-btn {
width: 386rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 40rpx;
color: $white;
}
.disable-btn {
width: 386rpx;
height: 80rpx;
line-height: 80rpx;
background: #e5e5e5;
border-radius: 40rpx;
color: $white;
}
.time {
font-size: 26rpx;
font-weight: 400;
color: #999999;
}
.coupon-line {
width: 95%;
border-bottom: 2rpx solid #eeeeee;
}
}
.bottom {
background-color: #fff;
border-radius: 0 0 20rpx 20rpx;
-webkit-mask: radial-gradient(circle at 16rpx 0%, #0000 16rpx, red 0) -16rpx;
padding: 40rpx 30rpx;
.type {
height: 96rpx;
border-bottom: 2rpx solid #eeeeee;
}
}
.des {
font-size: 24rpx;
font-weight: 400;
color: #666666;
}
}
.all-user {
width: 100%;
height: 300rpx;
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
</style>

222
pages/coupon/list.vue Normal file
View File

@@ -0,0 +1,222 @@
<!-- 优惠券中心 -->
<template>
<s-layout title="优惠券" :bgStyle="{ color: '#f2f2f2' }">
<su-sticky bgColor="#fff">
<su-tabs
:list="tabMaps"
:scrollable="false"
@change="onTabsChange"
:current="state.currentTab"
/>
</su-sticky>
<s-empty
v-if="state.pagination.total === 0"
icon="/static/coupon-empty.png"
text="暂无优惠券"
/>
<!-- 情况一领劵中心 -->
<template v-if="state.currentTab === 0">
<view v-for="item in state.pagination.list" :key="item.id">
<s-coupon-list
:data="item"
@tap="sheep.$router.go('/pages/coupon/detail', { id: item.id })"
>
<template #default>
<button
class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
:class="!item.canTake ? 'border-btn' : ''"
@click.stop="getBuy(item.id)"
:disabled="!item.canTake"
>
{{ item.canTake ? '立即领取' : '已领取' }}
</button>
</template>
</s-coupon-list>
</view>
</template>
<!-- 情况二我的优惠劵 -->
<template v-else>
<view v-for="item in state.pagination.list" :key="item.id">
<s-coupon-list
:data="item"
type="user"
@tap="sheep.$router.go('/pages/coupon/detail', { couponId: item.id })"
>
<template #default>
<button
class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
:class="item.status !== 1 ? 'disabled-btn' : ''"
:disabled="item.status !== 1"
@click.stop="sheep.$router.go('/pages/coupon/detail', { couponId: item.id })"
>
{{ item.status === 1 ? '查看详情' : item.status === 2 ? '已使用' : '已过期' }}
</button>
</template>
</s-coupon-list>
</view>
</template>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import { resetPagination } from '@/sheep/util';
import CouponApi from '@/sheep/api/promotion/coupon';
// 数据
const state = reactive({
currentTab: 0, // 当前 tab
type: '1',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 5,
},
loadStatus: '',
});
const tabMaps = [
{
name: '领券中心',
value: 'all',
},
{
name: '已领取',
value: '1',
},
{
name: '已使用',
value: '2',
},
{
name: '已失效',
value: '3',
},
];
function onTabsChange(e) {
state.currentTab = e.index;
state.type = e.value;
resetPagination(state.pagination);
if (state.currentTab === 0) {
getData();
} else {
getCoupon();
}
}
// 获得优惠劵模版列表
async function getData() {
state.loadStatus = 'loading';
const { data, code } = await CouponApi.getCouponTemplatePage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 获得我的优惠劵
async function getCoupon() {
state.loadStatus = 'loading';
const { data, code } = await CouponApi.getCouponPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
status: state.type,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 领取优惠劵
async function getBuy(id) {
const { code } = await CouponApi.takeCoupon(id);
if (code !== 0) {
return;
}
uni.showToast({
title: '领取成功',
});
setTimeout(() => {
resetPagination(state.pagination);
getData();
}, 1000);
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
if (state.currentTab === 0) {
getData();
} else {
getCoupon();
}
}
onLoad((Option) => {
// 领劵中心
if (Option.type === 'all' || !Option.type) {
getData();
// 我的优惠劵
} else {
Option.type === 'geted'
? (state.currentTab = 1)
: Option.type === 'used'
? (state.currentTab = 2)
: (state.currentTab = 3);
state.type = state.currentTab;
getCoupon();
}
});
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.card-btn {
// width: 144rpx;
padding: 0 16rpx;
height: 50rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: #ffffff;
font-size: 24rpx;
font-weight: 400;
}
.border-btn {
background: linear-gradient(90deg, var(--ui-BG-Main-opacity-4), var(--ui-BG-Main-light));
color: #fff !important;
}
.disabled-btn {
background: #cccccc;
background-color: #cccccc !important;
color: #fff !important;
}
</style>

190
pages/goods/comment/add.vue Normal file
View File

@@ -0,0 +1,190 @@
<!-- 评价 -->
<template>
<s-layout title="评价">
<view>
<view v-for="(item, index) in state.orderInfo.items" :key="item.id">
<view>
<view class="commont-from-wrap">
<!-- 评价商品 -->
<s-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.payPrice"
:num="item.count"
/>
</view>
<view class="form-item">
<!-- 评分 -->
<view class="star-box ss-flex ss-col-center">
<view class="star-title ss-m-r-40">商品质量</view>
<uni-rate v-model="state.commentList[index].descriptionScores" />
</view>
<view class="star-box ss-flex ss-col-center">
<view class="star-title ss-m-r-40">服务态度</view>
<uni-rate v-model="state.commentList[index].benefitScores" />
</view>
<!-- 评价 -->
<view class="area-box">
<uni-easyinput
:inputBorder="false"
type="textarea"
maxlength="120"
autoHeight
v-model="state.commentList[index].content"
placeholder="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~"
/>
<view class="img-box">
<s-uploader
v-model:url="state.commentList[index].images"
fileMediatype="image"
limit="9"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
@success="(payload) => uploadSuccess(payload, index)"
/>
</view>
</view>
<view class="checkbox-container">
<checkbox-group @change="(event) => toggleAnonymous(index, event)">
<label>
<checkbox value="anonymousChecked" />
匿名评论
</label>
</checkbox-group>
</view>
</view>
</view>
</view>
</view>
<su-fixed bottom placeholder>
<view class="foot_box ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button post-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onSubmit">
发布
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
orderInfo: {},
commentList: [],
id: null,
});
/**
* 切换是否匿名
*
* @param commentIndex 当前评论下标
* @param event 复选框事件
*/
function toggleAnonymous(commentIndex, event) {
state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
}
/**
* 发布评论
*
* @returns {Promise<void>}
*/
async function onSubmit() {
// 顺序提交评论
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
// 都评论好,返回
sheep.$router.back();
}
/**
* 图片添加到表单
*
* @param payload 上传成功后的回调数据
* @param commentIndex 当前评论的下标
*/
function uploadSuccess(payload, commentIndex) {
state.commentList[commentIndex].picUrls = state.commentList[commentIndex].images;
}
onLoad(async (options) => {
if (!options.id) {
sheep.$helper.toast(`缺少订单信息,请检查`);
return;
}
state.id = options.id;
const { code, data } = await OrderApi.getOrder(state.id);
if (code !== 0) {
sheep.$helper.toast('无待评价订单');
return;
}
// 处理评论
data.items.forEach((item) => {
state.commentList.push({
anonymous: false,
orderItemId: item.id,
descriptionScores: 5,
benefitScores: 5,
content: '',
picUrls: [],
});
});
state.orderInfo = data;
});
</script>
<style lang="scss" scoped>
// 评价商品
.goods-card {
margin: 10rpx 0;
padding: 20rpx;
background: #fff;
}
// 评论,选择图片
.form-item {
background: #fff;
.star-box {
height: 100rpx;
padding: 0 25rpx;
}
.star-title {
font-weight: 600;
}
}
.area-box {
width: 690rpx;
min-height: 306rpx;
background: rgba(249, 250, 251, 1);
border-radius: 20rpx;
padding: 28rpx;
margin: auto;
.img-box {
margin-top: 20rpx;
}
}
.checkbox-container {
padding: 10rpx;
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
</style>

View File

@@ -0,0 +1,180 @@
<!-- 评价 -->
<template>
<s-layout title="评价店员">
<view>
<view v-for="(item, index) in state.orderInfo.items" :key="item.id">
<view>
<view class="commont-from-wrap">
<!-- 评价商品 -->
<s-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.payPrice"
:num="item.count"
/>
</view>
<view class="form-item">
<!-- 评分 -->
<view class="star-box ss-flex ss-col-center">
<view class="star-title ss-m-r-40">店员质量</view>
<uni-rate v-model="state.commentList[index].descriptionScores" />
</view>
<view class="star-box ss-flex ss-col-center">
<view class="star-title ss-m-r-40">服务态度</view>
<uni-rate v-model="state.commentList[index].benefitScores" />
</view>
<!-- 评价 -->
<view class="area-box">
<uni-easyinput :inputBorder="false" type="textarea" maxlength="120" autoHeight
v-model="state.commentList[index].content"
placeholder="店员满足你的期待吗?说说你的体验心得,分享给想下单的宝子们吧~" />
<!-- TODO 卢越评论文件上传 -->
<view class="img-box">
<s-uploader v-model:url="state.commentList[index].images" fileMediatype="image"
@success="(payload) => uploadSuccess(payload, index)"
limit="9" mode="grid" :imageStyles="{ width: '168rpx', height: '168rpx' }" />
</view>
</view>
<view class="checkbox-container">
<checkbox-group @change="(event) => toggleAnonymous(index, event)">
<label>
<checkbox value="anonymousChecked" />
匿名评论
</label>
</checkbox-group>
</view>
</view>
</view>
</view>
</view>
<!-- TODO 卢越评论是否匿名 -->
<su-fixed bottom placeholder>
<view class="foot_box ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button post-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onSubmit">
发布
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
orderInfo: {},
commentList: [],
id: null
});
/**
* 切换是否匿名
*
* @param commentIndex 当前评论下标
* @param event 复选框事件
*/
function toggleAnonymous(commentIndex, event) {
state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
}
async function onSubmit() {
// 顺序提交评论
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
// 都评论好,返回
sheep.$router.go('/pages/order/my/list');
//sheep.$router.back();
}
/**
* 图片添加到表单
*
* @param payload 上传成功后的回调数据
* @param commentIndex 当前评论的下标
*/
function uploadSuccess(payload, commentIndex) {
state.commentList[commentIndex].picUrls = state.commentList[commentIndex].images;
}
onLoad(async (options) => {
if (!options.id) {
sheep.$helper.toast(`缺少订单信息,请检查`);
return
}
state.id = options.id;
const { code, data } = await OrderApi.getOrder(state.id);
if (code !== 0) {
sheep.$helper.toast('无待评价订单');
return
}
// 处理评论
data.items.forEach((item) => {
state.commentList.push({
anonymous: false,
orderItemId: item.id,
descriptionScores: 5,
benefitScores: 5,
content: '',
picUrls: []
});
});
state.orderInfo = data;
});
</script>
<style lang="scss" scoped>
// 评价商品
.goods-card {
margin: 10rpx 0;
padding: 20rpx;
background: #fff;
}
// 评论,选择图片
.form-item {
background: #fff;
padding-bottom: 30rpx;
.star-box {
height: 100rpx;
padding: 0 25rpx;
}
.star-title {
font-weight: 600;
}
}
.area-box {
width: 690rpx;
min-height: 306rpx;
background: rgba(249, 250, 251, 1);
border-radius: 20rpx;
padding: 28rpx;
margin: auto;
.img-box {
margin-top: 20rpx;
}
}
.checkbox-container {
padding: 20rpx 30rpx;
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<view>
<view class="card-notice">
<u-notice-bar mode="horizontal" :list="list"></u-notice-bar>
<view class="btn">投诉举报</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [
'平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!',
],
}
},
methods: {
onBanner(index) {
console.log(22222, index);
},
}
}
</script>
<style lang="scss" scoped>
.card-notice {
background-color: #fff;
.btn {
display: flex;
justify-content: center;
background-color: #fff;
height: 60px;
align-items: center;
color: #ffa31a;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<view>
<uni-swiper-dot class="uni-swiper-dot-box" @clickItem=clickItem :info="info" :current="current" :mode="mode" :dots-styles="dotsStyles" field="content">
<swiper class="swiper-box" @change="change" :current="swiperDotIndex">
<swiper-item v-for="(item, index) in 3" :key="index">
<view class="swiper-item">
<view class="card">
<image class="logo" src="http://rbtnet.oss-cn-hangzhou.aliyuncs.com/test/20240726/98a24075202b429d9c9722df541621c6.jpg"></image>
<view class="content-box">
<view class="left-box">
<view class="title">15开黑卡/</view>
<view class="text-gray">LOL手游</view>
<view class="text-gray">下单前聊一聊</view>
</view>
<view class="btn">找TA开黑</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
</uni-swiper-dot>
</view>
</template>
<script>
export default {
data() {
return {
info: [{
colorClass: 'uni-bg-red',
url: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg',
content: '内容 A'
},
{
colorClass: 'uni-bg-green',
url: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg',
content: '内容 B'
},
{
colorClass: 'uni-bg-blue',
url: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg',
content: '内容 C'
}
],
dotStyle: [{
backgroundColor: '#00c8b0',
border: '1px rgba(0, 0, 0, .3) solid',
color: '#fff',
selectedBackgroundColor: 'rgba(0, 0, 0, .9)',
selectedBorder: '1px rgba(0, 0, 0, .9) solid'
},
{
backgroundColor: 'rgba(255, 90, 95,0.3)',
border: '1px rgba(255, 90, 95,0.3) solid',
color: '#fff',
selectedBackgroundColor: 'rgba(255, 90, 95,0.9)',
selectedBorder: '1px rgba(255, 90, 95,0.9) solid'
},
{
backgroundColor: 'rgba(83, 200, 249,0.3)',
border: '1px rgba(83, 200, 249,0.3) solid',
color: '#fff',
selectedBackgroundColor: 'rgba(83, 200, 249,0.9)',
selectedBorder: '1px rgba(83, 200, 249,0.9) solid'
}
],
modeIndex: -1,
styleIndex: -1,
current: 0,
mode: 'default',
dotsStyles: {
selectedBackgroundColor: '#00c8b0',
},
swiperDotIndex: 0,
current: 0,
}
},
methods: {
change(e) {
this.current = e.detail.current
},
selectStyle(index) {
this.dotsStyles = this.dotStyle[index]
this.styleIndex = index
},
selectMode(mode, index) {
this.mode = mode
this.modeIndex = index
this.styleIndex = -1
this.dotsStyles = this.dotStyle[0]
},
clickItem(e) {
this.swiperDotIndex = e
},
onBanner(index) {
console.log(22222, index);
},
}
}
</script>
<style lang="scss" scoped>
.swiper-box {
height: 80px;
}
.swiper-item {
height: 80px;
}
.image {
width: 750rpx;
}
/* #ifndef APP-NVUE */
::v-deep .image img {
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
/* #endif */
.uni-bg-red {
background-color: #ff5a5f;
}
.uni-bg-green {
background-color: #09bb07;
}
.uni-bg-blue {
background-color: #007aff;
}
.example-body {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 20rpx;
}
.example-body-item {
flex-direction: row;
justify-content: center;
align-items: center;
margin: 15rpx;
padding: 15rpx;
height: 60rpx;
/* #ifndef APP-NVUE */
display: flex;
padding: 0 15rpx;
/* #endif */
flex: 1;
border-color: #e5e5e5;
border-style: solid;
border-width: 1px;
border-radius: 5px;
}
.example-body-item-text {
font-size: 28rpx;
color: #333;
}
.example-body-dots {
width: 16rpx;
height: 16rpx;
border-radius: 50px;
background-color: #333333;
margin-left: 10rpx;
}
.active {
border-style: solid;
border-color: #007aff;
border-width: 1px;
}
.card {
background-color: #fff;
display: flex;
align-items: center;
height: 80px;
padding: 10px;
.logo {
width: 40px;
height: 40px;
border-radius: 100%;;
}
.content-box {
display: flex;
flex: 1;
justify-content: space-between;
align-items: center;
margin-left: 10px;
.title {
font-size: 28rpx;
color: #333333;
}
.text-gray {
font-size: 24rpx;
color: #aaaaaa;
margin: 3px 0;
}
.btn {
background-color: #00c8b0;
color: #fff;
font-size: 24rpx;
border-radius: 45px;
padding: 5px 10px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,336 @@
<!-- 聊天记录模式+虚拟列表演示(vue)加载更多聊天记录无闪动&支持渲染大量数据 -->
<!-- 注意虚拟列表缓存高度默认仅在cell初始化时获取一次若您的聊天列表中带有图片强烈建议给图片一个固定的高度如果高度根据内容动态撑高可能导致虚拟列表抖动 -->
<!-- 如果必须实现高度根据内容动态撑高+虚拟列表功能可监听图片加载完毕事件并在其中调用z-paging的didUpdateVirtualListCell刷新缓存高度 -->
<template>
<view class="content">
<!-- use-chat-record-mode开启聊天记录模式 -->
<!-- use-virtual-list开启虚拟列表模式 -->
<!-- cell-height-mode设置虚拟列表模式高度不固定 -->
<!-- safe-area-inset-bottom开启底部安全区域适配 -->
<!-- bottom-bg-color设置slot="bottom"容器的背景色这里设置为和chat-input-bar的背景色一致 -->
<z-paging ref="paging" v-model="dataList" :paging-style="{ paddingTop: paddingTop + 'px' }" default-page-size="20" :innerListStyle="innerListStyle" use-chat-record-mode use-virtual-list cell-height-mode="dynamic" :auto-clean-list-when-reload="false" safe-area-inset-bottom bottom-bg-color="#fafafa"
@query="queryList" :back-to-top-style="backToTopStyle" :auto-show-back-to-top="showNewMessageTip" @scrolltoupper="onScrollToUpper" @backToTopClick="onBackToTopClick" @keyboardHeightChange="keyboardHeightChange" @hidedKeyboard="hidedKeyboard">
<!-- 顶部提示文字 -->
<template #top>
<navigation-Bar :isReconnecting="isReconnecting" :user="user" @initNav="initNav" @more="navMore">
<template #card-swiper>
<card-swiper v-if="navSwiper"></card-swiper>
</template>
<template #card-notice>
<card-notice v-if="navNotice"></card-notice>
</template>
</navigation-Bar>
</template>
<!-- style="transform: scaleY(-1)"必须写否则会导致列表倒置 -->
<!-- 注意不要直接在chat-item组件标签上设置style因为在微信小程序中是无效的请包一层view -->
<template #cell="{item,index}">
<view style="transform: scaleY(-1)">
<view-item @playAudio="playAudio" :playId="playId" :messageList="dataList" :messageIndex="index" :item="item"></view-item>
</view>
</template>
<!-- 底部聊天输入框 -->
<template #bottom>
<slot name="chat-bar" :user="user" :keyboardHeight="keyboardHeight"></slot>
</template>
<!-- 查看最新消息 -->
<template #backToTop>
<text>有新消息</text>
</template>
</z-paging>
</view>
</template>
<script>
import dayjs from 'dayjs';
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
import NavigationBar from '@/pages/im/components/navigationBar.vue';
import cardSwiper from '@/pages/im/components/cardSwiper.vue';
import cardNotice from '@/pages/im/components/cardNotice.vue';
import ViewItem from '@/pages/im/components/viewItem.vue';
import ImMessageApi from '@/sheep/api/im/memberMessage';
import UserApi from '@/sheep/api/member/user';
export default {
components: {
NavigationBar,
cardSwiper,
cardNotice,
ViewItem,
},
props: {
groupId: 0,
userId: 0,
isReconnecting: false,
},
data() {
return {
dataList: [],
keyboardHeight: -1,
paddingTop: 0,
navSwiper: false,
navSwiperH: 80,
navNotice: false,
navNoticeH: 90,
height: 0,
queryParams: {
pageNo: 1,
pageSize: 10,
groupId: 0,
},
showNewMessageTip: false,
backToTopStyle: {
'width': '100px',
'background-color': '#fff',
'border-radius': '30px',
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
'display': 'flex',
'justifyContent': 'center',
'alignItems': 'center',
},
innerListStyle: {
borderBottom: '20px solid #fafafa',
},
audioList: [],
voiceIndex: -1,
playId: null,
user: {},
}
},
created() {
this.getUserInfo();
},
methods: {
getUserInfo() {
UserApi.getUserInfoById(this.userId).then(res => {
this.user = res.data;
this.user.time = this.showDayTime(this.user.updateTime);
});
},
showDayTime(datetime) {
if (!datetime) return "";
return dayjs(datetime).fromNow();
},
initNav(e) {
this.height = e.height;
this.paddingTop = this.height;
if(this.navSwiper){
this.paddingTop = this.paddingTop + this.navSwiperH;
}
if(this.navNotice){
this.paddingTop = this.paddingTop + this.navNoticeH;
}
},
navMore() {
this.navNotice = !this.navNotice;
if(this.navNotice){
this.paddingTop = this.paddingTop + this.navNoticeH;
}else{
this.paddingTop = this.paddingTop - this.navNoticeH;
}
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
this.queryParams.pageNo = pageNo;
this.queryParams.pageSize = pageSize;
this.queryParams.groupId = this.groupId;
/* this.$refs.paging.complete([
{
dd:'2',
msgType: 'message',
message: {
time: false,
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'left',
isRead: true,
src: '',
sec: '30',
msgType: 'voice',
},
},
{
dd:'2',
msgType: 'info',
message: {
title: '温馨提示',
content: '请礼貌用语友好沟通,如遇骚扰等不文明行为,可以将对方屏蔽并投诉。',
msgType: 'card',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: true,
timeDesc: '14:52',
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'right',
isRead: true,
msgType: 'emoji',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: true,
timeDesc: '14:52',
src: 'http://rbtnet.oss-cn-hangzhou.aliyuncs.com/test/20240726/98a24075202b429d9c9722df541621c6.jpg',
position: 'right',
isRead: true,
msgType: 'img',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: false,
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'right',
isRead: true,
src: '',
sec: '130',
msgType: 'voice',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: false,
position: 'right',
content: '地址广东省广州市番禺区祈福新村129号',
msgType: 'map',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: false,
position: 'right',
content: '吃饭了吗',
msgType: 'text',
},
},
{
dd:'2',
msgType: 'info',
message: {
content: '长按图片、文字或视频,支持撤回消息哦~',
msgType: 'text',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: true,
timeDesc: '14:52',
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'left',
isRead: true,
msgType: 'emoji',
},
},
]);
*/
ImMessageApi.getMemberMessagePage(this.queryParams).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
},
/** 监听滚动到底部事件(因为 scroll 翻转了顶就是底) */
onScrollToUpper() {
// 若已是第一页则不做处理
if (this.queryParams.pageNo === 1) {
return;
}
this.showNewMessageTip = false;
// 到底重置消息列表
this.reload();
},
// 监听键盘高度改变请不要直接通过uni.onKeyboardHeightChange监听否则可能导致z-paging内置的键盘高度改变监听失效如果不需要切换表情面板则不用写
keyboardHeightChange(res) {
this.keyboardHeight = res.height;
},
// 用户尝试隐藏键盘此时如果表情面板在展示中应当通知chatInputBar隐藏表情面板如果不需要切换表情面板则不用写
hidedKeyboard() {
this.$emit('hidedKeyboard');
},
reload(message) {
if(message){
if(this.queryParams.pageNo > 1){
this.showNewMessageTip = true;
}
if(message.readStatus){
this.updateReadStatus(message);
}else{
this.$refs.paging.addChatRecordData([message]);
}
}else{
this.$refs.paging.reload();
}
},
updateReadStatus(message) {
this.dataList.forEach(item => {
if (item.id == message.id) {
item.readStatus = true;
}
});
},
/** 滚动到最新消息 */
onBackToTopClick(event) {
event(false); // 禁用默认操作
this.$refs.paging.scrollToBottom();
},
playAudio(e) {
this.playId = e.id;
this.clearAllAudio();
var audio = uni.createInnerAudioContext();
this.audioList.push(audio);
//语音自然播放结束
audio.onEnded((res) => {
this.playId = null;
});
audio.src = e.content;
audio.play();
},
clearAllAudio() {
this.audioList.forEach(function(e){
e.destroy();
})
},
}
}
</script>
<style scoped>
.content {
background-color: #fafafa;
height: calc(100vh);
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<view>
<tui-navigation-bar :isOpacity="false" @init="initNavigation" @change="opacityChange" :scrollTop="scrollTop" backgroundColor="#fff" color="#333">
<view>
<view class="nav-box">
<view class="title">
<text class="nickname">{{user.nickname}}</text>
<view class="sex-badge" v-if="user.sex == 2">
<u-icon name="ziyuan2" custom-prefix="iconfont"></u-icon>
</view>
<view class="sex-man" v-if="user.sex == 1">
<u-icon name="ziyuan3" custom-prefix="iconfont"></u-icon>
</view>
</view>
<view class="online" v-if="user.online">
<view v-if="isReconnecting">会话重连中</view>
<view class="text" v-else>
<tui-badge :scaleRatio="0.8" type="green" dot></tui-badge>
<text class="time">在线</text>
<!-- <u-icon name="arrow-right" size="16"></u-icon> -->
</view>
</view>
<!-- <view class="online" v-else>
<view v-if="isReconnecting">会话重连中</view>
<view v-else>
<text class="time">{{user.time}}</text>
</view>
</view> -->
<view class="action" @click="goBack">
<u-icon name="nav-back" size="44"></u-icon>
</view>
</view>
<view>
<slot name="card-swiper"></slot>
</view>
<view>
<slot name="card-notice"></slot>
</view>
<!-- <view class="more">
<view class="more-btn" @click="more">
<view class="line"></view>
<view class="line"></view>
</view>
</view> -->
</view>
</tui-navigation-bar>
</view>
</template>
<script>
import tuiBadge from "@/components/thorui/tui-badge/tui-badge.vue";
import tuiNavigationBar from "@/components/thorui/tui-navigation-bar/tui-navigation-bar.vue";
export default {
components: {
tuiBadge,
tuiNavigationBar,
},
props: {
user: {},
isReconnecting: false,
},
data() {
return {
top: 0, //标题图标距离顶部距离
opacity: 1,
height: 0,
scrollTop: 0.5,
}
},
methods: {
initNavigation(e) {
this.height = e.height;
this.opacity = e.opacity;
this.top = e.top;
this.$emit('initNav', e);
},
opacityChange(e) {
this.opacity = e.opacity;
},
goBack() {
uni.navigateBack();
},
more() {
this.$emit('more');
},
}
}
</script>
<style lang="scss" scoped>
.nav-box {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
flex-direction: column;
font-size: 22rpx;
position: relative;
.title {
justify-content: center;
align-items: center;
text-align: center;
margin-bottom: 3px;
display: flex;
width: 100%;
.nickname {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: calc(100% - 440rpx);
font-size: 12px;
}
}
.online {
font-size: 10px;
line-height: 10px;
color: #303133;
display: flex;
align-items: center;
.time {
margin: 0 3px;
}
.text {
display: flex;
align-items: center;
}
}
.action {
position: absolute;
left: 0;
top: 0;
bottom: 0;
display: flex;
padding: 0 10px;
align-items: center;
width: 50px;
}
}
.sex-badge {
display: flex;
align-items: center;
justify-content: center;
background-color: #e03997;
color: #ffffff;
border-radius: 3px;
font-size: 8px;
height: 30rpx;
width: 30rpx;
margin: 0 10rpx;
}
.sex-man {
display: flex;
align-items: center;
justify-content: center;
background-color: #0081ff;
color: #ffffff;
border-radius: 3px;
font-size: 8px;
height: 30rpx;
width: 30rpx;
margin: 0 10rpx;
}
.more {
display: flex;
justify-content: center;
align-items: center;
.more-btn {
background-color: #fff;
padding: 0 7px;
width: 31px;
height: 15px;
border-radius: 0 0 3px 3px;
.line {
background-color: rgba(0, 0, 0, 0.1);
margin: 3px 0;
height: 1px;
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,385 @@
<template>
<view v-if="userId > 0">
<view v-if="item.sendType == 1">
<view class="tip-box" v-if="item.contentType == 1">
<text class="time">{{item.content}}</text>
</view>
<view class="tip-card" v-if="item.contentType == 2">
<!-- <image class="icon" src="@/static/images/help.png"></image> -->
<view class="content-box">
<view class="title">温馨提示</view>
<view class="content">{{item.content}}</view>
</view>
</view>
</view>
<view v-if="item.sendType == 2">
<view class="tip-box" v-if="showTime(item, messageIndex)">
<text class="time">{{showDayTime(item.createTime)}}</text>
</view>
<view class="chat-box left" v-if="item.sendUserId != userId">
<view class="avatar" @click="detail(item)">
<u-avatar width="80rpx" height="80rpx" :src="item.sendAvatar"></u-avatar>
</view>
<view class="content-box" v-if="item.contentType == 1">
<view class="content" >{{item.content}}</view>
</view>
<view class="content-box" v-if="item.contentType == 2">
<view class="content" >{{item.content}}</view>
<u-icon name="map-fill" color="#3D7EFF" size="40"></u-icon>
</view>
<view class="content voice" :class="item.id == playId ?'play':''" @click="playAudio(item)" v-if="item.contentType == 3">
<view class="icon">
<u-icon name="voice1" custom-prefix="iconfont" color="#080808" size="40"></u-icon>
</view>
<text>{{item.voiceSec}}</text>
</view>
<view v-if="item.contentType == 4" @click="showPic(item)" class="msg-img">
<u-image width="420rpx" border-radius="5px" height="220rpx" :fade="false" :src="item.content"></u-image>
</view>
<view v-if="item.contentType == 5" class="msg-img">
<u-image width="200rpx" height="200rpx" :fade="false" :src="item.content"></u-image>
</view>
</view>
<view class="chat-box right" v-if="item.sendUserId == userId">
<view class="content-box" v-if="item.contentType == 1">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="content" >{{item.content}}</view>
</view>
<view class="content-box" v-if="item.contentType == 2">
<u-icon name="map-fill" color="#3D7EFF" size="40"></u-icon>
<view class="content" >{{item.content}}</view>
</view>
<view class="content-box" v-if="item.contentType == 3">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="content voice" :class="item.id == playId ?'play':''" @click="playAudio(item)">
<text>{{item.voiceSec}}</text>
<view class="icon">
<u-icon name="chat_voice" custom-prefix="iconfont" color="#fff" size="40"></u-icon>
</view>
</view>
</view>
<view class="content-box" v-if="item.contentType == 4">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="msg-img" @click="showPic(item)">
<u-image width="420rpx" border-radius="5px" height="220rpx" :fade="false" :src="item.content"></u-image>
</view>
</view>
<view class="content-box" v-if="item.contentType == 5">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="msg-img">
<u-image width="200rpx" height="200rpx" :fade="false" :src="item.content"></u-image>
</view>
</view>
<view class="avatar" @click="detail(item)">
<u-avatar width="80rpx" height="80rpx" :src="item.sendAvatar"></u-avatar>
</view>
</view>
</view>
</view>
</template>
<script>
import dayjs from 'dayjs';
import isToday from "dayjs/plugin/isToday";
import isYesterday from "dayjs/plugin/isYesterday";
dayjs.extend(isToday);
dayjs.extend(isYesterday);
import sheep from '@/sheep';
export default {
props: {
item: {
type: Object,
default: {
createTime: 0,
},
},
// 消息索引
messageIndex: {
type: Number,
default: 0,
},
// 消息列表
messageList:{
type: Array,
default: () => [],
},
playId: {
type: Number,
default: 0,
},
},
data() {
return {
userId: 0,
}
},
created() {
var user = sheep.$store('user').userInfo;
this.userId = user.id;
},
methods: {
// 预览图片
showPic(e) {
uni.previewImage({
indicator: "none",
current: e.content,
urls: [e.content],
});
},
detail(e) {
sheep.$router.go('/pages/user/detail/index',{id: e.sendUserId});
},
playAudio(e) {
this.$emit('playAudio', e);
},
formatDate(date, format= 'YYYY-MM-DD HH:mm:ss') {
// 日期不存在,则返回空
if (!date) {
return ''
}
// 日期存在,则进行格式化
if (format === undefined) {
format = 'YYYY-MM-DD HH:mm:ss'
}
return dayjs(date).format(format);
},
showDayTime(datetime) {
if (!datetime) return "";
const weeks = [
"星期日",
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
];
// 当天 显示时分
if (dayjs(datetime).isToday()) {
return dayjs(datetime).format("HH:mm");
}
// 昨天 昨天+时分
if (dayjs(datetime).isYesterday()) {
return `昨天 ${dayjs(datetime).format("HH:mm")}`;
}
// 本周 显示周几+时分
if (dayjs().isSame(datetime, "week")) {
const weekIndex = dayjs().day();
return `${weeks[weekIndex]} ${dayjs(datetime).format("HH:mm")}`;
}
// 本周之前 显示年月日
if (dayjs(datetime).isBefore(new Date(), "week")) {
return dayjs(datetime).format("YYYY-MM-DD");
}
},
showTime(item, index) {
// 最后一个
var createTime = new Date().getTime();
if(index > 0){
var message = this.messageList[index + 1];
if(message){
createTime = message.createTime;
}
}
let dateString = dayjs(createTime).fromNow();
let nowDateString = dayjs(item.createTime).fromNow();
return dateString !== nowDateString;
},
}
}
</script>
<style lang="scss" scoped>
.chat-box {
padding: 6px 12px;
display: flex;
.content-box {
display: flex;
align-items: center;
}
}
.left {
.content {
border-radius: 0px 8px 8px;
width: fit-content;
background-color: #fff;
color: #080808;
padding: 8px;
margin: 8px;
text-align: justify;
max-width: 470rpx;
font-size: 11px;
font-family: Helvetica Neue, Helvetica, sans-serif;
align-items: center;
display: flex;
}
.msg-img {
margin: 8px;
}
.icon {
margin-right: 5px;
}
.play{
.icon {
color: #fff;
}
.icon:after
{
border-right: solid 3px #fff;
animation: other-play 1s linear infinite;
z-index: 9;
}
}
}
.right {
display: flex;
justify-content: flex-end;
.content {
border-radius: 8px 0px 8px 8px;
width: fit-content;
background-color: #00c8b0;
color: #080808;
padding: 8px;
margin: 8px;
color: #fff;
text-align: justify;
max-width: 470rpx;
font-size: 11px;
font-family: Helvetica Neue, Helvetica, sans-serif;
align-items: center;
display: flex;
}
.msg-img {
margin: 8px;
}
.icon {
margin-left: 5px;
}
.play{
.icon {
color: #fff;
}
.icon:after
{
border-left: solid 3px #00c8b0;
animation: my-play 1s linear infinite;
z-index: 9;
}
}
}
@keyframes my-play {
0% {
transform: translateX(80%);
}
100% {
transform: translateX(0%);
}
}
@keyframes other-play {
0% {
transform: translateX(-80%);
}
100% {
transform: translateX(0%);
}
}
.voice{
.icon{
font-size: 40upx;
display: flex;
align-items: center;
}
.icon:after
{
content:" ";
width: 20px;
height: 20px;
border-radius: 100%;
position: absolute;
box-sizing: border-box;
}
}
.avatar {
margin-top: 8px;
}
.tip-box {
display: flex;
justify-content: center;
padding: 7px 0;
padding: 7px 20px;
text-align: center;
.time {
color: #aaaaaa;
font-size: 10px;
}
}
.tip-card {
display: flex;
justify-content: center;
background-color: #fff;
border-radius: 3px;
padding: 7px;
margin: 12px;
text-align: justify;
.icon {
width: 60rpx;
height: 60rpx;
}
.content-box {
display: flex;
flex-direction: column;
flex: 1;
margin-left: 7px;
.title {
font-size: 11px;
color: #333333;
}
.content {
margin-top: 3px;
color: #aaaaaa;
font-size: 10px;
}
}
}
</style>

204
pages/im/index - 副本.vue Normal file
View File

@@ -0,0 +1,204 @@
<template>
<view>
<message-list @hidedKeyboard="hidedKeyboard">
<template #chat-bar="{keyboardHeight}">
<chat-bar ref="chatBar" :keyboardHeight="keyboardHeight" @openGift="openGift"></chat-bar>
</template>
</message-list>
<gift-bar ref="gift" @sendGift="sendGift"></gift-bar>
<view class="svga-box" :class="giftFlag ? 'svga-show': 'svga-hide'">
<c-svga ref="cSvgaRef" :canvasId='canvasId' :src="src" :loops='1' :auto-play="true" @frame='onFrame' @finished='onFinished' @percentage='onPercentage' @loaded='onLoaded'></c-svga>
<view class="close-btn" @click="closeSvga"></view>
</view>
</view>
</template>
<script>
import MessageList from '@/pages/im/components/messageList.vue';
import ChatBar from "@/components/chat-bar/chat-bar.vue"
import GiftBar from "@/components/gift-bar/gift-bar.vue"
export default {
components: {
MessageList,
ChatBar,
GiftBar,
},
data() {
return {
giftFlag: false,
src: 'https://cos.duopei.feiniaowangluo.com/1866/reward/gift/171241866882267KgIoSuLt.svga',
canvasId: 'myCanvas',
modal: true,
current: 0, // tabs组件的current值表示当前活动的tab选项
swiperCurrent: 0,
typeidx: 0,
currentTab: 1,
showMoreSheet: false,
modalButton: [{
text: "置顶该聊天",
type: "green",
plain: false
},
{
text: "删除该聊天",
type: "green",
plain: false
},
{
text: "设置免打扰",
type: "green",
plain: false
}],
itemList: [{
text: "删除",
color: "#2B2B2B"
}, {
text: "举报",
color: "#2B2B2B"
}, {
text: "分享",
color: "#2B2B2B"
}],
}
},
created() {
},
computed: {
swiperHeight: {
get() {
let that = this;
return ' calc( 100vh - 100rpx )';
},
set(v) {}
},
},
methods: {
hidedKeyboard() {
this.$refs.chatBar.hideDrawer();
},
toChat() {
this.$u.route({
url: 'pages/chat/index',
});
},
//调用此方法显示组件
showModal() {
this.modal = true;
},
//隐藏组件
hideModal() {
this.modal = false;
},
handleClick(e){
let index = e.index;
if (index === 0) {
} else {
}
this.hideModal();
},
openGift() {
this.$refs.gift.open();
},
sendGift(e) {
this.src = e.src;
this.$refs.cSvgaRef.call('startAnimation');
this.giftFlag = true;
},
onFinished() {
this.giftFlag = false;
console.log('动画停止播放时回调');
},
onFrame(frame) {//动画播放至某帧后回调
// console.log(frame);
},
onPercentage(percentage) { //动画播放至某进度后回调
// console.log(percentage);
},
onLoaded() {
this.$refs.cSvgaRef.call('setContentMode', 'AspectFill');
console.log('加载完成');
},
closeSvga() {
this.$refs.cSvgaRef.call('stopAnimation');
this.giftFlag = false;
}
}
}
</script>
<style lang="scss" scoped>
.container {
background-color: #fafafa;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
.nav-box {
display: flex;
justify-content: center;
align-items: center;
}
.tab-box {
display: flex;
justify-content: center;
align-items: center;
}
.tab-box .title-label {
height: 44px;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
color: #333333;
font-size: 28rpx;
}
.tab-box .title-label text {
font-weight: bold;
transition: all 0.3s;
}
.svga-box {
position: fixed;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .7);
.close-btn {
color: #fff;
font-size: 14px;
position: absolute;
z-index: 999999999;
//background: rgba(0, 0, 0, .5);
padding: 5px 10px;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
.svga-hide {
transform: translate(-100%, 0);
}
.svga-show {
transform: translate(0, 0);
}
</style>

194
pages/im/index.vue Normal file
View File

@@ -0,0 +1,194 @@
<template>
<view class="container page-app theme-light main-green font-1">
<layout v-if="groupId > 0 && receiveUserId > 0" @hidedKeyboard="hidedKeyboard" :isReconnecting="isReconnecting" :groupId="groupId" :userId="receiveUserId" ref="im">
<template #chat-bar="{keyboardHeight, user}">
<chat-bar ref="chatBar" :user="user" :keyboardHeight="keyboardHeight" @send="sendMessage" @openGift="openGift" @voiceOk="voiceOk" @getWeixin="getWeixin" @imageOk="imageOk"></chat-bar>
</template>
</layout>
<s-auth-modal />
<qrcode-modal />
</view>
</template>
<script>
import Layout from '@/pages/im/components/layout.vue';
import ChatBar from "@/pages/im/components/chatBar.vue";
import ImMessageApi from '@/sheep/api/im/memberMessage';
import qrcodeModal from '@/components/qrcode-modal/qrcode-modal.vue';
import FileApi from '@/sheep/api/infra/file';
import UserApi from '@/sheep/api/member/user';
import sheep from '@/sheep';
import { WxaSubscribeTemplate, WebSocketMessageTypeConstants } from '@/sheep/util/const';
import { useWebSocket } from '@/sheep/hooks/useWebSocket';
export default {
components: {
Layout,
ChatBar,
qrcodeModal,
},
data() {
return {
groupId: 0,
receiveUserId: 0,
Reconnect: {},
}
},
onLoad(option) {
this.groupId = option.groupId;
this.receiveUserId = option.receiveUserId;
},
onShow() {
//======================= 聊天工具相关 end =======================
const { options } = useWebSocket({
// 连接成功
onConnected: async () => {
console.log('连接成功');
},
// 收到消息
onMessage: async (data) => {
console.log('接收消息');
const type = data.type;
if (!type) {
console.error('未知的消息类型:' + data.value);
return;
}
if (type == WebSocketMessageTypeConstants.IM_MESSAGE_READ) {
console.log('刷新消息');
var that = this;
setTimeout(function(){
var message = JSON.parse(data.content);
if(message.groupId != that.groupId){
return;
}
that.$nextTick(function() {
that.$refs.im.reload(message);
});
},1000);
return;
}
if (type == WebSocketMessageTypeConstants.IM_MESSAGE_NEWS) {
var message = JSON.parse(data.content);
if(message.groupId != this.groupId){
return;
}
ImMessageApi.updateReadStatus(message.id);
this.$nextTick(function() {
this.$refs.im.reload(message);
});
return;
}
// 2.2 消息类型KEFU_MESSAGE_TYPE
if (type == WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
console.log('刷新消息');
// 刷新消息列表
//await messageListRef.value.refreshMessageList(JSON.parse(data.content));
return;
}
// 2.3 消息类型KEFU_MESSAGE_ADMIN_READ
if (type == WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
console.log('管理员已读消息');
}
},
});
this.Reconnect = options; // 重连状态
},
computed: {
isReconnecting() {
return this.Reconnect.isReconnecting;
},
},
onHide() {
},
methods: {
hidedKeyboard() {
this.$refs.chatBar.hideDrawer();
},
sendMessage(e) {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.autoSubscribeMessage();
// #endif
ImMessageApi.sendImMessage({
groupId: this.groupId,
receiveUserId: this.receiveUserId,
contentType: e.contentType,
content: e.content,
voiceSec: e.voiceSec,
imgWidth: e.imgWidth,
imgHeight: e.imgHeight,
}).then(res => {
this.$refs.im.reload();
});
},
async imageOk(e) {
const res = await FileApi.uploadFile(e.path);
var data = {
contentType: 4,
content: res.data,
imgWidth: e.imgWidth,
imgHeight: e.imgHeight,
}
this.sendMessage(data);
},
openGift() {
sheep.$router.go('/pages/user/detail/index',{id: this.receiveUserId});
},
getWeixin() {
UserApi.getQrcodeByUserId(this.receiveUserId).then(res => {
if(res.data){
uni.previewImage({
current: 0, //预览图片的下标
urls: [res.data], //预览图片的地址,必须要数组形式,如果不是数组形式就转换成数组形式就可以
indicator: 'number',
loop: true
});
}
});
},
showDayTime(datetime) {
if (!datetime) return "";
return dayjs(datetime).fromNow();
},
async voiceOk(e) {
const res = await FileApi.uploadFile(e.path);
var data = {
contentType: 3,
content: res.data,
voiceSec: e.sec,
}
this.sendMessage(data);
},
subscribeMessage() {
const event = [WxaSubscribeTemplate.UNREAD_MESSAGE];
event.push(WxaSubscribeTemplate.CLERK_BLIND);
event.push(WxaSubscribeTemplate.CLERK_ORDER);
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
// 订阅后记录一下订阅状态
uni.removeStorageSync(WxaSubscribeTemplate.UNREAD_MESSAGE);
uni.setStorageSync(WxaSubscribeTemplate.UNREAD_MESSAGE, '已订阅');
});
},
async autoSubscribeMessage() {
// 2. 订阅消息
this.subscribeMessage();
},
}
}
</script>
<style lang="scss" scoped>
.container {
background-color: #fafafa;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

33
pages/im/util/emoji.js Normal file
View File

@@ -0,0 +1,33 @@
export default [
"😀", "😁", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍",
"😘", "😗", "😙", "😚", "😇", "😐", "😑", "😶", "😏", "😣", "😥", "😮", "😯", "😪",
"😫", "😴", "😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "😲", "😷", "😖", "😞", "😟",
"😤", "😢", "😭", "😦", "😧", "😨", "😬", "😰", "😱", "😳", "😵", "😡", "😠",
"👦", "👧", "👨", "👩", "👴", "👵", "👶", "👱", "👮", "👲", "👳", "👷", "👸", "💂", "🎅", "👰", "👼",
"💆", "💇", "🙍", "🙎", "🙅", "🙆", "💁", "🙋", "🙇", "🙌", "🙏", "👤", "👥", "🚶", "🏃", "👯",
"💃", "👫", "👬", "👭", "💏", "💑", "👪", "💪", "👈", "👉", "☝", "👆", "👇", "✌", "✋", "👌",
"👍", "👎", "✊", "👊", "👋", "👏", "👐", "✍", "👣", "👀", "👂", "👃", "👅", "👄", "💋", "👓",
"👔", "👙", "👛", "👜", "👝", "🎒", "💼", "👞", "👟", "👠", "👡", "👢", "👑",
"👒", "🎩", "🎓", "💄", "💅", "💍", "🌂", "📶", "📳", "📴", "♻", "🏧","🚮", "🚰", "♿", "🚹", "🚺",
"🚻", "🚼", "🚾", "⚠", "🚸", "⛔", "🚫", "🚳", "🚭", "🚯", "🚱", "🚷", "🔞", "💈",
"🙈", "🐒", "🐶", "🐕", "🐩", "🐺", "🐱","🐈", "🐯", "🐅", "🐆", "🐴", "🐎", "🐮", "🐂",
"🐃","🐄","🐷","🐖","🐗","🐽","🐏","🐑","🐐","🐪","🐫","🐘","🐭",
"🐁","🐀","🐹","🐰","🐇","🐻","🐨","🐼","🐾","🐔","🐓","🐣","🐤","🐥",
"🐦", "🐧", "🐸", "🐊","🐢", "🐍", "🐲", "🐉", "🐳", "🐋", "🐬", "🐟", "🐠", "🐡",
"🐙", "🐚", "🐌", "🐛", "🐜", "🐝", "🐞", "🦋", "💐", "🌸", "💮", "🌹", "🌺",
"🌻", "🌼", "🌷", "🌱", "🌲", "🌳", "🌴", "🌵", "🌾", "🌿", "🍀", "🍁", "🍂", "🍃",
"🌍","🌎","🌏","🌐","🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘","🌙","🌚",
"🌛","🌜","☀","🌝","🌞","⭐","🌟","🌠","☁","⛅","☔","⚡","❄","🔥","💧","🌊",
"🏀", "🏈", "🏉", "🎾", "🎱", "🎳", "⛳", "🎣", "🎽", "🎿",
"😈", "👿", "👹", "👺", "💀", "☠", "👻", "👽", "👾", "💣",
"🌋", "🗻", "🏠", "🏡", "🏢", "🏣", "🏤", "🏥", "🏦", "🏨",
"⛲", "🌁", "🌃", "🌆", "🌇", "🎠", "🎡", "🎢", "🚂",
"🚌", "🚍", "🚎", "🚏", "🚐", "🚑", "🚒", "🚓", "🚔", "🚕", "🚖", "🚗", "🚘",
"💌", "💎", "🔪", "💈", "🚪", "🚽", "🚿", "🛁", "⌛", "⏳", "⌚", "⏰", "🎈", "🎉",
"💤", "💢", "💬", "💭", "♨", "🌀", "🔔", "🔕", "✡", "✝", "🔯", "📛", "🔰", "🔱", "⭕", "✅",
"☑", "✔", "✖", "❌", "❎", "", "", "➗", "➰", "➿", "〽", "✳", "✴", "❇", "‼", "⁉", "❓", "❔", "❕", "❗",
"🕛", "🕧", "🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡",
"🕖", "🕢", "🕗", "🕣", "🕘", "🕤", "🕙", "🕥", "🕚", "🕦", "⏱", "⏲", "🕰",
"💘", "❤", "💓", "💔", "💕", "💖", "💗", "💙", "💚", "💛", "💜", "💝", "💞", "💟❣",
"🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍", "🍎", "🍏", "🍐", "🍑", "🍒", "🍓",
]

192
pages/index/cart.vue Normal file
View File

@@ -0,0 +1,192 @@
<template>
<s-layout title="购物车" tabbar="/pages/index/cart" :bgStyle="{ color: '#fff' }">
<s-empty v-if="state.list.length === 0" text="购物车空空如也,快去逛逛吧~" icon="/static/cart-empty.png" />
<!-- 头部 -->
<view class="cart-box ss-flex ss-flex-col ss-row-between" v-if="state.list.length">
<view class="cart-header ss-flex ss-col-center ss-row-between ss-p-x-30">
<view class="header-left ss-flex ss-col-center ss-font-26">
<text class="goods-number ui-TC-Main ss-flex">{{ state.list.length }}</text>
件商品
</view>
<view class="header-right">
<button v-if="state.editMode" class="ss-reset-button" @tap="state.editMode = false">
取消
</button>
<button v-else class="ss-reset-button ui-TC-Main" @tap="state.editMode = true">
编辑
</button>
</view>
</view>
<!-- 内容 -->
<view class="cart-content ss-flex-1 ss-p-x-30 ss-m-b-40">
<view class="goods-box ss-r-10 ss-m-b-14" v-for="item in state.list" :key="item.id">
<view class="ss-flex ss-col-center">
<label class="check-box ss-flex ss-col-center ss-p-l-10" @tap="onSelectSingle(item.id)">
<radio :checked="state.selectedIds.includes(item.id)" color="var(--ui-BG-Main)"
style="transform: scale(0.8)" @tap.stop="onSelectSingle(item.id)" />
</label>
<s-goods-item :title="item.spu.name" :img="item.spu.picUrl || item.goods.image"
:price="item.sku.price"
:skuText="item.sku.properties.length>1? item.sku.properties.reduce((items2,items)=>items2.valueName+' '+items.valueName):item.sku.properties[0].valueName"
priceColor="#FF3000" :titleWidth="400">
<template v-if="!state.editMode" v-slot:tool>
<su-number-box :min="0" :max="item.sku.stock" :step="1" v-model="item.count" @change="onNumberChange($event, item)" />
</template>
</s-goods-item>
</view>
</view>
</view>
<!-- 底部 -->
<su-fixed bottom :val="48" placeholder v-if="state.list.length > 0" :isInset="false">
<view class="cart-footer ss-flex ss-col-center ss-row-between ss-p-x-30 border-bottom">
<view class="footer-left ss-flex ss-col-center">
<label class="check-box ss-flex ss-col-center ss-p-r-30" @tap="onSelectAll">
<radio :checked="state.isAllSelected" color="var(--ui-BG-Main)"
style="transform: scale(0.8)" @tap.stop="onSelectAll" />
<view class="ss-m-l-8"> 全选 </view>
</label>
<text>合计</text>
<view class="text-price price-text">
{{ fen2yuan(state.totalPriceSelected) }}
</view>
</view>
<view class="footer-right">
<button v-if="state.editMode" class="ss-reset-button ui-BG-Main-Gradient pay-btn ui-Shadow-Main"
@tap="onDelete">
删除
</button>
<button v-else class="ss-reset-button ui-BG-Main-Gradient pay-btn ui-Shadow-Main"
@tap="onConfirm">
去结算
{{ state.selectedIds?.length ? `(${state.selectedIds.length})` : '' }}
</button>
</view>
</view>
</su-fixed>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive } from 'vue';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const sys_navBar = sheep.$platform.navbar;
const cart = sheep.$store('cart');
const state = reactive({
editMode: false,
list: computed(() => cart.list),
selectedList: [],
selectedIds: computed(() => cart.selectedIds),
isAllSelected: computed(() => cart.isAllSelected),
totalPriceSelected: computed(() => cart.totalPriceSelected),
});
// 单选选中
function onSelectSingle(id) {
cart.selectSingle(id);
}
// 全选
function onSelectAll() {
cart.selectAll(!state.isAllSelected);
}
// 结算
function onConfirm() {
let items = []
let goods_list = [];
state.selectedList = state.list.filter((item) => state.selectedIds.includes(item.id));
state.selectedList.map((item) => {
// 此处前端做出修改
items.push({
skuId: item.sku.id,
count: item.count,
cartId: item.id,
categoryId: item.spu.categoryId
})
goods_list.push({
// goods_id: item.goods_id,
goods_id: item.spu.id,
// goods_num: item.goods_num,
goods_num: item.count,
// 商品价格id真没有
// goods_sku_price_id: item.goods_sku_price_id,
});
});
// return;
if (goods_list.length === 0) {
sheep.$helper.toast('请选择商品');
return;
}
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
items
}),
});
}
function onNumberChange(e, cartItem) {
if (e === 0) {
cart.delete(cartItem.id);
return;
}
if (cartItem.goods_num === e) return;
cartItem.goods_num = e;
cart.update({
goods_id: cartItem.id,
goods_num: e,
goods_sku_price_id: cartItem.goods_sku_price_id,
});
}
async function onDelete() {
cart.delete(state.selectedIds);
}
</script>
<style lang="scss" scoped>
:deep(.ui-fixed) {
height: 72rpx;
}
.cart-box {
width: 100%;
.cart-header {
height: 70rpx;
background-color: #f6f6f6;
width: 100%;
position: fixed;
left: 0;
top: v-bind('sys_navBar') rpx;
z-index: 1000;
box-sizing: border-box;
}
.cart-footer {
height: 100rpx;
background-color: #fff;
.pay-btn {
width: 180rpx;
height: 70rpx;
font-size: 28rpx;
line-height: 28rpx;
font-weight: 500;
border-radius: 40rpx;
}
}
.cart-content {
margin-top: 70rpx;
.goods-box {
background-color: #fff;
}
}
}
</style>

237
pages/index/category.vue Normal file
View File

@@ -0,0 +1,237 @@
<!-- 商品分类列表 -->
<template>
<s-layout title="分类" tabbar="/pages/index/category" :bgStyle="{ color: '#fff' }">
<view class="s-category">
<view class="three-level-wrap ss-flex ss-col-top" :style="[{ height: pageHeight + 'px' }]">
<!-- 商品分类 -->
<scroll-view class="side-menu-wrap" scroll-y :style="[{ height: pageHeight + 'px' }]">
<view
class="menu-item ss-flex"
v-for="(item, index) in state.categoryList"
:key="item.id"
:class="[{ 'menu-item-active': index === state.activeMenu }]"
@tap="onMenu(index)"
>
<view class="menu-title ss-line-1">
{{ item.name }}
</view>
</view>
</scroll-view>
<!-- 商品分类 -->
<scroll-view
class="goods-list-box"
scroll-y
:style="[{ height: pageHeight + 'px' }]"
v-if="state.categoryList?.length"
>
<image
v-if="state.categoryList[state.activeMenu].picUrl"
class="banner-img"
:src="sheep.$url.cdn(state.categoryList[state.activeMenu].picUrl)"
mode="widthFix"
/>
<first-one v-if="state.style === 'first_one'" :pagination="state.pagination" />
<first-two v-if="state.style === 'first_two'" :pagination="state.pagination" />
<second-one
v-if="state.style === 'second_one'"
:data="state.categoryList"
:activeMenu="state.activeMenu"
/>
<uni-load-more
v-if="
(state.style === 'first_one' || state.style === 'first_two') &&
state.pagination.total > 0
"
:status="state.loadStatus"
:content-text="{
contentdown: '点击查看更多',
}"
@tap="loadMore"
/>
</scroll-view>
</view>
</view>
</s-layout>
</template>
<script setup>
import secondOne from './components/second-one.vue';
import firstOne from './components/first-one.vue';
import firstTwo from './components/first-two.vue';
import sheep from '@/sheep';
import CategoryApi from '@/sheep/api/product/category';
import SpuApi from '@/sheep/api/product/spu';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
import _ from 'lodash-es';
import { handleTree } from '@/sheep/util';
const state = reactive({
style: 'second_one', // first_one一级 - 样式一), first_two二级 - 样式二), second_one二级
categoryList: [], // 商品分类树
activeMenu: 0, // 选中的一级菜单,在 categoryList 的下标
pagination: {
// 商品分页
list: [], // 商品列表
total: [], // 商品总数
pageNo: 1,
pageSize: 6,
},
loadStatus: '',
});
const { safeArea } = sheep.$platform.device;
const pageHeight = computed(() => safeArea.height - 44 - 50);
// 加载商品分类
async function getList() {
const { code, data } = await CategoryApi.getCategoryList();
if (code !== 0) {
return;
}
state.categoryList = handleTree(data);
}
// 选中菜单
const onMenu = (val) => {
state.activeMenu = val;
if (state.style === 'first_one' || state.style === 'first_two') {
state.pagination.pageNo = 1;
state.pagination.list = [];
state.pagination.total = 0;
getGoodsList();
}
};
// 加载商品列表
async function getGoodsList() {
// 加载列表
state.loadStatus = 'loading';
const res = await SpuApi.getSpuPage({
categoryId: state.categoryList[state.activeMenu].id,
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
if (res.code !== 0) {
return;
}
// 合并列表
state.pagination.list = _.concat(state.pagination.list, res.data.list);
state.pagination.total = res.data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 加载更多商品
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGoodsList();
}
onLoad(async (params) => {
await getList();
// 首页点击分类的处理:查找满足条件的分类
const foundCategory = state.categoryList.find(category => category.id === params.id);
// 如果找到则调用 onMenu 自动勾选相应分类,否则调用 onMenu(0) 勾选第一个分类
onMenu(foundCategory ? state.categoryList.indexOf(foundCategory) : 0);
});
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.s-category {
:deep() {
.side-menu-wrap {
width: 200rpx;
height: 100%;
padding-left: 12rpx;
background-color: #f6f6f6;
.menu-item {
width: 100%;
height: 88rpx;
position: relative;
transition: all linear 0.2s;
.menu-title {
line-height: 32rpx;
font-size: 30rpx;
font-weight: 400;
color: #333;
margin-left: 28rpx;
position: relative;
z-index: 0;
&::before {
content: '';
width: 64rpx;
height: 12rpx;
background: linear-gradient(
90deg,
var(--ui-BG-Main-gradient),
var(--ui-BG-Main-light)
) !important;
position: absolute;
left: -64rpx;
bottom: 0;
z-index: -1;
transition: all linear 0.2s;
}
}
&.menu-item-active {
background-color: #fff;
border-radius: 20rpx 0 0 20rpx;
&::before {
content: '';
position: absolute;
right: 0;
bottom: -20rpx;
width: 20rpx;
height: 20rpx;
background: radial-gradient(circle at 0 100%, transparent 20rpx, #fff 0);
}
&::after {
content: '';
position: absolute;
top: -20rpx;
right: 0;
width: 20rpx;
height: 20rpx;
background: radial-gradient(circle at 0% 0%, transparent 20rpx, #fff 0);
}
.menu-title {
font-weight: 600;
&::before {
left: 0;
}
}
}
}
}
.goods-list-box {
background-color: #fff;
width: calc(100vw - 100px);
padding: 10px;
}
.banner-img {
width: calc(100vw - 130px);
border-radius: 5px;
margin-bottom: 20rpx;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More