|
|
@@ -0,0 +1,485 @@
|
|
|
+<template>
|
|
|
+ <view class="container" :style="{ '--font-scale': fontScale }">
|
|
|
+ <!-- 页头:当前账号 + 成员选择 -->
|
|
|
+ <view class="page-header">
|
|
|
+ <view class="header-account">
|
|
|
+ <text class="account-label">当前账号</text>
|
|
|
+ <text class="account-name">{{ selfMember ? selfMember.nickname : '我' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="header-tip">查看已完成的历史任务记录</view>
|
|
|
+ <!-- 成员选择 -->
|
|
|
+ <MemberSelector
|
|
|
+ variant="minimal"
|
|
|
+ embedded
|
|
|
+ :members="familyMembers"
|
|
|
+ :selected-member-id="selectedMemberId"
|
|
|
+ :show-self="true"
|
|
|
+ self-label="本人"
|
|
|
+ :self-active="!selectedMemberId"
|
|
|
+ @select="onMemberSelect" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 分类筛选 -->
|
|
|
+ <view class="category-filter" v-if="categories.length > 0">
|
|
|
+ <view class="category-scroll" scroll-x show-scrollbar="false">
|
|
|
+ <view class="category-inner">
|
|
|
+ <view
|
|
|
+ class="category-chip"
|
|
|
+ :class="{ active: !currentCategory }"
|
|
|
+ @click="selectCategory('')"
|
|
|
+ >
|
|
|
+ <text class="chip-text">全部</text>
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ v-for="cat in categories"
|
|
|
+ :key="cat"
|
|
|
+ class="category-chip"
|
|
|
+ :class="{ active: currentCategory === cat }"
|
|
|
+ @click="selectCategory(cat)"
|
|
|
+ >
|
|
|
+ <text class="chip-text">{{ cat }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 历史任务列表 -->
|
|
|
+ <view class="task-list">
|
|
|
+ <view class="task-card" v-for="task in taskList" :key="task.id">
|
|
|
+ <view class="card-accent-line"></view>
|
|
|
+ <view class="card-body">
|
|
|
+ <view class="card-left">
|
|
|
+ <view class="card-top-row">
|
|
|
+ <text class="card-type-icon" v-if="task.category === '小游戏类'">🎮</text>
|
|
|
+ <text class="card-type-icon" v-else-if="task.actionType === 'cart' || task.actionType === 'order'">🛒</text>
|
|
|
+ <text class="card-type-icon" v-else-if="task.actionType === 'focus'">⏱️</text>
|
|
|
+ <text class="card-type-icon" v-else-if="task.actionType === 'survey'">📝</text>
|
|
|
+ <text class="card-type-icon" v-else-if="task.actionType === 'article'">📖</text>
|
|
|
+ <text class="card-type-icon" v-else-if="task.actionType === 'activity'">🎯</text>
|
|
|
+ <text class="card-type-icon" v-else-if="task.requireInput === 1">✍️</text>
|
|
|
+ <text class="card-title">{{ task.title }}</text>
|
|
|
+ <text class="member-pill" v-if="task.memberOnly === 1">会员</text>
|
|
|
+ </view>
|
|
|
+ <view class="card-meta">
|
|
|
+ <text class="points-pill">+{{ task.points }} CF</text>
|
|
|
+ <text class="meta-dot">·</text>
|
|
|
+ <text class="card-category" v-if="task.category">{{ task.category }}</text>
|
|
|
+ <text class="meta-dot" v-if="task.category">·</text>
|
|
|
+ <text class="card-completed-time">完成于 {{ formatCompletedTime(task.completedAt || task.updatedAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="card-right">
|
|
|
+ <view class="badge badge-completed">已完成</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 空态 -->
|
|
|
+ <view class="empty-state" v-if="!loading && taskList.length === 0">
|
|
|
+ <view class="empty-icon">📋</view>
|
|
|
+ <text class="empty-main">暂无已完成任务</text>
|
|
|
+ <text class="empty-sub">完成任务后会自动记录到这里</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载更多 -->
|
|
|
+ <view class="load-more" v-if="loading">
|
|
|
+ <view class="spinner"></view>
|
|
|
+ <text class="load-more-text">加载中...</text>
|
|
|
+ </view>
|
|
|
+ <view class="load-more" v-if="!loading && hasMore">
|
|
|
+ <text class="load-more-text" @click="loadMore">点击加载更多</text>
|
|
|
+ </view>
|
|
|
+ <view class="load-more" v-if="!loading && !hasMore && taskList.length > 0">
|
|
|
+ <text class="load-more-text end-text">已加载全部</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import MemberSelector from '@/components/MemberSelector.vue'
|
|
|
+import { getFamilyMemberList, getTaskHistory } from '../../utils/api.js'
|
|
|
+import { formatDateTime } from '../../utils/format.js'
|
|
|
+import fontScaleMixin from '@/mixins/font-scale.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ mixins: [fontScaleMixin],
|
|
|
+ components: { MemberSelector },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ familyMembers: [],
|
|
|
+ selfMember: null,
|
|
|
+ selectedMemberId: null,
|
|
|
+ currentCategory: '',
|
|
|
+ categories: [],
|
|
|
+ taskList: [],
|
|
|
+ loading: false,
|
|
|
+ hasMore: true,
|
|
|
+ page: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ currentMemberId: null,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ this.currentMemberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || uni.getStorageSync('userId')
|
|
|
+ this.loadFamilyMembers()
|
|
|
+ },
|
|
|
+ onShow() {
|
|
|
+ // 页面返回时刷新
|
|
|
+ if (this._loaded) {
|
|
|
+ this.loadHistory(true)
|
|
|
+ }
|
|
|
+ this._loaded = true
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ loadFamilyMembers() {
|
|
|
+ const self = this
|
|
|
+ getFamilyMemberList({ visibleOnly: true }).then(function(res) {
|
|
|
+ var data = res && res.data ? res.data : []
|
|
|
+ self.familyMembers = Array.isArray(data) ? data : []
|
|
|
+ // 找到当前登录成员作为默认
|
|
|
+ var currentId = self.currentMemberId
|
|
|
+ if (currentId) {
|
|
|
+ var found = self.familyMembers.find(function(m) { return m.id == currentId })
|
|
|
+ if (found) {
|
|
|
+ self.selfMember = found
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!self.selfMember && self.familyMembers.length > 0) {
|
|
|
+ self.selfMember = self.familyMembers[0]
|
|
|
+ }
|
|
|
+ self.loadHistory(true)
|
|
|
+ }).catch(function() {
|
|
|
+ self.familyMembers = []
|
|
|
+ self.loadHistory(true)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onMemberSelect(member) {
|
|
|
+ this.selectedMemberId = member ? member.id : null
|
|
|
+ this.currentMemberId = member ? member.id : (this.currentMemberId || uni.getStorageSync('userId'))
|
|
|
+ this.selfMember = member || this.familyMembers[0] || null
|
|
|
+ this.loadHistory(true)
|
|
|
+ },
|
|
|
+ selectCategory(category) {
|
|
|
+ if (this.currentCategory === category) return
|
|
|
+ this.currentCategory = category
|
|
|
+ this.loadHistory(true)
|
|
|
+ },
|
|
|
+ loadHistory(reset) {
|
|
|
+ if (this.loading) return
|
|
|
+ if (reset) {
|
|
|
+ this.page = 1
|
|
|
+ this.taskList = []
|
|
|
+ this.hasMore = true
|
|
|
+ }
|
|
|
+ this.loading = true
|
|
|
+
|
|
|
+ var memberId = this.selectedMemberId || this.currentMemberId || uni.getStorageSync('userId')
|
|
|
+ if (!memberId) {
|
|
|
+ this.loading = false
|
|
|
+ this.hasMore = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const self = this
|
|
|
+ getTaskHistory(memberId, this.page, this.pageSize, this.currentCategory || null).then(function(res) {
|
|
|
+ self.loading = false
|
|
|
+ if (res && res.code === 200 && res.data) {
|
|
|
+ var pageData = res.data
|
|
|
+ var records = pageData.records || pageData.list || []
|
|
|
+ // 提取分类用于筛选
|
|
|
+ if (self.page === 1) {
|
|
|
+ var cats = []
|
|
|
+ records.forEach(function(t) {
|
|
|
+ if (t.category && cats.indexOf(t.category) === -1) {
|
|
|
+ cats.push(t.category)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ self.categories = cats
|
|
|
+ }
|
|
|
+ if (reset) {
|
|
|
+ self.taskList = records
|
|
|
+ } else {
|
|
|
+ self.taskList = self.taskList.concat(records)
|
|
|
+ }
|
|
|
+ self.hasMore = records.length >= self.pageSize
|
|
|
+ if (self.hasMore) {
|
|
|
+ self.page++
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ self.hasMore = false
|
|
|
+ }
|
|
|
+ }).catch(function(err) {
|
|
|
+ self.loading = false
|
|
|
+ self.hasMore = false
|
|
|
+ console.error('加载历史任务失败', err)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ loadMore() {
|
|
|
+ if (!this.loading && this.hasMore) {
|
|
|
+ this.loadHistory(false)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ formatCompletedTime(timeStr) {
|
|
|
+ if (!timeStr) return ''
|
|
|
+ var d = new Date(timeStr.replace(/-/g, '/'))
|
|
|
+ if (isNaN(d.getTime())) return timeStr
|
|
|
+ var y = d.getFullYear()
|
|
|
+ var m = ('0' + (d.getMonth() + 1)).slice(-2)
|
|
|
+ var day = ('0' + d.getDate()).slice(-2)
|
|
|
+ var h = ('0' + d.getHours()).slice(-2)
|
|
|
+ var min = ('0' + d.getMinutes()).slice(-2)
|
|
|
+ return y + '-' + m + '-' + day + ' ' + h + ':' + min
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+$c-text-gray-400: $facade-text-gray-400;
|
|
|
+$c-fill-2b: #F2F2F2;
|
|
|
+
|
|
|
+.container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: $facade-fill-warm-gray;
|
|
|
+ padding-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 页头 */
|
|
|
+.page-header {
|
|
|
+ margin: 0 $uni-space-lg 16rpx;
|
|
|
+ background: $app-surface;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.header-account {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.account-label {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-muted;
|
|
|
+}
|
|
|
+
|
|
|
+.account-name {
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
+ font-weight: 600;
|
|
|
+ color: $facade-text-strong;
|
|
|
+}
|
|
|
+
|
|
|
+.header-tip {
|
|
|
+ font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-body;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分类筛选 */
|
|
|
+.category-filter {
|
|
|
+ margin: 0 $uni-space-lg 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.category-scroll {
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.category-inner {
|
|
|
+ display: flex;
|
|
|
+ gap: 16rpx;
|
|
|
+ padding: 8rpx 8rpx 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.category-chip {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8rpx 20rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ background: $facade-fill;
|
|
|
+ border: 1rpx solid $app-border-subtle;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.category-chip.active {
|
|
|
+ background: $brand-primary;
|
|
|
+ border-color: $brand-primary;
|
|
|
+}
|
|
|
+
|
|
|
+.category-chip.active .chip-text {
|
|
|
+ color: $app-surface;
|
|
|
+}
|
|
|
+
|
|
|
+.chip-text {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-body;
|
|
|
+}
|
|
|
+
|
|
|
+/* 任务列表 */
|
|
|
+.task-list {
|
|
|
+ padding: 0 $uni-space-lg;
|
|
|
+}
|
|
|
+
|
|
|
+.task-card {
|
|
|
+ background: $app-surface;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.card-accent-line {
|
|
|
+ width: 6rpx;
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(180deg, $brand-primary, $brand-primary-light);
|
|
|
+}
|
|
|
+
|
|
|
+.card-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.card-left {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.card-top-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.card-type-icon {
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
+ font-weight: 600;
|
|
|
+ color: $facade-text-strong;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.member-pill {
|
|
|
+ font-size: calc(20rpx * var(--font-scale, 1));
|
|
|
+ background: $dim-wealth-tint;
|
|
|
+ color: $dim-wealth-deep;
|
|
|
+ padding: 2rpx 10rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.card-meta {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6rpx;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-body;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.points-pill {
|
|
|
+ background: $dim-action-tint;
|
|
|
+ color: $dim-action-deep;
|
|
|
+ padding: 2rpx 10rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.meta-dot {
|
|
|
+ color: $facade-text-ghost;
|
|
|
+}
|
|
|
+
|
|
|
+.card-category {
|
|
|
+ color: $brand-primary;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.card-completed-time {
|
|
|
+ color: $facade-text-muted;
|
|
|
+}
|
|
|
+
|
|
|
+.card-right {
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.badge {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 6rpx 16rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.badge-completed {
|
|
|
+ background: $dim-body-tint;
|
|
|
+ color: $dim-body-deep;
|
|
|
+}
|
|
|
+
|
|
|
+/* 空态 */
|
|
|
+.empty-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 80rpx 24rpx 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-icon {
|
|
|
+ font-size: calc(80rpx * var(--font-scale, 1));
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-main {
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
+ font-weight: 600;
|
|
|
+ color: $facade-text-strong;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-sub {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-muted;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载更多 */
|
|
|
+.load-more {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 32rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+.spinner {
|
|
|
+ width: 32rpx;
|
|
|
+ height: 32rpx;
|
|
|
+ border: 3rpx solid $facade-fill;
|
|
|
+ border-top-color: $brand-primary;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.load-more-text {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-muted;
|
|
|
+ margin-left: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.end-text {
|
|
|
+ color: $facade-text-ghost;
|
|
|
+}
|
|
|
+</style>
|