init
This commit is contained in:
2025-06-27 17:42:38 +08:00
commit bd6402478b
5317 changed files with 785994 additions and 0 deletions
@@ -0,0 +1,114 @@
<template>
<div class="add-sport">
<div class="sport-header">
<van-icon name="search" />
<input class="sport-input" placeholder="请输入运动名称" />
</div>
<div class="sport-container">
<div class="nav-list">
<van-sidebar v-model="activeTab">
<van-sidebar-item title="标签名称" />
<van-sidebar-item title="标签名称" />
<van-sidebar-item title="标签名称" />
</van-sidebar>
</div>
<div v-if="detailVOList.length > 0" class="nav-right">
<div v-for="(item, index) in detailVOList" :key="index">
<div class="nav-right-item {{rightTab===index?'active':'' }}">
{{ item?.detailName }}
</div>
</div>
</div>
<div v-else class="nocate">
<div @click="openSportForm">该分类暂无数据</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useTabHooks } from '/@/hooks/useTab';
import { newPageParams, openPage } from '/@/hooks/openPage';
const { activeTab, setActiveTab } = useTabHooks();
const { activeTab: rightTab } = useTabHooks();
const detailVOList = ref([]);
onMounted(() => {
initData();
});
function initData() {}
// function getList() {}
function openSportForm() {
openPage('/sport-form', newPageParams());
}
</script>
<style lang="less" scoped>
@import '/@/assets/less/index';
.add-sport {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow: hidden;
.flex-column();
.sport-header {
width: 86%;
margin: 10px 7% 10px 7%;
height: 32px;
line-height: 32px;
padding: 0 10px;
background-color: #f8f8f8;
border-radius: 20px;
.sport-input {
font-size: 14px;
padding-left: 10px;
}
}
:deep(.van-sidebar) {
width: 100%;
.van-sidebar-item {
padding-left: 20px;
}
}
:deep(.van-sidebar-item--select)::before {
left: 6px;
background-color: #21bebe !important;
}
.sport-container {
flex: 1;
display: flex;
.nav-list {
width: 125px;
background-color: #f3f3f3;
}
.nav-right {
flex: 1;
overflow: auto;
.nav-right-item {
height: 45px;
padding-left: 30px;
border-bottom: 1rpx solid #e0e0e0;
font-size: 14px;
color: #666666;
display: flex;
flex-wrap: wrap;
align-items: center;
}
}
}
}
</style>
+24
View File
@@ -0,0 +1,24 @@
import { get, post } from '/@/views/mobile/api/api';
import { apiPrefix } from '/@/api/apiPrefix';
const prefix: string = import.meta.env.VITE_GLOB_APP_PREFIX;
const { healthFoodExercise } = apiPrefix;
export const Api = {
exerciseTypeSum: prefix + `/rest/${healthFoodExercise}/record/exerciseTypeSum`,
topData: prefix + `/rest/${healthFoodExercise}/record/day`,
rankList: prefix + `/rest/${healthFoodExercise}/record/score/rank`,
exerciseSum: prefix + `/rest/${healthFoodExercise}/record/exerciseSum`,
sportAdd: prefix + `/rest/${healthFoodExercise}/record/build`,
};
// 运动类别统计
export const getSportType = (params: any) => {
return get(Api.exerciseTypeSum, params);
};
//今日运动首页
export const topData = (params: any) => get(Api.topData, params);
// 排行榜
export const rankListApi = (params: any) => get(Api.rankList, params);
//运动周报-运动统计
export const exerciseSumApi = (params: any) => get(Api.exerciseSum, params);
//运动录入
export const sportAdd = (params: any) => post(Api.sportAdd, params);
+220
View File
@@ -0,0 +1,220 @@
import { ref } from 'vue';
import { getChooseWeek, weekTime } from '/@/hooks/utils.ts';
export const firstIcon = new URL('/@/assets/images/health-interventions/first-icon.png', import.meta.url).href;
export const dateText = [
{
type: 'week',
text: '周',
img: new URL('/@/assets/images/health-interventions/week-icon.png', import.meta.url).href,
},
{
type: 'month',
text: '月',
img: new URL('/@/assets/images/health-interventions/month-icon.png', import.meta.url).href,
},
{
type: 'year',
text: '年',
img: new URL('/@/assets/images/health-interventions/year-icon.png', import.meta.url).href,
},
];
export const dateTypeEnum = {
year: '1',
month: '2',
week: '3',
};
export const highestText = ['日', '日', '月'];
export const getHighestText = (index: number) => `${highestText[index]}最高纪录`;
// 运动统计柱状图
export const stepChart = ({ xData, yData }) => {
return {
tooltip: {
trigger: 'item',
},
xAxis: {
type: 'category',
data: xData,
axisLine: {
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
axisLabel: {
textStyle: {
color: '#333333',
},
},
},
grid: {
top: '8%',
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true,
},
yAxis: {
axisLabel: {
show: true,
textStyle: {
color: '#333333',
},
},
axisLine: {
show: true,
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
},
series: [
{
data: yData,
type: 'bar',
symbol: 'circle',
symbolSize: 0,
itemStyle: {
color: '#13C2C2',
borderRadius: 5,
},
barWidth: 10,
},
],
};
};
//运动周报折线图
export const sportsStatisticLine = ({ xData, yData1, yData2 }) => {
return {
tooltip: {
show: true,
trigger: 'item',
},
legend: {
width: 120,
right: '10px',
},
xAxis: {
type: 'category',
data: xData || ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisLine: {
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
axisLabel: {
textStyle: {
color: '#333333',
},
},
},
grid: {
top: '50px',
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true,
},
yAxis: {
axisLabel: {
show: true,
textStyle: {
color: '#333333',
},
},
axisLine: {
show: true,
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
},
series: [
{
name: '分数',
data: yData1,
type: 'line',
symbol: 'circle',
symbolSize: 0,
itemStyle: {
color: '#13C2C2',
},
},
{
name: '运动消耗',
data: yData2,
type: 'line',
symbol: 'circle',
symbolSize: 0,
itemStyle: {
color: '#52C41A',
},
},
],
};
};
export const usePanelData = () => {
const panelData = ref({
totalTime: 0,
highDurationTime: 0,
totalTimeDail: 0,
highDurationTimeDail: 0,
score: 0,
distance: 0,
exerciseConsumeSum: 0,
conclusion: 0,
averageDaily: {
exerciseConsumeDail: 0,
totalTimeDail: 0,
highDurationConsume: 0,
highDurationTimeDail: 0,
highDurationConsumeDail: 0,
glycogenConsumeDail: 0,
fatConsumeDail: 0,
},
});
function setPanelData(data) {
const { averageDaily } = data;
const defaultVal = 0;
panelData.value = {
totalTime: parseInt(data?.exerciseSum?.totalTime || defaultVal),
highDurationTime: parseInt(data?.exerciseSum?.totalTime || defaultVal), // 中高强度(分钟)
totalTimeDail: parseInt(data?.averageDaily?.totalTimeDail || defaultVal), // 日均活动时长
highDurationTimeDail: parseInt(data?.averageDaily?.highDurationTimeDail || defaultVal), // 日均中高强度时间
score: parseInt(data?.exerciseSum?.score || defaultVal),
distance: parseInt(data?.exerciseSum?.distance || defaultVal),
exerciseConsumeSum: parseInt(data?.exerciseSum?.exerciseConsumeSum || defaultVal),
conclusion: data?.conclusion,
averageDaily: {
exerciseConsumeDail: averageDaily?.exerciseConsumeDail || defaultVal, //日均活动消耗
totalTimeDail: parseInt(data?.averageDaily?.totalTimeDail || defaultVal), //日均活动时长
highDurationConsume: averageDaily?.highDurationConsume || defaultVal, //中高强度活动消耗
highDurationTimeDail: parseInt(data?.averageDaily?.highDurationTimeDail || defaultVal), // 日均中高强度时间
highDurationConsumeDail: averageDaily?.highDurationConsumeDail || defaultVal, //日均中高强度能耗
glycogenConsumeDail: averageDaily?.glycogenConsumeDail || defaultVal, //糖原消耗
fatConsumeDail: averageDaily?.fatConsumeDail || defaultVal, //脂肪消耗
},
};
}
return {
panelData,
setPanelData,
};
};
@@ -0,0 +1,55 @@
<template>
<div class="sport-tit">
<div class="left-txt">{{ leftText }}</div>
<div class="right-con" @click="handleDetail">
<span class="right-txt">{{ rightText }}</span>
<span class="circle" v-show="showCircle"></span>
<van-icon name="arrow" />
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
leftText: {
type: String,
default: '',
},
rightText: {
type: String,
default: '',
},
showCircle: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['handleDetail']);
function handleDetail() {
emit('handleDetail');
}
</script>
<style scoped lang="less">
@import '/@/assets/less/index';
.sport-tit {
.flex-jc-between();
padding-bottom: 10px;
.left-txt {
font-weight: bold;
color: #333333;
font-size: 16px;
}
.right-con {
.right-txt {
font-size: 16px;
}
.circle {
display: inline-flex;
width: 12px;
height: 12px;
margin: 0 5px;
background: #52c41a;
border-radius: 50%;
}
}
}
</style>
@@ -0,0 +1,134 @@
<template>
<div class="date-panel">
<van-icon name="arrow-left" @click="lastDate" />
<span class="date-time" @click="openPopup">{{ timeText }}</span>
<van-icon name="arrow" @click="nextDate" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch, unref } from 'vue';
import { getMondayDate, getChooseWeek, timeFormat, weekTime, getMonth, usTime, weekRange } from '/@/hooks/utils';
import { showToast } from 'vant';
const props = defineProps({
dateType: {
type: String,
},
});
const emit = defineEmits(['emitTime', 'emitPopup']);
const dateType = computed(() => props.dateType);
// 时间区间文本
const timeText = ref('');
// week 开始时间-结束时间
const timeInterval = ref({
start: getMondayDate()[0],
end: getMondayDate()[1],
});
onMounted(() => {
timeText.value = `${timeInterval.value.start}${timeInterval.value.end}`;
});
// 周月年变化
watch(dateType, (nVal) => {
if (nVal == 'week') {
let [start, end] = getMondayDate();
timeInterval.value = { start, end };
timeText.value = `${timeInterval.value.start}${timeInterval.value.end}`;
return;
}
const { YYYY, MM } = usTime();
if (nVal == 'month') {
timeText.value = `${YYYY}-${MM}`;
return;
}
if (nVal == 'year') {
timeText.value = `${YYYY}`;
return;
}
});
watch(
timeText,
() => {
emit('emitTime', unref(timeText));
},
{
immediate: true,
}
);
// 获取上一个日期
function lastDate() {
changeDate(-1);
}
// 获取下一个日期
function nextDate() {
changeDate(1);
}
function isDateCross(checkTime: Date): boolean {
return new Date(checkTime).getTime() <= Date.now();
}
// type 周=3 月=2 年=1
function changeDate(direction: number) {
let date;
// if (direction == 1) {
// debugger;
// }
if (dateType.value == 'week') {
let [start, end] = getChooseWeek(new Date(timeInterval.value.start).getTime() + direction * weekTime);
const checkTime = direction > 0 ? start : end;
// 判断是否越界
if (!isDateCross(checkTime)) {
return showToast('没有更多数据了~');
}
timeInterval.value = { start, end };
timeText.value = `${start}${end}`;
} else if (dateType.value == 'month') {
date = new Date(timeText.value);
date.setMonth(date.getMonth() + direction);
const month = getMonth(date);
// 判断是否越界
if (!isDateCross(month)) {
return showToast('没有更多数据了~');
}
timeText.value = month;
} else if (dateType.value == 'year') {
date = new Date(timeText.value);
date.setFullYear(date.getFullYear() + direction);
const year = timeFormat(date, 'yyyy');
// 判断是否越界
if (!isDateCross(year)) {
return showToast('没有更多数据了~');
}
timeText.value = year;
}
}
function openPopup() {
if (dateType.value !== 'week') return;
let arr = weekRange();
emit('emitPopup', { column: arr, nowTime: timeInterval.value });
}
function setTimeText(text: string) {
timeText.value = text;
}
defineExpose({
setTimeText,
});
</script>
<style scoped lang="less">
.date-panel {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
.date-time {
margin: 0 16px;
color: #333;
}
}
</style>
@@ -0,0 +1,63 @@
<template>
<van-popup v-model:show="show" position="bottom" :style="{ height: '50%' }">
<van-date-picker
v-model="currentDate"
title="选择日期"
:max-date="maxDate"
:columnsType="columnsType"
:close-on-click-overlay="false"
@cancel="cancel"
@confirm="confirm"
@change="change"
/>
<!-- <van-date-picker-->
<!-- v-model="currentDate"-->
<!-- title="选择日期"-->
<!-- :columnsType="['day']"-->
<!-- :close-on-click-overlay="false"-->
<!-- :filter="filter"-->
<!-- @cancel="cancel"-->
<!-- @confirm="confirm"-->
<!-- @change="change"-->
<!-- />-->
</van-popup>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const show = ref(false);
const showPopup = () => {
show.value = true;
};
const currentDate = ref();
const maxDate = new Date();
const columnsType = ['year', 'month'];
const option = [
{ text: '04至10', value: '04至10' },
{ text: '28至03', value: '28至03' },
{ text: '21至27', value: '21至27' },
];
const filter = (type, options) => {
console.log('options', options);
if (type === 'day') {
return options.filter((option) => Number(option.value) % 6 === 0);
}
return options;
};
function cancel() {
show.value = false;
}
function change() {}
function confirm() {
show.value = false;
}
defineExpose({
showPopup,
});
</script>
<style scoped lang="less"></style>
@@ -0,0 +1,90 @@
<template>
<div class="sport-card-module">
<div class="sport-box" @click="openDetail">
<div class="sport-left">
<img class="img" :src="sportImg" alt="" />
<span class="sport-name">{{ cardData?.className }}</span>
</div>
<van-icon name="arrow" color="#999999" />
</div>
<div class="sport-box info-bottom">
<div>
<span class="sports-num">{{ toFixed1(cardData?.consume) }}</span>
<span class="sport-text">&nbsp;千卡</span>
</div>
<div>
<span class="sport-text">{{ toFixed1(cardData?.totalMinute) }}</span>
<span class="sport-text">分钟</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { sportImg } from '/@/views/motion-data/sports-platform/data';
const props = defineProps({
cardData: {
type: Object,
},
});
const emit = defineEmits(['sportDetail']);
function toFixed1(num) {
try {
return parseFloat(Number(num).toFixed(1));
} catch {
return 0;
}
}
// 打开详情页
function openDetail() {
emit('sportDetail', props.cardData);
}
</script>
<style scoped lang="less">
@import '/@/assets/less/index';
.sport-card-module {
width: 100%;
background-color: #ffffff;
border-radius: 10px;
padding: 10px 10px;
.flex-column();
justify-content: space-between;
.sport-box {
.flex-center();
justify-content: space-between;
.sport-left {
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.img {
width: 30px;
height: 30px;
}
.sport-name {
padding-left: 6px;
}
}
.sports-num {
font-size: 16px;
font-weight: bold;
}
.sport-text {
color: #999999;
}
}
.info-bottom {
padding-top: 10px;
}
}
</style>
@@ -0,0 +1,65 @@
<template>
<div class="tab-time-components">
<div
class="tab-time-item"
v-for="(item, i) in dynamicText"
:key="i"
:class="[activeTab == i ? 'active' : '']"
:style="propsStyle(activeTab == i)"
@click="setActiveTab(i)"
v-text="item.text"
></div>
</div>
</template>
<script setup lang="ts">
import { computed, watch, unref } from 'vue';
import { dateText } from '/@/views/motion-data/common.data';
import { useTabHooks } from '/@/hooks/useTab';
const emit = defineEmits(['emitActiveTab']);
const props = defineProps({
activeColor: {
type: String, //激活背景色
},
});
function propsStyle(flag: boolean) {
if (props.activeColor) {
return flag && `background:${props.activeColor}`;
}
}
const dynamicText = computed(() => dateText);
const { activeTab, setActiveTab } = useTabHooks(0);
watch(
activeTab,
() => {
emit('emitActiveTab', dateText[unref(activeTab)]);
},
{
immediate: true,
}
);
</script>
<style scoped lang="less">
@import '/@/assets/less/index.less';
.tab-time-components {
margin: 10px 0;
padding: 2px;
height: 35px;
background: #ffffff;
border-radius: 35px;
display: flex;
.tab-time-item {
flex: 1;
color: #666666;
font-size: 16px;
.flex-center();
&.active {
color: #fff;
border-radius: 20px;
background: #13c2c2;
}
}
}
</style>
@@ -0,0 +1,18 @@
// 骑行
export const rideCardText = [
{
text1: '时速',
text2: '时长',
text3: '消耗',
},
{
text1: '日均时速',
text2: '日均时长',
text3: '日均消耗',
},
{
text1: '月均时速',
text2: '月均时长',
text3: '月均消耗',
},
];
@@ -0,0 +1,284 @@
<template>
<div class="ride-number">
<div class="ride-top">
<div class="tab-time">
<div
class="tab-time-item"
v-for="(item, i) in dynamicText"
:key="i"
:class="[activeTab == i ? 'active' : '']"
@click="setActiveTab(i)"
v-text="item.text"
></div>
</div>
<div class="date-panel">
<van-icon name="arrow-left" />
<span class="date-time">9月4日至9月10日</span>
<van-icon name="arrow" />
</div>
<div class="statistics-container panel-m b-12">
<div class="img-container">
<img class="img" :src="dynamicText[activeTab].img" alt="" />
</div>
<div class="data-container">
<div class="data-count">
<p class="data-number">487</p>
<p class="data-text">累计{{ dynamicText[activeTab].text }}里程</p>
</div>
<div class="data-count">
<p class="data-number">123</p>
<p class="data-text">平均时速(km/h)</p>
</div>
<div class="data-count">
<p class="data-number">123</p>
<p class="data-text">累计{{ dynamicText[activeTab].text }}时长(分钟)</p>
</div>
<div class="data-count">
<p class="data-number">123</p>
<p class="data-text">累计{{ dynamicText[activeTab].text }}消耗/千卡</p>
</div>
</div>
</div>
<div class="statistics-chart panel-m card-bg b-12">
<div class="statistics-chart-inner"> 图表1图表1 图表1图表1</div>
</div>
<div class="bar-data panel-m card-bg b-12">
<div class="bar-data-item">
<p>{{ cardText.text1 }}</p>
<p><span class="bar-data-number">123</span> <span>km/h</span></p>
</div>
<div class="bar-data-item">
<p>{{ cardText.text2 }}</p>
<p><span class="bar-data-number">34</span> <span>分钟</span></p>
</div>
<div class="bar-data-item">
<p>{{ cardText.text3 }}</p>
<p><span class="bar-data-number">23</span> <span>千卡</span></p>
</div>
</div>
<div class="highest-record panel-m">
<p>{{ getHighestText(activeTab) }}</p>
<div class="highest-content card-bg b-12">
<div class="icon-time">
<img class="first-icon" :src="firstIcon" alt="" />
<span class="highest-time">2020-11-28</span>
</div>
<div class="highest-info">
<span style="opacity: 0"> </span>
<span>1362 <span>公里</span></span>
</div>
</div>
</div>
</div>
<div class="ride-btn">录入</div>
</div>
</template>
<script lang="ts" setup>
import { useTabHooks } from '/@/hooks/useTab';
import { dateText, firstIcon, getHighestText } from '/@/views/motion-data/common.data';
import { rideCardText } from './hooks';
import { computed } from 'vue';
const { activeTab, setActiveTab } = useTabHooks(0);
let query = 'ride';
const dynamicText = computed(() => {
if (query == 'ride') {
return dateText;
}
});
const cardText = computed(() => {
if (query == 'ride') {
return rideCardText[activeTab.value];
}
});
</script>
<style lang="less" scoped>
@import '/@/assets/less/index.less';
p {
margin: 0;
}
.panel-m {
margin: 6px 16px;
}
.card-bg {
background: #ffffff;
}
.b-12 {
border-radius: 12px;
}
.ride-number {
height: 100vh;
background: #eeeeee;
box-sizing: border-box;
//padding-top: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
.ride-top {
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
overflow: auto;
.tab-time {
margin: 10px 20px;
padding: 2px;
height: 35px;
background: #ffffff;
border-radius: 35px;
display: flex;
.tab-time-item {
flex: 1;
color: #666666;
font-size: 16px;
.flex-center();
&.active {
color: #fff;
border-radius: 20px;
background: #52c41a;
}
}
}
.date-panel {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
.date-time {
margin: 0 10px;
}
}
.statistics-container {
height: 140px;
padding: 6px 0;
background: #ffffff;
.flex-center();
.img-container {
width: 60px;
height: 60px;
margin: 0 16px;
.img {
width: 100%;
height: 100%;
}
}
.data-container {
flex: 4;
display: flex;
flex-wrap: wrap;
.data-count {
width: 50%;
text-align: center;
.data-number {
font-size: 20px;
font-weight: bold;
color: #333333;
}
.data-text {
font-size: 14px;
font-weight: 500;
color: #666666;
}
}
}
}
.statistics-chart {
//min-height: 240px;
.statistics-chart-inner {
min-height: 240px;
}
}
.bar-data {
display: flex;
flex-wrap: wrap;
padding: 16px;
.bar-data-item {
width: 50%;
& p,
span {
font-size: 14px;
font-weight: 500;
color: #666666;
}
.bar-data-number {
font-size: 20px;
font-weight: bold;
color: #333333;
}
}
}
.highest-record {
& > p {
margin-bottom: 6px;
font-size: 16px;
}
.highest-content {
display: flex;
align-items: center;
padding: 16px;
.icon-time {
display: flex;
align-items: center;
}
.first-icon {
width: 36px;
height: 36px;
}
.highest-time {
flex: 1;
margin: 0 15px 0 6px;
}
}
.highest-info {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: bold;
color: #333333;
}
}
}
.ride-btn {
margin: 12px 15% 28px 15%;
height: 40px;
.flex-center();
.qs-btn();
border-radius: 40px;
font-size: 20px;
font-weight: 500;
color: #ffffff;
}
}
</style>
@@ -0,0 +1,5 @@
import { get } from '/@/views/mobile/api/api';
const prefix: string = import.meta.env.VITE_GLOB_APP_PREFIX || '';
// 评估列表
export const sportListApi = (params) => get(`${prefix}/rest/health-food-exercise/record/getExerciseClassAndDetail`, params);
@@ -0,0 +1,64 @@
import { timeFormat, toInt } from '/@/hooks/utils';
// 含有里程的运动种类
export const withMileageCode = ['1', '8', '9', '10', '14'];
export function validate(params, keys) {
// const keys = ['way', 'mode', 'mileageVal', 'durationVal', 'date', 'division'];
let flag = true;
keys.forEach((item) => {
if (!params[item]) {
flag = false;
}
});
return flag;
}
export function createFormParams(params) {
if (params?.mileageVal?.indexOf('.') == 0) {
params.mileageVal = '0' + params.mileageVal;
}
if (params?.exerciseDuration?.indexOf('.') == 0) {
params.exerciseDuration = '0' + params.exerciseDuration;
}
return {
classNo: params?.way, //运动方式编号
detailNo: params?.mode, //运动模式编号
exerciseTime: params?.date + ' ' + params?.division + ':00', //运动日期
exerciseDistance: params?.mileageVal, // 运动里程
exerciseDuration: params?.durationVal && parseInt(params?.durationVal), // 运动时长
};
}
export interface SelectedOptions {
value: string;
text: string;
}
export function useNowTime() {
const [YYYY, MM, DD] = timeFormat(new Date(), 'YYYY-MM-DD').split('-');
const [hh, mm] = timeFormat(new Date(), 'HH:mm').split(':');
return {
YYYY,
MM,
DD,
hh,
mm,
};
}
export function useDateRule(YYYY: string | number, MM: string | number, DD: string | number) {
return {
minDate: new Date(2020, 0, 1),
maxDate: new Date(toInt(YYYY), toInt(MM), toInt(DD)),
};
}
export function returnModeList(sportList: any, value: string) {
return (
sportList
.find((item: any) => item?.exerciseClassNo == value)
?.detailVOList.map((item: any) => ({
text: item?.detailName,
value: item?.detailNo,
})) || []
);
}
@@ -0,0 +1,281 @@
<template>
<div class="sport-container">
<div class="sport-mode">
<div class="mode-item" @click="openPopup('3')">
<span class="title">运动方式</span>
<div class="mode-select-text">
<span>{{ sportParams?.wayText }}</span>
<span class="con arrow"></span
></div>
</div>
<div class="mode-item" @click="openPopup('4')">
<span class="title">运动模式</span>
<div class="mode-select-text">
<span>{{ sportParams?.modeText }}</span>
<span class="con arrow"></span>
</div>
</div>
<div class="mode-item">
<span class="title">运动时间</span>
<div class="time">
<div @click="openPopup('1')">{{ sportParams.date }}</div>
<div @click="openPopup('2')">{{ sportParams.division }}</div>
</div>
</div>
<div class="mode-item" v-if="showMileageVal">
<span class="title">运动里程</span>
<div class="con">
<van-field type="number" v-model.trim="sportParams.mileageVal" class="sport-input" placeholder="请输入运动里程" />
<span class="unit">公里</span>
</div>
</div>
<div class="mode-item">
<span class="title">运动时长</span>
<div class="con">
<van-field type="number" v-model.trim="sportParams.durationVal" class="sport-input" placeholder="请输入运动时长" />
<span class="unit">分钟</span>
</div>
</div>
</div>
<van-popup v-model:show="timePicker.show" position="bottom">
<van-date-picker
v-if="timesType === '1'"
v-model="timePicker.currentDate"
:max-date="dateRule.maxDate"
title="选择年月"
@cancel="closePopup"
@confirm="handleConfirm"
/>
<van-time-picker v-if="timesType === '2'" v-model="timePicker.date" title="选择时间" @cancel="closePopup" @confirm="handleConfirm" />
<van-picker v-if="timesType === '3'" :columns="wayList" title="运动方式" @cancel="closePopup" @confirm="sportConfirm" />
<van-picker v-if="timesType === '4'" :columns="modeList" :title="'运动模式'" @cancel="closePopup" @confirm="sportConfirm" />
</van-popup>
<div class="sport-button">
<div class="button-text" @click="submit">保存</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, Ref, watch } from 'vue';
import { sportListApi } from './api';
import { showToast } from 'vant';
import { sportAdd } from '/@/views/motion-data/api';
import { useRouter, useRoute } from 'vue-router';
import {
validate,
createFormParams,
SelectedOptions,
useNowTime,
useDateRule,
returnModeList,
withMileageCode,
} from '/@/views/motion-data/sport-form/data';
import { debounce } from 'lodash-es';
import { destroyPage } from '/@/hooks/openPage';
import { useLoading } from '/@/utils/compUtils';
const router = useRouter();
const route = useRoute();
const { YYYY, MM, DD, hh, mm } = useNowTime();
const dateRule = useDateRule(YYYY, MM, DD);
const timePicker = ref({
show: false,
currentDate: [YYYY, MM, DD],
date: [hh, mm],
});
const sportParams: Ref<{ [key: string]: string }> = ref({
way: '',
wayText: '',
mode: '',
modeText: '',
mileageVal: '', // 运动里程
durationVal: '', // 运动时长
date: `${YYYY}-${MM}-${DD}`,
division: `${hh}:${mm}`,
});
const sportList = ref([]); //接口原始数据
const wayList = ref([]); //运动方式
const modeList = ref([]); //运动模式
const timesType = ref('');
const showMileageVal = ref(true);
watch(
() => sportParams.value.way,
(n) => {
if (!withMileageCode.includes(n)) {
showMileageVal.value = false;
delete sportParams.value.mileageVal;
} else {
showMileageVal.value = true;
sportParams.value.mileageVal = '';
}
}
);
onMounted(() => {
getSportList();
});
// 时间类型 1年月 2 时间 3运动方式 4运动模式
function openPopup(type: string) {
if (type == '4' && !sportParams.value.way) {
return showToast('请先选择运动方式');
}
timesType.value = type;
timePicker.value.show = true;
}
function closePopup() {
timePicker.value.show = false;
}
// 获取运动列表
function getSportList() {
sportListApi({
detailName: '',
}).then((res: any) => {
const NotNullData = res?.data?.filter((item) => item !== null && item !== undefined);
sportList.value = NotNullData;
wayList.value = NotNullData?.map((item: any) => ({
text: item?.exerciseClassName,
value: item?.exerciseClassNo,
}));
});
}
// 时间选择
function handleConfirm(data: any) {
switch (timesType.value) {
//年月入
case '1':
sportParams.value.date = data.selectedValues.join('-');
break;
//时分
case '2':
sportParams.value.division = data.selectedValues.join(':');
break;
}
closePopup();
}
function sportConfirm({ selectedOptions }: { selectedOptions: SelectedOptions[] }) {
const { value, text } = selectedOptions[0] || '';
if (timesType.value == '3') {
sportParams.value.way = value;
sportParams.value.wayText = text;
sportParams.value.mode = '';
sportParams.value.modeText = '';
modeList.value = returnModeList(sportList.value, value);
}
if (timesType.value == '4') {
sportParams.value.mode = value;
sportParams.value.modeText = text;
}
closePopup();
}
const submit = debounce(submitData, 500);
const { loadingSpinner, loadingClose } = useLoading();
function submitData() {
const flag = validate(sportParams.value, Object.keys(sportParams.value));
if (!flag) {
return showToast('请填写所有数据');
}
loadingSpinner();
const params = createFormParams(sportParams.value);
sportAdd(params)
.then(({ code, data }) => {
loadingClose();
if (code == 200) {
showToast('录入成功!');
// 判断安卓是否需要销毁当前页
if (route.query.destroyPage) {
try {
destroyPage();
} catch {
router.go(-1);
}
} else {
router.go(-1);
}
} else {
showToast(data.msg);
}
})
.catch(() => {
loadingClose();
});
}
</script>
<style lang="less" scoped>
@import '../../../assets/less/index';
.sport-container {
background-color: @card-gray;
height: 100%;
min-height: 100%;
.flex-column();
justify-content: space-between;
.sport-mode {
flex: 1;
.flex-column();
overflow: auto;
.mode-item {
height: 60px;
padding: 20px;
background-color: @card-fff;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e0e0e0;
.mode-select-text {
display: flex;
}
.title {
color: #666666;
}
.con {
display: flex;
align-items: center;
justify-content: flex-end;
.sport-input {
width: 60%;
}
}
.time {
display: flex;
color: #333333;
> div:nth-child(1) {
background-color: #f3f3f3;
padding: 5px 16px;
border-radius: 5px;
}
> div:nth-child(2) {
background-color: #f3f3f3;
padding: 5px 16px;
border-radius: 10px;
margin-left: 5px;
}
}
}
}
.sport-button {
height: 60px;
display: flex;
align-items: center;
.button-text {
height: 40px;
flex: 1;
.flex-center();
margin: 0 10%;
.footerButton();
}
}
}
</style>
@@ -0,0 +1,80 @@
<template>
<div class="card-data">
<div class="card-row">
<div class="card-col-item">
<div> 日均活动消耗 </div>
<div> {{ cardData.exerciseConsumeDail }}<span class="unit">千卡</span> </div>
</div>
<div class="card-col-item">
<div> 日均活动时长 </div>
<div> {{ cardData.totalTimeDail }}<span class="unit">分钟</span> </div>
</div>
</div>
<div class="card-row">
<div class="card-col-item">
<div> 中高强度活动消耗 </div>
<div> {{ cardData.highDurationConsume }}<span class="unit">千卡</span> </div>
</div>
<div class="card-col-item">
<div> 日均中高强度时间 </div>
<div> {{ cardData.highDurationTimeDail }}<span class="unit">分钟</span> </div>
</div>
</div>
<div class="card-row">
<div class="card-col-item">
<div> 日均中高强度能耗 </div>
<div> {{ cardData.highDurationConsumeDail }}<span class="unit">千卡</span> </div>
</div>
<div class="card-col-item">
<div> 糖源消耗 </div>
<div> {{ cardData.glycogenConsumeDail }}<span class="unit">千卡</span> </div>
</div>
</div>
<div class="card-row">
<div class="card-col-item">
<div> 脂肪消耗 </div>
<div> {{ cardData.fatConsumeDail }}<span class="unit">千卡</span> </div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
data: {
type: Object,
},
});
const cardData = computed(() => props.data);
</script>
<style scoped lang="less">
.two-outer {
background-color: #ffffff;
border-radius: 25px;
padding: 5px 20px;
}
.card-row {
display: flex;
align-items: center;
height: 70px;
> :nth-child(n) {
width: 50%;
text-align: left;
}
.card-col-item {
position: relative;
> :nth-child(1),
.unit {
font-size: 12px;
color: #666666;
}
span {
margin-left: 5px;
}
> :nth-child(2) {
font-size: 20px;
}
}
}
</style>
@@ -0,0 +1,292 @@
<template>
<div class="sport-week-page">
<div>
<tab-time @emit-active-tab="emitActiveTab" />
<date-panel ref="datePanel" :dateType="dateType" @emit-time="emitTime" @emit-popup="openPopup" />
<div class="statistics-card">
<div class="card-row">
<div class="card-row-col">
<span class="num">{{ panelData.totalTime }}</span>
<span class="text">分钟</span>
</div>
<div class="card-row-col line-left line-right">
<span class="num">{{ panelData.highDurationTime }}</span>
<span class="text">中高强度(分钟)</span>
</div>
<div class="card-row-col">
<span class="num">{{ panelData.score }}</span>
<span class="text">当日运动得分</span>
</div>
</div>
<div class="card-row">
<div class="card-row-col">
<span class="num">{{ panelData.distance }}</span>
<span class="text">公里</span>
</div>
<div class="card-row-col line-left">
<span class="num">{{ panelData.exerciseConsumeSum }}</span>
<span class="text">千卡</span>
</div>
<div class="card-row-col"></div>
</div>
</div>
<div class="summarize">
<div class="tit">本周运动总结</div>
<div class="text-area" v-text="panelData.conclusion"></div>
<div class="link" @click="openArticle">
<span>{{ panelData?.highDurationTime > 150 ? '运动的益处' : '运动不足的伤害' }}</span>
<van-icon name="arrow" />
</div>
</div>
<div class="card-data-container">
<card-data :data="panelData.averageDaily" />
</div>
<!--运动列表-->
<div class="card-list">
<SportCard v-for="(item, i) in groupList" :key="i" :card-data="item" @sport-detail="openSportDetail" />
</div>
<!--运动统计图-->
<div class="chart-container" id="chartContainer">
<div class="title">{{ dateText }}运动总结()</div>
<div class="sport-statistics-chart" id="sportStatisticsChart"></div>
</div>
</div>
<van-popup v-model:show="popupData.show" position="bottom">
<van-picker title="选择周" :columns="popupData.columns" @cancel="onCancel" @confirm="onConfirm" />
</van-popup>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue';
import TabTime from '/@/views/motion-data/components/tab-time/tabTime.vue';
import DatePanel from '/@/views/motion-data/components/date-panel/datePanel.vue';
import CardData from '/@/views/motion-data/sport-week-page/components/cardData.vue';
import SportCard from '/@/views/motion-data/components/sport-card/sportCard.vue';
import * as echarts from 'echarts';
import { dateTypeEnum, sportsStatisticLine, usePanelData } from '/@/views/motion-data/common.data';
import { debounce } from 'lodash-es';
import { exerciseSumApi } from '/@/views/motion-data/api';
import { timeFormat } from '/@/hooks/utils';
import { newPageParams, openPage } from '/@/hooks/openPage';
const dateType = ref('week');
const dateText = ref('周');
// 年月周切换
function emitActiveTab({ type, text }: { type: string; text: string }) {
dateType.value = type;
dateText.value = text;
}
onMounted(() => {
initData();
});
// 初始化页面
function initData() {
getSportStatistic();
}
const t = timeFormat(new Date(), 'YYYY-MM-DD');
const exerciseTime = ref(t);
const emitTime = debounce(debounceInitData, 500);
function debounceInitData(nowTime: string) {
if (!nowTime) return;
// 周
if (nowTime.includes('至')) {
exerciseTime.value = nowTime.split('至')[0];
} else {
//月或者年
exerciseTime.value = nowTime;
}
initData();
}
// 运动统计
const { panelData, setPanelData } = usePanelData();
const groupList = ref([]);
const myChart = ref();
function getSportStatistic() {
const params = {
exerciseTime: timeFormat(exerciseTime.value, 'YYYY-MM-DD'),
type: dateTypeEnum[unref(dateType)],
};
exerciseSumApi(params).then(({ code, data }) => {
if (code !== 200) return;
setPanelData(data);
groupList.value = data?.groupList;
initChart({ xData: data?.nameList, yData1: data?.scoreList, yData2: data?.consumeValueList });
});
}
function initChart({ xData, yData1, yData2 }: { xData: string[]; yData1: number[]; yData2: number[] }) {
myChart.value = echarts.init(document.getElementById('sportStatisticsChart'));
myChart.value.setOption(sportsStatisticLine({ xData, yData1, yData2 }));
}
const popupData = ref({
columns: [],
show: false,
});
// 打开周选择
function openPopup({ column }) {
popupData.value.columns = column;
popupData.value.show = true;
}
const datePanel = ref();
function onConfirm(val) {
let text = val.selectedValues[0];
exerciseTime.value = text.split('至')[0];
initData();
datePanel.value.setTimeText(text);
popupData.value.show = false;
}
function onCancel() {
popupData.value.show = false;
}
//进入运动统计页
function openSportDetail({ classId }: { classId: string }) {
openPage('/step-statistics', newPageParams({ classId }));
}
//打开文章详情
function openArticle() {
let detailId = panelData.value.highDurationTime > 150 ? '506' : '505';
openPage('/knowledge-page', newPageParams({ detailId: detailId }));
}
</script>
<style scoped lang="less">
@import '/@/assets/less/index.less';
.sport-week-page {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 16px 20px 16px;
overflow: auto;
.flex-column();
background: #eeeeee;
.statistics-card {
margin: 10px 0;
padding: 10px 0;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
.card-row {
display: flex;
.card-row-col {
position: relative;
flex: 1;
.flex-column();
align-items: center;
color: #666666;
.num {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.text {
//color: #fefefe;
//font-weight: 500;
}
}
.card-row-col.line-left:before {
content: '';
position: absolute;
left: 0;
top: 25%;
width: 2px;
height: 50%;
background-color: rgba(51, 51, 51, 0.3);
}
.card-row-col.line-right:after {
content: '';
position: absolute;
right: 0;
top: 25%;
width: 2px;
height: 50%;
background-color: rgba(51, 51, 51, 0.3);
}
}
.card-row:first-of-type {
padding-bottom: 10px;
}
}
.summarize {
padding: 10px 20px;
border-radius: 8px;
background-color: #ffffff;
.tit {
font-size: 16px;
}
.text-area {
font-size: 16px;
color: #999999;
margin-top: 5px;
}
.link {
font-size: 14px;
color: #3390ff;
text-align: right;
&:active {
text-decoration: underline;
}
}
}
.card-data-container {
padding: 10px 20px;
border-radius: 8px;
background-color: #ffffff;
margin: 10px 0;
}
.card-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
& > div {
width: calc(50% - 4px);
}
}
.chart-container {
position: relative;
height: 400px;
background: #ffffff;
border-radius: 8px;
padding: 10px 20px;
.title {
font-size: 16px;
font-weight: 500;
color: #333333;
position: absolute;
}
.sport-statistics-chart {
height: 100%;
}
}
}
</style>
@@ -0,0 +1,51 @@
import { ref, Ref } from 'vue';
import { toFixedNum, toInt } from '/@/hooks/utils';
export const sportImg = new URL('/@/assets/images/sports-statistics/sport-icon.png', import.meta.url).href;
interface SportData {
exerciseSum: {
totalTime: string;
highDurationTime: string;
activityLevel: string;
distance: string;
exerciseConsumeSum: string;
score: string;
glycogenConsume: string;
fatConsume: string;
};
groupList: any[];
}
export function useSportData() {
const sportData: Ref<SportData> = ref({
exerciseSum: {
totalTime: '',
highDurationTime: '',
activityLevel: '',
distance: '',
exerciseConsumeSum: '',
score: '',
glycogenConsume: '',
fatConsume: '',
},
groupList: [],
});
function setSportData(data: SportData): void {
const level = toFixedNum(data?.exerciseSum?.activityLevel, 1);
sportData.value = {
...data,
exerciseSum: {
...data.exerciseSum,
activityLevel: level == '0.0' ? '0' : level,
},
groupList: data?.groupList?.map((item) => ({
...item,
totalMinute: toInt(item.totalMinute),
})),
};
}
return [sportData, setSportData];
}
@@ -0,0 +1,368 @@
<template>
<div class="sports-platform">
<div class="scroll-container">
<CardTitle leftText="总运动" rightText="运动周报" :showCircle="true" @handle-detail="weekPage" />
<div class="basic-card">
<div class="card-top">
<div class="card-row">
<div class="card-row-col">
<span class="num">{{ toInt(sportData?.exerciseSum?.totalTime) }}</span>
<span class="text">分钟</span>
</div>
<div class="card-row-col">
<span class="num">{{ toInt(sportData?.exerciseSum?.highDurationTime) }}</span>
<span class="text">中高强度(分钟)</span>
</div>
<div class="card-row-col">
<span class="num">{{ sportData?.exerciseSum?.activityLevel }}</span>
<span class="text">活动水平等级</span>
</div>
</div>
<div class="card-row">
<div class="card-row-col">
<span class="num">{{ toInt(sportData?.exerciseSum?.distance) }}</span>
<span class="text">公里</span>
</div>
<div class="card-row-col">
<span class="num">{{ toInt(sportData?.exerciseSum?.exerciseConsumeSum) }}</span>
<span class="text">千卡</span>
</div>
<div class="card-row-col">
<span class="num">{{ toInt(sportData?.exerciseSum?.score) }}</span>
<span class="text">当日运动得分</span>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-col">
<span>糖原消耗: </span>
<span>{{ sportData?.exerciseSum?.glycogenConsume }}千卡</span>
</div>
<div class="card-footer-col">
<span>脂肪消耗: </span>
<span>{{ sportData?.exerciseSum?.fatConsume }}千卡</span>
</div>
</div>
</div>
<!--运动卡片-->
<div class="card-list">
<SportCard v-for="item in sportData?.groupList" :card-data="item" :key="item.id" @sport-detail="openSportDetail" />
</div>
<CardTitle leftText="运动排行" rightText="查看完整榜单" @handle-detail="moreRankList" />
<!--运动排行-->
<div class="rank-list">
<div class="rank-header">
<div class="head-title">
<span class="no">排名</span>
<span>姓名</span>
</div>
<div class="head-name">能耗</div>
<div class="head-name">分数</div>
<div class="head-name">时长</div>
</div>
<van-skeleton :row="10" :loading="loading">
<div class="rank-table" v-if="rankList.length">
<div v-for="(item, i) in rankList" :key="i">
<div class="body-column">
<div class="head-title">
<div class="no">
<img class="no-img" v-if="i <= 2" :src="computedNo(i)" alt="" />
<span v-else>{{ item.rank }}</span>
</div>
<div>
<div class="d1">{{ item?.userName }}</div>
<div class="d2">{{ item?.orgName }}</div>
</div>
</div>
<div class="head-name">{{ item?.consume }}</div>
<div class="head-name">{{ item?.score }}</div>
<div class="head-name">{{ item?.duration }}</div>
</div>
</div>
</div>
<van-empty v-else image-size="100" description="暂无数据" />
</van-skeleton>
</div>
</div>
<div class="page-footer">
<div class="btn" @click="addSportInfo">运动录入</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue';
import CardTitle from '/@/views/motion-data/components/card-title/cardTitle.vue';
import SportCard from '/@/views/motion-data/components/sport-card/sportCard.vue';
import { imgList } from '/@/views/sport-ranking/sportRanking.data';
import { openPage, newPageParams } from '/@/hooks/openPage';
import { timeFormat, toFixedNum, toInt } from '/@/hooks/utils';
import { topData, rankListApi } from '/@/views/motion-data/api';
import { useSportData } from '/@/views/motion-data/sports-platform/data';
// 排名图标
const computedNo = (index: string | number) => {
return imgList[index];
};
onMounted(() => {
initData();
});
// 初始化数据
function initData() {
getTopData();
getRankList();
}
// 今日运动数据
const topDataParams = ref({
exerciseTime: timeFormat(new Date(), 'YYYY-MM-DD'),
});
const [sportData, setSportData] = useSportData();
function getTopData() {
topData({ ...unref(topDataParams) }).then(({ code, data }) => {
if (code !== 200) return;
setSportData(data);
});
}
const loading = ref(true); //加载骨架屏
const rankList = ref(); //排行榜
// 运动排行
function getRankList() {
loading.value = true;
rankListApi({ type: 'day' })
.then(({ code, data }) => {
loading.value = false;
if (code !== 200) return;
rankList.value = data?.userScores.slice(0, 4);
})
.catch(() => {
loading.value = false;
});
}
// 运动周报
function weekPage() {
openPage('/sport-week', newPageParams());
}
// 运动详情页
function openSportDetail(val: { classId: any }) {
const { classId, className } = val;
openPage('/step-statistics', newPageParams({ classId: classId, className: className }));
}
// 完整排行榜单
function moreRankList() {
openPage('/sport-ranking', newPageParams());
}
function addSportInfo() {
openPage('/sport-form', newPageParams({ destroyPage: true }));
}
</script>
<style scoped lang="less">
@import '/@/assets/less/index';
.sports-platform {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 12px 16px 0 16px;
overflow: hidden;
.flex-column();
background-color: #efefef;
.scroll-container {
flex: 1;
.flex-column();
overflow: auto;
padding-bottom: 40px;
.basic-card {
.flex-column();
background: linear-gradient(180deg, #21bebe 0%, #26e0e0 100%);
border-radius: 20px;
.card-top {
padding: 10px 0;
.card-row {
display: flex;
.card-row-col {
position: relative;
flex: 1;
.flex-column();
align-items: center;
color: #ffffff;
.num {
font-size: 18px;
font-weight: bold;
}
.text {
color: #fefefe;
//font-weight: 500;
}
}
.card-row-col:nth-of-type(2):before {
content: '';
position: absolute;
right: 0;
top: 25%;
width: 2px;
height: 50%;
background-color: rgba(255, 255, 255, 0.3);
}
.card-row-col:nth-of-type(2):after {
content: '';
position: absolute;
left: 0;
top: 25%;
width: 2px;
height: 50%;
background-color: rgba(255, 255, 255, 0.3);
}
}
.card-row:first-of-type {
padding-bottom: 10px;
}
}
.card-footer {
padding: 10px 0;
display: flex;
background: #21bfbf;
border-radius: 0 0 20px 20px;
.card-footer-col {
flex: 1;
font-weight: 500;
color: #ffffff;
.flex-center();
}
}
}
.card-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
justify-content: space-between;
& > div {
width: calc(50% - 4px);
}
}
}
//排行榜
.rank-list {
background: #ffffff;
border-radius: 10px;
padding-bottom: 20px;
.rank-header {
display: flex;
align-items: center;
font-size: 16px;
margin: 12px 0;
color: #999;
.head-title {
flex: 3;
display: flex;
.no {
width: 60px;
display: flex;
justify-content: center;
position: relative;
}
.no:after {
content: '';
position: absolute;
right: 7px;
top: 20%;
width: 1px;
height: 60%;
background-color: #999;
}
}
.head-name {
flex: 1;
}
}
.rank-table {
flex: 1;
overflow: auto;
}
.body-column {
display: flex;
align-items: center;
margin: 10px 0;
.head-title {
flex: 3;
display: flex;
align-items: center;
.no {
width: 60px;
font-size: 16px;
font-weight: 800;
color: #666666;
display: flex;
justify-content: center;
.no-img {
width: 36px;
height: 36px;
}
}
.d1 {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.d2 {
font-size: 14px;
font-weight: 500;
color: #999999;
}
}
.head-name {
flex: 1;
}
}
}
.page-footer {
.flex-center();
padding: 8px 14px 12px 14px;
.btn {
width: 70%;
height: 40px;
.flex-center();
.footerButton();
.qs-btn();
}
}
}
</style>
@@ -0,0 +1,32 @@
// 步数
import { timeFormat } from '/@/hooks/utils';
export const stepCardText = [
{
text1: '步数',
text2: '距离',
text3: '时长',
text4: '消耗',
},
{
text1: '日均步数',
text2: '日均距离',
text3: '日均时长',
text4: '日消耗',
},
{
text1: '月均步数',
text2: '月均距离',
text3: '月均时长',
text4: '月消耗',
},
];
export function timeParams(nowTime: string) {
if (nowTime.includes('至')) {
const d = nowTime.split('至')[0];
return timeFormat(d);
} else {
return timeFormat(nowTime);
}
}
@@ -0,0 +1,380 @@
<template>
<div class="step-number">
<div class="step-top">
<TabTime activeColor="#52c41a" class="tab-time" @emit-active-tab="emitActiveTab" />
<date-panel ref="datePanel" :dateType="dateType" @emit-time="emitTime" @emit-popup="openPopup" />
<div class="statistics-container panel-m b-12">
<div class="img-container">
<img class="img" :src="dynamicText.img" alt="" />
</div>
<div class="data-container">
<div class="data-count">
<p class="data-number">{{ cardInfo?.totalTime || 0 }}</p>
<p class="data-text">累计时长()</p>
</div>
<div class="data-count" v-if="route.query?.classId == '1'">
<p class="data-number"> {{ cardInfo?.statisticsObj?.exerciseSum?.step || 0 }}</p>
<p class="data-text">累计步数()</p>
</div>
<div class="data-count" v-if="route.query?.classId == '1'">
<p class="data-number">{{ cardInfo?.totalFinishCount || 0 }}</p>
<p class="data-text">累计运动()</p>
</div>
<div class="data-count">
<p class="data-number"> {{ cardInfo?.totalConsumeSum || 0 }}</p>
<p class="data-text">累计消耗(千卡)</p>
</div>
</div>
</div>
<div class="statistics-chart panel-m card-bg b-12" v-show="showChart">
<div class="statistics-chart-inner" id="columnar" ref="chartRef"></div>
</div>
<div class="highest-record panel-m" v-if="cardInfo?.topRecord">
<p>{{ getHighestText(activeTab) }}</p>
<div class="highest-content card-bg b-12">
<div class="icon-time">
<img class="first-icon" :src="firstIcon" alt="" />
<span class="highest-time">{{ cardInfo?.topRecord?.time || '' }}</span>
</div>
<div class="highest-info">
<span>{{ cardInfo?.topRecord?.consume || '' }} <span>千卡</span></span>
<span>{{ cardInfo?.topRecord?.duration || '' }} <span>分钟</span></span>
</div>
</div>
</div>
<div class="motion panel-m card-bg b-12">
<div class="bb-item">
<div>活动消耗</div>
<div>
<span class="highest-num">{{ cardInfo?.statisticsObj?.exerciseConsumeDail || 0 }}</span>
<span>千卡</span>
</div>
</div>
<div class="bb-item">
<div>活动时长</div>
<div>
<span class="highest-num">{{ cardInfo?.totalTimeDail || 0 }}</span>
<span>分钟</span>
</div>
</div>
</div>
</div>
<div class="step-btn" @click="fillData">录入</div>
<van-popup v-model:show="popupData.show" position="bottom">
<van-picker title="选择周" :columns="popupData.columns" @cancel="onCancel" @confirm="onConfirm" />
</van-popup>
</div>
</template>
<script lang="ts" setup>
import { useTabHooks } from '/@/hooks/useTab';
import { dateText, dateTypeEnum, firstIcon, getHighestText, stepChart } from '/@/views/motion-data/common.data';
import { timeParams } from './hooks';
import { ref } from 'vue';
import { getSportType } from '/@/views/motion-data/api';
import { useRoute } from 'vue-router';
import { showToast } from 'vant';
import * as echarts from 'echarts';
import { newPageParams, openPage } from '/@/hooks/openPage';
import TabTime from '/@/views/motion-data/components/tab-time/tabTime.vue';
import DatePanel from '/@/views/motion-data/components/date-panel/datePanel.vue';
const route = useRoute();
document.title = route.query?.className || '';
const { activeTab } = useTabHooks(0);
// 当前日期类型
const dateType = ref('week');
const timeText = ref('');
const dynamicText = ref(dateText[0]);
// 年月周切换
function emitActiveTab({ type }) {
dateType.value = type;
dynamicText.value = dateText.find((item) => item.type == dateType.value);
}
// 卡片数据
const cardInfo = ref({
totalTime: '',
totalTimeDail: '',
chartVis: true,
totalFinishCount: 0,
statisticsObj: {},
xData: [],
yData: [],
topRecord: {},
totalConsumeSum: 0,
});
function emitTime(nowTime: string) {
if (!nowTime) return;
timeText.value = timeParams(nowTime);
const params = createParams(timeText.value, dateType.value);
initData(params);
}
const chartRef = ref<HTMLElement>();
const myChart = ref<any>();
const showChart = ref(false);
function createParams(exerciseTime: string, type: string) {
return {
classId: route.query.classId,
exerciseTime: exerciseTime,
type: dateTypeEnum[type],
};
}
function initData(params: any) {
showChart.value = false;
getSportType(params).then(({ code, data }) => {
if (code !== 200) {
return showToast('暂无数据');
}
cardInfo.value = {
totalTime: data?.exerciseSum?.totalTime,
totalFinishCount: data?.exerciseSum?.totalFinishCount,
totalTimeDail: data.totalTimeDail,
chartVis: true,
statisticsObj: data,
xData: data?.nameList,
yData: data?.consumeValueList,
topRecord: data?.singleDayRecord,
totalConsumeSum: data?.exerciseSum?.totalConsumeSum,
};
if (data?.nameList.length) {
showChart.value = true;
setTimeout(() => {
initChart({ xData: data?.nameList, yData: data?.consumeValueList });
}, 300);
}
});
}
function initChart({ xData, yData }) {
myChart.value = echarts.init(document.getElementById('columnar'));
myChart.value.setOption(stepChart({ xData, yData }));
}
function fillData() {
openPage('/sport-form', newPageParams());
}
const popupData = ref({
columns: [],
show: false,
});
// 打开周选择
function openPopup({ column }) {
popupData.value.columns = column;
popupData.value.show = true;
}
const datePanel = ref();
function onConfirm(val) {
let text = val.selectedValues[0];
timeText.value = timeParams(text);
const params = createParams(timeText.value, dateType.value);
initData(params);
datePanel.value.setTimeText(text);
popupData.value.show = false;
}
function onCancel() {
popupData.value.show = false;
}
</script>
<style lang="less" scoped>
@import '/@/assets/less/index.less';
p {
margin: 0;
}
.panel-m {
margin: 6px 16px;
}
.card-bg {
background: #ffffff;
}
.b-12 {
border-radius: 12px;
}
.step-number {
height: 100vh;
background: #eeeeee;
box-sizing: border-box;
//padding-top: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
.step-top {
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
overflow: auto;
.tab-time {
margin: 10px 20px;
padding: 2px;
height: 35px;
background: #ffffff;
border-radius: 35px;
display: flex;
}
.date-panel {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
.date-time {
margin: 0 10px;
}
}
.statistics-container {
padding: 13px 0;
background: #ffffff;
.flex-center();
.img-container {
width: 60px;
height: 60px;
margin: 0 16px;
.img {
width: 100%;
height: 100%;
}
}
.data-container {
flex: 4;
display: flex;
flex-wrap: wrap;
.data-count {
width: 50%;
text-align: center;
.data-number {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.data-text {
font-size: 14px;
font-weight: 500;
color: #666666;
}
}
}
}
.statistics-chart {
//min-height: 240px;
.statistics-chart-inner {
width: 100%;
min-height: 240px;
}
}
.bar-data {
display: flex;
flex-wrap: wrap;
padding: 16px;
.bar-data-item {
width: 50%;
& p,
span {
font-size: 14px;
font-weight: 500;
color: #666666;
}
.bar-data-number {
font-size: 20px;
font-weight: bold;
color: #333333;
}
}
}
.highest-record {
& > p {
font-size: 16px;
margin-bottom: 6px;
}
.highest-content {
display: flex;
align-items: center;
padding: 16px;
.icon-time {
display: flex;
align-items: center;
}
.first-icon {
width: 36px;
height: 36px;
}
.highest-time {
flex: 1;
margin: 0 15px 0 6px;
}
}
.highest-info {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: bold;
color: #333333;
}
}
.motion {
padding: 14px;
display: flex;
font-size: 14px;
font-weight: 500;
color: #666666;
.bb-item {
flex: 1;
}
.highest-num {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-right: 5px;
}
}
}
.step-btn {
margin: 12px 15% 28px 15%;
height: 40px;
.flex-center();
.qs-btn();
border-radius: 40px;
font-size: 16px;
font-weight: 500;
color: #ffffff;
}
}
</style>
@@ -0,0 +1,14 @@
export const yogaCardText = [
{
text1: '日均时长',
text2: '日均消耗',
},
{
text1: '日均时长',
text2: '日均消耗',
},
{
text1: '月均时长',
text2: '月均消耗',
},
];
@@ -0,0 +1,283 @@
<template>
<div class="yoga-number">
<div class="yoga-top">
<div class="tab-time">
<div
class="tab-time-item"
v-for="(item, i) in dynamicText"
:key="i"
:class="[activeTab == i ? 'active' : '']"
@click="setActiveTab(i)"
v-text="item.text"
></div>
</div>
<div class="date-panel">
<van-icon name="arrow-left" />
<span class="date-time" @click="changeDate">9月4日至9月10日</span>
<van-icon name="arrow" />
</div>
<div class="statistics-container panel-m b-12">
<div class="img-container">
<img class="img" :src="dynamicText[activeTab].img" alt="" />
</div>
<div class="data-container">
<div class="data-count">
<p class="data-number">487</p>
<p class="data-text">时长(分钟)</p>
</div>
<div class="data-count">
<p class="data-number">123</p>
<p class="data-text">累计{{ dynamicText[activeTab].text }}消耗</p>
</div>
<div class="data-count">
<p class="data-number">123</p>
<p class="data-text">累计完成()</p>
</div>
</div>
</div>
<div class="statistics-chart panel-m card-bg b-12">
<div class="statistics-chart-inner"> 图表1图表1 图表1图表1</div>
</div>
<div class="bar-data panel-m card-bg b-12">
<div class="bar-data-item">
<p>{{ cardText.text1 }}</p>
<p><span class="bar-data-number">123</span> <span>分钟</span></p>
</div>
<div class="bar-data-item">
<p>{{ cardText.text2 }}</p>
<p><span class="bar-data-number">34</span> <span></span></p>
</div>
</div>
<div class="highest-record panel-m">
<p>{{ getHighestText(activeTab) }}</p>
<div class="highest-content card-bg b-12">
<div class="icon-time">
<img class="first-icon" :src="firstIcon" alt="" />
<span class="highest-time">2020-11-28</span>
</div>
<div class="highest-info">
<span style="opacity: 0"></span>
<span>13.62 <span>分钟</span></span>
</div>
</div>
</div>
</div>
<div class="yoga-btn">录入</div>
<DatePopup ref="datePopup" />
</div>
</template>
<script lang="ts" setup>
import { useTabHooks } from '/@/hooks/useTab';
import { dateText, firstIcon, getHighestText } from '/@/views/motion-data/common.data';
import { yogaCardText } from './hooks';
import { computed, ref } from 'vue';
import DatePopup from '/@/views/motion-data/components/date-popup/datePopup.vue';
const { activeTab, setActiveTab } = useTabHooks(0);
let query = 'yoga';
const dynamicText = computed(() => {
if (query == 'yoga') {
return dateText;
}
});
const cardText = computed(() => {
if (query == 'yoga') {
return yogaCardText[activeTab.value];
}
});
const datePopup = ref();
function changeDate() {
datePopup.value.showPopup();
}
</script>
<style lang="less" scoped>
@import '/@/assets/less/index.less';
p {
margin: 0;
}
.panel-m {
margin: 6px 16px;
}
.card-bg {
background: #ffffff;
}
.b-12 {
border-radius: 12px;
}
.yoga-number {
height: 100vh;
background: #eeeeee;
box-sizing: border-box;
//padding-top: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
.yoga-top {
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
overflow: auto;
.tab-time {
margin: 10px 20px;
padding: 2px;
height: 35px;
background: #ffffff;
border-radius: 35px;
display: flex;
.tab-time-item {
flex: 1;
color: #333;
font-weight: bold;
.flex-center();
&.active {
color: #fff;
border-radius: 20px;
background: #52c41a;
}
}
}
.date-panel {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
.date-time {
margin: 0 10px;
}
}
.statistics-container {
height: 140px;
padding: 6px 0;
background: #ffffff;
.flex-center();
.img-container {
width: 60px;
height: 60px;
margin: 0 16px;
.img {
width: 100%;
height: 100%;
}
}
.data-container {
flex: 4;
display: flex;
flex-wrap: wrap;
.data-count {
width: 50%;
text-align: center;
.data-number {
font-size: 20px;
font-weight: bold;
color: #333333;
}
.data-text {
font-size: 14px;
font-weight: 500;
color: #666666;
}
}
}
}
.statistics-chart {
//min-height: 240px;
.statistics-chart-inner {
min-height: 240px;
}
}
.bar-data {
display: flex;
flex-wrap: wrap;
padding: 16px;
.bar-data-item {
width: 50%;
& p,
span {
font-size: 14px;
font-weight: 500;
color: #666666;
}
.bar-data-number {
font-size: 20px;
font-weight: bold;
color: #333333;
}
}
}
.highest-record {
& > p {
font-size: 16px;
margin-bottom: 6px;
}
.highest-content {
display: flex;
align-items: center;
padding: 16px;
.icon-time {
display: flex;
align-items: center;
}
.first-icon {
width: 36px;
height: 36px;
}
.highest-time {
flex: 1;
margin: 0 15px 0 6px;
}
}
.highest-info {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: bold;
color: #333333;
}
}
}
.yoga-btn {
margin: 12px 15% 28px 15%;
height: 40px;
.flex-center();
.qs-btn();
border-radius: 40px;
font-size: 20px;
font-weight: 500;
color: #ffffff;
}
}
</style>