1641 lines
42 KiB
Markdown
1641 lines
42 KiB
Markdown
以下是 **「毛球手账」** 的完整技术实现方案,包含数据库 Schema、页面规划和云函数框架。
|
||
|
||
---
|
||
|
||
# 一、数据库 Schema(uniCloud DB Schema)
|
||
|
||
在 `uniCloud/database/` 目录下创建以下 `.schema.json` 文件。
|
||
|
||
## 1. users.schema.json
|
||
|
||
```json
|
||
{
|
||
"bsonType": "object",
|
||
"required": ["wx_openid", "createdAt"],
|
||
"permission": {
|
||
"read": "doc._id == auth.uid",
|
||
"create": "auth.uid != null",
|
||
"update": "doc._id == auth.uid",
|
||
"delete": false
|
||
},
|
||
"properties": {
|
||
"_id": {
|
||
"description": "用户ID,系统自动生成",
|
||
"bsonType": "string"
|
||
},
|
||
"wx_openid": {
|
||
"bsonType": "string",
|
||
"description": "微信openid",
|
||
"trim": "both"
|
||
},
|
||
"wx_unionid": {
|
||
"bsonType": "string",
|
||
"description": "微信unionid"
|
||
},
|
||
"nickName": {
|
||
"bsonType": "string",
|
||
"description": "微信昵称",
|
||
"trim": "both"
|
||
},
|
||
"avatarUrl": {
|
||
"bsonType": "string",
|
||
"description": "微信头像URL"
|
||
},
|
||
"phone": {
|
||
"bsonType": "string",
|
||
"description": "绑定手机号"
|
||
},
|
||
"membership": {
|
||
"bsonType": "object",
|
||
"properties": {
|
||
"type": {
|
||
"bsonType": "string",
|
||
"enum": ["free", "premium"],
|
||
"default": "free"
|
||
},
|
||
"expireAt": {
|
||
"bsonType": "timestamp",
|
||
"description": "会员过期时间"
|
||
}
|
||
}
|
||
},
|
||
"settings": {
|
||
"bsonType": "object",
|
||
"properties": {
|
||
"defaultPetId": {
|
||
"bsonType": "string",
|
||
"description": "默认选中宠物ID"
|
||
},
|
||
"reminderEnabled": {
|
||
"bsonType": "bool",
|
||
"default": true
|
||
}
|
||
}
|
||
},
|
||
"createdAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
},
|
||
"updatedAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 2. pets.schema.json
|
||
|
||
```json
|
||
{
|
||
"bsonType": "object",
|
||
"required": ["user_id", "name", "type", "createdAt"],
|
||
"permission": {
|
||
"read": "doc.user_id == auth.uid",
|
||
"create": "auth.uid != null",
|
||
"update": "doc.user_id == auth.uid",
|
||
"delete": "doc.user_id == auth.uid"
|
||
},
|
||
"properties": {
|
||
"_id": {
|
||
"bsonType": "string"
|
||
},
|
||
"user_id": {
|
||
"bsonType": "string",
|
||
"description": "关联用户ID",
|
||
"foreignKey": "users._id"
|
||
},
|
||
"name": {
|
||
"bsonType": "string",
|
||
"description": "宠物昵称",
|
||
"minLength": 1,
|
||
"maxLength": 10,
|
||
"trim": "both"
|
||
},
|
||
"type": {
|
||
"bsonType": "string",
|
||
"enum": ["cat", "dog", "other"],
|
||
"description": "宠物类型"
|
||
},
|
||
"breed": {
|
||
"bsonType": "string",
|
||
"description": "品种"
|
||
},
|
||
"gender": {
|
||
"bsonType": "string",
|
||
"enum": ["male", "female", "unknown"],
|
||
"default": "unknown"
|
||
},
|
||
"birthday": {
|
||
"bsonType": "timestamp",
|
||
"description": "出生日期"
|
||
},
|
||
"isBirthdayEstimated": {
|
||
"bsonType": "bool",
|
||
"default": false,
|
||
"description": "生日是否为估算"
|
||
},
|
||
"color": {
|
||
"bsonType": "string",
|
||
"description": "毛色/特征"
|
||
},
|
||
"avatar": {
|
||
"bsonType": "string",
|
||
"description": "宠物头像云存储URL"
|
||
},
|
||
"weight": {
|
||
"bsonType": "array",
|
||
"description": "体重历史记录",
|
||
"items": {
|
||
"bsonType": "object",
|
||
"properties": {
|
||
"value": {
|
||
"bsonType": "double",
|
||
"description": "体重kg"
|
||
},
|
||
"date": {
|
||
"bsonType": "timestamp"
|
||
},
|
||
"note": {
|
||
"bsonType": "string"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"neutered": {
|
||
"bsonType": "bool",
|
||
"default": false
|
||
},
|
||
"allergies": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "string"
|
||
}
|
||
},
|
||
"chronicDiseases": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "string"
|
||
}
|
||
},
|
||
"emergencyContact": {
|
||
"bsonType": "object",
|
||
"properties": {
|
||
"name": {
|
||
"bsonType": "string"
|
||
},
|
||
"phone": {
|
||
"bsonType": "string"
|
||
}
|
||
}
|
||
},
|
||
"status": {
|
||
"bsonType": "string",
|
||
"enum": ["active", "archived"],
|
||
"default": "active"
|
||
},
|
||
"createdAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
},
|
||
"updatedAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 3. health_records.schema.json
|
||
|
||
```json
|
||
{
|
||
"bsonType": "object",
|
||
"required": ["pet_id", "user_id", "type", "date", "createdAt"],
|
||
"permission": {
|
||
"read": "doc.user_id == auth.uid",
|
||
"create": "auth.uid != null",
|
||
"update": "doc.user_id == auth.uid",
|
||
"delete": "doc.user_id == auth.uid"
|
||
},
|
||
"properties": {
|
||
"_id": {
|
||
"bsonType": "string"
|
||
},
|
||
"pet_id": {
|
||
"bsonType": "string",
|
||
"foreignKey": "pets._id"
|
||
},
|
||
"user_id": {
|
||
"bsonType": "string",
|
||
"foreignKey": "users._id"
|
||
},
|
||
"type": {
|
||
"bsonType": "string",
|
||
"enum": ["vaccine", "deworm", "medical", "checkup", "other"],
|
||
"description": "记录类型:疫苗/驱虫/病历/体检/其他"
|
||
},
|
||
"title": {
|
||
"bsonType": "string",
|
||
"description": "记录标题"
|
||
},
|
||
"vaccineName": {
|
||
"bsonType": "string",
|
||
"description": "疫苗名称"
|
||
},
|
||
"batchNumber": {
|
||
"bsonType": "string",
|
||
"description": "疫苗批次号"
|
||
},
|
||
"dewormType": {
|
||
"bsonType": "string",
|
||
"enum": ["internal", "external", "both"],
|
||
"description": "驱虫类型"
|
||
},
|
||
"brand": {
|
||
"bsonType": "string",
|
||
"description": "品牌(驱虫药/疫苗品牌)"
|
||
},
|
||
"hospital": {
|
||
"bsonType": "string",
|
||
"description": "医院名称"
|
||
},
|
||
"doctor": {
|
||
"bsonType": "string"
|
||
},
|
||
"date": {
|
||
"bsonType": "timestamp",
|
||
"description": "发生日期"
|
||
},
|
||
"nextDate": {
|
||
"bsonType": "timestamp",
|
||
"description": "下次提醒日期"
|
||
},
|
||
"description": {
|
||
"bsonType": "string",
|
||
"description": "描述/主诉"
|
||
},
|
||
"diagnosis": {
|
||
"bsonType": "string",
|
||
"description": "诊断结果"
|
||
},
|
||
"medications": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "object",
|
||
"properties": {
|
||
"name": {
|
||
"bsonType": "string"
|
||
},
|
||
"dosage": {
|
||
"bsonType": "string"
|
||
},
|
||
"frequency": {
|
||
"bsonType": "string"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"images": {
|
||
"bsonType": "array",
|
||
"description": "云存储图片URL数组",
|
||
"items": {
|
||
"bsonType": "string"
|
||
}
|
||
},
|
||
"cost": {
|
||
"bsonType": "double",
|
||
"description": "费用"
|
||
},
|
||
"tags": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "string"
|
||
}
|
||
},
|
||
"createdAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
},
|
||
"updatedAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 4. schedules.schema.json
|
||
|
||
```json
|
||
{
|
||
"bsonType": "object",
|
||
"required": ["pet_id", "user_id", "title", "scheduledDate", "createdAt"],
|
||
"permission": {
|
||
"read": "doc.user_id == auth.uid",
|
||
"create": "auth.uid != null",
|
||
"update": "doc.user_id == auth.uid",
|
||
"delete": "doc.user_id == auth.uid"
|
||
},
|
||
"properties": {
|
||
"_id": {
|
||
"bsonType": "string"
|
||
},
|
||
"pet_id": {
|
||
"bsonType": "string",
|
||
"foreignKey": "pets._id"
|
||
},
|
||
"user_id": {
|
||
"bsonType": "string",
|
||
"foreignKey": "users._id"
|
||
},
|
||
"type": {
|
||
"bsonType": "string",
|
||
"enum": ["vaccine", "deworm", "checkup", "custom"],
|
||
"description": "日程类型"
|
||
},
|
||
"title": {
|
||
"bsonType": "string",
|
||
"description": "提醒标题"
|
||
},
|
||
"scheduledDate": {
|
||
"bsonType": "timestamp",
|
||
"description": "计划日期"
|
||
},
|
||
"frequency": {
|
||
"bsonType": "string",
|
||
"enum": ["once", "daily", "weekly", "monthly", "yearly"],
|
||
"default": "once"
|
||
},
|
||
"interval": {
|
||
"bsonType": "int",
|
||
"default": 1,
|
||
"description": "每N个frequency"
|
||
},
|
||
"status": {
|
||
"bsonType": "string",
|
||
"enum": ["pending", "completed", "overdue", "skipped"],
|
||
"default": "pending"
|
||
},
|
||
"reminderDays": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "int"
|
||
},
|
||
"default": [7, 3, 1],
|
||
"description": "提前提醒天数"
|
||
},
|
||
"relatedRecordId": {
|
||
"bsonType": "string",
|
||
"foreignKey": "health_records._id",
|
||
"description": "关联的健康记录"
|
||
},
|
||
"note": {
|
||
"bsonType": "string"
|
||
},
|
||
"lastRemindedAt": {
|
||
"bsonType": "timestamp",
|
||
"description": "上次提醒时间"
|
||
},
|
||
"createdAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
},
|
||
"updatedAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. hospitals.schema.json
|
||
|
||
```json
|
||
{
|
||
"bsonType": "object",
|
||
"required": ["name", "location", "createdAt"],
|
||
"permission": {
|
||
"read": true,
|
||
"create": "auth.uid != null",
|
||
"update": false,
|
||
"delete": false
|
||
},
|
||
"properties": {
|
||
"_id": {
|
||
"bsonType": "string"
|
||
},
|
||
"name": {
|
||
"bsonType": "string",
|
||
"description": "医院名称"
|
||
},
|
||
"address": {
|
||
"bsonType": "string"
|
||
},
|
||
"location": {
|
||
"bsonType": "object",
|
||
"description": "GeoJSON Point",
|
||
"properties": {
|
||
"type": {
|
||
"bsonType": "string",
|
||
"enum": ["Point"],
|
||
"default": "Point"
|
||
},
|
||
"coordinates": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "double"
|
||
},
|
||
"description": "[longitude, latitude]"
|
||
}
|
||
}
|
||
},
|
||
"phone": {
|
||
"bsonType": "string"
|
||
},
|
||
"hours": {
|
||
"bsonType": "string",
|
||
"description": "营业时间"
|
||
},
|
||
"is24Hour": {
|
||
"bsonType": "bool",
|
||
"default": false
|
||
},
|
||
"tags": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "string"
|
||
},
|
||
"description": "特色标签"
|
||
},
|
||
"rating": {
|
||
"bsonType": "double",
|
||
"minimum": 0,
|
||
"maximum": 5
|
||
},
|
||
"reviewCount": {
|
||
"bsonType": "int",
|
||
"default": 0
|
||
},
|
||
"verified": {
|
||
"bsonType": "bool",
|
||
"default": false,
|
||
"description": "是否官方认证"
|
||
},
|
||
"status": {
|
||
"bsonType": "string",
|
||
"enum": ["active", "pending", "rejected"],
|
||
"default": "pending"
|
||
},
|
||
"createdAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. articles.schema.json(养宠百科)
|
||
|
||
```json
|
||
{
|
||
"bsonType": "object",
|
||
"required": ["title", "category", "status", "createdAt"],
|
||
"permission": {
|
||
"read": true,
|
||
"create": false,
|
||
"update": false,
|
||
"delete": false
|
||
},
|
||
"properties": {
|
||
"_id": {
|
||
"bsonType": "string"
|
||
},
|
||
"title": {
|
||
"bsonType": "string"
|
||
},
|
||
"category": {
|
||
"bsonType": "string",
|
||
"enum": ["vaccine", "deworm", "disease", "nutrition", "behavior", "other"]
|
||
},
|
||
"petType": {
|
||
"bsonType": "string",
|
||
"enum": ["cat", "dog", "all"],
|
||
"default": "all"
|
||
},
|
||
"cover": {
|
||
"bsonType": "string"
|
||
},
|
||
"content": {
|
||
"bsonType": "string",
|
||
"description": "富文本内容"
|
||
},
|
||
"summary": {
|
||
"bsonType": "string"
|
||
},
|
||
"tags": {
|
||
"bsonType": "array",
|
||
"items": {
|
||
"bsonType": "string"
|
||
}
|
||
},
|
||
"viewCount": {
|
||
"bsonType": "int",
|
||
"default": 0
|
||
},
|
||
"status": {
|
||
"bsonType": "string",
|
||
"enum": ["published", "draft"],
|
||
"default": "draft"
|
||
},
|
||
"createdAt": {
|
||
"bsonType": "timestamp",
|
||
"defaultValue": {
|
||
"$env": "now"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 二、核心页面前端组件结构(uni-app)
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
uni-app-project/
|
||
├── pages/
|
||
│ ├── index/
|
||
│ │ └── index.vue # 首页(Dashboard)
|
||
│ ├── pet/
|
||
│ │ ├── list.vue # 宠物列表/切换
|
||
│ │ ├── detail.vue # 宠物档案详情
|
||
│ │ ├── edit.vue # 创建/编辑宠物
|
||
│ │ └── weight.vue # 体重记录
|
||
│ ├── record/
|
||
│ │ ├── list.vue # 健康记录列表(时间轴)
|
||
│ │ ├── detail.vue # 记录详情
|
||
│ │ ├── edit.vue # 创建/编辑记录
|
||
│ │ └── type-select.vue # 记录类型选择
|
||
│ ├── schedule/
|
||
│ │ ├── list.vue # 日程列表
|
||
│ │ └── edit.vue # 创建/编辑提醒
|
||
│ ├── discovery/
|
||
│ │ ├── index.vue # 发现页(医院+百科)
|
||
│ │ ├── hospital-list.vue # 附近医院列表
|
||
│ │ ├── hospital-detail.vue # 医院详情
|
||
│ │ ├── article-list.vue # 百科列表
|
||
│ │ └── article-detail.vue # 百科详情
|
||
│ └── mine/
|
||
│ ├── index.vue # 我的页面
|
||
│ ├── settings.vue # 设置
|
||
│ └── member.vue # 会员中心
|
||
├── components/
|
||
│ ├── pet-selector.vue # 顶部宠物切换器
|
||
│ ├── schedule-card.vue # 日程卡片
|
||
│ ├── record-timeline.vue # 时间轴组件
|
||
│ ├── weight-chart.vue # 体重曲线图(echarts/uni-chart)
|
||
│ ├── quick-action.vue # 底部快速操作栏
|
||
│ ├── empty-state.vue # 空状态插画
|
||
│ └── share-card.vue # 分享卡片生成
|
||
├── static/
|
||
│ └── images/ # 静态资源
|
||
├── uniCloud/
|
||
│ └── cloudfunctions/ # 云函数
|
||
└── App.vue
|
||
```
|
||
|
||
## 关键页面组件详解
|
||
|
||
### 1. 首页 `pages/index/index.vue`
|
||
|
||
```vue
|
||
<template>
|
||
<view class="container">
|
||
<!-- 顶部欢迎语 -->
|
||
<view class="header">
|
||
<text class="greeting">{{ greeting }},铲屎官</text>
|
||
<image class="avatar" :src="userInfo.avatarUrl" />
|
||
</view>
|
||
|
||
<!-- 宠物切换器 -->
|
||
<pet-selector
|
||
:pets="petList"
|
||
:current="currentPetId"
|
||
@change="onPetChange"
|
||
@add="goToAddPet"
|
||
/>
|
||
|
||
<!-- 今日日程看板 -->
|
||
<view class="section">
|
||
<view class="section-title">
|
||
<text>📅 今日日程</text>
|
||
<text class="more" @click="goToSchedule">查看全部</text>
|
||
</view>
|
||
|
||
<view v-if="todaySchedules.length > 0">
|
||
<schedule-card
|
||
v-for="item in todaySchedules"
|
||
:key="item._id"
|
||
:data="item"
|
||
:pet="getPetById(item.pet_id)"
|
||
@complete="onComplete(item)"
|
||
@postpone="onPostpone(item)"
|
||
/>
|
||
</view>
|
||
<empty-state v-else text="今天可以安心撸猫啦~" />
|
||
</view>
|
||
|
||
<!-- 宠物健康概览 -->
|
||
<view class="section" v-if="currentPet">
|
||
<view class="section-title">
|
||
<text>📊 {{ currentPet.name }} 健康概览</text>
|
||
</view>
|
||
<view class="overview-grid">
|
||
<view class="overview-item" @click="goToWeight">
|
||
<text class="value">{{ latestWeight }}kg</text>
|
||
<text class="label">最新体重</text>
|
||
</view>
|
||
<view class="overview-item" @click="goToRecords('vaccine')">
|
||
<text class="value">{{ nextVaccineDays }}天</text>
|
||
<text class="label">下次疫苗</text>
|
||
</view>
|
||
<view class="overview-item" @click="goToRecords('deworm')">
|
||
<text class="value">{{ nextDewormDays }}天</text>
|
||
<text class="label">下次驱虫</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 快速操作 -->
|
||
<quick-action @action="onQuickAction" />
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
userInfo: {},
|
||
petList: [],
|
||
currentPetId: '',
|
||
todaySchedules: [],
|
||
latestWeight: 0,
|
||
nextVaccineDays: '--',
|
||
nextDewormDays: '--'
|
||
}
|
||
},
|
||
computed: {
|
||
currentPet() {
|
||
return this.petList.find(p => p._id === this.currentPetId)
|
||
},
|
||
greeting() {
|
||
const hour = new Date().getHours()
|
||
if (hour < 12) return '早上好'
|
||
if (hour < 18) return '下午好'
|
||
return '晚上好'
|
||
}
|
||
},
|
||
onShow() {
|
||
this.loadData()
|
||
},
|
||
methods: {
|
||
async loadData() {
|
||
// 并行加载数据
|
||
const [petsRes, scheduleRes] = await Promise.all([
|
||
uniCloud.callFunction({ name: 'petCrud', data: { action: 'list' } }),
|
||
uniCloud.callFunction({ name: 'scheduleManager', data: { action: 'today' } })
|
||
])
|
||
this.petList = petsRes.result.data
|
||
this.todaySchedules = scheduleRes.result.data
|
||
if (this.petList.length > 0 && !this.currentPetId) {
|
||
this.currentPetId = this.petList[0]._id
|
||
}
|
||
this.loadPetStats()
|
||
},
|
||
async loadPetStats() {
|
||
if (!this.currentPetId) return
|
||
// 加载体重、疫苗、驱虫统计
|
||
const res = await uniCloud.callFunction({
|
||
name: 'petCrud',
|
||
data: { action: 'stats', petId: this.currentPetId }
|
||
})
|
||
const stats = res.result.data
|
||
this.latestWeight = stats.latestWeight || 0
|
||
this.nextVaccineDays = stats.nextVaccineDays || '--'
|
||
this.nextDewormDays = stats.nextDewormDays || '--'
|
||
},
|
||
onPetChange(petId) {
|
||
this.currentPetId = petId
|
||
this.loadPetStats()
|
||
uni.setStorageSync('currentPetId', petId)
|
||
},
|
||
goToAddPet() {
|
||
uni.navigateTo({ url: '/pages/pet/edit' })
|
||
},
|
||
goToSchedule() {
|
||
uni.navigateTo({ url: '/pages/schedule/list' })
|
||
},
|
||
goToWeight() {
|
||
uni.navigateTo({ url: `/pages/pet/weight?petId=${this.currentPetId}` })
|
||
},
|
||
goToRecords(type) {
|
||
uni.navigateTo({ url: `/pages/record/list?petId=${this.currentPetId}&type=${type}` })
|
||
},
|
||
onQuickAction(action) {
|
||
const urls = {
|
||
record: `/pages/record/type-select?petId=${this.currentPetId}`,
|
||
schedule: `/pages/schedule/edit?petId=${this.currentPetId}`,
|
||
hospital: '/pages/discovery/hospital-list'
|
||
}
|
||
uni.navigateTo({ url: urls[action] })
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 2. 宠物切换器组件 `components/pet-selector.vue`
|
||
|
||
```vue
|
||
<template>
|
||
<scroll-view class="pet-selector" scroll-x>
|
||
<view
|
||
v-for="pet in pets"
|
||
:key="pet._id"
|
||
class="pet-item"
|
||
:class="{ active: current === pet._id }"
|
||
@click="$emit('change', pet._id)"
|
||
>
|
||
<image class="pet-avatar" :src="pet.avatar || '/static/default-pet.png'" />
|
||
<text class="pet-name">{{ pet.name }}</text>
|
||
<text class="pet-age">{{ calcAge(pet.birthday) }}</text>
|
||
</view>
|
||
<view class="pet-item add" @click="$emit('add')">
|
||
<view class="add-icon">+</view>
|
||
<text class="pet-name">添加</text>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
props: {
|
||
pets: { type: Array, default: () => [] },
|
||
current: String
|
||
},
|
||
methods: {
|
||
calcAge(birthday) {
|
||
if (!birthday) return '未知'
|
||
// 计算年龄逻辑
|
||
const birth = new Date(birthday)
|
||
const now = new Date()
|
||
const years = now.getFullYear() - birth.getFullYear()
|
||
const months = now.getMonth() - birth.getMonth()
|
||
if (years > 0) return `${years}岁`
|
||
return `${Math.max(0, months)}个月`
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 3. 时间轴记录列表 `pages/record/list.vue`
|
||
|
||
```vue
|
||
<template>
|
||
<view class="container">
|
||
<view class="filter-bar">
|
||
<text
|
||
v-for="tab in tabs"
|
||
:key="tab.value"
|
||
:class="{ active: currentType === tab.value }"
|
||
@click="currentType = tab.value"
|
||
>
|
||
{{ tab.label }}
|
||
</text>
|
||
</view>
|
||
|
||
<view class="timeline">
|
||
<view
|
||
v-for="(group, date) in groupedRecords"
|
||
:key="date"
|
||
class="timeline-group"
|
||
>
|
||
<view class="timeline-date">{{ date }}</view>
|
||
<view
|
||
v-for="record in group"
|
||
:key="record._id"
|
||
class="timeline-item"
|
||
@click="goToDetail(record._id)"
|
||
>
|
||
<view class="timeline-dot" :class="record.type"></view>
|
||
<view class="timeline-content">
|
||
<view class="timeline-header">
|
||
<text class="title">{{ recordTitle(record) }}</text>
|
||
<text class="pet-tag">{{ getPetName(record.pet_id) }}</text>
|
||
</view>
|
||
<text class="desc">{{ record.description || record.hospital }}</text>
|
||
<view class="timeline-footer" v-if="record.nextDate">
|
||
<text class="next">下次: {{ formatDate(record.nextDate) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<uni-fab
|
||
horizontal="right"
|
||
vertical="bottom"
|
||
@trigger="onFabTrigger"
|
||
:content="fabContent"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
petId: '',
|
||
currentType: 'all',
|
||
tabs: [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '疫苗', value: 'vaccine' },
|
||
{ label: '驱虫', value: 'deworm' },
|
||
{ label: '病历', value: 'medical' }
|
||
],
|
||
records: [],
|
||
petMap: {}
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.petId = options.petId || ''
|
||
this.loadRecords()
|
||
},
|
||
computed: {
|
||
groupedRecords() {
|
||
// 按日期分组
|
||
const groups = {}
|
||
this.filteredRecords.forEach(r => {
|
||
const date = this.formatDate(r.date, 'YYYY-MM-DD')
|
||
if (!groups[date]) groups[date] = []
|
||
groups[date].push(r)
|
||
})
|
||
return groups
|
||
},
|
||
filteredRecords() {
|
||
if (this.currentType === 'all') return this.records
|
||
return this.records.filter(r => r.type === this.currentType)
|
||
},
|
||
fabContent() {
|
||
return [
|
||
{ text: '疫苗', iconPath: '/static/vaccine.png' },
|
||
{ text: '驱虫', iconPath: '/static/deworm.png' },
|
||
{ text: '病历', iconPath: '/static/medical.png' }
|
||
]
|
||
}
|
||
},
|
||
methods: {
|
||
async loadRecords() {
|
||
const res = await uniCloud.callFunction({
|
||
name: 'recordCrud',
|
||
data: {
|
||
action: 'list',
|
||
petId: this.petId,
|
||
type: this.currentType === 'all' ? '' : this.currentType
|
||
}
|
||
})
|
||
this.records = res.result.data
|
||
// 构建宠物名称映射
|
||
this.petMap = {}
|
||
this.records.forEach(r => {
|
||
if (!this.petMap[r.pet_id]) {
|
||
this.petMap[r.pet_id] = r.petName || '未知'
|
||
}
|
||
})
|
||
},
|
||
recordTitle(record) {
|
||
const titles = {
|
||
vaccine: record.vaccineName || '疫苗接种',
|
||
deworm: `${record.brand || ''} 驱虫`,
|
||
medical: record.diagnosis || '就诊记录',
|
||
checkup: '体检'
|
||
}
|
||
return titles[record.type] || record.title
|
||
},
|
||
getPetName(petId) {
|
||
return this.petMap[petId] || ''
|
||
},
|
||
goToDetail(id) {
|
||
uni.navigateTo({ url: `/pages/record/detail?id=${id}` })
|
||
},
|
||
onFabTrigger(e) {
|
||
const types = ['vaccine', 'deworm', 'medical']
|
||
uni.navigateTo({
|
||
url: `/pages/record/edit?type=${types[e.index]}&petId=${this.petId}`
|
||
})
|
||
},
|
||
formatDate(date, fmt = 'YYYY-MM-DD') {
|
||
// 日期格式化工具
|
||
const d = new Date(date)
|
||
const o = {
|
||
'Y+': d.getFullYear(),
|
||
'M+': d.getMonth() + 1,
|
||
'D+': d.getDate()
|
||
}
|
||
let str = fmt
|
||
for (let k in o) {
|
||
str = str.replace(new RegExp(k), match => {
|
||
const v = o[k]
|
||
return match.length > 1 ? (v < 10 ? '0' + v : v) : v
|
||
})
|
||
}
|
||
return str
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
# 三、云函数代码框架
|
||
|
||
在 `uniCloud/cloudfunctions/` 目录下创建以下云函数。
|
||
|
||
## 1. login(微信登录)
|
||
|
||
```javascript
|
||
// uniCloud/cloudfunctions/login/index.js
|
||
const db = uniCloud.database()
|
||
|
||
exports.main = async (event, context) => {
|
||
const { code, userInfo } = event
|
||
|
||
try {
|
||
// 1. 获取openid
|
||
const loginRes = await uniCloud.getWXContext()
|
||
const { OPENID, UNIONID } = loginRes
|
||
|
||
if (!OPENID) {
|
||
return { code: -1, message: '获取openid失败' }
|
||
}
|
||
|
||
// 2. 查询或创建用户
|
||
const userCollection = db.collection('users')
|
||
let user = await userCollection.where({ wx_openid: OPENID }).get()
|
||
|
||
let userId
|
||
if (user.data.length === 0) {
|
||
// 新用户
|
||
const createRes = await userCollection.add({
|
||
wx_openid: OPENID,
|
||
wx_unionid: UNIONID || '',
|
||
nickName: userInfo?.nickName || '',
|
||
avatarUrl: userInfo?.avatarUrl || '',
|
||
phone: '',
|
||
membership: {
|
||
type: 'free',
|
||
expireAt: null
|
||
},
|
||
settings: {
|
||
reminderEnabled: true
|
||
},
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now()
|
||
})
|
||
userId = createRes.id
|
||
} else {
|
||
// 更新用户信息
|
||
userId = user.data[0]._id
|
||
await userCollection.doc(userId).update({
|
||
nickName: userInfo?.nickName || user.data[0].nickName,
|
||
avatarUrl: userInfo?.avatarUrl || user.data[0].avatarUrl,
|
||
updatedAt: Date.now()
|
||
})
|
||
}
|
||
|
||
// 3. 生成自定义登录token(可选,用于后续请求鉴权)
|
||
const token = await uniCloud.createToken({
|
||
uid: userId
|
||
})
|
||
|
||
return {
|
||
code: 0,
|
||
message: '登录成功',
|
||
data: {
|
||
uid: userId,
|
||
token,
|
||
isNewUser: user.data.length === 0
|
||
}
|
||
}
|
||
} catch (err) {
|
||
return { code: -1, message: err.message }
|
||
}
|
||
}
|
||
```
|
||
|
||
## 2. petCrud(宠物增删改查)
|
||
|
||
```javascript
|
||
// uniCloud/cloudfunctions/petCrud/index.js
|
||
const db = uniCloud.database()
|
||
const $ = db.command.aggregate
|
||
|
||
exports.main = async (event, context) => {
|
||
const { action, data, petId } = event
|
||
const { uid } = context // 通过uni-id获取当前用户ID
|
||
|
||
const petCollection = db.collection('pets')
|
||
|
||
try {
|
||
switch (action) {
|
||
case 'create': {
|
||
const res = await petCollection.add({
|
||
...data,
|
||
user_id: uid,
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now()
|
||
})
|
||
return { code: 0, data: { id: res.id } }
|
||
}
|
||
|
||
case 'list': {
|
||
const res = await petCollection.where({
|
||
user_id: uid,
|
||
status: 'active'
|
||
}).orderBy('createdAt', 'desc').get()
|
||
return { code: 0, data: res.data }
|
||
}
|
||
|
||
case 'detail': {
|
||
const res = await petCollection.doc(petId).get()
|
||
if (res.data[0].user_id !== uid) {
|
||
return { code: 403, message: '无权限' }
|
||
}
|
||
return { code: 0, data: res.data[0] }
|
||
}
|
||
|
||
case 'update': {
|
||
const pet = await petCollection.doc(petId).get()
|
||
if (pet.data[0].user_id !== uid) {
|
||
return { code: 403, message: '无权限' }
|
||
}
|
||
await petCollection.doc(petId).update({
|
||
...data,
|
||
updatedAt: Date.now()
|
||
})
|
||
return { code: 0, message: '更新成功' }
|
||
}
|
||
|
||
case 'delete': {
|
||
// 软删除
|
||
await petCollection.doc(petId).update({
|
||
status: 'archived',
|
||
updatedAt: Date.now()
|
||
})
|
||
return { code: 0, message: '删除成功' }
|
||
}
|
||
|
||
case 'stats': {
|
||
// 获取宠物统计信息
|
||
const pet = await petCollection.doc(petId).get()
|
||
const petData = pet.data[0]
|
||
|
||
// 最新体重
|
||
const latestWeight = petData.weight?.length > 0
|
||
? petData.weight[petData.weight.length - 1].value
|
||
: 0
|
||
|
||
// 查询下次疫苗和驱虫(从schedules表)
|
||
const scheduleCollection = db.collection('schedules')
|
||
const now = Date.now()
|
||
|
||
const nextVaccine = await scheduleCollection.where({
|
||
pet_id: petId,
|
||
user_id: uid,
|
||
type: 'vaccine',
|
||
status: 'pending',
|
||
scheduledDate: db.command.gte(now)
|
||
}).orderBy('scheduledDate', 'asc').limit(1).get()
|
||
|
||
const nextDeworm = await scheduleCollection.where({
|
||
pet_id: petId,
|
||
user_id: uid,
|
||
type: 'deworm',
|
||
status: 'pending',
|
||
scheduledDate: db.command.gte(now)
|
||
}).orderBy('scheduledDate', 'asc').limit(1).get()
|
||
|
||
const calcDays = (date) => {
|
||
if (!date) return '--'
|
||
const days = Math.ceil((date - now) / (1000 * 60 * 60 * 24))
|
||
return days
|
||
}
|
||
|
||
return {
|
||
code: 0,
|
||
data: {
|
||
latestWeight,
|
||
nextVaccineDays: calcDays(nextVaccine.data[0]?.scheduledDate),
|
||
nextDewormDays: calcDays(nextDeworm.data[0]?.scheduledDate)
|
||
}
|
||
}
|
||
}
|
||
|
||
default:
|
||
return { code: -1, message: '未知操作' }
|
||
}
|
||
} catch (err) {
|
||
return { code: -1, message: err.message }
|
||
}
|
||
}
|
||
```
|
||
|
||
## 3. recordCrud(健康记录增删改查)
|
||
|
||
```javascript
|
||
// uniCloud/cloudfunctions/recordCrud/index.js
|
||
const db = uniCloud.database()
|
||
|
||
exports.main = async (event, context) => {
|
||
const { action, data, recordId, petId, type } = event
|
||
const { uid } = context
|
||
|
||
const recordCollection = db.collection('health_records')
|
||
const scheduleCollection = db.collection('schedules')
|
||
|
||
try {
|
||
switch (action) {
|
||
case 'create': {
|
||
const recordData = {
|
||
...data,
|
||
user_id: uid,
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now()
|
||
}
|
||
|
||
const res = await recordCollection.add(recordData)
|
||
|
||
// 如果有nextDate,自动创建日程提醒
|
||
if (data.nextDate) {
|
||
await scheduleCollection.add({
|
||
pet_id: data.pet_id,
|
||
user_id: uid,
|
||
type: data.type,
|
||
title: this.getScheduleTitle(data),
|
||
scheduledDate: data.nextDate,
|
||
status: 'pending',
|
||
relatedRecordId: res.id,
|
||
createdAt: Date.now()
|
||
})
|
||
}
|
||
|
||
return { code: 0, data: { id: res.id } }
|
||
}
|
||
|
||
case 'list': {
|
||
let where = { user_id: uid }
|
||
if (petId) where.pet_id = petId
|
||
if (type) where.type = type
|
||
|
||
const res = await recordCollection.where(where)
|
||
.orderBy('date', 'desc')
|
||
.get()
|
||
|
||
// 关联宠物名称
|
||
const petIds = [...new Set(res.data.map(r => r.pet_id))]
|
||
const petCollection = db.collection('pets')
|
||
const pets = await petCollection.where({
|
||
_id: db.command.in(petIds)
|
||
}).get()
|
||
const petMap = {}
|
||
pets.data.forEach(p => petMap[p._id] = p.name)
|
||
|
||
const records = res.data.map(r => ({
|
||
...r,
|
||
petName: petMap[r.pet_id] || '未知'
|
||
}))
|
||
|
||
return { code: 0, data: records }
|
||
}
|
||
|
||
case 'detail': {
|
||
const res = await recordCollection.doc(recordId).get()
|
||
if (res.data[0].user_id !== uid) {
|
||
return { code: 403, message: '无权限' }
|
||
}
|
||
return { code: 0, data: res.data[0] }
|
||
}
|
||
|
||
case 'update': {
|
||
const record = await recordCollection.doc(recordId).get()
|
||
if (record.data[0].user_id !== uid) {
|
||
return { code: 403, message: '无权限' }
|
||
}
|
||
|
||
await recordCollection.doc(recordId).update({
|
||
...data,
|
||
updatedAt: Date.now()
|
||
})
|
||
|
||
// 同步更新关联的schedule
|
||
if (data.nextDate && record.data[0].relatedRecordId) {
|
||
await scheduleCollection.where({
|
||
relatedRecordId: recordId
|
||
}).update({
|
||
scheduledDate: data.nextDate,
|
||
updatedAt: Date.now()
|
||
})
|
||
}
|
||
|
||
return { code: 0, message: '更新成功' }
|
||
}
|
||
|
||
case 'delete': {
|
||
const record = await recordCollection.doc(recordId).get()
|
||
if (record.data[0].user_id !== uid) {
|
||
return { code: 403, message: '无权限' }
|
||
}
|
||
|
||
await recordCollection.doc(recordId).remove()
|
||
|
||
// 删除关联schedule
|
||
if (record.data[0].relatedRecordId) {
|
||
await scheduleCollection.where({
|
||
relatedRecordId: recordId
|
||
}).remove()
|
||
}
|
||
|
||
return { code: 0, message: '删除成功' }
|
||
}
|
||
|
||
default:
|
||
return { code: -1, message: '未知操作' }
|
||
}
|
||
} catch (err) {
|
||
return { code: -1, message: err.message }
|
||
}
|
||
}
|
||
|
||
// 辅助方法
|
||
function getScheduleTitle(data) {
|
||
const titles = {
|
||
vaccine: `${data.vaccineName || '疫苗'}接种`,
|
||
deworm: `${data.brand || ''}驱虫`,
|
||
checkup: '体检',
|
||
medical: '复查'
|
||
}
|
||
return titles[data.type] || '健康提醒'
|
||
}
|
||
```
|
||
|
||
## 4. scheduleManager(日程管理)
|
||
|
||
```javascript
|
||
// uniCloud/cloudfunctions/scheduleManager/index.js
|
||
const db = uniCloud.database()
|
||
|
||
exports.main = async (event, context) => {
|
||
const { action, data, scheduleId, petId } = event
|
||
const { uid } = context
|
||
|
||
const scheduleCollection = db.collection('schedules')
|
||
const now = Date.now()
|
||
|
||
try {
|
||
switch (action) {
|
||
case 'create': {
|
||
const res = await scheduleCollection.add({
|
||
...data,
|
||
user_id: uid,
|
||
status: 'pending',
|
||
createdAt: now,
|
||
updatedAt: now
|
||
})
|
||
return { code: 0, data: { id: res.id } }
|
||
}
|
||
|
||
case 'list': {
|
||
let where = { user_id: uid }
|
||
if (petId) where.pet_id = petId
|
||
|
||
const res = await scheduleCollection.where(where)
|
||
.orderBy('scheduledDate', 'asc')
|
||
.get()
|
||
return { code: 0, data: res.data }
|
||
}
|
||
|
||
case 'today': {
|
||
// 获取今日及逾期的日程
|
||
const startOfDay = new Date().setHours(0, 0, 0, 0)
|
||
const endOfDay = new Date().setHours(23, 59, 59, 999)
|
||
|
||
const res = await scheduleCollection.where({
|
||
user_id: uid,
|
||
status: db.command.in(['pending', 'overdue']),
|
||
scheduledDate: db.command.lte(endOfDay)
|
||
}).orderBy('scheduledDate', 'asc').get()
|
||
|
||
return { code: 0, data: res.data }
|
||
}
|
||
|
||
case 'upcoming': {
|
||
// 获取未来7天的日程
|
||
const endOfWeek = now + 7 * 24 * 60 * 60 * 1000
|
||
|
||
const res = await scheduleCollection.where({
|
||
user_id: uid,
|
||
status: 'pending',
|
||
scheduledDate: db.command.gte(now).and(db.command.lte(endOfWeek))
|
||
}).orderBy('scheduledDate', 'asc').get()
|
||
|
||
return { code: 0, data: res.data }
|
||
}
|
||
|
||
case 'complete': {
|
||
await scheduleCollection.doc(scheduleId).update({
|
||
status: 'completed',
|
||
updatedAt: now
|
||
})
|
||
return { code: 0, message: '已完成' }
|
||
}
|
||
|
||
case 'postpone': {
|
||
const { days } = data
|
||
const schedule = await scheduleCollection.doc(scheduleId).get()
|
||
const newDate = schedule.data[0].scheduledDate + days * 24 * 60 * 60 * 1000
|
||
|
||
await scheduleCollection.doc(scheduleId).update({
|
||
scheduledDate: newDate,
|
||
status: 'pending',
|
||
updatedAt: now
|
||
})
|
||
return { code: 0, message: `已推迟${days}天` }
|
||
}
|
||
|
||
case 'skip': {
|
||
await scheduleCollection.doc(scheduleId).update({
|
||
status: 'skipped',
|
||
updatedAt: now
|
||
})
|
||
return { code: 0, message: '已忽略' }
|
||
}
|
||
|
||
case 'delete': {
|
||
await scheduleCollection.doc(scheduleId).remove()
|
||
return { code: 0, message: '删除成功' }
|
||
}
|
||
|
||
default:
|
||
return { code: -1, message: '未知操作' }
|
||
}
|
||
} catch (err) {
|
||
return { code: -1, message: err.message }
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. reminderPush(定时提醒触发器)
|
||
|
||
```javascript
|
||
// uniCloud/cloudfunctions/reminderPush/index.js
|
||
// 配置:在uniCloud后台设置定时触发器,每天上午9:00执行
|
||
|
||
const db = uniCloud.database()
|
||
|
||
exports.main = async (event, context) => {
|
||
const now = Date.now()
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
const tomorrow = today.getTime() + 24 * 60 * 60 * 1000
|
||
|
||
try {
|
||
const scheduleCollection = db.collection('schedules')
|
||
const userCollection = db.collection('users')
|
||
|
||
// 1. 标记逾期
|
||
await scheduleCollection.where({
|
||
status: 'pending',
|
||
scheduledDate: db.command.lt(today)
|
||
}).update({
|
||
status: 'overdue',
|
||
updatedAt: now
|
||
})
|
||
|
||
// 2. 查找今天需要提醒的日程
|
||
// 提醒逻辑:在 reminderDays 中的某一天,且今天就是提醒日
|
||
const schedules = await scheduleCollection.where({
|
||
status: db.command.in(['pending', 'overdue']),
|
||
scheduledDate: db.command.gte(today).and(db.command.lt(tomorrow + 7 * 24 * 60 * 60 * 1000))
|
||
}).get()
|
||
|
||
// 3. 按用户分组,批量发送订阅消息
|
||
const userSchedules = {}
|
||
schedules.data.forEach(s => {
|
||
if (!userSchedules[s.user_id]) {
|
||
userSchedules[s.user_id] = []
|
||
}
|
||
userSchedules[s.user_id].push(s)
|
||
})
|
||
|
||
for (const userId in userSchedules) {
|
||
const user = await userCollection.doc(userId).get()
|
||
const openid = user.data[0]?.wx_openid
|
||
|
||
if (!openid) continue
|
||
|
||
// 获取该用户今天需要提醒的日程
|
||
const todaySchedules = userSchedules[userId].filter(s => {
|
||
const scheduledDate = new Date(s.scheduledDate)
|
||
scheduledDate.setHours(0, 0, 0, 0)
|
||
const diffDays = Math.ceil((scheduledDate - today) / (24 * 60 * 60 * 1000))
|
||
return s.reminderDays.includes(diffDays) && diffDays >= 0
|
||
})
|
||
|
||
if (todaySchedules.length === 0) continue
|
||
|
||
// 构建订阅消息(需先在微信公众平台申请模板)
|
||
const templateId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' // 你的模板ID
|
||
|
||
for (const schedule of todaySchedules) {
|
||
try {
|
||
await uniCloud.openapi.weixin.subscribeMessage.send({
|
||
touser: openid,
|
||
templateId: templateId,
|
||
page: `pages/schedule/list?petId=${schedule.pet_id}`,
|
||
data: {
|
||
thing1: { value: schedule.title }, // 事项
|
||
time2: { value: formatDate(schedule.scheduledDate) }, // 时间
|
||
thing3: { value: schedule.status === 'overdue' ? '已逾期,请尽快处理' : '即将到期' } // 备注
|
||
},
|
||
miniprogramState: 'formal'
|
||
})
|
||
|
||
// 更新最后提醒时间
|
||
await scheduleCollection.doc(schedule._id).update({
|
||
lastRemindedAt: now
|
||
})
|
||
} catch (err) {
|
||
console.error(`发送失败: ${userId}`, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
code: 0,
|
||
message: '提醒处理完成',
|
||
data: {
|
||
totalChecked: schedules.data.length,
|
||
notifiedUsers: Object.keys(userSchedules).length
|
||
}
|
||
}
|
||
} catch (err) {
|
||
return { code: -1, message: err.message }
|
||
}
|
||
}
|
||
|
||
function formatDate(timestamp) {
|
||
const d = new Date(timestamp)
|
||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
|
||
}
|
||
```
|
||
|
||
**定时触发器配置**(在 `reminderPush` 云函数的 `package.json` 中):
|
||
|
||
```json
|
||
{
|
||
"name": "reminderPush",
|
||
"version": "1.0.0",
|
||
"description": "每日健康提醒推送",
|
||
"main": "index.js",
|
||
"extensions": {
|
||
"uni-cloud-jql": {}
|
||
},
|
||
"cloudfunction-config": {
|
||
"concurrency": 1,
|
||
"triggers": [
|
||
{
|
||
"name": "dailyReminder",
|
||
"type": "timer",
|
||
"config": "0 0 9 * * * *"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
> 说明:`0 0 9 * * * *` 表示每天上午9:00执行(UTC+8 时区)。
|
||
|
||
## 6. nearbyHospitals(附近医院查询)
|
||
|
||
```javascript
|
||
// uniCloud/cloudfunctions/nearbyHospitals/index.js
|
||
const db = uniCloud.database()
|
||
|
||
exports.main = async (event, context) => {
|
||
const { longitude, latitude, maxDistance = 5000, limit = 20 } = event
|
||
// maxDistance 单位:米,默认5km
|
||
|
||
try {
|
||
const hospitalCollection = db.collection('hospitals')
|
||
|
||
const res = await hospitalCollection.where({
|
||
status: 'active',
|
||
location: db.command.geoNear({
|
||
geometry: db.Geo.Point(longitude, latitude),
|
||
maxDistance: maxDistance,
|
||
distanceField: 'distance'
|
||
})
|
||
}).limit(limit).get()
|
||
|
||
// 格式化距离
|
||
const hospitals = res.data.map(h => ({
|
||
...h,
|
||
distance: h.distance ? Math.round(h.distance) : null,
|
||
distanceText: h.distance
|
||
? (h.distance < 1000 ? `${Math.round(h.distance)}m` : `${(h.distance / 1000).toFixed(1)}km`)
|
||
: '未知'
|
||
}))
|
||
|
||
return {
|
||
code: 0,
|
||
data: hospitals
|
||
}
|
||
} catch (err) {
|
||
return { code: -1, message: err.message }
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 四、关键配置补充
|
||
|
||
## 1. 地理位置索引(必须)
|
||
|
||
在 uniCloud 控制台 → 数据库 → `hospitals` 集合 → 索引管理,创建:
|
||
|
||
```json
|
||
{
|
||
"location": "2dsphere"
|
||
}
|
||
```
|
||
|
||
## 2. manifest.json 小程序配置
|
||
|
||
```json
|
||
{
|
||
"mp-weixin": {
|
||
"appid": "你的小程序appid",
|
||
"setting": {
|
||
"urlCheck": false
|
||
},
|
||
"permission": {
|
||
"scope.userLocation": {
|
||
"desc": "你的位置信息将用于查找附近宠物医院"
|
||
}
|
||
},
|
||
"requiredPrivateInfos": [
|
||
"getLocation"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
## 3. 云存储目录规划
|
||
|
||
```
|
||
cloudstorage/
|
||
├── pet-avatars/ # 宠物头像
|
||
├── record-images/ # 病历/疫苗证照片
|
||
├── article-covers/ # 百科文章封面
|
||
└── share-cards/ # 生成的分享卡片
|
||
```
|
||
|
||
---
|
||
|
||
这套框架覆盖了 **MVP 阶段 90% 的核心功能**。建议你先实现 **login → petCrud → recordCrud → scheduleManager → reminderPush** 这个主线,跑通"创建宠物 → 记录疫苗 → 生成提醒 → 推送通知"的完整闭环,再逐步扩展发现页和会员功能。
|
||
|
||
需要我针对某个具体模块(如**微信支付开通会员**、**订阅消息授权引导**、**图片上传云存储**)继续展开吗? |