uniapp高仿小米商城Vue3+uniCloud+后台管理支持小程序+Android+iOS+H5

1,165 阅读4分钟

效果

bbb.png

项目简介

这是一门使用uniapp,vk uniCloud框架,从0开发一个接近企业级商业级项目之云商城(仿小米商城),课程包含了基础内容,高级内容,项目封装,项目重构等知识;主要是讲解如何使用系统功能,流行的第三方框架,第三方服务完成一个接近企业级商业级项目,目的是通过对课程质量的苛刻要求,以达到学习完我们课程的同学能真真正正的学习到知识和经验,能让他们成为行业的高端人才,同时拥有更好的人生规划和职业发展前景。

功能点

预备知识:Git,Postman,HTML,CSS,Flex布局,JavaScript等

搭建环境:搭建HBuilderX,Android,iOS,微信和支付小程序环境等

基础知识:uniapp,uniCloud项目创建,开发和网络请求封装等

项目实战:实现轮播图,商品列表,商品分类,商品详情,订单等

登录注册:用户注册和登录,手机号登录,微信App登录,微信小程序登录等

支付功能:微信App支付,微信小程序支付,支付宝App支付等

应用打包:自定义基座,调试前面和云函数,应用升级,版本检测等

苹果开发:加入开发者计划,签名和设备,自定义基座打包,上架等

后台管理:轮播图管理,商品分类管理,商品管理,首页统计,权限和菜单

...

部分项目视频

b站搜索爱学啊,然后在专辑里面找仿小米商城。

发环境概述

2022年11月开发完成的,所以全部都是最新的,平均每3年会重新制作,云商城还是第一版,其他云音乐项目现在已经是第三版了。

HBuilder X 3.6

编译和运行

用最新HBuilder X打开mall,然后可运行到h5,微信小程序,Android,iOS等平台;后台管理系统运行到PC端,部分功能不兼容手机网页,不过管理后台兼容手机网页屏幕也太窄了,体验也不好。

首页

是一个多类型列表,最底部是标题栏,微信小程序自定义了,兼容小程序胶囊按钮,其他平台使用框架自带的;后面就是轮播图,使用官方的swiper组件,然后快捷按钮,使用flex布局实现九宫格效果;然后是左右大封面图,也是使用flex布局实现;接下来就是热门商品,最新商品,最后是推荐商品;并实现了下拉刷新,上拉加载更多。

1.png
<template>
    <!-- #ifdef MP -->
    <page-meta :page-style="'overflow:'+(isShowNewUserCoupon?'hidden':'visible')"></page-meta>
    <!-- #endif -->
    <view>
        <!-- #ifdef MP -->
        <view class="bg-surface fixed left-0 right-0 top-0" style="z-index: 999;">
            <!-- 状态栏 -->
            <StatusBar />

            <!-- 导航栏 -->
            <view :style="{height:navibarHeight+'px'}" class="flex items-center">
                <image @click="scanClick" src="/static/images/navigation-logo.png" class="image-navigation ml-10r"
                    mode="heightFix"></image>
                <view @click="searchClick" class="flex flex-1 bg rounded-50r items-center justify-center"
                    style="height: 32px;">
                    <text class="text-28r text-on-secondary">搜索商品</text>
                </view>

                <view :style="'width: '+menuButtonWidth+'px'">

                </view>
            </view>

        </view>

        <!-- 导航栏占位控件 -->
        <view>
            <StatusBar />
            <view :style="{height:navibarHeight+'px'}">

            </view>
        </view>
        <!-- #endif -->

        <view v-if="data">
            <!-- banner -->
            <swiper class="banner" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000">
                <block v-for="(item,index) in data.banners" :key="index">
                    <swiper-item class="banner">
                        <view @click="bannerClick(item)" class="swiper-item banner">
                            <image :src="item.bannerfile" lazy-load="true" mode="aspectFill" class="banner"></image>
                        </view>
                    </swiper-item>
                </block>
            </swiper><!-- end banner -->

            <!-- 按钮 -->
            <view class="flex flex-wrap bg-surface">
                <image @click="buttonClick(item)" v-for="(item,index) in data.buttons" :src="item.icon"
                    class="image-button">
                </image>
            </view>
            <!-- end 按钮 -->

            ...

            <!-- 最新商品 -->
            <view class="flex items-center pl-23r pr-23r pt-23r mt-16r bg-surface">
                <text class="text-30r font-bold flex-1">新上好货</text>
                <text class="text-26r text-on-secondary">查看更多</text>
                <fui-icon name="arrowright" :size="40" :color="theme.colorOnSecondary"></fui-icon>
            </view>

            <view class="bg-surface">
                <sui-goods-list :datum="data.news" />
            </view>

            <image class="mt-16r mb-16r" :src="data.banner1" style="width: 750rpx;height: 291.6666666667rpx;"></image>

            <sui-goods-list :datum="datum" :column="2" />
        </view>

        <fui-loadmore v-if="isShowLoadingStatus" :state="loadMoreState" :activeColor="theme.colorPrimary">
        </fui-loadmore>

        <!-- 添加到小程序提示 -->
        <xzj-firsthint :isCustom="true" />
        <!-- <fui-loading v-else type="row"></fui-loading> -->

        <sui-new-user-coupon time="122800" @closeClick="isShowNewUserCoupon = false" v-if="isShowNewUserCoupon">
        </sui-new-user-coupon>
    </view>
