前期准备

飞书官方客户端文档:https://open.feishu.cn/document/home/intro

飞书官方工具资源文档:https://open.feishu.cn/document/uYjL24iN/uEzMzUjLxMzM14SMzMTN/develop-gadget-with-uni-app

经过对比选型,决定使用uniapp框架进行开发,因为需求较简单,所以ui库就直接用了uniapp官方提供的库。

uniapp官方文档:https://uniapp.dcloud.net.cn/tutorial/

uniapp的论坛也提供了一些轮子:https://ext.dcloud.net.cn/

附:

(taro官网和ui库打不开:https://taro.jd.com/
ps:muse-ui没有日期范围组件,uView没有表格组件,vant没有飞书小程序版本,uniapp的ui库有一丢丢古早
 

开始开发

根据官方文档的步骤一路操作下来后,已经可以用hbuilder搭建一个新项目,配置好飞书开发者工具的路径后,通过运行将飞书开发者工具唤醒了。

导入项目后,就可以正式开发了。

由于基础的api,飞书和uniapp的官方文档中已经写得很清楚,可以直接参阅文档。

引入官方ui库:https://uniapp.dcloud.net.cn/component/uniui/quickstart.html

接下来开始配置store。

uniaap生成的项目中,已经内嵌了vuex,我因为一直使用React开发,已经很久没有接触过vue了,因此对照着文档进行了学习:https://uniapp.dcloud.net.cn/tutorial/vue3-vuex.html

整理一下配置步骤:

1.首先在项目根目录下新建store文件夹,其下新建index.js:

2.index.js的内容为:

// // 组装模块并导出 store 的地方
import {
createStore
} from 'vuex'
import {
tabbarList
} from '@/utils.js'; const store = createStore({
// 存放状态
state: {
"code": '',
"openId": '',
"userInfo": {},
},
getters: {
getCode(state) {
return state.code || ''
},
getToken(state) {
return state.openId || ''
},
getUserInfo(state) {
return state.userInfo || {}
},
},
// 同步函数
mutations: {
setCode(state, payload) {
state.code = payload.code || ''
},
setUserInfo(state, payload) {
state.userInfo = payload || {}
},
setOpenId(state, payload) {
state.openId = payload || ''
},
},
// 提交 mutation,通过 mutation 改变 state ,而不是直接变更状态,可以包含任意异步操作
actions: {
// 登录系统
adsLogin({
commit,
state
}, payload) {
// 清理本地ads登录相关的缓存
uni.removeStorageSync('OPEN_ID');
uni.removeStorageSync('USER_INFO');
return new Promise((resolve, reject) => {
uni.request({
url: '/login',
method: 'POST',
data: {
code: state.code,
},
success: (res) => {
const {
code,
message,
result
} = res.data;
if (code === 0 && result) {
commit('setUserInfo', result)
commit('setOpenId', result.open_id)
uni.setStorageSync('USER_INFO', result) // 存储userInfo
uni.setStorageSync('OPEN_ID', result.open_id) // 存储open_id
if (resolve) resolve(result)
} else {
uni.showToast({
title: message || '操作失败',
icon: 'error',
duration: 3000
})
if (reject) reject(res)
}
},
fail: err => {
console.log(err, 'err');
uni.showToast({
title: err.errMsg || '请求错误',
icon: 'fail',
duration: 2000
})
if (reject) reject(err)
}
});
}) }
}
}) export default store

  

其中的一些API,文档中都有很详细的介绍:

------------------------------------

state 用于存放数据(be like React中的state)
getters 用于获取数据
mutations 为同步函数,我理解为对数据进行处理和存储
actions 为提交mutation的一种行为,我理解为需要复杂操作操作(比如异步请求)时,可以配置在这里(be like React开发中的Redux中的dispatch,不过现在都用hooks了)
------------------------------------
我这里只配置了一个actions,那就是登录后台系统的操作,使用Promise的两个回调把接口请求的结果拿出来,外部调用时就可以获取到。下面是App.vue的代码:
 
<script>
import store from '@/store/index.js'; // 引入store
import {
mapGetters,
mapActions
} from 'vuex';
import qs from 'qs'; export default {
computed: {
...mapGetters({
code: 'getCode',
token: 'getToken'
})
},
// 监听小程序初始化
onLaunch: function() {
// 小程序初始化后全局执行一次,若【未登录ads|token过期】则触发登录,否则直接进入主页面
const initCommon = () => {
uni.request({
url: '/jzData/common/init',
header: {
Authorization: `Bearer ${uni.getStorageSync('OPEN_ID')}`,
},
success: (res) => {
const {
code,
message,
result
} = res.data;
if (code === 0 && result) {
uni.$emit('hasLogin');
store.commit('setCommon', result)
} else if (code === 50000) {
// 如果接口返回code为50000,则说明ads登录过期,需要重新登录
getAdsLogin()
} else {
uni.showToast({
title: message || '操作失败',
icon: 'error',
duration: 2000
})
}
}
});
} const getAdsLogin = () => {
// 服务器问题-服务器缺省页;账号不存在-权限缺省页;网络问题-网络缺省页
store.dispatch('adsLogin').then(() => {
uni.$emit('hasLogin');
initCommon()
})
.catch((res) => {
uni.$emit('notLogin');
if (res.statusCode === 500) {
uni.redirectTo({
url: `/pages/500/500`
});
} else {
const message = res?.data?.message || '';
//关闭当前页面,跳转到403无权限页面
uni.redirectTo({
url: `/pages/403/403?msg=${message}`
});
}
});
}
// 登录并获取用户信息[每次进入小程序都执行,只对ads系统的登录状态做判断]
tt.login({
success(res) {
// 存储飞书code,用于请求时传参
store.commit({
type: 'setCode',
code: res.code || ''
})
// 如果已有openid在缓存,则不需要登录ads系统,存储userInfo&open_id
if (uni.getStorageSync('OPEN_ID')) {
store.commit('setUserInfo', uni.getStorageSync('USER_INFO') || {})
store.commit('setOpenId', uni.getStorageSync('OPEN_ID') || '')
initCommon()
} else {
// 使用小程序登录后返回的code登录ads系统
// 服务器问题-服务器缺省页;账号不存在-权限缺省页;网络问题-网络缺省页
store.dispatch('adsLogin').then((res) => {
uni.$emit('hasLogin');
const openId = res?.open_id;
uni.request({
url: '/init',
header: {
Authorization: `Bearer ${openId}`,
},
success: (res) => {
const {
code,
message,
result
} = res.data;
if (code === 0 && result) {
store.commit('setCommon', result)
} else {
uni.showToast({
title: message || '操作失败',
icon: 'error',
duration: 2000
})
}
}
});
})
.catch((res) => {
uni.$emit('notLogin');
if (res.statusCode === 500) {
uni.redirectTo({
url: `/pages/500/500`
});
} else {
const message = res?.data?.message || '';
//关闭当前页面,跳转到403无权限页面
uni.redirectTo({
url: `/pages/403/403?msg=${message}`
});
}
});
}
},
fail(res) {
console.log(`飞书小程序登陆失败: ${JSON.stringify(res)}`);
uni.$emit('failLogin');
uni.redirectTo({
url: `/pages/404/404`
});
}
}); // 全局添加拦截器
uni.addInterceptor('request', {
invoke(args) {
const dev = 'https://xx.com';
const pre = 'https://yy.com';
const pro = 'https://zz.com';
// args.url = (process.env.NODE_ENV === 'development' ? dev : pro) + args.url;
// 发布测试版
const params = args.data;
if (args.method === 'GET' || !args.method) {
args.url = pre + args.url + `?${qs.stringify(params, { arrayFormat: 'brackets' })}`;
args.data = {}
} else {
args.url = pre + args.url;
}
console.log('请求内容:', args)
// args.header = {
// ...args.header,
// Authorization: `Bearer ${this.token}`,
// }
},
success(args) {
console.log('请求成功:', args)
},
fail(err) {
console.log('请求失败:', err)
},
})
},
onShow: function() {
// console.log('App Show')
},
onHide: function() {},
onPageNotFound() {
uni.redirectTo({
url: '/pages/404/404'
})
},
methods: {
...mapActions([
'adsLogin',
]),
}
}
</script> <style lang="scss">
/*每个页面公共css */
@import './static/font/iconfont.css'; body {
color: $uni-text-color;
font-size: 28rpx;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
padding-bottom: 40rpx;
}
</style>

  

逻辑处理很简单(因为真的很小一项目,请教了大佬后确定就简单做):先登录飞书,拿到飞书的code之后,请求后台系统,获取后台系统返回的openId,这个字段用于后续所有接口请求时拼接在头部。

3.store的主文件写完后,需要配置到main.js中(爷直接复制官方文档),就可以生效了:

import App from './App'
import store from './store'
import {
createSSRApp
} from 'vue' // #ifndef VUE3
import Vue from 'vue' Vue.prototype.$store = store
Vue.config.productionTip = false App.mpType = 'app'
const app = new Vue({
store,
...App
})
app.$mount()
// #endif // #ifdef VUE3
export function createApp() {
const app = createSSRApp(App)
app.use(store)
return {
app
}
}
// #endif

  

4.页面中使用:

方法中就可以直接获取到:

同样模板代码中也可以直接拿到:

接下来就是页面的开发。首先明确页面配置都是在pages.json中进行,包括tabber页的各种配置,这些文档中都有提及。
但是开发过程中遇到了tabber需要权限控制的问题,所以没有用原生的tabber,自己写了个组件(但是pages.json中仍旧需要配置tabber的地址),以下是pages.json的代码:
{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index"
},
{
"path": "pages/summary/summary",
"style": {
"enablePullDownRefresh": true
}
},
// 项目概况
{
"path": "pages/overview/overview",
"style": {
"navigationBarTitleText": "项目概况",
"enablePullDownRefresh": true
}
},
// 买量概况
{
"path": "pages/buyVolume/buyVolume",
"style": {
"navigationBarTitleText": "买量概况",
"enablePullDownRefresh": true
}
},
// 媒体概况
{
"path": "pages/media/media",
"style": {
"navigationBarTitleText": "媒体概况",
"enablePullDownRefresh": true
}
},
// 人员概况
{
"path": "pages/person/person",
"style": {
"navigationBarTitleText": "人员概况",
"enablePullDownRefresh": true
}
},
{
"path": "pages/500/500",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/404/404",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/403/403",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "Data(应用)",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {},
"tabBar": {
"list": [{
"pagePath": "pages/overview/overview"
},
{
"pagePath": "pages/buyVolume/buyVolume"
},
{
"pagePath": "pages/media/media"
},
{
"pagePath": "pages/person/person"
}
]
}
}

  

关于自定义组件,就记录一个自定义tabber来参考:

首先在components文件夹下新建组件:

功能较简单,就不赘述了,贴一下代码万一以后拿去复制:

<template>
<view class="tab-bar">
<view class="tab-bar-border"></view>
<view v-for="(item,index) in tabBarList" :key="index" class="tab-bar-item" :data-id="index" @click="jump(item)">
<image :src="current === item.index ? item.selectedIconPath : item.iconPath"></image>
<view :style="{'color':current === item.index ? '#70b603' : '#909399'}" style="margin-top: 10rpx;">
{{item.text}}
</view>
</view>
</view> </template> <script> export default {
name: "footer-tabbar",
props: {
tabBarList: {
type: Array,
default: uni.getStorageSync('tabBarList')
},
current: Number,
gameId: String | Number
},
data() {
return {
value1: 0, // 默认页面
inactiveColor: '#909399' // 高亮颜色
}
},
onShow() {
},
methods: {
// 点击跳转对应tabbar页面
jump(e) {
uni.switchTab({
url: e.pagePath
})
}
}
}
</script> <style lang="scss" scoped>
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
border-top: 1px solid #ccc;
background: white;
display: flex;
z-index: 98;
} .tab-bar-border {
// background-color: rgba(0, 0, 0, 0.33);
background-color: white;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
border-top: 2rpx solid rgba(187, 187, 187, 0.3);
transform: scaleY(0.5);
} .tab-bar-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
} .tab-bar-item image {
width: 24px;
height: 24px;
} .tab-bar-item view {
font-size: 10px;
}
</style>

  

