|
@@ -1,142 +1,178 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <view class="challenge-manage-page">
|
|
|
|
|
|
|
+ <view class="page">
|
|
|
<BaseLoading :loading="loading" text="加载中..." />
|
|
<BaseLoading :loading="loading" text="加载中..." />
|
|
|
|
|
|
|
|
- <view v-if="!loading" class="page-content">
|
|
|
|
|
- <!-- 模板推荐区 -->
|
|
|
|
|
- <view class="template-section" v-if="hasFamily">
|
|
|
|
|
- <view class="section-title-row">
|
|
|
|
|
- <text class="section-title">快速创建</text>
|
|
|
|
|
- <text class="section-hint">选择模板,自定义参数</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>
|
|
|
- <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 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>
|
|
|
- </scroll-view>
|
|
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
- <view v-else>
|
|
|
|
|
- <FamilyEmptyState type="card" />
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 操作栏 -->
|
|
|
|
|
+ <view class="action-bar">
|
|
|
|
|
+ <button class="btn-new" @click="openCreateModal">
|
|
|
|
|
+ <text class="btn-new-icon">+</text>
|
|
|
|
|
+ <text>新建挑战</text>
|
|
|
|
|
+ </button>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 挑战列表 -->
|
|
|
|
|
- <view class="list-section" v-if="hasFamily">
|
|
|
|
|
- <view class="section-title-row">
|
|
|
|
|
|
|
+ <!-- 挑战分类列表 -->
|
|
|
|
|
+ <view class="section">
|
|
|
|
|
+ <view class="section-header">
|
|
|
<text class="section-title">我的挑战</text>
|
|
<text class="section-title">我的挑战</text>
|
|
|
- <text class="list-add-btn" @click="openCreateModal(null)">+ 新建</text>
|
|
|
|
|
</view>
|
|
</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 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>
|
|
|
- <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>
|
|
</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 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>
|
|
|
- <text class="challenge-item-desc" v-if="ch.description">{{ ch.description }}</text>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
<!-- 空状态 -->
|
|
|
- <view class="empty-state" v-if="activeChallenges.length === 0 && historyChallenges.length === 0">
|
|
|
|
|
|
|
+ <view v-if="activeChallenges.length === 0 && historyChallenges.length === 0" class="empty">
|
|
|
<text class="empty-icon">🎯</text>
|
|
<text class="empty-icon">🎯</text>
|
|
|
<text class="empty-title">还没有挑战</text>
|
|
<text class="empty-title">还没有挑战</text>
|
|
|
- <text class="empty-desc">创建第一个家庭挑战,让全家一起成长!</text>
|
|
|
|
|
- <view class="empty-btn" @click="openCreateModal(null)">立即创建</view>
|
|
|
|
|
|
|
+ <text class="empty-desc">创建挑战,让全家一起动起来!</text>
|
|
|
|
|
+ <button class="btn-empty" @click="openCreateModal">立即创建</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 创建/编辑弹窗 -->
|
|
<!-- 创建/编辑弹窗 -->
|
|
|
- <view class="modal-overlay" v-if="showModal" @click="closeModal">
|
|
|
|
|
- <view class="modal-content" @click.stop>
|
|
|
|
|
|
|
+ <view class="overlay" v-if="showModal" @click="closeModal">
|
|
|
|
|
+ <view class="modal" @click.stop>
|
|
|
<view class="modal-header">
|
|
<view class="modal-header">
|
|
|
- <text class="modal-title">{{ editMode ? '编辑挑战' : '创建挑战' }}</text>
|
|
|
|
|
|
|
+ <text class="modal-title">{{ editMode ? '编辑挑战' : '新建挑战' }}</text>
|
|
|
<text class="modal-close" @click="closeModal">×</text>
|
|
<text class="modal-close" @click="closeModal">×</text>
|
|
|
</view>
|
|
</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 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>
|
|
</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>
|
|
|
- <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 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>
|
|
</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 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>
|
|
</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 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>
|
|
|
- <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 class="form-item" v-if="!editMode">
|
|
|
|
|
- <text class="form-label">参与成员</text>
|
|
|
|
|
- <view class="member-picker">
|
|
|
|
|
- <text class="member-pick-item" :class="{ active: isMemberSelected(m.id) }"
|
|
|
|
|
- v-for="m in familyMembers" :key="m.id"
|
|
|
|
|
- @click="toggleMember(m.id)">{{ m.nickname }}</text>
|
|
|
|
|
|
|
+ <view class="field half">
|
|
|
|
|
+ <text class="field-label">奖励积分</text>
|
|
|
|
|
+ <input class="field-input" type="number" v-model="form.rewardPoints" placeholder="50" />
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</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">
|
|
<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>
|
|
|
|
|
|
|
+ <button class="btn-cancel" @click="closeModal">取消</button>
|
|
|
|
|
+ <button class="btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -144,15 +180,12 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
-import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, getParentDashboard, listFamilyMembers } from '../../utils/api.js'
|
|
|
|
|
-import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
|
|
|
|
|
|
|
+import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, listFamilyMembers } from '../../utils/api.js'
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
- components: { FamilyChallengeCard },
|
|
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
loading: true,
|
|
loading: true,
|
|
|
- familyId: null,
|
|
|
|
|
templates: [],
|
|
templates: [],
|
|
|
activeChallenges: [],
|
|
activeChallenges: [],
|
|
|
historyChallenges: [],
|
|
historyChallenges: [],
|
|
@@ -172,415 +205,405 @@ export default {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- computed: {
|
|
|
|
|
- hasFamily: function() {
|
|
|
|
|
- if (this.loading) return true
|
|
|
|
|
- if (this.familyId) return true
|
|
|
|
|
- return this.$store.getters.hasFamily
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
onLoad() {
|
|
onLoad() {
|
|
|
- this.loadFamilyId()
|
|
|
|
|
- this.loadData()
|
|
|
|
|
this.loadTemplates()
|
|
this.loadTemplates()
|
|
|
this.loadMembers()
|
|
this.loadMembers()
|
|
|
|
|
+ this.loadData()
|
|
|
},
|
|
},
|
|
|
onShow() {
|
|
onShow() {
|
|
|
- if (this.familyId) {
|
|
|
|
|
- this.loadData()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.loadData()
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
- getUserRole: function() {
|
|
|
|
|
- // store state 顶层有 role 字段(见 store/index.js:15)
|
|
|
|
|
- return this.$store && this.$store.state ? this.$store.state.role : ''
|
|
|
|
|
- },
|
|
|
|
|
- loadFamilyId: function() {
|
|
|
|
|
- var self = this
|
|
|
|
|
|
|
+ async loadData() {
|
|
|
|
|
+ this.loading = true
|
|
|
try {
|
|
try {
|
|
|
- var dashPromise = getParentDashboard()
|
|
|
|
|
- dashPromise.then(function(res) {
|
|
|
|
|
- var data = res.data || {}
|
|
|
|
|
- if (data.familyId) {
|
|
|
|
|
- self.familyId = data.familyId
|
|
|
|
|
- }
|
|
|
|
|
- }).catch(function() {})
|
|
|
|
|
|
|
+ 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) {
|
|
} catch (e) {
|
|
|
- console.log('获取familyId失败', e)
|
|
|
|
|
|
|
+ this.activeChallenges = []
|
|
|
|
|
+ this.historyChallenges = []
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- 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) {
|
|
|
|
|
|
|
+ async loadTemplates() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getChallengeTemplates()
|
|
|
if (res.code === 200) {
|
|
if (res.code === 200) {
|
|
|
- self.templates = res.data || []
|
|
|
|
|
|
|
+ this.templates = (res.data || []).map(t => ({
|
|
|
|
|
+ ...t,
|
|
|
|
|
+ emoji: this.getEmoji(t.type)
|
|
|
|
|
+ }))
|
|
|
}
|
|
}
|
|
|
- }).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: '每天感恩记录' }
|
|
|
|
|
|
|
+ } 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 }
|
|
|
]
|
|
]
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
- getTemplateEmoji: function(type) {
|
|
|
|
|
- var map = {
|
|
|
|
|
- full_checkin: '✅',
|
|
|
|
|
- sports: '🏃',
|
|
|
|
|
- health_week: '💚',
|
|
|
|
|
- reading: '📚',
|
|
|
|
|
- no_screen: '📵',
|
|
|
|
|
- gratitude: '🙏',
|
|
|
|
|
- custom: '✏️'
|
|
|
|
|
|
|
+ getEmoji(type) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ full_checkin: '✅', sports: '🏃', reading: '📚',
|
|
|
|
|
+ gratitude: '🙏', no_screen: '📵', custom: '✏️'
|
|
|
}
|
|
}
|
|
|
return map[type] || '🎯'
|
|
return map[type] || '🎯'
|
|
|
},
|
|
},
|
|
|
- loadMembers: function() {
|
|
|
|
|
- var self = this
|
|
|
|
|
- listFamilyMembers().then(function(res) {
|
|
|
|
|
- var list = (res && res.data) || []
|
|
|
|
|
- self.familyMembers = Array.isArray(list) ? list : []
|
|
|
|
|
- // 默认全选
|
|
|
|
|
- self.selectedMemberIds = []
|
|
|
|
|
- for (var i = 0; i < self.familyMembers.length; i++) {
|
|
|
|
|
- self.selectedMemberIds.push(self.familyMembers[i].id)
|
|
|
|
|
- }
|
|
|
|
|
- }).catch(function() {
|
|
|
|
|
- self.familyMembers = []
|
|
|
|
|
- self.selectedMemberIds = []
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ 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: function(memberId) {
|
|
|
|
|
- return this.selectedMemberIds.indexOf(memberId) >= 0
|
|
|
|
|
|
|
+ isMemberSelected(id) {
|
|
|
|
|
+ return this.selectedMemberIds.indexOf(id) >= 0
|
|
|
},
|
|
},
|
|
|
- toggleMember: function(memberId) {
|
|
|
|
|
- var idx = this.selectedMemberIds.indexOf(memberId)
|
|
|
|
|
- if (idx >= 0) {
|
|
|
|
|
- this.selectedMemberIds.splice(idx, 1)
|
|
|
|
|
|
|
+ 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 {
|
|
} else {
|
|
|
- this.selectedMemberIds.push(memberId)
|
|
|
|
|
|
|
+ this.form.targetMode = 'all_members'
|
|
|
|
|
+ this.form.targetValue = '7'
|
|
|
|
|
+ this.form.durationDays = '7'
|
|
|
|
|
+ this.form.rewardPoints = '50'
|
|
|
|
|
+ this.form.title = ''
|
|
|
|
|
+ this.form.description = ''
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- createFromTemplate: function(template) {
|
|
|
|
|
|
|
+ openCreateModal() {
|
|
|
this.editMode = false
|
|
this.editMode = false
|
|
|
this.editingId = null
|
|
this.editingId = null
|
|
|
this.form = {
|
|
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'
|
|
|
|
|
|
|
+ title: '', description: '', challengeType: 'custom',
|
|
|
|
|
+ targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '50'
|
|
|
}
|
|
}
|
|
|
this.showModal = true
|
|
this.showModal = true
|
|
|
},
|
|
},
|
|
|
- openCreateModal: function(template) {
|
|
|
|
|
- this.createFromTemplate(template)
|
|
|
|
|
- },
|
|
|
|
|
- openEditModal: function(challenge) {
|
|
|
|
|
|
|
+ openEditModal(ch) {
|
|
|
this.editMode = true
|
|
this.editMode = true
|
|
|
- this.editingId = challenge.id
|
|
|
|
|
|
|
+ this.editingId = ch.id
|
|
|
this.form = {
|
|
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')
|
|
|
|
|
|
|
+ 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
|
|
this.showModal = true
|
|
|
},
|
|
},
|
|
|
- closeModal: function() {
|
|
|
|
|
|
|
+ closeModal() {
|
|
|
this.showModal = false
|
|
this.showModal = false
|
|
|
},
|
|
},
|
|
|
- submitForm: function() {
|
|
|
|
|
- var self = this
|
|
|
|
|
- var title = self.form.title && self.form.title.trim()
|
|
|
|
|
- if (!title) {
|
|
|
|
|
|
|
+ async submitForm() {
|
|
|
|
|
+ if (!this.form.title.trim()) {
|
|
|
uni.showToast({ title: '请填写挑战标题', icon: 'none' })
|
|
uni.showToast({ title: '请填写挑战标题', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- var targetValue = parseInt(self.form.targetValue || '0', 10)
|
|
|
|
|
|
|
+ var targetValue = parseInt(this.form.targetValue || 0, 10)
|
|
|
if (targetValue <= 0) {
|
|
if (targetValue <= 0) {
|
|
|
uni.showToast({ title: '目标值必须大于0', icon: 'none' })
|
|
uni.showToast({ title: '目标值必须大于0', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
var data = {
|
|
var data = {
|
|
|
- title: title,
|
|
|
|
|
- description: self.form.description && self.form.description.trim(),
|
|
|
|
|
- challengeType: self.form.challengeType,
|
|
|
|
|
- targetMode: self.form.targetMode,
|
|
|
|
|
|
|
+ title: this.form.title.trim(),
|
|
|
|
|
+ description: this.form.description.trim(),
|
|
|
|
|
+ challengeType: this.form.challengeType,
|
|
|
|
|
+ targetMode: this.form.targetMode,
|
|
|
targetValue: targetValue,
|
|
targetValue: targetValue,
|
|
|
- durationDays: parseInt(self.form.durationDays || '7', 10),
|
|
|
|
|
- rewardPoints: parseInt(self.form.rewardPoints || '0', 10)
|
|
|
|
|
|
|
+ durationDays: parseInt(this.form.durationDays || 7, 10),
|
|
|
|
|
+ rewardPoints: parseInt(this.form.rewardPoints || 0, 10)
|
|
|
}
|
|
}
|
|
|
- if (!self.editMode) {
|
|
|
|
|
- data.participantMemberIds = self.selectedMemberIds
|
|
|
|
|
- }
|
|
|
|
|
- self.loading = true
|
|
|
|
|
- var promise
|
|
|
|
|
- if (self.editMode && self.editingId) {
|
|
|
|
|
- promise = updateChallenge(self.editingId, data)
|
|
|
|
|
- } else {
|
|
|
|
|
- promise = createChallenge(data)
|
|
|
|
|
- }
|
|
|
|
|
- promise.then(function(res) {
|
|
|
|
|
|
|
+ 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) {
|
|
if (res.code === 200) {
|
|
|
- var msg = self.editMode ? '更新成功' : '创建成功'
|
|
|
|
|
- uni.showToast({ title: msg, icon: 'success' })
|
|
|
|
|
- self.closeModal()
|
|
|
|
|
- self.loadData()
|
|
|
|
|
|
|
+ uni.showToast({ title: this.editMode ? '保存成功' : '创建成功', icon: 'success' })
|
|
|
|
|
+ this.closeModal()
|
|
|
|
|
+ this.loadData()
|
|
|
} else {
|
|
} else {
|
|
|
uni.showToast({ title: res.message || '操作失败', icon: 'none' })
|
|
uni.showToast({ title: res.message || '操作失败', icon: 'none' })
|
|
|
}
|
|
}
|
|
|
- }).catch(function(e) {
|
|
|
|
|
|
|
+ } catch (e) {
|
|
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
|
|
- console.log('挑战操作失败', e)
|
|
|
|
|
- }).finally(function() {
|
|
|
|
|
- self.loading = false
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
- deleteChallenge: function(id) {
|
|
|
|
|
- var self = this
|
|
|
|
|
|
|
+ deleteChallenge(id) {
|
|
|
uni.showModal({
|
|
uni.showModal({
|
|
|
title: '确认删除',
|
|
title: '确认删除',
|
|
|
content: '确定要取消这个挑战吗?',
|
|
content: '确定要取消这个挑战吗?',
|
|
|
- success: function(r) {
|
|
|
|
|
|
|
+ success: (r) => {
|
|
|
if (r.confirm) {
|
|
if (r.confirm) {
|
|
|
- deleteChallenge(id).then(function(res) {
|
|
|
|
|
|
|
+ deleteChallenge(id).then((res) => {
|
|
|
if (res.code === 200) {
|
|
if (res.code === 200) {
|
|
|
uni.showToast({ title: '已删除', icon: 'success' })
|
|
uni.showToast({ title: '已删除', icon: 'success' })
|
|
|
- self.loadData()
|
|
|
|
|
|
|
+ this.loadData()
|
|
|
} else {
|
|
} else {
|
|
|
uni.showToast({ title: res.message || '删除失败', icon: 'none' })
|
|
uni.showToast({ title: res.message || '删除失败', icon: 'none' })
|
|
|
}
|
|
}
|
|
|
- }).catch(function() {
|
|
|
|
|
- uni.showToast({ title: '删除失败', icon: 'none' })
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ }).catch(() => uni.showToast({ title: '删除失败', icon: 'none' }))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
- viewChallenge: function(challenge) {
|
|
|
|
|
- uni.showToast({ title: challenge.title, icon: 'none' })
|
|
|
|
|
|
|
+ viewChallenge(ch) {
|
|
|
|
|
+ // 跳转到挑战详情
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/activity/index' })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.challenge-manage-page {
|
|
|
|
|
|
|
+.page {
|
|
|
min-height: 100vh;
|
|
min-height: 100vh;
|
|
|
- background: #F5F9FC;
|
|
|
|
|
- padding: 20rpx 24rpx 40rpx;
|
|
|
|
|
|
|
+ background: #F5F7FA;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ padding-bottom: 60rpx;
|
|
|
}
|
|
}
|
|
|
-.page-content {
|
|
|
|
|
|
|
+.content {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 28rpx;
|
|
|
|
|
|
|
+ gap: 24rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 模板推荐区 */
|
|
|
|
|
-.template-section {
|
|
|
|
|
- margin-bottom: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-.section-title-row {
|
|
|
|
|
|
|
+/* 概览卡片 */
|
|
|
|
|
+.overview-card {
|
|
|
|
|
+ background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ padding: 28rpx;
|
|
|
|
|
+ border: 2rpx solid #FED7AA;
|
|
|
|
|
+}
|
|
|
|
|
+.overview-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
}
|
|
}
|
|
|
-.section-title {
|
|
|
|
|
|
|
+.overview-title {
|
|
|
font-size: 30rpx;
|
|
font-size: 30rpx;
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
- color: #1E293B;
|
|
|
|
|
|
|
+ color: #92400E;
|
|
|
}
|
|
}
|
|
|
-.section-hint {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #94A3B8;
|
|
|
|
|
-}
|
|
|
|
|
-.template-scroll {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
|
|
+.overview-count {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #B45309;
|
|
|
}
|
|
}
|
|
|
-.template-scroll-inner {
|
|
|
|
|
|
|
+.active-list {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 12rpx;
|
|
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
}
|
|
}
|
|
|
-.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);
|
|
|
|
|
|
|
+.active-item {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
|
|
|
|
+ justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 6rpx;
|
|
|
|
|
|
|
+ background: rgba(255,255,255,0.7);
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
}
|
|
}
|
|
|
-.template-card:active { opacity: 0.8; }
|
|
|
|
|
-.template-card-custom {
|
|
|
|
|
- background: linear-gradient(135deg, #10B981, #34D399);
|
|
|
|
|
|
|
+.active-item-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #7C2D12;
|
|
|
}
|
|
}
|
|
|
-.template-icon {
|
|
|
|
|
- font-size: 40rpx;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
|
|
+.active-item-meta {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #B45309;
|
|
|
}
|
|
}
|
|
|
-.template-name {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #475569;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 操作栏 */
|
|
|
|
|
+.action-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
}
|
|
}
|
|
|
-.template-card-custom .template-name {
|
|
|
|
|
|
|
+.btn-new {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #10B981, #34D399);
|
|
|
color: #fff;
|
|
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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 列表区 */
|
|
/* 列表区 */
|
|
|
-.list-section {
|
|
|
|
|
|
|
+.section {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
}
|
|
}
|
|
|
-.list-add-btn {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #10B981;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
|
|
+.section-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
}
|
|
}
|
|
|
-.list-add-btn:active { opacity: 0.7; }
|
|
|
|
|
-
|
|
|
|
|
-.group {
|
|
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1E293B;
|
|
|
}
|
|
}
|
|
|
.group-label {
|
|
.group-label {
|
|
|
font-size: 26rpx;
|
|
font-size: 26rpx;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
color: #64748B;
|
|
color: #64748B;
|
|
|
- margin-bottom: 10rpx;
|
|
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
}
|
|
}
|
|
|
-.challenge-item {
|
|
|
|
|
|
|
+.card-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.card {
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
border-radius: 16rpx;
|
|
border-radius: 16rpx;
|
|
|
- padding: 16rpx;
|
|
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
- box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
|
|
|
|
|
|
+ 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;
|
|
|
}
|
|
}
|
|
|
-.challenge-item-header {
|
|
|
|
|
|
|
+.card-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- margin-bottom: 4rpx;
|
|
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
}
|
|
}
|
|
|
-.challenge-item-title {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
|
|
+.card-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
color: #1E293B;
|
|
color: #1E293B;
|
|
|
}
|
|
}
|
|
|
-.challenge-item-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-.action-btn {
|
|
|
|
|
|
|
+.card-status {
|
|
|
font-size: 22rpx;
|
|
font-size: 22rpx;
|
|
|
padding: 4rpx 12rpx;
|
|
padding: 4rpx 12rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
}
|
|
}
|
|
|
-.action-edit {
|
|
|
|
|
- color: #5B9BD5;
|
|
|
|
|
- background: rgba(91,155,213,0.1);
|
|
|
|
|
|
|
+.status-active {
|
|
|
|
|
+ background: #DCFCE7;
|
|
|
|
|
+ color: #16A34A;
|
|
|
}
|
|
}
|
|
|
-.action-delete {
|
|
|
|
|
- color: #EF4444;
|
|
|
|
|
- background: rgba(239,68,68,0.1);
|
|
|
|
|
|
|
+.status-done {
|
|
|
|
|
+ background: #F1F5F9;
|
|
|
|
|
+ color: #64748B;
|
|
|
|
|
+}
|
|
|
|
|
+.card-meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
}
|
|
|
-.challenge-item-desc {
|
|
|
|
|
|
|
+.meta-item {
|
|
|
font-size: 22rpx;
|
|
font-size: 22rpx;
|
|
|
color: #64748B;
|
|
color: #64748B;
|
|
|
- margin-bottom: 6rpx;
|
|
|
|
|
|
|
+ background: #F8FAFC;
|
|
|
|
|
+ padding: 4rpx 12rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
}
|
|
}
|
|
|
-.challenge-item-footer {
|
|
|
|
|
|
|
+.card-actions {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 8rpx;
|
|
|
|
|
- margin-bottom: 4rpx;
|
|
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ margin-left: 16rpx;
|
|
|
}
|
|
}
|
|
|
-.tag {
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- padding: 2rpx 10rpx;
|
|
|
|
|
- border-radius: 999rpx;
|
|
|
|
|
- background: #E2E8F0;
|
|
|
|
|
- color: #475569;
|
|
|
|
|
|
|
+.action-btn {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
}
|
|
}
|
|
|
-.tag-days { background: #DBEAFE; color: #2563EB; }
|
|
|
|
|
-.tag-points { background: #FEF3C7; color: #D97706; }
|
|
|
|
|
-.status-badge {
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- padding: 2rpx 10rpx;
|
|
|
|
|
- border-radius: 999rpx;
|
|
|
|
|
|
|
+.action-btn.edit {
|
|
|
|
|
+ color: #5B9BD5;
|
|
|
|
|
+ background: rgba(91,155,213,0.1);
|
|
|
}
|
|
}
|
|
|
-.status-completed {
|
|
|
|
|
- background: #F1F5F9;
|
|
|
|
|
- color: #64748B;
|
|
|
|
|
|
|
+.action-btn.del {
|
|
|
|
|
+ color: #EF4444;
|
|
|
|
|
+ background: rgba(239,68,68,0.1);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 空状态 */
|
|
/* 空状态 */
|
|
|
-.empty-state {
|
|
|
|
|
|
|
+.empty {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 60rpx 0 20rpx;
|
|
|
|
|
|
|
+ padding: 80rpx 0;
|
|
|
}
|
|
}
|
|
|
.empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
|
|
.empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
|
|
|
.empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
|
|
.empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
|
|
|
-.empty-desc { font-size: 24rpx; color: #94A3B8; margin: 8rpx 0 24rpx; }
|
|
|
|
|
-.empty-btn {
|
|
|
|
|
|
|
+.empty-desc { font-size: 26rpx; color: #94A3B8; margin: 8rpx 0 32rpx; }
|
|
|
|
|
+.btn-empty {
|
|
|
background: #10B981;
|
|
background: #10B981;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
- font-size: 26rpx;
|
|
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
- padding: 16rpx 40rpx;
|
|
|
|
|
- border-radius: 44rpx;
|
|
|
|
|
|
|
+ padding: 16rpx 48rpx;
|
|
|
|
|
+ border-radius: 40rpx;
|
|
|
|
|
+ border: none;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 弹窗 */
|
|
/* 弹窗 */
|
|
|
-.modal-overlay {
|
|
|
|
|
|
|
+.overlay {
|
|
|
position: fixed;
|
|
position: fixed;
|
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
|
background: rgba(0,0,0,0.5);
|
|
background: rgba(0,0,0,0.5);
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: flex-end;
|
|
align-items: flex-end;
|
|
|
- justify-content: center;
|
|
|
|
|
z-index: 999;
|
|
z-index: 999;
|
|
|
}
|
|
}
|
|
|
-.modal-content {
|
|
|
|
|
|
|
+.modal {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
border-radius: 32rpx 32rpx 0 0;
|
|
border-radius: 32rpx 32rpx 0 0;
|
|
|
- padding: 24rpx;
|
|
|
|
|
|
|
+ padding: 32rpx 28rpx 40rpx;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
|
|
+ max-height: 85vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
}
|
|
|
.modal-header {
|
|
.modal-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
}
|
|
}
|
|
|
.modal-title {
|
|
.modal-title {
|
|
|
font-size: 32rpx;
|
|
font-size: 32rpx;
|
|
@@ -588,122 +611,156 @@ export default {
|
|
|
color: #1E293B;
|
|
color: #1E293B;
|
|
|
}
|
|
}
|
|
|
.modal-close {
|
|
.modal-close {
|
|
|
- font-size: 40rpx;
|
|
|
|
|
|
|
+ font-size: 44rpx;
|
|
|
color: #94A3B8;
|
|
color: #94A3B8;
|
|
|
line-height: 1;
|
|
line-height: 1;
|
|
|
}
|
|
}
|
|
|
-.modal-body {
|
|
|
|
|
|
|
+.form-body {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 16rpx;
|
|
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
- max-height: 70vh;
|
|
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+ flex: 1;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
}
|
|
}
|
|
|
-.form-item {
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 模板行 */
|
|
|
|
|
+.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;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
gap: 6rpx;
|
|
gap: 6rpx;
|
|
|
|
|
+ padding: 14rpx 18rpx;
|
|
|
|
|
+ background: #F8FAFC;
|
|
|
|
|
+ border-radius: 14rpx;
|
|
|
|
|
+ border: 2rpx solid transparent;
|
|
|
}
|
|
}
|
|
|
-.form-label {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
|
|
+.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;
|
|
color: #475569;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
}
|
|
}
|
|
|
-.form-input {
|
|
|
|
|
|
|
+.required { color: #EF4444; }
|
|
|
|
|
+.field-input {
|
|
|
background: #F8FAFC;
|
|
background: #F8FAFC;
|
|
|
border: 1rpx solid #E2E8F0;
|
|
border: 1rpx solid #E2E8F0;
|
|
|
border-radius: 12rpx;
|
|
border-radius: 12rpx;
|
|
|
- padding: 14rpx 16rpx;
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
|
|
+ padding: 16rpx 18rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
color: #1E293B;
|
|
color: #1E293B;
|
|
|
}
|
|
}
|
|
|
-.form-textarea {
|
|
|
|
|
|
|
+.field-textarea {
|
|
|
background: #F8FAFC;
|
|
background: #F8FAFC;
|
|
|
border: 1rpx solid #E2E8F0;
|
|
border: 1rpx solid #E2E8F0;
|
|
|
border-radius: 12rpx;
|
|
border-radius: 12rpx;
|
|
|
- padding: 14rpx 16rpx;
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
|
|
+ padding: 16rpx 18rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
color: #1E293B;
|
|
color: #1E293B;
|
|
|
- height: 80rpx;
|
|
|
|
|
|
|
+ height: 100rpx;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
}
|
|
}
|
|
|
-.form-row {
|
|
|
|
|
|
|
+.field-row {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 12rpx;
|
|
|
|
|
|
|
+ gap: 16rpx;
|
|
|
}
|
|
}
|
|
|
-.type-picker {
|
|
|
|
|
|
|
+.field.half { flex: 1; }
|
|
|
|
|
+
|
|
|
|
|
+/* 单选组 */
|
|
|
|
|
+.radio-group {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- gap: 8rpx;
|
|
|
|
|
|
|
+ gap: 12rpx;
|
|
|
}
|
|
}
|
|
|
-.type-item {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- padding: 6rpx 14rpx;
|
|
|
|
|
- border-radius: 999rpx;
|
|
|
|
|
- background: #F1F5F9;
|
|
|
|
|
|
|
+.radio-btn {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 12rpx 0;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
color: #475569;
|
|
color: #475569;
|
|
|
|
|
+ background: #F1F5F9;
|
|
|
}
|
|
}
|
|
|
-.type-item.active {
|
|
|
|
|
|
|
+.radio-btn.active {
|
|
|
background: #10B981;
|
|
background: #10B981;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
}
|
|
}
|
|
|
-.mode-picker {
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 成员选择 */
|
|
|
|
|
+.member-grid {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 8rpx;
|
|
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
}
|
|
}
|
|
|
-.mode-item {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
|
|
+.member-item {
|
|
|
|
|
+ padding: 10rpx 22rpx;
|
|
|
|
|
+ border-radius: 24rpx;
|
|
|
font-size: 24rpx;
|
|
font-size: 24rpx;
|
|
|
- padding: 10rpx 0;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- background: #F1F5F9;
|
|
|
|
|
color: #475569;
|
|
color: #475569;
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+ background: #F1F5F9;
|
|
|
|
|
+ border: 2rpx solid transparent;
|
|
|
}
|
|
}
|
|
|
-.mode-item.active {
|
|
|
|
|
- background: #10B981;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
|
|
+.member-item.active {
|
|
|
|
|
+ background: #DCFCE7;
|
|
|
|
|
+ color: #16A34A;
|
|
|
|
|
+ border-color: #10B981;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 底部按钮 */
|
|
|
.modal-footer {
|
|
.modal-footer {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 12rpx;
|
|
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ padding-top: 8rpx;
|
|
|
}
|
|
}
|
|
|
-.modal-btn {
|
|
|
|
|
|
|
+.btn-cancel, .btn-submit {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 16rpx 0;
|
|
|
|
|
- border-radius: 44rpx;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
|
|
+ height: 84rpx;
|
|
|
|
|
+ line-height: 84rpx;
|
|
|
|
|
+ border-radius: 42rpx;
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
|
|
+ border: none;
|
|
|
}
|
|
}
|
|
|
-.modal-btn-cancel {
|
|
|
|
|
|
|
+.btn-cancel {
|
|
|
background: #F1F5F9;
|
|
background: #F1F5F9;
|
|
|
color: #64748B;
|
|
color: #64748B;
|
|
|
}
|
|
}
|
|
|
-.modal-btn-submit {
|
|
|
|
|
- background: #10B981;
|
|
|
|
|
|
|
+.btn-submit {
|
|
|
|
|
+ background: linear-gradient(135deg, #10B981, #34D399);
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
}
|
|
}
|
|
|
-.member-picker {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- gap: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-.member-pick-item {
|
|
|
|
|
- padding: 8rpx 20rpx;
|
|
|
|
|
- border-radius: 999rpx;
|
|
|
|
|
- background: #F1F5F9;
|
|
|
|
|
- color: #475569;
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- border: 2rpx solid transparent;
|
|
|
|
|
-}
|
|
|
|
|
-.member-pick-item.active {
|
|
|
|
|
- background: #DCFCE7;
|
|
|
|
|
- color: #16A34A;
|
|
|
|
|
- border-color: #10B981;
|
|
|
|
|
-}
|
|
|
|
|
</style>
|
|
</style>
|