</template>

<script>
    // import GoodsList from '@/components/superui/sui-goods-list/sui-goods-list.vue'
    import StatusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue";

    var vk = uni.vk;
    export default {
        components: {
            StatusBar
        },
        data() {
            // 页面数据变量
            return {
                isShowNewUserCoupon: false,

                navibarHeight: 0,

                /**
                 * 胶囊按钮外部容器宽度
                 */
                menuButtonWidth: 0,

                data: null,
                datum: [],

                isShowLoadingStatus: false,

                //加载更多状态
                loadMoreState: 1,

                //分页
                page: 1,

                // 表单请求数据
                form1: {

                },
                scrollTop: 0,
            }
        },
        onPageScroll(e) {
            this.scrollTop = e.scrollTop;
        },
        // 监听 - 页面每次【加载时】执行(如:前进)
        onLoad(options = {}) {
            vk = uni.vk;
            this.options = options;
            this.init(options);
        },
        // 监听 - 页面【首次渲染完成时】执行。注意如果渲染速度快,会在页面进入动画完成前触发
        onReady() {
            if (!this.config.DEBUG) {
                setTimeout(() => {
                    // #ifdef APP
                    this.start('/components/superui/sui-new-user-coupon/sui-new-user-coupon-page');
                    // #endif

                    // #ifndef APP
                    this.isShowNewUserCoupon = true;
                    // #endif

                }, 3000);
            }
        },
        // 监听 - 页面每次【显示时】执行(如:前进和返回) (页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面)
        onShow() {

        },
        // 监听 - 页面每次【隐藏时】执行(如:返回)
        onHide() {

        },
        // 监听 - 点击右上角转发时
        onShareAppMessage(options) {

        },
        // 函数
        methods: {
            // 页面数据初始化函数
            init(options) {
                // #ifdef MP
                uni.getSystemInfo({
                    success: (r) => {
                        console.log(r);
                        //获取小程序右上角胶囊按钮位置信息,自定义的时候避开他
                        //https://uniapp.dcloud.net.cn/api/ui/menuButton.html#getmenubuttonboundingclientrect
                        let menuButtonInfo = uni.getMenuButtonBoundingClientRect();
                        console.log(menuButtonInfo);

                        //状态栏高度
                        let statusBarHeight = r.statusBarHeight;
                        this.navibarHeight = menuButtonInfo.height + (menuButtonInfo.top -
                            statusBarHeight) * 2;
                        console.log(this.navibarHeight);

                        this.menuButtonWidth = menuButtonInfo.width + (r.windowWidth - menuButtonInfo.right) *
                            2;
                    }
                })
                // #endif

                // vk.reLaunch("/pages_template/uni-id/index/index");

                uni.startPullDownRefresh();

                // vk.myfn.test2();
            },
            pageTo(path) {
                vk.navigateTo(path);
            },
            /**
             * 轮播图点击
             * @param {Object} data
             */
            bannerClick(data) {
                if (data.open_url.startsWith('http')) {
                    uni.navigateTo({
                        url: '/components/superui/webview/webview?url=' + data.open_url + "&title=" + (data
                            .title || '')
                    })
                } else {
                    uni.navigateTo({
                        url: data.open_url
                    })
                }
            },
            ...
            loadMore() {
                vk.callFunction({
                    url: 'client/goods/pub/getList',
                    data: {
                        page: this.page
                    }
                }).then((r) => {
                    //把两个数组加一起
                    this.datum = [...this.datum, ...r.rows];

                    //计算分页相关信息
                    let totalPage = this.util.page(r.total, r.pagination.pageSize);

                    if (this.page == totalPage) {
                        //没有数据了
                        this.loadMoreState = 3;
                    } else {
                        this.loadMoreState = 1;
                        this.page += 1;
                    }
                }).catch(r => {
                    this.loadMoreState = 1;
                });
            },
            scanClick() {
                uni.scanCode({
                    success: function(res) {
                        console.log('条码类型:' + res.scanType);
                        console.log('条码内容:' + res.result);
                    }
                });
            },
            searchClick() {
                vk.navigateTo('/pages/goods/goods?style=10');
            },
            // #ifndef MP
            openOverlay() {
                let uniPlatform = uni.getSystemInfoSync().uniPlatform;
                // H5禁止滚动
                if (uniPlatform == 'web') {
                    var mo = function(e) {
                        e.preventDefault();
                    };

                    document.body.style.overflow = 'hidden';
                    document.addEventListener("touchmove", mo, false); //禁止页面滑动
                }

            },
            //关闭遮罩
            closeOverlay: function() {
                let uniPlatform = uni.getSystemInfoSync().uniPlatform;
                if (uniPlatform == 'web') {
                    var mo = function(e) {
                        e.preventDefault();
                    };
                    document.body.style.overflow = ''; //出现滚动条
                    document.removeEventListener("touchmove", mo, false);
                }
            },
            // #endif
        },
        // 监听器
        watch: {
            isShowNewUserCoupon(newValue, oldValue) {
                // #ifndef MP
                if (newValue) {
                    this.openOverlay();
                } else {
                    this.closeOverlay();
                }
                // #endif
            }
        },
    }
