| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321 |
- <template>
- <view class="page">
- <BaseLoading :loading="loading" text="加载中..." />
- <!-- 顶部双 Tab:共同挑战 / 个人目标 -->
- <view class="doa-tabs">
- <view class="doa-tab active-tab" @click="goToDoa">🎯 共同挑战</view>
- <view class="doa-tab" @click="goToDoaIndex">📋 个人目标</view>
- </view>
- <view v-if="!loading" class="content">
- <!-- 首页挑战状态概览 -->
- <view class="overview-card" v-if="activeChallenges.length > 0">
- <view class="overview-header">
- <text class="overview-title">🔥 进行中</text>
- <text class="overview-count">{{ activeChallenges.length }} 个挑战</text>
- </view>
- <view class="active-list">
- <view class="active-item" v-for="ch in activeChallenges.slice(0, 2)" :key="ch.id" @click="viewChallenge(ch)">
- <text class="active-item-title">{{ ch.title }}</text>
- <text class="active-item-meta">{{ ch.targetValue }}/{{ ch.durationDays }}天</text>
- </view>
- </view>
- </view>
- <!-- 操作栏 -->
- <view class="action-bar">
- <button class="btn-new" @click="openCreateModal">
- <text class="btn-new-icon">+</text>
- <text>新建挑战</text>
- </button>
- </view>
- <!-- 挑战分类列表 -->
- <view class="section">
- <view class="section-header">
- <text class="section-title">我的挑战</text>
- </view>
- <!-- 进行中 -->
- <view v-if="activeChallenges.length > 0" class="group">
- <view class="group-label">进行中</view>
- <view class="card-list">
- <view class="card" v-for="ch in activeChallenges" :key="ch.id" @click="viewChallenge(ch)">
- <view class="card-main">
- <view class="card-header">
- <text class="card-title">{{ ch.title }}</text>
- <text class="card-status status-active">进行中</text>
- </view>
- <view class="card-meta">
- <text class="meta-item">{{ ch.targetMode === 'aggregate' ? '全家合计' : '每人目标' }}</text>
- <text class="meta-item">{{ ch.durationDays }}天</text>
- <text class="meta-item" v-if="ch.rewardPoints">+{{ ch.rewardPoints }}分</text>
- </view>
- </view>
- <view class="card-actions">
- <text class="action-btn edit" @click.stop="openEditModal(ch)">编辑</text>
- <text class="action-btn del" @click.stop="deleteChallenge(ch.id)">删除</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 已完成 -->
- <view v-if="historyChallenges.length > 0" class="group">
- <view class="group-label">已完成</view>
- <view class="card-list">
- <view class="card" v-for="ch in historyChallenges" :key="ch.id" @click="viewChallenge(ch)">
- <view class="card-main">
- <view class="card-header">
- <text class="card-title">{{ ch.title }}</text>
- <text class="card-status status-done">已完成</text>
- </view>
- <view class="card-meta">
- <text class="meta-item">{{ ch.durationDays }}天</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- <!-- 空状态 -->
- <view v-if="activeChallenges.length === 0 && historyChallenges.length === 0" class="empty">
- <text class="empty-icon">🎯</text>
- <text class="empty-title">还没有挑战</text>
- <text class="empty-desc">创建挑战,让全家一起动起来!</text>
- <button class="btn-empty" @click="openCreateModal">立即创建</button>
- </view>
- </view>
- </view>
- <!-- 创建/编辑弹窗 -->
- <view class="overlay" v-if="showModal" @click="closeModal">
- <view class="modal" @click.stop>
- <view class="modal-header">
- <text class="modal-title">{{ editMode ? '编辑挑战' : '新建挑战' }}</text>
- <text class="modal-close" @click="closeModal">×</text>
- </view>
- <!-- 模板选择(仅创建时) -->
- <view v-if="!editMode" class="template-row">
- <text class="tpl-label">快速模板</text>
- <scroll-view scroll-x class="tpl-scroll">
- <view class="tpl-list">
- <view
- class="tpl-item"
- :class="{ active: form.challengeType === 'custom' }"
- @click="selectTemplate(null)"
- >
- <text class="tpl-icon">✏️</text>
- <text class="tpl-name">自定义</text>
- </view>
- <view
- class="tpl-item"
- v-for="tpl in templates"
- :key="tpl.type"
- :class="{ active: form.challengeType === tpl.type }"
- @click="selectTemplate(tpl)"
- >
- <text class="tpl-icon">{{ tpl.emoji }}</text>
- <text class="tpl-name">{{ tpl.title }}</text>
- </view>
- </view>
- </scroll-view>
- </view>
- <view class="form-body">
- <view class="field">
- <text class="field-label">挑战标题 <text class="required">*</text></text>
- <input class="field-input" v-model="form.title" placeholder="给挑战起个名字" maxlength="50" />
- </view>
- <view class="field">
- <text class="field-label">挑战描述</text>
- <textarea class="field-textarea" v-model="form.description" placeholder="描述挑战内容(可选)" maxlength="200" />
- </view>
- <view class="field-row">
- <view class="field half">
- <text class="field-label">目标模式</text>
- <view class="radio-group">
- <view class="radio-btn" :class="{ active: form.targetMode === 'aggregate' }" @click="form.targetMode = 'aggregate'">全家合计</view>
- <view class="radio-btn" :class="{ active: form.targetMode === 'all_members' }" @click="form.targetMode = 'all_members'">每人目标</view>
- </view>
- </view>
- <view class="field half">
- <text class="field-label">目标值 <text class="required">*</text></text>
- <input class="field-input" type="digit" v-model="form.targetValue" placeholder="如: 300" />
- </view>
- </view>
- <view class="field-row">
- <view class="field half">
- <text class="field-label">持续天数</text>
- <input class="field-input" type="number" v-model="form.durationDays" placeholder="7" />
- </view>
- <view class="field half">
- <text class="field-label">奖励积分</text>
- <input class="field-input" type="number" v-model="form.rewardPoints" placeholder="50" />
- </view>
- </view>
- <view class="field-row" v-if="!editMode">
- <view class="field half">
- <text class="field-label">回复时效</text>
- <view class="radio-group">
- <view class="radio-btn" :class="{ active: form.respondDeadlineHours === '24' }" @click="form.respondDeadlineHours = '24'">24h</view>
- <view class="radio-btn" :class="{ active: form.respondDeadlineHours === '48' }" @click="form.respondDeadlineHours = '48'">48h</view>
- <view class="radio-btn" :class="{ active: form.respondDeadlineHours === '72' }" @click="form.respondDeadlineHours = '72'">72h</view>
- </view>
- </view>
- <view class="field half">
- <text class="field-label">超时处理</text>
- <view class="field-tip">超时未回复的成员将自动视为拒绝</view>
- </view>
- </view>
- <view class="field" v-if="!editMode || editingStatus === 'pending'">
- <text class="field-label">参与成员 <text class="required" v-if="editMode && editingStatus === 'pending'">(待同意挑战可修改)</text></text>
- <view class="member-grid">
- <view
- class="member-item"
- :class="{ active: isMemberSelected(m.id) }"
- v-for="m in familyMembers"
- :key="m.id"
- @click="toggleMember(m.id)"
- >
- <text>{{ m.nickname || m.phone || '成员' }}</text>
- </view>
- </view>
- </view>
- </view>
- <view class="modal-footer">
- <button class="btn-cancel" @click="closeModal">取消</button>
- <button class="btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</button>
- </view>
- </view>
- </view>
- <!-- 挑战详情弹窗 -->
- <view class="overlay" v-if="showDetailModal" @click="closeDetailModal">
- <view class="modal detail-modal" @click.stop>
- <view class="modal-header">
- <text class="modal-title">挑战详情</text>
- <text class="modal-close" @click="closeDetailModal">×</text>
- </view>
- <view v-if="detailChallenge" class="detail-body">
- <view class="detail-header">
- <text class="detail-title">{{ detailChallenge.title }}</text>
- <text class="detail-badge" :class="detailStatusClass">{{ detailStatusText }}</text>
- </view>
- <text class="detail-desc">{{ detailChallenge.description || '' }}</text>
- <!-- 待同意:显示确认进度(pending 状态挑战) -->
- <view class="pending-section" v-if="detailChallenge.status === 'pending'">
- <text class="pending-hint">等待成员确认(已同意 {{ detailAgreedCount }}/{{ detailTotalCount }})</text>
- </view>
- <!-- 目标展示(两种模式) -->
- <view class="detail-target">
- <view class="detail-row">
- <text class="detail-label">目标模式:</text>
- <text>{{ detailChallenge.targetMode === 'aggregate' ? '全家合计' : '每人目标' }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">目标值:</text>
- <text>{{ detailChallenge.targetValue }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">持续天数:</text>
- <text>{{ detailChallenge.durationDays }}天</text>
- </view>
- <view class="detail-row" v-if="detailChallenge.startDate">
- <text class="detail-label">开始日期:</text>
- <text>{{ detailChallenge.startDate.slice(0,10) }}</text>
- </view>
- <view class="detail-row" v-if="detailChallenge.endDate">
- <text class="detail-label">截止日期:</text>
- <text>{{ detailChallenge.endDate.slice(0,10) }}</text>
- </view>
- <view class="detail-row" v-if="detailChallenge.respondDeadline">
- <text class="detail-label">回复截止:</text>
- <text :class="detailDeadlinePassed ? 'detail-label detail-label-danger' : 'detail-label'">{{ detailChallenge.respondDeadline.slice(0,16).replace('T',' ') }}{{ detailDeadlinePassed ? '(已超时)' : '' }}</text>
- </view>
- <view class="detail-row" v-if="detailChallenge.rewardPoints">
- <text class="detail-label">奖励积分:</text>
- <text class="detail-label detail-label-reward">+{{ detailChallenge.rewardPoints }}分</text>
- </view>
- </view>
- <!-- 活动/已完成进度 -->
- <view class="detail-progress" v-if="detailChallenge.status !== 'pending'">
- <text class="detail-progress-title">进度</text>
- <view class="detail-progress-bar-wrap">
- <view class="detail-progress-bar-bg">
- <view class="detail-progress-bar-fill" :style="{width: detailProgressPercent + '%'}"></view>
- </view>
- <text class="detail-progress-text">{{ detailChallenge.totalProgress || 0 }}/{{ detailChallenge.targetValue }}</text>
- </view>
- <view class="detail-member-list" v-if="detailChallenge.memberProgress && detailChallenge.memberProgress.length > 0">
- <view class="detail-member-row" v-for="mp in detailChallenge.memberProgress" :key="mp.memberId">
- <text class="detail-member-name">{{ mp.nickname }}</text>
- <text class="detail-member-val">{{ mp.progressValue || 0 }}/{{ detailChallenge.targetValue }}</text>
- <text class="detail-member-status" :class="'member-' + mp.status">{{ detailMemberStatusLabel(mp.status) }}</text>
- </view>
- </view>
- </view>
- <!-- 同意/拒绝按钮 -->
- <view class="detail-action" v-if="detailChallenge.status === 'pending' && detailChallenge.myStatus === 'pending'">
- <view class="respond-btn respond-agree" @click="onDetailRespond('agree')">✅ 同意</view>
- <view class="respond-btn respond-reject" @click="onDetailRespond('reject')">✖ 拒绝</view>
- </view>
- <!-- 编辑/删除按钮(仅创建者) -->
- <view class="detail-action" v-if="creatorCanOperate">
- <button class="btn-edit-detail" @click="closeDetailModal; openEditModal(detailChallenge)">编辑</button>
- <button class="btn-delete-detail" @click="confirmDeleteDetail">删除</button>
- </view>
- </view>
- <view class="modal-footer">
- <button class="btn-cancel" @click="closeDetailModal">关闭</button>
- </view>
- </view>
- </view>
- <!-- 家庭成员不足提示弹窗 -->
- <view class="overlay" v-if="showMemberPrompt" @click="showMemberPrompt = false">
- <view class="modal prompt-modal" @click.stop>
- <view class="modal-header">
- <text class="modal-title">无法创建挑战</text>
- <text class="modal-close" @click="showMemberPrompt = false">×</text>
- </view>
- <view class="prompt-content">
- <text class="prompt-icon">👨👩👧👦</text>
- <text class="prompt-title">家庭成员不足</text>
- <text class="prompt-desc">家庭挑战至少需要 2 名成员参与,请先添加家庭成员</text>
- </view>
- <view class="modal-footer">
- <button class="btn-cancel" @click="showMemberPrompt = false">暂不添加</button>
- <button class="btn-submit" @click="goToAddMember">去添加成员</button>
- </view>
- </view>
- </view>
- <!-- 免费会员引导添加孩子弹窗 -->
- <view class="overlay" v-if="showAddChildPrompt" @click="showAddChildPrompt = false">
- <view class="modal prompt-modal" @click.stop>
- <view class="modal-header">
- <text class="modal-title">免费会员专属提示</text>
- <text class="modal-close" @click="showAddChildPrompt = false">×</text>
- </view>
- <view class="prompt-content">
- <text class="prompt-icon">👶</text>
- <text class="prompt-title">需先添加孩子成员</text>
- <text class="prompt-desc">免费会员只能添加 1 位孩子成员,添加后即可创建家庭挑战。<br/>挑战创建后,可切换到孩子身份同意参与。</text>
- </view>
- <view class="modal-footer">
- <button class="btn-cancel" @click="showAddChildPrompt = false">暂不添加</button>
- <button class="btn-submit" @click="goToAddChild">去添加孩子</button>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, getFamilyMemberList, getMyMembership, respondChallenge } from '../../utils/api.js'
- import store from '../../store/index.js'
- export default {
- data() {
- return {
- loading: true,
- templates: [],
- activeChallenges: [],
- historyChallenges: [],
- familyMembers: [],
- selectedMemberIds: [],
- showModal: false,
- showDetailModal: false,
- detailChallenge: null,
- showMemberPrompt: false,
- showAddChildPrompt: false,
- editMode: false,
- editingId: null,
- editingStatus: null,
- form: {
- title: '',
- description: '',
- challengeType: 'custom',
- targetMode: 'all_members',
- targetValue: '7',
- durationDays: '7',
- rewardPoints: '50',
- respondDeadlineHours: '48'
- },
- isFreeMember: false
- }
- },
- computed: {
- detailStatusClass: function() {
- if (!this.detailChallenge) return ''
- if (this.detailChallenge.status === 'active') return 'status-active'
- if (this.detailChallenge.status === 'pending') return 'status-pending'
- if (this.detailChallenge.status === 'completed') return 'status-completed'
- if (this.detailChallenge.status === 'cancelled') return 'status-cancelled'
- return 'status-active'
- },
- detailStatusText: function() {
- if (!this.detailChallenge) return ''
- if (this.detailChallenge.status === 'active') return '进行中'
- if (this.detailChallenge.status === 'pending') return '待同意'
- if (this.detailChallenge.status === 'completed') return '已完成'
- if (this.detailChallenge.status === 'cancelled') return '已取消'
- return '进行中'
- },
- detailAgreedCount: function() {
- if (!this.detailChallenge || !this.detailChallenge.memberProgress) return 0
- var count = 0
- for (var i = 0; i < this.detailChallenge.memberProgress.length; i++) {
- if (this.detailChallenge.memberProgress[i].status === 'agreed') count++
- }
- return count
- },
- detailTotalCount: function() {
- if (!this.detailChallenge || !this.detailChallenge.memberProgress) return 0
- return this.detailChallenge.memberProgress.length
- },
- detailProgressPercent: function() {
- if (!this.detailChallenge || this.detailChallenge.targetValue <= 0) return 0
- return Math.round(this.detailChallenge.totalProgress / this.detailChallenge.targetValue * 100)
- },
- detailDeadlinePassed: function() {
- if (!this.detailChallenge || !this.detailChallenge.respondDeadline) return false
- return new Date().getTime() > new Date(this.detailChallenge.respondDeadline).getTime()
- },
- creatorCanOperate: function() {
- if (!this.detailChallenge) return false
- return String(this.detailChallenge.creatorId) === String(this.$store.state.userId)
- },
- detailMemberStatusLabel: function(status) {
- var map = { pending: '待确认', agreed: '已同意', rejected: '已拒绝' }
- return map[status] || status
- }
- },
- onLoad(options) {
- this._pendingChallengeId = options && options.id
- this.loadTemplates()
- this.loadMembers()
- this.checkMembership()
- this.loadData()
- this._onLoadFired = true
- },
- onShow() {
- if (this._onLoadFired) {
- this._onLoadFired = false
- } else {
- this.loadData()
- }
- },
- methods: {
- goToDoa() {
- // 当前即共同挑战页
- },
- goToDoaIndex() {
- uni.navigateTo({ url: '/pages/health/doa-index' })
- },
- async loadData() {
- this.loading = true
- try {
- const [res1, res2] = await Promise.all([
- getChallengeList({}),
- getChallengeHistory({})
- ])
- this.activeChallenges = Array.isArray(res1.data) ? res1.data : []
- this.historyChallenges = Array.isArray(res2.data) ? res2.data : []
- this.openPendingChallengeIfNeeded()
- } catch (e) {
- this.activeChallenges = []
- this.historyChallenges = []
- } finally {
- this.loading = false
- }
- },
- // 从首页卡片点击进入时,自动展开对应挑战详情弹窗
- openPendingChallengeIfNeeded() {
- var id = this._pendingChallengeId
- if (!id) return
- this._pendingChallengeId = null
- var found = null
- for (var i = 0; i < this.activeChallenges.length; i++) {
- if (String(this.activeChallenges[i].id) === String(id)) { found = this.activeChallenges[i]; break }
- }
- if (!found) {
- for (var j = 0; j < this.historyChallenges.length; j++) {
- if (String(this.historyChallenges[j].id) === String(id)) { found = this.historyChallenges[j]; break }
- }
- }
- if (found) this.viewChallenge(found)
- },
- async loadTemplates() {
- try {
- const res = await getChallengeTemplates()
- if (res.code === 200) {
- this.templates = (res.data || []).map(t => ({
- ...t,
- emoji: this.getEmoji(t.type)
- }))
- }
- } catch (e) {
- this.templates = [
- { type: 'full_checkin', title: '全员打卡', emoji: '✅', targetMode: 'all_members', targetValue: 3, durationDays: 3, rewardPoints: 50 },
- { type: 'sports', title: '运动PK', emoji: '🏃', targetMode: 'aggregate', targetValue: 300, durationDays: 7, rewardPoints: 100 },
- { type: 'reading', title: '阅读挑战', emoji: '📚', targetMode: 'aggregate', targetValue: 200, durationDays: 7, rewardPoints: 80 },
- { type: 'gratitude', title: '感恩日记', emoji: '🙏', targetMode: 'all_members', targetValue: 7, durationDays: 7, rewardPoints: 40 }
- ]
- }
- },
- getEmoji(type) {
- const map = {
- full_checkin: '✅', sports: '🏃', reading: '📚',
- gratitude: '🙏', no_screen: '📵', custom: '✏️'
- }
- return map[type] || '🎯'
- },
- async loadMembers() {
- try {
- const res = await getFamilyMemberList()
- this.familyMembers = Array.isArray(res.data) ? res.data : []
- this.selectedMemberIds = (this.familyMembers || []).map(m => m.id)
- } catch (e) {
- this.familyMembers = []
- this.selectedMemberIds = []
- }
- },
- async checkMembership() {
- try {
- const res = await getMyMembership()
- const m = res.data || {}
- const level = m.memberLevel || m.levelCode || ''
- // 免费会员:level 为空或 FREE
- this.isFreeMember = !level || level === 'FREE'
- } catch (e) {
- this.isFreeMember = true // 默认按免费处理,保守策略
- }
- },
- isMemberSelected(id) {
- return this.selectedMemberIds.indexOf(id) >= 0
- },
- toggleMember(id) {
- var idx = this.selectedMemberIds.indexOf(id)
- if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
- else this.selectedMemberIds.push(id)
- },
- selectTemplate(tpl) {
- this.form.challengeType = tpl ? tpl.type : 'custom'
- if (tpl) {
- this.form.targetMode = tpl.targetMode || 'all_members'
- this.form.targetValue = String(tpl.targetValue || 7)
- this.form.durationDays = String(tpl.durationDays || 7)
- this.form.rewardPoints = String(tpl.rewardPoints || 50)
- this.form.title = tpl.title || ''
- this.form.description = tpl.description || ''
- } else {
- this.form.targetMode = 'all_members'
- this.form.targetValue = '7'
- this.form.durationDays = '7'
- this.form.rewardPoints = '50'
- this.form.title = ''
- this.form.description = ''
- }
- },
- openCreateModal() {
- // 免费会员且只有1个成员(自己)时,引导先添加孩子
- if (this.familyMembers.length < 2) {
- if (this.isFreeMember && this.familyMembers.length === 1) {
- this.showAddChildPrompt = true
- } else {
- this.showMemberPrompt = true
- }
- return
- }
- this.editMode = false
- this.editingId = null
- this.form = {
- title: '', description: '', challengeType: 'custom',
- targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '50',
- respondDeadlineHours: '48'
- }
- this.selectedMemberIds = this.familyMembers.map(m => m.id)
- this.showModal = true
- },
- openEditModal(ch) {
- this.editMode = true
- this.editingId = ch.id
- this.editingStatus = ch.status
- this.form = {
- title: ch.title || '',
- description: ch.description || '',
- challengeType: ch.challengeType || 'custom',
- targetMode: ch.targetMode || 'all_members',
- targetValue: String(ch.targetValue || 7),
- durationDays: String(ch.durationDays || 7),
- rewardPoints: String(ch.rewardPoints || 50)
- }
- // 编辑时预填充参与成员:从 memberProgress 读取所有状态的成员
- var selected = []
- if (ch.memberProgress && ch.memberProgress.length) {
- for (var i = 0; i < ch.memberProgress.length; i++) {
- selected.push(ch.memberProgress[i].memberId)
- }
- }
- this.selectedMemberIds = selected
- this.showModal = true
- },
- closeModal() {
- this.showModal = false
- },
- async submitForm() {
- if (!this.form.title.trim()) {
- uni.showToast({ title: '请填写挑战标题', icon: 'none' })
- return
- }
- var targetValue = parseInt(this.form.targetValue || 0, 10)
- if (targetValue <= 0) {
- uni.showToast({ title: '目标值必须大于0', icon: 'none' })
- return
- }
- if (!this.editMode && this.selectedMemberIds.length < 2) {
- uni.showToast({ title: '家庭挑战至少需要选择2名成员参与', icon: 'none' })
- return
- }
- var data = {
- title: this.form.title.trim(),
- description: this.form.description.trim(),
- challengeType: this.form.challengeType,
- targetMode: this.form.targetMode,
- targetValue: targetValue,
- durationDays: parseInt(this.form.durationDays || 7, 10),
- rewardPoints: parseInt(this.form.rewardPoints || 0, 10)
- }
- if (!this.editMode || this.editingStatus === 'pending') data.participantMemberIds = this.selectedMemberIds
- if (!this.editMode && this.form.respondDeadlineHours) data.respondDeadlineHours = parseInt(this.form.respondDeadlineHours, 10)
- this.loading = true
- try {
- var promise
- if (this.editMode) {
- promise = updateChallenge(this.editingId, data)
- } else {
- promise = createChallenge(data)
- }
- var res = await promise
- if (res.code === 200) {
- uni.showToast({ title: this.editMode ? '保存成功' : '创建成功', icon: 'success' })
- this.closeModal()
- this.loadData()
- } else {
- uni.showToast({ title: res.message || '操作失败', icon: 'none' })
- }
- } catch (e) {
- uni.showToast({ title: '操作失败', icon: 'none' })
- } finally {
- this.loading = false
- }
- },
- deleteChallenge(id) {
- uni.showModal({
- title: '确认删除',
- content: '确定要取消这个挑战吗?',
- success: (r) => {
- if (r.confirm) {
- deleteChallenge(id).then((res) => {
- if (res.code === 200) {
- uni.showToast({ title: '已删除', icon: 'success' })
- this.loadData()
- } else {
- uni.showToast({ title: res.message || '删除失败', icon: 'none' })
- }
- }).catch(() => uni.showToast({ title: '删除失败', icon: 'none' }))
- }
- }
- })
- },
- viewChallenge(ch) {
- // 打开挑战详情弹窗
- this.detailChallenge = ch
- this.showDetailModal = true
- },
- closeDetailModal() {
- this.showDetailModal = false
- this.detailChallenge = null
- },
- async onDetailRespond(action) {
- var self = this
- uni.showModal({
- title: '确认操作',
- content: action === 'agree' ? '确认同意参与该挑战吗?' : '确认拒绝该挑战吗?',
- success: function(res) {
- if (res.confirm) {
- respondChallenge({ challengeId: self.detailChallenge.id, action: action }).then(function(resp) {
- if (resp && resp.code === 200) {
- uni.showToast({ title: action === 'agree' ? '已同意' : '已拒绝', icon: 'success' })
- self.closeDetailModal()
- self.loadData()
- } else {
- uni.showToast({ title: (resp && resp.message) || '操作失败', icon: 'none' })
- }
- }).catch(function() {
- uni.showToast({ title: '操作失败,请重试', icon: 'none' })
- })
- }
- }
- })
- },
- confirmDeleteDetail() {
- var self = this
- if (!this.detailChallenge) return
- uni.showModal({
- title: '确认删除',
- content: '确定要取消这个挑战吗?',
- success: function(r) {
- if (r.confirm) {
- deleteChallenge(self.detailChallenge.id).then(function(res) {
- if (res.code === 200) {
- uni.showToast({ title: '已删除', icon: 'success' })
- self.closeDetailModal()
- self.loadData()
- } else {
- uni.showToast({ title: res.message || '删除失败', icon: 'none' })
- }
- }).catch(function() {
- uni.showToast({ title: '删除失败', icon: 'none' })
- })
- }
- }
- })
- },
- goToAddMember() {
- this.showMemberPrompt = false
- uni.navigateTo({ url: '/pages/profile-extra/family-members' })
- },
- goToAddChild() {
- this.showAddChildPrompt = false
- uni.navigateTo({ url: '/pages/family/add-member' })
- }
- }
- }
- </script>
- <style scoped>
- .doa-tabs {
- display: flex;
- background: #fff;
- border-radius: 16rpx;
- margin-bottom: 20rpx;
- overflow: hidden;
- border: 2rpx solid #E2E8F0;
- }
- .doa-tab {
- flex: 1;
- text-align: center;
- padding: 20rpx 0;
- font-size: 28rpx;
- font-weight: 600;
- color: #64748B;
- background: #fff;
- }
- .active-tab {
- background: #10B981;
- color: #fff;
- }
- .page {
- min-height: 100vh;
- background: #F5F7FA;
- padding: 24rpx;
- padding-bottom: 60rpx;
- }
- .content {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
- }
- /* 概览卡片 */
- .overview-card {
- background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
- border-radius: 20rpx;
- padding: 28rpx;
- border: 2rpx solid #FED7AA;
- }
- .overview-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20rpx;
- }
- .overview-title {
- font-size: 30rpx;
- font-weight: 700;
- color: #92400E;
- }
- .overview-count {
- font-size: 24rpx;
- color: #B45309;
- }
- .active-list {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- .active-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: rgba(255,255,255,0.7);
- border-radius: 12rpx;
- padding: 16rpx 20rpx;
- }
- .active-item-title {
- font-size: 28rpx;
- font-weight: 600;
- color: #7C2D12;
- }
- .active-item-meta {
- font-size: 24rpx;
- color: #B45309;
- }
- /* 操作栏 */
- .action-bar {
- display: flex;
- justify-content: flex-end;
- }
- .btn-new {
- display: flex;
- align-items: center;
- gap: 8rpx;
- background: linear-gradient(135deg, #10B981, #34D399);
- color: #fff;
- font-size: 26rpx;
- font-weight: 600;
- padding: 16rpx 32rpx;
- border-radius: 40rpx;
- border: none;
- }
- .btn-new-icon {
- font-size: 32rpx;
- line-height: 1;
- }
- /* 列表区 */
- .section {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .section-title {
- font-size: 30rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .group-label {
- font-size: 26rpx;
- font-weight: 600;
- color: #64748B;
- margin-bottom: 12rpx;
- }
- .card-list {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- .card {
- background: #fff;
- border-radius: 16rpx;
- padding: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .card-main {
- flex: 1;
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10rpx;
- }
- .card-title {
- font-size: 28rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .card-status {
- font-size: 22rpx;
- padding: 4rpx 12rpx;
- border-radius: 20rpx;
- }
- .status-active {
- background: #DCFCE7;
- color: #16A34A;
- }
- .status-done {
- background: #F1F5F9;
- color: #64748B;
- }
- .card-meta {
- display: flex;
- gap: 16rpx;
- flex-wrap: wrap;
- }
- .meta-item {
- font-size: 22rpx;
- color: #64748B;
- background: #F8FAFC;
- padding: 4rpx 12rpx;
- border-radius: 8rpx;
- }
- .card-actions {
- display: flex;
- gap: 12rpx;
- margin-left: 16rpx;
- }
- .action-btn {
- font-size: 24rpx;
- padding: 8rpx 16rpx;
- border-radius: 8rpx;
- }
- .action-btn.edit {
- color: #5B9BD5;
- background: rgba(91,155,213,0.1);
- }
- .action-btn.del {
- color: #EF4444;
- background: rgba(239,68,68,0.1);
- }
- /* 空状态 */
- .empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 80rpx 0;
- }
- .empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
- .empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
- .empty-desc { font-size: 26rpx; color: #94A3B8; margin: 8rpx 0 32rpx; }
- .btn-empty {
- background: #10B981;
- color: #fff;
- font-size: 28rpx;
- font-weight: 600;
- padding: 16rpx 48rpx;
- border-radius: 40rpx;
- border: none;
- }
- /* 弹窗 */
- .overlay {
- position: fixed;
- top: 0; left: 0; right: 0; bottom: 0;
- background: rgba(0,0,0,0.5);
- display: flex;
- align-items: flex-end;
- z-index: 999;
- }
- .modal {
- width: 100%;
- background: #fff;
- border-radius: 32rpx 32rpx 0 0;
- padding: 32rpx 28rpx 40rpx;
- box-sizing: border-box;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- }
- .modal-title {
- font-size: 32rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .modal-close {
- font-size: 44rpx;
- color: #94A3B8;
- line-height: 1;
- }
- .form-body {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- flex: 1;
- overflow-y: auto;
- margin-bottom: 24rpx;
- }
- /* 模板行 */
- .template-row {
- display: flex;
- align-items: center;
- gap: 12rpx;
- margin-bottom: 4rpx;
- }
- .tpl-label {
- font-size: 26rpx;
- color: #475569;
- font-weight: 600;
- white-space: nowrap;
- }
- .tpl-scroll {
- flex: 1;
- }
- .tpl-list {
- display: flex;
- gap: 12rpx;
- }
- .tpl-item {
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6rpx;
- padding: 14rpx 18rpx;
- background: #F8FAFC;
- border-radius: 14rpx;
- border: 2rpx solid transparent;
- }
- .tpl-item.active {
- border-color: #10B981;
- background: #DCFCE7;
- }
- .tpl-icon { font-size: 32rpx; }
- .tpl-name { font-size: 22rpx; color: #475569; font-weight: 500; }
- .tpl-item.active .tpl-name { color: #16A34A; }
- /* 表单字段 */
- .field {
- display: flex;
- flex-direction: column;
- gap: 8rpx;
- }
- .field-label {
- font-size: 26rpx;
- color: #475569;
- font-weight: 600;
- }
- .required { color: #EF4444; }
- .field-input {
- background: #F8FAFC;
- border: 1rpx solid #E2E8F0;
- border-radius: 12rpx;
- padding: 16rpx 18rpx;
- font-size: 28rpx;
- color: #1E293B;
- }
- .field-textarea {
- background: #F8FAFC;
- border: 1rpx solid #E2E8F0;
- border-radius: 12rpx;
- padding: 16rpx 18rpx;
- font-size: 28rpx;
- color: #1E293B;
- height: 100rpx;
- width: 100%;
- box-sizing: border-box;
- }
- .field-row {
- display: flex;
- gap: 16rpx;
- }
- .field.half { flex: 1; }
- /* 单选组 */
- .radio-group {
- display: flex;
- gap: 12rpx;
- }
- .radio-btn {
- flex: 1;
- text-align: center;
- padding: 12rpx 0;
- border-radius: 12rpx;
- font-size: 26rpx;
- color: #475569;
- background: #F1F5F9;
- }
- .radio-btn.active {
- background: #10B981;
- color: #fff;
- font-weight: 600;
- }
- /* 成员选择 */
- .member-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 12rpx;
- }
- .member-item {
- padding: 10rpx 22rpx;
- border-radius: 24rpx;
- font-size: 24rpx;
- color: #475569;
- background: #F1F5F9;
- border: 2rpx solid transparent;
- }
- .member-item.active {
- background: #DCFCE7;
- color: #16A34A;
- border-color: #10B981;
- }
- /* 详情弹窗 */
- .detail-modal {
- max-width: 90vw;
- padding: 32rpx 28rpx;
- background: #fff;
- border-radius: 24rpx;
- box-shadow: 0 8rpx 48rpx rgba(0,0,0,0.12);
- }
- .detail-modal .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- padding-bottom: 16rpx;
- border-bottom: 2rpx solid #E2E8F0;
- }
- .detail-modal .modal-title {
- font-size: 34rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .detail-modal .modal-close {
- font-size: 30rpx;
- color: #64748B;
- cursor: pointer;
- }
- .detail-modal .modal-close:hover {
- color: #DC2626;
- }
- .detail-body {
- font-size: 26rpx;
- line-height: 1.6;
- color: #475569;
- }
- .detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12rpx;
- }
- .detail-header .detail-title {
- font-size: 30rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .detail-header .detail-badge {
- font-size: 22rpx;
- padding: 2rpx 8rpx;
- border-radius: 999rpx;
- }
- .detail-header .status-active { background: #DCFCE7; color: #16A34A; }
- .detail-header .status-pending { background: #FEF3C7; color: #B45309; }
- .detail-header .status-completed { background: #F1F5F9; color: #64748B; }
- .detail-header .status-cancelled { background: #FEE2E2; color: #DC2626; }
- .detail-header .status-not-launched { background: #E5E7EB; color: #6B7280; }
- .detail-desc {
- font-size: 24rpx;
- color: #64748B;
- line-height: 1.4;
- margin-bottom: 16rpx;
- display: block;
- }
- .detail-target {
- margin-top: 16rpx;
- }
- .detail-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 8rpx;
- }
- .detail-row .detail-label {
- font-size: 24rpx;
- color: #647489;
- font-weight: 500;
- }
- .detail-row .detail-value {
- font-size: 26rpx;
- color: #10B981;
- font-weight: 600;
- }
- .detail-row .detail-value-danger {
- font-size: 26rpx;
- color: #DC2626;
- }
- .detail-progress {
- margin-top: 16rpx;
- padding-top: 16rpx;
- border-top: 2rpx solid #E2E8F0;
- }
- .detail-progress-title {
- font-size: 24rpx;
- color: #64748B;
- margin-bottom: 12rpx;
- }
- .detail-progress-bar-wrap {
- height: 10rpx;
- background: #E2E8F0;
- border-radius: 999rpx;
- overflow: hidden;
- margin-bottom: 8rpx;
- }
- .detail-progress-bar-fill {
- height: 100%;
- background: linear-gradient(90deg, #10B981, #34D399);
- border-radius: 999rpx;
- transition: width 0.3s ease;
- }
- .detail-progress-text {
- display: flex;
- justify-content: space-between;
- font-size: 22rpx;
- color: #64748B;
- }
- .detail-member-list {
- margin-top: 12rpx;
- }
- .detail-member-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8rpx 0;
- border-bottom: 1rpx solid #F1F5F9;
- font-size: 24rpx;
- }
- .detail-member-name {
- color: #1E293B;
- font-weight: 500;
- }
- .detail-member-val {
- color: #64748B;
- font-weight: 400;
- }
- .detail-member-status {
- padding: 2rpx 6rpx;
- border-radius: 999rpx;
- font-size: 20rpx;
- font-weight: 400;
- }
- .detail-member-pending { background: #FEF3C7; color: #B45309; }
- .detail-member-agreed { background: #DCFCE7; color: #16A34A; }
- .detail-member-rejected { background: #FEE2E2; color: #DC2626; }
- .detail-action {
- display: flex;
- gap: 12rpx;
- margin-top: 20rpx;
- }
- .detail-action .respond-btn {
- flex: 1;
- height: 72rpx;
- line-height: 72rpx;
- font-size: 28rpx;
- font-weight: 500;
- }
- .detail-action .btn-edit-detail {
- flex: 1;
- height: 72rpx;
- line-height: 72rpx;
- font-size: 28rpx;
- background: #F3F4F6;
- color: #374151;
- border-radius: 12rpx;
- }
- .detail-action .btn-delete-detail {
- flex: 1;
- height: 72rpx;
- line-height: 72rpx;
- font-size: 28rpx;
- background: #FEE2E2;
- color: #DC2626;
- border-radius: 12rpx;
- }
- .detail-label-reward {
- color: #F59E0B;
- font-weight: 500;
- }
- .detail-label-danger {
- color: #DC2626;
- }
- /* 底部按钮 */
- .modal-footer {
- display: flex;
- gap: 16rpx;
- padding-top: 8rpx;
- }
- .btn-cancel, .btn-submit {
- flex: 1;
- height: 84rpx;
- line-height: 84rpx;
- border-radius: 42rpx;
- font-size: 30rpx;
- font-weight: 600;
- border: none;
- }
- .btn-cancel {
- background: #F1F5F9;
- color: #64748B;
- }
- .btn-submit {
- background: linear-gradient(135deg, #10B981, #34D399);
- color: #fff;
- }
- /* 家庭成员不足提示弹窗 */
- .prompt-modal {
- max-height: 60vh;
- padding: 32rpx 28rpx 40rpx;
- }
- .prompt-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 24rpx 0 16rpx;
- }
- .prompt-icon {
- font-size: 80rpx;
- margin-bottom: 16rpx;
- }
- .prompt-title {
- font-size: 32rpx;
- font-weight: 700;
- color: #1E293B;
- margin-bottom: 12rpx;
- }
- .prompt-desc {
- font-size: 26rpx;
- color: #64748B;
- text-align: center;
- line-height: 1.6;
- }
- </style>
|