| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- <template>
- <view class="challenge-manage-page">
- <BaseLoading :loading="loading" text="加载中..." />
- <view v-if="!loading" class="page-content">
- <!-- 模板推荐区 -->
- <view class="template-section">
- <view class="section-title-row">
- <text class="section-title">快速创建</text>
- <text class="section-hint">选择模板,自定义参数</text>
- </view>
- <scroll-view class="template-scroll" scroll-x>
- <view class="template-scroll-inner">
- <view class="template-card" v-for="tpl in templates" :key="tpl.type" @click="createFromTemplate(tpl)">
- <view class="template-icon">{{ getTemplateEmoji(tpl.type) }}</view>
- <text class="template-name">{{ tpl.title }}</text>
- </view>
- <view class="template-card template-card-custom" @click="createFromTemplate(null)">
- <view class="template-icon">✏️</view>
- <text class="template-name">自定义</text>
- </view>
- </view>
- </scroll-view>
- </view>
- <!-- 挑战列表 -->
- <view class="list-section">
- <view class="section-title-row">
- <text class="section-title">我的挑战</text>
- <text class="list-add-btn" @click="openCreateModal(null)">+ 新建</text>
- </view>
- <!-- 进行中 -->
- <view class="group" v-if="activeChallenges.length > 0">
- <text class="group-label">进行中</text>
- <view class="challenge-item" v-for="ch in activeChallenges" :key="ch.id" @click="viewChallenge(ch)">
- <view class="challenge-item-header">
- <text class="challenge-item-title">{{ ch.title }}</text>
- <view class="challenge-item-actions">
- <text class="action-btn action-edit" @click.stop="openEditModal(ch)">编辑</text>
- <text class="action-btn action-delete" @click.stop="deleteChallenge(ch.id)">删除</text>
- </view>
- </view>
- <text class="challenge-item-desc" v-if="ch.description">{{ ch.description }}</text>
- <view class="challenge-item-footer">
- <text class="tag">{{ ch.targetMode === 'aggregate' ? '全家目标' : '每人目标' }}</text>
- <text class="tag tag-days">{{ ch.durationDays }}天</text>
- <text class="tag tag-points" v-if="ch.rewardPoints">🏆 +{{ ch.rewardPoints }}分</text>
- </view>
- <FamilyChallengeCard :challenge="ch" />
- </view>
- </view>
- <!-- 已完成 -->
- <view class="group" v-if="historyChallenges.length > 0">
- <text class="group-label">已完成</text>
- <view class="challenge-item" v-for="ch in historyChallenges" :key="ch.id" @click="viewChallenge(ch)">
- <view class="challenge-item-header">
- <text class="challenge-item-title">{{ ch.title }}</text>
- <text class="status-badge status-completed">已完成</text>
- </view>
- <text class="challenge-item-desc" v-if="ch.description">{{ ch.description }}</text>
- </view>
- </view>
- <!-- 空状态 -->
- <view class="empty-state" v-if="activeChallenges.length === 0 && historyChallenges.length === 0">
- <text class="empty-icon">🎯</text>
- <text class="empty-title">还没有挑战</text>
- <text class="empty-desc">创建第一个家庭挑战,让全家一起成长!</text>
- <view class="empty-btn" @click="openCreateModal(null)">立即创建</view>
- </view>
- </view>
- </view>
- <!-- 创建/编辑弹窗 -->
- <view class="modal-overlay" v-if="showModal" @click="closeModal">
- <view class="modal-content" @click.stop>
- <view class="modal-header">
- <text class="modal-title">{{ editMode ? '编辑挑战' : '创建挑战' }}</text>
- <text class="modal-close" @click="closeModal">×</text>
- </view>
- <view class="modal-body">
- <view class="form-item">
- <text class="form-label">挑战标题 *</text>
- <input class="form-input" v-model="form.title" placeholder="输入挑战标题" maxlength="50" />
- </view>
- <view class="form-item">
- <text class="form-label">挑战描述</text>
- <textarea class="form-textarea" v-model="form.description" placeholder="描述挑战内容" maxlength="200" />
- </view>
- <view class="form-item">
- <text class="form-label">挑战类型</text>
- <view class="type-picker">
- <text class="type-item" :class="{ active: form.challengeType === t.type }"
- v-for="t in templates" :key="t.type"
- @click="form.challengeType = t.type">{{ t.title }}</text>
- </view>
- </view>
- <view class="form-row">
- <view class="form-item" style="flex:1">
- <text class="form-label">目标模式 *</text>
- <view class="mode-picker">
- <text class="mode-item" :class="{ active: form.targetMode === 'aggregate' }"
- @click="form.targetMode = 'aggregate'">全家合计</text>
- <text class="mode-item" :class="{ active: form.targetMode === 'all_members' }"
- @click="form.targetMode = 'all_members'">每人目标</text>
- </view>
- </view>
- <view class="form-item" style="flex:1">
- <text class="form-label">目标值 *</text>
- <input class="form-input" type="digit" v-model="form.targetValue" placeholder="如: 300" />
- </view>
- </view>
- <view class="form-row">
- <view class="form-item" style="flex:1">
- <text class="form-label">持续天数</text>
- <input class="form-input" type="number" v-model="form.durationDays" placeholder="7" />
- </view>
- <view class="form-item" style="flex:1">
- <text class="form-label">奖励积分</text>
- <input class="form-input" type="number" v-model="form.rewardPoints" placeholder="50" />
- </view>
- </view>
- </view>
- <view class="modal-footer">
- <view class="modal-btn modal-btn-cancel" @click="closeModal">取消</view>
- <view class="modal-btn modal-btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, getParentDashboard } from '../../utils/api.js'
- import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
- export default {
- components: { FamilyChallengeCard },
- data() {
- return {
- loading: true,
- familyId: null,
- templates: [],
- activeChallenges: [],
- historyChallenges: [],
- showModal: false,
- editMode: false,
- editingId: null,
- form: {
- title: '',
- description: '',
- challengeType: 'custom',
- targetMode: 'all_members',
- targetValue: '7',
- durationDays: '7',
- rewardPoints: '50'
- }
- }
- },
- onLoad() {
- this.loadFamilyId()
- this.loadData()
- this.loadTemplates()
- },
- onShow() {
- if (this.familyId) {
- this.loadData()
- }
- },
- methods: {
- loadFamilyId: function() {
- var self = this
- try {
- var dashPromise = getParentDashboard()
- dashPromise.then(function(res) {
- var data = res.data || {}
- if (data.familyId) {
- self.familyId = data.familyId
- }
- }).catch(function() {})
- } catch (e) {
- console.log('获取familyId失败', e)
- }
- },
- loadData: function() {
- var self = this
- if (!self.familyId) return
- self.loading = true
- Promise.all([
- getChallengeList({}),
- getChallengeHistory({})
- ]).then(function(results) {
- self.activeChallenges = Array.isArray(results[0].data) ? results[0].data : []
- self.historyChallenges = Array.isArray(results[1].data) ? results[1].data : []
- }).catch(function() {
- self.activeChallenges = []
- self.historyChallenges = []
- }).finally(function() {
- self.loading = false
- })
- },
- loadTemplates: function() {
- var self = this
- getChallengeTemplates().then(function(res) {
- if (res.code === 200) {
- self.templates = res.data || []
- }
- }).catch(function() {
- self.templates = [
- { type: 'full_checkin', title: '全员打卡', targetMode: 'all_members', targetValue: '3', durationDays: '3', rewardPoints: '50', description: '全家连续打卡' },
- { type: 'sports', title: '运动PK', targetMode: 'aggregate', targetValue: '300', durationDays: '7', rewardPoints: '100', description: '全家运动时长' },
- { type: 'reading', title: '阅读挑战', targetMode: 'aggregate', targetValue: '200', durationDays: '7', rewardPoints: '80', description: '全家阅读时长' },
- { type: 'gratitude', title: '感恩日记', targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '40', description: '每天感恩记录' }
- ]
- })
- },
- getTemplateEmoji: function(type) {
- var map = {
- full_checkin: '✅',
- sports: '🏃',
- health_week: '💚',
- reading: '📚',
- no_screen: '📵',
- gratitude: '🙏',
- custom: '✏️'
- }
- return map[type] || '🎯'
- },
- createFromTemplate: function(template) {
- this.editMode = false
- this.editingId = null
- this.form = {
- title: template ? template.title : '',
- description: template ? template.description : '',
- challengeType: template ? template.type : 'custom',
- targetMode: template ? template.targetMode : 'all_members',
- targetValue: template ? String(template.targetValue) : '7',
- durationDays: template ? String(template.durationDays) : '7',
- rewardPoints: template ? String(template.rewardPoints) : '50'
- }
- this.showModal = true
- },
- openCreateModal: function(template) {
- this.createFromTemplate(template)
- },
- openEditModal: function(challenge) {
- this.editMode = true
- this.editingId = challenge.id
- this.form = {
- title: challenge.title || '',
- description: challenge.description || '',
- challengeType: challenge.challengeType || 'custom',
- targetMode: challenge.targetMode || 'all_members',
- targetValue: String(challenge.targetValue || '7'),
- durationDays: String(challenge.durationDays || '7'),
- rewardPoints: String(challenge.rewardPoints || '50')
- }
- this.showModal = true
- },
- closeModal: function() {
- this.showModal = false
- },
- submitForm: function() {
- var self = this
- var title = self.form.title && self.form.title.trim()
- if (!title) {
- uni.showToast({ title: '请填写挑战标题', icon: 'none' })
- return
- }
- var targetValue = parseInt(self.form.targetValue || '0', 10)
- if (targetValue <= 0) {
- uni.showToast({ title: '目标值必须大于0', icon: 'none' })
- return
- }
- var data = {
- title: title,
- description: self.form.description && self.form.description.trim(),
- challengeType: self.form.challengeType,
- targetMode: self.form.targetMode,
- targetValue: targetValue,
- durationDays: parseInt(self.form.durationDays || '7', 10),
- rewardPoints: parseInt(self.form.rewardPoints || '0', 10)
- }
- self.loading = true
- var promise
- if (self.editMode && self.editingId) {
- promise = updateChallenge(self.editingId, data)
- } else {
- promise = createChallenge(data)
- }
- promise.then(function(res) {
- if (res.code === 200) {
- var msg = self.editMode ? '更新成功' : '创建成功'
- uni.showToast({ title: msg, icon: 'success' })
- self.closeModal()
- self.loadData()
- } else {
- uni.showToast({ title: res.message || '操作失败', icon: 'none' })
- }
- }).catch(function(e) {
- uni.showToast({ title: '操作失败', icon: 'none' })
- console.log('挑战操作失败', e)
- }).finally(function() {
- self.loading = false
- })
- },
- deleteChallenge: function(id) {
- var self = this
- uni.showModal({
- title: '确认删除',
- content: '确定要取消这个挑战吗?',
- success: function(r) {
- if (r.confirm) {
- deleteChallenge(id).then(function(res) {
- if (res.code === 200) {
- uni.showToast({ title: '已删除', icon: 'success' })
- self.loadData()
- } else {
- uni.showToast({ title: res.message || '删除失败', icon: 'none' })
- }
- }).catch(function() {
- uni.showToast({ title: '删除失败', icon: 'none' })
- })
- }
- }
- })
- },
- viewChallenge: function(challenge) {
- uni.showToast({ title: challenge.title, icon: 'none' })
- }
- }
- }
- </script>
- <style scoped>
- .challenge-manage-page {
- min-height: 100vh;
- background: #F5F9FC;
- padding: 20rpx 24rpx 40rpx;
- }
- .page-content {
- display: flex;
- flex-direction: column;
- gap: 28rpx;
- }
- /* 模板推荐区 */
- .template-section {
- margin-bottom: 8rpx;
- }
- .section-title-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12rpx;
- }
- .section-title {
- font-size: 30rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .section-hint {
- font-size: 22rpx;
- color: #94A3B8;
- }
- .template-scroll {
- width: 100%;
- }
- .template-scroll-inner {
- display: flex;
- gap: 12rpx;
- }
- .template-card {
- flex-shrink: 0;
- width: 130rpx;
- padding: 16rpx 12rpx;
- background: #fff;
- border-radius: 16rpx;
- box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6rpx;
- }
- .template-card:active { opacity: 0.8; }
- .template-card-custom {
- background: linear-gradient(135deg, #10B981, #34D399);
- }
- .template-icon {
- font-size: 40rpx;
- line-height: 1;
- }
- .template-name {
- font-size: 22rpx;
- color: #475569;
- font-weight: 600;
- }
- .template-card-custom .template-name {
- color: #fff;
- }
- /* 列表区 */
- .list-section {
- display: flex;
- flex-direction: column;
- }
- .list-add-btn {
- font-size: 24rpx;
- color: #10B981;
- font-weight: 600;
- }
- .list-add-btn:active { opacity: 0.7; }
- .group {
- margin-bottom: 20rpx;
- }
- .group-label {
- font-size: 26rpx;
- font-weight: 600;
- color: #64748B;
- margin-bottom: 10rpx;
- }
- .challenge-item {
- background: #fff;
- border-radius: 16rpx;
- padding: 16rpx;
- margin-bottom: 12rpx;
- box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
- }
- .challenge-item-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 4rpx;
- }
- .challenge-item-title {
- font-size: 26rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .challenge-item-actions {
- display: flex;
- gap: 12rpx;
- }
- .action-btn {
- font-size: 22rpx;
- padding: 4rpx 12rpx;
- border-radius: 8rpx;
- }
- .action-edit {
- color: #5B9BD5;
- background: rgba(91,155,213,0.1);
- }
- .action-delete {
- color: #EF4444;
- background: rgba(239,68,68,0.1);
- }
- .challenge-item-desc {
- font-size: 22rpx;
- color: #64748B;
- margin-bottom: 6rpx;
- }
- .challenge-item-footer {
- display: flex;
- gap: 8rpx;
- margin-bottom: 4rpx;
- }
- .tag {
- font-size: 20rpx;
- padding: 2rpx 10rpx;
- border-radius: 999rpx;
- background: #E2E8F0;
- color: #475569;
- }
- .tag-days { background: #DBEAFE; color: #2563EB; }
- .tag-points { background: #FEF3C7; color: #D97706; }
- .status-badge {
- font-size: 20rpx;
- padding: 2rpx 10rpx;
- border-radius: 999rpx;
- }
- .status-completed {
- background: #F1F5F9;
- color: #64748B;
- }
- /* 空状态 */
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 60rpx 0 20rpx;
- }
- .empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
- .empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
- .empty-desc { font-size: 24rpx; color: #94A3B8; margin: 8rpx 0 24rpx; }
- .empty-btn {
- background: #10B981;
- color: #fff;
- font-size: 26rpx;
- font-weight: 600;
- padding: 16rpx 40rpx;
- border-radius: 44rpx;
- }
- /* 弹窗 */
- .modal-overlay {
- position: fixed;
- top: 0; left: 0; right: 0; bottom: 0;
- background: rgba(0,0,0,0.5);
- display: flex;
- align-items: flex-end;
- justify-content: center;
- z-index: 999;
- }
- .modal-content {
- width: 100%;
- background: #fff;
- border-radius: 32rpx 32rpx 0 0;
- padding: 24rpx;
- box-sizing: border-box;
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20rpx;
- }
- .modal-title {
- font-size: 32rpx;
- font-weight: 700;
- color: #1E293B;
- }
- .modal-close {
- font-size: 40rpx;
- color: #94A3B8;
- line-height: 1;
- }
- .modal-body {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- margin-bottom: 20rpx;
- max-height: 70vh;
- overflow-y: auto;
- }
- .form-item {
- display: flex;
- flex-direction: column;
- gap: 6rpx;
- }
- .form-label {
- font-size: 24rpx;
- color: #475569;
- font-weight: 600;
- }
- .form-input {
- background: #F8FAFC;
- border: 1rpx solid #E2E8F0;
- border-radius: 12rpx;
- padding: 14rpx 16rpx;
- font-size: 26rpx;
- color: #1E293B;
- }
- .form-textarea {
- background: #F8FAFC;
- border: 1rpx solid #E2E8F0;
- border-radius: 12rpx;
- padding: 14rpx 16rpx;
- font-size: 26rpx;
- color: #1E293B;
- height: 80rpx;
- width: 100%;
- box-sizing: border-box;
- }
- .form-row {
- display: flex;
- gap: 12rpx;
- }
- .type-picker {
- display: flex;
- flex-wrap: wrap;
- gap: 8rpx;
- }
- .type-item {
- font-size: 22rpx;
- padding: 6rpx 14rpx;
- border-radius: 999rpx;
- background: #F1F5F9;
- color: #475569;
- }
- .type-item.active {
- background: #10B981;
- color: #fff;
- font-weight: 600;
- }
- .mode-picker {
- display: flex;
- gap: 8rpx;
- }
- .mode-item {
- flex: 1;
- font-size: 24rpx;
- padding: 10rpx 0;
- border-radius: 12rpx;
- background: #F1F5F9;
- color: #475569;
- text-align: center;
- }
- .mode-item.active {
- background: #10B981;
- color: #fff;
- font-weight: 600;
- }
- .modal-footer {
- display: flex;
- gap: 12rpx;
- }
- .modal-btn {
- flex: 1;
- text-align: center;
- padding: 16rpx 0;
- border-radius: 44rpx;
- font-size: 28rpx;
- font-weight: 600;
- }
- .modal-btn-cancel {
- background: #F1F5F9;
- color: #64748B;
- }
- .modal-btn-submit {
- background: #10B981;
- color: #fff;
- }
- </style>
|