</script>
<style lang="scss">
    page {
        background-color: #EDEDED !important;
    }

    .banner {
        width: 750rpx;
        height: 375rpx;
    }

    // 图片按钮
    .image-button {
        width: 150rpx;
        height: 158rpx;
    }

    // 左侧大轮播图
    .banner-left {
        width: 351rpx;
        height: 498.0670391109rpx;
        border-radius: 10rpx;
    }

    .banner-right {
        width: 351rpx;
        height: 241.0335195555rpx;
        border-radius: 10rpx;
    }

    .image-navigation {
        height: 28px;
    }
</style>

商品详情

顶部在所有平台自定义了标题栏,主要实现图片能显示到状态栏下,滚动列表,标题栏会有渐变效果,这种效果在市面上商城商品详情用的比较的;接下来就是轮播图,和首页实现方法差不多;然后是商品信息,优惠券;商品规格;卖家,以及推荐商品;然后是商品详情富文本;最后底部是快捷按钮。

2.png
<template>
    <view class="" v-if="data">
        <view class="fixed left-0 right-0 top-0" style="z-index: 999;"
            :style="'background-color:rgba(255,255,255,'+naviBarAlpha+')'">
            <!-- #ifndef MP -->
            <StatusBar />
            <!-- #endif -->
            <!-- 状态栏 -->

            <!-- 导航栏 -->
            <view class="flex items-center navibar">
                <!-- #ifndef MP -->
                <image @click="backClick" :src="'/static/images/back'+naviBarIconFlag+'.png'" class="ml-10r"
                    mode="heightFix"></image>
                <!-- #endif -->
                <fui-tabs background="rgba(255,255,255,0)" :short="false" scale="1" :selectedColor="theme.colorPrimary"
                    :sliderBackground="theme.colorPrimary" class="flex flex-1" :tabs="tabs" @change="change"></fui-tabs>
                <!-- #ifndef MP -->
                <image @click="shareClick" :src="'/static/images/share'+naviBarIconFlag+'.png'" class="ml-10r"
                    mode="heightFix"></image>
                <!-- #endif -->
            </view>

        </view>

        <!-- banner -->
        <swiper class="banner" :indicator-dots="true">
            <block v-for="(item,index) in data.goods_banner_imgs" :key="index">
                <swiper-item class="banner">
                    <view @click="bannerClick(item)" class="swiper-item banner">
                        <image :src="item" lazy-load="true" mode="aspectFill" class="banner"></image>
                    </view>
                </swiper-item>
            </block>
        </swiper><!-- end banner -->

        <!-- 商品信息 -->
        <view class="p-25r bg-surface">
            <text class="text-primary text-44r font-bold">¥{{this.util.price(price)}}</text>

            <view class="flex mt-25r">
                <view class="flex flex-wrap flex-1">
                    <fui-tag v-for="(item,index) in activitys" :background="theme.colorActivity"
                        :color="theme.colorPrimary" :text="item.title" margin-bottom="24" theme="light"
                        margin-right="24"></fui-tag>
                </view>
                <fui-button @click="showCouponPopup" type="primary" width="100rpx" height="55rpx" :size="27">领券
                </fui-button>
            </view>

            <view class="mt-25r">
                <text class="text-35r font-bold text-on-surface">{{data.name}}</text>
            </view>

            <view class="mt-25r">
                <text class="text-27r text-on-surface">{{data.highlight}}</text>
            </view>
        </view>
        <!--end 商品信息 -->

        <!-- 规格信息 -->
        <view class="mt-16r">
            <sui-setting @click="skuClick" title="已选" :subTitle="skuText" :isShowMoreIcon="true" />
            <sui-setting title="送至" subTitle="四川省成都市高新区" :isShowMoreIcon="true" />
            <sui-setting title="服务" subTitle="假一赔十 · 退货运费险 · 急速退款" :isShowMoreIcon="true" />
        </view>

        <!-- 评论 -->
        <view id="comment-container" class="mt-16r">
            <sui-setting :titleSize="32" titleFontWeight="700" @click="commentMoreClick" title="用户评价(100)"
                moreTitle="查看更多" :isShowMoreIcon="true" />
            <!-- 标签 -->
            <view class="pl-25r pr-25r bg-surface">
                <fui-tag size="25" :text="item.content+'('+item.count+')'" margin-bottom="15" theme="plain"
                    margin-right="25" background="#FFEEEC" radius="40" v-for="(item,index) in tags" :key="index"
                    @click="commentTagClick(item)">
                </fui-tag>
            </view>

            <!-- 一个评论 -->
            <view class="pt-25r pb-25r bg-surface mb-2r" v-for="(item,index) in data.comments" :key="index"
                v-if="data.comments">
                <!-- 内容 -->
                <view class="pl-25r pr-25r flex justify-between">
                    <view class="flex items-center">
                        <image :src="item.user.avatar || this.config.r(
                        '0024e600-e6eb-42af-8673-2ad66d75c0bb.jpg')" class="icon-comment rounded-10r" mode=""></image>
                        <view class="flex flex-col ml-16r">
                            <fui-text :text="item.user.nickname" text-type="name" format size="29">
                            </fui-text>
                            <fui-text color="#aaa" text="6天前" format size="26">
                            </fui-text>
                        </view>
                    </view>
                    <uni-rate :readonly="true" :value="item.score" :size="18" />
                </view>

                <view class="pl-25r pr-25r flex justify-between items-center pt-16r">
                    <fui-text :text="item.content" format size="28"></fui-text>
                </view>

                <!-- 图片 -->
                <view v-if="item.medias" class="pl-25r bg-surface pt-16r flex flex-wrap">
                    <image :src="it.url" class="rounded-10r mr-16r mb-16r"
                        :style="'width:'+imageWidth+'rpx;height:'+imageWidth+'rpx;'" mode="aspectFill"
                        v-for="(it,index) in item.medias" :key="index" @click="commentImageClick(item.medias,index)">
                    </image>
                </view>

                <view class="pl-25r pr-25r flex justify-between items-center pt-16r" v-if="item.spec_value">
                    <text class="text-on-secondary text-27r">已买: {{item.spec_value}}</text>
                </view>
            </view>
            <!--/ 一个评论 -->
        </view>

       ...

        <!-- 底部按钮 -->
        <view class="fixed bottom-0 left-0 right-0 bg-surface">
            <DividerSmall />
            <view class="flex pr-35r bottombar items-center ">
                <view class="flex flex-col ml-35r">
                    <fui-icon name="kefu" :size="40"></fui-icon>
                    <text class="text-on-surface text-26r">客服</text>
                </view>
                <view class="flex flex-col ml-35r mr-35r">
                    <fui-icon name="cart" :size="40"></fui-icon>
                    <text class="text-on-surface text-26r">购物车</text>
                </view>

                <fui-button @click="addCartClick" class="flex-1" text="加入购物车" radius="96rpx 0rpx 0rpx 96rpx"
                    height="80rpx" :size="28" type="warning"></fui-button>
                <fui-button @click="primaryClick" class="flex-1" text="立即购买" radius="0rpx 96rpx 96rpx 0rpx"
                    height="80rpx" :size="28" type="primary"></fui-button>
            </view>
        </view>

        <view style="height: 102rpx;">

        </view>
        <!-- end 底部按钮 -->

        <sui-coupon-popup @primaryClick="couponPrimaryClick" :datum="activitys" ref="couponPopup" />

        <!-- sku选择组件 -->
        <vk-data-goods-sku-popup ref="skuPopup" v-model="skuKey" border-radius="20" :localdata="data" :mode="skuMode"
            @open="onOpenSkuPopup" @close="onCloseSkuPopup" @add-cart="addCart" @buy-now="buyNow">
        </vk-data-goods-sku-popup>
    </view>
