index.uvue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. <template>
  2. <!-- <uni-navbar-lite :showLeft=false title="首页"></uni-navbar-lite> -->
  3. <view class="page-container">
  4. <view>
  5. <text class="page-header"> 首页 </text>
  6. </view>
  7. <!-- 快捷功能卡片 -->
  8. <view class="quick-cards">
  9. <view v-for="(item, index) in quickFunctions" :key="index" class="quick-card" @click="handleQuickClick(item)">
  10. <image class="quick-card-bg" :src="item.bgImage" mode="aspectFill"></image>
  11. <view class="quick-card-content">
  12. <text class="quick-card-title">{{ item.title }}</text>
  13. </view>
  14. <view class="management-badge" v-if="item.badge > 0">
  15. <text class="badge-text">{{item.badge}}</text>
  16. </view>
  17. </view>
  18. </view>
  19. <view class="receive-list-container">
  20. <!-- 待签收 -->
  21. <view class="section-title-container">
  22. <view class="section-header">
  23. <text class="section-title">待签收</text>
  24. <text class="view-all" @click="navigateToOut" v-if="receiveListData.length > 0">查看全部</text>
  25. </view>
  26. </view>
  27. <scroll-view class="receive-list" scroll-y="true">
  28. <template v-if="receiveListData.length > 0">
  29. <view
  30. v-for="(item, index) in receiveListData"
  31. :key="index"
  32. class="list-item"
  33. >
  34. <view class="item-container">
  35. <view class="item-header">
  36. <text class="item-title">{{ getItemName(item) }}</text>
  37. <text class="item-type">{{ getItemTypeName(item) }}</text>
  38. </view>
  39. <view class="item-info">
  40. <view class="info-row">
  41. <view class="info-item">
  42. <text class="info-label">数量</text>
  43. <text class="info-value">{{ getQuantity(item) }} {{ getMeasureName(item) }}</text>
  44. </view>
  45. <view class="info-item">
  46. <text class="info-label">状态</text>
  47. <text class="info-value status-pending">待签收</text>
  48. </view>
  49. </view>
  50. </view>
  51. <view class="item-footer">
  52. <button class="receive-btn" @click="handleSignReceive(item)">签收</button>
  53. </view>
  54. </view>
  55. </view>
  56. </template>
  57. <template v-else>
  58. <view class="empty-tips">
  59. <text class="empty-text">暂无待签收物料</text>
  60. </view>
  61. </template>
  62. </scroll-view>
  63. </view>
  64. <!-- 底部 TabBar -->
  65. <custom-tabbar :current="0" />
  66. </view>
  67. </template>
  68. <script setup lang="uts">
  69. import { ref, onMounted, onUnmounted } from 'vue'
  70. import { getUserInfo } from '../../utils/storage'
  71. import { getPendingReceiveLines, signReceiveLine, getProductSalseList } from '../../api/out/index'
  72. type QuickFunction = {
  73. id: number
  74. title: string
  75. bgImage: string
  76. icon: string
  77. path: string
  78. badge:number
  79. }
  80. const receiveListData = ref<UTSJSONObject[]>([])
  81. const receiveLoading = ref<boolean>(false)
  82. let currentUserId: string = ''
  83. let refreshTimer: number | null = null
  84. let timerLock = false
  85. // 快捷功能列表
  86. const quickFunctions = ref<QuickFunction[]>([
  87. {
  88. id: 1,
  89. title: '物料申请',
  90. bgImage: '/static/images/workbench/3.png',
  91. icon: '',
  92. path: '/pages/apply/index',
  93. badge: 0
  94. },
  95. {
  96. id: 2,
  97. title: '出库单',
  98. bgImage: '/static/images/workbench/2.png',
  99. icon: '',
  100. path: '/pages/out/index',
  101. badge: 0
  102. },
  103. {
  104. id: 3,
  105. title: '系统物料',
  106. bgImage: '/static/images/workbench/4.png',
  107. icon: '',
  108. path: '/pages/item/index',
  109. badge: 0
  110. }
  111. ])
  112. // 快捷功能点击
  113. const handleQuickClick = (item: QuickFunction): void => {
  114. if (item.path.length > 0) {
  115. uni.navigateTo({
  116. url: item.path,
  117. fail: (err: any) => {
  118. console.log('导航失败', err)
  119. }
  120. })
  121. } else {
  122. uni.showToast({
  123. title: item.title,
  124. icon: 'none'
  125. })
  126. }
  127. }
  128. // 跳转到申请单列表
  129. const navigateToApply = (): void => {
  130. // 跳转到申请单列表
  131. uni.navigateTo({
  132. url: '/pages/apply/index'
  133. })
  134. }
  135. // 加载待签收列表
  136. const loadReceiveList = (): void => {
  137. if (currentUserId.length === 0) return
  138. receiveLoading.value = true
  139. // 查询待签收物料,最多显示10条
  140. getPendingReceiveLines(1, 10, currentUserId).then((res: any) => {
  141. const result = res as UTSJSONObject
  142. const rows = result['rows']
  143. if (rows != null) {
  144. const data = rows as UTSJSONObject[]
  145. receiveListData.value = data
  146. } else {
  147. receiveListData.value = []
  148. }
  149. receiveLoading.value = false
  150. }).catch((e) => {
  151. console.error('加载待签收列表失败:', e)
  152. receiveListData.value = []
  153. receiveLoading.value = false
  154. })
  155. }
  156. // 获取物料名称
  157. const getItemName = (item: any): string => {
  158. if (item == null) return ''
  159. const jsonItem = item as UTSJSONObject
  160. const val = jsonItem['itemName']
  161. return val != null ? val.toString() : ''
  162. }
  163. // 获取物料分类
  164. const getItemTypeName = (item: any): string => {
  165. if (item == null) return ''
  166. const jsonItem = item as UTSJSONObject
  167. const val = jsonItem['itemTypeName']
  168. return val != null ? val.toString() : ''
  169. }
  170. // 获取数量
  171. const getQuantity = (item: any): string => {
  172. if (item == null) return '0'
  173. const jsonItem = item as UTSJSONObject
  174. const val = jsonItem['quantitySalse']
  175. return val != null ? val.toString() : '0'
  176. }
  177. // 获取单位
  178. const getMeasureName = (item: any): string => {
  179. if (item == null) return ''
  180. const jsonItem = item as UTSJSONObject
  181. const val = jsonItem['measureName']
  182. return val != null ? val.toString() : ''
  183. }
  184. // 处理签收
  185. const handleSignReceive = (item: any): void => {
  186. if (item == null) return
  187. const jsonItem = item as UTSJSONObject
  188. const lineId = jsonItem['lineId']
  189. if (lineId == null) {
  190. uni.showToast({ title: '明细ID不存在', icon: 'none' })
  191. return
  192. }
  193. uni.showModal({
  194. title: '提示',
  195. content: '确认签收该物料?',
  196. success: (res) => {
  197. if (res.confirm) {
  198. signReceiveLine(lineId.toString()).then(() => {
  199. uni.showToast({ title: '签收成功', icon: 'success' })
  200. // 重新加载待签收列表
  201. loadReceiveList()
  202. }).catch((e) => {
  203. const error = e as UTSError
  204. const errMsg = error?.message
  205. uni.showToast({ title: errMsg.toString(), icon: 'none' })
  206. })
  207. }
  208. }
  209. })
  210. }
  211. // 跳转到出库单列表
  212. const navigateToOut = (): void => {
  213. uni.navigateTo({
  214. url: '/pages/out/index'
  215. })
  216. }
  217. // 用户名
  218. const userName = ref<string>('用户')
  219. // 停止定时刷新
  220. const stopRefreshTimer = (): void => {
  221. if (timerLock && refreshTimer != null) {
  222. clearInterval(refreshTimer as number)
  223. refreshTimer = null
  224. timerLock = false
  225. }
  226. }
  227. // 加载出库单 badge 数量
  228. const loadOutstockBadge = (): void => {
  229. if (currentUserId.length === 0) return
  230. // 查询待签收的出库单
  231. getProductSalseList(1, 100, '', currentUserId, 'PENDING').then((res: any) => {
  232. const result = res as UTSJSONObject
  233. const rows = result['rows']
  234. if (rows != null) {
  235. const data = rows as UTSJSONObject[]
  236. // 统计待签收数量大于0的出库单个数
  237. const pendingCount = data.filter((item: UTSJSONObject) => {
  238. const unsignedCount = item['unsignedCount']
  239. return unsignedCount != null && parseInt(unsignedCount.toString()) > 0
  240. }).length
  241. // 更新快捷功能中的出库单 badge
  242. quickFunctions.value.forEach((functionItem) => {
  243. if (functionItem.id === 2) {
  244. functionItem.badge = pendingCount
  245. }
  246. })
  247. }
  248. }).catch((e) => {
  249. console.error('加载出库单 badge 失败:', e)
  250. })
  251. }
  252. // 启动定时刷新
  253. const startRefreshTimer = (): void => {
  254. stopRefreshTimer() // 先清除已有的定时器
  255. timerLock = true
  256. refreshTimer = setInterval(() => {
  257. if (currentUserId.length > 0) {
  258. loadReceiveList()
  259. loadOutstockBadge()
  260. }
  261. }, 5000) // 5秒刷新一次
  262. }
  263. // 初始化
  264. onMounted(() => {
  265. const userInfo = getUserInfo()
  266. if (userInfo != null) {
  267. const realName = userInfo['nickName'] as string | null
  268. userName.value = realName ?? '用户'
  269. const userId = userInfo['userId']
  270. currentUserId = userId != null ? userId.toString() : ''
  271. // 加载待签收列表
  272. loadReceiveList()
  273. // 加载出库单 badge 数量
  274. loadOutstockBadge()
  275. // 启动定时刷新,每5秒刷新一次
  276. startRefreshTimer()
  277. }
  278. })
  279. // 组件卸载时清除定时器
  280. onUnmounted(() => {
  281. stopRefreshTimer()
  282. })
  283. </script>
  284. <style lang="scss">
  285. .page-container {
  286. position: relative;
  287. flex: 1;
  288. padding-top: calc(env(safe-area-inset-top) + 20rpx);
  289. padding-bottom: 140rpx; /* 为底部标签栏留出更多空间 */
  290. padding-left: 20rpx;
  291. padding-right: 20rpx;
  292. background-color: #e8f0f9;
  293. display: flex;
  294. flex-direction: column;
  295. box-sizing: border-box;
  296. }
  297. .bg-image {
  298. position: absolute;
  299. top: 0;
  300. left: 0;
  301. width: 100%;
  302. height: 100%;
  303. z-index: 0;
  304. }
  305. .bg-color {
  306. position: absolute;
  307. top: 0;
  308. left: 0;
  309. width: 100%;
  310. height: 100%;
  311. z-index: -1;
  312. background: #71a6e4;
  313. }
  314. .page-content {
  315. position: relative;
  316. flex: 1;
  317. margin-bottom: 150rpx;
  318. padding: 20rpx;
  319. z-index: 10;
  320. }
  321. .header {
  322. flex-direction: row;
  323. height: 40px;
  324. &-title {
  325. font-size: 40rpx;
  326. color: #ffffff;
  327. font-weight: bold;
  328. margin-bottom: 15rpx;
  329. }
  330. &-greeting {
  331. font-size: 28rpx;
  332. color: rgba(255, 255, 255, 0.9);
  333. }
  334. }
  335. .section-title {
  336. font-size: 32rpx;
  337. color: #333333;
  338. font-weight: bold;
  339. margin-bottom: 20rpx;
  340. }
  341. .page-header{
  342. font-size: 32rpx;
  343. color: #333333;
  344. font-weight: bold;
  345. padding-top:20px;
  346. padding-bottom: 10px;
  347. }
  348. .quick-cards {
  349. flex-direction: row;
  350. flex-wrap: wrap;
  351. justify-content: space-between;
  352. margin-bottom: 20rpx;
  353. .quick-card {
  354. position: relative;
  355. width: 212rpx;
  356. height: 120rpx;
  357. margin-bottom: 20rpx;
  358. overflow: hidden;
  359. border-radius: 16rpx;
  360. &-bg {
  361. position: absolute;
  362. top: 0;
  363. left: 0;
  364. width: 226rpx;
  365. height: 140rpx;
  366. z-index: 0;
  367. }
  368. &-content {
  369. position: relative;
  370. z-index: 1;
  371. padding: 20rpx;
  372. height: 140rpx;
  373. justify-content: flex-start;
  374. }
  375. &-title {
  376. font-size: 28rpx;
  377. color: #333333;
  378. font-weight: bold;
  379. }
  380. .management-badge {
  381. position: absolute;
  382. top: 20rpx;
  383. right: 20rpx;
  384. width: 32rpx;
  385. height: 32rpx;
  386. background-color: #ff3b30;
  387. border-radius: 16rpx;
  388. justify-content: center;
  389. align-items: center;
  390. .badge-text {
  391. font-size: 20rpx;
  392. color: #ffffff;
  393. line-height: 20rpx;
  394. }
  395. }
  396. }
  397. }
  398. .receive-list-container {
  399. flex: 1;
  400. margin-bottom: 20rpx;
  401. display: flex;
  402. flex-direction: column;
  403. }
  404. .section-title-container {
  405. // margin-top: 20rpx;
  406. padding: 20rpx 30rpx;
  407. .section-header {
  408. flex-direction: row;
  409. justify-content: space-between;
  410. align-items: center;
  411. .section-title {
  412. font-size: 32rpx;
  413. color: #333333;
  414. font-weight: bold;
  415. // margin-bottom: 20rpx;
  416. }
  417. .view-all {
  418. font-size: 28rpx;
  419. color: #165dff;
  420. }
  421. }
  422. }
  423. .receive-list {
  424. flex: 1;
  425. margin: 0 30rpx;
  426. border-radius: 16rpx;
  427. background-color: #ffffff;
  428. padding: 10rpx;
  429. box-sizing: border-box;
  430. /* 隐藏滚动条但保留滚动功能 */
  431. &::-webkit-scrollbar {
  432. width: 0;
  433. height: 0;
  434. }
  435. /* 为了兼容性,添加一些额外样式 */
  436. scrollbar-width: none;
  437. -ms-overflow-style: none;
  438. }
  439. .list-item {
  440. margin-bottom: 16rpx;
  441. background-color: #ffffff;
  442. border-radius: 12rpx;
  443. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  444. .item-container {
  445. padding: 20rpx;
  446. }
  447. .item-header {
  448. flex-direction: row;
  449. align-items: center;
  450. justify-content: space-between;
  451. margin-bottom: 12rpx;
  452. .location-icon {
  453. width: 24rpx;
  454. height: 24rpx;
  455. margin-right: 6rpx;
  456. }
  457. .item-title {
  458. flex: 1;
  459. font-size: 28rpx;
  460. color: #333333;
  461. font-weight: bold;
  462. }
  463. .detail-link {
  464. font-size: 26rpx;
  465. color: #999999;
  466. }
  467. .item-type {
  468. font-size: 22rpx;
  469. color: #007aff;
  470. background-color: #e6f7ff;
  471. padding: 6rpx 12rpx;
  472. border-radius: 6rpx;
  473. white-space: nowrap;
  474. }
  475. }
  476. .item-address {
  477. font-size: 24rpx;
  478. color: #999999;
  479. margin-bottom: 12rpx;
  480. line-height: 36rpx;
  481. }
  482. .item-info {
  483. padding: 16rpx;
  484. background-color: #f8f9fa;
  485. border-radius: 6rpx;
  486. .info-row {
  487. flex-direction: row;
  488. margin-bottom: 12rpx;
  489. &:last-child {
  490. margin-bottom: 0;
  491. }
  492. .info-item {
  493. flex: 1;
  494. flex-direction: row;
  495. align-items: center;
  496. .info-label {
  497. font-size: 28rpx;
  498. color: #666666;
  499. margin-right: 6rpx;
  500. white-space: nowrap;
  501. }
  502. .info-value {
  503. flex: 1;
  504. font-size: 28rpx;
  505. color: #333333;
  506. &.status-pending {
  507. color: #faad14;
  508. }
  509. }
  510. }
  511. }
  512. }
  513. .item-footer {
  514. margin-top: 12rpx;
  515. display: flex;
  516. justify-content: flex-end;
  517. }
  518. .receive-btn {
  519. padding: 0rpx 20rpx;
  520. background-color: #007aff;
  521. color: #ffffff;
  522. font-size: 28rpx;
  523. border-radius: 6rpx;
  524. border: none;
  525. }
  526. }
  527. .empty-tips {
  528. padding: 30rpx;
  529. justify-content: center;
  530. align-items: center;
  531. .empty-text {
  532. font-size: 26rpx;
  533. color: #999999;
  534. }
  535. }
  536. </style>