customerService.vue 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  1. <template>
  2. <view class="customer-service-page">
  3. <!-- 顶部栏 -->
  4. <!-- 顶部导航栏 -->
  5. <view class="custom-navbar">
  6. <view class="navbar-left">
  7. <text class="fa fa-angle-left" @click="goBack"></text>
  8. <view class="navbar-title one-omit">
  9. {{ orderCardData.creator_nickname }}
  10. </view>
  11. </view>
  12. <view class="navbar-right" @click="toggleDropdown">
  13. <image src="@/static/icon/more2.png" style="width: 64rpx; height: 64rpx; margin-top: 15rpx"
  14. mode="widthFix"></image>
  15. <view class="dropdown-menu" v-if="showDropdown">
  16. <view class="dropdown-item" @tap="handleOption('report')">举报内容</view>
  17. </view>
  18. </view>
  19. </view>
  20. <!-- 聊天内容区 -->
  21. <scroll-view class="cs-chat-list" :scroll-y="true" :scroll-with-animation="true"
  22. :scroll-into-view="scrollToView" :style="{
  23. paddingBottom: (keyboardHeight ? keyboardHeight + 100 : 100) + 'rpx',
  24. }">
  25. <view v-for="(msg, idx) in chatList" :key="msg.id">
  26. <template v-if="msg.message_type == 1 || msg.message_type == 2 || msg.message_type == 3 || msg.message_type == 4 || msg.message_type == 5 ">
  27. <view v-if="shouldShowTime(idx)" class="cs-time-bar">
  28. <view class="cs-time-inner">{{ formatTime(msg.time) }}</view>
  29. </view>
  30. <template v-if="msg.message_type == 5">
  31. <view class="cs-msg-order-card-box">
  32. <image class="order-card-avatar" :src="msg.avatar" />
  33. <view class="cs-msg-order-card">
  34. <image class="order-card-img" v-if="msg.order.main_image" :src="msg.order.main_image"
  35. mode="aspectFill" />
  36. <view class="order-card-info">
  37. <view class="order-card-title">{{ msg.order.title }}</view>
  38. <view class="order-card-row" v-if="msg.order.orderNo">
  39. <text class="order-card-label">订单编号</text>
  40. <text class="order-card-value">{{ msg.order.orderNo }}</text>
  41. </view>
  42. <view class="order-card-row" v-if="msg.order.orderTime">
  43. <text class="order-card-label">下单时间</text>
  44. <text class="order-card-value">{{
  45. msg.order.orderTime
  46. }}</text>
  47. </view>
  48. <view class="order-card-btn-box">
  49. <button class="order-card-btn" @click="goDetails(msg.order)">查看详情</button>
  50. </view>
  51. </view>
  52. </view>
  53. </view>
  54. </template>
  55. <template v-else>
  56. <view :id="'msg-' + msg.id" :class="[
  57. 'cs-msg-item',
  58. msg.type === 'user' ? 'cs-msg-self' : 'cs-msg-other',
  59. ]">
  60. <image class="cs-avatar" :src="msg.avatar" />
  61. <view class="cs-msg-bubble">
  62. <!-- 文本消息 -->
  63. <template v-if="msg.message_type === 1">
  64. {{ msg.content }}
  65. </template>
  66. <!-- 图片消息 -->
  67. <template v-else-if="msg.message_type === 2">
  68. <view style="position:relative;display:inline-block;">
  69. <image :src="msg.media_url" mode="widthFix"
  70. style="max-width: 180rpx; border-radius: 8rpx;"
  71. @tap="!msg.uploading && previewImage(msg.media_url)" />
  72. <view v-if="msg.uploading"
  73. style="position:absolute;left:0;top:0;width:100%;height:100%;background:rgba(255,255,255,0.7);display:flex;align-items:center;justify-content:center;">
  74. <text style="color:#a6e22e;font-size:24rpx;">{{ msg.progress || 0 }}%</text>
  75. </view>
  76. </view>
  77. </template>
  78. <!-- 语音消息 -->
  79. <template v-else-if="msg.message_type === 3">
  80. <view class="cs-msg-voice">[语音消息] <text style="color:#a6e22e">(暂不支持播放)</text></view>
  81. </template>
  82. <!-- 视频消息 -->
  83. <template v-else-if="msg.message_type === 4">
  84. <video :src="msg.media_url" controls
  85. style="max-width: 220rpx; max-height: 180rpx; border-radius: 8rpx;" />
  86. </template>
  87. <!-- 其它未知类型 -->
  88. <template v-else>
  89. <text style="color:#bbb">[未知消息类型]</text>
  90. </template>
  91. </view>
  92. </view>
  93. </template>
  94. </template>
  95. </view>
  96. <view style="height: 200rpx; width: 100%"></view>
  97. <view :id="'bottom-anchor'"></view>
  98. <view v-if="adShow" :style="{ height: orderCardHeight + 'rpx' }"></view>
  99. </scroll-view>
  100. <!-- 底部输入栏 -->
  101. <view class="cs-input-bar" :style="{ bottom: keyboardHeight + 'rpx' }">
  102. <!-- 广告条 -->
  103. <view class="order-card" v-if="adShow && zcId">
  104. <view class="order-card-header">
  105. <image class="order-card-img" :src="orderCardData.main_image" mode="aspectFill" />
  106. <view class="order-card-info">
  107. <view class="order-card-title one-omit">{{ orderCardData.title }}</view>
  108. <view class="order-card-btn-box">
  109. <button class="order-card-btn" @click="sendOrderCardMsg()">
  110. 发给客服
  111. </button>
  112. </view>
  113. </view>
  114. <image class="order-card-close" @click="closeOrderCard" src="@/static/icon/wd_icon_guanbi.png"
  115. mode="widthFix"></image>
  116. </view>
  117. <view class="order-card-row" v-if="orderCardData.orderNo">
  118. <text class="order-card-label">订单编号</text>
  119. <text class="order-card-value">{{ orderCardData.orderNo }}</text>
  120. </view>
  121. <view class="order-card-row" v-if="orderCardData.orderTime">
  122. <text class="order-card-label">下单时间</text>
  123. <text class="order-card-value">{{ orderCardData.orderTime }}</text>
  124. </view>
  125. </view>
  126. <!-- 假输入框 -->
  127. <view v-if="!showRealInput" class="fake-input-bar" @click="showInputAndFocus">
  128. <view class="fake-input-placeholder">在这里输入新消息</view>
  129. <view class="fake-input-icons">
  130. <image src="/static/icon/icon-picture.png" class="fake-input-icon" />
  131. <image src="/static/icon/icon-expression.png" class="fake-input-icon" />
  132. </view>
  133. </view>
  134. <!-- 真输入框 -->
  135. <view v-else class="cs-input-area">
  136. <textarea class="cs-textarea" v-model="inputValue" placeholder="在这里输入新消息" :focus="inputFocused"
  137. :adjust-position="false" @focus="onInputFocus" @blur="onInputBlur"
  138. @keyboardheightchange="onKeyboardHeightChange" maxlength="300" auto-height
  139. :style="{ 'max-height': '120rpx', 'overflow-y': 'auto' }"></textarea>
  140. <view class="bottom-bar">
  141. <view>
  142. <image @tap="upload" src="/static/icon/icon-picture.png" class="fake-input-icon" />
  143. <image @tap="toggleEmojiPanel" src="/static/icon/icon-expression.png" class="fake-input-icon" />
  144. </view>
  145. <view :class="['send_btn', inputValue.trim() ? '' : 'prohibit']" @tap="sendMsg(1)">发送</view>
  146. </view>
  147. </view>
  148. </view>
  149. <view class="emoji-panel" :class="{ show: showEmojiPanel }" v-if="showEmojiPanel">
  150. <view class="emoji-mask" @click="showEmojiPanel = false"></view>
  151. <view class="emoji-grid">
  152. <view class="emoji-item" v-for="(emoji, index) in emojiList" :key="index" @tap="selectEmoji(emoji)">
  153. {{ emoji }}
  154. </view>
  155. </view>
  156. </view>
  157. </view>
  158. </template>
  159. <script>
  160. import permission from '@/common/permission.js';
  161. export default {
  162. data() {
  163. return {
  164. chatList: [],
  165. inputValue: "",
  166. pollTimer: null,
  167. scrollToView: "bottom-anchor",
  168. adShow: true,
  169. keyboardHeight: 0,
  170. inputFocused: false,
  171. orderCardHeight: 260,
  172. showEmojiPanel: false,
  173. showDropdown: false,
  174. emojiList: ["😀", "😁", "😂", "🤣", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍", "😘", "🥰", "😗", "😙", "😚", "🙂", "🤗", "🤩", "🤔", "🤨", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "🤐", "😯", "😪", "😫", "🥱", "😴", "😌", "😛", "😜", "😝", "🤤", "😒", "😓", "😔", "😕", "🙃", "🤑", "😲", "☹️", "🙁", "😖", "😞", "😟", "😤", "😢", "😭", "😦", "😧", "😨", "😩", "🤯", "😬", "😰", "😱", "🥵", "🥶", "😳", "🤪", "😵", "😡", "😠", "🤬", "😷", "🤒", "🤕", "🤢", "🤮", "🥴", "😇", "🥳", "🥺", "🤠", "😈", "👿", "👹", "👺", "💀", "👻", "👽", "🤖", "💩", "😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿", "😾", "🙈", "🙉", "🙊", "💋", "💌", "💘", "💝", "💖", "💗", "💓", "💞", "💕", "💟", "❣️", "💔", "❤️", "🧡", "💛", "💚", "💙", "💜", "🤎", "🖤", "🤍", "💯", "💢", "💥", "💫", "💦", "💨", "🕳️", "💣", "💬", "👋", "🤚", "🖐️", "✋", "🖖", "👌", "🤏", "✌️", "🤞", "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "☝️", "👍", "👎", "✊", "👊", "🤛", "🤜", "👏", "🙌", "👐", "🤲", "🙏", "✍️", "💅", "🤳", "💪", "🦾", "🦵", "🦶", "👂", "👃", "🧠", "🦷", "🦴", "��", "👁️", "👅", "👄"],
  175. conversationId: 0,
  176. creatorId: 0,
  177. zcId: 0,
  178. lastMsgId: 0,
  179. pageSize: 20,
  180. userInfo: {},
  181. showRealInput: false, // 控制显示真输入框还是假输入框
  182. orderCardData: {},
  183. };
  184. },
  185. onLoad(options) {
  186. this.creatorId = options.id || 0;
  187. this.zcId = options.zc_id || 0;
  188. this.fetchMessages();
  189. this.startPolling();
  190. this.getOrderCardData()
  191. },
  192. onHide() {
  193. this.clearPolling();
  194. },
  195. onUnload() {
  196. this.clearPolling();
  197. },
  198. methods: {
  199. goBack() {
  200. uni.navigateBack();
  201. },
  202. // 轮询获取消息
  203. startPolling() {
  204. this.clearPolling();
  205. this.pollTimer = setInterval(() => this.fetchMessages(false), 5000);
  206. this.fetchMessages(false);
  207. },
  208. clearPolling() {
  209. if (this.pollTimer) {
  210. clearInterval(this.pollTimer);
  211. this.pollTimer = null;
  212. }
  213. },
  214. // 获取消息
  215. fetchMessages(isAppend = false) {
  216. uni.request({
  217. url: this.$apiHost + '/App/kefuGetMessages',
  218. method: 'GET',
  219. data: {
  220. uuid: getApp().globalData.uuid,
  221. skey: getApp().globalData.skey,
  222. creator_id: this.creatorId,
  223. conversation_id: this.conversationId,
  224. last_id: isAppend ? this.lastMsgId : 0,
  225. page_size: this.pageSize,
  226. // page_size: 40,
  227. zc_id: this.zcId
  228. },
  229. success: (res) => {
  230. if (res.data && res.data.success === 'yes' && res.data.data) {
  231. const { messages, conversation, user_info } = res.data.data;
  232. this.userInfo = user_info || {};
  233. this.conversationId = conversation?.id || 0;
  234. // 格式化消息,保留 message_type、media_url 等字段
  235. const msgList = (messages || []).map(msg => {
  236. let returnValue = {
  237. id: msg.id,
  238. content: msg.content,
  239. type: msg.from_type === 1 ? 'user' : 'customerService',
  240. avatar: msg.from_type === 1 ? (user_info.avatar || '/static/makedetail/characterProfilePicture.png') : '/static/home/avator.png',
  241. time: msg.create_time,
  242. message_type: msg.message_type, // 消息类型
  243. media_url: msg.media_url, // 媒体文件URL
  244. progress: msg.progress,
  245. uploading: msg.uploading
  246. }
  247. if (msg.message_type == 5) {
  248. returnValue.order = JSON.parse(msg.content);
  249. }
  250. return returnValue;
  251. });
  252. console.log(msgList, "消息列表");
  253. // 合并未上传完成的临时消息
  254. const tempMsgs = this.chatList.filter(m => m.uploading);
  255. let newList;
  256. if (isAppend) {
  257. newList = [...msgList, ...this.chatList.filter(m => !m.uploading), ...tempMsgs];
  258. } else {
  259. newList = [...msgList, ...tempMsgs];
  260. }
  261. this.chatList = newList;
  262. // 记录最后一条消息ID
  263. if (messages && messages.length > 0) {
  264. this.lastMsgId = messages[0].id;
  265. }
  266. this.$nextTick(() => {
  267. this.scrollToView = "bottom-anchor";
  268. });
  269. } else {
  270. // 只保留临时消息
  271. this.chatList = this.chatList.filter(m => m.uploading);
  272. }
  273. },
  274. fail: () => {
  275. // 只保留临时消息
  276. this.chatList = this.chatList.filter(m => m.uploading);
  277. }
  278. });
  279. },
  280. // 发送消息
  281. sendMsg(message_type) {
  282. if (!this.inputValue.trim() && message_type == 1) return;
  283. const content = this.inputValue;
  284. this.inputValue = "";
  285. console.log(content, "发送消息", message_type);
  286. uni.request({
  287. url: this.$apiHost + '/App/kefuSendMessage',
  288. method: 'POST',
  289. header: {
  290. "content-type": "application/x-www-form-urlencoded",
  291. uuid: getApp().globalData.uuid,
  292. skey: getApp().globalData.skey,
  293. },
  294. data: {
  295. uuid: getApp().globalData.uuid,
  296. skey: getApp().globalData.skey,
  297. creator_id: this.creatorId,
  298. conversation_id: this.conversationId,
  299. message_type: message_type, // 文本
  300. content: content,
  301. zc_id: this.zcId
  302. },
  303. success: (res) => {
  304. if (res.data && res.data.success === 'yes') {
  305. // 发送成功后刷新消息
  306. this.fetchMessages(false);
  307. } else {
  308. uni.showToast({ title: res.data.str || '发送失败', icon: 'none' });
  309. }
  310. },
  311. fail: () => {
  312. uni.showToast({ title: '网络错误', icon: 'none' });
  313. }
  314. });
  315. },
  316. closeOrderCard() {
  317. this.adShow = false;
  318. },
  319. shouldShowTime(idx) {
  320. if (idx === 0) return true;
  321. // 找到上一个显示时间条的消息
  322. let lastShowIdx = -1;
  323. for (let i = idx - 1; i >= 0; i--) {
  324. if (this.shouldShowTime(i)) {
  325. lastShowIdx = i;
  326. break;
  327. }
  328. }
  329. if (lastShowIdx === -1) return true;
  330. const curTime = new Date(
  331. this.chatList[idx].time.replace(/-/g, "/")
  332. ).getTime();
  333. const lastTime = new Date(
  334. this.chatList[lastShowIdx].time.replace(/-/g, "/")
  335. ).getTime();
  336. // 20分钟 = 20*60*1000 毫秒
  337. return curTime - lastTime > 20 * 60 * 1000;
  338. },
  339. formatTime(timeStr) {
  340. // 例:'2025-05-21 20:01' => '05月21日 20:01'
  341. if (!timeStr) return "";
  342. const d = new Date(timeStr.replace(/-/g, "/"));
  343. const MM = String(d.getMonth() + 1).padStart(2, "0");
  344. const DD = String(d.getDate()).padStart(2, "0");
  345. const hhmm = timeStr.slice(11, 16);
  346. return `${MM}月${DD}日 ${hhmm}`;
  347. },
  348. onInputFocus(e) {
  349. this.inputFocused = true;
  350. },
  351. onInputBlur(e) {
  352. this.inputFocused = false;
  353. this.keyboardHeight = 0;
  354. },
  355. px2rpx(px) {
  356. // 以750设计稿为例,1rpx = 屏幕宽度/750
  357. const screenWidth = uni.getSystemInfoSync().windowWidth;
  358. return (px * 750) / screenWidth;
  359. },
  360. onKeyboardHeightChange(e) {
  361. let pxHeight = e.detail ? e.detail.height : e.height || 0;
  362. // #ifdef H5 || APP-PLUS
  363. this.keyboardHeight = this.px2rpx(pxHeight);
  364. // #endif
  365. // #ifdef MP-WEIXIN
  366. this.keyboardHeight = pxHeight; // 小程序下直接用rpx
  367. // #endif
  368. },
  369. toggleEmojiPanel() {
  370. this.inputFocused = false;
  371. setTimeout(() => {
  372. this.showEmojiPanel = !this.showEmojiPanel;
  373. }, 100);
  374. },
  375. selectEmoji(emoji) {
  376. this.inputValue += emoji;
  377. this.showEmojiPanel = false;
  378. },
  379. sendOrderCardMsg() {
  380. this.chatList.push({
  381. id: Date.now(),
  382. message_type: 5,
  383. avatar: this.avatar, // 用户头像
  384. order: {
  385. img: "/static/crowdFunding/top-img.png",
  386. title: "【Woh】灯塔 塔罗牌 治愈风泛伟特系",
  387. orderNo: "12201544521215415415415",
  388. orderTime: "2025-05-27 09:35:30",
  389. },
  390. time: this.getNowTime(),
  391. });
  392. this.sendMsg(5);
  393. this.$nextTick(() => {
  394. this.closeOrderCard();
  395. setTimeout(() => {
  396. this.scrollToView = "bottom-anchor";
  397. }, 1000);
  398. });
  399. },
  400. getNowTime() {
  401. const d = new Date();
  402. const pad = (n) => (n < 10 ? "0" + n : n);
  403. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
  404. d.getDate()
  405. )} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  406. },
  407. // 切换下拉菜单显示状态
  408. toggleDropdown() {
  409. this.showDropdown = !this.showDropdown;
  410. },
  411. // 处理下拉菜单选项点击
  412. handleOption(type) {
  413. this.showDropdown = false;
  414. switch (type) {
  415. case 'report':
  416. uni.navigateTo({
  417. url: '/pages/my/feedback?isReportContent=true'
  418. });
  419. break;
  420. }
  421. },
  422. showInputAndFocus() {
  423. this.showRealInput = true;
  424. this.$nextTick(() => {
  425. this.inputFocused = true;
  426. });
  427. },
  428. // 上传图片并发送图片消息,增加进度展示
  429. async upload() {
  430. uni.showActionSheet({
  431. itemList: ['拍照', '从相册选择'],
  432. success: async (res) => {
  433. const sourceType = res.tapIndex === 0 ? 'camera' : 'album';
  434. let hasPermission = false;
  435. try {
  436. if (sourceType === 'camera') {
  437. hasPermission = await permission.request(permission.PermissionType.CAMERA, {
  438. title: '“萌创星球”想访问你的相机',
  439. describe: '萌创星球想访问您的摄像头,便于拍摄获取图片来与其他用户进行交流'
  440. });
  441. } else {
  442. hasPermission = await permission.request(permission.PermissionType.PHOTO_LIBRARY, {
  443. title: '“萌创星球”想访问你的照片图库',
  444. describe: '萌创星球想访问您本地照片图库,便于获取图片来与其他用户进行交流'
  445. });
  446. }
  447. if (!hasPermission) {
  448. uni.showToast({
  449. title: sourceType === 'camera' ? '未获得相机权限' : '未获得相册权限',
  450. icon: 'none'
  451. });
  452. return;
  453. }
  454. // 权限通过后,选择图片
  455. uni.chooseImage({
  456. count: 1,
  457. sizeType: ['compressed'],
  458. sourceType: [sourceType],
  459. success: (res) => {
  460. const filePath = res.tempFilePaths[0];
  461. // 1. 先插入临时消息
  462. const tempMsgId = 'temp_' + Date.now();
  463. this.chatList.push({
  464. id: tempMsgId,
  465. type: 'user',
  466. avatar: this.userInfo.avatar || '/static/makedetail/characterProfilePicture.png',
  467. time: this.getNowTime(),
  468. message_type: 2,
  469. media_url: filePath,
  470. progress: 0,
  471. uploading: true
  472. });
  473. this.$nextTick(() => {
  474. this.scrollToView = "bottom-anchor";
  475. });
  476. // 2. 上传图片
  477. uni.uploadFile({
  478. url: this.$apiHost + '/Xweb/upload_img?skey=' + getApp().globalData.skey,
  479. filePath: filePath,
  480. name: 'file',
  481. // 上传进度
  482. progress: (e) => {
  483. const idx = this.chatList.findIndex(m => m.id === tempMsgId);
  484. if (idx !== -1) this.$set(this.chatList, idx, { ...this.chatList[idx], progress: e.progress });
  485. },
  486. success: (uploadFileRes) => {
  487. let resdata = JSON.parse(uploadFileRes.data);
  488. const idx = this.chatList.findIndex(m => m.id === tempMsgId);
  489. if (resdata.success == 'no') {
  490. if (idx !== -1) this.chatList.splice(idx, 1);
  491. uni.showToast({ title: resdata.str, icon: 'none' });
  492. return;
  493. }
  494. if (resdata.code == 0) {
  495. // 替换为正式消息
  496. if (idx !== -1) this.$set(this.chatList, idx, {
  497. id: Date.now(),
  498. type: 'user',
  499. avatar: this.userInfo.avatar || '/static/makedetail/characterProfilePicture.png',
  500. time: this.getNowTime(),
  501. message_type: 2,
  502. media_url: resdata.data.path,
  503. progress: 100,
  504. uploading: false
  505. });
  506. // 发送图片消息到服务端
  507. this.sendImageMsg(resdata.data.path);
  508. }
  509. },
  510. fail: () => {
  511. const idx = this.chatList.findIndex(m => m.id === tempMsgId);
  512. if (idx !== -1) this.chatList.splice(idx, 1);
  513. uni.showToast({ title: '图片上传失败', icon: 'none' });
  514. }
  515. });
  516. }
  517. });
  518. } catch (error) {
  519. uni.showToast({
  520. title: '权限检查失败',
  521. icon: 'none'
  522. });
  523. }
  524. }
  525. });
  526. },
  527. sendImageMsg(imgUrl) {
  528. uni.request({
  529. url: this.$apiHost + '/App/kefuSendMessage',
  530. method: 'POST',
  531. header: {
  532. "content-type": "application/x-www-form-urlencoded",
  533. uuid: getApp().globalData.uuid,
  534. skey: getApp().globalData.skey,
  535. },
  536. data: {
  537. uuid: getApp().globalData.uuid,
  538. skey: getApp().globalData.skey,
  539. creator_id: this.creatorId,
  540. conversation_id: this.conversationId,
  541. message_type: 2, // 2为图片
  542. content: '', // 图片消息content可为空
  543. media_url: imgUrl, // 图片地址
  544. zc_id: this.zcId
  545. },
  546. success: (res) => {
  547. if (res.data && res.data.success === 'yes') {
  548. this.fetchMessages(false);
  549. } else {
  550. uni.showToast({ title: res.data.str || '发送失败', icon: 'none' });
  551. }
  552. },
  553. fail: () => {
  554. uni.showToast({ title: '网络错误', icon: 'none' });
  555. }
  556. });
  557. },
  558. goDetails(item) {
  559. console.log(item, 'item');
  560. let url = '';
  561. if (item.orderNo) {
  562. url = '/pages/crowdFunding/orderDetails?id=' + item.orderNo;
  563. } else {
  564. url = '/pages/crowdFunding/crowdfundingDetails?id=' + item.id;
  565. }
  566. uni.navigateTo({
  567. url: url
  568. });
  569. },
  570. getOrderCardData() {
  571. if (this.zcId) {
  572. uni.request({
  573. url: this.$apiHost + '/crowdfund/detail',
  574. method: 'GET',
  575. data: {
  576. id: this.zcId,
  577. uuid: getApp().globalData.uuid,
  578. skey: getApp().globalData.skey
  579. },
  580. success: (res) => {
  581. if (res.data && res.data.success === 'yes' && res.data.data) {
  582. this.orderCardData = res.data.data;
  583. console.log(this.orderCardData, 'this.orderCardData');
  584. // 可根据接口返回字段设置isFavorite等
  585. }
  586. }
  587. });
  588. }
  589. },
  590. // 图片预览
  591. previewImage(url) {
  592. // 收集当前消息列表中的所有图片URL
  593. const imageUrls = this.chatList
  594. .filter(msg => msg.message_type === 2 && msg.media_url)
  595. .map(msg => msg.media_url);
  596. const currentIndex = imageUrls.indexOf(url);
  597. uni.previewImage({
  598. current: currentIndex >= 0 ? currentIndex : 0,
  599. urls: imageUrls
  600. });
  601. },
  602. },
  603. };
  604. </script>
  605. <style lang="scss">
  606. .customer-service-page {
  607. min-height: 100vh;
  608. background: #f2f6f2;
  609. display: flex;
  610. flex-direction: column;
  611. position: relative;
  612. .custom-navbar {
  613. display: flex;
  614. flex-direction: row;
  615. align-items: center;
  616. justify-content: space-between;
  617. height: 90rpx;
  618. padding: 0 20rpx;
  619. padding-top: var(--status-bar-height);
  620. background-color: #ffffff;
  621. position: sticky;
  622. top: 0;
  623. height: calc(90rpx + var(--status-bar-height));
  624. z-index: 100;
  625. .navbar-left {
  626. height: 80rpx;
  627. display: flex;
  628. align-items: center;
  629. justify-content: center;
  630. .fa-angle-left {
  631. font-size: 48rpx;
  632. color: #333;
  633. }
  634. .navbar-title {
  635. max-width: 450rpx;
  636. font-family: "PingFang SC-Bold";
  637. font-weight: 400;
  638. font-size: 32rpx;
  639. color: #1f1f1f;
  640. padding-left: 20rpx;
  641. }
  642. }
  643. .navbar-right {
  644. width: 80rpx;
  645. height: 80rpx;
  646. display: flex;
  647. justify-content: center;
  648. align-items: center;
  649. .fa-ellipsis-h {
  650. font-size: 36rpx;
  651. color: #333;
  652. }
  653. }
  654. }
  655. .cs-chat-list {
  656. flex: 1;
  657. padding: 24rpx 0 0 0;
  658. overflow-y: auto;
  659. background: #f6f7f9;
  660. .cs-msg-item {
  661. display: flex;
  662. // align-items: flex-end;
  663. margin-bottom: 18rpx;
  664. padding: 0 24rpx;
  665. &.cs-msg-other {
  666. flex-direction: row;
  667. .cs-avatar {
  668. margin-right: 12rpx;
  669. }
  670. .cs-msg-bubble {
  671. background: #fff;
  672. color: #1f1f1f;
  673. border-top-left-radius: 0;
  674. border-top-right-radius: 12rpx;
  675. border-bottom-left-radius: 12rpx;
  676. border-bottom-right-radius: 12rpx;
  677. }
  678. }
  679. &.cs-msg-self {
  680. flex-direction: row-reverse;
  681. .cs-avatar {
  682. margin-left: 12rpx;
  683. }
  684. .cs-msg-bubble {
  685. background: #e6f6d9;
  686. color: #1f1f1f;
  687. border-top-right-radius: 0;
  688. border-top-left-radius: 12rpx;
  689. border-bottom-left-radius: 12rpx;
  690. border-bottom-right-radius: 12rpx;
  691. }
  692. }
  693. .cs-avatar {
  694. width: 64rpx;
  695. height: 64rpx;
  696. border-radius: 50%;
  697. }
  698. .cs-msg-bubble {
  699. max-width: 70vw;
  700. min-height: 40rpx;
  701. font-size: 28rpx;
  702. padding: 18rpx 24rpx;
  703. word-break: break-all;
  704. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  705. margin-bottom: 2rpx;
  706. display: flex;
  707. align-items: center;
  708. }
  709. }
  710. .cs-time-bar {
  711. display: flex;
  712. justify-content: center;
  713. align-items: center;
  714. margin: 18rpx 0 12rpx 0;
  715. .cs-time-inner {
  716. background: #fff;
  717. color: #b2b2b2;
  718. font-size: 24rpx;
  719. border-radius: 16rpx;
  720. padding: 8rpx 24rpx;
  721. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  722. display: inline-block;
  723. }
  724. }
  725. }
  726. .order-card {
  727. background: #fff;
  728. border-radius: 20rpx;
  729. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  730. padding: 24rpx 24rpx 18rpx 24rpx;
  731. margin: 24rpx;
  732. position: absolute;
  733. top: -26rpx;
  734. transform: translateY(-100%);
  735. font-size: 28rpx;
  736. width: 670rpx;
  737. .order-card-header {
  738. display: flex;
  739. align-items: flex-start;
  740. position: relative;
  741. .order-card-img {
  742. width: 100rpx;
  743. height: 100rpx;
  744. border-radius: 12rpx;
  745. margin-right: 18rpx;
  746. flex-shrink: 0;
  747. }
  748. .order-card-info {
  749. flex: 1;
  750. display: flex;
  751. flex-direction: column;
  752. justify-content: flex-start;
  753. .order-card-title {
  754. font-size: 30rpx;
  755. color: #1f1f1f;
  756. font-weight: 500;
  757. margin-bottom: 18rpx;
  758. margin-top: 2rpx;
  759. line-height: 1.3;
  760. max-width: 450rpx;
  761. }
  762. .order-card-btn-box {
  763. display: flex;
  764. align-items: center;
  765. justify-content: flex-end;
  766. .order-card-btn {
  767. font-family: "PingFang SC-Bold";
  768. font-weight: 400;
  769. font-size: 24rpx;
  770. color: #acf934;
  771. background: #1f1f1f;
  772. border-radius: 128rpx;
  773. padding: 8rpx 14rpx;
  774. line-height: 1.2;
  775. margin: 0;
  776. }
  777. }
  778. }
  779. .order-card-close {
  780. position: absolute;
  781. right: -10rpx;
  782. top: -10rpx;
  783. font-size: 36rpx;
  784. color: #1f1f1f;
  785. background: #fff;
  786. border-radius: 50%;
  787. width: 34rpx;
  788. height: 34rpx;
  789. display: flex;
  790. align-items: center;
  791. justify-content: center;
  792. z-index: 2;
  793. }
  794. }
  795. .order-card-row {
  796. display: flex;
  797. align-items: center;
  798. margin-top: 12rpx;
  799. .order-card-label {
  800. color: #b2b2b2;
  801. font-size: 26rpx;
  802. width: 140rpx;
  803. flex-shrink: 0;
  804. }
  805. .order-card-value {
  806. color: #1f1f1f;
  807. font-size: 26rpx;
  808. margin-left: 12rpx;
  809. word-break: break-all;
  810. }
  811. }
  812. }
  813. .cs-input-bar {
  814. display: flex;
  815. align-items: center;
  816. background: #fff;
  817. padding: 12rpx 16rpx;
  818. border-top: 1rpx solid #ededed;
  819. position: fixed;
  820. left: 0;
  821. right: 0;
  822. bottom: 0;
  823. z-index: 10;
  824. padding-bottom: calc(12rpx + var(--window-bottom));
  825. .fake-input-bar {
  826. display: flex;
  827. align-items: center;
  828. background: #f6f7f9;
  829. border-radius: 24rpx;
  830. padding: 12rpx 20rpx;
  831. margin: 0 12rpx;
  832. flex: 1;
  833. min-height: 64rpx;
  834. border: 1rpx solid #ededed;
  835. .fake-input-placeholder {
  836. color: #bbb;
  837. font-size: 28rpx;
  838. flex: 1;
  839. }
  840. .fake-input-icons {
  841. display: flex;
  842. align-items: center;
  843. }
  844. }
  845. .fake-input-icon {
  846. width: 40rpx;
  847. height: 40rpx;
  848. margin-left: 16rpx;
  849. }
  850. .cs-input-area {
  851. display: flex;
  852. align-items: flex-end;
  853. flex-direction: column;
  854. background: #fff;
  855. border-radius: 32rpx;
  856. padding: 8rpx 12rpx;
  857. flex: 1;
  858. .cs-textarea {
  859. flex: 1;
  860. min-height: 64rpx;
  861. max-height: 120rpx;
  862. overflow-y: auto;
  863. border: none;
  864. font-size: 28rpx;
  865. background: #f6f7f9;
  866. border-radius: 24rpx;
  867. padding: 12rpx 20rpx;
  868. resize: none;
  869. width: 100%;
  870. }
  871. .bottom-bar {
  872. width: 100%;
  873. padding: 12rpx 0;
  874. display: flex;
  875. align-items: center;
  876. justify-content: space-between;
  877. }
  878. .send_btn {
  879. background: #a6e22e;
  880. color: #fff;
  881. border-radius: 32rpx;
  882. font-size: 28rpx;
  883. padding: 26rpx 32rpx;
  884. line-height: 0;
  885. border: 1rpx solid transparent;
  886. &.prohibit {
  887. background: #fff;
  888. border: 1rpx solid #999;
  889. color: #999;
  890. }
  891. }
  892. }
  893. }
  894. .emoji-panel {
  895. position: fixed;
  896. left: 0;
  897. right: 0;
  898. bottom: 0;
  899. z-index: 1000;
  900. width: 100vw;
  901. height: 100vh;
  902. &.show {
  903. transform: translateY(0);
  904. }
  905. .emoji-mask {
  906. position: fixed;
  907. left: 0;
  908. right: 0;
  909. top: 0;
  910. bottom: 0;
  911. background: rgba(0, 0, 0, 0.1);
  912. z-index: 1001;
  913. }
  914. .emoji-grid {
  915. position: relative;
  916. z-index: 1002;
  917. display: flex;
  918. flex-wrap: wrap;
  919. justify-content: center;
  920. padding-bottom: 24rpx;
  921. max-height: 50vh;
  922. width: 100vw;
  923. border-top: 1rpx solid #ededed;
  924. background: #fff;
  925. padding: 12rpx 0 0 0;
  926. box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.08);
  927. overflow-y: auto;
  928. transform: translateY(100%);
  929. transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  930. border-top: 20rpx solid #fff;
  931. .emoji-item {
  932. width: 60rpx;
  933. height: 60rpx;
  934. display: flex;
  935. align-items: center;
  936. justify-content: center;
  937. font-size: 36rpx;
  938. color: #333;
  939. margin: 8rpx;
  940. border-radius: 8rpx;
  941. padding: 8rpx;
  942. }
  943. }
  944. }
  945. .cs-msg-order-card-box {
  946. display: flex;
  947. flex-direction: row-reverse;
  948. margin-bottom: 20rpx;
  949. .order-card-avatar {
  950. width: 64rpx;
  951. height: 64rpx;
  952. border-radius: 50%;
  953. margin-left: 12rpx;
  954. margin-right: 24rpx;
  955. }
  956. .cs-msg-order-card {
  957. position: relative;
  958. display: flex;
  959. flex-direction: row;
  960. align-items: flex-start;
  961. background: #fff;
  962. border-radius: 20rpx;
  963. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  964. padding: 24rpx 24rpx 18rpx 24rpx;
  965. margin: 24rpx 0 0 0;
  966. width: 600rpx;
  967. min-height: 160rpx;
  968. .order-card-img {
  969. width: 100rpx;
  970. height: 100rpx;
  971. border-radius: 12rpx;
  972. margin-right: 18rpx;
  973. flex-shrink: 0;
  974. }
  975. .order-card-info {
  976. flex: 1;
  977. display: flex;
  978. flex-direction: column;
  979. justify-content: flex-start;
  980. .order-card-title {
  981. font-size: 30rpx;
  982. color: #1f1f1f;
  983. font-weight: 500;
  984. margin-bottom: 18rpx;
  985. margin-top: 2rpx;
  986. line-height: 1.3;
  987. max-width: 350rpx;
  988. word-break: break-all;
  989. }
  990. .order-card-btn-box {
  991. display: flex;
  992. align-items: center;
  993. justify-content: flex-end;
  994. margin-top: 12rpx;
  995. .order-card-btn {
  996. font-family: "PingFang SC-Bold";
  997. font-weight: 400;
  998. font-size: 24rpx;
  999. color: #1f1f1f;
  1000. background: #acf934;
  1001. border-radius: 128rpx;
  1002. padding: 8rpx 24rpx;
  1003. line-height: 1.2;
  1004. margin: 0;
  1005. }
  1006. }
  1007. }
  1008. .order-card-row {
  1009. display: flex;
  1010. align-items: center;
  1011. margin-top: 8rpx;
  1012. .order-card-label {
  1013. color: #b2b2b2;
  1014. font-size: 26rpx;
  1015. width: 140rpx;
  1016. flex-shrink: 0;
  1017. }
  1018. .order-card-value {
  1019. color: #1f1f1f;
  1020. font-size: 26rpx;
  1021. margin-left: 12rpx;
  1022. word-break: break-all;
  1023. }
  1024. }
  1025. }
  1026. }
  1027. .dropdown-menu {
  1028. position: absolute;
  1029. top: calc(100% + 10rpx);
  1030. right: 20rpx;
  1031. background-color: #ffffff;
  1032. border-radius: 20rpx;
  1033. padding: 0;
  1034. width: 200rpx;
  1035. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  1036. z-index: 100;
  1037. transform-origin: top right;
  1038. animation: dropdownAnimation 0.2s ease-out;
  1039. overflow: hidden;
  1040. .dropdown-item {
  1041. padding: 24rpx 0;
  1042. color: #333333;
  1043. font-size: 28rpx;
  1044. position: relative;
  1045. text-align: center;
  1046. &:not(:last-child)::after {
  1047. content: '';
  1048. position: absolute;
  1049. left: 0;
  1050. right: 0;
  1051. bottom: 0;
  1052. height: 1rpx;
  1053. background-color: #EEEEEE;
  1054. }
  1055. &:active {
  1056. background-color: #f8f8f8;
  1057. }
  1058. }
  1059. }
  1060. @keyframes dropdownAnimation {
  1061. 0% {
  1062. opacity: 0;
  1063. transform: scale(0.95) translateY(-5rpx);
  1064. }
  1065. 100% {
  1066. opacity: 1;
  1067. transform: scale(1) translateY(0);
  1068. }
  1069. }
  1070. }
  1071. </style>