feat: 首頁介紹

This commit is contained in:
skytek_xinliang
2026-09-16 14:22:32 +08:00
parent ab8da775a6
commit 8b28112cbf
6 changed files with 538 additions and 12 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,345 @@
<script setup lang="ts">
import { ref } from "vue";
import introductionImage from "@/assets/index/introduction_0916.png";
import { PLATFORM_INTRODUCTION } from "@/data/platformIntroduction";
const expandedSections = ref([0, 1, 2, 3]);
</script>
<template>
<section
class="platform-introduction"
aria-labelledby="platform-introduction-title"
>
<div class="introduction-image-frame">
<v-img
alt="運動玩轉健康力全齡功能性體適能檢測平台資訊圖"
class="introduction-image"
:src="introductionImage"
/>
</div>
<v-card class="introduction-card" variant="flat">
<v-card-text class="pa-5 pa-sm-7">
<div class="introduction-heading">
<span class="eyebrow">{{ PLATFORM_INTRODUCTION.eyebrow }}</span>
<h2 id="platform-introduction-title" class="introduction-title">
{{ PLATFORM_INTRODUCTION.title }}
</h2>
<p class="introduction-subtitle">
{{ PLATFORM_INTRODUCTION.subtitle }}
</p>
</div>
<div class="journey-list" aria-label="全齡健康旅程">
<template
v-for="(journey, index) in PLATFORM_INTRODUCTION.journey"
:key="journey"
>
<span class="journey-item">{{ journey }}</span>
<v-icon
v-if="index < PLATFORM_INTRODUCTION.journey.length - 1"
class="journey-arrow"
color="secondary"
icon="mdi-arrow-right"
size="18"
/>
</template>
</div>
<v-expansion-panels v-model="expandedSections" class="mt-6" multiple>
<v-expansion-panel
v-for="(section, index) in PLATFORM_INTRODUCTION.sections"
:key="section.title"
class="introduction-panel"
:value="index"
>
<v-expansion-panel-title class="px-4 py-3">
<div class="d-flex align-center ga-3">
<span class="section-number">{{
String(index + 1).padStart(2, "0")
}}</span>
<span class="font-weight-bold text-primary">{{
section.title
}}</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<p v-if="section.intro" class="section-intro">
{{ section.intro }}
</p>
<ol v-if="section.steps" class="service-steps">
<li
v-for="(step, stepIndex) in section.steps"
:key="step.title"
>
<span class="step-number">{{ stepIndex + 1 }}</span>
<div>
<div class="font-weight-bold text-primary">
{{ step.title }}
</div>
<div class="text-grey-darken-1">{{ step.detail }}</div>
</div>
</li>
</ol>
<div v-if="section.features" class="feature-grid">
<div
v-for="(feature, featureIndex) in section.features"
:key="feature.title"
class="feature-item"
>
<span class="feature-number">{{
String(featureIndex + 1).padStart(2, "0")
}}</span>
<div class="font-weight-bold text-primary">
{{ feature.title }}
</div>
<div class="text-grey-darken-1">{{ feature.detail }}</div>
</div>
</div>
<div v-if="section.audiences" class="audience-list">
<div
v-for="audience in section.audiences"
:key="audience.title"
class="audience-item"
>
<div class="font-weight-bold text-primary mb-2">
{{ audience.title }}
</div>
<ul class="content-list">
<li v-for="item in audience.items" :key="item">
{{ item }}
</li>
</ul>
</div>
</div>
<ul v-if="section.items" class="content-list">
<li v-for="item in section.items" :key="item">{{ item }}</li>
</ul>
<p v-if="section.outro" class="section-outro">
{{ section.outro }}
</p>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
</section>
</template>
<style scoped>
.platform-introduction {
max-width: 760px;
margin: 24px auto 40px;
}
.introduction-image-frame {
padding: 6px;
border-radius: 28px;
background: linear-gradient(
135deg,
rgba(29, 112, 99, 0.16),
rgba(255, 163, 49, 0.22)
);
box-shadow: 0 14px 34px rgba(20, 82, 72, 0.12);
}
.introduction-image {
border-radius: 23px;
overflow: hidden;
}
.introduction-card {
margin-top: 20px;
border: 1px solid rgba(29, 112, 99, 0.12);
border-radius: 28px;
background: linear-gradient(160deg, #f6fcf9 0%, #ffffff 38%);
}
.introduction-heading {
text-align: center;
}
.eyebrow {
display: inline-block;
padding: 6px 12px;
border-radius: 999px;
color: rgb(var(--v-theme-primary));
background-color: rgba(var(--v-theme-primary), 0.1);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
}
.introduction-title {
margin: 14px 0 4px;
color: rgb(var(--v-theme-primary));
font-size: clamp(1.75rem, 5vw, 2.5rem);
font-weight: 900;
line-height: 1.2;
}
.introduction-subtitle {
margin: 0;
color: rgb(var(--v-theme-primary));
font-size: 1rem;
}
.journey-list {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 20px;
}
.journey-item {
padding: 8px 13px;
border-radius: 12px;
color: rgb(var(--v-theme-primary));
background-color: #ffffff;
box-shadow: 0 3px 10px rgba(20, 82, 72, 0.08);
font-weight: 700;
}
.journey-arrow {
flex: 0 0 auto;
}
.introduction-panel {
margin-bottom: 10px;
border: 1px solid rgba(29, 112, 99, 0.1);
border-radius: 16px !important;
overflow: hidden;
}
.section-number,
.feature-number,
.step-number {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: #ffffff;
background-color: rgb(var(--v-theme-secondary));
font-weight: 900;
}
.section-number {
width: 34px;
height: 34px;
border-radius: 10px;
font-size: 0.8rem;
}
.section-intro,
.section-outro {
margin: 0 0 14px;
color: rgb(var(--v-theme-primary));
line-height: 1.75;
}
.section-outro {
margin: 14px 0 0;
font-weight: 700;
}
.content-list {
display: grid;
gap: 10px;
margin: 0;
padding-left: 22px;
color: rgb(var(--v-theme-on-surface));
line-height: 1.7;
}
.content-list li::marker {
color: rgb(var(--v-theme-secondary));
}
.service-steps {
display: grid;
gap: 14px;
margin: 0;
padding: 0;
list-style: none;
}
.service-steps li {
display: flex;
gap: 12px;
line-height: 1.65;
}
.step-number {
width: 28px;
height: 28px;
border-radius: 50%;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.feature-item,
.audience-item {
padding: 16px;
border-radius: 16px;
background-color: rgba(var(--v-theme-primary), 0.06);
line-height: 1.65;
}
.feature-item:nth-child(even) {
background-color: rgba(var(--v-theme-secondary), 0.11);
}
.feature-number {
width: 30px;
height: 30px;
margin-bottom: 10px;
border-radius: 9px;
font-size: 0.75rem;
}
.audience-list {
display: grid;
gap: 12px;
}
@media (max-width: 600px) {
.platform-introduction {
margin-top: 20px;
}
.introduction-image-frame,
.introduction-card {
border-radius: 20px;
}
.introduction-image {
border-radius: 15px;
}
.journey-list {
gap: 6px;
}
.journey-item {
padding: 7px 10px;
font-size: 0.9rem;
}
.feature-grid {
grid-template-columns: 1fr;
}
}
</style>
+6 -6
View File
@@ -1,6 +1,6 @@
export const APPOINTMENT_ENABLED = false; export const APPOINTMENT_ENABLED = false
export const BOTTOM_NAVIGATION_ENABLED = false; export const BOTTOM_NAVIGATION_ENABLED = false
export const CHECKIN_ENABLED = false; export const CHECKIN_ENABLED = false
export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = true; export const EXERCISE_DURATION_RECOMMENDATION_ENABLED = false
export const RADAR_ENABLED = false; export const RADAR_ENABLED = false
export const SELF_ASSESSMENT_ENABLED = false; export const SELF_ASSESSMENT_ENABLED = false
@@ -0,0 +1,147 @@
{
"title": "運動玩轉健康力",
"subtitle": "全齡功能性體適能檢測平台簡介",
"eyebrow": "全齡功能性體適能檢測平台",
"journey": ["看見狀態", "知道怎麼做", "持續回來"],
"sections": [
{
"title": "平台定位",
"items": [
"以「運動 × AI × 數據 × 健康」為核心,建置全齡功能性體適能檢測與健康促進平台。",
"服務對象涵蓋兒童、青壯年、中高齡及銀髮族。",
"結合科學檢測、數位分析、體適能分級、運動建議及持續追蹤。",
"將一次性的體適能檢測,轉化為可執行、可追蹤、可持續的健康行動。"
]
},
{
"title": "核心理念",
"items": [
"秉持「把數據變成行動,把行動變成習慣」的推動理念。",
"協助參與者看見身體的真實狀態,了解自身優勢與需要改善的能力。",
"依據個人身體狀態提供安全、適合且能夠持續執行的運動方式。",
"強調沒有任何訓練目標高於身體的真實狀態,讓健康促進建立在安全與可恢復的基礎上。"
]
},
{
"title": "11項功能性體適能測驗",
"intro": "平台整合11項功能性體適能測驗,評估面向包括:",
"items": [
"上肢肌力、下肢肌力。",
"上肢柔軟度、下肢柔軟度。",
"敏捷與移動能力、靜態平衡能力。",
"心肺耐力、日常生活功能表現。"
],
"outro": "透過多面向檢測,協助參與者掌握目前的身體功能、潛在風險及需要優先改善的項目。"
},
{
"title": "平台服務流程",
"steps": [
{
"title": "功能檢測",
"detail": "完成11項功能性體適能測驗,建立個人基礎數據。"
},
{
"title": "數據分析",
"detail": "系統整合各項測驗結果,呈現個人身體功能表現。"
},
{
"title": "體適能分級",
"detail": "依據個人檢測結果進行綜合評估與體適能分級。"
},
{
"title": "運動建議",
"detail": "依照身體狀態提供有氧、肌力、平衡及柔軟伸展等建議。"
},
{
"title": "行動課表",
"detail": "將運動建議轉化為一週每日行動課表,提供清楚、容易執行的健康指引。"
},
{
"title": "定期複測",
"detail": "透過定期回測與前後數據比較,追蹤身體功能及運動成效。"
}
]
},
{
"title": "四大核心功能",
"features": [
{ "title": "11項功能測驗", "detail": "一次掌握身體功能與目前狀態。" },
{
"title": "個人化體適能分級",
"detail": "快速看懂自身優勢、潛在風險及需要加強的能力。"
},
{
"title": "精準運動建議",
"detail": "依據檢測結果安排有氧、肌力、平衡及伸展訓練。"
},
{
"title": "每週行動課表",
"detail": "提供早、中、晚清楚指引,協助建立可持續的運動習慣。"
}
]
},
{
"title": "數位健康履歷",
"items": [
"保存個人歷次功能性體適能檢測結果,建立個人專屬的數位健康紀錄。",
"比較檢測前後的身體功能變化,掌握運動介入及課程訓練成效。",
"提升參與者的運動持續性與自我健康管理能力。",
"作為後續課程調整、健康追蹤及定期複測的參考依據。"
]
},
{
"title": "平台應用場域",
"items": [
"社區健康促進活動、各級學校與教育場域。",
"企業員工健康管理、運動中心與健身場域。",
"樂齡學習及銀髮服務據點、長照機構與社會福利單位。",
"醫療及健康促進合作場域,以及ESG、CSR及公益普測活動。"
]
},
{
"title": "平台使用價值",
"audiences": [
{
"title": "對參與者",
"items": [
"認識自己的身體狀態。",
"找出需要優先改善的能力。",
"獲得適合自己的運動方向。",
"看見運動前後的身體改變,建立安全且可持續的健康習慣。"
]
},
{
"title": "對教練及專業人員",
"items": [
"掌握參與者的功能表現與風險。",
"作為課程分級及內容調整的依據。",
"提供個人化運動與訓練建議,必要時即時調整或停止不適合的訓練。"
]
},
{
"title": "對場域及政策推動單位",
"items": [
"建立可量化的健康促進成果。",
"支援課程規劃、成效管理及資源配置。",
"建立可追蹤、可複製、可擴散的服務模式。",
"在資料去識別化及適當授權下,作為健康促進政策分析參考。"
]
}
]
},
{
"title": "全齡健康旅程",
"intro": "平台陪伴每一位參與者完成三個重要階段:",
"items": [
"看見狀態:了解現在的身體功能與健康狀況。",
"知道怎麼做:找到安全、適合且可以實際執行的運動方式。",
"持續回來:透過運動、追蹤及定期複測,逐步建立健康習慣。"
]
},
{
"title": "平台願景",
"intro": "「運動玩轉健康力」不只提供一次檢測的分數,更希望透過科學檢測、數據分析、行動課表與持續追蹤,陪伴每一個人建立長期健康生活。",
"outro": "健康無價,從了解自己的身體開始。讓運動成為習慣,讓健康成為日常。"
}
]
}
@@ -0,0 +1,37 @@
import platformIntroduction from "./platform-introduction.json";
export interface IntroductionStep {
title: string;
detail: string;
}
export interface IntroductionFeature {
title: string;
detail: string;
}
export interface IntroductionAudience {
title: string;
items: string[];
}
export interface IntroductionSection {
title: string;
intro?: string;
items?: string[];
outro?: string;
steps?: IntroductionStep[];
features?: IntroductionFeature[];
audiences?: IntroductionAudience[];
}
export interface PlatformIntroductionContent {
title: string;
subtitle: string;
eyebrow: string;
journey: string[];
sections: IntroductionSection[];
}
export const PLATFORM_INTRODUCTION =
platformIntroduction as PlatformIntroductionContent;
+3 -6
View File
@@ -274,6 +274,8 @@
/> />
</template> </template>
<PlatformIntroduction />
<v-dialog v-model="generateDialog" max-width="400"> <v-dialog v-model="generateDialog" max-width="400">
<v-card class="rounded-xl overflow-hidden"> <v-card class="rounded-xl overflow-hidden">
<v-card-title class="bg-primary text-white font-weight-bold py-3"> <v-card-title class="bg-primary text-white font-weight-bold py-3">
@@ -404,6 +406,7 @@ import BrandAvatar from "@/components/BrandAvatar.vue";
import HealthRadarChart from "@/components/HealthRadarChart.vue"; import HealthRadarChart from "@/components/HealthRadarChart.vue";
import Layout from "@/components/layout/DefaultLayout.vue"; import Layout from "@/components/layout/DefaultLayout.vue";
import ModuleRouterCard from "@/components/ModuleRouterCard.vue"; import ModuleRouterCard from "@/components/ModuleRouterCard.vue";
import PlatformIntroduction from "@/components/PlatformIntroduction.vue";
import PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue"; import PhysicalFitnessSummary from "@/components/PhysicalFitnessSummary.vue";
import { useDisplayUserName } from "@/composables/useDisplayUserName"; import { useDisplayUserName } from "@/composables/useDisplayUserName";
import { import {
@@ -428,7 +431,6 @@ const physicalFitnessItems = ref<PhysicalFitnessItem[]>([]);
const physicalFitnessReceivedAt = ref<string | null>(null); const physicalFitnessReceivedAt = ref<string | null>(null);
const physicalFitnessLoading = ref(false); const physicalFitnessLoading = ref(false);
const physicalFitnessError = ref(""); const physicalFitnessError = ref("");
const redirectingToLogin = ref(false);
const displayUserName = useDisplayUserName(store.userProfile); const displayUserName = useDisplayUserName(store.userProfile);
@@ -498,11 +500,6 @@ function getRequiredUserID(): string | null {
return userID; return userID;
} }
if (!redirectingToLogin.value) {
redirectingToLogin.value = true;
void router.replace({ name: "login", query: { redirect: "/" } });
}
return null; return null;
} }