index.uvue 16 KB

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