publishTask.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <template>
  2. <view class="app-container pb-24">
  3. <TopBar title="发布任务" show-back />
  4. <view class="px-4 py-4">
  5. <!-- 板块一:项目基本信息 -->
  6. <view class="card p-4 mb-4">
  7. <view class="detail-section-title mb-4">
  8. <view class="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center mr-3">
  9. <text class="uni-icons uniui-folder-add-filled text-primary"></text>
  10. </view>
  11. <text>项目基本信息</text>
  12. </view>
  13. <!-- 项目选择提示 -->
  14. <view class="bg-blue-50 rounded-lg px-3 py-2 mb-4 flex items-center">
  15. <text class="uni-icons uniui-info-filled text-blue-500 mr-2 text-sm"></text>
  16. <text class="text-blue-600 text-sm">请从项目库选择或新增项目</text>
  17. </view>
  18. <!-- 项目选择 -->
  19. <view class="form-group">
  20. <text class="form-label">
  21. <text class="text-red-500">*</text>选择项目
  22. </text>
  23. <picker mode="selector" :range="projectNames" :value="projectIndex" @change="onProjectSelectChange">
  24. <view class="form-select flex justify-between items-center">
  25. <text :class="selectedProject ? 'text-gray-800' : 'text-gray-400'">{{ selectedProject?.name || '请选择项目' }}</text>
  26. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  27. </view>
  28. </picker>
  29. </view>
  30. <!-- 已选项目信息 -->
  31. <view v-if="selectedProject" class="bg-green-50 rounded-xl p-3 mb-4">
  32. <view class="flex items-center justify-between mb-3">
  33. <text class="text-green-600 text-sm font-medium">已选择:{{ selectedProject.name }}</text>
  34. <text class="text-green-500 text-xs" @click="resetProjectSelection">重新选择</text>
  35. </view>
  36. <view class="gap-y-2">
  37. <view class="flex justify-between text-sm">
  38. <text class="text-gray-500">合作类型:</text>
  39. <text class="text-gray-700">{{ selectedProject.type }}</text>
  40. </view>
  41. <view class="flex justify-between text-sm">
  42. <text class="text-gray-500">设施类型:</text>
  43. <text class="text-gray-700">油污管、化粪池</text>
  44. </view>
  45. <view class="flex justify-between text-sm">
  46. <text class="text-gray-500">项目地址:</text>
  47. <text class="text-gray-700 flex-1 text-right">{{ selectedProject.address }}</text>
  48. </view>
  49. </view>
  50. </view>
  51. <!-- 联系人列表 -->
  52. <view class="form-group mb-0">
  53. <text class="form-label">联系人</text>
  54. <view v-if="selectedProject" class="gap-y-2 mb-2">
  55. <view
  56. v-for="(contact, index) in selectedProject.contacts"
  57. :key="index"
  58. class="bg-gray-50 rounded-lg p-3 flex justify-between items-center"
  59. >
  60. <view>
  61. <text class="text-sm text-gray-800">{{ contact.name }}</text>
  62. <text class="text-xs text-gray-400 ml-2">{{ contact.role }}</text>
  63. </view>
  64. <text class="text-sm text-primary">{{ contact.phone }}</text>
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- 板块二:服务信息 -->
  70. <view class="card p-4">
  71. <view class="detail-section-title mb-4">
  72. <view class="w-8 h-8 rounded-lg bg-purple-100 flex items-center justify-center mr-3">
  73. <text class="uni-icons uniui-gear-filled text-purple-500"></text>
  74. </view>
  75. <text>服务信息</text>
  76. </view>
  77. <!-- 服务时间 -->
  78. <view class="form-group">
  79. <text class="form-label">
  80. <text class="text-red-500">*</text>服务时间
  81. </text>
  82. <view class="flex gap-2">
  83. <picker mode="date" :value="serviceDate" @change="onServiceDateChange" class="flex-1">
  84. <view class="form-select flex justify-between items-center">
  85. <text :class="serviceDate ? 'text-gray-800' : 'text-gray-400'">{{ serviceDate || '选择日期' }}</text>
  86. <text class="uni-icons uniui-calendar text-gray-400"></text>
  87. </view>
  88. </picker>
  89. <picker mode="time" :value="serviceTime" @change="onServiceTimeChange" style="flex: 0.6;">
  90. <view class="form-select flex justify-between items-center">
  91. <text :class="serviceTime ? 'text-gray-800' : 'text-gray-400'">{{ serviceTime || '选择时间' }}</text>
  92. <text class="uni-icons uniui-clock text-gray-400"></text>
  93. </view>
  94. </picker>
  95. </view>
  96. </view>
  97. <!-- 故障位置 -->
  98. <view class="form-group">
  99. <text class="form-label">
  100. <text class="text-red-500">*</text>故障位置
  101. </text>
  102. <uni-easyinput class="form-input" v-model="faultLocation" placeholder="请输入故障位置" />
  103. </view>
  104. <!-- 地理位置 -->
  105. <view class="form-group">
  106. <text class="form-label">地理位置</text>
  107. <view
  108. class="bg-gray-100 rounded-xl h-24 flex items-center justify-center border border-gray-200"
  109. @click="showToast('地图定位功能开发中')"
  110. >
  111. <view class="text-center">
  112. <text class="uni-icons uniui-location text-gray-400 text-2xl block mb-1"></text>
  113. <text class="text-gray-400 text-xs">点击获取当前位置</text>
  114. </view>
  115. </view>
  116. </view>
  117. <!-- 故障类型 -->
  118. <view class="form-group">
  119. <text class="form-label">
  120. <text class="text-red-500">*</text>故障类型
  121. </text>
  122. <view class="flex flex-wrap gap-2">
  123. <view
  124. v-for="type in faultTypes"
  125. :key="type"
  126. class="px-3 py-1 rounded-full text-sm border"
  127. :class="selectedFaultTypes.includes(type) ? 'bg-primary text-white border-primary' : 'bg-gray-100 text-gray-600 border-gray-200'"
  128. @click="toggleFaultType(type)"
  129. >
  130. {{ type }}
  131. </view>
  132. </view>
  133. </view>
  134. <!-- 客户原话 -->
  135. <view class="form-group">
  136. <text class="form-label">客户原话</text>
  137. <textarea class="form-textarea" v-model="customerWords" placeholder="请记录客户原话描述..."></textarea>
  138. </view>
  139. <!-- 故障描述 -->
  140. <view class="form-group">
  141. <text class="form-label">
  142. <text class="text-red-500">*</text>故障描述
  143. </text>
  144. <textarea class="form-textarea" v-model="faultDesc" placeholder="请详细描述故障情况..."></textarea>
  145. </view>
  146. <!-- 施工类型 -->
  147. <view class="form-group">
  148. <text class="form-label">
  149. <text class="text-red-500">*</text>施工类型
  150. </text>
  151. <picker mode="selector" :range="serviceTypes" :value="serviceTypeIndex" @change="onServiceTypeChange">
  152. <view class="form-select flex justify-between items-center">
  153. <text :class="serviceType ? 'text-gray-800' : 'text-gray-400'">{{ serviceType || '请选择施工类型' }}</text>
  154. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  155. </view>
  156. </picker>
  157. </view>
  158. <!-- 施工设施 -->
  159. <view class="form-group">
  160. <text class="form-label">
  161. <text class="text-red-500">*</text>施工设施
  162. </text>
  163. <picker mode="selector" :range="facilityOptions" :value="facilityIndex" @change="onFacilityChange">
  164. <view class="form-select flex justify-between items-center">
  165. <text :class="facility ? 'text-gray-800' : 'text-gray-400'">{{ facility || '请选择施工设施' }}</text>
  166. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  167. </view>
  168. </picker>
  169. </view>
  170. <!-- 服务方式 -->
  171. <view class="form-group">
  172. <text class="form-label">
  173. <text class="text-red-500">*</text>服务方式
  174. </text>
  175. <picker mode="selector" :range="serviceModes" :value="serviceModeIndex" @change="onServiceModeChange">
  176. <view class="form-select flex justify-between items-center">
  177. <text :class="serviceMode ? 'text-gray-800' : 'text-gray-400'">{{ serviceMode || '请选择服务方式' }}</text>
  178. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  179. </view>
  180. </picker>
  181. </view>
  182. <!-- 建议出车 -->
  183. <view class="form-group">
  184. <text class="form-label">
  185. <text class="text-red-500">*</text>建议出车
  186. </text>
  187. <picker mode="selector" :range="vehicleOptions" :value="vehicleIndex" @change="onVehicleChange">
  188. <view class="form-select flex justify-between items-center">
  189. <text :class="vehicle ? 'text-gray-800' : 'text-gray-400'">{{ vehicle || '请选择车辆' }}</text>
  190. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  191. </view>
  192. </picker>
  193. </view>
  194. <!-- 取水距离 -->
  195. <view class="form-group">
  196. <text class="form-label">取水距离</text>
  197. <picker mode="selector" :range="waterOptions" :value="waterIndex" @change="onWaterChange">
  198. <view class="form-select flex justify-between items-center">
  199. <text :class="waterSource ? 'text-gray-800' : 'text-gray-400'">{{ waterSource || '请选择' }}</text>
  200. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  201. </view>
  202. </picker>
  203. </view>
  204. <!-- 施工助力信息 -->
  205. <view class="form-group">
  206. <text class="form-label">施工助力信息</text>
  207. <textarea class="form-textarea" v-model="constructionHelp" placeholder="如地库限高、需搭脚手架、登高作业等"></textarea>
  208. </view>
  209. <!-- 预计作业量 -->
  210. <view class="form-group">
  211. <text class="form-label">预计作业量</text>
  212. <uni-easyinput class="form-input" v-model="estimatedQuantity" placeholder="如 2个堵点" />
  213. </view>
  214. <!-- 预计工时 -->
  215. <view class="form-group">
  216. <text class="form-label">预计工时</text>
  217. <uni-easyinput class="form-input" v-model="estimatedHours" placeholder="如 2小时" />
  218. </view>
  219. <!-- 验收要求 -->
  220. <view class="form-group">
  221. <text class="form-label">验收要求</text>
  222. <uni-easyinput class="form-input" v-model="acceptanceReq" placeholder="如 正常走水" />
  223. </view>
  224. <!-- 验收方式 -->
  225. <view class="form-group">
  226. <text class="form-label">
  227. <text class="text-red-500">*</text>验收方式
  228. </text>
  229. <picker mode="selector" :range="acceptTypes" :value="acceptTypeIndex" @change="onAcceptTypeChange">
  230. <view class="form-select flex justify-between items-center">
  231. <text :class="acceptType ? 'text-gray-800' : 'text-gray-400'">{{ acceptType || '请选择验收方式' }}</text>
  232. <text class="uni-icons uniui-arrowdown text-gray-400"></text>
  233. </view>
  234. </picker>
  235. </view>
  236. <!-- 故障图片 -->
  237. <view class="form-group">
  238. <text class="form-label">故障图片/视频</text>
  239. <PhotoUploader v-model="photos" :max-count="9" />
  240. </view>
  241. </view>
  242. <!-- 提交按钮 -->
  243. <view class="pt-4 pb-6">
  244. <button class="w-full py-4 btn-primary rounded-2xl text-base font-semibold" @click="submitTask">
  245. 发布任务
  246. </button>
  247. </view>
  248. </view>
  249. </view>
  250. </template>
  251. <script setup lang="ts">
  252. import { ref, computed } from 'vue'
  253. import { onLoad } from '@dcloudio/uni-app'
  254. import TopBar from '@/components/common/TopBar.vue'
  255. import PhotoUploader from '@/components/common/PhotoUploader.vue'
  256. import { useProjectStore } from '@/stores/project'
  257. import { useTaskStore } from '@/stores/task'
  258. import { showToast, generateId, navigateTo } from '@/utils'
  259. import type { Task } from '@/types'
  260. const projectStore = useProjectStore()
  261. const taskStore = useTaskStore()
  262. const projectNames = computed(() => [...projectStore.projects.map(p => p.name), '+ 新增项目'])
  263. const projects = computed(() => projectStore.projects)
  264. const selectedProject = ref<typeof projects.value[0] | null>(null)
  265. const projectIndex = computed(() => {
  266. if (!selectedProject.value) return -1
  267. return projects.value.findIndex(p => p.id === selectedProject.value?.id)
  268. })
  269. const serviceDate = ref('')
  270. const serviceTime = ref('')
  271. const faultLocation = ref('')
  272. const faultTypes = ['堵塞', '满溢', '排水慢', '异味', '其他']
  273. const selectedFaultTypes = ref<string[]>([])
  274. const customerWords = ref('')
  275. const faultDesc = ref('')
  276. const serviceTypes = ['疏通', '清淤', '排查']
  277. const serviceType = ref('')
  278. const serviceTypeIndex = computed(() => serviceTypes.indexOf(serviceType.value))
  279. const facilityOptions = ['PVC', '直抽直排', '油污', '排污', '油污管', '化粪池']
  280. const facility = ref('')
  281. const facilityIndex = computed(() => facilityOptions.indexOf(facility.value))
  282. const serviceModes = ['应急', '月计划']
  283. const serviceMode = ref('')
  284. const serviceModeIndex = computed(() => serviceModes.indexOf(serviceMode.value))
  285. const vehicleOptions = ['疏通车', '吸污车', '高压清洗车']
  286. const vehicle = ref('')
  287. const vehicleIndex = computed(() => vehicleOptions.indexOf(vehicle.value))
  288. const waterOptions = ['有消防水', '无消防水']
  289. const waterSource = ref('')
  290. const waterIndex = computed(() => waterOptions.indexOf(waterSource.value))
  291. const constructionHelp = ref('')
  292. const estimatedQuantity = ref('')
  293. const estimatedHours = ref('')
  294. const acceptanceReq = ref('')
  295. const acceptTypes = ['签署验收计量单', '累计自动计费']
  296. const acceptType = ref('')
  297. const acceptTypeIndex = computed(() => acceptTypes.indexOf(acceptType.value))
  298. const photos = ref<string[]>([])
  299. onLoad((query) => {
  300. if (query?.projectId) {
  301. const project = projectStore.projects.find(p => p.id === query.projectId)
  302. if (project) {
  303. selectedProject.value = project
  304. }
  305. }
  306. })
  307. function onProjectSelectChange(e: any) {
  308. const index = e.detail.value
  309. if (index >= projects.value.length) {
  310. navigateTo('/subPackages/pages-common/addProject')
  311. return
  312. }
  313. selectedProject.value = projects.value[index]
  314. }
  315. function resetProjectSelection() {
  316. selectedProject.value = null
  317. }
  318. function toggleFaultType(type: string) {
  319. const idx = selectedFaultTypes.value.indexOf(type)
  320. if (idx > -1) {
  321. selectedFaultTypes.value.splice(idx, 1)
  322. } else {
  323. selectedFaultTypes.value.push(type)
  324. }
  325. }
  326. function onServiceDateChange(e: any) {
  327. serviceDate.value = e.detail.value
  328. }
  329. function onServiceTimeChange(e: any) {
  330. serviceTime.value = e.detail.value
  331. }
  332. function onServiceTypeChange(e: any) {
  333. serviceType.value = serviceTypes[e.detail.value]
  334. }
  335. function onFacilityChange(e: any) {
  336. facility.value = facilityOptions[e.detail.value]
  337. }
  338. function onServiceModeChange(e: any) {
  339. serviceMode.value = serviceModes[e.detail.value]
  340. }
  341. function onVehicleChange(e: any) {
  342. vehicle.value = vehicleOptions[e.detail.value]
  343. }
  344. function onWaterChange(e: any) {
  345. waterSource.value = waterOptions[e.detail.value]
  346. }
  347. function onAcceptTypeChange(e: any) {
  348. acceptType.value = acceptTypes[e.detail.value]
  349. }
  350. function validateForm(): boolean {
  351. if (!selectedProject.value) {
  352. showToast('请选择项目')
  353. return false
  354. }
  355. if (!serviceDate.value) {
  356. showToast('请选择服务时间')
  357. return false
  358. }
  359. if (!faultLocation.value.trim()) {
  360. showToast('请输入故障位置')
  361. return false
  362. }
  363. if (selectedFaultTypes.value.length === 0) {
  364. showToast('请选择故障类型')
  365. return false
  366. }
  367. if (!faultDesc.value.trim()) {
  368. showToast('请输入故障描述')
  369. return false
  370. }
  371. if (!serviceType.value) {
  372. showToast('请选择施工类型')
  373. return false
  374. }
  375. if (!facility.value) {
  376. showToast('请选择施工设施')
  377. return false
  378. }
  379. if (!serviceMode.value) {
  380. showToast('请选择服务方式')
  381. return false
  382. }
  383. if (!vehicle.value) {
  384. showToast('请选择建议出车')
  385. return false
  386. }
  387. if (!acceptType.value) {
  388. showToast('请选择验收方式')
  389. return false
  390. }
  391. return true
  392. }
  393. function submitTask() {
  394. if (!validateForm()) return
  395. const newTask: Task = {
  396. id: generateId('t'),
  397. projectId: selectedProject.value!.id,
  398. projectName: selectedProject.value!.name,
  399. type: serviceMode.value === '应急' ? 'emergency' : 'regular',
  400. status: 'pending',
  401. urgent: serviceMode.value === '应急' ? 'high' : 'normal',
  402. createTime: new Date().toISOString(),
  403. timeline: [
  404. {
  405. time: new Date().toISOString(),
  406. action: '任务创建',
  407. operator: '张销售',
  408. role: 'sales'
  409. }
  410. ]
  411. }
  412. taskStore.tasks.push(newTask)
  413. showToast({ title: '任务发布成功', icon: 'success' })
  414. setTimeout(() => {
  415. navigateTo('/pages/task/task')
  416. }, 1500)
  417. }
  418. </script>