lhl 2 months ago
parent
commit
22a9586fca
7 changed files with 643 additions and 32 deletions
  1. 16 0
      api/user.js
  2. 13 7
      pages.json
  3. 1 0
      pages/public/agreement.vue
  4. 508 0
      pages/set/kf/index.vue
  5. 8 0
      pages/set/kf/order.vue
  6. 72 25
      pages/user/jf/rechange.vue
  7. 25 0
      pages/user/model/model.vue

+ 16 - 0
api/user.js

@@ -492,4 +492,20 @@ export function getHonor(data) {
 		method: 'get',
 		data
 	})
+}
+
+export function getChatConfig() {
+	return request({
+		url: '/api/chat/config',
+		method: 'get',
+	})
+}
+
+export function buyChatCount(data) {
+	console.log(data,'data');
+	return request({
+		url: '/api/chat/createOrder',
+		method: 'post',
+		data
+	})
 }

+ 13 - 7
pages.json

@@ -262,13 +262,13 @@
 						"navigationBarTitleText": "商品详情"
 					}
 				}
-				// ,
-				// {
-				// 	"path": "jf/rechange",
-				// 	"style": {
-				// 		"navigationBarTitleText": "次数购买"
-				// 	}
-				// }
+				,
+				{
+					"path": "jf/rechange",
+					"style": {
+						"navigationBarTitleText": "次数购买"
+					}
+				}
 				,
 				{
 					"path": "jf/store",
@@ -369,6 +369,12 @@
 					"style": {
 						"navigationBarTitleText": "修改资料"
 					}
+				},
+				{
+					"path": "kf/index",
+					"style": {
+						"navigationBarTitleText": ""
+					}
 				}
 			]
 		}, {

+ 1 - 0
pages/public/agreement.vue

@@ -27,6 +27,7 @@
 		methods: {
 
 			checkedChange() {
+				console.log('zheli');
 				this.checked = !this.checked;
 				this.$emit('checkedChange', this.checked)
 			}

+ 508 - 0
pages/set/kf/index.vue

@@ -0,0 +1,508 @@
+<template>
+	<view class="chat-container">
+		<view class="header">
+			<text class="title">{{name || '在线客服'}}</text>
+			<view class="status">
+				<text class="status-dot" :class="{ 'online': isConnected }"></text>
+				<text class="status-text">{{ isConnected ? '已连接' : '连接中...' }}</text>
+			</view>
+		</view>
+		
+		<scroll-view 
+			class="message-list" 
+			scroll-y 
+			:scroll-into-view="scrollToView"
+			:scroll-with-animation="true"
+		>
+			<view class="message-item" v-for="(msg, index) in messages" :key="index" :id="'msg-' + index">
+				<view v-if="msg.type === 'text'" class="message-bubble" :class="msg.from === 'me' ? 'right' : 'left'">
+					<view class="avatar">
+						<image v-if="msg.from === 'me' && userInfo.avatar" :src="userInfo.avatar" mode="aspectFill"></image>
+						<view v-else :class="msg.from === 'me' ? 'my-avatar' : 'service-avatar'">
+							<text>{{ msg.from === 'me' ? '我' : '客' }}</text>
+						</view>
+					</view>
+					<view class="bubble-content">
+						<text>{{ msg.content }}</text>
+					</view>
+				</view>
+				
+				<view v-else-if="msg.type === 'image'" class="message-bubble" :class="msg.from === 'me' ? 'right' : 'left'">
+					<view class="avatar">
+						<image v-if="msg.from === 'me' && userInfo.avatar" :src="userInfo.avatar" mode="aspectFill"></image>
+						<view v-else :class="msg.from === 'me' ? 'my-avatar' : 'service-avatar'">
+							<text>{{ msg.from === 'me' ? '我' : '客' }}</text>
+						</view>
+					</view>
+					<view class="bubble-image" @click="previewImage(msg.content)">
+						<image :src="msg.content" mode="widthFix" @tap="previewImage(msg.content)"></image>
+					</view>
+				</view>
+				
+				<view v-else-if="msg.type === 'system'" class="system-message">
+					<text>{{ msg.content }}</text>
+				</view>
+			</view>
+		</scroll-view>
+		
+		<view class="input-area">
+			<view class="input-wrapper">
+				<input 
+					class="input" 
+					type="text" 
+					v-model="inputText" 
+					placeholder="请输入消息..." 
+					placeholder-class="placeholder"
+					@confirm="sendText"
+				/>
+			</view>
+			<view class="action-buttons">
+				<view class="btn" @click="sendImage">
+					<text class="btn-icon">📷</text>
+				</view>
+				<view class="btn send-btn" :class="{ 'disabled': !inputText.trim() }" @click="sendText">
+					<text class="btn-text">发送</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		mapState,
+	} from 'vuex';
+	import {
+		upload
+	} from '@/api/set.js';
+	
+	export default {
+		data() {
+			return {
+				name: '',
+				socketTask: null,
+				isConnected: false,
+				reconnectCount: 0,
+				maxReconnect: 3,
+				messages: [],
+				inputText: '',
+				scrollToView: '',
+				wsUrl: 'ws://api.myje.cn:2345'
+			}
+		},
+		onLoad(opt) {
+			this.addSystemMessage('正在连接服务器...');
+			this.name = opt.name
+			this.connectWebSocket(opt.to_uid);
+		},
+		onUnload() {
+			this.closeWebSocket();
+		},
+		computed: {
+			...mapState('user', ['userInfo'])
+		},
+		methods: {
+			connectWebSocket(to_uid) {
+				const token = uni.getStorageSync('token') || '';
+				const wsUrl =`${this.wsUrl}?token=${token}` + '&type=user&from=3&to_uid=' + to_uid;
+				
+				this.socketTask = uni.connectSocket({
+					url: wsUrl,
+					success: () => {
+						console.log('WebSocket连接请求成功');
+					},
+					fail: (err) => {
+						console.log('WebSocket连接失败', err);
+						this.handleReconnect();
+					}
+				});
+				
+				this.socketTask.onOpen(() => {
+					console.log('WebSocket连接成功');
+					this.isConnected = true;
+					this.reconnectCount = 0;
+					this.addSystemMessage('已连接到服务器');
+				});
+				
+				this.socketTask.onMessage((res) => {
+					this.handleMessage(res.data);
+				});
+				
+				this.socketTask.onClose(() => {
+					console.log('WebSocket连接关闭');
+					this.isConnected = false;
+					this.handleReconnect();
+				});
+				
+				this.socketTask.onError((err) => {
+					console.log('WebSocket错误', err);
+					this.isConnected = false;
+					this.addSystemMessage('连接发生错误');
+				});
+			},
+			
+			handleReconnect() {
+				if (this.reconnectCount < this.maxReconnect) {
+					this.reconnectCount++;
+					this.addSystemMessage(`正在重连... (${this.reconnectCount}/${this.maxReconnect})`);
+					setTimeout(() => {
+						this.connectWebSocket();
+					}, 2000);
+				} else {
+					this.addSystemMessage('连接失败,请稍后重试');
+				}
+			},
+			
+			closeWebSocket() {
+				if (this.socketTask) {
+					this.socketTask.close();
+					this.socketTask = null;
+				}
+			},
+			
+			handleMessage(data) {
+				try {
+					const message = typeof data === 'string' ? JSON.parse(data) : data;
+					this.addMessage(message);
+				} catch (e) {
+					this.addMessage({
+						from: 'service',
+						type: 'text',
+						content: data
+					});
+				}
+			},
+			
+			addMessage(msg) {
+				this.messages.push({
+					from: msg.from || 'service',
+					type: msg.type || 'text',
+					content: msg.content,
+					time: new Date().getTime()
+				});
+				this.scrollToBottom();
+			},
+			
+			addSystemMessage(content) {
+				this.messages.push({
+					type: 'system',
+					content: content,
+					time: new Date().getTime()
+				});
+				this.scrollToBottom();
+			},
+			
+			sendText() {
+				if (!this.inputText.trim()) {
+					return;
+				}
+				
+				const text = this.inputText.trim();
+				
+				if (!this.isConnected) {
+					uni.showToast({
+						title: '连接未就绪',
+						icon: 'none'
+					});
+					return;
+				}
+				
+				const message = {
+					type: 'text',
+					content: text
+				};
+				
+				this.socketTask.send({
+					data: JSON.stringify(message),
+					success: () => {
+						this.addMessage({
+							from: 'me',
+							type: 'text',
+							content: text
+						});
+						this.inputText = '';
+					},
+					fail: () => {
+						uni.showToast({
+							title: '发送失败',
+							icon: 'none'
+						});
+					}
+				});
+			},
+			
+			sendImage() {
+				if (!this.isConnected) {
+					uni.showToast({
+						title: '连接未就绪',
+						icon: 'none'
+					});
+					return;
+				}
+				
+				upload({
+					filename: ''
+				}).then(data => {
+					const imageUrl = data[0].img;
+					
+					const message = {
+						type: 'image',
+						content: imageUrl
+					};
+					
+					this.socketTask.send({
+						data: JSON.stringify(message),
+						success: () => {
+							this.addMessage({
+								from: 'me',
+								type: 'image',
+								content: imageUrl
+							});
+						},
+						fail: () => {
+							uni.showToast({
+								title: '发送失败',
+								icon: 'none'
+							});
+						}
+					});
+				}).catch(err => {
+					console.log('上传图片失败', err);
+				});
+			},
+			
+			previewImage(url) {
+				uni.previewImage({
+					urls: [url],
+					current: url
+				});
+			},
+			
+			scrollToBottom() {
+				this.$nextTick(() => {
+					const index = this.messages.length - 1;
+					if (index >= 0) {
+						this.scrollToView = 'msg-' + index;
+					}
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.chat-container {
+		display: flex;
+		flex-direction: column;
+		height: 100vh;
+		background-color: $page-color-base;
+	}
+	
+	.header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		padding: 20rpx 30rpx;
+		background-color: #FFFFFF;
+		border-bottom: 1px solid $border-color-light;
+		
+		.title {
+			font-size: $font-lg;
+			color: $font-color-dark;
+			font-weight: bold;
+		}
+		
+		.status {
+			display: flex;
+			align-items: center;
+			
+			.status-dot {
+				width: 16rpx;
+				height: 16rpx;
+				border-radius: 50%;
+				background-color: $font-color-light;
+				margin-right: 10rpx;
+				
+				&.online {
+					background-color: $color-green;
+				}
+			}
+			
+			.status-text {
+				font-size: $font-sm;
+				color: $font-color-light;
+			}
+		}
+	}
+	
+	.message-list {
+		flex: 1;
+		padding: 20rpx;
+		overflow-y: auto;
+	}
+	
+	.message-item {
+		margin-bottom: 20rpx;
+		
+		.message-bubble {
+			display: flex;
+			align-items: flex-start;
+			
+			&.right {
+				flex-direction: row-reverse;
+				
+				.avatar {
+					margin-left: 16rpx;
+					margin-right: 0;
+				}
+				
+				.bubble-content {
+					background-color: $base-color;
+					color: #FFFFFF;
+					border-radius: 16rpx 0 16rpx 16rpx;
+				}
+			}
+			
+			&.left {
+				.avatar {
+					margin-right: 16rpx;
+				}
+				
+				.bubble-content {
+					background-color: #FFFFFF;
+					color: $font-color-dark;
+					border-radius: 0 16rpx 16rpx 16rpx;
+				}
+			}
+			
+			.avatar {
+				width: 80rpx;
+				height: 80rpx;
+				flex-shrink: 0;
+				
+				image {
+					width: 100%;
+					height: 100%;
+					border-radius: 50%;
+					background-color: $image-bg-color;
+				}
+				
+				.my-avatar, .service-avatar {
+					width: 100%;
+					height: 100%;
+					border-radius: 50%;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					
+					text {
+						color: #FFFFFF;
+						font-size: $font-sm;
+					}
+				}
+				
+				.my-avatar {
+					background-color: $base-color;
+				}
+				
+				.service-avatar {
+					background-color: $color-green;
+				}
+			}
+			
+			.bubble-content {
+				max-width: 60%;
+				padding: 20rpx 24rpx;
+				font-size: $font-base;
+				line-height: 1.6;
+				word-break: break-all;
+			}
+			
+			.bubble-image {
+				max-width: 60%;
+				
+				image {
+					max-width: 100%;
+					max-height: 400rpx;
+					border-radius: 16rpx;
+				}
+			}
+		}
+		
+		.system-message {
+			text-align: center;
+			padding: 16rpx;
+			
+			text {
+				font-size: $font-sm;
+				color: $font-color-light;
+				background-color: rgba(0, 0, 0, 0.06);
+				padding: 8rpx 20rpx;
+				border-radius: 20rpx;
+			}
+		}
+	}
+	
+	.input-area {
+		display: flex;
+		align-items: center;
+		padding: 20rpx 30rpx;
+		background-color: #FFFFFF;
+		border-top: 1px solid $border-color-light;
+		
+		.input-wrapper {
+			flex: 1;
+			margin-right: 20rpx;
+			
+			.input {
+				width: 100%;
+				height: 76rpx;
+				padding: 0 24rpx;
+				background-color: $page-color-base;
+				border-radius: 38rpx;
+				font-size: $font-base;
+			}
+			
+			.placeholder {
+				color: $font-color-light;
+			}
+		}
+		
+		.action-buttons {
+			display: flex;
+			align-items: center;
+			
+			.btn {
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				width: 76rpx;
+				height: 76rpx;
+				margin-right: 16rpx;
+				background-color: $page-color-base;
+				border-radius: 50%;
+				
+				&.send-btn {
+					width: auto;
+					padding: 0 30rpx;
+					background-color: $base-color;
+					border-radius: 38rpx;
+					margin-right: 0;
+					
+					&.disabled {
+						opacity: 0.5;
+					}
+					
+					.btn-text {
+						font-size: $font-base;
+						color: #FFFFFF;
+					}
+				}
+				
+				.btn-icon {
+					font-size: 36rpx;
+				}
+			}
+		}
+	}
+</style>

+ 8 - 0
pages/set/kf/order.vue

@@ -0,0 +1,8 @@
+<template>
+</template>
+
+<script>
+</script>
+
+<style>
+</style>

+ 72 - 25
pages/user/jf/rechange.vue

@@ -1,8 +1,13 @@
 <template>
 	<view class="content">
-		<view class="remain">
-			<text class="label">我的剩余次数:</text>
-			<text class="count">0</text>
+		<view class="flex" style="background-color: #fff;">
+			<view class="remain">
+				<text class="label">我的剩余次数:</text>
+				<text class="count">{{chatConfig.balance}}</text>
+			</view>
+			<view class="remain">
+				<text class="labels">我的订单</text>
+			</view>
 		</view>
 		<view class="listBox">
 			<view class="custom">
@@ -11,9 +16,9 @@
 			</view>
 			
 			<view class="list flex">
-				<view @click="changeCount(index)" :class="{action:count==item}" class="item flex" 
-					v-for="(item,index) in countList">
-					<text>{{item}}次</text>
+				<view :class="{action:count==item}" class="item flex" 
+					v-for="(item,index) in countList" :key="index" @click="changeCount(index)">
+					<text  >{{item}}次</text>
 				</view>
 			</view>
 			
@@ -24,7 +29,7 @@
 		
 		<view class="points-info" @click="navToRecharge">
 			<view class="left flex">
-				<text>积分支付(余额:300)</text>
+				<text>积分支付(余额:{{ userInfo.score|| '0'}})</text>
 			</view>
 			<view class="right flex">
 				<text>充值积分</text>
@@ -37,21 +42,42 @@
 </template>
 
 <script>
+	import { mapState,mapMutations } from 'vuex'
+	import { getChatConfig, buyChatCount,getUserInfo } from '@/api/user.js'
 	export default {
 		data() {
 			return {
+				chatConfig: {
+					balance: 0,
+					price: 0
+				},
 				actionIndex: 0, //当前选中的次数索引
 				count: 10, //购买次数
-				countList: [10, 20, 30, 40, 50, 60], //预设次数选项
+				countList: [1, 3, 5, 10, 30, 50], //预设次数选项
 				pointsPerCount: 20, //每次需要的积分
 			};
 		},
 		computed: {
+			...mapState('user', ['userInfo']),
 			estimatedPoints() {
-				return this.count * this.pointsPerCount;
+				return this.count * this.chatConfig.price;
 			}
 		},
+		onLoad() {
+			this.getChatConfig()
+		},
 		methods: {
+			...mapMutations('user',['setUserInfo']),
+			getUserInfo() {
+				getUserInfo().then(res => {
+					this.setUserInfo(res.data)
+				})
+			},
+			getChatConfig() {
+				getChatConfig().then(res => {
+					this.chatConfig = res.data
+				})
+			},
 			// 跳转
 			navTo(url) {
 				uni.navigateTo({
@@ -64,30 +90,45 @@
 			},
 			// 选择次数
 			changeCount(ind) {
+				console.log(ind);
 				this.actionIndex = ind;
 				this.count = this.countList[ind];
 			},
 			// 购买
 			buy() {
-				if (!this.count || this.count <= 0) {
-					uni.showModal({
-						title: '提示',
-						content: '请输入有效的购买次数',
-						showCancel: false
-					});
-					return;
+				let that = this
+				if (!that.count || that.count <= 0) {
+					
+					return that.$api.msg('请输入有效的购买次数');
 				}
+				uni.showModal({
+					title:'温馨提示',
+					content:'您确认花费'+ that.estimatedPoints + '积分购买' + that.count + '次聊天次数',
+					complete(e) {
+						console.log(e);
+						if(e.confirm) {
+							buyChatCount({
+								chat_num: that.count
+							}).then(res => {
+								console.log(res);
+								uni.showToast({
+									title: '购买成功',
+									icon: 'success'
+								});
+								that.getChatConfig()
+								that.getUserInfo()
+								// 购买成功后返回
+								// setTimeout(() => {
+								// 	this.navBack();
+								// }, 1500);
+							})
+						}
+					},
+					
+				})
 				
 				// 这里可以添加购买逻辑
-				uni.showToast({
-					title: '购买成功',
-					icon: 'success'
-				});
 				
-				// 购买成功后返回
-				setTimeout(() => {
-					this.navBack();
-				}, 1500);
 			}
 		}
 	};
@@ -134,12 +175,18 @@
 
 	.remain {
 		padding: 30rpx;
-		background-color: #FFFFFF;
 		
 		.label {
 			font-size: 28rpx;
 			color: #666666;
 		}
+		.labels {
+			font-size: 28rpx;
+			background-color:  #f55586;
+			color: #fff;
+			padding: 15rpx 20rpx;
+			border-radius:15rpx;
+		}
 		
 		.count {
 			font-size: 28rpx;

+ 25 - 0
pages/user/model/model.vue

@@ -99,6 +99,8 @@
 		</view>
 		<image src="/static/image/qy.png" mode="widthFix" class="qianyue" @click="goQy"
 			v-if="!userInfo.uid || (userInfo.uid && userInfo.uid != uid)"></image>
+		<image src="/static/image/qy.png" mode="widthFix" class="qianyues" @click="goZx"
+			v-if="!userInfo.uid || (userInfo.uid && userInfo.uid != uid)"></image>
 	</view>
 </template>
 
@@ -293,6 +295,20 @@
 		},
 		methods: {
 			...mapMutations('user', ['setUserInfo']),
+			goZx() {
+				let that = this
+				uni.showModal({
+					title:'温馨提示',
+					content:'建立聊天将消耗一次聊天次数,是否立即聊天?',
+					complete(e) {
+						if(e.confirm) {
+							uni.navigateTo({
+								url:'/pages/set/kf/index?to_uid=' + that.uid + '&name=' + that.userTemplate.name
+							})
+						}
+					}
+				})
+			},
 			showDk(num=0) {
 				this.$api.msg('总打卡' + num + '次')
 			},
@@ -680,4 +696,13 @@
 		bottom: 0;
 		top: 500rpx;
 	}
+	.qianyues {
+		width: 98rpx;
+		height: 108rpx;
+		position: fixed;
+		right: 20rpx;
+		// top:300rpx;
+		bottom: 0;
+		top: 380rpx;
+	}
 </style>