Authored by Bad

feat: 优化日历训练计划及自由训练流程

- 新增用户协议、隐私协议及使用教程页面
- 调整每日模板与历史记录的数据展示逻辑
- 支持未来日期保存为训练模板并锁定当日开练功能

Too many changes to show.

To preserve performance only 17 of 17+ files are displayed.

... ... @@ -8,7 +8,7 @@ SHOPRO_VERSION=v2.4.1
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
# SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
#SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
SHOPRO_DEV_BASE_URL=https://xunji.geaktec.com
# SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
... ... @@ -43,3 +43,5 @@ SHOPRO_TENANT_ID=1
# 默认头像
DEFAULT_AVATAR=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png
# appLogo
APP_LOGO=https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260728/自己练logo_1785218679080.png
... ...
... ... @@ -116,7 +116,7 @@
{
"path": "pages/xunji/wode-jihua-paike",
"style": {
"navigationBarTitleText": "我的计划详情的排课设置"
"navigationBarTitleText": "计划排课"
}
},
{
... ... @@ -144,7 +144,14 @@
"style": {
"navigationBarTitleText": ""
}
}
},
{
"path": "pages/xunji/shiyon-jiaochen",
"style": {
"navigationBarTitleText": "使用教程"
}
}
]
},
{
... ... @@ -209,7 +216,19 @@
"navigationBarTitleText": "设置",
"navigationStyle": "default"
}
}
},
{
"path": "pages/user/yonhu-xieyi",
"style": {
"navigationBarTitleText": "用户协议"
}
},
{
"path": "pages/user/yinsi-xieyi",
"style": {
"navigationBarTitleText": ""
}
}
]
}
],
... ...
... ... @@ -17,7 +17,7 @@
<!-- 未登录引导区 -->
<view v-else class="section-card login-guide-box">
<view class="guide-txt">
<text class="title">欢迎加入鸿星运动</text>
<text class="title">欢迎加入自己练运动</text>
<text class="desc">登录后即可享受课程预约及资产管理</text>
</view>
<button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button>
... ... @@ -52,8 +52,9 @@
<uni-icons type="right" size="14" color="#E0E0E0" />
</view>
<view class="logout-wrap">
<button class="logout-btn" @click="handleLogout">退出登录</button>
<view class="settings-item logout-item" @click="handleLogout">
<text class="logout-label">退出登录</text>
<uni-icons type="right" size="14" color="#E0E0E0" />
</view>
</view>
... ... @@ -457,32 +458,17 @@ $transition-tap: background-color 0.2s ease, opacity 0.2s ease;
font-size: $font-body;
color: $color-text-primary;
}
}
.logout-wrap {
margin-top: 40rpx;
.logout-btn {
width: 100%;
background-color: $color-danger;
color: $color-text-white;
border: none;
border-radius: $radius-button;
padding: 26rpx 0;
font-size: 30rpx;
font-weight: 500;
transition: $transition-tap;
&::after {
border: none;
}
&:active {
background-color: $color-danger-active;
}
.logout-label {
font-size: $font-body;
color: $color-text-tertiary;
}
}
.logout-item {
border-bottom: none;
}
// 触摸反馈
.card-hover {
background-color: $color-bg-hover;
... ...
... ... @@ -262,6 +262,13 @@ const exerciseNamesWithLabel = computed(() => {
})
const exposeRecordList = computed(() => {
// 每日模板编辑场景需要全量数据,不受 isActive 过滤影响
if (props.isDailyTemplates) {
return recordList.value.map(item => ({
...item,
weight: getRealWeight(item)
}))
}
return recordList.value.filter(item => item.isActive).map(item => ({
...item,
weight: getRealWeight(item)
... ... @@ -272,9 +279,11 @@ const exposeSuperRecordMap = computed(() => {
const map = {}
for (const key in superRecordMap.value) {
const subExercise = props.actionDetail?.exercises?.find(s => s.id == key)
map[key] = superRecordMap.value[key]
.filter(item => item.isActive)
.map(item => ({ ...item, weight: getRealWeight(item, subExercise?.exerciseType) }))
// 每日模板编辑场景需要全量数据,不受 isActive 过滤影响
const list = props.isDailyTemplates
? superRecordMap.value[key]
: superRecordMap.value[key]?.filter(item => item.isActive)
map[key] = (list || []).map(item => ({ ...item, weight: getRealWeight(item, subExercise?.exerciseType) }))
}
return map
})
... ... @@ -578,10 +587,13 @@ function onQuickConfirm(e) {
// ===================== 增删行/组 =====================
function addRow() {
recordList.value.push({
const last = recordList.value[recordList.value.length - 1]
const newItem = last ? JSON.parse(JSON.stringify(last)) : {
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
})
}
newItem.isActive = false
recordList.value.push(newItem)
}
function deleteRow(index) {
... ... @@ -595,10 +607,14 @@ function deleteRow(index) {
function addSuperSet() {
(props.actionDetail?.exercises || []).forEach(sub => {
if (superRecordMap.value[sub.id]) {
superRecordMap.value[sub.id].push({
const list = superRecordMap.value[sub.id]
const last = list[list.length - 1]
const newItem = last ? JSON.parse(JSON.stringify(last)) : {
h: '00', m: '00', s: '00', quickTimeDisplay: formatQuickTime(60),
distance: '', weight: '', reps: '', duration: '', restTime: '', isActive: false
})
}
newItem.isActive = false
list.push(newItem)
}
})
}
... ...
... ... @@ -155,7 +155,7 @@
<view class="section-title">训练部位</view>
<view class="muscle-card">
<image
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg"
:src="actionDetail.urlImage || 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/训练部位_1784878616826.jpg'"
mode="widthFix" class="muscle-map" />
<!-- <view class="legend">
<view class="legend-item">
... ... @@ -172,13 +172,16 @@
<view class="muscle-group" v-if="actionDetail.primaryMuscleNames?.length">
<view class="group-label primary">主要训练部位</view>
<view class="pills">
<text class="pill primary-pill" v-for="(value, index) in actionDetail.primaryMuscleNames" :key="index">{{ value }}</text>
<text class="pill primary-pill" v-for="(value, index) in actionDetail.primaryMuscleNames"
:key="index">{{
value }}</text>
</view>
</view>
<view class="muscle-group" v-if="actionDetail.secondaryMuscleNames?.length">
<view class="group-label secondary">次要训练部位</view>
<view class="pills">
<text class="pill secondary-pill" v-for="(value, index) in actionDetail.secondaryMuscleNames" :key="index">{{ value }}</text>
<text class="pill secondary-pill" v-for="(value, index) in actionDetail.secondaryMuscleNames"
:key="index">{{ value }}</text>
</view>
</view>
</view>
... ... @@ -187,7 +190,7 @@
<view v-if="contentTab === 1" class="tab-pane slide-up">
<view class="history-list">
<template v-if="historyList && historyList.length > 0">
<view class="history-item" v-for="item in historyList" :key="item.id">
<view class="history-item" v-for="(item, index) in historyList" :key="index">
<view class="item-header">
<text class="date">{{ formatDate(item.date) }}</text>
<text class="tag">{{ item.name }}</text>
... ... @@ -198,14 +201,14 @@
<view class="dot">· </view>
<!-- <view class="difficulty">困难</view> -->
<view class="difficulty">{{
item.weight ? item.weight + 'kg' : '无负重'
item.weight != null ? item.weight + 'kg' : '无负重'
}}</view>
</view>
<view class="group-chips">
<view class="chip" v-for="(set, index) in item.setConfigList" :key="index">
<view class="idx">{{ index + 1 }}</view>
<view class="group-data">
{{ formatSetData(set) }}
{{ formatSetData(set, item.displayFields) }}
</view>
</view>
</view>
... ... @@ -490,17 +493,32 @@ const loadTrainHistoryDetail = async (id, type) => {
}
};
// 格式化时间
const formatDate = (dateArr) => {
if (!Array.isArray(dateArr) || dateArr.length < 3) return '';
return dayjs(new Date(dateArr[0], dateArr[1] - 1, dateArr[2])).format('YYYY/MM/DD dd');
const formatDate = (date) => {
if (!date) return '';
// 支持数组格式 [2026, 7, 28] 和字符串格式
if (Array.isArray(date) && date.length >= 3) {
return dayjs(new Date(date[0], date[1] - 1, date[2])).format('YYYY/MM/DD');
}
return dayjs(date).format('YYYY/MM/DD');
};
const formatSetData = (set) => {
const formatSetData = (set, displayFields) => {
if (!displayFields || displayFields.length === 0) return '无数据';
const parts = [];
if (set.weight != null && set.weight !== '') parts.push(`${set.weight}kg`);
if (set.reps != null && set.reps !== '') parts.push(`${set.reps}次`);
if (set.duration != null && set.duration !== '') parts.push(dayjs.duration(set.duration, 'seconds').format('HH:mm:ss'));
if (set.distance != null && set.distance !== '') parts.push(`${set.distance}m`);
const fieldMap = {
weight: () => set.weight != null ? `${set.weight}kg` : null,
reps: () => set.reps != null ? `${set.reps}次` : null,
distance: () => set.distance != null ? `${set.distance}km` : null,
duration: () => set.duration != null ? dayjs.duration(set.duration, 'seconds').format('HH:mm:ss') : null,
restTime: () => set.restTime != null ? `休息${set.restTime}s` : null,
};
displayFields.forEach(field => {
const fn = fieldMap[field];
if (fn) {
const result = fn();
if (result) parts.push(result);
}
});
return parts.length > 0 ? parts.join(' × ') : '无数据';
};
... ...
... ... @@ -64,14 +64,17 @@
<WodeJihuaTianjiaTancuang
v-model:visible="showPlanPopup"
@get-plan-list-length="getPlanListLength"
@success="handlePlanAddSuccess"
:is-add="true"
:plan-id="lastPlanId"
:is-my-plan="true"
:selected-date="props.date"
/>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import dayjs from 'dayjs'
import WodeJihuaTianjiaTancuang from '@/pages/xunji/components/wode-jihua-tianjia-tancuang.vue'
import QueryPlanApi from '@/sheep/api/plan/queryplan'
... ... @@ -81,9 +84,13 @@ const props = defineProps({
type: Boolean,
default: false,
},
date: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:visible'])
const emit = defineEmits(['update:visible', 'successAddTrain'])
// ==================== 响应式状态 ====================
const showPlanPopup = ref(false)
... ... @@ -113,15 +120,29 @@ const handleAddFromPlan = () => {
showPlanPopup.value = true
}
/** 从训练计划添加成功后,通知父组件刷新 */
const handlePlanAddSuccess = () => {
emit('successAddTrain')
}
/** 使用训练模板:跳转模板选择页 */
const handleAddFromTemplate = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' })
uni.navigateTo({ url: `/pages4/pages/xunji/xunji-rili-tianjia-moban?date=${props.date}` })
close()
}
/** 自由训练:跳转动作练习页 */
/** 自由训练:非未来日期直接训练,未来日期保存为训练模板 */
const handleFreeTraining = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true' })
const today = dayjs().format('YYYY-MM-DD')
if (props.date && props.date > today) {
// 未来日期:跳转训练页构建动作,完成后保存为当日模板
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?isFutureTemplate=true&targetDate=${props.date}`,
})
} else {
// 非未来日期:直接开始训练,携带目标日期确保训练记录日期正确
uni.navigateTo({ url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?isTraining=true&targetDate=${props.date}` })
}
close()
}
... ...
... ... @@ -27,21 +27,17 @@
</view>
<template v-if="resdailyData.length > 0">
<!-- Tab 按钮:如���有多个训记,用来源名作为标签 -->
<!-- Tab 按钮:如有多个训记,用来源名作为标签 -->
<scroll-view class="plan-tabs" scroll-x v-if="resdailyData.length > 1">
<button class="tab-btn" :class="{ active: currentPlanIndex === index }" v-for="(item, index) in resdailyData"
:key="index" @click.stop="switchPlan(index)">
{{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' + (index + 1)) }}
{{ item.sourceType === 1 || item.sourceType === 2 ? (item.sourceName || item.name) : (item.name || '训记' +
(index + 1)) }}
</button>
</scroll-view>
<!-- 训练内容区域 -->
<view v-if="resdailyData.length > 0 && currentPlan" class="training-container">
<!-- 训练来源标签 -->
<view class="source-tag-row" :class="getSourceClass(currentPlan.sourceType || 4)">
<text class="source-text">
{{ currentPlan.sourceType === 1 || currentPlan.sourceType === 2 ? (currentPlan.sourceName || currentPlan.name) : getSourceLabel(currentPlan.sourceType || 4) }}
</text>
</view>
<view class="unitCart">
<!-- 训练计划头部卡片 -->
<view class="plan-header-card">
... ... @@ -60,8 +56,10 @@
<view class="plan-header-btns">
<button class="plan-btn more-btn" @click="handlePlanMore">更多</button>
<button class="plan-btn copy-btn" @click="openCopyCalendarPopup(currentPlan?.templateId)">复制到</button>
<button class="plan-btn go-train-btn-small" @click="handleTrainAgain">
<text class="go-train-text-sm">今日再练</text>
<button class="plan-btn go-train-btn-small" :class="{ locked: trainAgainDisabled }"
@click="handleTrainAgain">
<text class="go-train-text-sm">{{ trainAgainText }}</text>
<up-icon v-if="trainAgainText == '当日开练'" name="lock" size="15"></up-icon>
</button>
</view>
</view>
... ... @@ -88,7 +86,7 @@
}}kg</text>
</view>
<!-- 下半部分:组次行 → 向左对齐图片 ✅核心-->
<!-- 下半部分:组次行 → 向左对齐图片 ✅核心 -->
<view class="action-bottom">
<view class="action-sets">
<view class="set-item" v-for="(set, setIdx) in unit.exercises[0]?.sets || []" :key="setIdx">
... ... @@ -175,7 +173,7 @@
<!-- 空状态区域 -->
<view v-else class="empty-section">
<image class="empty-img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/空状态自助_1784875704380.png"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260727/空状态自助_1785117390333.png"
mode="aspectFit">
</image>
<text class="empty-tip">今天没有安排</text>
... ... @@ -184,7 +182,7 @@
</view>
<AddTrainPopup v-model:visible="showAddTrainPopup" @successAddTrain="handleAddTrain" />
<AddTrainPopup v-model:visible="showAddTrainPopup" :date="selectedDate" @successAddTrain="handleAddTrain" />
<!-- 日期备注弹窗 -->
<RiliRiqibeizhu v-model:visible="showRiqibeizhu" :date="date" :note-id="currentEditId"
... ... @@ -230,7 +228,7 @@
<!-- 复制到弹窗/移动到/添加到 -->
<AddToCalendarPopup ref="calendarPopupRef" :template-id="currentPlan?.templateId ?? 0"
@success="handleCalendarSuccess" :mask-click="true" @click.stop
:daily-template-id="currentPlan?.dailyTemplateId" :is-copy="isCopyMode" />
:daily-template-id="currentPlan?.id" :is-copy="isCopyMode" />
<!-- 设置日历颜色弹窗 -->
<CalendarColorPicker v-model:visible="showColorPopup" :daily-template-id="selectedPlanId"
:current-color="currentPlan?.templateBackgroundColor || '#ffffff'" @success="calendarColorPickerSuccess" />
... ... @@ -242,7 +240,7 @@
<script setup>
import dayjs from 'dayjs';
import { ref, onMounted, watch, computed, nextTick } from 'vue';
import { ref, watch, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
import TemplatesApi from '@/sheep/api/Template/Templates';
import RiliRiqibeizhu from '@/pages/xunji/components/rili-components/rili-riqibeizhu.vue'
... ... @@ -278,7 +276,7 @@ const moreShow = ref(false);
const showColorPopup = ref(false);
const calendarPopupRef = ref(null);
const currentEditId = ref(null);
const selectedPlanId = ref(null);
const selectedPlanId = ref(0);
// Store / 子组件引用
const trainingStore = useTrainingStore();
... ... @@ -325,7 +323,7 @@ const openXiuGai = (unit, idx) => {
// 打开日历弹窗
const openColorPopup = () => {
if (!currentPlan.value) { // 加这个
if (!currentPlan.value) {
uni.showToast({ title: '空白状态无法操作', icon: 'none' })
return
}
... ... @@ -340,11 +338,10 @@ const openColorPopup = () => {
const handleAddTrain = async () => {
await loaddailytemplate();
// 更新主页面
emit('refreshCalendar');
}
// ===== 计算属性 =====
// 当前正在显示的训记
const currentPlan = computed(() => {
if (!resdailyData.value.length) return null;
return resdailyData.value[currentPlanIndex.value] || null;
... ... @@ -353,29 +350,19 @@ const currentPlan = computed(() => {
// ===== API:加载每日模板数据 =====
const loaddailytemplate = async () => {
if (!selectedDate.value) return;
console.log('【子组件】开始加载数据...');
try {
const resdaily = await dailytemplateApi.getdailytemplate(String(selectedDate.value));
console.log('打印每日模板数据:resdaily', resdaily);
const noteListFromApi = resdaily.data.notes;
noteList.value = noteListFromApi;
console.log('打印每日模板详情的备注列表:noteList', noteList.value);
const historyFromApi = resdaily.data.noteHistoryList || [];
console.log('打印历史备注:', historyFromApi);
noteHistoryList.value = historyFromApi;
resdailyData.value = resdaily.data.templates || [];
// 切换回第一个训记
const oldIndex = currentPlanIndex.value;
if (resdailyData.value.length > oldIndex) {
currentPlanIndex.value = oldIndex;
} else {
currentPlanIndex.value = 0;
}
console.log('+++++【子组件】+++++加载完成,训记列表:', resdailyData.value)
} catch (error) {
console.error('加载训练模板失败:', error);
resdailyData.value = [];
... ... @@ -383,21 +370,18 @@ const loaddailytemplate = async () => {
};
const handleRenderSuccess = async () => {
await loaddailytemplate();
await nextTick()
emit('refreshCalendar');
uni.$emit('calendarDataRefresh');
}
const switchPlan = (index) => {
currentPlanIndex.value = index;
console.log('【切换标签后 - currentPlan】', currentPlan.value);
};
// ===== 更多弹窗:操作项 =====
const handlePlanMore = () => {
if (!currentPlan.value) { // 加这个
return
}
if (!currentPlan.value) return
moreShow.value = true;
}
... ... @@ -407,28 +391,20 @@ const closeMorePopup = () => {
// 一键打勾(完成训练)
const handleCompleteCheck = async () => {
// 日期校验:只能对今天及以前的日期一键打勾
const today = dayjs().format('YYYY-MM-DD');
if (selectedDate.value > today) {
uni.showToast({ title: '一键打勾只能用于今天及以前的日期', icon: 'none' });
return;
}
if (!currentPlan.value?.id) {
uni.showToast({ title: '未找到训练ID', icon: 'none' })
return
}
console.log('进入一键打勾函数训练id=', currentPlan.value.id);
try {
// 2. 调用接口
await dailytemplateApi.completeDailyTemplate(currentPlan.value.id)
emit('refreshCalendar');
uni.showToast({ title: '一键打勾成功', icon: 'success' })
closeMorePopup()
// 刷新页面数据
await loaddailytemplate(selectedDate.value)
} catch (err) {
console.error('一键打勾失败:', err)
... ... @@ -438,54 +414,39 @@ const handleCompleteCheck = async () => {
// ===== 保存训练为个人模板 =====
const openSaveTemplateDialog = async (item) => {
// 关闭所有可能遮挡的弹窗
moreShow.value = false;
showColorPopup.value = false;
showAddTrainPopup.value = false;
showRiqibeizhu.value = false;
show.value = false
// 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭
await nextTick();
// uni弹窗输入框:只填模板名称
uni.showModal({
title: '确认保存为你的训练模板?',
content: '',
editable: true, // 开启输入框
editable: true,
placeholderText: '请输入模板名称',
success: async (res) => {
// 点击确定
if (res.confirm) {
const templateName = res.content.trim()
// 非名校验
if (!templateName) {
return uni.showToast({ title: '请填写模板名称', icon: 'none' })
}
// 调用封装的数据转换函数,组装后端需要的入参
const reqData = formatTrainToTemplate(item, templateName)
// 发起创建接口
console.log('reqData', reqData);
await submitCreateTemplate(reqData)
}
}
})
}
/**
*/
const formatTrainToTemplate = (item, templateName) => {
// 初始化基础结构
const params = {
templateId: item.templateId,
// groupId: 0, // 默认存入根目录,后续选文件夹再传对应groupId
templateName: templateName,
scene: item.scene || 0,
templateCover: item.urlCover || '', // 封面取自训练封面
templateIntroduction: item.templateIntroduction || '', // 简介留空,可后续扩展
templateCover: item.urlCover || '',
templateIntroduction: item.templateIntroduction || '',
units: []
}
// 循环转换 units 结构(训练组→模板unit)
params.units = item.units.map(unit => {
return {
id: unit.unitId,
... ... @@ -495,16 +456,11 @@ const formatTrainToTemplate = (item, templateName) => {
exercises: unit.exercises
}
})
console.log('params', params);
return params
}
const submitCreateTemplate = async (reqData) => {
try {
// 调用创建个人模板接口
const res = await TemplatesApi.createCustTemplate(reqData)
if (res.code === 0) {
uni.showToast({ title: '模板保存成功' })
... ... @@ -521,9 +477,6 @@ const submitCreateTemplate = async (reqData) => {
const openAddTrainPopup = () => {
showAddTrainPopup.value = true;
};
const closeAddTrainPopup = () => {
showAddTrainPopup.value = false;
};
// ===== 工具函数 =====
const formatSecondsToHms = (seconds) => {
... ... @@ -531,7 +484,6 @@ const formatSecondsToHms = (seconds) => {
return dayjs.duration(parseInt(seconds, 10), 'seconds').format('HH:mm:ss');
}
// ===== 工具函数:根据动作类型生成组次显示文本 =====
const getSetDisplayText = (set, exerciseType) => {
if (!set) return '';
switch (exerciseType) {
... ... @@ -546,7 +498,6 @@ const getSetDisplayText = (set, exerciseType) => {
}
};
// 超级组中的动作显示(与普通动作在 type 4/5 上的展示略有差异)
const getSuperSetDisplayText = (set, exerciseType) => {
if (!set) return '';
switch (exerciseType) {
... ... @@ -561,17 +512,6 @@ const getSuperSetDisplayText = (set, exerciseType) => {
}
};
// 返回按钮逻辑
const goBack = () => {
uni.navigateBack({
delta: 1,
fail: () => {
uni.redirectTo({ url: '/pages/xunji/xunji-rili' });
},
});
};
// 日期格式化(使用 dayjs 处理,兼容性更好)
const formatDateWithWeek = (dateStr) => {
if (!dateStr) return '';
const d = dayjs(dateStr);
... ... @@ -582,18 +522,13 @@ const formatDateWithWeek = (dateStr) => {
// ===== 日期备注相关 =====
const handleDateNote = () => {
if (noteList.value.length >= 5) {
uni.showToast({
title: '最多添加5条,已超出数量无法添加',
icon: 'none',
duration: 2000
});
uni.showToast({ title: '最多添加5条,已超出数量无法添加', icon: 'none', duration: 2000 });
return;
}
showRiqibeizhu.value = true
}
const handleEditNote = (note) => {
console.log('准备编辑备注,ID:', note.id);
currentEditId.value = note.id;
showRiqibeizhu.value = true;
};
... ... @@ -608,8 +543,6 @@ const handleCloseNotePopup = () => {
// ===== 复制到 / 移动到 =====
const openCalendarPopup = (templateId) => {
console.log('点击了添加到日历按钮', templateId); // 新增日志
console.log('子组件实例', calendarPopupRef.value); // 新增日志
if (calendarPopupRef.value && typeof calendarPopupRef.value.open === 'function') {
calendarPopupRef.value.open();
} else {
... ... @@ -617,19 +550,47 @@ const openCalendarPopup = (templateId) => {
}
};
// 今日再练 / 去训练 / 当日开练
const trainAgainText = computed(() => {
const today = dayjs().format('YYYY-MM-DD');
if (!selectedDate.value) return '今日再练';
if (selectedDate.value === today) return '今日再练';
if (selectedDate.value > today) return '当日开练';
return '去训练';
});
const trainAgainDisabled = computed(() => {
const today = dayjs().format('YYYY-MM-DD');
return !!selectedDate.value && selectedDate.value > today;
});
// 2. 今日再练方法
const handleTrainAgain = async () => {
const today = dayjs().format('YYYY-MM-DD');
const planDate = selectedDate.value || today;
if (planDate > today) {
uni.showToast({ title: '训练将在当日解锁', icon: 'none' });
return;
}
if (planDate < today) {
trainingStore.isSystem = currentPlan.value.isSystem || false;
trainingStore.loadDailyTemplateForEdit(currentPlan.value);
trainingStore.initDailyTemplateRecords();
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3&dailyTemplateId=${currentPlan.value.id}&isTraining=true`,
});
return;
}
if (!currentPlan.value?.templateId) {
uni.showToast({ title: '未找到训练计划', icon: 'none' });
return;
}
const todayDate = new Date().toISOString().split('T')[0]; // 格式:2026-05-19
const reqData = {
id: currentPlan.value.templateId,
// planId: currentPlan.value.planId || 0,
trainDateList: [todayDate] // 训练日期列表,这里只传今天
trainDateList: [today],
};
try {
... ... @@ -637,7 +598,7 @@ const handleTrainAgain = async () => {
if (res.code === 0) {
uni.showToast({ title: '今日再练成功', icon: 'success' });
loaddailytemplate(props.date);
emit('refreshCalendar')
emit('refreshCalendar');
} else {
uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
}
... ... @@ -646,22 +607,19 @@ const handleTrainAgain = async () => {
uni.showToast({ title: '网络异常,请重试', icon: 'none' });
}
};
// 删除训练
const handleDeleteTemplate = async () => {
// 先判断有没有ID
if (!currentPlan.value?.id) {
uni.showToast({ title: '未找到模板ID', icon: 'none' });
return;
}
// 关闭所有可能遮挡的弹窗
moreShow.value = false;
showColorPopup.value = false;
showAddTrainPopup.value = false;
showRiqibeizhu.value = false;
show.value = false
// 等待 DOM 更新后再弹出模板名称输入框,确保弹窗完全关闭
await nextTick();
// 弹出确认框
uni.showModal({
title: '确认删除',
content: '确定要删除这条训练记录吗?删除后无法恢复',
... ... @@ -674,8 +632,8 @@ const handleDeleteTemplate = async () => {
await dailytemplateApi.deleteDailyTemplate(currentPlan.value.id);
emit('refreshCalendar');
uni.showToast({ title: '删除成功', icon: 'success' });
loaddailytemplate(props.date); // 刷新数据
moreShow.value = false; // 关闭弹窗
loaddailytemplate(props.date);
moreShow.value = false;
} catch (err) {
console.error('删除失败', err);
uni.showToast({ title: '删除失败,请重试', icon: 'none' });
... ... @@ -687,13 +645,8 @@ const handleDeleteTemplate = async () => {
// 更多弹窗的结束整个计划
const confirmDelete = async (id) => {
try {
console.log('打印计划个人id:', id);
const res = await QueryPlanApi.deletePlan(id);
console.log('删除结果:', res);
if (res.code === 0 && res.data) {
// 2. 删除成功,从列表移除
uni.showToast({ title: '计划已结束', icon: 'success' });
emit('refreshCalendar');
} else {
... ... @@ -710,18 +663,12 @@ const confirmDelete = async (id) => {
// 移动到:只打开弹窗
const handleMoveTo = async () => {
console.log('点击了更多弹窗的移动到');
// 安全判断
if (!currentPlan.value) {
uni.showToast({ title: '未找到训练信息', icon: 'none' });
return;
}
isMoveMode.value = true;
// 关闭更多弹窗
moreShow.value = false;
// 只做一件事:打开日期选择
nextTick(() => {
openCalendarPopup(currentPlan.value.templateId);
});
... ... @@ -730,9 +677,8 @@ const handleMoveTo = async () => {
// 复制到
const openCopyCalendarPopup = (templateId) => {
if (!templateId) return uni.showToast({ title: '模板ID不存在', icon: 'none' })
console.log('currentPlan?.dailyTemplateId=', currentPlan?.dailyTemplateId);
isCopyMode.value = true
isMoveMode.value = false // 清空移动标记,互斥
isMoveMode.value = false
moreShow.value = false
nextTick(() => {
calendarPopupRef.value.open();
... ... @@ -740,35 +686,13 @@ const openCopyCalendarPopup = (templateId) => {
}
const handleCalendarSuccess = () => {
// console.log('✅ 添加日历成功,当前模式:', isMoveMode.value ? '移动' : '复制');
// 复制模式:只提示成功,不用删原数据
// if (!isMoveMode.value) {
// uni.showToast({
// title: '复制成功',
// icon: 'success'
// });
// return;
// }
console.log('✅ 添加日历成功', {
move: isMoveMode.value,
copy: isCopyMode.value
});
// 复制模式:仅刷新日历,不删除原数据
if (isCopyMode.value) {
uni.showToast({
title: '复制成功',
icon: 'success'
});
// 重置标记
uni.showToast({ title: '复制成功', icon: 'success' });
isCopyMode.value = false
emit('refreshCalendar')
return;
}
// 移动模式:子组件已把数据新增到新日期,这里删除原日期的训练记录
if (!currentPlan.value?.id) {
return;
}
if (!currentPlan.value?.id) return;
dailytemplateApi.deleteDailyTemplate(currentPlan.value.id)
.then(() => {
uni.showToast({ title: '移动成功', icon: 'success' });
... ... @@ -788,15 +712,10 @@ const calendarColorPickerSuccess = () => {
// ===== 去训练 / 每日模板编辑 =====
const startTraining = () => {
if (trainingStore.isTraining) {
uni.showToast({
title: '当前已有正在进行的训练',
icon: 'none'
});
uni.showToast({ title: '当前已有正在进行的训练', icon: 'none' });
return;
}
trainingStore.isTraining = true;
if (resdailyData.value.length > 0) {
trainingStore.isSystem = currentPlan.value.isSystem || false;
... ... @@ -805,52 +724,44 @@ const startTraining = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${currentPlan.value.templateId}&type=3&dailyTemplateId=${currentPlan.value.id}&isTraining=true`,
});
console.log('去训练开始进入编辑模板页面,模板ID:', currentPlan.value.templateId, '每日模板ID:', currentPlan.value.id);
console.log('打印传递给动作训练页面的模板id', currentPlan.value.templateId);
} else {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' })
}
};
// 每日模板编辑
const templateEdit = (plan) => {
trainingStore.isSystem = plan.isSystem;
trainingStore.loadDailyTemplateForEdit(plan);
trainingStore.initDailyTemplateRecords()
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${plan.templateId}&type=3&dailyTemplateId=${plan.id}`,
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${plan.templateId}&type=3&dailyTemplateId=${plan.id}&isEdit=true`,
});
console.log('开始进入编辑模板页面,模板ID:', plan.templateId, '每日模板ID:', plan.id);
};
// ===== 监听 & 生命周期 =====
// 监听父组件传进来的 date 变化
watch(
() => props.date,
(newDate) => {
if (newDate && newDate.trim()) {
console.log('子组件监听到日期变化:', newDate);
selectedDate.value = newDate;
displayDate.value = formatDateWithWeek(newDate);
loaddailytemplate(newDate);
console.log('+++++++currentPlan:+++++++++++++++++', currentPlan.value)
// console.log('props.noteList=', props.noteList);
}
},
{ immediate: true }
);
// 监听来自编辑/训练页面的数据刷新事件,自动刷新弹窗数据
const handleCalendarDataRefresh = () => {
loaddailytemplate();
};
onMounted(() => {
console.log('【rilicell子组件】已挂载!!!');
console.log('【子组件】props.date:', props.date);
// loaddailytemplate(props.date);
uni.$on('calendarDataRefresh', handleCalendarDataRefresh);
});
onBeforeUnmount(() => {
uni.$off('calendarDataRefresh', handleCalendarDataRefresh);
});
</script>
... ... @@ -860,13 +771,6 @@ $color-primary: #333;
$color-accent: #ffc107;
$color-bg: #f2f3f5;
$color-white: #fff;
$color-danger: #ff4d4f;
$color-muted: #a2a2a2;
$color-card-bg: #f9f9f9;
$radius-sm: 8rpx;
$radius-md: 12rpx;
$radius-lg: 16rpx;
$radius-round: 40rpx;
/* 全局容器 */
.container {
... ... @@ -888,108 +792,70 @@ $radius-round: 40rpx;
justify-content: space-between;
padding: 24rpx 20rpx;
background-color: #fff;
// position: sticky;
top: 0;
z-index: 10;
gap: 16rpx;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.date-wrapper {
flex-grow: 1;
text-align: left;
}
.close-btn {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f2f3f5;
border-radius: 50%;
flex-shrink: 0;
}
.date-text {
font-size: 28rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.date-note-btn {
display: inline-flex;
align-items: center;
font-size: 28rpx;
color: #333;
background: #e7e7e9;
border-radius: 40rpx;
padding: 12rpx 28rpx;
border: none;
margin-right: 0;
flex-shrink: 0;
}
.close-btn {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f2f3f5;
border-radius: 50%;
flex-shrink: 0;
}
.go-train-btn {
display: flex;
align-items: center;
background: #ffc107;
border-radius: 40rpx;
padding: 12rpx 24rpx;
border: none;
flex-shrink: 0;
}
.date-wrapper {
flex-grow: 1;
text-align: left;
}
.go-train-text {
font-size: 28rpx;
color: #000;
margin-right: 8rpx;
font-weight: 500;
}
.date-text {
font-size: 28rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.go-train-tag {
font-size: 22rpx;
color: #ffc107;
background: #000;
border-radius: 50%;
padding: 4rpx 8rpx;
font-weight: bold;
}
.date-note-btn {
display: inline-flex;
align-items: center;
font-size: 28rpx;
color: #333;
background: #e7e7e9;
border-radius: 40rpx;
padding: 12rpx 28rpx;
border: none;
margin-right: 0;
flex-shrink: 0;
}
/* 训练来源标签 */
.source-tag-row {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
margin: 0rpx 20rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: 500;
.go-train-btn {
display: flex;
align-items: center;
background: #ffc107;
border-radius: 40rpx;
padding: 12rpx 24rpx;
border: none;
flex-shrink: 0;
.source-text {
color: #fff;
}
.go-train-text {
font-size: 28rpx;
color: #000;
margin-right: 8rpx;
font-weight: 500;
}
&.source-plan {
background: #3b82f6; /* 蓝色 - 计划 */
}
&.source-tpl {
background: #8b5cf6; /* 紫色 - 模板 */
}
&.source-custom {
background: #f59e0b; /* 橙色 - 自定义 */
}
&.source-free {
background: #10b981; /* 绿色 - 自由 */
.go-train-tag {
font-size: 22rpx;
color: #ffc107;
background: #000;
border-radius: 50%;
padding: 4rpx 8rpx;
font-weight: bold;
}
}
}
... ... @@ -1003,102 +869,84 @@ $radius-round: 40rpx;
padding: 32rpx 24rpx;
margin: 20rpx;
margin-bottom: 24rpx;
}
.plan-header-img {
display: none;
}
.plan-header-info {
flex-grow: 1;
margin-bottom: 32rpx;
}
.plan-header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.plan-heander {
display: flex;
}
.plan-title {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
color: #fff;
}
.plan-header-img {
display: none;
}
// 卡片头部的编辑按钮
.editButton {
display: flex;
align-items: center;
gap: 4rpx;
color: #fff;
font-size: 26rpx;
opacity: 0.8;
/* 让文字和图标稍微淡一点,和例图更像 */
border: 1rpx solid rgba(255, 255, 255, 0.7);
border-radius: 25rpx;
padding: 6rpx 12rpx;
}
.plan-header-info {
flex-grow: 1;
margin-bottom: 32rpx;
}
.plan-header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.plan-meta {
font-size: 26rpx;
color: #ccc;
display: block;
}
.plan-title {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
color: #fff;
}
.plan-header-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 20rpx;
justify-content: flex-start;
}
.editButton {
display: flex;
align-items: center;
gap: 4rpx;
color: #fff;
font-size: 26rpx;
opacity: 0.8;
border: 1rpx solid rgba(255, 255, 255, 0.7);
border-radius: 25rpx;
padding: 6rpx 12rpx;
}
.plan-btn {
font-size: 26rpx;
border-radius: 40rpx;
border: none;
padding: 12rpx 28rpx;
line-height: 1;
}
.plan-meta {
font-size: 26rpx;
color: #ccc;
display: block;
}
.more-btn,
.copy-btn {
background: #fff;
color: #000;
border: none;
}
.plan-header-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 20rpx;
justify-content: flex-start;
.plan-btn {
font-size: 26rpx;
border-radius: 40rpx;
border: none;
padding: 12rpx 28rpx;
line-height: 1;
}
.go-train-btn-small {
color: #000;
display: flex;
align-items: center;
padding: 12rpx 20rpx;
background: inherit;
}
.more-btn,
.copy-btn {
background: #fff;
color: #000;
border: none;
}
.go-train-text-sm {
font-size: 24rpx;
margin-right: 4rpx;
font-weight: 500;
}
.go-train-btn-small {
color: #000;
display: flex;
align-items: center;
padding: 12rpx 20rpx;
.go-train-tag-sm {
font-size: 20rpx;
background: #000;
color: #ffc107;
border-radius: 4rpx;
padding: 2rpx 6rpx;
font-weight: bold;
.go-train-text-sm {
font-size: 24rpx;
margin-right: 4rpx;
font-weight: 500;
}
}
}
}
/* 训练动作列表 */
... ... @@ -1114,29 +962,49 @@ $radius-round: 40rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.set-index {
display: none;
}
.set-content {
flex-grow: 1;
color: inherit;
display: flex;
justify-content: space-between;
white-space: nowrap;
}
.set-item {
display: flex;
align-items: center;
font-size: 26rpx;
color: #666;
justify-content: space-between;
.set-left {
display: flex;
align-items: center;
gap: 8rpx;
}
.set-content {
flex-grow: 1;
color: inherit;
display: flex;
justify-content: space-between;
white-space: nowrap;
.set-right {
display: flex;
gap: 20rpx;
}
}
.rest-time {
font-size: 24rpx;
margin-left: 40rpx;
flex-shrink: 0;
}
.detai-data {
white-space: nowrap;
.check {
font-size: 26rpx;
color: #333;
margin-left: 20rpx;
flex-shrink: 0;
}
}
}
// 主卡片:横向排列
/* 主卡片:横向排列 */
.action-item {
display: flex;
align-items: flex-start;
... ... @@ -1145,153 +1013,71 @@ $radius-round: 40rpx;
padding: 24rpx 20rpx;
position: relative;
gap: 20rpx;
}
// 左侧:序号 + 图片
.action-left {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
}
// 大序号
.action-index {
font-size: 34rpx;
font-weight: 600;
color: #333;
line-height: 1;
}
// 动作图片
.action-img {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
}
// 中间区域
.action-middle {
flex: 1;
display: flex;
flex-direction: column;
}
// 上半部分:名称 + 重量(左对齐)
.action-top {
margin-bottom: 8rpx;
}
.action-name {
font-size: 32rpx;
font-weight: 500;
color: #111;
display: block;
}
.action-totalWeight {
font-size: 26rpx;
color: #333;
margin-top: 6rpx;
display: block;
}
// 下半部分:组次行 → 向左对齐图片 ✅ 核心
.action-bottom {
margin-top: 6rpx;
margin-left: -96rpx;
/* 往左移动,对齐图片位置 */
}
.action-left {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
}
.set-item {
display: flex;
align-items: center;
font-size: 26rpx;
color: #666;
justify-content: space-between;
}
.action-index {
font-size: 34rpx;
font-weight: 600;
color: #333;
line-height: 1;
}
.set-left {
display: flex;
align-items: center;
gap: 8rpx;
/* 控制 indexData、set-content、rest-time 之间的间距 */
}
.action-img {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
}
.set-right {
display: flex;
gap: 20rpx;
}
.action-middle {
flex: 1;
display: flex;
flex-direction: column;
}
// 灰色小圆点序号
.indexData {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #f2f3f5;
font-size: 22rpx;
color: #999;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
}
.action-top {
margin-bottom: 8rpx;
// .set-content {
// color: #333;
// }
.action-name {
font-size: 32rpx;
font-weight: 500;
color: #111;
display: block;
}
.rest-time {
// margin-left: 100rpx;
font-size: 24rpx;
// color: inherit;
margin-left: 40rpx;
flex-shrink: 0;
}
.action-totalWeight {
font-size: 26rpx;
color: #333;
margin-top: 6rpx;
display: block;
}
}
.check {
font-size: 26rpx;
color: #333;
margin-left: 20rpx;
/* 这里控制和左边内容的距离,数值你可以自己调 */
flex-shrink: 0;
/* 防止被压缩 */
}
.action-bottom {
margin-top: 6rpx;
margin-left: -96rpx;
}
// 右侧:修改 + 对勾
// .action-right {
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: space-between;
// height: 100%;
// }
.action-right {
flex-shrink: 0;
}
.action-right {
flex-shrink: 0;
.modify-btn {
font-size: 26rpx;
color: #333;
background: #f2f3f5;
border: none;
border-radius: 40rpx;
padding: 8rpx 20rpx;
line-height: 1;
.modify-btn {
font-size: 26rpx;
color: #333;
background: #f2f3f5;
border: none;
border-radius: 40rpx;
padding: 8rpx 20rpx;
line-height: 1;
}
}
}
// .check-icon {
// font-size: 30rpx;
// color: #333;
// margin-top: 24rpx;
// }
//=========================
/* 空状态区域 */
.empty-section {
flex: 1;
... ... @@ -1301,32 +1087,32 @@ $radius-round: 40rpx;
justify-content: center;
padding: 80rpx 40rpx 120rpx;
box-sizing: border-box;
}
.empty-img {
width: 360rpx;
height: 360rpx;
margin-bottom: 40rpx;
transform: scale(3);
}
.empty-img {
width: 360rpx;
height: 360rpx;
margin-bottom: 40rpx;
transform: scale(1.5);
}
.empty-tip {
font-size: 32rpx;
color: #666;
font-weight: 500;
margin-bottom: 80rpx;
letter-spacing: 2rpx;
}
.empty-tip {
font-size: 32rpx;
color: #666;
font-weight: 500;
margin-bottom: 80rpx;
letter-spacing: 2rpx;
}
.add-train-btn {
font-size: 30rpx;
color: #000;
background: $color-accent;
border-radius: $radius-round;
padding: 24rpx 80rpx;
border: none;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3);
.add-train-btn {
font-size: 30rpx;
color: #000;
background: $color-accent;
border-radius: 40rpx;
padding: 24rpx 80rpx;
border: none;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3);
}
}
/* 底部课程类型区域 */
... ... @@ -1346,25 +1132,25 @@ $radius-round: 40rpx;
background: #fff;
border-radius: 12rpx;
padding: 30rpx 0;
}
.course-icon {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
margin-bottom: 16rpx;
}
.course-icon {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
margin-bottom: 16rpx;
}
.course-title {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.course-title {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.course-desc {
font-size: 24rpx;
color: #999;
.course-desc {
font-size: 24rpx;
color: #999;
}
}
/* 添加自助训练底部弹窗 */
... ... @@ -1389,14 +1175,6 @@ $radius-round: 40rpx;
box-sizing: border-box;
}
// .popup-indicator {
// width: 80rpx;
// height: 8rpx;
// background: #ddd;
// border-radius: 4rpx;
// margin: 0 auto 30rpx;
// }
.popup-title {
font-size: 34rpx;
color: #333;
... ... @@ -1410,46 +1188,41 @@ $radius-round: 40rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.popup-option-btn {
width: 100%;
height: 80rpx;
font-size: 30rpx;
// color: #ffffff;
// background: #f5f5f5;
border: none;
// border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15rpx;
.popup-option-btn {
width: 100%;
height: 80rpx;
font-size: 30rpx;
border: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15rpx;
.small {
font-size: 18rpx;
.small {
font-size: 18rpx;
}
}
}
// 更多弹窗
/* ========== 更多弹窗样式 ========== */
:deep(.up-popup__content) {
border-radius: 10rpx 10rpx 0 0;
background: #f5f5f5;
padding: 30rpx 20rpx;
}
/* 更多弹窗 */
// ::deep(.up-popup__content) {
// border-radius: 10rpx 10rpx 0 0;
// background: #f5f5f5;
// padding: 30rpx 20rpx;
// }
.popup-header {
text-align: center;
margin-top: 30rpx;
margin-bottom: 30rpx;
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
color: #111;
.popup-title {
font-size: 34rpx;
font-weight: 600;
color: #111;
}
}
.popup-list {
... ... @@ -1464,15 +1237,23 @@ $radius-round: 40rpx;
padding: 24rpx 20rpx;
background: transparent;
gap: 16rpx;
}
.popup-item:not(:last-child) {
border-bottom: 1rpx solid #eee;
}
&:not(:last-child) {
border-bottom: 1rpx solid #eee;
}
.item-text {
font-size: 30rpx;
color: #333;
.item-text {
font-size: 30rpx;
color: #333;
}
&.delete-item {
background: #fff;
.item-text {
color: #ff4d4f;
}
}
}
.popup-divider {
... ... @@ -1481,56 +1262,46 @@ $radius-round: 40rpx;
margin: 20rpx 0;
}
.delete-item {
background: #fff;
}
.delete-text {
color: #ff4d4f;
}
/* 按钮通用重置 */
button {
line-height: 1;
}
.container {
/* 按钮通用重置 */
button {
line-height: 1;
button::after {
border: none;
&::after {
border: none;
}
}
}
/* 训记切换标签 */
.plan-tabs {
white-space: nowrap;
padding: 16rpx 20rpx;
background: #fff;
// 给内部的标签留出间距,同时让它不换行
// display: flex;
gap: 16rpx;
}
.tab-btn {
display: inline-block;
padding: 10rpx 24rpx;
border-radius: 32rpx;
font-size: 26rpx;
background: #f2f2f2;
border: none;
flex-shrink: 0;
white-space: nowrap;
}
.tab-btn {
display: inline-block;
padding: 10rpx 24rpx;
border-radius: 32rpx;
font-size: 26rpx;
background: #f2f2f2;
border: none;
flex-shrink: 0;
white-space: nowrap;
.tab-btn.active {
background: #000;
color: #fff;
&.active {
background: #000;
color: #fff;
}
}
}
/* ========== 超级组样式 ========== */
/* 超级组样式 */
.superset-wrapper {
margin-bottom: 20rpx;
background: $color-white;
border-radius: $radius-md;
border-radius: 12rpx;
overflow: hidden;
.super-head {
... ... @@ -1613,9 +1384,8 @@ button::after {
width: 37rpx;
height: 37rpx;
text-align: center;
flex-shrink: 0; // /* 不被父容器挤压变形,保持固定大小 */
flex-shrink: 0;
border-radius: 50%;
// line-height: 60rpx;
}
.set-column {
... ... @@ -1624,23 +1394,20 @@ button::after {
}
.action-in-set {
// display: flex;
// flex-direction: column;
width: 100%;
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.action-in-set:last-child {
margin-bottom: 0;
&:last-child {
margin-bottom: 0;
}
}
.action-letter {
display: inline-block;
font-size: 26rpx;
color: #666;
// width: 35rpx;
white-space: nowrap;
margin-right: 12rpx;
flex-shrink: 0;
... ... @@ -1650,11 +1417,14 @@ button::after {
font-size: 26rpx;
color: inherit;
flex: 1;
.detai-data {
white-space: nowrap;
}
}
.rest-time {
font-size: 24rpx;
// color: inherit;
margin-left: 10rpx;
flex-shrink: 0;
}
... ... @@ -1665,44 +1435,43 @@ button::after {
margin-left: 275rpx !important;
flex-shrink: 0;
}
}
.superset-title {
font-size: 28rpx;
font-weight: bold;
padding: 12rpx 20rpx;
background: #fff6cc;
border-radius: 12rpx;
margin-bottom: 12rpx;
.superset-title {
font-size: 28rpx;
font-weight: bold;
padding: 12rpx 20rpx;
background: #fff6cc;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
}
// 日程备注
/* 日程备注 */
.noteContent {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 20rpx;
background: $color-white;
}
.noteTitle {
font-size: 26rpx;
color: #666;
flex-shrink: 0;
width: 150rpx;
}
.noteTitle {
font-size: 26rpx;
color: #666;
flex-shrink: 0;
width: 150rpx;
}
.noteTags {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.noteTags {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
}
// u-tag 微调
:deep(.noteTag) {
width: auto !important;
font-size: 24rpx;
border-radius: 10rpx;
}
</style>
\ No newline at end of file
// ::deep(.noteTag) {
// width: auto !important;
// font-size: 24rpx;
// border-radius: 10rpx;
// }
</style>
... ...
... ... @@ -25,7 +25,7 @@
<script setup>
import dayjs from 'dayjs';
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, onMounted } from 'vue';
const emit = defineEmits(['dateTypeChange', 'dateRangeChange']);
... ... @@ -102,6 +102,12 @@ watch(
},
{ immediate: true },
);
// 首次挂载时补发初始 dateTypeChange,确保父组件 handleTypeChange 被调用,
// 解决首次进入页面"周"数据不显示的问题(与手动切换行为保持一致)。
onMounted(() => {
emit('dateTypeChange', currentDateType.value);
});
</script>
<style scoped lang="scss">
... ...
... ... @@ -57,6 +57,7 @@
<script setup>
import { ref, onMounted, watch } from 'vue';
import dayjs from 'dayjs';
import QueryPlanApi from '@/sheep/api/plan/queryplan';
import WodeJihuaLibiaoTancuang from '@/pages/xunji/components/wode-jihua-libiao-tancuang.vue'
import { useTrainingStore } from '@/sheep/store/trainingStore'
... ... @@ -88,9 +89,13 @@ const props = defineProps({
isMyPlan: {
type: Boolean,
default: false
},
selectedDate: {
type: String,
default: () => dayjs().format('YYYY-MM-DD')
}
})
const emit = defineEmits(['update:visible', 'getPlanListLength'])
const emit = defineEmits(['update:visible', 'getPlanListLength', 'success'])
// 计划详情
const MyPlanDetail = ref({})
... ... @@ -135,21 +140,21 @@ const onSelectPlan = (planId) => {
}, 500)
}
// ====================== 添加到今日 ======================
// ====================== 添加到选中日期 ======================
const addTemplateToToday = async (templateId) => {
if (!templateId) {
uni.showToast({ title: '未找到训练模板', icon: 'none' });
return;
}
const todayDate = new Date().toISOString().split('T')[0];
const reqData = {
id: templateId,
trainDateList: [todayDate]
trainDateList: [props.selectedDate]
};
try {
const res = await QueryPlanApi.addPlanToCalendar(reqData);
if (res.code === 0) {
uni.showToast({ title: '添加到今日成功', icon: 'success' });
uni.showToast({ title: '添加成功', icon: 'success' });
emit('success');
emit('update:visible', false);
} else {
uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
... ...
... ... @@ -4,7 +4,7 @@
<view class="top-bar">
<view class="search-wrapper">
<up-search v-model="searchKeyword" placeholder="搜索动作名称" :showAction="false" shape="round" bgColor="#f3f4f6"
placeholderColor="#9ca3af" searchIconColor="#6b7280" @clear="searchKeyword = ''" />
placeholderColor="#9ca3af" searchIconColor="#6b7280" @search="handleSearch" @clear="handleClearSearch" />
</view>
<view class="add-btn-wrapper">
... ... @@ -36,13 +36,16 @@
<view class="layout-container">
<!-- 左侧一级分类导航 -->
<scroll-view scroll-y class="left-nav" :show-scrollbar="false">
<view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick">
<up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>
<text class="nav-text">收藏</text>
</view>
<template v-if="!isSearchMode">
<view class="nav-item" :class="{ active: activeNav === 'collect' }" @click="handleCollectClick">
<up-icon name="star-fill" size="14" :color="activeNav === 'collect' ? '#10b981' : '#9ca3af'"></up-icon>
<text class="nav-text">收藏</text>
</view>
</template>
<view v-for="nav in navItems" :key="nav.id" class="nav-item" :class="{ active: activeNav === nav.id }"
@click="switchNav(nav.id)">
<view v-for="nav in displayNavItems" :key="nav.id" class="nav-item"
:class="{ active: isSearchMode ? activeSearchCategoryId === nav.id : activeNav === nav.id }"
@click="handleNavClick(nav.id)">
<text class="nav-text">{{ nav.name }}</text>
</view>
</scroll-view>
... ... @@ -142,28 +145,30 @@ const searchKeyword = ref('');
const addShow = ref(false);
const actionDetailRef = ref(null);
// 搜索模式状态
const isSearchMode = computed(() => !!searchKeyword.value.trim());
const searchCategoryList = ref([]);
const activeSearchCategoryId = ref(null);
const searchCategoryData = ref([]);
// 左栏导航项:搜索模式显示部位列表,正常模式显示分类列表
const displayNavItems = computed(() => {
return isSearchMode.value ? searchCategoryList.value : navItems.value;
});
// 右栏动作列表:搜索模式返回选中部位的器械分组,正常模式直接返回
const displayExercises = computed(() => {
if (!searchKeyword.value.trim()) {
return exercises.value;
if (isSearchMode.value && activeSearchCategoryId.value) {
const category = searchCategoryData.value.find(item => item.categoryId === activeSearchCategoryId.value);
return category ? category.equipments : [];
}
const kw = searchKeyword.value.toLowerCase().trim();
return exercises.value
.map((group) => {
const filtered = (group.exercises || []).filter((ex) =>
ex.name && ex.name.toLowerCase().includes(kw)
);
return {
...group,
exercises: filtered,
};
})
.filter((group) => group.exercises && group.exercises.length > 0);
return exercises.value;
});
const handlePartClick = async (id) => {
try {
activeMotionPart.value = id;
searchKeyword.value = '';
const exerciseRes = await ExercisesApi.getexercises({
categoriesId: activeNav.value,
subCategoriesId: id,
... ... @@ -174,6 +179,60 @@ const handlePartClick = async (id) => {
}
};
// 服务端搜索动作
const handleSearch = async () => {
const keyword = searchKeyword.value.trim();
if (!keyword) return;
// 收藏Tab和超级组Tab不支持关键词搜索
if (activeNav.value === 'collect' || activeNav.value === 'super') return;
try {
await refreshCategories();
const exerciseRes = await ExercisesApi.getExercisesByCategory({
name: keyword,
});
const data = exerciseRes.data || [];
searchCategoryData.value = data;
// 搜索结果按部位分组,提取部位列表作为左栏导航
searchCategoryList.value = data.map(item => ({
id: item.categoryId,
name: item.categoryName,
}));
// 默认选中第一个部位
activeSearchCategoryId.value = searchCategoryList.value.length > 0 ? searchCategoryList.value[0].id : null;
} catch (e) {
console.error('搜索动作失败:', e);
}
};
// 清空搜索,重新加载当前分类数据
const handleClearSearch = async () => {
searchKeyword.value = '';
searchCategoryList.value = [];
activeSearchCategoryId.value = null;
searchCategoryData.value = [];
if (activeNav.value === 'collect') {
loadCollectList();
} else if (activeNav.value === 'super') {
// 超级组不需要重新加载
} else if (activeNav.value) {
await refreshCategories();
loadExercises(activeNav.value);
}
};
// 左栏导航点击:搜索模式按部位筛选,正常模式切换分类
const handleNavClick = (id) => {
if (isSearchMode.value) {
activeSearchCategoryId.value = id;
} else {
switchNav(id);
}
};
const handleCollectClick = () => {
activeNav.value = 'collect';
activeMotionPart.value = '';
... ... @@ -203,20 +262,24 @@ const loadSuperFavoriteList = async () => {
}
};
// 加载大类
const loadCategories = async () => {
// 仅刷新分类列表,不切换当前选中分类
const refreshCategories = async () => {
try {
await actionStore.getloadCategories();
navItems.value = actionStore.showCategories;
if (navItems.value.length > 0) {
switchNav(navItems.value[0].id);
}
} catch (error) {
console.error('获取分类失败:', error);
}
};
// 加载大类(首次加载,自动切换到第一个分类)
const loadCategories = async () => {
await refreshCategories();
if (navItems.value.length > 0) {
switchNav(navItems.value[0].id);
}
};
// 加载动作列表
const loadExercises = async (categoriesId) => {
try {
... ... @@ -374,7 +437,7 @@ onMounted(() => {
.left-nav {
width: 180rpx;
height: 88%;
background-color: #f9fafb;
border-right: 1rpx solid #f3f4f6;
... ...
... ... @@ -28,36 +28,41 @@
</view>
<!-- 日期网格 (动态 5行 或 6行) -->
<view class="date-grid">
<view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{
'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0)
}" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)">
<!-- 日期数字 / 今日徽章 -->
<view class="date-header">
<text v-if="date.today" class="today-badge">今</text>
<text v-else class="date-number">
{{ date.day }}
</text>
</view>
<!-- 计划/容量数据列表 -->
<view class="plan-list"
v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)">
<template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx">
<view class="plan-tag" :class="`tag-type-${item.type}`"
:style="item.bg ? { backgroundColor: item.bg } : {}">
<text class="tag-text">{{ item.text }}</text>
</view>
</template>
<scroll-view class="scroll-view" scroll-y>
<view class="date-grid">
<view v-for="(date, index) in dates" :key="index" class="date-cell" :class="{
'has-data': date.planList.length || date.noteList.length || (date.weight && date.weight > 0)
}" :style="{ backgroundColor: getCellBgColor(date) }" @tap="selectDate(date)">
<!-- 日期数字 / 今日徽章 -->
<view class="date-header">
<text v-if="date.today" class="today-badge">今</text>
<text v-else class="date-number">
{{ date.day }}
</text>
</view>
<!-- 溢出剩余条数 -->
<view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">
+{{ getCellVisibleItems(date).remaining }}
<!-- 计划/容量数据列表 -->
<view class="plan-list"
v-if="date.planList.length || date.noteList.length || (date.weight && date.weight > 0)">
<template v-for="(item, idx) in getCellVisibleItems(date).items" :key="idx">
<view class="plan-tag" :class="`tag-type-${item.type}`"
:style="item.bg ? { backgroundColor: item.bg } : {}">
<text class="tag-text">{{ item.text }}</text>
</view>
</template>
<!-- 溢出剩余条数 -->
<view v-if="getCellVisibleItems(date).remaining > 0" class="plan-extra">
+{{ getCellVisibleItems(date).remaining }}
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 单元格详情弹窗 -->
... ... @@ -74,34 +79,50 @@
<view class="explain-list">
<view class="explain-item">
<view class="color-block blue"></view>
<view class="item-info">
<text class="item-label">时长</text>
<text class="item-desc">在场馆的实际运动时长</text>
<view class="explain-item-left">
<view class="color-block yellow">
</view>
<view class="item-info">
<text class="item-label">已完成</text>
<text class="item-desc">已完成或已结课的训练记录</text>
</view>
</view>
</view>
<view class="explain-item">
<view class="color-block green"></view>
<view class="item-info">
<text class="item-label">容量</text>
<text class="item-desc">力量训练累计产生的总负重重量</text>
<view class="explain-item-left">
<view class="color-block gray">
</view>
<view class="item-info">
<text class="item-label">待训练</text>
<text class="item-desc">已安排的待训练计划</text>
</view>
</view>
</view>
<view class="explain-item">
<view class="color-block yellow"></view>
<view class="item-info">
<text class="item-label">课程</text>
<text class="item-desc">训记完成或已结课的线下课程</text>
<view class="explain-item-left">
<view class="color-block green">
</view>
<view class="item-info">
<text class="item-label">训练容量</text>
<text class="item-desc">力量训练累计产生的总负重</text>
</view>
</view>
</view>
<view class="explain-item">
<view class="color-block gray"></view>
<view class="item-info">
<text class="item-label">排课</text>
<text class="item-desc">训记排课计划或待上课的课程</text>
<view class="explain-item-left">
<view class="color-block blue">
</view>
<view class="item-info">
<text class="item-label">日程备注</text>
<text class="item-desc">相关日期的备注标记</text>
</view>
</view>
</view>
</view>
... ... @@ -111,7 +132,7 @@
</template>
<script setup>
import { ref, watch, computed, nextTick } from 'vue';
import { ref, watch, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
import dayjs from 'dayjs';
import DailyTemplateApi from '@/sheep/api/calendar/date';
import GridCellContentPopup from '@/pages/xunji/components/rili-components/grid-cell-content-popup.vue';
... ... @@ -250,7 +271,7 @@ const loadPlans = async (monthStr) => {
// 按 sourceType + sourceName 分组
const sourceMap = new Map();
plan.templates.forEach((template) => {
if (!template || !template.name) return;
if (!template) return;
const sourceKey = template.sourceType
? `src_${template.sourceType}_${template.sourceName || ''}`
: `tpl_${template.dailyTemplateId}`;
... ... @@ -323,6 +344,14 @@ const handleRefreshCalendar = () => {
loadPlans(currentMonth.value);
};
// 监听来自 xunji-moban-xiangqing 模板添加成功的刷新事件
onMounted(() => {
uni.$on('calendarDataRefresh', handleRefreshCalendar);
});
onBeforeUnmount(() => {
uni.$off('calendarDataRefresh', handleRefreshCalendar);
});
watch(
currentMonth,
(newVal) => {
... ... @@ -395,136 +424,146 @@ const selectDate = async (date) => {
}
}
.date-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10rpx;
}
/* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
.date-cell {
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 12rpx;
height: 200rpx;
padding: 8rpx 4rpx;
box-sizing: border-box;
transition: all 0.2s ease;
&:active {
filter: brightness(0.96);
}
&.other-month {
opacity: 0.5;
border-color: #f9fafc;
}
.scroll-view {
height: 100%;
.date-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10rpx;
// #ifdef MP-WEIXIN
padding-bottom: 100rpx;
// #endif
/* 日期格子基础样式:保证每个格子都有独立背景与微细边框 */
.date-cell {
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 12rpx;
height: 200rpx;
padding: 8rpx 4rpx;
box-sizing: border-box;
transition: all 0.2s ease;
&.is-today {
border: 2rpx solid #3b82f6;
}
&:active {
filter: brightness(0.96);
}
&.has-data {
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);
}
&.other-month {
opacity: 0.5;
border-color: #f9fafc;
}
.date-header {
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6rpx;
&.is-today {
border: 2rpx solid #3b82f6;
}
.date-number {
font-size: 26rpx;
font-weight: 600;
color: #1f2937;
&.has-data {
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.02);
}
.date-header {
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6rpx;
}
.date-number {
font-size: 26rpx;
font-weight: 600;
color: #1f2937;
.today-badge {
font-weight: 600;
}
}
}
.today-badge {
.plan-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 6rpx;
font-weight: 600;
.plan-tag {
width: 100%;
border-radius: 6rpx;
padding: 2rpx 4rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.85);
/* 使 Tag 悬浮于彩色格子之上 */
.tag-text {
font-size: 18rpx;
line-height: 22rpx;
color: #111827;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
width: 100%;
}
&.tag-type-weight {
background-color: #bbf7d0 !important;
.tag-text {
color: #14532d;
font-weight: 700;
}
}
&.tag-type-note {
background-color: #ffffff;
.plan-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 6rpx;
.plan-tag {
width: 100%;
border-radius: 6rpx;
padding: 2rpx 4rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.85);
/* 使 Tag 悬浮于彩色格子之上 */
.tag-text {
font-size: 18rpx;
line-height: 22rpx;
color: #111827;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
width: 100%;
}
&.tag-type-weight {
background-color: #bbf7d0 !important;
.tag-text {
color: #14532d;
font-weight: 700;
}
}
&.tag-type-note {
background-color: #ffffff;
.tag-text {
color: #374151;
}
}
}
.tag-text {
color: #374151;
.plan-extra {
font-size: 16rpx;
color: #4b5563;
text-align: center;
line-height: 20rpx;
font-weight: 600;
}
}
}
.plan-extra {
font-size: 16rpx;
color: #4b5563;
text-align: center;
line-height: 20rpx;
font-weight: 600;
}
}
}
}
}
.explain-popup-content {
padding: 36rpx 32rpx 48rpx 32rpx;
background-color: #ffffff;
padding: 40rpx 32rpx 60rpx 32rpx;
background-color: #f8f9fb;
.explain-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
margin-bottom: 36rpx;
.explain-title {
font-size: 32rpx;
font-weight: 600;
font-size: 34rpx;
font-weight: 700;
color: #111827;
}
}
... ... @@ -532,50 +571,105 @@ const selectDate = async (date) => {
.explain-list {
display: flex;
flex-direction: column;
gap: 28rpx;
gap: 20rpx;
.explain-item {
display: flex;
align-items: center;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 24rpx 24rpx 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.explain-item-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.color-block {
width: 36rpx;
height: 36rpx;
border-radius: 8rpx;
margin-right: 24rpx;
width: 100rpx;
height: 92rpx;
border-radius: 14rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
&.blue {
background-color: #bae6fd;
&.yellow {
background-color: #ffdd44;
}
&.gray {
background-color: #e8e8e8;
}
&.green {
background-color: #bbf7d0;
}
&.yellow {
background-color: #fef08a;
&.blue {
background-color: #bae6fd;
}
&.gray {
background-color: #e5e7eb;
.mini-cell-bg {
width: 58rpx;
height: 58rpx;
background: rgba(255, 255, 255, 0.7);
border-radius: 8rpx;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 4rpx;
}
.mini-tag {
font-size: 14rpx;
font-weight: 600;
border-radius: 4rpx;
padding: 1rpx 6rpx;
line-height: 20rpx;
white-space: nowrap;
&.yellow-tag {
background: #fff3b0;
color: #8b6914;
}
&.gray-tag {
background: #f0f0f0;
color: #555;
}
&.green-tag {
background: #a7f3d0;
color: #14532d;
}
&.blue-tag {
background: #7dd3fc;
color: #0c4a6e;
}
}
}
.item-info {
display: flex;
align-items: center;
gap: 16rpx;
flex-direction: column;
gap: 6rpx;
flex: 1;
min-width: 0;
.item-label {
font-size: 28rpx;
font-size: 30rpx;
font-weight: 600;
color: #1f2937;
width: 72rpx;
line-height: 1.3;
}
.item-desc {
font-size: 26rpx;
color: #6b7280;
font-size: 24rpx;
color: #9ca3af;
line-height: 1.4;
}
}
}
... ...
... ... @@ -50,7 +50,7 @@
<view class="part-select-wrapper">
<up-select v-model:current="partId"
:label="partOptions.find((item) => item.id === partId)?.name || '全部'" :options="partOptions"
@select="onPartChange"></up-select>
maxHeight="400rpx" @select="onPartChange"></up-select>
</view>
</view>
... ... @@ -65,22 +65,25 @@
<!-- 统计图表(根据 showLastPeriod 切换 柱状图 / 折线图) -->
<view class="chart-container">
<template v-if="!showLastPeriod">
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
chartType="column" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
</template>
<template v-else>
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
chartType="line" :reshow="true" :extra="{ column: { width: trendChartColumnWidth } }" />
</template>
<LineChart v-if="chartCategories.length > 0" :categories="chartCategories" :series="chartSeries"
:chartType="showLastPeriod ? 'line' : 'column'"
:reshow="true"
:extra="{ column: { width: trendChartColumnWidth } }"
:chartId="'trend-chart'"
/>
</view>
<!-- 显示上周/上月数据勾选框 -->
<view class="toggle-checkbox" @click="toggleLastWeek">
<checkbox :checked="showLastPeriod" color="#10b981" style="transform: scale(0.8);" />
<text class="toggle-label">
{{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
</text>
<view class="toggle-checkbox">
<up-checkbox-group @change="toggleLastWeek">
<up-checkbox name="showLastPeriod" :checked="showLastPeriod" activeColor="#10b981" size="20" shape="square">
<template #label>
<text class="toggle-label">
{{ MainCurrentDateType === 'week' ? '显示上周数据' : '显示上月数据' }}
</text>
</template>
</up-checkbox>
</up-checkbox-group>
</view>
<!-- 3. 数据对比区域 -->
... ... @@ -138,19 +141,19 @@
<!-- 容量/组数 切换标签 -->
<view class="muscle-tabs">
<view class="tab-item" :class="{ active: muscleTabIndex === 0 }" @click="muscleTabIndex = 0">
<view class="tab-item" :class="{ active: partTabIndex === 0 }" @click="partTabIndex = 0">
容量
</view>
<view class="tab-item" :class="{ active: muscleTabIndex === 1 }" @click="muscleTabIndex = 1">
<view class="tab-item" :class="{ active: partTabIndex === 1 }" @click="partTabIndex = 1">
组数
</view>
</view>
<view class="tip">{{ TREND_TIPS[muscleTabIndex] }}</view>
<view class="tip">{{ TREND_TIPS[partTabIndex] }}</view>
<!-- 图表区域 -->
<view class="statistical">
<LineChart v-if="muscleShow && muscleChartCategories.length"
:key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${muscleTabIndex}`"
:key="`muscle-chart-${currentMuscleId}-${MainCurrentDateType}-${partTabIndex}`"
:categories="muscleChartCategories" :series="muscleChartSeries" chartType="column" :reshow="true"
:extra="{ column: { width: muscleChartColumnWidth } }" />
</view>
... ... @@ -266,7 +269,6 @@ const trainingData = ref({ setCountMap: {}, totalVolumeMap: {} });
// --- 肌肉弹窗 ---
const muscleShow = ref(false);
const muscleTabIndex = ref(0);
const currentMuscleId = ref(null);
const currentMuscleName = ref('');
const muscleDetailData = ref({ muscleId: 0, dailyStatsList: [] });
... ... @@ -345,11 +347,10 @@ const chartSeries = computed(() => {
const dataKey = TREND_DATA_KEYS[NumberTabIndex.value] || 'volume';
/** 单位转换:秒→分钟,米→公里,保留一位小数 */
/** 单位转换:秒→分钟,保留一位小数;距离后端已返回 km,无需转换 */
const convertVal = (raw) => {
const num = Number(raw) || 0;
if (dataKey === 'duration') return parseFloat((num / 60).toFixed(1));
if (dataKey === 'distance') return parseFloat((num / 1000).toFixed(1));
return num;
};
... ... @@ -369,9 +370,9 @@ const chartSeries = computed(() => {
return showLastPeriod.value
? [
{ name: lastName, data: lastData, color: '#5b8ff9' },
{ name: currName, data: currData, color: '#ffc53d' },
]
{ name: lastName, data: lastData, color: '#5b8ff9' },
{ name: currName, data: currData, color: '#ffc53d' },
]
: [{ name: currName, data: currData, color: '#ffc53d' }];
});
... ... @@ -380,15 +381,13 @@ const listData = computed(() => {
if (!weeklyData.value) return [];
const d = weeklyData.value;
/** 单位转换:秒→分钟,米→公里,保留一位小数 */
/** 单位转换:秒→分钟,保留一位小数;距离后端已返回 km,无需转换 */
const toMin = (v) => parseFloat(((v ?? 0) / 60).toFixed(1));
const toKm = (v) => parseFloat(((v ?? 0) / 1000).toFixed(1));
const fields = [
{ label: '容量', last: d.lastWeekTotalVolume, curr: d.totalVolume },
{ label: '组数', last: d.lastWeekTotalSets, curr: d.totalSets },
{ label: '时长', last: toMin(d.lastWeekTotalDuration), curr: toMin(d.totalDuration) },
{ label: '距离', last: toKm(d.lastWeekTotalDistance), curr: toKm(d.totalDistance) },
{ label: '距离', last: d.lastWeekTotalDistance, curr: d.totalDistance },
{ label: '次数', last: d.lastWeekTotalCount, curr: d.totalCount },
];
... ... @@ -420,10 +419,10 @@ const convertUnit = (val, key) => {
/** 肌肉弹窗图表 X 轴 */
const muscleChartCategories = computed(() => {
if (!muscleFullDates.value.length) return [];
if (MainCurrentDateType.value === 'week') {
return WEEKDAY_LABELS;
}
if (!muscleFullDates.value.length) return [];
return muscleFullDates.value.map((d) => dayjs(d).format('M/D'));
});
... ... @@ -432,9 +431,9 @@ const muscleSetsArr = computed(() => muscleProcessedData.value.setsArr || []);
/** 肌肉弹窗图表 Series */
const muscleChartSeries = computed(() => {
const data = muscleTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value;
const name = muscleTabIndex.value === 0 ? '容量' : '组数';
const color = muscleTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9';
const data = partTabIndex.value === 0 ? muscleVolumeArr.value : muscleSetsArr.value;
const name = partTabIndex.value === 0 ? '容量' : '组数';
const color = partTabIndex.value === 0 ? '#ffc53d' : '#5b8ff9';
return [{ name, data: data || [], color }];
});
... ... @@ -566,20 +565,14 @@ const onPartChange = (item) => {
loadCategoryData(startDate.value);
};
/** 时间类型切换(周/月) */
const handleTypeChange = async (type) => {
/** 时间类型切换(周/月)
* 只负责更新类型状态,不加载数据。
* switchDateType 会同步触发 watch → emit dateRangeChange → handleDateChange,
* handleDateChange 拿到正确的日期后再统一加载数据,避免用旧日期请求接口。
*/
const handleTypeChange = (type) => {
MainCurrentDateType.value = type;
showLastPeriod.value = false;
loadTrainingData(startDate.value, endDate.value);
if (startDate.value) {
await loadCategoryData(startDate.value);
}
// 如果肌肉弹窗打开,同步刷新
if (muscleShow.value && currentMuscleId.value) {
await loadMuscleData(currentMuscleId.value, startDate.value);
}
};
/** 日期范围变更 */
... ... @@ -587,7 +580,7 @@ const handleDateChange = async (data) => {
startDate.value = data.startDate;
endDate.value = data.endDate;
await loadTrainingData(startDate.value, endDate.value);
loadCategoryData(startDate.value);
await loadCategoryData(startDate.value);
if (muscleShow.value && currentMuscleId.value) {
await loadMuscleData(currentMuscleId.value, startDate.value);
... ... @@ -598,7 +591,8 @@ const handleDateChange = async (data) => {
const openMuscleDetail = async (muscleId) => {
currentMuscleId.value = muscleId;
muscleShow.value = true;
muscleTabIndex.value = 0;
muscleFullDates.value = [];
muscleProcessedData.value = { volumeArr: [], setsArr: [] };
const muscle = subCategorieList.value.find((item) => item.id === muscleId);
currentMuscleName.value = muscle?.name || '';
... ... @@ -619,8 +613,8 @@ const closeMuscleDetail = () => {
};
/** 切换显示上一周期数据 */
const toggleLastWeek = () => {
showLastPeriod.value = !showLastPeriod.value;
const toggleLastWeek = (vals) => {
showLastPeriod.value = vals.includes('showLastPeriod');
if (startDate.value) {
loadCategoryData(startDate.value);
}
... ... @@ -631,6 +625,7 @@ const toggleLastWeek = () => {
onMounted(async () => {
await loadbodyPartOptions();
await loadGetSubCategorieList();
if (startDate.value && endDate.value) {
await loadTrainingData(startDate.value, endDate.value);
await loadCategoryData(startDate.value);
... ... @@ -701,7 +696,7 @@ $radius-item: 12rpx;
.training-data-page {
display: flex;
flex-direction: column;
width: 100vw;
background-color: $color-bg;
overflow: hidden;
... ... @@ -865,8 +860,13 @@ $radius-item: 12rpx;
border-radius: 4rpx;
margin-right: 8rpx;
&.blue { background-color: $color-blue; }
&.yellow { background-color: $color-yellow; }
&.blue {
background-color: $color-blue;
}
&.yellow {
background-color: $color-yellow;
}
}
}
}
... ... @@ -886,8 +886,13 @@ $radius-item: 12rpx;
border-bottom: 1rpx solid #f0f0f0;
font-size: 26rpx;
&:last-child { border-bottom: none; }
&:nth-child(odd) { background-color: #f9fafc; }
&:last-child {
border-bottom: none;
}
&:nth-child(odd) {
background-color: #f9fafc;
}
.label {
flex: 1.5;
... ... @@ -903,8 +908,13 @@ $radius-item: 12rpx;
&.diff {
font-weight: 600;
&.positive { color: $color-green; }
&.negative { color: $color-red; }
&.positive {
color: $color-green;
}
&.negative {
color: $color-red;
}
}
}
}
... ...
... ... @@ -311,6 +311,9 @@ onMounted(() => {
width: 180rpx;
background-color: #ffffff;
border-radius: 16rpx;
min-height: 70vh;
height: 88%;
// #ifdef MP-WEIXIN
height: 86%;
... ... @@ -348,6 +351,7 @@ onMounted(() => {
flex: 1;
padding: 0 20rpx;
box-sizing: border-box;
min-height: 70vh;
height: 88%;
// #ifdef MP-WEIXIN
height: 86%;
... ...
... ... @@ -36,7 +36,7 @@
@click="navigateTo('/pages4/pages/xunji/xunji-wode-moban')" />
<up-cell title="我的计划" icon="order" :isLink="true"
@click="navigateTo('/pages4/pages/xunji/xunji-wode-jihua')" />
<up-cell title="训记使用教程" icon="question-circle" :isLink="true" @click="goTutorial" />
<up-cell title="训记使用教程" icon="question-circle" :isLink="true" @click="navigateTo('/pages4/pages/xunji/shiyon-jiaochen')" />
</up-cell-group>
</view>
</up-popup>
... ... @@ -61,7 +61,7 @@ import TrainingFloating from '@/pages/TrainingFloating.vue';
const userStore = useUserStore();
const drawerShow = ref(false);
const currentTab = ref(3);
const currentTab = ref(1);
const menuButtonHeight = ref(getMenuButtonHeight());
const topSafeArea = ref(getTopSafeArea());
... ... @@ -73,10 +73,7 @@ PageHeight.value = menuButtonHeight.value + topSafeArea.value;
const userInfo = computed(() => userStore.userInfo || {});
onLoad((options) => {
if (!userStore.isLogin) {
uni.redirectTo({ url: '/pages7/pages/index/login' });
return;
}
userStore.getInfo();
if (options && options.currentTab) {
currentTab.value = Number(options.currentTab);
... ... @@ -103,7 +100,7 @@ const handleTabClick = (index) => {
const goTutorial = () => { };
defineExpose({ switchTabIndex: handleTabClick });
</script>
... ...
... ... @@ -87,7 +87,7 @@ const sceneList = ref([
])
// 缺省图
const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260724/健身模板_1784871730506.png"
// ==================================
// 获取部位分类
... ...
<template>
<view class="tutorial-container">
<!-- 顶部导航栏 -->
<u-navbar title="使用教程" :autoBack="true" bgColor="#ffffff"
titleStyle="color: #303133; font-weight: 600; font-size: 32rpx;" leftIconColor="#303133" :placeholder="true"
fixed />
<view class="tutorial-body">
<!-- 1. 顶部应用概览 -->
<view class="overview-card">
<view class="overview-icon-wrapper">
<u-icon name="file-text-fill" color="#ffffff" size="28"></u-icon>
</view>
<view class="overview-content">
<text class="overview-title">什么是训记?</text>
<text class="overview-desc">
训记是一款专业的健身训练记录工具,帮助你科学规划训练计划、精准记录每次训练数据,让健身更高效。
</text>
</view>
</view>
<!-- 2. 核心功能 -->
<view class="section-container">
<view class="section-header">
<view class="header-line"></view>
<text class="header-title">核心功能</text>
<view class="header-line"></view>
</view>
<view class="features-grid">
<view class="feature-card" v-for="(item, index) in features" :key="index">
<view class="feature-icon" :style="{ backgroundColor: item.bgColor }">
<u-icon :name="item.icon" :color="item.iconColor" size="22"></u-icon>
</view>
<text class="feature-name">{{ item.name }}</text>
<text class="feature-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<!-- 3. 使用指南(步骤流程) -->
<view class="section-container">
<view class="section-header">
<view class="header-line"></view>
<text class="header-title">使用指南</text>
<view class="header-line"></view>
</view>
<view class="steps-wrapper">
<view class="step-card" v-for="(step, sIndex) in steps" :key="sIndex">
<view class="step-header">
<view class="step-badge">{{ sIndex + 1 }}</view>
<view class="step-titles">
<text class="step-main-title">{{ step.title }}</text>
<text class="step-sub-title">{{ step.subtitle }}</text>
</view>
</view>
<view class="step-details">
<view class="detail-item" v-for="(detail, dIndex) in step.details" :key="dIndex">
<view class="dot"></view>
<text class="detail-text">{{ detail }}</text>
</view>
</view>
<!-- 提示块:使用 uview-plus 标准图标 info-circle-fill -->
<view class="step-tip" v-if="step.tip">
<u-icon name="info-circle-fill" color="#e6a23c" size="16"></u-icon>
<text class="tip-text">{{ step.tip }}</text>
</view>
</view>
</view>
</view>
<!-- 4. 常见问题 (手风琴交互) -->
<view class="section-container">
<view class="section-header">
<view class="header-line"></view>
<text class="header-title">常见问题</text>
<view class="header-line"></view>
</view>
<view class="faq-wrapper">
<u-collapse accordion :border="false">
<u-collapse-item v-for="(faq, fIndex) in faqs" :key="fIndex" :name="fIndex">
<template #title>
<view class="faq-title-box">
<view class="faq-tag q-tag">Q</view>
<text class="faq-question-text">{{ faq.question }}</text>
</view>
</template>
<view class="faq-answer-box">
<view class="faq-tag a-tag">A</view>
<text class="faq-answer-text">{{ faq.answer }}</text>
</view>
</u-collapse-item>
</u-collapse>
</view>
</view>
<!-- 5. 底部标语 -->
<view class="footer-section">
<text class="footer-text">训练贵在坚持,训记陪你一同成长</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 1. 核心功能数据 (均为 uview-plus 官方内置标准图标名)
const features = ref([
{
icon: 'order',
iconColor: '#ff9900',
name: '训练计划',
desc: '创建和管理个性化训练计划',
bgColor: '#fff7cc'
},
{
icon: 'grid',
iconColor: '#19be6b',
name: '动作库',
desc: '丰富的训练动作库与详情',
bgColor: '#dbf5e6'
},
{
icon: 'calendar',
iconColor: '#2979ff',
name: '训练日历',
desc: '日历视图记录每日训练',
bgColor: '#ecf5ff'
},
{
icon: 'list-dot',
iconColor: '#fa3534',
name: '数据分析',
desc: '多维度训练数据统计分析',
bgColor: '#fef0f0'
}
])
// 2. 使用步骤数据
const steps = ref([
{
title: '创建训练计划',
subtitle: '打造你的专属训练方案',
details: [
'进入「我的」页面,点击「训练计划」进入计划管理',
'选择「新建计划」,可从官方模板快速创建,也可自定义空白计划',
'设置训练周期、训练日安排,拖拽调整动作顺序',
'保存后即可在训练日历中按计划执行'
],
tip: '建议新手从官方模板开始,逐步了解后再自定义计划'
},
{
title: '添加训练动作',
subtitle: '每个动作都值得被记录',
details: [
'在训练计划中点击「添加动作」,进入动作库',
'按部位分类浏览动作,或直接搜索动作名称',
'点击动作可查看详细说明、肌肉示意图和训练要点',
'选择动作后设置组数、次数、重量等训练参数'
],
tip: '支持创建超级组,将两个动作组合交替训练,提升效率'
},
{
title: '使用训练日历',
subtitle: '一目了然的训练记录',
details: [
'进入「训练」页面查看训练日历,日历标记已训练日期',
'点击日期可添加当日训练内容,从计划模板一键导入',
'已完成的训练会用不同颜色标识,方便回顾',
'长按训练记录可编辑或删除'
],
tip: '坚持每天打卡,日历会越来越丰富哦'
},
{
title: '记录训练数据',
subtitle: '精准追踪每次进步',
details: [
'训练过程中逐组录入重量、次数、休息时间等数据',
'内置计时器功能,方便控制组间休息时长',
'支持查看每个动作的历史训练数据对比',
'训练完成后自动汇总本次训练总量'
],
tip: '详细记录每次数据,才能看到自己的进步轨迹'
},
{
title: '查看训练分析',
subtitle: '数据驱动,科学训练',
details: [
'在「数据」页面查看训练频率、训练量趋势图表',
'统计各部位训练次数,发现训练偏重或遗漏',
'查看个人纪录(PR),见证力量增长',
'训练日历热力图直观展示训练频率'
],
tip: '定期回顾训练数据,及时调整训练计划方向'
}
])
// 3. 常见问题数据
const faqs = ref([
{
question: '训记是免费的吗?',
answer: '训记基础功能完全免费,包括训练计划创建、动作记录、日历打卡等核心功能均可畅快使用。'
},
{
question: '训练数据会丢失吗?',
answer: '训练数据会自动保存在云端,只要登录同一账号,更换设备数据也不会丢失。'
},
{
question: '如何从模板快速创建计划?',
answer: '在「我的模板」或「模板中心」选择适合自己的训练模板,点击「使用模板」即可一键生成训练计划。'
},
{
question: '一个计划可以包含多少动作?',
answer: '训练计划对动作数量没有硬性限制,你可以根据实际训练需求自由添加和排序。'
},
{
question: '支持自定义动作吗?',
answer: '支持!你可以创建个人专属动作,自定义动作名称、目标部位和训练参数。'
}
])
</script>
<style lang="scss" scoped>
.tutorial-container {
min-height: 100vh;
background-color: #f6f7f9;
padding-bottom: calc(40rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.tutorial-body {
padding: 24rpx 30rpx;
}
/* 顶部概览卡片 */
.overview-card {
display: flex;
align-items: center;
background: linear-gradient(135deg, #2979ff 0%, #609cff 100%);
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(41, 121, 255, 0.15);
.overview-icon-wrapper {
width: 90rpx;
height: 90rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.overview-content {
flex: 1;
.overview-title {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 8rpx;
display: block;
}
.overview-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
line-height: 36rpx;
display: block;
}
}
}
/* 通用分区标题 */
.section-container {
margin-bottom: 32rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
.header-line {
width: 40rpx;
height: 2rpx;
background-color: #dcdfe6;
}
.header-title {
font-size: 28rpx;
font-weight: 600;
color: #909399;
margin: 0 20rpx;
}
}
/* 核心功能网格 */
.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.feature-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 28rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
.feature-icon {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.feature-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.feature-desc {
font-size: 22rpx;
color: #909399;
text-align: center;
line-height: 32rpx;
}
}
}
/* 步骤流程 */
.steps-wrapper {
.step-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
.step-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.step-badge {
width: 44rpx;
height: 44rpx;
background: #2979ff;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
color: #ffffff;
margin-right: 16rpx;
flex-shrink: 0;
}
.step-titles {
display: flex;
flex-direction: column;
.step-main-title {
font-size: 30rpx;
font-weight: 600;
color: #303133;
}
.step-sub-title {
font-size: 22rpx;
color: #909399;
margin-top: 2rpx;
}
}
}
.step-details {
padding-left: 60rpx;
.detail-item {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
.dot {
width: 10rpx;
height: 10rpx;
background-color: #2979ff;
border-radius: 50%;
margin-top: 12rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.detail-text {
font-size: 25rpx;
color: #606266;
line-height: 36rpx;
flex: 1;
}
}
}
.step-tip {
display: flex;
align-items: center;
background-color: #fdf6ec;
border-radius: 8rpx;
padding: 12rpx 16rpx;
margin-top: 16rpx;
margin-left: 60rpx;
.tip-text {
font-size: 22rpx;
color: #e6a23c;
margin-left: 10rpx;
line-height: 32rpx;
flex: 1;
}
}
}
}
/* FAQ 折叠面板适配 */
.faq-wrapper {
background-color: #ffffff;
border-radius: 16rpx;
padding: 10rpx 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
.faq-title-box {
display: flex;
align-items: center;
.faq-question-text {
font-size: 26rpx;
font-weight: 600;
color: #303133;
}
}
.faq-answer-box {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
.faq-answer-text {
font-size: 24rpx;
color: #606266;
line-height: 36rpx;
flex: 1;
}
}
.faq-tag {
width: 36rpx;
height: 36rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
font-weight: 700;
margin-right: 16rpx;
flex-shrink: 0;
&.q-tag {
background-color: #2979ff;
color: #ffffff;
}
&.a-tag {
background-color: #e1f3d8;
color: #67c23a;
}
}
}
/* 底部提示 */
.footer-section {
padding: 30rpx 0 10rpx;
text-align: center;
.footer-text {
font-size: 22rpx;
color: #c0c4cc;
}
}
</style>
\ No newline at end of file
... ...
<template>
<view class="paike-wrap">
<!-- 顶部导航 -->
<view class="page-header" hover-class="none">
<view class="nav-bar" :style="{
paddingTop: menuButtonInfo.top + 'px',
height: menuButtonInfo.height + 'px'
}">
<view class="nav-left" @click="goBack">
<uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>
<view class="paike-container">
<!-- 1. 自定义顶部导航栏 (uview-plus) -->
<u-navbar
title="排课日历"
:auto-back="true"
placeholder
bg-color="#121212"
title-style="color: #FFFFFF; font-size: 34rpx; font-weight: 600;"
left-icon-color="#FFFFFF"
/>
<!-- 2. 星期吸顶头部 -->
<view class="week-header-sticky">
<view class="week-grid">
<view v-for="item in WEEK_TEXTS" :key="item" class="week-cell">
{{ item }}
</view>
<view class="nav-title">计划排课</view>
<view class="nav-right"></view>
</view>
</view>
<!-- 星期头部:一二三四五六日 -->
<view class="week-head">
<view v-for="item in weekText" :key="item" class="week-cell">{{ item }}</view>
<!-- 3. 加载中状态 -->
<view v-if="loading" class="state-box">
<u-loading-icon
mode="semicircle"
color="#E9EE50"
text="正在加载排课数据..."
text-color="#999999"
vertical
/>
</view>
<!-- 日历主体容器 -->
<view class="calendar-box">
<!-- 月份标题行 -->
<view v-for="monthItem in monthList" :key="monthItem.month" class="month-wrap">
<view class="month-title">{{ monthItem.month }}月</view>
<!-- 当月日期网格 Grid7列 -->
<!-- 4. 日历主体列表 -->
<view v-else-if="monthList.length > 0" class="calendar-content">
<view
v-for="monthItem in monthList"
:key="`${monthItem.year}-${monthItem.month}`"
class="month-card"
>
<!-- 月份标题 -->
<view class="month-header">
<text class="month-title">{{ monthItem.year }}年 {{ monthItem.month }}月</text>
</view>
<!-- 日期 7 列网格 -->
<view class="date-grid">
<!-- 前置空白占位格子(当月开头非周一填充空白) -->
<view v-for="empty in monthItem.emptyCount" :key="'empty' + empty" class="date-cell empty-cell"></view>
<!-- 有效日期格子 -->
<view v-for="day in monthItem.dayList" :key="day.day" class="date-cell"
@click="openTemplateDatil(day.DailyTemplateId, day.year, day.month, day.day)"
:class="{ hasClass: day.trainName }">
<!-- 日期数字 -->
<text class="day-num">{{ day.day }}</text>
<!-- 黄色排课标签 -->
<view class="train-label-box">
<view v-if="day.trainName" class="train-label">{{ day.trainName }}</view>
<view v-else class="empty-label"></view>
<!-- 开头空白填充格 -->
<view
v-for="emptyIdx in monthItem.emptyCount"
:key="`empty-${emptyIdx}`"
class="date-cell cell-empty"
/>
<!-- 有效日期格 -->
<view
v-for="day in monthItem.dayList"
:key="`${day.year}-${day.month}-${day.day}`"
class="date-cell"
:class="{ 'has-schedule': day.trainName }"
@tap="handleDateClick(day)"
>
<!-- 日期数 -->
<text class="day-number">{{ day.day }}</text>
<!-- 排课标签/未排课点位 -->
<view class="schedule-box">
<view v-if="day.trainName" class="schedule-label">
{{ day.trainName }}
</view>
<view v-else class="schedule-placeholder">
<view class="dot" />
</view>
</view>
</view>
</view>
</view>
<!-- 底部提示 -->
<view class="bottom-tip">
<text>最多仅可设置到未来 {{ MONTHS_AHEAD }} 个月哦~</text>
</view>
</view>
<!-- 5. 空数据状态 -->
<view v-else class="state-box">
<u-empty mode="data" text="暂无排课计划数据" icon-size="160rpx" />
</view>
<!-- 底部提示文案(适配底部安全区) -->
<view class="bottom-tip">最多仅可设置到未来2个月哦~</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import QueryPlanApi from '@/sheep/api/plan/queryplan';
import { onLoad } from '@dcloudio/uni-app';
// 顶部星期文案
const weekText = ['一', '二', '三', '四', '五', '六', '日']
import { ref, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import QueryPlanApi from '@/sheep/api/plan/queryplan'
// 排课接口信息
const resData = ref({})
// ==================== 常量定义 ====================
const WEEK_TEXTS = ['一', '二', '三', '四', '五', '六', '日']
const MONTHS_AHEAD = 2 // 最多展示未来月份数(当前月 + 2个月)
// 最终渲染日历数据
// ==================== 响应式状态 ====================
const planId = ref(0)
const loading = ref(true)
const monthList = ref([])
// ==================== 生命周期 ====================
onLoad((options) => {
planId.value = Number(options?.planid) || 0
})
const planId = ref(0)
onMounted(() => {
fetchPlanCalendar()
uni.$on('calendarDataRefresh', fetchPlanCalendar)
})
onLoad((options) => {
// 接收从详情页传过来的 planid
planId.value = Number(options.planid) || 0
console.log('收到计划ID:', planId.value, typeof planId.value)
onUnmounted(() => {
uni.$off('calendarDataRefresh', fetchPlanCalendar)
})
// 格式化日期、后端数据转日历
const formatCalendar = () => {
// ==================== 工具函数(数据解耦) ====================
console.log('进入格式化,resData全部数据:', resData.value)
// 没有data直接退出
if (!resData.value?.data || !Array.isArray(resData.value.data)) {
console.log('data不存在,终止格式化')
monthList.value = []
return
/**
* 根据基础日期与月份偏移量,计算目标的年、月
*/
const getTargetYearMonth = (baseDate, offsetMonth) => {
const d = new Date(baseDate.getFullYear(), baseDate.getMonth() + offsetMonth, 1)
return {
year: d.getFullYear(),
month: d.getMonth() + 1,
}
const now = new Date()
const currentYear = now.getFullYear()
const currentMonth = now.getMonth() + 1
const today = now.getDate()
const calcMonthArr = [currentMonth, currentMonth + 1, currentMonth + 2]
}
const groupObj = {}
/**
* 计算某月某日是周几,并转换为“以周一为每周第一天”的前置空白格数量
*/
const calcEmptySlots = (year, month, day = 1) => {
const dayOfWeek = new Date(year, month - 1, day).getDay()
const isoWeekDay = dayOfWeek === 0 ? 7 : dayOfWeek // 周日转为7
return isoWeekDay - 1
}
resData.value.data.forEach(item => {
// trainingDate 是 [年, 月, 日] 数组,直接解构
// const [y, m, d] = item.trainingDate
// const name = item.templates?.[0]?.templateName || ''
/**
* 将后端排课接口数组构建成映射 Map,Key: `${year}-${month}`, Value: { [day]: { trainName, DailyTemplateId } }
*/
const buildTrainMap = (rawList = []) => {
const map = {}
if (!Array.isArray(rawList)) return map
// const key = `${y}-${m}`
// if (!groupObj[key]) groupObj[key] = {}
// groupObj[key][d] = name
rawList.forEach((item) => {
if (!item?.trainingDate || !Array.isArray(item.trainingDate)) return
const [y, m, d] = item.trainingDate
const template = item.templates?.[0]
const name = template?.templateName || ''
const DailyTemplateId = template?.id || null
const key = `${y}-${m}`
if (!groupObj[key]) groupObj[key] = {}
groupObj[key][d] = {
trainName: name,
DailyTemplateId: DailyTemplateId
if (!map[key]) map[key] = {}
map[key][d] = {
trainName: template?.templateName || '',
DailyTemplateId: template?.id || null,
}
})
return map
}
/**
* 生成多月份日历完整渲染结构
*/
const generateCalendarList = (apiData) => {
const now = new Date()
const today = now.getDate()
const trainMap = buildTrainMap(apiData)
const result = []
calcMonthArr.forEach((month, idx) => {
const year = currentYear + Math.floor((month - 1) / 12)
const realMonth = month > 12 ? month - 12 : month
const totalDay = new Date(year, realMonth, 0).getDate()
let firstWeek = new Date(`${year}-${realMonth}-01`).getDay()
firstWeek = firstWeek === 0 ? 7 : firstWeek
let emptyCount = firstWeek - 1
const dayList = []
const startDay = idx === 0 ? today : 1
for (let d = startDay; d <= totalDay; d++) {
// const key = `${year}-${realMonth}`
// const trainName = groupObj[key]?.[d] || ''
// dayList.push({ day: d, trainName })
const key = `${year}-${realMonth}`
const trainInfo = groupObj[key]?.[d] || {}
for (let i = 0; i <= MONTHS_AHEAD; i++) {
const { year, month } = getTargetYearMonth(now, i)
const isCurrentMonth = i === 0
const startDay = isCurrentMonth ? today : 1
const totalDaysInMonth = new Date(year, month, 0).getDate()
const mapKey = `${year}-${month}`
const dayList = []
for (let d = startDay; d <= totalDaysInMonth; d++) {
const scheduleInfo = trainMap[mapKey]?.[d] || {}
dayList.push({
year: year, // 年
month: realMonth, // 月
year,
month,
day: d,
trainName: trainInfo.trainName || '',
// templateId: trainInfo.templateId || null
DailyTemplateId: trainInfo.DailyTemplateId || null
trainName: scheduleInfo.trainName || '',
DailyTemplateId: scheduleInfo.DailyTemplateId || null,
})
}
if (idx === 0) {
const firstDayWeek = new Date(`${year}-${realMonth}-${startDay}`).getDay()
const currentWeek = firstDayWeek === 0 ? 7 : firstDayWeek
emptyCount = currentWeek - 1
}
result.push({
month: realMonth,
emptyCount,
dayList
year,
month,
emptyCount: calcEmptySlots(year, month, startDay),
dayList,
})
})
}
monthList.value = result
return result
}
// 返回上一页
const goBack = () => uni.navigateBack()
// 胶囊导航信息
const menuButtonInfo = ref({
top: 44,
height: 32
});
// ==================== 接口交互 & 事件处理 ====================
/** 加载排课数据 */
const fetchPlanCalendar = async () => {
if (!planId.value) {
loading.value = false
return
}
// 获取排课日历数据
const loadPlanCalendar = async () => {
if (!planId.value) return
loading.value = true
try {
const apiRes = await QueryPlanApi.getPlanArrangeCalendar(planId.value)
console.log('接口返回的计划日历数据:', apiRes.value);
resData.value = apiRes
// 渲染日历
formatCalendar()
const res = await QueryPlanApi.getPlanArrangeCalendar(planId.value)
// 根据后端统一响应结构兜底处理
const data = res?.data || res
monthList.value = generateCalendarList(Array.isArray(data) ? data : [])
} catch (err) {
console.error('日历接口报错', err)
console.error('[PlanCalendar] 获取排课日历失败:', err)
uni.showToast({ title: '加载排课失败', icon: 'none' })
} finally {
loading.value = false
}
}
//查看模板详情
const openTemplateDatil = (DailyTemplateId, year, month, day) => {
console.log('每日模板ID=', DailyTemplateId);
console.log('完整日期=', year, month, day);
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
/** 日期卡片点击事件 */
const handleDateClick = (dayItem) => {
const { DailyTemplateId, year, month, day } = dayItem
if (!DailyTemplateId) return
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${DailyTemplateId}&date=${dateStr}&isOffice=false&isMyPlan=true&isDailytemplateId=true`,
});
};
onMounted(async () => {
// #ifdef MP-WEIXIN
try {
const rect = uni.getMenuButtonBoundingClientRect();
if (rect) {
menuButtonInfo.value = {
top: rect.top,
height: rect.height
};
}
} catch (e) { }
// #endif
// #ifndef MP-WEIXIN
try {
const systemInfo = uni.getSystemInfoSync();
const statusBarHeight = systemInfo.statusBarHeight || 20;
menuButtonInfo.value = {
top: statusBarHeight,
height: 44
};
} catch (e) { }
// #endif
// 加载排课信息
loadPlanCalendar()
// formatCalendar()
});
const formattedMonth = String(month).padStart(2, '0')
const formattedDay = String(day).padStart(2, '0')
const dateStr = `${year}-${formattedMonth}-${formattedDay}`
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${DailyTemplateId}&date=${dateStr}&isOffice=false&isMyPlan=true&isDailytemplateId=true`,
})
}
</script>
<style scoped lang="scss">
$bg-color: #121212;
$text-white: #fff;
$text-gray: #2f2f2f;
$train-yellow: #e9ee50;
$cell-h: 110rpx;
.paike-wrap {
background: $bg-color;
// ==================== 主题色系与变量 ====================
$bg-main: #121212;
$bg-card: #1e1e1e;
$bg-cell: #262626;
$text-primary: #ffffff;
$text-secondary: #8c8c8c;
$theme-yellow: #e9ee50;
$theme-yellow-light: rgba(233, 238, 80, 0.15);
$theme-yellow-border: rgba(233, 238, 80, 0.35);
$cell-height: 116rpx;
// ==================== 容器根样式 ====================
.paike-container {
min-height: 100vh;
padding-bottom: env(safe-area-inset-bottom);
overflow: hidden;
background-color: $bg-main;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.page-header {
width: 100%;
flex-shrink: 0;
background-color: $bg-color;
}
// ==================== 星期头吸顶 ====================
.week-header-sticky {
position: sticky;
/* #ifdef H5 */
top: 44px;
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
z-index: 10;
background-color: $bg-main;
padding: 20rpx 16rpx 12rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
background-color: $bg-color;
padding-left: 16rpx;
padding-right: 16rpx;
box-sizing: content-box;
.week-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
.week-cell {
text-align: center;
font-size: 24rpx;
font-weight: 500;
color: $text-secondary;
}
}
}
.nav-left {
width: 60rpx;
// ==================== 状态容器 (Loading/Empty) ====================
.state-box {
display: flex;
align-items: center;
flex-shrink: 0;
justify-content: center;
min-height: 500rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: bold;
color: $text-white;
// ==================== 日历主体 ====================
.calendar-content {
padding: 20rpx 16rpx 0;
}
.nav-right {
width: 60rpx;
flex-shrink: 0;
}
.month-card {
margin-bottom: 32rpx;
background: $bg-card;
border-radius: 24rpx;
padding: 20rpx 12rpx 16rpx;
border: 1rpx solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
.week-head {
margin-top: 10rpx;
display: grid;
grid-template-columns: repeat(7, 1fr);
margin: 20rpx 0;
.week-cell {
.month-header {
text-align: center;
font-size: 25rpx;
color: $text-white;
}
}
padding-bottom: 20rpx;
.month-wrap {
margin-bottom: 30rpx;
background-color: #121212;
.month-title {
font-size: 25rpx;
color: $text-white;
padding: 15rpx 10rpx;
text-align: center;
.month-title {
font-size: 30rpx;
font-weight: 600;
color: $text-primary;
letter-spacing: 1rpx;
}
}
.date-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1rpx;
gap: 8rpx;
}
.date-cell {
height: $cell-h;
background: $bg-color;
border-radius: 8rpx;
padding: 8rpx;
height: $cell-height;
background-color: $bg-cell;
border-radius: 12rpx;
padding: 8rpx 6rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
transition: all 0.2s ease;
.day-num {
font-size: 25rpx;
color: $text-white;
text-align: center;
display: block;
width: 100%;
/* 点击效果 */
&:active {
transform: scale(0.95);
opacity: 0.8;
}
.train-label-box {
min-height: 56rpx;
width: 100%;
/* 空白填补格 */
&.cell-empty {
background-color: transparent !important;
pointer-events: none;
}
.train-label {
background: $train-yellow;
color: #000;
font-size: 22rpx;
padding: 4rpx 3rpx;
border-radius: 2rpx;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
height: 56rpx;
text-align: center;
/* 有排课状态 */
&.has-schedule {
background-color: $theme-yellow-light;
border: 1rpx solid $theme-yellow-border;
.day-number {
color: $theme-yellow;
font-weight: 700;
}
}
.empty-label {
width: 100%;
min-height: 56rpx;
background: $text-gray;
border-radius: 2rpx;
.day-number {
font-size: 26rpx;
color: $text-primary;
font-weight: 500;
line-height: 1;
}
}
.empty-cell {
background: transparent !important;
/* 排课内容展示区 */
.schedule-box {
width: 100%;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
.schedule-label {
width: 100%;
height: 100%;
background-color: $theme-yellow;
color: #000000;
font-size: 20rpx;
font-weight: 600;
border-radius: 6rpx;
padding: 4rpx;
box-sizing: border-box;
text-align: center;
line-height: 1.25;
/* 超出两行省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
}
/* 无排课时的精致小点,替代原本突兀的大灰块 */
.schedule-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.15);
}
}
}
}
}
// ==================== 底部提示 ====================
.bottom-tip {
text-align: center;
font-size: 30rpx;
color: $text-white;
margin-top: 60rpx;
/* #ifdef H5 */
margin-bottom: 20px;
/* #endif */
padding: 24rpx 0 10rpx;
text {
font-size: 24rpx;
color: $text-secondary;
}
}
</style>
\ No newline at end of file
... ...