Authored by Bad

初始化

Too many changes to show.

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

# 版本号
SHOPRO_VERSION=v2.4.1
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
# SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
SHOPRO_BASE_URL=http://mall.hcxtec.com
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
SHOPRO_DEV_BASE_URL=http://192.168.1.200:48081
# SHOPRO_DEV_BASE_URL=http://192.168.1.85:48080
SHOPRO_DEV_BASE_URL=https://fitness.hcxtec.com
# SHOPRO_DEV_BASE_URL=http://api-dashboard.yudao.iocoder.cn/
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
SHOPRO_UPLOAD_TYPE=server
# 后端接口前缀(一般不建议调整)
SHOPRO_API_PATH=/app-api
# SHOPRO_API_PATH=/api
# 后端 websocket 接口前缀
SHOPRO_WEBSOCKET_PATH=/infra/ws
# 开发环境运行端口
SHOPRO_DEV_PORT=3000
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
### SHOPRO_STATIC_URL = https://file.sheepjs.com
# 前端 H5 访问域名
SHOPRO_H5_URL=http://127.0.0.1:3000
# 是否开启直播 1 开启直播 | 0 关闭直播
SHOPRO_MPLIVE_ON=0
# 租户ID 默认 1
SHOPRO_TENANT_ID=1
... ...
unpackage/*
node_modules/*
.idea/*
deploy.sh
.hbuilderx/
.vscode/
**/.DS_Store
yarn.lock
package-lock.json
*.keystore
pnpm-lock.yaml
.history/*
... ...
/unpackage/*
/node_modules/**
/uni_modules/**
/public/*
**/*.svg
**/*.sh
... ...
{
"printWidth": 100,
"semi": true,
"vueIndentScriptAndStyle": true,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"htmlWhitespaceSensitivity": "strict",
"endOfLine": "auto"
}
... ...
<script setup>
import { onLaunch, onShow, onError, onLoad } from '@dcloudio/uni-app';
import { ShoproInit } from './sheep';
onLaunch(() => {
// 隐藏原生导航栏 使用自定义底部导航
// uni.hideTabBar({
// fail: () => {},
// });
// 加载Shopro底层依赖
ShoproInit();
});
onShow(async (options) => {
// #ifdef APP-PLUS
// 获取urlSchemes参数
const args = plus.runtime.arguments;
if (args) {
}
// 获取剪贴板
uni.getClipboardData({
success: (res) => {
},
});
// #endif
});
</script>
<style lang="scss">
@import 'uview-plus/index.scss';
@import '@/sheep/scss/index.scss';
</style>
... ...
MIT License
Copyright (c) 2022 lidongtony
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
... ...
**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!**
**「我喜欢写代码,乐此不疲」**
**「我喜欢做开源,以此为乐」**
我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
## 🐶 新手必读
* 演示地址:<https://doc.iocoder.cn/mall-preview/>
* 启动文档:<https://doc.iocoder.cn/quick-start/>
* 视频教程:<https://doc.iocoder.cn/video/>
## 🐯 商城简介
**芋道商城**,基于 [芋道开发平台](https://github.com/YunaiV/ruoyi-vue-pro) 构建,以开发者为中心,打造中国第一流的 Java 开源商城系统,全部开源,个人与企业可 100% 免费使用。
> 有任何问题,或者想要的功能,可以在 Issues 中提给艿艿。
>
> 😜 给项目点点 Star 吧,这对我们真的很重要!
![功能图](/.image/common/mall-feature.png)
* 基于 uni-app + Vue3 开发,支持微信小程序、微信公众号、H5 移动端,未来会支持支付宝小程序、抖音小程序等
* 支持 SaaS 多租户,可满足商品、订单、支付、会员、优惠券、秒杀、拼团、砍价、分销、积分等多种经营需求
## 🔥 后端架构
支持 Spring Boot、Spring Cloud 两种架构:
① Spring Boot 单体架构:<https://doc.iocoder.cn>
![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
![架构图](/.image/common/yudao-cloud-architecture.png)
## 🐱 移动端预览
![移动端预览](/.image/common/mall-preview.png)
## 🐶 管理端预览
![店铺装修](/.image/mall/店铺装修.png)
![会员详情](/.image/mall/会员详情.png)
![商品详情](/.image/mall/商品详情.png)
![订单详情](/.image/mall/订单详情.png)
![营销中心](/.image/mall/营销中心.png)
... ...
{
"prompt" : "template"
}
... ...
<template>
<view class="ad-modal">
<u-popup
:show="show"
mode="center"
@close="close"
bgColor="transparent"
:safeAreaInsetBottom="false"
>
<view class="ad-container">
<view class="swiper-wrapper">
<u-swiper
:list="list"
keyName="image"
height="700rpx"
:autoplay="false"
circular
@click="handleAdClick"
radius="16rpx"
indicator
bgColor="transparent"
indicatorMode="dot"
></u-swiper>
</view>
<view class="close-section" @click="close">
<u-icon name="close-circle" color="#ffffff" size="34"></u-icon>
</view>
</view>
</u-popup>
</view>
</template>
<script setup>
const props = defineProps({
show: {
type: Boolean,
default: false,
},
list: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['updateShow', 'clickAd', 'close']);
// 关闭弹窗
const close = () => {
emit('close', false);
};
// 点击广告图触发
const handleAdClick = (index) => {
emit('clickAd', props.list[index]);
};
</script>
<style lang="scss" scoped>
.ad-container {
width: 580rpx; // 弹窗宽度
display: flex;
flex-direction: column;
align-items: center;
.swiper-wrapper {
width: 100%;
overflow: hidden;
}
.close-section {
margin-top: 40rpx;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
/* 增加点击区域面积 */
padding: 20rpx;
}
}
</style>
... ...
<template>
<view class="tabbar">
<up-tabbar
:value="activePath"
@change="handleTabChange"
:placeholder="true"
activeColor="#000"
:fixed="true"
>
<up-tabbar-item
v-for="(item, index) in tabList"
:key="index"
:text="item.text"
:name="item.path"
>
<template #active-icon>
<image :class="['icon', item.special ? 'mid-icon' : '']" :src="item.selectedIcon"></image>
</template>
<template #inactive-icon>
<image :class="['icon', item.special ? 'mid-icon' : '']" :src="item.icon"></image>
</template>
</up-tabbar-item>
</up-tabbar>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
const activePath = ref('pages/xunji/xunji');
// 配置化 Tabbar
const tabList = [
{
text: '训记',
path: 'pages/xunji/xunji',
icon: '/static/tabbar/xunji.png',
selectedIcon: '/static/tabbar/xunji-sel.png',
special: false,
},
{
text: '我的',
path: 'pages/user/user',
icon: '/static/tabbar/wode.png',
selectedIcon: '/static/tabbar/wode-sel.png',
special: false,
},
];
const handleTabChange = (name) => {
activePath.value = name;
uni.switchTab({ url: '/' + name });
};
onShow(() => {
const pages = getCurrentPages();
const currPage = pages[pages.length - 1];
if (currPage && !currPage.route.includes('entry')) {
activePath.value = currPage.route;
}
});
</script>
<style lang="scss" scoped>
.icon {
width: 48rpx;
height: 48rpx;
transition: all 0.2s;
}
</style>
... ...
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
... ...
{
"compilerOptions": {
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
... ...
import App from './App';
import { createSSRApp } from 'vue';
import { setupPinia } from './sheep/store';
import uviewPlus from 'uview-plus';
import Tabbar from '@/components/tabbar.vue';
export function createApp() {
const app = createSSRApp(App);
setupPinia(app);
app.use(uviewPlus, () => {
return {
options: {
// 修改config对象的属性
config: {
// 只加载一次字体图标
loadFontOnce: true,
unit:'rpx'
},
},
};
});
app.component('Tabbar', Tabbar);
return {
app,
};
}
... ...
{
"name": "鸿星健身",
"appid": "__UNI__0A1E345",
"description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
"versionName": "2025.10",
"versionCode": "183",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"nvueLaunchMode": "fast",
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"safearea": {
"bottom": {
"offset": "none"
}
},
"modules": {
"Payment": {},
"Share": {},
"VideoPlayer": {},
"OAuth": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.READ_SMS\"/>",
"<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
"<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
"<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
],
"minSdkVersion": 21,
"schemes": "shopro"
},
"ios": {
"urlschemewhitelist": [
"baidumap",
"iosamap"
],
"dSYMs": false,
"privacyDescription": {
"NSPhotoLibraryUsageDescription": "需要同意访问您的相册选取图片才能完善该条目",
"NSPhotoLibraryAddUsageDescription": "需要同意访问您的相册才能保存该图片",
"NSCameraUsageDescription": "需要同意访问您的摄像头拍摄照片才能完善该条目",
"NSUserTrackingUsageDescription": "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
},
"urltypes": "shopro",
"capabilities": {
"entitlements": {
"com.apple.developer.associated-domains": [
"applinks:shopro.sheepjs.com"
]
}
},
"idfa": true
},
"sdkConfigs": {
"speech": {},
"ad": {},
"oauth": {
"apple": {},
"weixin": {
"appid": "wxae7a0c156da9383b",
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/",
"mergeVirtualHostAttributes": true
}
},
"payment": {
"weixin": {
"__platform__": [
"ios",
"android"
],
"appid": "wxae7a0c156da9383b",
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
},
"alipay": {
"__platform__": [
"ios",
"android"
]
}
},
"share": {
"weixin": {
"appid": "wxae7a0c156da9383b",
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
}
}
},
"orientation": [
"portrait-primary"
],
"splashscreen": {
"androidStyle": "common",
"iosStyle": "common",
"useOriginalMsgbox": true
},
"icons": {
"android": {
"hdpi": "unpackage/res/icons/72x72.png",
"xhdpi": "unpackage/res/icons/96x96.png",
"xxhdpi": "unpackage/res/icons/144x144.png",
"xxxhdpi": "unpackage/res/icons/192x192.png"
},
"ios": {
"appstore": "unpackage/res/icons/1024x1024.png",
"ipad": {
"app": "unpackage/res/icons/76x76.png",
"app@2x": "unpackage/res/icons/152x152.png",
"notification": "unpackage/res/icons/20x20.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"proapp@2x": "unpackage/res/icons/167x167.png",
"settings": "unpackage/res/icons/29x29.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"spotlight": "unpackage/res/icons/40x40.png",
"spotlight@2x": "unpackage/res/icons/80x80.png"
},
"iphone": {
"app@2x": "unpackage/res/icons/120x120.png",
"app@3x": "unpackage/res/icons/180x180.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"notification@3x": "unpackage/res/icons/60x60.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"settings@3x": "unpackage/res/icons/87x87.png",
"spotlight@2x": "unpackage/res/icons/80x80.png",
"spotlight@3x": "unpackage/res/icons/120x120.png"
}
}
}
}
},
"quickapp": {},
"quickapp-native": {
"icon": "/static/logo.png",
"package": "com.example.demo",
"features": [
{
"name": "system.clipboard"
}
]
},
"quickapp-webview": {
"icon": "/static/logo.png",
"package": "com.example.demo",
"minPlatformVersion": 1070,
"versionName": "1.0.0",
"versionCode": 100
},
"mp-weixin": {
"appid": "wxb827c923ce0aad4b",
"setting": {
"urlCheck": true,
"minified": true,
"postcss": true
},
"optimization": {
"subPackages": true
},
"plugins": {},
"lazyCodeLoading": "requiredComponents",
"usingComponents": {},
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于展示附近的服务"
}
},
"requiredPrivateInfos": [
"chooseAddress",
"getLocation"
],
"mergeVirtualHostAttributes": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"mp-jd": {
"usingComponents": true
},
"h5": {
"template": "index.html",
"router": {
"mode": "history",
"base": "/"
},
"sdkConfigs": {
"maps": {}
},
"async": {
"timeout": 20000
},
"title": "芋道商城",
"optimization": {
"treeShaking": {
"enable": true
}
}
},
"vueVersion": "3",
"_spaceID": "192b4892-5452-4e1d-9f09-eee1ece40639",
"locale": "zh-Hans",
"fallbackLocale": "zh-Hans"
}
\ No newline at end of file
... ...
{
"id": "shopro",
"name": "shopro",
"displayName": "芋道商城",
"version": "2025.10.0",
"description": "芋道商城,一套代码,同时发行到iOS、Android、H5、微信小程序多个平台,请使用手机扫码快速体验强大功能",
"scripts": {
"prettier": "prettier --write \"{pages,sheep}/**/*.{js,json,tsx,css,less,scss,vue,html,md}\""
},
"repository": "https://github.com/sheepjs/shop.git",
"keywords": [
"商城",
"B2C",
"商城模板"
],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/sheepjs/shop/issues"
},
"homepage": "https://github.com/dcloudio/hello-uniapp#readme",
"dcloudext": {
"category": [
"前端页面模板",
"uni-app前端项目模板"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "u",
"vue3": "y"
}
}
}
},
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.7",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"luch-request": "^3.0.8",
"pinia": "^2.3.1",
"pinia-plugin-persist-uni": "^1.2.0",
"uview-plus": "^3.6.29",
"vue": "^2.7.16",
"weixin-js-sdk": "^1.6.0"
},
"devDependencies": {
"prettier": "^2.8.7",
"vconsole": "^3.15.0"
}
}
... ...
{
"easycom": {
"autoscan": true,
"custom": {
"^s-(.*)": "@/sheep/components/s-$1/s-$1.vue",
"^su-(.*)": "@/sheep/ui/su-$1/su-$1.vue",
"^u--(.*)": "@/node_modules/uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "@/node_modules/uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "@/node_modules/uview-plus/components/u-$1/u-$1.vue",
"^qiun-(.*)": "@/uni_modules/qiun-data-charts/components/qiun-$1/qiun-$1.vue"
}
},
"pages": [
{
"path": "pages/xunji/xunji",
"style": {
"navigationBarTitleText": "训记"
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "default"
}
}
],
"subPackages": [
{
"root": "pages4",
"name": "分包4",
"pages": [
{
"path": "pages/xunji/xunji-xunlian-jihua",
"style": {
"navigationBarTitleText": "训练计划"
}
},
{
"path": "pages/xunji/xunji-wode-zhuye",
"style": {
"navigationBarTitleText": "我的主页"
}
},
{
"path": "pages/xunji/xunji-wode-qianming",
"style": {
"navigationBarTitleText": "个性签名",
"navigationStyle": "default"
}
},
{
"path": "pages/xunji/xunji-wode-moban",
"style": {
"navigationBarTitleText": "我的模版"
}
},
{
"path": "pages/xunji/xunji-rili-tianjia",
"style": {
"navigationBarTitleText": "日历添加训练动作"
}
},
{
"path": "pages/xunji/xunji-rili-tianjia-moban",
"style": {
"navigationBarTitleText": "训记-日历-训练模版"
}
},
{
"path": "pages/xunji/xunji-moban",
"style": {
"navigationBarTitleText": "动作模板"
}
},
{
"path": "pages/xunji/xunji-moban-xiangqing",
"style": {
"navigationBarTitleText": "模板详情查看"
}
},
{
"path": "pages/xunji/xunji-dongzuo-xinzeng",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "default"
}
},
{
"path": "pages/xunji/xunji-dongzuo-xiangqing",
"style": {
"navigationBarTitleText": "动作详情",
"navigationStyle": "default"
}
},
{
"path": "pages/xunji/xunji-dongzuo-xiangqing-chaojizu",
"style": {
"navigationBarTitleText": "超级组详情",
"backgroundColor": "#1a1a1a"
}
},
{
"path": "pages/xunji/xunji-dongzuo-lianxi",
"style": {
"navigationBarTitleText": "动作练习/训练"
}
},
{
"path": "pages/xunji/dongzuo-xinzengchaojizu",
"style": {
"navigationBarTitleText": "新增超级组"
}
},
{
"path": "pages/xunji/dongzuo-muluguanli",
"style": {
"navigationBarTitleText": "动作目录管理",
"navigationStyle": "default"
}
},
{
"path": "pages/xunji/jihua-search",
"style": {
"navigationBarTitleText": "训记-计划-搜索"
}
},
{
"path": "pages/xunji/xunji-shiping",
"style": {
"navigationBarTitleText": ""
}
}
]
},
{
"root": "pages7",
"name": "分包7",
"pages": [
{
"path": "pages/index/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "default"
}
}
]
},
{
"root": "pages5",
"name": "分包5",
"pages": [
{
"path": "pages/user/wode-geren-ziliao",
"style": {
"navigationBarTitleText": "个人资料",
"navigationStyle": "default"
}
}
]
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "芋道商城",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#FFFFFF",
"navigationStyle": "custom"
},
"pageTransition": {
"style": "slide-in-bottom",
"duration": 300
},
"tabBar": {
"height": 0,
"custom": true,
"list": [
{
"pagePath": "pages/xunji/xunji"
},
{
"pagePath": "pages/user/user"
}
]
}
}
... ...
<template>
<view class="my-page">
<!-- 登录头部区 -->
<view
v-if="userStore.isLogin"
class="section-card user-header"
hover-class="card-hover"
@tap="goMyPersonalData"
>
<image
:src="
userInfo.avatar ||
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260526/默认头像_1779779926983.png'
"
mode="aspectFill"
class="avatar"
/>
<view class="info-content">
<view class="name-row">
<text class="nickname">{{ userInfo.nickname || '微信用户' }}</text>
<text v-if="memberLevelName" class="tag">{{ memberLevelName }}</text>
</view>
</view>
<view class="right">
<uni-icons type="right" size="16" color="#000" />
</view>
</view>
<!-- 未登录引导区 -->
<view v-else class="section-card login-guide-box">
<view class="guide-txt">
<text class="title">欢迎加入鸿星运动</text>
<text class="desc">登录后即可享受课程预约及资产管理</text>
</view>
<button class="login-btn" hover-class="btn-hover" @click="goLogin">立即登录</button>
</view>
<!-- -->
<view class="vip-banner" hover-class="opacity-hover" @click="goAddVip">
<view class="vip-info">
<uni-icons type="vip-filled" size="22" color="#f1c40f" />
<text class="vip-text">
{{ userInfo.deposit === 1 ? '鸿星·尊享会员 | 已开通' : '鸿星·会员 | 开通只需押金¥199' }}
</text>
</view>
<view class="vip-btn">{{ userInfo.deposit === 1 ? '查看权益' : '立即开通' }}</view>
</view>
<!-- 课程状态快速入口 -->
<view class="section-card quick-entry">
<view
v-for="entry in quickEntryConfig"
:key="entry.type"
class="entry-item"
hover-class="opacity-hover"
@click="handleQuickEntry(entry.type)"
>
<text class="num">{{ userInfo[entry.key] || 0 }}</text>
<text class="label">{{ entry.label }}</text>
</view>
</view>
<!-- 广告位 -->
<view class="banner-box" @click="goJiamen">
<image
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/4_1773627891703.png"
mode="aspectFill"
class="banner-img"
/>
</view>
<!-- 核心应用区 -->
<view class="section-card apply-section">
<view
v-for="(item, index) in APPLY_CONFIG_LIST"
:key="index"
class="apply-item"
@click="authNavigateTo(item.url)"
>
<view class="icon-bg">
<image :src="item.icon" class="img" mode="aspectFit" />
</view>
<text class="text">{{ item.text }}</text>
</view>
</view>
<!-- 资产账户网格区 -->
<view v-if="userStore.isLogin" class="section-card">
<view class="account-grid">
<view
v-for="(acc, idx) in accountConfig"
:key="idx"
class="account-item"
@click="authNavigateTo(acc.url)"
>
<text class="acc-lab">{{ acc.label }}</text>
<text class="acc-val">
{{ formatAccountValue(userInfo[acc.key], acc.isFloat) }}
<text v-if="acc.unit" class="unit">{{ acc.unit }}</text>
</text>
</view>
</view>
</view>
<!-- 功能矩阵九宫格 -->
<view class="section-card icon-grid-box">
<view class="icon-grid">
<view
v-for="(item, index) in FUNCTION_CONFIG_LIST"
:key="index"
class="icon-item"
@click="handleGridItemClick(item)"
>
<view class="icon-img-wrap">
<image :src="item.icon" mode="aspectFit" class="icon-img" />
<view v-if="item.text === '订单记录' && userInfo.orderRecordMark" class="badge">
{{ userInfo.orderRecordMark }}
</view>
</view>
<text class="icon-text">{{ item.text }}</text>
<button
v-if="item.text === '联系客服'"
open-type="contact"
class="mp-contact-overlay-btn"
/>
</view>
</view>
</view>
<!-- 设置区 -->
<view v-if="userStore.isLogin" class="section-card">
<view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-shezhi')">
<text class="setting-text">个人设置</text>
<uni-icons type="right" size="14" color="#E0E0E0" />
</view>
<view class="setting-item" @click="authNavigateTo('/pages5/pages/user/wode-yinsishezhi')">
<text class="setting-text">隐私中心</text>
<uni-icons type="right" size="14" color="#E0E0E0" />
</view>
</view>
<Tabbar />
</view>
</template>
<script setup>
import { ref } from 'vue';
import UserApi from '@/sheep/api/member/user';
import MemberApi from '@/sheep/api/member/member';
import useUserStore from '@/sheep/store/user';
import { onShow } from '@dcloudio/uni-app';
// 响应式数据挂载
const userStore = useUserStore();
const userInfo = ref({});
const memberLevelName = ref('');
// 固定的 UI 配置
const APPLY_CONFIG_LIST = [];
const FUNCTION_CONFIG_LIST = [];
// 课程计数状态配置映射
const quickEntryConfig = [
{ type: 1, key: 'courseNum', label: '待上课' },
{ type: 2, key: 'courseWaitNum', label: '等待中' },
{ type: 3, key: 'courseEvaluationWaitNum', label: '历史课程' },
];
// 资产账户字段清洗配置映射 (对应接口数据类型:number 与 integer)
const accountConfig = [];
/**
* JSDoc 核心资产数值洗涤函数
* @param {number|undefined} val 原始金钱/数量值
* @param {boolean} isFloat 是否需要保留两位小数
* @returns {string|number} 格式化后的安全渲染字符串
*/
const formatAccountValue = (val, isFloat) => {
if (val === undefined || val === null) return isFloat ? '0.00' : 0;
return isFloat ? Number(val).toFixed(2) : Math.floor(val);
};
/**
* 路由守卫拦截转发
* @param {string} url 目标绝对/相对地址
*/
const authNavigateTo = (url) => {
if (!userStore.isLogin) {
goLogin();
return;
}
uni.navigateTo({
url,
fail: (err) => console.error(`[Router] 页面跳转失败 ${url}: `, err),
});
};
/**
* 统一处理功能网格的点击分发
* @param {Object} item 节点配置项
*/
const handleGridItemClick = (item) => {
if (item.text === '联系客服') {
// #ifndef MP-WEIXIN
// 兜底非微信小程序平台(如H5、App),可以正常走原来的普通客服页面路由
authNavigateTo(item.url);
// #endif
return;
}
// 其他正常功能,正常走路由拦截守卫
authNavigateTo(item.url);
};
/**
* 异步高内聚合并请求
* 解决因先后触发 setData 导致微信小程序底层 AppService 与 WebView 之间高频拥堵卡顿的问题
*/
const fetchPageData = async () => {
try {
const [userRes, levelRes] = await Promise.all([
UserApi.getUserInfo(),
MemberApi.getMemberLevel(),
]);
const rawUser = userRes.data || {};
userInfo.value = rawUser;
// 架构重构:数据拉取后一次性计算出等级映射结果,拒绝在 computed 内部循环执行实例化
const levels = levelRes.data?.detailList || [];
if (rawUser.level !== undefined && levels.length > 0) {
const target = levels.find((item) => item.id === rawUser.level);
memberLevelName.value = target ? target.name : '';
} else {
memberLevelName.value = '';
}
} catch (error) {
console.error('[API Error] 拉取个人资产信息流失败:', error);
}
};
onShow(() => {
if (userStore.isLogin) {
fetchPageData();
} else {
userInfo.value = {};
memberLevelName.value = '';
}
});
// 路由跳转原子原子层
const goLogin = () => uni.navigateTo({ url: '/pages7/pages/index/login' });
const goMyPersonalData = () => authNavigateTo('/pages5/pages/user/wode-geren-ziliao');
const goAddVip = () => authNavigateTo('/pages5/pages/user/wode-hongxing-huiyuan');
const goJiamen = () => uni.navigateTo({ url: '/pages7/pages/index/shouye-jiamen-hongxing' });
const handleQuickEntry = (type) => authNavigateTo(`/pages5/pages/user/wode-shangke?type=${type}`);
</script>
<style scoped lang="scss">
$brand-color: #ff6b00;
$page-bg: #f8f8f8;
.my-page {
min-height: 100vh;
background-color: $page-bg;
padding: 20rpx 28rpx calc(40rpx + env(safe-area-inset-bottom)); /* 解决部分 iOS 底部 Tabbar 高度塌陷 */
box-sizing: border-box;
.section-card {
background: #ffffff;
border-radius: 24rpx;
padding: 30rpx 20rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
}
.user-header {
display: flex;
align-items: center;
padding: 34rpx 30rpx;
.avatar {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background: #f0f0f0;
border: 4rpx solid #fff;
}
.info-content {
margin-left: 24rpx;
flex: 1;
.name-row {
display: flex;
flex-direction: column;
.nickname {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.tag {
font-size: 20rpx;
color: $brand-color;
padding: 4rpx 12rpx;
border-radius: 8rpx;
vertical-align: middle;
}
}
}
.right {
display: flex;
align-items: center;
gap: 60rpx;
.img {
width: 60rpx;
height: 60rpx;
}
}
}
.login-guide-box {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.desc {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
display: block;
}
.login-btn {
margin: 0;
background: $brand-color;
color: #fff;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
border-radius: 32rpx;
padding: 0 30rpx;
&::after {
border: none;
} /* 清理小程序 button 默认黑边线 */
}
}
.vip-banner {
background: #2b2b2b;
border-radius: 20rpx;
padding: 24rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.vip-info {
display: flex;
align-items: center;
}
.vip-text {
color: #f1c40f;
font-size: 24rpx;
margin-left: 14rpx;
}
.vip-btn {
background: linear-gradient(90deg, #f1c40f, #f39c12);
color: #333;
font-size: 20rpx;
font-weight: bold;
padding: 8rpx 20rpx;
border-radius: 30rpx;
}
}
.quick-entry {
display: flex;
justify-content: space-around;
.entry-item {
text-align: center;
.num {
font-size: 38rpx;
font-weight: bold;
color: #333;
}
.label {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
display: block;
}
}
}
.banner-box {
height: 160rpx;
margin-bottom: 24rpx;
border-radius: 20rpx;
overflow: hidden;
.banner-img {
width: 100%;
height: 100%;
}
}
.apply-section {
display: grid;
grid-template-columns: repeat(5, 1fr);
.apply-item {
display: flex;
flex-direction: column;
align-items: center;
.icon-bg {
width: 80rpx;
height: 80rpx;
background: #f9f9f9;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
.img {
width: 44rpx;
height: 44rpx;
}
}
.text {
font-size: 22rpx;
color: #666;
}
}
}
.account-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
row-gap: 34rpx;
.account-item {
display: flex;
flex-direction: column;
align-items: center;
.acc-val {
font-size: 30rpx;
font-weight: bold;
color: #333;
.unit {
font-size: 20rpx;
font-weight: normal;
color: #666;
margin-left: 2rpx;
}
}
.acc-lab {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
}
}
.icon-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
row-gap: 40rpx;
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.icon-img-wrap {
position: relative;
margin-bottom: 12rpx;
.icon-img {
width: 46rpx;
height: 46rpx;
}
.badge {
position: absolute;
top: -12rpx;
right: -42rpx; /* 适当拓宽右移,容纳后端多字文本 */
background: #ff4d4f;
color: #fff;
font-size: 18rpx;
padding: 2rpx 10rpx;
border-radius: 16rpx;
white-space: nowrap;
}
/* 未读状态小红点 */
.dot-badge {
position: absolute;
top: -4rpx;
right: -4rpx;
width: 14rpx;
height: 14rpx;
background: #ff4d4f;
border-radius: 50%;
}
}
.icon-text {
font-size: 24rpx;
color: #555;
}
.mp-contact-overlay-btn {
position: absolute;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
opacity: 0 !important; /* 核心:完全透明 */
border: none !important;
padding: 0 !important;
margin: 0 !important;
z-index: 10; /* 确保盖在图表和文字的最上方 */
&::after {
border: none !important;
}
}
}
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f9f9f9;
&:last-child {
border-bottom: none;
}
.setting-text {
font-size: 28rpx;
color: #444;
}
}
.opacity-hover {
opacity: 0.7;
}
.card-hover {
background-color: #fcfcfc;
}
}
</style>
... ...
<template>
<view class="charts-box">
<qiun-data-charts
type="line"
:opts="opts"
:chartData="chartData"
:reshow="reshow"
:canvas2d="true"
/>
</view>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
// 1. 定义 Props 接收父组件数据
const props = defineProps({
// 传入的分类数据 (横坐标)
categories: {
type: Array,
default: () => [],
},
// 传入的系列数据 (纵坐标内容)
series: {
type: Array,
default: () => [],
},
// 专门用于解决弹窗不显示问题的属性
reshow: {
type: Boolean,
default: false,
},
});
const chartData = ref({});
// 2. 图表配置项
const opts = reactive({
color: [
'#1890FF',
'#91CB74',
'#FAC858',
'#EE6666',
'#73C0DE',
'#3CA272',
'#FC8452',
'#9A60B4',
'#ea7ccc',
],
padding: [15, 10, 0, 15],
enableScroll: false,
legend: {},
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: 'dash',
dashLength: 2,
},
extra: {
line: {
type: 'straight',
width: 2,
activeType: 'hollow',
},
},
});
// 3. 核心逻辑:格式化数据
const formatData = () => {
if (props.categories.length > 0) {
chartData.value = {
categories: props.categories,
series: props.series,
};
}
};
// 4. 监听 Props 变化,当父组件传入新数据时自动重绘
watch(
() => [props.categories, props.series],
() => {
formatData();
},
{ immediate: true, deep: true },
);
</script>
<style lang="scss" scoped>
.charts-box {
width: 100%;
height: 100%;
}
</style>
... ...
<template>
<up-popup :show="show" mode="bottom" round="16" closeable @close="show = false" :safeAreaInsetBottom="false">
<view class="desc-container">
<view class="title">动作备注</view>
<view class="input-box">
<up-textarea
v-model="tempNoteContent"
placeholder="此处填写个人备注"
autoHeight
border="none"
customStyle="background: #242424; padding: 20rpx; border-radius: 12rpx; color: #fff"
placeholderStyle="color: #999"
></up-textarea>
</view>
<view class="footer">
<view class="btn" @click="saveNoteContent">保存</view>
</view>
</view>
</up-popup>
</template>
<script setup>
import { ref } from 'vue';
const show = ref(false);
const tempNoteContent = ref(''); // 临时编辑的备注内容
const emit = defineEmits(['saveSuccess']);
// 保存备注
const saveNoteContent = async () => {
const content = tempNoteContent.value.trim();
// 如果内容为空,直接返回,不提交
if (!content) {
uni.showToast({ title: '备注不能为空', icon: 'none' });
return;
}
emit('saveSuccess', content);
// 关闭弹窗
show.value = false;
};
const open = () => {
show.value = true;
};
defineExpose({ open });
</script>
<style lang="scss" scoped>
.desc-container {
background-color: #1a1a1a;
padding: 40rpx 30rpx 40rpx;
.title {
color: #fff;
font-size: 32rpx;
font-weight: 500;
text-align: center;
margin-bottom: 40rpx;
}
.input-box {
margin-bottom: 60rpx;
/* 穿透修改 u-textarea 内部文字颜色 */
:deep(.u-textarea__field) {
color: #ccc !important;
}
}
.footer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.btn {
width: 100%;
height: 80rpx;
background-color: #fedc1f;
color: #333;
border-radius: 12rpx;
text-align: center;
line-height: 80rpx;
}
}
}
</style>
... ...
<template>
<up-popup
:show="actionShow"
mode="bottom"
minHeight="90vh"
@close="actionShow = false"
bgColor="#1a1a1a"
>
<scroll-view class="action-container" scroll-y>
<view class="header" v-if="type == 1">
<view class="explain">
<image
v-if="modeTab < 2"
:src="modeTab == 0 ? actionDetail.url3dAnimation : actionDetail.urlRealPerson"
class="media-content"
mode="aspectFill"
/>
<view class="video" v-else @click="playVideo(actionDetail.urlTutorial)">
<video
:src="actionDetail.urlTutorial"
class="media-content"
:autoplay="false"
:show-center-play-btn="false"
:controls="false"
/>
<view class="play-icon">
<up-icon name="play-right" size="28" class="icon" color="#fff"></up-icon>
</view>
</view>
</view>
<view class="mode-tabs" v-if="actionDetail.urlRealPerson || actionDetail.urlTutorial">
<view
class="tab-item"
v-if="actionDetail.url3dAnimation"
:class="{ active: modeTab == 0 }"
@click="switchModeTab(0)"
>
3D
</view>
<view
class="tab-item"
v-if="actionDetail.urlRealPerson"
:class="{ active: modeTab == 1 }"
@click="switchModeTab(1)"
>
真人
</view>
<view
class="tab-item"
v-if="actionDetail.urlTutorial"
:class="{ active: modeTab == 2 }"
@click="switchModeTab(2)"
>
讲解
</view>
</view>
</view>
<view class="main">
<view class="main-header">
<view class="title-bar">
<text class="title">{{ actionDetail?.name }}</text>
<view class="action-icons">
<!-- <up-icon name="share-square" color="#fff" size="24"></up-icon> -->
<button class="share-btn" open-type="share">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</button>
<up-icon
:name="isFavorite ? 'star-fill' : 'star'"
:color="isFavorite ? '#fedc1f' : '#fff'"
size="24"
@click="toggleCollect"
></up-icon>
<!-- <FavoriteBtn :id="actionId" :type="type" /> -->
</view>
</view>
<!-- 要点,历史,平替动作标签 -->
<view class="content-tabs">
<view class="tab-item" :class="{ active: contentTab === 0 }" @click="contentTab = 0">
要点
</view>
<view class="tab-item" :class="{ active: contentTab === 1 }" @click="contentTab = 1">
历史
</view>
<view
class="tab-item"
:class="{ active: contentTab === 2 }"
@click="contentTab = 2"
v-if="type === 1"
>
平替动作
</view>
</view>
</view>
<view class="main-content">
<!-- 1 要点 -->
<view v-if="contentTab === 0" class="tab-pane slide-up">
<view class="section" v-if="actionDetail.urlTutorial">
<view class="section-title">视频讲解</view>
<view class="video-grid">
<!-- <view class="video-card" v-for="i in 2" :key="i"></view> -->
<view class="video-card" @click="playVideo(actionDetail.urlTutorial)">
<video
:src="actionDetail.urlTutorial"
class="video"
:autoplay="false"
:show-center-play-btn="false"
:controls="false"
/>
<view class="play-overlay"
><up-icon name="play-circle-fill" color="#fff" size="30"></up-icon
></view>
</view>
</view>
</view>
<view class="memo-box" @click="openBeizhu">
<view class="section-title">训练备注</view>
<up-textarea
class="textarea"
v-model="actionDetail.userNote"
placeholder="点击填写备注"
autoHeight
customStyle="background: transparent; border: none; padding: 10rpx 0;"
placeholderStyle="color: #666"
disabled
border="none"
></up-textarea>
</view>
<!-- 动作列表,只有超级组才有 -->
<view class="section" v-if="type === 2">
<view class="section-title">动作列表</view>
<view class="action-list">
<!-- 动作循环列表 -->
<view
class="action-item"
v-for="item in actionDetail?.exercises"
:key="item.id"
@click="openActionItem(item)"
>
<image :src="item.url3dAnimation || lostImage" mode="aspectFill" class="img" />
<view class="middle">
<view class="name">{{ item.name }}</view>
<view class="tips">
<!-- 渲染主练肌肉标签 -->
<view class="tip" v-for="p in item.primaryMuscles" :key="p">
{{ p }}
</view>
<view v-for="s in item.secondaryMuscles" :key="s">{{ s }}</view>
</view>
</view>
<up-icon name="arrow-right" color="#fff" size="16"></up-icon>
</view>
</view>
</view>
<view class="section" v-if="type == 1">
<view class="section-title">步骤</view>
<view class="steps-list">
<rich-text class="step-text" :nodes="actionDetail.stepDescription"></rich-text>
</view>
</view>
<view class="section">
<view class="section-title">训练部位</view>
<image :src="actionDetail.urlImage" mode="widthFix" class="muscle-map" />
<view class="legend">
<view class="legend-item">
<view class="legend-color primary"></view>
<text class="legend-text">主要部位</text>
</view>
<view class="legend-item">
<view class="legend-color secondary"></view>
<text class="legend-text">次要部位</text>
</view>
</view>
</view>
</view>
<!-- 2 历史 -->
<view v-if="contentTab === 1" class="tab-pane slide-up">
<!-- <view class="stat-card highlight">
<text class="label">最长时长</text>
<text class="value">01:02:03</text>
<text class="date">记录于 2026/03/04</text>
</view> -->
<!-- <view class="chart-container" v-if="chartData && chartData.series">
<qiun-data-charts type="line" :opts="chartOpts" :chartData="chartData" />
</view> -->
<view class="history-list">
<template v-if="historyList.length > 0">
<view class="history-item" v-for="item in historyList" :key="item.id">
<view class="item-header">
<text class="date">{{ formatDate(item.date) }}</text>
<text class="tag">{{ item.name }}</text>
</view>
<view class="item-body">
<view class="count">
<view>共 {{ item.setCount }} 组训练</view>
<view class="dot">· </view>
<!-- <view class="difficulty">困难</view> -->
<view class="difficulty">{{
item.weight ? item.weight + 'kg' : '无负重'
}}</view>
</view>
<view class="group-chips">
<view class="chip" v-for="(set, index) in item.setConfigList" :key="index">
<view class="idx">{{ index + 1 }}</view>
<view class="group-data">
{{ formatSetData(set) }}
</view>
</view>
</view>
</view>
</view>
</template>
<template v-else>
<up-empty text="暂无训练历史"> </up-empty>
</template>
</view>
</view>
<!-- 3 平替动作(只有动作组才有) -->
<view v-if="contentTab === 2 && type === 1" class="tab-pane slide-up">
<view class="substitute-list">
<view
class="sub-item"
v-for="item in alternativeActions"
:key="item.id"
@click="openActionItem(item)"
>
<image :src="item.urlImage || lostImage" mode="aspectFill" class="img" />
<view class="sub-info">
<text class="name">{{ item.name }}</text>
<text class="meta">练过{{ item.trainingReps }}次</text>
</view>
<up-icon name="arrow-right" color="#666" size="16"></up-icon>
</view>
</view>
</view>
</view>
</view>
<view class="footer">
<view class="btn" @click="startTraining">开始训练</view>
</view>
</scroll-view>
<!-- 备注弹窗组件 -->
<beizhu ref="showBeizhuRef" @saveSuccess="handleNoteSave" />
</up-popup>
</template>
<script setup>
import { onMounted, ref, nextTick } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import beizhu from '@/pages/xunji/components/beizhu.vue';
import SupersetsApi from '@/sheep/api/motion/supersets';
import TrainingApi from '@/sheep/api/Training/traininghistory';
import { onShareAppMessage } from '@dcloudio/uni-app';
// 静态配置
const alternativeActions = ref([]); // 平替动作列表接口数据
// 响应式状态
const actionShow = ref(false);
const modeTab = ref(0);
const contentTab = ref(0);
const isFavorite = ref(false);
// 记录当前动作的详细数据
const actionDetail = ref({});
// 记录当前动作的id
const actionId = ref(0);
// 记录是超级组还是动作组 1=动作组,2=超级组
const type = ref(0);
const showBeizhuRef = ref(null);
const historyList = ref([]); // 训练历史列表接口数据
const lostImage =
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png';
// 切换图表模式
const switchModeTab = (index) => {
modeTab.value = index;
};
// 跳转到视频播放的页面
const playVideo = (url) => {
uni.navigateTo({
url: '/pages4/pages/xunji/xunji-shiping?url=' + url,
});
};
// 获取动作收藏状态
const checkExerciseFavorited = async () => {
try {
const res = await ExercisesApi.checkExerciseFavorited(actionId.value);
isFavorite.value = res.data;
} catch (err) {
console.log(err);
}
};
// 收藏
const toggleCollect = async () => {
try {
const status = isFavorite.value ? 0 : 1;
if (type == 1) {
await ExercisesApi.toggleFavorite(actionId.value, status);
} else {
await SupersetsApi.toggleFavorite(actionId.value, status);
}
isFavorite.value = !isFavorite.value;
} catch (err) {
console.log(err);
}
};
// 获取超级组收藏状态
const checkSupersetFavorited = async () => {
try {
const res = await SupersetsApi.checkSupersetFavorited(actionId.value);
isFavorite.value = res.data;
} catch (err) {
console.log(err);
}
};
const startTraining = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-dongzuo-lianxi?id=${actionId.value}&type=${type.value}`,
});
};
const open = (id, typeData) => {
actionId.value = Number(id);
type.value = typeData;
contentTab.value = 0;
// 如何判断是动作还是超级组
if (typeData == 1) {
loadexercisedetail(actionId.value);
loadAlternativeActions(actionId.value);
checkExerciseFavorited();
} else {
loadsuperdetail(actionId.value);
checkSupersetFavorited();
}
loadTrainHistoryDetail(actionId.value);
actionShow.value = true;
};
// 打开备注弹窗
const openBeizhu = () => {
nextTick(() => {
if (showBeizhuRef.value) {
showBeizhuRef.value.open(actionId.value);
}
});
};
// 接收子组件传过来的备注内容
const handleNoteSave = async (content) => {
try {
if (type.value == 2) {
await SupersetsApi.addNotes({
supersetsId: actionId.value,
content: content,
});
} else {
await ExercisesApi.addNotes({
exerciseId: actionId.value,
content: content,
});
}
actionDetail.value.userNote = content;
} catch (e) {
console.log(e);
}
};
// 启用分享菜单
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
});
// #endif
// 定义分享内容
onShareAppMessage((res) => {
// res.from 可区分触发来源:'button'(按钮触发)或 'menu'(右上角菜单触发)[reference:3]
console.log('分享触发来源:', res.from);
return {
title: actionDetail.value.name || '健身动作分享', // 分享标题
// path: `/pages4/pages/xunji/xunji-dongzuo-xiangqing?id=${id.value}`, // 分享路径
path: `/pages/xunji/xunji?currentTab=${3}`,
imageUrl: actionDetail.value.urlImage || lostImage, // 分享图片
};
});
// 加载单个动作详情
const loadexercisedetail = async (id) => {
const response = await ExercisesApi.getExerciseById(id);
actionDetail.value = response.data;
if (actionDetail.value.url3dAnimation) {
modeTab.value = 0;
} else if (actionDetail.value.urlRealPerson) {
modeTab.value = 1;
} else {
modeTab.value = 2;
}
};
// 加载超级组详情
const loadsuperdetail = async (id) => {
const response = await SupersetsApi.getSupersetsInfo(id);
actionDetail.value = response.data;
console.log('显示超级组详情:', actionDetail.value);
};
// 2. 加载平替动作的函数
const loadAlternativeActions = async (id) => {
if (!id || isNaN(Number(id))) {
console.warn('平替动作id非法,跳过请求:', id);
alternativeActions.value = [];
return;
}
try {
const res = await ExercisesApi.getalternatives(id);
if (res.code === 0 && res.data) {
alternativeActions.value = Array.isArray(res.data) ? res.data : [];
} else {
alternativeActions.value = [];
}
} catch (error) {
console.error('加载平替动作失败:', error);
alternativeActions.value = [];
}
};
// 加载训练历史
const loadTrainHistoryDetail = async (id) => {
try {
const res = await TrainingApi.getTrainHistoryList(id);
historyList.value = res.data;
console.log('训练历史列表接口返回结果historyList.value', historyList.value);
} catch (error) {
console.error('加载训练历史失败:', error);
}
};
// 格式化时间
const formatDate = (dateArr) => {
if (!Array.isArray(dateArr) || dateArr.length < 3) return '';
const [year, month, day] = dateArr;
const date = new Date(year, month - 1, day);
const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const week = weekArr[date.getDay()];
return `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')} ${week}`;
};
// 格式化每组数据:自动拼接 weight/reps/duration/distance
const formatSetData = (set) => {
const parts = [];
// 重量
if (set.weight != null && set.weight !== '') {
parts.push(`${set.weight}kg`);
}
// 次数
if (set.reps != null && set.reps !== '') {
parts.push(`${set.reps}次`);
}
// 时长(自动转 时:分:秒)
if (set.duration != null && set.duration !== '') {
parts.push(formatTime(set.duration));
}
// 距离
if (set.distance != null && set.distance !== '') {
parts.push(`${set.distance}m`);
}
return parts.length > 0 ? parts.join(' × ') : '无数据';
};
// 新增:秒数转 00:00:00 格式
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const hh = h > 0 ? String(h).padStart(2, '0') + ':' : '';
const mm = String(m).padStart(2, '0') + ':';
const ss = String(s).padStart(2, '0');
return hh + mm + ss;
};
// 点击超级组内部的动作 → 打开动作详情(复用同一个组件)
const openActionItem = (item) => {
open(item.id, 1);
};
defineExpose({ open });
onMounted(() => {});
</script>
<style lang="scss" scoped>
.action-container {
width: 100%;
height: 80vh;
background-color: #1a1a1a;
color: #ffffff;
.header {
width: 100%;
height: 50vh;
position: relative;
.explain {
width: 100%;
height: 100%;
.media-content {
width: 100%;
height: 100%;
}
.video {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.play-icon {
width: 50px;
height: 50px;
background-color: rgb(216, 209, 209, 0.8);
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
position: absolute;
z-index: 10;
}
}
}
.mode-tabs {
position: absolute;
bottom: 30rpx;
left: 30rpx;
display: flex;
background: rgba(0, 0, 0, 0.6);
padding: 6rpx;
border-radius: 12rpx;
backdrop-filter: blur(10px);
z-index: 99;
.tab-item {
padding: 8rpx 24rpx;
font-size: 24rpx;
color: #999;
transition: all 0.3s;
&.active {
background: #444;
color: #fff;
border-radius: 8rpx;
}
}
}
}
.main {
.main-header {
position: sticky;
top: 0;
z-index: 10;
background: #1a1a1a;
padding: 30rpx 30rpx 0;
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.title {
font-size: 48rpx;
font-weight: bold;
}
.action-icons {
display: flex;
gap: 30rpx;
.share-btn {
background: transparent;
border: none;
padding: 0;
margin: 0;
line-height: 1;
}
}
}
.content-tabs {
display: flex;
gap: 60rpx;
border-bottom: 1rpx solid #333;
.tab-item {
padding-bottom: 20rpx;
font-size: 30rpx;
color: #666;
position: relative;
&.active {
color: #fff;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4rpx;
background: #fedc1f;
border-radius: 2rpx;
}
}
}
}
}
.main-content {
padding: 40rpx 30rpx 160rpx;
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
color: #ddd;
}
.action-list {
.action-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
box-sizing: border-box;
gap: 15rpx;
background-color: #262626;
border-radius: 10rpx;
margin-bottom: 15rpx;
.img {
width: 120rpx;
height: 120rpx;
border-radius: 5rpx;
}
.middle {
flex: 1;
height: 120rpx;
.name {
margin-bottom: 20rpx;
}
.tips {
display: flex;
gap: 10rpx;
align-items: center;
flex-wrap: wrap;
font-size: 20rpx;
.tip {
color: #fedc1f;
}
}
}
}
}
// 要点板块样式
.video-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
margin-bottom: 40rpx;
.video-card {
height: 360rpx;
position: relative;
border-radius: 16rpx;
overflow: hidden;
background: #333;
.video {
width: 100%;
height: 100%;
}
.play-overlay {
position: absolute;
top: 20rpx;
right: 20rpx;
pointer-events: none;
}
}
}
.memo-box {
background: #262626;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 40rpx;
.textarea {
height: 50rpx;
:deep(.u-textarea--disabled) {
background-color: #262626;
}
}
}
.steps-list {
display: flex;
flex-direction: column;
gap: 16rpx;
background: #262626;
padding: 20rpx;
box-sizing: border-box;
border-radius: 16rpx;
.step-text {
font-size: 26rpx;
line-height: 1.6;
color: #bbb;
list-style: none;
}
}
.muscle-map {
width: 100%;
border-radius: 16rpx;
}
.legend {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
margin-top: 20rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 10rpx;
}
.legend-color {
width: 16rpx;
height: 16rpx;
border-radius: 4rpx;
}
.legend-color.primary {
background-color: #ffd700;
}
.legend-color.secondary {
background-color: #999;
}
.legend-text {
font-size: 24rpx;
color: #fff;
}
// 历史记录样式
.stat-card {
background: linear-gradient(135deg, #333, #222);
padding: 40rpx;
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
border: 1rpx solid #444;
.label {
font-size: 24rpx;
color: #888;
}
.value {
font-size: 60rpx;
font-weight: bold;
color: #fedc1f;
margin: 10rpx 0;
}
.date {
font-size: 22rpx;
color: #666;
}
}
.chart-container {
height: 450rpx;
margin: 40rpx 0;
}
.history-item {
background: #262626;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
.count {
display: flex;
gap: 10rpx;
}
.item-header {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: 20rpx;
.date {
font-size: 24rpx;
color: #888;
}
.tag {
// font-size: 20rpx;
// background: #444;
margin: 5rpx 5rpx;
border-radius: 4rpx;
}
}
.group-chips {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 20rpx;
.chip {
// background: #333;
padding: 8rpx 20rpx;
font-size: 24rpx;
display: flex;
align-items: center;
gap: 10rpx;
.idx {
background: #444;
font-weight: bold;
border-radius: 30rpx;
padding: 4rpx 10rpx;
}
}
}
}
// 平替动作样式
.sub-item {
display: flex;
align-items: center;
background: #262626;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
.img {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
margin-right: 24rpx;
}
.sub-info {
flex: 1;
.name {
font-size: 30rpx;
font-weight: 500;
display: block;
}
.meta {
font-size: 22rpx;
color: #666;
margin-top: 8rpx;
}
}
}
}
}
.footer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
bottom: 0;
height: 120rpx;
background-color: #242424;
z-index: 999;
.btn {
width: 80%;
height: 80rpx;
background-color: #fedc1f;
color: #333;
border-radius: 12rpx;
text-align: center;
line-height: 80rpx;
border-radius: 50rpx;
}
}
}
// 动画
.slide-up {
animation: slideUp 0.4s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
... ...
<template>
<view class="container">
<!-- 顶部日期栏 -->
<view class="header-bar">
<!-- <view class="back-btn" @click="goBack">
<uni-icons class="back-arrow" type="left" size="24" color="#333"></uni-icons>
</view> -->
<view class="date-wrapper">
<text class="date-text">{{ displayDate }}</text>
</view>
<button class="date-note-btn" @click.stop="handleDateNote">日期备注</button>
<button class="go-train-btn" @click="handleGoTrain">
<text class="go-train-text">去训练</text>
<text class="go-train-tag">GO</text>
</button>
</view>
<!-- 训练内容区域 -->
<view v-if="resdailyData.id" class="training-container">
<!-- 训练计划头部卡片 -->
<view class="plan-header-card">
<image class="plan-header-img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
mode="aspectFill"></image>
<view class="plan-header-info">
<text class="plan-title">{{ resdailyData.name || '全能玩家DoubleDelight 05' }}</text>
<text class="plan-meta">{{ resdailyData.exerciseCount }}个动作 · {{ resdailyData.totalSets }}组 ·
{{ resdailyData.totalWeight }}kg</text>
</view>
<view class="plan-header-btns">
<button class="plan-btn more-btn" @click="handlePlanMore">更多</button>
<button class="plan-btn copy-btn" @click="handlePlanCopy">复制到</button>
<button class="plan-btn go-train-btn-small" @click="handleGoTrain">
<text class="go-train-text-sm">去训练</text>
<text class="go-train-tag-sm">GO</text>
</button>
</view>
</view>
<!-- 训练动作列表 -->
<view class="action-list">
<view class="action-item" v-for="(item, index) in resdailyData.actionList" :key="index">
<text class="action-index">{{ index + 1 }}</text>
<image class="action-img" :src="item.img ||
'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png'
" mode="aspectFill"></image>
<view class="action-info">
<text class="action-name">{{ item.name }}</text>
<view class="action-sets">
<view class="set-item" v-for="(set, setIdx) in item.sets" :key="setIdx">
<text class="set-index">{{ setIdx + 1 }}</text>
<text class="set-content">{{ set.content }}</text>
<text class="rest-time" v-if="set.restTime">{{ set.restTime }}</text>
</view>
</view>
</view>
<button class="modify-btn" @click="handleModifyAction(item, index)">修改</button>
</view>
</view>
</view>
<!-- 空状态区域 -->
<view v-else class="empty-section">
<image class="empty-img"
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/empty-calendar.png"
mode="aspectFit"></image>
<text class="empty-tip">今天没有安排</text>
<button class="add-train-btn" @click="openAddTrainPopup">+ 添加自助训练</button>
<!-- 底部课程类型卡片 -->
<view class="course-section">
<view class="course-item" v-for="(item, index) in courseList" :key="index"
@click="handleCourseClick(item.type)">
<image class="course-icon" :src="item.icon" mode="aspectFill"></image>
<text class="course-title">{{ item.title }}</text>
<text class="course-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<!-- 添加自助训练底部弹窗 -->
<view v-if="showAddTrainPopup" class="popup-overlay" @click="closeAddTrainPopup">
<view class="popup-bottom" @click.stop>
<!-- 顶部拖动条 -->
<view class="popup-indicator"></view>
<text class="popup-title">添加自助训练</text>
<view class="popup-options">
<button class="popup-option-btn" @click="handleAddFromPlan">
<text>从训练计划中添加</text>
</button>
<button class="popup-option-btn" @click="handleAddFromTemplate">
<text>使用训练模板</text>
</button>
<button class="popup-option-btn" @click="handleFreeTraining">
<text>自由训练</text>
</button>
</view>
</view>
</view>
<!-- 日期备注弹窗 -->
<RiliRiqibeizhu v-model:visible="showRiqibeizhu" />
</view>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import dailytemplateApi from '@/sheep/api/Template/Dailytemplate';
import RiliRiqibeizhu from '@/pages/xunji/components/rili-components/rili-riqibeizhu.vue'
const props = defineProps({
date: {
type: String,
default: ''
}
})
const showAddTrainPopup = ref(false);
const selectedDate = ref('');
const displayDate = ref('');
const resdailyData = ref({
id: '',
name: '',
exerciseCount: 0,
totalSets: 0,
totalWeight: 0,
actionList: []
});
const showRiqibeizhu = ref(false)
// 模拟接口返回的 data 数据
const mockTemplateData = [
{
id: 10001,
name: "上肢力量基础训练模板",
urlCover: "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png",
exerciseCount: 5,
totalSets: 15,
totalWeight: 625,
units: [
{
unitId: 20001,
unitName: "胸肌训练单元",
unitType: 1,
setCount: 3,
sortOrder: 1,
supersetId: null,
exercises: [
{
unitId: 20001,
exerciseId: 30001,
exerciseType: 1,
setCount: 3,
weight: 50,
reps: 12,
distance: null,
duration: null,
restTime: 60,
innerOrder: 1,
exerciseName: "杠铃卧推",
exerciseCover: "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png",
urlImage: "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png",
sets: [
{
setIndex: 1,
weight: 40,
reps: 12,
duration: null,
distance: null,
restTime: 60
},
{
setIndex: 2,
weight: 50,
reps: 10,
duration: null,
distance: null,
restTime: 60
},
{
setIndex: 3,
weight: 55,
reps: 8,
duration: null,
distance: null,
restTime: 90
}
]
}
]
}
]
}
]
// 没有训练模板时推荐课程
const courseList = ref([
{
type: 'group',
title: '团课',
desc: '大家一起练',
icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/group-course.png'
},
{
type: 'private',
title: '私教',
desc: '1对1专人指导',
icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/private-course.png'
},
{
type: 'smallClass',
title: '小班课',
desc: '28天极速瘦身',
icon: 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/small-class.png'
}
]);
// 把模拟数据 转成 页面能渲染的数据
const formatTemplateData = (data) => {
if (!data) return { actionList: [] };
// 模板的动作列表
const actionList = [];
// 循环 unit
(data.units || []).forEach(unit => {
// 循环每个动作
(unit.exercises || []).forEach(ex => {
actionList.push({
name: ex.exerciseName,
img: ex.exerciseCover,
sets: ex.sets.map(s => ({
content: `${s.weight}kg × ${s.reps}次`,
restTime: s.restTime ? `${s.restTime}s` : ''
}))
});
});
});
return {
id: data.id,
name: data.name,
exerciseCount: data.exerciseCount,
totalSets: data.totalSets,
totalWeight: data.totalWeight,
actionList
};
};
// 加载每日训练模板数据
const loaddailytemplate = async () => {
if (!selectedDate.value) return; // 没有日期就不请求
console.log('【子组件】开始加载数据...');
try {
// const resdaily = await dailytemplateApi.getdailytemplate(String(selectedDate.value));
// resdailyData.value = resdaily.data || {};
resdailyData.value = formatTemplateData(mockTemplateData[0]);
console.log('【子组件】加载完成,resdailyData:', resdailyData.value);
console.log('打印resdailyData', resdailyData.value)
} catch (error) {
console.error('加载训练模板失败:', error);
resdailyData.value = {};
}
};
// 弹窗控制
const openAddTrainPopup = () => {
showAddTrainPopup.value = true;
};
const closeAddTrainPopup = () => {
showAddTrainPopup.value = false;
};
// 弹窗选项点击事件
const handleAddFromPlan = () => {
uni.showToast({ title: '从训练计划添加', icon: 'none' });
closeAddTrainPopup();
};
const handleAddFromTemplate = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-rili-tianjia-moban' });
closeAddTrainPopup();
};
const handleFreeTraining = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-lianxi' });
closeAddTrainPopup();
};
// 返回按钮逻辑
const goBack = () => {
uni.navigateBack({
delta: 1,
fail: () => {
uni.redirectTo({ url: '/pages/xunji/xunji-rili' });
},
});
};
// 日期格式化(兼容iOS)
const formatDateWithWeek = (dateStr) => {
if (!dateStr) return '';
const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
const iosSafeDate = new Date(dateStr.replace(/-/g, '/'));
const year = iosSafeDate.getFullYear();
const month = String(iosSafeDate.getMonth() + 1).padStart(2, '0');
const day = String(iosSafeDate.getDate()).padStart(2, '0');
const week = weekMap[iosSafeDate.getDay()];
return `${year}/${month}/${day} 周${week}`;
};
// 打开日期备注组件
const handleDateNote = () => {
showRiqibeizhu.value = true
}
// 监听父组件传进来的 date 变化
// 子组件 script setup 中替换 watch
watch(
() => props.date,
(newDate) => {
if (newDate && newDate.trim()) {
console.log('子组件监听到日期变化:', newDate);
selectedDate.value = newDate;
displayDate.value = formatDateWithWeek(newDate);
loaddailytemplate();
}
},
{ immediate: true }
);
onMounted(() => {
console.log('【子组件】已挂载!!!');
console.log('【子组件】props.date:', props.date);
});
</script>
<style scoped>
/* 全局容器 */
.container {
width: 100vw;
min-height: 85vh;
background-color: #f7f8fa;
box-sizing: border-box;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* 顶部日期栏 */
.header-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
background-color: #fff;
position: sticky;
top: 0;
z-index: 10;
gap: 16rpx;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.date-wrapper {
flex-grow: 1;
text-align: left;
}
.date-text {
font-size: 28rpx;
}
.date-note-btn {
font-size: 28rpx;
color: #333;
background: #f2f3f5;
border-radius: 40rpx;
padding: 12rpx 28rpx;
border: none;
margin-right: 0;
flex-shrink: 0;
}
.go-train-btn {
display: flex;
align-items: center;
background: #ffc107;
border-radius: 40rpx;
padding: 12rpx 24rpx;
border: none;
flex-shrink: 0;
}
.go-train-text {
font-size: 28rpx;
color: #000;
margin-right: 8rpx;
font-weight: 500;
}
.go-train-tag {
font-size: 22rpx;
color: #ffc107;
background: #000;
border-radius: 50%;
padding: 4rpx 8rpx;
font-weight: bold;
}
/* 训练计划头部卡片 */
.plan-header-card {
display: flex;
flex-direction: column;
background-color: #2a2a2a;
color: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
margin: 20rpx;
margin-bottom: 24rpx;
}
.plan-header-img {
display: none;
}
.plan-header-info {
flex-grow: 1;
margin-bottom: 32rpx;
}
.plan-title {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
color: #fff;
}
.plan-meta {
font-size: 26rpx;
color: #ccc;
display: block;
}
.plan-header-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 20rpx;
justify-content: flex-start;
}
.plan-btn {
font-size: 26rpx;
border-radius: 40rpx;
border: none;
padding: 12rpx 28rpx;
line-height: 1;
}
.more-btn,
.copy-btn {
background: #fff;
color: #000;
border: none;
}
.go-train-btn-small {
background: #ffc107;
color: #000;
display: flex;
align-items: center;
padding: 12rpx 20rpx;
}
.go-train-text-sm {
font-size: 24rpx;
margin-right: 4rpx;
font-weight: 500;
}
.go-train-tag-sm {
font-size: 20rpx;
background: #000;
color: #ffc107;
border-radius: 4rpx;
padding: 2rpx 6rpx;
font-weight: bold;
}
/* 训练动作列表 */
.action-list {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 0 20rpx;
}
.action-item {
display: flex;
align-items: flex-start;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx 20rpx;
position: relative;
gap: 16rpx;
}
.action-index {
font-size: 34rpx;
color: #333;
font-weight: 600;
line-height: 80rpx;
width: 40rpx;
text-align: center;
flex-shrink: 0;
}
.action-img {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.action-info {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.action-name {
font-size: 32rpx;
color: #111;
font-weight: 500;
display: block;
margin-bottom: 8rpx;
}
.action-sets {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.set-item {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 26rpx;
color: #666;
padding-left: 8rpx;
}
.set-index {
display: none;
}
.set-content {
flex-grow: 1;
color: #333;
}
.set-content::before {
content: "热";
display: inline-block;
font-size: 22rpx;
color: #999;
background: #f2f3f5;
border-radius: 50%;
width: 32rpx;
height: 32rpx;
line-height: 32rpx;
text-align: center;
margin-right: 12rpx;
}
.rest-time {
font-size: 24rpx;
color: #999;
margin-left: 16rpx;
flex-shrink: 0;
}
.modify-btn {
position: absolute;
top: 24rpx;
right: 20rpx;
font-size: 26rpx;
color: #333;
background: #f2f3f5;
border: none;
border-radius: 40rpx;
padding: 8rpx 20rpx;
line-height: 1;
}
/* 空状态区域 */
.empty-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 20rpx 60rpx;
}
.empty-img {
width: 280rpx;
height: 280rpx;
margin-bottom: 30rpx;
}
.empty-tip {
font-size: 32rpx;
color: #666;
margin-bottom: 60rpx;
}
.add-train-btn {
font-size: 30rpx;
color: #000;
background: #ffc107;
border-radius: 40rpx;
padding: 20rpx 80rpx;
border: none;
margin-bottom: 60rpx;
}
/* 底部课程类型区域 */
.course-section {
display: flex;
justify-content: space-around;
padding: 0 20rpx;
width: 100%;
box-sizing: border-box;
}
.course-item {
display: flex;
flex-direction: column;
align-items: center;
width: 220rpx;
background: #fff;
border-radius: 12rpx;
padding: 30rpx 0;
}
.course-icon {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
margin-bottom: 16rpx;
}
.course-title {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.course-desc {
font-size: 24rpx;
color: #999;
}
/* 添加自助训练底部弹窗 */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 9999;
display: flex;
align-items: flex-end;
}
.popup-bottom {
width: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 30rpx;
box-sizing: border-box;
}
.popup-indicator {
width: 80rpx;
height: 8rpx;
background: #ddd;
border-radius: 4rpx;
margin: 0 auto 30rpx;
}
.popup-title {
font-size: 34rpx;
color: #333;
text-align: center;
font-weight: bold;
display: block;
margin-bottom: 30rpx;
}
.popup-options {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.popup-option-btn {
width: 100%;
height: 80rpx;
font-size: 30rpx;
color: #333;
background: #f5f5f5;
border: none;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
/* 按钮通用重置 */
button {
line-height: 1;
}
button::after {
border: none;
}
</style>
\ No newline at end of file
... ...
<template>
<u-popup :show="visible" mode="bottom" :round="24" :closeable="false" :custom-style="{ height: '80vh' }"
@close="handleClose">
<view class="date-note-popup">
<!-- 顶部导航栏 -->
<view class="popup-header">
<view class="close-btn" @click="handleClose">
<text class="close-icon">×</text>
</view>
<text class="popup-title">新增日程备注</text>
<button class="save-btn" @click="handleSave">保存</button>
</view>
<!-- 颜色选择区 -->
<view class="color-section">
<text class="section-title">选择显示颜色</text>
<view class="color-list">
<view v-for="(color, index) in colorList" :key="index" class="color-item"
:class="{ active: selectedColorIndex === index }" :style="{ backgroundColor: color }"
@click="selectedColorIndex = index" />
</view>
</view>
<!-- 输入框区域 -->
<view class="input-section">
<text class="section-title">日程标题</text>
<textarea v-model="noteContent" class="note-textarea" placeholder="输入当天的日程情况,如休息日、生病受伤了、经期等"
placeholder-class="placeholder" />
</view>
<!-- 历史备注区域 -->
<view class="history-section">
<view class="history-header">
<text class="section-title history-title">历史备注</text>
<text class="history-tip">点击填充到文本内容</text>
</view>
<view class="history-tags">
<view v-for="(tag, index) in historyTags" :key="index" class="tag-item"
:style="{ backgroundColor: tag.color }" @click="fillNote(tag.text)">
<text class="tag-text">+{{ tag.text }}</text>
</view>
</view>
</view>
</view>
</u-popup>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:visible', 'save', 'close']);
// 响应式数据
const noteContent = ref('');
const selectedColorIndex = ref(0);
// 颜色列表(和截图一致)
const colorList = ref([
'#ff9944',
'#ff7766',
'#ddaaff',
'#aabbff',
'#cccccc'
]);
// 历史备注数据(模拟截图效果)
const historyTags = ref([
{ text: '休息日', color: '#ff9944' },
{ text: '累了', color: '#ff9944' },
]);
// 事件处理
const handleClose = () => {
emit('update:visible', false);
emit('close');
};
const handleSave = () => {
const data = {
content: noteContent.value,
color: colorList.value[selectedColorIndex.value]
};
emit('save', data);
handleClose();
};
const fillNote = (text) => {
noteContent.value = text;
};
</script>
<style scoped lang="scss">
.date-note-popup {
width: 100%;
height: 100%;
background-color: #fff;
box-sizing: border-box;
padding: 24rpx 32rpx;
}
/* 顶部导航 */
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
.close-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
font-size: 40rpx;
color: #333;
line-height: 1;
}
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
color: #111;
text-align: center;
display: block;
flex: 1;
}
.save-btn {
margin-right: 0;
background: #ffd100;
color: #000;
border: none;
border-radius: 20rpx;
padding: 5rpx 25rpx;
font-size: 28rpx;
font-weight: 500;
}
}
/* 通用标题 */
.section-title {
font-size: 30rpx;
color: #111;
font-weight: 500;
display: block;
margin-bottom: 24rpx;
}
/* 颜色选择区 */
.color-section {
margin-bottom: 40rpx;
.color-list {
display: flex;
gap: 40rpx;
.color-item {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
border: 4rpx solid transparent;
&.active {
border-color: #333;
}
}
}
}
/* 输入框区域 */
.input-section {
margin-bottom: 40rpx;
.note-textarea {
width: 100%;
height: 200rpx;
background: #f7f8fa;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
color: #333;
line-height: 1.5;
box-sizing: border-box;
.placeholder {
color: #999;
}
}
}
.history-title {
margin-bottom: 0 !important;
/* 覆盖原来的 margin-bottom */
}
/* 历史备注区域 */
.history-section {
.history-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
margin-top: 0;
.history-tip {
font-size: 24rpx;
color: #999;
}
}
.history-tags {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.tag-item {
border-radius: 8rpx;
padding: 16rpx 24rpx;
display: flex;
align-items: center;
.tag-text {
font-size: 26rpx;
color: #fff;
}
}
}
}
</style>
\ No newline at end of file
... ...
<template>
<view class="time-select">
<view class="tab-container">
<view
class="tab-item"
:class="{ active: currentDateType === 'week' }"
@click="switchDateType('week')"
>
</view>
<view
class="tab-item"
:class="{ active: currentDateType === 'month' }"
@click="switchDateType('month')"
>
</view>
</view>
<view class="date-stepper">
<view class="arrow-icon" @click="handlePrev">
<up-icon name="arrow-left" color="#333" size="12" bold></up-icon>
</view>
<view class="date-display"> {{ dateRangeText }} </view>
<!-- 修改: 根据 isNextDisabled 判断是否隐藏下一个按钮 -->
<view class="arrow-icon" @click="handleNext" v-if="!isNextDisabled">
<up-icon name="arrow-right" color="#333" size="12" bold></up-icon>
</view>
<view v-else class="occupy"></view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
// 当前日期类型: 'week' | 'month'
const currentDateType = ref('week');
// 基准日期,用于计算范围
const baseDate = ref(new Date());
// 格式化日期辅助函数 YYYY/MM/DD
const formatDate = (date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}/${m}/${d}`;
};
// 计算显示的日期范围文本
const dateRangeText = computed(() => {
const current = new Date(baseDate.value);
if (currentDateType.value === 'week') {
// 计算本周的周一和周日
const day = current.getDay();
const diffToMonday = day === 0 ? -6 : 1 - day; // 调整到周一
const startOfWeek = new Date(current);
startOfWeek.setDate(current.getDate() + diffToMonday);
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
return `${formatDate(startOfWeek)} - ${formatDate(endOfWeek)}`;
} else {
// 计算本月的一号和最后一天
const startOfMonth = new Date(current.getFullYear(), current.getMonth(), 1);
const endOfMonth = new Date(current.getFullYear(), current.getMonth() + 1, 0);
return `${formatDate(startOfMonth)} - ${formatDate(endOfMonth)}`;
}
});
// 新增: 计算下一个日期是否不可用(即是否为未来日期)
const isNextDisabled = computed(() => {
const newDate = new Date(baseDate.value);
if (currentDateType.value === 'week') {
newDate.setDate(newDate.getDate() + 7);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
// 重置时分秒进行比较
const now = new Date();
now.setHours(0, 0, 0, 0);
const checkDate = new Date(newDate);
checkDate.setHours(0, 0, 0, 0);
return checkDate > now;
});
// 切换周/月
const switchDateType = (type) => {
currentDateType.value = type;
};
// 上一段时间
const handlePrev = () => {
const newDate = new Date(baseDate.value);
if (currentDateType.value === 'week') {
newDate.setDate(newDate.getDate() - 7);
} else {
newDate.setMonth(newDate.getMonth() - 1);
}
baseDate.value = newDate;
};
// 下一段时间
const handleNext = () => {
// 虽然按钮已隐藏,但保留逻辑判断以防直接调用或其他边界情况
if (isNextDisabled.value) {
return;
}
const newDate = new Date(baseDate.value);
if (currentDateType.value === 'week') {
newDate.setDate(newDate.getDate() + 7);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
baseDate.value = newDate;
};
</script>
<style scoped lang="scss">
.time-select {
flex-shrink: 0; /* 修改: 防止顶部选择器被压缩 */
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
// 周月切换 Tab
.tab-container {
width: 100%;
height: 72rpx;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8rpx;
background-color: #eee;
border-radius: 12rpx;
padding: 6rpx;
box-sizing: border-box;
.tab-item {
display: flex;
align-items: center;
justify-content: center;
color: #8c8c8c;
font-size: 28rpx;
transition: all 0.2s ease;
&.active {
background-color: #ffffff;
color: #1a1a1a;
font-weight: 600;
border-radius: 8rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
}
}
// 日期翻页器
.date-stepper {
margin-top: 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.date-display {
margin: 0 40rpx;
font-size: 28rpx;
font-weight: 500;
color: #333;
font-variant-numeric: tabular-nums; // 防止数字切换时抖动
}
.arrow-icon {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
border-radius: 50%;
transition: background-color 0.2s;
}
}
.occupy {
width: 56rpx;
height: 56rpx;
}
}
</style>
... ...
<template>
<view class="exercise-page" @click="closeAddMenu">
<!-- 搜索框 -->
<view class="search-bar">
<!-- <view class="search">
<uni-icons type="search" size="18"></uni-icons>
<input class="search-input" type="text" placeholder="搜索动作名称" @input="onSearch" />
</view> -->
<view class="add-btn" @click.stop="addExercise">
<uni-icons type="plus" size="35" color="#333"></uni-icons>
<view class="floating-menu" v-show="addshow" @click.stop>
<view class="menu-item">
<uni-icons type="plus" size="24" color="#333"></uni-icons>
<text class="menu-text" @click="addnewmotion">新增动作</text>
</view>
<view class="menu-item">
<uni-icons type="link" size="24" color="#333"></uni-icons>
<text class="menu-text" @click="addnewsupersets">新增超级组</text>
</view>
<view class="menu-item">
<uni-icons type="list" size="24" color="#333"></uni-icons>
<text class="menu-text" @click="actionmanagement">动作目录管理</text>
</view>
</view>
</view>
</view>
<!-- 浮动菜单 -->
<!-- 左右布局 -->
<view class="layout-container">
<scroll-view scroll-y class="left-nav" enable-flex>
<view
class="nav-item"
@click="handleCollectClick('collect')"
:class="{ active: activeNav === 'collect' }"
>
收藏
</view>
<view
v-for="nav in navItems"
:key="nav.id"
class="nav-item"
:class="{ active: activeNav === nav.id }"
@click="switchNav(nav.id)"
>
{{ nav.name }}
</view>
</scroll-view>
<!-- 小动作列表 -->
<scroll-view scroll-y class="right-content" enable-flex>
<view class="tip" v-if="motionPart.length > 0">
<view
class="item"
@click="handlePartClick('')"
:class="{ active: activeMotionPart == '' }"
>全部</view
>
<view
class="item"
v-for="item in motionPart"
:key="item.id"
:class="{ active: activeMotionPart == item.id }"
@click="handlePartClick(item.id)"
>
{{ item.name }}
</view>
</view>
<view class="exercise-grid">
<view class="content" v-if="exercises.length > 0 || superGroupInfo.length > 0">
<view class="equipment-list">
<view class="equipment-item" v-for="item in exercises" :key="item.equipmentId">
<view class="equipment-name"> {{ item.equipmentName }} </view>
<view class="action-list">
<view class="action-item" v-for="e in item.exercises" :key="e.id">
<image
:src="e.url3dAnimation"
mode="aspectFill"
lazy-load
class="action-img"
@click="goToDetail(e.id, 1)"
></image>
<view class="action-name">{{ e.name }}</view>
<view class="trainingReps" v-if="e.trainingReps">{{ e.trainingReps }}次</view>
</view>
</view>
</view>
<view class="supers" v-if="superGroupInfo.length > 0">
<view class="supers-name"> 超级组 </view>
<view
class="super"
v-for="item in superGroupInfo"
:key="item.id"
@click="goToDetail(item.id, 2)"
>
<image
src="https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260507/超级组_1778117889451.png"
mode="aspectFill"
lazy-load
class="super-img"
></image>
<view class="right">
<view class="super-name">{{ item.name }}</view>
<view class="parts">{{ item.primaryMuscles.join() }}</view>
</view>
</view>
</view>
<!-- <view class="add-super">
<view class="header">
<up-icon name="plus" color="#000" size="15" bold></up-icon>
<text>添加自定义动作</text>
</view>
<view class="tip">添加官方库没有的动作</view>
</view> -->
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无动作数据</text>
</view>
</view>
</scroll-view>
</view>
<!-- 详情弹窗 -->
<dongzuoXianqing ref="actionDetailRef" />
</view>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
import ExercisesApi from '@/sheep/api/motion/exercises';
import SupersetsApi from '@/sheep/api/motion/supersets';
import dongzuoXianqing from '@/pages/xunji/components/dongzuo-xianqing.vue';
import { useActionStore } from '@/sheep/store/action';
const actionStore = useActionStore();
// 响应式数据
const activeNav = ref('');
const navItems = ref([]);
const exercises = ref([]);
const superGroupInfo = ref([]);
const motionPart = ref([]);
const activeMotionPart = ref('');
const currentCategoryId = ref('');
const allExercises = ref([]);
const addshow = ref(false);
const superFavoriteList = ref([]);
const actionDetailRef = ref(null);
// 关闭菜单
const closeAddMenu = () => {
addshow.value = false;
};
// 部位筛选
const handlePartClick = async (id) => {
try {
activeMotionPart.value = id;
const exerciseRes = await ExercisesApi.getexercises({
categoriesId: activeNav.value,
subCategoriesId: id,
});
exercises.value = exerciseRes.data || [];
} catch (e) {
console.error('加载动作失败:', e);
}
};
// 收藏
const handleCollectClick = async (id) => {
activeNav.value = id;
loadCollectList();
loadSuperFavoriteList();
};
// 动作收藏
const loadCollectList = async () => {
try {
const res = await ExercisesApi.getFavoriteExercises();
exercises.value = res.data || [];
} catch (err) {
console.error('加载动作收藏失败', err);
}
};
// 超级组收藏
const loadSuperFavoriteList = async () => {
try {
const res = await SupersetsApi.getFavoriteSuperset();
superGroupInfo.value = res.data || [];
} catch (err) {
superFavoriteList.value = [];
console.error('加载超级组收藏失败', err);
}
};
// 加载分类
const loadCategories = async () => {
try {
await actionStore.getloadCategories();
navItems.value = actionStore.showCategories;
if (navItems.value.length > 0) {
switchNav(navItems.value[0].id);
}
} catch (error) {
console.error('获取分类失败:', error);
}
};
// 加载动作
const loadExercises = async (categoriesId) => {
try {
const partRes = await ExercisesApi.getMotionPart(categoriesId);
motionPart.value = partRes.data || [];
console.log(motionPart.value, 'motionPart.value');
const exerciseRes = await ExercisesApi.getexercises({ categoriesId });
exercises.value = exerciseRes.data;
} catch (error) {
console.error('加载动作失败:', error);
}
};
// 切换导航
const switchNav = (id) => {
if (activeNav.value === id) return;
activeNav.value = id;
activeMotionPart.value = '';
if (id == 'super') {
exercises.value = [];
motionPart.value = [];
loadsupersetsinfo();
} else {
superGroupInfo.value = [];
loadExercises(id);
}
};
// 搜索
const onSearch = (e) => {
const searchText = e.detail.value.trim();
if (!currentCategoryId.value) return;
searchText ? searchExercises(searchText) : loadExercises(currentCategoryId.value);
};
const searchExercises = (keyword) => {
const key = keyword.toLowerCase().trim();
exercises.value = allExercises.value.filter((item) => {
return item.name && item.name.toLowerCase().includes(key);
});
};
const loadsupersetsinfo = async () => {
try {
const response = await SupersetsApi.getsupersets();
superGroupInfo.value = response.data || [];
} catch (error) {
console.error('获取超级组失败:', error);
}
};
// 页面跳转
const goToDetail = (id, type) => {
nextTick(() => {
actionDetailRef.value.open(id, type);
});
};
const addExercise = () => {
addshow.value = !addshow.value;
};
const addnewmotion = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/xunji-dongzuo-xinzeng' });
};
const addnewsupersets = () => {
uni.navigateTo({
url: `/pages4/pages/xunji/dongzuo-xinzengchaojizu?navItems=${encodeURIComponent(
JSON.stringify(navItems.value),
)}`,
});
};
const actionmanagement = () => {
uni.navigateTo({ url: '/pages4/pages/xunji/dongzuo-muluguanli' });
};
onMounted(() => {
loadCategories();
});
</script>
<style lang="scss" scoped>
.exercise-page {
background-color: white;
box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
.search-bar {
box-sizing: border-box;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 20rpx;
// background-color: #f5f5f5;
border-radius: 12rpx;
.search {
display: flex;
align-items: center;
padding: 10rpx 20rpx;
flex: 1;
border-radius: 30rpx;
background-color: #f5f5f5;
.search-icon {
width: 30rpx;
height: 30rpx;
}
.search-input {
flex: 1;
padding-left: 15rpx;
font-size: 26rpx;
color: #333;
border: none;
outline: none;
background: transparent;
}
}
.add-btn {
width: 60rpx;
height: 60rpx;
background-color: transparent;
border: none;
margin-left: 10rpx;
display: flex;
justify-content: center;
align-items: center;
z-index: 99;
position: relative;
.plus-icon {
width: 40rpx;
height: 40rpx;
}
.floating-menu {
position: absolute;
top: 30rpx;
right: 30rpx;
width: 300rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
padding: 20rpx;
z-index: 100;
.menu-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
.u-icon {
margin-right: 16rpx;
}
.menu-text {
font-size: 28rpx;
color: #333;
}
&:last-child {
border-bottom: none;
}
}
}
}
}
.layout-container {
box-sizing: border-box;
display: flex;
height: calc(82vh + 25rpx);
background-color: #f5f5f5;
.left-nav {
width: 160rpx;
background-color: white;
border-right: 1px solid #eee;
// 添加底部 padding,避免最后一项被底部导航栏遮挡
padding-bottom: 20rpx;
// #ifdef MP-WEIXIN
// 微信小程序端可能需要更多底部空间
padding-bottom: 100rpx;
// #endif
.nav-item {
padding: 24rpx 0;
text-align: center;
font-size: 26rpx;
color: #333;
}
.active {
color: #16ad40;
background-color: #e1f6e9;
border-right: 3px solid #16ad40;
font-weight: bold;
}
}
.right-content {
box-sizing: border-box;
flex: 1;
padding: 20rpx;
height: 100%;
// 添加底部 padding,避免内容被底部导航栏遮挡
// 底部导航栏高度约 100rpx(50px),加上安全区域
padding-bottom: 20rpx;
// #ifdef MP-WEIXIN
// 微信小程序端可能需要更多底部空间
padding-bottom: 100rpx;
// #endif
.tip {
display: flex;
padding-left: 20rpx;
align-items: center;
column-gap: 15rpx;
margin-bottom: 20rpx;
.item {
margin: 0;
padding: 12rpx 20rpx;
font-size: 22rpx;
line-height: 1;
background-color: #fff;
border-radius: 20rpx;
&.active {
background-color: #d8ede0;
color: #43b05e;
}
}
}
.exercise-grid {
display: flex;
flex-direction: column;
gap: 24rpx;
.title {
font-weight: 600;
padding-left: 20rpx;
font-size: 30rpx;
}
.content {
width: 100%;
// display: grid;
// grid-template-columns: repeat(2, 1fr);
// /* 改为3列布局 */
// gap: 16rpx;
// .exercise-item {
// display: flex;
// height: 150rpx;
// /* 减小项目高度 */
// flex-direction: column;
// align-items: center;
// justify-content: center;
// background-color: white;
// padding: 16rpx 8rpx;
// box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
// .exercise-img {
// width: 90%;
// height: 140rpx;
// /* 固定图片高度 */
// border-radius: 8rpx;
// margin-bottom: 12rpx;
// }
// .exercise-title {
// font-size: 24rpx;
// color: #333;
// text-align: center;
// line-height: 1.3;
// max-width: 100%;
// text-overflow: ellipsis;
// display: -webkit-box;
// -webkit-box-orient: vertical;
// }
// }
.equipment-item {
width: 100%;
margin-bottom: 40rpx;
.equipment-name {
color: #000;
font-size: 32rpx;
margin-bottom: 20rpx;
}
.action-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10rpx;
.action-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10rpx;
background: #fff;
border-radius: 10rpx;
padding: 20rpx;
box-sizing: border-box;
position: relative;
.action-img {
width: 100%;
height: 250rpx;
border-radius: 10rpx;
}
.trainingReps {
position: absolute;
top: 20rpx;
right: 20rpx;
z-index: 1;
font-size: 22rpx;
}
}
}
}
.supers-name {
color: #000;
font-size: 32rpx;
margin-bottom: 20rpx;
}
.super {
display: flex;
align-items: center;
background: #fff;
padding: 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
gap: 20rpx;
font-size: 26rpx;
margin-bottom: 20rpx;
.super-img {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
}
.super-name {
margin-bottom: 10rpx;
}
}
.add-super {
background: #fff;
padding: 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
.header {
display: flex;
gap: 10rpx;
align-items: center;
margin-bottom: 20rpx;
}
.tip {
margin: 0rpx;
padding: 0rpx;
font-size: 22rpx;
}
}
}
.nav-item .category-group {
/* 超级组特殊样式 */
background-color: #f0f9ff;
font-weight: bold;
border-top: 1px solid #eee;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
}
</style>
... ...
<!--
顶部两个下拉筛选器,部位筛选和场景筛选
<!-- 它是给用户选训练计划模板的页面比如:列表显示的是模板大类,里面包含好几个模板
<!-- 还未实现 部位和场景筛选到对应的模板 -->
<!-- 第一层 模板大类 -->
<template>
<view class="template-page" @click="closeAllDropdowns">
<!-- 筛选按钮 -->
<view class="filter-section" @click.stop>
<!-- 部位筛选 -->
<view class="filter-wrapper">
<view class="filter-btn" :class="{ active: showPartDropdown }" @click="togglePartDropdown">
<text class="text">{{ activePart }}</text>
<uni-icons :type="showPartDropdown ? 'up' : 'down'" size="13"></uni-icons>
</view>
<!-- 部位下拉菜单 -->
<view class="dropdown-menu" v-if="showPartDropdown">
<view v-for="(item, index) in partList" :key="index" class="dropdown-item"
:class="{ selected: activePart === item.title }" @click="selectPart(item)">
{{ item.title }}
</view>
</view>
</view>
<!-- 场景筛选 -->
<view class="filter-wrapper">
<view class="filter-btn" :class="{ active: showSceneDropdown }" @click="toggleSceneDropdown">
<text class="text">{{ activeScene }}</text>
<uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="13"></uni-icons>
</view>
<!-- 场景下拉菜单 -->
<view class="dropdown-menu" v-if="showSceneDropdown">
<view v-for="(item, index) in sceneList" :key="index" class="dropdown-item"
:class="{ selected: activeSceneId === item.id }" @click="selectScene(item)">
{{ item.title }}
</view>
</view>
</view>
</view>
<!-- 模板列表 (根据状态自动切换显示) -->
<scroll-view class="template-list" enable-flex scroll-y>
<!-- 用父容器包裹 v-if 分支,解决 key 冲突 -->
<view v-if="!isFiltering">
<view v-for="(item, index) in templateList" :key="index" class="template-item">
<image :src="item.urlCover" mode="aspectFill" class="template-img"></image>
<view>
<view class="template-content" @click="gototemplate(item)">
<view class="template-title">{{ item.name }}</view>
<view class="template-count">{{ item.templatesCount }}个模板</view>
<view class="template-desc">{{ item.description }}</view>
</view>
</view>
</view>
</view>
<!-- 用父容器包裹 v-else 分支,解决 key 冲突 -->
<view v-else>
<view v-for="(template, index) in filteredTemplateList" :key="index" class="template-item">
<image :src="template.urlCover" mode="aspectFill" class="template-img"></image>
<view>
<view class="template-content" @click="goToTemplateDetail(template)">
<view class="template-title">{{ template.name }}</view>
<!-- 空数据判断移到了这里 -->
<view class="template-count" v-if="template.primaryMuscles">
主要训练部位:{{ template.primaryMuscleNames.join(', ') || '无' }}
</view>
<view class="template-desc" v-if="template.secondaryMuscles">
次要训练部位:{{ template.secondaryMuscleNames.join(', ') || '无' }}
</view>
</view>
</view>
</view>
<!-- 空数据提示:移到 view-else 内部,且作为独立块 -->
<view v-if="filteredTemplateList.length === 0" class="empty-tip">
暂无符合条件的训练模板
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import TemplatesApi from '@/sheep/api/Template/Templates';
const templateList = ref([]);
// 1. 存储【筛选出来的具体模板】
const filteredTemplateList = ref([]);
// 2. 控制页面显示:false=显示大类,true=显示筛选后的模板
const isFiltering = ref(false);
// 3. 场景ID(接口需要,你之前漏了存场景ID!)
const activeSceneId = ref('0');
// 下拉框部位筛选相关状态
const partList = ref([]);
const rawPartData = ref([]); // 存储接口返回的原始部位数据
const showPartDropdown = ref(false);
const activePart = ref('不限');
const activePartId = ref('0'); // 新增:默认选中"不限",id=0
// 场景筛选相关状态
const sceneList = ref([
{ id: '0', title: '不限' },
{ id: '1', title: '健身房' },
{ id: '2', title: '仅哑铃' },
{ id: '3', title: '仅哑铃+杠铃' },
// { id: 4, title: '办公室' },
]);
const showSceneDropdown = ref(false);
const activeScene = ref('不限');
// 获取模板大类列表
const TemplatesList = async () => {
try {
const response = await TemplatesApi.getTemplates();
templateList.value = response.data;
console.log('模板大类接口返回数据', templateList.value)
} catch (error) {
console.log('模板大类获取失败', error);
}
};
// 获取所有细分锻炼部位(接口)
const getPartCategories = async () => {
try {
const res = await TemplatesApi.getPartAllCategories();
if (res.code === 0) {
rawPartData.value = res.data;
console.log('部位分类接口返回数据rawPartData.value:', rawPartData.value)
// 下拉列表赋值
partList.value = [
{ title: '不限', id: '0' }, // 给"不限"加id='0',方便后续筛选
...res.data.map(item => ({
title: item.name, // 接口返回的name作为显示标题
id: String(item.id) // 保存接口的id,用于后续筛选传参
}))
];
}
} catch (error) {
console.log('部位列表获取失败', error);
}
};
// 切换部位下拉菜单
const togglePartDropdown = () => {
showPartDropdown.value = !showPartDropdown.value;
showSceneDropdown.value = false;
};
// 切换场景下拉菜单
const toggleSceneDropdown = () => {
showSceneDropdown.value = !showSceneDropdown.value;
showPartDropdown.value = false;
};
// 选择部位
const selectPart = (item) => {
activePart.value = item.title;
activePartId.value = item.id;
showPartDropdown.value = false;
// 加这一行:选完部位立刻筛选
doFilter();
};
// 选择场景
const selectScene = (item) => {
activeScene.value = item.title;
// 关键:把选中的场景ID存起来
activeSceneId.value = item.id;
showSceneDropdown.value = false;
// 选择完,立即执行筛选!
doFilter();
};
// 点击页面其他地方关闭所有下拉菜单
const closeAllDropdowns = () => {
showPartDropdown.value = false;
showSceneDropdown.value = false;
};
// 核心:执行筛选逻辑
const doFilter = async () => {
const musclesId = activePartId.value; // 对应接口 musclesId
const scene = activeSceneId.value; // 对应接口 scene
// 2. 判断:是否是【全部不限】
if (musclesId === '0' && scene === '0') {
// 情况A:都不限 -> 恢复显示【模板大类】
isFiltering.value = false;
// 大类列表之前已经加载过了,直接显示
return;
}
// 情况B:有筛选条件 -> 请求【筛选接口】
try {
isFiltering.value = true; // 切换成模板列表模式
const res = await TemplatesApi.queryTemplatesByCondition({
musclesId: musclesId,
scene: scene
});
if (res.code === 0) {
// 把接口返回的模板数据存起来,页面会自动刷新
filteredTemplateList.value = res.data;
console.log('筛选接口返回数据filteredTemplateList.value:', filteredTemplateList.value)
}
} catch (error) {
console.log('筛选失败', error);
// 失败了也清空数据
filteredTemplateList.value = [];
}
};
// 跳转到模板详情页,传入模板id
// 跳转地址是F:\hongxing-app\hongxing-new\pages4\pages\xunji\xunji-moban-xiangqing.vue
const goToTemplateDetail = (template) => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${template.id}`,
});
};
const gototemplate = (item) => {
// 传入的是模板大类的id,可以通过模板大类id查询到模板大类包含的模板,这个页面在page4
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban?id=${item.id}`,
});
};
onMounted(() => {
TemplatesList();
getPartCategories();
});
</script>
<style lang="scss" scoped>
.template-page {
width: 100%;
height: 100%;
box-sizing: border-box;
.filter-section {
position: fixed;
top: 80rpx;
// #ifdef MP-WEIXIN
top: 234rpx;
// #endif
left: 0;
right: 0;
z-index: 999;
display: flex;
gap: 24rpx;
flex-wrap: wrap;
padding: 20rpx;
background-color: white;
.filter-btn {
display: flex;
align-items: center;
justify-content: center;
width: auto;
min-width: 150rpx;
height: 50rpx;
background-color: #fff;
color: #333;
font-size: 24rpx;
border: 2rpx solid #ddd;
border-radius: 25rpx;
.text {
margin-right: 5rpx;
}
}
}
.template-list {
margin-top: 120rpx;
padding: 0 30rpx;
box-sizing: border-box;
// 设置明确的高度,使 scroll-view 可以滚动
height: calc(100vh - 120rpx);
// #ifdef MP-WEIXIN
height: calc(100vh - 274rpx);
// #endif
// 添加底部 padding,避免内容被底部导航栏遮挡
padding-bottom: 180rpx;
// #ifdef MP-WEIXIN
padding-bottom: 90rpx;
// #endif
.template-item {
display: flex;
background-color: white;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
margin-bottom: 20rpx;
padding: 20rpx;
.template-img {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.template-content {
flex: 1;
}
.template-title {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.template-count {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.template-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
}
/* 筛选包装器 */
.filter-wrapper {
position: relative;
z-index: 10;
}
/* 筛选按钮激活状态 */
.filter-btn.active {
border-color: #43b05e;
background-color: #e6f7f0;
color: #43b05e;
}
.filter-section {
display: flex;
gap: 24rpx;
padding: 20rpx 30rpx;
background-color: white;
flex-wrap: wrap;
}
.filter-btn {
display: flex;
align-items: center;
justify-content: center;
width: auto;
min-width: 160rpx;
height: 50rpx;
background: #fff;
color: #333;
font-size: 24rpx;
border: 2rpx solid #ddd;
border-radius: 25rpx;
padding: 0 20rpx;
white-space: nowrap;
}
.filter-btn .text {
margin-right: 8rpx;
}
.filter-btn.active {
border-color: #43b05e;
background: #e6f7f0;
color: #43b05e;
}
.filter-wrapper {
position: relative;
z-index: 10;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 8rpx);
left: 0;
background: #fff;
border-radius: 14rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
padding: 12rpx 0;
z-index: 100;
min-width: 160rpx;
max-height: 320rpx;
overflow-y: auto;
}
.dropdown-item {
padding: 16rpx 24rpx;
font-size: 26rpx;
color: #333;
white-space: nowrap;
}
.dropdown-item.selected {
background: #f0f9f4;
color: #2e9d5a;
font-weight: 500;
}
/* 下拉菜单样式 */
.dropdown-menu {
position: absolute;
top: calc(100% + 8rpx);
left: 0;
background: #fff;
border-radius: 14rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
padding: 12rpx 0;
z-index: 100;
min-width: 150rpx;
max-height: 320rpx;
overflow-y: auto;
border: 1rpx solid #f0f0f0;
}
/* 下拉菜单项样式 */
.dropdown-item {
padding: 16rpx 24rpx;
font-size: 26rpx;
color: #333;
}
/* 下拉菜单项悬停样式 */
.dropdown-item:hover {
background-color: #43b05e;
}
/* 下拉菜单项选中样式 */
.dropdown-item.selected {
background-color: #f0f9f4;
color: #2e9d5a;
font-weight: 500;
}
}
}
</style>
... ...