index.uvue 16 KB

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