| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 |
- <template>
- <view class="container">
- <!-- 顶部导航 -->
- <view class="header">
- <view class="header-left" @click="goBack">
- <text class="back-arrow">‹</text>
- </view>
- <text class="header-title">任务完成率分析</text>
- <view class="header-right" @click="togglePeriod">
- <text class="period-btn">{{ periodLabel }}</text>
- </view>
- </view>
- <view class="scroll-area">
- <!-- 加载状态 -->
- <view class="loading-state" v-if="loading">
- <view class="loading-spinner"></view>
- <text class="loading-text">正在分析数据...</text>
- </view>
- <template v-if="!loading">
- <!-- 整体完成率 (环形图) -->
- <view class="rate-card">
- <view class="rate-ring-wrapper">
- <view class="rate-ring">
- <svg viewBox="0 0 120 120" class="ring-svg">
- <circle cx="60" cy="60" r="52" fill="none" stroke="#FFF0E6" stroke-width="8" />
- <circle cx="60" cy="60" r="52" fill="none" stroke="url(#rateGradient)" stroke-width="8"
- stroke-linecap="round"
- :stroke-dasharray="circumference"
- :stroke-dashoffset="rateOffset" transform="rotate(-90 60 60)" />
- <defs>
- <linearGradient id="rateGradient" x1="0%" y1="0%" x2="100%" y2="100%">
- <stop offset="0%" stop-color="#4ECDC4" />
- <stop offset="100%" stop-color="#44B09E" />
- </linearGradient>
- </defs>
- </svg>
- <view class="rate-ring-text">
- <text class="rate-value">{{ stats.completionRate }}%</text>
- <text class="rate-label">完成率</text>
- </view>
- </view>
- </view>
- <view class="rate-legend">
- <view class="legend-item">
- <view class="legend-dot" style="background: #4ECDC4;"></view>
- <text class="legend-label">已完成 {{ stats.completedTasks }}</text>
- </view>
- <view class="legend-item">
- <view class="legend-dot" style="background: #FFB088;"></view>
- <text class="legend-label">进行中 {{ stats.pendingTasks }}</text>
- </view>
- <view class="legend-item">
- <view class="legend-dot" style="background: #F97316;"></view>
- <text class="legend-label">已逾期 {{ stats.overdueTasks }}</text>
- </view>
- </view>
- </view>
- <!-- 快捷统计卡片 -->
- <view class="quick-stats">
- <view class="quick-stat-card">
- <text class="quick-stat-value primary-color">{{ stats.totalTasks }}</text>
- <text class="quick-stat-label">总任务数</text>
- </view>
- <view class="quick-stat-card">
- <text class="quick-stat-value success-color">{{ stats.completedTasks }}</text>
- <text class="quick-stat-label">已完成</text>
- </view>
- <view class="quick-stat-card">
- <text class="quick-stat-value danger-color">{{ stats.overdueTasks }}</text>
- <text class="quick-stat-label">已逾期</text>
- </view>
- </view>
- <!-- 逾期任务警示 -->
- <view class="alert-card" v-if="stats.overdueTasks > 0">
- <text class="alert-icon">⚠️</text>
- <view class="alert-body">
- <text class="alert-title">有 {{ stats.overdueTasks }} 个任务已逾期</text>
- <text class="alert-desc">请督促孩子尽快完成,避免影响成长积分</text>
- </view>
- </view>
- <!-- 按孩子对比 (仅家庭维度) -->
- <view class="section-block" v-if="stats.byChild && stats.byChild.length > 1">
- <view class="section-header">
- <text class="section-title">孩子对比</text>
- </view>
- <view class="child-compare">
- <view class="compare-bar-wrapper" v-for="child in stats.byChild" :key="child.memberId">
- <view class="compare-row">
- <text class="compare-name">{{ child.childName }}</text>
- <text class="compare-rate">{{ child.completionRate }}%</text>
- </view>
- <view class="compare-bar-bg">
- <view class="compare-bar-fill" :style="{ width: child.completionRate + '%' }"
- :class="'bar-color-' + (child.memberId % 5)"></view>
- </view>
- </view>
- </view>
- </view>
- <!-- 分类完成情况 -->
- <view class="section-block" v-if="stats.categoryBreakdown && stats.categoryBreakdown.length > 0">
- <view class="section-header">
- <text class="section-title">分类详情</text>
- </view>
- <view class="category-list">
- <view class="category-item" v-for="cat in stats.categoryBreakdown" :key="cat.category">
- <view class="category-header">
- <text class="category-name">{{ cat.category }}</text>
- <text class="category-count">{{ cat.completed }}/{{ cat.total }}</text>
- </view>
- <view class="category-bar-bg">
- <view class="category-bar-fill" :style="{ width: cat.rate + '%' }"></view>
- </view>
- <text class="category-rate">{{ cat.rate }}%</text>
- </view>
- </view>
- </view>
- <!-- 无数据占位 -->
- <view class="empty-section" v-if="stats.totalTasks === 0">
- <view class="empty-icon-area">
- <text class="empty-main-icon">📊</text>
- </view>
- <text class="empty-title">暂无任务数据</text>
- <text class="empty-hint">创建任务后将在这里展示完成率分析</text>
- </view>
- <!-- 已完成任务 -->
- <view class="section-block" v-if="stats.completedTaskList && stats.completedTaskList.length > 0">
- <view class="section-header">
- <text class="section-title">已完成打卡</text>
- </view>
- <view class="completed-list">
- <view class="completed-item" v-for="item in stats.completedTaskList" :key="item.id">
- <text class="completed-title">{{ item.title }}</text>
- <text class="completed-meta">+{{ item.points || 0 }} 积分</text>
- </view>
- </view>
- </view>
- </template>
- </view>
- </view>
- </template>
- <script>
- import { getChildCompletionStats, getFamilyCompletionStats, getChildren } from '../../utils/api.js'
- export default {
- data() {
- return {
- loading: true,
- memberId: '',
- isFamilyView: false,
- periodIndex: 0,
- periods: [
- { label: '本周', days: 7 },
- { label: '本月', days: 30 },
- { label: '近三月', days: 90 }
- ],
- stats: {
- totalTasks: 0,
- completedTasks: 0,
- pendingTasks: 0,
- inReviewTasks: 0,
- overdueTasks: 0,
- completionRate: 0,
- byChild: [],
- categoryBreakdown: [],
- completedTaskList: []
- }
- }
- },
- computed: {
- periodLabel() {
- return this.periods[this.periodIndex].label
- },
- circumference() {
- return 2 * Math.PI * 52
- },
- rateOffset() {
- const pct = this.stats.completionRate || 0
- return this.circumference * (1 - pct / 100)
- }
- },
- onLoad(options) {
- if (options.memberId) {
- this.memberId = options.memberId
- this.isFamilyView = false
- } else if (options.familyView === '1') {
- this.isFamilyView = true
- }
- this.loadStats()
- },
- methods: {
- async loadStats() {
- this.loading = true
- try {
- const period = this.periods[this.periodIndex]
- const endDate = this.formatDate(new Date())
- const startDate = this.formatDate(new Date(Date.now() - period.days * 24 * 60 * 60 * 1000))
- const params = { startDate, endDate }
- if (this.memberId) {
- params.memberId = this.memberId
- }
- let res
- if (this.isFamilyView || !this.memberId) {
- res = await getFamilyCompletionStats(params)
- } else {
- res = await getChildCompletionStats(params)
- }
- if (res && res.data) {
- // 兼容后端可能返回 byChild 或 categoryBreakdown
- const data = res.data
- this.stats = {
- totalTasks: data.totalTasks || 0,
- completedTasks: data.completedTasks || 0,
- pendingTasks: data.pendingTasks || 0,
- inReviewTasks: data.inReviewTasks || 0,
- overdueTasks: data.overdueTasks || 0,
- completionRate: data.completionRate || 0,
- byChild: data.byChild || [],
- categoryBreakdown: data.categoryBreakdown || [],
- completedTaskList: data.completedTaskList || []
- }
- }
- } catch (e) {
- console.error('加载完成率失败', e)
- uni.showToast({ title: '数据加载失败', icon: 'none' })
- } finally {
- this.loading = false
- }
- },
- togglePeriod() {
- this.periodIndex = (this.periodIndex + 1) % this.periods.length
- this.loadStats()
- },
- goBack() {
- uni.navigateBack()
- },
- formatDate(date) {
- const y = date.getFullYear()
- const m = String(date.getMonth() + 1).padStart(2, '0')
- const d = String(date.getDate()).padStart(2, '0')
- return y + '-' + m + '-' + d
- }
- }
- }
- </script>
- <style scoped>
- .container {
- background: #FFF9F0;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- }
- /* 顶部导航 */
- .header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 24rpx 32rpx;
- padding-top: calc(88rpx + env(safe-area-inset-top));
- background: #FFF9F0;
- position: sticky;
- top: 0;
- z-index: 10;
- }
- .header-left, .header-right {
- min-width: 120rpx;
- }
- .header-right {
- display: flex;
- justify-content: flex-end;
- }
- .back-arrow {
- font-size: 48rpx;
- color: #2D3436;
- font-weight: 300;
- line-height: 1;
- }
- .header-title {
- font-size: 34rpx;
- font-weight: 700;
- color: #2D3436;
- }
- .period-btn {
- font-size: 26rpx;
- color: #FF8C42;
- background: #FFF5EB;
- padding: 8rpx 20rpx;
- border-radius: 24rpx;
- font-weight: 500;
- }
- /* 滚动区 */
- .scroll-area {
- flex: 1;
- padding: 0 32rpx 48rpx;
- overflow-y: auto;
- }
- /* 加载状态 */
- .loading-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 160rpx 0;
- }
- .loading-spinner {
- width: 64rpx;
- height: 64rpx;
- border: 6rpx solid #FFF0E6;
- border-top-color: #FF8C42;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- .loading-text {
- font-size: 26rpx;
- color: #B2BEC3;
- margin-top: 20rpx;
- }
- /* 整体完成率卡片 */
- .rate-card {
- background: #FFFFFF;
- border-radius: 28rpx;
- padding: 40rpx 32rpx;
- margin-bottom: 24rpx;
- display: flex;
- align-items: center;
- box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
- }
- .rate-ring-wrapper {
- flex-shrink: 0;
- margin-right: 32rpx;
- }
- .rate-ring {
- width: 200rpx;
- height: 200rpx;
- position: relative;
- }
- .ring-svg {
- width: 100%;
- height: 100%;
- }
- .rate-ring-text {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .rate-value {
- font-size: 40rpx;
- font-weight: 800;
- color: #2D3436;
- line-height: 1.2;
- }
- .rate-label {
- font-size: 22rpx;
- color: #B2BEC3;
- margin-top: 4rpx;
- }
- .rate-legend {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- .legend-item {
- display: flex;
- align-items: center;
- }
- .legend-dot {
- width: 16rpx;
- height: 16rpx;
- border-radius: 50%;
- margin-right: 12rpx;
- flex-shrink: 0;
- }
- .legend-label {
- font-size: 26rpx;
- color: #636E72;
- }
- /* 快捷统计卡片 */
- .quick-stats {
- display: flex;
- gap: 16rpx;
- margin-bottom: 24rpx;
- }
- .quick-stat-card {
- flex: 1;
- background: #FFFFFF;
- border-radius: 20rpx;
- padding: 24rpx 16rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
- }
- .quick-stat-value {
- font-size: 40rpx;
- font-weight: 800;
- line-height: 1.2;
- }
- .quick-stat-label {
- font-size: 22rpx;
- color: #B2BEC3;
- margin-top: 8rpx;
- }
- .primary-color { color: #FF8C42; }
- .success-color { color: #4ECDC4; }
- .danger-color { color: #F97316; }
- /* 逾期警示 */
- .alert-card {
- background: #FFF5F5;
- border-radius: 20rpx;
- padding: 24rpx;
- margin-bottom: 24rpx;
- display: flex;
- align-items: flex-start;
- border: 1rpx solid #FFE0E0;
- }
- .alert-icon {
- font-size: 40rpx;
- margin-right: 16rpx;
- flex-shrink: 0;
- }
- .alert-body {
- flex: 1;
- }
- .alert-title {
- font-size: 28rpx;
- font-weight: 600;
- color: #E74C3C;
- display: block;
- }
- .alert-desc {
- font-size: 24rpx;
- color: #E17055;
- margin-top: 6rpx;
- display: block;
- }
- /* 区块 */
- .section-block {
- background: #FFFFFF;
- border-radius: 24rpx;
- padding: 28rpx;
- margin-bottom: 24rpx;
- box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
- }
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- }
- .section-title {
- font-size: 30rpx;
- font-weight: 700;
- color: #2D3436;
- }
- /* 孩子对比 */
- .child-compare {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .compare-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 8rpx;
- }
- .compare-name {
- font-size: 28rpx;
- color: #2D3436;
- font-weight: 500;
- }
- .compare-rate {
- font-size: 28rpx;
- color: #FF8C42;
- font-weight: 700;
- }
- .compare-bar-bg {
- height: 12rpx;
- background: #FFF0E6;
- border-radius: 999rpx;
- overflow: hidden;
- }
- .compare-bar-fill {
- height: 100%;
- border-radius: 999rpx;
- transition: width 0.5s ease;
- }
- .bar-color-0 { background: linear-gradient(90deg, #FF8C42, #FFD93D); }
- .bar-color-1 { background: linear-gradient(90deg, #4ECDC4, #44B09E); }
- .bar-color-2 { background: linear-gradient(90deg, #667eea, #764ba2); }
- .bar-color-3 { background: linear-gradient(90deg, #F97316, #FB923C); }
- .bar-color-4 { background: linear-gradient(90deg, #A8E063, #56AB2F); }
- /* 分类列表 */
- .category-list {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .category-item {
- display: flex;
- align-items: center;
- gap: 12rpx;
- }
- .category-header {
- width: 140rpx;
- flex-shrink: 0;
- }
- .category-name {
- font-size: 24rpx;
- color: #636E72;
- display: block;
- }
- .category-count {
- font-size: 20rpx;
- color: #B2BEC3;
- display: block;
- margin-top: 2rpx;
- }
- .category-bar-bg {
- flex: 1;
- height: 16rpx;
- background: #FFF0E6;
- border-radius: 999rpx;
- overflow: hidden;
- }
- .category-bar-fill {
- height: 100%;
- background: linear-gradient(90deg, #4ECDC4, #44B09E);
- border-radius: 999rpx;
- transition: width 0.5s ease;
- }
- .category-rate {
- width: 64rpx;
- text-align: right;
- font-size: 24rpx;
- font-weight: 600;
- color: #4ECDC4;
- flex-shrink: 0;
- }
- /* 已完成任务列表 */
- .completed-list {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- .completed-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: #fff;
- border-radius: 16rpx;
- padding: 24rpx;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
- }
- .completed-title {
- font-size: 28rpx;
- color: #2D3436;
- flex: 1;
- padding-right: 16rpx;
- }
- .completed-meta {
- font-size: 24rpx;
- color: #10B981;
- flex-shrink: 0;
- }
- /* 空状态 */
- .empty-section {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 120rpx 0;
- }
- .empty-icon-area {
- width: 160rpx;
- height: 160rpx;
- background: #FFF5EB;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 24rpx;
- }
- .empty-main-icon {
- font-size: 72rpx;
- }
- .empty-title {
- font-size: 30rpx;
- font-weight: 600;
- color: #2D3436;
- margin-bottom: 12rpx;
- }
- .empty-hint {
- font-size: 26rpx;
- color: #B2BEC3;
- text-align: center;
- }
- </style>
|