You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							508 lines
						
					
					
						
							9.6 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							508 lines
						
					
					
						
							9.6 KiB
						
					
					
				| <template> | |
| 	<view> | |
| 		<image @click="togglePopup" class="suspension-img" :class="{'rotated': isOpen}" | |
| 			src="https://des.dayunyuanjian.cn/epicSoul/taozi/suspension-icon.png" mode=""></image> | |
| 
 | |
| 		<view v-if="isOpen" class="mask" @click="closeMessage"></view> | |
|  | |
| 		<view v-if="isOpen" class="message-popup" :style="popupStyle"> | |
| 			<view class="popup-content"> | |
| 				<view class="popup-header"> | |
| 					<text class="popup-title">我要留言</text> | |
| 					<view class="header-line"></view> | |
| 				</view> | |
| 				<input v-model="nickname" class="input-field" placeholder="我的昵称" maxlength="20" :disabled="loading" /> | |
| 				<input v-model="email" type="text" class="input-field" placeholder="我的邮箱" :disabled="loading" /> | |
| 				<textarea v-model="messageText" class="textarea-field" placeholder="我想对TA说:" maxlength="500" | |
| 					:disabled="loading"></textarea> | |
| 				<view class="char-count"> | |
| 					<text>{{ messageText.length }}/500</text> | |
| 				</view> | |
| 				<view class="button-group"> | |
| 					<button @click="closeMessage" :disabled="loading" class="btn-cancel"> | |
| 						返回 | |
| 					</button> | |
| 					<button @click="handleSubmit" :disabled="loading" class="btn-submit" | |
| 						:class="{'btn-disabled': !canSubmit}"> | |
| 						<view v-if="loading" class="loading-icon"></view> | |
| 						<text>{{ loading ? '发布中...' : '发布' }}</text> | |
| 					</button> | |
| 				</view> | |
| 			</view> | |
| 		</view> | |
|  | |
| 		<view v-if="toast.show" class="toast" :class="toast.type"> | |
| 			<view class="toast-icon" :class="toast.type"> | |
| 				<view v-if="toast.type === 'success'" class="success-icon"></view> | |
| 				<view v-if="toast.type === 'error'" class="error-icon"></view> | |
| 			</view> | |
| 			<text class="toast-message">{{ toast.message }}</text> | |
| 			<view class="toast-progress"> | |
| 				<view class="progress-bar" :style="{animationDuration: '3s'}"></view> | |
| 			</view> | |
| 		</view> | |
| 	</view> | |
| </template> | |
| 
 | |
| <script> | |
| 	import { | |
| 		sendMessage | |
| 	} from '@/static/js/common'; | |
| 
 | |