默认配置:

export function tabbarList() {
return [{
iconPath: "/static/biaoqian.png",
selectedIconPath: "/static/biaoqian_active.png",
text: '项目概况',
pagePath: "/pages/overview/overview",
name: "overview",
index: 0,
permission: "JzDataSummaryGame"
},
{
iconPath: "/static/shezhi.png",
selectedIconPath: "/static/shezhi_active.png",
text: '买量概况',
pagePath: "/pages/buyVolume/buyVolume",
name: "buyVolume",
index: 1,
permission: "JzDataSummaryAdvertise"
}, {
iconPath: "/static/wenjian.png",
selectedIconPath: "/static/wenjian_active.png",
text: '媒体概况',
pagePath: "/pages/media/media",
name: "media",
index: 2,
permission: "JzDataSummaryChannel"
}, {
iconPath: "/static/bianxie.png",
selectedIconPath: "/static/bianxie_active.png",
text: '人员概况',
pagePath: "/pages/person/person",
name: "person",
index: 3,
permission: "JzDataSummaryUser"
},
]
}

当接口返回权限时,就可以直接进行处理,存储起来使用

页面中引用:

点击的时候就可以切换到对应页面了。

关于下拉刷新,文档中有示例,使用也很简单:

需要注意的是最后要关闭。

