| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766 |
- <template>
- <view class="page">
- <BaseLoading :loading="loading" text="加载中..." />
- <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" v-if="!editMode">
- <text class="field-label">参与成员</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>
- </template>
- <script>
- import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, listFamilyMembers } from '../../utils/api.js'
- export default {
- data() {
- return {
- loading: true,
- templates: [],
- activeChallenges: [],
- historyChallenges: [],
- familyMembers: [],
- selectedMemberIds: [],
- showModal: false,
- editMode: false,
- editingId: null,
- form: {
- title: '',
- description: '',
- challengeType: 'custom',
- targetMode: 'all_members',
- targetValue: '7',
- durationDays: '7',
- rewardPoints: '50'
- }
- }
- },
- onLoad() {
- this.loadTemplates()
- this.loadMembers()
- this.loadData()
- },
- onShow() {
- this.loadData()
- },
- methods: {
- 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 : []
- } catch (e) {
- this.activeChallenges = []
- this.historyChallenges = []
- } finally {
- this.loading = false
- }
- },
- 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 listFamilyMembers()
- this.familyMembers = Array.isArray(res.data) ? res.data : []
- this.selectedMemberIds = (this.familyMembers || []).map(m => m.id)
- } catch (e) {
- this.familyMembers = []
- this.selectedMemberIds = []
- }
- },
- 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() {
- this.editMode = false
- this.editingId = null
- this.form = {
- title: '', description: '', challengeType: 'custom',
- targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '50'
- }
- this.showModal = true
- },
- openEditModal(ch) {
- this.editMode = true
- this.editingId = ch.id
- 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)
- }
- 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
- }
- 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) data.participantMemberIds = this.selectedMemberIds
- 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) {
- // 跳转到挑战详情
- uni.navigateTo({ url: '/pages/activity/index' })
- }
- }
- }
- </script>
- <style scoped>
- .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;
- }
- /* 底部按钮 */
- .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;
- }
- </style>
|