放心行(约客多 2026/6/22 最新版本改版)
hejianhao
2026-06-24 717daadcdc78906411feb4bb5aa0fbb2cc771c40
feat(miniprogram): 新增出行卡兑换全流程功能,更新后端接口地址

新增我的余额页面,支持查看当前余额、交易记录,可按交易类型和时间范围筛选。
新增出行卡兑换页面,支持输入卡号和密码提交兑换申请。
新增兑换成功提示页面,展示到账金额并可返回余额页面。
在首页菜单添加我的余额入口,注册新页面到小程序配置中,新增兑换成功图标资源。
在我的余额页面引入vant-popup组件,实现时间范围筛选的弹窗功能。
13个文件已添加
4个文件已修改
754 ■■■■■ 已修改文件
miniprogram/app.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/app.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/img/conversion-success.png 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/home/home.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/my-balance/my-balance.js 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/my-balance/my-balance.json 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/my-balance/my-balance.wxml 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/my-balance/my-balance.wxss 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-exchange/index.js 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-exchange/index.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-exchange/index.wxml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-exchange/index.wxss 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-success/index.js 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-success/index.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-success/index.wxml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/pages/travel-card-success/index.wxss 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/utils/ajax.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
miniprogram/app.js
@@ -7,8 +7,8 @@
// const apiHost = 'http://123.56.89.63:8080/user-server'
// 上面地址不管用,下面的地址才用
// const apiHost = 'http://221.182.45.100:7894/user'; //本地
const apiHost = 'http://192.168.110.11:5000/user' //线上
const SOCKET_URL = "ws://192.168.110.11:8887/websocket";
const apiHost = 'http://192.168.110.34:5000/user' //线上
const SOCKET_URL = "ws://192.168.110.34:8887/websocket";
const SOCKET_HEARTBEAT_INTERVAL = 10000;
const SOCKET_RECONNECT_DELAY = 3000;
@@ -667,7 +667,7 @@
        // 上面地址不管用,下面的地址才用
        // Url: 'http://221.182.45.100:7894/user', //本地
        Url : 'http://192.168.110.11:5000/user',
        Url : 'http://192.168.110.34:5000/user',
        localSocket: {},
