list.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <template>
  2. <view class="db-page">
  3. <view class="db-header" :style="safeAreaStyle">
  4. <view class="db-header__nav">
  5. <view class="db-header__left">
  6. <text class="db-header__back" @click="goBack">←</text>
  7. <text class="db-header__title">计划管理</text>
  8. </view>
  9. <view /><!-- 胶囊占位 -->
  10. </view>
  11. </view>
  12. <view class="db-search-wrap">
  13. <db-search
  14. v-model="keyword"
  15. placeholder="搜索往来单位 / 计划号"
  16. @change="onKeyword"
  17. @search="reload"
  18. @clear="onClear"
  19. />
  20. </view>
  21. <view class="db-filter-bar">
  22. <view
  23. v-for="item in dateFilters"
  24. :key="item.value"
  25. class="db-filter-chip"
  26. :class="{ 'db-filter-chip--active': dateFilter === item.value }"
  27. @click="setFilter(item.value)"
  28. >
  29. {{ item.label }}
  30. </view>
  31. </view>
  32. <view class="db-list">
  33. <view v-if="!loading && !list.length" class="db-empty">暂无数据</view>
  34. <view v-for="item in list" :key="item.id" class="db-card" @click="viewItem(item)">
  35. <view class="db-card__head">
  36. <text class="db-card__title">{{ item.planNo || '未填计划号' }}</text>
  37. <db-tag :type="item.businessType === '入库' ? 'success' : 'warning'">
  38. {{ item.businessType }}
  39. </db-tag>
  40. </view>
  41. <view class="db-card__row">
  42. <text class="db-card__label">往来单位</text>
  43. <text>{{ item.customerName || '未填' }}</text>
  44. </view>
  45. <view class="db-card__row">
  46. <text class="db-card__label">产品</text>
  47. <text>{{ item.product || '未填' }}</text>
  48. </view>
  49. <view class="db-card__row">
  50. <text class="db-card__label">计划量</text>
  51. <text>{{ item.plannedQuantity ?? '--' }} 吨</text>
  52. </view>
  53. <view class="db-card__row">
  54. <text class="db-card__label">状态</text>
  55. <db-tag :type="statusType(execStatus(item))">
  56. {{ execStatus(item) }}
  57. </db-tag>
  58. </view>
  59. <view class="db-card__foot" @click.stop>
  60. <db-btn size="small" type="primary" plain @click="viewRelated(item)">查看</db-btn>
  61. <db-btn size="small" type="primary" plain @click="editItem(item)">编辑</db-btn>
  62. <db-btn size="small" type="primary" @click="genInventory(item)">生成出入库单</db-btn>
  63. <db-btn size="small" type="danger" plain @click="removeItem(item)">删除</db-btn>
  64. </view>
  65. </view>
  66. <view v-if="loading" class="db-empty">加载中...</view>
  67. </view>
  68. <db-popup :show="relatedOpen" height="75vh" @close="relatedOpen = false" @update:show="(v) => (relatedOpen = v)">
  69. <view style="padding: 16px; max-height: 75vh; overflow: auto">
  70. <view class="db-header__title" style="margin-bottom: 12px">关联数据</view>
  71. <view v-if="relatedLoading" class="db-empty">加载中...</view>
  72. <view v-else-if="!mergedRelated.length" class="db-empty">暂无关联数据</view>
  73. <view v-for="(row, idx) in mergedRelated" :key="idx" class="db-card">
  74. <db-tag :type="row.type === 'inventory' ? 'primary' : 'warning'">
  75. {{ row.type === 'inventory' ? '出入库单' : '地磅单' }}
  76. </db-tag>
  77. <view class="db-card__row" style="margin-top: 8px">
  78. <text class="db-card__label">业务</text>
  79. <text>{{ row.businessType || '--' }}</text>
  80. </view>
  81. <view class="db-card__row">
  82. <text class="db-card__label">产品</text>
  83. <text>{{ row.product || row.cargoName || '--' }}</text>
  84. </view>
  85. <view class="db-card__row">
  86. <text class="db-card__label">车牌</text>
  87. <text>{{ row.vehicleNo || '--' }}</text>
  88. </view>
  89. </view>
  90. </view>
  91. </db-popup>
  92. </view>
  93. </template>
  94. <script setup>
  95. import { ref, computed } from 'vue'
  96. import { onShow } from '@dcloudio/uni-app'
  97. import { PAGE_PATHS } from '@/config/api'
  98. import { getSafeAreaStyle } from '@/utils/safeArea'
  99. import { planApi } from '@/pages/db/api/db'
  100. import { formatYmd, addDays, weekRange } from '@/pages/db/utils/dbDate'
  101. import DbSearch from '@/components/db-ui/db-search.vue'
  102. import DbTag from '@/components/db-ui/db-tag.vue'
  103. import DbBtn from '@/components/db-ui/db-btn.vue'
  104. import DbPopup from '@/components/db-ui/db-popup.vue'
  105. const safeAreaStyle = getSafeAreaStyle()
  106. const keyword = ref('')
  107. const dateFilter = ref('all')
  108. const loading = ref(false)
  109. const list = ref([])
  110. const relatedOpen = ref(false)
  111. const relatedLoading = ref(false)
  112. const relatedData = ref({ inventories: [], weighings: [] })
  113. let debounce = null
  114. const dateFilters = [
  115. { label: '全部', value: 'all' },
  116. { label: '今日', value: 'today' },
  117. { label: '明日', value: 'tomorrow' },
  118. { label: '昨日', value: 'yesterday' },
  119. { label: '本周', value: 'week' }
  120. ]
  121. const mergedRelated = computed(() => {
  122. const items = []
  123. ;(relatedData.value.inventories || []).forEach((inv) => {
  124. items.push({ ...inv, type: 'inventory', sortTime: inv.createdAt || inv.date || '' })
  125. })
  126. ;(relatedData.value.weighings || []).forEach((w) => {
  127. items.push({ ...w, type: 'weighing', sortTime: w.grossWeightTime || w.createdAt || '' })
  128. })
  129. return items.sort((a, b) => {
  130. const ta = a.sortTime ? new Date(a.sortTime).getTime() : 0
  131. const tb = b.sortTime ? new Date(b.sortTime).getTime() : 0
  132. if (!ta && !tb) return 0
  133. if (!ta) return 1
  134. if (!tb) return -1
  135. return tb - ta
  136. })
  137. })
  138. function execStatus(row) {
  139. if (!row.executionStatus) return row.businessType === '入库' ? '待入库' : '待出库'
  140. if (row.businessType === '入库') {
  141. if (['待出库', '出库中', '已出库'].includes(row.executionStatus)) {
  142. return (row.executedQuantity || 0) > 0 ? '入库中' : '待入库'
  143. }
  144. } else if (['待入库', '入库中', '已入库'].includes(row.executionStatus)) {
  145. return (row.executedQuantity || 0) > 0 ? '出库中' : '待出库'
  146. }
  147. return row.executionStatus
  148. }
  149. function statusType(status) {
  150. if (status === '已入库' || status === '已出库') return 'success'
  151. if (status === '入库中' || status === '出库中') return 'primary'
  152. if (status === '待入库' || status === '待出库') return 'warning'
  153. return 'default'
  154. }
  155. function buildParams() {
  156. const params = { q: keyword.value || undefined }
  157. const today = new Date()
  158. if (dateFilter.value === 'today') {
  159. const d = formatYmd(today)
  160. params.createdFrom = d
  161. params.createdTo = d
  162. } else if (dateFilter.value === 'tomorrow') {
  163. const t = formatYmd(addDays(today, 1))
  164. params.createdFrom = t
  165. params.createdTo = t
  166. } else if (dateFilter.value === 'yesterday') {
  167. const y = formatYmd(addDays(today, -1))
  168. params.createdFrom = y
  169. params.createdTo = y
  170. } else if (dateFilter.value === 'week') {
  171. const w = weekRange(today)
  172. params.createdFrom = w.from
  173. params.createdTo = w.to
  174. }
  175. return params
  176. }
  177. async function reload() {
  178. loading.value = true
  179. try {
  180. list.value = (await planApi.list(buildParams())) || []
  181. } catch (e) {
  182. list.value = []
  183. } finally {
  184. loading.value = false
  185. }
  186. }
  187. function onKeyword(v) {
  188. keyword.value = typeof v === 'string' ? v : keyword.value
  189. if (debounce) clearTimeout(debounce)
  190. debounce = setTimeout(reload, 300)
  191. }
  192. function onClear() {
  193. keyword.value = ''
  194. reload()
  195. }
  196. function setFilter(v) {
  197. if (dateFilter.value === v) return
  198. dateFilter.value = v
  199. reload()
  200. }
  201. function goBack() {
  202. uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
  203. }
  204. function editItem(item) {
  205. uni.navigateTo({ url: `${PAGE_PATHS.dbPlansDetail}?id=${item.id}&mode=edit` })
  206. }
  207. function viewItem(item) {
  208. uni.navigateTo({ url: `${PAGE_PATHS.dbPlansDetail}?id=${item.id}&mode=view` })
  209. }
  210. async function viewRelated(item) {
  211. relatedOpen.value = true
  212. relatedLoading.value = true
  213. try {
  214. relatedData.value = (await planApi.getRelated(item.id)) || { inventories: [], weighings: [] }
  215. } catch (e) {
  216. relatedOpen.value = false
  217. } finally {
  218. relatedLoading.value = false
  219. }
  220. }
  221. function genInventory(item) {
  222. const q = [
  223. `planNo=${encodeURIComponent(item.planNo || '')}`,
  224. `product=${encodeURIComponent(item.product || '')}`,
  225. `productId=${encodeURIComponent(item.productId || '')}`,
  226. `businessType=${encodeURIComponent(item.businessType || '')}`,
  227. `customerName=${encodeURIComponent(item.customerName || '')}`,
  228. `purchasePlanId=${encodeURIComponent(item.id)}`,
  229. 'from=plan',
  230. 'mode=create'
  231. ].join('&')
  232. uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?${q}` })
  233. }
  234. function removeItem(item) {
  235. uni.showModal({
  236. title: '确认删除',
  237. content: '确定要删除这个计划吗?',
  238. success: async (res) => {
  239. if (!res.confirm) return
  240. try {
  241. await planApi.delete(item.id)
  242. uni.showToast({ title: '删除成功', icon: 'success' })
  243. reload()
  244. } catch (e) {
  245. // toast by request
  246. }
  247. }
  248. })
  249. }
  250. onShow(reload)
  251. </script>
  252. <style lang="scss">
  253. @import '@/styles/db-theme.scss';
  254. </style>