其次是关于登陆与否的监听,当没有登录/登陆失败时,进入首页时应当要进行页面跳转。前面登录相关的代码中,已经用了uni提供的监听方法进行登录状态的监听,接下来就是在首页中进行监听:

需要注意的是,页面卸载时需要关闭监听,否则会出问题:

关于字体图标,因为我引入后发现uni-icon提供的还蛮好看的,所以配置了也暂时没用,如需使用的话参考文档就好,阿里图标库也可以直接进行下载,很方便(但某种意义上还挺麻烦),使用的话也是按文档写法即可:

关于颜色,uniapp内置了一个uni.scss的文件,其中配置了许多常用样式变量,可以直接在代码中使用:

还有一个是获取跳转时携带的参数,这里贴一下403页面的代码:
<template>
<view>
<default-page :imgUrl="imgUrl" :text="text" />
</view>
</template> <script>
import defaultPage from '../../components/default-page.vue'; export default {
data() {
return {
imgUrl: '/static/403.png',
text: '暂无极致Data账号,请前往飞书审批提交账号权限申请',
}
},
onShow() {
// 展示后端返回的信息
const pages = getCurrentPages();
const curPage = pages[pages.length - 1].options;
if (curPage.msg) {
this.text = curPage.msg
}
},
methods: { },
components: {
defaultPage
}
}
</script> <style> </style>

  

