只因为给我打了0.1分,重新撸了个记账小程序
2019年的时候和朋友出去旅行,因为需要A账单,所以前一天开发了一个记账小程序,时间匆忙,就随便完成基础记账和AA计算功能后就上线,旅行结束后也就没用过了,前几天无意登录,发现被打了1.0分。 叔能忍,婶婶不可忍,摸鱼两周升级完成新的记账小程序。
技术栈:uni-app + uniCloud + TM-vuetify
具体实现功能思路:
- 用户进入小程序自动登录,未注册自动注册,免去登录步骤
- 记账功能
- 多种记账类型(ICON图标)
- 可选记账日期
- 计算器(因为小程序为了安全起见不支持eval,所以这块需要自己实现)
- 月度账单图表查看(收支排行榜、当月结余)
- 年度账单查看
- 共享协同账单(目前只实现了AA协同方式,因为太懒了,其他先不搞了)
- 个人中心
- 用户设置(头像啊、昵称什么的,共享账单不得知道谁是谁)
- 意见反馈(用于收集用户反馈bug等,ps:不得给用户留个发泄口,免得再给我打1.0分)
- 分享功能
- 在线客服
需求理清楚了,开干!(好吧,其实并没有,因为没有设计天赋,所以摆烂了两天,o(╥﹏╥)o)
用户登录/注册
因为微信小程序一直在变更用户信息获取方式 从# UserInfo => # wx.getUserInfo => # wx.getUserProfile 最后到现在的需要通过用户授权才能获取到头像和昵称,所以初始化注册无法自动补全用户信息 # 小程序用户头像昵称获取规则
前端通过 uni.login 获取用户code
uni.login({
success: async (res) => {
// 获取用户code
const {code} = res
// 调用云函数登录/注册
uniCloud.callFunction({
name: 'user',
data: {code}
}).then(res => {
// 存储返回的当前用户的信息
uni.setStorageSync('userInfo', JSON.stringify(res.result))
this.$store.dispatch('setUserInfo', res.result)
})
}
})
user云函数负责处理登录返回用户信息,未注册则自动注册后返回用户信息
/*
* 通过请求session接口获取用户openId(微信用户唯一标识)
* appid、secret在公众平台查看
*/
const res = await uniCloud.httpclient.request('https://api.weixin.qq.com/sns/jscode2session', {
method: 'GET',
data: {
appid,
secret,
js_code,
grant_type: 'authorization_code'
},
contentType: 'json',
dataType: 'json'
})
/*
* 通过user集合的length可判断是否注册
*/
const {data:user} = await db.collection('user').where({openId}).get()
if(user.length > 0){
// 已注册过直接返回用户信息
return user[0]
}else{
// 未注册则创建用户后返回用户信息
const userObj = {
nickname: `微信用户${str}`, // 因为初始化无法获取,所以给默认昵称,str:随机字符串
avatar: null,
openId,
phone: ''
}
await db.collection('user').add(userObj)
return userObj
}
记账
ICON图标
引用阿里字体图标,将资源包导入项目static目录,App.vue引入iconfont.css
创建icon组件,通过name参数生成图标
<text :class="'iconfont icon-' + name"></text>
计算器
因为微信小程序因为安全策略不支持eval函数,所以这块通过另类实现计算器功能
// 操作符
const code_symbol = ['.', '+', '-']
let str = (this.bill.money === '0' ? '0' : this.bill.money)
const end = str[str.length - 1]
// 禁止触发多个操作符
if (
(code_symbol.includes(key) && !code_symbol.includes(end)) ||
(!code_symbol.includes(key))
) str += key
// 判断操作符变更
if (code_symbol.includes(end) && end !== key) {
let key_arr = str.split('')
key_arr[key_arr.length - 1] = key
str = key_arr.join('')
}
/*
* 计算同理
*/
const code_symbol = ['+', '-']
// 格式化处理展示金额值
const arr = this.formatStr(this.bill.money)
let prev, result
for (let i = 0; i < arr.length - 1; i++) {
const item = arr[i]
const next = arr[i + 1]
// 处理点位符,处理最后位数为操作符
if (code_symbol.includes(item)) {
item === '+' ? (result = parseFloat(prev || 0) + parseFloat(next || 0)) : (result =parseFloat( prev || 0) - parseFloat(next || 0))
prev = result
} else {
prev = result || arr[i]
}
}
/*
* 格式化处理计算 string
* return {Array}
*/
formatStr(str) {
let arr = []
const code_symbol = ['+', '-']
for (let i = 0; i < str.length; i++) {
let len = arr.length === 0 ? 0 : arr.length - 1
const v = str[i]
if (code_symbol.includes(v)) {
arr.push(v)
} else {
if (code_symbol.includes(arr[len])) {
arr[len + 1] = (arr[len + 1] || '') + v
} else {
arr[len] = (arr[len] || '') + v
}
}
}
return arr
}
月度账单
引入echarts图表,实现收支饼状图
this.$refs.incomeChart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
grid: {
left: 20,
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 10,
bottom: 30,
data: this.incomeData.map((v) => {
return v.name
}),
formatter: function(name) {
return name.length > 4 ? `${name.substring(0,4)}...` : name;
}
},
toolbox: {
show: true,
feature: {
mark: {
show: true
},
dataView: {
show: true,
readOnly: false
},
restore: {
show: true
},
saveAsImage: {
show: true
}
}
},
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ["30%", "50%"],
label: {
show: false
},
emphasis: {
label: {
show: true
}
},
data: this.incomeData
}]
年度账单
这块需要判断查询年份是否为当年,如果为本年度则只展示到当前月份,如果为过去年份,则展示全年统计
const now = new Date().getFullYear()
const months = ((this.year === now) ? (new Date().getMonth() + 1) : 12)
协同账单
// 账单Schema
{
name, // 账单名称
img, // 缩略图
users: [userId], // 成员、默认加入创建人
createTime: Date.now(),
bills: [], // 账单
createUser: userId,
audit, // 是否需要审核
member, // 是否允许成员记账
allocation // 计算方式
}
// 邀请用户自动加入
if (!res.users.includes(this.userInfo.openId)) {
// 判断是否开始审核
if (!res.audit) {
await uniCloud.callFunction({
name: 'add-share-user',
data: {
_id: this.shareId,
openId: this.userInfo.openId
}
})
this.getDetail(this.shareId)
} else {
// 触发审核
const params = {
ownId: res.createUser,
shareBillId: this.shareId,
auditId: this.userInfo.openId
}
await uniCloud.callFunction({
name: 'send-audit',
data: params
})
setTimeout(() => {
this.$refs.toast.show({
model: 'warn',
label: '创建者已开启审核,待审核通过后自动加入'
})
}, 1000)
}
}
我的
这块主要说下个人设置,因为现在获取用户头像和昵称需要用户授权 官方文档说的是通过个人设置页面让用户授权获取,参考文档:头像昵称填写能力
注:如果需要存储用户手机号等敏感信息,务必在填写页面声明用户协议及隐私政策,否则不予通过
总结
记录下时隔好几年重新写小程序,官方生态很多都变了,现在官方越来越注重用户隐私这块了,基本上所以牵扯到用户的地方都需要用户授权后才可调用。代码大部分还是无脑梭出来的,毕竟时间有限,后续还需要优化代码,但是喜欢摆烂就这样吧。对了,我还接入了ChatGPT,但是被官方禁止了,不允许接入,那页面都做了能怎么办,接了个睿智机器人。