processFeedback.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. <template>
  2. <zj-page-layout :hasFooter="true">
  3. <view class="all-container">
  4. <view class="type-container mt30">
  5. <view class="common-title"><text>*</text>反馈类型</view>
  6. <view class="list">
  7. <view
  8. class="item"
  9. v-for="(item, index) in typeList"
  10. :key="index"
  11. :class="typeVal === item.value ? 'active' : ''"
  12. @tap="changeType(item.value)"
  13. >{{ item.name }}</view
  14. >
  15. </view>
  16. </view>
  17. <view class="date-conteiner mt30" v-if="typeVal == '2'">
  18. <view class="item">
  19. <view class="label"><text>*</text>预约上门开始时间</view>
  20. <view class="picker" @tap="isShowDatePicker = true">
  21. <text class="value" v-if="date">{{ date }}</text>
  22. <text class="placeholder" v-else>请选择</text>
  23. <text class="iconfont icon-jinru"></text>
  24. </view>
  25. </view>
  26. </view>
  27. <view class="date-conteiner mt30" v-if="typeVal == '2'">
  28. <view class="item">
  29. <view class="label"><text>*</text>预约上门结束时间</view>
  30. <view class="picker" @tap="setisShowDatePicker2">
  31. <text class="value" v-if="date2">{{ date2 }}</text>
  32. <text class="placeholder" v-else>请选择</text>
  33. <text class="iconfont icon-jinru"></text>
  34. </view>
  35. </view>
  36. </view>
  37. <view class="over-container mt30" v-if="typeVal == '3'">
  38. <view class="row">
  39. <view class="label"><text>*</text>完工采集</view>
  40. <view class="btn"
  41. ><u-button
  42. type="primary"
  43. :text="orderDetail && orderDetail.isGather == 'YES' ? '查看采集' : '数据采集'"
  44. @click="toCollect"
  45. ></u-button>
  46. </view>
  47. </view>
  48. <view class="row">
  49. <view class="label"
  50. >收费项目<u-icon
  51. name="question-circle"
  52. color="#999999"
  53. @click="$tips('在服务过程中消耗的产品(配件)')"
  54. ></u-icon
  55. ></view>
  56. <view class="btn"><u-button type="primary" text="添加" @click="toCharge"></u-button></view>
  57. </view>
  58. </view>
  59. <view class="sign-container mt30" v-if="typeVal == '3'">
  60. <view class="common-title"><text>*</text>客户签名</view>
  61. <view class="empty" @tap="toSignName" v-if="!signNameUrl">点我签名</view>
  62. <view class="img" v-else>
  63. <image :src="signNameUrl" mode="widthFix" @tap="prevSignImg(signNameUrl)"></image>
  64. <text class="iconfont icon-guanbi1" @tap="signNameUrl = ''"></text>
  65. </view>
  66. </view>
  67. <view class="mt30 remark-container">
  68. <view class="common-title"><text v-if="typeVal == '1' || typeVal == '4'">*</text>备注信息</view>
  69. <u--textarea v-model="remarkVal" placeholder="请输入备注内容" fixed border="none" height="120"></u--textarea>
  70. </view>
  71. <view class="mt30 image-container" v-if="typeVal == '1' || typeVal == '4'">
  72. <view class="common-title">上传图片(最多6张)</view>
  73. <view class="images">
  74. <block v-for="(item, index) in imageList" :key="index">
  75. <view class="img">
  76. <image :src="item.url" mode="aspectFill" @tap="prevImg(item.url)"></image>
  77. <text class="iconfont icon-guanbi1" @tap="delImage(index)"></text>
  78. </view>
  79. </block>
  80. <view class="add" @tap="addImage" v-if="imageList.length < 6">
  81. <text class="iconfont icon-xiangji"></text>
  82. <text class="text">添加图片</text>
  83. </view>
  84. </view>
  85. </view>
  86. </view>
  87. <template slot="footer">
  88. <view class="footer-btn-group">
  89. <u-button text="确定" type="primary" size="large" @click="submitData"></u-button>
  90. </view>
  91. </template>
  92. <u-datetime-picker
  93. :show="isShowDatePicker"
  94. v-model="datePickerValue"
  95. type="datetime"
  96. placeholder="选择日期和时间"
  97. @confirm="confirmDate"
  98. @cancel="isShowDatePicker = false"
  99. @close="isShowDatePicker = false"
  100. :minDate="minDate"
  101. :formatter="formatter"
  102. :style="{ width: '100%' }"
  103. />
  104. <u-datetime-picker
  105. :show="isShowDatePicker2"
  106. v-model="datePickerValue2"
  107. type="datetime"
  108. placeholder="选择日期和时间"
  109. @confirm="confirmDate2"
  110. @cancel="isShowDatePicker2 = false"
  111. @close="isShowDatePicker2 = false"
  112. :minDate="minDate2"
  113. :formatter="formatter"
  114. :style="{ width: '100%' }"
  115. />
  116. </zj-page-layout>
  117. </template>
  118. <script>
  119. import { uploadImgFull } from '@/common/utils/util.js'
  120. export default {
  121. data() {
  122. return {
  123. datetime: null,
  124. limitDateTime: new Date('2024-07-19 12:23:36'),
  125. id: '',
  126. orderDetail: {},
  127. typeList: [
  128. { name: '其他', value: '1' },
  129. { name: '改约', value: '2' },
  130. { name: '完工反馈', value: '3' },
  131. { name: '异常反馈', value: '4' }
  132. ],
  133. typeVal: '1',
  134. minDate: new Date().getTime(),
  135. minDate2: null,
  136. date: '',
  137. datePickerValue: new Date().getTime(),
  138. isShowDatePicker: false,
  139. date2: '',
  140. datePickerValue2: new Date().getTime(),
  141. isShowDatePicker2: false,
  142. remarkVal: '', // 备注值
  143. imageList: [],
  144. signNameUrl: '',
  145. canClickBtn: true
  146. }
  147. },
  148. async onLoad({ id }) {
  149. this.id = id
  150. this.getOrderDetail()
  151. this.crossPage.$on('finishSign', async data => {
  152. // this.$showLoading();
  153. this.signNameUrl = data
  154. })
  155. this.crossPage.$on('refreshFeedbackForm', () => {
  156. this.getOrderDetail()
  157. })
  158. },
  159. onUnload() {
  160. this.crossPage.$off('finishSign')
  161. this.crossPage.$off('refreshFeedbackForm')
  162. },
  163. methods: {
  164. confirmDate(e) {
  165. this.isShowDatePicker = false
  166. this.date = ''
  167. this.$nextTick(() => {
  168. this.date = this.formatTimestamp(e.value)
  169. this.date2 = ''
  170. this.datePickerValue2 = new Date(e.value).getTime()
  171. })
  172. },
  173. confirmDate2(e) {
  174. this.isShowDatePicker2 = false
  175. this.date2 = this.formatTimestamp(e.value)
  176. },
  177. setisShowDatePicker2() {
  178. if (!this.date) {
  179. return this.$tips('请先选择预约开始时间')
  180. }
  181. this.minDate2 = Number(new Date(this.date))
  182. this.$nextTick(() => {
  183. this.isShowDatePicker2 = true
  184. })
  185. },
  186. formatter(type, value) {
  187. if (type === 'year') {
  188. return `${value}年`
  189. }
  190. if (type === 'month') {
  191. return `${value}月`
  192. }
  193. if (type === 'day') {
  194. return `${value}日`
  195. }
  196. if (type === 'hour') {
  197. return `${value}时`
  198. }
  199. if (type === 'minute') {
  200. return `${value}分`
  201. }
  202. return value
  203. },
  204. // 获取详情
  205. getOrderDetail() {
  206. return new Promise((resolve, reject) => {
  207. this.$api
  208. .post('/pg/order/base/detail', {
  209. orderBaseId: this.id
  210. })
  211. .then(res => {
  212. this.orderDetail = res.data
  213. if (res.data.appointmentTime) {
  214. this.typeList[1].name = '改约'
  215. } else {
  216. this.typeList[1].name = '预约'
  217. }
  218. resolve(1)
  219. })
  220. .catch(res => {
  221. reject(0)
  222. })
  223. })
  224. },
  225. changeType(val) {
  226. if (!this.orderDetail.appointmentTime && val == 3) {
  227. return this.$tips('请先预约工单再完工反馈')
  228. }
  229. this.typeVal = val
  230. },
  231. // 去采集
  232. toCollect() {
  233. this.$navToPage({
  234. url: `/packageWorkorder/pages/infoCollect/list?id=${this.id}`
  235. })
  236. },
  237. confirmDate(e) {
  238. this.isShowDatePicker = false
  239. this.date = ''
  240. this.$nextTick(() => {
  241. this.date = this.formatTimestamp(e.value)
  242. this.date2 = ''
  243. this.datePickerValue2 = new Date(e.value).getTime()
  244. })
  245. },
  246. confirmDate2(e) {
  247. this.isShowDatePicker2 = false
  248. this.date2 = this.formatTimestamp(e.value)
  249. },
  250. formatTimestamp(timestamp) {
  251. const date = new Date(timestamp)
  252. const year = date.getFullYear()
  253. const month = (date.getMonth() + 1).toString().padStart(2, '0')
  254. const day = date.getDate().toString().padStart(2, '0')
  255. const hours = date.getHours().toString().padStart(2, '0')
  256. const minutes = date.getMinutes().toString().padStart(2, '0')
  257. const seconds = date.getSeconds().toString().padStart(2, '0')
  258. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  259. },
  260. // 添加图片
  261. async addImage() {
  262. uni.chooseImage({
  263. count: 6 - this.imageList.length,
  264. success: res => {
  265. uni.showLoading()
  266. res.tempFilePaths.forEach(async item => {
  267. let data = await uploadImgFull(item)
  268. this.imageList.push(data)
  269. })
  270. uni.hideLoading()
  271. },
  272. fail: err => {}
  273. })
  274. },
  275. // 删除图片
  276. delImage(index) {
  277. this.imageList.splice(index, 1)
  278. },
  279. //预览图片
  280. prevImg(current) {
  281. uni.previewImage({
  282. current,
  283. urls: this.imageList.map(item => {
  284. return item.url
  285. })
  286. })
  287. },
  288. prevSignImg(url) {
  289. uni.previewImage({
  290. current: url,
  291. urls: [url]
  292. })
  293. },
  294. // 去收费
  295. toCharge() {
  296. if (this.orderDetail.orderType == 'INSTALL') {
  297. this.$navToPage({
  298. url: `/packageMaterial/pages/sale/index?type=${this.orderDetail.orderType == 'INSTALL' ? 'M' : 'P'}&oid=${
  299. this.id
  300. }`
  301. })
  302. } else {
  303. this.$navToPage({
  304. url: `/packageMaterial/pages/newSale/index?type=${this.orderDetail.orderType == 'INSTALL' ? 'M' : 'P'}&oid=${
  305. this.id
  306. }`
  307. })
  308. }
  309. },
  310. toSignName() {
  311. this.$navToPage({
  312. url: `/packageWorkorder/pages/signName`
  313. })
  314. },
  315. // 提交
  316. async submitData() {
  317. if (!this.canClickBtn) return this.$toast('请勿频繁操作')
  318. this.canClickBtn = false
  319. setTimeout(() => {
  320. this.canClickBtn = true
  321. }, 3000)
  322. if (this.typeVal == '2' && !this.date) return this.$toast('请选择改约时间')
  323. if (this.typeVal == '2' && !this.date2) return this.$toast('请选择改约时间')
  324. if (this.typeVal == '3' && !this.signNameUrl) return this.$toast('请签名')
  325. if ((this.typeVal == '1' || this.typeVal == '4') && !this.remarkVal) return this.$toast('请填写备注信息')
  326. let params = {}
  327. if (this.typeVal == '2') {
  328. params.id = this.id
  329. params.appointmentTime = this.date
  330. params.appointmentEndTime = this.date2
  331. params.remark = this.remarkVal
  332. } else {
  333. params.orderBaseId = this.id
  334. params.content = this.remarkVal
  335. }
  336. if (this.typeVal == '1') {
  337. params.type = '其他'
  338. params.typeText = '其他'
  339. }
  340. if (this.typeVal == '4') {
  341. params.type = '异常反馈'
  342. params.typeText = '异常反馈'
  343. }
  344. if (this.typeVal == '1' || this.typeVal == '4') {
  345. params.imgSrc = this.imageList
  346. .map(item => {
  347. return item.url
  348. })
  349. .join(',')
  350. }
  351. if (this.typeVal == '3') {
  352. const lo = await this.$getAddress()
  353. params.orderBaseId = this.id
  354. params.overRemark = this.remarkVal
  355. params.userSign = this.signNameUrl
  356. params.address = lo.address || ''
  357. }
  358. // 其他/异常反馈
  359. if (this.typeVal == '1' || this.typeVal == '4') {
  360. this.$api
  361. .postJson('/pg/order/base/operator/add', {
  362. ...params
  363. })
  364. .then(res => {
  365. this.submitSuccess()
  366. })
  367. }
  368. // 改约
  369. else if (this.typeVal == '2') {
  370. this.$api
  371. .post('/changeOrder/changeAppointmentTime', {
  372. ...params
  373. })
  374. .then(res => {
  375. this.submitSuccess()
  376. })
  377. }
  378. // 完工反馈
  379. else if (this.typeVal == '3') {
  380. this.$api
  381. .post('/changeOrder/submitAll', {
  382. ...params
  383. })
  384. .then(res => {
  385. this.submitSuccess('YWG')
  386. })
  387. }
  388. },
  389. submitSuccess(tab = '') {
  390. this.$successToast('反馈成功')
  391. this.crossPage.$emit('refreshWorkorderList', { tab: tab })
  392. this.crossPage.$emit('refreshWorkorderDetail', '')
  393. setTimeout(() => {
  394. this.$navToPage(
  395. {
  396. delta: 1
  397. },
  398. 'navigateBack'
  399. )
  400. }, 500)
  401. }
  402. }
  403. }
  404. </script>
  405. <style lang="scss" scoped>
  406. .all-container {
  407. padding-bottom: 30rpx;
  408. }
  409. .common-title {
  410. font-size: 32rpx;
  411. font-weight: 600;
  412. padding: 30rpx 0;
  413. text {
  414. color: $minor-color;
  415. }
  416. }
  417. .type-container {
  418. background: #ffffff;
  419. padding: 0 30rpx;
  420. .list {
  421. display: flex;
  422. flex-wrap: wrap;
  423. .item {
  424. width: calc((100% - 30rpx) / 2);
  425. height: 80rpx;
  426. line-height: 80rpx;
  427. text-align: center;
  428. border-radius: 20rpx;
  429. background: #f4f5f9;
  430. margin-bottom: 30rpx;
  431. &:nth-child(2n) {
  432. margin-left: 30rpx;
  433. }
  434. &.active {
  435. background: $theme-color;
  436. color: #ffffff;
  437. }
  438. }
  439. }
  440. }
  441. .date-conteiner {
  442. background: #ffffff;
  443. padding: 30rpx 30rpx;
  444. .item {
  445. display: flex;
  446. align-items: center;
  447. justify-content: space-between;
  448. height: 50rpx;
  449. .label {
  450. margin-right: 30rpx;
  451. text {
  452. color: $error-color;
  453. }
  454. }
  455. .picker {
  456. .placeholder {
  457. color: $sec-font;
  458. }
  459. .iconfont {
  460. margin-left: 12rpx;
  461. color: $sec-font;
  462. }
  463. }
  464. }
  465. }
  466. .over-container {
  467. background: #ffffff;
  468. padding: 10rpx 30rpx;
  469. .row {
  470. height: 100rpx;
  471. display: flex;
  472. align-items: center;
  473. justify-content: space-between;
  474. .label {
  475. font-size: 32rpx;
  476. font-weight: 600;
  477. display: flex;
  478. align-items: center;
  479. text {
  480. color: $minor-color;
  481. }
  482. ::v-deep .u-icon {
  483. margin-left: 8rpx;
  484. }
  485. }
  486. .btn {
  487. ::v-deep .u-button {
  488. height: 60rpx;
  489. }
  490. }
  491. }
  492. }
  493. .sign-container {
  494. background: #ffffff;
  495. padding: 0 30rpx 30rpx;
  496. .empty {
  497. height: 200rpx;
  498. background: #f4f5f9;
  499. display: flex;
  500. align-items: center;
  501. justify-content: center;
  502. border-radius: 12rpx;
  503. color: $sec-font;
  504. }
  505. .img {
  506. position: relative;
  507. image {
  508. width: 100%;
  509. height: 200rpx;
  510. }
  511. text {
  512. position: absolute;
  513. right: -10rpx;
  514. top: -10rpx;
  515. width: 40rpx;
  516. height: 40rpx;
  517. border-radius: 50%;
  518. background: #ff3f42;
  519. font-size: 24rpx;
  520. color: #ffffff;
  521. text-align: center;
  522. line-height: 40rpx;
  523. display: block;
  524. }
  525. }
  526. }
  527. .remark-container {
  528. padding: 1rpx 30rpx 30rpx;
  529. background: #ffffff;
  530. ::v-deep .u-textarea {
  531. background: #f4f5f9;
  532. }
  533. }
  534. .image-container {
  535. padding: 1rpx 30rpx 30rpx;
  536. background: #ffffff;
  537. .images {
  538. display: flex;
  539. flex-wrap: wrap;
  540. .add {
  541. display: flex;
  542. flex-direction: column;
  543. align-items: center;
  544. justify-content: center;
  545. width: 146rpx;
  546. height: 146rpx;
  547. border: 2rpx dashed #dadada;
  548. border-radius: 10rpx;
  549. margin-top: 20rpx;
  550. .iconfont {
  551. font-size: 48rpx;
  552. color: #999999;
  553. }
  554. .text {
  555. font-size: 24rpx;
  556. color: #999999;
  557. margin-top: 10rpx;
  558. }
  559. }
  560. .img {
  561. position: relative;
  562. margin-right: 20rpx;
  563. margin-top: 20rpx;
  564. &:nth-child(4n) {
  565. margin-right: 0;
  566. }
  567. image {
  568. width: 150rpx;
  569. height: 150rpx;
  570. border-radius: 10rpx;
  571. overflow: hidden;
  572. display: block;
  573. }
  574. text {
  575. position: absolute;
  576. right: -10rpx;
  577. top: -10rpx;
  578. width: 40rpx;
  579. height: 40rpx;
  580. border-radius: 50%;
  581. background: #ff3f42;
  582. font-size: 24rpx;
  583. color: #ffffff;
  584. text-align: center;
  585. line-height: 40rpx;
  586. display: block;
  587. }
  588. }
  589. }
  590. }
  591. </style>