-
4app.json
-
7pages/home/home.js
-
2pages/home/home.wxml
-
BINpagesA/images/1.png
-
BINpagesA/images/2.png
-
BINpagesA/images/3.png
-
BINpagesA/images/dzyx.png
-
BINpagesA/images/gzdw.png
-
BINpagesA/images/kzt.png
-
BINpagesA/images/phone.png
-
BINpagesA/images/ss.png
-
91pagesA/pages/askingSy/askingSy.js
-
45pagesA/pages/askingSy/askingSy.wxml
-
381pagesA/pages/askingSy/askingSy.wxss
-
192pagesA/pages/askingSyAdd/askingSyAdd.js
-
75pagesA/pages/askingSyAdd/askingSyAdd.wxml
-
11pagesA/pages/askingSyAdd/askingSyAdd.wxss
-
166pagesA/pages/askingSyDetails/askingSyDetails.js
-
49pagesA/pages/askingSyDetails/askingSyDetails.wxml
-
40pagesA/pages/askingSyDetails/askingSyDetails.wxss
-
431pagesA/pages/expert/expert.js
-
4pagesA/pages/expert/expert.json
-
248pagesA/pages/expert/expert.wxml
-
769pagesA/pages/expert/expert.wxss
-
753pagesA/pages/expertChat/expertChat.js
-
4pagesA/pages/expertChat/expertChat.json
-
296pagesA/pages/expertChat/expertChat.wxml
-
928pagesA/pages/expertChat/expertChat.wxss
-
23utils/api.js
|
After Width: 556 | Height: 674 | Size: 368 KiB |
|
After Width: 466 | Height: 678 | Size: 374 KiB |
|
After Width: 459 | Height: 682 | Size: 285 KiB |
|
After Width: 200 | Height: 200 | Size: 2.4 KiB |
|
After Width: 200 | Height: 200 | Size: 2.4 KiB |
|
After Width: 320 | Height: 320 | Size: 8.5 KiB |
|
After Width: 200 | Height: 200 | Size: 4.0 KiB |
|
After Width: 200 | Height: 200 | Size: 4.1 KiB |
@ -0,0 +1,431 @@ |
|||
import http from '../../../utils/api' |
|||
|
|||
Page({ |
|||
data: { |
|||
// 搜索关键词
|
|||
searchKeyword: '', |
|||
|
|||
// 当前筛选条件
|
|||
currentFilter: 'all', |
|||
|
|||
// 所有专家数据
|
|||
allExperts: [ |
|||
{ |
|||
id: 1, |
|||
name: '张建国', |
|||
title: '畜牧学博士', |
|||
expertise: '牛类养殖与疾病防治', |
|||
experience: 15, |
|||
online: true, |
|||
senior: true, |
|||
phone: '138-0013-8001', |
|||
email: 'zhangjg@agri-expert.com', |
|||
institution: '国家畜牧业研究所', |
|||
address: '北京市朝阳区农业科技大厦', |
|||
bio: '15年牛类养殖研究经验,发表SCI论文20余篇,擅长规模化养殖场管理与疾病防控。', |
|||
tags: ['肉牛养殖', '疾病预防', '饲料配方', '规模化养殖'], |
|||
successRate: 98, |
|||
responseTime: '15分钟内响应', |
|||
avatar: '/pagesA/images/1.png' |
|||
}, |
|||
{ |
|||
id: 2, |
|||
name: '李秀英', |
|||
title: '兽医学硕士', |
|||
expertise: '猪病诊断与治疗', |
|||
experience: 12, |
|||
online: true, |
|||
senior: true, |
|||
phone: '139-0013-9002', |
|||
email: 'lixy@vet-hospital.cn', |
|||
institution: '农业大学动物医学院', |
|||
address: '上海市徐汇区农业路58号', |
|||
bio: '资深猪病防治专家,参与多项国家级科研项目,成功解决多个大规模猪场疫情。', |
|||
tags: ['猪病诊疗', '疫苗管理', '养殖场规划', '传染病防治'], |
|||
successRate: 96, |
|||
responseTime: '20分钟内响应', |
|||
avatar: '/pagesA/images/2.png' |
|||
}, |
|||
{ |
|||
id: 3, |
|||
name: '王伟民', |
|||
title: '高级畜牧师', |
|||
expertise: '羊类繁殖与饲养管理', |
|||
experience: 20, |
|||
online: false, |
|||
senior: true, |
|||
phone: '137-0013-7003', |
|||
email: 'wangwm@livestock.cn', |
|||
institution: '草原畜牧业研究中心', |
|||
address: '内蒙古呼和浩特市牧业科技园', |
|||
bio: '羊类养殖领域权威专家,拥有20年草原牧区养殖经验,精通各类羊只繁殖技术。', |
|||
tags: ['绵羊养殖', '山羊繁殖', '牧草种植', '草原管理'], |
|||
successRate: 95, |
|||
responseTime: '1小时内响应', |
|||
avatar: '/pagesA/images/3.png' |
|||
}, |
|||
{ |
|||
id: 4, |
|||
name: '陈秀兰', |
|||
title: '家禽养殖专家', |
|||
expertise: '鸡鸭养殖技术', |
|||
experience: 18, |
|||
online: true, |
|||
senior: false, |
|||
phone: '136-0013-6004', |
|||
email: 'chenxl@poultry-expert.com', |
|||
institution: '家禽养殖技术中心', |
|||
address: '广东省广州市禽业科技园', |
|||
bio: '家禽养殖技术推广专家,帮助300+养殖户实现科学化、标准化养殖管理。', |
|||
tags: ['蛋鸡养殖', '肉鸭饲养', '疾病防控', '禽舍设计'], |
|||
successRate: 94, |
|||
responseTime: '30分钟内响应', |
|||
avatar: '/pagesA/images/1.png' |
|||
}, |
|||
{ |
|||
id: 5, |
|||
name: '刘志强', |
|||
title: '特种养殖专家', |
|||
expertise: '鹿、兔等特种动物养殖', |
|||
experience: 10, |
|||
online: false, |
|||
senior: false, |
|||
phone: '135-0013-5005', |
|||
email: 'liuzq@special-livestock.cn', |
|||
institution: '特种动物养殖研究所', |
|||
address: '四川省成都市农业科技园', |
|||
bio: '特种动物养殖新兴力量,专注于梅花鹿、肉兔等特种动物的现代化养殖技术。', |
|||
tags: ['梅花鹿养殖', '肉兔饲养', '市场分析', '特色养殖'], |
|||
successRate: 92, |
|||
responseTime: '2小时内响应', |
|||
avatar: '/pagesA/images/2.png' |
|||
}, |
|||
{ |
|||
id: 6, |
|||
name: '赵雪梅', |
|||
title: '畜牧营养学博士', |
|||
expertise: '牲畜饲料配方与营养', |
|||
experience: 14, |
|||
online: true, |
|||
senior: true, |
|||
phone: '134-0013-4006', |
|||
email: 'zhaoxm@feed-research.cn', |
|||
institution: '农业科学院饲料研究所', |
|||
address: '江苏省南京市科研路88号', |
|||
bio: '饲料营养学专家,研发多个高效饲料配方,帮助养殖户降低30%饲料成本。', |
|||
tags: ['饲料配方', '营养管理', '成本控制', '饲料安全'], |
|||
successRate: 97, |
|||
responseTime: '25分钟内响应', |
|||
avatar: '/pagesA/images/3.png' |
|||
}, |
|||
{ |
|||
id: 7, |
|||
name: '孙建国', |
|||
title: '养殖场管理专家', |
|||
expertise: '现代化养殖场设计与运营', |
|||
experience: 22, |
|||
online: true, |
|||
senior: true, |
|||
phone: '133-0013-3007', |
|||
email: 'sunjg@farm-management.com', |
|||
institution: '现代农业发展中心', |
|||
address: '浙江省杭州市创新产业园', |
|||
bio: '养殖场规划设计专家,参与设计200+现代化养殖场,精通自动化养殖设备应用。', |
|||
tags: ['场舍设计', '设备管理', '成本核算', '环保处理'], |
|||
successRate: 99, |
|||
responseTime: '10分钟内响应', |
|||
avatar: '/pagesA/images/2.png' |
|||
}, |
|||
{ |
|||
id: 8, |
|||
name: '周小芳', |
|||
title: '兽医病理学硕士', |
|||
expertise: '牲畜常见病与传染病防治', |
|||
experience: 11, |
|||
online: false, |
|||
senior: false, |
|||
phone: '132-0013-2008', |
|||
email: 'zhouxf@vet-pathology.cn', |
|||
institution: '动物疫病防控中心', |
|||
address: '湖北省武汉市动物防疫站', |
|||
bio: '专攻牲畜病理诊断与传染病防控,在快速诊断和精准治疗方面有丰富经验。', |
|||
tags: ['传染病防治', '病理诊断', '用药指导', '疫情处理'], |
|||
successRate: 93, |
|||
responseTime: '1.5小时内响应', |
|||
avatar: '/pagesA/images/3.png' |
|||
} |
|||
], |
|||
|
|||
// 筛选后的专家列表
|
|||
filteredExperts: [], |
|||
|
|||
// 在线专家数量
|
|||
onlineCount: 0, |
|||
|
|||
// 当前选中的专家
|
|||
currentExpert: null, |
|||
|
|||
// 是否显示联系方式弹窗
|
|||
showContactDialog: false |
|||
}, |
|||
|
|||
onLoad() { |
|||
this.sortAndFilterExperts() |
|||
this.getexpertsList() |
|||
}, |
|||
|
|||
onShow() { |
|||
// 页面显示时随机更新一些专家的在线状态(模拟实时状态)
|
|||
this.updateOnlineStatus(); |
|||
}, |
|||
|
|||
|
|||
// 专家列表
|
|||
getexpertsList(){ |
|||
http.expertsList({ |
|||
data:{}, |
|||
success:res=>{ |
|||
console.log(111,res); |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 更新在线状态
|
|||
updateOnlineStatus() { |
|||
const experts = this.data.allExperts.map(expert => ({ |
|||
...expert, |
|||
online: Math.random() > 0.4 // 60%的概率在线
|
|||
})); |
|||
|
|||
this.setData({ |
|||
allExperts: experts |
|||
}, () => { |
|||
this.sortAndFilterExperts(); |
|||
}); |
|||
}, |
|||
|
|||
// 处理搜索输入
|
|||
onSearchInput(e) { |
|||
this.setData({ |
|||
searchKeyword: e.detail.value |
|||
}); |
|||
this.sortAndFilterExperts(); |
|||
}, |
|||
|
|||
// 清空搜索
|
|||
clearSearch() { |
|||
this.setData({ |
|||
searchKeyword: '', |
|||
currentFilter: 'all' |
|||
}); |
|||
this.sortAndFilterExperts(); |
|||
}, |
|||
|
|||
// 更改筛选条件
|
|||
changeFilter(e) { |
|||
const filter = e.currentTarget.dataset.filter; |
|||
this.setData({ |
|||
currentFilter: filter |
|||
}); |
|||
this.sortAndFilterExperts(); |
|||
}, |
|||
|
|||
// 排序和筛选专家
|
|||
sortAndFilterExperts() { |
|||
const { allExperts, searchKeyword, currentFilter } = this.data; |
|||
|
|||
let filtered = [...allExperts]; |
|||
|
|||
// 关键词搜索
|
|||
if (searchKeyword.trim()) { |
|||
const keyword = searchKeyword.toLowerCase(); |
|||
filtered = filtered.filter(expert => |
|||
expert.name.toLowerCase().includes(keyword) || |
|||
expert.expertise.toLowerCase().includes(keyword) || |
|||
expert.tags.some(tag => tag.toLowerCase().includes(keyword)) || |
|||
expert.institution.toLowerCase().includes(keyword) |
|||
); |
|||
} |
|||
|
|||
// 筛选条件
|
|||
switch (currentFilter) { |
|||
case 'online': |
|||
filtered = filtered.filter(expert => expert.online); |
|||
break; |
|||
case 'offline': |
|||
filtered = filtered.filter(expert => !expert.online); |
|||
break; |
|||
case 'senior': |
|||
filtered = filtered.filter(expert => expert.senior); |
|||
break; |
|||
case 'veterinary': |
|||
filtered = filtered.filter(expert => |
|||
expert.expertise.includes('病') || |
|||
expert.tags.some(tag => tag.includes('病') || tag.includes('医')) |
|||
); |
|||
break; |
|||
} |
|||
|
|||
// 排序:在线优先 > 资深专家 > 经验丰富 > 解决率高
|
|||
filtered.sort((a, b) => { |
|||
// 在线状态优先
|
|||
if (a.online !== b.online) { |
|||
return a.online ? -1 : 1; |
|||
} |
|||
// 资深专家优先
|
|||
if (a.senior !== b.senior) { |
|||
return a.senior ? -1 : 1; |
|||
} |
|||
// 经验丰富优先
|
|||
if (a.experience !== b.experience) { |
|||
return b.experience - a.experience; |
|||
} |
|||
// 解决率高优先
|
|||
return (b.successRate || 0) - (a.successRate || 0); |
|||
}); |
|||
|
|||
// 计算在线人数
|
|||
const onlineCount = filtered.filter(expert => expert.online).length; |
|||
|
|||
this.setData({ |
|||
filteredExperts: filtered, |
|||
onlineCount: onlineCount |
|||
}); |
|||
}, |
|||
|
|||
// 显示专家联系方式
|
|||
showContactInfo(e) { |
|||
const index = e.currentTarget.dataset.index; |
|||
const expert = this.data.filteredExperts[index]; |
|||
|
|||
this.setData({ |
|||
currentExpert: expert, |
|||
showContactDialog: true |
|||
}); |
|||
}, |
|||
|
|||
// 隐藏联系方式弹窗
|
|||
hideContactDialog() { |
|||
this.setData({ |
|||
showContactDialog: false |
|||
}); |
|||
}, |
|||
|
|||
// 阻止事件冒泡
|
|||
stopPropagation() { |
|||
// 阻止事件冒泡
|
|||
}, |
|||
|
|||
// 拨打电话
|
|||
makePhoneCall(e) { |
|||
const phone = e.currentTarget.dataset.phone; |
|||
const cleanPhone = phone.replace(/-/g, ''); |
|||
|
|||
wx.showModal({ |
|||
title: '拨打电话', |
|||
content: `确定要拨打 ${phone} 吗?`, |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
wx.makePhoneCall({ |
|||
phoneNumber: cleanPhone, |
|||
success: () => { |
|||
wx.showToast({ |
|||
title: '拨号成功', |
|||
icon: 'success' |
|||
}); |
|||
}, |
|||
fail: (err) => { |
|||
console.error('拨打电话失败:', err); |
|||
wx.showToast({ |
|||
title: '拨号失败', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 复制邮箱
|
|||
copyEmail(e) { |
|||
const email = e.currentTarget.dataset.email; |
|||
|
|||
wx.setClipboardData({ |
|||
data: email, |
|||
success: () => { |
|||
wx.showToast({ |
|||
title: '邮箱已复制', |
|||
icon: 'success' |
|||
}); |
|||
}, |
|||
fail: () => { |
|||
wx.showToast({ |
|||
title: '复制失败', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 查看位置
|
|||
viewLocation(e) { |
|||
const address = e.currentTarget.dataset.address |
|||
wx.showModal({ |
|||
title: '单位地址', |
|||
content: address, |
|||
showCancel: false, |
|||
confirmText: '知道了' |
|||
}); |
|||
}, |
|||
|
|||
// 开始咨询
|
|||
startConsultation() { |
|||
const expert = this.data.currentExpert; |
|||
|
|||
wx.showModal({ |
|||
title: '咨询确认', |
|||
content: expert.online |
|||
? `确定要立即咨询 ${expert.name} 专家吗?` |
|||
: `确定要预约咨询 ${expert.name} 专家吗?`, |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
if (expert.online) { |
|||
wx.showToast({ |
|||
title: '正在为您连接专家...', |
|||
icon: 'loading', |
|||
duration: 2000 |
|||
}); |
|||
|
|||
setTimeout(() => { |
|||
// 跳转一对一咨询专家
|
|||
wx.navigateTo({ |
|||
url: '/pagesA/pages/expertChat/expertChat', |
|||
}) |
|||
|
|||
this.hideContactDialog(); |
|||
}, 2000); |
|||
|
|||
|
|||
|
|||
} else { |
|||
wx.showToast({ |
|||
title: '预约成功,专家将尽快回复', |
|||
icon: 'success' |
|||
}); |
|||
this.hideContactDialog(); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 分享专家
|
|||
onShareAppMessage() { |
|||
return { |
|||
title: '牲畜专家咨询平台', |
|||
path: '/pages/expert/expert', |
|||
imageUrl: '/images/share-cover.jpg' |
|||
}; |
|||
} |
|||
}); |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText":"专家列表", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,248 @@ |
|||
<view class="expert-page"> |
|||
<!-- 顶部区域 --> |
|||
<view class="header-section"> |
|||
<!-- 背景装饰 --> |
|||
<view class="header-bg"> |
|||
<view class="bg-circle bg-circle-1"></view> |
|||
<view class="bg-circle bg-circle-2"></view> |
|||
<view class="bg-circle bg-circle-3"></view> |
|||
</view> |
|||
|
|||
<!-- 标题内容 --> |
|||
<view class="header-content"> |
|||
<view class="title-row"> |
|||
<text class="main-title">牲畜专家</text> |
|||
<view class="expert-badge"> |
|||
<text class="badge-text">专业咨询</text> |
|||
</view> |
|||
</view> |
|||
<text class="sub-title">连接行业专家 · 解决养殖难题</text> |
|||
|
|||
<!-- 统计信息 --> |
|||
<view class="stats-row"> |
|||
<view class="stat-item"> |
|||
<text class="stat-number">{{allExperts.length}}</text> |
|||
<text class="stat-label">总专家数</text> |
|||
</view> |
|||
<view class="stat-divider"></view> |
|||
<view class="stat-item"> |
|||
<text class="stat-number">{{onlineCount}}</text> |
|||
<text class="stat-label">在线专家</text> |
|||
</view> |
|||
<view class="stat-divider"></view> |
|||
<view class="stat-item"> |
|||
<text class="stat-number">24h</text> |
|||
<text class="stat-label">快速响应</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 搜索和筛选区域 --> |
|||
<view class="search-filter-section"> |
|||
<!-- 搜索框 --> |
|||
<view class="search-wrapper"> |
|||
<view class="search-box"> |
|||
<image src="/pagesA/images/ss.png" class="search-icon"></image> |
|||
<input type="text" placeholder="搜索专家姓名、擅长领域..." placeholder-class="placeholder" bindinput="onSearchInput" value="{{searchKeyword}}" /> |
|||
<view wx:if="{{searchKeyword}}" class="clear-btn" bindtap="clearSearch"> |
|||
<image src="/pagesA/images/ch.png" class="clear-icon"></image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 筛选标签 --> |
|||
<scroll-view class="filter-scroll" scroll-x> |
|||
<view class="filter-tags"> |
|||
<view class="filter-tag {{currentFilter === 'all' ? 'active' : ''}}" bindtap="changeFilter" data-filter="all"> |
|||
<text>全部专家</text> |
|||
<view class="tag-count">{{allExperts.length}}</view> |
|||
</view> |
|||
|
|||
<view class="filter-tag {{currentFilter === 'online' ? 'active' : ''}}" bindtap="changeFilter" data-filter="online"> |
|||
<text>在线专家</text> |
|||
<view class="tag-count online-count">{{onlineCount}}</view> |
|||
</view> |
|||
|
|||
<view class="filter-tag {{currentFilter === 'offline' ? 'active' : ''}}" bindtap="changeFilter" data-filter="offline"> |
|||
<text>离线专家</text> |
|||
<view class="tag-count">{{allExperts.length - onlineCount}}</view> |
|||
</view> |
|||
|
|||
<view class="filter-tag {{currentFilter === 'senior' ? 'active' : ''}}" bindtap="changeFilter" data-filter="senior"> |
|||
<text>资深专家</text> |
|||
|
|||
</view> |
|||
|
|||
<view class="filter-tag {{currentFilter === 'veterinary' ? 'active' : ''}}" bindtap="changeFilter" data-filter="veterinary"> |
|||
<text>兽医专家</text> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
|
|||
<!-- 专家列表 --> |
|||
<view class="expert-list-section"> |
|||
<!-- 列表标题 --> |
|||
<view class="list-header"> |
|||
<text class="list-title">专家列表</text> |
|||
<text class="list-count">已为您找到 {{filteredExperts.length}} 位专家</text> |
|||
</view> |
|||
|
|||
<!-- 专家卡片列表 --> |
|||
<scroll-view class="expert-cards" scroll-y> |
|||
<!-- 空状态 --> |
|||
<view wx:if="{{filteredExperts.length === 0}}" class="empty-state"> |
|||
<image src="/pagesA/images/kzt.png" class="empty-image"></image> |
|||
<text class="empty-title">暂无相关专家</text> |
|||
<text class="empty-desc">换个关键词试试,或联系客服为您推荐</text> |
|||
</view> |
|||
|
|||
<!-- 专家卡片 --> |
|||
<view wx:else> |
|||
<block wx:for="{{filteredExperts}}" wx:key="id"> |
|||
<view class="expert-card"> |
|||
<!-- 专家头像和在线状态 --> |
|||
<view class="card-left"> |
|||
<view class="avatar-container"> |
|||
<image src="{{item.avatar}}" class="expert-avatar" mode="aspectFill"></image> |
|||
<view class="online-badge {{item.online ? 'online' : 'offline'}}"> |
|||
{{item.online ? '在线' : '离线'}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 专家信息 --> |
|||
<view class="card-middle"> |
|||
<view class="name-title-row"> |
|||
<text class="expert-name">{{item.name}}</text> |
|||
<text class="expert-title">{{item.title}}</text> |
|||
</view> |
|||
|
|||
<!-- 专门的信息展示区域 --> |
|||
<view class="info-display"> |
|||
<view class="info-item"> |
|||
<text class="info-label">擅长领域:</text> |
|||
<text class="info-value">{{item.expertise}}</text> |
|||
</view> |
|||
|
|||
<view class="info-item"> |
|||
<text class="info-label">从业经验:</text> |
|||
<text class="info-value">{{item.experience}}年</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 点击按钮 --> |
|||
<view class="contact-btn {{item.online ? 'online-btn' : 'offline-btn'}}" bindtap="showContactInfo" data-index="{{index}}"> |
|||
{{item.online ? '立即咨询' : '查看联系'}} |
|||
</view> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
|
|||
<!-- 联系方式弹窗 --> |
|||
<view wx:if="{{showContactDialog}}" class="dialog-overlay" bindtap="hideContactDialog"> |
|||
<view class="contact-modal" catchtap="stopPropagation"> |
|||
<!-- 可滚动的内容区域 --> |
|||
<scroll-view class="modal-content" scroll-y> |
|||
<!-- 模态框头部 --> |
|||
<view class="modal-header"> |
|||
<view class="header-left"> |
|||
<image src="/pages/images/tx.png" class="modal-avatar"></image> |
|||
<view class="expert-intro"> |
|||
<view class="modallei"> |
|||
<view class="modal-name">{{currentExpert.name}}</view> |
|||
<view class="modal-title">{{currentExpert.title}}</view> |
|||
</view> |
|||
<view class="title-status"> |
|||
<view class="modal-status {{currentExpert.online ? 'online' : 'offline'}}"> |
|||
<view class="status-dot"></view> |
|||
{{currentExpert.online ? '在线可咨询' : '暂时离线'}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 专家简介 --> |
|||
<view class="expert-bio"> |
|||
<view class="bio-header"> |
|||
<text class="bio-title">专家简介</text> |
|||
</view> |
|||
<text class="bio-content">{{currentExpert.bio || '资深牲畜养殖专家,拥有丰富的实践经验和理论知识,擅长解决各类养殖难题'}}</text> |
|||
</view> |
|||
|
|||
<!-- 联系方式 --> |
|||
<view class="contact-section"> |
|||
<!-- 电话 --> |
|||
<view class="contact-item phone-item"> |
|||
<view class="contact-icon-container"> |
|||
<image src="/pagesA/images/phone.png" class="contact-item-icon"></image> |
|||
</view> |
|||
<view class="contact-info"> |
|||
<text class="contact-label">联系电话</text> |
|||
<text class="contact-value">{{currentExpert.phone}}</text> |
|||
<text class="contact-desc">可直接拨打电话咨询</text> |
|||
</view> |
|||
<button class="action-btn call-btn" bindtap="makePhoneCall" data-phone="{{currentExpert.phone}}"> |
|||
拨打 |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 邮箱 --> |
|||
<view class="contact-item email-item"> |
|||
<view class="contact-icon-container"> |
|||
<image src="/pagesA/images/dzyx.png" class="contact-item-icon"></image> |
|||
</view> |
|||
<view class="contact-info"> |
|||
<text class="contact-label">电子邮箱</text> |
|||
<text class="contact-value">{{currentExpert.email}}</text> |
|||
<text class="contact-desc">发送邮件可获得详细回复</text> |
|||
</view> |
|||
<button class="action-btn copy-btn" bindtap="copyEmail" data-email="{{currentExpert.email}}"> |
|||
复制 |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 工作单位 --> |
|||
<view class="contact-item institution-item"> |
|||
<view class="contact-icon-container"> |
|||
<image src="/pagesA/images/gzdw.png" class="contact-item-icon"></image> |
|||
</view> |
|||
<view class="contact-info"> |
|||
<text class="contact-label">工作单位</text> |
|||
<text class="contact-value">{{currentExpert.institution}}</text> |
|||
<text class="contact-desc">专业机构认证专家</text> |
|||
</view> |
|||
<button class="action-btn address-btn" bindtap="viewLocation" data-address="{{currentExpert.address}}"> |
|||
位置 |
|||
</button> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 咨询时间 --> |
|||
<view class="consultation-time"> |
|||
<view class="time-header"> |
|||
<text class="time-title">咨询时间建议</text> |
|||
</view> |
|||
<text class="time-content">{{currentExpert.online ? '专家当前在线,可直接联系咨询' : '专家当前不在线,建议在工作时间联系或发送邮件咨询'}}</text> |
|||
<view class="time-slots"> |
|||
<text class="time-slot">工作日 9:00-18:00</text> |
|||
<text class="time-slot">周末 10:00-16:00</text> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
|
|||
<!-- 操作按钮(固定在底部) --> |
|||
<view class="modal-actions"> |
|||
<button class="secondary-btn" bindtap="hideContactDialog">稍后联系</button> |
|||
<button class="primary-btn" bindtap="startConsultation"> |
|||
{{currentExpert.online ? '立即咨询' : '预约咨询'}} |
|||
</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,769 @@ |
|||
.expert-page { |
|||
min-height: 100vh; |
|||
background: linear-gradient(180deg, #f5f7fa 0%, #ffffff 100%); |
|||
} |
|||
|
|||
/* 顶部区域样式 */ |
|||
.header-section { |
|||
background: linear-gradient(135deg, #2c8c34 0%, #4caf50 100%); |
|||
padding: 40rpx 40rpx 50rpx; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.header-bg { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
.bg-circle { |
|||
position: absolute; |
|||
border-radius: 50%; |
|||
background: rgba(255, 255, 255, 0.1); |
|||
} |
|||
|
|||
.bg-circle-1 { |
|||
width: 300rpx; |
|||
height: 300rpx; |
|||
top: -150rpx; |
|||
right: -100rpx; |
|||
} |
|||
|
|||
.bg-circle-2 { |
|||
width: 200rpx; |
|||
height: 200rpx; |
|||
bottom: -80rpx; |
|||
left: -80rpx; |
|||
} |
|||
|
|||
.bg-circle-3 { |
|||
width: 150rpx; |
|||
height: 150rpx; |
|||
top: 50%; |
|||
right: 20%; |
|||
} |
|||
|
|||
.header-content { |
|||
position: relative; |
|||
z-index: 2; |
|||
} |
|||
|
|||
.title-row { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.main-title { |
|||
font-size: 52rpx; |
|||
font-weight: bold; |
|||
color: white; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.expert-badge { |
|||
background: rgba(255, 255, 255, 0.2); |
|||
border: 2rpx solid rgba(255, 255, 255, 0.3); |
|||
border-radius: 30rpx; |
|||
padding: 6rpx 20rpx; |
|||
backdrop-filter: blur(10rpx); |
|||
} |
|||
|
|||
.badge-text { |
|||
font-size: 24rpx; |
|||
color: white; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.sub-title { |
|||
font-size: 28rpx; |
|||
color: rgba(255, 255, 255, 0.9); |
|||
margin-bottom: 40rpx; |
|||
display: block; |
|||
} |
|||
|
|||
.stats-row { |
|||
display: flex; |
|||
align-items: center; |
|||
background: rgba(255, 255, 255, 0.15); |
|||
border-radius: 24rpx; |
|||
padding: 20rpx; |
|||
backdrop-filter: blur(10rpx); |
|||
} |
|||
|
|||
.stat-item { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
.stat-number { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: white; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.stat-label { |
|||
font-size: 24rpx; |
|||
color: rgba(255, 255, 255, 0.85); |
|||
} |
|||
|
|||
.stat-divider { |
|||
width: 2rpx; |
|||
height: 40rpx; |
|||
background: rgba(255, 255, 255, 0.3); |
|||
} |
|||
|
|||
/* 搜索和筛选区域 */ |
|||
.search-filter-section { |
|||
background: white; |
|||
border-radius: 40rpx 40rpx 0 0; |
|||
margin-top: -30rpx; |
|||
position: relative; |
|||
z-index: 10; |
|||
padding: 30rpx 40rpx 0; |
|||
box-shadow: 0 -10rpx 30rpx rgba(44, 140, 52, 0.1); |
|||
} |
|||
|
|||
.search-wrapper { |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.search-box { |
|||
display: flex; |
|||
align-items: center; |
|||
background: #f8faf9; |
|||
border-radius: 50rpx; |
|||
padding: 20rpx 30rpx; |
|||
border: 2rpx solid #e8f5e9; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.search-box:active { |
|||
background: #f0f9f1; |
|||
border-color: #2c8c34; |
|||
} |
|||
|
|||
.search-icon { |
|||
width: 36rpx; |
|||
height: 36rpx; |
|||
margin-right: 20rpx; |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
.search-box input { |
|||
flex: 1; |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
height: 40rpx; |
|||
line-height: 40rpx; |
|||
} |
|||
|
|||
.placeholder { |
|||
color: #999; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.clear-btn { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: #EAEBED; |
|||
border-radius: 50%; |
|||
margin-left: 10rpx; |
|||
} |
|||
|
|||
.clear-icon { |
|||
width: 28rpx; |
|||
height: 28rpx; |
|||
} |
|||
|
|||
/* 筛选标签 */ |
|||
.filter-scroll { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.filter-tags { |
|||
display: inline-flex; |
|||
padding-bottom: 20rpx; |
|||
} |
|||
|
|||
.filter-tag { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
padding: 16rpx 28rpx; |
|||
margin-right: 20rpx; |
|||
background: #f8faf9; |
|||
border-radius: 30rpx; |
|||
border: 2rpx solid #e8f5e9; |
|||
color: #666; |
|||
font-size: 28rpx; |
|||
font-weight: 500; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.filter-tag.active { |
|||
background: #2c8c34; |
|||
border-color: #2c8c34; |
|||
color: white; |
|||
} |
|||
|
|||
.tag-count { |
|||
background: rgba(255, 255, 255, 0.2); |
|||
color: #2c8c34; |
|||
font-size: 22rpx; |
|||
padding: 2rpx 10rpx; |
|||
border-radius: 20rpx; |
|||
margin-left: 8rpx; |
|||
} |
|||
|
|||
.filter-tag.active .tag-count { |
|||
background: rgba(255, 255, 255, 0.3); |
|||
color: white; |
|||
} |
|||
|
|||
.online-count { |
|||
color: #4caf50; |
|||
} |
|||
|
|||
|
|||
/* 专家列表区域 */ |
|||
.expert-list-section { |
|||
padding: 0 30rpx 30rpx; |
|||
} |
|||
|
|||
.list-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 30rpx 0 20rpx; |
|||
border-bottom: 2rpx solid #f0f5f1; |
|||
} |
|||
|
|||
.list-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.list-count { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.expert-cards { |
|||
height: calc(100vh - 700rpx); |
|||
padding-bottom: 30rpx; |
|||
} |
|||
|
|||
/* 空状态 */ |
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 120rpx 0; |
|||
} |
|||
|
|||
.empty-image { |
|||
width: 240rpx; |
|||
height: 240rpx; |
|||
margin-bottom: 40rpx; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.empty-title { |
|||
font-size: 34rpx; |
|||
color: #999; |
|||
font-weight: bold; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
|
|||
/* 专家卡片 */ |
|||
.expert-card { |
|||
background: white; |
|||
border-radius: 30rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 10rpx; |
|||
display: grid; |
|||
grid-template-columns: 1fr 2fr; |
|||
align-items: center; |
|||
box-shadow: 0 6rpx 30rpx rgba(0, 0, 0, 0.05); |
|||
border: 2rpx solid #f0f5f1; |
|||
transition: all 0.3s; |
|||
position: relative; |
|||
} |
|||
|
|||
.expert-card:active { |
|||
transform: translateY(-4rpx); |
|||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.1); |
|||
border-color: #e8f5e9; |
|||
} |
|||
|
|||
.card-left { |
|||
margin-right: 25rpx; |
|||
} |
|||
|
|||
.avatar-container { |
|||
position: relative; |
|||
width: 150rpx; |
|||
height: 180rpx; |
|||
border-radius: 16rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.expert-avatar { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #f5f7fa; |
|||
} |
|||
|
|||
.online-badge { |
|||
position: absolute; |
|||
bottom: 0rpx; |
|||
right: 0rpx; |
|||
padding: 5rpx 15rpx; |
|||
border-radius: 20rpx; |
|||
font-size: 18rpx; |
|||
font-weight: bold; |
|||
color: white; |
|||
text-align: center; |
|||
} |
|||
|
|||
.online-badge.online { |
|||
background: linear-gradient(135deg, #4caf50 0%, #2c8c34 100%); |
|||
} |
|||
|
|||
.online-badge.offline { |
|||
background: linear-gradient(135deg, #9e9e9e 0%, #757575 100%); |
|||
} |
|||
|
|||
.card-middle { |
|||
flex: 1; |
|||
} |
|||
|
|||
.name-title-row { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.expert-name { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-right: 15rpx; |
|||
} |
|||
|
|||
.expert-title { |
|||
font-size: 26rpx; |
|||
color: #2c8c34; |
|||
background: #f0f9f1; |
|||
padding: 4rpx 12rpx; |
|||
border-radius: 8rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 信息展示区域 */ |
|||
.info-display { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.info-item { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
font-size: 28rpx; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.info-label { |
|||
color: #666; |
|||
min-width: 150rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.info-value { |
|||
color: #333; |
|||
flex: 1; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.contact-btn { |
|||
font-size: 22rpx; |
|||
padding: 6rpx 20rpx; |
|||
border-radius: 30rpx; |
|||
border: none; |
|||
color: white; |
|||
font-weight: bold; |
|||
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1); |
|||
transition: all 0.3s; |
|||
display: inline-block; |
|||
position: absolute; |
|||
right: 20rpx; |
|||
bottom: 20rpx; |
|||
} |
|||
|
|||
.contact-btn.online-btn { |
|||
background: linear-gradient(135deg, #4caf50 0%, #2c8c34 100%); |
|||
} |
|||
|
|||
.contact-btn.offline-btn { |
|||
background: linear-gradient(135deg, #9e9e9e 0%, #757575 100%); |
|||
} |
|||
|
|||
.contact-btn:active { |
|||
transform: scale(0.95); |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
/* 联系方式弹窗 */ |
|||
.dialog-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 1000; |
|||
animation: fadeIn 0.3s ease; |
|||
} |
|||
|
|||
@keyframes fadeIn { |
|||
from { |
|||
opacity: 0; |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.contact-modal { |
|||
width: 86vw; |
|||
max-height: 80vh; |
|||
background: white; |
|||
border-radius: 20rpx; |
|||
overflow: hidden; |
|||
animation: slideUp 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); |
|||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.2); |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
@keyframes slideUp { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(100rpx) scale(0.95); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0) scale(1); |
|||
} |
|||
} |
|||
|
|||
/* 可滚动的内容区域 */ |
|||
.modal-content { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
max-height: calc(80vh - 150rpx); |
|||
} |
|||
|
|||
/* 模态框头部 */ |
|||
.modal-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: flex-start; |
|||
padding: 40rpx 40rpx 30rpx; |
|||
background: linear-gradient(135deg, #f0f9f1 0%, #ffffff 100%); |
|||
border-bottom: 2rpx solid #f0f5f1; |
|||
} |
|||
|
|||
.header-left { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
flex: 1; |
|||
} |
|||
|
|||
.modal-avatar { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
border-radius: 50%; |
|||
margin-right: 25rpx; |
|||
border: 4rpx solid white; |
|||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.expert-intro { |
|||
flex: 1; |
|||
} |
|||
|
|||
.modallei { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.modal-name { |
|||
display: block; |
|||
font-size: 38rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.title-status { |
|||
display: flex; |
|||
align-items: center; |
|||
margin: 10rpx 0; |
|||
} |
|||
|
|||
.modal-title { |
|||
font-size: 28rpx; |
|||
color: #2c8c34; |
|||
background: #f0f9f1; |
|||
padding: 0rpx 10rpx; |
|||
border-radius: 10rpx; |
|||
font-weight: 500; |
|||
margin: 0 20rpx; |
|||
} |
|||
|
|||
.modal-status { |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 26rpx; |
|||
padding: 6rpx 15rpx; |
|||
border-radius: 10rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.modal-status.online { |
|||
background: #e8f5e9; |
|||
color: #2c8c34; |
|||
} |
|||
|
|||
.modal-status.offline { |
|||
background: #f5f5f5; |
|||
color: #757575; |
|||
} |
|||
|
|||
.status-dot { |
|||
width: 12rpx; |
|||
height: 12rpx; |
|||
border-radius: 50%; |
|||
margin-right: 8rpx; |
|||
background: currentColor; |
|||
} |
|||
|
|||
/* 专家简介 */ |
|||
.expert-bio { |
|||
padding: 30rpx 40rpx; |
|||
border-bottom: 2rpx solid #f0f5f1; |
|||
} |
|||
|
|||
.bio-header { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.bio-title { |
|||
font-size: 30rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.bio-content { |
|||
font-size: 28rpx; |
|||
color: #555; |
|||
line-height: 1.6; |
|||
} |
|||
|
|||
/* 联系方式区域 */ |
|||
.contact-section { |
|||
padding: 30rpx 40rpx; |
|||
border-bottom: 2rpx solid #f0f5f1; |
|||
} |
|||
|
|||
.contact-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 25rpx 0; |
|||
} |
|||
|
|||
.contact-item:not(:last-child) { |
|||
border-bottom: 1rpx solid #f5f5f5; |
|||
} |
|||
|
|||
.contact-icon-container { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
border-radius: 18rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.phone-item .contact-icon-container { |
|||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); |
|||
} |
|||
|
|||
.email-item .contact-icon-container { |
|||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); |
|||
} |
|||
|
|||
.institution-item .contact-icon-container { |
|||
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%); |
|||
} |
|||
|
|||
.contact-item-icon { |
|||
width: 32rpx; |
|||
height: 32rpx; |
|||
} |
|||
|
|||
.contact-info { |
|||
flex: 1; |
|||
} |
|||
|
|||
.contact-label { |
|||
display: block; |
|||
font-size: 26rpx; |
|||
color: #888; |
|||
margin-bottom: 6rpx; |
|||
} |
|||
|
|||
.contact-value { |
|||
display: block; |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
font-weight: 600; |
|||
margin-bottom: 4rpx; |
|||
} |
|||
|
|||
.contact-desc { |
|||
display: block; |
|||
font-size: 24rpx; |
|||
color: #aaa; |
|||
} |
|||
|
|||
.action-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 50rpx; |
|||
font-size: 22rpx; |
|||
font-weight: 600; |
|||
border: none; |
|||
margin-left: 20rpx; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.call-btn { |
|||
background: linear-gradient(135deg, #4caf50 0%, #2c8c34 100%); |
|||
color: white; |
|||
} |
|||
|
|||
.copy-btn { |
|||
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%); |
|||
color: white; |
|||
} |
|||
|
|||
.address-btn { |
|||
background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%); |
|||
color: white; |
|||
} |
|||
|
|||
.action-btn:active { |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
|
|||
|
|||
/* 咨询时间 */ |
|||
.consultation-time { |
|||
padding: 30rpx 40rpx; |
|||
border-bottom: 2rpx solid #f0f5f1; |
|||
} |
|||
|
|||
.time-header { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
|
|||
|
|||
.time-title { |
|||
font-size: 30rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.time-content { |
|||
display: block; |
|||
font-size: 28rpx; |
|||
color: #555; |
|||
line-height: 1.5; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.time-slots { |
|||
display: flex; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.time-slot { |
|||
background: #f8faf9; |
|||
padding: 10rpx 20rpx; |
|||
border-radius: 15rpx; |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
border: 1rpx solid #e8f5e9; |
|||
} |
|||
|
|||
/* 操作按钮(固定在底部) */ |
|||
.modal-actions { |
|||
display: flex; |
|||
padding: 30rpx 40rpx; |
|||
gap: 20rpx; |
|||
border-top: 2rpx solid #f0f5f1; |
|||
background: white; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.secondary-btn, |
|||
.primary-btn { |
|||
flex: 1; |
|||
border-radius: 50rpx; |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.secondary-btn { |
|||
background: #f8faf9; |
|||
color: #666; |
|||
border: 2rpx solid #e8f5e9; |
|||
} |
|||
|
|||
.primary-btn { |
|||
background: linear-gradient(135deg, #4caf50 0%, #2c8c34 100%); |
|||
color: white; |
|||
box-shadow: 0 6rpx 20rpx rgba(44, 140, 52, 0.3); |
|||
} |
|||
|
|||
.secondary-btn:active, |
|||
.primary-btn:active { |
|||
transform: scale(0.98); |
|||
} |
|||
@ -0,0 +1,753 @@ |
|||
// 获取应用实例
|
|||
const app = getApp() |
|||
|
|||
Page({ |
|||
data: { |
|||
// 专家信息
|
|||
expertInfo: { |
|||
id: 1, |
|||
name: '张明专家', |
|||
title: '资深畜牧兽医', |
|||
expertise: '牛羊疾病防治', |
|||
avatar: '/images/avatars/expert1.png', |
|||
online: true, |
|||
phone: '13800138000' |
|||
}, |
|||
|
|||
// 用户信息
|
|||
userInfo: { |
|||
id: 1001, |
|||
name: '养殖户', |
|||
avatar: '/images/avatars/user.png' |
|||
}, |
|||
|
|||
// 消息列表
|
|||
messageList: [], |
|||
scrollToView: '', |
|||
|
|||
// 输入相关
|
|||
inputValue: '', |
|||
inputFocus: false, |
|||
inputMode: 'keyboard', // keyboard or voice
|
|||
inputPlaceholder: '输入消息...', |
|||
|
|||
// 多媒体
|
|||
showMediaSheet: false, |
|||
|
|||
// 录音相关
|
|||
isRecording: false, |
|||
recordingTime: 0, |
|||
recordingTip: '上滑取消录音', |
|||
recordingTimer: null, |
|||
recordManager: null, |
|||
|
|||
// 页面状态
|
|||
isFirstLoad: true, |
|||
showDateDivider: true, |
|||
todayDate: '', |
|||
|
|||
// 消息ID计数器
|
|||
messageId: 1000, |
|||
|
|||
// 存储键名
|
|||
storageKey: 'consult_messages_' |
|||
}, |
|||
|
|||
onLoad: function(options) { |
|||
// 初始化录音管理器
|
|||
this.initRecordManager() |
|||
|
|||
// 获取今天日期
|
|||
this.setTodayDate() |
|||
|
|||
// 加载用户信息
|
|||
this.loadUserInfo() |
|||
|
|||
// 加载专家信息
|
|||
if (options.expertId) { |
|||
this.loadExpertInfo(options.expertId) |
|||
} |
|||
|
|||
// 加载聊天记录
|
|||
this.loadChatHistory() |
|||
|
|||
// 设置键盘监听
|
|||
wx.onKeyboardHeightChange(this.onKeyboardHeightChange) |
|||
|
|||
// 模拟首次进入时的欢迎消息
|
|||
setTimeout(() => { |
|||
this.setData({ isFirstLoad: false }) |
|||
}, 2000) |
|||
}, |
|||
|
|||
onUnload: function() { |
|||
// 清理定时器
|
|||
if (this.data.recordingTimer) { |
|||
clearInterval(this.data.recordingTimer) |
|||
} |
|||
|
|||
// 移除监听器
|
|||
wx.offKeyboardHeightChange() |
|||
|
|||
// 保存聊天记录
|
|||
this.saveChatHistory() |
|||
}, |
|||
|
|||
onShow: function() { |
|||
// 页面显示时自动滚动到底部
|
|||
setTimeout(() => { |
|||
this.scrollToBottom() |
|||
}, 300) |
|||
}, |
|||
|
|||
// 初始化录音管理器
|
|||
initRecordManager: function() { |
|||
this.recordManager = wx.getRecorderManager() |
|||
|
|||
this.recordManager.onStart(() => { |
|||
console.log('录音开始') |
|||
}) |
|||
|
|||
this.recordManager.onStop((res) => { |
|||
const { tempFilePath, duration } = res |
|||
if (tempFilePath) { |
|||
this.sendAudioMessage(tempFilePath, Math.floor(duration / 1000)) |
|||
} |
|||
}) |
|||
|
|||
this.recordManager.onError((err) => { |
|||
console.error('录音失败:', err) |
|||
wx.showToast({ |
|||
title: '录音失败', |
|||
icon: 'none' |
|||
}) |
|||
this.setData({ isRecording: false }) |
|||
}) |
|||
}, |
|||
|
|||
// 设置今天日期
|
|||
setTodayDate: function() { |
|||
const now = new Date() |
|||
const month = now.getMonth() + 1 |
|||
const date = now.getDate() |
|||
const week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] |
|||
this.setData({ |
|||
todayDate: `${month}月${date}日 星期${week}` |
|||
}) |
|||
}, |
|||
|
|||
// 加载用户信息
|
|||
loadUserInfo: function() { |
|||
const userInfo = wx.getStorageSync('userInfo') || this.data.userInfo |
|||
this.setData({ userInfo }) |
|||
}, |
|||
|
|||
// 加载专家信息
|
|||
loadExpertInfo: function(expertId) { |
|||
// 这里应该是API请求,暂时使用模拟数据
|
|||
wx.showLoading({ title: '加载中...' }) |
|||
|
|||
setTimeout(() => { |
|||
const expertInfo = { |
|||
id: expertId, |
|||
name: ['张明', '李华', '王强', '赵刚'][expertId - 1] + '专家', |
|||
title: '资深畜牧兽医', |
|||
expertise: '牛羊疾病防治', |
|||
avatar: `/images/avatars/expert${expertId}.png`, |
|||
online: Math.random() > 0.3, |
|||
phone: '138' + Math.floor(Math.random() * 100000000).toString().padStart(8, '0') |
|||
} |
|||
|
|||
this.setData({ |
|||
expertInfo, |
|||
storageKey: `consult_messages_${expertId}_${this.data.userInfo.id}` |
|||
}) |
|||
|
|||
wx.hideLoading() |
|||
}, 500) |
|||
}, |
|||
|
|||
// 加载聊天记录
|
|||
loadChatHistory: function() { |
|||
const storageKey = this.data.storageKey |
|||
const savedMessages = wx.getStorageSync(storageKey) || [] |
|||
|
|||
if (savedMessages.length > 0) { |
|||
// 处理消息时间显示
|
|||
const processedMessages = this.processMessageTimes(savedMessages) |
|||
this.setData({ |
|||
messageList: processedMessages, |
|||
isFirstLoad: false |
|||
}) |
|||
|
|||
// 滚动到底部
|
|||
setTimeout(() => { |
|||
this.scrollToBottom() |
|||
}, 200) |
|||
} else { |
|||
// 初始化第一条欢迎消息
|
|||
this.initWelcomeMessage() |
|||
} |
|||
}, |
|||
|
|||
// 处理消息时间显示
|
|||
processMessageTimes: function(messages) { |
|||
if (!messages || messages.length === 0) return messages |
|||
|
|||
let lastTimestamp = 0 |
|||
|
|||
return messages.map((msg, index) => { |
|||
const currentTimestamp = msg.timestamp |
|||
|
|||
// 如果两条消息间隔超过5分钟,显示时间
|
|||
const timeDiff = currentTimestamp - lastTimestamp |
|||
msg.showTime = timeDiff > 5 * 60 * 1000 || index === 0 |
|||
|
|||
if (msg.showTime) { |
|||
lastTimestamp = currentTimestamp |
|||
} |
|||
|
|||
return msg |
|||
}) |
|||
}, |
|||
|
|||
// 初始化欢迎消息
|
|||
initWelcomeMessage: function() { |
|||
const welcomeMessage = { |
|||
id: 'welcome-' + Date.now(), |
|||
sender: 'expert', |
|||
type: 'text', |
|||
content: '您好,我是' + this.data.expertInfo.name + ',有什么可以帮您?', |
|||
timestamp: Date.now() - 60000, |
|||
showTime: true |
|||
} |
|||
|
|||
this.setData({ |
|||
messageList: [welcomeMessage] |
|||
}) |
|||
}, |
|||
|
|||
// 保存聊天记录
|
|||
saveChatHistory: function() { |
|||
const { storageKey, messageList } = this.data |
|||
if (messageList.length > 0) { |
|||
try { |
|||
wx.setStorageSync(storageKey, messageList) |
|||
} catch (e) { |
|||
console.error('保存聊天记录失败:', e) |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// 返回上一页
|
|||
goBack: function() { |
|||
wx.navigateBack() |
|||
}, |
|||
|
|||
// 打电话
|
|||
makePhoneCall: function() { |
|||
const phone = this.data.expertInfo.phone |
|||
wx.makePhoneCall({ |
|||
phoneNumber: phone, |
|||
fail: () => { |
|||
wx.showToast({ |
|||
title: '拨打失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 输入处理
|
|||
onInput: function(e) { |
|||
this.setData({ |
|||
inputValue: e.detail.value |
|||
}) |
|||
}, |
|||
|
|||
// 发送文本消息
|
|||
sendTextMessage: function() { |
|||
const content = this.data.inputValue.trim() |
|||
if (!content) return |
|||
|
|||
const newMessage = { |
|||
id: 'msg-' + (++this.data.messageId), |
|||
sender: 'user', |
|||
type: 'text', |
|||
content: content, |
|||
timestamp: Date.now(), |
|||
status: 'sending' |
|||
} |
|||
|
|||
// 添加到消息列表
|
|||
this.addMessageToList(newMessage) |
|||
|
|||
// 清空输入框
|
|||
this.setData({ |
|||
inputValue: '', |
|||
inputFocus: false |
|||
}) |
|||
|
|||
// 模拟发送成功
|
|||
setTimeout(() => { |
|||
this.updateMessageStatus(newMessage.id, 'success') |
|||
|
|||
// 模拟专家回复
|
|||
setTimeout(() => { |
|||
this.receiveExpertReply() |
|||
}, 1000) |
|||
}, 500) |
|||
}, |
|||
|
|||
// 添加消息到列表
|
|||
addMessageToList: function(message) { |
|||
const { messageList } = this.data |
|||
|
|||
// 确定是否需要显示时间
|
|||
const lastMessage = messageList[messageList.length - 1] |
|||
const timeDiff = lastMessage ? message.timestamp - lastMessage.timestamp : 0 |
|||
message.showTime = timeDiff > 5 * 60 * 1000 || !lastMessage |
|||
|
|||
messageList.push(message) |
|||
|
|||
this.setData({ |
|||
messageList, |
|||
scrollToView: 'msg-' + message.id |
|||
}) |
|||
}, |
|||
|
|||
// 更新消息状态
|
|||
updateMessageStatus: function(messageId, status) { |
|||
const { messageList } = this.data |
|||
const index = messageList.findIndex(msg => msg.id === messageId) |
|||
|
|||
if (index !== -1) { |
|||
messageList[index].status = status |
|||
this.setData({ messageList }) |
|||
} |
|||
}, |
|||
|
|||
// 接收专家回复
|
|||
receiveExpertReply: function() { |
|||
const replies = [ |
|||
'收到您的消息,让我分析一下您说的情况。', |
|||
'建议您提供更多细节,比如发病时间、具体症状等。', |
|||
'根据描述,可能是饲料问题引起的,建议调整饲料配方。', |
|||
'可以考虑添加一些维生素补充剂,改善食欲问题。', |
|||
'最好能提供照片,这样我可以更准确地判断情况。' |
|||
] |
|||
|
|||
const randomReply = replies[Math.floor(Math.random() * replies.length)] |
|||
|
|||
const newMessage = { |
|||
id: 'exp-' + Date.now(), |
|||
sender: 'expert', |
|||
type: 'text', |
|||
content: randomReply, |
|||
timestamp: Date.now(), |
|||
showTime: false |
|||
} |
|||
|
|||
this.addMessageToList(newMessage) |
|||
}, |
|||
|
|||
// 切换输入模式
|
|||
switchInputMode: function() { |
|||
const newMode = this.data.inputMode === 'keyboard' ? 'voice' : 'keyboard' |
|||
const placeholder = newMode === 'voice' ? '按住说话' : '输入消息...' |
|||
|
|||
this.setData({ |
|||
inputMode: newMode, |
|||
inputPlaceholder: placeholder, |
|||
inputFocus: newMode === 'keyboard' |
|||
}) |
|||
}, |
|||
|
|||
// 显示多媒体选择面板
|
|||
showMediaActionSheet: function() { |
|||
this.setData({ |
|||
showMediaSheet: true, |
|||
inputFocus: false |
|||
}) |
|||
}, |
|||
|
|||
// 隐藏多媒体选择面板
|
|||
hideMediaActionSheet: function() { |
|||
this.setData({ |
|||
showMediaSheet: false |
|||
}) |
|||
}, |
|||
|
|||
// 选择图片
|
|||
chooseImage: function() { |
|||
this.hideMediaActionSheet() |
|||
|
|||
wx.chooseImage({ |
|||
count: 9, |
|||
sizeType: ['compressed'], |
|||
sourceType: ['album'], |
|||
success: (res) => { |
|||
this.uploadImages(res.tempFilePaths) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 拍照
|
|||
takePhoto: function() { |
|||
this.hideMediaActionSheet() |
|||
|
|||
wx.chooseImage({ |
|||
count: 1, |
|||
sizeType: ['compressed'], |
|||
sourceType: ['camera'], |
|||
success: (res) => { |
|||
this.uploadImages(res.tempFilePaths) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 选择视频
|
|||
chooseVideo: function() { |
|||
this.hideMediaActionSheet() |
|||
|
|||
wx.chooseVideo({ |
|||
sourceType: ['album'], |
|||
compressed: true, |
|||
maxDuration: 60, |
|||
success: (res) => { |
|||
this.uploadVideo(res.tempFilePath, res.thumbTempFilePath) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 录制语音
|
|||
recordAudio: function() { |
|||
this.hideMediaActionSheet() |
|||
this.startVoiceRecord() |
|||
}, |
|||
|
|||
// 选择文件
|
|||
chooseFile: function() { |
|||
this.hideMediaActionSheet() |
|||
|
|||
wx.chooseMessageFile({ |
|||
count: 1, |
|||
type: 'all', |
|||
success: (res) => { |
|||
const file = res.tempFiles[0] |
|||
this.uploadFile(file.path, file.name, file.size) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 上传图片
|
|||
uploadImages: function(tempFilePaths) { |
|||
tempFilePaths.forEach((tempFilePath, index) => { |
|||
const fileName = 'image_' + Date.now() + '_' + index + '.jpg' |
|||
this.uploadFile(tempFilePath, fileName, 0, 'image') |
|||
}) |
|||
}, |
|||
|
|||
// 上传视频
|
|||
uploadVideo: function(tempFilePath, thumbPath) { |
|||
const fileName = 'video_' + Date.now() + '.mp4' |
|||
this.uploadFile(tempFilePath, fileName, 0, 'video', thumbPath) |
|||
}, |
|||
|
|||
// 通用文件上传
|
|||
uploadFile: function(tempFilePath, fileName, fileSize = 0, type = 'file', thumbPath = '') { |
|||
// 获取文件扩展名
|
|||
const extension = fileName.split('.').pop().toLowerCase() |
|||
|
|||
// 创建消息
|
|||
const messageId = 'file-' + Date.now() |
|||
const message = { |
|||
id: messageId, |
|||
sender: 'user', |
|||
type: type, |
|||
content: tempFilePath, |
|||
thumb: thumbPath, |
|||
fileName: fileName, |
|||
fileSize: fileSize, |
|||
extension: extension, |
|||
timestamp: Date.now(), |
|||
status: 'uploading', |
|||
progress: 0 |
|||
} |
|||
|
|||
this.addMessageToList(message) |
|||
|
|||
// 模拟上传过程
|
|||
let progress = 0 |
|||
const uploadInterval = setInterval(() => { |
|||
progress += Math.random() * 20 + 10 |
|||
|
|||
if (progress >= 100) { |
|||
progress = 100 |
|||
clearInterval(uploadInterval) |
|||
|
|||
setTimeout(() => { |
|||
this.updateMessageStatus(messageId, 'success') |
|||
|
|||
// 清除进度信息
|
|||
const { messageList } = this.data |
|||
const index = messageList.findIndex(msg => msg.id === messageId) |
|||
if (index !== -1) { |
|||
delete messageList[index].progress |
|||
this.setData({ messageList }) |
|||
|
|||
// 模拟专家回复
|
|||
if (type === 'image' || type === 'video') { |
|||
setTimeout(() => { |
|||
this.receiveMediaReply(type) |
|||
}, 800) |
|||
} |
|||
} |
|||
}, 200) |
|||
} |
|||
|
|||
// 更新进度
|
|||
const { messageList } = this.data |
|||
const index = messageList.findIndex(msg => msg.id === messageId) |
|||
if (index !== -1) { |
|||
messageList[index].progress = Math.min(progress, 100) |
|||
this.setData({ messageList }) |
|||
} |
|||
}, 100) |
|||
}, |
|||
|
|||
// 接收媒体回复
|
|||
receiveMediaReply: function(type) { |
|||
const imageReplies = [ |
|||
'照片收到了,牛的状况看起来确实不太理想。', |
|||
'从照片看,饲养环境需要改善一下。', |
|||
'图片清晰,我可以更准确地判断问题了。' |
|||
] |
|||
|
|||
const videoReplies = [ |
|||
'视频看到了,动物的精神状态需要关注。', |
|||
'从视频可以观察到更多细节,这很有帮助。', |
|||
'视频内容很有价值,让我了解了具体情况。' |
|||
] |
|||
|
|||
const replies = type === 'image' ? imageReplies : videoReplies |
|||
const randomReply = replies[Math.floor(Math.random() * replies.length)] |
|||
|
|||
const newMessage = { |
|||
id: 'exp-media-' + Date.now(), |
|||
sender: 'expert', |
|||
type: 'text', |
|||
content: randomReply, |
|||
timestamp: Date.now(), |
|||
showTime: false |
|||
} |
|||
|
|||
this.addMessageToList(newMessage) |
|||
}, |
|||
|
|||
// 开始语音录制
|
|||
startVoiceRecord: function(e) { |
|||
this.setData({ |
|||
isRecording: true, |
|||
recordingTime: 0, |
|||
recordingTip: '上滑取消录音' |
|||
}) |
|||
|
|||
// 开始录音
|
|||
this.recordManager.start({ |
|||
duration: 60000, // 最长60秒
|
|||
sampleRate: 44100, |
|||
numberOfChannels: 1, |
|||
encodeBitRate: 192000, |
|||
format: 'mp3' |
|||
}) |
|||
|
|||
// 开始计时
|
|||
const timer = setInterval(() => { |
|||
const time = this.data.recordingTime + 1 |
|||
this.setData({ recordingTime: time }) |
|||
}, 1000) |
|||
|
|||
this.setData({ recordingTimer: timer }) |
|||
}, |
|||
|
|||
// 结束语音录制
|
|||
endVoiceRecord: function() { |
|||
if (this.data.isRecording) { |
|||
this.recordManager.stop() |
|||
|
|||
if (this.data.recordingTimer) { |
|||
clearInterval(this.data.recordingTimer) |
|||
} |
|||
|
|||
this.setData({ |
|||
isRecording: false, |
|||
recordingTime: 0 |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 发送语音消息
|
|||
sendAudioMessage: function(tempFilePath, duration) { |
|||
const message = { |
|||
id: 'audio-' + Date.now(), |
|||
sender: 'user', |
|||
type: 'audio', |
|||
content: tempFilePath, |
|||
duration: duration, |
|||
timestamp: Date.now(), |
|||
status: 'sending' |
|||
} |
|||
|
|||
this.addMessageToList(message) |
|||
|
|||
// 模拟发送成功
|
|||
setTimeout(() => { |
|||
this.updateMessageStatus(message.id, 'success') |
|||
|
|||
// 模拟专家回复
|
|||
setTimeout(() => { |
|||
const reply = { |
|||
id: 'exp-audio-' + Date.now(), |
|||
sender: 'expert', |
|||
type: 'text', |
|||
content: '语音收到了,我会仔细听取分析。', |
|||
timestamp: Date.now(), |
|||
showTime: false |
|||
} |
|||
this.addMessageToList(reply) |
|||
}, 1500) |
|||
}, 800) |
|||
}, |
|||
|
|||
// 预览图片
|
|||
previewImage: function(e) { |
|||
const url = e.currentTarget.dataset.url |
|||
wx.previewImage({ |
|||
current: url, |
|||
urls: [url] |
|||
}) |
|||
}, |
|||
|
|||
// 下载文件
|
|||
downloadFile: function(e) { |
|||
const url = e.currentTarget.dataset.url |
|||
wx.showLoading({ title: '下载中...' }) |
|||
|
|||
wx.downloadFile({ |
|||
url: url, |
|||
success: (res) => { |
|||
wx.hideLoading() |
|||
wx.showToast({ |
|||
title: '下载成功', |
|||
icon: 'success' |
|||
}) |
|||
|
|||
// 保存到相册(如果是图片)
|
|||
if (url.match(/\.(jpg|jpeg|png|gif)$/i)) { |
|||
wx.saveImageToPhotosAlbum({ |
|||
filePath: res.tempFilePath, |
|||
success: () => { |
|||
wx.showToast({ |
|||
title: '已保存到相册', |
|||
icon: 'success' |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
fail: () => { |
|||
wx.hideLoading() |
|||
wx.showToast({ |
|||
title: '下载失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 键盘高度变化
|
|||
onKeyboardHeightChange: function(res) { |
|||
if (res.height > 0) { |
|||
// 键盘弹出时隐藏多媒体面板
|
|||
this.setData({ showMediaSheet: false }) |
|||
|
|||
// 滚动到底部
|
|||
setTimeout(() => { |
|||
this.scrollToBottom() |
|||
}, 100) |
|||
} |
|||
}, |
|||
|
|||
// 滚动到底部
|
|||
scrollToBottom: function() { |
|||
if (this.data.messageList.length > 0) { |
|||
const lastMessage = this.data.messageList[this.data.messageList.length - 1] |
|||
this.setData({ |
|||
scrollToView: lastMessage.id |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 格式化时间(微信样式)
|
|||
formatTime: function(timestamp) { |
|||
const now = new Date() |
|||
const date = new Date(timestamp) |
|||
const diff = now - date |
|||
|
|||
// 今天
|
|||
if (date.toDateString() === now.toDateString()) { |
|||
return this.formatMessageTime(timestamp) |
|||
} |
|||
|
|||
// 昨天
|
|||
const yesterday = new Date(now) |
|||
yesterday.setDate(yesterday.getDate() - 1) |
|||
if (date.toDateString() === yesterday.toDateString()) { |
|||
return '昨天 ' + this.formatMessageTime(timestamp) |
|||
} |
|||
|
|||
// 一周内
|
|||
if (diff < 7 * 24 * 60 * 60 * 1000) { |
|||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] |
|||
return weekDays[date.getDay()] + ' ' + this.formatMessageTime(timestamp) |
|||
} |
|||
|
|||
// 今年内
|
|||
if (date.getFullYear() === now.getFullYear()) { |
|||
return `${date.getMonth() + 1}月${date.getDate()}日 ${this.formatMessageTime(timestamp)}` |
|||
} |
|||
|
|||
// 更早
|
|||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${this.formatMessageTime(timestamp)}` |
|||
}, |
|||
|
|||
// 格式化消息时间(HH:mm)
|
|||
formatMessageTime: function(timestamp) { |
|||
const date = new Date(timestamp) |
|||
const hours = date.getHours().toString().padStart(2, '0') |
|||
const minutes = date.getMinutes().toString().padStart(2, '0') |
|||
return `${hours}:${minutes}` |
|||
}, |
|||
|
|||
// 格式化文件大小
|
|||
formatFileSize: function(bytes) { |
|||
if (bytes === 0) return '未知大小' |
|||
|
|||
const units = ['B', 'KB', 'MB', 'GB'] |
|||
let size = bytes |
|||
let unitIndex = 0 |
|||
|
|||
while (size >= 1024 && unitIndex < units.length - 1) { |
|||
size /= 1024 |
|||
unitIndex++ |
|||
} |
|||
|
|||
return size.toFixed(1) + units[unitIndex] |
|||
}, |
|||
|
|||
// 阻止事件冒泡
|
|||
stopPropagation: function() { |
|||
// 空函数,仅用于阻止事件冒泡
|
|||
} |
|||
}) |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText":"问专家", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,296 @@ |
|||
<view class="consult-page"> |
|||
<!-- 头部专家信息 --> |
|||
<view class="consult-header"> |
|||
<view class="header-content"> |
|||
<view class="header-left"> |
|||
<button class="back-btn" bindtap="goBack"> |
|||
<image src="/images/icons/back.png" class="back-icon"></image> |
|||
</button> |
|||
</view> |
|||
|
|||
<view class="header-center"> |
|||
<text class="expert-name">{{expertInfo.name}}</text> |
|||
<view class="expert-status"> |
|||
<view class="status-dot {{expertInfo.online ? 'online' : 'offline'}}"></view> |
|||
<text class="status-text">{{expertInfo.online ? '在线' : '离线'}}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="header-right"> |
|||
<button class="header-action-btn" bindtap="makePhoneCall"> |
|||
<image src="/images/icons/phone.png" class="header-action-icon"></image> |
|||
</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 聊天内容区域 --> |
|||
<scroll-view class="chat-container" scroll-y scroll-into-view="{{scrollToView}}" scroll-with-animation="true"> |
|||
<!-- 首次咨询欢迎语 --> |
|||
<view class="system-welcome" wx:if="{{isFirstLoad}}"> |
|||
<view class="welcome-avatar"> |
|||
<image src="{{expertInfo.avatar}}"></image> |
|||
</view> |
|||
<text class="welcome-name">{{expertInfo.name}}</text> |
|||
<text class="welcome-title">{{expertInfo.title}} · {{expertInfo.expertise}}</text> |
|||
<text class="welcome-tip">您好,我是{{expertInfo.name}},很高兴为您服务!</text> |
|||
</view> |
|||
|
|||
<!-- 日期分隔线 --> |
|||
<view class="date-divider" wx:if="{{showDateDivider}}"> |
|||
<text class="date-text">{{todayDate}}</text> |
|||
</view> |
|||
|
|||
<!-- 消息列表 --> |
|||
<block wx:for="{{messageList}}" wx:key="id"> |
|||
<!-- 时间分隔 --> |
|||
<view class="time-divider" wx:if="{{item.showTime}}"> |
|||
<text>{{formatTime(item.timestamp)}}</text> |
|||
</view> |
|||
|
|||
<!-- 对方消息 --> |
|||
<view class="message-item message-left" wx:if="{{item.sender === 'expert'}}"> |
|||
<view class="message-avatar"> |
|||
<image src="{{expertInfo.avatar}}"></image> |
|||
</view> |
|||
|
|||
<view class="message-content-wrapper"> |
|||
<!-- 文本消息 --> |
|||
<view class="message-bubble message-bubble-left" wx:if="{{item.type === 'text'}}"> |
|||
<text class="message-text">{{item.content}}</text> |
|||
</view> |
|||
|
|||
<!-- 图片消息 --> |
|||
<view class="message-bubble message-bubble-left" wx:elif="{{item.type === 'image'}}"> |
|||
<image src="{{item.content}}" class="message-image" mode="widthFix" bindtap="previewImage" data-url="{{item.content}}"></image> |
|||
</view> |
|||
|
|||
<!-- 视频消息 --> |
|||
<view class="message-bubble message-bubble-left" wx:elif="{{item.type === 'video'}}"> |
|||
<video src="{{item.content}}" class="message-video" controls poster="{{item.thumb}}"></video> |
|||
<view class="video-play-btn"> |
|||
<image src="/images/icons/play.png"></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 语音消息 --> |
|||
<view class="message-bubble message-bubble-left message-audio" wx:elif="{{item.type === 'audio'}}"> |
|||
<image src="/images/icons/audio-left.png" class="audio-icon-left"></image> |
|||
<view class="audio-content"> |
|||
<view class="audio-wave"> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
</view> |
|||
<text class="audio-duration">{{item.duration}}''</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 文件消息 --> |
|||
<view class="message-bubble message-bubble-left message-file" wx:elif="{{item.type === 'file'}}"> |
|||
<view class="file-icon-box"> |
|||
<image src="/images/icons/file.png" class="file-icon"></image> |
|||
<text class="file-extension">{{item.extension}}</text> |
|||
</view> |
|||
<view class="file-info"> |
|||
<text class="file-name">{{item.fileName}}</text> |
|||
<text class="file-size">{{formatFileSize(item.fileSize)}}</text> |
|||
</view> |
|||
<button class="file-download-btn" bindtap="downloadFile" data-url="{{item.content}}">下载</button> |
|||
</view> |
|||
|
|||
<text class="message-time">{{formatMessageTime(item.timestamp)}}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 我的消息 --> |
|||
<view class="message-item message-right" wx:else> |
|||
<view class="message-content-wrapper"> |
|||
<!-- 消息状态 --> |
|||
<view class="message-status"> |
|||
<image wx:if="{{item.status === 'sending'}}" src="/images/icons/sending.png" class="status-icon"></image> |
|||
<image wx:if="{{item.status === 'success'}}" src="/images/icons/success.png" class="status-icon"></image> |
|||
<image wx:if="{{item.status === 'error'}}" src="/images/icons/error.png" class="status-icon"></image> |
|||
</view> |
|||
|
|||
<!-- 文本消息 --> |
|||
<view class="message-bubble message-bubble-right" wx:if="{{item.type === 'text'}}"> |
|||
<text class="message-text">{{item.content}}</text> |
|||
</view> |
|||
|
|||
<!-- 图片消息 --> |
|||
<view class="message-bubble message-bubble-right" wx:elif="{{item.type === 'image'}}"> |
|||
<image src="{{item.content}}" class="message-image" mode="widthFix" bindtap="previewImage" data-url="{{item.content}}"></image> |
|||
<view class="upload-progress" wx:if="{{item.status === 'uploading'}}"> |
|||
<view class="progress-circle"> |
|||
<view class="progress-fill" style="transform: rotate({{item.progress * 3.6}}deg);"></view> |
|||
<text class="progress-text">{{item.progress}}%</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 视频消息 --> |
|||
<view class="message-bubble message-bubble-right" wx:elif="{{item.type === 'video'}}"> |
|||
<video src="{{item.content}}" class="message-video" controls poster="{{item.thumb}}"></video> |
|||
<view class="video-play-btn"> |
|||
<image src="/images/icons/play.png"></image> |
|||
</view> |
|||
<view class="upload-progress" wx:if="{{item.status === 'uploading'}}"> |
|||
<view class="progress-circle"> |
|||
<view class="progress-fill" style="transform: rotate({{item.progress * 3.6}}deg);"></view> |
|||
<text class="progress-text">{{item.progress}}%</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 语音消息 --> |
|||
<view class="message-bubble message-bubble-right message-audio" wx:elif="{{item.type === 'audio'}}"> |
|||
<view class="audio-content"> |
|||
<view class="audio-wave"> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
<view class="wave-bar"></view> |
|||
</view> |
|||
<text class="audio-duration">{{item.duration}}''</text> |
|||
</view> |
|||
<image src="/images/icons/audio-right.png" class="audio-icon-right"></image> |
|||
</view> |
|||
|
|||
<!-- 文件消息 --> |
|||
<view class="message-bubble message-bubble-right message-file" wx:elif="{{item.type === 'file'}}"> |
|||
<view class="file-icon-box"> |
|||
<image src="/images/icons/file.png" class="file-icon"></image> |
|||
<text class="file-extension">{{item.extension}}</text> |
|||
</view> |
|||
<view class="file-info"> |
|||
<text class="file-name">{{item.fileName}}</text> |
|||
<text class="file-size">{{formatFileSize(item.fileSize)}}</text> |
|||
</view> |
|||
<view class="upload-progress" wx:if="{{item.status === 'uploading'}}"> |
|||
<view class="progress-circle"> |
|||
<view class="progress-fill" style="transform: rotate({{item.progress * 3.6}}deg);"></view> |
|||
<text class="progress-text">{{item.progress}}%</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<text class="message-time">{{formatMessageTime(item.timestamp)}}</text> |
|||
</view> |
|||
|
|||
<view class="message-avatar"> |
|||
<image src="{{userInfo.avatar}}"></image> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
</scroll-view> |
|||
|
|||
<!-- 输入区域 --> |
|||
<view class="input-section"> |
|||
<!-- 录音面板 --> |
|||
<view class="voice-panel" wx:if="{{inputMode === 'voice'}}"> |
|||
<button class="voice-record-btn" bindtouchstart="startVoiceRecord" bindtouchend="endVoiceRecord"> |
|||
<image src="/images/icons/mic.png" class="mic-icon"></image> |
|||
<text class="voice-tip">按住说话</text> |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 正常输入面板 --> |
|||
<view class="input-panel" wx:else> |
|||
<!-- 语音/键盘切换按钮 --> |
|||
<button class="input-mode-btn" bindtap="switchInputMode"> |
|||
<image src="{{inputMode === 'keyboard' ? '/images/icons/voice.png' : '/images/icons/keyboard.png'}}" class="mode-icon"></image> |
|||
</button> |
|||
|
|||
<!-- 输入框 --> |
|||
<view class="input-box-wrapper"> |
|||
<input |
|||
type="text" |
|||
class="chat-input" |
|||
placeholder="{{inputPlaceholder}}" |
|||
placeholder-class="placeholder" |
|||
value="{{inputValue}}" |
|||
bindinput="onInput" |
|||
bindconfirm="sendTextMessage" |
|||
confirm-type="send" |
|||
focus="{{inputFocus}}" |
|||
adjust-position="{{false}}" |
|||
/> |
|||
</view> |
|||
|
|||
<!-- 多媒体按钮 --> |
|||
<button class="media-btn" bindtap="showMediaActionSheet"> |
|||
<image src="/images/icons/add.png" class="media-icon"></image> |
|||
</button> |
|||
|
|||
<!-- 发送按钮 --> |
|||
<button class="send-btn" bindtap="sendTextMessage" wx:if="{{inputValue.trim()}}"> |
|||
<image src="/images/icons/send.png" class="send-icon"></image> |
|||
</button> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 多媒体选择面板 --> |
|||
<view class="media-action-sheet" wx:if="{{showMediaSheet}}" catchtap="hideMediaActionSheet"> |
|||
<view class="media-sheet-content" catchtap="stopPropagation"> |
|||
<view class="media-sheet-header"> |
|||
<text class="sheet-title">发送内容</text> |
|||
<button class="close-sheet-btn" bindtap="hideMediaActionSheet"> |
|||
<image src="/images/icons/close.png"></image> |
|||
</button> |
|||
</view> |
|||
|
|||
<view class="media-options"> |
|||
<button class="media-option" bindtap="chooseImage"> |
|||
<view class="option-icon photo-icon"> |
|||
<image src="/images/icons/photo.png"></image> |
|||
</view> |
|||
<text class="option-text">照片</text> |
|||
</button> |
|||
|
|||
<button class="media-option" bindtap="takePhoto"> |
|||
<view class="option-icon camera-icon"> |
|||
<image src="/images/icons/camera.png"></image> |
|||
</view> |
|||
<text class="option-text">拍摄</text> |
|||
</button> |
|||
|
|||
<button class="media-option" bindtap="chooseVideo"> |
|||
<view class="option-icon video-icon"> |
|||
<image src="/images/icons/video.png"></image> |
|||
</view> |
|||
<text class="option-text">视频</text> |
|||
</button> |
|||
|
|||
<button class="media-option" bindtap="recordAudio"> |
|||
<view class="option-icon audio-icon"> |
|||
<image src="/images/icons/voice.png"></image> |
|||
</view> |
|||
<text class="option-text">语音</text> |
|||
</button> |
|||
|
|||
<button class="media-option" bindtap="chooseFile"> |
|||
<view class="option-icon file-icon"> |
|||
<image src="/images/icons/file2.png"></image> |
|||
</view> |
|||
<text class="option-text">文件</text> |
|||
</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 录音提示 --> |
|||
<view class="recording-modal" wx:if="{{isRecording}}"> |
|||
<view class="recording-box"> |
|||
<view class="recording-wave-box"> |
|||
<view class="recording-wave"></view> |
|||
<image src="/images/icons/mic2.png" class="recording-mic-icon"></image> |
|||
</view> |
|||
<text class="recording-tip">{{recordingTip}}</text> |
|||
<text class="recording-time">{{recordingTime}}s</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,928 @@ |
|||
/* 页面整体样式 */ |
|||
.consult-page { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|||
display: flex; |
|||
flex-direction: column; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; |
|||
} |
|||
|
|||
/* 头部样式 */ |
|||
.consult-header { |
|||
background: rgba(255, 255, 255, 0.95); |
|||
backdrop-filter: blur(20px); |
|||
padding: 15rpx 30rpx; |
|||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.1); |
|||
position: relative; |
|||
z-index: 100; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.header-content { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 90rpx; |
|||
} |
|||
|
|||
.header-left { |
|||
flex: 1; |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
.header-center { |
|||
flex: 2; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
.header-right { |
|||
flex: 1; |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
} |
|||
|
|||
.back-btn { |
|||
width: 70rpx; |
|||
height: 70rpx; |
|||
border-radius: 50%; |
|||
border: none; |
|||
background: rgba(0, 0, 0, 0.05); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.back-btn:active { |
|||
background: rgba(0, 0, 0, 0.1); |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.back-icon { |
|||
width: 30rpx; |
|||
height: 30rpx; |
|||
} |
|||
|
|||
.expert-name { |
|||
font-size: 34rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 5rpx; |
|||
} |
|||
|
|||
.expert-status { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.status-dot { |
|||
width: 14rpx; |
|||
height: 14rpx; |
|||
border-radius: 50%; |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
.status-dot.online { |
|||
background: #07c160; |
|||
box-shadow: 0 0 10rpx rgba(7, 193, 96, 0.5); |
|||
} |
|||
|
|||
.status-dot.offline { |
|||
background: #999; |
|||
} |
|||
|
|||
.status-text { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.header-action-btn { |
|||
width: 70rpx; |
|||
height: 70rpx; |
|||
border-radius: 50%; |
|||
border: none; |
|||
background: rgba(0, 0, 0, 0.05); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.header-action-btn:active { |
|||
background: rgba(0, 0, 0, 0.1); |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.header-action-icon { |
|||
width: 32rpx; |
|||
height: 32rpx; |
|||
} |
|||
|
|||
/* 聊天容器 */ |
|||
.chat-container { |
|||
flex: 1; |
|||
padding: 30rpx 20rpx 0; |
|||
background: linear-gradient(180deg, #f5f7fa 0%, #f0f2f5 100%); |
|||
overflow-y: auto; |
|||
position: relative; |
|||
} |
|||
|
|||
/* 欢迎语 */ |
|||
.system-welcome { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 60rpx 40rpx; |
|||
text-align: center; |
|||
background: white; |
|||
border-radius: 30rpx; |
|||
margin: 0 auto 40rpx; |
|||
max-width: 80%; |
|||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.08); |
|||
animation: fadeInUp 0.6s ease; |
|||
} |
|||
|
|||
@keyframes fadeInUp { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(50rpx); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.welcome-avatar { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
margin-bottom: 20rpx; |
|||
border: 4rpx solid white; |
|||
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.welcome-avatar image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.welcome-name { |
|||
font-size: 34rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.welcome-title { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
margin-bottom: 25rpx; |
|||
} |
|||
|
|||
.welcome-tip { |
|||
font-size: 28rpx; |
|||
color: #4caf50; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
/* 日期分隔线 */ |
|||
.date-divider { |
|||
display: flex; |
|||
justify-content: center; |
|||
margin: 40rpx 0; |
|||
} |
|||
|
|||
.date-text { |
|||
background: rgba(0, 0, 0, 0.05); |
|||
padding: 8rpx 24rpx; |
|||
border-radius: 20rpx; |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 时间分隔 */ |
|||
.time-divider { |
|||
display: flex; |
|||
justify-content: center; |
|||
margin: 30rpx 0; |
|||
animation: fadeIn 0.3s ease; |
|||
} |
|||
|
|||
@keyframes fadeIn { |
|||
from { |
|||
opacity: 0; |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.time-divider text { |
|||
background: rgba(0, 0, 0, 0.05); |
|||
padding: 6rpx 20rpx; |
|||
border-radius: 15rpx; |
|||
font-size: 22rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
/* 消息项 */ |
|||
.message-item { |
|||
display: flex; |
|||
margin-bottom: 40rpx; |
|||
animation: slideIn 0.3s ease; |
|||
} |
|||
|
|||
@keyframes slideIn { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(20rpx); |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.message-left { |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
.message-right { |
|||
justify-content: flex-end; |
|||
} |
|||
|
|||
.message-avatar { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 10rpx; |
|||
overflow: hidden; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.message-left .message-avatar { |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.message-right .message-avatar { |
|||
margin-left: 20rpx; |
|||
} |
|||
|
|||
.message-avatar image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.message-content-wrapper { |
|||
max-width: 65%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.message-left .message-content-wrapper { |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.message-right .message-content-wrapper { |
|||
align-items: flex-end; |
|||
} |
|||
|
|||
/* 消息气泡 */ |
|||
.message-bubble { |
|||
padding: 20rpx 25rpx; |
|||
border-radius: 20rpx; |
|||
position: relative; |
|||
word-break: break-word; |
|||
line-height: 1.5; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.message-bubble-left { |
|||
background: white; |
|||
border-radius: 0 20rpx 20rpx 20rpx; |
|||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08); |
|||
} |
|||
|
|||
.message-bubble-right { |
|||
background: linear-gradient(135deg, #95ec69 0%, #5ac725 100%); |
|||
color: white; |
|||
border-radius: 20rpx 0 20rpx 20rpx; |
|||
box-shadow: 0 4rpx 20rpx rgba(90, 199, 37, 0.2); |
|||
} |
|||
|
|||
.message-text { |
|||
font-size: 30rpx; |
|||
line-height: 1.5; |
|||
} |
|||
|
|||
.message-bubble-left .message-text { |
|||
color: #333; |
|||
} |
|||
|
|||
.message-bubble-right .message-text { |
|||
color: white; |
|||
} |
|||
|
|||
/* 消息时间 */ |
|||
.message-time { |
|||
font-size: 22rpx; |
|||
color: #999; |
|||
margin-top: 8rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
/* 消息状态 */ |
|||
.message-status { |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.status-icon { |
|||
width: 28rpx; |
|||
height: 28rpx; |
|||
} |
|||
|
|||
/* 图片消息 */ |
|||
.message-image { |
|||
max-width: 300rpx; |
|||
max-height: 400rpx; |
|||
border-radius: 12rpx; |
|||
display: block; |
|||
} |
|||
|
|||
/* 视频消息 */ |
|||
.message-video { |
|||
width: 300rpx; |
|||
height: 200rpx; |
|||
border-radius: 12rpx; |
|||
background: #000; |
|||
} |
|||
|
|||
.video-play-btn { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
background: rgba(0, 0, 0, 0.6); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.video-play-btn image { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
} |
|||
|
|||
/* 语音消息 */ |
|||
.message-audio { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 25rpx; |
|||
} |
|||
|
|||
.audio-icon-left { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.audio-icon-right { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
margin-left: 20rpx; |
|||
} |
|||
|
|||
.audio-content { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.audio-wave { |
|||
display: flex; |
|||
align-items: flex-end; |
|||
height: 40rpx; |
|||
margin-right: 15rpx; |
|||
} |
|||
|
|||
.wave-bar { |
|||
width: 4rpx; |
|||
margin: 0 2rpx; |
|||
background: currentColor; |
|||
animation: wave 1.2s ease-in-out infinite; |
|||
} |
|||
|
|||
.wave-bar:nth-child(1) { |
|||
height: 20rpx; |
|||
animation-delay: 0s; |
|||
} |
|||
|
|||
.wave-bar:nth-child(2) { |
|||
height: 30rpx; |
|||
animation-delay: 0.2s; |
|||
} |
|||
|
|||
.wave-bar:nth-child(3) { |
|||
height: 40rpx; |
|||
animation-delay: 0.4s; |
|||
} |
|||
|
|||
.wave-bar:nth-child(4) { |
|||
height: 30rpx; |
|||
animation-delay: 0.6s; |
|||
} |
|||
|
|||
.wave-bar:nth-child(5) { |
|||
height: 20rpx; |
|||
animation-delay: 0.8s; |
|||
} |
|||
|
|||
@keyframes wave { |
|||
0%, 100% { |
|||
transform: scaleY(0.5); |
|||
} |
|||
50% { |
|||
transform: scaleY(1); |
|||
} |
|||
} |
|||
|
|||
.audio-duration { |
|||
font-size: 28rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.message-bubble-left .audio-duration { |
|||
color: #666; |
|||
} |
|||
|
|||
.message-bubble-right .audio-duration { |
|||
color: white; |
|||
} |
|||
|
|||
/* 文件消息 */ |
|||
.message-file { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 25rpx; |
|||
min-width: 300rpx; |
|||
} |
|||
|
|||
.file-icon-box { |
|||
position: relative; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.file-icon { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
} |
|||
|
|||
.file-extension { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
font-size: 18rpx; |
|||
font-weight: 600; |
|||
color: white; |
|||
} |
|||
|
|||
.file-info { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.file-name { |
|||
font-size: 28rpx; |
|||
font-weight: 500; |
|||
margin-bottom: 8rpx; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
max-width: 200rpx; |
|||
} |
|||
|
|||
.message-bubble-left .file-name { |
|||
color: #333; |
|||
} |
|||
|
|||
.message-bubble-right .file-name { |
|||
color: white; |
|||
} |
|||
|
|||
.file-size { |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.message-bubble-left .file-size { |
|||
color: #999; |
|||
} |
|||
|
|||
.message-bubble-right .file-size { |
|||
color: rgba(255, 255, 255, 0.9); |
|||
} |
|||
|
|||
.file-download-btn { |
|||
background: rgba(0, 0, 0, 0.1); |
|||
border: none; |
|||
padding: 10rpx 20rpx; |
|||
border-radius: 15rpx; |
|||
font-size: 24rpx; |
|||
color: white; |
|||
margin-left: 15rpx; |
|||
} |
|||
|
|||
.file-download-btn:active { |
|||
background: rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
/* 上传进度 */ |
|||
.upload-progress { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
border-radius: 12rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.progress-circle { |
|||
position: relative; |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
} |
|||
|
|||
.progress-fill { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 100%; |
|||
border: 6rpx solid #4caf50; |
|||
border-radius: 50%; |
|||
border-top-color: transparent; |
|||
border-right-color: transparent; |
|||
} |
|||
|
|||
.progress-text { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
font-size: 22rpx; |
|||
color: white; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
/* 输入区域 */ |
|||
.input-section { |
|||
background: white; |
|||
border-top: 1rpx solid #e0e0e0; |
|||
padding: 20rpx 30rpx; |
|||
position: relative; |
|||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
/* 语音面板 */ |
|||
.voice-panel { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 20rpx 0; |
|||
} |
|||
|
|||
.voice-record-btn { |
|||
width: 100%; |
|||
height: 100rpx; |
|||
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); |
|||
border: none; |
|||
border-radius: 50rpx; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.voice-record-btn:active { |
|||
background: linear-gradient(135deg, #e0e0e0 0%, #d0d0d0 100%); |
|||
transform: scale(0.98); |
|||
} |
|||
|
|||
.mic-icon { |
|||
width: 50rpx; |
|||
height: 50rpx; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.voice-tip { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
/* 输入面板 */ |
|||
.input-panel { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.input-mode-btn { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
border: none; |
|||
background: #f5f5f5; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.input-mode-btn:active { |
|||
background: #e0e0e0; |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.mode-icon { |
|||
width: 36rpx; |
|||
height: 36rpx; |
|||
} |
|||
|
|||
.input-box-wrapper { |
|||
flex: 1; |
|||
background: #f5f5f5; |
|||
border-radius: 40rpx; |
|||
padding: 0 30rpx; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.input-box-wrapper:active { |
|||
background: #e8e8e8; |
|||
} |
|||
|
|||
.chat-input { |
|||
flex: 1; |
|||
height: 100%; |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
} |
|||
|
|||
.placeholder { |
|||
color: #999; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.media-btn, |
|||
.send-btn { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
border: none; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.media-btn { |
|||
background: #f5f5f5; |
|||
} |
|||
|
|||
.media-btn:active { |
|||
background: #e0e0e0; |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.send-btn { |
|||
background: linear-gradient(135deg, #95ec69 0%, #5ac725 100%); |
|||
} |
|||
|
|||
.send-btn:active { |
|||
background: linear-gradient(135deg, #5ac725 0%, #4caf50 100%); |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.media-icon, |
|||
.send-icon { |
|||
width: 36rpx; |
|||
height: 36rpx; |
|||
} |
|||
|
|||
.send-icon { |
|||
filter: brightness(0) invert(1); |
|||
} |
|||
|
|||
/* 多媒体选择面板 */ |
|||
.media-action-sheet { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: flex-end; |
|||
justify-content: center; |
|||
z-index: 1000; |
|||
animation: fadeIn 0.3s ease; |
|||
} |
|||
|
|||
.media-sheet-content { |
|||
width: 100%; |
|||
background: white; |
|||
border-radius: 40rpx 40rpx 0 0; |
|||
padding: 40rpx 30rpx 60rpx; |
|||
animation: slideUp 0.3s ease; |
|||
} |
|||
|
|||
@keyframes slideUp { |
|||
from { |
|||
transform: translateY(100%); |
|||
} |
|||
to { |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.media-sheet-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.sheet-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.close-sheet-btn { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
border-radius: 50%; |
|||
border: none; |
|||
background: #f5f5f5; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.close-sheet-btn image { |
|||
width: 24rpx; |
|||
height: 24rpx; |
|||
} |
|||
|
|||
.media-options { |
|||
display: grid; |
|||
grid-template-columns: repeat(4, 1fr); |
|||
gap: 30rpx; |
|||
} |
|||
|
|||
.media-option { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
border: none; |
|||
background: none; |
|||
padding: 20rpx 10rpx; |
|||
border-radius: 20rpx; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.media-option:active { |
|||
background: #f5f5f5; |
|||
} |
|||
|
|||
.option-icon { |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
border-radius: 25rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.photo-icon { |
|||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); |
|||
} |
|||
|
|||
.camera-icon { |
|||
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%); |
|||
} |
|||
|
|||
.video-icon { |
|||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); |
|||
} |
|||
|
|||
.audio-icon { |
|||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); |
|||
} |
|||
|
|||
.file-icon { |
|||
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); |
|||
} |
|||
|
|||
.option-icon image { |
|||
width: 50rpx; |
|||
height: 50rpx; |
|||
} |
|||
|
|||
.option-text { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
/* 录音模态框 */ |
|||
.recording-modal { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 1001; |
|||
animation: fadeIn 0.2s ease; |
|||
} |
|||
|
|||
.recording-box { |
|||
background: white; |
|||
border-radius: 40rpx; |
|||
padding: 60rpx; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.recording-wave-box { |
|||
position: relative; |
|||
width: 200rpx; |
|||
height: 200rpx; |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.recording-wave { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
border: 8rpx solid #5ac725; |
|||
border-radius: 50%; |
|||
animation: pulse 1.5s ease-in-out infinite; |
|||
} |
|||
|
|||
@keyframes pulse { |
|||
0% { |
|||
transform: scale(1); |
|||
opacity: 1; |
|||
} |
|||
100% { |
|||
transform: scale(1.2); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
.recording-mic-icon { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
} |
|||
|
|||
.recording-tip { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
font-weight: 500; |
|||
margin-bottom: 15rpx; |
|||
} |
|||
|
|||
.recording-time { |
|||
font-size: 36rpx; |
|||
color: #5ac725; |
|||
font-weight: bold; |
|||
} |
|||