docs(api): 更新API接口文档并调整应用配置
- 添加完整的后端接口文档,包含用户、打卡记录、统计等模块 - 修复应用名称从"喝酒了么"统一更正为"喝了么" - 重构App.vue中的启动逻辑,增加数据迁移和路由守卫功能 - 调整DrinkCalendar组件的UI间距和尺寸参数 - 优化DrinkCard组件的品牌展示文案 - 修改manifest.json中的应用描述和微信小程序配置 - 调整pages.json中的页面路径顺序和tabBar配置 - 更新全局样式文件中的应用名称标识 - 修改mock数据中的用户头像默认值 - 优化卡片导出功能的绘制逻辑和视觉效果
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
onLaunch() {
|
onLaunch() {
|
||||||
console.log('喝酒了么 App Launch')
|
console.log('喝了么 App Launch')
|
||||||
// 初始化Mock数据(仅首次)
|
// 初始化Mock数据(仅首次)
|
||||||
const hasData = uni.getStorageSync('drink_records')
|
const hasData = uni.getStorageSync('drink_records')
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
@@ -10,19 +10,52 @@ export default {
|
|||||||
console.log('Mock数据已初始化')
|
console.log('Mock数据已初始化')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 数据迁移:清除旧版 logo.png 头像引用
|
||||||
|
this.migrateUserData()
|
||||||
|
// 启动路由守卫
|
||||||
|
this.checkLaunchRoute()
|
||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
console.log('App Show')
|
console.log('App Show')
|
||||||
},
|
},
|
||||||
onHide() {
|
onHide() {
|
||||||
console.log('App Hide')
|
console.log('App Hide')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 数据迁移:清除旧版 logo.png 头像引用
|
||||||
|
migrateUserData() {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync('user_info')
|
||||||
|
if (raw) {
|
||||||
|
const user = JSON.parse(raw)
|
||||||
|
if (user.avatar && user.avatar.includes('logo.png')) {
|
||||||
|
user.avatar = ''
|
||||||
|
uni.setStorageSync('user_info', JSON.stringify(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
},
|
||||||
|
// 启动时检查登录与引导状态,决定跳转目标
|
||||||
|
checkLaunchRoute() {
|
||||||
|
const isFirst = uni.getStorageSync('is_first_launch')
|
||||||
|
const isLoggedIn = uni.getStorageSync('is_logged_in')
|
||||||
|
|
||||||
|
if (!isFirst || isFirst === 'true') {
|
||||||
|
// 首次使用 → 引导页
|
||||||
|
uni.reLaunch({ url: '/pages/onboarding/onboarding' })
|
||||||
|
} else if (!isLoggedIn || isLoggedIn !== 'true') {
|
||||||
|
// 未登录 → 登录页
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
// 否则正常进入首页(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
* 喝酒了么 - 全局样式
|
* 喝了么 - 全局样式
|
||||||
* 设计概念: 琥珀夜光 Amber Night
|
* 设计概念: 琥珀夜光 Amber Night
|
||||||
* ========================================== */
|
* ========================================== */
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
/* 喝酒了么 - 常量定义 */
|
/* 喝了么 - 常量定义 */
|
||||||
|
|
||||||
// 酒类分类
|
// 酒类分类
|
||||||
export const DRINK_CATEGORIES = [
|
export const DRINK_CATEGORIES = [
|
||||||
|
|||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
/* 喝酒了么 - Mock 数据 */
|
/* 喝了么 - Mock 数据 */
|
||||||
import { formatDate, calcStandardCups, unitToMl } from './utils'
|
import { formatDate, calcStandardCups, unitToMl } from './utils'
|
||||||
|
|
||||||
// 模拟用户数据
|
// 模拟用户数据
|
||||||
export const MOCK_USER = {
|
export const MOCK_USER = {
|
||||||
id: 'user_001',
|
id: 'user_001',
|
||||||
nickname: '酒友小王',
|
nickname: '酒友小王',
|
||||||
avatar: '/static/logo.png',
|
avatar: '',
|
||||||
preferences: {
|
preferences: {
|
||||||
favoriteCategories: ['baijiu', 'beer'],
|
favoriteCategories: ['baijiu', 'beer'],
|
||||||
frequency: 'often'
|
frequency: 'often'
|
||||||
@@ -113,7 +113,7 @@ export function generateMockRecords() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化并缓存Mock数据
|
// 初始化并缓存Mock数据
|
||||||
export function initMockData() {自己看
|
export function initMockData() {
|
||||||
const records = generateMockRecords()
|
const records = generateMockRecords()
|
||||||
uni.setStorageSync('drink_records', JSON.stringify(records))
|
uni.setStorageSync('drink_records', JSON.stringify(records))
|
||||||
uni.setStorageSync('user_info', JSON.stringify(MOCK_USER))
|
uni.setStorageSync('user_info', JSON.stringify(MOCK_USER))
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
/* 喝酒了么 - 工具函数 */
|
/* 喝了么 - 工具函数 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标准杯换算引擎
|
* 标准杯换算引擎
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: $sp-lg;
|
margin-bottom: $sp-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cal-nav {
|
.cal-nav {
|
||||||
width: 64rpx;
|
width: 56rpx;
|
||||||
height: 64rpx;
|
height: 56rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -123,20 +123,20 @@ export default {
|
|||||||
.cal-weekdays {
|
.cal-weekdays {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
margin-bottom: $sp-sm;
|
margin-bottom: $sp-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cal-weekday {
|
.cal-weekday {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: $fs-xs;
|
font-size: $fs-xs;
|
||||||
color: $text-tertiary;
|
color: $text-tertiary;
|
||||||
padding: $sp-xs 0;
|
padding: 4rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cal-grid {
|
.cal-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
gap: $sp-xs;
|
gap: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cal-cell {
|
.cal-cell {
|
||||||
@@ -145,7 +145,7 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: $radius-md;
|
border-radius: $radius-sm;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all $duration-fast $ease-out;
|
transition: all $duration-fast $ease-out;
|
||||||
|
|
||||||
@@ -163,17 +163,17 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cal-day-num {
|
.cal-day-num {
|
||||||
font-size: $fs-sm;
|
font-size: $fs-xs;
|
||||||
font-weight: $fw-medium;
|
font-weight: $fw-medium;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-family: $font-num;
|
font-family: $font-num;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cal-dot {
|
.cal-dot {
|
||||||
width: 10rpx;
|
width: 8rpx;
|
||||||
height: 10rpx;
|
height: 8rpx;
|
||||||
border-radius: $radius-full;
|
border-radius: $radius-full;
|
||||||
margin-top: $sp-xs;
|
margin-top: 4rpx;
|
||||||
|
|
||||||
&.cal-dot-drank {
|
&.cal-dot-drank {
|
||||||
background: $amber;
|
background: $amber;
|
||||||
|
|||||||
@@ -183,7 +183,7 @@
|
|||||||
<!-- 统一底部品牌 + 二维码 -->
|
<!-- 统一底部品牌 + 二维码 -->
|
||||||
<view class="card-brand-bar">
|
<view class="card-brand-bar">
|
||||||
<view class="brand-left">
|
<view class="brand-left">
|
||||||
<text class="brand-name">🍻 喝酒了么</text>
|
<text class="brand-name">🍻 喝了么</text>
|
||||||
<text class="brand-slogan">记录每一杯的故事</text>
|
<text class="brand-slogan">记录每一杯的故事</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="brand-qr">
|
<view class="brand-qr">
|
||||||
|
|||||||
+700
@@ -0,0 +1,700 @@
|
|||||||
|
# 喝了么 - 后端接口文档
|
||||||
|
|
||||||
|
> 版本: v1.0
|
||||||
|
> 日期: 2026-07-13
|
||||||
|
> 小程序: 喝了么(uni-app 微信小程序)
|
||||||
|
> 基础URL: `https://your-domain.com/api/v1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 通用说明
|
||||||
|
|
||||||
|
### 1.1 请求格式
|
||||||
|
- Content-Type: `application/json`
|
||||||
|
- 鉴权方式: 请求头 `Authorization: Bearer {token}`(微信登录获取)
|
||||||
|
- 所有时间字段使用 ISO 8601 格式
|
||||||
|
|
||||||
|
### 1.2 统一响应格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| code | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| 0 | 成功 |
|
||||||
|
| 400 | 参数错误 |
|
||||||
|
| 401 | 未登录/token过期 |
|
||||||
|
| 403 | 无权限 |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 500 | 服务器错误 |
|
||||||
|
|
||||||
|
### 1.3 分页参数
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码,默认1 |
|
||||||
|
| pageSize | int | 否 | 每页数量,默认20 |
|
||||||
|
|
||||||
|
### 1.4 分页响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [],
|
||||||
|
"total": 100,
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 用户模块
|
||||||
|
|
||||||
|
### 2.1 微信登录
|
||||||
|
```
|
||||||
|
POST /auth/wechat-login
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| code | string | 是 | 微信登录code(wx.login获取) |
|
||||||
|
| nickName | string | 否 | 用户昵称 |
|
||||||
|
| avatarUrl | string | 否 | 头像URL |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "jwt_token_xxx",
|
||||||
|
"user": {
|
||||||
|
"id": "user_001",
|
||||||
|
"nickname": "酒友小王",
|
||||||
|
"avatar": "https://xxx/avatar.jpg",
|
||||||
|
"joinedAt": "2026-01-15T00:00:00.000Z",
|
||||||
|
"preferences": {
|
||||||
|
"favoriteCategories": ["baijiu", "beer"],
|
||||||
|
"frequency": "often"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isNew": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 获取用户信息
|
||||||
|
```
|
||||||
|
GET /user/profile
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "user_001",
|
||||||
|
"nickname": "酒友小王",
|
||||||
|
"avatar": "https://xxx/avatar.jpg",
|
||||||
|
"joinedAt": "2026-01-15T00:00:00.000Z",
|
||||||
|
"preferences": {
|
||||||
|
"favoriteCategories": ["baijiu", "beer"],
|
||||||
|
"frequency": "often"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 更新用户偏好
|
||||||
|
```
|
||||||
|
PUT /user/preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"favoriteCategories": ["baijiu", "beer"],
|
||||||
|
"frequency": "often"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| favoriteCategories | string[] | 偏好酒类ID列表 |
|
||||||
|
| frequency | string | 饮酒频率:rarely / sometimes / often |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 打卡记录模块(核心)
|
||||||
|
|
||||||
|
### 3.1 创建打卡记录
|
||||||
|
```
|
||||||
|
POST /records
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"date": "2026-07-13",
|
||||||
|
"mode": "drank",
|
||||||
|
"drinks": [
|
||||||
|
{
|
||||||
|
"category": "baijiu",
|
||||||
|
"brand": "茅台",
|
||||||
|
"product": "飞天茅台53度",
|
||||||
|
"amount": 100,
|
||||||
|
"unit": "ml",
|
||||||
|
"degree": 53,
|
||||||
|
"standardCups": 2.12
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": {
|
||||||
|
"category": "hotpot",
|
||||||
|
"name": "重庆老火锅"
|
||||||
|
},
|
||||||
|
"feeling": "tipsy",
|
||||||
|
"photos": [],
|
||||||
|
"visibility": "friends",
|
||||||
|
"quote": "人生得意须尽欢,莫使金樽空对月"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| date | string | 是 | 日期 YYYY-MM-DD |
|
||||||
|
| mode | string | 是 | 打卡模式:`drank`(喝了) / `abstain`(未喝) |
|
||||||
|
| drinks | DrinkItem[] | 否 | 酒水列表(mode=abstain时为空数组) |
|
||||||
|
| food | FoodInfo / null | 否 | 配餐信息 |
|
||||||
|
| feeling | string / null | 否 | 感受ID |
|
||||||
|
| photos | string[] | 否 | 照片URL数组(最多9张) |
|
||||||
|
| visibility | string | 否 | 可见范围:`public` / `friends` / `private`,默认 `friends` |
|
||||||
|
| quote | string | 否 | 酒言酒语(用户选择或自定义) |
|
||||||
|
|
||||||
|
**DrinkItem 结构:**
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| category | string | 是 | 酒类分类ID |
|
||||||
|
| brand | string | 是 | 品牌名称 |
|
||||||
|
| product | string | 否 | 产品名 |
|
||||||
|
| amount | number | 是 | 饮用量数值 |
|
||||||
|
| unit | string | 是 | 单位:ml / liang(两) / bottle(瓶) / cup(杯) / can(听) / shot |
|
||||||
|
| degree | number | 是 | 酒精度数(%) |
|
||||||
|
| standardCups | number | 否 | 标准杯数(前端计算,后端应校验) |
|
||||||
|
|
||||||
|
**FoodInfo 结构:**
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| category | string | 是 | 配餐分类ID |
|
||||||
|
| name | string | 否 | 具体菜名 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "record_1689234567890",
|
||||||
|
"date": "2026-07-13",
|
||||||
|
"mode": "drank",
|
||||||
|
"drinks": [...],
|
||||||
|
"food": {...},
|
||||||
|
"feeling": "tipsy",
|
||||||
|
"photos": [],
|
||||||
|
"visibility": "friends",
|
||||||
|
"quote": "...",
|
||||||
|
"standardCupsTotal": 2.12,
|
||||||
|
"createdAt": "2026-07-13T22:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **标准杯计算公式:**
|
||||||
|
> `标准杯 = (饮用量ml × 酒精度数% × 0.8) / 10`
|
||||||
|
> 1标准杯 = 10g纯酒精
|
||||||
|
|
||||||
|
> **单位换算为ml:**
|
||||||
|
> | 单位 | 换算 |
|
||||||
|
> |------|------|
|
||||||
|
> | ml | ×1 |
|
||||||
|
> | liang(两) | ×50 |
|
||||||
|
> | bottle(瓶) | ×500 |
|
||||||
|
> | cup(杯) | ×150 |
|
||||||
|
> | can(听) | ×330 |
|
||||||
|
> | shot | ×30 |
|
||||||
|
|
||||||
|
### 3.2 获取记录列表
|
||||||
|
```
|
||||||
|
GET /records
|
||||||
|
```
|
||||||
|
|
||||||
|
**查询参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码 |
|
||||||
|
| pageSize | int | 否 | 每页数量 |
|
||||||
|
| month | string | 否 | 月份筛选 YYYY-MM |
|
||||||
|
| mode | string | 否 | 打卡模式筛选 drank/abstain |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "record_xxx",
|
||||||
|
"date": "2026-07-13",
|
||||||
|
"mode": "drank",
|
||||||
|
"drinks": [...],
|
||||||
|
"food": {...},
|
||||||
|
"feeling": "tipsy",
|
||||||
|
"photos": [],
|
||||||
|
"visibility": "friends",
|
||||||
|
"standardCupsTotal": 2.12,
|
||||||
|
"createdAt": "2026-07-13T22:30:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 30,
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 获取单条记录详情
|
||||||
|
```
|
||||||
|
GET /records/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应 data:** 同 3.1 创建记录的返回结构
|
||||||
|
|
||||||
|
### 3.4 删除记录
|
||||||
|
```
|
||||||
|
DELETE /records/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 获取日历打卡数据
|
||||||
|
```
|
||||||
|
GET /records/calendar
|
||||||
|
```
|
||||||
|
|
||||||
|
**查询参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| year | int | 是 | 年份 |
|
||||||
|
| month | int | 是 | 月份(1-12) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 7,
|
||||||
|
"days": {
|
||||||
|
"2026-07-01": { "mode": "drank", "standardCupsTotal": 2.5 },
|
||||||
|
"2026-07-02": { "mode": "abstain", "standardCupsTotal": 0 },
|
||||||
|
"2026-07-03": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 前端根据此数据渲染日历打卡状态。null表示当天无记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 照片上传
|
||||||
|
|
||||||
|
### 4.1 上传照片
|
||||||
|
```
|
||||||
|
POST /upload/photo
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| file | File | 是 | 图片文件(jpg/png,最大10MB) |
|
||||||
|
| recordId | string | 否 | 关联记录ID(可选,用于后续关联) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"url": "https://cdn.xxx.com/photos/2026/07/abc123.jpg",
|
||||||
|
"width": 1080,
|
||||||
|
"height": 1920,
|
||||||
|
"size": 256000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 批量上传
|
||||||
|
```
|
||||||
|
POST /upload/photos
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| files | File[] | 是 | 图片文件数组(最多9张) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"photos": [
|
||||||
|
{ "url": "https://...", "width": 1080, "height": 1920, "size": 256000 },
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 统计模块
|
||||||
|
|
||||||
|
### 5.1 获取用户统计概览
|
||||||
|
```
|
||||||
|
GET /stats/overview
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"weekDrinkCount": 3,
|
||||||
|
"weekCups": 6.5,
|
||||||
|
"monthDrinkCount": 12,
|
||||||
|
"monthCups": 28.3,
|
||||||
|
"streak": 5,
|
||||||
|
"streakType": "drank",
|
||||||
|
"totalDays": 45,
|
||||||
|
"totalRecords": 48,
|
||||||
|
"totalCups": 134.4,
|
||||||
|
"favCategory": "baijiu",
|
||||||
|
"categoryVariety": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| weekDrinkCount | int | 本周饮酒天数 |
|
||||||
|
| weekCups | float | 本周标准杯总数 |
|
||||||
|
| monthDrinkCount | int | 本月饮酒天数 |
|
||||||
|
| monthCups | float | 本月标准杯总数 |
|
||||||
|
| streak | int | 当前连续打卡天数 |
|
||||||
|
| streakType | string | 连续类型:drank/abstain |
|
||||||
|
| totalDays | int | 总饮酒天数(去重) |
|
||||||
|
| totalRecords | int | 总记录数 |
|
||||||
|
| totalCups | float | 历史累计标准杯 |
|
||||||
|
| favCategory | string/null | 最爱酒类ID |
|
||||||
|
| categoryVariety | int | 尝试过的酒类品种数 |
|
||||||
|
|
||||||
|
### 5.2 获取月度统计
|
||||||
|
```
|
||||||
|
GET /stats/monthly
|
||||||
|
```
|
||||||
|
|
||||||
|
**查询参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| year | int | 否 | 年份,默认当前年 |
|
||||||
|
| month | int | 否 | 月份,默认当前月 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 7,
|
||||||
|
"drinkDays": 18,
|
||||||
|
"abstainDays": 12,
|
||||||
|
"totalCups": 45.6,
|
||||||
|
"avgCupsPerDay": 2.53,
|
||||||
|
"categoryBreakdown": [
|
||||||
|
{ "category": "baijiu", "count": 8, "cups": 22.4 },
|
||||||
|
{ "category": "beer", "count": 6, "cups": 15.2 },
|
||||||
|
{ "category": "wine", "count": 4, "cups": 8.0 }
|
||||||
|
],
|
||||||
|
"feelingBreakdown": [
|
||||||
|
{ "feeling": "tipsy", "count": 10 },
|
||||||
|
{ "feeling": "buzzed", "count": 5 },
|
||||||
|
{ "feeling": "drunk", "count": 2 },
|
||||||
|
{ "feeling": "blackout", "count": 1 }
|
||||||
|
],
|
||||||
|
"foodBreakdown": [
|
||||||
|
{ "category": "hotpot", "count": 5 },
|
||||||
|
{ "category": "bbq", "count": 4 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 获取酒类分布统计
|
||||||
|
```
|
||||||
|
GET /stats/categories
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": "baijiu",
|
||||||
|
"name": "白酒",
|
||||||
|
"recordCount": 20,
|
||||||
|
"totalCups": 56.8,
|
||||||
|
"percentage": 42.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "beer",
|
||||||
|
"name": "啤酒",
|
||||||
|
"recordCount": 15,
|
||||||
|
"totalCups": 38.2,
|
||||||
|
"percentage": 28.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 成就模块
|
||||||
|
|
||||||
|
### 6.1 获取成就列表
|
||||||
|
```
|
||||||
|
GET /achievements
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"achievements": [
|
||||||
|
{
|
||||||
|
"id": "first_checkin",
|
||||||
|
"name": "初来乍到",
|
||||||
|
"desc": "完成第一次打卡,开启你的饮酒记录之旅",
|
||||||
|
"icon": "🏅",
|
||||||
|
"unlocked": true,
|
||||||
|
"unlockedAt": "2026-01-15T20:00:00.000Z",
|
||||||
|
"condition": {
|
||||||
|
"type": "total_records",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"progress": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hundred_cups",
|
||||||
|
"name": "百杯不醉",
|
||||||
|
"desc": "累计饮用100标准杯",
|
||||||
|
"icon": "🏆",
|
||||||
|
"unlocked": false,
|
||||||
|
"unlockedAt": null,
|
||||||
|
"condition": {
|
||||||
|
"type": "total_standard_cups",
|
||||||
|
"value": 100
|
||||||
|
},
|
||||||
|
"progress": 0.75
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成就条件类型:**
|
||||||
|
| type | 说明 | value含义 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| total_records | 总记录数 | 次数 |
|
||||||
|
| total_standard_cups | 累计标准杯 | 杯数 |
|
||||||
|
| streak_days | 连续打卡天数 | 天数 |
|
||||||
|
| category_records | 某酒类打卡次数 | 次数(需配合category字段) |
|
||||||
|
| category_variety | 尝试酒类品种数 | 品种数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 社交模块(朋友圈/卡片分享)
|
||||||
|
|
||||||
|
### 7.1 获取朋友圈动态
|
||||||
|
```
|
||||||
|
GET /feed
|
||||||
|
```
|
||||||
|
|
||||||
|
**查询参数:**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码 |
|
||||||
|
| pageSize | int | 否 | 每页数量 |
|
||||||
|
| lastId | string | 否 | 游标分页,上一页最后一条ID |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "record_xxx",
|
||||||
|
"user": {
|
||||||
|
"id": "user_002",
|
||||||
|
"nickname": "酒友小李",
|
||||||
|
"avatar": "https://xxx/avatar2.jpg"
|
||||||
|
},
|
||||||
|
"date": "2026-07-13",
|
||||||
|
"mode": "drank",
|
||||||
|
"drinks": [...],
|
||||||
|
"food": {...},
|
||||||
|
"feeling": "tipsy",
|
||||||
|
"photos": ["https://..."],
|
||||||
|
"standardCupsTotal": 3.5,
|
||||||
|
"quote": "酒逢知己千杯少",
|
||||||
|
"likeCount": 5,
|
||||||
|
"commentCount": 2,
|
||||||
|
"liked": false,
|
||||||
|
"createdAt": "2026-07-13T22:30:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 100,
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 点赞/取消点赞
|
||||||
|
```
|
||||||
|
POST /records/:id/like
|
||||||
|
POST /records/:id/unlike
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 数据字典
|
||||||
|
|
||||||
|
### 8.1 酒类分类 (category)
|
||||||
|
| ID | 名称 | 默认度数 |
|
||||||
|
|----|------|----------|
|
||||||
|
| baijiu | 白酒 | 52 |
|
||||||
|
| beer | 啤酒 | 5 |
|
||||||
|
| wine | 红酒 | 13 |
|
||||||
|
| whisky | 洋酒 | 40 |
|
||||||
|
| huangjiu | 黄酒 | 15 |
|
||||||
|
| sake | 清酒 | 15 |
|
||||||
|
| fruit | 果酒 | 8 |
|
||||||
|
| cocktail | 调酒 | 15 |
|
||||||
|
| other | 其他 | 10 |
|
||||||
|
|
||||||
|
### 8.2 饮用量单位 (unit)
|
||||||
|
| ID | 名称 | 换算(ml) |
|
||||||
|
|----|------|----------|
|
||||||
|
| ml | ml | 1 |
|
||||||
|
| liang | 两 | 50 |
|
||||||
|
| bottle | 瓶 | 500 |
|
||||||
|
| cup | 杯 | 150 |
|
||||||
|
| can | 听 | 330 |
|
||||||
|
| shot | shot | 30 |
|
||||||
|
|
||||||
|
### 8.3 配餐分类 (food category)
|
||||||
|
| ID | 名称 |
|
||||||
|
|----|------|
|
||||||
|
| hotpot | 火锅 |
|
||||||
|
| bbq | 烧烤 |
|
||||||
|
| stirfry | 炒菜 |
|
||||||
|
| seafood | 海鲜 |
|
||||||
|
| japanese | 日料 |
|
||||||
|
| western | 西餐 |
|
||||||
|
| snack | 小吃卤味 |
|
||||||
|
| junk | 零食 |
|
||||||
|
| none | 无配餐 |
|
||||||
|
|
||||||
|
### 8.4 饮用感受 (feeling)
|
||||||
|
| ID | 名称 | 描述 |
|
||||||
|
|----|------|------|
|
||||||
|
| tipsy | 微醺 | 刚刚好的快乐 |
|
||||||
|
| buzzed | 到位 | 今夜刚刚好 |
|
||||||
|
| drunk | 醉了 | 有点上头了 |
|
||||||
|
| blackout | 断片 | 什么都不记得了 |
|
||||||
|
|
||||||
|
### 8.5 可见范围 (visibility)
|
||||||
|
| ID | 名称 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| public | 公开 | 所有人可见 |
|
||||||
|
| friends | 仅好友 | 好友可见 |
|
||||||
|
| private | 仅自己 | 仅自己可见 |
|
||||||
|
|
||||||
|
### 8.6 打卡模式 (mode)
|
||||||
|
| ID | 名称 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| drank | 今日喝了 | 有饮酒记录 |
|
||||||
|
| abstain | 今日未喝 | 戒酒打卡 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 数据库表设计参考
|
||||||
|
|
||||||
|
### 9.1 用户表 (users)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE users (
|
||||||
|
id VARCHAR(32) PRIMARY KEY,
|
||||||
|
openid VARCHAR(64) UNIQUE NOT NULL,
|
||||||
|
union_id VARCHAR(64),
|
||||||
|
nickname VARCHAR(64),
|
||||||
|
avatar VARCHAR(512),
|
||||||
|
preferences JSON,
|
||||||
|
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 打卡记录表 (records)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE records (
|
||||||
|
id VARCHAR(32) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(32) NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
mode ENUM('drank', 'abstain') NOT NULL,
|
||||||
|
drinks JSON,
|
||||||
|
food JSON,
|
||||||
|
feeling VARCHAR(16),
|
||||||
|
photos JSON,
|
||||||
|
visibility ENUM('public', 'friends', 'private') DEFAULT 'friends',
|
||||||
|
quote VARCHAR(256),
|
||||||
|
standard_cups_total DECIMAL(8,2) DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_date (user_id, date),
|
||||||
|
INDEX idx_user_mode (user_id, mode, date)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 照片表 (photos)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE photos (
|
||||||
|
id VARCHAR(32) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(32) NOT NULL,
|
||||||
|
record_id VARCHAR(32),
|
||||||
|
url VARCHAR(512) NOT NULL,
|
||||||
|
width INT,
|
||||||
|
height INT,
|
||||||
|
size INT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_record (record_id),
|
||||||
|
INDEX idx_user (user_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 点赞表 (likes)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE likes (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id VARCHAR(32) NOT NULL,
|
||||||
|
record_id VARCHAR(32) NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE INDEX idx_user_record (user_id, record_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.5 成就记录表 (user_achievements)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE user_achievements (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id VARCHAR(32) NOT NULL,
|
||||||
|
achievement_id VARCHAR(32) NOT NULL,
|
||||||
|
unlocked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE INDEX idx_user_achievement (user_id, achievement_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 备注
|
||||||
|
|
||||||
|
1. **标准杯计算建议由后端完成**,前端传入的 `standardCups` 仅作参考,后端应以 `amount`、`unit`、`degree` 为准重新计算
|
||||||
|
2. **照片上传**建议走对象存储(OSS/COS),返回CDN链接
|
||||||
|
3. **日历接口**建议缓存,避免每次查全月记录
|
||||||
|
4. **成就检测**可在创建记录后异步触发,避免影响主流程
|
||||||
|
5. **可见范围**控制需在 feed 接口和记录详情接口中做权限过滤
|
||||||
|
6. 前端目前使用 Mock 数据(`mock-data.js`),切换到真实接口时只需替换 `common/mock-data.js` 中的方法为 HTTP 调用即可
|
||||||
+2
-71
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "喝酒了么",
|
"name" : "喝了么",
|
||||||
"appid" : "__UNI__871D23D",
|
"appid" : "__UNI__871D23D",
|
||||||
"description" : "让每一杯酒都有迹可循",
|
"description" : "让每一杯酒都有迹可循",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
"quickapp" : {},
|
"quickapp" : {},
|
||||||
"mp-weixin" : {
|
"mp-weixin" : {
|
||||||
"appid": "",
|
"appid" : "wx16e19044f4f57f71",
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false,
|
"urlCheck" : false,
|
||||||
"es6" : true,
|
"es6" : true,
|
||||||
@@ -57,72 +57,3 @@
|
|||||||
},
|
},
|
||||||
"vueVersion" : "3"
|
"vueVersion" : "3"
|
||||||
}
|
}
|
||||||
{
|
|
||||||
"name" : "hekjiu",
|
|
||||||
"appid" : "__UNI__871D23D",
|
|
||||||
"description" : "",
|
|
||||||
"versionName" : "1.0.0",
|
|
||||||
"versionCode" : "100",
|
|
||||||
"transformPx" : false,
|
|
||||||
/* 5+App特有相关 */
|
|
||||||
"app-plus" : {
|
|
||||||
"usingComponents" : true,
|
|
||||||
"nvueStyleCompiler" : "uni-app",
|
|
||||||
"compilerVersion" : 3,
|
|
||||||
"splashscreen" : {
|
|
||||||
"alwaysShowBeforeRender" : true,
|
|
||||||
"waiting" : true,
|
|
||||||
"autoclose" : true,
|
|
||||||
"delay" : 0
|
|
||||||
},
|
|
||||||
/* 模块配置 */
|
|
||||||
"modules" : {},
|
|
||||||
/* 应用发布信息 */
|
|
||||||
"distribute" : {
|
|
||||||
/* android打包配置 */
|
|
||||||
"android" : {
|
|
||||||
"permissions" : [
|
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
|
||||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
/* ios打包配置 */
|
|
||||||
"ios" : {},
|
|
||||||
/* SDK配置 */
|
|
||||||
"sdkConfigs" : {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/* 快应用特有相关 */
|
|
||||||
"quickapp" : {},
|
|
||||||
/* 小程序特有相关 */
|
|
||||||
"mp-weixin" : {
|
|
||||||
"appid" : "",
|
|
||||||
"setting" : {
|
|
||||||
"urlCheck" : false
|
|
||||||
},
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"mp-alipay" : {
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"mp-baidu" : {
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"mp-toutiao" : {
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"vueVersion" : "3"
|
|
||||||
}
|
|
||||||
|
|||||||
+24
-21
@@ -7,6 +7,13 @@
|
|||||||
"navigationBarTitleText": ""
|
"navigationBarTitleText": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/profile/profile",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/onboarding/onboarding",
|
"path": "pages/onboarding/onboarding",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -34,39 +41,35 @@
|
|||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": ""
|
"navigationBarTitleText": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/profile/profile",
|
|
||||||
"style": {
|
|
||||||
"navigationStyle": "custom",
|
|
||||||
"navigationBarTitleText": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "white",
|
||||||
"navigationBarTitleText": "喝酒了么",
|
"navigationBarTitleText": "喝了么",
|
||||||
"navigationBarBackgroundColor": "#0B0B14",
|
"navigationBarBackgroundColor": "#0B0B14",
|
||||||
"backgroundColor": "#0B0B14",
|
"backgroundColor": "#0B0B14",
|
||||||
"backgroundColorTop": "#0B0B14",
|
"backgroundColorTop": "#0B0B14",
|
||||||
"backgroundColorBottom": "#0B0B14"
|
"backgroundColorBottom": "#0B0B14"
|
||||||
},
|
},
|
||||||
"uniIdRouter": {}
|
"tabBar": {
|
||||||
}
|
"color": "#4A4A62",
|
||||||
|
"selectedColor": "#E8A838",
|
||||||
|
"backgroundColor": "#151520",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"list": [
|
||||||
{
|
{
|
||||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
"pagePath": "pages/index/index",
|
||||||
|
"text": "首页",
|
||||||
|
"iconPath": "static/tab-home.png",
|
||||||
|
"selectedIconPath": "static/tab-home-active.png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"pagePath": "pages/profile/profile",
|
||||||
"style": {
|
"text": "我的",
|
||||||
"navigationBarTitleText": "uni-app"
|
"iconPath": "static/tab-profile.png",
|
||||||
|
"selectedIconPath": "static/tab-profile-active.png"
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
],
|
|
||||||
"globalStyle": {
|
|
||||||
"navigationBarTextStyle": "black",
|
|
||||||
"navigationBarTitleText": "uni-app",
|
|
||||||
"navigationBarBackgroundColor": "#F8F8F8",
|
|
||||||
"backgroundColor": "#F8F8F8"
|
|
||||||
},
|
},
|
||||||
"uniIdRouter": {}
|
"uniIdRouter": {}
|
||||||
}
|
}
|
||||||
|
|||||||
+136
-87
@@ -87,8 +87,8 @@ export default {
|
|||||||
if (this.saving) return
|
if (this.saving) return
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
await this.drawExportCard()
|
const contentH = await this.drawExportCard()
|
||||||
const path = await this.exportImg()
|
const path = await this.exportImg(contentH)
|
||||||
await this.saveImg(path)
|
await this.saveImg(path)
|
||||||
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -105,64 +105,92 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// ===== 简洁导出卡片绘制 =====
|
// ===== 海报导出绘制(优化版) =====
|
||||||
async drawExportCard() {
|
async drawExportCard() {
|
||||||
const ctx = uni.createCanvasContext('shareCanvas', this)
|
const ctx = uni.createCanvasContext('shareCanvas', this)
|
||||||
const W = 750, H = 1200
|
const W = 750, H = 1200
|
||||||
|
const P = 56 // 全局padding增大
|
||||||
const rec = this.record
|
const rec = this.record
|
||||||
const P = 50 // padding
|
|
||||||
const tpl = this.currentTemplate
|
const tpl = this.currentTemplate
|
||||||
|
|
||||||
// 配色
|
// 模板配色体系
|
||||||
const TH = {
|
const TH = {
|
||||||
amber: { bg: '#111111', line: '#2A2218', accent: '#E8A838', tagBg: '#1E1A14' },
|
amber: { bg: '#111111', bgGrad: '#1A1510', line: '#2A2218', accent: '#E8A838', accentLight: '#F5C963', tagBg: '#1E1A14', tagBorder: '#3A3228' },
|
||||||
aurora: { bg: '#0E0E16', line: '#1E1E2E', accent: '#5EC6A0', tagBg: '#141420' },
|
aurora: { bg: '#0E0E16', bgGrad: '#141420', line: '#1E1E2E', accent: '#5EC6A0', accentLight: '#8EDCBE', tagBg: '#141420', tagBorder: '#2A2A3E' },
|
||||||
film: { bg: '#100F0D', line: '#252218', accent: '#C8B48C', tagBg: '#1A1814' }
|
film: { bg: '#100F0D', bgGrad: '#181610', line: '#252218', accent: '#C8B48C', accentLight: '#DED0B0', tagBg: '#1A1814', tagBorder: '#352E22' }
|
||||||
}
|
}
|
||||||
const c = TH[tpl] || TH.amber
|
const c = TH[tpl] || TH.amber
|
||||||
const WHITE = '#F0F0F0'
|
const WHITE = '#F2F2F2'
|
||||||
const GRAY = '#999999'
|
const SUB = '#B0B0B0'
|
||||||
const DIM = '#666666'
|
const GRAY = '#888888'
|
||||||
const F = 'PingFang SC'
|
const DIM = '#5A5A5A'
|
||||||
|
const F = '-apple-system, PingFang SC, Helvetica Neue, sans-serif'
|
||||||
|
|
||||||
// 背景
|
// ===== 背景绘制 =====
|
||||||
ctx.setFillStyle(c.bg)
|
ctx.setFillStyle(c.bg)
|
||||||
ctx.fillRect(0, 0, W, H)
|
ctx.fillRect(0, 0, W, H)
|
||||||
|
// 底部微渐变装饰
|
||||||
// 顶部细线装饰
|
ctx.setFillStyle(c.bgGrad)
|
||||||
|
ctx.setGlobalAlpha(0.5)
|
||||||
|
ctx.fillRect(0, H * 0.6, W, H * 0.4)
|
||||||
|
ctx.setGlobalAlpha(1)
|
||||||
|
// 顶部强调线条(加粗)
|
||||||
ctx.setFillStyle(c.accent)
|
ctx.setFillStyle(c.accent)
|
||||||
ctx.fillRect(0, 0, W, 4)
|
ctx.fillRect(0, 0, W, 5)
|
||||||
|
|
||||||
let y = P + 10
|
let y = P + 16
|
||||||
|
|
||||||
// === 日期行 ===
|
// ===== 顶部行:日期药丸 + 标准杯 =====
|
||||||
ctx.font = `normal 26px ${F}`
|
// 日期药丸样式
|
||||||
ctx.setFillStyle(GRAY)
|
const dateStr = rec.date || ''
|
||||||
ctx.fillText(rec.date || '', P, y + 20)
|
ctx.font = `500 22px ${F}`
|
||||||
|
const dateW = ctx.measureText(dateStr).width + 28
|
||||||
|
const pillH = 36, pillR = 18
|
||||||
|
ctx.setFillStyle(c.tagBg)
|
||||||
|
this.rr(ctx, P, y - 4, dateW, pillH, pillR, 'fill')
|
||||||
|
ctx.setStrokeStyle(c.tagBorder)
|
||||||
|
ctx.setLineWidth(1)
|
||||||
|
this.rr(ctx, P, y - 4, dateW, pillH, pillR, 'stroke')
|
||||||
|
ctx.font = `500 22px ${F}`
|
||||||
|
ctx.setFillStyle(SUB)
|
||||||
|
ctx.fillText(dateStr, P + 14, y + 18)
|
||||||
|
|
||||||
// 标准杯(右对齐)
|
// 标准杯:数字+文字垂直堆叠(右对齐)
|
||||||
ctx.font = `bold 40px ${F}`
|
|
||||||
ctx.setFillStyle(c.accent)
|
|
||||||
const num = String(rec.standardCupsTotal)
|
const num = String(rec.standardCupsTotal)
|
||||||
|
ctx.font = `800 44px ${F}`
|
||||||
|
ctx.setFillStyle(c.accent)
|
||||||
const nw = ctx.measureText(num).width
|
const nw = ctx.measureText(num).width
|
||||||
ctx.fillText(num, W - P - nw - ctx.measureText(' 标准杯').width - 6, y + 24)
|
ctx.font = `normal 18px ${F}`
|
||||||
ctx.font = `normal 22px ${F}`
|
const lw = ctx.measureText('标准杯').width
|
||||||
|
const cupBlockW = Math.max(nw, lw) + 4
|
||||||
|
const cupX = W - P - cupBlockW
|
||||||
|
ctx.font = `800 44px ${F}`
|
||||||
|
ctx.setFillStyle(c.accent)
|
||||||
|
ctx.fillText(num, cupX + (cupBlockW - nw) / 2, y + 16)
|
||||||
|
ctx.font = `normal 18px ${F}`
|
||||||
ctx.setFillStyle(GRAY)
|
ctx.setFillStyle(GRAY)
|
||||||
ctx.fillText(' 标准杯', W - P - ctx.measureText(' 标准杯').width, y + 22)
|
ctx.fillText('标准杯', cupX + (cupBlockW - lw) / 2, y + 34)
|
||||||
y += 50
|
y += 58
|
||||||
|
|
||||||
// === 照片 ===
|
// ===== 照片区域 =====
|
||||||
const photos = rec.photos || []
|
const photos = rec.photos || []
|
||||||
if (photos.length > 0) {
|
if (photos.length > 0) {
|
||||||
const loaded = await Promise.all(photos.slice(0, 9).map(p => this.loadImg(p)))
|
const loaded = await Promise.all(photos.slice(0, 9).map(p => this.loadImg(p)))
|
||||||
const ph = this.drawPhotos(ctx, loaded, P, y, W - P * 2, c.accent)
|
const ph = this.drawPhotos(ctx, loaded, P, y, W - P * 2, c.accent)
|
||||||
|
// 照片底部渐变蒙版
|
||||||
|
const gradH = 80
|
||||||
|
const grd = ctx.createLinearGradient(P, y + ph - gradH, P, y + ph)
|
||||||
|
grd.addColorStop(0, 'rgba(0,0,0,0)')
|
||||||
|
grd.addColorStop(1, 'rgba(0,0,0,0.45)')
|
||||||
|
ctx.setFillStyle(grd)
|
||||||
|
ctx.fillRect(P, y + ph - gradH, W - P * 2, gradH)
|
||||||
y += ph + 28
|
y += ph + 28
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 酒水信息行 ===
|
// ===== 酒水信息行 =====
|
||||||
const drinks = rec.drinks || []
|
const drinks = rec.drinks || []
|
||||||
if (drinks.length > 0) {
|
if (drinks.length > 0) {
|
||||||
ctx.font = `bold 28px ${F}`
|
ctx.font = `bold 30px ${F}`
|
||||||
ctx.setFillStyle(WHITE)
|
ctx.setFillStyle(WHITE)
|
||||||
let line = ''
|
let line = ''
|
||||||
drinks.forEach(d => {
|
drinks.forEach(d => {
|
||||||
@@ -170,91 +198,109 @@ export default {
|
|||||||
const nm = cat ? cat.name : '酒水'
|
const nm = cat ? cat.name : '酒水'
|
||||||
line += (line ? ' · ' : '') + `${nm} ${d.brand} · ${d.standardCups}杯`
|
line += (line ? ' · ' : '') + `${nm} ${d.brand} · ${d.standardCups}杯`
|
||||||
})
|
})
|
||||||
ctx.fillText(line, P, y + 22)
|
ctx.fillText(line, P, y + 24)
|
||||||
y += 42
|
y += 48
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 配餐 + 感受 标签 ===
|
// ===== 配餐 + 感受 胶囊标签 =====
|
||||||
let tags = []
|
let tags = []
|
||||||
|
let tagTypes = [] // 'food' | 'feeling'
|
||||||
if (rec.food) {
|
if (rec.food) {
|
||||||
const food = FOOD_CATEGORIES.find(f => f.id === rec.food.category)
|
const food = FOOD_CATEGORIES.find(f => f.id === rec.food.category)
|
||||||
if (food) tags.push(rec.food.name ? `${food.name} · ${rec.food.name}` : food.name)
|
if (food) {
|
||||||
|
tags.push(rec.food.name ? `${food.name} · ${rec.food.name}` : food.name)
|
||||||
|
tagTypes.push('food')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (rec.feeling) {
|
if (rec.feeling) {
|
||||||
const f = FEELINGS.find(fe => fe.id === rec.feeling)
|
const f = FEELINGS.find(fe => fe.id === rec.feeling)
|
||||||
if (f) tags.push(f.name)
|
if (f) { tags.push(f.name); tagTypes.push('feeling') }
|
||||||
}
|
}
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
let tx = P
|
let tx = P
|
||||||
ctx.font = `normal 22px ${F}`
|
ctx.font = `normal 22px ${F}`
|
||||||
tags.forEach(tag => {
|
tags.forEach((tag, i) => {
|
||||||
const tw = ctx.measureText(tag).width + 24
|
const tw = ctx.measureText(tag).width + 32
|
||||||
|
const th = 36
|
||||||
|
// 胶囊背景+边框
|
||||||
ctx.setFillStyle(c.tagBg)
|
ctx.setFillStyle(c.tagBg)
|
||||||
this.rr(ctx, tx, y, tw, 32, 6, 'fill')
|
this.rr(ctx, tx, y, tw, th, th / 2, 'fill')
|
||||||
ctx.setFillStyle(GRAY)
|
ctx.setStrokeStyle(tagTypes[i] === 'food' ? c.accent : c.accentLight)
|
||||||
ctx.fillText(tag, tx + 12, y + 22)
|
ctx.setLineWidth(1)
|
||||||
tx += tw + 10
|
this.rr(ctx, tx, y, tw, th, th / 2, 'stroke')
|
||||||
|
// 标签文字(场景用强调色,感受用次要白)
|
||||||
|
ctx.setFillStyle(tagTypes[i] === 'food' ? c.accentLight : SUB)
|
||||||
|
ctx.fillText(tag, tx + 16, y + 24)
|
||||||
|
tx += tw + 14
|
||||||
})
|
})
|
||||||
y += 48
|
y += 56
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 分隔线 ===
|
// ===== 分隔线 =====
|
||||||
ctx.setStrokeStyle(c.line)
|
ctx.setStrokeStyle(c.line)
|
||||||
ctx.setLineWidth(1)
|
ctx.setLineWidth(1)
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(P, y)
|
ctx.moveTo(P, y)
|
||||||
ctx.lineTo(W - P, y)
|
ctx.lineTo(W - P, y)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
y += 24
|
|
||||||
|
|
||||||
// === 酒言酒语 ===
|
|
||||||
const quote = DRINK_QUOTES[Math.floor(Math.random() * DRINK_QUOTES.length)]
|
|
||||||
// 左侧装饰竖线
|
|
||||||
ctx.setFillStyle(c.accent)
|
|
||||||
this.rr(ctx, P, y, 4, 40, 2, 'fill')
|
|
||||||
// 引号
|
|
||||||
ctx.font = `bold 48px ${F}`
|
|
||||||
ctx.setGlobalAlpha(0.2)
|
|
||||||
ctx.fillText('"', P + 14, y + 38)
|
|
||||||
ctx.setGlobalAlpha(1)
|
|
||||||
// 引文
|
|
||||||
ctx.font = `normal 26px ${F}`
|
|
||||||
ctx.setFillStyle(GRAY)
|
|
||||||
const dq = quote.length > 14 ? quote.substring(0, 14) + '...' : quote
|
|
||||||
ctx.fillText(dq, P + 16, y + 28)
|
|
||||||
y += 60
|
|
||||||
|
|
||||||
// === 底部品牌栏 ===
|
|
||||||
ctx.setStrokeStyle(c.line)
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(P, y)
|
|
||||||
ctx.lineTo(W - P, y)
|
|
||||||
ctx.stroke()
|
|
||||||
y += 28
|
y += 28
|
||||||
|
|
||||||
// 品牌名
|
// ===== 酒言酒语(优化装饰) =====
|
||||||
ctx.font = `bold 26px ${F}`
|
const quote = DRINK_QUOTES[Math.floor(Math.random() * DRINK_QUOTES.length)]
|
||||||
|
// 左侧装饰竖条(加宽、渐变感)
|
||||||
|
ctx.setFillStyle(c.accent)
|
||||||
|
this.rr(ctx, P, y + 2, 5, 48, 2.5, 'fill')
|
||||||
|
// 装饰引号(大字、半透明)
|
||||||
|
ctx.font = `bold 60px ${F}`
|
||||||
|
ctx.setGlobalAlpha(0.15)
|
||||||
|
ctx.setFillStyle(c.accentLight)
|
||||||
|
ctx.fillText('\u201C', P + 16, y + 44)
|
||||||
|
ctx.setGlobalAlpha(1)
|
||||||
|
// 引文内容
|
||||||
|
ctx.font = `italic 26px ${F}`
|
||||||
|
ctx.setFillStyle(SUB)
|
||||||
|
const maxQW = W - P * 2 - 40
|
||||||
|
let dq = quote
|
||||||
|
if (ctx.measureText(dq).width > maxQW) {
|
||||||
|
while (dq.length > 0 && ctx.measureText(dq + '...').width > maxQW) dq = dq.slice(0, -1)
|
||||||
|
dq += '...'
|
||||||
|
}
|
||||||
|
ctx.fillText(dq, P + 20, y + 32)
|
||||||
|
y += 72
|
||||||
|
|
||||||
|
// ===== 底部品牌栏 =====
|
||||||
|
ctx.setStrokeStyle(c.line)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(P, y)
|
||||||
|
ctx.lineTo(W - P, y)
|
||||||
|
ctx.stroke()
|
||||||
|
y += 32
|
||||||
|
|
||||||
|
// 品牌名 + 副标语纵向排列
|
||||||
|
ctx.font = `bold 28px ${F}`
|
||||||
ctx.setFillStyle(WHITE)
|
ctx.setFillStyle(WHITE)
|
||||||
ctx.fillText('喝酒了么', P, y + 20)
|
ctx.fillText('喝了么', P, y + 20)
|
||||||
// 副标语
|
|
||||||
ctx.font = `normal 20px ${F}`
|
ctx.font = `normal 20px ${F}`
|
||||||
ctx.setFillStyle(DIM)
|
ctx.setFillStyle(DIM)
|
||||||
ctx.fillText('记录每一杯的故事', P, y + 50)
|
ctx.fillText('记录每一杯的故事', P, y + 50)
|
||||||
|
|
||||||
// 二维码占位
|
// 二维码占位(优化样式)
|
||||||
const qr = 80, qx = W - P - qr, qy = y
|
const qr = 80, qx = W - P - qr, qy = y + 2
|
||||||
ctx.setFillStyle(c.tagBg)
|
ctx.setFillStyle(c.tagBg)
|
||||||
this.rr(ctx, qx, qy, qr, qr, 8, 'fill')
|
this.rr(ctx, qx, qy, qr, qr, 10, 'fill')
|
||||||
ctx.setStrokeStyle(c.line)
|
ctx.setStrokeStyle(c.tagBorder)
|
||||||
ctx.setLineWidth(1)
|
ctx.setLineWidth(1.5)
|
||||||
this.rr(ctx, qx, qy, qr, qr, 8, 'stroke')
|
this.rr(ctx, qx, qy, qr, qr, 10, 'stroke')
|
||||||
ctx.font = `normal 18px ${F}`
|
ctx.font = `500 16px ${F}`
|
||||||
ctx.setFillStyle(DIM)
|
ctx.setFillStyle(GRAY)
|
||||||
const sw = ctx.measureText('扫码').width
|
const sw = ctx.measureText('扫码').width
|
||||||
ctx.fillText('扫码', qx + (qr - sw) / 2, qy + qr / 2 + 7)
|
ctx.fillText('扫码', qx + (qr - sw) / 2, qy + qr / 2 + 6)
|
||||||
|
|
||||||
|
// 计算实际内容高度(用于动态裁剪导出)
|
||||||
|
const contentH = qy + qr + P
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
ctx.draw(false, () => setTimeout(resolve, 600))
|
ctx.draw(false, () => setTimeout(() => resolve(contentH), 600))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -338,11 +384,14 @@ export default {
|
|||||||
return [3, 3, 3]
|
return [3, 3, 3]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出
|
// 导出(动态裁剪到实际内容区域)
|
||||||
exportImg() {
|
exportImg(contentH) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const cropH = Math.min(contentH || 1200, 1200)
|
||||||
uni.canvasToTempFilePath({
|
uni.canvasToTempFilePath({
|
||||||
canvasId: 'shareCanvas', quality: 1,
|
canvasId: 'shareCanvas', quality: 1,
|
||||||
|
x: 0, y: 0, width: 750, height: cropH,
|
||||||
|
destWidth: 750, destHeight: cropH,
|
||||||
success: res => resolve(res.tempFilePath),
|
success: res => resolve(res.tempFilePath),
|
||||||
fail: err => reject({ msg: '导出失败: ' + (err.errMsg || '') })
|
fail: err => reject({ msg: '导出失败: ' + (err.errMsg || '') })
|
||||||
}, this)
|
}, this)
|
||||||
@@ -368,11 +417,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
shareToFriend() { uni.showToast({ title: '点击右上角分享', icon: 'none' }) },
|
shareToFriend() { uni.showToast({ title: '点击右上角分享', icon: 'none' }) },
|
||||||
goHome() { uni.reLaunch({ url: '/pages/index/index' }) },
|
goHome() { uni.switchTab({ url: '/pages/index/index' }) },
|
||||||
goBack() { uni.navigateBack() }
|
goBack() { uni.navigateBack() }
|
||||||
},
|
},
|
||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
return { title: '今晚的酒局记录 - 喝酒了么', path: '/pages/index/index' }
|
return { title: '今晚的酒局记录 - 喝了么', path: '/pages/index/index' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+69
-99
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container home safe-bottom">
|
<view class="page-container home">
|
||||||
<!-- 顶部问候 -->
|
<!-- 顶部问候 -->
|
||||||
<view class="greeting-bar">
|
<view class="greeting-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<view class="greeting-left">
|
<view class="greeting-left">
|
||||||
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
||||||
<text class="greeting-sub">今晚喝一杯?</text>
|
<text class="greeting-sub">今晚喝一杯?</text>
|
||||||
</view>
|
</view>
|
||||||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
<image v-if="userAvatar" class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||||
|
<view v-else class="avatar avatar-placeholder">
|
||||||
|
<text class="avatar-placeholder-icon">👤</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 核心CTA - 首页最醒目位置 -->
|
<!-- 核心CTA - 首页最醒目位置 -->
|
||||||
@@ -81,21 +84,9 @@
|
|||||||
<text>过量饮酒有害健康 · 请理性记录</text>
|
<text>过量饮酒有害健康 · 请理性记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部导航占位 -->
|
<!-- 悬浮记录按钮 -->
|
||||||
<view class="tab-bar">
|
<view class="fab-record" @click="goRecord">
|
||||||
<view class="tab-item tab-active" @click="goHome">
|
<text class="fab-icon">+</text>
|
||||||
<text class="tab-icon">🏠</text>
|
|
||||||
<text class="tab-label">首页</text>
|
|
||||||
</view>
|
|
||||||
<view class="tab-center" @click="goRecord">
|
|
||||||
<view class="tab-fab">
|
|
||||||
<text class="tab-fab-icon">+</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="tab-item" @click="goProfile">
|
|
||||||
<text class="tab-icon">👤</text>
|
|
||||||
<text class="tab-label">我的</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 日期记录弹窗 -->
|
<!-- 日期记录弹窗 -->
|
||||||
@@ -141,14 +132,16 @@ export default {
|
|||||||
components: { DrinkCalendar },
|
components: { DrinkCalendar },
|
||||||
data() {
|
data() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
return {
|
return {
|
||||||
|
statusBarHeight: sysInfo.statusBarHeight || 25,
|
||||||
calendarYear: now.getFullYear(),
|
calendarYear: now.getFullYear(),
|
||||||
calendarMonth: now.getMonth() + 1,
|
calendarMonth: now.getMonth() + 1,
|
||||||
records: [],
|
records: [],
|
||||||
stats: {},
|
stats: {},
|
||||||
greeting: '',
|
greeting: '',
|
||||||
userName: '',
|
userName: '',
|
||||||
userAvatar: '/static/logo.png',
|
userAvatar: '',
|
||||||
showDayDetail: false,
|
showDayDetail: false,
|
||||||
selectedDate: '',
|
selectedDate: '',
|
||||||
selectedRecord: null
|
selectedRecord: null
|
||||||
@@ -181,7 +174,7 @@ export default {
|
|||||||
this.records = getRecords()
|
this.records = getRecords()
|
||||||
const user = getUserInfo()
|
const user = getUserInfo()
|
||||||
this.userName = user.nickname || '酒友'
|
this.userName = user.nickname || '酒友'
|
||||||
this.userAvatar = user.avatar || '/static/logo.png'
|
this.userAvatar = user.avatar || ''
|
||||||
this.greeting = getGreeting()
|
this.greeting = getGreeting()
|
||||||
this.stats = calcStats(this.records)
|
this.stats = calcStats(this.records)
|
||||||
},
|
},
|
||||||
@@ -207,11 +200,8 @@ export default {
|
|||||||
goRecord() {
|
goRecord() {
|
||||||
uni.navigateTo({ url: '/pages/record/record' })
|
uni.navigateTo({ url: '/pages/record/record' })
|
||||||
},
|
},
|
||||||
goHome() {
|
|
||||||
// 已在首页
|
|
||||||
},
|
|
||||||
goProfile() {
|
goProfile() {
|
||||||
uni.navigateTo({ url: '/pages/profile/profile' })
|
uni.switchTab({ url: '/pages/profile/profile' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +209,7 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.home {
|
.home {
|
||||||
padding: $sp-lg;
|
padding: $sp-md;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,50 +217,64 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: $sp-xl 0 $sp-lg;
|
padding: $sp-md 0 $sp-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.greeting-text {
|
.greeting-text {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $fs-xl;
|
font-size: $fs-lg;
|
||||||
font-weight: $fw-bold;
|
font-weight: $fw-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.greeting-sub {
|
.greeting-sub {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-xs;
|
||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
margin-top: $sp-xs;
|
margin-top: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 80rpx;
|
width: 72rpx;
|
||||||
height: 80rpx;
|
height: 72rpx;
|
||||||
border-radius: $radius-full;
|
border-radius: $radius-full;
|
||||||
border: 4rpx solid $amber-glow;
|
border: 4rpx solid $amber-glow;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
background: $bg-elevated;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 4rpx dashed rgba(232,168,56,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-section {
|
.calendar-section {
|
||||||
margin-bottom: $sp-lg;
|
margin-bottom: $sp-md;
|
||||||
padding: $sp-lg;
|
padding: $sp-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-row {
|
.stats-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $sp-sm;
|
gap: $sp-xs;
|
||||||
margin-bottom: $sp-lg;
|
margin-bottom: $sp-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $sp-lg $sp-sm;
|
padding: $sp-md $sp-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $fs-2xl;
|
font-size: $fs-xl;
|
||||||
margin-bottom: $sp-xs;
|
margin-bottom: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
@@ -279,11 +283,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.week-summary {
|
.week-summary {
|
||||||
margin-bottom: $sp-lg;
|
margin-bottom: $sp-md;
|
||||||
|
padding: $sp-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.week-header {
|
.week-header {
|
||||||
margin-bottom: $sp-lg;
|
margin-bottom: $sp-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.week-stats {
|
.week-stats {
|
||||||
@@ -297,9 +302,9 @@ export default {
|
|||||||
|
|
||||||
.week-stat-value {
|
.week-stat-value {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $fs-2xl;
|
font-size: $fs-xl;
|
||||||
font-weight: $fw-bold;
|
font-weight: $fw-bold;
|
||||||
margin-bottom: $sp-xs;
|
margin-bottom: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.week-stat-label {
|
.week-stat-label {
|
||||||
@@ -314,7 +319,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.health-ref {
|
.health-ref {
|
||||||
margin-top: $sp-lg;
|
margin-top: $sp-md;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 12rpx;
|
height: 12rpx;
|
||||||
background: $bg-elevated;
|
background: $bg-elevated;
|
||||||
@@ -339,10 +344,10 @@ export default {
|
|||||||
/* 核心CTA - 首页英雄区域 */
|
/* 核心CTA - 首页英雄区域 */
|
||||||
.hero-cta {
|
.hero-cta {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: $sp-lg;
|
margin-bottom: $sp-md;
|
||||||
border-radius: $radius-xl;
|
border-radius: $radius-lg;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: $sp-xl $sp-lg;
|
padding: $sp-md $sp-lg;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
@@ -381,11 +386,11 @@ export default {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $sp-lg;
|
gap: $sp-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-cta-emoji {
|
.hero-cta-emoji {
|
||||||
font-size: 72rpx;
|
font-size: 56rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-cta-text {
|
.hero-cta-text {
|
||||||
@@ -394,7 +399,7 @@ export default {
|
|||||||
|
|
||||||
.hero-cta-title {
|
.hero-cta-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $fs-2xl;
|
font-size: $fs-xl;
|
||||||
font-weight: $fw-black;
|
font-weight: $fw-black;
|
||||||
color: $text-on-amber;
|
color: $text-on-amber;
|
||||||
letter-spacing: -1rpx;
|
letter-spacing: -1rpx;
|
||||||
@@ -402,13 +407,13 @@ export default {
|
|||||||
|
|
||||||
.hero-cta-sub {
|
.hero-cta-sub {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-xs;
|
||||||
color: rgba(11, 11, 20, 0.6);
|
color: rgba(11, 11, 20, 0.6);
|
||||||
margin-top: $sp-xs;
|
margin-top: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-cta-arrow {
|
.hero-cta-arrow {
|
||||||
font-size: 56rpx;
|
font-size: 44rpx;
|
||||||
color: $text-on-amber;
|
color: $text-on-amber;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-weight: $fw-bold;
|
font-weight: $fw-bold;
|
||||||
@@ -416,58 +421,18 @@ export default {
|
|||||||
|
|
||||||
.bottom-health-tip {
|
.bottom-health-tip {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $sp-lg 0;
|
padding: $sp-sm 0;
|
||||||
padding-bottom: 160rpx;
|
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
|
||||||
font-size: $fs-xs;
|
font-size: $fs-xs;
|
||||||
color: $text-tertiary;
|
color: $text-tertiary;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部TabBar */
|
/* 悬浮记录按钮 */
|
||||||
.tab-bar {
|
.fab-record {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
right: 40rpx;
|
||||||
left: 0;
|
bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||||
right: 0;
|
|
||||||
height: 120rpx;
|
|
||||||
background: $bg-card;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
border-top: 2rpx solid rgba(255,255,255,0.04);
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: $sp-xs;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-icon {
|
|
||||||
font-size: $fs-xl;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-label {
|
|
||||||
font-size: $fs-xs;
|
|
||||||
color: $text-tertiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-active {
|
|
||||||
.tab-icon { opacity: 1; }
|
|
||||||
.tab-label { color: $amber; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-center {
|
|
||||||
position: relative;
|
|
||||||
top: -20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-fab {
|
|
||||||
width: 100rpx;
|
width: 100rpx;
|
||||||
height: 100rpx;
|
height: 100rpx;
|
||||||
background: linear-gradient(135deg, $amber-light, $amber-deep);
|
background: linear-gradient(135deg, $amber-light, $amber-deep);
|
||||||
@@ -476,9 +441,14 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: $shadow-amber;
|
box-shadow: $shadow-amber;
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-fab-icon {
|
.fab-icon {
|
||||||
font-size: $fs-2xl;
|
font-size: $fs-2xl;
|
||||||
font-weight: $fw-bold;
|
font-weight: $fw-bold;
|
||||||
color: $text-on-amber;
|
color: $text-on-amber;
|
||||||
|
|||||||
+296
-18
@@ -6,7 +6,7 @@
|
|||||||
<view class="brand-icon">
|
<view class="brand-icon">
|
||||||
<text class="brand-emoji">🍻</text>
|
<text class="brand-emoji">🍻</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="brand-name">喝酒了么</text>
|
<text class="brand-name">喝了么</text>
|
||||||
<text class="brand-slogan">让每一杯酒都有迹可循</text>
|
<text class="brand-slogan">让每一杯酒都有迹可循</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -27,11 +27,42 @@
|
|||||||
<button class="btn-primary notice-btn" @click="acceptNotice">我已了解</button>
|
<button class="btn-primary notice-btn" @click="acceptNotice">我已了解</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 主登录按钮 -->
|
<!-- 主登录区域 -->
|
||||||
<view v-else class="login-actions">
|
<view v-else class="login-actions">
|
||||||
<button class="btn-cta login-btn" @click="handleLogin">
|
<!-- 头像 + 昵称填写(微信新规范) -->
|
||||||
<text class="login-btn-icon">📱</text>
|
<view class="profile-form card">
|
||||||
<text class="login-btn-text">微信一键登录</text>
|
<view class="form-title-row">
|
||||||
|
<text class="form-title">完善你的资料</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 头像选择 -->
|
||||||
|
<view class="avatar-row">
|
||||||
|
<text class="form-label">头像</text>
|
||||||
|
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||||
|
<image v-if="avatarUrl" class="avatar-preview" :src="avatarUrl" mode="aspectFill"></image>
|
||||||
|
<view v-else class="avatar-placeholder">
|
||||||
|
<text class="avatar-placeholder-icon">👤</text>
|
||||||
|
</view>
|
||||||
|
<text class="avatar-edit">{{ avatarUrl ? '换头像' : '选头像' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 昵称输入 -->
|
||||||
|
<view class="nickname-row">
|
||||||
|
<text class="form-label">昵称</text>
|
||||||
|
<input
|
||||||
|
type="nickname"
|
||||||
|
class="nickname-input"
|
||||||
|
v-model="nickname"
|
||||||
|
placeholder="点击获取微信昵称"
|
||||||
|
@blur="onNicknameBlur"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 确认登录 -->
|
||||||
|
<button class="btn-cta login-btn" :loading="loginLoading" :disabled="!agreed || loginLoading" @click="handleLogin">
|
||||||
|
<text class="login-btn-text">{{ loginLoading ? '登录中...' : '完成并进入' }}</text>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<view class="login-divider">
|
<view class="login-divider">
|
||||||
@@ -40,9 +71,20 @@
|
|||||||
<view class="login-divider-line"></view>
|
<view class="login-divider-line"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<button class="btn-secondary" @click="handleGuest">
|
<button class="btn-secondary" :disabled="loginLoading" @click="handleGuest">
|
||||||
先看看再说
|
先看看再说
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- 协议勾选 -->
|
||||||
|
<view class="agreement-check" @click="agreed = !agreed">
|
||||||
|
<view class="check-box" :class="{ 'check-box-active': agreed }">
|
||||||
|
<text v-if="agreed" class="check-mark">✓</text>
|
||||||
|
</view>
|
||||||
|
<text class="agreement-check-text">我已阅读并同意</text>
|
||||||
|
<text class="agreement-check-text text-amber" @click.stop="openAgreement('user')">《用户协议》</text>
|
||||||
|
<text class="agreement-check-text">和</text>
|
||||||
|
<text class="agreement-check-text text-amber" @click.stop="openAgreement('privacy')">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -51,7 +93,7 @@
|
|||||||
<view class="pref-card card-elevated">
|
<view class="pref-card card-elevated">
|
||||||
<view class="pref-header">
|
<view class="pref-header">
|
||||||
<text class="text-h2">你的饮酒偏好</text>
|
<text class="text-h2">你的饮酒偏好</text>
|
||||||
<text class="text-caption">可跳过,稍后在设置中修改</text>
|
<text class="text-caption">选择你常喝的酒类(可多选)</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="pref-section">
|
<view class="pref-section">
|
||||||
@@ -86,13 +128,13 @@
|
|||||||
|
|
||||||
<view class="pref-actions">
|
<view class="pref-actions">
|
||||||
<button class="btn-text" @click="skipPreferences">跳过</button>
|
<button class="btn-text" @click="skipPreferences">跳过</button>
|
||||||
<button class="btn-primary" @click="savePreferences">保存并进入</button>
|
<button class="btn-primary" :loading="prefSaving" @click="savePreferences">{{ selectedCategories.length > 0 || selectedFrequency ? '保存并进入' : '进入首页' }}</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部协议 -->
|
<!-- 底部协议(非登录状态时显示) -->
|
||||||
<view class="agreement">
|
<view v-if="showNotice" class="agreement">
|
||||||
<text class="text-tiny">登录即代表同意</text>
|
<text class="text-tiny">登录即代表同意</text>
|
||||||
<text class="text-tiny text-amber">《用户协议》</text>
|
<text class="text-tiny text-amber">《用户协议》</text>
|
||||||
<text class="text-tiny">和</text>
|
<text class="text-tiny">和</text>
|
||||||
@@ -109,6 +151,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
showNotice: true,
|
showNotice: true,
|
||||||
showPreferences: false,
|
showPreferences: false,
|
||||||
|
loginLoading: false,
|
||||||
|
prefSaving: false,
|
||||||
|
agreed: false,
|
||||||
|
avatarUrl: '',
|
||||||
|
nickname: '',
|
||||||
drinkCategories: DRINK_CATEGORIES,
|
drinkCategories: DRINK_CATEGORIES,
|
||||||
selectedCategories: [],
|
selectedCategories: [],
|
||||||
selectedFrequency: '',
|
selectedFrequency: '',
|
||||||
@@ -124,15 +171,94 @@ export default {
|
|||||||
acceptNotice() {
|
acceptNotice() {
|
||||||
this.showNotice = false
|
this.showNotice = false
|
||||||
},
|
},
|
||||||
handleLogin() {
|
|
||||||
// Mock登录,直接显示偏好设置
|
// ===== 微信新规范:头像选择 =====
|
||||||
|
onChooseAvatar(e) {
|
||||||
|
if (e.detail && e.detail.avatarUrl) {
|
||||||
|
this.avatarUrl = e.detail.avatarUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 微信新规范:昵称输入 =====
|
||||||
|
onNicknameBlur(e) {
|
||||||
|
// 用户输入内容会自动更新到 v-model,这里可以做额外处理
|
||||||
|
if (e.detail && e.detail.value) {
|
||||||
|
this.nickname = e.detail.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 确认登录 =====
|
||||||
|
async handleLogin() {
|
||||||
|
if (this.loginLoading) return
|
||||||
|
if (!this.agreed) {
|
||||||
|
uni.showToast({ title: '请先同意用户协议和隐私政策', icon: 'none', duration: 2000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loginLoading = true
|
||||||
|
try {
|
||||||
|
// 获取微信登录code(用于后续服务端鉴权)
|
||||||
|
const loginRes = await this.wxLogin().catch(() => null)
|
||||||
|
|
||||||
|
const userInfo = {
|
||||||
|
id: `user_${Date.now()}`,
|
||||||
|
nickname: this.nickname.trim() || '酒友',
|
||||||
|
avatar: this.avatarUrl || '',
|
||||||
|
wxCode: (loginRes && loginRes.code) || '',
|
||||||
|
joinedAt: new Date().toISOString().slice(0, 10),
|
||||||
|
preferences: null
|
||||||
|
}
|
||||||
|
// 暂存用户信息,登录完成标记延迟到 finishLogin
|
||||||
|
uni.setStorageSync('user_info', JSON.stringify(userInfo))
|
||||||
|
// 显示偏好设置(此时不算正式登录)
|
||||||
this.showPreferences = true
|
this.showPreferences = true
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('登录失败,降级为本地登录:', e)
|
||||||
|
const userInfo = {
|
||||||
|
id: `user_${Date.now()}`,
|
||||||
|
nickname: this.nickname.trim() || '酒友',
|
||||||
|
avatar: '',
|
||||||
|
joinedAt: new Date().toISOString().slice(0, 10),
|
||||||
|
preferences: null
|
||||||
|
}
|
||||||
|
// 暂存用户信息,登录完成标记延迟到 finishLogin
|
||||||
|
uni.setStorageSync('user_info', JSON.stringify(userInfo))
|
||||||
|
this.showPreferences = true
|
||||||
|
} finally {
|
||||||
|
this.loginLoading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 微信login获取code
|
||||||
|
wxLogin() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: res => resolve(res),
|
||||||
|
fail: err => reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 游客模式 =====
|
||||||
handleGuest() {
|
handleGuest() {
|
||||||
|
if (this.loginLoading) return
|
||||||
uni.setStorageSync('is_logged_in', 'true')
|
uni.setStorageSync('is_logged_in', 'true')
|
||||||
|
uni.setStorageSync('is_guest', 'true')
|
||||||
uni.setStorageSync('is_first_launch', 'false')
|
uni.setStorageSync('is_first_launch', 'false')
|
||||||
uni.reLaunch({ url: '/pages/index/index' })
|
// 游客也设置一个基础用户信息
|
||||||
|
const guestInfo = {
|
||||||
|
id: `guest_${Date.now()}`,
|
||||||
|
nickname: '游客模式',
|
||||||
|
avatar: '',
|
||||||
|
isGuest: true,
|
||||||
|
joinedAt: new Date().toISOString().slice(0, 10),
|
||||||
|
preferences: null
|
||||||
|
}
|
||||||
|
uni.setStorageSync('user_info', JSON.stringify(guestInfo))
|
||||||
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===== 偏好选择 =====
|
||||||
toggleCategory(id) {
|
toggleCategory(id) {
|
||||||
const idx = this.selectedCategories.indexOf(id)
|
const idx = this.selectedCategories.indexOf(id)
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
@@ -141,22 +267,38 @@ export default {
|
|||||||
this.selectedCategories.push(id)
|
this.selectedCategories.push(id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
skipPreferences() {
|
skipPreferences() {
|
||||||
this.finishLogin()
|
this.finishLogin()
|
||||||
},
|
},
|
||||||
savePreferences() {
|
|
||||||
|
async savePreferences() {
|
||||||
|
if (this.prefSaving) return
|
||||||
|
this.prefSaving = true
|
||||||
|
try {
|
||||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||||
user.preferences = {
|
user.preferences = {
|
||||||
favoriteCategories: this.selectedCategories,
|
favoriteCategories: this.selectedCategories,
|
||||||
frequency: this.selectedFrequency
|
frequency: this.selectedFrequency
|
||||||
}
|
}
|
||||||
uni.setStorageSync('user_info', JSON.stringify(user))
|
uni.setStorageSync('user_info', JSON.stringify(user))
|
||||||
|
await new Promise(r => setTimeout(r, 300)) // 短暂反馈
|
||||||
this.finishLogin()
|
this.finishLogin()
|
||||||
|
} finally {
|
||||||
|
this.prefSaving = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
finishLogin() {
|
finishLogin() {
|
||||||
uni.setStorageSync('is_logged_in', 'true')
|
uni.setStorageSync('is_logged_in', 'true')
|
||||||
uni.setStorageSync('is_first_launch', 'false')
|
uni.setStorageSync('is_first_launch', 'false')
|
||||||
uni.reLaunch({ url: '/pages/index/index' })
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 协议查看 =====
|
||||||
|
openAgreement(type) {
|
||||||
|
const title = type === 'user' ? '用户协议' : '隐私政策'
|
||||||
|
uni.showToast({ title: `${title}页面开发中`, icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,6 +405,93 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 资料表单 */
|
||||||
|
.profile-form {
|
||||||
|
padding: $sp-xl;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title-row {
|
||||||
|
margin-bottom: $sp-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: $fs-lg;
|
||||||
|
font-weight: $fw-bold;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $sp-md 0;
|
||||||
|
border-bottom: 2rpx solid rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-sm;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: normal;
|
||||||
|
&::after { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
border: 3rpx solid $amber;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
border: 3rpx dashed rgba(232,168,56,0.4);
|
||||||
|
background: $bg-elevated;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-edit {
|
||||||
|
font-size: $fs-sm;
|
||||||
|
color: $amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-lg;
|
||||||
|
padding: $sp-md 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: $fs-base;
|
||||||
|
color: $text-secondary;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: $fs-base;
|
||||||
|
color: $text-primary;
|
||||||
|
padding: $sp-xs 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 登录按钮 */
|
/* 登录按钮 */
|
||||||
.login-actions {
|
.login-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -275,10 +504,10 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: $sp-sm;
|
gap: $sp-sm;
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn-icon {
|
&[disabled] {
|
||||||
font-size: $fs-xl;
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-btn-text {
|
.login-btn-text {
|
||||||
@@ -303,6 +532,43 @@ export default {
|
|||||||
color: $text-tertiary;
|
color: $text-tertiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 协议勾选 */
|
||||||
|
.agreement-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $sp-xs;
|
||||||
|
padding-top: $sp-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-box {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
border: 2rpx solid $text-tertiary;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all $duration-fast $ease-out;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-box-active {
|
||||||
|
background: $amber;
|
||||||
|
border-color: $amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-mark {
|
||||||
|
font-size: $fs-xs;
|
||||||
|
color: $text-on-amber;
|
||||||
|
font-weight: $fw-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-check-text {
|
||||||
|
font-size: $fs-xs;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
/* 偏好设置弹层 */
|
/* 偏好设置弹层 */
|
||||||
.pref-overlay {
|
.pref-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -315,12 +581,14 @@ export default {
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
padding: $sp-lg;
|
padding: $sp-lg;
|
||||||
|
animation: fadeIn $duration-base $ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pref-card {
|
.pref-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: $sp-xl;
|
padding: $sp-xl;
|
||||||
border-radius: $radius-xl $radius-xl 0 0;
|
border-radius: $radius-xl $radius-xl 0 0;
|
||||||
|
animation: slideUp $duration-base $ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pref-header {
|
.pref-header {
|
||||||
@@ -391,4 +659,14 @@ export default {
|
|||||||
gap: $sp-xs;
|
gap: $sp-xs;
|
||||||
padding: $sp-lg 0;
|
padding: $sp-lg 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { transform: translateY(100%); }
|
||||||
|
to { transform: translateY(0); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+64
-77
@@ -3,7 +3,10 @@
|
|||||||
<!-- 用户头部 -->
|
<!-- 用户头部 -->
|
||||||
<view class="profile-header">
|
<view class="profile-header">
|
||||||
<view class="profile-glow"></view>
|
<view class="profile-glow"></view>
|
||||||
<image class="profile-avatar" :src="user.avatar" mode="aspectFill"></image>
|
<image v-if="user.avatar" class="profile-avatar" :src="user.avatar" mode="aspectFill"></image>
|
||||||
|
<view v-else class="profile-avatar profile-avatar-placeholder">
|
||||||
|
<text class="profile-avatar-icon">👤</text>
|
||||||
|
</view>
|
||||||
<text class="profile-name">{{ user.nickname }}</text>
|
<text class="profile-name">{{ user.nickname }}</text>
|
||||||
<text class="profile-joined">加入于 {{ user.joinedAt || '2026' }}</text>
|
<text class="profile-joined">加入于 {{ user.joinedAt || '2026' }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -73,26 +76,19 @@
|
|||||||
<view class="divider"></view>
|
<view class="divider"></view>
|
||||||
<view class="settings-item" @click="showComingSoon('关于')">
|
<view class="settings-item" @click="showComingSoon('关于')">
|
||||||
<text class="settings-icon">ℹ️</text>
|
<text class="settings-icon">ℹ️</text>
|
||||||
<text class="settings-name">关于喝酒了么</text>
|
<text class="settings-name">关于喝了么</text>
|
||||||
<text class="settings-arrow">›</text>
|
<text class="settings-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部TabBar -->
|
<!-- 登出/登录按钮 -->
|
||||||
<view class="tab-bar">
|
<view class="logout-area">
|
||||||
<view class="tab-item" @click="goHome">
|
<button v-if="isGuest" class="btn-ghost logout-btn" @click="goLogin">
|
||||||
<text class="tab-icon">🏠</text>
|
登录账号,同步数据
|
||||||
<text class="tab-label">首页</text>
|
</button>
|
||||||
</view>
|
<button v-else class="btn-text logout-btn" @click="handleLogout">
|
||||||
<view class="tab-center" @click="goRecord">
|
退出登录
|
||||||
<view class="tab-fab">
|
</button>
|
||||||
<text class="tab-fab-icon">+</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="tab-item tab-active" @click="goProfile">
|
|
||||||
<text class="tab-icon">👤</text>
|
|
||||||
<text class="tab-label">我的</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 成就详情弹窗 -->
|
<!-- 成就详情弹窗 -->
|
||||||
@@ -124,7 +120,8 @@ export default {
|
|||||||
stats: {},
|
stats: {},
|
||||||
achievementList: [],
|
achievementList: [],
|
||||||
showDetail: false,
|
showDetail: false,
|
||||||
detailAchievement: {}
|
detailAchievement: {},
|
||||||
|
isGuest: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -135,6 +132,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
|
this.checkLoginState()
|
||||||
this.loadData()
|
this.loadData()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -152,13 +150,38 @@ export default {
|
|||||||
uni.showToast({ title: `${name}即将上线`, icon: 'none' })
|
uni.showToast({ title: `${name}即将上线`, icon: 'none' })
|
||||||
},
|
},
|
||||||
goHome() {
|
goHome() {
|
||||||
uni.reLaunch({ url: '/pages/index/index' })
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
},
|
},
|
||||||
goRecord() {
|
goRecord() {
|
||||||
uni.navigateTo({ url: '/pages/record/record' })
|
uni.navigateTo({ url: '/pages/record/record' })
|
||||||
},
|
},
|
||||||
goProfile() {
|
checkLoginState() {
|
||||||
// 已在个人页面
|
const isLoggedIn = uni.getStorageSync('is_logged_in')
|
||||||
|
const isGuest = uni.getStorageSync('is_guest')
|
||||||
|
if (!isLoggedIn || isLoggedIn !== 'true') {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.isGuest = isGuest === 'true'
|
||||||
|
},
|
||||||
|
handleLogout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '退出登录',
|
||||||
|
content: '退出后本地数据将保留,重新登录后可同步',
|
||||||
|
confirmText: '退出',
|
||||||
|
confirmColor: '#FF6B6B',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.removeStorageSync('is_logged_in')
|
||||||
|
uni.removeStorageSync('is_guest')
|
||||||
|
uni.removeStorageSync('user_info')
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goLogin() {
|
||||||
|
uni.navigateTo({ url: '/pages/login/login' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +190,7 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.profile-page {
|
.profile-page {
|
||||||
padding: $sp-lg;
|
padding: $sp-lg;
|
||||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 头部 */
|
/* 头部 */
|
||||||
@@ -199,6 +222,20 @@ export default {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-avatar-placeholder {
|
||||||
|
background: $bg-elevated;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: rgba(232,168,56,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-name {
|
.profile-name {
|
||||||
font-size: $fs-xl;
|
font-size: $fs-xl;
|
||||||
font-weight: $fw-bold;
|
font-weight: $fw-bold;
|
||||||
@@ -309,65 +346,15 @@ export default {
|
|||||||
color: $text-tertiary;
|
color: $text-tertiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TabBar */
|
/* 登出区域 */
|
||||||
.tab-bar {
|
.logout-area {
|
||||||
position: fixed;
|
padding: $sp-xl 0 $sp-3xl;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 120rpx;
|
|
||||||
background: $bg-card;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
border-top: 2rpx solid rgba(255,255,255,0.04);
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: $sp-xs;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-icon {
|
|
||||||
font-size: $fs-xl;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-label {
|
|
||||||
font-size: $fs-xs;
|
|
||||||
color: $text-tertiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-active {
|
|
||||||
.tab-icon { opacity: 1; }
|
|
||||||
.tab-label { color: $amber; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-center {
|
|
||||||
position: relative;
|
|
||||||
top: -20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-fab {
|
|
||||||
width: 100rpx;
|
|
||||||
height: 100rpx;
|
|
||||||
background: linear-gradient(135deg, $amber-light, $amber-deep);
|
|
||||||
border-radius: $radius-full;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: $shadow-amber;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-fab-icon {
|
.logout-btn {
|
||||||
font-size: $fs-2xl;
|
min-width: 300rpx;
|
||||||
font-weight: $fw-bold;
|
|
||||||
color: $text-on-amber;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 成就详情弹窗 */
|
/* 成就详情弹窗 */
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 313 B |
Binary file not shown.
|
After Width: | Height: | Size: 310 B |
Binary file not shown.
|
After Width: | Height: | Size: 312 B |
Binary file not shown.
|
After Width: | Height: | Size: 310 B |
@@ -1,5 +1,5 @@
|
|||||||
/* ==========================================
|
/* ==========================================
|
||||||
* 喝酒了么 - 设计令牌 (Design Tokens)
|
* 喝了么 - 设计令牌 (Design Tokens)
|
||||||
* 设计概念: 琥珀夜光 Amber Night
|
* 设计概念: 琥珀夜光 Amber Night
|
||||||
* ========================================== */
|
* ========================================== */
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
// 本文件用于,使用JQL语法操作项目关联的uniCloud空间的数据库,方便开发调试和远程数据库管理
|
|
||||||
// 编写clientDB的js API(也支持常规js语法,比如var),可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法
|
|
||||||
// 可以全部运行,也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码
|
|
||||||
// 如果文档中存在多条JQL语句,只有最后一条语句生效
|
|
||||||
// 如果混写了普通js,最后一条语句需是数据库操作语句
|
|
||||||
// 此处代码运行不受DB Schema的权限控制,移植代码到实际业务中注意在schema中配好permission
|
|
||||||
// 不支持clientDB的action
|
|
||||||
// 数据库查询有最大返回条数限制,详见:https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit
|
|
||||||
// 详细JQL语法,请参考:https://uniapp.dcloud.net.cn/uniCloud/jql.html
|
|
||||||
|
|
||||||
// 下面示例查询uni-id-users表的所有数据
|
|
||||||
db.collection('uni-id-users').get();
|
|
||||||
Reference in New Issue
Block a user