</template>

<script>
    import StatusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue";
    import mpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue'

    import UniShare from '@/uni_modules/uni-share/js_sdk/uni-share.js';
    const uniShare = new UniShare();

    export default {
        components: {
            StatusBar,
            mpHtml
        },
        data() {
            return {
                // (750-(25*2)-(18*2))/3
                imageWidth: 222.6666666667,
                tabs: ['商品', '评价', '推荐', '详情'],
                naviBarIconFlag: 1,
                naviBarAlpha: 0,
                data: null,
                activitys: [],
                coupon_id: null,
                price: 0,
                skuText:'',
                tags: [{
                    "id": "12312",
                    "content": "质量好",
                    "count": 175
                }, {
                    "id": "12312",
                    "content": "描述真实",
                    "count": 50
                }, {
                    "id": "12312",
                    "content": "客服好",
                    "count": 25
                }, {
                    "id": "12312",
                    "content": "老板人好",
                    "count": 38
                }, {
                    "id": "12312",
                    "content": "课程很好",
                    "count": 150
                }],
                icon: "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dce8d661-99e9-4c7f-a4d9-df08f9c8f4e4/8ed9516f-1199-44fb-bbaa-5c4a11c3a637.jpg",
                id: null,
                // 是否打开SKU弹窗
                skuKey: false,
                // SKU弹窗模式
                skuMode: 1,
            }
        },
        onLoad({
            id
        }) {
            this.id = id;
            vk.callFunction({
                url: 'client/goods/pub/detail',
                data: {
                    id: id
                },
                success: (r) => {
                    this.data = r.data;

                    this.data.goods_thumb = this.data.goods_banner_imgs[0];
                    
                    this.defaultSKU();

                    this.loadActivity();
                }
            });

            // #ifdef MP
            //设置小程序内分享菜单可点击
            //https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html
            wx.showShareMenu({
                withShareTicket: true,
                menus: ["shareAppMessage", "shareTimeline"]
            });
            // #endif
        },
        /**
         * 小程序分享到消息点击
         * @param {Object} res
         */
        onShareAppMessage(res) {
            if (res.from === 'button') { // 来自页面内分享按钮
                console.log(res.target)
            }
            return {
                title: this.shareTitle, //分享的名称
                path: '/pages/goods/goods-detail?id=' + this.id,
                imageUrl: this.icon
            }
        },
        /**
         * 小程序分享到朋友圈点击
         * @param {Object} res
         */
        onShareTimeline(res) {
            return {
                title: this.shareTitle,
                imageUrl: this.icon
            }
        },
        computed: {
            shareTitle() {
                return '这个商品不错,' + this.data.name;
            },
            shareSummary() {
                return '我们是一家专注于IT职业教育的在线教育企业。目的是通过对课程质量的苛刻要求,以达到学习完我们课程的同学能真真正正的学习到知识和经验,能让他们成为行业的高端人才,同时拥有更好的人生规划和职业发展前景。';
            },
        },
        methods: {
            defaultSKU(){
                this.price = this.data.price;
                this.skuText = "请选择商品规格";
            },
            backClick() {
                uni.navigateBack();
            },

            shareClick() {
                uniShare.show({
                    content: { //公共的分享参数配置  类型(type)、链接(herf)、标题(title)、summary(描述)、imageUrl(缩略图)
                        type: 0,
                        href: this.config.H5_ENDPOINT + '/#/pages/goods/goods-detail?id=' + this.data._id,
                        title: this.shareTitle,
                        summary: this.shareSummary,
                        imageUrl: this.icon
                    },
                    menus: [{
                            "img": this.config.r('23913917-f867-4f4e-a376-198ffb4c7b19.png'),
                            "text": "微信好友",
                            "share": { //当前项的分享参数配置。可覆盖公共的配置如下:分享到微信小程序,配置了type=5
                                "provider": "weixin",
                                "scene": "WXSceneSession"
                            }
                        },
                        {
                            "img": this.config.r('64f1ca72-9e10-4b93-b982-6e9445dcb4b9.png'),
                            "text": "微信朋友圈",
                            "share": {
                                "provider": "weixin",
                                "scene": "WXSceneTimeline"
                            }
                        },
                        {
                            "img": this.config.r('1a9e835d-1547-444a-8342-353ad0c4a5d2.png'),
                            "text": "微信小程序",
                            "share": {
                                provider: "weixin",
                                scene: "WXSceneSession",
                                type: 5,
                                miniProgram: {
                                    id: '123',
                                    path: '/pages/list/detail',
                                    webUrl: '/#/pages/list/detail',
                                    type: 0
                                },
                            }
                        },
                        {
                            "img": this.config.r('76c1d6f4-62cd-4538-8f7b-45baaa4ad4ad.png'),
                            "text": "微博",
                            "share": {
                                "provider": "sinaweibo"
                            }
                        },
                        {
                            "img": this.config.r('a3d1536a-9c65-4c54-906a-1e3d82c45ce8.png'),
                            "text": "QQ",
                            "share": {
                                "provider": "qq"
                            }
                        },
                        {
                            "img": this.config.r('c3793a70-61c0-4dec-816b-adec2652f7a1.png'),
                            "text": "复制",
                            "share": "copyurl"
                        },
                        {
                            "img": this.config.r('af7fe0c0-42d9-4d4d-b260-513b90e97bf2.png'),
                            "text": "更多",
                            "share": "shareSystem"
                        }
                    ],
                    cancelText: "取消分享",
                }, e => { //callback
                    console.log(uniShare.isShow);
                    console.log(e);
                })
            },
        }
        ...
        /**
         * 页面滚动
         * @param {Object} data
         */
        onPageScroll(data) {
            this.naviBarAlpha = data.scrollTop / 255;
            if (this.naviBarAlpha > 1) {
                this.naviBarAlpha = 1;
            }

            if (this.naviBarAlpha > 0.5) {
                this.naviBarIconFlag = 2;
            } else {
                this.naviBarIconFlag = 1;
            }
        }
    }
