docs(api): 更新API接口文档并调整应用配置
- 添加完整的后端接口文档,包含用户、打卡记录、统计等模块 - 修复应用名称从"喝酒了么"统一更正为"喝了么" - 重构App.vue中的启动逻辑,增加数据迁移和路由守卫功能 - 调整DrinkCalendar组件的UI间距和尺寸参数 - 优化DrinkCard组件的品牌展示文案 - 修改manifest.json中的应用描述和微信小程序配置 - 调整pages.json中的页面路径顺序和tabBar配置 - 更新全局样式文件中的应用名称标识 - 修改mock数据中的用户头像默认值 - 优化卡片导出功能的绘制逻辑和视觉效果
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch() {
|
||||
console.log('喝酒了么 App Launch')
|
||||
console.log('喝了么 App Launch')
|
||||
// 初始化Mock数据(仅首次)
|
||||
const hasData = uni.getStorageSync('drink_records')
|
||||
if (!hasData) {
|
||||
@@ -10,19 +10,52 @@ export default {
|
||||
console.log('Mock数据已初始化')
|
||||
})
|
||||
}
|
||||
// 数据迁移:清除旧版 logo.png 头像引用
|
||||
this.migrateUserData()
|
||||
// 启动路由守卫
|
||||
this.checkLaunchRoute()
|
||||
},
|
||||
onShow() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide() {
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
/* ==========================================
|
||||
* 喝酒了么 - 全局样式
|
||||
* 喝了么 - 全局样式
|
||||
* 设计概念: 琥珀夜光 Amber Night
|
||||
* ========================================== */
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* 喝酒了么 - 常量定义 */
|
||||
/* 喝了么 - 常量定义 */
|
||||
|
||||
// 酒类分类
|
||||
export const DRINK_CATEGORIES = [
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
/* 喝酒了么 - Mock 数据 */
|
||||
/* 喝了么 - Mock 数据 */
|
||||
import { formatDate, calcStandardCups, unitToMl } from './utils'
|
||||
|
||||
// 模拟用户数据
|
||||
export const MOCK_USER = {
|
||||
id: 'user_001',
|
||||
nickname: '酒友小王',
|
||||
avatar: '/static/logo.png',
|
||||
avatar: '',
|
||||
preferences: {
|
||||
favoriteCategories: ['baijiu', 'beer'],
|
||||
frequency: 'often'
|
||||
@@ -113,7 +113,7 @@ export function generateMockRecords() {
|
||||
}
|
||||
|
||||
// 初始化并缓存Mock数据
|
||||
export function initMockData() {自己看
|
||||
export function initMockData() {
|
||||
const records = generateMockRecords()
|
||||
uni.setStorageSync('drink_records', JSON.stringify(records))
|
||||
uni.setStorageSync('user_info', JSON.stringify(MOCK_USER))
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* 喝酒了么 - 工具函数 */
|
||||
/* 喝了么 - 工具函数 */
|
||||
|
||||
/**
|
||||
* 标准杯换算引擎
|
||||
|
||||
@@ -94,12 +94,12 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $sp-lg;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.cal-nav {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -123,20 +123,20 @@ export default {
|
||||
.cal-weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
margin-bottom: $sp-sm;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
|
||||
.cal-weekday {
|
||||
text-align: center;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
padding: $sp-xs 0;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
.cal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: $sp-xs;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.cal-cell {
|
||||
@@ -145,7 +145,7 @@ export default {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $radius-md;
|
||||
border-radius: $radius-sm;
|
||||
position: relative;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
@@ -163,17 +163,17 @@ export default {
|
||||
}
|
||||
|
||||
.cal-day-num {
|
||||
font-size: $fs-sm;
|
||||
font-size: $fs-xs;
|
||||
font-weight: $fw-medium;
|
||||
color: $text-primary;
|
||||
font-family: $font-num;
|
||||
}
|
||||
|
||||
.cal-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
border-radius: $radius-full;
|
||||
margin-top: $sp-xs;
|
||||
margin-top: 4rpx;
|
||||
|
||||
&.cal-dot-drank {
|
||||
background: $amber;
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
<!-- 统一底部品牌 + 二维码 -->
|
||||
<view class="card-brand-bar">
|
||||
<view class="brand-left">
|
||||
<text class="brand-name">🍻 喝酒了么</text>
|
||||
<text class="brand-name">🍻 喝了么</text>
|
||||
<text class="brand-slogan">记录每一杯的故事</text>
|
||||
</view>
|
||||
<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 调用即可
|
||||
+13
-82
@@ -1,70 +1,10 @@
|
||||
{
|
||||
"name": "喝酒了么",
|
||||
"appid": "__UNI__871D23D",
|
||||
"description": "让每一杯酒都有迹可循",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {},
|
||||
"sdkConfigs": {}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {},
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
}
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"vueVersion": "3"
|
||||
}
|
||||
{
|
||||
"name" : "hekjiu",
|
||||
"name" : "喝了么",
|
||||
"appid" : "__UNI__871D23D",
|
||||
"description" : "",
|
||||
"description" : "让每一杯酒都有迹可循",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
@@ -75,45 +15,36 @@
|
||||
"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\"/>"
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"appid" : "wx16e19044f4f57f71",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
"urlCheck" : false,
|
||||
"es6" : true,
|
||||
"postcss" : true,
|
||||
"minified" : true
|
||||
},
|
||||
"usingComponents" : true
|
||||
"usingComponents" : true,
|
||||
"permission" : {},
|
||||
"optimization" : {
|
||||
"subPackages" : true
|
||||
}
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
|
||||
+26
-23
@@ -7,6 +7,13 @@
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/onboarding/onboarding",
|
||||
"style": {
|
||||
@@ -34,39 +41,35 @@
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "喝酒了么",
|
||||
"navigationBarTitleText": "喝了么",
|
||||
"navigationBarBackgroundColor": "#0B0B14",
|
||||
"backgroundColor": "#0B0B14",
|
||||
"backgroundColorTop": "#0B0B14",
|
||||
"backgroundColorBottom": "#0B0B14"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "uni-app"
|
||||
"tabBar": {
|
||||
"color": "#4A4A62",
|
||||
"selectedColor": "#E8A838",
|
||||
"backgroundColor": "#151520",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/tab-home.png",
|
||||
"selectedIconPath": "static/tab-home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的",
|
||||
"iconPath": "static/tab-profile.png",
|
||||
"selectedIconPath": "static/tab-profile-active.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
|
||||
+136
-87
@@ -87,8 +87,8 @@ export default {
|
||||
if (this.saving) return
|
||||
this.saving = true
|
||||
try {
|
||||
await this.drawExportCard()
|
||||
const path = await this.exportImg()
|
||||
const contentH = await this.drawExportCard()
|
||||
const path = await this.exportImg(contentH)
|
||||
await this.saveImg(path)
|
||||
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
} catch (e) {
|
||||
@@ -105,64 +105,92 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// ===== 简洁导出卡片绘制 =====
|
||||
// ===== 海报导出绘制(优化版) =====
|
||||
async drawExportCard() {
|
||||
const ctx = uni.createCanvasContext('shareCanvas', this)
|
||||
const W = 750, H = 1200
|
||||
const P = 56 // 全局padding增大
|
||||
const rec = this.record
|
||||
const P = 50 // padding
|
||||
const tpl = this.currentTemplate
|
||||
|
||||
// 配色
|
||||
// 模板配色体系
|
||||
const TH = {
|
||||
amber: { bg: '#111111', line: '#2A2218', accent: '#E8A838', tagBg: '#1E1A14' },
|
||||
aurora: { bg: '#0E0E16', line: '#1E1E2E', accent: '#5EC6A0', tagBg: '#141420' },
|
||||
film: { bg: '#100F0D', line: '#252218', accent: '#C8B48C', tagBg: '#1A1814' }
|
||||
amber: { bg: '#111111', bgGrad: '#1A1510', line: '#2A2218', accent: '#E8A838', accentLight: '#F5C963', tagBg: '#1E1A14', tagBorder: '#3A3228' },
|
||||
aurora: { bg: '#0E0E16', bgGrad: '#141420', line: '#1E1E2E', accent: '#5EC6A0', accentLight: '#8EDCBE', tagBg: '#141420', tagBorder: '#2A2A3E' },
|
||||
film: { bg: '#100F0D', bgGrad: '#181610', line: '#252218', accent: '#C8B48C', accentLight: '#DED0B0', tagBg: '#1A1814', tagBorder: '#352E22' }
|
||||
}
|
||||
const c = TH[tpl] || TH.amber
|
||||
const WHITE = '#F0F0F0'
|
||||
const GRAY = '#999999'
|
||||
const DIM = '#666666'
|
||||
const F = 'PingFang SC'
|
||||
const WHITE = '#F2F2F2'
|
||||
const SUB = '#B0B0B0'
|
||||
const GRAY = '#888888'
|
||||
const DIM = '#5A5A5A'
|
||||
const F = '-apple-system, PingFang SC, Helvetica Neue, sans-serif'
|
||||
|
||||
// 背景
|
||||
// ===== 背景绘制 =====
|
||||
ctx.setFillStyle(c.bg)
|
||||
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.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)
|
||||
ctx.fillText(rec.date || '', P, y + 20)
|
||||
// ===== 顶部行:日期药丸 + 标准杯 =====
|
||||
// 日期药丸样式
|
||||
const dateStr = rec.date || ''
|
||||
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)
|
||||
ctx.font = `800 44px ${F}`
|
||||
ctx.setFillStyle(c.accent)
|
||||
const nw = ctx.measureText(num).width
|
||||
ctx.fillText(num, W - P - nw - ctx.measureText(' 标准杯').width - 6, y + 24)
|
||||
ctx.font = `normal 22px ${F}`
|
||||
ctx.font = `normal 18px ${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.fillText(' 标准杯', W - P - ctx.measureText(' 标准杯').width, y + 22)
|
||||
y += 50
|
||||
ctx.fillText('标准杯', cupX + (cupBlockW - lw) / 2, y + 34)
|
||||
y += 58
|
||||
|
||||
// === 照片 ===
|
||||
// ===== 照片区域 =====
|
||||
const photos = rec.photos || []
|
||||
if (photos.length > 0) {
|
||||
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 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
|
||||
}
|
||||
|
||||
// === 酒水信息行 ===
|
||||
// ===== 酒水信息行 =====
|
||||
const drinks = rec.drinks || []
|
||||
if (drinks.length > 0) {
|
||||
ctx.font = `bold 28px ${F}`
|
||||
ctx.font = `bold 30px ${F}`
|
||||
ctx.setFillStyle(WHITE)
|
||||
let line = ''
|
||||
drinks.forEach(d => {
|
||||
@@ -170,91 +198,109 @@ export default {
|
||||
const nm = cat ? cat.name : '酒水'
|
||||
line += (line ? ' · ' : '') + `${nm} ${d.brand} · ${d.standardCups}杯`
|
||||
})
|
||||
ctx.fillText(line, P, y + 22)
|
||||
y += 42
|
||||
ctx.fillText(line, P, y + 24)
|
||||
y += 48
|
||||
}
|
||||
|
||||
// === 配餐 + 感受 标签 ===
|
||||
// ===== 配餐 + 感受 胶囊标签 =====
|
||||
let tags = []
|
||||
let tagTypes = [] // 'food' | 'feeling'
|
||||
if (rec.food) {
|
||||
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) {
|
||||
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) {
|
||||
let tx = P
|
||||
ctx.font = `normal 22px ${F}`
|
||||
tags.forEach(tag => {
|
||||
const tw = ctx.measureText(tag).width + 24
|
||||
tags.forEach((tag, i) => {
|
||||
const tw = ctx.measureText(tag).width + 32
|
||||
const th = 36
|
||||
// 胶囊背景+边框
|
||||
ctx.setFillStyle(c.tagBg)
|
||||
this.rr(ctx, tx, y, tw, 32, 6, 'fill')
|
||||
ctx.setFillStyle(GRAY)
|
||||
ctx.fillText(tag, tx + 12, y + 22)
|
||||
tx += tw + 10
|
||||
this.rr(ctx, tx, y, tw, th, th / 2, 'fill')
|
||||
ctx.setStrokeStyle(tagTypes[i] === 'food' ? c.accent : c.accentLight)
|
||||
ctx.setLineWidth(1)
|
||||
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.setLineWidth(1)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(P, y)
|
||||
ctx.lineTo(W - P, y)
|
||||
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
|
||||
|
||||
// 品牌名
|
||||
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.fillText('喝酒了么', P, y + 20)
|
||||
// 副标语
|
||||
ctx.fillText('喝了么', P, y + 20)
|
||||
ctx.font = `normal 20px ${F}`
|
||||
ctx.setFillStyle(DIM)
|
||||
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)
|
||||
this.rr(ctx, qx, qy, qr, qr, 8, 'fill')
|
||||
ctx.setStrokeStyle(c.line)
|
||||
ctx.setLineWidth(1)
|
||||
this.rr(ctx, qx, qy, qr, qr, 8, 'stroke')
|
||||
ctx.font = `normal 18px ${F}`
|
||||
ctx.setFillStyle(DIM)
|
||||
this.rr(ctx, qx, qy, qr, qr, 10, 'fill')
|
||||
ctx.setStrokeStyle(c.tagBorder)
|
||||
ctx.setLineWidth(1.5)
|
||||
this.rr(ctx, qx, qy, qr, qr, 10, 'stroke')
|
||||
ctx.font = `500 16px ${F}`
|
||||
ctx.setFillStyle(GRAY)
|
||||
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 => {
|
||||
ctx.draw(false, () => setTimeout(resolve, 600))
|
||||
ctx.draw(false, () => setTimeout(() => resolve(contentH), 600))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -338,11 +384,14 @@ export default {
|
||||
return [3, 3, 3]
|
||||
},
|
||||
|
||||
// 导出
|
||||
exportImg() {
|
||||
// 导出(动态裁剪到实际内容区域)
|
||||
exportImg(contentH) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cropH = Math.min(contentH || 1200, 1200)
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId: 'shareCanvas', quality: 1,
|
||||
x: 0, y: 0, width: 750, height: cropH,
|
||||
destWidth: 750, destHeight: cropH,
|
||||
success: res => resolve(res.tempFilePath),
|
||||
fail: err => reject({ msg: '导出失败: ' + (err.errMsg || '') })
|
||||
}, this)
|
||||
@@ -368,11 +417,11 @@ export default {
|
||||
},
|
||||
|
||||
shareToFriend() { uni.showToast({ title: '点击右上角分享', icon: 'none' }) },
|
||||
goHome() { uni.reLaunch({ url: '/pages/index/index' }) },
|
||||
goHome() { uni.switchTab({ url: '/pages/index/index' }) },
|
||||
goBack() { uni.navigateBack() }
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '今晚的酒局记录 - 喝酒了么', path: '/pages/index/index' }
|
||||
return { title: '今晚的酒局记录 - 喝了么', path: '/pages/index/index' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+69
-99
@@ -1,12 +1,15 @@
|
||||
<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">
|
||||
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
||||
<text class="greeting-sub">今晚喝一杯?</text>
|
||||
</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>
|
||||
|
||||
<!-- 核心CTA - 首页最醒目位置 -->
|
||||
@@ -81,21 +84,9 @@
|
||||
<text>过量饮酒有害健康 · 请理性记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item tab-active" @click="goHome">
|
||||
<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 class="fab-record" @click="goRecord">
|
||||
<text class="fab-icon">+</text>
|
||||
</view>
|
||||
|
||||
<!-- 日期记录弹窗 -->
|
||||
@@ -141,14 +132,16 @@ export default {
|
||||
components: { DrinkCalendar },
|
||||
data() {
|
||||
const now = new Date()
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
return {
|
||||
statusBarHeight: sysInfo.statusBarHeight || 25,
|
||||
calendarYear: now.getFullYear(),
|
||||
calendarMonth: now.getMonth() + 1,
|
||||
records: [],
|
||||
stats: {},
|
||||
greeting: '',
|
||||
userName: '',
|
||||
userAvatar: '/static/logo.png',
|
||||
userAvatar: '',
|
||||
showDayDetail: false,
|
||||
selectedDate: '',
|
||||
selectedRecord: null
|
||||
@@ -181,7 +174,7 @@ export default {
|
||||
this.records = getRecords()
|
||||
const user = getUserInfo()
|
||||
this.userName = user.nickname || '酒友'
|
||||
this.userAvatar = user.avatar || '/static/logo.png'
|
||||
this.userAvatar = user.avatar || ''
|
||||
this.greeting = getGreeting()
|
||||
this.stats = calcStats(this.records)
|
||||
},
|
||||
@@ -207,11 +200,8 @@ export default {
|
||||
goRecord() {
|
||||
uni.navigateTo({ url: '/pages/record/record' })
|
||||
},
|
||||
goHome() {
|
||||
// 已在首页
|
||||
},
|
||||
goProfile() {
|
||||
uni.navigateTo({ url: '/pages/profile/profile' })
|
||||
uni.switchTab({ url: '/pages/profile/profile' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +209,7 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
padding: $sp-lg;
|
||||
padding: $sp-md;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@@ -227,50 +217,64 @@ export default {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $sp-xl 0 $sp-lg;
|
||||
padding: $sp-md 0 $sp-sm;
|
||||
}
|
||||
|
||||
.greeting-text {
|
||||
display: block;
|
||||
font-size: $fs-xl;
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.greeting-sub {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
font-size: $fs-xs;
|
||||
color: $text-secondary;
|
||||
margin-top: $sp-xs;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: $radius-full;
|
||||
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 {
|
||||
margin-bottom: $sp-lg;
|
||||
padding: $sp-lg;
|
||||
margin-bottom: $sp-md;
|
||||
padding: $sp-md;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-lg;
|
||||
gap: $sp-xs;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: $sp-lg $sp-sm;
|
||||
padding: $sp-md $sp-xs;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
margin-bottom: $sp-xs;
|
||||
font-size: $fs-xl;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@@ -279,11 +283,12 @@ export default {
|
||||
}
|
||||
|
||||
.week-summary {
|
||||
margin-bottom: $sp-lg;
|
||||
margin-bottom: $sp-md;
|
||||
padding: $sp-md;
|
||||
}
|
||||
|
||||
.week-header {
|
||||
margin-bottom: $sp-lg;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.week-stats {
|
||||
@@ -297,9 +302,9 @@ export default {
|
||||
|
||||
.week-stat-value {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
margin-bottom: $sp-xs;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.week-stat-label {
|
||||
@@ -314,7 +319,7 @@ export default {
|
||||
}
|
||||
|
||||
.health-ref {
|
||||
margin-top: $sp-lg;
|
||||
margin-top: $sp-md;
|
||||
position: relative;
|
||||
height: 12rpx;
|
||||
background: $bg-elevated;
|
||||
@@ -339,10 +344,10 @@ export default {
|
||||
/* 核心CTA - 首页英雄区域 */
|
||||
.hero-cta {
|
||||
position: relative;
|
||||
margin-bottom: $sp-lg;
|
||||
border-radius: $radius-xl;
|
||||
margin-bottom: $sp-md;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
padding: $sp-xl $sp-lg;
|
||||
padding: $sp-md $sp-lg;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
@@ -381,11 +386,11 @@ export default {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-lg;
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
.hero-cta-emoji {
|
||||
font-size: 72rpx;
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.hero-cta-text {
|
||||
@@ -394,7 +399,7 @@ export default {
|
||||
|
||||
.hero-cta-title {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-on-amber;
|
||||
letter-spacing: -1rpx;
|
||||
@@ -402,13 +407,13 @@ export default {
|
||||
|
||||
.hero-cta-sub {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
font-size: $fs-xs;
|
||||
color: rgba(11, 11, 20, 0.6);
|
||||
margin-top: $sp-xs;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.hero-cta-arrow {
|
||||
font-size: 56rpx;
|
||||
font-size: 44rpx;
|
||||
color: $text-on-amber;
|
||||
opacity: 0.5;
|
||||
font-weight: $fw-bold;
|
||||
@@ -416,58 +421,18 @@ export default {
|
||||
|
||||
.bottom-health-tip {
|
||||
text-align: center;
|
||||
padding: $sp-lg 0;
|
||||
padding-bottom: 160rpx;
|
||||
padding: $sp-sm 0;
|
||||
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* 底部TabBar */
|
||||
.tab-bar {
|
||||
/* 悬浮记录按钮 */
|
||||
.fab-record {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
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 {
|
||||
right: 40rpx;
|
||||
bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, $amber-light, $amber-deep);
|
||||
@@ -476,9 +441,14 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-amber;
|
||||
z-index: 50;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-fab-icon {
|
||||
.fab-icon {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
|
||||
+303
-25
@@ -6,7 +6,7 @@
|
||||
<view class="brand-icon">
|
||||
<text class="brand-emoji">🍻</text>
|
||||
</view>
|
||||
<text class="brand-name">喝酒了么</text>
|
||||
<text class="brand-name">喝了么</text>
|
||||
<text class="brand-slogan">让每一杯酒都有迹可循</text>
|
||||
</view>
|
||||
|
||||
@@ -27,11 +27,42 @@
|
||||
<button class="btn-primary notice-btn" @click="acceptNotice">我已了解</button>
|
||||
</view>
|
||||
|
||||
<!-- 主登录按钮 -->
|
||||
<!-- 主登录区域 -->
|
||||
<view v-else class="login-actions">
|
||||
<button class="btn-cta login-btn" @click="handleLogin">
|
||||
<text class="login-btn-icon">📱</text>
|
||||
<text class="login-btn-text">微信一键登录</text>
|
||||
<!-- 头像 + 昵称填写(微信新规范) -->
|
||||
<view class="profile-form card">
|
||||
<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>
|
||||
|
||||
<view class="login-divider">
|
||||
@@ -40,9 +71,20 @@
|
||||
<view class="login-divider-line"></view>
|
||||
</view>
|
||||
|
||||
<button class="btn-secondary" @click="handleGuest">
|
||||
<button class="btn-secondary" :disabled="loginLoading" @click="handleGuest">
|
||||
先看看再说
|
||||
</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>
|
||||
|
||||
@@ -51,7 +93,7 @@
|
||||
<view class="pref-card card-elevated">
|
||||
<view class="pref-header">
|
||||
<text class="text-h2">你的饮酒偏好</text>
|
||||
<text class="text-caption">可跳过,稍后在设置中修改</text>
|
||||
<text class="text-caption">选择你常喝的酒类(可多选)</text>
|
||||
</view>
|
||||
|
||||
<view class="pref-section">
|
||||
@@ -86,13 +128,13 @@
|
||||
|
||||
<view class="pref-actions">
|
||||
<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 class="agreement">
|
||||
<!-- 底部协议(非登录状态时显示) -->
|
||||
<view v-if="showNotice" class="agreement">
|
||||
<text class="text-tiny">登录即代表同意</text>
|
||||
<text class="text-tiny text-amber">《用户协议》</text>
|
||||
<text class="text-tiny">和</text>
|
||||
@@ -109,6 +151,11 @@ export default {
|
||||
return {
|
||||
showNotice: true,
|
||||
showPreferences: false,
|
||||
loginLoading: false,
|
||||
prefSaving: false,
|
||||
agreed: false,
|
||||
avatarUrl: '',
|
||||
nickname: '',
|
||||
drinkCategories: DRINK_CATEGORIES,
|
||||
selectedCategories: [],
|
||||
selectedFrequency: '',
|
||||
@@ -124,15 +171,94 @@ export default {
|
||||
acceptNotice() {
|
||||
this.showNotice = false
|
||||
},
|
||||
handleLogin() {
|
||||
// Mock登录,直接显示偏好设置
|
||||
this.showPreferences = true
|
||||
|
||||
// ===== 微信新规范:头像选择 =====
|
||||
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
|
||||
} 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() {
|
||||
if (this.loginLoading) return
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
uni.setStorageSync('is_guest', 'true')
|
||||
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) {
|
||||
const idx = this.selectedCategories.indexOf(id)
|
||||
if (idx > -1) {
|
||||
@@ -141,22 +267,38 @@ export default {
|
||||
this.selectedCategories.push(id)
|
||||
}
|
||||
},
|
||||
|
||||
skipPreferences() {
|
||||
this.finishLogin()
|
||||
},
|
||||
savePreferences() {
|
||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
user.preferences = {
|
||||
favoriteCategories: this.selectedCategories,
|
||||
frequency: this.selectedFrequency
|
||||
|
||||
async savePreferences() {
|
||||
if (this.prefSaving) return
|
||||
this.prefSaving = true
|
||||
try {
|
||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
user.preferences = {
|
||||
favoriteCategories: this.selectedCategories,
|
||||
frequency: this.selectedFrequency
|
||||
}
|
||||
uni.setStorageSync('user_info', JSON.stringify(user))
|
||||
await new Promise(r => setTimeout(r, 300)) // 短暂反馈
|
||||
this.finishLogin()
|
||||
} finally {
|
||||
this.prefSaving = false
|
||||
}
|
||||
uni.setStorageSync('user_info', JSON.stringify(user))
|
||||
this.finishLogin()
|
||||
},
|
||||
|
||||
finishLogin() {
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
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%;
|
||||
}
|
||||
|
||||
/* 资料表单 */
|
||||
.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 {
|
||||
display: flex;
|
||||
@@ -275,10 +504,10 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $sp-sm;
|
||||
}
|
||||
|
||||
.login-btn-icon {
|
||||
font-size: $fs-xl;
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
@@ -303,6 +532,43 @@ export default {
|
||||
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 {
|
||||
position: fixed;
|
||||
@@ -315,12 +581,14 @@ export default {
|
||||
align-items: flex-end;
|
||||
z-index: 100;
|
||||
padding: $sp-lg;
|
||||
animation: fadeIn $duration-base $ease-out;
|
||||
}
|
||||
|
||||
.pref-card {
|
||||
width: 100%;
|
||||
padding: $sp-xl;
|
||||
border-radius: $radius-xl $radius-xl 0 0;
|
||||
animation: slideUp $duration-base $ease-out;
|
||||
}
|
||||
|
||||
.pref-header {
|
||||
@@ -391,4 +659,14 @@ export default {
|
||||
gap: $sp-xs;
|
||||
padding: $sp-lg 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
+64
-77
@@ -3,7 +3,10 @@
|
||||
<!-- 用户头部 -->
|
||||
<view class="profile-header">
|
||||
<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-joined">加入于 {{ user.joinedAt || '2026' }}</text>
|
||||
</view>
|
||||
@@ -73,26 +76,19 @@
|
||||
<view class="divider"></view>
|
||||
<view class="settings-item" @click="showComingSoon('关于')">
|
||||
<text class="settings-icon">ℹ️</text>
|
||||
<text class="settings-name">关于喝酒了么</text>
|
||||
<text class="settings-name">关于喝了么</text>
|
||||
<text class="settings-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部TabBar -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item" @click="goHome">
|
||||
<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 tab-active" @click="goProfile">
|
||||
<text class="tab-icon">👤</text>
|
||||
<text class="tab-label">我的</text>
|
||||
</view>
|
||||
<!-- 登出/登录按钮 -->
|
||||
<view class="logout-area">
|
||||
<button v-if="isGuest" class="btn-ghost logout-btn" @click="goLogin">
|
||||
登录账号,同步数据
|
||||
</button>
|
||||
<button v-else class="btn-text logout-btn" @click="handleLogout">
|
||||
退出登录
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 成就详情弹窗 -->
|
||||
@@ -124,7 +120,8 @@ export default {
|
||||
stats: {},
|
||||
achievementList: [],
|
||||
showDetail: false,
|
||||
detailAchievement: {}
|
||||
detailAchievement: {},
|
||||
isGuest: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -135,6 +132,7 @@ export default {
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.checkLoginState()
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
@@ -152,13 +150,38 @@ export default {
|
||||
uni.showToast({ title: `${name}即将上线`, icon: 'none' })
|
||||
},
|
||||
goHome() {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
},
|
||||
goRecord() {
|
||||
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>
|
||||
.profile-page {
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
@@ -309,65 +346,15 @@ export default {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* TabBar */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120rpx;
|
||||
background: $bg-card;
|
||||
/* 登出区域 */
|
||||
.logout-area {
|
||||
padding: $sp-xl 0 $sp-3xl;
|
||||
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;
|
||||
box-shadow: $shadow-amber;
|
||||
}
|
||||
|
||||
.tab-fab-icon {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
.logout-btn {
|
||||
min-width: 300rpx;
|
||||
}
|
||||
|
||||
/* 成就详情弹窗 */
|
||||
|
||||
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
|
||||
* ========================================== */
|
||||
|
||||
|
||||
@@ -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