| 	export default { | |
| 		data() { | |
| 			return { | |
| 				isOpen: false, | |
| 				popupStyle: { | |
| 					opacity: 0, | |
| 					transform: 'translate(-50%, -50%) scale(0.9)' | |
| 				}, | |
| 				nickname: "", | |
| 				email: "", | |
| 				messageText: "", | |
| 				loading: false, | |
| 				toast: { | |
| 					show: false, | |
| 					type: 'success', | |
| 					message: '' | |
| 				} | |
| 			}; | |
| 		}, | |
| 		computed: { | |
| 			canSubmit() { | |
| 				const hasNickname = this.nickname.trim().length > 0; | |
| 				const hasMessage = this.messageText.trim().length > 0; | |
| 				const validEmail = this.isValidEmail(this.email); | |
| 
 | |
| 				return hasNickname && hasMessage && validEmail; | |
| 			} | |
| 		}, | |
| 		watch: { | |
| 			nickname() { | |
| 				// 可以在这里添加日志输出 | |
| 			}, | |
| 			email() { | |
| 				// 可以在这里添加日志输出 | |
| 			}, | |
| 			messageText() { | |
| 				// 可以在这里添加日志输出 | |
| 			} | |
| 		}, | |
| 		methods: { | |
| 			togglePopup() { | |
| 				if (this.isOpen) { | |
| 					this.closeMessage(); | |
| 				} else { | |
| 					this.openPop(); | |
| 				} | |
| 			}, | |
| 			openPop() { | |
| 				this.isOpen = true; | |
| 				setTimeout(() => { | |
| 					this.popupStyle = { | |
| 						opacity: 1, | |
| 						transform: 'translate(-50%, -50%) scale(1)' | |
| 					}; | |
| 				}, 50); | |
| 			}, | |
| 			closeMessage() { | |
| 				this.popupStyle = { | |
| 					opacity: 0, | |
| 					transform: 'translate(-50%, -50%) scale(0.9)' | |
| 				}; | |
| 				setTimeout(() => { | |
| 					this.isOpen = false; | |
| 				}, 400); | |
| 			}, | |
| 			isValidEmail(email) { | |
| 				if (!email.trim()) return false; | |
| 				const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| 				return emailRegex.test(email); | |
| 			}, | |
| 			showToast(type, message) { | |
| 				this.toast.show = true; | |
| 				this.toast.type = type; | |
| 				this.toast.message = message; | |
| 				setTimeout(() => { | |
| 					this.toast.show = false; | |
| 				}, 3000); | |
| 			}, | |
| 			handleSubmit() { | |
| 				if (!this.nickname.trim()) { | |
| 					this.showToast('error', '请填写昵称'); | |
| 					return; | |
| 				} | |
| 
 | |
| 				if (!this.isValidEmail(this.email)) { | |
| 					this.showToast('error', '请填写正确的邮箱地址'); | |
| 					return; | |
| 				} | |
| 
 | |
| 				if (!this.messageText.trim()) { | |
| 					this.showToast('error', '请填写留言内容'); | |
| 					return; | |
| 				} | |
| 				this.submitMessage(); | |
| 			}, | |
| 			async submitMessage() { | |
| 				try { | |
| 					this.loading = true; | |
| 					const res = await sendMessage({ | |
| 						nickname: this.nickname.trim(), | |
| 						email: this.email.trim(), | |
| 						content: this.messageText.trim() | |
| 					}); | |
| 
 | |
| 					if (res && res.code === 1) { | |
| 						this.nickname = ""; | |
| 						this.email = ""; | |
| 						this.messageText = ""; | |
| 						this.showToast('success', '留言发布成功!'); | |
| 						setTimeout(() => { | |
| 							this.closeMessage(); | |
| 						}, 1500); | |
| 					} else { | |
| 						this.showToast('error', res?.msg || '发布失败,请重试'); | |
| 					} | |
| 				} catch (error) { | |
| 					console.error('发布留言失败:', error); | |
| 					this.showToast('error', '网络错误,请重试'); | |
| 				} finally { | |
| 					this.loading = false; | |
| 				} | |
| 			} | |
| 		} | |
| 	}; | |
| </script> | |
| 
 | |
| <style lang="scss" scoped> | |
| 	.suspension-img { | |
| 		width: 80rpx; | |
| 		height: 80rpx; | |
| 		position: fixed; | |
| 		right: 0; | |
| 		top: 18%; | |
| 		z-index: 9; | |
| 		transition: transform 0.3s ease; | |
| 
 | |
| 		&.rotated { | |
| 			transform: rotate(180deg); | |
| 		} | |
| 
 | |
| 		&:active { | |
| 			opacity: 0.8; | |
| 		} | |
| 	} | |
| 
 | |
| 	.mask { | |
| 		position: fixed; | |
| 		top: 0; | |
| 		left: 0; | |
| 		right: 0; | |
| 		bottom: 0; | |
| 		background-color: rgba(0, 0, 0, 0.5); | |
| 		z-index: 10; | |
| 		animation: fadeIn 0.3s ease forwards; | |
| 	} | |
| 
 | |
| 	.message-popup { | |
| 		position: fixed; | |
| 		top: 50%; | |
| 		left: 50%; | |
| 		transform: translate(-50%, -50%) scale(0.9); | |
| 		z-index: 11; | |
| 		width: 75%; | |
| 		max-width: 650rpx; | |
| 		transition: opacity 0.4s ease, transform 0.4s ease; | |
| 	} | |
| 
 | |
| 	.popup-content { | |
| 		background-color: #fff; | |
| 		border-radius: 20rpx; | |
| 		box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.2); | |
| 		padding: 30rpx; | |
| 		display: flex; | |
| 		flex-direction: column; | |
| 		max-height: 80vh; | |
| 	} | |
| 
 | |
| 	.popup-header { | |
| 		margin-bottom: 30rpx; | |
| 	} | |
| 
 | |
