提交 7918d64a 编写于 作者: R root

Auto Commit

上级 aa50c4cb
# VueJS-with-Vite
# 智慧养老平台(Vue3 + Vite)
Vue.js 是基于 JavaScript 构建用户界面的库。该模板使用 Vite 来提供应用程序服务。
## 项目简介
本项目是基于 Vue3 + Vite + Element Plus 的"智慧养老"主题平台,涵盖登录注册、新闻资讯、商品展示、社区互动、服务预约等多模块,界面美观、交互丰富,适合课程设计、项目考核和实际展示。
## 推荐的IDE设置
## 主要功能
- **首页**:轮播图、健康打卡、欢迎文案
- **商品页**:商品卡片、父子组件、插槽、智能设备推荐
- **新闻页**:新闻列表、详情页、配图/视频、评论区、相关文章推荐
- **社区页**:社区活动、家属留言
- **登录/注册**:正则验证、记住我、验证码、第三方登录、自适应美化
- **服务预约**:表单填写、预约历史
- **紧急呼叫**:右下角悬浮按钮
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## 技术栈
- Vue3 + Vite
- vue-router
- Element Plus
- 本地存储(localStorage)数据持久化
- 响应式布局与页面美化
## 自定义配置
请参阅 [[Vite配置参考](https://vitejs.dev/config/).
## 项目设置
```sh
npm install
## 目录结构
```
├── public/assets # 图片、视频等素材
├── src/
│ ├── components/ # 公共组件
│ ├── views/ # 页面视图
│ ├── router/ # 路由配置
│ └── main.js # 入口文件
├── package.json
├── README.md
└── ...
```
### 在开发环境中启动和热更新
## 运行方法
1. 安装依赖
```bash
npm install
# 或 yarn install
```
2. 启动开发环境
```bash
npm run dev
# 或 yarn dev
```
3. 打开浏览器访问 [http://localhost:5173](http://localhost:5173)
```sh
npm run dev
```
## 主要特色与考核点
- 结构规范,代码分层清晰
- 路由、父子组件、插槽、正则验证等 Vue3 核心考点
- 页面美观,主色调统一,动画丰富,响应式适配
- 本地存储实现健康打卡、评论、预约、留言等数据持久化
- 健壮性处理,异常友好提示
- 素材管理与路径规范
### 编译用于生产环境
## 扩展建议
- 可接入后端API实现数据同步
- 增加更多智能设备、健康服务、社区互动功能
- 优化移动端体验
```sh
npm run build
```
## 截图与演示
(请自行补充项目截图、演示视频等)
---
如有问题欢迎交流!
\ No newline at end of file
项目名称:智慧养老网站
运行方式:
1. 安装依赖:npm install
2. 启动项目:npm run dev
3. 打包项目:npm run build
评分点说明:
- 父子组件与插槽:Products.vue 和 ProductCard.vue
- 路由跳转:VueRouter 配置,所有页面可跳转
- 正则验证:Login.vue/Register.vue
- 页面布局与样式:App.vue 使用 element-plus
- 至少5个功能模块:登录、注册、简介、商品展示、新闻、社区
- 图片素材:放在 public/assets 目录
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<title>智慧养老平台</title>
</head>
<body>
<div id="app"></div>
......
{
"name": "vuejs-with-vite",
"version": "0.0.1",
"name": "smart-elderly-care",
"version": "1.0.0",
"scripts": {
"dev": "vite --host",
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173"
"serve": "vite preview"
},
"dependencies": {
"guess": "^1.0.2",
"vue": "^3.2.37"
"element-plus": "^2.3.0",
"vue": "^3.4.0",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.1",
"vite": "^5.0.1"
"@vitejs/plugin-vue": "^5.2.4",
"vite": "^5.0.0"
}
}
# 保持public目录存在
\ No newline at end of file
# 保持assets目录存在
\ No newline at end of file
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<el-container>
<el-header>
<el-menu :default-active="$route.path" mode="horizontal" router>
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/about">简介</el-menu-item>
<el-menu-item index="/products">商品展示</el-menu-item>
<el-menu-item index="/news">新闻列表</el-menu-item>
<el-menu-item index="/community">社区</el-menu-item>
<el-menu-item index="/login">登录</el-menu-item>
<el-menu-item index="/register">注册</el-menu-item>
</el-menu>
</el-header>
<el-main>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</el-main>
<el-button class="emergency-btn" type="danger" circle size="large" @click="emergencyDialog=true">
<el-icon><i class="el-icon-phone"></i></el-icon>
</el-button>
<el-dialog v-model="emergencyDialog" title="紧急呼叫" width="320px">
<div style="text-align:center;">
<el-icon style="font-size:40px;color:#f56c6c;"><i class="el-icon-phone"></i></el-icon>
<p style="margin:16px 0 8px 0;font-size:18px;">紧急联系电话:<b>120</b></p>
<p style="color:#888;">如遇突发状况,请立即拨打急救电话并告知您的位置。</p>
</div>
</header>
<main>
<TheWelcome />
</main>
<template #footer>
<el-button @click="emergencyDialog=false">关闭</el-button>
</template>
</el-dialog>
</el-container>
</template>
<style scoped>
header {
line-height: 1.5;
}
<script setup>
import { ref } from 'vue'
const emergencyDialog = ref(false)
</script>
.logo {
display: block;
margin: 0 auto 2rem;
<style>
:root {
--el-color-primary: #67C23A;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
body {
background: #f6f8fa;
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
color: #222;
}
h1, h2, h3 {
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
font-weight: bold;
color: #333;
}
.el-button {
font-size: 1.1rem;
letter-spacing: 1px;
border-radius: 8px;
transition: background 0.3s, box-shadow 0.3s;
}
.el-button:hover {
background: #85ce61;
box-shadow: 0 4px 16px #b0c4de80;
}
.product-card, .el-card, .news-card, .community-card, .detail-card {
transition: box-shadow 0.3s, transform 0.3s;
}
.product-card:hover, .el-card:hover, .news-card:hover, .community-card:hover, .detail-card:hover {
box-shadow: 0 12px 36px #b0c4de80;
transform: translateY(-8px) scale(1.04);
}
img, .carousel-img, .community-img, .home-img {
transition: box-shadow 0.3s, transform 0.3s;
}
img:hover, .carousel-img:hover, .community-img:hover, .home-img:hover {
box-shadow: 0 8px 32px #b0c4de80;
transform: scale(1.03);
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.emergency-btn {
position: fixed;
right: 32px;
bottom: 32px;
z-index: 9999;
box-shadow: 0 4px 16px #f56c6c40;
font-size: 28px;
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
align-items: center;
justify-content: center;
}
</style>
\ No newline at end of file
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
\ No newline at end of file
@import "./base.css";
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a target="_blank" href="https://vitejs.dev/">Vite</a> +
<a target="_blank" href="https://vuejs.org/">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
<template>
<el-col :xs="24" :sm="12" :md="6">
<el-card class="product-card" shadow="hover">
<img :src="product.img" class="product-img" />
<h3>{{ product.name }}</h3>
<p class="desc">{{ product.desc }}</p>
<p class="price">{{ product.price }}</p>
<slot name="footer"></slot>
</el-card>
</el-col>
</template>
<script setup>
defineProps(['product'])
</script>
<style scoped>
.product-card {
border-radius: 18px;
transition: box-shadow 0.3s, transform 0.3s;
margin-bottom: 32px;
text-align: center;
box-shadow: 0 2px 12px #e0e0e0;
}
.product-card:hover {
box-shadow: 0 8px 32px #b0c4de80;
transform: translateY(-6px) scale(1.03);
}
.product-img {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 16px;
}
.price {
color: #e67e22;
font-weight: bold;
font-size: 20px;
}
.desc {
color: #888;
min-height: 32px;
}
</style>
\ No newline at end of file
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vue’s
<a target="_blank" href="https://vuejs.org/">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank">Vite</a>. The recommended IDE
setup is <a href="https://code.visualstudio.com/" target="_blank">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>. If you need to test
your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a target="_blank" href="https://pinia.vuejs.org/">Pinia</a>,
<a target="_blank" href="https://router.vuejs.org/">Vue Router</a>,
<a target="_blank" href="https://test-utils.vuejs.org/">Vue Test Utils</a>, and
<a target="_blank" href="https://github.com/vuejs/devtools">Vue Dev Tools</a>. If you need more
resources, we suggest paying
<a target="_blank" href="https://github.com/vuejs/awesome-vue">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a target="_blank" href="https://chat.vuejs.org">Vue Land</a>, our official Discord server, or
<a target="_blank" href="https://stackoverflow.com/questions/tagged/vue.js">StackOverflow</a>.
You should also subscribe to
<a target="_blank" href="https://news.vuejs.org">our mailing list</a> and follow the official
<a target="_blank" href="https://twitter.com/vuejs">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a target="_blank" href="https://vuejs.org/sponsor/">becoming a sponsor</a>.
</WelcomeItem>
</template>
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './assets/main.css'
createApp(App).mount('#app')
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
\ No newline at end of file
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Products from '../views/Products.vue'
import News from '../views/News.vue'
import Community from '../views/Community.vue'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import ProductDetail from '../views/ProductDetail.vue'
import NewsDetail from '../views/NewsDetail.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/products', component: Products },
{ path: '/products/:id', component: ProductDetail },
{ path: '/news', component: News },
{ path: '/news/:id', component: NewsDetail },
{ path: '/community', component: Community },
{ path: '/login', component: Login },
{ path: '/register', component: Register },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// 登录拦截
router.beforeEach((to, from, next) => {
const isLogin = localStorage.getItem('isLogin') === 'true'
if (!isLogin && to.path !== '/login' && to.path !== '/register') {
next('/login')
} else {
next()
}
})
export default router
\ No newline at end of file
<template>
<el-card class="reserve-card">
<h2>服务预约</h2>
<el-form :model="form" label-width="80px" style="max-width:400px;margin:0 auto;">
<el-form-item label="服务类型">
<el-select v-model="form.type" placeholder="请选择服务类型">
<el-option label="体检" value="体检" />
<el-option label="家政" value="家政" />
<el-option label="陪护" value="陪护" />
<el-option label="康复" value="康复" />
</el-select>
</el-form-item>
<el-form-item label="预约时间">
<el-date-picker v-model="form.date" type="datetime" placeholder="选择日期时间" style="width:100%" />
</el-form-item>
<el-form-item label="联系方式">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitReserve">提交预约</el-button>
</el-form-item>
</el-form>
<el-divider />
<h3>预约历史</h3>
<el-table :data="history" size="small" v-if="history.length">
<el-table-column prop="type" label="服务类型" width="100" />
<el-table-column prop="date" label="预约时间" width="180" />
<el-table-column prop="phone" label="联系方式" width="120" />
</el-table>
<div v-else style="color:#888;text-align:center;">暂无预约记录</div>
</el-card>
</template>
<script setup>
import { ref } from 'vue'
const form = ref({ type: '', date: '', phone: '' })
const history = ref(JSON.parse(localStorage.getItem('reserveHistory') || '[]'))
function submitReserve() {
if (!form.value.type || !form.value.date || !form.value.phone) {
window.$message?.warning('请填写完整信息')
return
}
history.value.unshift({ ...form.value })
history.value = history.value.slice(0, 10)
localStorage.setItem('reserveHistory', JSON.stringify(history.value))
form.value = { type: '', date: '', phone: '' }
window.$message?.success('预约成功!')
}
</script>
<style scoped>
.reserve-card {
max-width: 600px;
margin: 40px auto;
border-radius: 20px;
box-shadow: 0 8px 32px #b0c4de80;
padding: 32px 24px;
}
</style>
\ No newline at end of file
<template>
<el-card class="community-card">
<h2 class="community-title">智慧养老社区</h2>
<img src="/assets/community-event.jpg" alt="社区活动" class="community-img" />
<p>欢迎大家在此交流智慧养老经验与心得!</p>
<el-divider />
<h3>家属留言</h3>
<el-form :model="msgForm" label-width="60px" style="max-width:400px;margin:0 auto;">
<el-form-item label="姓名">
<el-input v-model="msgForm.name" placeholder="您的姓名" />
</el-form-item>
<el-form-item label="留言">
<el-input v-model="msgForm.content" placeholder="写下您的关怀或祝福" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitMsg">留言</el-button>
</el-form-item>
</el-form>
<el-card class="msg-history-card" v-if="msgHistory.length">
<h4>留言墙</h4>
<ul class="msg-list">
<li v-for="(item, idx) in msgHistory" :key="idx">
<b>{{ item.name }}</b>{{ item.content }}
</li>
</ul>
</el-card>
</el-card>
</template>
<script setup>
import { ref } from 'vue'
const msgForm = ref({ name: '', content: '' })
const msgHistory = ref(JSON.parse(localStorage.getItem('msgHistory') || '[]'))
function submitMsg() {
if (!msgForm.value.name || !msgForm.value.content) {
window.$message?.warning('请填写完整留言')
return
}
msgHistory.value.unshift({ ...msgForm.value })
msgHistory.value = msgHistory.value.slice(0, 10)
localStorage.setItem('msgHistory', JSON.stringify(msgHistory.value))
msgForm.value = { name: '', content: '' }
window.$message?.success('留言成功!')
}
</script>
<style scoped>
.community-card {
max-width: 600px;
margin: 40px auto;
border-radius: 20px;
box-shadow: 0 8px 32px #b0c4de80;
padding: 32px 24px;
}
.community-title {
color: #409EFF;
margin-bottom: 16px;
font-size: 2rem;
font-weight: bold;
}
.community-img {
width: 100%;
border-radius: 16px;
margin: 16px 0;
box-shadow: 0 2px 8px #e0e0e0;
}
.msg-history-card {
margin: 24px auto 0 auto;
border-radius: 12px;
box-shadow: 0 2px 8px #e0e0e0;
padding: 16px 20px;
max-width: 400px;
}
.msg-list {
margin: 0;
padding: 0 0 0 16px;
color: #555;
font-size: 15px;
}
.msg-list li {
margin-bottom: 8px;
}
</style>
\ No newline at end of file
<template>
<div class="home-bg">
<el-card class="home-card">
<el-carousel height="220px" indicator-position="outside" class="home-carousel">
<el-carousel-item v-for="img in carouselImages" :key="img">
<img :src="img" class="carousel-img" />
</el-carousel-item>
</el-carousel>
<div class="home-content">
<img src="/assets/elderly-home.png" alt="智慧养老" class="home-img" />
<h2>欢迎来到智慧养老平台</h2>
<p>为长者提供智能、便捷、健康的生活服务。</p>
<el-button type="primary" size="large" @click="$router.push('/about')">了解更多</el-button>
<el-divider />
<el-button type="success" size="large" @click="openHealthDialog">健康打卡</el-button>
</div>
</el-card>
<el-dialog v-model="healthDialog" title="今日健康打卡" width="350px">
<el-form :model="healthForm">
<el-form-item label="步数">
<el-input v-model="healthForm.steps" placeholder="请输入今日步数" />
</el-form-item>
<el-form-item label="心情">
<el-select v-model="healthForm.mood" placeholder="请选择心情">
<el-option label="愉快" value="愉快" />
<el-option label="一般" value="一般" />
<el-option label="低落" value="低落" />
</el-select>
</el-form-item>
<el-form-item label="血压">
<el-input v-model="healthForm.bp" placeholder="如120/80" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="healthDialog=false">取消</el-button>
<el-button type="primary" @click="submitHealth">打卡</el-button>
</template>
</el-dialog>
<el-card class="health-history-card" v-if="healthHistory.length">
<h3>本周健康打卡历史</h3>
<el-table :data="healthHistory" size="small" style="margin-top:10px">
<el-table-column prop="date" label="日期" width="100" />
<el-table-column prop="steps" label="步数" width="80" />
<el-table-column prop="mood" label="心情" width="80" />
<el-table-column prop="bp" label="血压" width="100" />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
const carouselImages = [
'/assets/elderly-home.png',
'/assets/community-event.jpg',
'/assets/band1.jpg',
'/assets/band2.jpg'
]
const healthDialog = ref(false)
const healthForm = ref({ steps: '', mood: '', bp: '' })
const healthHistory = ref(JSON.parse(localStorage.getItem('healthHistory') || '[]'))
function openHealthDialog() {
healthDialog.value = true
}
function submitHealth() {
const today = new Date().toLocaleDateString()
healthHistory.value.unshift({
date: today,
steps: healthForm.value.steps,
mood: healthForm.value.mood,
bp: healthForm.value.bp
})
// 只保留最近7天
healthHistory.value = healthHistory.value.slice(0, 7)
localStorage.setItem('healthHistory', JSON.stringify(healthHistory.value))
healthDialog.value = false
healthForm.value = { steps: '', mood: '', bp: '' }
}
</script>
<style scoped>
.home-bg {
min-height: 100vh;
background: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.home-card {
max-width: 600px;
margin: 40px auto 0 auto;
text-align: center;
background: #fff;
border-radius: 24px;
box-shadow: 0 8px 32px #b0c4de80;
padding: 40px 32px 24px 32px;
}
.home-carousel {
margin-bottom: 16px;
border-radius: 16px;
overflow: hidden;
}
.carousel-img {
width: 100%;
height: 220px;
object-fit: cover;
border-radius: 16px;
}
.home-img {
width: 140px;
margin-bottom: 24px;
border-radius: 20px;
box-shadow: 0 2px 12px #e0e0e0;
}
.home-content h2 {
margin: 20px 0 12px 0;
color: #409EFF;
font-size: 2.2rem;
font-weight: bold;
}
.home-content p {
color: #666;
font-size: 1.1rem;
margin-bottom: 24px;
}
.health-history-card {
max-width: 600px;
margin: 24px auto;
border-radius: 16px;
box-shadow: 0 4px 24px #e0e0e0;
padding: 16px 24px;
}
</style>
\ No newline at end of file
<template>
<div class="form-bg">
<div class="form-center">
<el-card class="form-card">
<div class="form-logo">
<img src="/assets/elderly-home.png" alt="logo" />
<h2>智慧养老平台</h2>
</div>
<el-form :model="form" :rules="rules" ref="loginForm" label-width="80px">
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="验证码" prop="captcha">
<el-row :gutter="8">
<el-col :span="16">
<el-input v-model="form.captcha" maxlength="4" placeholder="请输入验证码" />
</el-col>
<el-col :span="8">
<div class="captcha-box" @click="refreshCaptcha">{{ captcha }}</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.remember">记住我</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" size="large" style="width:100%">登录</el-button>
</el-form-item>
</el-form>
<div class="third-login">
<span>第三方登录:</span>
<el-button type="info" icon="el-icon-user" circle size="small" title="微信" />
<el-button type="primary" icon="el-icon-user" circle size="small" title="QQ" style="background:#55acee;border:none;" />
</div>
<div class="form-switch">
还没有账号?<el-link type="primary" @click="$router.push('/register')">去注册</el-link>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = ref({ phone: '', password: '', captcha: '', remember: false })
const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6位', trigger: 'blur' }
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ validator: (rule, value) => value === captcha.value, message: '验证码错误', trigger: 'blur' }
]
}
const loginForm = ref(null)
const captcha = ref(randomCaptcha())
function randomCaptcha() {
return Math.random().toString(36).slice(2, 6).toUpperCase()
}
function refreshCaptcha() {
captcha.value = randomCaptcha()
}
const onSubmit = () => {
loginForm.value.validate(valid => {
if (valid) {
if (form.value.remember) {
localStorage.setItem('rememberPhone', form.value.phone)
} else {
localStorage.removeItem('rememberPhone')
}
localStorage.setItem('isLogin', 'true')
alert('登录成功')
router.push('/')
}
})
}
// 自动填充记住的手机号
if (localStorage.getItem('rememberPhone')) {
form.value.phone = localStorage.getItem('rememberPhone')
form.value.remember = true
}
</script>
<style scoped>
.form-bg {
min-height: 100vh;
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
display: flex;
align-items: center;
justify-content: center;
}
.form-center {
width: 100vw;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.form-card {
border-radius: 20px;
box-shadow: 0 8px 32px #fda08580;
padding: 32px 24px 16px 24px;
max-width: 400px;
width: 100%;
}
.form-logo {
text-align: center;
margin-bottom: 18px;
}
.form-logo img {
width: 56px;
height: 56px;
border-radius: 16px;
box-shadow: 0 2px 8px #e0e0e0;
margin-bottom: 8px;
}
.form-logo h2 {
font-size: 1.5rem;
color: #409EFF;
margin: 0;
font-weight: bold;
}
.captcha-box {
background: #f6f8fa;
border: 1px solid #dcdfe6;
border-radius: 6px;
text-align: center;
font-size: 18px;
font-weight: bold;
letter-spacing: 2px;
cursor: pointer;
user-select: none;
height: 38px;
line-height: 38px;
}
.third-login {
text-align: center;
margin: 16px 0 0 0;
color: #888;
font-size: 15px;
}
.form-switch {
text-align: center;
margin-top: 12px;
color: #888;
font-size: 15px;
}
</style>
\ No newline at end of file
<template>
<div>
<el-card class="news-card">
<h2 class="news-title">新闻资讯</h2>
<el-table :data="news" stripe @row-click="showDetail">
<el-table-column prop="title" label="标题">
<template #default="scope">
<el-link type="primary" @click.stop="$router.push(`/news/${scope.$index}`)">{{ scope.row.title }}</el-link>
</template>
</el-table-column>
<el-table-column prop="date" label="日期" />
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="primary" size="small" @click.stop="$router.push(`/news/${scope.$index}`)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="currentNews.title" width="600px">
<div v-if="currentNews.img">
<img :src="currentNews.img" alt="新闻配图" style="width:100%;border-radius:12px;margin-bottom:16px;object-fit:cover;max-height:220px;" />
</div>
<div v-html="currentNews.content"></div>
<el-divider />
<h4>评论区</h4>
<el-form :model="commentForm" inline style="margin-bottom:10px;">
<el-form-item>
<el-input v-model="commentForm.user" placeholder="昵称" size="small" style="width:100px" />
</el-form-item>
<el-form-item>
<el-input v-model="commentForm.text" placeholder="写下你的评论... 可@用户" size="small" style="width:180px" />
</el-form-item>
<el-form-item>
<el-popover placement="top" width="220" trigger="click">
<template #reference>
<el-button icon="el-icon-smile" size="small" />
</template>
<div class="emoji-panel">
<span v-for="e in emojiList" :key="e" @click="insertEmoji(e, 'comment')" class="emoji-item">{{ e }}</span>
</div>
</el-popover>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="submitComment">评论</el-button>
</el-form-item>
</el-form>
<el-row style="margin-bottom:10px;align-items:center;" v-if="currentNews.comments && currentNews.comments.length">
<el-col :span="12">
<el-radio-group v-model="sortType" size="small">
<el-radio-button label="new">最新</el-radio-button>
<el-radio-button label="hot">最热</el-radio-button>
</el-radio-group>
</el-col>
<el-col :span="12" style="text-align:right;">
<el-pagination
v-if="(currentNews.comments || []).length > pageSize"
:current-page="currentPage"
:page-size="pageSize"
:total="(currentNews.comments || []).length"
layout="prev, pager, next"
@current-change="val => currentPage = val"
small
/>
</el-col>
</el-row>
<el-empty v-if="!currentNews.comments || !currentNews.comments.length" description="暂无评论" />
<el-timeline v-else style="max-height:220px;overflow:auto;">
<el-timeline-item v-for="(c, idx) in pagedComments" :key="idx" :timestamp="c.time" color="#67C23A" :class="{top: c.isTop}">
<div style="display:flex;align-items:flex-start;gap:8px;">
<img :src="c.avatar" style="width:28px;height:28px;border-radius:50%;object-fit:cover;" />
<div style="flex:1;">
<b :style="{color: roleColor[c.role] || '#333'}">{{ c.user }}</b>
<span v-if="c.role==='admin'" class="admin-badge">管理员</span>
<span v-else-if="c.role==='user'" class="user-badge">用户</span>
<span v-else class="guest-badge">游客</span>
<span v-if="c.emoji" style="margin-left:4px;">{{ c.emoji }}</span>
<span v-if="c.replyTo" style="color:#409EFF;margin-left:8px;">@{{ c.replyTo }}</span>
{{ c.text }}
<div v-if="c.replyList && c.replyList.length" class="reply-list">
<div v-for="(r, ridx) in c.replyList" :key="ridx" class="reply-item">
<img :src="r.avatar" style="width:22px;height:22px;border-radius:50%;object-fit:cover;vertical-align:middle;" />
<b>{{ r.user }}</b>
<span v-if="r.emoji" style="margin-left:2px;">{{ r.emoji }}</span>
<span v-if="r.replyTo" style="color:#409EFF;margin-left:6px;">@{{ r.replyTo }}</span>
{{ r.text }}
<el-button type="text" size="small" @click="deleteReply(idx, ridx)" style="color:#f56c6c;">删除</el-button>
</div>
</div>
<div style="margin-top:4px;">
<el-button type="text" size="small" @click="startReply(idx, c.user)">回复</el-button>
<el-button type="text" size="small" @click="likeComment(idx)">
<el-icon><i class="el-icon-thumb"></i></el-icon>
<span style="margin-left:2px;">{{ c.likes || 0 }}</span>
</el-button>
<el-button type="text" size="small" @click="reportComment(idx)" style="color:#e6a23c;">举报</el-button>
<el-button v-if="userInfo.role==='admin'" type="text" size="small" @click="toggleTop(idx)" style="color:#67C23A;">{{ c.isTop ? '取消置顶' : '置顶' }}</el-button>
<el-button type="text" size="small" @click="deleteComment(idx)" style="color:#f56c6c;">删除</el-button>
</div>
<div v-if="replyingIdx === idx" style="margin-top:6px;">
<el-input v-model="replyForm.text" placeholder="回复内容,可@用户" size="small" style="width:180px" />
<el-popover placement="top" width="220" trigger="click">
<template #reference>
<el-button icon="el-icon-smile" size="small" style="margin-left:4px;" />
</template>
<div class="emoji-panel">
<span v-for="e in emojiList" :key="e" @click="insertEmoji(e, 'reply')" class="emoji-item">{{ e }}</span>
</div>
</el-popover>
<el-button type="primary" size="small" @click="submitReply(idx, c.user)">回复</el-button>
<el-button size="small" @click="cancelReply">取消</el-button>
</div>
</div>
</div>
</el-timeline-item>
</el-timeline>
<template #footer>
<el-button @click="dialogVisible=false">关闭</el-button>
</template>
</el-dialog>
<el-card class="health-knowledge-card">
<h3>健康知识</h3>
<ul class="knowledge-list">
<li v-for="item in knowledge" :key="item.title">
<b>{{ item.title }}</b>:{{ item.desc }}
</li>
</ul>
</el-card>
<el-dialog v-model="reportDialog.visible" title="举报评论" width="340px">
<el-form :model="reportDialog.form">
<el-form-item label="理由">
<el-input v-model="reportDialog.form.reason" type="textarea" placeholder="请输入举报理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="reportDialog.visible=false">取消</el-button>
<el-button type="primary" @click="submitReport">提交</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.news-card {
border-radius: 20px;
box-shadow: 0 8px 32px #b0c4de80;
max-width: 800px;
margin: 40px auto;
padding: 32px 24px;
}
.news-title {
color: #409EFF;
margin-bottom: 20px;
font-size: 2rem;
font-weight: bold;
}
.health-knowledge-card {
margin: 32px auto;
max-width: 800px;
border-radius: 16px;
box-shadow: 0 4px 24px #e0e0e0;
padding: 24px 16px;
}
.knowledge-list {
margin: 0;
padding: 0 0 0 16px;
color: #555;
font-size: 15px;
}
.knowledge-list li {
margin-bottom: 8px;
}
.reply-list {
margin: 6px 0 0 0;
padding-left: 16px;
border-left: 2px solid #e0e0e0;
}
.reply-item {
font-size: 14px;
color: #555;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.emoji-panel {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 6px 0;
}
.emoji-item {
font-size: 22px;
cursor: pointer;
transition: transform 0.2s;
}
.emoji-item:hover {
transform: scale(1.2);
}
.top {
background: #f6ffed !important;
border-left: 4px solid #67C23A !important;
}
</style>
<script setup>
import { ref, watch, computed } from 'vue'
const newsKey = 'newsWithComments'
const userKey = 'userInfo'
const defaultNews = [
{
title: '智慧养老新政策',
date: '2024-06-01',
img: '/assets/elderly-home.png',
author: {
name: '张主任',
avatar: '/assets/author1.png'
},
video: '/assets/policy-video.mp4',
content: `
<h3>智慧养老新政策解读</h3>
<p>2024年6月1日,国家发布了最新智慧养老政策,推动信息化、智能化服务进社区、进家庭。政策鼓励各地建设智慧养老平台,推广智能设备应用,提升老年人生活质量。</p>
<img src="/assets/policy-banner.jpg" style="width:100%;border-radius:12px;margin:12px 0;" />
<video src="/assets/policy-video.mp4" controls style="width:100%;border-radius:12px;margin:12px 0;"></video>
<p>主要内容包括:</p>
<ul>
<li><b>加大对智慧养老企业的扶持:</b>政府将提供专项资金和税收优惠,鼓励企业研发适老化智能产品。</li>
<li><b>推动社区智能化改造:</b>各地社区将逐步实现智能门禁、健康监测、紧急呼叫等功能全覆盖。</li>
<li><b>加强老年人数字素养培训:</b>组织志愿者和专业人员为老年人开展智能手机、健康设备等数字技能培训。</li>
<li><b>智慧健康档案:</b>每位老年人将拥有专属电子健康档案,实现跨机构、跨平台健康数据互通。</li>
<li><b>医养结合创新:</b>推动医疗资源与养老服务深度融合,社区卫生服务中心与养老机构协同为长者提供健康管理、慢病随访、远程问诊等服务。</li>
<li><b>家庭智能看护:</b>推广智能摄像头、跌倒报警器、远程健康监测设备,家属可通过手机App实时掌握长者状况。</li>
</ul>
<h4>政策意义</h4>
<p>本次政策的出台,标志着我国养老服务进入智能化、信息化新阶段。通过政策引导和资金支持,智慧养老产业链将进一步完善,老年人将享受到更高质量、更便捷的服务。</p>
<h4>长者故事</h4>
<p>家住南京的王奶奶说:"自从社区安装了智能健康设备,孩子们在外地也能随时看到我的健康状况,我心里很踏实。"</p>
<h4>未来展望</h4>
<p>政策提出,到2025年,80%以上的城市社区要建成智慧养老服务站点,老年人满意度显著提升。未来,人工智能、大数据、物联网等新技术将持续赋能养老服务,让科技更有温度,让长者生活更有尊严。</p>
<blockquote>"智慧养老让科技更有温度,让长者生活更有尊严。"——政策发布会发言人</blockquote>
`,
comments: []
},
{
title: 'AI助力长者健康管理',
date: '2024-06-10',
img: '/assets/ai-health.jpg',
author: {
name: '李医生',
avatar: '/assets/author2.png'
},
content: `
<h3>人工智能赋能智慧养老</h3>
<p>随着AI技术的发展,越来越多的养老机构引入智能健康管理系统。通过智能手环、健康一体机等设备,AI可实时分析长者的健康数据,自动识别异常并推送预警。</p>
<img src="/assets/ai-health.jpg" style="width:100%;border-radius:12px;margin:12px 0;" />
<ul>
<li>AI算法可预测慢病风险,提前干预。</li>
<li>语音助手为老人提供用药提醒、健康咨询等贴心服务。</li>
<li>智能摄像头实现跌倒检测,保障居家安全。</li>
</ul>
<h4>技术亮点</h4>
<p>AI系统可自动生成健康报告,家属和医生可远程查看。部分社区还引入了AI陪伴机器人,为长者提供情感关怀和日常提醒。</p>
<h4>长者体验</h4>
<p>张大爷表示:"AI手环每天提醒我按时吃药,家人也能随时看到我的健康数据,感觉很安心。"</p>
<p>专家表示,AI将成为未来养老服务的重要助手,让长者享受更科学、更便捷的健康管理。</p>
`,
comments: []
},
{
title: '社区举办智慧养老体验日',
date: '2024-06-05',
img: '/assets/community-experience.jpg',
author: {
name: '社区记者',
avatar: '/assets/author3.png'
},
content: `
<h3>智慧养老体验日活动精彩纷呈</h3>
<p>本周,XX社区举办了"智慧养老体验日"活动,吸引了众多长者和家属参与。现场展示了智能手环、健康一体机、跌倒报警器等多种智能设备。</p>
<img src="/assets/community-experience.jpg" style="width:100%;border-radius:12px;margin:12px 0;" />
<ul>
<li>长者现场体验健康监测,数据实时同步到手机App。</li>
<li>志愿者为老人讲解智能设备使用方法,提升数字素养。</li>
<li>活动还设有健康讲座、趣味运动会等环节,气氛热烈。</li>
</ul>
<h4>活动亮点</h4>
<p>社区医生现场为长者答疑解惑,志愿者手把手教学,许多老人第一次用上了智能健康设备,感受到科技带来的便利。</p>
<h4>居民反馈</h4>
<p>李阿姨说:"体验日让我学会了用健康手环,家人也更放心了。"</p>
<p>许多长者表示,通过体验活动,感受到科技带来的便利和关爱。</p>
`,
comments: []
},
{
title: '智能设备助力健康',
date: '2024-05-10',
img: '/assets/band1.jpg',
content: `<h3>智能设备守护长者健康</h3>
<p>智能手环、血压计、跌倒报警器等智能设备在社区广泛应用,帮助老人实时监测健康数据,提升了健康管理的科学性和便捷性。</p>
<ul>
<li><b>智能手环:</b>可24小时监测心率、步数、睡眠质量,异常自动提醒家属和社区医生。</li>
<li><b>智能血压计:</b>测量数据自动同步到健康档案,便于医生远程管理。</li>
<li><b>跌倒报警器:</b>一旦检测到跌倒,自动发出警报并通知紧急联系人。</li>
</ul>
<h4>科技创新</h4>
<p>部分社区还引入了智能床垫、远程健康监测仪等新设备,实现了全天候健康守护。</p>
<h4>长者故事</h4>
<p>社区健康服务中心负责人表示:"智能设备让健康管理更主动、更科学,家属也更安心。"</p>
<blockquote>"科技守护健康,智慧养老让爱不缺席。"——社区健康服务中心</blockquote>`,
comments: []
}
]
const news = ref(JSON.parse(localStorage.getItem(newsKey) || 'null') || defaultNews)
const knowledge = [
{ title: '合理膳食', desc: '多吃蔬果,少油少盐,均衡营养。' },
{ title: '适量运动', desc: '每天坚持适度锻炼,增强体质。' },
{ title: '心理健康', desc: '保持乐观心态,积极与人交流。' },
{ title: '慢病管理', desc: '按时服药,定期体检,关注血压血糖。' }
]
const dialogVisible = ref(false)
const currentNews = ref({})
const commentForm = ref({ user: '', text: '', emoji: '' })
const replyForm = ref({ text: '', emoji: '' })
const replyingIdx = ref(-1)
const emojiList = ['😀','😃','😄','😁','😆','😅','😂','😊','😍','😎','🥰','👍','👏','🎉','❤️']
const avatarList = [
'https://randomuser.me/api/portraits/men/32.jpg',
'https://randomuser.me/api/portraits/women/44.jpg',
'https://randomuser.me/api/portraits/men/65.jpg',
'https://randomuser.me/api/portraits/women/68.jpg',
'https://randomuser.me/api/portraits/men/12.jpg',
'https://randomuser.me/api/portraits/women/21.jpg'
]
const pageSize = 10
const currentPage = ref(1)
const sortType = ref('new')
const roleColor = { admin: '#e67e22', user: '#409EFF', guest: '#909399' }
const pagedComments = computed(() => {
if (!currentNews.value.comments) return []
let list = [...currentNews.value.comments]
if (sortType.value === 'hot') {
list.sort((a, b) => (b.likes || 0) - (a.likes || 0))
} else {
list.sort((a, b) => new Date(b.time) - new Date(a.time))
}
const start = (currentPage.value - 1) * pageSize
return list.slice(start, start + pageSize)
})
const reportDialog = ref({ visible: false, idx: -1, form: { reason: '' } })
function randomAvatar() {
return avatarList[Math.floor(Math.random() * avatarList.length)]
}
function getUserInfo() {
let info = JSON.parse(localStorage.getItem(userKey) || 'null')
if (!info) {
const roles = ['guest', 'user', 'admin']
const idx = Math.floor(Math.random() * 10)
let role = idx === 0 ? 'admin' : (idx < 5 ? 'user' : 'guest')
info = {
name: role === 'guest' ? '游客' + Math.floor(Math.random() * 1000) : (role === 'admin' ? '管理员' : '用户' + Math.floor(Math.random() * 1000)),
avatar: randomAvatar(),
role
}
localStorage.setItem(userKey, JSON.stringify(info))
}
return info
}
const userInfo = getUserInfo()
function showDetail(row) {
currentNews.value = row
commentForm.value = { user: userInfo.name, text: '', emoji: '' }
replyForm.value = { text: '', emoji: '' }
replyingIdx.value = -1
}
function submitComment() {
if (!commentForm.value.user || !commentForm.value.text) return
if (!currentNews.value.comments) currentNews.value.comments = []
currentNews.value.comments.unshift({
user: commentForm.value.user,
text: commentForm.value.text,
emoji: commentForm.value.emoji,
time: new Date().toLocaleString(),
avatar: userInfo.avatar,
likes: 0,
replyList: [],
role: userInfo.role
})
commentForm.value = { user: userInfo.name, text: '', emoji: '' }
localStorage.setItem(newsKey, JSON.stringify(news.value))
}
function startReply(idx, replyTo) {
replyingIdx.value = idx
replyForm.value = { text: '', emoji: '', replyTo }
}
function cancelReply() {
replyingIdx.value = -1
replyForm.value = { text: '', emoji: '' }
}
function submitReply(idx, replyTo) {
if (!replyForm.value.text) return
const reply = {
user: userInfo.name,
text: replyForm.value.text,
emoji: replyForm.value.emoji,
replyTo,
time: new Date().toLocaleString(),
avatar: userInfo.avatar,
role: userInfo.role
}
if (!currentNews.value.comments[idx].replyList) currentNews.value.comments[idx].replyList = []
currentNews.value.comments[idx].replyList.push(reply)
replyingIdx.value = -1
replyForm.value = { text: '', emoji: '' }
localStorage.setItem(newsKey, JSON.stringify(news.value))
}
function likeComment(idx) {
if (currentNews.value.comments && currentNews.value.comments[idx]) {
currentNews.value.comments[idx].likes = (currentNews.value.comments[idx].likes || 0) + 1
localStorage.setItem(newsKey, JSON.stringify(news.value))
}
}
function deleteComment(idx) {
const c = currentNews.value.comments[idx]
if (userInfo.role === 'admin' || c.user === userInfo.name) {
currentNews.value.comments.splice(idx, 1)
localStorage.setItem(newsKey, JSON.stringify(news.value))
}
}
function deleteReply(cidx, ridx) {
const r = currentNews.value.comments[cidx].replyList[ridx]
if (userInfo.role === 'admin' || r.user === userInfo.name) {
currentNews.value.comments[cidx].replyList.splice(ridx, 1)
localStorage.setItem(newsKey, JSON.stringify(news.value))
}
}
function insertEmoji(e, type) {
if (type === 'comment') {
commentForm.value.text += e
} else {
replyForm.value.text += e
}
}
function reportComment(idx) {
reportDialog.value.visible = true
reportDialog.value.idx = idx
reportDialog.value.form.reason = ''
}
function submitReport() {
reportDialog.value.visible = false
// 这里只做UI演示,可扩展为后端接口
window.$message?.success('举报已提交,感谢您的反馈!')
}
function toggleTop(idx) {
const c = currentNews.value.comments[idx]
c.isTop = !c.isTop
// 置顶评论优先显示
currentNews.value.comments.sort((a, b) => (b.isTop ? 1 : 0) - (a.isTop ? 1 : 0))
localStorage.setItem(newsKey, JSON.stringify(news.value))
}
watch([() => currentNews.value, sortType], () => { currentPage.value = 1 })
watch(news, n => {
localStorage.setItem(newsKey, JSON.stringify(n))
}, { deep: true })
</script>
\ No newline at end of file
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { ref, watchEffect } from 'vue'
const route = useRoute()
const router = useRouter()
const newsList = JSON.parse(localStorage.getItem('newsWithComments') || '[]')
const news = ref(null)
watchEffect(() => {
const idx = Number(route.params.id)
if (isNaN(idx) || idx < 0 || idx >= newsList.length) {
news.value = null
} else {
news.value = newsList[idx]
}
})
</script>
<template>
<el-card v-if="news">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<img :src="news.author?.avatar" style="width:36px;height:36px;border-radius:50%;" v-if="news.author" />
<div>
<b>{{ news.author?.name || '佚名' }}</b>
<div style="color:#888;font-size:13px;">{{ news.date }}</div>
</div>
</div>
<h2>{{ news.title }}</h2>
<img v-if="news.img" :src="news.img" style="width:100%;max-height:220px;object-fit:cover;border-radius:12px;margin-bottom:16px;" />
<div v-html="news.content"></div>
<video v-if="news.video" :src="news.video" controls style="width:100%;border-radius:12px;margin:12px 0;"></video>
<el-divider />
<h3>评论区</h3>
<el-timeline v-if="news.comments && news.comments.length" style="max-height:220px;overflow:auto;">
<el-timeline-item v-for="(c, idx) in news.comments" :key="idx" :timestamp="c.time" color="#67C23A">
<div style="display:flex;align-items:flex-start;gap:8px;">
<img :src="c.avatar" style="width:28px;height:28px;border-radius:50%;object-fit:cover;" />
<div style="flex:1;">
<b>{{ c.user }}</b>
<span v-if="c.emoji" style="margin-left:4px;">{{ c.emoji }}</span>
<span v-if="c.replyTo" style="color:#409EFF;margin-left:8px;">@{{ c.replyTo }}</span>
{{ c.text }}
</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无评论" />
<el-divider />
<h3>相关文章推荐</h3>
<el-row :gutter="16">
<el-col :span="12" v-for="(item, i) in newsList.filter((n, i2) => i2 !== Number(route.params.id)).slice(0, 2)" :key="i">
<el-card @click="$router.push(`/news/${newsList.indexOf(item)}`)" style="cursor:pointer;">
<img :src="item.img" style="width:100%;height:80px;object-fit:cover;border-radius:8px;" />
<div style="margin:8px 0 0 0;"><b>{{ item.title }}</b></div>
<div style="color:#888;font-size:13px;">{{ item.date }}</div>
</el-card>
</el-col>
</el-row>
<el-button @click="$router.back()" style="margin-top:16px;">返回</el-button>
</el-card>
<el-empty v-else description="未找到该新闻" />
</template>
\ No newline at end of file
<template>
<el-card class="detail-card">
<el-carousel height="240px" indicator-position="outside">
<el-carousel-item v-for="img in images" :key="img">
<img :src="img" class="carousel-img" />
</el-carousel-item>
</el-carousel>
<!-- 嵌入B站视频 -->
<div style="margin: 20px 0; border-radius: 12px; overflow: hidden;">
<iframe
src="https://player.bilibili.com/player.html?bvid=BV1xu411272w&autoplay=0"
scrolling="no"
border="0"
frameborder="no"
framespacing="0"
allowfullscreen="true"
style="width:100%;height:400px;"
></iframe>
</div>
<h2>{{ product.name }}</h2>
<p>{{ product.desc }}</p>
<p>{{ product.price }}</p>
</el-card>
</template>
<style scoped>
.detail-card {
border-radius: 20px;
box-shadow: 0 8px 32px #b0c4de80;
max-width: 800px;
margin: 40px auto;
padding: 32px 24px;
}
.carousel-img {
width: 100%;
height: 220px;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 2px 8px #e0e0e0;
}
</style>
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params.id
const product = ref({ name: '智能手环', desc: '健康监测', price: 299 })
const images = ['/assets/band1.jpg', '/assets/band2.jpg']
</script>
\ No newline at end of file
<template>
<div>
<el-row :gutter="20">
<ProductCard
v-for="item in products"
:key="item.id"
:product="item"
@click="goDetail(item.id)"
>
<template #footer>
<el-button type="primary" @click.stop="goDetail(item.id)">查看详情</el-button>
</template>
</ProductCard>
</el-row>
<el-card class="recommend-card">
<h3>智能设备推荐</h3>
<el-row :gutter="16">
<el-col :xs="24" :sm="12" :md="8" v-for="item in recommends" :key="item.id">
<el-card class="recommend-item" shadow="hover">
<img :src="item.img" style="width:100%;height:100px;object-fit:cover;border-radius:8px;">
<div style="margin:10px 0 0 0;">
<b>{{ item.name }}</b>
<p style="color:#888;font-size:13px;">{{ item.desc }}</p>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import ProductCard from '../components/ProductCard.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const products = [
{ id: 1, name: '智能手环', desc: '健康监测', price: 299, img: '/assets/band.jpg' },
{ id: 2, name: '智能床垫', desc: '睡眠监测', price: 999, img: '/assets/mat.jpg' },
{ id: 3, name: '健康手杖', desc: '定位报警', price: 199, img: '/assets/stick.jpg' },
{ id: 4, name: '智能药盒', desc: '定时提醒', price: 149, img: '/assets/box.jpg' },
]
const goDetail = id => router.push(`/products/${id}`)
const recommends = [
{ id: 101, name: '智能血压计', desc: '实时监测血压,异常提醒', img: '/assets/band1.jpg' },
{ id: 102, name: '跌倒报警器', desc: '自动检测跌倒并报警', img: '/assets/band2.jpg' },
{ id: 103, name: '智能体温计', desc: '精准测温,数据同步', img: '/assets/box.jpg' }
]
</script>
<style scoped>
.recommend-card {
margin: 32px auto;
max-width: 900px;
border-radius: 16px;
box-shadow: 0 4px 24px #e0e0e0;
padding: 24px 16px;
}
.recommend-item {
margin-bottom: 16px;
border-radius: 12px;
text-align: center;
}
</style>
\ No newline at end of file
<template>
<div class="form-bg">
<div class="form-center">
<el-card class="form-card">
<div class="form-logo">
<img src="/assets/elderly-home.png" alt="logo" />
<h2>智慧养老平台</h2>
</div>
<el-form :model="form" :rules="rules" ref="registerForm" label-width="80px">
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="确认密码" prop="confirm">
<el-input v-model="form.confirm" type="password" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" size="large" style="width:100%">注册</el-button>
</el-form-item>
</el-form>
<div class="form-switch">
已有账号?<el-link type="primary" @click="$router.push('/login')">去登录</el-link>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = ref({ phone: '', password: '', confirm: '' })
const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6位', trigger: 'blur' }
],
confirm: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{ validator: (rule, value) => value === form.value.password, message: '两次密码不一致', trigger: 'blur' }
]
}
const registerForm = ref(null)
const onSubmit = () => {
registerForm.value.validate(valid => {
if (valid) {
localStorage.setItem('isLogin', 'true')
alert('注册成功')
router.push('/')
}
})
}
</script>
<style scoped>
.form-bg {
min-height: 100vh;
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
display: flex;
align-items: center;
justify-content: center;
}
.form-center {
width: 100vw;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.form-card {
border-radius: 20px;
box-shadow: 0 8px 32px #fda08580;
padding: 32px 24px 16px 24px;
max-width: 400px;
width: 100%;
}
.form-logo {
text-align: center;
margin-bottom: 18px;
}
.form-logo img {
width: 56px;
height: 56px;
border-radius: 16px;
box-shadow: 0 2px 8px #e0e0e0;
margin-bottom: 8px;
}
.form-logo h2 {
font-size: 1.5rem;
color: #409EFF;
margin: 0;
font-weight: bold;
}
.form-switch {
text-align: center;
margin-top: 12px;
color: #888;
font-size: 15px;
}
</style>
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: true,
allowedHosts: true
},
plugins: [vue()]
port: 5173
}
})
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册