diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..b6a9009
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = tab
+indent_size = 4
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f0cf1f3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+unpackage/
+.DS_Store
+.hbuilderx/
diff --git a/App.vue b/App.vue
new file mode 100644
index 0000000..ff6781c
--- /dev/null
+++ b/App.vue
@@ -0,0 +1,305 @@
+
+
+
diff --git a/common/constants.js b/common/constants.js
new file mode 100644
index 0000000..87a0754
--- /dev/null
+++ b/common/constants.js
@@ -0,0 +1,202 @@
+/* 喝酒了么 - 常量定义 */
+
+// 酒类分类
+export const DRINK_CATEGORIES = [
+ { id: 'baijiu', name: '白酒', emoji: '🥃', defaultDegree: 52 },
+ { id: 'beer', name: '啤酒', emoji: '🍺', defaultDegree: 5 },
+ { id: 'wine', name: '红酒', emoji: '🍷', defaultDegree: 13 },
+ { id: 'whisky', name: '洋酒', emoji: '🥂', defaultDegree: 40 },
+ { id: 'huangjiu', name: '黄酒', emoji: '🍶', defaultDegree: 15 },
+ { id: 'sake', name: '清酒', emoji: '🍶', defaultDegree: 15 },
+ { id: 'fruit', name: '果酒', emoji: '🍹', defaultDegree: 8 },
+ { id: 'cocktail', name: '调酒', emoji: '🍸', defaultDegree: 15 },
+ { id: 'other', name: '其他', emoji: '🫗', defaultDegree: 10 }
+]
+
+// 饮用量单位
+export const DRINK_UNITS = [
+ { id: 'ml', name: 'ml', toMl: 1 },
+ { id: 'liang', name: '两', toMl: 50 },
+ { id: 'bottle', name: '瓶', toMl: 500 },
+ { id: 'cup', name: '杯', toMl: 150 },
+ { id: 'can', name: '听', toMl: 330 },
+ { id: 'shot', name: 'shot', toMl: 30 }
+]
+
+// 预设品牌库 (各酒类常见品牌)
+export const BRANDS = {
+ baijiu: [
+ { name: '茅台', product: '飞天茅台53度', degree: 53 },
+ { name: '五粮液', product: '普五52度', degree: 52 },
+ { name: '泸州老窖', product: '国窖1573', degree: 52 },
+ { name: '洋河', product: '梦之蓝M6', degree: 52 },
+ { name: '汾酒', product: '青花汾酒20', degree: 53 },
+ { name: '郎酒', product: '红花郎', degree: 53 },
+ { name: '剑南春', product: '水晶剑南春', degree: 52 },
+ { name: '牛栏山', product: '陈酿42度', degree: 42 },
+ { name: '红星', product: '二锅头56度', degree: 56 },
+ { name: '江小白', product: '表达瓶', degree: 40 }
+ ],
+ beer: [
+ { name: '青岛啤酒', product: '经典', degree: 4.3 },
+ { name: '雪花', product: '勇闯天涯', degree: 3.3 },
+ { name: '百威', product: '经典', degree: 5.0 },
+ { name: '喜力', product: '星银', degree: 5.0 },
+ { name: '科罗娜', product: 'Extra', degree: 4.5 },
+ { name: '嘉士伯', product: '特醇', degree: 5.0 },
+ { name: '燕京', product: 'U8', degree: 2.5 },
+ { name: '哈尔滨', product: '冰纯', degree: 4.5 },
+ { name: '精酿', product: '自选', degree: 6.0 },
+ { name: '朝日', product: '超爽', degree: 5.0 }
+ ],
+ wine: [
+ { name: '拉菲', product: '传奇', degree: 13 },
+ { name: '奔富', product: 'Bin 389', degree: 14.5 },
+ { name: '长城', product: '五星赤霞珠', degree: 13 },
+ { name: '张裕', product: '解百纳', degree: 12.5 },
+ { name: '杰卡斯', product: '西拉', degree: 14 },
+ { name: '黄尾袋鼠', product: '西拉', degree: 13.5 },
+ { name: '蒙特斯', product: '紫天使', degree: 14.5 },
+ { name: '云雾之湾', product: '长相思', degree: 13 }
+ ],
+ whisky: [
+ { name: '麦卡伦', product: '12年雪莉桶', degree: 40 },
+ { name: '格兰菲迪', product: '12年', degree: 40 },
+ { name: '杰克丹尼', product: 'No.7', degree: 40 },
+ { name: '芝华士', product: '12年', degree: 40 },
+ { name: '尊尼获加', product: '黑牌', degree: 40 },
+ { name: '山崎', product: '12年', degree: 43 },
+ { name: '百龄坛', product: '12年', degree: 40 },
+ { name: '人头马', product: 'XO', degree: 40 }
+ ],
+ huangjiu: [
+ { name: '古越龙山', product: '花雕五年', degree: 15 },
+ { name: '会稽山', product: '纯正五年', degree: 15 },
+ { name: '塔牌', product: '手工冬酿', degree: 14 },
+ { name: '女儿红', product: '三年陈', degree: 15 }
+ ],
+ sake: [
+ { name: '獺祭', product: '纯米大吟酿45', degree: 15 },
+ { name: '久保田', product: '纯米吟酿', degree: 15 },
+ { name: '松竹梅', product: '上撰', degree: 15 }
+ ],
+ fruit: [
+ { name: '梅乃宿', product: '柚子酒', degree: 8 },
+ { name: 'RIO', product: '微醺', degree: 3 },
+ { name: '冰锐', product: '朗姆预调', degree: 4.5 },
+ { name: '梅见', product: '青梅酒', degree: 12 }
+ ],
+ cocktail: [
+ { name: '自制', product: 'Mojito', degree: 15 },
+ { name: '自制', product: 'Gin Tonic', degree: 12 },
+ { name: '自制', product: 'Old Fashioned', degree: 30 }
+ ],
+ other: []
+}
+
+// 配餐分类
+export const FOOD_CATEGORIES = [
+ { id: 'hotpot', name: '火锅', emoji: '🍲' },
+ { id: 'bbq', name: '烧烤', emoji: '🍢' },
+ { id: 'stirfry', name: '炒菜', emoji: '🥘' },
+ { id: 'seafood', name: '海鲜', emoji: '🦐' },
+ { id: 'japanese', name: '日料', emoji: '🍣' },
+ { id: 'western', name: '西餐', emoji: '🥩' },
+ { id: 'snack', name: '小吃卤味', emoji: '🥜' },
+ { id: 'junk', name: '零食', emoji: '🍿' },
+ { id: 'none', name: '无配餐', emoji: '🚫' }
+]
+
+// 饮用感受
+export const FEELINGS = [
+ { id: 'tipsy', name: '微醺', emoji: '😊', desc: '刚刚好的快乐' },
+ { id: 'buzzed', name: '到位', emoji: '😌', desc: '今夜刚刚好' },
+ { id: 'drunk', name: '醉了', emoji: '😵', desc: '有点上头了' },
+ { id: 'blackout', name: '断片', emoji: '🫠', desc: '什么都不记得了' }
+]
+
+// 酒言酒语
+export const DRINK_QUOTES = [
+ '人生得意须尽欢,莫使金樽空对月',
+ '微醺是最好的状态',
+ '今朝有酒今朝醉,明日愁来明日愁',
+ '对酒当歌,人生几何',
+ '举杯邀明月,对影成三人',
+ '酒逢知己千杯少',
+ '晚来天欲雪,能饮一杯无',
+ '醉后不知天在水,满船清梦压星河',
+ '白日放歌须纵酒,青春作伴好还乡',
+ '且将新火试新茶,诗酒趁年华',
+ '一壶浊酒喜相逢,古今多少事,都付笑谈中',
+ '何以解忧?唯有杜康',
+ '把酒祝东风,且共从容',
+ '明月几时有,把酒问青天',
+ '劝君更尽一杯酒,西出阳关无故人',
+ '葡萄美酒夜光杯,欲饮琵琶马上催',
+ '借问酒家何处有,牧童遥指杏花村',
+ '花间一壶酒,独酌无相亲',
+ '金樽清酒斗十千,玉盘珍羞直万钱',
+ '东篱把酒黄昏后,有暗香盈袖',
+ '今夜不设防,快乐至上',
+ '干了这杯,烦恼全忘',
+ '好友配好酒,人间值得',
+ '生活有点苦,需要加点酒',
+ '酒杯太浅,敬不到来日方长',
+ '世间万物,唯有美食与酒不可辜负',
+ '把酒言欢,不醉不归',
+ '夜色温柔,酒也温柔',
+ '小酌怡情,大饮伤身,今日刚好',
+ '酒是粮食精,越喝越年轻'
+]
+
+// 成就定义
+export const ACHIEVEMENTS = [
+ {
+ id: 'first_checkin',
+ name: '初来乍到',
+ desc: '完成第一次打卡,开启你的饮酒记录之旅',
+ icon: '🏅',
+ condition: { type: 'total_records', value: 1 }
+ },
+ {
+ id: 'hundred_cups',
+ name: '百杯不醉',
+ desc: '累计饮用100标准杯',
+ icon: '🏆',
+ condition: { type: 'total_standard_cups', value: 100 }
+ },
+ {
+ id: 'streak_30',
+ name: '签到王者',
+ desc: '连续打卡30天,自律的记录者',
+ icon: '👑',
+ condition: { type: 'streak_days', value: 30 }
+ },
+ {
+ id: 'baijiu_master',
+ name: '白酒达人',
+ desc: '白酒累计打卡20次',
+ icon: '🥃',
+ condition: { type: 'category_records', category: 'baijiu', value: 20 }
+ },
+ {
+ id: 'tasting_master',
+ name: '品鉴师',
+ desc: '尝试过5种以上不同类型的酒',
+ icon: '🌈',
+ condition: { type: 'category_variety', value: 5 }
+ }
+]
+
+// 可见范围选项
+export const VISIBILITY_OPTIONS = [
+ { id: 'public', name: '公开', desc: '所有人都能看到' },
+ { id: 'friends', name: '仅好友', desc: '只有好友可见' },
+ { id: 'private', name: '仅自己', desc: '只有自己能看' }
+]
+
+// 打卡模式
+export const CHECKIN_MODES = [
+ { id: 'drank', name: '今日喝了', color: '$amber' },
+ { id: 'abstain', name: '今日未喝', color: '$mint' }
+]
diff --git a/common/mock-data.js b/common/mock-data.js
new file mode 100644
index 0000000..2ae4572
--- /dev/null
+++ b/common/mock-data.js
@@ -0,0 +1,150 @@
+/* 喝酒了么 - Mock 数据 */
+import { formatDate, calcStandardCups, unitToMl } from './utils'
+
+// 模拟用户数据
+export const MOCK_USER = {
+ id: 'user_001',
+ nickname: '酒友小王',
+ avatar: '/static/logo.png',
+ preferences: {
+ favoriteCategories: ['baijiu', 'beer'],
+ frequency: 'often'
+ },
+ joinedAt: '2026-01-15'
+}
+
+// 生成过去一个月的模拟打卡记录
+export function generateMockRecords() {
+ const records = []
+ const today = new Date()
+
+ // 预设一些打卡日
+ const drinkDays = [0, 1, 3, 5, 6, 8, 10, 12, 13, 15, 17, 19, 20, 22, 24, 25, 27, 29]
+ const abstainDays = [2, 4, 7, 9, 11, 14, 16, 18, 21, 23, 26, 28]
+
+ const drinkTemplates = [
+ {
+ drinks: [{ category: 'baijiu', brand: '茅台', product: '飞天茅台53度', amount: 100, unit: 'ml', degree: 53 }],
+ food: { category: 'hotpot', name: '重庆老火锅' },
+ feeling: 'tipsy'
+ },
+ {
+ drinks: [
+ { category: 'beer', brand: '青岛啤酒', product: '经典', amount: 2, unit: 'bottle', degree: 4.3 },
+ ],
+ food: { category: 'bbq', name: '烤串配毛豆' },
+ feeling: 'buzzed'
+ },
+ {
+ drinks: [{ category: 'wine', brand: '奔富', product: 'Bin 389', amount: 2, unit: 'cup', degree: 14.5 }],
+ food: { category: 'western', name: '牛排' },
+ feeling: 'tipsy'
+ },
+ {
+ drinks: [
+ { category: 'baijiu', brand: '五粮液', product: '普五52度', amount: 150, unit: 'ml', degree: 52 },
+ { category: 'beer', brand: '雪花', product: '勇闯天涯', amount: 1, unit: 'bottle', degree: 3.3 }
+ ],
+ food: { category: 'stirfry', name: '水煮鱼+回锅肉' },
+ feeling: 'drunk'
+ },
+ {
+ drinks: [{ category: 'whisky', brand: '麦卡伦', product: '12年雪莉桶', amount: 3, unit: 'shot', degree: 40 }],
+ food: { category: 'snack', name: '坚果拼盘' },
+ feeling: 'tipsy'
+ },
+ {
+ drinks: [{ category: 'beer', brand: '百威', product: '经典', amount: 3, unit: 'bottle', degree: 5.0 }],
+ food: { category: 'seafood', name: '小龙虾' },
+ feeling: 'buzzed'
+ },
+ {
+ drinks: [{ category: 'fruit', brand: '梅见', product: '青梅酒', amount: 200, unit: 'ml', degree: 12 }],
+ food: { category: 'japanese', name: '刺身拼盘' },
+ feeling: 'tipsy'
+ }
+ ]
+
+ drinkDays.forEach(daysAgo => {
+ const date = new Date(today)
+ date.setDate(date.getDate() - daysAgo)
+ const dateStr = formatDate(date, 'YYYY-MM-DD')
+
+ const template = drinkTemplates[daysAgo % drinkTemplates.length]
+ const standardCupsTotal = template.drinks.reduce((sum, d) => {
+ const ml = unitToMl(d.amount, d.unit)
+ return sum + calcStandardCups(ml, d.degree)
+ }, 0)
+
+ records.push({
+ id: `record_${daysAgo}`,
+ date: dateStr,
+ mode: 'drank',
+ drinks: template.drinks.map(d => ({ ...d, categoryId: d.category })),
+ food: template.food,
+ feeling: template.feeling,
+ photos: [],
+ visibility: 'friends',
+ standardCupsTotal: Math.round(standardCupsTotal * 100) / 100,
+ createdAt: date.toISOString()
+ })
+ })
+
+ abstainDays.forEach(daysAgo => {
+ const date = new Date(today)
+ date.setDate(date.getDate() - daysAgo)
+ const dateStr = formatDate(date, 'YYYY-MM-DD')
+
+ records.push({
+ id: `record_abs_${daysAgo}`,
+ date: dateStr,
+ mode: 'abstain',
+ drinks: [],
+ food: null,
+ feeling: null,
+ photos: [],
+ visibility: 'private',
+ standardCupsTotal: 0,
+ createdAt: date.toISOString()
+ })
+ })
+
+ return records.sort((a, b) => new Date(b.date) - new Date(a.date))
+}
+
+// 初始化并缓存Mock数据
+export function initMockData() {
+ const records = generateMockRecords()
+ uni.setStorageSync('drink_records', JSON.stringify(records))
+ uni.setStorageSync('user_info', JSON.stringify(MOCK_USER))
+ uni.setStorageSync('is_first_launch', 'true')
+ return records
+}
+
+// 获取记录
+export function getRecords() {
+ const data = uni.getStorageSync('drink_records')
+ if (data) {
+ try { return JSON.parse(data) } catch (e) { return [] }
+ }
+ return initMockData()
+}
+
+// 添加记录
+export function addRecord(record) {
+ const records = getRecords()
+ record.id = `record_${Date.now()}`
+ record.createdAt = new Date().toISOString()
+ records.unshift(record)
+ uni.setStorageSync('drink_records', JSON.stringify(records))
+ return record
+}
+
+// 获取用户信息
+export function getUserInfo() {
+ const data = uni.getStorageSync('user_info')
+ if (data) {
+ try { return JSON.parse(data) } catch (e) { return MOCK_USER }
+ }
+ return MOCK_USER
+}
diff --git a/common/utils.js b/common/utils.js
new file mode 100644
index 0000000..537b2d1
--- /dev/null
+++ b/common/utils.js
@@ -0,0 +1,222 @@
+/* 喝酒了么 - 工具函数 */
+
+/**
+ * 标准杯换算引擎
+ * 核心公式: 标准杯 = (饮用量ml × 酒精度数% × 0.8) / 10
+ * 1标准杯 = 10g纯酒精
+ */
+export function calcStandardCups(amountMl, degreePercent) {
+ if (!amountMl || !degreePercent) return 0
+ return Math.round((amountMl * (degreePercent / 100) * 0.8 / 10) * 100) / 100
+}
+
+/**
+ * 单位换算为ml
+ */
+const UNIT_TO_ML = {
+ ml: 1,
+ liang: 50, // 1两 = 50ml
+ bottle: 500, // 1瓶 = 500ml (啤酒默认)
+ cup: 150, // 1杯 = 150ml
+ can: 330, // 1听 = 330ml
+ shot: 30 // 1shot = 30ml
+}
+
+export function unitToMl(amount, unit) {
+ const factor = UNIT_TO_ML[unit] || 1
+ return amount * factor
+}
+
+/**
+ * 计算一组饮品的总标准杯
+ */
+export function calcTotalStandardCups(drinks) {
+ return drinks.reduce((sum, d) => {
+ const ml = unitToMl(d.amount, d.unit)
+ return sum + calcStandardCups(ml, d.degree)
+ }, 0)
+}
+
+/**
+ * 日期格式化
+ */
+export function formatDate(date, format = 'YYYY-MM-DD') {
+ const d = new Date(date)
+ const year = d.getFullYear()
+ const month = String(d.getMonth() + 1).padStart(2, '0')
+ const day = String(d.getDate()).padStart(2, '0')
+ const hours = String(d.getHours()).padStart(2, '0')
+ const minutes = String(d.getMinutes()).padStart(2, '0')
+
+ return format
+ .replace('YYYY', year)
+ .replace('MM', month)
+ .replace('DD', day)
+ .replace('HH', hours)
+ .replace('mm', minutes)
+}
+
+/**
+ * 获取指定月份的日历数据
+ * @param {number} year
+ * @param {number} month 1-12
+ * @returns {Array} 日历数组,包含前置空白、日期、后置空白
+ */
+export function getCalendarDays(year, month) {
+ const firstDay = new Date(year, month - 1, 1)
+ const lastDay = new Date(year, month, 0)
+ const daysInMonth = lastDay.getDate()
+ // 周一为一周起始 (0=周一, 6=周日)
+ let startWeekday = firstDay.getDay() - 1
+ if (startWeekday < 0) startWeekday = 6
+
+ const days = []
+ // 前置空白
+ for (let i = 0; i < startWeekday; i++) {
+ days.push({ day: null, date: null })
+ }
+ // 日期
+ for (let i = 1; i <= daysInMonth; i++) {
+ const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`
+ days.push({
+ day: i,
+ date: dateStr,
+ isToday: dateStr === formatDate(new Date(), 'YYYY-MM-DD')
+ })
+ }
+ return days
+}
+
+/**
+ * 获取本周一的日期
+ */
+export function getWeekStart(date = new Date()) {
+ const d = new Date(date)
+ const day = d.getDay()
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1)
+ return new Date(d.setDate(diff))
+}
+
+/**
+ * 获取时间问候语
+ */
+export function getGreeting() {
+ const hour = new Date().getHours()
+ if (hour < 6) return '夜深了'
+ if (hour < 12) return '早上好'
+ if (hour < 14) return '中午好'
+ if (hour < 18) return '下午好'
+ if (hour < 22) return '晚上好'
+ return '夜深了'
+}
+
+/**
+ * 随机获取一条酒言酒语
+ */
+export function getRandomQuote(quotes) {
+ return quotes[Math.floor(Math.random() * quotes.length)]
+}
+
+/**
+ * 统计数据计算
+ */
+export function calcStats(records) {
+ const now = new Date()
+ const today = formatDate(now, 'YYYY-MM-DD')
+
+ // 本周统计
+ const weekStart = getWeekStart(now)
+ const weekRecords = records.filter(r => new Date(r.date) >= weekStart)
+ const weekDrinkCount = weekRecords.filter(r => r.mode === 'drank').length
+ const weekCups = weekRecords.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
+
+ // 本月统计
+ const monthStr = formatDate(now, 'YYYY-MM')
+ const monthRecords = records.filter(r => r.date && r.date.startsWith(monthStr))
+ const monthDrinkCount = monthRecords.filter(r => r.mode === 'drank').length
+ const monthCups = monthRecords.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
+
+ // 连续天数
+ let streak = 0
+ let streakType = 'drank'
+ const d = new Date()
+ // 检查今天是否有记录
+ const todayRecord = records.find(r => r.date === today)
+ if (todayRecord) {
+ streakType = todayRecord.mode
+ streak = 1
+ // 向前追溯
+ for (let i = 1; i < 365; i++) {
+ const prevDate = new Date(d)
+ prevDate.setDate(prevDate.getDate() - i)
+ const prevStr = formatDate(prevDate, 'YYYY-MM-DD')
+ const prev = records.find(r => r.date === prevStr)
+ if (prev && prev.mode === streakType) {
+ streak++
+ } else {
+ break
+ }
+ }
+ }
+
+ // 总打卡天数
+ const totalDays = new Set(records.filter(r => r.mode === 'drank').map(r => r.date)).size
+ const totalRecords = records.filter(r => r.mode === 'drank').length
+ const totalCups = records.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
+
+ // 最爱酒类
+ const categoryCount = {}
+ records.forEach(r => {
+ if (r.drinks) {
+ r.drinks.forEach(d => {
+ categoryCount[d.category] = (categoryCount[d.category] || 0) + 1
+ })
+ }
+ })
+ const favCategory = Object.entries(categoryCount).sort((a, b) => b[1] - a[1])[0]
+
+ return {
+ weekDrinkCount,
+ weekCups: Math.round(weekCups * 10) / 10,
+ monthDrinkCount,
+ monthCups: Math.round(monthCups * 10) / 10,
+ streak,
+ streakType,
+ totalDays,
+ totalRecords,
+ totalCups: Math.round(totalCups * 10) / 10,
+ favCategory: favCategory ? favCategory[0] : null,
+ categoryVariety: Object.keys(categoryCount).length
+ }
+}
+
+/**
+ * 检查成就解锁
+ */
+export function checkAchievements(stats, achievements) {
+ return achievements.map(a => {
+ let unlocked = false
+ const { type, value, category } = a.condition
+
+ switch (type) {
+ case 'total_records':
+ unlocked = stats.totalRecords >= value
+ break
+ case 'total_standard_cups':
+ unlocked = stats.totalCups >= value
+ break
+ case 'streak_days':
+ unlocked = stats.streak >= value
+ break
+ case 'category_variety':
+ unlocked = stats.categoryVariety >= value
+ break
+ case 'category_records':
+ // 需要更细粒度的数据,这里简化处理
+ unlocked = false
+ break
+ }
+
+ return { ...a, unlocked }
+ })
+}
diff --git a/components/AchievementBadge.vue b/components/AchievementBadge.vue
new file mode 100644
index 0000000..81fa6e1
--- /dev/null
+++ b/components/AchievementBadge.vue
@@ -0,0 +1,99 @@
+
+
+
+ {{ achievement.icon }}
+
+ 🔒
+
+
+ {{ achievement.name }}
+
+
+
+
+
+
diff --git a/components/DrinkCalendar.vue b/components/DrinkCalendar.vue
new file mode 100644
index 0000000..8788035
--- /dev/null
+++ b/components/DrinkCalendar.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+ {{ day }}
+
+
+
+
+
+ {{ cell.day }}
+
+
+
+
+
+
+
+
+
+
diff --git a/components/DrinkCard.vue b/components/DrinkCard.vue
new file mode 100644
index 0000000..3d92f8c
--- /dev/null
+++ b/components/DrinkCard.vue
@@ -0,0 +1,767 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getCatEmoji(drink.category) }}
+
+ {{ drink.brand }}
+ {{ drink.product }} · {{ drink.amount }}{{ drink.unit }}
+
+ {{ drink.standardCups }}杯
+
+
+
+
+
+
+ {{ record.standardCupsTotal }}
+ 标准杯
+
+
+
+ 🍲
+ {{ getFoodName() }}
+
+
+
+
+ "
+ {{ quote }}
+
+
+
+
+
+
+
+
+
+
+ {{ record.date }}
+
+
+
+
+
+
+ {{ getCatEmoji(drink.category) }} {{ drink.brand }} {{ drink.standardCups }}杯
+
+
+
+
+ {{ record.standardCupsTotal }} 标准杯
+ {{ getFeelingEmoji() }} {{ getFeelingName() }}
+
+
+
+ "
+ {{ quote }}
+
+
+
+
+
+
+
+
+
+ {{ record.date }}
+
+
+ {{ record.standardCupsTotal }}
+ 标准杯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +{{ photos.length - 5 }}
+
+
+
+
+
+
+
+
+
+ {{ getCatEmoji(drink.category) }} {{ drink.brand }} · {{ drink.standardCups }}杯
+
+
+
+
+ 🍲 {{ getFoodName() }}
+ {{ getFeelingEmoji() }} {{ getFeelingName() }}
+
+
+
+ "
+ {{ quote }}
+
+
+
+
+
+
+ 🍻 喝酒了么
+
+ 记录每一杯的故事
+
+
+
+
+
+
+
diff --git a/he.md b/he.md
new file mode 100644
index 0000000..7b47af2
--- /dev/null
+++ b/he.md
@@ -0,0 +1,559 @@
+# 喝酒了么 - 微信小程序产品规划方案
+
+
+
+## 一、产品定位与用户画像
+
+
+
+### 1.1 一句话定位
+
+**"喝酒了么"是一款以饮酒打卡为入口、以酒友社交为粘性、以趣味分享为裂变引擎的轻量级生活方式小程序。**
+
+
+
+### 1.2 核心价值主张
+
+- **记录价值**:帮用户建立个人饮酒档案,让每一杯酒都有迹可循
+
+- **社交价值**:让喝酒从"一个人闷"变成"一群人嗨",满足分享欲和归属感
+
+- **趣味价值**:用游戏化机制消解"饮酒=不健康"的负面联想,塑造轻松有趣的品牌调性
+
+- **数据价值**:长期积累后,用户可获得个人饮酒画像和年度报告,满足自我认知需求
+
+
+
+### 1.3 目标用户画像
+
+
+
+| 画像类型 | 年龄 | 饮酒场景 | 核心诉求 | 活跃时段 |
+
+|---------|------|---------|---------|---------|
+
+| **社交酒友** | 25-35岁 | 朋友聚会、公司团建、约饭 | 记录酒局、分享给朋友看、存图晒图 | 周五/周六晚 18:00-02:00 |
+
+| **独酌爱好者** | 28-40岁 | 下班小酌、周末独饮 | 记录品酒心得、建立品酒档案 | 工作日 20:00-23:00 |
+
+| **酒类入门者** | 22-28岁 | 尝试不同酒类、学习品酒 | 了解酒类知识、记录成长轨迹 | 不定时 |
+
+| **轻度社交用户** | 20-40岁 | 偶尔应酬、节日聚会 | 快速记录、看看朋友喝了啥 | 节假日 |
+
+
+
+### 1.4 使用场景拆解
+
+
+
+**核心场景A:酒局进行时**
+
+> 小王周五晚上和朋友聚餐吃火锅,开了两瓶白酒。他打开"喝酒了么",选择"火锅"配餐,录入"茅台·53度飞天 200ml",拍了一张碰杯照片,一键生成带滤镜的酒局卡片分享到微信群。朋友们看到后也纷纷打开小程序加入打卡。
+
+
+
+**核心场景B:周末回顾**
+
+> 周日晚上,小李收到小程序推送:"本周你喝了3次,白酒2次啤酒1次,打败了78%的酒友"。他点开查看本周饮酒日历,觉得有趣,截图发到朋友圈。
+
+
+
+**核心场景C:年度盘点**
+
+> 年底,小程序生成"2026年度饮酒报告":全年喝了186次,白酒占比62%,最爱配餐是烧烤,最爱品牌是青岛啤酒。排行榜显示他是朋友圈"年度酒神"第3名。他分享后获得专属年度徽章。
+
+
+
+---
+
+
+
+## 二、核心功能模块细化
+
+
+
+### 2.1 每日饮酒打卡(基础入口)
+
+
+
+**功能点:**
+
+- **快速打卡**:首页醒目大按钮"今日喝了",一键标记当天饮酒状态
+
+- **饮酒日历**:月视图日历,用不同颜色标记饮酒日(浅黄=未喝/深红=喝了),直观展示月度饮酒频率
+
+- **连续打卡统计**:显示"连续饮酒 X 天"或"连续戒酒 X 天",支持用户自主切换追踪模式
+
+- **补打卡**:支持补录过去7天内的饮酒记录(防止遗漏)
+
+- **未喝也打卡**:支持"今日未喝"打卡,满足戒酒/控制饮酒用户的需求
+
+
+
+### 2.2 酒水详情记录(核心数据层)
+
+
+
+**酒类分类体系:**
+
+- 白酒 | 啤酒 | 红酒 | 洋酒(威士忌/白兰地/伏特加/朗姆酒/金酒/龙舌兰)| 黄酒 | 清酒 | 果酒/气泡酒 | 自制调酒 | 其他
+
+
+
+**记录字段:**
+
+- 酒类类型(必选,下拉选择)
+
+- 品牌名称(支持搜索+自定义输入,建立品牌数据库)
+
+- 具体产品名称/型号(如"飞天茅台53度"、"百威经典")
+
+- 饮用量(核心功能,见2.3)
+
+- 酒精度数(自动匹配或手动输入)
+
+- 饮用感受(可选:微醺/到位/醉了/断片,用表情化选择降低输入门槛)
+
+- 照片(最多9张,支持滤镜)
+
+- 饮用地点标签(家/餐厅/酒吧/KTV/户外 等)
+
+
+
+### 2.3 饮用量换算引擎(差异化亮点)
+
+
+
+**核心逻辑:统一换算为"标准杯"(1标准杯=10g纯酒精)**
+
+
+
+| 酒类 | 常见规格 | 标准杯换算 |
+
+|------|---------|-----------|
+
+| 白酒(52°) | 1两=50ml | ≈2.1标准杯 |
+
+| 啤酒(5°) | 1瓶=500ml | ≈2.0标准杯 |
+
+| 红酒(13°) | 1杯=150ml | ≈1.5标准杯 |
+
+| 威士忌(40°) | 1 shot=30ml | ≈1.0标准杯 |
+
+
+
+**功能表现:**
+
+- 输入时支持多种单位(ml/两/瓶/杯/听/shot),自动换算标准杯
+
+- 单次酒局展示"累计 XX 标准杯",帮助用户感知饮酒量
+
+- 健康参考线:标注"建议每日不超过2标准杯(《中国居民膳食指南》)"
+
+
+
+### 2.4 配餐记录
+
+
+
+- **菜品分类快捷选择**:火锅、烧烤、炒菜、海鲜、日料、西餐、小吃卤味、零食、无配餐
+
+- **具体菜名输入**:支持文字输入+历史记录联想
+
+- **酒配餐推荐**(后期AI功能):根据酒类推荐经典搭配(如"红酒配牛排"、"啤酒配烧烤")
+
+- **配餐热力图**:统计分析"你最爱的下酒菜 TOP 5"
+
+
+
+### 2.5 饮酒健康/安全提醒(社会责任模块)
+
+
+
+- **单次提醒**:当日累计超过3标准杯时,弹出温和提醒"今日已饮用X标准杯,适度为宜"
+
+- **周度报告**:每周总结饮酒总量和频率,给予健康建议
+
+- **代驾快捷入口**:打卡页面底部固定"叫代驾"入口(对接滴滴/ e代驾小程序跳转)
+
+- **安全提示**:首次使用展示《理性饮酒须知》,打卡页面底部常驻"过量饮酒有害健康"提示
+
+- **戒酒挑战模式**:用户可设定"戒酒X天"目标,每日签到确认,失败可重置,完成获得成就徽章
+
+
+
+---
+
+
+
+## 三、进阶与社交功能建议
+
+
+
+### 3.1 酒局卡片(核心分享载体)
+
+
+
+每次完成打卡后,自动生成一张精美图片卡片,包含:
+
+- 酒水信息(类型+品牌+饮用量)
+
+- 配餐信息
+
+- 饮用感受表情
+
+- 日期和地点
+
+- 一句随机"酒言酒语"文案(如"人生得意须尽欢"、"微醺是最好的状态")
+
+- 小程序二维码水印(引流)
+
+
+
+提供 5-8 种卡片模板风格(简约/国潮/文艺/酷炫等),支持一键保存图片或分享到微信聊天/朋友圈。
+
+
+
+### 3.2 酒友圈(社区Feed流)
+
+
+
+- **关注制内容流**:用户可选择关注微信好友或平台推荐用户
+
+- **动态展示**:展示关注用户的公开打卡记录(以酒局卡片形式)
+
+- **互动功能**:点赞、评论(仅文字,控制内容风险)
+
+- **隐私控制**:每条打卡可设为"公开/仅好友可见/仅自己可见"
+
+- **热门推荐**:算法推荐的优质打卡内容(需人工审核机制)
+
+
+
+### 3.3 成就与徽章系统(游戏化)
+
+
+
+| 徽章类别 | 徽章示例 | 获取条件 |
+
+|---------|---------|---------|
+
+| 打卡类 | 初来乍到、百杯不醉、签到王者 | 首次打卡/累计100次/连续30天打卡 |
+
+| 酒类类 | 白酒达人、啤酒之王、红酒绅士 | 某类酒累计打卡N次 |
+
+| 场景类 | 火锅搭档、烧烤之王、日料控 | 某类配餐累计打卡N次 |
+
+| 挑战类 | 戒酒勇士、自律达人 | 完成戒酒挑战 |
+
+| 稀有类 | 跨年酒神、中秋月饮 | 特定节日打卡 |
+
+| 社交类 | 人气之星、酒友满天下 | 获得N个赞/关注N个好友 |
+
+
+
+### 3.4 排行榜(社交裂变引擎)
+
+
+
+- **好友排行(核心)**:基于微信好友关系链,排行维度包括:
+
+ - 本月饮酒次数
+
+ - 本月累计标准杯
+
+ - 酒类丰富度(喝过的不同酒类数量)
+
+- **全平台排行**:匿名化展示,仅显示昵称首尾字符
+
+- **警惕设计**:排行榜不鼓励过量饮酒,强调"丰富度"和"品鉴力"而非单纯的"喝得多"
+
+- **排行卡片**:支持生成个人排行卡片分享(含排名和趣味称号)
+
+
+
+### 3.5 年度饮酒报告(年度爆款功能)
+
+
+
+参照网易云音乐年度报告的模式,包含:
+
+- 年度饮酒天数/次数
+
+- 最爱酒类 & 最爱品牌
+
+- 最爱配餐 TOP 3
+
+- 累计标准杯(换算成趣味等价物,如"相当于喝了XX瓶啤酒")
+
+- 年度关键词/标签(如"白酒老炮"、"精酿探索者")
+
+- 好友排行中的年度位置
+
+- 月度饮酒趋势曲线
+
+
+
+### 3.6 酒局组局(社交场景延伸)
+
+
+
+- 发起酒局:设置时间、地点、主题,邀请好友参与
+
+- 参与者共同打卡,自动关联到同一酒局
+
+- 酒局结束后生成"酒局战报":参与人数、总消耗、人均标准杯、MVP(喝得最多的)
+
+
+
+---
+
+
+
+## 四、核心用户交互流程
+
+
+
+### 4.1 首次使用流程
+
+
+
+```
+
+用户搜索/扫码进入小程序
+
+ ↓
+
+欢迎页(3屏滑动引导:记录饮酒 / 分享酒局 / 成就徽章)
+
+ ↓
+
+微信一键授权登录(获取头像昵称)
+
+ ↓
+
+《理性饮酒须知》确认(仅首次,含健康提示和内容规范)
+
+ ↓
+
+设置饮酒偏好(可选跳过):
+
+ · 常饮酒类(多选)
+
+ · 饮酒频率(每天/经常/偶尔/很少)
+
+ ↓
+
+进入首页(今日打卡主界面)
+
+```
+
+
+
+### 4.2 一次完整"酒局打卡"流程
+
+
+
+```
+
+【首页】点击大按钮"🍻 开始记录"
+
+ ↓
+
+【Step 1 - 选酒】选择酒类类型 → 搜索/输入品牌 → 选择具体产品
+
+ ↓ (支持快速添加多种酒,如同时喝白酒和啤酒)
+
+ ↓
+
+【Step 2 - 记量】输入饮用量 → 自动换算标准杯 → 实时显示累计
+
+ ↓
+
+【Step 3 - 配餐】选择配餐类型 → 可选输入具体菜名
+
+ ↓
+
+【Step 4 - 感受】选择饮用感受表情(微醺/到位/醉了/断片/自定义)
+
+ ↓
+
+【Step 5 - 照片】拍照或从相册选择(最多9张)→ 可选滤镜
+
+ ↓
+
+【Step 6 - 发布】设置可见范围 → 预览酒局卡片 → 确认发布
+
+ ↓
+
+【完成页】展示酒局卡片 → 一键分享微信好友/朋友圈 → 返回首页
+
+ ↓
+
+【首页更新】日历标记今日已喝 → 更新连续打卡天数 → 解锁新成就弹窗
+
+```
+
+
+
+### 4.3 浏览与互动流程
+
+
+
+```
+
+【底部导航】
+
+ 首页(打卡) | 酒友圈(发现) | +(发布) | 排行榜 | 我的
+
+```
+
+
+
+- **首页**:日历视图 + 快速打卡按钮 + 饮酒统计卡片(本周/本月)
+
+- **酒友圈**:关注用户的打卡动态Feed + 热门内容推荐
+
+- **+号**:发布按钮,快速发起打卡
+
+- **排行榜**:好友排行/全平台排行,切换维度
+
+- **我的**:个人主页、饮酒档案、成就墙、设置
+
+
+
+---
+
+
+
+## 五、MVP版本规划与迭代路线
+
+
+
+### 5.1 MVP V1.0 —— 核心闭环(建议4-6周开发)
+
+
+
+**必须上线的功能:**
+
+
+
+| 模块 | 功能点 | 优先级理由 |
+
+|------|--------|-----------|
+
+| 登录体系 | 微信一键授权登录 | 基础能力 |
+
+| 饮酒打卡 | 快速打卡+日历视图+补打卡 | 核心功能闭环 |
+
+| 酒水记录 | 酒类选择+品牌输入+饮用量(含标准杯换算) | 数据基础 |
+
+| 配餐记录 | 配餐分类选择+文字输入 | 数据基础 |
+
+| 酒局卡片 | 自动生成分享卡片(2-3种模板) | 社交裂变最小单元 |
+
+| 我的页面 | 基础统计(打卡天数/总次数)+ 简单成就(3-5个) | 留存基础 |
+
+
+
+**V1.0 不做的:**
+
+- 酒友圈Feed流(内容审核成本高,先靠卡片分享裂变)
+
+- 排行榜(依赖好友关系链数据积累)
+
+- 年度报告(需要数据积累)
+
+- 酒局组局
+
+
+
+### 5.2 V1.1 —— 社交激活(+3-4周)
+
+
+
+- 排行榜功能上线(好友排行优先)
+
+- 成就徽章扩展至 15-20 个
+
+- 卡片模板增加至 5-6 种
+
+- 健康提醒 & 代驾入口
+
+- "戒酒挑战"模式
+
+
+
+### 5.3 V1.2 —— 社区建设(+3-4周)
+
+
+
+- 酒友圈Feed流上线
+
+- 点赞评论互动
+
+- 关注/粉丝体系
+
+- 内容举报与审核后台
+
+
+
+### 5.4 V2.0 —— 生态扩展(+6-8周)
+
+
+
+- 年度饮酒报告
+
+- 酒局组局功能 `
+
+- 酒配餐智能推荐
+
+- 品牌数据库完善(UGC+人工维护)
+
+- 个性化推送
+
+
+
+---
+
+
+
+## 六、关键风险与应对
+
+
+
+| 风险点 | 应对策略 |
+
+|--------|---------|
+
+| **内容合规风险** | 所有页面底部标注"过量饮酒有害健康";不出现劝酒、拼酒等引导性内容;建立UGC内容审核机制 |
+
+| **微信审核风险** | 避免"酒"字过度突出,小程序简介侧重"生活方式记录";不涉及在线酒类销售 |
+
+| **用户隐私** | 打卡默认"仅好友可见";不强制获取位置等敏感权限;隐私政策清晰可读 |
+
+| **留存难题** | 用成就徽章+连续打卡+排行榜制造回访钩子;周报推送保持存在感 |
+
+| **冷启动** | V1.0重点打磨卡片分享体验,让每次分享都是一次拉新;可考虑种子用户内测+邀请制 |
+
+
+
+---
+
+
+
+## 七、下一步行动建议
+
+
+
+1. **竞品调研**:体验"DrinkNote"、"Untappd"(啤酒领域标杆)、"Vivino"(红酒领域),重点看它们的分享卡片和社区机制
+
+2. **品牌名确认**:"喝酒了么"有记忆点,但需确认微信小程序命名规范(避免"酒"字被限制),备选名可考虑"饮记"、"微醺日记"等
+
+3. **技术选型决策**:确定前端框架(原生/uniapp/Taro)和后端技术栈,建议 uniapp 以兼顾多端能力
+
+4. **数据库设计**:酒类品牌库是核心资产,需尽早规划数据结构(酒类→品牌→产品的三级体系)
+
+5. **视觉风格确定**:建议年轻化、轻量化的设计风格,避免传统白酒的"厚重感"
+
+6. **MVP原型设计**:根据本方案V1.0范围,产出交互原型图(可使用墨刀/Figma),进行内部评审
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..bfb9a1b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..3ac0c69
--- /dev/null
+++ b/main.js
@@ -0,0 +1,24 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+ ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import {
+ createSSRApp
+} from 'vue'
+export function createApp() {
+ const app = createSSRApp(App)
+ return {
+ app
+ }
+}
+// #endif
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..70fda3d
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,128 @@
+{
+ "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": [
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]
+ },
+ "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",
+ "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" : [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]
+ },
+ /* 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"
+}
diff --git a/pages.json b/pages.json
new file mode 100644
index 0000000..3c68878
--- /dev/null
+++ b/pages.json
@@ -0,0 +1,72 @@
+{
+ "pages": [
+ {
+ "path": "pages/index/index",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/onboarding/onboarding",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/login/login",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/record/record",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/card/card",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": ""
+ }
+ },
+ {
+ "path": "pages/profile/profile",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": ""
+ }
+ }
+ ],
+ "globalStyle": {
+ "navigationBarTextStyle": "white",
+ "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"
+ }
+ }
+ ],
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "uni-app",
+ "navigationBarBackgroundColor": "#F8F8F8",
+ "backgroundColor": "#F8F8F8"
+ },
+ "uniIdRouter": {}
+}
diff --git a/pages/card/card.vue b/pages/card/card.vue
new file mode 100644
index 0000000..5339331
--- /dev/null
+++ b/pages/card/card.vue
@@ -0,0 +1,291 @@
+
+
+
+
+
+ ‹
+
+ 你的酒局卡片
+
+
+
+
+
+ {{ photoHintText }}
+
+
+
+
+
+
+
+
+
+ {{ t.name }}
+ {{ t.desc }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ✅ 已保存到相册
+
+
+
+
+
+
+
diff --git a/pages/index/index.vue b/pages/index/index.vue
new file mode 100644
index 0000000..c82c326
--- /dev/null
+++ b/pages/index/index.vue
@@ -0,0 +1,540 @@
+
+
+
+
+
+ {{ greeting }},{{ userName }}
+ 今晚喝一杯?
+
+
+
+
+
+
+
+
+ 🍻
+
+ 开始记录
+ 记录今晚的酒局
+
+ ›
+
+
+
+
+
+
+ {{ stats.monthDrinkCount }}
+ 本月饮酒
+
+
+ {{ stats.monthCups }}
+ 标准杯
+
+
+
+ {{ stats.streak }}
+
+ {{ stats.streakType === 'drank' ? '连喝天数' : '戒酒天数' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ stats.weekDrinkCount }}
+ 次饮酒
+
+
+
+ {{ stats.weekCups }}
+ 标准杯
+
+
+
+
+
+ {{ stats.weekCups > 14 ? '本周超标了,注意休息' : '本周控制得不错' }}
+
+
+
+
+
+
+ 过量饮酒有害健康 · 请理性记录
+
+
+
+
+
+ 🏠
+ 首页
+
+
+
+ +
+
+
+
+ 👤
+ 我的
+
+
+
+
+
+
+
+
+
+
+
+ {{ drink.emoji || '🥃' }} {{ drink.brand }} {{ drink.product }}
+
+
+
+ {{ getFeelingName(selectedRecord.feeling) }}
+ {{ selectedRecord.standardCupsTotal }} 标准杯
+
+
+
+ 🚫
+ 今日未饮酒,好样的
+
+
+
+ 当天无记录
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/login/login.vue b/pages/login/login.vue
new file mode 100644
index 0000000..8554f45
--- /dev/null
+++ b/pages/login/login.vue
@@ -0,0 +1,394 @@
+
+
+
+
+
+
+ 🍻
+
+ 喝酒了么
+ 让每一杯酒都有迹可循
+
+
+
+
+
+
+
+
+ • 过量饮酒有害健康,请理性记录
+ • 本应用不鼓励未成年人使用
+ • 不涉及任何酒类销售与推荐
+ • 您的数据仅用于个人记录
+
+
+
+
+
+
+
+
+
+
+ 或
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 常喝的酒类
+
+
+ {{ cat.emoji }} {{ cat.name }}
+
+
+
+
+
+ 饮酒频率
+
+
+ {{ freq.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录即代表同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+
+
+
+
diff --git a/pages/onboarding/onboarding.vue b/pages/onboarding/onboarding.vue
new file mode 100644
index 0000000..e4a5adc
--- /dev/null
+++ b/pages/onboarding/onboarding.vue
@@ -0,0 +1,235 @@
+
+
+
+
+ 跳过
+
+
+
+
+
+
+
+
+
+ {{ slide.emoji }}
+
+
+
+
+
+
+
+ {{ slide.title }}
+ {{ slide.desc }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/profile/profile.vue b/pages/profile/profile.vue
new file mode 100644
index 0000000..2d67788
--- /dev/null
+++ b/pages/profile/profile.vue
@@ -0,0 +1,433 @@
+
+
+
+
+
+
+
+ 饮酒档案
+
+
+ {{ stats.totalDays }}
+ 打卡天数
+
+
+ {{ stats.totalRecords }}
+ 饮酒次数
+
+
+ {{ stats.totalCups }}
+ 标准杯
+
+
+ {{ favCategoryName }}
+ 最爱酒类
+
+
+
+
+
+
+
+
+ {{ stats.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }}
+
+ {{ stats.streak }} 天
+
+
+ {{ stats.streakType === 'drank' ? '🔥' : '💪' }}
+
+
+
+
+
+ 成就徽章
+
+
+
+
+
+
+
+
+ ⚙️
+ 饮酒偏好设置
+ ›
+
+
+
+ 🔒
+ 隐私设置
+ ›
+
+
+
+ ℹ️
+ 关于喝酒了么
+ ›
+
+
+
+
+
+
+ 🏠
+ 首页
+
+
+
+ +
+
+
+
+ 👤
+ 我的
+
+
+
+
+
+
+ {{ detailAchievement.icon }}
+ {{ detailAchievement.name }}
+ {{ detailAchievement.desc }}
+
+ {{ detailAchievement.unlocked ? '✓ 已解锁' : '🔒 未解锁' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/record/record.vue b/pages/record/record.vue
new file mode 100644
index 0000000..62191a4
--- /dev/null
+++ b/pages/record/record.vue
@@ -0,0 +1,1262 @@
+
+
+
+
+
+
+
+ {{ stepTitles[currentStep - 1] }}
+ {{ stepSubtitles[currentStep - 1] }}
+
+
+
+
+
+
+
+ {{ cat.emoji }}
+ {{ cat.name }}
+
+
+
+
+
+ 品牌
+
+
+ {{ brand.name }}
+
+
+
+
+
+
+
+ 产品名称
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentDrink.unit === 'ml' ? 'ml' : currentDrink.unit }}
+
+
+
+
+
+ {{ u.name }}
+
+
+
+
+
+ 酒精度数
+
+
+ °
+
+
+
+
+
+
+
+
+ 本次饮酒量
+
+ ?
+
+
+ {{ currentCups }} 标准杯
+
+
+
+
+
+ 已添加的酒水
+
+
+ {{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }}
+ {{ d.standardCups }}杯
+
+
+
+
+
+
+ 本次累计
+ {{ totalCups }} 标准杯
+
+
+
+
+ ⚠️
+ 今日已饮用 {{ totalCups }} 标准杯,建议每日不超过2标准杯
+
+
+ 建议每日不超过2标准杯(《中国居民膳食指南》)
+
+
+
+
+
+
+
+
+ 标准杯是国际通用的饮酒量度量单位,方便你比较不同酒类的实际酒精摄入量。
+
+ 1 标准杯 = 10g 纯酒精
+
+ 常见换算参考:
+
+
+ 🥃 白酒 (52°) 1两
+ ≈ 2.1 杯
+
+
+ 🍺 啤酒 (5°) 1瓶
+ ≈ 2.0 杯
+
+
+ 🍷 红酒 (13°) 1杯
+ ≈ 1.5 杯
+
+
+ 🥂 威士忌 (40°) 1shot
+ ≈ 1.0 杯
+
+
+
+ 💡 中国居民膳食指南建议:成年人每日饮酒不超过 2 标准杯
+
+
+
+
+
+
+
+
+
+ {{ food.emoji }}
+ {{ food.name }}
+
+
+
+
+ 具体吃了什么
+
+
+
+
+
+
+
+
+ {{ f.emoji }}
+ {{ f.name }}
+ {{ f.desc }}
+
+
+
+ 不想记录感受?可以直接下一步
+
+
+
+
+
+
+
+
+
+ ✕
+
+
+
+ 📷
+ 拍照/相册
+
+
+
+ 最多选择9张照片,记录今夜的精彩瞬间
+
+
+
+
+
+
+
+ 记录预览
+
+
+ {{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }} · {{ d.amount }}{{ d.unit }}
+ {{ d.standardCups }}杯
+
+
+
+
+ 🍲 {{ getFoodName() }}
+ {{ getFeelingEmoji() }} {{ getFeelingName() }}
+ 📍 {{ backfillDate || '今天' }}
+
+
+ 总计
+ {{ totalCups }} 标准杯
+
+
+
+
+
+ ✨
+
+ 保存后可生成酒局卡片
+ 卡片可保存到相册发朋友圈,或分享给好友
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/logo.png b/static/logo.png
new file mode 100644
index 0000000..b5771e2
Binary files /dev/null and b/static/logo.png differ
diff --git a/uni.promisify.adaptor.js b/uni.promisify.adaptor.js
new file mode 100644
index 0000000..92ea3cf
--- /dev/null
+++ b/uni.promisify.adaptor.js
@@ -0,0 +1,14 @@
+uni.addInterceptor({
+ returnValue(res) {
+ if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then ===
+ "function")) {
+ return res;
+ }
+ return new Promise((resolve, reject) => {
+ res.then((res) => {
+ if (!res) return resolve(res)
+ return res[0] ? reject(res[0]) : resolve(res[1])
+ });
+ });
+ },
+});
diff --git a/uni.scss b/uni.scss
new file mode 100644
index 0000000..63f638d
--- /dev/null
+++ b/uni.scss
@@ -0,0 +1,152 @@
+/* ==========================================
+ * 喝酒了么 - 设计令牌 (Design Tokens)
+ * 设计概念: 琥珀夜光 Amber Night
+ * ========================================== */
+
+/* --- 色彩系统 --- */
+$bg-base: #0B0B14; // 深夜底色
+$bg-card: #151520; // 卡片底色
+$bg-card-alt: #1C1C2A; // 卡片悬浮/选中态
+$bg-elevated: #222234; // 弹出层/输入框底色
+
+$amber: #E8A838; // 琥珀金 - 品牌主色
+$amber-light: #F5C563; // 浅金 - 高亮/数字
+$amber-deep: #C47F17; // 深琥珀 - 按压态
+$amber-glow: rgba(232, 168, 56, 0.15); // 琥珀光晕
+
+$coral: #FF6B6B; // 珊瑚红 - 警告/健康提醒
+$mint: #5EC6A0; // 薄荷绿 - 成功/正面
+$lavender: #8B85B8; // 淡紫灰 - 次要文本
+
+$text-primary: #FFFFFF; // 主文本
+$text-secondary: #7E7E96; // 次要文本
+$text-tertiary: #4A4A62; // 辅助文本
+$text-on-amber: #0B0B14; // 琥珀色上的文字
+
+/* --- 字体 --- */
+$font-base: -apple-system, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
+$font-num: 'DIN Alternate', 'Helvetica Neue', -apple-system, sans-serif;
+
+$fs-xs: 20rpx;
+$fs-sm: 24rpx;
+$fs-base: 28rpx;
+$fs-md: 30rpx;
+$fs-lg: 34rpx;
+$fs-xl: 40rpx;
+$fs-2xl: 52rpx;
+$fs-3xl: 64rpx;
+$fs-hero: 88rpx;
+
+$fw-normal: 400;
+$fw-medium: 500;
+$fw-bold: 700;
+$fw-black: 800;
+
+$lh-tight: 1.2;
+$lh-normal: 1.5;
+$lh-loose: 1.8;
+
+/* --- 间距 --- */
+$sp-xs: 8rpx;
+$sp-sm: 16rpx;
+$sp-md: 24rpx;
+$sp-lg: 32rpx;
+$sp-xl: 48rpx;
+$sp-2xl: 64rpx;
+$sp-3xl: 96rpx;
+
+/* --- 圆角 --- */
+$radius-sm: 12rpx;
+$radius-md: 20rpx;
+$radius-lg: 28rpx;
+$radius-xl: 40rpx;
+$radius-full: 999rpx;
+
+/* --- 阴影 --- */
+$shadow-sm: 0 4rpx 12rpx rgba(0, 0, 0, 0.25);
+$shadow-md: 0 8rpx 32rpx rgba(0, 0, 0, 0.35);
+$shadow-amber: 0 8rpx 32rpx rgba(232, 168, 56, 0.25);
+$shadow-lg: 0 16rpx 48rpx rgba(0, 0, 0, 0.45);
+
+/* --- 动画 --- */
+$ease-out: cubic-bezier(0.22, 1, 0.36, 1);
+$ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
+$duration-fast: 0.15s;
+$duration-base: 0.3s;
+$duration-slow: 0.5s;
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16px;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;
diff --git a/uniCloud-aliyun/database/JQL查询.jql b/uniCloud-aliyun/database/JQL查询.jql
new file mode 100644
index 0000000..35d21de
--- /dev/null
+++ b/uniCloud-aliyun/database/JQL查询.jql
@@ -0,0 +1,12 @@
+// 本文件用于,使用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();
diff --git a/uni_modules/uni-config-center/changelog.md b/uni_modules/uni-config-center/changelog.md
new file mode 100644
index 0000000..57dbcb5
--- /dev/null
+++ b/uni_modules/uni-config-center/changelog.md
@@ -0,0 +1,6 @@
+## 0.0.3(2022-11-11)
+- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
+## 0.0.2(2021-04-16)
+- 修改插件package信息
+## 0.0.1(2021-03-15)
+- 初始化项目
diff --git a/uni_modules/uni-config-center/package.json b/uni_modules/uni-config-center/package.json
new file mode 100644
index 0000000..bace866
--- /dev/null
+++ b/uni_modules/uni-config-center/package.json
@@ -0,0 +1,81 @@
+{
+ "id": "uni-config-center",
+ "displayName": "uni-config-center",
+ "version": "0.0.3",
+ "description": "uniCloud 配置中心",
+ "keywords": [
+ "配置",
+ "配置中心"
+],
+ "repository": "",
+ "engines": {
+ "HBuilderX": "^3.1.0"
+ },
+"dcloudext": {
+ "sale": {
+ "regular": {
+ "price": "0.00"
+ },
+ "sourcecode": {
+ "price": "0.00"
+ }
+ },
+ "contact": {
+ "qq": ""
+ },
+ "declaration": {
+ "ads": "无",
+ "data": "无",
+ "permissions": "无"
+ },
+ "npmurl": "",
+ "type": "unicloud-template-function"
+ },
+ "directories": {
+ "example": "../../../scripts/dist"
+ },
+ "uni_modules": {
+ "dependencies": [],
+ "encrypt": [],
+ "platforms": {
+ "cloud": {
+ "tcb": "y",
+ "aliyun": "y"
+ },
+ "client": {
+ "App": {
+ "app-vue": "u",
+ "app-nvue": "u"
+ },
+ "H5-mobile": {
+ "Safari": "u",
+ "Android Browser": "u",
+ "微信浏览器(Android)": "u",
+ "QQ浏览器(Android)": "u"
+ },
+ "H5-pc": {
+ "Chrome": "u",
+ "IE": "u",
+ "Edge": "u",
+ "Firefox": "u",
+ "Safari": "u"
+ },
+ "小程序": {
+ "微信": "u",
+ "阿里": "u",
+ "百度": "u",
+ "字节跳动": "u",
+ "QQ": "u"
+ },
+ "快应用": {
+ "华为": "u",
+ "联盟": "u"
+ },
+ "Vue": {
+ "vue2": "y",
+ "vue3": "u"
+ }
+ }
+ }
+ }
+}
diff --git a/uni_modules/uni-config-center/readme.md b/uni_modules/uni-config-center/readme.md
new file mode 100644
index 0000000..03f7fc2
--- /dev/null
+++ b/uni_modules/uni-config-center/readme.md
@@ -0,0 +1,93 @@
+# 为什么使用uni-config-center
+
+实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
+
+```bash
+cloudfunctions
+└─────common 公共模块
+ ├─plugin-a // 插件A对应的目录
+ │ ├─index.js
+ │ ├─config.json // plugin-a对应的配置文件
+ │ └─other-file.cert // plugin-a依赖的其他文件
+ └─plugin-b // plugin-b对应的目录
+ ├─index.js
+ └─config.json // plugin-b对应的配置文件
+```
+
+假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
+
+uni-config-center就是用了统一管理这些配置文件的,使用uni-config-center后的目录结构如下
+
+```bash
+cloudfunctions
+└─────common 公共模块
+ ├─plugin-a // 插件A对应的目录
+ │ └─index.js
+ ├─plugin-b // plugin-b对应的目录
+ │ └─index.js
+ └─uni-config-center
+ ├─index.js // config-center入口文件
+ ├─plugin-a
+ │ ├─config.json // plugin-a对应的配置文件
+ │ └─other-file.cert // plugin-a依赖的其他文件
+ └─plugin-b
+ └─config.json // plugin-b对应的配置文件
+```
+
+使用uni-config-center后的优势
+
+- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
+- 支持对config.json设置schema,插件使用者在HBuilderX内编写config.json文件时会有更好的提示(后续HBuilderX会提供支持)
+
+# 用法
+
+在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖,请参考:[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
+
+```js
+const createConfig = require('uni-config-center')
+
+const uniIdConfig = createConfig({
+ pluginId: 'uni-id', // 插件id
+ defaultConfig: { // 默认配置
+ tokenExpiresIn: 7200,
+ tokenExpiresThreshold: 600,
+ },
+ customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
+ // defaudltConfig 默认配置
+ // userConfig 用户配置
+ return Object.assign(defaultConfig, userConfig)
+ }
+})
+
+
+// 以如下配置为例
+// {
+// "tokenExpiresIn": 7200,
+// "passwordErrorLimit": 6,
+// "bindTokenToDevice": false,
+// "passwordErrorRetryTime": 3600,
+// "app-plus": {
+// "tokenExpiresIn": 2592000
+// },
+// "service": {
+// "sms": {
+// "codeExpiresIn": 300
+// }
+// }
+// }
+
+// 获取配置
+uniIdConfig.config() // 获取全部配置,注意:uni-config-center内不存在对应插件目录时会返回空对象
+uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置,返回:7200
+uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置,返回:300
+uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置,如果不存在则取传入的默认值,返回:600
+
+// 获取文件绝对路径
+uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
+
+// 引用文件(require)
+uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined,文件内有其他错误导致require失败时会抛出错误。
+
+// 判断是否包含某文件
+uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件,true: 文件存在,false: 文件不存在
+```
\ No newline at end of file
diff --git a/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js b/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js
new file mode 100644
index 0000000..00ba62f
--- /dev/null
+++ b/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js
@@ -0,0 +1 @@
+"use strict";var t=require("fs"),r=require("path");function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t),o=e(r),i="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var u=function(t){var r={exports:{}};return t(r,r.exports),r.exports}((function(t,r){var e="__lodash_hash_undefined__",n=9007199254740991,o="[object Arguments]",u="[object Function]",c="[object Object]",a=/^\[object .+?Constructor\]$/,f=/^(?:0|[1-9]\d*)$/,s={};s["[object Float32Array]"]=s["[object Float64Array]"]=s["[object Int8Array]"]=s["[object Int16Array]"]=s["[object Int32Array]"]=s["[object Uint8Array]"]=s["[object Uint8ClampedArray]"]=s["[object Uint16Array]"]=s["[object Uint32Array]"]=!0,s[o]=s["[object Array]"]=s["[object ArrayBuffer]"]=s["[object Boolean]"]=s["[object DataView]"]=s["[object Date]"]=s["[object Error]"]=s[u]=s["[object Map]"]=s["[object Number]"]=s[c]=s["[object RegExp]"]=s["[object Set]"]=s["[object String]"]=s["[object WeakMap]"]=!1;var l="object"==typeof i&&i&&i.Object===Object&&i,h="object"==typeof self&&self&&self.Object===Object&&self,p=l||h||Function("return this")(),_=r&&!r.nodeType&&r,v=_&&t&&!t.nodeType&&t,d=v&&v.exports===_,y=d&&l.process,g=function(){try{var t=v&&v.require&&v.require("util").types;return t||y&&y.binding&&y.binding("util")}catch(t){}}(),b=g&&g.isTypedArray;function j(t,r,e){switch(e.length){case 0:return t.call(r);case 1:return t.call(r,e[0]);case 2:return t.call(r,e[0],e[1]);case 3:return t.call(r,e[0],e[1],e[2])}return t.apply(r,e)}var w,O,m,A=Array.prototype,z=Function.prototype,M=Object.prototype,x=p["__core-js_shared__"],C=z.toString,F=M.hasOwnProperty,U=(w=/[^.]+$/.exec(x&&x.keys&&x.keys.IE_PROTO||""))?"Symbol(src)_1."+w:"",S=M.toString,I=C.call(Object),P=RegExp("^"+C.call(F).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),T=d?p.Buffer:void 0,q=p.Symbol,E=p.Uint8Array,$=T?T.allocUnsafe:void 0,D=(O=Object.getPrototypeOf,m=Object,function(t){return O(m(t))}),k=Object.create,B=M.propertyIsEnumerable,N=A.splice,L=q?q.toStringTag:void 0,R=function(){try{var t=vt(Object,"defineProperty");return t({},"",{}),t}catch(t){}}(),G=T?T.isBuffer:void 0,V=Math.max,W=Date.now,H=vt(p,"Map"),J=vt(Object,"create"),K=function(){function t(){}return function(r){if(!xt(r))return{};if(k)return k(r);t.prototype=r;var e=new t;return t.prototype=void 0,e}}();function Q(t){var r=-1,e=null==t?0:t.length;for(this.clear();++r-1},X.prototype.set=function(t,r){var e=this.__data__,n=nt(e,t);return n<0?(++this.size,e.push([t,r])):e[n][1]=r,this},Y.prototype.clear=function(){this.size=0,this.__data__={hash:new Q,map:new(H||X),string:new Q}},Y.prototype.delete=function(t){var r=_t(this,t).delete(t);return this.size-=r?1:0,r},Y.prototype.get=function(t){return _t(this,t).get(t)},Y.prototype.has=function(t){return _t(this,t).has(t)},Y.prototype.set=function(t,r){var e=_t(this,t),n=e.size;return e.set(t,r),this.size+=e.size==n?0:1,this},Z.prototype.clear=function(){this.__data__=new X,this.size=0},Z.prototype.delete=function(t){var r=this.__data__,e=r.delete(t);return this.size=r.size,e},Z.prototype.get=function(t){return this.__data__.get(t)},Z.prototype.has=function(t){return this.__data__.has(t)},Z.prototype.set=function(t,r){var e=this.__data__;if(e instanceof X){var n=e.__data__;if(!H||n.length<199)return n.push([t,r]),this.size=++e.size,this;e=this.__data__=new Y(n)}return e.set(t,r),this.size=e.size,this};var it,ut=function(t,r,e){for(var n=-1,o=Object(t),i=e(t),u=i.length;u--;){var c=i[it?u:++n];if(!1===r(o[c],c,o))break}return t};function ct(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":L&&L in Object(t)?function(t){var r=F.call(t,L),e=t[L];try{t[L]=void 0;var n=!0}catch(t){}var o=S.call(t);n&&(r?t[L]=e:delete t[L]);return o}(t):function(t){return S.call(t)}(t)}function at(t){return Ct(t)&&ct(t)==o}function ft(t){return!(!xt(t)||function(t){return!!U&&U in t}(t))&&(zt(t)?P:a).test(function(t){if(null!=t){try{return C.call(t)}catch(t){}try{return t+""}catch(t){}}return""}(t))}function st(t){if(!xt(t))return function(t){var r=[];if(null!=t)for(var e in Object(t))r.push(e);return r}(t);var r=yt(t),e=[];for(var n in t)("constructor"!=n||!r&&F.call(t,n))&&e.push(n);return e}function lt(t,r,e,n,o){t!==r&&ut(r,(function(i,u){if(o||(o=new Z),xt(i))!function(t,r,e,n,o,i,u){var a=gt(t,e),f=gt(r,e),s=u.get(f);if(s)return void rt(t,e,s);var l=i?i(a,f,e+"",t,r,u):void 0,h=void 0===l;if(h){var p=Ot(f),_=!p&&At(f),v=!p&&!_&&Ft(f);l=f,p||_||v?Ot(a)?l=a:Ct(j=a)&&mt(j)?l=function(t,r){var e=-1,n=t.length;r||(r=Array(n));for(;++e-1&&t%1==0&&t0){if(++r>=800)return arguments[0]}else r=0;return t.apply(void 0,arguments)}}(pt);function jt(t,r){return t===r||t!=t&&r!=r}var wt=at(function(){return arguments}())?at:function(t){return Ct(t)&&F.call(t,"callee")&&!B.call(t,"callee")},Ot=Array.isArray;function mt(t){return null!=t&&Mt(t.length)&&!zt(t)}var At=G||function(){return!1};function zt(t){if(!xt(t))return!1;var r=ct(t);return r==u||"[object GeneratorFunction]"==r||"[object AsyncFunction]"==r||"[object Proxy]"==r}function Mt(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=n}function xt(t){var r=typeof t;return null!=t&&("object"==r||"function"==r)}function Ct(t){return null!=t&&"object"==typeof t}var Ft=b?function(t){return function(r){return t(r)}}(b):function(t){return Ct(t)&&Mt(t.length)&&!!s[ct(t)]};function Ut(t){return mt(t)?tt(t,!0):st(t)}var St,It=(St=function(t,r,e){lt(t,r,e)},ht((function(t,r){var e=-1,n=r.length,o=n>1?r[n-1]:void 0,i=n>2?r[2]:void 0;for(o=St.length>3&&"function"==typeof o?(n--,o):void 0,i&&function(t,r,e){if(!xt(e))return!1;var n=typeof r;return!!("number"==n?mt(e)&&dt(r,e.length):"string"==n&&r in e)&&jt(e[r],t)}(r[0],r[1],i)&&(o=n<3?void 0:o,n=1),t=Object(t);++ec.call(t,r);class f{constructor({pluginId:t,defaultConfig:r={},customMerge:e,root:n}){this.pluginId=t,this.defaultConfig=r,this.pluginConfigPath=o.default.resolve(n||__dirname,t),this.customMerge=e,this._config=void 0}resolve(t){return o.default.resolve(this.pluginConfigPath,t)}hasFile(t){return n.default.existsSync(this.resolve(t))}requireFile(t){try{return require(this.resolve(t))}catch(t){if("MODULE_NOT_FOUND"===t.code)return;throw t}}_getUserConfig(){return this.requireFile("config.json")}config(t,r){if(!this._config){const t=this._getUserConfig();this._config=Array.isArray(t)?t:(this.customMerge||u)(this.defaultConfig,t)}let e=this._config;return t?function(t,r,e){if("number"==typeof r)return t[r];if("symbol"==typeof r)return a(t,r)?t[r]:e;const n="string"!=typeof(o=r)?o:o.split(".").reduce(((t,r)=>(r.split(/\[([^}]+)\]/g).forEach((r=>r&&t.push(r))),t)),[]);var o;let i=t;for(let t=0;tparseInt(e)):void 0}function o(e,n){const i=r(e),t=r(n);return i?t?function(e,n){const i=Math.max(e.length,n.length);for(let t=0;tr)return 1;if(i=e)throw new Error("Config error, tokenExpiresThreshold should be less than tokenExpiresIn");n>e/2&&console.warn(`Please check whether the tokenExpiresThreshold configuration is set too large, tokenExpiresThreshold: ${n}, tokenExpiresIn: ${e}`)}get customToken(){return this.uniId.interceptorMap.get("customToken")}isTokenInDb(e){return o(e,"1.0.10")>=0}async getUserRecord(){if(this.userRecord)return this.userRecord;const e=await C.doc(this.uid).get();if(this.userRecord=e.data[0],!this.userRecord)throw{errCode:i.ACCOUNT_NOT_EXISTS};switch(this.userRecord.status){case void 0:case 0:break;case 1:throw{errCode:i.ACCOUNT_BANNED};case 2:throw{errCode:i.ACCOUNT_AUDITING};case 3:throw{errCode:i.ACCOUNT_AUDIT_FAILED};case 4:throw{errCode:i.ACCOUNT_CLOSED}}if(this.oldTokenPayload){if(this.isTokenInDb(this.oldTokenPayload.uniIdVersion)){if(-1===(this.userRecord.token||[]).indexOf(this.oldToken))throw{errCode:i.CHECK_TOKEN_FAILED}}if(this.userRecord.valid_token_date&&this.userRecord.valid_token_date>1e3*this.oldTokenPayload.iat)throw{errCode:i.TOKEN_EXPIRED}}return this.userRecord}async updateUserRecord(e){await C.doc(this.uid).update(e)}async getUserPermission(){if(this.userPermission)return this.userPermission;const e=(await this.getUserRecord()).role||[];if(0===e.length)return this.userPermission={role:[],permission:[]},this.userPermission;if(e.includes("admin"))return this.userPermission={role:e,permission:[]},this.userPermission;const n=await T.where({role_id:I.in(e)}).get(),i=(t=n.data.reduce((e,n)=>(n.permission&&e.push(...n.permission),e),[]),Array.from(new Set(t)));var t;return this.userPermission={role:e,permission:i},this.userPermission}async _createToken({uid:e,role:n,permission:t}={}){if(!n||!t){const e=await this.getUserPermission();n=e.role,t=e.permission}let r={uid:e,role:n,permission:t};if(this.uniId.interceptorMap.has("customToken")){const i=this.uniId.interceptorMap.get("customToken");if("function"!=typeof i)throw new Error("Invalid custom token file");r=await i({uid:e,role:n,permission:t})}const o=Date.now(),{tokenSecret:s,tokenExpiresIn:c,maxTokenLength:a=10}=this.config,u=g({...r,uniIdVersion:"1.0.19"},s,{expiresIn:c}),d=await this.getUserRecord(),l=(d.token||[]).filter(e=>{try{const n=this._checkToken(e);if(d.valid_token_date&&d.valid_token_date>1e3*n.iat)return!1}catch(e){if(e.errCode===i.TOKEN_EXPIRED)return!1}return!0});return l.push(u),l.length>a&&l.splice(0,l.length-a),await this.updateUserRecord({last_login_ip:this.clientInfo.clientIP,last_login_date:o,token:l}),{token:u,tokenExpired:o+1e3*c}}async createToken({uid:e,role:n,permission:t}={}){if(!e)throw{errCode:i.PARAM_REQUIRED,errMsgValue:{param:"uid"}};this.uid=e;const{token:r,tokenExpired:o}=await this._createToken({uid:e,role:n,permission:t});return{errCode:0,token:r,tokenExpired:o}}async refreshToken({token:e}={}){if(!e)throw{errCode:i.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const n=this._checkToken(e);this.uid=n.uid,this.oldTokenPayload=n;const{uid:t}=n,{role:r,permission:o}=await this.getUserPermission(),{token:s,tokenExpired:c}=await this._createToken({uid:t,role:r,permission:o});return{errCode:0,token:s,tokenExpired:c}}_checkToken(e){const{tokenSecret:n}=this.config;let t;try{t=k(e,n)}catch(e){if("TokenExpiredError"===e.name)throw{errCode:i.TOKEN_EXPIRED};throw{errCode:i.CHECK_TOKEN_FAILED}}return t}async checkToken(e,{autoRefresh:n=!0}={}){if(!e)throw{errCode:i.CHECK_TOKEN_FAILED};this.oldToken=e;const t=this._checkToken(e);this.uid=t.uid,this.oldTokenPayload=t;const{tokenExpiresThreshold:r}=this.config,{uid:o,role:s,permission:c}=t,a={role:s,permission:c};if(!s&&!c){const{role:e,permission:n}=await this.getUserPermission();a.role=e,a.permission=n}if(!r||!n){const e={code:0,errCode:0,...t,...a};return delete e.uniIdVersion,e}const u=Date.now();let d={};1e3*t.exp-u<1e3*r&&(d=await this._createToken({uid:o}));const l={code:0,errCode:0,...t,...a,...d};return delete l.uniIdVersion,l}}var E=Object.freeze({__proto__:null,checkToken:async function(e,{autoRefresh:n=!0}={}){return new m({uniId:this}).checkToken(e,{autoRefresh:n})},createToken:async function({uid:e,role:n,permission:i}={}){return new m({uniId:this}).createToken({uid:e,role:n,permission:i})},refreshToken:async function({token:e}={}){return new m({uniId:this}).refreshToken({token:e})}});const w=require("uni-config-center")({pluginId:"uni-id"});class x{constructor({context:e,clientInfo:n,config:i}={}){this._clientInfo=e?function(e){return{appId:e.APPID,platform:e.PLATFORM,locale:e.LOCALE,clientIP:e.CLIENTIP,deviceId:e.DEVICEID}}(e):n,this._config=i,this.config=this._getOriginConfig(),this.interceptorMap=new Map,w.hasFile("custom-token.js")&&this.setInterceptor("customToken",require(w.resolve("custom-token.js")));this._i18n=uniCloud.initI18n({locale:this._clientInfo.locale,fallbackLocale:"zh-Hans",messages:JSON.parse(JSON.stringify(d))}),d[this._i18n.locale]||this._i18n.setLocale("zh-Hans")}setInterceptor(e,n){this.interceptorMap.set(e,n)}_t(...e){return this._i18n.t(...e)}_parseOriginConfig(e){return Array.isArray(e)?e:e[0]?Object.values(e):e}_getOriginConfig(){if(this._config)return this._config;if(w.hasFile("config.json")){let e;try{e=w.config()}catch(e){throw new Error("Invalid uni-id config file\n"+e.message)}return this._parseOriginConfig(e)}try{return this._parseOriginConfig(require("uni-id/config.json"))}catch(e){throw new Error("Invalid uni-id config file")}}_getAppConfig(){const e=this._getOriginConfig();return Array.isArray(e)?e.find(e=>e.dcloudAppid===this._clientInfo.appId)||e.find(e=>e.isDefaultConfig):e}_getPlatformConfig(){const e=this._getAppConfig();if(!e)throw new Error(`Config for current app (${this._clientInfo.appId}) was not found, please check your config file or client appId`);let n;switch(["app-plus","app-android","app-ios"].indexOf(this._clientInfo.platform)>-1&&(this._clientInfo.platform="app"),"h5"===this._clientInfo.platform&&(this._clientInfo.platform="web"),this._clientInfo.platform){case"web":n="h5";break;case"app":n="app-plus"}const i=[{tokenExpiresIn:7200,tokenExpiresThreshold:1200,passwordErrorLimit:6,passwordErrorRetryTime:3600},e];n&&e[n]&&i.push(e[n]),i.push(e[this._clientInfo.platform]);const t=Object.assign(...i);return["tokenSecret","tokenExpiresIn"].forEach(e=>{if(!t||!t[e])throw new Error(`Config parameter missing, ${e} is required, please check your config file uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json`)}),t}_getConfig(){return this._getPlatformConfig()}}for(const e in E)x.prototype[e]=E[e];function y(e){const n=new x(e);return new Proxy(n,{get(e,n){if(n in e&&0!==n.indexOf("_")){if("function"==typeof e[n])return(i=e[n],function(){let e;try{e=i.apply(this,arguments)}catch(e){if(a(e))return c.call(this,e),e;throw e}return t(e)?e.then(e=>(a(e)&&c.call(this,e),e),e=>{if(a(e))return c.call(this,e),e;throw e}):(a(e)&&c.call(this,e),e)}).bind(e);if("context"!==n&&"config"!==n)return e[n]}var i}})}x.prototype.createInstance=y;const O={createInstance:y};module.exports=O;
diff --git a/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json b/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json
new file mode 100644
index 0000000..37b425a
--- /dev/null
+++ b/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "uni-id-common",
+ "version": "1.0.19",
+ "description": "uni-id token生成、校验、刷新",
+ "main": "index.js",
+ "homepage": "https:\/\/uniapp.dcloud.io\/uniCloud\/uni-id-common.html",
+ "repository": {
+ "type": "git",
+ "url": "git+https:\/\/gitee.com\/dcloud\/uni-id-common.git"
+ },
+ "author": "DCloud",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "uni-config-center": "file:..\/..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center"
+ },
+ "origin-plugin-dev-name": "uni-id-common",
+ "origin-plugin-version": "1.0.19",
+ "plugin-dev-name": "uni-id-common",
+ "plugin-version": "1.0.19"
+}
\ No newline at end of file