| 	.popup-title { | |
| 		font-size: 40rpx; | |
| 		font-weight: bold; | |
| 		color: #ec4899; | |
| 		margin-bottom: 10rpx; | |
| 	} | |
| 
 | |
| 	.header-line { | |
| 		height: 2rpx; | |
| 		background-color: #ec4899; | |
| 		width: 100%; | |
| 	} | |
| 
 | |
| 	.input-field { | |
| 		height: 90rpx; | |
| 		padding: 0 20rpx; | |
| 		border: 2rpx solid #ccc; | |
| 		border-radius: 10rpx; | |
| 		margin-bottom: 20rpx; | |
| 		font-size: 28rpx; | |
| 
 | |
| 		&:focus { | |
| 			border-color: #ec4899; | |
| 		} | |
| 	} | |
| 
 | |
| 	.textarea-field { | |
| 		text-indent: 10rpx; | |
| 		width: 100%; | |
| 		height: 300rpx; | |
| 		border: 2rpx solid #ccc; | |
| 		border-radius: 10rpx; | |
| 		margin-bottom: 20rpx; | |
| 		font-size: 28rpx; | |
| 
 | |
| 		&:focus { | |
| 			border-color: #ec4899; | |
| 		} | |
| 	} | |
| 
 | |
| 	.char-count { | |
| 		text-align: right; | |
| 		font-size: 24rpx; | |
| 		color: #888; | |
| 		margin-bottom: 20rpx; | |
| 	} | |
| 
 | |
| 	.button-group { | |
| 		display: flex; | |
| 		justify-content: space-between; | |
| 		gap: 20rpx; | |
| 	} | |
| 
 | |
| 	.btn-cancel { | |
| 		flex: 1; | |
| 		background-color: #fff; | |
| 		color: #ec4899; | |
| 		height: 80rpx; | |
| 		line-height: 80rpx; | |
| 		border-radius: 40rpx; | |
| 		border: 2rpx solid #ccc; | |
| 		font-size: 28rpx; | |
| 		text-align: center; | |
| 
 | |
| 		&:active { | |
| 			background-color: #f5f5f5; | |
| 		} | |
| 	} | |
| 
 | |
| 	.btn-submit { | |
| 		flex: 1; | |
| 		background-color: #f5a3cc; | |
| 		color: #fff; | |
| 		height: 80rpx; | |
| 		line-height: 80rpx; | |
| 		border-radius: 40rpx; | |
| 		border: none; | |
| 		font-size: 28rpx; | |
| 		text-align: center; | |
| 		display: flex; | |
| 		align-items: center; | |
| 		justify-content: center; | |
| 
 | |
| 		&:active { | |
| 			background-color: darken(#ec4899, 10%); | |
| 		} | |
| 	} | |
| 
 | |
| 	.loading-icon { | |
| 		width: 30rpx; | |
| 		height: 30rpx; | |
| 		border: 4rpx solid #fff; | |
| 		border-top-color: transparent; | |
| 		border-radius: 50%; | |
| 		margin-right: 10rpx; | |
| 		animation: spin 1s linear infinite; | |
| 	} | |
| 
 | |
| 	@keyframes spin { | |
| 		from { | |
| 			transform: rotate(0deg); | |
| 		} | |
| 
 | |
| 		to { | |
| 			transform: rotate(360deg); | |
| 		} | |
| 	} | |
| 
 | |
| 	.toast { | |
| 		position: fixed; | |
| 		top: 160rpx; | |
| 		left: 50%; | |
| 		transform: translateX(-50%); | |
| 		z-index: 100000; | |
| 		display: flex; | |
| 		align-items: center; | |
| 		padding: 20rpx 30rpx; | |
| 		border-radius: 16rpx; | |
| 		box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3); | |
| 		backdrop-filter: blur(10rpx); | |
| 		max-width: 80%; | |
| 		animation: toastIn 0.5s ease forwards; | |
| 
 | |
| 		&.success { | |
| 			background-color: rgba(34, 197, 94, 0.9); | |
| 		} | |
| 
 | |
| 		&.error { | |
| 			background-color: rgba(239, 68, 68, 0.9); | |
| 		} | |
| 	} | |
| 
 | |
