created.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <template>
  2. <view class="common-page" style="padding: 0;">
  3. <view class="form-page">
  4. <u-form ref="formRef" :model="form" :rules="rules" labelPosition="left" labelWidth="85px">
  5. <u-form-item label="快递单号:" prop="waybillCode" required>
  6. <template v-if="isReadonly && form.waybillCode">
  7. <text class="readonly-text">{{ form.waybillCode }}</text>
  8. </template>
  9. <u-input v-else v-model="form.waybillCode" placeholder="请输入快递单号" clearable></u-input>
  10. </u-form-item>
  11. <!-- <u-form-item label="订单编号:" prop="orderId" v-if="form.orderId || isReadonly">
  12. <template v-if="isReadonly && form.orderId">
  13. <text class="clickable-text" @click="goToOrderDetail">{{ form.orderId }}</text>
  14. </template>
  15. <u-input v-else v-model="form.orderId" placeholder="请输入订单编号" clearable></u-input>
  16. </u-form-item> -->
  17. <u-form-item label="承运商:" prop="carrier" required>
  18. <view class="picker-field" @click="openCarrierPicker" @tap="openCarrierPicker">
  19. <u-input v-model="carrierText" readonly suffixIcon="arrow-down" placeholder="请选择"></u-input>
  20. </view>
  21. </u-form-item>
  22. <u-form-item label="验货状态:" prop="verifyStatus" required>
  23. <view class="picker-field" @tap="showVerifyPicker = true">
  24. <u-input v-model="verifyText" readonly suffixIcon="arrow-down" placeholder="请选择"></u-input>
  25. </view>
  26. </u-form-item>
  27. <u-form-item label="任务类型:" prop="taskType" required>
  28. <view class="picker-field" @tap="showTaskTypePicker = true">
  29. <u-input v-model="taskTypeText" readonly suffixIcon="arrow-down" placeholder="请选择"></u-input>
  30. </view>
  31. </u-form-item>
  32. <u-form-item label="任务详情:" prop="taskDetail" required>
  33. <u-textarea v-model="form.taskDetail" placeholder="请输入" :height="120"
  34. :autoHeight="true"></u-textarea>
  35. </u-form-item>
  36. <u-form-item label="上传图片:" prop="images" required>
  37. <cy-upload :filename="form.images" @update:filename="form.images = $event" :multiple="true" :maxCount="9" />
  38. </u-form-item>
  39. <view class="divider"></view>
  40. <u-form-item label="指派给:" prop="assignTo">
  41. <view class="picker-field" @tap="openAssignPicker">
  42. <u-input v-model="assignText" readonly suffixIcon="arrow-down" placeholder="请选择 (非必填)"></u-input>
  43. </view>
  44. </u-form-item>
  45. </u-form>
  46. </view>
  47. <u-picker :show="showCarrierPicker" :columns="[carrierOptions]" title="选择承运商" @confirm="onPickCarrier"
  48. @cancel="showCarrierPicker = false" @close="showCarrierPicker = false"></u-picker>
  49. <u-picker :show="showVerifyPicker" :columns="[verifyOptions]" title="选择验货状态" @confirm="onPickVerify"
  50. @cancel="showVerifyPicker = false" @close="showVerifyPicker = false"></u-picker>
  51. <u-picker :show="showTaskTypePicker" :columns="[taskTypeOptions]" title="选择任务类型" @confirm="onPickTaskType"
  52. @cancel="showTaskTypePicker = false" @close="showTaskTypePicker = false"></u-picker>
  53. <!-- 指派给选择器 - 使用 popup + checkbox 实现多选 -->
  54. <u-popup v-model:show="showAssignPicker" mode="bottom" :round="20">
  55. <view class="assign-picker-content">
  56. <view class="assign-header">
  57. <text class="title">选择指派人</text>
  58. <view class="actions">
  59. <text class="cancel-btn" @click="showAssignPicker = false">取消</text>
  60. <text class="confirm-btn" @click="confirmAssign">确定</text>
  61. </view>
  62. </view>
  63. <view class="assign-body">
  64. <view
  65. v-for="(item, index) in assignOptions"
  66. :key="index"
  67. class="assign-item"
  68. @click="toggleAssign(index)"
  69. >
  70. <text class="item-text">{{ item.text }}</text>
  71. <view
  72. class="custom-checkbox"
  73. :class="{ checked: tempAssignIndexes.includes(index) }"
  74. >
  75. </view>
  76. </view>
  77. </view>
  78. </view>
  79. </u-popup>
  80. <view class="fixed-bottom">
  81. <u-button type="warning" size="large" @click="onCancel">取消</u-button>
  82. <u-button type="primary" size="large" :loading="submitting" @click="onSubmit">提交</u-button>
  83. </view>
  84. </view>
  85. </template>
  86. <script setup>
  87. import { ref } from 'vue'
  88. import { onLoad } from '@dcloudio/uni-app'
  89. import CyUpload from '@/components/cy-upload/index.vue'
  90. const formRef = ref(null)
  91. const submitting = ref(false)
  92. const form = ref({
  93. waybillCode: '',
  94. orderId: '',
  95. expressType: '',
  96. verifyStatus: '',
  97. taskType: '',
  98. taskDetail: '',
  99. images: [],
  100. assignTo: []
  101. })
  102. const rules = {
  103. waybillCode: [{ required: true, message: '请输入快递单号' }],
  104. expressType: [{ required: true, message: '请选择承运商' }],
  105. verifyStatus: [{ required: true, message: '请选择验货状态' }],
  106. taskType: [{ required: true, message: '请选择任务类型' }],
  107. taskDetail: [{ required: true, message: '请输入任务详情' }],
  108. assignTo: [{ required: false, message: '请选择指派对象' }]
  109. }
  110. const carrierOptions = ref([])
  111. const verifyOptions = [
  112. { text: '已验货', value: 1 },
  113. { text: '未验货', value: 0 }
  114. ]
  115. const taskTypeOptions = ref([])
  116. const assignOptions = ref([{ text: '默认(不指派)', value: '' }])
  117. // 获取快递公司字典
  118. const getCarrierOptions = async () => {
  119. try {
  120. const res = await uni.$u.http.get('/system/dict/data/type/shop_order_express_name')
  121. if (res.code === 200 && res.data) {
  122. carrierOptions.value = res.data.map(item => ({
  123. text: item.dictLabel,
  124. value: item.dictValue
  125. }))
  126. if (form.value.expressType !== '' && form.value.expressType !== undefined && form.value.expressType !== null) {
  127. const matchedCarrier = carrierOptions.value.find(item => String(item.value) === String(form.value.expressType))
  128. if (matchedCarrier) carrierText.value = matchedCarrier.text
  129. }
  130. }
  131. } catch (e) {
  132. console.error('获取快递公司失败', e)
  133. }
  134. }
  135. // 获取任务类型字典
  136. const getTaskTypeOptions = async () => {
  137. try {
  138. const res = await uni.$u.http.get('/system/dict/data/type/sell_task_type')
  139. if (res.code === 200 && res.data) {
  140. taskTypeOptions.value = res.data.map(item => ({
  141. text: item.dictLabel,
  142. value: parseInt(item.dictValue)
  143. }))
  144. }
  145. } catch (e) {
  146. console.error('获取任务类型失败', e)
  147. }
  148. }
  149. // 获取指派人列表
  150. const getAssignOptions = async () => {
  151. try {
  152. const res = await uni.$u.http.get('/system/user/list', {
  153. params: {
  154. pageSize: 1000
  155. }
  156. })
  157. if (res.code === 200 && res.rows) {
  158. assignOptions.value = res.rows.map(item => ({
  159. text: item.nickName,
  160. value: item.userId
  161. }))
  162. }
  163. } catch (e) {
  164. console.error('获取指派人失败', e)
  165. }
  166. }
  167. const showCarrierPicker = ref(false)
  168. const showVerifyPicker = ref(false)
  169. const showTaskTypePicker = ref(false)
  170. const showAssignPicker = ref(false)
  171. // 临时存储选中的索引数组
  172. const tempAssignIndexes = ref([])
  173. const carrierText = ref('')
  174. const verifyText = ref('')
  175. const taskTypeText = ref('')
  176. const assignText = ref('')
  177. const onPickCarrier = (e) => {
  178. const cell = e.value?.[0]
  179. form.value.expressType = cell?.value || ''
  180. carrierText.value = cell?.text || ''
  181. showCarrierPicker.value = false
  182. console.log('选择承运商:', form.value.expressType, carrierText.value)
  183. }
  184. const onPickVerify = (e) => {
  185. const cell = e.value?.[0]
  186. form.value.verifyStatus = cell?.value || ''
  187. verifyText.value = cell?.text || ''
  188. showVerifyPicker.value = false
  189. console.log('选择验货状态:', form.value.verifyStatus, verifyText.value)
  190. }
  191. const onPickTaskType = (e) => {
  192. const cell = e.value?.[0]
  193. form.value.taskType = cell?.value || ''
  194. taskTypeText.value = cell?.text || ''
  195. showTaskTypePicker.value = false
  196. console.log('选择任务类型:', form.value.taskType, taskTypeText.value)
  197. }
  198. // 打开指派人选择器时,初始化临时选中状态
  199. const openCarrierPicker = () => {
  200. if (isEdit.value) return
  201. showCarrierPicker.value = true
  202. }
  203. const openAssignPicker = () => {
  204. if (isEdit.value) return
  205. // 根据当前已选择的值,初始化 tempAssignIndexes
  206. tempAssignIndexes.value = []
  207. if (form.value.assignTo && form.value.assignTo.length > 0) {
  208. assignOptions.value.forEach((item, index) => {
  209. if (form.value.assignTo.includes(item.value)) {
  210. tempAssignIndexes.value.push(index)
  211. }
  212. })
  213. }
  214. showAssignPicker.value = true
  215. console.log('打开选择器,已选中索引:', tempAssignIndexes.value)
  216. }
  217. const onPickAssign = (e) => {
  218. // 这个方法不再使用,改用 toggleAssign 和 confirmAssign
  219. }
  220. // 切换选中状态
  221. const toggleAssign = (index) => {
  222. const idx = tempAssignIndexes.value.indexOf(index)
  223. if (idx > -1) {
  224. tempAssignIndexes.value.splice(idx, 1)
  225. } else {
  226. tempAssignIndexes.value.push(index)
  227. }
  228. }
  229. // 确认选择
  230. const confirmAssign = () => {
  231. const selectedList = []
  232. tempAssignIndexes.value.forEach(index => {
  233. selectedList.push(assignOptions.value[index])
  234. })
  235. form.value.assignTo = selectedList.map(item => item.value)
  236. assignText.value = selectedList.map(item => item.text).join(',')
  237. showAssignPicker.value = false
  238. console.log('选择指派人:', form.value.assignTo, assignText.value)
  239. }
  240. const onCancel = () => {
  241. uni.navigateBack()
  242. }
  243. const goToOrderDetail = () => {
  244. if (!form.value.orderId) return
  245. uni.navigateTo({
  246. url: `/pages/index/detail/index?id=${form.value.orderId}`
  247. })
  248. }
  249. const workOrderId = ref('')
  250. const isEdit = ref(false)
  251. const isReadonly = ref(false)
  252. const onLoadDetail = async (id) => {
  253. try {
  254. const res = await uni.$u.http.get('/app/workOrder/getWorkOrderDetail', {
  255. params: { workOrderId: id }
  256. })
  257. if (res.code === 200 && res.data) {
  258. const data = res.data
  259. form.value.waybillCode = data.waybillCode || ''
  260. form.value.orderId = data.orderId || ''
  261. form.value.taskDetail = data.deatil || data.taskDetail || ''
  262. form.value.images = data.imgInfo?.imgUrlList.length ? data.imgInfo.imgUrlList : []
  263. // 加载任务类型字典
  264. await getTaskTypeOptions()
  265. // 回显任务类型
  266. if (data.taskType) {
  267. form.value.taskType = data.taskType
  268. const matchedType = taskTypeOptions.value.find(item => item.value === data.taskType)
  269. if (matchedType) {
  270. taskTypeText.value = matchedType.text
  271. }
  272. }
  273. // 回显指派人(多选)
  274. if (data.handleUsers && data.handleUsers.length > 0) {
  275. form.value.assignTo = data.handleUsers.map(user => user.userId)
  276. assignText.value = data.handleUsers.map(user => user.userName).join(',')
  277. }
  278. if (data.inspectionStatus !== undefined && data.inspectionStatus !== null) {
  279. form.value.verifyStatus = data.inspectionStatus
  280. const matchedVerify = verifyOptions.find(item => item.value === data.inspectionStatus)
  281. if (matchedVerify) {
  282. verifyText.value = matchedVerify.text
  283. }
  284. }
  285. // 回显承运商
  286. if (data.expressType) {
  287. form.value.expressType = data.expressType
  288. // 从字典中查找对应的文本
  289. const matchedCarrier = carrierOptions.value.find(item => {
  290. return item.value === data.expressType
  291. })
  292. if (matchedCarrier) {
  293. carrierText.value = matchedCarrier.text
  294. }
  295. }
  296. }
  297. } catch (e) {
  298. console.error(e)
  299. }
  300. }
  301. const onSubmit = async () => {
  302. if (submitting.value) return
  303. try {
  304. await formRef.value?.validate?.()
  305. } catch (e) {
  306. return
  307. }
  308. submitting.value = true
  309. const payload = {
  310. taskType: form.value.taskType || 9,
  311. inspectionStatus: form.value.verifyStatus ?? 0,
  312. deatil: form.value.taskDetail,
  313. expressType: form.value.expressType || 0,
  314. waybillCode: form.value.waybillCode,
  315. orderId: form.value.orderId || '',
  316. type: 1, // 工单类型:1-卖书
  317. imgInfo: form.value.images.length > 0 ? form.value.images : [],
  318. handleUsers: form.value.assignTo.length > 0 ? form.value.assignTo : []
  319. }
  320. if (isEdit.value) {
  321. payload.id = workOrderId.value
  322. payload.updateType = 2
  323. }
  324. const apiUrl = isEdit.value ? '/app/workOrder/update' : '/app/workOrder/createWorkOrder'
  325. try {
  326. const res = await uni.$u.http.post(apiUrl, payload)
  327. if (res?.code === 200) {
  328. uni.$u.ttsModule?.speak?.('提交成功')
  329. uni.$u.toast('提交成功')
  330. if (isEdit.value) {
  331. uni.navigateBack()
  332. } else {
  333. uni.redirectTo({ url: '/pages/index/work-order/mall' })
  334. }
  335. } else {
  336. uni.$u.toast(res?.msg || '提交失败')
  337. }
  338. } catch (err) {
  339. uni.$u.toast(err?.data?.msg || err?.msg || '网络错误')
  340. } finally {
  341. submitting.value = false
  342. }
  343. }
  344. onLoad((options) => {
  345. form.value.waybillCode = options?.waybillCode ? decodeURIComponent(options.waybillCode) : ''
  346. form.value.orderId = options?.orderId ? decodeURIComponent(options.orderId) : ''
  347. if (options?.readonly == 1) {
  348. isReadonly.value = true
  349. // 尝试回显承运商
  350. if (options?.expressType !== undefined && options?.expressType !== null) {
  351. form.value.expressType = parseInt(options.expressType)
  352. // 从字典中查找对应的文本
  353. const matchedCarrier = carrierOptions.value.find(item => String(item.value) === String(options.expressType))
  354. if (matchedCarrier) {
  355. carrierText.value = matchedCarrier.text
  356. } else {
  357. // 字典异步加载后再回显
  358. setTimeout(() => {
  359. const c = carrierOptions.value.find(item => String(item.value) === String(options.expressType))
  360. if (c) carrierText.value = c.text
  361. }, 300)
  362. }
  363. }
  364. }
  365. if (options?.mode === 'edit' && options?.workOrderId) {
  366. isEdit.value = true
  367. isReadonly.value = true
  368. workOrderId.value = options.workOrderId
  369. uni.setNavigationBarTitle({ title: '编辑工单' })
  370. onLoadDetail(options.workOrderId)
  371. } else {
  372. uni.setNavigationBarTitle({ title: '提交工单' })
  373. }
  374. // 加载字典数据
  375. getCarrierOptions()
  376. getTaskTypeOptions()
  377. getAssignOptions()
  378. })
  379. </script>
  380. <style lang="scss" scoped>
  381. .form-page {
  382. padding: 24rpx 24rpx 140rpx;
  383. background: #ffffff;
  384. }
  385. .divider {
  386. height: 16rpx;
  387. background: #f4f5f5;
  388. margin: 20rpx 0;
  389. }
  390. // 指派人多选选择器样式
  391. .assign-picker-content {
  392. padding: 0 24rpx;
  393. background: #fff;
  394. border-radius: 20rpx 20rpx 0 0;
  395. .assign-header {
  396. display: flex;
  397. justify-content: space-between;
  398. align-items: center;
  399. padding: 24rpx 0;
  400. border-bottom: 1px solid #eee;
  401. .title {
  402. font-size: 32rpx;
  403. font-weight: bold;
  404. color: #333;
  405. }
  406. .actions {
  407. display: flex;
  408. gap: 24rpx;
  409. .cancel-btn {
  410. font-size: 28rpx;
  411. color: #666;
  412. padding: 8rpx 16rpx;
  413. }
  414. .confirm-btn {
  415. font-size: 28rpx;
  416. color: #2979ff;
  417. font-weight: 500;
  418. padding: 8rpx 16rpx;
  419. }
  420. }
  421. }
  422. .assign-body {
  423. max-height: 600rpx;
  424. overflow-y: auto;
  425. padding: 16rpx 0;
  426. .assign-item {
  427. display: flex;
  428. justify-content: space-between;
  429. align-items: center;
  430. padding: 24rpx 16rpx;
  431. border-bottom: 1px solid #f5f5f5;
  432. &:active {
  433. background: #f8f9fa;
  434. }
  435. .item-text {
  436. font-size: 30rpx;
  437. color: #333;
  438. }
  439. .custom-checkbox {
  440. width: 40rpx;
  441. height: 40rpx;
  442. border: 2px solid #c8c9cc;
  443. border-radius: 8rpx;
  444. position: relative;
  445. transition: all 0.2s;
  446. &.checked {
  447. background: #2979ff;
  448. border-color: #2979ff;
  449. &::after {
  450. content: '✓';
  451. position: absolute;
  452. top: 50%;
  453. left: 50%;
  454. transform: translate(-50%, -50%);
  455. color: #fff;
  456. font-size: 28rpx;
  457. font-weight: bold;
  458. }
  459. }
  460. }
  461. }
  462. }
  463. }
  464. .readonly-text {
  465. font-size: 28rpx;
  466. color: #333;
  467. line-height: 60rpx;
  468. }
  469. .clickable-text {
  470. font-size: 28rpx;
  471. color: #2979ff;
  472. line-height: 60rpx;
  473. }
  474. .picker-field {
  475. width: 100%;
  476. min-height: 60rpx;
  477. }
  478. .picker-field :deep(.u-input),
  479. .picker-field :deep(input) {
  480. pointer-events: none;
  481. }
  482. </style>