|
|
@@ -0,0 +1,439 @@
|
|
|
+<template>
|
|
|
+ <view class="task-package-list">
|
|
|
+ <view class="task-filter-header">
|
|
|
+ <view class="task-filter-header__search">
|
|
|
+ <wd-search
|
|
|
+ v-model="params.keyword"
|
|
|
+ hide-cancel
|
|
|
+ placeholder-left
|
|
|
+ placeholder="搜索任务包名称/编号"
|
|
|
+ @change="onKeywordChange"
|
|
|
+ @clear="handleClearSearch"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="task-list-controls">
|
|
|
+ <view class="task-list-controls__tabs">
|
|
|
+ <view
|
|
|
+ v-for="tab in tabs"
|
|
|
+ :key="tab.value"
|
|
|
+ class="task-list-controls__tab"
|
|
|
+ :class="{ 'task-list-controls__tab--active': params.activeTab === tab.value }"
|
|
|
+ @click="handleTabChange(tab.value)"
|
|
|
+ >
|
|
|
+ {{ tab.label }}
|
|
|
+ <text v-if="tab.value === TaskPackageTab.Available">({{ params.total }})</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="task-list-controls__date" @click="openDatePicker">
|
|
|
+ <view class="task-list-controls__divider" />
|
|
|
+ <text class="task-list-controls__date-text">{{ selectedDateText }}</text>
|
|
|
+ <wd-icon name="arrow-down" size="28rpx" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="showEmpty" class="task-empty">
|
|
|
+ <wd-empty icon="no-content" tip="暂无内容" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else class="task-list-content">
|
|
|
+ <view v-for="item in packageListArr" :key="item.id" class="task-card">
|
|
|
+ <view class="task-card__header">
|
|
|
+ <view class="task-card__title">
|
|
|
+ {{ item.scorePackageName }}{{ item.packageSn ? `(${item.packageSn})` : '' }}
|
|
|
+ <wd-tag
|
|
|
+ v-if="getPackageTypeTag(item.packageType1)"
|
|
|
+ custom-class="task-card__tag"
|
|
|
+ plain
|
|
|
+ :color="getPackageTypeTag(item.packageType1)?.color"
|
|
|
+ :bg-color="getPackageTypeTag(item.packageType1)?.bgColor"
|
|
|
+ >
|
|
|
+ {{ getPackageTypeTag(item.packageType1)?.label }}
|
|
|
+ </wd-tag>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <wd-button
|
|
|
+ v-if="params.activeTab === TaskPackageTab.Available"
|
|
|
+ size="small"
|
|
|
+ round
|
|
|
+ custom-class="task-card__button"
|
|
|
+ @click.stop="handleReceive(item)"
|
|
|
+ >
|
|
|
+ 立即领取
|
|
|
+ </wd-button>
|
|
|
+
|
|
|
+ <view v-else class="task-card__status">
|
|
|
+ <view class="task-card__dot" />
|
|
|
+ <text>{{ getPackageStatusText(item.packageStatus) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="task-card__info">
|
|
|
+ <view>发包企业:{{ item.packageEntName || '-' }}</view>
|
|
|
+
|
|
|
+ <view class="task-card__meta">
|
|
|
+ <text>需求人数:{{ item.userGetNum }}/{{ item.userNum }}</text>
|
|
|
+ <text>预计积分值:{{ item.score }}分</text>
|
|
|
+ <text v-if="item.taskNum">任务数:{{ item.taskNum }}个</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view>发布时间:{{ formatDate(item.createTime) }}</view>
|
|
|
+ <view>需求描述:{{ item.description || '无' }}</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="item.packageStatus === '2'" class="task-card__expired">
|
|
|
+ <image src="/static/images/common/expired.png" mode="heightFix" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="task-load-status">
|
|
|
+ <text v-if="loading">加载中...</text>
|
|
|
+ <text v-else-if="finished">没有更多了</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
|
|
|
+
|
|
|
+import { onReachBottom } from '@dcloudio/uni-app'
|
|
|
+
|
|
|
+import { debounce } from '@/plugins/debounce'
|
|
|
+
|
|
|
+import { getUserScorePackageListApi } from '@/services/modules/task/list'
|
|
|
+import type {
|
|
|
+ getUserScorePackageListRequest,
|
|
|
+ TaskPackageItem,
|
|
|
+} from '@/services/modules/task/list/type'
|
|
|
+
|
|
|
+enum TaskPackageTab {
|
|
|
+ Available = '1',
|
|
|
+ Received = '2',
|
|
|
+}
|
|
|
+
|
|
|
+const tabs = [
|
|
|
+ { label: '可领取', value: TaskPackageTab.Available },
|
|
|
+ { label: '已领取', value: TaskPackageTab.Received },
|
|
|
+] as const
|
|
|
+
|
|
|
+const params = reactive({
|
|
|
+ keyword: '',
|
|
|
+ total: 0,
|
|
|
+ activeTab: TaskPackageTab.Available,
|
|
|
+ current: 1,
|
|
|
+ size: 10,
|
|
|
+ selectedYear: '',
|
|
|
+ selectedMonth: '',
|
|
|
+})
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const finished = ref(false)
|
|
|
+const packageListArr = ref<TaskPackageItem[]>([])
|
|
|
+
|
|
|
+let requestId = 0
|
|
|
+
|
|
|
+const selectedDate = computed(() => {
|
|
|
+ const { selectedYear, selectedMonth } = params
|
|
|
+ return selectedYear && selectedMonth ? `${selectedYear}-${selectedMonth}` : ''
|
|
|
+})
|
|
|
+
|
|
|
+const selectedDateText = computed(() => {
|
|
|
+ const { selectedYear, selectedMonth } = params
|
|
|
+ return selectedYear && selectedMonth ? `${selectedYear}年${selectedMonth}月` : '日期'
|
|
|
+})
|
|
|
+
|
|
|
+const requestParams = computed<getUserScorePackageListRequest>(() => ({
|
|
|
+ current: params.current,
|
|
|
+ size: params.size,
|
|
|
+ selType: params.activeTab,
|
|
|
+ selDate: selectedDate.value,
|
|
|
+ packageName: params.keyword.trim(),
|
|
|
+}))
|
|
|
+
|
|
|
+const showEmpty = computed(() => !loading.value && packageListArr.value.length === 0)
|
|
|
+
|
|
|
+const resetList = () => {
|
|
|
+ params.current = 1
|
|
|
+ finished.value = false
|
|
|
+ packageListArr.value = []
|
|
|
+}
|
|
|
+
|
|
|
+const fetchTaskPackageList = async () => {
|
|
|
+ if (loading.value || finished.value) return
|
|
|
+
|
|
|
+ loading.value = true
|
|
|
+ const currentRequestId = ++requestId
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await getUserScorePackageListApi(requestParams.value)
|
|
|
+ if (currentRequestId !== requestId) return
|
|
|
+ if (res.code !== 0) return
|
|
|
+
|
|
|
+ const records = res.data?.records ?? []
|
|
|
+ const total = res.data?.total ?? 0
|
|
|
+
|
|
|
+ if (params.activeTab === TaskPackageTab.Available) {
|
|
|
+ params.total = total
|
|
|
+ }
|
|
|
+
|
|
|
+ packageListArr.value = params.current === 1 ? records : packageListArr.value.concat(records)
|
|
|
+
|
|
|
+ finished.value = packageListArr.value.length >= total || records.length < params.size
|
|
|
+ } finally {
|
|
|
+ if (currentRequestId === requestId) {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const refreshTaskPackageList = () => {
|
|
|
+ resetList()
|
|
|
+ fetchTaskPackageList()
|
|
|
+}
|
|
|
+
|
|
|
+const loadMoreTaskPackageList = () => {
|
|
|
+ console.log('cc')
|
|
|
+
|
|
|
+ if (loading.value || finished.value) return
|
|
|
+ params.current += 1
|
|
|
+ fetchTaskPackageList()
|
|
|
+}
|
|
|
+
|
|
|
+const debouncedSearch = debounce(fetchTaskPackageList, {
|
|
|
+ wait: 500,
|
|
|
+ leading: false,
|
|
|
+ trailing: true,
|
|
|
+})
|
|
|
+
|
|
|
+const onKeywordChange = () => {
|
|
|
+ resetList()
|
|
|
+ debouncedSearch()
|
|
|
+}
|
|
|
+
|
|
|
+const handleClearSearch = () => {
|
|
|
+ params.keyword = ''
|
|
|
+ debouncedSearch.cancel()
|
|
|
+ refreshTaskPackageList()
|
|
|
+}
|
|
|
+
|
|
|
+const handleTabChange = (value: TaskPackageTab) => {
|
|
|
+ if (params.activeTab === value) return
|
|
|
+ params.activeTab = value
|
|
|
+ debouncedSearch.cancel()
|
|
|
+ refreshTaskPackageList()
|
|
|
+}
|
|
|
+
|
|
|
+const openDatePicker = () => {
|
|
|
+ console.log('打开日期选择器')
|
|
|
+}
|
|
|
+
|
|
|
+const handleReceive = (item: TaskPackageItem) => {
|
|
|
+ console.log('立即领取:', item)
|
|
|
+}
|
|
|
+
|
|
|
+const getPackageTypeTag = (type: string | null) => {
|
|
|
+ const tagMap = {
|
|
|
+ '1': { label: '积分值', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
|
|
|
+ '6': { label: '积分值', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
|
|
|
+ '2': { label: '任务量', color: '#4cd964', bgColor: 'rgba(76, 217, 100, 0.1)' },
|
|
|
+ '3': { label: '通用', color: '#dd524d', bgColor: 'rgba(221, 82, 77, 0.1)' },
|
|
|
+ '4': { label: '特定', color: '#dd524d', bgColor: 'rgba(221, 82, 77, 0.1)' },
|
|
|
+ } as const
|
|
|
+
|
|
|
+ return type ? tagMap[type as keyof typeof tagMap] : undefined
|
|
|
+}
|
|
|
+
|
|
|
+const getPackageStatusText = (status: string | null) => status || '-'
|
|
|
+
|
|
|
+const formatDate = (value: string | null) => value?.slice(0, 10) || '-'
|
|
|
+
|
|
|
+onReachBottom(loadMoreTaskPackageList)
|
|
|
+
|
|
|
+onBeforeMount(fetchTaskPackageList)
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ debouncedSearch.cancel()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.task-package-list {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #fafafa;
|
|
|
+
|
|
|
+ .task-filter-header {
|
|
|
+ background: #ffffff;
|
|
|
+
|
|
|
+ .task-filter-header__search {
|
|
|
+ margin-bottom: 6rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 88rpx;
|
|
|
+
|
|
|
+ .task-list-controls__tabs {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-controls__tab {
|
|
|
+ position: relative;
|
|
|
+ flex: 1;
|
|
|
+ height: 100%;
|
|
|
+ color: #666666;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 88rpx;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-controls__tab--active {
|
|
|
+ color: #333333;
|
|
|
+ font-weight: 600;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 50%;
|
|
|
+ width: 78rpx;
|
|
|
+ height: 6rpx;
|
|
|
+ background: #3fa3f1;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ content: '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-controls__date {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 220rpx;
|
|
|
+ height: 100%;
|
|
|
+ color: #333333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-controls__divider {
|
|
|
+ width: 2rpx;
|
|
|
+ height: 42rpx;
|
|
|
+ margin-right: 42rpx;
|
|
|
+ background: #eeeeee;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-controls__date-text {
|
|
|
+ margin-right: 14rpx;
|
|
|
+ color: #333333;
|
|
|
+ font-size: 32rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-list-content {
|
|
|
+ padding: 24rpx 32rpx;
|
|
|
+
|
|
|
+ .task-card {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ padding: 28rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
|
|
|
+
|
|
|
+ .task-card__header {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 18rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-card__title {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ color: #333333;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 48rpx;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.task-card__tag) {
|
|
|
+ display: inline-flex;
|
|
|
+ height: 32rpx;
|
|
|
+ margin-left: 10rpx;
|
|
|
+ padding: 0 10rpx;
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 30rpx;
|
|
|
+ vertical-align: 4rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.task-card__button) {
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 132rpx;
|
|
|
+ height: 58rpx;
|
|
|
+ padding: 0 18rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 28rpx;
|
|
|
+ background: #3fa3f1;
|
|
|
+ border-color: #3fa3f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-card__status {
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 48rpx;
|
|
|
+ color: #333333;
|
|
|
+ font-size: 26rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-card__dot {
|
|
|
+ width: 10rpx;
|
|
|
+ height: 10rpx;
|
|
|
+ margin-right: 8rpx;
|
|
|
+ background: #3fa3f1;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-card__info {
|
|
|
+ margin-top: 12rpx;
|
|
|
+ color: #999999;
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 42rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-card__meta {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 0 18rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-card__expired {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 8rpx;
|
|
|
+
|
|
|
+ image {
|
|
|
+ width: 150rpx;
|
|
|
+ height: 150rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-load-status {
|
|
|
+ padding: 12rpx 0 32rpx;
|
|
|
+ color: #999999;
|
|
|
+ font-size: 24rpx;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-empty {
|
|
|
+ padding-top: 160rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|