taskDetail.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <template>
  2. <view class="app-container pb-20">
  3. <TopBar :title="'任务详情'" show-back />
  4. <view v-if="taskStore.loading" class="py-20 text-center">
  5. <text class="text-gray-400">加载中...</text>
  6. </view>
  7. <view v-else-if="!task" class="px-4 py-6">
  8. <EmptyState message="任务不存在" />
  9. </view>
  10. <view v-else class="px-4 py-4 gap-y-4">
  11. <!-- 项目信息 -->
  12. <uni-card title="项目信息" :is-shadow="true">
  13. <view class="flex justify-between items-start mb-3">
  14. <view class="flex-1 min-w-0 mr-3">
  15. <text class="text-base font-semibold text-gray-800 block">{{ task.projectName }}</text>
  16. </view>
  17. <StatusTag :status="task.status" />
  18. </view>
  19. <uni-list :border="false">
  20. <uni-list-item title="地址" :note="task.address || '暂无地址'" icon="uniui-location-filled" />
  21. <uni-list-item title="客户" :note="task.customerName || '暂无客户'" icon="uniui-person-filled" />
  22. <uni-list-item
  23. title="电话"
  24. :note="task.customerPhone || '暂无电话'"
  25. icon="uniui-phone-filled"
  26. @click="callPhone(task.customerPhone)"
  27. />
  28. <uni-list-item v-if="task.coopType" title="合作类型" :note="task.coopType" icon="uniui-folder-add-filled" />
  29. <uni-list-item v-if="task.serviceMode" title="服务方式" :note="task.serviceMode" icon="uniui-calendar-filled" />
  30. </uni-list>
  31. </uni-card>
  32. <!-- 施工信息 -->
  33. <uni-card title="施工信息" :is-shadow="true">
  34. <uni-list :border="false">
  35. <uni-list-item title="施工类型" :note="task.serviceType || '未填写'" icon="uniui-tools" />
  36. <uni-list-item title="设施类型" :note="task.facilityType || '未填写'" icon="uniui-flag-filled" />
  37. <uni-list-item title="服务时间" :note="serviceTimeText" icon="uniui-clock-filled" />
  38. <uni-list-item v-if="task.team" title="施工班组" :note="task.team" icon="uniui-personadd-filled" />
  39. <uni-list-item v-if="task.vehicleNo" title="车辆" :note="task.vehicleNo" icon="uniui-cart-filled" />
  40. <uni-list-item v-if="task.acceptType" title="验收方式" :note="task.acceptType" icon="uniui-checkmarkempty" />
  41. </uni-list>
  42. </uni-card>
  43. <!-- 故障信息 -->
  44. <uni-card v-if="task.faultLocation || task.faultDesc" title="故障信息" :is-shadow="true">
  45. <uni-list :border="false">
  46. <uni-list-item v-if="task.faultLocation" title="故障位置" :note="task.faultLocation" icon="uniui-location-filled" />
  47. <uni-list-item v-if="task.faultDesc" title="故障描述" :note="task.faultDesc" icon="uniui-compose" />
  48. </uni-list>
  49. </uni-card>
  50. <!-- 任务进度 -->
  51. <uni-card title="任务进度" :is-shadow="true">
  52. <view class="flex items-center justify-between px-2">
  53. <view
  54. v-for="(step, index) in progressSteps"
  55. :key="step.key"
  56. class="flex-1 flex flex-col items-center relative"
  57. >
  58. <view
  59. class="w-8 h-8 rounded-full flex items-center justify-center text-xs mb-1"
  60. :class="step.active ? 'bg-primary text-white' : 'bg-gray-100 text-gray-400'"
  61. >
  62. <text class="uni-icons" :class="step.icon"></text>
  63. </view>
  64. <text class="text-xs" :class="step.active ? 'text-primary font-medium' : 'text-gray-400'">{{ step.label }}</text>
  65. </view>
  66. </view>
  67. </uni-card>
  68. <!-- 施工照片 -->
  69. <uni-card title="施工照片" icon="uniui-camera-filled" :is-shadow="true">
  70. <ImageUploader v-model="taskImages" tip="上传施工过程照片" />
  71. </uni-card>
  72. <!-- 地图位置 -->
  73. <uni-card title="项目位置" icon="uniui-location-filled" :is-shadow="true">
  74. <MapView
  75. :latitude="taskLatitude"
  76. :longitude="taskLongitude"
  77. :title="task.projectName"
  78. :address="task.address"
  79. :height="180"
  80. />
  81. </uni-card>
  82. <!-- 操作按钮 -->
  83. <uni-card :is-shadow="true">
  84. <view class="grid grid-cols-2 gap-3">
  85. <button
  86. v-if="canUrge"
  87. class="py-3 bg-warning text-white rounded-xl text-sm flex items-center justify-center"
  88. @click="urgeTask"
  89. >
  90. <text class="uni-icons uniui-notification-filled mr-1"></text>
  91. 催单
  92. </button>
  93. <button
  94. v-if="canCancel"
  95. class="py-3 bg-danger text-white rounded-xl text-sm flex items-center justify-center"
  96. @click="cancelTask"
  97. >
  98. <text class="uni-icons uniui-closeempty mr-1"></text>
  99. 取消任务
  100. </button>
  101. <button
  102. v-if="canViewAcceptance"
  103. class="py-3 bg-success text-white rounded-xl text-sm flex items-center justify-center"
  104. @click="viewAcceptance"
  105. >
  106. <text class="uni-icons uniui-checkmarkempty mr-1"></text>
  107. 查看验收单
  108. </button>
  109. <button
  110. v-if="canRebook"
  111. class="py-3 bg-primary text-white rounded-xl text-sm flex items-center justify-center"
  112. @click="rebookTask"
  113. >
  114. <text class="uni-icons uniui-calendar-filled mr-1"></text>
  115. 再次预约
  116. </button>
  117. </view>
  118. </uni-card>
  119. </view>
  120. </view>
  121. </template>
  122. <script setup lang="ts">
  123. import { ref, computed, onMounted, onUnmounted } from 'vue'
  124. import { useTaskStore } from '../../stores/task'
  125. import { useAuthStore } from '../../stores/auth'
  126. import TopBar from '../../components/common/TopBar.vue'
  127. import StatusTag from '../../components/common/StatusTag.vue'
  128. import EmptyState from '../../components/common/EmptyState.vue'
  129. import UniCard from '../../components/uni-card/uni-card.vue'
  130. import UniList from '../../components/uni-list/uni-list.vue'
  131. import UniListItem from '../../components/uni-list/uni-list-item.vue'
  132. import { urgeTask as apiUrgeTask, cancelTask as apiCancelTask } from '../../api/task'
  133. import ImageUploader from '../../components/common/ImageUploader.vue'
  134. import MapView from '../../components/common/MapView.vue'
  135. const taskStore = useTaskStore()
  136. const authStore = useAuthStore()
  137. const task = computed(() => taskStore.currentTask)
  138. const taskImages = ref<string[]>([])
  139. const taskLatitude = computed(() => task.value?.location?.lat || 39.9)
  140. const taskLongitude = computed(() => task.value?.location?.lng || 116.4)
  141. const serviceTimeText = computed(() => {
  142. if (!task.value) return '待定'
  143. const date = task.value.serviceDate || task.value.scheduleDate || ''
  144. const time = task.value.serviceTime || task.value.scheduleTime || ''
  145. return date || time ? `${date} ${time}`.trim() : '待定'
  146. })
  147. const progressSteps = computed(() => {
  148. const status = task.value?.status || 'pending'
  149. return [
  150. { key: 'publish', label: '发布', icon: 'uniui-compose', active: true },
  151. { key: 'dispatch', label: '调度确认', icon: 'uniui-flag-filled', active: ['confirmed', 'scheduled', 'departed', 'arrived', 'constructing', 'inspecting', 'completed'].includes(status) },
  152. { key: 'constructing', label: '施工中', icon: 'uniui-gear-filled', active: ['constructing', 'inspecting', 'completed'].includes(status) },
  153. { key: 'completed', label: '完成', icon: 'uniui-checkmarkempty', active: status === 'completed' },
  154. ]
  155. })
  156. const canUrge = computed(() => ['pending', 'confirmed', 'scheduled'].includes(task.value?.status || ''))
  157. const canCancel = computed(() => ['pending', 'confirmed', 'scheduled'].includes(task.value?.status || ''))
  158. const canViewAcceptance = computed(() => task.value?.status === 'completed')
  159. const canRebook = computed(() => task.value?.status === 'completed')
  160. function callPhone(phone: string | undefined) {
  161. if (!phone) return
  162. uni.makePhoneCall({ phoneNumber: phone })
  163. }
  164. async function urgeTask() {
  165. if (!task.value) return
  166. try {
  167. await apiUrgeTask(
  168. Number(task.value.id),
  169. '客户催单',
  170. Number(authStore.user?.id || 0),
  171. authStore.user?.name || '销售'
  172. )
  173. uni.showToast({ title: '催单成功', icon: 'success' })
  174. } catch (error) {
  175. uni.showToast({ title: '催单失败', icon: 'none' })
  176. }
  177. }
  178. async function cancelTask() {
  179. if (!task.value) return
  180. uni.showModal({
  181. title: '确认取消',
  182. content: '确定要取消此任务吗?',
  183. success: async (res) => {
  184. if (res.confirm) {
  185. try {
  186. await apiCancelTask(
  187. Number(task.value!.id),
  188. '销售取消',
  189. Number(authStore.user?.id || 0),
  190. authStore.user?.name || '销售'
  191. )
  192. uni.showToast({ title: '取消成功', icon: 'success' })
  193. await taskStore.fetchTaskDetail(Number(task.value!.id))
  194. } catch (error) {
  195. uni.showToast({ title: '取消失败', icon: 'none' })
  196. }
  197. }
  198. },
  199. })
  200. }
  201. function viewAcceptance() {
  202. uni.showToast({ title: '验收单功能开发中', icon: 'none' })
  203. }
  204. function rebookTask() {
  205. uni.navigateTo({ url: '/subPackages/pages-common/publishTask' })
  206. }
  207. onMounted(() => {
  208. if (!task.value) {
  209. const pages = getCurrentPages()
  210. const currentPage = pages[pages.length - 1] as any
  211. const taskId = currentPage.options?.id
  212. if (taskId) {
  213. taskStore.fetchTaskDetail(Number(taskId))
  214. }
  215. }
  216. })
  217. onUnmounted(() => {
  218. taskStore.currentTask = null
  219. })
  220. </script>