list.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. <template>
  2. <view class="task-package-list">
  3. <view class="task-filter-header">
  4. <view class="task-filter-header__search">
  5. <wd-search
  6. v-model="params.keyword"
  7. hide-cancel
  8. placeholder-left
  9. placeholder="搜索任务包名称/编号"
  10. @change="onKeywordChange"
  11. @clear="handleClearSearch"
  12. />
  13. </view>
  14. <view class="task-list-controls">
  15. <view class="task-list-controls__tabs">
  16. <view
  17. v-for="tab in tabs"
  18. :key="tab.value"
  19. class="task-list-controls__tab"
  20. :class="{ 'task-list-controls__tab--active': params.activeTab === tab.value }"
  21. @click="handleTabChange(tab.value)"
  22. >
  23. {{ tab.label }}
  24. <text v-if="tab.value === TaskPackageTab.Available">({{ params.total }})</text>
  25. </view>
  26. </view>
  27. <view class="task-list-controls__date">
  28. <view class="task-list-controls__divider" />
  29. <view v-if="!selectedDate" @click="openDatePicker">
  30. <text class="task-list-controls__date-text">{{ selectedDateText }}</text>
  31. <wd-icon name="caret-down" size="28rpx" />
  32. </view>
  33. <view v-else @click="clearDatePicker">
  34. <text class="task-list-controls__date-text">{{ selectedDateText }}</text>
  35. <wd-icon name="close-circle" size="28rpx" />
  36. </view>
  37. </view>
  38. </view>
  39. </view>
  40. <view v-if="showEmpty" class="task-empty">
  41. <wd-empty icon="no-content" tip="暂无内容" />
  42. </view>
  43. <view v-else class="task-list-content">
  44. <view v-for="item in packageListArr" :key="item.id" class="task-card">
  45. <view class="task-card__header">
  46. <view class="task-card__title">
  47. {{ item.scorePackageName }}{{ item.packageSn ? `(${item.packageSn})` : '' }}
  48. <wd-tag
  49. v-if="getPackageTypeTag(item.packageType1)"
  50. custom-class="task-card__tag"
  51. plain
  52. :color="getPackageTypeTag(item.packageType1)?.color"
  53. :bg-color="getPackageTypeTag(item.packageType1)?.bgColor"
  54. >
  55. {{ getPackageTypeTag(item.packageType1)?.label }}
  56. </wd-tag>
  57. </view>
  58. <wd-button
  59. v-if="params.activeTab === TaskPackageTab.Available"
  60. size="small"
  61. round
  62. custom-class="task-card__button"
  63. @click.stop="handleReceive(item)"
  64. >
  65. 立即领取
  66. </wd-button>
  67. <view v-else class="task-card__status">
  68. <view class="task-card__dot" />
  69. <text>{{ getPackageStatusText(item.packageStatus) }}</text>
  70. </view>
  71. </view>
  72. <view class="task-card__info">
  73. <view>发包企业:{{ item.packageEntName || '-' }}</view>
  74. <view class="task-card__meta">
  75. <text>需求人数:{{ item.userGetNum }}/{{ item.userNum }}</text>
  76. <text>预计积分值:{{ item.score }}分</text>
  77. <text v-if="item.taskNum">任务数:{{ item.taskNum }}个</text>
  78. </view>
  79. <view>发布时间:{{ formatDate(item.createTime) }}</view>
  80. <view>需求描述:{{ item.description || '无' }}</view>
  81. </view>
  82. <view v-if="item.packageStatus === '2'" class="task-card__expired">
  83. <image src="/static/images/common/expired.png" mode="heightFix" />
  84. </view>
  85. </view>
  86. <view class="task-load-status">
  87. <text v-if="finished">没有更多了</text>
  88. </view>
  89. </view>
  90. <!-- 时间选择器 -->
  91. <wd-datetime-picker
  92. v-model:visible="datetimePickerShow"
  93. v-model="datetimePickerValue"
  94. type="year-month"
  95. @confirm="datetimePickerHandleConfirm"
  96. />
  97. <!-- 领取积分包测试提示 -->
  98. <wd-dialog />
  99. <wd-toast />
  100. </view>
  101. </template>
  102. <script setup lang="ts">
  103. import { computed, onBeforeMount, reactive, ref } from 'vue'
  104. import { onReachBottom } from '@dcloudio/uni-app'
  105. import { useDialog, useToast } from '@wot-ui/ui'
  106. import dayjs from 'dayjs'
  107. import { getDictTypeApi } from '@/services/modules/common'
  108. import type { DictItem } from '@/services/modules/common/type'
  109. import { getUserScorePackageListApi, pickupPackageApi } from '@/services/modules/task/list'
  110. import type {
  111. getUserScorePackageListRequest,
  112. TaskPackageItem,
  113. } from '@/services/modules/task/list/type'
  114. const dialog = useDialog()
  115. const toast = useToast()
  116. enum TaskPackageTab {
  117. Available = '1',
  118. Received = '2',
  119. }
  120. const tabs = [
  121. { label: '可领取', value: TaskPackageTab.Available },
  122. { label: '已领取', value: TaskPackageTab.Received },
  123. ] as const
  124. const params = reactive({
  125. keyword: '',
  126. total: 0,
  127. activeTab: TaskPackageTab.Available,
  128. current: 1,
  129. size: 10,
  130. selectedYear: '',
  131. selectedMonth: '',
  132. })
  133. const loading = ref(false)
  134. const finished = ref(false)
  135. const packageListArr = ref<TaskPackageItem[]>([])
  136. let requestId = 0
  137. const selectedDate = computed(() => {
  138. const { selectedYear, selectedMonth } = params
  139. return selectedYear && selectedMonth ? `${selectedYear}-${selectedMonth}` : ''
  140. })
  141. const selectedDateText = computed(() => {
  142. const { selectedYear, selectedMonth } = params
  143. return selectedYear && selectedMonth ? `${selectedYear}年${selectedMonth}月` : '日期'
  144. })
  145. const requestParams = computed<getUserScorePackageListRequest>(() => ({
  146. current: params.current,
  147. size: params.size,
  148. selType: params.activeTab,
  149. selDate: selectedDate.value,
  150. packageName: params.keyword.trim(),
  151. }))
  152. const showEmpty = computed(() => !loading.value && packageListArr.value.length === 0)
  153. const resetList = () => {
  154. params.current = 1
  155. finished.value = false
  156. packageListArr.value = []
  157. }
  158. const refreshTaskPackageList = () => {
  159. resetList()
  160. fetchTaskPackageList()
  161. }
  162. const fetchTaskPackageList = async () => {
  163. if (loading.value || finished.value) return
  164. loading.value = true
  165. const currentRequestId = ++requestId
  166. try {
  167. const res = await getUserScorePackageListApi(requestParams.value)
  168. if (currentRequestId !== requestId) return
  169. if (res.code !== 0) return
  170. const records = res.data?.records ?? []
  171. const total = res.data?.total ?? 0
  172. if (params.activeTab === TaskPackageTab.Available) {
  173. params.total = total
  174. }
  175. packageListArr.value = params.current === 1 ? records : packageListArr.value.concat(records)
  176. finished.value = packageListArr.value.length >= total || records.length < params.size
  177. } finally {
  178. if (currentRequestId === requestId) {
  179. loading.value = false
  180. }
  181. }
  182. }
  183. const loadMoreTaskPackageList = () => {
  184. if (loading.value || finished.value) return
  185. params.current += 1
  186. fetchTaskPackageList()
  187. }
  188. let searchTimer: ReturnType<typeof setTimeout> | null = null
  189. const onKeywordChange = () => {
  190. if (searchTimer) {
  191. clearTimeout(searchTimer)
  192. }
  193. searchTimer = setTimeout(() => {
  194. refreshTaskPackageList()
  195. }, 1500)
  196. }
  197. const handleClearSearch = () => {
  198. params.keyword = ''
  199. refreshTaskPackageList()
  200. }
  201. const handleTabChange = (value: TaskPackageTab) => {
  202. if (params.activeTab === value) return
  203. params.activeTab = value
  204. refreshTaskPackageList()
  205. }
  206. const datetimePickerValue = ref(dayjs().startOf('month').valueOf())
  207. const datetimePickerShow = ref(false)
  208. const openDatePicker = () => {
  209. datetimePickerShow.value = true
  210. }
  211. const datetimePickerHandleConfirm = (e: { value: string }) => {
  212. const d = dayjs(e.value)
  213. params.selectedYear = d.format('YYYY')
  214. params.selectedMonth = d.format('MM')
  215. refreshTaskPackageList()
  216. }
  217. const clearDatePicker = () => {
  218. datetimePickerValue.value = dayjs().startOf('month').valueOf()
  219. params.selectedYear = ''
  220. params.selectedMonth = ''
  221. refreshTaskPackageList()
  222. }
  223. const GET_STATUS = {
  224. UN_GET: {
  225. value: '0',
  226. desc: '未领取',
  227. },
  228. ING: {
  229. value: '1',
  230. desc: '审核中',
  231. },
  232. OK: {
  233. value: '2',
  234. desc: '审核通过',
  235. },
  236. NO: {
  237. value: '3',
  238. desc: '审核不通过',
  239. },
  240. OVER: {
  241. value: '4',
  242. desc: '已终止',
  243. },
  244. } as const
  245. const currentPackage = ref<TaskPackageItem | null>(null)
  246. const hasRequiredQuiz = (item: TaskPackageItem) => {
  247. return Array.isArray(item.quizRelations) && item.quizRelations.length > 0
  248. }
  249. const isUnreceived = (item: TaskPackageItem) => {
  250. return item.status === GET_STATUS.UN_GET.value
  251. }
  252. const openTestValidationDialog = () => {
  253. dialog
  254. .confirm({
  255. title: '提示',
  256. msg: '该积分包需要通过测试才能领取,是否进行测试?',
  257. confirmButtonText: '去测试',
  258. cancelButtonText: '取消',
  259. })
  260. .then(() => {
  261. handleGoTest()
  262. })
  263. .catch(() => {
  264. // 点击取消 / 关闭,不处理
  265. })
  266. }
  267. /**
  268. * 点击立即领取
  269. */
  270. const handleReceive = async (item: TaskPackageItem) => {
  271. currentPackage.value = { ...item }
  272. // 有测试要求,且当前状态不允许直接领取:弹出测试校验提示
  273. if (hasRequiredQuiz(item) && !isUnreceived(item)) {
  274. openTestValidationDialog()
  275. return
  276. }
  277. await submitReceiveAfterTestPassed(item)
  278. }
  279. const submitReceiveAfterTestPassed = async (item: TaskPackageItem) => {
  280. const res = await pickupPackageApi({
  281. pkgId: item.id,
  282. })
  283. if (res.code === 0) {
  284. toast.success('领取成功')
  285. removePackageFromList(item.id)
  286. return
  287. }
  288. if (res.code === 1) {
  289. uni.showModal({
  290. content: res.msg,
  291. confirmText: '去认证',
  292. success: (modalRes) => {
  293. if (modalRes.confirm) {
  294. console.log('认证页面')
  295. }
  296. },
  297. })
  298. }
  299. }
  300. const removePackageFromList = (pkgId: string | number) => {
  301. packageListArr.value = packageListArr.value.filter((item) => item.id !== pkgId)
  302. if (params.activeTab === TaskPackageTab.Available && params.total > 0) {
  303. params.total -= 1
  304. }
  305. }
  306. const handleGoTest = () => {
  307. if (!currentPackage.value) return
  308. // uni.navigateTo({
  309. // url: `/pages/xxx/test/index?id=${currentPackage.value.id}`,
  310. // })
  311. }
  312. const getPackageTypeTag = (type: string | null) => {
  313. const tagMap = {
  314. '1': { label: '积分值', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
  315. '6': { label: '积分值', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
  316. '2': { label: '任务量', color: '#4cd964', bgColor: 'rgba(76, 217, 100, 0.1)' },
  317. '3': { label: '通用', color: '#dd524d', bgColor: 'rgba(221, 82, 77, 0.1)' },
  318. '4': { label: '特定', color: '#dd524d', bgColor: 'rgba(221, 82, 77, 0.1)' },
  319. } as const
  320. return type ? tagMap[type as keyof typeof tagMap] : undefined
  321. }
  322. const packageStatusMap = ref<DictItem[]>([])
  323. const getPackageStatus = async () => {
  324. const res = await getDictTypeApi('package_new_status')
  325. if (res.code === 0) {
  326. packageStatusMap.value = res.data ?? []
  327. }
  328. }
  329. const getPackageStatusText = (status: string | null) => {
  330. const statusItem = packageStatusMap.value.find((item) => item.value === status)
  331. return statusItem ? statusItem.label : '-'
  332. }
  333. const formatDate = (value: string | null) => value?.slice(0, 10) || '-'
  334. onReachBottom(loadMoreTaskPackageList)
  335. onBeforeMount(() => {
  336. fetchTaskPackageList()
  337. getPackageStatus()
  338. })
  339. </script>
  340. <style lang="scss" scoped>
  341. .task-package-list {
  342. min-height: 100vh;
  343. background: #fafafa;
  344. .task-filter-header {
  345. background: #ffffff;
  346. position: fixed;
  347. top: 0;
  348. left: 0;
  349. right: 0;
  350. z-index: 99;
  351. padding-bottom: 16rpx;
  352. .task-filter-header__search {
  353. margin-bottom: 6rpx;
  354. }
  355. .task-list-controls {
  356. display: flex;
  357. align-items: center;
  358. height: 88rpx;
  359. padding: 0 18rpx;
  360. .task-list-controls__tabs {
  361. display: flex;
  362. flex: 1;
  363. height: 100%;
  364. }
  365. .task-list-controls__tab {
  366. position: relative;
  367. flex: 1;
  368. height: 100%;
  369. color: #666666;
  370. font-size: 32rpx;
  371. font-weight: 400;
  372. line-height: 88rpx;
  373. text-align: center;
  374. }
  375. .task-list-controls__tab--active {
  376. color: #333333;
  377. font-weight: 600;
  378. &::after {
  379. position: absolute;
  380. bottom: 0;
  381. left: 50%;
  382. width: 78rpx;
  383. height: 6rpx;
  384. background: #3fa3f1;
  385. border-radius: 999rpx;
  386. transform: translateX(-50%);
  387. content: '';
  388. }
  389. }
  390. .task-list-controls__date {
  391. display: flex;
  392. align-items: center;
  393. justify-content: center;
  394. width: 280rpx;
  395. height: 100%;
  396. color: #333333;
  397. }
  398. .task-list-controls__divider {
  399. width: 2rpx;
  400. height: 42rpx;
  401. margin-right: 42rpx;
  402. background: #eeeeee;
  403. }
  404. .task-list-controls__date-text {
  405. margin-right: 14rpx;
  406. color: #333333;
  407. font-size: 32rpx;
  408. }
  409. }
  410. }
  411. .task-list-content {
  412. padding: 24rpx 32rpx;
  413. margin-top: 200rpx;
  414. .task-card {
  415. margin-bottom: 24rpx;
  416. position: relative;
  417. padding: 28rpx;
  418. background: #ffffff;
  419. border-radius: 20rpx;
  420. box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
  421. .task-card__header {
  422. display: flex;
  423. align-items: flex-start;
  424. gap: 18rpx;
  425. }
  426. .task-card__title {
  427. flex: 1;
  428. min-width: 0;
  429. color: #333333;
  430. font-size: 34rpx;
  431. font-weight: 600;
  432. line-height: 48rpx;
  433. word-break: break-all;
  434. }
  435. :deep(.task-card__tag) {
  436. display: inline-flex;
  437. height: 32rpx;
  438. margin-left: 10rpx;
  439. padding: 0 10rpx;
  440. font-size: 22rpx;
  441. line-height: 30rpx;
  442. vertical-align: 4rpx;
  443. border-radius: 6rpx;
  444. }
  445. :deep(.task-card__button) {
  446. flex-shrink: 0;
  447. min-width: 132rpx;
  448. height: 58rpx;
  449. padding: 0 18rpx;
  450. color: #ffffff;
  451. font-size: 28rpx;
  452. background: #3fa3f1;
  453. border-color: #3fa3f1;
  454. }
  455. .task-card__status {
  456. display: flex;
  457. flex-shrink: 0;
  458. align-items: center;
  459. min-height: 48rpx;
  460. color: #333333;
  461. font-size: 26rpx;
  462. }
  463. .task-card__dot {
  464. width: 10rpx;
  465. height: 10rpx;
  466. margin-right: 8rpx;
  467. background: #3fa3f1;
  468. border-radius: 50%;
  469. }
  470. .task-card__info {
  471. margin-top: 12rpx;
  472. color: #999999;
  473. font-size: 26rpx;
  474. line-height: 42rpx;
  475. }
  476. .task-card__meta {
  477. display: flex;
  478. flex-wrap: wrap;
  479. gap: 0 18rpx;
  480. }
  481. .task-card__expired {
  482. z-index: 99;
  483. position: absolute;
  484. right: 30rpx;
  485. bottom: 25rpx;
  486. width: 150rpx;
  487. height: 150rpx;
  488. image {
  489. width: 150rpx;
  490. height: 150rpx;
  491. }
  492. }
  493. }
  494. .task-load-status {
  495. padding: 12rpx 0 32rpx;
  496. color: #999999;
  497. font-size: 24rpx;
  498. text-align: center;
  499. }
  500. }
  501. .task-empty {
  502. padding-top: 360rpx;
  503. }
  504. }
  505. </style>