其中基础组件会进行展示:

<template>
<view class="default-page">
<view class="default-page-icon">
<image class="default-page-icon-img" :src="imgUrl"></image>
</view>
<view class="default-page-text">
<view>{{text}}</view>
</view>
<view>
<slot></slot>
</view>
</view>
</template> <script>
export default {
name: "default-page",
props: {
imgUrl: String,
text: String,
},
data() {
return {};
}, }
</script> <style lang="scss">
.default-page {
text-align: center; &-icon {
&-img {
display: inline-block;
width: 340rpx;
height: 340rpx;
margin: 180rpx auto 32rpx;
}
} &-text {
text-align: center;
font-size: 30rpx;
padding: 0 120rpx;
line-height: 48rpx;
} &-button {
width: 320rpx;
}
}
</style>

  

项目打包

开发完后,会需要进行发布,只要在hbuilder中选择发布对应的小程序就好,跟运行差不多的步骤,但是打包好的代码是在build下面,从飞书开发者工具导入时需要注意,然后改好应用id,就可以上传代码啦~上传好后会给一个弹窗询问是否去设置,点击去设置的话就会自动打开到开发者后台,就可以更新最新版本咯。

好像也没什么特殊的了~暂时就记到这里~

 

基于uniapp框架开发飞书小程序总结的更多相关文章

  1. 基于NopCommerce框架开发的微信小程序UrShop

    Urshop小程序商城 介绍 UrShop小程序商城 2.0发布啦,发布地址https://gitee.com/urselect/urshop UrShop 根据NopCommerce框架开发的,基于 ...

  2. 基于vs2015 SignalR开发的微信小程序使用websocket实现聊天功能

    一)前言 在微信小程上实现聊天功能,大致有三种方式:1)小程序云开发 2)购买第三方IM服务 3)使用自己的服务器自己开发. 这里重要讲使用自己的服务器自己开发,并且是基于vs的开发. 网上提供的解决 ...

  3. TinkPHP框架开发的CRMEB小程序商城v4.0二次开发集成支付宝支付

    前言 大家都知道支付宝支付和微信支付宝都只能局限在自己的平台,微信内支付宝支付是根本就不能使用,即使是公众号支付也需要跳转到外部浏览器才可以唤起支付宝支付,并且QQ浏览器唤起支付宝支付还是问题很多,所 ...

  4. rtvue-lowcode:一款基于uniapp框架和uview组件库的开源低代码开发平台

    rtvue-lowcode低代码开发平台 rtvue-lowcode一款基于uniapp框架和uview组件库的低代码开发平台,项目提供可视化拖拽编辑器,采用MIT开源协议,适用于app.小程序等项目 ...

  5. 基于spring-boot的社区社交微信小程序,适合做脚手架、二次开发

    基于spring-boot的社区社交微信小程序,适合做脚手架.二次开发 代码地址如下:http://www.demodashi.com/demo/13867.html 1 概述 笔者做的一个后端基于s ...

  6. 开发一个微信小程序项目教程

    一.注册小程序账号 1.进入微信公众平台(https://mp.weixin.qq.com/),注册小程序账号,根据提示填写对应的信息即可.2.注册成功后进入首页,在 小程序发布流程->小程序开 ...

  7. 全栈开发工程师微信小程序-中(中)

    全栈开发工程师微信小程序-中(中) 开放能力 open-data 用于展示微信开放的数据 type 开放数据类型 open-gid 当 type="groupName" 时生效, ...

  8. 全栈开发工程师微信小程序-上(下)

    全栈开发工程师微信小程序-上(下) icon 图标 success, success_no_circle, info, warn, waiting, cancel, download, search, ...

  9. 开发一个微信小程序实例教程

    一.注册小程序账号 1.进入微信公众平台(https://mp.weixin.qq.com/),注册小程序账号,根据提示填写对应的信息即可.2.注册成功后进入首页,在 小程序发布流程->小程序开 ...

  10. 开发一个微信小程序教程

    一.注册小程序账号 1.进入微信公众平台(https://mp.weixin.qq.com/),注册小程序账号,根据提示填写对应的信息即可. 2.注册成功后进入首页,在 小程序发布流程->小程序 ...

随机推荐

  1. 自我介绍&学习心得

    这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzzcxy/2023learning/join?id=CfDJ8GXQNXLgcs5PrnWvMs4xAGN4cH ...

  2. ES 基础操作

    集群 健康值的三种状态 Green:所有索引的所有分片均可用 primary 和 replice 均可用. Yellow 至少有一个 replice不可以用, 但是所有的 primary 正常. Re ...

  3. 在输入shell命令的list_namespace时,报 :org.apache.hadoop.hbase.PleaseHoldException: Master is initializing。

    今天弄了一下午这个问题,弄到了将近十点,终于解决了,终于解决这个问题了,感谢旭旭大佬相助,不再报错了. 本来今天中午,我已经弄好了,结果我午睡了一下再看就报错了,哎.今天本来已经绝望了,后来问了一下大 ...

  4. oralce sql 缓存查询及删除

    --缓存查询语句 V$SQLAREA 视图记录sql 执行情况(加载次数/用时/Id....) 常用字段 ADDRESS:SQL语句在SGA中的地址. 这两列被用于鉴别SQL语句,有时,两条不同的语句 ...

  5. skype网络异常无法登录

    在有些win7电脑上安装最新版skype软件后,打开skype软件后显示无法访问网络 检查网络及防火墙,确定无异常 最后排查原因定位到操作系统的根证书 发现系统缺少部分DigiCert的根证书 从其他 ...

  6. 你的ASP.NET Pages项目编译时为何总是很慢慢慢~?

    摘要 很多同学在运行同一个Asp.net Pages项目解决方案时会发现,有时候很快,有时候超级慢,甚至时间超过10几分钟才可以完全编译完,随后才能调试! 其实这都是跟配置有关 有句话说的话,约定  ...

  7. yolov5s yolov8n 在自己数据集上测试比较(640*640)

    yolov5s -> 0.5map:  96.5 -> ncnn:75ms yolov8n -> 0.5map:  94.1 -> ncnn:52ms

  8. 官网jdk8,jdk11下载时需要登录Oracle账号的问题解决

    当到这一步骤时先勾选同意,在这个下载按钮上点鼠标右键复制链接地址 文件的下载地址 我们需要把地址做些修改.把等号前面的地址删掉,然后找到等号后面地址中的otn后面加上-pub 然后把这个地址直接复制到 ...

  9. OpenLayer——模拟运动轨迹

    模拟在人地图上移动,动态绘制行动轨迹的功能,附带一个跟随的气泡弹窗. <!DOCTYPE html> <html lang="en"> <head&g ...

  10. MYSQL-数据操作DDL,DML,DCL,DQL

    前言:MYSQL数据操作语言分为四种 1.DDL(数据定义语言):用来创建数据库中的表.索引.视图.存储过程.触发器等. 2.DML(数据操作语言):用来对表内数据的添加.更新.删除等. 3.DCL( ...