Authored by Bad

初始化

Too many changes to show.

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

  1 +# 版本号
  2 +SHOPRO_VERSION=v2.4.1
  3 +
  4 +# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
  5 +# SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
  6 +SHOPRO_BASE_URL=http://mall.hcxtec.com
  7 +
  8 +# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
  9 +SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
  10 +# SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
  11 +SHOPRO_DEV_BASE_URL=https://fitness.hcxtec.com
  12 +# SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
  13 +### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
  14 +### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
  15 +
  16 +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
  17 +SHOPRO_UPLOAD_TYPE=server
  18 +
  19 +# 后端接口前缀(一般不建议调整)
  20 +SHOPRO_API_PATH=/app-api
  21 +
  22 +# SHOPRO_API_PATH=/api
  23 +
  24 +# 后端 websocket 接口前缀
  25 +SHOPRO_WEBSOCKET_PATH=/infra/ws
  26 +
  27 +# 开发环境运行端口
  28 +SHOPRO_DEV_PORT=3000
  29 +
  30 +# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
  31 +SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
  32 +### SHOPRO_STATIC_URL = https://file.sheepjs.com
  33 +
  34 +# 前端 H5 访问域名
  35 +SHOPRO_H5_URL=http://127.0.0.1:3000
  36 +
  37 +# 是否开启直播 1 开启直播 | 0 关闭直播
  38 +SHOPRO_MPLIVE_ON=0
  39 +
  40 +# 租户ID 默认 1
  41 +SHOPRO_TENANT_ID=1
  1 +unpackage/*
  2 +node_modules/*
  3 +.idea/*
  4 +deploy.sh
  5 +.hbuilderx/
  6 +.vscode/
  7 +**/.DS_Store
  8 +yarn.lock
  9 +package-lock.json
  10 +*.keystore
  11 +pnpm-lock.yaml
  12 +.history/*
  1 +/unpackage/*
  2 +/node_modules/**
  3 +/uni_modules/**
  4 +/public/*
  5 +**/*.svg
  6 +**/*.sh
  1 +{
  2 + "printWidth": 100,
  3 + "semi": true,
  4 + "vueIndentScriptAndStyle": true,
  5 + "singleQuote": true,
  6 + "trailingComma": "all",
  7 + "proseWrap": "never",
  8 + "htmlWhitespaceSensitivity": "strict",
  9 + "endOfLine": "auto"
  10 +}
  1 +<script setup>
  2 + import { onLaunch, onShow, onError, onLoad } from '@dcloudio/uni-app';
  3 + import { ShoproInit } from './sheep';
  4 +
  5 + onLaunch(() => {
  6 + // 隐藏原生导航栏 使用自定义底部导航
  7 + // uni.hideTabBar({
  8 + // fail: () => {},
  9 + // });
  10 + // 加载Shopro底层依赖
  11 + ShoproInit();
  12 + });
  13 +
  14 + onShow(async (options) => {
  15 + // #ifdef APP-PLUS
  16 + // 获取urlSchemes参数
  17 + const args = plus.runtime.arguments;
  18 + if (args) {
  19 + }
  20 + // 获取剪贴板
  21 + uni.getClipboardData({
  22 + success: (res) => {
  23 +
  24 + },
  25 + });
  26 + // #endif
  27 + });
  28 +</script>
  29 +
  30 +<style lang="scss">
  31 + @import 'uview-plus/index.scss';
  32 + @import '@/sheep/scss/index.scss';
  33 +</style>
  1 +MIT License
  2 +
  3 +Copyright (c) 2022 lidongtony
  4 +
  5 +Permission is hereby granted, free of charge, to any person obtaining a copy
  6 +of this software and associated documentation files (the "Software"), to deal
  7 +in the Software without restriction, including without limitation the rights
  8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 +copies of the Software, and to permit persons to whom the Software is
  10 +furnished to do so, subject to the following conditions:
  11 +
  12 +The above copyright notice and this permission notice shall be included in all
  13 +copies or substantial portions of the Software.
  14 +
  15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 +SOFTWARE.
  1 +**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!**
  2 +
  3 +**「我喜欢写代码,乐此不疲」**
  4 +**「我喜欢做开源,以此为乐」**
  5 +
  6 +我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
  7 +
  8 +如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
  9 +
  10 +## 🐶 新手必读
  11 +
  12 +* 演示地址:<https://doc.iocoder.cn/mall-preview/>
  13 +* 启动文档:<https://doc.iocoder.cn/quick-start/>
  14 +* 视频教程:<https://doc.iocoder.cn/video/>
  15 +
  16 +## 🐯 商城简介
  17 +
  18 +**芋道商城**,基于 [芋道开发平台](https://github.com/YunaiV/ruoyi-vue-pro) 构建,以开发者为中心,打造中国第一流的 Java 开源商城系统,全部开源,个人与企业可 100% 免费使用。
  19 +
  20 +> 有任何问题,或者想要的功能,可以在 Issues 中提给艿艿。
  21 +>
  22 +> 😜 给项目点点 Star 吧,这对我们真的很重要!
  23 +
  24 +![功能图](/.image/common/mall-feature.png)
  25 +
  26 +* 基于 uni-app + Vue3 开发,支持微信小程序、微信公众号、H5 移动端,未来会支持支付宝小程序、抖音小程序等
  27 +* 支持 SaaS 多租户,可满足商品、订单、支付、会员、优惠券、秒杀、拼团、砍价、分销、积分等多种经营需求
  28 +
  29 +## 🔥 后端架构
  30 +
  31 +支持 Spring Boot、Spring Cloud 两种架构:
  32 +
  33 +① Spring Boot 单体架构:<https://doc.iocoder.cn>
  34 +
  35 +![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
  36 +
  37 +② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
  38 +
  39 +![架构图](/.image/common/yudao-cloud-architecture.png)
  40 +
  41 +## 🐱 移动端预览
  42 +
  43 +![移动端预览](/.image/common/mall-preview.png)
  44 +
  45 +## 🐶 管理端预览
  46 +
  47 +![店铺装修](/.image/mall/店铺装修.png)
  48 +
  49 +![会员详情](/.image/mall/会员详情.png)
  50 +
  51 +![商品详情](/.image/mall/商品详情.png)
  52 +
  53 +![订单详情](/.image/mall/订单详情.png)
  54 +
  55 +![营销中心](/.image/mall/营销中心.png)
  56 +
  1 +{
  2 + "prompt" : "template"
  3 +}
  1 +<template>
  2 + <view class="ad-modal">
  3 + <u-popup
  4 + :show="show"
  5 + mode="center"
  6 + @close="close"
  7 + bgColor="transparent"
  8 + :safeAreaInsetBottom="false"
  9 + >
  10 + <view class="ad-container">
  11 + <view class="swiper-wrapper">
  12 + <u-swiper
  13 + :list="list"
  14 + keyName="image"
  15 + height="700rpx"
  16 + :autoplay="false"
  17 + circular
  18 + @click="handleAdClick"
  19 + radius="16rpx"
  20 + indicator
  21 + bgColor="transparent"
  22 + indicatorMode="dot"
  23 + ></u-swiper>
  24 + </view>
  25 +
  26 + <view class="close-section" @click="close">
  27 + <u-icon name="close-circle" color="#ffffff" size="34"></u-icon>
  28 + </view>
  29 + </view>
  30 + </u-popup>
  31 + </view>
  32 +</template>
  33 +
  34 +<script setup>
  35 + const props = defineProps({
  36 + show: {
  37 + type: Boolean,
  38 + default: false,
  39 + },
  40 + list: {
  41 + type: Array,
  42 + default: () => [],
  43 + },
  44 + });
  45 +
  46 + const emit = defineEmits(['updateShow', 'clickAd', 'close']);
  47 +
  48 + // 关闭弹窗
  49 + const close = () => {
  50 + emit('close', false);
  51 + };
  52 +
  53 + // 点击广告图触发
  54 + const handleAdClick = (index) => {
  55 + emit('clickAd', props.list[index]);
  56 + };
  57 +</script>
  58 +
  59 +<style lang="scss" scoped>
  60 + .ad-container {
  61 + width: 580rpx; // 弹窗宽度
  62 + display: flex;
  63 + flex-direction: column;
  64 + align-items: center;
  65 +
  66 + .swiper-wrapper {
  67 + width: 100%;
  68 + overflow: hidden;
  69 + }
  70 +
  71 + .close-section {
  72 + margin-top: 40rpx;
  73 + display: flex;
  74 + justify-content: center;
  75 + align-items: center;
  76 + cursor: pointer;
  77 +
  78 + /* 增加点击区域面积 */
  79 + padding: 20rpx;
  80 + }
  81 + }
  82 +</style>
  1 +<template>
  2 + <view class="tabbar">
  3 + <up-tabbar
  4 + :value="activePath"
  5 + @change="handleTabChange"
  6 + :placeholder="true"
  7 + activeColor="#000"
  8 + :fixed="true"
  9 + >
  10 + <up-tabbar-item
  11 + v-for="(item, index) in tabList"
  12 + :key="index"
  13 + :text="item.text"
  14 + :name="item.path"
  15 + >
  16 + <template #active-icon>
  17 + <image :class="['icon', item.special ? 'mid-icon' : '']" :src="item.selectedIcon"></image>
  18 + </template>
  19 + <template #inactive-icon>
  20 + <image :class="['icon', item.special ? 'mid-icon' : '']" :src="item.icon"></image>
  21 + </template>
  22 + </up-tabbar-item>
  23 + </up-tabbar>
  24 + </view>
  25 +</template>
  26 +
  27 +<script setup>
  28 + import { ref } from 'vue';
  29 + import { onShow } from '@dcloudio/uni-app';
  30 +
  31 + const activePath = ref('pages/xunji/xunji');
  32 +
  33 + // 配置化 Tabbar
  34 + const tabList = [
  35 + {
  36 + text: '训记',
  37 + path: 'pages/xunji/xunji',
  38 + icon: '/static/tabbar/xunji.png',
  39 + selectedIcon: '/static/tabbar/xunji-sel.png',
  40 + special: false,
  41 + },
  42 + {
  43 + text: '我的',
  44 + path: 'pages/user/user',
  45 + icon: '/static/tabbar/wode.png',
  46 + selectedIcon: '/static/tabbar/wode-sel.png',
  47 + special: false,
  48 + },
  49 + ];
  50 +
  51 + const handleTabChange = (name) => {
  52 + activePath.value = name;
  53 + uni.switchTab({ url: '/' + name });
  54 + };
  55 +
  56 + onShow(() => {
  57 + const pages = getCurrentPages();
  58 + const currPage = pages[pages.length - 1];
  59 + if (currPage && !currPage.route.includes('entry')) {
  60 + activePath.value = currPage.route;
  61 + }
  62 + });
  63 +</script>
  64 +
  65 +<style lang="scss" scoped>
  66 + .icon {
  67 + width: 48rpx;
  68 + height: 48rpx;
  69 + transition: all 0.2s;
  70 + }
  71 +</style>
  1 +<!DOCTYPE html>
  2 +<html lang="en">
  3 + <head>
  4 + <meta charset="UTF-8" />
  5 + <meta
  6 + name="viewport"
  7 + content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
  8 + />
  9 + <title></title>
  10 + <!--preload-links-->
  11 + <!--app-context-->
  12 + </head>
  13 + <body>
  14 + <div id="app"><!--app-html--></div>
  15 + <script type="module" src="/main.js"></script>
  16 + </body>
  17 +</html>
  1 +{
  2 + "compilerOptions": {
  3 + "jsx": "preserve",
  4 + "baseUrl": ".",
  5 + "paths": {
  6 + "@/*": ["./*"]
  7 + }
  8 + }
  9 +}
  1 +import App from './App';
  2 +import { createSSRApp } from 'vue';
  3 +import { setupPinia } from './sheep/store';
  4 +import uviewPlus from 'uview-plus';
  5 +import Tabbar from '@/components/tabbar.vue';
  6 +export function createApp() {
  7 + const app = createSSRApp(App);
  8 +
  9 + setupPinia(app);
  10 + app.use(uviewPlus, () => {
  11 + return {
  12 + options: {
  13 + // 修改config对象的属性
  14 + config: {
  15 + // 只加载一次字体图标
  16 + loadFontOnce: true,
  17 + unit:'rpx'
  18 + },
  19 + },
  20 + };
  21 + });
  22 + app.component('Tabbar', Tabbar);
  23 +
  24 + return {
  25 + app,
  26 + };
  27 +}
  1 +{
  2 + "name": "鸿星健身",
  3 + "appid": "__UNI__0A1E345",
  4 + "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
  5 + "versionName": "2025.10",
  6 + "versionCode": "183",
  7 + "transformPx": false,
  8 + "app-plus": {
  9 + "usingComponents": true,
  10 + "nvueCompiler": "uni-app",
  11 + "nvueStyleCompiler": "uni-app",
  12 + "compilerVersion": 3,
  13 + "nvueLaunchMode": "fast",
  14 + "splashscreen": {
  15 + "alwaysShowBeforeRender": true,
  16 + "waiting": true,
  17 + "autoclose": true,
  18 + "delay": 0
  19 + },
  20 + "safearea": {
  21 + "bottom": {
  22 + "offset": "none"
  23 + }
  24 + },
  25 + "modules": {
  26 + "Payment": {},
  27 + "Share": {},
  28 + "VideoPlayer": {},
  29 + "OAuth": {}
  30 + },
  31 + "distribute": {
  32 + "android": {
  33 + "permissions": [
  34 + "<uses-feature android:name=\"android.hardware.camera\"/>",
  35 + "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
  36 + "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
  37 + "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
  38 + "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
  39 + "<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
  40 + "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
  41 + "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
  42 + "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
  43 + "<uses-permission android:name=\"android.permission.CAMERA\"/>",
  44 + "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
  45 + "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
  46 + "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
  47 + "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
  48 + "<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
  49 + "<uses-permission android:name=\"android.permission.INTERNET\"/>",
  50 + "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
  51 + "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
  52 + "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
  53 + "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
  54 + "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
  55 + "<uses-permission android:name=\"android.permission.READ_SMS\"/>",
  56 + "<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
  57 + "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
  58 + "<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
  59 + "<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
  60 + "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
  61 + "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
  62 + "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
  63 + "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
  64 + "<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
  65 + "<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
  66 + ],
  67 + "minSdkVersion": 21,
  68 + "schemes": "shopro"
  69 + },
  70 + "ios": {
  71 + "urlschemewhitelist": [
  72 + "baidumap",
  73 + "iosamap"
  74 + ],
  75 + "dSYMs": false,
  76 + "privacyDescription": {
  77 + "NSPhotoLibraryUsageDescription": "需要同意访问您的相册选取图片才能完善该条目",
  78 + "NSPhotoLibraryAddUsageDescription": "需要同意访问您的相册才能保存该图片",
  79 + "NSCameraUsageDescription": "需要同意访问您的摄像头拍摄照片才能完善该条目",
  80 + "NSUserTrackingUsageDescription": "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
  81 + },
  82 + "urltypes": "shopro",
  83 + "capabilities": {
  84 + "entitlements": {
  85 + "com.apple.developer.associated-domains": [
  86 + "applinks:shopro.sheepjs.com"
  87 + ]
  88 + }
  89 + },
  90 + "idfa": true
  91 + },
  92 + "sdkConfigs": {
  93 + "speech": {},
  94 + "ad": {},
  95 + "oauth": {
  96 + "apple": {},
  97 + "weixin": {
  98 + "appid": "wxae7a0c156da9383b",
  99 + "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/",
  100 + "mergeVirtualHostAttributes": true
  101 + }
  102 + },
  103 + "payment": {
  104 + "weixin": {
  105 + "__platform__": [
  106 + "ios",
  107 + "android"
  108 + ],
  109 + "appid": "wxae7a0c156da9383b",
  110 + "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
  111 + },
  112 + "alipay": {
  113 + "__platform__": [
  114 + "ios",
  115 + "android"
  116 + ]
  117 + }
  118 + },
  119 + "share": {
  120 + "weixin": {
  121 + "appid": "wxae7a0c156da9383b",
  122 + "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
  123 + }
  124 + }
  125 + },
  126 + "orientation": [
  127 + "portrait-primary"
  128 + ],
  129 + "splashscreen": {
  130 + "androidStyle": "common",
  131 + "iosStyle": "common",
  132 + "useOriginalMsgbox": true
  133 + },
  134 + "icons": {
  135 + "android": {
  136 + "hdpi": "unpackage/res/icons/72x72.png",
  137 + "xhdpi": "unpackage/res/icons/96x96.png",
  138 + "xxhdpi": "unpackage/res/icons/144x144.png",
  139 + "xxxhdpi": "unpackage/res/icons/192x192.png"
  140 + },
  141 + "ios": {
  142 + "appstore": "unpackage/res/icons/1024x1024.png",
  143 + "ipad": {
  144 + "app": "unpackage/res/icons/76x76.png",
  145 + "app@2x": "unpackage/res/icons/152x152.png",
  146 + "notification": "unpackage/res/icons/20x20.png",
  147 + "notification@2x": "unpackage/res/icons/40x40.png",
  148 + "proapp@2x": "unpackage/res/icons/167x167.png",
  149 + "settings": "unpackage/res/icons/29x29.png",
  150 + "settings@2x": "unpackage/res/icons/58x58.png",
  151 + "spotlight": "unpackage/res/icons/40x40.png",
  152 + "spotlight@2x": "unpackage/res/icons/80x80.png"
  153 + },
  154 + "iphone": {
  155 + "app@2x": "unpackage/res/icons/120x120.png",
  156 + "app@3x": "unpackage/res/icons/180x180.png",
  157 + "notification@2x": "unpackage/res/icons/40x40.png",
  158 + "notification@3x": "unpackage/res/icons/60x60.png",
  159 + "settings@2x": "unpackage/res/icons/58x58.png",
  160 + "settings@3x": "unpackage/res/icons/87x87.png",
  161 + "spotlight@2x": "unpackage/res/icons/80x80.png",
  162 + "spotlight@3x": "unpackage/res/icons/120x120.png"
  163 + }
  164 + }
  165 + }
  166 + }
  167 + },
  168 + "quickapp": {},
  169 + "quickapp-native": {
  170 + "icon": "/static/logo.png",
  171 + "package": "com.example.demo",
  172 + "features": [
  173 + {
  174 + "name": "system.clipboard"
  175 + }
  176 + ]
  177 + },
  178 + "quickapp-webview": {
  179 + "icon": "/static/logo.png",
  180 + "package": "com.example.demo",
  181 + "minPlatformVersion": 1070,
  182 + "versionName": "1.0.0",
  183 + "versionCode": 100
  184 + },
  185 + "mp-weixin": {
  186 + "appid": "wxb827c923ce0aad4b",
  187 + "setting": {
  188 + "urlCheck": true,
  189 + "minified": true,
  190 + "postcss": true
  191 + },
  192 + "optimization": {
  193 + "subPackages": true
  194 + },
  195 + "plugins": {},
  196 + "lazyCodeLoading": "requiredComponents",
  197 + "usingComponents": {},
  198 + "permission": {
  199 + "scope.userLocation": {
  200 + "desc": "你的位置信息将用于展示附近的服务"
  201 + }
  202 + },
  203 + "requiredPrivateInfos": [
  204 + "chooseAddress",
  205 + "getLocation"
  206 + ],
  207 + "mergeVirtualHostAttributes": true
  208 + },
  209 + "mp-alipay": {
  210 + "usingComponents": true
  211 + },
  212 + "mp-baidu": {
  213 + "usingComponents": true
  214 + },
  215 + "mp-toutiao": {
  216 + "usingComponents": true
  217 + },
  218 + "mp-jd": {
  219 + "usingComponents": true
  220 + },
  221 + "h5": {
  222 + "template": "index.html",
  223 + "router": {
  224 + "mode": "history",
  225 + "base": "/"
  226 + },
  227 + "sdkConfigs": {
  228 + "maps": {}
  229 + },
  230 + "async": {
  231 + "timeout": 20000
  232 + },
  233 + "title": "芋道商城",
  234 + "optimization": {
  235 + "treeShaking": {
  236 + "enable": true
  237 + }
  238 + }
  239 + },
  240 + "vueVersion": "3",
  241 + "_spaceID": "192b4892-5452-4e1d-9f09-eee1ece40639",
  242 + "locale": "zh-Hans",
  243 + "fallbackLocale": "zh-Hans"
  244 +}
  1 +{
  2 + "id": "shopro",
  3 + "name": "shopro",
  4 + "displayName": "芋道商城",
  5 + "version": "2025.10.0",
  6 + "description": "芋道商城,一套代码,同时发行到iOS、Android、H5、微信小程序多个平台,请使用手机扫码快速体验强大功能",
  7 + "scripts": {
  8 + "prettier": "prettier --write \"{pages,sheep}/**/*.{js,json,tsx,css,less,scss,vue,html,md}\""
  9 + },
  10 + "repository": "https://github.com/sheepjs/shop.git",
  11 + "keywords": [
  12 + "商城",
  13 + "B2C",
  14 + "商城模板"
  15 + ],
  16 + "author": "",
  17 + "license": "MIT",
  18 + "bugs": {
  19 + "url": "https://github.com/sheepjs/shop/issues"
  20 + },
  21 + "homepage": "https://github.com/dcloudio/hello-uniapp#readme",
  22 + "dcloudext": {
  23 + "category": [
  24 + "前端页面模板",
  25 + "uni-app前端项目模板"
  26 + ],
  27 + "sale": {
  28 + "regular": {
  29 + "price": "0.00"
  30 + },
  31 + "sourcecode": {
  32 + "price": "0.00"
  33 + }
  34 + },
  35 + "contact": {
  36 + "qq": ""
  37 + },
  38 + "declaration": {
  39 + "ads": "无",
  40 + "data": "无",
  41 + "permissions": "无"
  42 + },
  43 + "npmurl": ""
  44 + },
  45 + "uni_modules": {
  46 + "dependencies": [],
  47 + "encrypt": [],
  48 + "platforms": {
  49 + "cloud": {
  50 + "tcb": "u",
  51 + "aliyun": "u"
  52 + },
  53 + "client": {
  54 + "App": {
  55 + "app-vue": "y",
  56 + "app-nvue": "u"
  57 + },
  58 + "H5-mobile": {
  59 + "Safari": "y",
  60 + "Android Browser": "y",
  61 + "微信浏览器(Android)": "y",
  62 + "QQ浏览器(Android)": "y"
  63 + },
  64 + "H5-pc": {
  65 + "Chrome": "y",
  66 + "IE": "y",
  67 + "Edge": "y",
  68 + "Firefox": "y",
  69 + "Safari": "y"
  70 + },
  71 + "小程序": {
  72 + "微信": "y",
  73 + "阿里": "u",
  74 + "百度": "u",
  75 + "字节跳动": "u",
  76 + "QQ": "u",
  77 + "京东": "u"
  78 + },
  79 + "快应用": {
  80 + "华为": "u",
  81 + "联盟": "u"
  82 + },
  83 + "Vue": {
  84 + "vue2": "u",
  85 + "vue3": "y"
  86 + }
  87 + }
  88 + }
  89 + },
  90 + "dependencies": {
  91 + "clipboard": "^2.0.11",
  92 + "dayjs": "^1.11.7",
  93 + "lodash": "^4.17.21",
  94 + "lodash-es": "^4.17.21",
  95 + "luch-request": "^3.0.8",
  96 + "pinia": "^2.3.1",
  97 + "pinia-plugin-persist-uni": "^1.2.0",
  98 + "uview-plus": "^3.6.29",
  99 + "vue": "^2.7.16",
  100 + "weixin-js-sdk": "^1.6.0"
  101 + },
  102 + "devDependencies": {
  103 + "prettier": "^2.8.7",
  104 + "vconsole": "^3.15.0"
  105 + }
  106 +}
  1 +{
  2 + "easycom": {
  3 + "autoscan": true,
  4 + "custom": {
  5 + "^s-(.*)": "@/sheep/components/s-$1/s-$1.vue",
  6 + "^su-(.*)": "@/sheep/ui/su-$1/su-$1.vue",
  7 + "^u--(.*)": "@/node_modules/uview-plus/components/u-$1/u-$1.vue",
  8 + "^up-(.*)": "@/node_modules/uview-plus/components/u-$1/u-$1.vue",
  9 + "^u-([^-].*)": "@/node_modules/uview-plus/components/u-$1/u-$1.vue",
  10 + "^qiun-(.*)": "@/uni_modules/qiun-data-charts/components/qiun-$1/qiun-$1.vue"
  11 + }
  12 + },
  13 + "pages": [
  14 + {
  15 + "path": "pages/xunji/xunji",
  16 + "style": {
  17 + "navigationBarTitleText": "训记"
  18 + }
  19 + },
  20 + {
  21 + "path": "pages/user/user",
  22 + "style": {
  23 + "navigationBarTitleText": "我的",
  24 + "navigationStyle": "default"
  25 + }
  26 + }
  27 + ],
  28 + "subPackages": [
  29 + {
  30 + "root": "pages4",
  31 + "name": "分包4",
  32 + "pages": [
  33 + {
  34 + "path": "pages/xunji/xunji-xunlian-jihua",
  35 + "style": {
  36 + "navigationBarTitleText": "训练计划"
  37 + }
  38 + },
  39 + {
  40 + "path": "pages/xunji/xunji-wode-zhuye",
  41 + "style": {
  42 + "navigationBarTitleText": "我的主页"
  43 + }
  44 + },
  45 + {
  46 + "path": "pages/xunji/xunji-wode-qianming",
  47 + "style": {
  48 + "navigationBarTitleText": "个性签名",
  49 + "navigationStyle": "default"
  50 + }
  51 + },
  52 + {
  53 + "path": "pages/xunji/xunji-wode-moban",
  54 + "style": {
  55 + "navigationBarTitleText": "我的模版"
  56 + }
  57 + },
  58 + {
  59 + "path": "pages/xunji/xunji-rili-tianjia",
  60 + "style": {
  61 + "navigationBarTitleText": "日历添加训练动作"
  62 + }
  63 + },
  64 + {
  65 + "path": "pages/xunji/xunji-rili-tianjia-moban",
  66 + "style": {
  67 + "navigationBarTitleText": "训记-日历-训练模版"
  68 + }
  69 + },
  70 + {
  71 + "path": "pages/xunji/xunji-moban",
  72 + "style": {
  73 + "navigationBarTitleText": "动作模板"
  74 + }
  75 + },
  76 + {
  77 + "path": "pages/xunji/xunji-moban-xiangqing",
  78 + "style": {
  79 + "navigationBarTitleText": "模板详情查看"
  80 + }
  81 + },
  82 + {
  83 + "path": "pages/xunji/xunji-dongzuo-xinzeng",
  84 + "style": {
  85 + "navigationBarTitleText": "",
  86 + "navigationStyle": "default"
  87 + }
  88 + },
  89 + {
  90 + "path": "pages/xunji/xunji-dongzuo-xiangqing",
  91 + "style": {
  92 + "navigationBarTitleText": "动作详情",
  93 + "navigationStyle": "default"
  94 + }
  95 + },
  96 + {
  97 + "path": "pages/xunji/xunji-dongzuo-xiangqing-chaojizu",
  98 + "style": {
  99 + "navigationBarTitleText": "超级组详情",
  100 + "backgroundColor": "#1a1a1a"
  101 + }
  102 + },
  103 +
  104 + {
  105 + "path": "pages/xunji/xunji-dongzuo-lianxi",
  106 + "style": {
  107 + "navigationBarTitleText": "动作练习/训练"
  108 + }
  109 + },
  110 + {
  111 + "path": "pages/xunji/dongzuo-xinzengchaojizu",
  112 + "style": {
  113 + "navigationBarTitleText": "新增超级组"
  114 + }
  115 + },
  116 + {
  117 + "path": "pages/xunji/dongzuo-muluguanli",
  118 + "style": {
  119 + "navigationBarTitleText": "动作目录管理",
  120 + "navigationStyle": "default"
  121 + }
  122 + },
  123 + {
  124 + "path": "pages/xunji/jihua-search",
  125 + "style": {
  126 + "navigationBarTitleText": "训记-计划-搜索"
  127 + }
  128 + },
  129 + {
  130 + "path": "pages/xunji/xunji-shiping",
  131 + "style": {
  132 + "navigationBarTitleText": ""
  133 + }
  134 + }
  135 + ]
  136 + },
  137 + {
  138 + "root": "pages7",
  139 + "name": "分包7",
  140 + "pages": [
  141 + {
  142 + "path": "pages/index/login",
  143 + "style": {
  144 + "navigationBarTitleText": "登录",
  145 + "navigationStyle": "default"
  146 + }
  147 + }
  148 + ]
  149 + },
  150 + {
  151 + "root": "pages5",
  152 + "name": "分包5",
  153 + "pages": [
  154 + {
  155 + "path": "pages/user/wode-geren-ziliao",
  156 + "style": {
  157 + "navigationBarTitleText": "个人资料",
  158 + "navigationStyle": "default"
  159 + }
  160 + }
  161 + ]
  162 + }
  163 + ],
  164 + "globalStyle": {
  165 + "navigationBarTextStyle": "black",
  166 + "navigationBarTitleText": "芋道商城",
  167 + "navigationBarBackgroundColor": "#FFFFFF",
  168 + "backgroundColor": "#FFFFFF",
  169 + "navigationStyle": "custom"
  170 + },
  171 + "pageTransition": {
  172 + "style": "slide-in-bottom",
  173 + "duration": 300
  174 + },
  175 + "tabBar": {
  176 + "height": 0,
  177 + "custom": true,
  178 + "list": [
  179 + {
  180 + "pagePath": "pages/xunji/xunji"
  181 + },
  182 + {
  183 + "pagePath": "pages/user/user"
  184 + }
  185 + ]
  186 + }
  187 +}
  1 +<template>
  2 + <view class="my-page">
  3 + <!-- 登录头部区 -->
  4 + <view
  5 + v-if="userStore.isLogin"
  6 + class="section-card user-header"
  7 + hover-class="card-hover"
  8 + @tap="goMyPersonalData"
  9 + >
  10 + <image
  11 + :src="
  12 + userInfo.avatar ||
  13 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
  14 + "
  15 + mode="aspectFill"
  16 + class="avatar"
  17 + />
  18 + <view class="info-content">
  19 + <view class="name-row">
  20 + <text class="nickname">{{ userInfo.nickname || '微信用户' }}</text>
  21 + <text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text>
  22 + </view>
  23 + </view>
  24 + <view class="right">
  25 + <uni-icons type="right" size="16" color="#000" />
  26 + </view>
  27 + </view>
  28 +
  29 + <!-- 未登录引导区 -->
  30 + <view v-else class="section-card login-guide-box">
  31 + <view class="guide-txt">
  32 + <text class="title">欢迎加入鸿星运动</text>
  33 + <text class="desc">登录后即可享受课程预约及资产管理</text>
  34 + </view>
  35 + <button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button>
  36 + </view>
  37 +
  38 + <!-- -->
  39 + <view class="vip-banner" hover-class="opacity-hover" @click="goAddVip">
  40 + <view class="vip-info">
  41 + <uni-icons type="vip-filled" size="22" color="#f1c40f" />
  42 + <text class="vip-text">
  43 + {{ userInfo.deposit === 1 ? '鸿星·尊享会员 | 已开通' : '鸿星·会员 | 开通只需押金¥199' }}
  44 + </text>
  45 + </view>
  46 + <view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view>
  47 + </view>
  48 +
  49 + <!-- 课程状态快速入口 -->
  50 + <view class="section-card quick-entry">
  51 + <view
  52 + v-for="entry in quickEntryConfig"
  53 + :key="entry.type"
  54 + class="entry-item"
  55 + hover-class="opacity-hover"
  56 + @click="handleQuickEntry(entry.type)"
  57 + >
  58 + <text class="num">{{ userInfo[entry.key] || 0 }}</text>
  59 + <text class="label">{{ entry.label }}</text>
  60 + </view>
  61 + </view>
  62 +
  63 + <!-- 广告位 -->
  64 + <view class="banner-box" @click="goJiamen">
  65 + <image
  66 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png"
  67 + mode="aspectFill"
  68 + class="banner-img"
  69 + />
  70 + </view>
  71 +
  72 + <!-- 核心应用区 -->
  73 + <view class="section-card apply-section">
  74 + <view
  75 + v-for="(item, index) in APPLY_CONFIG_LIST"
  76 + :key="index"
  77 + class="apply-item"
  78 + @click="authNavigateTo(item.url)"
  79 + >
  80 + <view class="icon-bg">
  81 + <image :src="item.icon" class="img" mode="aspectFit" />
  82 + </view>
  83 + <text class="text">{{ item.text }}</text>
  84 + </view>
  85 + </view>
  86 +
  87 + <!-- 资产账户网格区 -->
  88 + <view v-if="userStore.isLogin" class="section-card">
  89 + <view class="account-grid">
  90 + <view
  91 + v-for="(acc, idx) in accountConfig"
  92 + :key="idx"
  93 + class="account-item"
  94 + @click="authNavigateTo(acc.url)"
  95 + >
  96 + <text class="acc-lab">{{ acc.label }}</text>
  97 + <text class="acc-val">
  98 + {{ formatAccountValue(userInfo[acc.key], acc.isFloat) }}
  99 + <text v-if="acc.unit" class="unit">{{ acc.unit }}</text>
  100 + </text>
  101 + </view>
  102 + </view>
  103 + </view>
  104 +
  105 + <!-- 功能矩阵九宫格 -->
  106 + <view class="section-card icon-grid-box">
  107 + <view class="icon-grid">
  108 + <view
  109 + v-for="(item, index) in FUNCTION_CONFIG_LIST"
  110 + :key="index"
  111 + class="icon-item"
  112 + @click="handleGridItemClick(item)"
  113 + >
  114 + <view class="icon-img-wrap">
  115 + <image :src="item.icon" mode="aspectFit" class="icon-img" />
  116 +
  117 + <view v-if="item.text === '订单记录' && userInfo.orderRecordMark" class="badge">
  118 + {{ userInfo.orderRecordMark }}
  119 + </view>
  120 + </view>
  121 + <text class="icon-text">{{ item.text }}</text>
  122 +
  123 + <button
  124 + v-if="item.text === '联系客服'"
  125 + open-type="contact"
  126 + class="mp-contact-overlay-btn"
  127 + />
  128 + </view>
  129 + </view>
  130 + </view>
  131 +
  132 + <!-- 设置区 -->
  133 + <view v-if="userStore.isLogin" class="section-card">
  134 + <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-shezhi')">
  135 + <text class="setting-text">个人设置</text>
  136 + <uni-icons type="right" size="14" color="#E0E0E0" />
  137 + </view>
  138 + <view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')">
  139 + <text class="setting-text">隐私中心</text>
  140 + <uni-icons type="right" size="14" color="#E0E0E0" />
  141 + </view>
  142 + </view>
  143 +
  144 + <Tabbar />
  145 + </view>
  146 +</template>
  147 +
  148 +<script setup>
  149 + import { ref } from 'vue';
  150 + import UserApi from '@/sheep/api/member/user';
  151 + import MemberApi from '@/sheep/api/member/member';
  152 + import useUserStore from '@/sheep/store/user';
  153 + import { onShow } from '@dcloudio/uni-app';
  154 + // 响应式数据挂载
  155 + const userStore = useUserStore();
  156 + const userInfo = ref({});
  157 + const memberLevelName = ref('');
  158 +
  159 + // 固定的 UI 配置
  160 + const APPLY_CONFIG_LIST = [];
  161 +
  162 + const FUNCTION_CONFIG_LIST = [];
  163 +
  164 + // 课程计数状态配置映射
  165 + const quickEntryConfig = [
  166 + { type: 1, key: 'courseNum', label: '待上课' },
  167 + { type: 2, key: 'courseWaitNum', label: '等待中' },
  168 + { type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' },
  169 + ];
  170 +
  171 + // 资产账户字段清洗配置映射 (对应接口数据类型:number 与 integer)
  172 + const accountConfig = [];
  173 +
  174 + /**
  175 + * JSDoc 核心资产数值洗涤函数
  176 + * @param {number|undefined} val 原始金钱/数量值
  177 + * @param {boolean} isFloat 是否需要保留两位小数
  178 + * @returns {string|number} 格式化后的安全渲染字符串
  179 + */
  180 + const formatAccountValue = (val, isFloat) => {
  181 + if (val === undefined || val === null) return isFloat ? '0.00' : 0;
  182 + return isFloat ? Number(val).toFixed(2) : Math.floor(val);
  183 + };
  184 +
  185 + /**
  186 + * 路由守卫拦截转发
  187 + * @param {string} url 目标绝对/相对地址
  188 + */
  189 + const authNavigateTo = (url) => {
  190 + if (!userStore.isLogin) {
  191 + goLogin();
  192 + return;
  193 + }
  194 + uni.navigateTo({
  195 + url,
  196 + fail: (err) => console.error(`[Router] 页面跳转失败 ${url}: `, err),
  197 + });
  198 + };
  199 +
  200 + /**
  201 + * 统一处理功能网格的点击分发
  202 + * @param {Object} item 节点配置项
  203 + */
  204 + const handleGridItemClick = (item) => {
  205 + if (item.text === '联系客服') {
  206 + // #ifndef MP-WEIXIN
  207 + // 兜底非微信小程序平台(如H5、App),可以正常走原来的普通客服页面路由
  208 + authNavigateTo(item.url);
  209 + // #endif
  210 + return;
  211 + }
  212 +
  213 + // 其他正常功能,正常走路由拦截守卫
  214 + authNavigateTo(item.url);
  215 + };
  216 +
  217 + /**
  218 + * 异步高内聚合并请求
  219 + * 解决因先后触发 setData 导致微信小程序底层 AppService 与 WebView 之间高频拥堵卡顿的问题
  220 + */
  221 + const fetchPageData = async () => {
  222 + try {
  223 + const [userRes, levelRes] = await Promise.all([
  224 + UserApi.getUserInfo(),
  225 + MemberApi.getMemberLevel(),
  226 + ]);
  227 +
  228 + const rawUser = userRes.data || {};
  229 + userInfo.value = rawUser;
  230 +
  231 + // 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化
  232 + const levels = levelRes.data?.detailList || [];
  233 + if (rawUser.level !== undefined && levels.length > 0) {
  234 + const target = levels.find((item) => item.id === rawUser.level);
  235 + memberLevelName.value = target ? target.name : '';
  236 + } else {
  237 + memberLevelName.value = '';
  238 + }
  239 + } catch (error) {
  240 + console.error('[API Error] 拉取个人资产信息流失败:', error);
  241 + }
  242 + };
  243 +
  244 + onShow(() => {
  245 + if (userStore.isLogin) {
  246 + fetchPageData();
  247 + } else {
  248 + userInfo.value = {};
  249 + memberLevelName.value = '';
  250 + }
  251 + });
  252 +
  253 + // 路由跳转原子原子层
  254 + const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' });
  255 + const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao');
  256 + const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan');
  257 +
  258 + const goJiamen = () => uni.navigateTo({ url: '/pages7/pages/index/shouye-jiamen-hongxing' });
  259 + const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shangke?type=${type}`);
  260 +</script>
  261 +
  262 +<style scoped lang="scss">
  263 + $brand-color: #ff6b00;
  264 + $page-bg: #f8f8f8;
  265 +
  266 + .my-page {
  267 + min-height: 100vh;
  268 + background-color: $page-bg;
  269 + padding: 20rpx 28rpx calc(40rpx + env(safe-area-inset-bottom)); /* 解决部分 iOS 底部 Tabbar 高度塌陷 */
  270 + box-sizing: border-box;
  271 +
  272 + .section-card {
  273 + background: #ffffff;
  274 + border-radius: 24rpx;
  275 + padding: 30rpx 20rpx;
  276 + margin-bottom: 24rpx;
  277 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
  278 + }
  279 +
  280 + .user-header {
  281 + display: flex;
  282 + align-items: center;
  283 + padding: 34rpx 30rpx;
  284 + .avatar {
  285 + width: 110rpx;
  286 + height: 110rpx;
  287 + border-radius: 50%;
  288 + background: #f0f0f0;
  289 + border: 4rpx solid #fff;
  290 + }
  291 + .info-content {
  292 + margin-left: 24rpx;
  293 + flex: 1;
  294 + .name-row {
  295 + display: flex;
  296 + flex-direction: column;
  297 + .nickname {
  298 + font-size: 34rpx;
  299 + font-weight: bold;
  300 + color: #333;
  301 + }
  302 + .tag {
  303 + font-size: 20rpx;
  304 + color: $brand-color;
  305 +
  306 + padding: 4rpx 12rpx;
  307 + border-radius: 8rpx;
  308 +
  309 + vertical-align: middle;
  310 + }
  311 + }
  312 + }
  313 + .right {
  314 + display: flex;
  315 + align-items: center;
  316 + gap: 60rpx;
  317 + .img {
  318 + width: 60rpx;
  319 + height: 60rpx;
  320 + }
  321 + }
  322 + }
  323 +
  324 + .login-guide-box {
  325 + display: flex;
  326 + justify-content: space-between;
  327 + align-items: center;
  328 + .title {
  329 + font-size: 32rpx;
  330 + font-weight: bold;
  331 + color: #333;
  332 + }
  333 + .desc {
  334 + font-size: 22rpx;
  335 + color: #999;
  336 + margin-top: 4rpx;
  337 + display: block;
  338 + }
  339 + .login-btn {
  340 + margin: 0;
  341 + background: $brand-color;
  342 + color: #fff;
  343 + font-size: 24rpx;
  344 + height: 64rpx;
  345 + line-height: 64rpx;
  346 + border-radius: 32rpx;
  347 + padding: 0 30rpx;
  348 + &::after {
  349 + border: none;
  350 + } /* 清理小程序 button 默认黑边线 */
  351 + }
  352 + }
  353 +
  354 + .vip-banner {
  355 + background: #2b2b2b;
  356 + border-radius: 20rpx;
  357 + padding: 24rpx 30rpx;
  358 + display: flex;
  359 + justify-content: space-between;
  360 + align-items: center;
  361 + margin-bottom: 24rpx;
  362 + .vip-info {
  363 + display: flex;
  364 + align-items: center;
  365 + }
  366 + .vip-text {
  367 + color: #f1c40f;
  368 + font-size: 24rpx;
  369 + margin-left: 14rpx;
  370 + }
  371 + .vip-btn {
  372 + background: linear-gradient(90deg, #f1c40f, #f39c12);
  373 + color: #333;
  374 + font-size: 20rpx;
  375 + font-weight: bold;
  376 + padding: 8rpx 20rpx;
  377 + border-radius: 30rpx;
  378 + }
  379 + }
  380 +
  381 + .quick-entry {
  382 + display: flex;
  383 + justify-content: space-around;
  384 + .entry-item {
  385 + text-align: center;
  386 + .num {
  387 + font-size: 38rpx;
  388 + font-weight: bold;
  389 + color: #333;
  390 + }
  391 + .label {
  392 + font-size: 22rpx;
  393 + color: #999;
  394 + margin-top: 4rpx;
  395 + display: block;
  396 + }
  397 + }
  398 + }
  399 +
  400 + .banner-box {
  401 + height: 160rpx;
  402 + margin-bottom: 24rpx;
  403 + border-radius: 20rpx;
  404 + overflow: hidden;
  405 + .banner-img {
  406 + width: 100%;
  407 + height: 100%;
  408 + }
  409 + }
  410 +
  411 + .apply-section {
  412 + display: grid;
  413 + grid-template-columns: repeat(5, 1fr);
  414 + .apply-item {
  415 + display: flex;
  416 + flex-direction: column;
  417 + align-items: center;
  418 + .icon-bg {
  419 + width: 80rpx;
  420 + height: 80rpx;
  421 + background: #f9f9f9;
  422 + border-radius: 20rpx;
  423 + display: flex;
  424 + align-items: center;
  425 + justify-content: center;
  426 + margin-bottom: 12rpx;
  427 + .img {
  428 + width: 44rpx;
  429 + height: 44rpx;
  430 + }
  431 + }
  432 + .text {
  433 + font-size: 22rpx;
  434 + color: #666;
  435 + }
  436 + }
  437 + }
  438 +
  439 + .account-grid {
  440 + display: grid;
  441 + grid-template-columns: repeat(4, 1fr);
  442 + row-gap: 34rpx;
  443 + .account-item {
  444 + display: flex;
  445 + flex-direction: column;
  446 + align-items: center;
  447 + .acc-val {
  448 + font-size: 30rpx;
  449 + font-weight: bold;
  450 + color: #333;
  451 + .unit {
  452 + font-size: 20rpx;
  453 + font-weight: normal;
  454 + color: #666;
  455 + margin-left: 2rpx;
  456 + }
  457 + }
  458 + .acc-lab {
  459 + font-size: 22rpx;
  460 + color: #999;
  461 + margin-top: 4rpx;
  462 + }
  463 + }
  464 + }
  465 +
  466 + .icon-grid {
  467 + display: grid;
  468 + grid-template-columns: repeat(4, 1fr);
  469 + row-gap: 40rpx;
  470 + .icon-item {
  471 + display: flex;
  472 + flex-direction: column;
  473 + align-items: center;
  474 + position: relative;
  475 + .icon-img-wrap {
  476 + position: relative;
  477 + margin-bottom: 12rpx;
  478 + .icon-img {
  479 + width: 46rpx;
  480 + height: 46rpx;
  481 + }
  482 + .badge {
  483 + position: absolute;
  484 + top: -12rpx;
  485 + right: -42rpx; /* 适当拓宽右移,容纳后端多字文本 */
  486 + background: #ff4d4f;
  487 + color: #fff;
  488 + font-size: 18rpx;
  489 + padding: 2rpx 10rpx;
  490 + border-radius: 16rpx;
  491 + white-space: nowrap;
  492 + }
  493 + /* 未读状态小红点 */
  494 + .dot-badge {
  495 + position: absolute;
  496 + top: -4rpx;
  497 + right: -4rpx;
  498 + width: 14rpx;
  499 + height: 14rpx;
  500 + background: #ff4d4f;
  501 + border-radius: 50%;
  502 + }
  503 + }
  504 + .icon-text {
  505 + font-size: 24rpx;
  506 + color: #555;
  507 + }
  508 + .mp-contact-overlay-btn {
  509 + position: absolute;
  510 + top: 0;
  511 + left: 0;
  512 + width: 100% !important;
  513 + height: 100% !important;
  514 + opacity: 0 !important; /* 核心:完全透明 */
  515 + border: none !important;
  516 + padding: 0 !important;
  517 + margin: 0 !important;
  518 + z-index: 10; /* 确保盖在图表和文字的最上方 */
  519 +
  520 + &::after {
  521 + border: none !important;
  522 + }
  523 + }
  524 + }
  525 + }
  526 +
  527 + .setting-item {
  528 + display: flex;
  529 + justify-content: space-between;
  530 + align-items: center;
  531 + padding: 24rpx 0;
  532 + border-bottom: 1rpx solid #f9f9f9;
  533 + &:last-child {
  534 + border-bottom: none;
  535 + }
  536 + .setting-text {
  537 + font-size: 28rpx;
  538 + color: #444;
  539 + }
  540 + }
  541 +
  542 + .opacity-hover {
  543 + opacity: 0.7;
  544 + }
  545 + .card-hover {
  546 + background-color: #fcfcfc;
  547 + }
  548 + }
  549 +</style>
  1 +<template>
  2 + <view class="charts-box">
  3 + <qiun-data-charts
  4 + type="line"
  5 + :opts="opts"
  6 + :chartData="chartData"
  7 + :reshow="reshow"
  8 + :canvas2d="true"
  9 + />
  10 + </view>
  11 +</template>
  12 +
  13 +<script setup>
  14 + import { ref, reactive, watch } from 'vue';
  15 +
  16 + // 1. 定义 Props 接收父组件数据
  17 + const props = defineProps({
  18 + // 传入的分类数据 (横坐标)
  19 + categories: {
  20 + type: Array,
  21 + default: () => [],
  22 + },
  23 + // 传入的系列数据 (纵坐标内容)
  24 + series: {
  25 + type: Array,
  26 + default: () => [],
  27 + },
  28 + // 专门用于解决弹窗不显示问题的属性
  29 + reshow: {
  30 + type: Boolean,
  31 + default: false,
  32 + },
  33 + });
  34 +
  35 + const chartData = ref({});
  36 +
  37 + // 2. 图表配置项
  38 + const opts = reactive({
  39 + color: [
  40 + '#1890FF',
  41 + '#91CB74',
  42 + '#FAC858',
  43 + '#EE6666',
  44 + '#73C0DE',
  45 + '#3CA272',
  46 + '#FC8452',
  47 + '#9A60B4',
  48 + '#ea7ccc',
  49 + ],
  50 + padding: [15, 10, 0, 15],
  51 + enableScroll: false,
  52 + legend: {},
  53 + xAxis: {
  54 + disableGrid: true,
  55 + },
  56 + yAxis: {
  57 + gridType: 'dash',
  58 + dashLength: 2,
  59 + },
  60 + extra: {
  61 + line: {
  62 + type: 'straight',
  63 + width: 2,
  64 + activeType: 'hollow',
  65 + },
  66 + },
  67 + });
  68 +
  69 + // 3. 核心逻辑:格式化数据
  70 + const formatData = () => {
  71 + if (props.categories.length > 0) {
  72 + chartData.value = {
  73 + categories: props.categories,
  74 + series: props.series,
  75 + };
  76 + }
  77 + };
  78 +
  79 + // 4. 监听 Props 变化,当父组件传入新数据时自动重绘
  80 + watch(
  81 + () => [props.categories, props.series],
  82 + () => {
  83 + formatData();
  84 + },
  85 + { immediate: true, deep: true },
  86 + );
  87 +</script>
  88 +
  89 +<style lang="scss" scoped>
  90 + .charts-box {
  91 + width: 100%;
  92 + height: 100%;
  93 + }
  94 +</style>
  1 +<template>
  2 + <up-popup :show="show" mode="bottom" round="16" closeable @close="show = false" :safeAreaInsetBottom="false">
  3 + <view class="desc-container">
  4 + <view class="title">动作备注</view>
  5 +
  6 + <view class="input-box">
  7 + <up-textarea
  8 + v-model="tempNoteContent"
  9 + placeholder="此处填写个人备注"
  10 + autoHeight
  11 + border="none"
  12 + customStyle="background: #242424; padding: 20rpx; border-radius: 12rpx; color: #fff"
  13 + placeholderStyle="color: #999"
  14 + ></up-textarea>
  15 + </view>
  16 +
  17 + <view class="footer">
  18 + <view class="btn" @click="saveNoteContent">保存</view>
  19 + </view>
  20 + </view>
  21 + </up-popup>
  22 +</template>
  23 +
  24 +<script setup>
  25 + import { ref } from 'vue';
  26 +
  27 + const show = ref(false);
  28 + const tempNoteContent = ref(''); // 临时编辑的备注内容
  29 +
  30 + const emit = defineEmits(['saveSuccess']);
  31 + // 保存备注
  32 + const saveNoteContent = async () => {
  33 + const content = tempNoteContent.value.trim();
  34 + // 如果内容为空,直接返回,不提交
  35 + if (!content) {
  36 + uni.showToast({ title: '备注不能为空', icon: 'none' });
  37 + return;
  38 + }
  39 + emit('saveSuccess', content);
  40 + // 关闭弹窗
  41 + show.value = false;
  42 + };
  43 +
  44 + const open = () => {
  45 + show.value = true;
  46 + };
  47 +
  48 + defineExpose({ open });
  49 +</script>
  50 +
  51 +<style lang="scss" scoped>
  52 + .desc-container {
  53 + background-color: #1a1a1a;
  54 + padding: 40rpx 30rpx 40rpx;
  55 +
  56 + .title {
  57 + color: #fff;
  58 + font-size: 32rpx;
  59 + font-weight: 500;
  60 + text-align: center;
  61 + margin-bottom: 40rpx;
  62 + }
  63 +
  64 + .input-box {
  65 + margin-bottom: 60rpx;
  66 +
  67 + /* 穿透修改 u-textarea 内部文字颜色 */
  68 + :deep(.u-textarea__field) {
  69 + color: #ccc !important;
  70 + }
  71 + }
  72 +
  73 + .footer {
  74 + width: 100%;
  75 + display: flex;
  76 + justify-content: center;
  77 + align-items: center;
  78 +
  79 + .btn {
  80 + width: 100%;
  81 + height: 80rpx;
  82 + background-color: #fedc1f;
  83 + color: #333;
  84 + border-radius: 12rpx;
  85 + text-align: center;
  86 + line-height: 80rpx;
  87 + }
  88 + }
  89 + }
  90 +</style>
  1 +<template>
  2 + <up-popup
  3 + :show="actionShow"
  4 + mode="bottom"
  5 + minHeight="90vh"
  6 + @close="actionShow = false"
  7 + bgColor="#1a1a1a"
  8 + >
  9 + <scroll-view class="action-container" scroll-y>
  10 + <view class="header" v-if="type == 1">
  11 + <view class="explain">
  12 + <image
  13 + v-if="modeTab < 2"
  14 + :src="modeTab == 0 ? actionDetail.url3dAnimation : actionDetail.urlRealPerson"
  15 + class="media-content"
  16 + mode="aspectFill"
  17 + />
  18 + <view class="video" v-else @click="playVideo(actionDetail.urlTutorial)">
  19 + <video
  20 + :src="actionDetail.urlTutorial"
  21 + class="media-content"
  22 + :autoplay="false"
  23 + :show-center-play-btn="false"
  24 + :controls="false"
  25 + />
  26 + <view class="play-icon">
  27 + <up-icon name="play-right" size="28" class="icon" color="#fff"></up-icon>
  28 + </view>
  29 + </view>
  30 + </view>
  31 +
  32 + <view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial">
  33 + <view
  34 + class="tab-item"
  35 + v-if="actionDetail.url3dAnimation"
  36 + :class="{ active: modeTab == 0 }"
  37 + @click="switchModeTab(0)"
  38 + >
  39 + 3D
  40 + </view>
  41 + <view
  42 + class="tab-item"
  43 + v-if="actionDetail.urlRealPerson"
  44 + :class="{ active: modeTab == 1 }"
  45 + @click="switchModeTab(1)"
  46 + >
  47 + 真人
  48 + </view>
  49 + <view
  50 + class="tab-item"
  51 + v-if="actionDetail.urlTutorial"
  52 + :class="{ active: modeTab == 2 }"
  53 + @click="switchModeTab(2)"
  54 + >
  55 + 讲解
  56 + </view>
  57 + </view>
  58 + </view>
  59 +
  60 + <view class="main">
  61 + <view class="main-header">
  62 + <view class="title-bar">
  63 + <text class="title">{{ actionDetail?.name }}</text>
  64 + <view class="action-icons">
  65 + <!-- <up-icon name="share-square" color="#fff" size="24"></up-icon> -->
  66 + <button class="share-btn" open-type="share">
  67 + <uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
  68 + </button>
  69 + <up-icon
  70 + :name="isFavorite ? 'star-fill' : 'star'"
  71 + :color="isFavorite ? '#fedc1f' : '#fff'"
  72 + size="24"
  73 + @click="toggleCollect"
  74 + ></up-icon>
  75 + <!-- <FavoriteBtn :id="actionId" :type="type" /> -->
  76 + </view>
  77 + </view>
  78 + <!-- 要点,历史,平替动作标签 -->
  79 + <view class="content-tabs">
  80 + <view class="tab-item" :class="{ active: contentTab === 0 }" @click="contentTab = 0">
  81 + 要点
  82 + </view>
  83 + <view class="tab-item" :class="{ active: contentTab === 1 }" @click="contentTab = 1">
  84 + 历史
  85 + </view>
  86 + <view
  87 + class="tab-item"
  88 + :class="{ active: contentTab === 2 }"
  89 + @click="contentTab = 2"
  90 + v-if="type === 1"
  91 + >
  92 + 平替动作
  93 + </view>
  94 + </view>
  95 + </view>
  96 +
  97 + <view class="main-content">
  98 + <!-- 1 要点 -->
  99 + <view v-if="contentTab === 0" class="tab-pane slide-up">
  100 + <view class="section" v-if="actionDetail.urlTutorial">
  101 + <view class="section-title">视频讲解</view>
  102 + <view class="video-grid">
  103 + <!-- <view class="video-card" v-for="i in 2" :key="i"></view> -->
  104 + <view class="video-card" @click="playVideo(actionDetail.urlTutorial)">
  105 + <video
  106 + :src="actionDetail.urlTutorial"
  107 + class="video"
  108 + :autoplay="false"
  109 + :show-center-play-btn="false"
  110 + :controls="false"
  111 + />
  112 + <view class="play-overlay"
  113 + ><up-icon name="play-circle-fill" color="#fff" size="30"></up-icon
  114 + ></view>
  115 + </view>
  116 + </view>
  117 + </view>
  118 +
  119 + <view class="memo-box" @click="openBeizhu">
  120 + <view class="section-title">训练备注</view>
  121 + <up-textarea
  122 + class="textarea"
  123 + v-model="actionDetail.userNote"
  124 + placeholder="点击填写备注"
  125 + autoHeight
  126 + customStyle="background: transparent; border: none; padding: 10rpx 0;"
  127 + placeholderStyle="color: #666"
  128 + disabled
  129 + border="none"
  130 + ></up-textarea>
  131 + </view>
  132 + <!-- 动作列表,只有超级组才有 -->
  133 + <view class="section" v-if="type === 2">
  134 + <view class="section-title">动作列表</view>
  135 + <view class="action-list">
  136 + <!-- 动作循环列表 -->
  137 + <view
  138 + class="action-item"
  139 + v-for="item in actionDetail?.exercises"
  140 + :key="item.id"
  141 + @click="openActionItem(item)"
  142 + >
  143 + <image :src="item.url3dAnimation || lostImage" mode="aspectFill" class="img" />
  144 + <view class="middle">
  145 + <view class="name">{{ item.name }}</view>
  146 + <view class="tips">
  147 + <!-- 渲染主练肌肉标签 -->
  148 + <view class="tip" v-for="p in item.primaryMuscles" :key="p">
  149 + {{ p }}
  150 + </view>
  151 + <view v-for="s in item.secondaryMuscles" :key="s">{{ s }}</view>
  152 + </view>
  153 + </view>
  154 + <up-icon name="arrow-right" color="#fff" size="16"></up-icon>
  155 + </view>
  156 + </view>
  157 + </view>
  158 +
  159 + <view class="section" v-if="type == 1">
  160 + <view class="section-title">步骤</view>
  161 + <view class="steps-list">
  162 + <rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>
  163 + </view>
  164 + </view>
  165 +
  166 + <view class="section">
  167 + <view class="section-title">训练部位</view>
  168 + <image :src="actionDetail.urlImage" mode="widthFix" class="muscle-map" />
  169 + <view class="legend">
  170 + <view class="legend-item">
  171 + <view class="legend-color primary"></view>
  172 + <text class="legend-text">主要部位</text>
  173 + </view>
  174 + <view class="legend-item">
  175 + <view class="legend-color secondary"></view>
  176 + <text class="legend-text">次要部位</text>
  177 + </view>
  178 + </view>
  179 + </view>
  180 + </view>
  181 + <!-- 2 历史 -->
  182 + <view v-if="contentTab === 1" class="tab-pane slide-up">
  183 + <!-- <view class="stat-card highlight">
  184 + <text class="label">最长时长</text>
  185 + <text class="value">01:02:03</text>
  186 + <text class="date">记录于 2026/03/04</text>
  187 + </view> -->
  188 +
  189 + <!-- <view class="chart-container" v-if="chartData && chartData.series">
  190 + <qiun-data-charts type="line" :opts="chartOpts" :chartData="chartData" />
  191 + </view> -->
  192 +
  193 + <view class="history-list">
  194 + <template v-if="historyList.length > 0">
  195 + <view class="history-item" v-for="item in historyList" :key="item.id">
  196 + <view class="item-header">
  197 + <text class="date">{{ formatDate(item.date) }}</text>
  198 + <text class="tag">{{ item.name }}</text>
  199 + </view>
  200 + <view class="item-body">
  201 + <view class="count">
  202 + <view>共 {{ item.setCount }} 组训练</view>
  203 + <view class="dot">· </view>
  204 + <!-- <view class="difficulty">困难</view> -->
  205 + <view class="difficulty">{{
  206 + item.weight ? item.weight + 'kg' : '无负重'
  207 + }}</view>
  208 + </view>
  209 + <view class="group-chips">
  210 + <view class="chip" v-for="(set, index) in item.setConfigList" :key="index">
  211 + <view class="idx">{{ index + 1 }}</view>
  212 + <view class="group-data">
  213 + {{ formatSetData(set) }}
  214 + </view>
  215 + </view>
  216 + </view>
  217 + </view>
  218 + </view>
  219 + </template>
  220 + <template v-else>
  221 + <up-empty text="暂无训练历史"> </up-empty>
  222 + </template>
  223 + </view>
  224 + </view>
  225 + <!-- 3 平替动作(只有动作组才有) -->
  226 + <view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up">
  227 + <view class="substitute-list">
  228 + <view
  229 + class="sub-item"
  230 + v-for="item in alternativeActions"
  231 + :key="item.id"
  232 + @click="openActionItem(item)"
  233 + >
  234 + <image :src="item.urlImage || lostImage" mode="aspectFill" class="img" />
  235 + <view class="sub-info">
  236 + <text class="name">{{ item.name }}</text>
  237 +
  238 + <text class="meta">练过{{ item.trainingReps }}次</text>
  239 + </view>
  240 + <up-icon name="arrow-right" color="#666" size="16"></up-icon>
  241 + </view>
  242 + </view>
  243 + </view>
  244 + </view>
  245 + </view>
  246 + <view class="footer">
  247 + <view class="btn" @click="startTraining">开始训练</view>
  248 + </view>
  249 + </scroll-view>
  250 + <!-- 备注弹窗组件 -->
  251 + <beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" />
  252 + </up-popup>
  253 +</template>
  254 +
  255 +<script setup>
  256 + import { onMounted, ref, nextTick } from 'vue';
  257 + import ExercisesApi from '@/sheep/api/motion/exercises';
  258 + import beizhu from '@/pages/xunji/components/beizhu.vue';
  259 + import SupersetsApi from '@/sheep/api/motion/supersets';
  260 + import TrainingApi from '@/sheep/api/Training/traininghistory';
  261 + import { onShareAppMessage } from '@dcloudio/uni-app';
  262 +
  263 + // 静态配置
  264 +
  265 + const alternativeActions = ref([]); // 平替动作列表接口数据
  266 +
  267 + // 响应式状态
  268 + const actionShow = ref(false);
  269 + const modeTab = ref(0);
  270 + const contentTab = ref(0);
  271 + const isFavorite = ref(false);
  272 +
  273 + // 记录当前动作的详细数据
  274 + const actionDetail = ref({});
  275 + // 记录当前动作的id
  276 + const actionId = ref(0);
  277 + // 记录是超级组还是动作组 1=动作组,2=超级组
  278 + const type = ref(0);
  279 + const showBeizhuRef = ref(null);
  280 + const historyList = ref([]); // 训练历史列表接口数据
  281 + const lostImage =
  282 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
  283 +
  284 + // 切换图表模式
  285 + const switchModeTab = (index) => {
  286 + modeTab.value = index;
  287 + };
  288 +
  289 + // 跳转到视频播放的页面
  290 + const playVideo = (url) => {
  291 + uni.navigateTo({
  292 + url: '/pages4/pages/xunji/xunji-shiping?url=' + url,
  293 + });
  294 + };
  295 + // 获取动作收藏状态
  296 + const checkExerciseFavorited = async () => {
  297 + try {
  298 + const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
  299 + isFavorite.value = res.data;
  300 + } catch (err) {
  301 + console.log(err);
  302 + }
  303 + };
  304 + // 收藏
  305 + const toggleCollect = async () => {
  306 + try {
  307 + const status = isFavorite.value ? 0 : 1;
  308 + if (type == 1) {
  309 + await ExercisesApi.toggleFavorite(actionId.value, status);
  310 + } else {
  311 + await SupersetsApi.toggleFavorite(actionId.value, status);
  312 + }
  313 +
  314 + isFavorite.value = !isFavorite.value;
  315 + } catch (err) {
  316 + console.log(err);
  317 + }
  318 + };
  319 +
  320 + // 获取超级组收藏状态
  321 + const checkSupersetFavorited = async () => {
  322 + try {
  323 + const res = await SupersetsApi.checkSupersetFavorited(actionId.value);
  324 + isFavorite.value = res.data;
  325 + } catch (err) {
  326 + console.log(err);
  327 + }
  328 + };
  329 +
  330 + const startTraining = () => {
  331 + uni.navigateTo({
  332 + url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}`,
  333 + });
  334 + };
  335 +
  336 + const open = (id, typeData) => {
  337 + actionId.value = Number(id);
  338 +
  339 + type.value = typeData;
  340 + contentTab.value = 0;
  341 + // 如何判断是动作还是超级组
  342 + if (typeData == 1) {
  343 + loadexercisedetail(actionId.value);
  344 + loadAlternativeActions(actionId.value);
  345 + checkExerciseFavorited();
  346 + } else {
  347 + loadsuperdetail(actionId.value);
  348 + checkSupersetFavorited();
  349 + }
  350 +
  351 + loadTrainHistoryDetail(actionId.value);
  352 + actionShow.value = true;
  353 + };
  354 + // 打开备注弹窗
  355 + const openBeizhu = () => {
  356 + nextTick(() => {
  357 + if (showBeizhuRef.value) {
  358 + showBeizhuRef.value.open(actionId.value);
  359 + }
  360 + });
  361 + };
  362 +
  363 + // 接收子组件传过来的备注内容
  364 + const handleNoteSave = async (content) => {
  365 + try {
  366 + if (type.value == 2) {
  367 + await SupersetsApi.addNotes({
  368 + supersetsId: actionId.value,
  369 + content: content,
  370 + });
  371 + } else {
  372 + await ExercisesApi.addNotes({
  373 + exerciseId: actionId.value,
  374 + content: content,
  375 + });
  376 + }
  377 + actionDetail.value.userNote = content;
  378 + } catch (e) {
  379 + console.log(e);
  380 + }
  381 + };
  382 +
  383 + // 启用分享菜单
  384 + // #ifdef MP-WEIXIN
  385 + wx.showShareMenu({
  386 + withShareTicket: true,
  387 + menus: ['shareAppMessage', 'shareTimeline'],
  388 + });
  389 + // #endif
  390 + // 定义分享内容
  391 + onShareAppMessage((res) => {
  392 + // res.from 可区分触发来源:'button'(按钮触发)或 'menu'(右上角菜单触发)[reference:3]
  393 + console.log('分享触发来源:', res.from);
  394 + return {
  395 + title: actionDetail.value.name || '健身动作分享', // 分享标题
  396 + // path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径
  397 + path: `/pages/xunji/xunji?currentTab=${3}`,
  398 + imageUrl: actionDetail.value.urlImage || lostImage, // 分享图片
  399 + };
  400 + });
  401 +
  402 + // 加载单个动作详情
  403 + const loadexercisedetail = async (id) => {
  404 + const response = await ExercisesApi.getExerciseById(id);
  405 + actionDetail.value = response.data;
  406 + if (actionDetail.value.url3dAnimation) {
  407 + modeTab.value = 0;
  408 + } else if (actionDetail.value.urlRealPerson) {
  409 + modeTab.value = 1;
  410 + } else {
  411 + modeTab.value = 2;
  412 + }
  413 + };
  414 +
  415 + // 加载超级组详情
  416 + const loadsuperdetail = async (id) => {
  417 + const response = await SupersetsApi.getSupersetsInfo(id);
  418 + actionDetail.value = response.data;
  419 + console.log('显示超级组详情:', actionDetail.value);
  420 + };
  421 + // 2. 加载平替动作的函数
  422 + const loadAlternativeActions = async (id) => {
  423 + if (!id || isNaN(Number(id))) {
  424 + console.warn('平替动作id非法,跳过请求:', id);
  425 + alternativeActions.value = [];
  426 + return;
  427 + }
  428 + try {
  429 + const res = await ExercisesApi.getalternatives(id);
  430 + if (res.code === 0 && res.data) {
  431 + alternativeActions.value = Array.isArray(res.data) ? res.data : [];
  432 + } else {
  433 + alternativeActions.value = [];
  434 + }
  435 + } catch (error) {
  436 + console.error('加载平替动作失败:', error);
  437 + alternativeActions.value = [];
  438 + }
  439 + };
  440 +
  441 + // 加载训练历史
  442 + const loadTrainHistoryDetail = async (id) => {
  443 + try {
  444 + const res = await TrainingApi.getTrainHistoryList(id);
  445 + historyList.value = res.data;
  446 + console.log('训练历史列表接口返回结果historyList.value', historyList.value);
  447 + } catch (error) {
  448 + console.error('加载训练历史失败:', error);
  449 + }
  450 + };
  451 + // 格式化时间
  452 + const formatDate = (dateArr) => {
  453 + if (!Array.isArray(dateArr) || dateArr.length < 3) return '';
  454 + const [year, month, day] = dateArr;
  455 + const date = new Date(year, month - 1, day);
  456 + const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
  457 + const week = weekArr[date.getDay()];
  458 + return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')} ${week}`;
  459 + };
  460 +
  461 + // 格式化每组数据:自动拼接 weight/reps/duration/distance
  462 + const formatSetData = (set) => {
  463 + const parts = [];
  464 +
  465 + // 重量
  466 + if (set.weight != null && set.weight !== '') {
  467 + parts.push(`${set.weight}kg`);
  468 + }
  469 +
  470 + // 次数
  471 + if (set.reps != null && set.reps !== '') {
  472 + parts.push(`${set.reps}次`);
  473 + }
  474 +
  475 + // 时长(自动转 时:分:秒)
  476 + if (set.duration != null && set.duration !== '') {
  477 + parts.push(formatTime(set.duration));
  478 + }
  479 +
  480 + // 距离
  481 + if (set.distance != null && set.distance !== '') {
  482 + parts.push(`${set.distance}m`);
  483 + }
  484 +
  485 + return parts.length > 0 ? parts.join(' × ') : '无数据';
  486 + };
  487 +
  488 + // 新增:秒数转 00:00:00 格式
  489 + const formatTime = (seconds) => {
  490 + const h = Math.floor(seconds / 3600);
  491 + const m = Math.floor((seconds % 3600) / 60);
  492 + const s = seconds % 60;
  493 +
  494 + const hh = h > 0 ? String(h).padStart(2, '0') + ':' : '';
  495 + const mm = String(m).padStart(2, '0') + ':';
  496 + const ss = String(s).padStart(2, '0');
  497 +
  498 + return hh + mm + ss;
  499 + };
  500 +
  501 +
  502 + // 点击超级组内部的动作 → 打开动作详情(复用同一个组件)
  503 + const openActionItem = (item) => {
  504 + open(item.id, 1);
  505 + };
  506 +
  507 + defineExpose({ open });
  508 +
  509 + onMounted(() => {});
  510 +</script>
  511 +
  512 +<style lang="scss" scoped>
  513 + .action-container {
  514 + width: 100%;
  515 + height: 80vh;
  516 + background-color: #1a1a1a;
  517 + color: #ffffff;
  518 +
  519 + .header {
  520 + width: 100%;
  521 + height: 50vh;
  522 + position: relative;
  523 +
  524 + .explain {
  525 + width: 100%;
  526 + height: 100%;
  527 +
  528 + .media-content {
  529 + width: 100%;
  530 + height: 100%;
  531 + }
  532 + .video {
  533 + width: 100%;
  534 + height: 100%;
  535 + display: flex;
  536 + justify-content: center;
  537 + align-items: center;
  538 + position: relative;
  539 + .play-icon {
  540 + width: 50px;
  541 + height: 50px;
  542 + background-color: rgb(216, 209, 209, 0.8);
  543 + display: flex;
  544 + justify-content: center;
  545 + align-items: center;
  546 + border-radius: 50%;
  547 + position: absolute;
  548 + z-index: 10;
  549 + }
  550 + }
  551 + }
  552 +
  553 + .mode-tabs {
  554 + position: absolute;
  555 + bottom: 30rpx;
  556 + left: 30rpx;
  557 + display: flex;
  558 + background: rgba(0, 0, 0, 0.6);
  559 + padding: 6rpx;
  560 + border-radius: 12rpx;
  561 + backdrop-filter: blur(10px);
  562 + z-index: 99;
  563 + .tab-item {
  564 + padding: 8rpx 24rpx;
  565 + font-size: 24rpx;
  566 + color: #999;
  567 + transition: all 0.3s;
  568 +
  569 + &.active {
  570 + background: #444;
  571 + color: #fff;
  572 + border-radius: 8rpx;
  573 + }
  574 + }
  575 + }
  576 + }
  577 +
  578 + .main {
  579 + .main-header {
  580 + position: sticky;
  581 + top: 0;
  582 + z-index: 10;
  583 + background: #1a1a1a;
  584 + padding: 30rpx 30rpx 0;
  585 +
  586 + .title-bar {
  587 + display: flex;
  588 + justify-content: space-between;
  589 + align-items: center;
  590 + margin-bottom: 30rpx;
  591 +
  592 + .title {
  593 + font-size: 48rpx;
  594 + font-weight: bold;
  595 + }
  596 +
  597 + .action-icons {
  598 + display: flex;
  599 + gap: 30rpx;
  600 +
  601 + .share-btn {
  602 + background: transparent;
  603 + border: none;
  604 + padding: 0;
  605 + margin: 0;
  606 + line-height: 1;
  607 + }
  608 + }
  609 + }
  610 +
  611 + .content-tabs {
  612 + display: flex;
  613 + gap: 60rpx;
  614 + border-bottom: 1rpx solid #333;
  615 +
  616 + .tab-item {
  617 + padding-bottom: 20rpx;
  618 + font-size: 30rpx;
  619 + color: #666;
  620 + position: relative;
  621 +
  622 + &.active {
  623 + color: #fff;
  624 + font-weight: 500;
  625 +
  626 + &::after {
  627 + content: '';
  628 + position: absolute;
  629 + bottom: 0;
  630 + left: 0;
  631 + width: 100%;
  632 + height: 4rpx;
  633 + background: #fedc1f;
  634 + border-radius: 2rpx;
  635 + }
  636 + }
  637 + }
  638 + }
  639 + }
  640 +
  641 + .main-content {
  642 + padding: 40rpx 30rpx 160rpx;
  643 +
  644 + .section {
  645 + margin-bottom: 40rpx;
  646 + }
  647 +
  648 + .section-title {
  649 + font-size: 32rpx;
  650 + font-weight: bold;
  651 + margin-bottom: 24rpx;
  652 + color: #ddd;
  653 + }
  654 +
  655 + .action-list {
  656 + .action-item {
  657 + display: flex;
  658 + align-items: center;
  659 + justify-content: space-between;
  660 + padding: 20rpx;
  661 + box-sizing: border-box;
  662 + gap: 15rpx;
  663 + background-color: #262626;
  664 + border-radius: 10rpx;
  665 + margin-bottom: 15rpx;
  666 +
  667 + .img {
  668 + width: 120rpx;
  669 + height: 120rpx;
  670 + border-radius: 5rpx;
  671 + }
  672 +
  673 + .middle {
  674 + flex: 1;
  675 + height: 120rpx;
  676 +
  677 + .name {
  678 + margin-bottom: 20rpx;
  679 + }
  680 +
  681 + .tips {
  682 + display: flex;
  683 + gap: 10rpx;
  684 + align-items: center;
  685 + flex-wrap: wrap;
  686 + font-size: 20rpx;
  687 +
  688 + .tip {
  689 + color: #fedc1f;
  690 + }
  691 + }
  692 + }
  693 + }
  694 + }
  695 +
  696 + // 要点板块样式
  697 + .video-grid {
  698 + display: grid;
  699 + grid-template-columns: 1fr 1fr;
  700 + gap: 20rpx;
  701 + margin-bottom: 40rpx;
  702 +
  703 + .video-card {
  704 + height: 360rpx;
  705 + position: relative;
  706 + border-radius: 16rpx;
  707 + overflow: hidden;
  708 + background: #333;
  709 +
  710 + .video {
  711 + width: 100%;
  712 + height: 100%;
  713 + }
  714 +
  715 + .play-overlay {
  716 + position: absolute;
  717 + top: 20rpx;
  718 + right: 20rpx;
  719 +
  720 + pointer-events: none;
  721 + }
  722 + }
  723 + }
  724 +
  725 + .memo-box {
  726 + background: #262626;
  727 + padding: 24rpx;
  728 + border-radius: 16rpx;
  729 + margin-bottom: 40rpx;
  730 +
  731 + .textarea {
  732 + height: 50rpx;
  733 +
  734 + :deep(.u-textarea--disabled) {
  735 + background-color: #262626;
  736 + }
  737 + }
  738 + }
  739 +
  740 + .steps-list {
  741 + display: flex;
  742 + flex-direction: column;
  743 + gap: 16rpx;
  744 + background: #262626;
  745 + padding: 20rpx;
  746 + box-sizing: border-box;
  747 + border-radius: 16rpx;
  748 +
  749 + .step-text {
  750 + font-size: 26rpx;
  751 + line-height: 1.6;
  752 + color: #bbb;
  753 + list-style: none;
  754 + }
  755 + }
  756 +
  757 + .muscle-map {
  758 + width: 100%;
  759 + border-radius: 16rpx;
  760 + }
  761 +
  762 + .legend {
  763 + display: flex;
  764 + justify-content: flex-end;
  765 + align-items: center;
  766 + gap: 20rpx;
  767 + margin-top: 20rpx;
  768 + }
  769 +
  770 + .legend-item {
  771 + display: flex;
  772 + align-items: center;
  773 + gap: 10rpx;
  774 + }
  775 +
  776 + .legend-color {
  777 + width: 16rpx;
  778 + height: 16rpx;
  779 + border-radius: 4rpx;
  780 + }
  781 +
  782 + .legend-color.primary {
  783 + background-color: #ffd700;
  784 + }
  785 +
  786 + .legend-color.secondary {
  787 + background-color: #999;
  788 + }
  789 +
  790 + .legend-text {
  791 + font-size: 24rpx;
  792 + color: #fff;
  793 + }
  794 +
  795 + // 历史记录样式
  796 + .stat-card {
  797 + background: linear-gradient(135deg, #333, #222);
  798 + padding: 40rpx;
  799 + border-radius: 20rpx;
  800 + display: flex;
  801 + flex-direction: column;
  802 + align-items: center;
  803 + border: 1rpx solid #444;
  804 +
  805 + .label {
  806 + font-size: 24rpx;
  807 + color: #888;
  808 + }
  809 +
  810 + .value {
  811 + font-size: 60rpx;
  812 + font-weight: bold;
  813 + color: #fedc1f;
  814 + margin: 10rpx 0;
  815 + }
  816 +
  817 + .date {
  818 + font-size: 22rpx;
  819 + color: #666;
  820 + }
  821 + }
  822 +
  823 + .chart-container {
  824 + height: 450rpx;
  825 + margin: 40rpx 0;
  826 + }
  827 +
  828 + .history-item {
  829 + background: #262626;
  830 + border-radius: 16rpx;
  831 + padding: 30rpx;
  832 + margin-bottom: 20rpx;
  833 +
  834 + .count {
  835 + display: flex;
  836 + gap: 10rpx;
  837 + }
  838 +
  839 + .item-header {
  840 + display: flex;
  841 + flex-direction: column;
  842 + justify-content: space-between;
  843 + margin-bottom: 20rpx;
  844 +
  845 + .date {
  846 + font-size: 24rpx;
  847 + color: #888;
  848 + }
  849 +
  850 + .tag {
  851 + // font-size: 20rpx;
  852 + // background: #444;
  853 + margin: 5rpx 5rpx;
  854 + border-radius: 4rpx;
  855 + }
  856 + }
  857 +
  858 + .group-chips {
  859 + display: flex;
  860 + flex-direction: column;
  861 + flex-wrap: wrap;
  862 + gap: 16rpx;
  863 + margin-top: 20rpx;
  864 +
  865 + .chip {
  866 + // background: #333;
  867 + padding: 8rpx 20rpx;
  868 + font-size: 24rpx;
  869 + display: flex;
  870 + align-items: center;
  871 + gap: 10rpx;
  872 +
  873 + .idx {
  874 + background: #444;
  875 + font-weight: bold;
  876 + border-radius: 30rpx;
  877 + padding: 4rpx 10rpx;
  878 + }
  879 + }
  880 + }
  881 + }
  882 +
  883 + // 平替动作样式
  884 + .sub-item {
  885 + display: flex;
  886 + align-items: center;
  887 + background: #262626;
  888 + padding: 24rpx;
  889 + border-radius: 16rpx;
  890 + margin-bottom: 20rpx;
  891 +
  892 + .img {
  893 + width: 120rpx;
  894 + height: 120rpx;
  895 + border-radius: 12rpx;
  896 + margin-right: 24rpx;
  897 + }
  898 +
  899 + .sub-info {
  900 + flex: 1;
  901 +
  902 + .name {
  903 + font-size: 30rpx;
  904 + font-weight: 500;
  905 + display: block;
  906 + }
  907 +
  908 + .meta {
  909 + font-size: 22rpx;
  910 + color: #666;
  911 + margin-top: 8rpx;
  912 + }
  913 + }
  914 + }
  915 + }
  916 + }
  917 +
  918 + .footer {
  919 + width: 100%;
  920 + display: flex;
  921 + justify-content: center;
  922 + align-items: center;
  923 + position: fixed;
  924 + bottom: 0;
  925 + height: 120rpx;
  926 + background-color: #242424;
  927 + z-index: 999;
  928 + .btn {
  929 + width: 80%;
  930 + height: 80rpx;
  931 + background-color: #fedc1f;
  932 + color: #333;
  933 + border-radius: 12rpx;
  934 + text-align: center;
  935 + line-height: 80rpx;
  936 + border-radius: 50rpx;
  937 + }
  938 + }
  939 + }
  940 +
  941 + // 动画
  942 + .slide-up {
  943 + animation: slideUp 0.4s ease-out;
  944 + }
  945 +
  946 + @keyframes slideUp {
  947 + from {
  948 + opacity: 0;
  949 + transform: translateY(20rpx);
  950 + }
  951 +
  952 + to {
  953 + opacity: 1;
  954 + transform: translateY(0);
  955 + }
  956 + }
  957 +</style>
  1 +<template>
  2 + <view class="container">
  3 + <!-- 顶部日期栏 -->
  4 + <view class="header-bar">
  5 + <!-- <view class="back-btn" @click="goBack">
  6 + <uni-icons class="back-arrow" type="left" size="24" color="#333"></uni-icons>
  7 + </view> -->
  8 + <view class="date-wrapper">
  9 + <text class="date-text">{{ displayDate }}</text>
  10 + </view>
  11 + <button class="date-note-btn" @click.stop="handleDateNote">日期备注</button>
  12 + <button class="go-train-btn" @click="handleGoTrain">
  13 + <text class="go-train-text">去训练</text>
  14 + <text class="go-train-tag">GO</text>
  15 + </button>
  16 + </view>
  17 +
  18 + <!-- 训练内容区域 -->
  19 + <view v-if="resdailyData.id" class="training-container">
  20 + <!-- 训练计划头部卡片 -->
  21 + <view class="plan-header-card">
  22 + <image class="plan-header-img"
  23 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
  24 + mode="aspectFill"></image>
  25 + <view class="plan-header-info">
  26 + <text class="plan-title">{{ resdailyData.name || '全能玩家DoubleDelight 05' }}</text>
  27 + <text class="plan-meta">{{ resdailyData.exerciseCount }}个动作 · {{ resdailyData.totalSets }}组 ·
  28 + {{ resdailyData.totalWeight }}kg</text>
  29 + </view>
  30 + <view class="plan-header-btns">
  31 + <button class="plan-btn more-btn" @click="handlePlanMore">更多</button>
  32 + <button class="plan-btn copy-btn" @click="handlePlanCopy">复制到</button>
  33 + <button class="plan-btn go-train-btn-small" @click="handleGoTrain">
  34 + <text class="go-train-text-sm">去训练</text>
  35 + <text class="go-train-tag-sm">GO</text>
  36 + </button>
  37 + </view>
  38 + </view>
  39 +
  40 + <!-- 训练动作列表 -->
  41 + <view class="action-list">
  42 + <view class="action-item" v-for="(item, index) in resdailyData.actionList" :key="index">
  43 + <text class="action-index">{{ index + 1 }}</text>
  44 + <image class="action-img" :src="item.img ||
  45 + 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
  46 + " mode="aspectFill"></image>
  47 + <view class="action-info">
  48 + <text class="action-name">{{ item.name }}</text>
  49 + <view class="action-sets">
  50 + <view class="set-item" v-for="(set, setIdx) in item.sets" :key="setIdx">
  51 + <text class="set-index">{{ setIdx + 1 }}</text>
  52 + <text class="set-content">{{ set.content }}</text>
  53 + <text class="rest-time" v-if="set.restTime">{{ set.restTime }}</text>
  54 + </view>
  55 + </view>
  56 + </view>
  57 + <button class="modify-btn" @click="handleModifyAction(item, index)">修改</button>
  58 + </view>
  59 + </view>
  60 + </view>
  61 +
  62 + <!-- 空状态区域 -->
  63 + <view v-else class="empty-section">
  64 + <image class="empty-img"
  65 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/empty-calendar.png"
  66 + mode="aspectFit"></image>
  67 + <text class="empty-tip">今天没有安排</text>
  68 + <button class="add-train-btn" @click="openAddTrainPopup">+ 添加自助训练</button>
  69 +
  70 + <!-- 底部课程类型卡片 -->
  71 + <view class="course-section">
  72 + <view class="course-item" v-for="(item, index) in courseList" :key="index"
  73 + @click="handleCourseClick(item.type)">
  74 + <image class="course-icon" :src="item.icon" mode="aspectFill"></image>
  75 + <text class="course-title">{{ item.title }}</text>
  76 + <text class="course-desc">{{ item.desc }}</text>
  77 + </view>
  78 + </view>
  79 + </view>
  80 +
  81 + <!-- 添加自助训练底部弹窗 -->
  82 + <view v-if="showAddTrainPopup" class="popup-overlay" @click="closeAddTrainPopup">
  83 + <view class="popup-bottom" @click.stop>
  84 + <!-- 顶部拖动条 -->
  85 + <view class="popup-indicator"></view>
  86 + <text class="popup-title">添加自助训练</text>
  87 + <view class="popup-options">
  88 + <button class="popup-option-btn" @click="handleAddFromPlan">
  89 + <text>从训练计划中添加</text>
  90 + </button>
  91 + <button class="popup-option-btn" @click="handleAddFromTemplate">
  92 + <text>使用训练模板</text>
  93 + </button>
  94 + <button class="popup-option-btn" @click="handleFreeTraining">
  95 + <text>自由训练</text>
  96 + </button>
  97 + </view>
  98 + </view>
  99 + </view>
  100 +
  101 + <!-- 日期备注弹窗 -->
  102 + <RiliRiqibeizhu v-model:visible="showRiqibeizhu" />
  103 + </view>
  104 +</template>
  105 +
  106 +<script setup>
  107 +import { ref, onMounted, watch } from 'vue';
  108 +import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
  109 +import RiliRiqibeizhu from '@/pages/xunji/components/rili-components/rili-riqibeizhu.vue'
  110 +
  111 +const props = defineProps({
  112 + date: {
  113 + type: String,
  114 + default: ''
  115 + }
  116 +})
  117 +
  118 +const showAddTrainPopup = ref(false);
  119 +const selectedDate = ref('');
  120 +const displayDate = ref('');
  121 +const resdailyData = ref({
  122 + id: '',
  123 + name: '',
  124 + exerciseCount: 0,
  125 + totalSets: 0,
  126 + totalWeight: 0,
  127 + actionList: []
  128 +});
  129 +
  130 +const showRiqibeizhu = ref(false)
  131 +
  132 +// 模拟接口返回的 data 数据
  133 +const mockTemplateData = [
  134 + {
  135 + id: 10001,
  136 + name: "上肢力量基础训练模板",
  137 + urlCover: "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png",
  138 + exerciseCount: 5,
  139 + totalSets: 15,
  140 + totalWeight: 625,
  141 + units: [
  142 + {
  143 + unitId: 20001,
  144 + unitName: "胸肌训练单元",
  145 + unitType: 1,
  146 + setCount: 3,
  147 + sortOrder: 1,
  148 + supersetId: null,
  149 + exercises: [
  150 + {
  151 + unitId: 20001,
  152 + exerciseId: 30001,
  153 + exerciseType: 1,
  154 + setCount: 3,
  155 + weight: 50,
  156 + reps: 12,
  157 + distance: null,
  158 + duration: null,
  159 + restTime: 60,
  160 + innerOrder: 1,
  161 + exerciseName: "杠铃卧推",
  162 + exerciseCover: "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png",
  163 + urlImage: "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png",
  164 + sets: [
  165 + {
  166 + setIndex: 1,
  167 + weight: 40,
  168 + reps: 12,
  169 + duration: null,
  170 + distance: null,
  171 + restTime: 60
  172 + },
  173 + {
  174 + setIndex: 2,
  175 + weight: 50,
  176 + reps: 10,
  177 + duration: null,
  178 + distance: null,
  179 + restTime: 60
  180 + },
  181 + {
  182 + setIndex: 3,
  183 + weight: 55,
  184 + reps: 8,
  185 + duration: null,
  186 + distance: null,
  187 + restTime: 90
  188 + }
  189 + ]
  190 + }
  191 + ]
  192 + }
  193 + ]
  194 + }
  195 +]
  196 +
  197 +// 没有训练模板时推荐课程
  198 +const courseList = ref([
  199 + {
  200 + type: 'group',
  201 + title: '团课',
  202 + desc: '大家一起练',
  203 + icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/group-course.png'
  204 + },
  205 + {
  206 + type: 'private',
  207 + title: '私教',
  208 + desc: '1对1专人指导',
  209 + icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/private-course.png'
  210 + },
  211 + {
  212 + type: 'smallClass',
  213 + title: '小班课',
  214 + desc: '28天极速瘦身',
  215 + icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/small-class.png'
  216 + }
  217 +]);
  218 +
  219 +// 把模拟数据 转成 页面能渲染的数据
  220 +const formatTemplateData = (data) => {
  221 + if (!data) return { actionList: [] };
  222 + // 模板的动作列表
  223 + const actionList = [];
  224 + // 循环 unit
  225 + (data.units || []).forEach(unit => {
  226 + // 循环每个动作
  227 + (unit.exercises || []).forEach(ex => {
  228 + actionList.push({
  229 + name: ex.exerciseName,
  230 + img: ex.exerciseCover,
  231 + sets: ex.sets.map(s => ({
  232 + content: `${s.weight}kg × ${s.reps}次`,
  233 + restTime: s.restTime ? `${s.restTime}s` : ''
  234 + }))
  235 + });
  236 + });
  237 + });
  238 + return {
  239 + id: data.id,
  240 + name: data.name,
  241 + exerciseCount: data.exerciseCount,
  242 + totalSets: data.totalSets,
  243 + totalWeight: data.totalWeight,
  244 + actionList
  245 + };
  246 +};
  247 +
  248 +// 加载每日训练模板数据
  249 +const loaddailytemplate = async () => {
  250 + if (!selectedDate.value) return; // 没有日期就不请求
  251 + console.log('【子组件】开始加载数据...');
  252 + try {
  253 + // const resdaily = await dailytemplateApi.getdailytemplate(String(selectedDate.value));
  254 + // resdailyData.value = resdaily.data || {};
  255 + resdailyData.value = formatTemplateData(mockTemplateData[0]);
  256 + console.log('【子组件】加载完成,resdailyData:', resdailyData.value);
  257 + console.log('打印resdailyData', resdailyData.value)
  258 + } catch (error) {
  259 + console.error('加载训练模板失败:', error);
  260 + resdailyData.value = {};
  261 + }
  262 +};
  263 +
  264 +// 弹窗控制
  265 +const openAddTrainPopup = () => {
  266 + showAddTrainPopup.value = true;
  267 +};
  268 +const closeAddTrainPopup = () => {
  269 + showAddTrainPopup.value = false;
  270 +};
  271 +
  272 +// 弹窗选项点击事件
  273 +const handleAddFromPlan = () => {
  274 + uni.showToast({ title: '从训练计划添加', icon: 'none' });
  275 + closeAddTrainPopup();
  276 +};
  277 +const handleAddFromTemplate = () => {
  278 + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' });
  279 + closeAddTrainPopup();
  280 +};
  281 +const handleFreeTraining = () => {
  282 + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' });
  283 + closeAddTrainPopup();
  284 +};
  285 +
  286 +// 返回按钮逻辑
  287 +const goBack = () => {
  288 + uni.navigateBack({
  289 + delta: 1,
  290 + fail: () => {
  291 + uni.redirectTo({ url: '/pages/xunji/xunji-rili' });
  292 + },
  293 + });
  294 +};
  295 +
  296 +// 日期格式化(兼容iOS)
  297 +const formatDateWithWeek = (dateStr) => {
  298 + if (!dateStr) return '';
  299 + const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
  300 + const iosSafeDate = new Date(dateStr.replace(/-/g, '/'));
  301 + const year = iosSafeDate.getFullYear();
  302 + const month = String(iosSafeDate.getMonth() + 1).padStart(2, '0');
  303 + const day = String(iosSafeDate.getDate()).padStart(2, '0');
  304 + const week = weekMap[iosSafeDate.getDay()];
  305 + return `${year}/${month}/${day} 周${week}`;
  306 +};
  307 +
  308 +// 打开日期备注组件
  309 +const handleDateNote = () => {
  310 + showRiqibeizhu.value = true
  311 +}
  312 +
  313 +// 监听父组件传进来的 date 变化
  314 +// 子组件 script setup 中替换 watch
  315 +watch(
  316 + () => props.date,
  317 + (newDate) => {
  318 + if (newDate && newDate.trim()) {
  319 + console.log('子组件监听到日期变化:', newDate);
  320 + selectedDate.value = newDate;
  321 + displayDate.value = formatDateWithWeek(newDate);
  322 + loaddailytemplate();
  323 + }
  324 + },
  325 + { immediate: true }
  326 +);
  327 +
  328 +onMounted(() => {
  329 + console.log('【子组件】已挂载!!!');
  330 + console.log('【子组件】props.date:', props.date);
  331 +});
  332 +</script>
  333 +
  334 +<style scoped>
  335 +/* 全局容器 */
  336 +.container {
  337 + width: 100vw;
  338 + min-height: 85vh;
  339 + background-color: #f7f8fa;
  340 + box-sizing: border-box;
  341 + margin: 0;
  342 + padding: 0;
  343 + overflow-x: hidden;
  344 +}
  345 +
  346 +/* 顶部日期栏 */
  347 +.header-bar {
  348 + display: flex;
  349 + align-items: center;
  350 + justify-content: space-between;
  351 + padding: 24rpx 20rpx;
  352 + background-color: #fff;
  353 + position: sticky;
  354 + top: 0;
  355 + z-index: 10;
  356 + gap: 16rpx;
  357 +}
  358 +
  359 +.back-btn {
  360 + width: 60rpx;
  361 + height: 60rpx;
  362 + display: flex;
  363 + align-items: center;
  364 + justify-content: center;
  365 + flex-shrink: 0;
  366 +}
  367 +
  368 +.date-wrapper {
  369 + flex-grow: 1;
  370 + text-align: left;
  371 +}
  372 +
  373 +.date-text {
  374 + font-size: 28rpx;
  375 +}
  376 +
  377 +.date-note-btn {
  378 + font-size: 28rpx;
  379 + color: #333;
  380 + background: #f2f3f5;
  381 + border-radius: 40rpx;
  382 + padding: 12rpx 28rpx;
  383 + border: none;
  384 + margin-right: 0;
  385 + flex-shrink: 0;
  386 +}
  387 +
  388 +.go-train-btn {
  389 + display: flex;
  390 + align-items: center;
  391 + background: #ffc107;
  392 + border-radius: 40rpx;
  393 + padding: 12rpx 24rpx;
  394 + border: none;
  395 + flex-shrink: 0;
  396 +}
  397 +
  398 +.go-train-text {
  399 + font-size: 28rpx;
  400 + color: #000;
  401 + margin-right: 8rpx;
  402 + font-weight: 500;
  403 +}
  404 +
  405 +.go-train-tag {
  406 + font-size: 22rpx;
  407 + color: #ffc107;
  408 + background: #000;
  409 + border-radius: 50%;
  410 + padding: 4rpx 8rpx;
  411 + font-weight: bold;
  412 +}
  413 +
  414 +/* 训练计划头部卡片 */
  415 +.plan-header-card {
  416 + display: flex;
  417 + flex-direction: column;
  418 + background-color: #2a2a2a;
  419 + color: #fff;
  420 + border-radius: 16rpx;
  421 + padding: 32rpx 24rpx;
  422 + margin: 20rpx;
  423 + margin-bottom: 24rpx;
  424 +}
  425 +
  426 +.plan-header-img {
  427 + display: none;
  428 +}
  429 +
  430 +.plan-header-info {
  431 + flex-grow: 1;
  432 + margin-bottom: 32rpx;
  433 +}
  434 +
  435 +.plan-title {
  436 + font-size: 36rpx;
  437 + font-weight: bold;
  438 + display: block;
  439 + margin-bottom: 12rpx;
  440 + color: #fff;
  441 +}
  442 +
  443 +.plan-meta {
  444 + font-size: 26rpx;
  445 + color: #ccc;
  446 + display: block;
  447 +}
  448 +
  449 +.plan-header-btns {
  450 + display: flex;
  451 + flex-direction: row;
  452 + align-items: center;
  453 + gap: 20rpx;
  454 + justify-content: flex-start;
  455 +}
  456 +
  457 +.plan-btn {
  458 + font-size: 26rpx;
  459 + border-radius: 40rpx;
  460 + border: none;
  461 + padding: 12rpx 28rpx;
  462 + line-height: 1;
  463 +}
  464 +
  465 +.more-btn,
  466 +.copy-btn {
  467 + background: #fff;
  468 + color: #000;
  469 + border: none;
  470 +}
  471 +
  472 +.go-train-btn-small {
  473 + background: #ffc107;
  474 + color: #000;
  475 + display: flex;
  476 + align-items: center;
  477 + padding: 12rpx 20rpx;
  478 +}
  479 +
  480 +.go-train-text-sm {
  481 + font-size: 24rpx;
  482 + margin-right: 4rpx;
  483 + font-weight: 500;
  484 +}
  485 +
  486 +.go-train-tag-sm {
  487 + font-size: 20rpx;
  488 + background: #000;
  489 + color: #ffc107;
  490 + border-radius: 4rpx;
  491 + padding: 2rpx 6rpx;
  492 + font-weight: bold;
  493 +}
  494 +
  495 +/* 训练动作列表 */
  496 +.action-list {
  497 + display: flex;
  498 + flex-direction: column;
  499 + gap: 16rpx;
  500 + padding: 0 20rpx;
  501 +}
  502 +
  503 +.action-item {
  504 + display: flex;
  505 + align-items: flex-start;
  506 + background-color: #fff;
  507 + border-radius: 12rpx;
  508 + padding: 24rpx 20rpx;
  509 + position: relative;
  510 + gap: 16rpx;
  511 +}
  512 +
  513 +.action-index {
  514 + font-size: 34rpx;
  515 + color: #333;
  516 + font-weight: 600;
  517 + line-height: 80rpx;
  518 + width: 40rpx;
  519 + text-align: center;
  520 + flex-shrink: 0;
  521 +}
  522 +
  523 +.action-img {
  524 + width: 80rpx;
  525 + height: 80rpx;
  526 + border-radius: 8rpx;
  527 + flex-shrink: 0;
  528 +}
  529 +
  530 +.action-info {
  531 + flex-grow: 1;
  532 + display: flex;
  533 + flex-direction: column;
  534 + justify-content: center;
  535 +}
  536 +
  537 +.action-name {
  538 + font-size: 32rpx;
  539 + color: #111;
  540 + font-weight: 500;
  541 + display: block;
  542 + margin-bottom: 8rpx;
  543 +}
  544 +
  545 +.action-sets {
  546 + display: flex;
  547 + flex-direction: column;
  548 + gap: 12rpx;
  549 +}
  550 +
  551 +.set-item {
  552 + display: flex;
  553 + align-items: center;
  554 + justify-content: space-between;
  555 + font-size: 26rpx;
  556 + color: #666;
  557 + padding-left: 8rpx;
  558 +}
  559 +
  560 +.set-index {
  561 + display: none;
  562 +}
  563 +
  564 +.set-content {
  565 + flex-grow: 1;
  566 + color: #333;
  567 +}
  568 +
  569 +.set-content::before {
  570 + content: "热";
  571 + display: inline-block;
  572 + font-size: 22rpx;
  573 + color: #999;
  574 + background: #f2f3f5;
  575 + border-radius: 50%;
  576 + width: 32rpx;
  577 + height: 32rpx;
  578 + line-height: 32rpx;
  579 + text-align: center;
  580 + margin-right: 12rpx;
  581 +}
  582 +
  583 +.rest-time {
  584 + font-size: 24rpx;
  585 + color: #999;
  586 + margin-left: 16rpx;
  587 + flex-shrink: 0;
  588 +}
  589 +
  590 +.modify-btn {
  591 + position: absolute;
  592 + top: 24rpx;
  593 + right: 20rpx;
  594 + font-size: 26rpx;
  595 + color: #333;
  596 + background: #f2f3f5;
  597 + border: none;
  598 + border-radius: 40rpx;
  599 + padding: 8rpx 20rpx;
  600 + line-height: 1;
  601 +}
  602 +
  603 +/* 空状态区域 */
  604 +.empty-section {
  605 + display: flex;
  606 + flex-direction: column;
  607 + align-items: center;
  608 + padding: 100rpx 20rpx 60rpx;
  609 +}
  610 +
  611 +.empty-img {
  612 + width: 280rpx;
  613 + height: 280rpx;
  614 + margin-bottom: 30rpx;
  615 +}
  616 +
  617 +.empty-tip {
  618 + font-size: 32rpx;
  619 + color: #666;
  620 + margin-bottom: 60rpx;
  621 +}
  622 +
  623 +.add-train-btn {
  624 + font-size: 30rpx;
  625 + color: #000;
  626 + background: #ffc107;
  627 + border-radius: 40rpx;
  628 + padding: 20rpx 80rpx;
  629 + border: none;
  630 + margin-bottom: 60rpx;
  631 +}
  632 +
  633 +/* 底部课程类型区域 */
  634 +.course-section {
  635 + display: flex;
  636 + justify-content: space-around;
  637 + padding: 0 20rpx;
  638 + width: 100%;
  639 + box-sizing: border-box;
  640 +}
  641 +
  642 +.course-item {
  643 + display: flex;
  644 + flex-direction: column;
  645 + align-items: center;
  646 + width: 220rpx;
  647 + background: #fff;
  648 + border-radius: 12rpx;
  649 + padding: 30rpx 0;
  650 +}
  651 +
  652 +.course-icon {
  653 + width: 100rpx;
  654 + height: 100rpx;
  655 + border-radius: 50%;
  656 + margin-bottom: 16rpx;
  657 +}
  658 +
  659 +.course-title {
  660 + font-size: 28rpx;
  661 + color: #333;
  662 + margin-bottom: 8rpx;
  663 + font-weight: 500;
  664 +}
  665 +
  666 +.course-desc {
  667 + font-size: 24rpx;
  668 + color: #999;
  669 +}
  670 +
  671 +/* 添加自助训练底部弹窗 */
  672 +.popup-overlay {
  673 + position: fixed;
  674 + top: 0;
  675 + left: 0;
  676 + width: 100%;
  677 + height: 100%;
  678 + background: rgba(0, 0, 0, 0.4);
  679 + z-index: 9999;
  680 + display: flex;
  681 + align-items: flex-end;
  682 +}
  683 +
  684 +.popup-bottom {
  685 + width: 100%;
  686 + background: #fff;
  687 + border-radius: 24rpx 24rpx 0 0;
  688 + padding: 30rpx;
  689 + box-sizing: border-box;
  690 +}
  691 +
  692 +.popup-indicator {
  693 + width: 80rpx;
  694 + height: 8rpx;
  695 + background: #ddd;
  696 + border-radius: 4rpx;
  697 + margin: 0 auto 30rpx;
  698 +}
  699 +
  700 +.popup-title {
  701 + font-size: 34rpx;
  702 + color: #333;
  703 + text-align: center;
  704 + font-weight: bold;
  705 + display: block;
  706 + margin-bottom: 30rpx;
  707 +}
  708 +
  709 +.popup-options {
  710 + display: flex;
  711 + flex-direction: column;
  712 + gap: 20rpx;
  713 +}
  714 +
  715 +.popup-option-btn {
  716 + width: 100%;
  717 + height: 80rpx;
  718 + font-size: 30rpx;
  719 + color: #333;
  720 + background: #f5f5f5;
  721 + border: none;
  722 + border-radius: 12rpx;
  723 + display: flex;
  724 + align-items: center;
  725 + justify-content: center;
  726 +}
  727 +
  728 +/* 按钮通用重置 */
  729 +button {
  730 + line-height: 1;
  731 +}
  732 +
  733 +button::after {
  734 + border: none;
  735 +}
  736 +</style>
  1 +<template>
  2 + <u-popup :show="visible" mode="bottom" :round="24" :closeable="false" :custom-style="{ height: '80vh' }"
  3 + @close="handleClose">
  4 + <view class="date-note-popup">
  5 + <!-- 顶部导航栏 -->
  6 + <view class="popup-header">
  7 + <view class="close-btn" @click="handleClose">
  8 + <text class="close-icon">×</text>
  9 + </view>
  10 + <text class="popup-title">新增日程备注</text>
  11 + <button class="save-btn" @click="handleSave">保存</button>
  12 + </view>
  13 +
  14 + <!-- 颜色选择区 -->
  15 + <view class="color-section">
  16 + <text class="section-title">选择显示颜色</text>
  17 + <view class="color-list">
  18 + <view v-for="(color, index) in colorList" :key="index" class="color-item"
  19 + :class="{ active: selectedColorIndex === index }" :style="{ backgroundColor: color }"
  20 + @click="selectedColorIndex = index" />
  21 + </view>
  22 + </view>
  23 +
  24 + <!-- 输入框区域 -->
  25 + <view class="input-section">
  26 + <text class="section-title">日程标题</text>
  27 + <textarea v-model="noteContent" class="note-textarea" placeholder="输入当天的日程情况,如休息日、生病受伤了、经期等"
  28 + placeholder-class="placeholder" />
  29 + </view>
  30 +
  31 + <!-- 历史备注区域 -->
  32 + <view class="history-section">
  33 + <view class="history-header">
  34 + <text class="section-title history-title">历史备注</text>
  35 + <text class="history-tip">点击填充到文本内容</text>
  36 + </view>
  37 + <view class="history-tags">
  38 + <view v-for="(tag, index) in historyTags" :key="index" class="tag-item"
  39 + :style="{ backgroundColor: tag.color }" @click="fillNote(tag.text)">
  40 + <text class="tag-text">+{{ tag.text }}</text>
  41 + </view>
  42 + </view>
  43 + </view>
  44 + </view>
  45 + </u-popup>
  46 +</template>
  47 +
  48 +<script setup>
  49 +import { ref } from 'vue';
  50 +
  51 +const props = defineProps({
  52 + visible: {
  53 + type: Boolean,
  54 + default: false
  55 + }
  56 +});
  57 +
  58 +const emit = defineEmits(['update:visible', 'save', 'close']);
  59 +
  60 +// 响应式数据
  61 +const noteContent = ref('');
  62 +const selectedColorIndex = ref(0);
  63 +
  64 +// 颜色列表(和截图一致)
  65 +const colorList = ref([
  66 + '#ff9944',
  67 + '#ff7766',
  68 + '#ddaaff',
  69 + '#aabbff',
  70 + '#cccccc'
  71 +]);
  72 +
  73 +// 历史备注数据(模拟截图效果)
  74 +const historyTags = ref([
  75 + { text: '休息日', color: '#ff9944' },
  76 + { text: '累了', color: '#ff9944' },
  77 +]);
  78 +
  79 +// 事件处理
  80 +const handleClose = () => {
  81 + emit('update:visible', false);
  82 + emit('close');
  83 +};
  84 +
  85 +const handleSave = () => {
  86 + const data = {
  87 + content: noteContent.value,
  88 + color: colorList.value[selectedColorIndex.value]
  89 + };
  90 + emit('save', data);
  91 + handleClose();
  92 +};
  93 +
  94 +const fillNote = (text) => {
  95 + noteContent.value = text;
  96 +};
  97 +</script>
  98 +
  99 +<style scoped lang="scss">
  100 +.date-note-popup {
  101 + width: 100%;
  102 + height: 100%;
  103 + background-color: #fff;
  104 + box-sizing: border-box;
  105 + padding: 24rpx 32rpx;
  106 +}
  107 +
  108 +/* 顶部导航 */
  109 +.popup-header {
  110 + display: flex;
  111 + align-items: center;
  112 + justify-content: space-between;
  113 + margin-bottom: 40rpx;
  114 +
  115 + .close-btn {
  116 + width: 48rpx;
  117 + height: 48rpx;
  118 + display: flex;
  119 + align-items: center;
  120 + justify-content: center;
  121 +
  122 + .close-icon {
  123 + font-size: 40rpx;
  124 + color: #333;
  125 + line-height: 1;
  126 + }
  127 + }
  128 +
  129 + .popup-title {
  130 + font-size: 34rpx;
  131 + font-weight: 600;
  132 + color: #111;
  133 + text-align: center;
  134 + display: block;
  135 + flex: 1;
  136 + }
  137 +
  138 + .save-btn {
  139 + margin-right: 0;
  140 + background: #ffd100;
  141 + color: #000;
  142 + border: none;
  143 + border-radius: 20rpx;
  144 + padding: 5rpx 25rpx;
  145 + font-size: 28rpx;
  146 + font-weight: 500;
  147 + }
  148 +}
  149 +
  150 +/* 通用标题 */
  151 +.section-title {
  152 + font-size: 30rpx;
  153 + color: #111;
  154 + font-weight: 500;
  155 + display: block;
  156 + margin-bottom: 24rpx;
  157 +}
  158 +
  159 +/* 颜色选择区 */
  160 +.color-section {
  161 + margin-bottom: 40rpx;
  162 +
  163 + .color-list {
  164 + display: flex;
  165 + gap: 40rpx;
  166 +
  167 + .color-item {
  168 + width: 80rpx;
  169 + height: 80rpx;
  170 + border-radius: 12rpx;
  171 + border: 4rpx solid transparent;
  172 +
  173 + &.active {
  174 + border-color: #333;
  175 + }
  176 + }
  177 + }
  178 +}
  179 +
  180 +/* 输入框区域 */
  181 +.input-section {
  182 + margin-bottom: 40rpx;
  183 +
  184 + .note-textarea {
  185 + width: 100%;
  186 + height: 200rpx;
  187 + background: #f7f8fa;
  188 + border-radius: 12rpx;
  189 + padding: 20rpx;
  190 + font-size: 28rpx;
  191 + color: #333;
  192 + line-height: 1.5;
  193 + box-sizing: border-box;
  194 +
  195 + .placeholder {
  196 + color: #999;
  197 + }
  198 + }
  199 +}
  200 +
  201 +.history-title {
  202 + margin-bottom: 0 !important;
  203 + /* 覆盖原来的 margin-bottom */
  204 +}
  205 +
  206 +/* 历史备注区域 */
  207 +.history-section {
  208 + .history-header {
  209 + display: flex;
  210 + align-items: center;
  211 + gap: 16rpx;
  212 + margin-bottom: 24rpx;
  213 + margin-top: 0;
  214 +
  215 + .history-tip {
  216 + font-size: 24rpx;
  217 + color: #999;
  218 + }
  219 + }
  220 +
  221 + .history-tags {
  222 + display: flex;
  223 + flex-wrap: wrap;
  224 + gap: 20rpx;
  225 +
  226 + .tag-item {
  227 + border-radius: 8rpx;
  228 + padding: 16rpx 24rpx;
  229 + display: flex;
  230 + align-items: center;
  231 +
  232 + .tag-text {
  233 + font-size: 26rpx;
  234 + color: #fff;
  235 + }
  236 + }
  237 + }
  238 +}
  239 +</style>
  1 +<template>
  2 + <view class="time-select">
  3 + <view class="tab-container">
  4 + <view
  5 + class="tab-item"
  6 + :class="{ active: currentDateType === 'week' }"
  7 + @click="switchDateType('week')"
  8 + >
  9 +
  10 + </view>
  11 + <view
  12 + class="tab-item"
  13 + :class="{ active: currentDateType === 'month' }"
  14 + @click="switchDateType('month')"
  15 + >
  16 +
  17 + </view>
  18 + </view>
  19 +
  20 + <view class="date-stepper">
  21 + <view class="arrow-icon" @click="handlePrev">
  22 + <up-icon name="arrow-left" color="#333" size="12" bold></up-icon>
  23 + </view>
  24 + <view class="date-display"> {{ dateRangeText }} </view>
  25 + <!-- 修改: 根据 isNextDisabled 判断是否隐藏下一个按钮 -->
  26 + <view class="arrow-icon" @click="handleNext" v-if="!isNextDisabled">
  27 + <up-icon name="arrow-right" color="#333" size="12" bold></up-icon>
  28 + </view>
  29 + <view v-else class="occupy"></view>
  30 + </view>
  31 + </view>
  32 +</template>
  33 +
  34 +<script setup>
  35 + import { ref, computed } from 'vue';
  36 +
  37 + // 当前日期类型: 'week' | 'month'
  38 + const currentDateType = ref('week');
  39 + // 基准日期,用于计算范围
  40 + const baseDate = ref(new Date());
  41 +
  42 + // 格式化日期辅助函数 YYYY/MM/DD
  43 + const formatDate = (date) => {
  44 + const y = date.getFullYear();
  45 + const m = String(date.getMonth() + 1).padStart(2, '0');
  46 + const d = String(date.getDate()).padStart(2, '0');
  47 + return `${y}/${m}/${d}`;
  48 + };
  49 +
  50 + // 计算显示的日期范围文本
  51 + const dateRangeText = computed(() => {
  52 + const current = new Date(baseDate.value);
  53 +
  54 + if (currentDateType.value === 'week') {
  55 + // 计算本周的周一和周日
  56 + const day = current.getDay();
  57 + const diffToMonday = day === 0 ? -6 : 1 - day; // 调整到周一
  58 +
  59 + const startOfWeek = new Date(current);
  60 + startOfWeek.setDate(current.getDate() + diffToMonday);
  61 +
  62 + const endOfWeek = new Date(startOfWeek);
  63 + endOfWeek.setDate(startOfWeek.getDate() + 6);
  64 +
  65 + return `${formatDate(startOfWeek)} - ${formatDate(endOfWeek)}`;
  66 + } else {
  67 + // 计算本月的一号和最后一天
  68 + const startOfMonth = new Date(current.getFullYear(), current.getMonth(), 1);
  69 + const endOfMonth = new Date(current.getFullYear(), current.getMonth() + 1, 0);
  70 +
  71 + return `${formatDate(startOfMonth)} - ${formatDate(endOfMonth)}`;
  72 + }
  73 + });
  74 +
  75 + // 新增: 计算下一个日期是否不可用(即是否为未来日期)
  76 + const isNextDisabled = computed(() => {
  77 + const newDate = new Date(baseDate.value);
  78 + if (currentDateType.value === 'week') {
  79 + newDate.setDate(newDate.getDate() + 7);
  80 + } else {
  81 + newDate.setMonth(newDate.getMonth() + 1);
  82 + }
  83 +
  84 + // 重置时分秒进行比较
  85 + const now = new Date();
  86 + now.setHours(0, 0, 0, 0);
  87 + const checkDate = new Date(newDate);
  88 + checkDate.setHours(0, 0, 0, 0);
  89 +
  90 + return checkDate > now;
  91 + });
  92 +
  93 + // 切换周/月
  94 + const switchDateType = (type) => {
  95 + currentDateType.value = type;
  96 + };
  97 +
  98 + // 上一段时间
  99 + const handlePrev = () => {
  100 + const newDate = new Date(baseDate.value);
  101 + if (currentDateType.value === 'week') {
  102 + newDate.setDate(newDate.getDate() - 7);
  103 + } else {
  104 + newDate.setMonth(newDate.getMonth() - 1);
  105 + }
  106 + baseDate.value = newDate;
  107 + };
  108 +
  109 + // 下一段时间
  110 + const handleNext = () => {
  111 + // 虽然按钮已隐藏,但保留逻辑判断以防直接调用或其他边界情况
  112 + if (isNextDisabled.value) {
  113 + return;
  114 + }
  115 +
  116 + const newDate = new Date(baseDate.value);
  117 + if (currentDateType.value === 'week') {
  118 + newDate.setDate(newDate.getDate() + 7);
  119 + } else {
  120 + newDate.setMonth(newDate.getMonth() + 1);
  121 + }
  122 +
  123 + baseDate.value = newDate;
  124 + };
  125 +</script>
  126 +
  127 +<style scoped lang="scss">
  128 + .time-select {
  129 + flex-shrink: 0; /* 修改: 防止顶部选择器被压缩 */
  130 + width: 100%;
  131 + display: flex;
  132 + flex-direction: column;
  133 + align-items: center;
  134 +
  135 + // 周月切换 Tab
  136 + .tab-container {
  137 + width: 100%;
  138 + height: 72rpx;
  139 + display: grid;
  140 + grid-template-columns: 1fr 1fr;
  141 + gap: 8rpx;
  142 + background-color: #eee;
  143 + border-radius: 12rpx;
  144 + padding: 6rpx;
  145 + box-sizing: border-box;
  146 +
  147 + .tab-item {
  148 + display: flex;
  149 + align-items: center;
  150 + justify-content: center;
  151 + color: #8c8c8c;
  152 + font-size: 28rpx;
  153 + transition: all 0.2s ease;
  154 +
  155 + &.active {
  156 + background-color: #ffffff;
  157 + color: #1a1a1a;
  158 + font-weight: 600;
  159 + border-radius: 8rpx;
  160 + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  161 + }
  162 + }
  163 + }
  164 + // 日期翻页器
  165 + .date-stepper {
  166 + margin-top: 32rpx;
  167 + display: flex;
  168 + align-items: center;
  169 + justify-content: space-between;
  170 + width: 100%;
  171 +
  172 + .date-display {
  173 + margin: 0 40rpx;
  174 + font-size: 28rpx;
  175 + font-weight: 500;
  176 + color: #333;
  177 + font-variant-numeric: tabular-nums; // 防止数字切换时抖动
  178 + }
  179 +
  180 + .arrow-icon {
  181 + width: 56rpx;
  182 + height: 56rpx;
  183 + display: flex;
  184 + align-items: center;
  185 + justify-content: center;
  186 + background-color: #fff;
  187 + border-radius: 50%;
  188 + transition: background-color 0.2s;
  189 + }
  190 + }
  191 + .occupy {
  192 + width: 56rpx;
  193 + height: 56rpx;
  194 + }
  195 + }
  196 +</style>
  1 +<template>
  2 + <view class="exercise-page" @click="closeAddMenu">
  3 + <!-- 搜索框 -->
  4 + <view class="search-bar">
  5 + <!-- <view class="search">
  6 + <uni-icons type="search" size="18"></uni-icons>
  7 + <input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" />
  8 + </view> -->
  9 + <view class="add-btn" @click.stop="addExercise">
  10 + <uni-icons type="plus" size="35" color="#333"></uni-icons>
  11 + <view class="floating-menu" v-show="addshow" @click.stop>
  12 + <view class="menu-item">
  13 + <uni-icons type="plus" size="24" color="#333"></uni-icons>
  14 + <text class="menu-text" @click="addnewmotion">新增动作</text>
  15 + </view>
  16 + <view class="menu-item">
  17 + <uni-icons type="link" size="24" color="#333"></uni-icons>
  18 + <text class="menu-text" @click="addnewsupersets">新增超级组</text>
  19 + </view>
  20 + <view class="menu-item">
  21 + <uni-icons type="list" size="24" color="#333"></uni-icons>
  22 + <text class="menu-text" @click="actionmanagement">动作目录管理</text>
  23 + </view>
  24 + </view>
  25 + </view>
  26 + </view>
  27 +
  28 + <!-- 浮动菜单 -->
  29 +
  30 + <!-- 左右布局 -->
  31 + <view class="layout-container">
  32 + <scroll-view scroll-y class="left-nav" enable-flex>
  33 + <view
  34 + class="nav-item"
  35 + @click="handleCollectClick('collect')"
  36 + :class="{ active: activeNav === 'collect' }"
  37 + >
  38 + 收藏
  39 + </view>
  40 + <view
  41 + v-for="nav in navItems"
  42 + :key="nav.id"
  43 + class="nav-item"
  44 + :class="{ active: activeNav === nav.id }"
  45 + @click="switchNav(nav.id)"
  46 + >
  47 + {{ nav.name }}
  48 + </view>
  49 + </scroll-view>
  50 +
  51 + <!-- 小动作列表 -->
  52 + <scroll-view scroll-y class="right-content" enable-flex>
  53 + <view class="tip" v-if="motionPart.length > 0">
  54 + <view
  55 + class="item"
  56 + @click="handlePartClick('')"
  57 + :class="{ active: activeMotionPart == '' }"
  58 + >全部</view
  59 + >
  60 + <view
  61 + class="item"
  62 + v-for="item in motionPart"
  63 + :key="item.id"
  64 + :class="{ active: activeMotionPart == item.id }"
  65 + @click="handlePartClick(item.id)"
  66 + >
  67 + {{ item.name }}
  68 + </view>
  69 + </view>
  70 +
  71 + <view class="exercise-grid">
  72 + <view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0">
  73 + <view class="equipment-list">
  74 + <view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
  75 + <view class="equipment-name"> {{ item.equipmentName }} </view>
  76 + <view class="action-list">
  77 + <view class="action-item" v-for="e in item.exercises" :key="e.id">
  78 + <image
  79 + :src="e.url3dAnimation"
  80 + mode="aspectFill"
  81 + lazy-load
  82 + class="action-img"
  83 + @click="goToDetail(e.id, 1)"
  84 + ></image>
  85 + <view class="action-name">{{ e.name }}</view>
  86 + <view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view>
  87 + </view>
  88 + </view>
  89 + </view>
  90 +
  91 + <view class="supers" v-if="superGroupInfo.length > 0">
  92 + <view class="supers-name"> 超级组 </view>
  93 + <view
  94 + class="super"
  95 + v-for="item in superGroupInfo"
  96 + :key="item.id"
  97 + @click="goToDetail(item.id, 2)"
  98 + >
  99 + <image
  100 + src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
  101 + mode="aspectFill"
  102 + lazy-load
  103 + class="super-img"
  104 + ></image>
  105 + <view class="right">
  106 + <view class="super-name">{{ item.name }}</view>
  107 + <view class="parts">{{ item.primaryMuscles.join() }}</view>
  108 + </view>
  109 + </view>
  110 + </view>
  111 +
  112 + <!-- <view class="add-super">
  113 + <view class="header">
  114 + <up-icon name="plus" color="#000" size="15" bold></up-icon>
  115 + <text>添加自定义动作</text>
  116 + </view>
  117 + <view class="tip">添加官方库没有的动作</view>
  118 + </view> -->
  119 + </view>
  120 + </view>
  121 +
  122 + <!-- 空状态 -->
  123 + <view class="empty-state" v-else>
  124 + <text class="empty-text">暂无动作数据</text>
  125 + </view>
  126 + </view>
  127 + </scroll-view>
  128 + </view>
  129 +
  130 + <!-- 详情弹窗 -->
  131 + <dongzuoXianqing ref="actionDetailRef" />
  132 + </view>
  133 +</template>
  134 +
  135 +<script setup>
  136 + import { ref, onMounted, nextTick } from 'vue';
  137 + import ExercisesApi from '@/sheep/api/motion/exercises';
  138 + import SupersetsApi from '@/sheep/api/motion/supersets';
  139 + import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue';
  140 + import { useActionStore } from '@/sheep/store/action';
  141 +
  142 + const actionStore = useActionStore();
  143 +
  144 + // 响应式数据
  145 + const activeNav = ref('');
  146 + const navItems = ref([]);
  147 + const exercises = ref([]);
  148 + const superGroupInfo = ref([]);
  149 + const motionPart = ref([]);
  150 + const activeMotionPart = ref('');
  151 + const currentCategoryId = ref('');
  152 +
  153 + const allExercises = ref([]);
  154 +
  155 + const addshow = ref(false);
  156 +
  157 + const superFavoriteList = ref([]);
  158 +
  159 + const actionDetailRef = ref(null);
  160 +
  161 + // 关闭菜单
  162 + const closeAddMenu = () => {
  163 + addshow.value = false;
  164 + };
  165 +
  166 + // 部位筛选
  167 + const handlePartClick = async (id) => {
  168 + try {
  169 + activeMotionPart.value = id;
  170 + const exerciseRes = await ExercisesApi.getexercises({
  171 + categoriesId: activeNav.value,
  172 + subCategoriesId: id,
  173 + });
  174 + exercises.value = exerciseRes.data || [];
  175 + } catch (e) {
  176 + console.error('加载动作失败:', e);
  177 + }
  178 + };
  179 +
  180 + // 收藏
  181 + const handleCollectClick = async (id) => {
  182 + activeNav.value = id;
  183 + loadCollectList();
  184 + loadSuperFavoriteList();
  185 + };
  186 + // 动作收藏
  187 + const loadCollectList = async () => {
  188 + try {
  189 + const res = await ExercisesApi.getFavoriteExercises();
  190 + exercises.value = res.data || [];
  191 + } catch (err) {
  192 + console.error('加载动作收藏失败', err);
  193 + }
  194 + };
  195 + // 超级组收藏
  196 + const loadSuperFavoriteList = async () => {
  197 + try {
  198 + const res = await SupersetsApi.getFavoriteSuperset();
  199 + superGroupInfo.value = res.data || [];
  200 + } catch (err) {
  201 + superFavoriteList.value = [];
  202 + console.error('加载超级组收藏失败', err);
  203 + }
  204 + };
  205 +
  206 + // 加载分类
  207 + const loadCategories = async () => {
  208 + try {
  209 + await actionStore.getloadCategories();
  210 + navItems.value = actionStore.showCategories;
  211 +
  212 + if (navItems.value.length > 0) {
  213 + switchNav(navItems.value[0].id);
  214 + }
  215 + } catch (error) {
  216 + console.error('获取分类失败:', error);
  217 + }
  218 + };
  219 +
  220 + // 加载动作
  221 + const loadExercises = async (categoriesId) => {
  222 + try {
  223 + const partRes = await ExercisesApi.getMotionPart(categoriesId);
  224 + motionPart.value = partRes.data || [];
  225 + console.log(motionPart.value, 'motionPart.value');
  226 +
  227 + const exerciseRes = await ExercisesApi.getexercises({ categoriesId });
  228 + exercises.value = exerciseRes.data;
  229 + } catch (error) {
  230 + console.error('加载动作失败:', error);
  231 + }
  232 + };
  233 +
  234 + // 切换导航
  235 + const switchNav = (id) => {
  236 + if (activeNav.value === id) return;
  237 + activeNav.value = id;
  238 + activeMotionPart.value = '';
  239 + if (id == 'super') {
  240 + exercises.value = [];
  241 + motionPart.value = [];
  242 + loadsupersetsinfo();
  243 + } else {
  244 + superGroupInfo.value = [];
  245 + loadExercises(id);
  246 + }
  247 + };
  248 +
  249 + // 搜索
  250 + const onSearch = (e) => {
  251 + const searchText = e.detail.value.trim();
  252 + if (!currentCategoryId.value) return;
  253 +
  254 + searchText ? searchExercises(searchText) : loadExercises(currentCategoryId.value);
  255 + };
  256 +
  257 + const searchExercises = (keyword) => {
  258 + const key = keyword.toLowerCase().trim();
  259 + exercises.value = allExercises.value.filter((item) => {
  260 + return item.name && item.name.toLowerCase().includes(key);
  261 + });
  262 + };
  263 +
  264 + const loadsupersetsinfo = async () => {
  265 + try {
  266 + const response = await SupersetsApi.getsupersets();
  267 + superGroupInfo.value = response.data || [];
  268 + } catch (error) {
  269 + console.error('获取超级组失败:', error);
  270 + }
  271 + };
  272 +
  273 + // 页面跳转
  274 + const goToDetail = (id, type) => {
  275 + nextTick(() => {
  276 + actionDetailRef.value.open(id, type);
  277 + });
  278 + };
  279 +
  280 + const addExercise = () => {
  281 + addshow.value = !addshow.value;
  282 + };
  283 +
  284 + const addnewmotion = () => {
  285 + uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });
  286 + };
  287 +
  288 + const addnewsupersets = () => {
  289 + uni.navigateTo({
  290 + url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(
  291 + JSON.stringify(navItems.value),
  292 + )}`,
  293 + });
  294 + };
  295 +
  296 + const actionmanagement = () => {
  297 + uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' });
  298 + };
  299 +
  300 + onMounted(() => {
  301 + loadCategories();
  302 + });
  303 +</script>
  304 +<style lang="scss" scoped>
  305 + .exercise-page {
  306 + background-color: white;
  307 + box-sizing: border-box;
  308 + height: 100%;
  309 + display: flex;
  310 + flex-direction: column;
  311 + box-sizing: border-box;
  312 +
  313 + .search-bar {
  314 + box-sizing: border-box;
  315 + width: 100%;
  316 + display: flex;
  317 + align-items: center;
  318 + justify-content: flex-end;
  319 + padding: 20rpx;
  320 + // background-color: #f5f5f5;
  321 + border-radius: 12rpx;
  322 +
  323 + .search {
  324 + display: flex;
  325 + align-items: center;
  326 + padding: 10rpx 20rpx;
  327 + flex: 1;
  328 + border-radius: 30rpx;
  329 + background-color: #f5f5f5;
  330 +
  331 + .search-icon {
  332 + width: 30rpx;
  333 + height: 30rpx;
  334 + }
  335 +
  336 + .search-input {
  337 + flex: 1;
  338 + padding-left: 15rpx;
  339 + font-size: 26rpx;
  340 + color: #333;
  341 + border: none;
  342 + outline: none;
  343 + background: transparent;
  344 + }
  345 + }
  346 +
  347 + .add-btn {
  348 + width: 60rpx;
  349 + height: 60rpx;
  350 + background-color: transparent;
  351 + border: none;
  352 + margin-left: 10rpx;
  353 + display: flex;
  354 + justify-content: center;
  355 + align-items: center;
  356 + z-index: 99;
  357 + position: relative;
  358 + .plus-icon {
  359 + width: 40rpx;
  360 + height: 40rpx;
  361 + }
  362 + .floating-menu {
  363 + position: absolute;
  364 + top: 30rpx;
  365 + right: 30rpx;
  366 + width: 300rpx;
  367 + background-color: #fff;
  368 + border-radius: 12rpx;
  369 + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
  370 + padding: 20rpx;
  371 + z-index: 100;
  372 + .menu-item {
  373 + display: flex;
  374 + align-items: center;
  375 + padding: 20rpx 0;
  376 + border-bottom: 1rpx solid #eee;
  377 + .u-icon {
  378 + margin-right: 16rpx;
  379 + }
  380 + .menu-text {
  381 + font-size: 28rpx;
  382 + color: #333;
  383 + }
  384 +
  385 + &:last-child {
  386 + border-bottom: none;
  387 + }
  388 + }
  389 + }
  390 + }
  391 + }
  392 +
  393 + .layout-container {
  394 + box-sizing: border-box;
  395 + display: flex;
  396 + height: calc(82vh + 25rpx);
  397 + background-color: #f5f5f5;
  398 +
  399 + .left-nav {
  400 + width: 160rpx;
  401 + background-color: white;
  402 + border-right: 1px solid #eee;
  403 + // 添加底部 padding,避免最后一项被底部导航栏遮挡
  404 + padding-bottom: 20rpx;
  405 + // #ifdef MP-WEIXIN
  406 + // 微信小程序端可能需要更多底部空间
  407 + padding-bottom: 100rpx;
  408 +
  409 + // #endif
  410 + .nav-item {
  411 + padding: 24rpx 0;
  412 + text-align: center;
  413 + font-size: 26rpx;
  414 + color: #333;
  415 + }
  416 +
  417 + .active {
  418 + color: #16ad40;
  419 + background-color: #e1f6e9;
  420 + border-right: 3px solid #16ad40;
  421 + font-weight: bold;
  422 + }
  423 + }
  424 +
  425 + .right-content {
  426 + box-sizing: border-box;
  427 + flex: 1;
  428 + padding: 20rpx;
  429 + height: 100%;
  430 + // 添加底部 padding,避免内容被底部导航栏遮挡
  431 + // 底部导航栏高度约 100rpx(50px),加上安全区域
  432 + padding-bottom: 20rpx;
  433 + // #ifdef MP-WEIXIN
  434 + // 微信小程序端可能需要更多底部空间
  435 + padding-bottom: 100rpx;
  436 +
  437 + // #endif
  438 + .tip {
  439 + display: flex;
  440 + padding-left: 20rpx;
  441 + align-items: center;
  442 + column-gap: 15rpx;
  443 + margin-bottom: 20rpx;
  444 +
  445 + .item {
  446 + margin: 0;
  447 + padding: 12rpx 20rpx;
  448 + font-size: 22rpx;
  449 + line-height: 1;
  450 + background-color: #fff;
  451 + border-radius: 20rpx;
  452 + &.active {
  453 + background-color: #d8ede0;
  454 + color: #43b05e;
  455 + }
  456 + }
  457 + }
  458 +
  459 + .exercise-grid {
  460 + display: flex;
  461 + flex-direction: column;
  462 +
  463 + gap: 24rpx;
  464 +
  465 + .title {
  466 + font-weight: 600;
  467 + padding-left: 20rpx;
  468 + font-size: 30rpx;
  469 + }
  470 +
  471 + .content {
  472 + width: 100%;
  473 +
  474 + // display: grid;
  475 + // grid-template-columns: repeat(2, 1fr);
  476 + // /* 改为3列布局 */
  477 + // gap: 16rpx;
  478 +
  479 + // .exercise-item {
  480 + // display: flex;
  481 + // height: 150rpx;
  482 + // /* 减小项目高度 */
  483 + // flex-direction: column;
  484 + // align-items: center;
  485 + // justify-content: center;
  486 + // background-color: white;
  487 + // padding: 16rpx 8rpx;
  488 + // box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
  489 +
  490 + // .exercise-img {
  491 + // width: 90%;
  492 + // height: 140rpx;
  493 + // /* 固定图片高度 */
  494 + // border-radius: 8rpx;
  495 + // margin-bottom: 12rpx;
  496 + // }
  497 +
  498 + // .exercise-title {
  499 + // font-size: 24rpx;
  500 + // color: #333;
  501 + // text-align: center;
  502 + // line-height: 1.3;
  503 + // max-width: 100%;
  504 +
  505 + // text-overflow: ellipsis;
  506 + // display: -webkit-box;
  507 + // -webkit-box-orient: vertical;
  508 + // }
  509 + // }
  510 + .equipment-item {
  511 + width: 100%;
  512 + margin-bottom: 40rpx;
  513 + .equipment-name {
  514 + color: #000;
  515 + font-size: 32rpx;
  516 + margin-bottom: 20rpx;
  517 + }
  518 + .action-list {
  519 + display: grid;
  520 + grid-template-columns: repeat(2, 1fr);
  521 + grid-gap: 10rpx;
  522 + .action-item {
  523 + display: flex;
  524 + flex-direction: column;
  525 + justify-content: center;
  526 + align-items: center;
  527 + gap: 10rpx;
  528 + background: #fff;
  529 + border-radius: 10rpx;
  530 + padding: 20rpx;
  531 + box-sizing: border-box;
  532 + position: relative;
  533 + .action-img {
  534 + width: 100%;
  535 + height: 250rpx;
  536 + border-radius: 10rpx;
  537 + }
  538 + .trainingReps {
  539 + position: absolute;
  540 + top: 20rpx;
  541 + right: 20rpx;
  542 + z-index: 1;
  543 + font-size: 22rpx;
  544 + }
  545 + }
  546 + }
  547 + }
  548 + .supers-name {
  549 + color: #000;
  550 + font-size: 32rpx;
  551 + margin-bottom: 20rpx;
  552 + }
  553 +
  554 + .super {
  555 + display: flex;
  556 + align-items: center;
  557 + background: #fff;
  558 + padding: 20rpx;
  559 + box-sizing: border-box;
  560 + border-radius: 10rpx;
  561 + gap: 20rpx;
  562 + font-size: 26rpx;
  563 + margin-bottom: 20rpx;
  564 + .super-img {
  565 + width: 120rpx;
  566 + height: 120rpx;
  567 + border-radius: 10rpx;
  568 + }
  569 + .super-name {
  570 + margin-bottom: 10rpx;
  571 + }
  572 + }
  573 + .add-super {
  574 + background: #fff;
  575 + padding: 20rpx;
  576 + box-sizing: border-box;
  577 + border-radius: 10rpx;
  578 + .header {
  579 + display: flex;
  580 + gap: 10rpx;
  581 + align-items: center;
  582 + margin-bottom: 20rpx;
  583 + }
  584 + .tip {
  585 + margin: 0rpx;
  586 + padding: 0rpx;
  587 + font-size: 22rpx;
  588 + }
  589 + }
  590 + }
  591 +
  592 + .nav-item .category-group {
  593 + /* 超级组特殊样式 */
  594 + background-color: #f0f9ff;
  595 + font-weight: bold;
  596 + border-top: 1px solid #eee;
  597 + }
  598 +
  599 + .empty-state {
  600 + display: flex;
  601 + justify-content: center;
  602 + align-items: center;
  603 +
  604 + .empty-text {
  605 + font-size: 28rpx;
  606 + color: #999;
  607 + }
  608 + }
  609 + }
  610 + }
  611 + }
  612 + }
  613 +</style>
  1 +<!--
  2 + 顶部两个下拉筛选器,部位筛选和场景筛选
  3 +<!-- 它是给用户选训练计划模板的页面比如:列表显示的是模板大类,里面包含好几个模板
  4 +<!-- 还未实现 部位和场景筛选到对应的模板 -->
  5 +<!-- 第一层 模板大类 -->
  6 +<template>
  7 + <view class="template-page" @click="closeAllDropdowns">
  8 + <!-- 筛选按钮 -->
  9 + <view class="filter-section" @click.stop>
  10 + <!-- 部位筛选 -->
  11 + <view class="filter-wrapper">
  12 + <view class="filter-btn" :class="{ active: showPartDropdown }" @click="togglePartDropdown">
  13 + <text class="text">{{ activePart }}</text>
  14 + <uni-icons :type="showPartDropdown ? 'up' : 'down'" size="13"></uni-icons>
  15 + </view>
  16 + <!-- 部位下拉菜单 -->
  17 + <view class="dropdown-menu" v-if="showPartDropdown">
  18 + <view v-for="(item, index) in partList" :key="index" class="dropdown-item"
  19 + :class="{ selected: activePart === item.title }" @click="selectPart(item)">
  20 + {{ item.title }}
  21 + </view>
  22 + </view>
  23 + </view>
  24 +
  25 + <!-- 场景筛选 -->
  26 + <view class="filter-wrapper">
  27 + <view class="filter-btn" :class="{ active: showSceneDropdown }" @click="toggleSceneDropdown">
  28 + <text class="text">{{ activeScene }}</text>
  29 + <uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="13"></uni-icons>
  30 + </view>
  31 + <!-- 场景下拉菜单 -->
  32 + <view class="dropdown-menu" v-if="showSceneDropdown">
  33 + <view v-for="(item, index) in sceneList" :key="index" class="dropdown-item"
  34 + :class="{ selected: activeSceneId === item.id }" @click="selectScene(item)">
  35 + {{ item.title }}
  36 + </view>
  37 + </view>
  38 + </view>
  39 + </view>
  40 + <!-- 模板列表 (根据状态自动切换显示) -->
  41 + <scroll-view class="template-list" enable-flex scroll-y>
  42 + <!-- 用父容器包裹 v-if 分支,解决 key 冲突 -->
  43 + <view v-if="!isFiltering">
  44 + <view v-for="(item, index) in templateList" :key="index" class="template-item">
  45 + <image :src="item.urlCover" mode="aspectFill" class="template-img"></image>
  46 + <view>
  47 + <view class="template-content" @click="gototemplate(item)">
  48 + <view class="template-title">{{ item.name }}</view>
  49 + <view class="template-count">{{ item.templatesCount }}个模板</view>
  50 + <view class="template-desc">{{ item.description }}</view>
  51 + </view>
  52 + </view>
  53 + </view>
  54 + </view>
  55 +
  56 + <!-- 用父容器包裹 v-else 分支,解决 key 冲突 -->
  57 + <view v-else>
  58 + <view v-for="(template, index) in filteredTemplateList" :key="index" class="template-item">
  59 + <image :src="template.urlCover" mode="aspectFill" class="template-img"></image>
  60 + <view>
  61 + <view class="template-content" @click="goToTemplateDetail(template)">
  62 + <view class="template-title">{{ template.name }}</view>
  63 + <!-- 空数据判断移到了这里 -->
  64 + <view class="template-count" v-if="template.primaryMuscles">
  65 + 主要训练部位:{{ template.primaryMuscleNames.join(', ') || '无' }}
  66 + </view>
  67 + <view class="template-desc" v-if="template.secondaryMuscles">
  68 + 次要训练部位:{{ template.secondaryMuscleNames.join(', ') || '无' }}
  69 + </view>
  70 + </view>
  71 + </view>
  72 + </view>
  73 +
  74 + <!-- 空数据提示:移到 view-else 内部,且作为独立块 -->
  75 + <view v-if="filteredTemplateList.length === 0" class="empty-tip">
  76 + 暂无符合条件的训练模板
  77 + </view>
  78 + </view>
  79 + </scroll-view>
  80 + </view>
  81 +</template>
  82 +
  83 +<script setup>
  84 +import { onMounted, ref } from 'vue';
  85 +import TemplatesApi from '@/sheep/api/Template/Templates';
  86 +
  87 +const templateList = ref([]);
  88 +// 1. 存储【筛选出来的具体模板】
  89 +const filteredTemplateList = ref([]);
  90 +// 2. 控制页面显示:false=显示大类,true=显示筛选后的模板
  91 +const isFiltering = ref(false);
  92 +// 3. 场景ID(接口需要,你之前漏了存场景ID!)
  93 +const activeSceneId = ref('0');
  94 +
  95 +// 下拉框部位筛选相关状态
  96 +const partList = ref([]);
  97 +const rawPartData = ref([]); // 存储接口返回的原始部位数据
  98 +const showPartDropdown = ref(false);
  99 +const activePart = ref('不限');
  100 +const activePartId = ref('0'); // 新增:默认选中"不限",id=0
  101 +// 场景筛选相关状态
  102 +const sceneList = ref([
  103 + { id: '0', title: '不限' },
  104 + { id: '1', title: '健身房' },
  105 + { id: '2', title: '仅哑铃' },
  106 + { id: '3', title: '仅哑铃+杠铃' },
  107 + // { id: 4, title: '办公室' },
  108 +]);
  109 +const showSceneDropdown = ref(false);
  110 +const activeScene = ref('不限');
  111 +
  112 +// 获取模板大类列表
  113 +const TemplatesList = async () => {
  114 + try {
  115 + const response = await TemplatesApi.getTemplates();
  116 + templateList.value = response.data;
  117 + console.log('模板大类接口返回数据', templateList.value)
  118 + } catch (error) {
  119 + console.log('模板大类获取失败', error);
  120 + }
  121 +};
  122 +// 获取所有细分锻炼部位(接口)
  123 +const getPartCategories = async () => {
  124 + try {
  125 + const res = await TemplatesApi.getPartAllCategories();
  126 + if (res.code === 0) {
  127 + rawPartData.value = res.data;
  128 + console.log('部位分类接口返回数据rawPartData.value:', rawPartData.value)
  129 + // 下拉列表赋值
  130 + partList.value = [
  131 + { title: '不限', id: '0' }, // 给"不限"加id='0',方便后续筛选
  132 + ...res.data.map(item => ({
  133 + title: item.name, // 接口返回的name作为显示标题
  134 + id: String(item.id) // 保存接口的id,用于后续筛选传参
  135 + }))
  136 + ];
  137 + }
  138 + } catch (error) {
  139 + console.log('部位列表获取失败', error);
  140 + }
  141 +};
  142 +// 切换部位下拉菜单
  143 +const togglePartDropdown = () => {
  144 + showPartDropdown.value = !showPartDropdown.value;
  145 + showSceneDropdown.value = false;
  146 +};
  147 +
  148 +// 切换场景下拉菜单
  149 +const toggleSceneDropdown = () => {
  150 + showSceneDropdown.value = !showSceneDropdown.value;
  151 + showPartDropdown.value = false;
  152 +};
  153 +
  154 +// 选择部位
  155 +const selectPart = (item) => {
  156 + activePart.value = item.title;
  157 + activePartId.value = item.id;
  158 + showPartDropdown.value = false;
  159 + // 加这一行:选完部位立刻筛选
  160 + doFilter();
  161 +};
  162 +
  163 +// 选择场景
  164 +const selectScene = (item) => {
  165 + activeScene.value = item.title;
  166 + // 关键:把选中的场景ID存起来
  167 + activeSceneId.value = item.id;
  168 + showSceneDropdown.value = false;
  169 + // 选择完,立即执行筛选!
  170 + doFilter();
  171 +};
  172 +
  173 +// 点击页面其他地方关闭所有下拉菜单
  174 +const closeAllDropdowns = () => {
  175 + showPartDropdown.value = false;
  176 + showSceneDropdown.value = false;
  177 +};
  178 +// 核心:执行筛选逻辑
  179 +const doFilter = async () => {
  180 + const musclesId = activePartId.value; // 对应接口 musclesId
  181 + const scene = activeSceneId.value; // 对应接口 scene
  182 + // 2. 判断:是否是【全部不限】
  183 + if (musclesId === '0' && scene === '0') {
  184 + // 情况A:都不限 -> 恢复显示【模板大类】
  185 + isFiltering.value = false;
  186 + // 大类列表之前已经加载过了,直接显示
  187 + return;
  188 + }
  189 + // 情况B:有筛选条件 -> 请求【筛选接口】
  190 + try {
  191 + isFiltering.value = true; // 切换成模板列表模式
  192 + const res = await TemplatesApi.queryTemplatesByCondition({
  193 + musclesId: musclesId,
  194 + scene: scene
  195 + });
  196 + if (res.code === 0) {
  197 + // 把接口返回的模板数据存起来,页面会自动刷新
  198 + filteredTemplateList.value = res.data;
  199 + console.log('筛选接口返回数据filteredTemplateList.value:', filteredTemplateList.value)
  200 + }
  201 + } catch (error) {
  202 + console.log('筛选失败', error);
  203 + // 失败了也清空数据
  204 + filteredTemplateList.value = [];
  205 + }
  206 +};
  207 +// 跳转到模板详情页,传入模板id
  208 +// 跳转地址是F:\hongxing-app\hongxing-new\pages4\pages\xunji\xunji-moban-xiangqing.vue
  209 +const goToTemplateDetail = (template) => {
  210 + uni.navigateTo({
  211 + url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`,
  212 + });
  213 +};
  214 +const gototemplate = (item) => {
  215 + // 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4
  216 + uni.navigateTo({
  217 + url: `/pages4/pages/xunji/xunji-moban?id=${item.id}`,
  218 + });
  219 +};
  220 +
  221 +onMounted(() => {
  222 + TemplatesList();
  223 + getPartCategories();
  224 +});
  225 +</script>
  226 +
  227 +<style lang="scss" scoped>
  228 +.template-page {
  229 + width: 100%;
  230 + height: 100%;
  231 + box-sizing: border-box;
  232 +
  233 + .filter-section {
  234 + position: fixed;
  235 + top: 80rpx;
  236 + // #ifdef MP-WEIXIN
  237 + top: 234rpx;
  238 + // #endif
  239 +
  240 + left: 0;
  241 + right: 0;
  242 + z-index: 999;
  243 +
  244 + display: flex;
  245 + gap: 24rpx;
  246 + flex-wrap: wrap;
  247 + padding: 20rpx;
  248 + background-color: white;
  249 +
  250 + .filter-btn {
  251 + display: flex;
  252 + align-items: center;
  253 + justify-content: center;
  254 + width: auto;
  255 + min-width: 150rpx;
  256 + height: 50rpx;
  257 + background-color: #fff;
  258 + color: #333;
  259 + font-size: 24rpx;
  260 + border: 2rpx solid #ddd;
  261 + border-radius: 25rpx;
  262 +
  263 + .text {
  264 + margin-right: 5rpx;
  265 + }
  266 + }
  267 + }
  268 +
  269 + .template-list {
  270 + margin-top: 120rpx;
  271 + padding: 0 30rpx;
  272 + box-sizing: border-box;
  273 + // 设置明确的高度,使 scroll-view 可以滚动
  274 + height: calc(100vh - 120rpx);
  275 + // #ifdef MP-WEIXIN
  276 + height: calc(100vh - 274rpx);
  277 + // #endif
  278 + // 添加底部 padding,避免内容被底部导航栏遮挡
  279 + padding-bottom: 180rpx;
  280 + // #ifdef MP-WEIXIN
  281 + padding-bottom: 90rpx;
  282 + // #endif
  283 +
  284 + .template-item {
  285 + display: flex;
  286 + background-color: white;
  287 + border-radius: 16rpx;
  288 + overflow: hidden;
  289 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  290 + margin-bottom: 20rpx;
  291 + padding: 20rpx;
  292 +
  293 + .template-img {
  294 + width: 120rpx;
  295 + height: 120rpx;
  296 + border-radius: 12rpx;
  297 + margin-right: 20rpx;
  298 + }
  299 +
  300 + .template-content {
  301 + flex: 1;
  302 + }
  303 +
  304 + .template-title {
  305 + font-size: 32rpx;
  306 + color: #333;
  307 + margin-bottom: 10rpx;
  308 + }
  309 +
  310 + .template-count {
  311 + font-size: 24rpx;
  312 + color: #666;
  313 + margin-bottom: 10rpx;
  314 + }
  315 +
  316 + .template-desc {
  317 + font-size: 24rpx;
  318 + color: #666;
  319 + line-height: 1.4;
  320 + }
  321 + }
  322 +
  323 + /* 筛选包装器 */
  324 + .filter-wrapper {
  325 + position: relative;
  326 + z-index: 10;
  327 + }
  328 +
  329 + /* 筛选按钮激活状态 */
  330 + .filter-btn.active {
  331 + border-color: #43b05e;
  332 + background-color: #e6f7f0;
  333 + color: #43b05e;
  334 + }
  335 +
  336 + .filter-section {
  337 + display: flex;
  338 + gap: 24rpx;
  339 + padding: 20rpx 30rpx;
  340 + background-color: white;
  341 + flex-wrap: wrap;
  342 + }
  343 +
  344 + .filter-btn {
  345 + display: flex;
  346 + align-items: center;
  347 + justify-content: center;
  348 + width: auto;
  349 + min-width: 160rpx;
  350 + height: 50rpx;
  351 + background: #fff;
  352 + color: #333;
  353 + font-size: 24rpx;
  354 + border: 2rpx solid #ddd;
  355 + border-radius: 25rpx;
  356 + padding: 0 20rpx;
  357 + white-space: nowrap;
  358 + }
  359 +
  360 + .filter-btn .text {
  361 + margin-right: 8rpx;
  362 + }
  363 +
  364 + .filter-btn.active {
  365 + border-color: #43b05e;
  366 + background: #e6f7f0;
  367 + color: #43b05e;
  368 + }
  369 +
  370 + .filter-wrapper {
  371 + position: relative;
  372 + z-index: 10;
  373 + }
  374 +
  375 + .dropdown-menu {
  376 + position: absolute;
  377 + top: calc(100% + 8rpx);
  378 + left: 0;
  379 + background: #fff;
  380 + border-radius: 14rpx;
  381 + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
  382 + padding: 12rpx 0;
  383 + z-index: 100;
  384 + min-width: 160rpx;
  385 + max-height: 320rpx;
  386 + overflow-y: auto;
  387 + }
  388 +
  389 + .dropdown-item {
  390 + padding: 16rpx 24rpx;
  391 + font-size: 26rpx;
  392 + color: #333;
  393 + white-space: nowrap;
  394 + }
  395 +
  396 + .dropdown-item.selected {
  397 + background: #f0f9f4;
  398 + color: #2e9d5a;
  399 + font-weight: 500;
  400 + }
  401 +
  402 + /* 下拉菜单样式 */
  403 + .dropdown-menu {
  404 + position: absolute;
  405 + top: calc(100% + 8rpx);
  406 + left: 0;
  407 + background: #fff;
  408 + border-radius: 14rpx;
  409 + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
  410 + padding: 12rpx 0;
  411 + z-index: 100;
  412 + min-width: 150rpx;
  413 + max-height: 320rpx;
  414 + overflow-y: auto;
  415 + border: 1rpx solid #f0f0f0;
  416 + }
  417 +
  418 + /* 下拉菜单项样式 */
  419 + .dropdown-item {
  420 + padding: 16rpx 24rpx;
  421 + font-size: 26rpx;
  422 + color: #333;
  423 + }
  424 +
  425 + /* 下拉菜单项悬停样式 */
  426 + .dropdown-item:hover {
  427 + background-color: #43b05e;
  428 + }
  429 +
  430 + /* 下拉菜单项选中样式 */
  431 + .dropdown-item.selected {
  432 + background-color: #f0f9f4;
  433 + color: #2e9d5a;
  434 + font-weight: 500;
  435 + }
  436 + }
  437 +}
  438 +</style>