</script>

<style lang="scss">
    page {
        background-color: #EDEDED !important;
    }

    .icon-comment {
        width: 80rpx;
        height: 80rpx;
    }

    .banner {
        width: 750rpx;
        height: 750rpx;
    }

    .navibar {
        height: 44px;

        image {
            height: 81rpx;
        }
    }
</style>

规格选择

规格选择简单的功能不算复杂,但细节还是很多的,这里使用第三方sku选择组件。

3.png

确认订单

大体效果仿照小米商品确认订单。

4.png
<template>
	<view v-if="data">
		<!-- 收货地址 -->
		<sui-address :data="vk.getVuex('$order.address')" class="mt-16r" @click="selectAddressClick"></sui-address>

		<!-- 商品 -->
		<view class="bg-surface mt-16r">
			<sui-goods-list-small :datum="data.carts" />
		</view>

		<sui-setting title="商品总价" :moreTitle="'¥'+ this.util.price(data.total_price)" :marginTop="16" />
		<sui-setting title="运费" moreTitle="包邮" />
		<sui-setting @click="couponClick" title="优惠券" :moreTitle="couponText" :moreTitleColor="couponColor"
			:isShowMoreIcon="true" />

		<!-- 底部按钮 -->
		<view class="fixed bottom-0 left-0 right-0 bg-surface">
			<DividerSmall />
			<view class="flex pr-35r pl-35r bottombar items-center">
				<text class="text-28r">共{{data.carts.length}}件 合计:</text>
				<text class="text-28r text-primary mr-35r ml-10r">{{this.util.price(data.price)}}</text>
				<fui-button @click="primaryClick" class="flex-1" text="去支付" height="80rpx" :size="28" type="primary"
					radius="96rpx"></fui-button>
			</view>
		</view>

		<view style="height: 102rpx;">

		</view>
		<!-- end 底部按钮 -->

		<sui-coupon-popup @itemClick="couponItemClick" :selectId="couponId" :selectModel="true" :datum="coupons"
			ref="couponPopup" />
	</view>
