展品维保小程序前端代码接口
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

748 lines
19 KiB

1 month ago
  1. <template>
  2. <view class="repair-change">
  3. <!-- 处理内容 -->
  4. <view class="process-content">
  5. <view class="info-header">
  6. <view class="red-line"></view>
  7. <text class="info-title">处理内容</text>
  8. </view>
  9. <!-- 维修人 -->
  10. <view class="form-item">
  11. <text class="label">维修人</text>
  12. <view class="input-area">
  13. <input
  14. v-model="processData.repairName"
  15. placeholder="请填写"
  16. class="input-field"
  17. ref="maintainerInput"
  18. />
  19. </view>
  20. </view>
  21. <!-- 联系方式 -->
  22. <view class="form-item">
  23. <text class="label">联系方式</text>
  24. <view class="input-area">
  25. <input
  26. v-model="processData.phone"
  27. placeholder="请填写"
  28. class="input-field"
  29. ref="maintainerInput"
  30. />
  31. </view>
  32. </view>
  33. <!-- 维修日期 -->
  34. <view class="form-item" @click="showDatePicker">
  35. <text class="label">维修日期</text>
  36. <view class="select-area">
  37. <text class="value" :class="{ placeholder: !processData.repairDate }">{{ processData.repairDate || '请选择' }}</text>
  38. <uv-icon name="arrow-down" size="18" color="#000"></uv-icon>
  39. </view>
  40. </view>
  41. <!-- 处理内容 -->
  42. <view class="form-item">
  43. <text class="label">处理内容</text>
  44. </view>
  45. <view class="textarea-container">
  46. <uv-textarea
  47. v-model="processData.content"
  48. placeholder="请输入处理内容"
  49. :maxlength="200"
  50. :show-confirm-bar="false"
  51. height="60"
  52. border="none"
  53. :textStyle="{ color: '#000' }"
  54. :custom-style="{ backgroundColor: '#f5f5f5' }"
  55. ></uv-textarea>
  56. </view>
  57. <!-- 上传图片 -->
  58. <view class="form-item">
  59. <text class="label">上传图片</text>
  60. </view>
  61. <view class="image-upload">
  62. <view v-for="(img, index) in getImageList(processData.image)" :key="index" class="image-item">
  63. <image :src="img.trim()" mode="aspectFill" @click="previewImage(img.trim())"></image>
  64. <view class="delete-btn" @click="deleteImage(index)">
  65. <uv-icon name="close" size="12" color="#fff"></uv-icon>
  66. </view>
  67. </view>
  68. <view class="upload-btn" @click="uploadImage">
  69. <uv-icon name="camera" size="34" color="#C70019"></uv-icon>
  70. </view>
  71. </view>
  72. <!-- 是否产生费用 -->
  73. <view class="form-item">
  74. <text class="label">是否产生费用</text>
  75. <view class="radio-options">
  76. <view
  77. class="radio-item"
  78. :class="{ active: processData.isExpend === '1' }"
  79. @click="selectCost('1')"
  80. >
  81. <view class="radio-dot" :class="{ active: processData.isExpend === '1' }"></view>
  82. <text :class="{ active: processData.isExpend === '1' }"></text>
  83. </view>
  84. <view
  85. class="radio-item"
  86. :class="{ active: processData.isExpend === '0' }"
  87. @click="selectCost('0')"
  88. >
  89. <view class="radio-dot" :class="{ active: processData.isExpend === '0' }"></view>
  90. <text :class="{ active: processData.isExpend === '0' }"></text>
  91. </view>
  92. </view>
  93. </view>
  94. <!-- 产生费用 -->
  95. <view class="form-item" v-if="processData.isExpend === '1'">
  96. <text class="label">产生费用</text>
  97. <view class="input-area">
  98. <input
  99. v-model="amount"
  100. placeholder="请输入费用"
  101. disabled
  102. class="input-field"
  103. ref="maintainerInput"
  104. />
  105. </view>
  106. </view>
  107. <!-- 费用详情表格 -->
  108. <view class="cost-table" v-if="processData.isExpend === '1'">
  109. <view class="table-header">
  110. <text class="header-cell">费用名称</text>
  111. <text class="header-cell">数量</text>
  112. <text class="header-cell">金额</text>
  113. <text class="header-cell"></text>
  114. </view>
  115. <view class="table-row" v-for="(item, index) in processData.costDetails" :key="index">
  116. <view class="cell-input">
  117. <uv-input
  118. v-model="item.name"
  119. placeholder="费用名称"
  120. border="none"
  121. :custom-style="{ backgroundColor: 'transparent', fontSize: '28rpx' }"
  122. ></uv-input>
  123. </view>
  124. <view class="cell-input">
  125. <uv-input
  126. v-model="item.quantity"
  127. placeholder="数量"
  128. border="none"
  129. :custom-style="{ backgroundColor: 'transparent', fontSize: '28rpx' }"
  130. ></uv-input>
  131. </view>
  132. <view class="cell-input">
  133. <uv-input
  134. v-model="item.amount"
  135. placeholder="金额"
  136. border="none"
  137. :custom-style="{ backgroundColor: 'transparent', fontSize: '28rpx' }"
  138. ></uv-input>
  139. </view>
  140. <view class="cell-action">
  141. <view class="action-btn delete-btn" @click="deleteCostItem(index)" v-if="processData.costDetails.length > 1">
  142. <uv-icon name="close" size="14" color="#fff"></uv-icon>
  143. </view>
  144. <view class="action-btn add-btn" @click="addCostItem" v-if="index === processData.costDetails.length - 1">
  145. <uv-icon name="plus" size="14" color="#fff"></uv-icon>
  146. </view>
  147. </view>
  148. </view>
  149. </view>
  150. <!-- 问题是否解决 -->
  151. <view class="form-item">
  152. <text class="label">问题是否解决</text>
  153. <view class="radio-options">
  154. <view
  155. class="radio-item"
  156. :class="{ active: processData.isFix === '1' }"
  157. @click="selectResolved('1')"
  158. >
  159. <view class="radio-dot" :class="{ active: processData.isFix === '1' }"></view>
  160. <text :class="{ active: processData.isFix === '1' }"></text>
  161. </view>
  162. <view
  163. class="radio-item"
  164. :class="{ active: processData.isFix === '0' }"
  165. @click="selectResolved('0')"
  166. >
  167. <view class="radio-dot" :class="{ active: processData.isFix === '0' }"></view>
  168. <text :class="{ active: processData.isFix === '0' }"></text>
  169. </view>
  170. </view>
  171. </view>
  172. <!-- 备注 -->
  173. <view class="form-item">
  174. <text class="label">备注</text>
  175. </view>
  176. <view class="textarea-container">
  177. <uv-textarea
  178. v-model="processData.remark"
  179. placeholder="备注"
  180. :maxlength="200"
  181. :show-confirm-bar="false"
  182. height="60"
  183. border="none"
  184. :custom-style="{ backgroundColor: '#f5f5f5' }"
  185. ></uv-textarea>
  186. </view>
  187. <!-- 立即维修按钮 -->
  188. <view class="repair-btn-container">
  189. <uv-button
  190. type="primary"
  191. text="点击提交"
  192. :custom-style="{ backgroundColor: '#C70019', borderRadius: '25px' }"
  193. @click="startRepair"
  194. ></uv-button>
  195. </view>
  196. </view>
  197. <!-- 日期选择器 -->
  198. <uv-datetime-picker
  199. confirm-color="#C70019"
  200. ref="datePicker"
  201. v-model="timeValue"
  202. mode="date"
  203. @confirm="confirmDate"
  204. ></uv-datetime-picker>
  205. </view>
  206. </template>
  207. <script>
  208. export default {
  209. data() {
  210. return {
  211. timeValue: Number(new Date()),
  212. id: '',
  213. showpieceId: '',
  214. // 处理内容(可编辑)
  215. processData: {
  216. repairName: '',
  217. phone: '',
  218. repairDate: '',
  219. content: '',
  220. image: '',
  221. isExpend: '0',
  222. costDetails: [
  223. { name: '', quantity: '', amount: '' }
  224. ],
  225. isFix: '0',
  226. remark: ''
  227. },
  228. }
  229. },
  230. computed: {
  231. // 计算总金额
  232. amount() {
  233. return this.processData.costDetails.reduce((sum, item) => {
  234. return sum + (Number(item.quantity) * parseFloat(item.amount || 0))
  235. }, 0)
  236. }
  237. },
  238. onLoad(args) {
  239. this.id = args.id
  240. this.showpieceId = args.showpieceId
  241. },
  242. methods: {
  243. // 获取图片列表
  244. getImageList(imageStr) {
  245. if (!imageStr || typeof imageStr !== 'string') {
  246. return []
  247. }
  248. return imageStr.split(',').filter(img => img && img.trim())
  249. },
  250. // 预览图片
  251. previewImage(img) {
  252. uni.previewImage({
  253. urls: [img],
  254. current: img
  255. })
  256. },
  257. // 显示日期选择器
  258. showDatePicker() {
  259. this.$refs.datePicker.open()
  260. },
  261. // 确认日期
  262. confirmDate(e) {
  263. this.processData.repairDate = this.$utils.formatTime(e.value)
  264. },
  265. // 上传图片
  266. async uploadImage() {
  267. try {
  268. const result = await this.$utils.chooseAndUpload()
  269. if (result && result.success) {
  270. console.log(result);
  271. // 将新图片URL添加到字符串中
  272. if (this.processData.image) {
  273. this.processData.image += ',' + result.url
  274. } else {
  275. this.processData.image = result.url
  276. }
  277. }
  278. } catch (error) {
  279. console.error('图片上传失败:', error)
  280. uni.showToast({
  281. title: '图片上传失败',
  282. icon: 'error'
  283. })
  284. }
  285. },
  286. // 删除图片
  287. deleteImage(index) {
  288. const imageList = this.getImageList(this.processData.image)
  289. imageList.splice(index, 1)
  290. this.processData.image = imageList.join(',')
  291. },
  292. // 选择是否产生费用
  293. selectCost(value) {
  294. this.processData.isExpend = value
  295. if (!value) {
  296. this.processData.amount = 0
  297. this.processData.costDetails = [{ name: '', quantity: '', amount: '' }]
  298. }
  299. },
  300. // 添加费用项
  301. addCostItem() {
  302. this.processData.costDetails.push({ name: '', quantity: '', amount: '' })
  303. },
  304. // 删除费用项
  305. deleteCostItem(index) {
  306. this.processData.costDetails.splice(index, 1)
  307. },
  308. // 选择问题是否解决
  309. selectResolved(value) {
  310. this.processData.isFix = value
  311. },
  312. // 立即维修
  313. async startRepair() {
  314. // 验证必填项
  315. if (!this.processData.repairName) {
  316. uni.showToast({
  317. title: '请输入维修人',
  318. icon: 'none'
  319. })
  320. return
  321. }
  322. if (!this.processData.phone) {
  323. uni.showToast({
  324. title: '请输入联系方式',
  325. icon: 'none'
  326. })
  327. return
  328. }
  329. if (!this.processData.repairDate) {
  330. uni.showToast({
  331. title: '请选择维修日期',
  332. icon: 'none'
  333. })
  334. return
  335. }
  336. if (!this.processData.content) {
  337. uni.showToast({
  338. title: '请输入处理内容',
  339. icon: 'none'
  340. })
  341. return
  342. }
  343. if (this.processData.isFix === null) {
  344. uni.showToast({
  345. title: '请选择问题是否解决',
  346. icon: 'none'
  347. })
  348. return
  349. }
  350. if (this.processData.isExpend === '1') {
  351. // 如果总金额为0则没有消费信息
  352. if (!this.amount) {
  353. uni.showToast({
  354. title: '请添加费用明细',
  355. icon: 'none'
  356. })
  357. return
  358. }
  359. }
  360. const tempData = {...this.processData}
  361. delete tempData.costDetails
  362. const tempDataList = [...this.processData.costDetails]
  363. if (this.processData.isExpend === '1') {
  364. tempData.expenseList = tempDataList.map(item => `${item.name},${item.quantity},${item.amount}`).join(';')
  365. } else {
  366. delete tempData.expenseList
  367. }
  368. const submitRes = await this.$api.exhibit.updateRepair({
  369. id: this.id,
  370. ...tempData,
  371. amount: this.amount
  372. })
  373. if (submitRes.code === 200) {
  374. uni.showToast({
  375. title: submitRes.message,
  376. icon: 'none'
  377. })
  378. setTimeout(() => {
  379. uni.navigateBack({
  380. delta: 1
  381. })
  382. }, 400)
  383. } else {
  384. uni.showToast({
  385. title: submitRes.message,
  386. icon: 'none'
  387. })
  388. }
  389. },
  390. async getDetail(){
  391. const res = await this.$api.exhibit.queryRepairById({ repairId: this.id })
  392. if (res.code === 200) {
  393. const detail = res.result
  394. // 基本信息字段映射
  395. this.processData.repairName = detail.repairName || ''
  396. this.processData.phone = detail.phone || ''
  397. this.processData.repairDate = detail.repairDate || ''
  398. this.processData.content = detail.content || ''
  399. this.processData.remark = detail.remark || ''
  400. // 维修状态字段映射
  401. this.processData.isFix = detail.isFix || '0'
  402. this.processData.isExpend = detail.isExpend || '0'
  403. // 处理图片字段
  404. if (detail.image && detail.image.trim()) {
  405. // 去除首尾空格,然后按逗号分割,过滤空字符串,并重新组合为字符串
  406. this.processData.image = detail.image.trim().split(',').filter(img => img.trim()).map(img => img.trim()).join(',')
  407. } else {
  408. this.processData.image = ''
  409. }
  410. // 设置amount字段
  411. this.amount = detail.amount || 0
  412. // 处理费用列表数据
  413. if (detail.exhibitRepairExpenseList && Array.isArray(detail.exhibitRepairExpenseList)) {
  414. this.processData.costDetails = detail.exhibitRepairExpenseList.map(item => ({
  415. name: item.title || '',
  416. quantity: item.quantity || 1,
  417. amount: item.amount || 0
  418. }))
  419. } else {
  420. this.processData.costDetails = []
  421. }
  422. }
  423. }
  424. },
  425. onLoad(args) {
  426. this.id = args.id
  427. this.getDetail()
  428. },
  429. }
  430. </script>
  431. <style lang="scss" scoped>
  432. .repair-change {
  433. min-height: 100vh;
  434. background-color: #f5f5f5;
  435. padding-bottom: 200rpx;
  436. }
  437. .process-content {
  438. margin: 18rpx;
  439. background: #ffffff;
  440. border-radius: 15rpx;
  441. box-shadow: 0rpx 3rpx 6rpx 0rpx rgba(0,0,0,0.16);
  442. padding: 30rpx;
  443. .info-header {
  444. display: flex;
  445. align-items: center;
  446. margin-bottom: 40rpx;
  447. .red-line {
  448. width: 9rpx;
  449. height: 33rpx;
  450. background-color: $primary-color;
  451. margin-right: 7rpx;
  452. border-radius: 5rpx;
  453. }
  454. .info-title {
  455. font-size: 30rpx;
  456. font-weight: bold;
  457. color: $primary-text-color;
  458. }
  459. }
  460. .form-item {
  461. display: flex;
  462. align-items: center;
  463. justify-content: space-between;
  464. padding: 18rpx 0;
  465. border-bottom: 2rpx solid #f0f0f0;
  466. &:last-child {
  467. border-bottom: none;
  468. }
  469. .input-area {
  470. flex: 1;
  471. border-radius: 8rpx;
  472. padding: 2rpx 24rpx;
  473. margin-left: 24rpx;
  474. .input-field {
  475. width: 100%;
  476. font-size: 30rpx;
  477. color: $primary-text-color;
  478. background: transparent;
  479. border: none;
  480. outline: none;
  481. text-align: right;
  482. &::placeholder {
  483. color: $secondary-text-color;
  484. }
  485. }
  486. }
  487. .label {
  488. font-size: 30rpx;
  489. color: $primary-text-color;
  490. flex-shrink: 0;
  491. &.active {
  492. font-weight: bold;
  493. }
  494. }
  495. .value {
  496. font-size: 30rpx;
  497. color: $primary-text-color;
  498. &.placeholder {
  499. color: $secondary-text-color;
  500. }
  501. &.red-text {
  502. color: $primary-color;
  503. }
  504. }
  505. .select-area {
  506. display: flex;
  507. align-items: center;
  508. gap: 16rpx;
  509. }
  510. }
  511. .textarea-container {
  512. margin: 16rpx 0;
  513. background-color: #f5f5f5;
  514. border-radius: 8rpx;
  515. padding: 8rpx 16rpx;
  516. }
  517. .image-upload {
  518. display: flex;
  519. flex-wrap: wrap;
  520. gap: 24rpx;
  521. margin: 16rpx 0;
  522. .upload-btn {
  523. width: 160rpx;
  524. height: 160rpx;
  525. border: 2rpx dashed $primary-color;
  526. display: flex;
  527. align-items: center;
  528. justify-content: center;
  529. background-color: #fff;
  530. border-radius: 8rpx;
  531. }
  532. .image-item {
  533. position: relative;
  534. width: 160rpx;
  535. height: 160rpx;
  536. image {
  537. width: 100%;
  538. height: 100%;
  539. border-radius: 8rpx;
  540. }
  541. .delete-btn {
  542. position: absolute;
  543. top: -12rpx;
  544. right: -12rpx;
  545. width: 40rpx;
  546. height: 40rpx;
  547. background-color: #ff4757;
  548. border-radius: 50%;
  549. display: flex;
  550. align-items: center;
  551. justify-content: center;
  552. }
  553. }
  554. }
  555. .radio-options {
  556. display: flex;
  557. gap: 80rpx;
  558. .radio-item {
  559. display: flex;
  560. align-items: center;
  561. gap: 16rpx;
  562. .radio-dot {
  563. width: 32rpx;
  564. height: 32rpx;
  565. border: 4rpx solid #ddd;
  566. border-radius: 50%;
  567. position: relative;
  568. &.active {
  569. border-color: $primary-color;
  570. &::after {
  571. content: '';
  572. position: absolute;
  573. top: 50%;
  574. left: 50%;
  575. transform: translate(-50%, -50%);
  576. width: 16rpx;
  577. height: 16rpx;
  578. background-color: $primary-color;
  579. border-radius: 50%;
  580. }
  581. }
  582. }
  583. text {
  584. font-size: 30rpx;
  585. color: $secondary-text-color;
  586. &.active {
  587. color: $primary-color;
  588. }
  589. }
  590. }
  591. }
  592. // 费用表格样式
  593. .cost-table {
  594. margin: 16rpx 0;
  595. border: 2rpx solid #f0f0f0;
  596. border-radius: 8rpx;
  597. overflow: hidden;
  598. .table-header {
  599. display: flex;
  600. background-color: #f8f8f8;
  601. .header-cell {
  602. flex: 1;
  603. padding: 24rpx 16rpx;
  604. font-size: 28rpx;
  605. font-weight: bold;
  606. color: $primary-text-color;
  607. text-align: left;
  608. border-right: 2rpx solid #f0f0f0;
  609. &:first-child {
  610. flex: 2;
  611. text-align: left;
  612. }
  613. &:last-child {
  614. border-right: none;
  615. }
  616. }
  617. }
  618. .table-row {
  619. display: flex;
  620. border-top: 2rpx solid #f0f0f0;
  621. .cell-input {
  622. flex: 1;
  623. padding: 8rpx;
  624. border-right: 2rpx solid #f0f0f0;
  625. &:first-child {
  626. flex: 2;
  627. }
  628. &:last-child {
  629. border-right: none;
  630. }
  631. }
  632. .cell-action {
  633. width: 120rpx;
  634. display: flex;
  635. align-items: center;
  636. justify-content: center;
  637. gap: 8rpx;
  638. padding: 8rpx;
  639. .action-btn {
  640. width: 36rpx;
  641. height: 36rpx;
  642. padding: 4rpx;
  643. border-radius: 50%;
  644. display: flex;
  645. align-items: center;
  646. justify-content: center;
  647. &.add-btn {
  648. background-color: #1a9c10;
  649. }
  650. &.delete-btn {
  651. background-color: $primary-color;
  652. }
  653. }
  654. }
  655. }
  656. }
  657. // 立即维修按钮
  658. .repair-btn-container {
  659. margin-top: 40rpx;
  660. padding-top: 32rpx;
  661. border-top: 2rpx solid #f0f0f0;
  662. }
  663. }
  664. </style>