PageOne.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. <template>
  2. <view class="page-one-container">
  3. <!-- 图片资料标题 -->
  4. <view class="page-header">
  5. <view class="detail-image-header">
  6. <text class="detail-image-title">图片资料</text>
  7. <view class="copy-btn" @click="handleSaveAllImages">
  8. <text>一键下载实物图到相册</text>
  9. </view>
  10. </view>
  11. </view>
  12. <!-- 实物图卡片 -->
  13. <view class="card-wrap">
  14. <view class="card-title">实物图</view>
  15. <view class="image-upload-container">
  16. <view class="image-list">
  17. <view v-for="(item, index) in truePicList" :key="`truePic-${index}`" class="image-item">
  18. <PicComp :src="item.fileUrl" @needPreviewPic="previewTrueImage" />
  19. <view class="delete-btn" @click="handleDeleteImage(item)">×</view>
  20. </view>
  21. <view class="upload-btn" @click="handleUploadImage('truePic')">
  22. <u-icon name="plus" size="40" color="#999" />
  23. </view>
  24. </view>
  25. </view>
  26. </view>
  27. <!-- 聊天记录/通话记录/前端跟进/跟进记录 -->
  28. <view class="card-wrap">
  29. <view class="card-title">
  30. <text :class="{ 'active': recordType === 'chat' }" @click="recordType = 'chat'">
  31. 聊天记录
  32. </text>
  33. <text class="divider">|</text>
  34. <text :class="{ 'active': recordType === 'call' }" @click="recordType = 'call'">
  35. 通话记录
  36. </text>
  37. <text class="divider">|</text>
  38. <text :class="{ 'active': recordType === 'frontendFollow' }" @click="recordType = 'frontendFollow'">
  39. 前端跟进
  40. </text>
  41. <text class="divider">|</text>
  42. <text :class="{ 'active': recordType === 'followRecord' }" @click="recordType = 'followRecord'">
  43. 跟进记录
  44. </text>
  45. </view>
  46. <!-- 聊天记录 -->
  47. <view v-if="recordType === 'chat'" class="image-upload-container">
  48. <view class="image-list">
  49. <view v-for="(item, index) in chatRecordsList" :key="`chat-${index}`" class="image-item">
  50. <PicComp :src="item.fileUrl" @needPreviewPic="previewImage" />
  51. <view class="delete-btn" @click="handleDeleteImage(item)">×</view>
  52. </view>
  53. <view class="upload-btn" @click="handleUploadImage('chatRecords')">
  54. <u-icon name="plus" size="40" color="#999" />
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 通话录音 -->
  59. <view v-if="recordType === 'call'" class="call-records-container">
  60. <sound-recorder v-for="item in soundRecordList" :key="item.fileName" :data="item"
  61. @handleDelectThisSoundRecord="handleDeleteSoundRecord" />
  62. </view>
  63. <!-- 前端跟进 -->
  64. <follow-card v-if="recordType === 'frontendFollow'" :key="'frontendFollow'" :clue-id="currentClueId" type="4" />
  65. <!-- 跟进记录 -->
  66. <follow-card v-if="recordType === 'followRecord'" :key="'followRecord'" :clue-id="currentClueId" type="5" />
  67. </view>
  68. <!-- 核价信息卡片 -->
  69. <view v-if="hasInquiryInfo" class="info-card">
  70. <view class="info-card-title">核价信息</view>
  71. <u-row class="info-row">
  72. <u-col span="6">
  73. <view class="info-label">核价次数</view>
  74. <view class="info-value">{{ currentReceipt.inquiryCount || 0 }}</view>
  75. </u-col>
  76. <u-col span="6">
  77. <view class="info-label">核价价格</view>
  78. <view class="info-value">¥{{ currentReceipt.inquiryPrice || 0 }}</view>
  79. </u-col>
  80. </u-row>
  81. </view>
  82. <!-- 基本信息卡片 -->
  83. <view class="info-card">
  84. <view class="info-card-title">基本信息</view>
  85. <u-row class="info-row">
  86. <u-col span="6">
  87. <view class="info-label">发单人</view>
  88. <view class="info-value">{{ orderDetail.createNickName || '未填写' }}</view>
  89. </u-col>
  90. <u-col span="6">
  91. <view class="info-label">型号</view>
  92. <view class="info-value">{{ orderDetail.model || '未填写' }}</view>
  93. </u-col>
  94. </u-row>
  95. <u-row class="info-row">
  96. <u-col span="6">
  97. <view class="info-label">上门时间</view>
  98. <view class="info-value">{{ orderDetail.visitTime || '未填写' }}</view>
  99. </u-col>
  100. <u-col span="6">
  101. <view class="info-label">地址</view>
  102. <view class="info-value">{{ orderDetail.address || '未填写' }}</view>
  103. </u-col>
  104. </u-row>
  105. </view>
  106. <!-- 联系方式卡片 -->
  107. <view class="contact-card">
  108. <view class="contact-item phone-card" @click="handlePhoneClick">
  109. <u-icon name="phone" size="40" color="#07C160" />
  110. <view class="contact-title">电话</view>
  111. <view v-if="orderDetail.phone" class="red-dot"></view>
  112. </view>
  113. <view class="contact-item wechat-card" @click="handleWechatClick">
  114. <u-icon name="chat" size="40" color="#07C160" />
  115. <view class="contact-title">微信</view>
  116. <view v-if="orderDetail.wechat" class="red-dot"></view>
  117. </view>
  118. </view>
  119. <!-- 下一步按钮 -->
  120. <view class="space-block"></view>
  121. <u-button class="next-btn" @click="handleNext" type="primary" size="middle">
  122. 下一步
  123. </u-button>
  124. </view>
  125. </template>
  126. <script>
  127. import PicComp from './PicComp.vue'
  128. import soundRecorder from '@/components/soundRecorder/soundRecorder.vue'
  129. import FollowCard from './FollowCard.vue'
  130. import imageUpload from '../utils/imageUpload.js'
  131. export default {
  132. name: 'PageOne',
  133. components: {
  134. PicComp,
  135. soundRecorder,
  136. FollowCard
  137. },
  138. props: {
  139. orderDetail: {
  140. type: Object,
  141. default: () => ({})
  142. },
  143. orderId: {
  144. type: String,
  145. default: ''
  146. },
  147. currentReceipt: {
  148. type: Object,
  149. default: () => ({})
  150. }
  151. },
  152. data() {
  153. return {
  154. recordType: 'chat', // 'chat' | 'call' | 'frontendFollow' | 'followRecord'
  155. chatRecordsList: [],
  156. truePicList: [],
  157. soundRecordList: []
  158. }
  159. },
  160. computed: {
  161. currentClueId() {
  162. return (this.currentReceipt && this.currentReceipt.clueId) || (this.orderDetail && this.orderDetail.clueId) || ''
  163. },
  164. // 是否有核价信息
  165. hasInquiryInfo() {
  166. return this.currentReceipt && (this.currentReceipt.inquiryCount || this.currentReceipt.inquiryPrice)
  167. }
  168. },
  169. watch: {
  170. recordType(newVal) {
  171. if (newVal === 'call') {
  172. this.loadCallRecords()
  173. }
  174. },
  175. currentReceipt: {
  176. handler(newVal) {
  177. if (newVal && newVal.id) {
  178. this.loadImageList()
  179. this.loadCallRecords()
  180. }
  181. },
  182. immediate: true,
  183. deep: true
  184. }
  185. },
  186. methods: {
  187. /**
  188. * 加载图片列表
  189. */
  190. async loadImageList() {
  191. if (!this.currentReceipt.id || !this.orderDetail.itemBrand) return
  192. try {
  193. // 加载聊天记录
  194. const chatList = await imageUpload.getFileList(
  195. '2',
  196. '1',
  197. this.currentReceipt.id,
  198. this.orderDetail.itemBrand,
  199. this.currentReceipt.clueId
  200. )
  201. this.chatRecordsList = chatList || []
  202. // 加载实物图
  203. const truePicList = await imageUpload.getFileList(
  204. '2',
  205. '2',
  206. this.currentReceipt.id,
  207. this.orderDetail.itemBrand,
  208. this.currentReceipt.clueId
  209. )
  210. this.truePicList = truePicList || []
  211. } catch (error) {
  212. console.error('加载图片列表失败:', error)
  213. }
  214. },
  215. /**
  216. * 加载通话记录
  217. */
  218. async loadCallRecords() {
  219. if (!this.currentReceipt.clueId) return
  220. try {
  221. const { data } = await uni.$u.api.getCallClueFileByClueId({
  222. clueId: this.currentReceipt.clueId
  223. })
  224. this.soundRecordList = data || []
  225. } catch (error) {
  226. console.error('加载通话记录失败:', error)
  227. }
  228. },
  229. /**
  230. * 上传图片
  231. */
  232. async handleUploadImage(type) {
  233. try {
  234. const filePaths = await imageUpload.chooseImage(9)
  235. const uploadResults = await imageUpload.uploadFiles(filePaths)
  236. // 绑定订单文件
  237. const orderFileType = type === 'truePic' ? '2' : '1'
  238. await imageUpload.bindOrderFile(
  239. this.currentReceipt.clueId,
  240. this.currentReceipt.id,
  241. orderFileType,
  242. uploadResults
  243. )
  244. // 刷新列表
  245. this.loadImageList()
  246. } catch (error) {
  247. console.error('上传图片失败:', error)
  248. }
  249. },
  250. /**
  251. * 删除图片
  252. */
  253. async handleDeleteImage(item) {
  254. uni.showModal({
  255. title: '提示',
  256. content: '确定要删除这张图片吗?',
  257. success: async (res) => {
  258. if (res.confirm) {
  259. try {
  260. await imageUpload.deleteFile(item.id)
  261. this.loadImageList()
  262. } catch (error) {
  263. console.error('删除图片失败:', error)
  264. }
  265. }
  266. }
  267. })
  268. },
  269. /**
  270. * 删除录音
  271. */
  272. async handleDeleteSoundRecord({ id }) {
  273. uni.showModal({
  274. title: '提示',
  275. content: '是否确定删除?',
  276. success: async (res) => {
  277. if (res.confirm) {
  278. try {
  279. await uni.$u.api.deleteClueFile([id])
  280. uni.showToast({
  281. title: '删除成功',
  282. icon: 'success'
  283. })
  284. this.loadCallRecords()
  285. } catch (error) {
  286. uni.showToast({
  287. title: '删除失败',
  288. icon: 'error'
  289. })
  290. }
  291. }
  292. }
  293. })
  294. },
  295. /**
  296. * 预览图片
  297. */
  298. previewImage(src) {
  299. const urlList = this.chatRecordsList.map(item => item.fileUrl)
  300. uni.previewImage({
  301. urls: urlList,
  302. current: src
  303. })
  304. },
  305. /**
  306. * 预览实物图
  307. */
  308. previewTrueImage(src) {
  309. const urlList = this.truePicList.map(item => item.fileUrl)
  310. uni.previewImage({
  311. urls: urlList,
  312. current: src
  313. })
  314. },
  315. //一键复制
  316. handleSaveAllImages() {
  317. // 合并所有图片
  318. const allImages = [...this.truePicList]
  319. //取出所有图的url
  320. const allUrls = allImages.map(item => item.fileUrl)
  321. if (allUrls.length > 0) {
  322. // 显示保存图片确认弹窗
  323. uni.showModal({
  324. title: '保存图片',
  325. content: `是否将 ${allUrls.length} 张图片保存到本地相册?`,
  326. confirmText: '保存',
  327. // cancelText: '仅复制链接',
  328. success: (res) => {
  329. if (res.confirm) {
  330. // 用户选择保存图片
  331. this.saveImagesToLocal(allUrls)
  332. } else if (res.cancel) {
  333. // 用户选择仅复制链接
  334. this.copyImageUrls(allUrls)
  335. }
  336. }
  337. })
  338. } else {
  339. uni.showToast({
  340. title: '没有图片可保存',
  341. icon: 'none'
  342. })
  343. }
  344. },
  345. // 保存图片到本地相册
  346. async saveImagesToLocal(imageUrls) {
  347. try {
  348. uni.showLoading({
  349. title: '正在保存图片...',
  350. mask: true
  351. })
  352. const savedImages = []
  353. const failedImages = []
  354. // 逐个保存图片
  355. for (let i = 0; i < imageUrls.length; i++) {
  356. const url = imageUrls[i]
  357. try {
  358. await this.saveSingleImage(url)
  359. savedImages.push(url)
  360. } catch (error) {
  361. console.error(`保存图片失败: ${url}`, error)
  362. failedImages.push(url)
  363. }
  364. // 更新进度
  365. uni.showLoading({
  366. title: `正在保存图片... (${i + 1}/${imageUrls.length})`,
  367. mask: true
  368. })
  369. }
  370. uni.hideLoading()
  371. // 显示结果
  372. let message = `成功保存 ${savedImages.length} 张图片`
  373. if (failedImages.length > 0) {
  374. message += `,${failedImages.length} 张保存失败`
  375. }
  376. uni.showToast({
  377. title: message,
  378. icon: 'none',
  379. duration: 3000
  380. })
  381. // 如果有失败的图片,也复制链接作为备选
  382. if (failedImages.length > 0) {
  383. const allUrls = [...savedImages, ...failedImages]
  384. this.copyImageUrls(allUrls)
  385. }
  386. } catch (error) {
  387. uni.hideLoading()
  388. console.error('保存图片过程中发生错误:', error)
  389. uni.showToast({
  390. title: '保存图片失败',
  391. icon: 'error'
  392. })
  393. }
  394. },
  395. // 保存单张图片
  396. saveSingleImage(url) {
  397. return new Promise((resolve, reject) => {
  398. // 先下载图片
  399. uni.downloadFile({
  400. url: url,
  401. success: (res) => {
  402. if (res.statusCode === 200) {
  403. // 保存到相册
  404. uni.saveImageToPhotosAlbum({
  405. filePath: res.tempFilePath,
  406. success: () => {
  407. console.log('图片保存成功:', url)
  408. resolve()
  409. },
  410. fail: (err) => {
  411. console.error('保存到相册失败:', err)
  412. // 如果是权限问题,尝试请求权限
  413. if (err.errMsg.includes('auth denied')) {
  414. uni.showModal({
  415. title: '权限不足',
  416. content: '需要访问相册权限来保存图片,是否去设置?',
  417. success: (modalRes) => {
  418. if (modalRes.confirm) {
  419. // 打开设置页面
  420. uni.openSetting({
  421. success: (settingRes) => {
  422. console.log('设置页面结果:', settingRes)
  423. }
  424. })
  425. }
  426. }
  427. })
  428. }
  429. reject(err)
  430. }
  431. })
  432. } else {
  433. reject(new Error('下载失败'))
  434. }
  435. },
  436. fail: (err) => {
  437. console.error('下载图片失败:', err)
  438. reject(err)
  439. }
  440. })
  441. })
  442. },
  443. // 复制图片链接
  444. copyImageUrls(urls) {
  445. uni.setClipboardData({
  446. data: JSON.stringify(urls),
  447. success: () => {
  448. uni.showToast({
  449. title: '图片链接已复制',
  450. icon: 'none'
  451. })
  452. }
  453. })
  454. },
  455. /**
  456. * 电话点击
  457. */
  458. handlePhoneClick() {
  459. if (!this.orderDetail.phone) {
  460. uni.showToast({
  461. title: '该订单暂时没有电话号码',
  462. icon: 'none'
  463. })
  464. return
  465. }
  466. uni.makePhoneCall({
  467. phoneNumber: this.orderDetail.phone,
  468. // phoneNumber:'13813737524',//开发者测试手机号
  469. success: () => {
  470. this.$store.commit('call/SET_FORM', {
  471. clueId: this.orderDetail.clueId,
  472. type: '3',
  473. callee: this.orderDetail.phone
  474. })
  475. }
  476. })
  477. },
  478. /**
  479. * 微信点击
  480. */
  481. handleWechatClick() {
  482. if (!this.orderDetail.wechat) {
  483. uni.showToast({
  484. title: '该订单暂时没有微信号',
  485. icon: 'none'
  486. })
  487. return
  488. }
  489. uni.setClipboardData({
  490. data: this.orderDetail.wechat,
  491. success: () => {
  492. uni.showToast({
  493. title: '微信号已复制',
  494. icon: 'none'
  495. })
  496. }
  497. })
  498. },
  499. /**
  500. * 下一步
  501. */
  502. handleNext() {
  503. this.$emit('next', {
  504. nowPage: 'formOne',
  505. form: {}
  506. })
  507. }
  508. }
  509. }
  510. </script>
  511. <style scoped lang="scss">
  512. @import '../styles/common.scss';
  513. .page-one-container {
  514. @extend .page-container;
  515. padding-bottom: 100rpx;
  516. }
  517. .page-header {
  518. display: flex;
  519. justify-content: space-between;
  520. align-items: center;
  521. margin-bottom: 20rpx;
  522. }
  523. .page-title {
  524. @include font-styles($size: title, $weight: bold, $color: primary);
  525. }
  526. .save-all-btn {
  527. border-radius: 20rpx;
  528. border-color: #007AFF;
  529. color: #007AFF;
  530. }
  531. .card-wrap {
  532. @extend .card-wrap;
  533. margin-top: 20rpx;
  534. }
  535. .card-title {
  536. padding: 20rpx 15rpx;
  537. border-bottom: 1rpx solid map-get($colors, border);
  538. display: flex;
  539. align-items: center;
  540. white-space: nowrap;
  541. overflow-x: auto;
  542. -webkit-overflow-scrolling: touch;
  543. text {
  544. padding: 0 6rpx;
  545. cursor: pointer;
  546. font-size: 26rpx;
  547. white-space: nowrap;
  548. flex-shrink: 0;
  549. &.active {
  550. color: map-get($colors, primary);
  551. font-weight: bold;
  552. }
  553. }
  554. .divider {
  555. margin: 0 4rpx;
  556. color: #ddd;
  557. font-size: 24rpx;
  558. flex-shrink: 0;
  559. }
  560. }
  561. .image-upload-container {
  562. padding: 20rpx;
  563. }
  564. .image-list {
  565. display: flex;
  566. flex-wrap: wrap;
  567. gap: 20rpx;
  568. }
  569. .image-item {
  570. position: relative;
  571. width: 200rpx;
  572. height: 200rpx;
  573. box-sizing: border-box;
  574. }
  575. .delete-btn {
  576. position: absolute;
  577. top: -10rpx;
  578. right: -10rpx;
  579. width: 40rpx;
  580. height: 40rpx;
  581. background-color: #ff4d4f;
  582. color: #fff;
  583. border-radius: 50%;
  584. display: flex;
  585. align-items: center;
  586. justify-content: center;
  587. font-size: 30rpx;
  588. font-weight: bold;
  589. z-index: 10;
  590. cursor: pointer;
  591. }
  592. .upload-btn {
  593. width: 200rpx;
  594. height: 200rpx;
  595. border: 8rpx dashed #ddd;
  596. border-radius: 30rpx;
  597. display: flex;
  598. align-items: center;
  599. justify-content: center;
  600. background-color: #f9f9f9;
  601. box-sizing: border-box;
  602. cursor: pointer;
  603. }
  604. .call-records-container {
  605. padding: 20rpx;
  606. }
  607. .info-card {
  608. @extend .card-wrap;
  609. padding: 20rpx;
  610. margin-top: 20rpx;
  611. box-sizing: border-box;
  612. width: 100%;
  613. max-width: 100%;
  614. }
  615. .info-card-title {
  616. @include font-styles($size: title, $weight: bold, $color: primary);
  617. margin-bottom: 25rpx;
  618. padding-bottom: 15rpx;
  619. border-bottom: 1rpx solid map-get($colors, border);
  620. }
  621. .info-row {
  622. margin-bottom: 20rpx;
  623. }
  624. .info-label {
  625. @include font-styles($size: tiny, $weight: regular, $color: tertiary);
  626. margin-bottom: 8rpx;
  627. }
  628. .info-value {
  629. @include font-styles($size: small, $weight: regular, $color: secondary);
  630. word-break: break-all;
  631. }
  632. .contact-card {
  633. display: flex;
  634. justify-content: space-between;
  635. margin: 20rpx 0;
  636. gap: 20rpx;
  637. }
  638. .contact-item {
  639. flex: 1;
  640. @extend .card-wrap;
  641. padding: 20rpx;
  642. display: flex;
  643. flex-direction: column;
  644. align-items: center;
  645. position: relative;
  646. cursor: pointer;
  647. }
  648. .contact-title {
  649. @include font-styles($size: tiny, $weight: regular, $color: tertiary);
  650. margin-top: 10rpx;
  651. }
  652. .red-dot {
  653. position: absolute;
  654. top: 15rpx;
  655. right: 15rpx;
  656. width: 25rpx;
  657. height: 25rpx;
  658. background-color: #ff4d4f;
  659. border-radius: 50%;
  660. box-shadow: 0 0 4rpx rgba(255, 77, 79, 0.3);
  661. }
  662. .space-block {
  663. height: 100rpx;
  664. }
  665. .next-btn {
  666. position: fixed;
  667. bottom: 10rpx;
  668. left: 2.5%;
  669. width: 95%;
  670. height: 80rpx;
  671. line-height: 80rpx;
  672. text-align: center;
  673. border-radius: 20rpx;
  674. }
  675. .detail-image-header {
  676. display: flex;
  677. justify-content: space-between;
  678. align-items: center;
  679. width: 100%;
  680. border-bottom: 1rpx solid map-get($colors, border);
  681. }
  682. .detail-image-title {
  683. @include font-styles($size: content, $weight: bold, $color: primary);
  684. }
  685. .copy-btn {
  686. border-radius: 20rpx;
  687. border: 1rpx solid #007AFF;
  688. background-color: transparent;
  689. color: #007AFF;
  690. padding: 0 24rpx;
  691. height: 64rpx;
  692. line-height: 64rpx;
  693. cursor: pointer;
  694. }
  695. </style>