style: index page
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
This repository is a private pnpm workspace. Workspace packages are declared in `pnpm-workspace.yaml` under `app/*`.
|
||||||
|
|
||||||
|
The active application is `app/citizen-frontend`, a Vue 3 + Vite + Vuetify frontend written in TypeScript. Its source lives in `app/citizen-frontend/src/`:
|
||||||
|
|
||||||
|
- `src/main.ts` bootstraps the app and global styles.
|
||||||
|
- `src/App.vue` hosts the router view.
|
||||||
|
- `src/pages/` contains route-level pages.
|
||||||
|
- `src/components/` contains reusable Vue components.
|
||||||
|
- `src/stores/` contains Pinia stores.
|
||||||
|
- `src/router/` contains Vue Router configuration.
|
||||||
|
- `src/assets/` stores static assets such as local fonts.
|
||||||
|
- `src/styles/` contains global style and Vuetify settings.
|
||||||
|
|
||||||
|
If a nested `AGENTS.md` exists, follow the closest one for files in that subtree.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
Run commands from `app/citizen-frontend` unless noted otherwise.
|
||||||
|
|
||||||
|
- `pnpm install`: install workspace dependencies.
|
||||||
|
- `pnpm dev`: start the Vite development server.
|
||||||
|
- `pnpm build`: run type-checking and production build.
|
||||||
|
- `pnpm build-only`: build the Vite app without type-checking.
|
||||||
|
- `pnpm type-check`: run `vue-tsc`.
|
||||||
|
- `pnpm lint`: run ESLint.
|
||||||
|
- `pnpm lint:fix`: apply ESLint auto-fixes.
|
||||||
|
- `pnpm preview`: preview the production build locally.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
Use TypeScript, Vue 3 Composition API, and `<script setup>` for Vue components. Follow the existing Vuetify component style and route/page organization. Use 2-space indentation, single quotes, no semicolons, and trailing commas where the linter expects them.
|
||||||
|
|
||||||
|
Name Vue pages and components descriptively, matching existing patterns such as `self-assessment.vue`, `DefaultLayout.vue`, and Pinia stores under `src/stores/`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
No dedicated test suite is currently configured. For changes, run at minimum:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm lint
|
||||||
|
pnpm type-check
|
||||||
|
```
|
||||||
|
|
||||||
|
For UI or routing changes, also run `pnpm build` and manually verify the affected flows in the browser.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
Recent commits use short Conventional Commit-style prefixes, for example `feat:`, `style:`, and `restyle:`. Keep commit messages concise and scoped to the change.
|
||||||
|
|
||||||
|
Pull requests should include a short summary, affected paths, verification commands run, and screenshots or screen recordings for visible UI changes. Link related issues when available.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
|
||||||
|
Do not commit secrets or environment-specific credentials. Keep third-party licenses with bundled assets, especially files under `src/assets/`.
|
||||||
@@ -9,3 +9,7 @@
|
|||||||
- Framework: Vue 3 + Vite
|
- Framework: Vue 3 + Vite
|
||||||
- UI Library: Vuetify
|
- UI Library: Vuetify
|
||||||
- Enabled Features: ESLint, Pinia, Vue I18n, Vue Router
|
- Enabled Features: ESLint, Pinia, Vue I18n, Vue Router
|
||||||
|
|
||||||
|
## UI Construction
|
||||||
|
- Prefer Vuetify built-in UI components and utility classes when building pages.
|
||||||
|
- Use custom CSS only when Vuetify components or utilities cannot express the required layout, spacing, color, or interaction clearly.
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ export default vuetify(
|
|||||||
'vue/script-indent': 'off',
|
'vue/script-indent': 'off',
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': 'off',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"lint:fix": "eslint --fix"
|
"lint:fix": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.2.10",
|
|
||||||
"@mdi/font": "7.4.47",
|
"@mdi/font": "7.4.47",
|
||||||
"vue": "^3.5.30",
|
"vue": "^3.5.30",
|
||||||
"vuetify": "^4.0.2",
|
"vuetify": "^4.0.2",
|
||||||
@@ -29,16 +28,10 @@
|
|||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"sass-embedded": "^1.98.0",
|
"sass-embedded": "^1.98.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"unplugin-fonts": "^1.4.0",
|
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-vuetify": "^2.1.3",
|
"vite-plugin-vuetify": "^2.1.3",
|
||||||
"vue-tsc": "^3.2.5",
|
"vue-tsc": "^3.2.5",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-vuetify": "^4.3.4"
|
"eslint-config-vuetify": "^4.3.4"
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"unplugin-fonts": {
|
|
||||||
"vite": "^8.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex justify-center my-2 position-relative">
|
||||||
|
<svg class="radar-svg" height="260" viewBox="0 0 120 120" width="260">
|
||||||
|
<polygon
|
||||||
|
v-for="grid in [20, 40, 60, 80, 100]"
|
||||||
|
:key="grid"
|
||||||
|
fill="none"
|
||||||
|
:points="getPolygonPoints(grid)"
|
||||||
|
stroke="#E0E0E0"
|
||||||
|
stroke-width="0.3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<line
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="`axis-${i}`"
|
||||||
|
stroke="#E0E0E0"
|
||||||
|
stroke-width="0.3"
|
||||||
|
x1="60"
|
||||||
|
:x2="getAxisEndPoint(i - 1).x"
|
||||||
|
y1="60"
|
||||||
|
:y2="getAxisEndPoint(i - 1).y"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<polygon
|
||||||
|
fill="rgba(33, 150, 243, 0.12)"
|
||||||
|
:points="getPolygonPoints(65)"
|
||||||
|
stroke="#2196F3"
|
||||||
|
stroke-dasharray="1 1"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<polygon
|
||||||
|
fill="rgba(71, 194, 166, 0.35)"
|
||||||
|
:points="userDataPoints"
|
||||||
|
stroke="#1D7063"
|
||||||
|
stroke-width="1.2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<circle
|
||||||
|
v-for="(point, index) in userDataCoordArray"
|
||||||
|
:key="`dot-${index}`"
|
||||||
|
:cx="point.x"
|
||||||
|
:cy="point.y"
|
||||||
|
fill="#E66926"
|
||||||
|
r="1.5"
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="middle" x="60" y="8">
|
||||||
|
心肺 ({{ radarData.cardio }})
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="start" x="110" y="44">
|
||||||
|
肌力 ({{ radarData.strength }})
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="start" x="94" y="104">
|
||||||
|
平衡 ({{ radarData.balance }})
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="end" x="26" y="104">
|
||||||
|
柔軟 ({{ radarData.flexibility }})
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text fill="#1D7063" font-size="4" font-weight="bold" text-anchor="end" x="10" y="44">
|
||||||
|
敏捷 ({{ radarData.agility }})
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- <div class="radar-legend d-flex flex-column">
|
||||||
|
<div class="d-flex align-center text-caption mb-1">
|
||||||
|
<span class="legend-color bg-primary mr-1" />
|
||||||
|
<span>我的指標</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-center text-caption">
|
||||||
|
<span class="legend-color bg-blue mr-1 border-dashed" />
|
||||||
|
<span>同齡參考</span>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface RadarData {
|
||||||
|
strength: number
|
||||||
|
balance: number
|
||||||
|
flexibility: number
|
||||||
|
agility: number
|
||||||
|
cardio: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const { radarData } = defineProps<{
|
||||||
|
radarData: RadarData
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const centerX = 60
|
||||||
|
const centerY = 60
|
||||||
|
const radius = 45
|
||||||
|
|
||||||
|
function getAxisEndPoint(index: number, pointRadius = radius) {
|
||||||
|
const angle = -Math.PI / 2 + (index * 2 * Math.PI) / 5
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: centerX + pointRadius * Math.cos(angle),
|
||||||
|
y: centerY + pointRadius * Math.sin(angle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPolygonPoints(percent: number) {
|
||||||
|
const pointRadius = (percent / 100) * radius
|
||||||
|
const points = []
|
||||||
|
|
||||||
|
for (let index = 0; index < 5; index++) {
|
||||||
|
const point = getAxisEndPoint(index, pointRadius)
|
||||||
|
points.push(`${point.x},${point.y}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return points.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDataCoordArray = computed(() => {
|
||||||
|
const values = [
|
||||||
|
radarData.cardio,
|
||||||
|
radarData.strength,
|
||||||
|
radarData.balance,
|
||||||
|
radarData.flexibility,
|
||||||
|
radarData.agility,
|
||||||
|
]
|
||||||
|
|
||||||
|
return values.map((value, index) => {
|
||||||
|
const pointRadius = (Math.max(5, Math.min(100, value)) / 100) * radius
|
||||||
|
return getAxisEndPoint(index, pointRadius)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const userDataPoints = computed(() => {
|
||||||
|
return userDataCoordArray.value.map((point) => `${point.x},${point.y}`).join(' ')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.radar-legend {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-dashed {
|
||||||
|
background-color: rgba(33, 150, 243, 0.12) !important;
|
||||||
|
border: 1px dashed #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radar-svg text {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
class="rounded-xl elevation-2 d-flex align-center justify-between pa-4 my-4"
|
||||||
|
color="surface"
|
||||||
|
:to="to"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-avatar class="mr-3 text-white elevation-1" :color="iconColor" size="48">
|
||||||
|
<v-icon :icon="icon" />
|
||||||
|
</v-avatar>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">{{ title }}</div>
|
||||||
|
|
||||||
|
<div class="text-caption text-grey">{{ description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-icon color="primary" icon="mdi-chevron-right" />
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
to: string
|
||||||
|
icon: string
|
||||||
|
iconColor: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -21,18 +21,6 @@
|
|||||||
>
|
>
|
||||||
<v-icon :color="isLargeText ? 'accent' : 'white'" icon="mdi-format-size" />
|
<v-icon :color="isLargeText ? 'accent' : 'white'" icon="mdi-format-size" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-avatar
|
|
||||||
v-if="store.isLoggedIn"
|
|
||||||
class="elevation-1 border-white"
|
|
||||||
color="accent"
|
|
||||||
size="36"
|
|
||||||
@click="drawer = !drawer"
|
|
||||||
>
|
|
||||||
<span class="text-white font-weight-bold text-caption">{{
|
|
||||||
store.userProfile.name.substring(1)
|
|
||||||
}}</span>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
@@ -189,7 +177,7 @@
|
|||||||
>
|
>
|
||||||
<v-btn to="/" value="home">
|
<v-btn to="/" value="home">
|
||||||
<v-icon :color="getBottomNavIconColor('home')">mdi-view-dashboard</v-icon>
|
<v-icon :color="getBottomNavIconColor('home')">mdi-view-dashboard</v-icon>
|
||||||
<span class="bottom-nav-text">主控台</span>
|
<span class="bottom-nav-text">首頁</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn to="/self-assessment" value="self-assessment">
|
<v-btn to="/self-assessment" value="self-assessment">
|
||||||
@@ -197,17 +185,17 @@
|
|||||||
mdi-clipboard-text-search
|
mdi-clipboard-text-search
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
|
||||||
<span class="bottom-nav-text">線上自評</span>
|
<span class="bottom-nav-text">自評</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn v-if="store.isLoggedIn" to="/appointment" value="appointment">
|
<v-btn v-if="store.isLoggedIn" to="/appointment" value="appointment">
|
||||||
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon>
|
<v-icon :color="getBottomNavIconColor('appointment')">mdi-calendar-check</v-icon>
|
||||||
<span class="bottom-nav-text">活動預約</span>
|
<span class="bottom-nav-text">預約</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn v-if="store.isLoggedIn" to="/checkin" value="checkin">
|
<v-btn v-if="store.isLoggedIn" to="/checkin" value="checkin">
|
||||||
<v-icon :color="getBottomNavIconColor('checkin')">mdi-qrcode-scan</v-icon>
|
<v-icon :color="getBottomNavIconColor('checkin')">mdi-qrcode-scan</v-icon>
|
||||||
<span class="bottom-nav-text">現場量測</span>
|
<span class="bottom-nav-text">量測</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-bottom-navigation>
|
</v-bottom-navigation>
|
||||||
|
|
||||||
@@ -307,27 +295,27 @@ const showEmergency = ref(false)
|
|||||||
// 無障礙狀態 (可持久化儲存於 localStorage)
|
// 無障礙狀態 (可持久化儲存於 localStorage)
|
||||||
const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true')
|
const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true')
|
||||||
|
|
||||||
watch(isLargeText, newVal => {
|
watch(isLargeText, (newVal) => {
|
||||||
localStorage.setItem('mode-large-text', String(newVal))
|
localStorage.setItem('mode-large-text', String(newVal))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 同步底部導航狀態與當前路由
|
// 同步底部導航狀態與當前路由
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
path => {
|
(path) => {
|
||||||
if (path === '/') activeTab.value = 'home'
|
if (path === '/') activeTab.value = 'home'
|
||||||
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment'
|
else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment'
|
||||||
else if (path.startsWith('/appointment')) activeTab.value = 'appointment'
|
else if (path.startsWith('/appointment')) activeTab.value = 'appointment'
|
||||||
else if (path.startsWith('/checkin')) activeTab.value = 'checkin'
|
else if (path.startsWith('/checkin')) activeTab.value = 'checkin'
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
function getBottomNavIconColor (value: string) {
|
function getBottomNavIconColor(value: string) {
|
||||||
return activeTab.value === value ? 'secondary' : 'secondary-darken-1'
|
return activeTab.value === value ? 'secondary' : 'secondary-darken-1'
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout () {
|
function handleLogout() {
|
||||||
store.logout()
|
store.logout()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
drawer.value = false
|
drawer.value = false
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import App from './App.vue'
|
|||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
import '@/styles/fonts.css'
|
import '@/styles/fonts.css'
|
||||||
import 'unfonts.css'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<div class="text-subtitle-1 text-grey font-weight-bold">目前沒有相關活動</div>
|
<div class="text-subtitle-1 text-grey font-weight-bold">目前沒有相關活動</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-row v-else dense>
|
<v-row v-else>
|
||||||
<v-col v-for="apt in filteredAppointments" :key="apt.id" class="mb-4" cols="12">
|
<v-col v-for="apt in filteredAppointments" :key="apt.id" class="mb-4" cols="12">
|
||||||
<v-card class="rounded-xl elevation-3 border-accent-light" color="surface">
|
<v-card class="rounded-xl elevation-3 border-accent-light" color="surface">
|
||||||
<!-- 卡片頂部狀態條 -->
|
<!-- 卡片頂部狀態條 -->
|
||||||
@@ -63,7 +63,12 @@
|
|||||||
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-human-capacity" size="18" />
|
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-human-capacity" size="18" />
|
||||||
|
|
||||||
<div class="text-body-2 text-grey-darken-3">
|
<div class="text-body-2 text-grey-darken-3">
|
||||||
剩餘名額:<span class="font-weight-bold" :class="apt.slots < 10 ? 'text-error' : 'text-success'">{{ apt.slots }}</span> 位
|
剩餘名額:<span
|
||||||
|
class="font-weight-bold"
|
||||||
|
:class="apt.slots < 10 ? 'text-error' : 'text-success'"
|
||||||
|
>{{ apt.slots }}</span
|
||||||
|
>
|
||||||
|
位
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@
|
|||||||
|
|
||||||
<!-- 已報名狀態 -->
|
<!-- 已報名狀態 -->
|
||||||
<template v-else-if="apt.status === '已報名'">
|
<template v-else-if="apt.status === '已報名'">
|
||||||
<v-row class="w-100" dense>
|
<v-row class="w-100">
|
||||||
<v-col cols="6">
|
<v-col cols="6">
|
||||||
<v-btn
|
<v-btn
|
||||||
block
|
block
|
||||||
@@ -166,7 +171,9 @@
|
|||||||
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
|
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
|
||||||
|
|
||||||
<!-- 模擬 QR Code SVG,更高級精緻 -->
|
<!-- 模擬 QR Code SVG,更高級精緻 -->
|
||||||
<div class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center">
|
<div
|
||||||
|
class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center"
|
||||||
|
>
|
||||||
<svg class="qrcode-svg" height="180" viewBox="0 0 100 100" width="180">
|
<svg class="qrcode-svg" height="180" viewBox="0 0 100 100" width="180">
|
||||||
<!-- 四個角定位框 -->
|
<!-- 四個角定位框 -->
|
||||||
<path d="M 0,0 H 25 V 8 H 8 V 25 H 0 Z" fill="#1D7063" />
|
<path d="M 0,0 H 25 V 8 H 8 V 25 H 0 Z" fill="#1D7063" />
|
||||||
@@ -174,142 +181,40 @@
|
|||||||
<path d="M 0,75 V 100 H 25 V 92 H 8 V 75 Z" fill="#1D7063" />
|
<path d="M 0,75 V 100 H 25 V 92 H 8 V 75 Z" fill="#1D7063" />
|
||||||
<path d="M 92,75 V 92 H 75 V 100 H 100 V 75 Z" fill="#1D7063" />
|
<path d="M 92,75 V 92 H 75 V 100 H 100 V 75 Z" fill="#1D7063" />
|
||||||
<!-- 內嵌二維碼方塊 -->
|
<!-- 內嵌二維碼方塊 -->
|
||||||
<rect
|
<rect fill="#1D7063" height="20" width="20" x="12" y="12" />
|
||||||
fill="#1D7063"
|
|
||||||
height="20"
|
|
||||||
width="20"
|
|
||||||
x="12"
|
|
||||||
y="12"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="white" height="12" width="12" x="16" y="16" />
|
||||||
fill="white"
|
|
||||||
height="12"
|
|
||||||
width="12"
|
|
||||||
x="16"
|
|
||||||
y="16"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#E66926" height="6" width="6" x="19" y="19" />
|
||||||
fill="#E66926"
|
|
||||||
height="6"
|
|
||||||
width="6"
|
|
||||||
x="19"
|
|
||||||
y="19"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#1D7063" height="20" width="20" x="68" y="12" />
|
||||||
fill="#1D7063"
|
|
||||||
height="20"
|
|
||||||
width="20"
|
|
||||||
x="68"
|
|
||||||
y="12"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="white" height="12" width="12" x="72" y="16" />
|
||||||
fill="white"
|
|
||||||
height="12"
|
|
||||||
width="12"
|
|
||||||
x="72"
|
|
||||||
y="16"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#E66926" height="6" width="6" x="75" y="75" />
|
||||||
fill="#E66926"
|
|
||||||
height="6"
|
|
||||||
width="6"
|
|
||||||
x="75"
|
|
||||||
y="75"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#1D7063" height="20" width="20" x="12" y="68" />
|
||||||
fill="#1D7063"
|
|
||||||
height="20"
|
|
||||||
width="20"
|
|
||||||
x="12"
|
|
||||||
y="68"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="white" height="12" width="12" x="16" y="72" />
|
||||||
fill="white"
|
|
||||||
height="12"
|
|
||||||
width="12"
|
|
||||||
x="16"
|
|
||||||
y="72"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#1D7063" height="6" width="6" x="19" y="75" />
|
||||||
fill="#1D7063"
|
|
||||||
height="6"
|
|
||||||
width="6"
|
|
||||||
x="19"
|
|
||||||
y="75"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 中間隨機小方塊 -->
|
<!-- 中間隨機小方塊 -->
|
||||||
<rect
|
<rect fill="#1D7063" height="8" width="8" x="40" y="20" />
|
||||||
fill="#1D7063"
|
|
||||||
height="8"
|
|
||||||
width="8"
|
|
||||||
x="40"
|
|
||||||
y="20"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#E66926" height="14" width="6" x="52" y="12" />
|
||||||
fill="#E66926"
|
|
||||||
height="14"
|
|
||||||
width="6"
|
|
||||||
x="52"
|
|
||||||
y="12"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#1D7063" height="6" width="16" x="42" y="36" />
|
||||||
fill="#1D7063"
|
|
||||||
height="6"
|
|
||||||
width="16"
|
|
||||||
x="42"
|
|
||||||
y="36"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#47C2A6" height="10" width="28" x="36" y="48" />
|
||||||
fill="#47C2A6"
|
|
||||||
height="10"
|
|
||||||
width="28"
|
|
||||||
x="36"
|
|
||||||
y="48"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#1D7063" height="12" width="14" x="68" y="42" />
|
||||||
fill="#1D7063"
|
|
||||||
height="12"
|
|
||||||
width="14"
|
|
||||||
x="68"
|
|
||||||
y="42"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#E66926" height="16" width="16" x="72" y="60" />
|
||||||
fill="#E66926"
|
|
||||||
height="16"
|
|
||||||
width="16"
|
|
||||||
x="72"
|
|
||||||
y="60"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#1D7063" height="12" width="18" x="44" y="68" />
|
||||||
fill="#1D7063"
|
|
||||||
height="12"
|
|
||||||
width="18"
|
|
||||||
x="44"
|
|
||||||
y="68"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
<rect fill="#47C2A6" height="6" width="24" x="40" y="82" />
|
||||||
fill="#47C2A6"
|
|
||||||
height="6"
|
|
||||||
width="24"
|
|
||||||
x="40"
|
|
||||||
y="82"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<!-- 紅色掃描雷射線 -->
|
<!-- 紅色掃描雷射線 -->
|
||||||
<div class="laser-line" />
|
<div class="laser-line" />
|
||||||
@@ -335,7 +240,12 @@
|
|||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-card-actions class="pa-4 justify-center">
|
<v-card-actions class="pa-4 justify-center">
|
||||||
<v-btn class="rounded-lg px-6" color="primary" variant="flat" @click="qrcodeDialog = false">
|
<v-btn
|
||||||
|
class="rounded-lg px-6"
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
@click="qrcodeDialog = false"
|
||||||
|
>
|
||||||
關閉條碼
|
關閉條碼
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
@@ -346,55 +256,59 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||||
import { type Appointment, useAppStore } from '@/stores/app'
|
import { type Appointment, useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const filterTab = ref('all')
|
const filterTab = ref('all')
|
||||||
|
|
||||||
const qrcodeDialog = ref(false)
|
const qrcodeDialog = ref(false)
|
||||||
const selectedApt = ref<Appointment | null>(null)
|
const selectedApt = ref<Appointment | null>(null)
|
||||||
|
|
||||||
const filteredAppointments = computed(() => {
|
const filteredAppointments = computed(() => {
|
||||||
if (filterTab.value === 'registered') {
|
if (filterTab.value === 'registered') {
|
||||||
return store.appointments.filter(a => a.status === '已報名' || a.status === '已報到')
|
return store.appointments.filter((a) => a.status === '已報名' || a.status === '已報到')
|
||||||
}
|
}
|
||||||
return store.appointments
|
return store.appointments
|
||||||
})
|
})
|
||||||
|
|
||||||
function getStatusColor (status: string) {
|
function getStatusColor(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case '未報名': { return 'grey-darken-1'
|
case '未報名': {
|
||||||
|
return 'grey-darken-1'
|
||||||
}
|
}
|
||||||
case '已報名': { return 'secondary'
|
case '已報名': {
|
||||||
|
return 'secondary'
|
||||||
}
|
}
|
||||||
case '已報到': { return 'success'
|
case '已報到': {
|
||||||
}
|
return 'success'
|
||||||
default: { return 'grey'
|
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
return 'grey'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleRegister (id: string) {
|
function handleRegister(id: string) {
|
||||||
store.registerAppointment(id)
|
store.registerAppointment(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel (id: string) {
|
function handleCancel(id: string) {
|
||||||
store.cancelAppointment(id)
|
store.cancelAppointment(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCheckinQRCode (apt: Appointment) {
|
function showCheckinQRCode(apt: Appointment) {
|
||||||
selectedApt.value = apt
|
selectedApt.value = apt
|
||||||
qrcodeDialog.value = true
|
qrcodeDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function simulateOnSiteCheckin () {
|
function simulateOnSiteCheckin() {
|
||||||
if (selectedApt.value) {
|
if (selectedApt.value) {
|
||||||
store.checkinAppointment(selectedApt.value.id)
|
store.checkinAppointment(selectedApt.value.id)
|
||||||
qrcodeDialog.value = false
|
qrcodeDialog.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -429,8 +343,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scan {
|
@keyframes scan {
|
||||||
0% { top: 0%; }
|
0% {
|
||||||
50% { top: 100%; }
|
top: 0%;
|
||||||
100% { top: 0%; }
|
}
|
||||||
|
50% {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 0%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 報到活動資訊 -->
|
<!-- 報到活動資訊 -->
|
||||||
<v-card v-if="activeAppointment" class="rounded-xl elevation-3 border-accent-light mb-6" color="surface">
|
<v-card
|
||||||
|
v-if="activeAppointment"
|
||||||
|
class="rounded-xl elevation-3 border-accent-light mb-6"
|
||||||
|
color="surface"
|
||||||
|
>
|
||||||
<v-card-text class="pa-4">
|
<v-card-text class="pa-4">
|
||||||
<div class="d-flex align-center justify-between mb-3">
|
<div class="d-flex align-center justify-between mb-3">
|
||||||
<span class="text-subtitle-2 font-weight-bold text-primary">
|
<span class="text-subtitle-2 font-weight-bold text-primary">
|
||||||
@@ -32,7 +36,9 @@
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="d-flex justify-space-between align-center mb-1">
|
<div class="d-flex justify-space-between align-center mb-1">
|
||||||
<span class="text-caption font-weight-bold text-grey-darken-2">測量站點完成度</span>
|
<span class="text-caption font-weight-bold text-grey-darken-2">測量站點完成度</span>
|
||||||
<span class="text-caption font-weight-bold text-primary">{{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%)</span>
|
<span class="text-caption font-weight-bold text-primary"
|
||||||
|
>{{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
@@ -51,12 +57,19 @@
|
|||||||
<v-card v-else class="rounded-xl elevation-3 pa-6 text-center mb-6" color="surface">
|
<v-card v-else class="rounded-xl elevation-3 pa-6 text-center mb-6" color="surface">
|
||||||
<v-icon class="mb-3" color="warning" icon="mdi-qrcode-scan" size="64" />
|
<v-icon class="mb-3" color="warning" icon="mdi-qrcode-scan" size="64" />
|
||||||
<h3 class="text-subtitle-1 font-weight-bold mb-2">尚未在活動現場合對報到</h3>
|
<h3 class="text-subtitle-1 font-weight-bold mb-2">尚未在活動現場合對報到</h3>
|
||||||
<p class="text-body-2 text-grey mb-4">請先前往「活動預約」頁面,出示您的報到條碼給現場志工進行核對。</p>
|
<p class="text-body-2 text-grey mb-4">
|
||||||
|
請先前往「活動預約」頁面,出示您的報到條碼給現場志工進行核對。
|
||||||
|
</p>
|
||||||
<v-btn class="rounded-lg" color="primary" to="/appointment">前往查看預約活動</v-btn>
|
<v-btn class="rounded-lg" color="primary" to="/appointment">前往查看預約活動</v-btn>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- 快捷測試功能 (對 Demo 非常友善) -->
|
<!-- 快捷測試功能 (對 Demo 非常友善) -->
|
||||||
<v-card v-if="activeAppointment && completedCount < totalCount" class="rounded-xl mb-6 border-accent-light bg-teal-lighten-5" color="primary" variant="outlined">
|
<v-card
|
||||||
|
v-if="activeAppointment && completedCount < totalCount"
|
||||||
|
class="rounded-xl mb-6 border-accent-light bg-teal-lighten-5"
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
<v-card-text class="pa-4 d-flex align-center justify-space-between">
|
<v-card-text class="pa-4 d-flex align-center justify-space-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-subtitle-2 font-weight-bold text-primary">
|
<div class="text-subtitle-2 font-weight-bold text-primary">
|
||||||
@@ -64,10 +77,17 @@
|
|||||||
高齡友善測試輔助
|
高齡友善測試輔助
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-caption text-grey-darken-1">一鍵填入所有剩餘關卡數值,便於檢視報告</div>
|
<div class="text-caption text-grey-darken-1">
|
||||||
|
一鍵填入所有剩餘關卡數值,便於檢視報告
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-btn class="rounded-lg font-weight-bold text-white" color="secondary" size="small" @click="simulateAllMeasurements">
|
<v-btn
|
||||||
|
class="rounded-lg font-weight-bold text-white"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
@click="simulateAllMeasurements"
|
||||||
|
>
|
||||||
一鍵模擬完成
|
一鍵模擬完成
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -77,7 +97,7 @@
|
|||||||
<div v-if="activeAppointment" class="mb-6">
|
<div v-if="activeAppointment" class="mb-6">
|
||||||
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">關卡列表</h3>
|
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">關卡列表</h3>
|
||||||
|
|
||||||
<v-row dense>
|
<v-row>
|
||||||
<v-col v-for="(station, key) in store.measurements" :key="key" class="mb-2" cols="12">
|
<v-col v-for="(station, key) in store.measurements" :key="key" class="mb-2" cols="12">
|
||||||
<v-card
|
<v-card
|
||||||
class="rounded-xl elevation-2 cursor-pointer transition-card"
|
class="rounded-xl elevation-2 cursor-pointer transition-card"
|
||||||
@@ -91,22 +111,28 @@
|
|||||||
:color="station.completed ? 'success' : 'grey-lighten-2'"
|
:color="station.completed ? 'success' : 'grey-lighten-2'"
|
||||||
size="40"
|
size="40"
|
||||||
>
|
>
|
||||||
<v-icon :icon="station.completed ? 'mdi-check' : getStationIcon(key)" size="20" />
|
<v-icon
|
||||||
|
:icon="station.completed ? 'mdi-check' : getStationIcon(key)"
|
||||||
|
size="20"
|
||||||
|
/>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="font-weight-bold text-body-1" :class="station.completed ? 'text-success' : 'text-grey-darken-4'">
|
<div
|
||||||
|
class="font-weight-bold text-body-1"
|
||||||
|
:class="station.completed ? 'text-success' : 'text-grey-darken-4'"
|
||||||
|
>
|
||||||
{{ station.name }}
|
{{ station.name }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 已完成,顯示數值 -->
|
<!-- 已完成,顯示數值 -->
|
||||||
<div v-if="station.completed" class="text-caption text-grey-darken-2">
|
<div v-if="station.completed" class="text-caption text-grey-darken-2">
|
||||||
{{ getMeasurementSummary(key, station.values) }}
|
{{ getMeasurementSummary(key, station.values) }}
|
||||||
<span class="ml-2 text-grey-lighten-1">| {{ station.time?.split(' ')[1] }}</span>
|
<span class="ml-2 text-grey-lighten-1"
|
||||||
|
>| {{ station.time?.split(' ')[1] }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<!-- 未完成,顯示待測 -->
|
<!-- 未完成,顯示待測 -->
|
||||||
<div v-else class="text-caption text-grey-darken-1">
|
<div v-else class="text-caption text-grey-darken-1">等待現場檢測員登錄</div>
|
||||||
等待現場檢測員登錄
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,54 +188,66 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const reportSuccessDialog = ref(false)
|
const reportSuccessDialog = ref(false)
|
||||||
|
|
||||||
// 尋找已報到的活動
|
// 尋找已報到的活動
|
||||||
const activeAppointment = computed(() => {
|
const activeAppointment = computed(() => {
|
||||||
return store.appointments.find(a => a.status === '已報到')
|
return store.appointments.find((a) => a.status === '已報到')
|
||||||
})
|
})
|
||||||
|
|
||||||
const completedCount = computed(() => store.completedStationsCount)
|
const completedCount = computed(() => store.completedStationsCount)
|
||||||
const totalCount = computed(() => store.totalStationsCount)
|
const totalCount = computed(() => store.totalStationsCount)
|
||||||
const progressPercent = computed(() => store.progressPercent)
|
const progressPercent = computed(() => store.progressPercent)
|
||||||
|
|
||||||
function getStationIcon (code: string) {
|
function getStationIcon(code: string) {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'BloodPressure': { return 'mdi-heart-flash'
|
case 'BloodPressure': {
|
||||||
|
return 'mdi-heart-flash'
|
||||||
}
|
}
|
||||||
case 'BodyFat': { return 'mdi-scale-bathroom'
|
case 'BodyFat': {
|
||||||
|
return 'mdi-scale-bathroom'
|
||||||
}
|
}
|
||||||
case 'GripStrength': { return 'mdi-hand-back-left'
|
case 'GripStrength': {
|
||||||
|
return 'mdi-hand-back-left'
|
||||||
}
|
}
|
||||||
case 'Sit5x': { return 'mdi-human-male-height'
|
case 'Sit5x': {
|
||||||
|
return 'mdi-human-male-height'
|
||||||
}
|
}
|
||||||
case 'Sit30s': { return 'mdi-chair-school'
|
case 'Sit30s': {
|
||||||
|
return 'mdi-chair-school'
|
||||||
}
|
}
|
||||||
case 'StepTest': { return 'mdi-run-fast'
|
case 'StepTest': {
|
||||||
|
return 'mdi-run-fast'
|
||||||
}
|
}
|
||||||
case 'ArmTest': { return 'mdi-arm-flex'
|
case 'ArmTest': {
|
||||||
|
return 'mdi-arm-flex'
|
||||||
}
|
}
|
||||||
case 'BackTest': { return 'mdi-human-stretching'
|
case 'BackTest': {
|
||||||
|
return 'mdi-human-stretching'
|
||||||
}
|
}
|
||||||
case 'SitReachTest': { return 'mdi-human-stretching'
|
case 'SitReachTest': {
|
||||||
|
return 'mdi-human-stretching'
|
||||||
}
|
}
|
||||||
case 'UpGo8ft': { return 'mdi-navigation'
|
case 'UpGo8ft': {
|
||||||
|
return 'mdi-navigation'
|
||||||
}
|
}
|
||||||
case 'OneLegTest': { return 'mdi-scale-balance'
|
case 'OneLegTest': {
|
||||||
}
|
return 'mdi-scale-balance'
|
||||||
default: { return 'mdi-checkbox-blank-circle-outline'
|
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
return 'mdi-checkbox-blank-circle-outline'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getMeasurementSummary (code: string, vals: Record<string, any>) {
|
function getMeasurementSummary(code: string, vals: Record<string, any>) {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'BloodPressure': {
|
case 'BloodPressure': {
|
||||||
return `收縮壓: ${vals.systolic} / 舒張壓: ${vals.diastolic} mmHg | 心率: ${vals.heartRate} bpm`
|
return `收縮壓: ${vals.systolic} / 舒張壓: ${vals.diastolic} mmHg | 心率: ${vals.heartRate} bpm`
|
||||||
@@ -248,10 +286,10 @@
|
|||||||
return '數據登錄成功'
|
return '數據登錄成功'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模擬一鍵完成所有測量 (對 Demo 极為有用)
|
// 模擬一鍵完成所有測量 (對 Demo 极為有用)
|
||||||
function simulateAllMeasurements () {
|
function simulateAllMeasurements() {
|
||||||
// 自動將剩餘的 9 站填入合理健康的隨機值
|
// 自動將剩餘的 9 站填入合理健康的隨機值
|
||||||
const m = store.measurements
|
const m = store.measurements
|
||||||
|
|
||||||
@@ -282,17 +320,17 @@
|
|||||||
if (!m.OneLegTest.completed) {
|
if (!m.OneLegTest.completed) {
|
||||||
store.saveMeasurement('OneLegTest', { leftSeconds: 24, rightSeconds: 26 })
|
store.saveMeasurement('OneLegTest', { leftSeconds: 24, rightSeconds: 26 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateReport () {
|
function generateReport() {
|
||||||
store.generateFinalReport()
|
store.generateFinalReport()
|
||||||
reportSuccessDialog.value = true
|
reportSuccessDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToDashboard () {
|
function goToDashboard() {
|
||||||
reportSuccessDialog.value = false
|
reportSuccessDialog.value = false
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<v-form @submit.prevent="handleSave">
|
<v-form @submit.prevent="handleSave">
|
||||||
<h3 class="text-subtitle-2 font-weight-bold text-grey-darken-3 mb-3">數據錄入:</h3>
|
<h3 class="text-subtitle-2 font-weight-bold text-grey-darken-3 mb-3">數據錄入:</h3>
|
||||||
|
|
||||||
<v-row dense>
|
<v-row>
|
||||||
<v-col v-for="field in station.fields" :key="field.key" class="mb-4" cols="12">
|
<v-col v-for="field in station.fields" :key="field.key" class="mb-4" cols="12">
|
||||||
<div class="text-body-2 font-weight-bold text-grey-darken-3 mb-1">
|
<div class="text-body-2 font-weight-bold text-grey-darken-3 mb-1">
|
||||||
{{ field.label }} <span class="text-caption text-grey">({{ field.unit }})</span>
|
{{ field.label }} <span class="text-caption text-grey">({{ field.unit }})</span>
|
||||||
|
|||||||
@@ -3,341 +3,105 @@
|
|||||||
<template #main>
|
<template #main>
|
||||||
<template v-if="store.isLoggedIn">
|
<template v-if="store.isLoggedIn">
|
||||||
<!-- 頂部:我的健康力護照卡片 (Taipei Masters Hub) -->
|
<!-- 頂部:我的健康力護照卡片 (Taipei Masters Hub) -->
|
||||||
<v-card
|
<v-card class="border-primary border-xl rounded-xl" variant="flat">
|
||||||
class="passport-card rounded-xl text-white pa-5 elevation-8 mb-6 position-relative overflow-hidden"
|
<v-row class="mt-4" no-gutters>
|
||||||
>
|
<v-col cols="8">
|
||||||
<!-- 背景裝飾圓圈 -->
|
<v-card-title> {{ store.userProfile.name }} 先生 </v-card-title>
|
||||||
<div class="bg-circle-1" />
|
<v-card-subtitle> 資料更新於 {{ todayTimeString }} </v-card-subtitle>
|
||||||
<div class="bg-circle-2" />
|
</v-col>
|
||||||
|
|
||||||
<div class="d-flex justify-space-between align-center mb-6 z-index-1 position-relative">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar class="mr-3 border-white" color="accent" size="44">
|
|
||||||
<span class="text-white font-weight-black text-subtitle-1">{{
|
|
||||||
store.userProfile.name.substring(1)
|
|
||||||
}}</span>
|
|
||||||
</v-avatar>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 class="text-subtitle-1 font-weight-black mb-0">
|
|
||||||
{{ store.userProfile.name }} 先生
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<span class="text-caption text-white-70">更新於今日 {{ todayTimeString }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<v-col class="d-flex justify-center" cols="4">
|
||||||
<!-- 健康力分數大圓盤 -->
|
<!-- 健康力分數大圓盤 -->
|
||||||
<div class="score-disk d-flex flex-column justify-center align-center">
|
<div class="score-disk d-flex flex-column justify-center align-center">
|
||||||
<span class="score-val">{{ store.healthScore }}</span>
|
<span class="score-val">{{ displayedHealthScore }}</span>
|
||||||
<span class="score-unit">分</span>
|
<span class="score-unit">分</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</v-col>
|
||||||
|
</v-row>
|
||||||
<!-- 功能儲備與運動處方簡介 -->
|
|
||||||
<div class="z-index-1 position-relative mb-4">
|
|
||||||
<div class="d-inline-flex align-center bg-white-20 px-3 py-1 rounded-pill mb-3">
|
|
||||||
<v-icon class="mr-1 text-accent-light" icon="mdi-heart-pulse" size="16" />
|
|
||||||
<span class="text-caption font-weight-bold">功能儲備:{{ functionalReserve }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-body-2 font-weight-medium bg-white-10 rounded-lg pa-3">
|
|
||||||
<div class="font-weight-black mb-1 text-accent-light d-flex align-center">
|
|
||||||
<v-icon class="mr-1" icon="mdi-notebook-check-outline" size="16" />
|
|
||||||
本週處方建議:
|
|
||||||
</div>
|
|
||||||
{{ currentWeeklyPrescription }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-divider class="bg-white-20 my-3 z-index-1 position-relative" />
|
|
||||||
|
|
||||||
<!-- 四項核心能力數值簡覽 -->
|
<!-- 四項核心能力數值簡覽 -->
|
||||||
<v-row class="z-index-1 position-relative text-center" dense>
|
<v-row class="mt-4" no-gutters>
|
||||||
<v-col cols="3">
|
<v-col class="text-center border pa-2" cols="3">
|
||||||
<div class="text-caption text-white-70">肌力</div>
|
<div class="">肌力</div>
|
||||||
|
|
||||||
<div class="text-subtitle-2 font-weight-black">
|
<div class="font-weight-black">
|
||||||
{{ store.radarData.strength }}
|
{{ store.radarData.strength }}
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="3">
|
<v-col class="text-center border pa-2" cols="3">
|
||||||
<div class="text-caption text-white-70">平衡</div>
|
<div class="">平衡</div>
|
||||||
|
|
||||||
<div class="text-subtitle-2 font-weight-black">
|
<div class="font-weight-black">
|
||||||
{{ store.radarData.balance }}
|
{{ store.radarData.balance }}
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="3">
|
<v-col class="text-center border pa-2" cols="3">
|
||||||
<div class="text-caption text-white-70">敏捷</div>
|
<div class="">敏捷</div>
|
||||||
|
|
||||||
<div class="text-subtitle-2 font-weight-black">
|
<div class="font-weight-black">
|
||||||
{{ store.radarData.agility }}
|
{{ store.radarData.agility }}
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="3">
|
<v-col class="text-center border pa-2" cols="3">
|
||||||
<div class="text-caption text-white-70">心肺</div>
|
<div class="">心肺</div>
|
||||||
|
|
||||||
<div class="text-subtitle-2 font-weight-black">
|
<div class="font-weight-black">
|
||||||
{{ store.radarData.cardio }}
|
{{ store.radarData.cardio }}
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 功能儲備與運動處方簡介 -->
|
||||||
|
|
||||||
|
<v-card-text class="bg-primary">
|
||||||
|
<div class="font-weight-bold text-accent-light">
|
||||||
|
<v-icon icon="mdi-notebook-check-outline" size="16" />
|
||||||
|
運動處方建議:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-white mt-2">
|
||||||
|
{{ currentWeeklyPrescription }}
|
||||||
|
</p>
|
||||||
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- 中間:功能指標與五維雷達圖 -->
|
<!-- 中間:功能指標與五維雷達圖 -->
|
||||||
<v-card class="rounded-xl elevation-3 border-accent-light mb-6 pa-4" color="surface">
|
<v-card class="border-xl border-primary rounded-xl mt-4" variant="flat">
|
||||||
<h3 class="text-subtitle-1 font-weight-black text-primary mb-2 d-flex align-center">
|
<v-card-title> 個人五維健康力指標 </v-card-title>
|
||||||
<v-icon class="mr-2" color="secondary" icon="mdi-radar" />
|
|
||||||
個人五維健康力指標
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p class="text-caption text-grey mb-4">
|
<v-card-subtitle>
|
||||||
綜合 11 項現場普測數值計算得出,藍色為同齡正常參考值區間。
|
綜合 11 項現場普測數值計算得出,<br />藍色為同齡正常參考值區間。
|
||||||
</p>
|
</v-card-subtitle>
|
||||||
|
|
||||||
<!-- 純 SVG 雷達圖,不依賴任何外部圖表庫,保證完美渲染 -->
|
<HealthRadarChart :radar-data="store.radarData" />
|
||||||
<div class="d-flex justify-center my-2 position-relative">
|
|
||||||
<svg class="radar-svg" height="260" viewBox="0 0 120 120" width="260">
|
|
||||||
<!-- 5 個同心五邊形網格 (背景線) -->
|
|
||||||
<polygon
|
|
||||||
v-for="grid in [20, 40, 60, 80, 100]"
|
|
||||||
:key="grid"
|
|
||||||
fill="none"
|
|
||||||
:points="getPolygonPoints(grid)"
|
|
||||||
stroke="#E0E0E0"
|
|
||||||
stroke-width="0.3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 五個維度軸線 -->
|
|
||||||
<line
|
|
||||||
v-for="i in 5"
|
|
||||||
:key="`axis-${i}`"
|
|
||||||
stroke="#E0E0E0"
|
|
||||||
stroke-width="0.3"
|
|
||||||
x1="60"
|
|
||||||
:x2="getAxisEndPoint(i - 1).x"
|
|
||||||
y1="60"
|
|
||||||
:y2="getAxisEndPoint(i - 1).y"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 同齡參考區間 (同齡常模基準:設為 65 分) -->
|
|
||||||
<polygon
|
|
||||||
fill="rgba(33, 150, 243, 0.12)"
|
|
||||||
:points="getPolygonPoints(65)"
|
|
||||||
stroke="#2196F3"
|
|
||||||
stroke-dasharray="1 1"
|
|
||||||
stroke-width="0.5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 使用者真實數據填充面 -->
|
|
||||||
<polygon
|
|
||||||
fill="rgba(71, 194, 166, 0.35)"
|
|
||||||
:points="userDataPoints"
|
|
||||||
stroke="#1D7063"
|
|
||||||
stroke-width="1.2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 使用者數據頂點標記圓圈 -->
|
|
||||||
<circle
|
|
||||||
v-for="(pt, idx) in userDataCoordArray"
|
|
||||||
:key="`dot-${idx}`"
|
|
||||||
:cx="pt.x"
|
|
||||||
:cy="pt.y"
|
|
||||||
fill="#E66926"
|
|
||||||
r="1.5"
|
|
||||||
stroke="#FFFFFF"
|
|
||||||
stroke-width="0.5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 頂點文字標記 (Cardio, Strength, Balance, Flexibility, Agility) -->
|
|
||||||
<text
|
|
||||||
fill="#1D7063"
|
|
||||||
font-size="4"
|
|
||||||
font-weight="bold"
|
|
||||||
text-anchor="middle"
|
|
||||||
x="60"
|
|
||||||
y="8"
|
|
||||||
>
|
|
||||||
心肺 ({{ store.radarData.cardio }})
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<text
|
|
||||||
fill="#1D7063"
|
|
||||||
font-size="4"
|
|
||||||
font-weight="bold"
|
|
||||||
text-anchor="start"
|
|
||||||
x="110"
|
|
||||||
y="44"
|
|
||||||
>
|
|
||||||
肌力 ({{ store.radarData.strength }})
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<text
|
|
||||||
fill="#1D7063"
|
|
||||||
font-size="4"
|
|
||||||
font-weight="bold"
|
|
||||||
text-anchor="start"
|
|
||||||
x="94"
|
|
||||||
y="104"
|
|
||||||
>
|
|
||||||
平衡 ({{ store.radarData.balance }})
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<text
|
|
||||||
fill="#1D7063"
|
|
||||||
font-size="4"
|
|
||||||
font-weight="bold"
|
|
||||||
text-anchor="end"
|
|
||||||
x="26"
|
|
||||||
y="104"
|
|
||||||
>
|
|
||||||
柔軟 ({{ store.radarData.flexibility }})
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<text
|
|
||||||
fill="#1D7063"
|
|
||||||
font-size="4"
|
|
||||||
font-weight="bold"
|
|
||||||
text-anchor="end"
|
|
||||||
x="10"
|
|
||||||
y="44"
|
|
||||||
>
|
|
||||||
敏捷 ({{ store.radarData.agility }})
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- 圖例標示 -->
|
|
||||||
<div class="radar-legend d-flex flex-column">
|
|
||||||
<div class="d-flex align-center text-caption mb-1">
|
|
||||||
<span class="legend-color bg-primary mr-1" />
|
|
||||||
<span>我的指標</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-center text-caption">
|
|
||||||
<span class="legend-color bg-blue mr-1 border-dashed" />
|
|
||||||
<span>同齡參考</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- 下一步行動引導 (根據量測進度動態顯示) -->
|
<ModuleRouterCard
|
||||||
<h3 class="text-subtitle-2 font-weight-black text-primary mb-3">下一步行動推薦</h3>
|
description="完成線上自評,取得初步風險分級。"
|
||||||
|
icon="mdi-clipboard-text-search"
|
||||||
|
icon-color="accent"
|
||||||
|
title="自評"
|
||||||
|
to="/self-assessment"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 情況 1:普測量測正在進行中 -->
|
<ModuleRouterCard
|
||||||
<v-card
|
description="完成數值紀錄,建立個人功能基線。"
|
||||||
v-if="isIncompleteMeasurement"
|
icon="mdi-run-fast"
|
||||||
class="rounded-xl elevation-3 mb-4 transition-card text-left"
|
icon-color="accent"
|
||||||
color="surface"
|
title="量測"
|
||||||
to="/checkin"
|
to="/checkin"
|
||||||
>
|
/>
|
||||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar class="mr-3 text-white elevation-1" color="secondary" size="48">
|
|
||||||
<v-icon icon="mdi-run-fast" />
|
|
||||||
</v-avatar>
|
|
||||||
|
|
||||||
<div>
|
<ModuleRouterCard
|
||||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
description="預訂首次量測或回測,追蹤健康狀態。"
|
||||||
繼續現場體能普測
|
icon="mdi-calendar-clock"
|
||||||
</div>
|
icon-color="accent"
|
||||||
|
title="預約"
|
||||||
<div class="text-caption text-grey">
|
|
||||||
目前已完成 {{ completedCount }} / 11 站,繼續完成登錄。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- 情況 2:量測全部完成,提醒可以看運動處方任務 -->
|
|
||||||
<v-card
|
|
||||||
v-else
|
|
||||||
class="rounded-xl elevation-3 mb-4 transition-card text-left"
|
|
||||||
color="surface"
|
|
||||||
@click="prescriptionDialog = true"
|
|
||||||
>
|
|
||||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar class="mr-3 text-white elevation-1" color="secondary" size="48">
|
|
||||||
<v-icon icon="mdi-calendar-multiselect" />
|
|
||||||
</v-avatar>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
|
||||||
執行本週運動處方課程
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-caption text-grey">
|
|
||||||
依普測結果產生,肌力 2 次、心肺 2 次、平衡 3 次。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- 普測預約快捷卡片 -->
|
|
||||||
<v-card
|
|
||||||
class="rounded-xl elevation-3 mb-6 transition-card text-left"
|
|
||||||
color="surface"
|
|
||||||
to="/appointment"
|
to="/appointment"
|
||||||
>
|
/>
|
||||||
<v-card-text class="pa-4 d-flex align-center justify-between">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar class="mr-3 text-white elevation-1" color="primary" size="48">
|
|
||||||
<v-icon icon="mdi-calendar-clock" />
|
|
||||||
</v-avatar>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="font-weight-bold text-subtitle-2 text-grey-darken-4">
|
|
||||||
預約 30 秒坐站與平衡回測
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-caption text-grey">4 週 / 12 週定期回測,動態追蹤體能進步狀態。</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-icon color="primary" icon="mdi-chevron-right" />
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- 最近一次量測結果摘要 -->
|
|
||||||
<v-card class="rounded-xl elevation-3 mb-8 pa-4" color="surface">
|
|
||||||
<div class="d-flex justify-space-between align-center mb-3">
|
|
||||||
<h3 class="text-subtitle-1 font-weight-black text-primary d-flex align-center">
|
|
||||||
<v-icon class="mr-2" color="secondary" icon="mdi-file-eye-outline" />
|
|
||||||
最近量測數值摘要
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
class="font-weight-bold px-0"
|
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
to="/checkin"
|
|
||||||
variant="text"
|
|
||||||
>查看全部</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-row dense>
|
|
||||||
<v-col v-for="item in recentItems" :key="item.name" class="mb-2" cols="6">
|
|
||||||
<div class="bg-grey-lighten-4 rounded-lg pa-3 h-100">
|
|
||||||
<div class="text-caption text-grey-darken-1 mb-1">{{ item.name }}</div>
|
|
||||||
|
|
||||||
<div class="text-body-1 font-weight-black text-grey-darken-4">
|
|
||||||
{{ item.value }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- 運動處方對話框 -->
|
<!-- 運動處方對話框 -->
|
||||||
<v-dialog v-model="prescriptionDialog" max-width="500">
|
<v-dialog v-model="prescriptionDialog" max-width="500">
|
||||||
@@ -417,17 +181,15 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<v-avatar class="elevation-3 mb-4 border-accent-light" color="primary" size="72">
|
<v-avatar class="elevation-3 mb-4 border-accent-light" color="primary" size="72">
|
||||||
<v-icon color="accent" icon="mdi-heart-pulse" size="42" />
|
<v-icon color="red-accent-1" icon="mdi-heart-pulse" size="42" />
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
|
||||||
<h1 class="text-h5 font-weight-black text-primary mb-2">運動玩轉健康力</h1>
|
<p class="text-body-medium text-grey-darken-1 mb-0">
|
||||||
|
不登入也可以先完成線上健康自評,<br />登入後可預約普測與查看量測紀錄。
|
||||||
<p class="text-body-2 text-grey-darken-1 mb-0">
|
|
||||||
不登入也可以先完成線上健康自評,登入後可預約普測與查看量測紀錄。
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-card class="rounded-xl elevation-3 border-accent-light mb-4" color="surface">
|
<v-card class="rounded-xl elevation-2 border-accent-light mb-4" color="surface">
|
||||||
<v-card-text class="pa-5">
|
<v-card-text class="pa-5">
|
||||||
<div class="d-flex align-center mb-4">
|
<div class="d-flex align-center mb-4">
|
||||||
<v-avatar class="mr-3 text-white" color="secondary" size="48">
|
<v-avatar class="mr-3 text-white" color="secondary" size="48">
|
||||||
@@ -442,13 +204,13 @@
|
|||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
block
|
block
|
||||||
class="rounded-lg font-weight-bold mb-3"
|
class="rounded-lg font-weight-bold elevation-1 mb-3"
|
||||||
color="primary"
|
color="secondary"
|
||||||
prepend-icon="mdi-arrow-right-circle"
|
prepend-icon="mdi-clipboard-text-search"
|
||||||
to="/self-assessment"
|
to="/self-assessment"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
>
|
>
|
||||||
開始健康自評
|
開始自評
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -459,7 +221,7 @@
|
|||||||
to="/login"
|
to="/login"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
>
|
>
|
||||||
登入健康平台
|
登入平台
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -483,116 +245,81 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from '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 { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const prescriptionDialog = ref(false)
|
const prescriptionDialog = ref(false)
|
||||||
|
const displayedHealthScore = ref(1)
|
||||||
|
let healthScoreTimer: number | undefined
|
||||||
|
|
||||||
|
function animateHealthScore (targetScore: number) {
|
||||||
|
if (healthScoreTimer !== undefined) {
|
||||||
|
window.clearInterval(healthScoreTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetScore <= 1) {
|
||||||
|
displayedHealthScore.value = targetScore
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
displayedHealthScore.value = 1
|
||||||
|
|
||||||
|
const duration = 900
|
||||||
|
const stepDuration = Math.max(12, Math.floor(duration / targetScore))
|
||||||
|
|
||||||
|
healthScoreTimer = window.setInterval(() => {
|
||||||
|
if (displayedHealthScore.value >= targetScore) {
|
||||||
|
window.clearInterval(healthScoreTimer)
|
||||||
|
healthScoreTimer = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
displayedHealthScore.value += 1
|
||||||
|
}, stepDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
animateHealthScore(store.healthScore)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (healthScoreTimer !== undefined) {
|
||||||
|
window.clearInterval(healthScoreTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.healthScore,
|
||||||
|
score => {
|
||||||
|
animateHealthScore(score)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const todayTimeString = computed(() => {
|
const todayTimeString = computed(() => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
const date = [
|
||||||
})
|
String(now.getMonth() + 1).padStart(2, '0'),
|
||||||
|
String(now.getDate()).padStart(2, '0'),
|
||||||
|
].join('/')
|
||||||
|
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||||
|
|
||||||
// 功能儲備評估
|
return `${date} ${time}`
|
||||||
const functionalReserve = computed(() => {
|
|
||||||
const score = store.healthScore
|
|
||||||
if (score >= 80) return '良好 (無衰弱風險)'
|
|
||||||
if (score >= 65) return '正常 (需持續運動維持)'
|
|
||||||
return '待加強 (有衰弱與跌倒風險)'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 每週處方文字
|
// 每週處方文字
|
||||||
const currentWeeklyPrescription = computed(() => {
|
const currentWeeklyPrescription = computed(() => {
|
||||||
if (store.healthScore >= 80) {
|
if (store.healthScore >= 80) {
|
||||||
return '本週處方:1次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
return '1次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||||
} else if (store.healthScore >= 65) {
|
} else if (store.healthScore >= 65) {
|
||||||
return '本週處方:2次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
return '2次肌力循環 + 1次平衡課 + 每日20分鐘快走'
|
||||||
} else {
|
} else {
|
||||||
return '本週處方:3次防跌平衡引導 + 每日15分鐘扶椅坐站與慢走'
|
return '3次防跌平衡引導 + 每日15分鐘扶椅坐站與慢走'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 檢測是否還有未完成的測量站
|
|
||||||
const isIncompleteMeasurement = computed(() => {
|
|
||||||
return store.completedStationsCount < store.totalStationsCount
|
|
||||||
})
|
|
||||||
|
|
||||||
const completedCount = computed(() => store.completedStationsCount)
|
|
||||||
|
|
||||||
// 最近一次量測結果摘要列表
|
|
||||||
const recentItems = computed(() => {
|
|
||||||
const m = store.measurements
|
|
||||||
const items = []
|
|
||||||
|
|
||||||
if (m.BloodPressure.completed) {
|
|
||||||
items.push({
|
|
||||||
name: '血壓量測',
|
|
||||||
value: `${m.BloodPressure.values.systolic}/${m.BloodPressure.values.diastolic} mmHg`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (m.BodyFat.completed) {
|
|
||||||
items.push({ name: '體組成 BMI', value: `${m.BodyFat.values.bmi}` })
|
|
||||||
}
|
|
||||||
if (m.GripStrength.completed) {
|
|
||||||
items.push({ name: '右手最大握力', value: `${m.GripStrength.values.rightHand} kg` })
|
|
||||||
} else {
|
|
||||||
items.push({ name: '左右握力', value: '未測量' })
|
|
||||||
}
|
|
||||||
if (m.OneLegTest.completed) {
|
|
||||||
items.push({
|
|
||||||
name: '單腳站立平衡',
|
|
||||||
value: `${Math.max(m.OneLegTest.values.leftSeconds, m.OneLegTest.values.rightSeconds)} 秒`,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
items.push({ name: '開眼單足立', value: '未測量' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- SVG 雷達圖點計算邏輯 ---
|
|
||||||
const cx = 60
|
|
||||||
const cy = 60
|
|
||||||
const r = 45
|
|
||||||
|
|
||||||
// 計算 5 角多邊形的頂點
|
|
||||||
// 頂點順序: i=0(心肺), i=1(肌力), i=2(平衡), i=3(柔軟), i=4(敏捷)
|
|
||||||
function getAxisEndPoint (i: number, radius = r) {
|
|
||||||
const angle = -Math.PI / 2 + (i * 2 * Math.PI) / 5
|
|
||||||
return {
|
|
||||||
x: cx + radius * Math.cos(angle),
|
|
||||||
y: cy + radius * Math.sin(angle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根據百分比半徑繪製背景網格五邊形
|
|
||||||
function getPolygonPoints (percent: number) {
|
|
||||||
const radius = (percent / 100) * r
|
|
||||||
const pts = []
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const pt = getAxisEndPoint(i, radius)
|
|
||||||
pts.push(`${pt.x},${pt.y}`)
|
|
||||||
}
|
|
||||||
return pts.join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用者五維數據對應的雷達圖頂點座標
|
|
||||||
const userDataCoordArray = computed(() => {
|
|
||||||
const data = store.radarData
|
|
||||||
// 順序與頂點對應:
|
|
||||||
// 0: cardio (心肺), 1: strength (肌力), 2: balance (平衡), 3: flexibility (柔軟), 4: agility (敏捷)
|
|
||||||
const values = [data.cardio, data.strength, data.balance, data.flexibility, data.agility]
|
|
||||||
return values.map((val, i) => {
|
|
||||||
const radius = (Math.max(5, Math.min(100, val)) / 100) * r
|
|
||||||
return getAxisEndPoint(i, radius)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const userDataPoints = computed(() => {
|
|
||||||
return userDataCoordArray.value.map(pt => `${pt.x},${pt.y}`).join(' ')
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -681,31 +408,4 @@ const userDataPoints = computed(() => {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 雷達圖圖例 */
|
|
||||||
.radar-legend {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: rgba(255, 255, 255, 0.85);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-color {
|
|
||||||
display: inline-block;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-dashed {
|
|
||||||
border: 1px dashed #2196f3;
|
|
||||||
background-color: rgba(33, 150, 243, 0.12) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radar-svg text {
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -50,13 +50,22 @@
|
|||||||
class="glow-sweep-button rounded-lg font-weight-bold elevation-2 mb-4"
|
class="glow-sweep-button rounded-lg font-weight-bold elevation-2 mb-4"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
size="large"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
登入健康平台
|
登入健康平台
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
class="rounded-lg font-weight-bold"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-clipboard-text-search"
|
||||||
|
to="/self-assessment"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
不登入,先做線上自評
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
<!-- <div class="d-flex align-center justify-center my-4">
|
<!-- <div class="d-flex align-center justify-center my-4">
|
||||||
<v-divider class="mr-3" />
|
<v-divider class="mr-3" />
|
||||||
<span class="text-caption text-grey font-weight-bold">或使用以下方式快捷登入</span>
|
<span class="text-caption text-grey font-weight-bold">或使用以下方式快捷登入</span>
|
||||||
@@ -111,7 +120,7 @@ const password = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const lineLoading = ref(false)
|
const lineLoading = ref(false)
|
||||||
|
|
||||||
function handleLogin () {
|
function handleLogin() {
|
||||||
if (!account.value) return
|
if (!account.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -121,7 +130,7 @@ function handleLogin () {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleLineLogin () {
|
function _handleLineLogin() {
|
||||||
lineLoading.value = true
|
lineLoading.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
store.login('0912-345-678') // LINE 模擬登入預設手機
|
store.login('0912-345-678') // LINE 模擬登入預設手機
|
||||||
@@ -155,7 +164,7 @@ function _handleLineLogin () {
|
|||||||
rgba(255, 255, 255, 0.15) 65%,
|
rgba(255, 255, 255, 0.15) 65%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
content: "";
|
content: '';
|
||||||
transform: rotate(18deg);
|
transform: rotate(18deg);
|
||||||
animation: glow-sweep 2.4s ease-in-out infinite;
|
animation: glow-sweep 2.4s ease-in-out infinite;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 進度指示器 -->
|
<!-- 進度指示器 -->
|
||||||
<v-row class="mb-6 justify-center" dense>
|
<v-row class="mb-6 justify-center">
|
||||||
<v-col v-for="(stepName, idx) in stepNames" :key="idx" class="text-center" cols="3">
|
<v-col v-for="(stepName, idx) in stepNames" :key="idx" class="text-center" cols="3">
|
||||||
<v-avatar
|
<v-avatar
|
||||||
class="text-white font-weight-bold mb-1 elevation-1"
|
class="text-white font-weight-bold mb-1 elevation-1"
|
||||||
:color="currentStep > idx ? 'primary' : currentStep === idx ? 'secondary' : 'grey-lighten-2'"
|
:color="
|
||||||
|
currentStep > idx ? 'primary' : currentStep === idx ? 'secondary' : 'grey-lighten-2'
|
||||||
|
"
|
||||||
size="28"
|
size="28"
|
||||||
>
|
>
|
||||||
<v-icon v-if="currentStep > idx" icon="mdi-check" size="14" />
|
<v-icon v-if="currentStep > idx" icon="mdi-check" size="14" />
|
||||||
@@ -20,7 +22,13 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="text-caption font-weight-bold"
|
class="text-caption font-weight-bold"
|
||||||
:class="currentStep === idx ? 'text-secondary' : currentStep > idx ? 'text-primary' : 'text-grey'"
|
:class="
|
||||||
|
currentStep === idx
|
||||||
|
? 'text-secondary'
|
||||||
|
: currentStep > idx
|
||||||
|
? 'text-primary'
|
||||||
|
: 'text-grey'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ stepName }}
|
{{ stepName }}
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +45,7 @@
|
|||||||
基本資料確認
|
基本資料確認
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<v-row dense>
|
<v-row>
|
||||||
<v-col cols="12" sm="6">
|
<v-col cols="12" sm="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="profileForm.name"
|
v-model="profileForm.name"
|
||||||
@@ -120,7 +128,9 @@
|
|||||||
活動習慣與想要改善的能力
|
活動習慣與想要改善的能力
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">1. 最近 7 天,您每週進行大運動幾次?</p>
|
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||||
|
1. 最近 7 天,您每週進行大運動幾次?
|
||||||
|
</p>
|
||||||
|
|
||||||
<v-radio-group v-model="assessmentForm.exerciseFrequency" class="mb-4" color="primary">
|
<v-radio-group v-model="assessmentForm.exerciseFrequency" class="mb-4" color="primary">
|
||||||
<v-card
|
<v-card
|
||||||
@@ -151,18 +161,26 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-radio-group>
|
</v-radio-group>
|
||||||
|
|
||||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">2. 您目前最想改善的身體功能? (可複選)</p>
|
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||||
|
2. 您目前最想改善的身體功能? (可複選)
|
||||||
|
</p>
|
||||||
|
|
||||||
<v-row dense>
|
<v-row>
|
||||||
<v-col v-for="imp in improvementsOptions" :key="imp" cols="6">
|
<v-col v-for="imp in improvementsOptions" :key="imp" cols="6">
|
||||||
<v-card
|
<v-card
|
||||||
class="rounded-lg py-2 px-1 text-center cursor-pointer transition-card"
|
class="rounded-lg py-2 px-1 text-center cursor-pointer transition-card"
|
||||||
:class="{ 'bg-teal-lighten-5': assessmentForm.wantedImprovements.includes(imp) }"
|
:class="{ 'bg-teal-lighten-5': assessmentForm.wantedImprovements.includes(imp) }"
|
||||||
:color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey-lighten-1'"
|
:color="
|
||||||
|
assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey-lighten-1'
|
||||||
|
"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@click="toggleImprovement(imp)"
|
@click="toggleImprovement(imp)"
|
||||||
>
|
>
|
||||||
<v-icon class="mb-1" :color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey'" :icon="getImprovementIcon(imp)" />
|
<v-icon
|
||||||
|
class="mb-1"
|
||||||
|
:color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey'"
|
||||||
|
:icon="getImprovementIcon(imp)"
|
||||||
|
/>
|
||||||
<div class="text-caption font-weight-bold">{{ imp }}</div>
|
<div class="text-caption font-weight-bold">{{ imp }}</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -176,14 +194,18 @@
|
|||||||
身體限制與慢性病史
|
身體限制與慢性病史
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">1. 過去一年是否有過跌倒史?</p>
|
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||||
|
1. 過去一年是否有過跌倒史?
|
||||||
|
</p>
|
||||||
|
|
||||||
<v-radio-group v-model="assessmentForm.hasFalls" class="mb-4" color="primary" inline>
|
<v-radio-group v-model="assessmentForm.hasFalls" class="mb-4" color="primary" inline>
|
||||||
<v-radio label="無跌倒" :value="false" />
|
<v-radio label="無跌倒" :value="false" />
|
||||||
<v-radio label="有跌倒過" :value="true" />
|
<v-radio label="有跌倒過" :value="true" />
|
||||||
</v-radio-group>
|
</v-radio-group>
|
||||||
|
|
||||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">2. 目前身體關節疼痛狀況?</p>
|
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||||
|
2. 目前身體關節疼痛狀況?
|
||||||
|
</p>
|
||||||
|
|
||||||
<v-select
|
<v-select
|
||||||
v-model="assessmentForm.painLevel"
|
v-model="assessmentForm.painLevel"
|
||||||
@@ -194,9 +216,11 @@
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">3. 您有哪些慢性疾病? (可複選)</p>
|
<p class="text-body-2 font-weight-bold text-grey-darken-2 mb-2">
|
||||||
|
3. 您有哪些慢性疾病? (可複選)
|
||||||
|
</p>
|
||||||
|
|
||||||
<v-row class="mb-4" dense>
|
<v-row class="mb-4">
|
||||||
<v-col v-for="disease in diseaseOptions" :key="disease" cols="6">
|
<v-col v-for="disease in diseaseOptions" :key="disease" cols="6">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="assessmentForm.chronicDiseases"
|
v-model="assessmentForm.chronicDiseases"
|
||||||
@@ -221,7 +245,10 @@
|
|||||||
@keyup.enter="addMedication"
|
@keyup.enter="addMedication"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="assessmentForm.medicationReminders.length > 0" class="d-flex flex-wrap gap-2 mt-2">
|
<div
|
||||||
|
v-if="assessmentForm.medicationReminders.length > 0"
|
||||||
|
class="d-flex flex-wrap gap-2 mt-2"
|
||||||
|
>
|
||||||
<v-chip
|
<v-chip
|
||||||
v-for="(med, idx) in assessmentForm.medicationReminders"
|
v-for="(med, idx) in assessmentForm.medicationReminders"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
@@ -241,7 +268,10 @@
|
|||||||
<v-icon color="white" :icon="getResultIcon(resultLevel)" size="40" />
|
<v-icon color="white" :icon="getResultIcon(resultLevel)" size="40" />
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
|
||||||
<h2 class="text-h5 font-weight-bold mb-1" :class="`text-${getResultColor(resultLevel)}`">
|
<h2
|
||||||
|
class="text-h5 font-weight-bold mb-1"
|
||||||
|
:class="`text-${getResultColor(resultLevel)}`"
|
||||||
|
>
|
||||||
自評分級:{{ resultLevel }}
|
自評分級:{{ resultLevel }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -254,7 +284,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="resultLevel === '低風險'" class="text-body-2 text-grey-darken-3 mb-2">
|
<p v-if="resultLevel === '低風險'" class="text-body-2 text-grey-darken-3 mb-2">
|
||||||
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3 次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
|
您的身體功能與日常活動習慣良好。建議您每週繼續保持 3
|
||||||
|
次以上的運動,並可以預約普測活動以獲得精準的五維健康指標與運動處方。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-else-if="resultLevel === '需追蹤'" class="text-body-2 text-grey-darken-3 mb-2">
|
<p v-else-if="resultLevel === '需追蹤'" class="text-body-2 text-grey-darken-3 mb-2">
|
||||||
@@ -266,7 +297,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="font-weight-bold text-caption text-secondary mt-3">
|
<div class="font-weight-bold text-caption text-secondary mt-3">
|
||||||
* 禁忌提醒:若收縮壓 > 160mmHg、有關節強烈疼痛或暈眩時,請立即暫停所有體能檢測與訓練!
|
* 禁忌提醒:若收縮壓 >
|
||||||
|
160mmHg、有關節強烈疼痛或暈眩時,請立即暫停所有體能檢測與訓練!
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,13 +342,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<div v-else class="w-100 d-flex gap-2">
|
<div v-else class="w-100 d-flex gap-2">
|
||||||
<v-btn
|
<v-btn block class="rounded-lg py-2" color="primary" to="/appointment" variant="flat">
|
||||||
block
|
|
||||||
class="rounded-lg py-2"
|
|
||||||
color="primary"
|
|
||||||
to="/appointment"
|
|
||||||
variant="flat"
|
|
||||||
>
|
|
||||||
立即預約普測
|
立即預約普測
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,78 +365,85 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const currentStep = ref(0)
|
const currentStep = ref(0)
|
||||||
const stepNames = ['基本資料', '功能狀態', '健康限制', '自評結果']
|
const stepNames = ['基本資料', '功能狀態', '健康限制', '自評結果']
|
||||||
|
|
||||||
const profileForm = reactive({
|
const profileForm = reactive({
|
||||||
name: store.userProfile.name,
|
name: store.userProfile.name,
|
||||||
birthday: store.userProfile.birthday,
|
birthday: store.userProfile.birthday,
|
||||||
gender: store.userProfile.gender,
|
gender: store.userProfile.gender,
|
||||||
height: store.userProfile.height,
|
height: store.userProfile.height,
|
||||||
weight: store.userProfile.weight,
|
weight: store.userProfile.weight,
|
||||||
consent: store.userProfile.consent,
|
consent: store.userProfile.consent,
|
||||||
})
|
})
|
||||||
|
|
||||||
const assessmentForm = reactive({
|
const assessmentForm = reactive({
|
||||||
exerciseFrequency: store.selfAssessment.exerciseFrequency,
|
exerciseFrequency: store.selfAssessment.exerciseFrequency,
|
||||||
wantedImprovements: [...store.selfAssessment.wantedImprovements],
|
wantedImprovements: [...store.selfAssessment.wantedImprovements],
|
||||||
hasFalls: store.selfAssessment.hasFalls,
|
hasFalls: store.selfAssessment.hasFalls,
|
||||||
painLevel: store.selfAssessment.painLevel,
|
painLevel: store.selfAssessment.painLevel,
|
||||||
chronicDiseases: [...store.selfAssessment.chronicDiseases],
|
chronicDiseases: [...store.selfAssessment.chronicDiseases],
|
||||||
medicationReminders: [...store.selfAssessment.medicationReminders],
|
medicationReminders: [...store.selfAssessment.medicationReminders],
|
||||||
})
|
})
|
||||||
|
|
||||||
const medicationInput = ref('')
|
const medicationInput = ref('')
|
||||||
const resultLevel = ref(store.selfAssessment.scoreLevel)
|
const resultLevel = ref(store.selfAssessment.scoreLevel)
|
||||||
|
|
||||||
const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI']
|
const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI']
|
||||||
const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆']
|
const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆']
|
||||||
|
|
||||||
function getImprovementIcon (imp: string) {
|
function getImprovementIcon(imp: string) {
|
||||||
switch (imp) {
|
switch (imp) {
|
||||||
case '肌力': { return 'mdi-arm-flex'
|
case '肌力': {
|
||||||
|
return 'mdi-arm-flex'
|
||||||
}
|
}
|
||||||
case '平衡': { return 'mdi-scale-balance'
|
case '平衡': {
|
||||||
|
return 'mdi-scale-balance'
|
||||||
}
|
}
|
||||||
case '心肺': { return 'mdi-heart-pulse'
|
case '心肺': {
|
||||||
|
return 'mdi-heart-pulse'
|
||||||
}
|
}
|
||||||
case '柔軟度': { return 'mdi-human-stretching'
|
case '柔軟度': {
|
||||||
|
return 'mdi-human-stretching'
|
||||||
}
|
}
|
||||||
case '敏捷': { return 'mdi-run-fast'
|
case '敏捷': {
|
||||||
|
return 'mdi-run-fast'
|
||||||
}
|
}
|
||||||
case '體脂/BMI': { return 'mdi-calculator'
|
case '體脂/BMI': {
|
||||||
}
|
return 'mdi-calculator'
|
||||||
default: { return 'mdi-help-circle'
|
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
return 'mdi-help-circle'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleImprovement (imp: string) {
|
function toggleImprovement(imp: string) {
|
||||||
const index = assessmentForm.wantedImprovements.indexOf(imp)
|
const index = assessmentForm.wantedImprovements.indexOf(imp)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
assessmentForm.wantedImprovements.push(imp)
|
assessmentForm.wantedImprovements.push(imp)
|
||||||
} else {
|
} else {
|
||||||
assessmentForm.wantedImprovements.splice(index, 1)
|
assessmentForm.wantedImprovements.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMedication () {
|
function addMedication() {
|
||||||
if (medicationInput.value.trim()) {
|
if (medicationInput.value.trim()) {
|
||||||
assessmentForm.medicationReminders.push(medicationInput.value.trim())
|
assessmentForm.medicationReminders.push(medicationInput.value.trim())
|
||||||
medicationInput.value = ''
|
medicationInput.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMedication (index: number) {
|
function removeMedication(index: number) {
|
||||||
assessmentForm.medicationReminders.splice(index, 1)
|
assessmentForm.medicationReminders.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit () {
|
function handleSubmit() {
|
||||||
// 儲存至 store
|
// 儲存至 store
|
||||||
store.updateProfile({
|
store.updateProfile({
|
||||||
name: profileForm.name,
|
name: profileForm.name,
|
||||||
@@ -433,19 +466,19 @@
|
|||||||
// 取得計算後的結果
|
// 取得計算後的結果
|
||||||
resultLevel.value = store.selfAssessment.scoreLevel
|
resultLevel.value = store.selfAssessment.scoreLevel
|
||||||
currentStep.value = 3
|
currentStep.value = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResultColor (lvl: string) {
|
function getResultColor(lvl: string) {
|
||||||
if (lvl === '低風險') return 'success'
|
if (lvl === '低風險') return 'success'
|
||||||
if (lvl === '需追蹤') return 'warning'
|
if (lvl === '需追蹤') return 'warning'
|
||||||
return 'error'
|
return 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResultIcon (lvl: string) {
|
function getResultIcon(lvl: string) {
|
||||||
if (lvl === '低風險') return 'mdi-check-decagram'
|
if (lvl === '低風險') return 'mdi-check-decagram'
|
||||||
if (lvl === '需追蹤') return 'mdi-alert-decagram'
|
if (lvl === '需追蹤') return 'mdi-alert-decagram'
|
||||||
return 'mdi-alert-octagon'
|
return 'mdi-alert-octagon'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { useAppStore } from '@/stores/app'
|
|||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
scrollBehavior () {
|
||||||
|
return { left: 0, top: 0 }
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export interface MeasurementStation {
|
|||||||
values: Record<string, any>
|
values: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultUserProfile(): UserProfile {
|
function createDefaultUserProfile (): UserProfile {
|
||||||
return {
|
return {
|
||||||
name: '林建國',
|
name: '林建國',
|
||||||
birthday: '1961-08-15',
|
birthday: '1961-08-15',
|
||||||
@@ -59,7 +59,7 @@ function createDefaultUserProfile(): UserProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultSelfAssessment(): SelfAssessment {
|
function createDefaultSelfAssessment (): SelfAssessment {
|
||||||
return {
|
return {
|
||||||
exerciseFrequency: '1-2',
|
exerciseFrequency: '1-2',
|
||||||
painLevel: '輕微',
|
painLevel: '輕微',
|
||||||
@@ -72,7 +72,7 @@ function createDefaultSelfAssessment(): SelfAssessment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultAppointments(): Appointment[] {
|
function createDefaultAppointments (): Appointment[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'apt-01',
|
id: 'apt-01',
|
||||||
@@ -122,7 +122,7 @@ function createDefaultAppointments(): Appointment[] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultMeasurements(): Record<string, MeasurementStation> {
|
function createDefaultMeasurements (): Record<string, MeasurementStation> {
|
||||||
return {
|
return {
|
||||||
BloodPressure: {
|
BloodPressure: {
|
||||||
name: '血壓量測',
|
name: '血壓量測',
|
||||||
@@ -184,7 +184,7 @@ function createDefaultMeasurements(): Record<string, MeasurementStation> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultRadarData() {
|
function createDefaultRadarData () {
|
||||||
return {
|
return {
|
||||||
strength: 65,
|
strength: 65,
|
||||||
balance: 60,
|
balance: 60,
|
||||||
@@ -205,7 +205,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const isReportGenerated = ref(false)
|
const isReportGenerated = ref(false)
|
||||||
|
|
||||||
const completedStationsCount = computed(() => {
|
const completedStationsCount = computed(() => {
|
||||||
return Object.values(measurements.value).filter((station) => station.completed).length
|
return Object.values(measurements.value).filter(station => station.completed).length
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalStationsCount = computed(() => {
|
const totalStationsCount = computed(() => {
|
||||||
@@ -218,24 +218,24 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const done = Object.values(measurements.value).filter((station) => station.completed).length
|
const done = Object.values(measurements.value).filter(station => station.completed).length
|
||||||
return Math.round((done / keys.length) * 100)
|
return Math.round((done / keys.length) * 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
function login(phone: string) {
|
function login (phone: string) {
|
||||||
isLoggedIn.value = true
|
isLoggedIn.value = true
|
||||||
userProfile.value.phone = phone
|
userProfile.value.phone = phone
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout () {
|
||||||
isLoggedIn.value = false
|
isLoggedIn.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProfile(profile: Partial<UserProfile>) {
|
function updateProfile (profile: Partial<UserProfile>) {
|
||||||
userProfile.value = { ...userProfile.value, ...profile }
|
userProfile.value = { ...userProfile.value, ...profile }
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitSelfAssessment(answers: Partial<SelfAssessment>) {
|
function submitSelfAssessment (answers: Partial<SelfAssessment>) {
|
||||||
selfAssessment.value = {
|
selfAssessment.value = {
|
||||||
...selfAssessment.value,
|
...selfAssessment.value,
|
||||||
...answers,
|
...answers,
|
||||||
@@ -248,9 +248,9 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
answers.painLevel === '中度' ||
|
answers.painLevel === '中度'
|
||||||
answers.painLevel === '重度' ||
|
|| answers.painLevel === '重度'
|
||||||
(answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
|| (answers.chronicDiseases && answers.chronicDiseases.length >= 2)
|
||||||
) {
|
) {
|
||||||
level = '建議諮詢'
|
level = '建議諮詢'
|
||||||
}
|
}
|
||||||
@@ -258,8 +258,8 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
selfAssessment.value.scoreLevel = level
|
selfAssessment.value.scoreLevel = level
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAppointment(appointmentId: string) {
|
function registerAppointment (appointmentId: string) {
|
||||||
const appointment = appointments.value.find((item) => item.id === appointmentId)
|
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||||
if (!appointment) {
|
if (!appointment) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -268,8 +268,8 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
appointment.slots -= 1
|
appointment.slots -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAppointment(appointmentId: string) {
|
function cancelAppointment (appointmentId: string) {
|
||||||
const appointment = appointments.value.find((item) => item.id === appointmentId)
|
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||||
if (!appointment) {
|
if (!appointment) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -278,8 +278,8 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
appointment.slots += 1
|
appointment.slots += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkinAppointment(appointmentId: string) {
|
function checkinAppointment (appointmentId: string) {
|
||||||
const appointment = appointments.value.find((item) => item.id === appointmentId)
|
const appointment = appointments.value.find(item => item.id === appointmentId)
|
||||||
if (!appointment) {
|
if (!appointment) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
appointment.status = '已報到'
|
appointment.status = '已報到'
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMeasurement(code: string, values: Record<string, any>) {
|
function saveMeasurement (code: string, values: Record<string, any>) {
|
||||||
const measurement = measurements.value[code]
|
const measurement = measurements.value[code]
|
||||||
if (!measurement) {
|
if (!measurement) {
|
||||||
return
|
return
|
||||||
@@ -301,15 +301,15 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
measurement.time = timeStr
|
measurement.time = timeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFinalReport() {
|
function generateFinalReport () {
|
||||||
const currentMeasurements = measurements.value
|
const currentMeasurements = measurements.value
|
||||||
|
|
||||||
let strScore = 65
|
let strScore = 65
|
||||||
if (currentMeasurements.GripStrength.completed) {
|
if (currentMeasurements.GripStrength.completed) {
|
||||||
const avgGrip =
|
const avgGrip
|
||||||
((currentMeasurements.GripStrength.values.leftHand || 0) +
|
= ((currentMeasurements.GripStrength.values.leftHand || 0)
|
||||||
(currentMeasurements.GripStrength.values.rightHand || 0)) /
|
+ (currentMeasurements.GripStrength.values.rightHand || 0))
|
||||||
2
|
/ 2
|
||||||
const limit = userProfile.value.gender === '男' ? 30 : 20
|
const limit = userProfile.value.gender === '男' ? 30 : 20
|
||||||
strScore = Math.min(95, Math.max(50, Math.round((avgGrip / limit) * 75)))
|
strScore = Math.min(95, Math.max(50, Math.round((avgGrip / limit) * 75)))
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
if (currentMeasurements.OneLegTest.completed) {
|
if (currentMeasurements.OneLegTest.completed) {
|
||||||
const maxSec = Math.max(
|
const maxSec = Math.max(
|
||||||
currentMeasurements.OneLegTest.values.leftSeconds || 0,
|
currentMeasurements.OneLegTest.values.leftSeconds || 0,
|
||||||
currentMeasurements.OneLegTest.values.rightSeconds || 0
|
currentMeasurements.OneLegTest.values.rightSeconds || 0,
|
||||||
)
|
)
|
||||||
balScore = Math.min(98, Math.max(40, Math.round((maxSec / 30) * 90)))
|
balScore = Math.min(98, Math.max(40, Math.round((maxSec / 30) * 90)))
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
isReportGenerated.value = true
|
isReportGenerated.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetMeasurements() {
|
function resetMeasurements () {
|
||||||
for (const [key, measurement] of Object.entries(measurements.value)) {
|
for (const [key, measurement] of Object.entries(measurements.value)) {
|
||||||
if (key !== 'BloodPressure' && key !== 'BodyFat') {
|
if (key !== 'BloodPressure' && key !== 'BodyFat') {
|
||||||
measurement.completed = false
|
measurement.completed = false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "LINE Seed TW";
|
font-family: "line-seed";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "LINE Seed TW";
|
font-family: "line-seed";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "LINE Seed TW";
|
font-family: "line-seed";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "LINE Seed TW";
|
font-family: "line-seed";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -31,12 +31,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: "LINE Seed TW", sans-serif;
|
font-family: "line-seed", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#app,
|
#app,
|
||||||
.v-application {
|
.v-application {
|
||||||
font-family: "LINE Seed TW", sans-serif;
|
font-family: "line-seed", sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
* Configures SASS variables and Vuetify overwrites
|
* Configures SASS variables and Vuetify overwrites
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// https://vuetifyjs.com/features/sass-variables/`
|
// https://vuetifyjs.com/features/sass-variables/
|
||||||
// @use 'vuetify/settings' with (
|
@use 'vuetify/settings' with (
|
||||||
// $color-pack: false
|
$body-font-family: ('line-seed', sans-serif),
|
||||||
// );
|
$heading-font-family: ('line-seed', sans-serif)
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import Vue from '@vitejs/plugin-vue'
|
import Vue from '@vitejs/plugin-vue'
|
||||||
import Fonts from 'unplugin-fonts/vite'
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||||
|
|
||||||
@@ -17,17 +16,6 @@ export default defineConfig({
|
|||||||
configFile: 'src/styles/settings.scss',
|
configFile: 'src/styles/settings.scss',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Fonts({
|
|
||||||
fontsource: {
|
|
||||||
families: [
|
|
||||||
{
|
|
||||||
name: 'Roboto',
|
|
||||||
weights: [100, 300, 400, 500, 700, 900],
|
|
||||||
styles: ['normal', 'italic'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
define: { 'process.env': {} },
|
define: { 'process.env': {} },
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Generated
-156
@@ -10,9 +10,6 @@ importers:
|
|||||||
|
|
||||||
app/citizen-frontend:
|
app/citizen-frontend:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fontsource/roboto':
|
|
||||||
specifier: ^5.2.10
|
|
||||||
version: 5.2.10
|
|
||||||
'@mdi/font':
|
'@mdi/font':
|
||||||
specifier: 7.4.47
|
specifier: 7.4.47
|
||||||
version: 7.4.47
|
version: 7.4.47
|
||||||
@@ -59,9 +56,6 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ~5.9.3
|
specifier: ~5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
unplugin-fonts:
|
|
||||||
specifier: ^1.4.0
|
|
||||||
version: 1.4.0(vite@8.0.16(@types/node@24.13.2)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.9.0))
|
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.16(@types/node@24.13.2)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.9.0)
|
version: 8.0.16(@types/node@24.13.2)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.9.0)
|
||||||
@@ -206,9 +200,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
'@fontsource/roboto@5.2.10':
|
|
||||||
resolution: {integrity: sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==}
|
|
||||||
|
|
||||||
'@humanfs/core@0.19.2':
|
'@humanfs/core@0.19.2':
|
||||||
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -270,18 +261,6 @@ packages:
|
|||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^1.7.1
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
'@nodelib/fs.stat@2.0.5':
|
|
||||||
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
'@nodelib/fs.walk@1.2.8':
|
|
||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
'@ota-meshi/ast-token-store@0.3.0':
|
'@ota-meshi/ast-token-store@0.3.0':
|
||||||
resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==}
|
resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
@@ -720,10 +699,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
braces@3.0.3:
|
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -1006,10 +981,6 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
|
||||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
|
||||||
engines: {node: '>=8.6.0'}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
@@ -1025,9 +996,6 @@ packages:
|
|||||||
fast-wrap-ansi@0.2.2:
|
fast-wrap-ansi@0.2.2:
|
||||||
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
||||||
|
|
||||||
fastq@1.20.1:
|
|
||||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -1041,10 +1009,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
find-up-simple@1.0.1:
|
find-up-simple@1.0.1:
|
||||||
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1065,10 +1029,6 @@ packages:
|
|||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
glob-parent@6.0.2:
|
glob-parent@6.0.2:
|
||||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -1123,10 +1083,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
is-number@7.0.0:
|
|
||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
|
||||||
engines: {node: '>=0.12.0'}
|
|
||||||
|
|
||||||
is-what@5.5.0:
|
is-what@5.5.0:
|
||||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1282,14 +1238,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
|
||||||
merge2@1.4.1:
|
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
micromatch@4.0.8:
|
|
||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
|
||||||
engines: {node: '>=8.6'}
|
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@@ -1390,10 +1338,6 @@ packages:
|
|||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
picomatch@2.3.2:
|
|
||||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
|
||||||
engines: {node: '>=8.6'}
|
|
||||||
|
|
||||||
picomatch@4.0.4:
|
picomatch@4.0.4:
|
||||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1444,9 +1388,6 @@ packages:
|
|||||||
quansync@0.2.11:
|
quansync@0.2.11:
|
||||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
|
||||||
|
|
||||||
read-package-json-fast@4.0.0:
|
read-package-json-fast@4.0.0:
|
||||||
resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
|
resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -1471,10 +1412,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
reusify@1.1.0:
|
|
||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
|
||||||
|
|
||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
@@ -1483,9 +1420,6 @@ packages:
|
|||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
|
||||||
|
|
||||||
rxjs@7.8.2:
|
rxjs@7.8.2:
|
||||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
@@ -1686,10 +1620,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
|
||||||
engines: {node: '>=8.0'}
|
|
||||||
|
|
||||||
ts-api-utils@2.5.0:
|
ts-api-utils@2.5.0:
|
||||||
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
@@ -1721,23 +1651,10 @@ packages:
|
|||||||
undici-types@7.18.2:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
unplugin-fonts@1.4.0:
|
|
||||||
resolution: {integrity: sha512-TIJqr5rSlK/+3oL5nnrrEJ+Ty2taQ/bTJY1C5abYnksl553Q3HoHVqS4pnRLDkwpZq8AYqywib3kEVvHH+CtRQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@nuxt/kit': ^3.0.0 || ^4.0.0
|
|
||||||
vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@nuxt/kit':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
unplugin-utils@0.3.1:
|
unplugin-utils@0.3.1:
|
||||||
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
|
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
unplugin@2.3.5:
|
|
||||||
resolution: {integrity: sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==}
|
|
||||||
engines: {node: '>=18.12.0'}
|
|
||||||
|
|
||||||
unplugin@3.0.0:
|
unplugin@3.0.0:
|
||||||
resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==}
|
resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -2049,8 +1966,6 @@ snapshots:
|
|||||||
'@eslint/core': 1.2.1
|
'@eslint/core': 1.2.1
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
'@fontsource/roboto@5.2.10': {}
|
|
||||||
|
|
||||||
'@humanfs/core@0.19.2':
|
'@humanfs/core@0.19.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanfs/types': 0.15.0
|
'@humanfs/types': 0.15.0
|
||||||
@@ -2113,18 +2028,6 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.2
|
'@tybys/wasm-util': 0.10.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
|
||||||
dependencies:
|
|
||||||
'@nodelib/fs.stat': 2.0.5
|
|
||||||
run-parallel: 1.2.0
|
|
||||||
|
|
||||||
'@nodelib/fs.stat@2.0.5': {}
|
|
||||||
|
|
||||||
'@nodelib/fs.walk@1.2.8':
|
|
||||||
dependencies:
|
|
||||||
'@nodelib/fs.scandir': 2.1.5
|
|
||||||
fastq: 1.20.1
|
|
||||||
|
|
||||||
'@ota-meshi/ast-token-store@0.3.0': {}
|
'@ota-meshi/ast-token-store@0.3.0': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.133.0': {}
|
'@oxc-project/types@0.133.0': {}
|
||||||
@@ -2552,10 +2455,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.4
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
braces@3.0.3:
|
|
||||||
dependencies:
|
|
||||||
fill-range: 7.1.1
|
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.38
|
baseline-browser-mapping: 2.10.38
|
||||||
@@ -2872,14 +2771,6 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
|
||||||
dependencies:
|
|
||||||
'@nodelib/fs.stat': 2.0.5
|
|
||||||
'@nodelib/fs.walk': 1.2.8
|
|
||||||
glob-parent: 5.1.2
|
|
||||||
merge2: 1.4.1
|
|
||||||
micromatch: 4.0.8
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
@@ -2894,10 +2785,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fast-string-width: 3.0.2
|
fast-string-width: 3.0.2
|
||||||
|
|
||||||
fastq@1.20.1:
|
|
||||||
dependencies:
|
|
||||||
reusify: 1.1.0
|
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -2906,10 +2793,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
|
|
||||||
fill-range@7.1.1:
|
|
||||||
dependencies:
|
|
||||||
to-regex-range: 5.0.1
|
|
||||||
|
|
||||||
find-up-simple@1.0.1: {}
|
find-up-simple@1.0.1: {}
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
@@ -2927,10 +2810,6 @@ snapshots:
|
|||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
|
||||||
dependencies:
|
|
||||||
is-glob: 4.0.3
|
|
||||||
|
|
||||||
glob-parent@6.0.2:
|
glob-parent@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -2968,8 +2847,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
is-number@7.0.0: {}
|
|
||||||
|
|
||||||
is-what@5.5.0: {}
|
is-what@5.5.0: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
@@ -3087,13 +2964,6 @@ snapshots:
|
|||||||
|
|
||||||
memorystream@0.3.1: {}
|
memorystream@0.3.1: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
|
||||||
|
|
||||||
micromatch@4.0.8:
|
|
||||||
dependencies:
|
|
||||||
braces: 3.0.3
|
|
||||||
picomatch: 2.3.2
|
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.6
|
brace-expansion: 5.0.6
|
||||||
@@ -3188,8 +3058,6 @@ snapshots:
|
|||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.2: {}
|
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.4: {}
|
||||||
|
|
||||||
pidtree@0.6.1: {}
|
pidtree@0.6.1: {}
|
||||||
@@ -3236,8 +3104,6 @@ snapshots:
|
|||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
|
||||||
|
|
||||||
read-package-json-fast@4.0.0:
|
read-package-json-fast@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-parse-even-better-errors: 4.0.0
|
json-parse-even-better-errors: 4.0.0
|
||||||
@@ -3260,8 +3126,6 @@ snapshots:
|
|||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
reusify@1.1.0: {}
|
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rolldown@1.0.3:
|
rolldown@1.0.3:
|
||||||
@@ -3285,10 +3149,6 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
|
||||||
dependencies:
|
|
||||||
queue-microtask: 1.2.3
|
|
||||||
|
|
||||||
rxjs@7.8.2:
|
rxjs@7.8.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -3446,10 +3306,6 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
|
||||||
dependencies:
|
|
||||||
is-number: 7.0.0
|
|
||||||
|
|
||||||
ts-api-utils@2.5.0(typescript@5.9.3):
|
ts-api-utils@2.5.0(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -3477,23 +3333,11 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
unplugin-fonts@1.4.0(vite@8.0.16(@types/node@24.13.2)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.9.0)):
|
|
||||||
dependencies:
|
|
||||||
fast-glob: 3.3.3
|
|
||||||
unplugin: 2.3.5
|
|
||||||
vite: 8.0.16(@types/node@24.13.2)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.9.0)
|
|
||||||
|
|
||||||
unplugin-utils@0.3.1:
|
unplugin-utils@0.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
unplugin@2.3.5:
|
|
||||||
dependencies:
|
|
||||||
acorn: 8.17.0
|
|
||||||
picomatch: 4.0.4
|
|
||||||
webpack-virtual-modules: 0.6.2
|
|
||||||
|
|
||||||
unplugin@3.0.0:
|
unplugin@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
|
|||||||
Reference in New Issue
Block a user