miniprogram/app.json
@@ -94,7 +94,10 @@
        "pages/web-page/wen-page",
        "pages/mallpay/mallpay",
        "pages/orderDetail/orderDetail",
        "pages/scanOrder/scanOrder"
        "pages/scanOrder/scanOrder",
        "pages/my-balance/my-balance",
        "pages/travel-card-exchange/index",
        "pages/travel-card-success/index"
    ],
    "subPackages": [{
            "root": "versionOne",
miniprogram/img/conversion-success.png
miniprogram/pages/home/home.js
@@ -2527,6 +2527,11 @@
                    url: "../../pages/customer/customer",
                });
                break;
            case "我的余额":
                wx.navigateTo({
                    url: "/pages/my-balance/my-balance",
                });
                break;
            case "设置":
                wx.navigateTo({
                    url: "../../pages/setting/setting",
miniprogram/pages/my-balance/my-balance.js
New file
@@ -0,0 +1,137 @@
// pages/my-balance/my-balance.js
const app = getApp()
const unit = require('../../utils/util')
Page({
    data: {
        statusBarHeight: 20,
        balance: '0.00',
        typeOptions: [
            { id: 1, name: '充值' },
            { id: 2, name: '消费' }
        ],
        typeIndex: 0,
        showTimePopup: false,
        startDate: '',
        endDate: '',
        timeText: '时间选择',
        records: []
    },
    onLoad: function (options) {
        const systemInfo = wx.getSystemInfoSync()
        this.setData({
            statusBarHeight: systemInfo.statusBarHeight
        })
        this.fetchRecords();
    },
    onShow: function () {
        this.fetchUserInfo();
    },
    fetchUserInfo: function () {
        unit.post(app.globalData.Url + "/api/user/queryUserInfo").then((res) => {
            if (res.data.code == 200) {
                // 有了token 才能拉取 亲密账号列表
                wx.setStorageSync("profile", res.data.data);
                this.setData({
                    balance: res.data.data.balance || '0.00'
                });
            }
        });
    },
    fetchRecords: function () {
        const { typeIndex, typeOptions, startDate, endDate } = this.data;
        const type = typeOptions[typeIndex].id;
        let params = {};
        if (type !== 0) {
            params.type = type;
        }
        if (startDate) {
            params.startDate = startDate;
        }
        if (endDate) {
            params.endDate = endDate;
        }
        unit.post(app.globalData.Url + '/api/user/balanceRecord', params).then(res => {
            if (res.data.code == 200) {
                this.setData({
                    records: res.data.data || []
                });
            }
        }).catch(err => {
            console.error('balanceRecord error:', err);
        });
    },
    goBack: function () {
        wx.navigateBack({
            delta: 1
        })
    },
    exchangeCard: function () {
        wx.navigateTo({
            url: '/pages/travel-card-exchange/index'
        })
    },
    onTypeChange: function (e) {
        const index = e.detail.value;
        this.setData({
            typeIndex: index
        })
        this.fetchRecords();
    },
    openTimePopup: function () {
        this.setData({
            showTimePopup: true
        })
    },
    closeTimePopup: function () {
        this.setData({
            showTimePopup: false
        })
    },
    onStartDateChange: function (e) {
        this.setData({
            startDate: e.detail.value
        })
    },
    onEndDateChange: function (e) {
        this.setData({
            endDate: e.detail.value
        })
    },
    confirmTime: function () {
        const { startDate, endDate } = this.data
        let timeText = '时间选择'
        if (startDate && endDate) {
            timeText = `${startDate}至${endDate}`
        } else if (startDate) {
            timeText = `${startDate}起`
        } else if (endDate) {
            timeText = `至${endDate}`
        }
        this.setData({
            showTimePopup: false,
            timeText
        })
        this.fetchRecords();
    },
    filterRecords: function () {
        // This function is no longer used for local filtering since we fetch from API.
        // It's kept here just in case, but fetchRecords is called directly instead.
    }
})
miniprogram/pages/my-balance/my-balance.json
New file
@@ -0,0 +1,6 @@
{
  "navigationStyle": "custom",
  "usingComponents": {
    "van-popup": "/ui/vant-weapp/dist/popup/index"
  }
}
miniprogram/pages/my-balance/my-balance.wxml
New file
@@ -0,0 +1,72 @@
<!--pages/my-balance/my-balance.wxml-->
<view class="container-balance">
    <!-- Header Section -->
    <view class="header">
        <view class="navbar-balance" style="margin-top: {{statusBarHeight}}px">
            <view class="back-btn" bindtap="goBack">
                <image class="back-icon" src="/img/yue-back-icon.png"></image>
            </view>
            <view class="nav-title">我的余额</view>
        </view>
        <view class="balance-info">
            <view class="balance-label">当前可用余额</view>
            <view class="balance-amount">
                <text class="currency">¥</text>
                <text class="amount">{{balance}}</text>
            </view>
            <view class="exchange-btn" bindtap="exchangeCard">出行卡兑换</view>
        </view>
    </view>
    <!-- Content Section -->
    <view class="content">
        <view class="section-title">余额记录</view>
        <!-- Filters -->
        <view class="filters">
            <picker bindchange="onTypeChange" value="{{typeIndex}}" range="{{typeOptions}}" range-key="name">
                <view class="filter-item">
                    <text>{{typeOptions[typeIndex].name || '类型选择'}}</text>
                    <image class="dropdown-icon" src="/img/down-select.png"></image>
                </view>
            </picker>
            <view class="filter-item" bindtap="openTimePopup">
                <text class="filter-text">{{timeText}}</text>
                <image class="dropdown-icon" src="/img/down-select.png"></image>
            </view>
        </view>
        <!-- Records -->
        <scroll-view class="record-list" scroll-y="true">
            <view class="record-item" wx:for="{{records}}" wx:key="index">
                <view class="record-left">
                    <view class="record-amount">{{item.type == 1 ? '+' : '-'}} ¥{{item.money}}</view>
                    <view class="record-time">{{item.payTime}}</view>
                </view>
                <view class="record-right">
                    <text class="record-type">{{item.typeDesc}}</text>
                </view>
            </view>
        </scroll-view>
    </view>
</view>
<van-popup show="{{ showTimePopup }}" position="bottom" bind:close="closeTimePopup" custom-style="border-radius: 20rpx 20rpx 0 0;">
  <view class="time-popup">
    <view class="time-popup-header">
      <view bindtap="closeTimePopup" class="cancel-btn">取消</view>
      <view class="title">选择时间范围</view>
      <view bindtap="confirmTime" class="confirm-btn">确定</view>
    </view>
    <view class="time-range">
      <picker mode="date" value="{{startDate}}" bindchange="onStartDateChange">
        <view class="date-picker-box {{startDate ? 'active' : ''}}">{{startDate || '开始日期'}}</view>
      </picker>
      <view class="to-text">至</view>
      <picker mode="date" value="{{endDate}}" bindchange="onEndDateChange">
        <view class="date-picker-box {{endDate ? 'active' : ''}}">{{endDate || '结束日期'}}</view>
      </picker>
    </view>
  </view>
</van-popup>
miniprogram/pages/my-balance/my-balance.wxss
New file
@@ -0,0 +1,240 @@
/* pages/my-balance/my-balance.wxss */
page {
    background-color: #ffffff;
    height: 100vh;
}
.container-balance {
    width: 750rpx;
    height: 100vh;
    display: flex;
    flex-direction: column;
}
.header {
    width: 750rpx;
    background-color: #ff8c3a;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.navbar-balance {
    width: 100%;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}
.back-btn {
    position: absolute;
    left: 30rpx;
    bottom: 0;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.back-icon {
    width: 34rpx;
    height: 28rpx;
}
.nav-title {
    font-size: 36rpx;
    color: #ffffff;
    font-weight: 500;
}
.balance-info {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 52rpx 0 38rpx;
}
.balance-label {
    font-size: 30rpx;
    color: rgba(255, 255, 255, 0.9);
}
.balance-amount {
    color: #ffffff;
    display: flex;
    align-items: baseline;
    margin-bottom: 10rpx;
}
.currency {
    font-size: 48rpx;
}
.amount {
    font-size: 80rpx;
    font-weight: 500;
}
.exchange-btn {
    background-color: rgba(255, 255, 255, 0.4);
    color: #ffffff;
    font-size: 26rpx;
    padding: 12rpx 36rpx;
    border-radius: 30rpx;
}
.content {
    background-color: #ffffff;
    width: 750rpx;
    flex: 1;
    display: flex;
    flex-direction: column;
    height: 0;
    /* Let it grow but establish a flex base for scroll-view */
}
.section-title {
    font-size: 32rpx;
    color: #333333;
    font-weight: 500;
    padding: 30rpx 30rpx 20rpx;
    flex-shrink: 0;
}
.filters {
    display: flex;
    padding: 0 30rpx;
    justify-content: space-between;
    margin-bottom: 20rpx;
    flex-shrink: 0;
}
.filter-item {
    background-color: #f5f5f5;
    border-radius: 16rpx;
    padding: 0 40rpx;
    height: 55rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 330rpx;
    box-sizing: border-box;
}
.filter-item text {
    font-size: 28rpx;
    color: #333333;
    flex: 1;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
}
.dropdown-icon {
    width: 12rpx;
    height: 8rpx;
    margin-left: 10rpx;
}
.record-list {
    flex: 1;
    height: 0;
}
.record-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 30rpx 0;
    margin: 0 30rpx;
    border-bottom: 1rpx solid #DEDEDE;
}
.record-left {
    display: flex;
    flex-direction: column;
}
.record-amount {
    font-size: 32rpx;
    font-weight: bold;
    margin-bottom: 16rpx;
}
.record-time {
    font-size: 24rpx;
    color: #999999;
}
.record-right {
    display: flex;
    align-items: center;
}
.record-type {
    font-size: 30rpx;
    color: #333333;
}
.time-popup {
    background-color: #fff;
    padding: 30rpx;
}
.time-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 50rpx;
    padding: 10rpx 0;
}
.cancel-btn {
    color: #999999;
    font-size: 32rpx;
    padding: 10rpx;
}
.confirm-btn {
    color: #ff8c3a;
    font-size: 32rpx;
    padding: 10rpx;
}
.title {
    font-size: 34rpx;
    color: #333333;
    font-weight: 500;
}
.time-range {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-bottom: 50rpx;
    padding-top: 10rpx;
}
.date-picker-box {
    background-color: #f5f5f5;
    width: 280rpx;
    height: 80rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 12rpx;
    font-size: 30rpx;
    color: #999999;
}
.date-picker-box.active {
    color: #333333;
}
.to-text {
    color: #333333;
    font-size: 30rpx;
    margin: 0 20rpx;
}
miniprogram/pages/travel-card-exchange/index.js
New file
@@ -0,0 +1,96 @@
// pages/travel-card-exchange/index.js
const app = getApp()
const unit = require('../../utils/util')
Page({
    data: {
        statusBarHeight: 20,
        cardNo: '',
        cardPassword: ''
    },
    onLoad: function (options) {
        const systemInfo = wx.getSystemInfoSync()
        this.setData({
            statusBarHeight: systemInfo.statusBarHeight
        })
    },
    goBack: function () {
        wx.navigateBack({
            delta: 1
        })
    },
    onCardNoInput: function (e) {
        this.setData({
            cardNo: e.detail.value
        })
    },
    onCardPasswordInput: function (e) {
        this.setData({
            cardPassword: e.detail.value
        })
    },
    submitExchange: function () {
        const { cardNo, cardPassword } = this.data;
        if (!cardNo) {
            return wx.showToast({
                title: '请输入出行卡号',
                icon: 'none'
            });
        }
        if (!cardPassword) {
            return wx.showToast({
                title: '请输入卡密码',
                icon: 'none'
            });
        }
        wx.showLoading({
            title: '兑换中...',
            mask: true
        });
        unit.post(app.globalData.Url + '/api/user/travelCardExchange', {
            cardNo,
            cardPassword
        }).then(res => {
            wx.hideLoading();
            if (res.data.code == 200) {
                // Determine the amount from response, assuming it might be returned directly or in a specific field
                let amount = '0.00';
                if (res.data.data) {
                    if (typeof res.data.data === 'number' || typeof res.data.data === 'string') {
                        amount = res.data.data;
                    } else if (res.data.data.amount) {
                        amount = res.data.data.amount;
                    } else if (res.data.data.money) {
                        amount = res.data.data.money;
                    } else if (res.data.data.balance) {
                        amount = res.data.data.balance;
                    }
                }
                wx.navigateTo({
                    url: '/pages/travel-card-success/index?amount=' + amount
                });
            } else {
                wx.showToast({
                    title: res.data.msg || '兑换失败',
                    icon: 'none'
                });
            }
        }).catch(err => {
            wx.hideLoading();
            wx.showToast({
                title: '网络错误,请稍后重试',
                icon: 'none'
            });
        });
    }
})
miniprogram/pages/travel-card-exchange/index.json
New file
@@ -0,0 +1,3 @@
{
  "navigationBarTitleText": "出行卡"
}
miniprogram/pages/travel-card-exchange/index.wxml
New file
@@ -0,0 +1,22 @@
<!--pages/travel-card-exchange/index.wxml-->
<view class="container-exchange">
    <view class="content">
        <view class="input-group">
            <view class="label">出行卡号</view>
            <view class="input-box">
                <input class="input" placeholder="请输入" value="{{cardNo}}" bindinput="onCardNoInput" placeholder-class="ph-color"/>
            </view>
        </view>
        <view class="input-group">
            <view class="label">卡密码</view>
            <view class="input-box">
                <input class="input" placeholder="请输入" value="{{cardPassword}}" bindinput="onCardPasswordInput" placeholder-class="ph-color" type="text"/>
            </view>
        </view>
    </view>
    <view class="bottom-btn">
        <view class="submit-btn" bindtap="submitExchange">兑换</view>
    </view>
</view>
miniprogram/pages/travel-card-exchange/index.wxss
New file
@@ -0,0 +1,71 @@
/* pages/travel-card-exchange/index.wxss */
page {
    background-color: #ffffff;
    height: 100vh;
}
.container-exchange {
    width: 750rpx;
    height: 100vh;
    display: flex;
    flex-direction: column;
}
.content {
    flex: 1;
    padding: 60rpx 90rpx;
}
.input-group {
    margin-bottom: 48rpx;
}
.label {
    font-size: 28rpx;
    color: #333333;
    margin-bottom: 20rpx;
    font-weight: 500;
}
.input-box {
    width: 100%;
    height: 88rpx;
    background: #ffffff;
    border-radius: 8rpx;
    border: 2rpx solid #eeeeee;
    display: flex;
    align-items: center;
    padding: 0 24rpx;
    box-sizing: border-box;
}
.input {
    flex: 1;
    font-size: 30rpx;
    color: #333333;
    height: 100%;
}
.ph-color {
    color: #cccccc;
}
.bottom-btn {
    padding: 40rpx;
    padding-bottom: env(safe-area-inset-bottom);
    display: flex;
    justify-content: center;
}
.submit-btn {
    width: calc(100% - 40rpx);
    height: 100rpx;
    background: linear-gradient(0deg, #d33233 0%, #ff8183 100%);
    border-radius: 50rpx;
    color: #ffffff;
    font-size: 34rpx;
    font-weight: 500;
    display: flex;
    align-items: center;
    justify-content: center;
}
miniprogram/pages/travel-card-success/index.js
New file
@@ -0,0 +1,24 @@
// pages/travel-card-success/index.js
Page({
    data: {
        statusBarHeight: 20,
        amount: '0.00'
    },
    onLoad: function (options) {
        const systemInfo = wx.getSystemInfoSync()
        this.setData({
            statusBarHeight: systemInfo.statusBarHeight,
            amount: options.amount || '0.00'
        })
    },
    goBack: function () {
        // Navigate back to the balance page.
        // Stack: [... -> my-balance -> travel-card-exchange -> travel-card-success]
        // delta: 2 returns to my-balance
        wx.navigateBack({
            delta: 2
        })
    }
})
miniprogram/pages/travel-card-success/index.json
New file
@@ -0,0 +1,3 @@
{
  "navigationBarTitleText": "出行卡"
}
miniprogram/pages/travel-card-success/index.wxml
New file
@@ -0,0 +1,10 @@
<!--pages/travel-card-success/index.wxml-->
<view class="container-success">
    <view class="content">
        <image class="success-icon" src="/img/conversion-success.png"></image>
        <view class="success-text">兑换成功,请前往我的余额查看</view>
        <view class="amount-text">+ ¥ {{amount}}</view>
        <view class="back-home-btn" bindtap="goBack">返回</view>
    </view>
</view>
miniprogram/pages/travel-card-success/index.wxss
New file
@@ -0,0 +1,52 @@
/* pages/travel-card-success/index.wxss */
page {
    background-color: #ffffff;
    height: 100vh;
}
.container-success {
    width: 750rpx;
    height: 100vh;
    display: flex;
    flex-direction: column;
}
.content {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding-top: 126rpx;
}
.success-icon {
    width: 104rpx;
    height: 104rpx;
    margin-bottom: 22rpx;
}
.success-text {
    font-size: 28rpx;
    color: #000000;
    margin-bottom: 16rpx;
}
.amount-text {
    font-size: 76rpx;
    color: #d33233;
    font-weight: 600;
    margin-bottom: 100rpx;
}
.back-home-btn {
    width: 200rpx;
    height: 80rpx;
    background: #ffffff;
    border: 2rpx solid #cccccc;
    border-radius: 40rpx;
    font-size: 30rpx;
    color: #4A4C59;
    display: flex;
    align-items: center;
    justify-content: center;
}
miniprogram/utils/ajax.js
@@ -113,7 +113,7 @@
// 上面地址不管用,下面的地址才用
  // baseURL:"http://221.182.45.100:7894/user"
  baseURL: "http://192.168.110.11:5000/user"
  baseURL: "http://192.168.110.34:5000/user"
});
/**