From f945ccf4c7f1d040a140cf2966fddc0961a348e2 Mon Sep 17 00:00:00 2001 From: cheng <545895878@qq.com> Date: Mon, 13 Jul 2026 22:15:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E5=96=9D=E9=85=92=E4=BA=86=E4=B9=88=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加项目配置文件(.editorconfig, .gitignore) - 创建App.vue主应用文件,包含启动时Mock数据初始化 - 添加index.html和main.js应用入口文件 - 配置manifest.json应用基本信息和权限设置 - 设置pages.json页面路由和全局样式 - 添加uni.promisify.adaptor.js适配器文件 - 创建uni.scss设计令牌系统,定义琥珀夜光主题色彩 - 新增constants.js常量定义,包含酒类分类和品牌数据 --- .editorconfig | 9 + .gitignore | 3 + App.vue | 305 ++++ common/constants.js | 202 +++ common/mock-data.js | 150 ++ common/utils.js | 222 +++ components/AchievementBadge.vue | 99 ++ components/DrinkCalendar.vue | 197 +++ components/DrinkCard.vue | 767 ++++++++++ he.md | 559 ++++++++ index.html | 20 + main.js | 24 + manifest.json | 128 ++ pages.json | 72 + pages/card/card.vue | 291 ++++ pages/index/index.vue | 540 +++++++ pages/login/login.vue | 394 +++++ pages/onboarding/onboarding.vue | 235 +++ pages/profile/profile.vue | 433 ++++++ pages/record/record.vue | 1262 +++++++++++++++++ static/logo.png | Bin 0 -> 4023 bytes uni.promisify.adaptor.js | 14 + uni.scss | 152 ++ uniCloud-aliyun/database/JQL查询.jql | 12 + uni_modules/uni-config-center/changelog.md | 6 + uni_modules/uni-config-center/package.json | 81 ++ uni_modules/uni-config-center/readme.md | 93 ++ .../common/uni-config-center/index.js | 1 + .../common/uni-config-center/package.json | 13 + uni_modules/uni-id-common/changelog.md | 38 + uni_modules/uni-id-common/package.json | 101 ++ uni_modules/uni-id-common/readme.md | 3 + .../common/uni-id-common/index.js | 1 + .../common/uni-id-common/package.json | 20 + 34 files changed, 6447 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 App.vue create mode 100644 common/constants.js create mode 100644 common/mock-data.js create mode 100644 common/utils.js create mode 100644 components/AchievementBadge.vue create mode 100644 components/DrinkCalendar.vue create mode 100644 components/DrinkCard.vue create mode 100644 he.md create mode 100644 index.html create mode 100644 main.js create mode 100644 manifest.json create mode 100644 pages.json create mode 100644 pages/card/card.vue create mode 100644 pages/index/index.vue create mode 100644 pages/login/login.vue create mode 100644 pages/onboarding/onboarding.vue create mode 100644 pages/profile/profile.vue create mode 100644 pages/record/record.vue create mode 100644 static/logo.png create mode 100644 uni.promisify.adaptor.js create mode 100644 uni.scss create mode 100644 uniCloud-aliyun/database/JQL查询.jql create mode 100644 uni_modules/uni-config-center/changelog.md create mode 100644 uni_modules/uni-config-center/package.json create mode 100644 uni_modules/uni-config-center/readme.md create mode 100644 uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js create mode 100644 uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/package.json create mode 100644 uni_modules/uni-id-common/changelog.md create mode 100644 uni_modules/uni-id-common/package.json create mode 100644 uni_modules/uni-id-common/readme.md create mode 100644 uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/index.js create mode 100644 uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5771e209bb677e2ebd5ff766ad5ee11790f305a GIT binary patch literal 4023 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 SYs}WS&%*SCNx89m_ { + 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