| 	@keyframes fadeIn { | |
| 		from { | |
| 			opacity: 0; | |
| 		} | |
| 
 | |
| 		to { | |
| 			opacity: 1; | |
| 		} | |
| 	} | |
| 
 | |
| 	@keyframes toastIn { | |
| 		from { | |
| 			opacity: 0; | |
| 			transform: translateX(-50%) translateY(-20rpx); | |
| 		} | |
| 
 | |
| 		to { | |
| 			opacity: 1; | |
| 			transform: translateX(-50%) translateY(0); | |
| 		} | |
| 	} | |
| 
 | |
| 	.toast-icon { | |
| 		width: 40rpx; | |
| 		height: 40rpx; | |
| 		margin-right: 20rpx; | |
| 		position: relative; | |
| 	} | |
| 
 | |
| 	.success-icon { | |
| 		width: 100%; | |
| 		height: 100%; | |
| 		border-radius: 50%; | |
| 		border: 4rpx solid #fff; | |
| 		position: relative; | |
| 
 | |
| 		&:after { | |
| 			content: ''; | |
| 			position: absolute; | |
| 			top: 40%; | |
| 			left: 28%; | |
| 			width: 45%; | |
| 			height: 25%; | |
| 			border-left: 4rpx solid #fff; | |
| 			border-bottom: 4rpx solid #fff; | |
| 			transform: rotate(-45deg); | |
| 			animation: checkmark 0.8s ease-out forwards; | |
| 		} | |
| 	} | |
| 
 | |
| 	.error-icon { | |
| 		width: 100%; | |
| 		height: 100%; | |
| 		border-radius: 50%; | |
| 		border: 4rpx solid #fff; | |
| 		position: relative; | |
| 
 | |
| 		&:before, | |
| 		&:after { | |
| 			content: ''; | |
| 			position: absolute; | |
| 			top: 50%; | |
| 			left: 50%; | |
| 			width: 60%; | |
| 			height: 4rpx; | |
| 			background-color: #fff; | |
| 			animation: crossmark 0.8s ease-out forwards; | |
| 		} | |
| 
 | |
| 		&:before { | |
| 			transform: translate(-50%, -50%) rotate(45deg); | |
| 		} | |
| 
 | |
| 		&:after { | |
| 			transform: translate(-50%, -50%) rotate(-45deg); | |
| 		} | |
| 	} | |
| 
 | |
| 	.toast-message { | |
| 		color: #fff; | |
| 		font-size: 28rpx; | |
| 		font-weight: 500; | |
| 	} | |
| 
 | |
| 	.toast-progress { | |
| 		position: absolute; | |
| 		bottom: 0; | |
| 		left: 0; | |
| 		right: 0; | |
| 		height: 4rpx; | |
| 		background-color: rgba(255, 255, 255, 0.2); | |
| 		border-radius: 0 0 16rpx 16rpx; | |
| 		overflow: hidden; | |
| 	} | |
| 
 | |
| 	.progress-bar { | |
| 		height: 100%; | |
| 		background-color: rgba(255, 255, 255, 0.5); | |
| 		width: 100%; | |
| 		animation: progress 3s linear forwards; | |
| 	} | |
| 
 | |
| 	@keyframes progress { | |
| 		from { | |
| 			width: 100%; | |
| 		} | |
| 
 | |
| 		to { | |
| 			width: 0%; | |
| 		} | |
| 	} | |
| 
 | |
| 	@keyframes checkmark { | |
| 		0% { | |
| 			opacity: 0; | |
| 			transform: scale(0) rotate(-45deg); | |
| 		} | |
| 
 | |
| 		50% { | |
| 			opacity: 1; | |
| 		} | |
| 
 | |
| 		100% { | |
| 			opacity: 1; | |
| 			transform: scale(1) rotate(-45deg); | |
| 		} | |
| 	} | |
| 
 | |
| 	@keyframes crossmark { | |
| 		0% { | |
| 			opacity: 0; | |
| 			transform: translate(-50%, -50%) scale(0) rotate(var(--rotation, 45deg)); | |
| 		} | |
| 
 | |
| 		50% { | |
| 			opacity: 1; | |
| 		} | |
| 
 | |
| 		100% { | |
| 			opacity: 1; | |
| 			transform: translate(-50%, -50%) scale(1) rotate(var(--rotation, 45deg)); | |
| 		} | |
| 	} | |
| </style> |