</template>

<script>
	export default {
		data() {
			return {
				goodsId: null,
				couponId: null,
				data: null,
				coupons: null,
				couponText: '',
				couponColor: ''
			}
		},
		onLoad({
			goods_id,
			coupon_id
		}) {
			this.goodsId = goods_id;
			this.couponId = coupon_id;
			this.loadData();
		},
		methods: {
			loadData() {
				vk.callFunction({
					url: 'client/order/kh/confirm-order',
					data: {
						goods_id: this.goodsId,
						coupon_id: this.couponId,
						address_id: vk.getVuex('$order.address._id'),
						source: vk.myfn.source(),
						sku:vk.getVuex('$order.sku'),
						carts:vk.getVuex('$order.carts')
					},
					success: (r) => {
						this.data = r.data;

						//收货地址
						if (r.data.address) {
							vk.setVuex('$order.address', r.data.address);
						} else {
							vk.setVuex('$order.address', null);
						}

						if (this.coupons) {
							this.showCoupon();
						} else {
							this.loadActivity();
						}
					}
				});
			},
			loadActivity() {
				vk.callFunction({
					url: 'client/activity/pub/getList',
					data: {
						need_user_info: true, // 如果pub下的云函数需要用到 `userInfo`,则传true
						goods_id: this.goodsId,
						style: 10
					},
					success: (r) => {
						this.coupons = r.data;

						this.showCoupon();
					}
				});

			},
			showCoupon() {
				this.couponColor = this.theme.colorPrimary;
				if (this.data.coupon) {
					//使用优惠券
					this.couponText = '-¥' + this.util.price2(this.data.coupon.activity.price);
				} else if (this.coupons) {
					//有优惠券
					this.couponText = this.coupons.filter((it) => {
						return it.coupon != null;
					}).length + '张可用';
				} else {
					//没有优惠券可用
					this.couponText = "没有可用优惠券";
					this.moreTitleColor = this.theme.colorOnSecondary;
				}
			},
			primaryClick() {
				if (!vk.getVuex('$order.address')) {
					vk.toast("请选择收货地址");
					return;
				}

				vk.callFunction({
					url: 'client/order/kh/create',
					data: {
						goods_id: this.goodsId,
						coupon_id: this.couponId,
						address_id: vk.getVuex('$order.address._id'),
						source: vk.myfn.source(),
						sku:vk.getVuex('$order.sku'),
						carts:vk.getVuex('$order.carts')
					},
					success: (r) => {
						vk.setVuex('$order.sku',null);
						vk.setVuex('$order.carts',null);
						vk.redirectTo("/pages/pay/pay?style=10&id=" + r.data._id);
					}
				});
			},
			showCouponPopup() {
				this.$refs.couponPopup.open();
			},
			closeCouponPopup() {
				this.$refs.couponPopup.close();
			},
			/**
			 * 优惠券文本点击
			 */
			couponClick() {
				if (this.coupons) {
					//显示优惠券列表
					this.showCouponPopup();
				}
			},
			couponItemClick({
				data,
				index
			}) {
				this.closeCouponPopup();
				this.couponId = data._id;
				this.loadData();
			},
			selectAddressClick() {
				vk.navigateTo("/pages/address/address?style=10");
			}
		}
	}
