基于uniapp框架开发飞书小程序总结

前期准备

飞书官方客户端文档: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的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// // 组装模块并导出 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的代码:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<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中(爷直接复制官方文档),就可以生效了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
{
    "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文件夹下新建组件:

 

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<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>

  

默认配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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页面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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>

  

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<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,就可以上传代码啦~上传好后会给一个弹窗询问是否去设置,点击去设置的话就会自动打开到开发者后台,就可以更新最新版本咯。

 

 

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

 

 

posted @   芝麻小仙女  阅读(2292)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示