</script>

<style>
	page {
		background-color: #EDEDED !important;
	}
</style>

支付界面

参考小米商城,实现为单独的界面,好处是应用中要支付的统一跳转到这个界面支付,更好的复用;这里只实现了微信,支付宝支付,真实大部分项目最常用的也就是这两个了;实现方法用的vk-uni-pay插件在云函数里面生成参数,然后客户端使用uni-pay组件调用支付组件。

5.png
<template>
	<view v-if="data" class="flex flex-col">
		<view class="flex flex-col items-center mt-90r mb-90r">
			<!-- 价格信息 -->
			<text class="text-primary text-50r font-bold">¥{{this.util.price(data.price)}}</text>

			<text style="background-color: #ddd;"
				class="text-on-secondary rounded-60r mt-30r text-24r pt-10r pb-10r pl-16r pr-16r">请在{{timeString}}前完成支付</text>
		</view>

		<!-- 支付渠道 -->
		<fui-radio-group @change="payChannelChange">
			<fui-label v-for="(item,index) in payChannels" :key="index">
				<fui-list-cell>
					<view class="fui-list__cell">
						<image class="h-60r w-60r" :src="item.icon"></image>
						<text class="flex-1 ml-20r">{{item.name}}</text>
						<fui-radio :checked="item.checked" :value="item.value">
						</fui-radio>
					</view>
				</fui-list-cell>
			</fui-label>
		</fui-radio-group>
		<!-- end 支付渠道 -->

		<view class="mt-100r pl-25r pr-25r">
			<fui-button @click="primaryClick" text="立即支付" radius="96rpx"></fui-button>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				timeoutFlag: null,
				time: 0,
				style: 0,
				loading: false,
				timeout: false,
				data: null,
				payChannel: '',
				payChannels: []
			}
		},
		onLoad({
			id,
			style
		}) {
			this.id = id;
			this.style = style;
			
			var temps = [];

			// #ifndef MP-WEIXIN
			temps.push({
				icon: this.config.r('a68f9f24-cfd2-4a99-a3aa-589c79c714fe.png'),
				name: '支付宝',
				value: "alipay",
				checked: true,
			});
			
			this.payChannel="alipay";
			// #endif

			// #ifndef MP-ALIPAY
			temps.push(this.wechatPay);
			// #endif
			
			// #ifdef MP-WEIXIN
			this.payChannel="wxpay";
			this.wechatPay.checked = true;
			// #endif
			
			this.payChannels = temps;

			this.loadData();
		},
		computed: {
			timeString() {
				return vk.pubfn.timeFormat(this.time, "mm分ss秒");
			},
			wechatPay(){
				return {
					icon: this.config.r('d2463164-ec0e-407b-92a2-cd9df58c0259.png'),
					name: '微信支付',
					value: "wxpay",
					checked: false,
				};
			}
		},
		methods: {
			loadData() {
				vk.callFunction({
					url: 'client/order/kh/detail',
					data: {
						id: this.id || "634ad720702e9d000164f8f2"
					},
					success: (r) => {
						this.data = r.data;

						if (this.loading) {
							uni.hideLoading();
						}

						if (this.constant.orderPaid(this.data.status)) {
							this.next();
						}

						if (!this.timeoutFlag) {
							this.startCountDown();
						}
					}
				});
			},
			payChannelChange(data) {
				this.payChannel = data.detail.value;
			},
			primaryClick() {
				vk.callFunction({
					url: 'client/pay/kh/pay',
					title: '',
					data: {
						provider: this.payChannel,
						id: this.data._id
					},
					success: (r) => {
						let orderInfo = r.orderInfo;

						uni.requestPayment({
							// #ifdef APP-PLUS
							provider: this.payChannel, // App端此参数必填,可以通过uni.getProvider获取
							// #endif

							// #ifdef MP-WEIXIN
							...orderInfo,
							// #endif

							// #ifdef APP-PLUS || MP-ALIPAY
							orderInfo: orderInfo,
							// #endif

							...orderInfo,

							success: (data) => {
								this.checkOrderStatus();
							},
							fail: (data) => {
								vk.toast('支付失败,请稍后再试或联系客服');
							}
						})
					}
				});

				// this.checkOrderStatus();
			},
			checkOrderStatus() {
				this.loading = true;
				uni.showLoading({
					title: '支付确认中.',
					mask: true
				});

				setTimeout(() => {
					this.loadData();
				}, 2000);
			},
			/**
			 * 显示返回确认对话框
			 */
			showBackConfirmDialog() {
				uni.showModal({
					title: "确认放弃付款吗?",
					content: "超时后,订单自动关闭",
					cancelText: "继续支付",
					confirmText: "确认离开",
					success: (r) => {
						if (r.confirm) {
							this.next();
						} else {

						}
					}
				})
			},
			next() {
				if (this.style == 10) {
					// 只有从确认订单跳转过来,才需要跳转到订单详情

					// 关闭当前页面,跳转到应用内的某个页面。
					uni.redirectTo({
						url: '/pages/order/order-detail?id=' + this.id
					})
				} else {
					this.finish();
				}
			},
			startCountDown() {
				this.cancelTimer();

				//订单创建时间+15分钟
				var t = vk.pubfn.getOffsetTime(new Date(this.data._add_time), {
					minutes: 15,
					mode: "after", // after 之后 before 之前
				});

				this.time = t - new Date().getTime();
				if (this.time <= 0) {
					this.showTimeoutDialog();
					return;
				}

				this.timeoutFlag = setInterval(() => {
					if (this.time <= 0) {
						this.cancelTimer();
						this.showTimeoutDialog();
						return;
					}

					this.time = this.time - 1000;
				}, 1000);
			},
			cancelTimer() {
				if (this.timeoutFlag) {
					clearInterval(this.timeoutFlag);
				}
			},
			showTimeoutDialog() {
				console.log('showTimeoutDialog');
				this.timeout = true;
				uni.showModal({
					title: "支付超时",
					content: "订单已自动关闭,请重新下单",
					confirmText: "确认",
					showCancel: false,
					success: (r) => {
						if (r.confirm) {
							this.next();
						}
					}
				})
			}
		},
		/**
		 * 返回按钮点击
		 * https://ask.dcloud.net.cn/article/35120
		 * @param {Object} data
		 */
		onBackPress(data) {
			if (this.timeout) {
				return false;
			}

			if (!this.loading) {
				//没有显示loading,才显示对话框
				this.showBackConfirmDialog();
			}

			//取消关闭界面
			return true;
		},
		onUnload() {
			this.cancelTimer();
		}
	}
</script>

<style>
	page {
		background-color: #EDEDED !important;
	}

	.fui-section__title {
		margin-left: 32rpx;
	}

	.fui-list__item {
		width: 100%;
		display: flex;
		align-items: center;
		background-color: #FFFFFF;
		padding: 28rpx 32rpx;
		box-sizing: border-box;
	}

	.fui-text {
		font-size: 30rpx;
	}

	.fui-list__cell {
		width: 100%;
		display: flex;
		align-items: center;
	}
</style>

其他界面效果

6.png 7.png 10.png 12.png 20.png 21.png

后台效果

a1.png

a2.png

a3.png