重构:移动源码到 src/ 目录,支持 CLI 构建
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
import { initEnv } from '@/utils/env.js';
|
||||
import { handleAuthCallback, onAuthSuccess } from '@/utils/auth-guard.js';
|
||||
|
||||
export default {
|
||||
onLaunch: function(options) {
|
||||
console.log('App Launch')
|
||||
|
||||
// 1. 环境检测 + 缓存 app_no/platform
|
||||
initEnv();
|
||||
|
||||
// #ifdef H5
|
||||
// 2. 处理授权回调(URL 中带 code/auth_code 参数)
|
||||
this.handleAuthRedirect();
|
||||
|
||||
// 3. 解析 URL 路径中的设备编号(如 /A1036)
|
||||
const path = window.location.pathname;
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
if (segments.length > 0) {
|
||||
const last = segments[segments.length - 1];
|
||||
// 匹配设备编号格式:大写字母开头 + 大写字母/数字
|
||||
if (/^[A-Z][A-Z0-9]+$/.test(last)) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?device_id=' + last
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
// #ifdef H5
|
||||
async handleAuthRedirect() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code') || urlParams.get('auth_code') || '';
|
||||
|
||||
if (!code) return;
|
||||
|
||||
// 有授权码,调用后端回调接口
|
||||
const result = await handleAuthCallback();
|
||||
if (result) {
|
||||
onAuthSuccess(result);
|
||||
|
||||
// 清除 URL 中的 code 参数,避免刷新重复处理
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('auth_code');
|
||||
url.searchParams.delete('state');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
|
||||
// 如果需要绑定手机号
|
||||
if (result.need_bindphone) {
|
||||
uni.setStorageSync('temp_token', result.temp_token);
|
||||
uni.showToast({ title: '请绑定手机号完成注册', icon: 'none' });
|
||||
} else {
|
||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/*每个页面公共css */
|
||||
@import '@/uni_modules/uni-scss/index.scss';
|
||||
/* #ifndef APP-NVUE */
|
||||
@import '@/static/customicons.css';
|
||||
// 设置整个项目的背景色
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
.example-info {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<view class="float-menu-card">
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="menu-item"
|
||||
:class="{ 'last-item': index === list.length - 1 }"
|
||||
@click="handleItemClick(item, index)"
|
||||
>
|
||||
<text class="menu-text">{{ item.title }}</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FloatMenuCard',
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleItemClick(item, index) {
|
||||
// 发出事件,让父组件处理跳转逻辑
|
||||
this.$emit('item-click', { item, index });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.float-menu-card {
|
||||
position: fixed;
|
||||
bottom: 40rpx;
|
||||
left: 28rpx;
|
||||
right: 28rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 32rpx 28rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item:last-child,
|
||||
.menu-item.last-item {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 28rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<view class="page-nav-bar">
|
||||
<view class="nav-btn left" @click="handlePrev">
|
||||
<text class="arrow-icon">‹</text>
|
||||
</view>
|
||||
<view class="nav-btn right" @click="handleNext">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageNavBar',
|
||||
props: {
|
||||
hasPrev: { type: Boolean, default: true },
|
||||
hasNext: { type: Boolean, default: true }
|
||||
},
|
||||
methods: {
|
||||
handlePrev() {
|
||||
if (!this.hasPrev) return;
|
||||
this.$emit('prev');
|
||||
},
|
||||
handleNext() {
|
||||
if (!this.hasNext) return;
|
||||
this.$emit('next');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-nav-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 110rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 160rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 72rpx;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-btn.right .arrow-icon {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<view class="page-nav">
|
||||
<!-- 首页按钮 -->
|
||||
<view class="nav-item" @click="handleNav('home')">
|
||||
<text class="nav-icon">🏠</text>
|
||||
<text class="nav-text">首页</text>
|
||||
</view>
|
||||
|
||||
<!-- 开门按钮 - 圆形凸起 -->
|
||||
<view class="door-btn-wrapper" @click="handleOpenDoor">
|
||||
<view class="door-btn">开门</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的按钮 -->
|
||||
<view class="nav-item" @click="handleNav('mine')">
|
||||
<text class="nav-icon">👤</text>
|
||||
<text class="nav-text">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageNav',
|
||||
props: {
|
||||
// 当前高亮的tab: 'home' | 'mine'
|
||||
activeTab: {
|
||||
type: String,
|
||||
default: 'home'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleNav(tab) {
|
||||
// 发出事件,由父组件处理导航
|
||||
this.$emit('change', tab);
|
||||
},
|
||||
handleOpenDoor() {
|
||||
uni.showModal({
|
||||
title: '开门',
|
||||
content: '确认打开柜门?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '柜门已打开', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
this.$emit('openDoor');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120rpx;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
z-index: 999;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 40rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.nav-item:active .nav-text,
|
||||
.nav-item.active .nav-text {
|
||||
color: #2979ff;
|
||||
}
|
||||
|
||||
.door-btn-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -80rpx;
|
||||
}
|
||||
|
||||
.door-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #2979ff, #1e60e0);
|
||||
color: #fff;
|
||||
font-size: 38rpx;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 36rpx rgba(41, 121, 255, 0.55);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 开发环境配置
|
||||
*/
|
||||
const dev = {
|
||||
// 业务 API 地址(vms-api,端口 4000)
|
||||
apiBaseUrl: 'http://192.168.0.23:4000',
|
||||
|
||||
// 网关地址(vms-gateway,端口 4001)— 用户授权、短信等接口走网关
|
||||
apiGatewayUrl: 'https://gateway.arklinksmart.cn',
|
||||
|
||||
// 授权回调地址(支付宝/微信 OAuth redirect_uri)
|
||||
authRedirectUri: 'https://gateway.arklinksmart.cn/api/v1/user/auth/callback',
|
||||
|
||||
// 开发阶段硬编码 Token(生产环境应从登录流程获取)
|
||||
bearerToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRlZXB2ZW5kaW5nIiwic3ViIjoiMTY0MDI0MTEyNiIsImZyb20iOiJwb3MiLCJyb2xlIjoicG9zIiwibWFpbmJvYXJkX2lkIjoiVDEwMDIiLCJwb3Nfc24iOiIxNjQwMjQxMTI2IiwiaWF0IjoxNzc4NDkxOTA3LCJleHAiOjE3NzkwOTY3MDd9.DjscZtrTCr30XOngToUifxoel4q2EGES_uqGQernkvg',
|
||||
|
||||
// 是否开启请求日志
|
||||
enableRequestLog: true,
|
||||
};
|
||||
|
||||
export default dev;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 环境配置入口
|
||||
* 根据编译模式自动选择 dev / prod 配置
|
||||
*/
|
||||
import common from './index.js';
|
||||
import dev from './dev.js';
|
||||
import prod from './prod.js';
|
||||
|
||||
// uni-app 编译时通过 define 注译 __UNI_PLATFORM__
|
||||
// H5 开发模式下 process.env.NODE_ENV === 'development'
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const envConfig = isProd ? prod : dev;
|
||||
|
||||
const config = {
|
||||
...common,
|
||||
...envConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 公共配置(开发/生产环境共享)
|
||||
*/
|
||||
const common = {
|
||||
// 应用信息
|
||||
appName: '悟空智能',
|
||||
appVersion: '1.0.0',
|
||||
|
||||
// 默认设备ID(开发调试用)
|
||||
defaultDeviceId: '',
|
||||
|
||||
// 客服电话
|
||||
servicePhone: '13264706088',
|
||||
|
||||
// 请求超时时间(ms)
|
||||
requestTimeout: 10000,
|
||||
};
|
||||
|
||||
export default common;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 生产环境配置
|
||||
*/
|
||||
const prod = {
|
||||
// 业务 API 地址(vms-api)
|
||||
apiBaseUrl: 'https://api.arklink.com',
|
||||
|
||||
// 网关地址(vms-gateway)— 用户授权、短信等接口走网关
|
||||
apiGatewayUrl: 'https://gateway.arklink.com',
|
||||
|
||||
// 授权回调地址(支付宝/微信 OAuth redirect_uri)
|
||||
authRedirectUri: 'https://gateway.arklink.com/api/v1/user/auth/callback',
|
||||
|
||||
// 生产环境 Token 从登录流程获取,此处为空
|
||||
bearerToken: '',
|
||||
|
||||
enableRequestLog: false,
|
||||
};
|
||||
|
||||
export default prod;
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import App from './App'
|
||||
import { initVConsole } from '@/utils/vconsole'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
App.mpType = 'app'
|
||||
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { initVConsole } from '@/utils/vconsole'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 初始化 vConsole(开发环境)
|
||||
initVConsole()
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name" : "consumer-front",
|
||||
"appid" : "__UNI__9A07903",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
"app-plus" : {
|
||||
/* 5+App特有相关 */
|
||||
"usingComponents" : true,
|
||||
"nvueCompiler" : "uni-app",
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
"modules" : {},
|
||||
/* 模块配置 */
|
||||
"distribute" : {
|
||||
/* 应用发布信息 */
|
||||
"android" : {
|
||||
/* android打包配置 */
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
"ios" : {},
|
||||
/* ios打包配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* SDK配置 */
|
||||
"quickapp" : {},
|
||||
/* 快应用特有相关 */
|
||||
"mp-weixin" : {
|
||||
/* 小程序特有相关 */
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"h5" : {
|
||||
"router" : {
|
||||
"mode" : "history"
|
||||
}
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/scan",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/faq/faq",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/order",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "智能货柜",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<view class="faq-page">
|
||||
<!-- 问题列表 -->
|
||||
<scroll-view class="faq-scroll" scroll-y>
|
||||
<view class="faq-list">
|
||||
<view
|
||||
class="faq-item"
|
||||
v-for="(item, index) in faqList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 问题标题(点击展开/收起) -->
|
||||
<view class="faq-question" @click="toggleItem(index)">
|
||||
<text class="q-text">{{ item.question }}</text>
|
||||
<text class="q-arrow" :class="{ expanded: item.expanded }">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 答案内容(展开时显示) -->
|
||||
<view class="faq-answer" v-if="item.expanded">
|
||||
<text class="a-text">{{ item.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部左右箭头导航条 -->
|
||||
<page-nav-bar
|
||||
:hasPrev="true"
|
||||
:hasNext="false"
|
||||
@prev="handleNavPrev"
|
||||
@next="handleNavNext"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageNavBar from '@/components/page-nav-bar/page-nav-bar.vue';
|
||||
|
||||
export default {
|
||||
components: { PageNavBar },
|
||||
data() {
|
||||
return {
|
||||
faqList: [
|
||||
{
|
||||
question: '如何购买商品?',
|
||||
answer: '在首页浏览商品,点击商品卡片选择商品,确认后即可完成购买。支持微信支付和支付宝支付。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '如何开门取货?',
|
||||
answer: '下单成功后,点击首页底部中间的"开门"按钮,柜门会自动打开,取出商品后关闭柜门即可。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '商品质量问题怎么办?',
|
||||
answer: '如发现商品有质量问题,请在24小时内通过"联系客服"反馈,我们会为您办理退款或换货。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '如何查看订单?',
|
||||
answer: '进入"我的"页面,点击"我的订单"可查看所有订单记录,包括待付款、已完成、已退款等状态。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '忘记密码怎么办?',
|
||||
answer: '在登录页面点击"忘记密码",输入注册手机号,通过短信验证码即可重置密码。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '如何修改收货地址?',
|
||||
answer: '进入"我的"页面,点击"收货地址",可以新增、编辑或删除收货地址信息。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '支持哪些支付方式?',
|
||||
answer: '目前支持微信支付、支付宝支付两种方式,后续将开通更多支付渠道。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
question: '退款多久到账?',
|
||||
answer: '退款申请审核通过后,微信支付1-3个工作日到账,支付宝1-5个工作日到账,具体以银行处理时间为准。',
|
||||
expanded: false
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleItem(index) {
|
||||
this.faqList[index].expanded = !this.faqList[index].expanded;
|
||||
},
|
||||
handleNavPrev() {
|
||||
uni.navigateBack({ fail: () => { uni.switchTab({ url: '/pages/index/index' }); } });
|
||||
},
|
||||
handleNavNext() {
|
||||
uni.showToast({ title: '暂无下一页', icon: 'none' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
page { height: 100%; background-color: #f5f5f5; }
|
||||
|
||||
.faq-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 滚动区域 */
|
||||
.faq-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* 问题列表 */
|
||||
.faq-list {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 问题标题 */
|
||||
.faq-question {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
.q-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.q-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
transition: transform 0.25s;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
.q-arrow.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* 答案 */
|
||||
.faq-answer {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 24rpx;
|
||||
background: #f8f9fc;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.a-text {
|
||||
font-size: 27rpx;
|
||||
color: #666;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* 底部占位 */
|
||||
.bottom-spacer {
|
||||
height: 120rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<!-- ========== 首页内容 ========== -->
|
||||
<view v-if="currentTab === 'home'" class="page-content home-page">
|
||||
<scroll-view
|
||||
class="product-area"
|
||||
scroll-y
|
||||
>
|
||||
<!-- 热销商品标题 + 装饰 -->
|
||||
<view class="section-title-wrap">
|
||||
<view class="section-title">热销商品</view>
|
||||
<view class="title-decoration">
|
||||
<view class="deco-dot"></view>
|
||||
<view class="deco-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类 Tab 横向滚动 -->
|
||||
<scroll-view class="category-tabs" scroll-x>
|
||||
<view class="tabs-inner">
|
||||
<view class="tab-item" :class="{ active: activeCategory === 'all' }" @click="switchCategory('all')">
|
||||
<text class="tab-text">全部商品</text>
|
||||
<view class="tab-underline"></view>
|
||||
</view>
|
||||
<view
|
||||
v-for="(cat, idx) in categoryList"
|
||||
:key="idx"
|
||||
class="tab-item"
|
||||
:class="{ active: activeCategory === cat.category }"
|
||||
@click="switchCategory(cat.category)"
|
||||
>
|
||||
<text class="tab-text">{{ cat.category }}</text>
|
||||
<view class="tab-underline"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 商品网格 -->
|
||||
<view class="product-grid">
|
||||
<view
|
||||
v-for="(item, idx) in displayProducts"
|
||||
:key="idx"
|
||||
class="product-card"
|
||||
>
|
||||
<image class="product-img" :src="item.image" mode="aspectFill" />
|
||||
<view class="product-name">{{ item.name }}</view>
|
||||
<view class="product-price">
|
||||
<text class="price-yen">¥</text>
|
||||
<text class="price-num">{{ getDisplayPrice(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无商品时 -->
|
||||
<view v-if="categoryList.length === 0 && !loading" class="empty-tip">
|
||||
<text>暂无商品</text>
|
||||
</view>
|
||||
|
||||
<!-- loading -->
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item" :class="{ active: currentTab === 'home' }" @click="switchTab('home')">
|
||||
<view class="home-icon">
|
||||
<view class="home-roof"></view>
|
||||
<view class="home-body"></view>
|
||||
</view>
|
||||
<text class="tab-text">首页</text>
|
||||
</view>
|
||||
|
||||
<view class="door-btn-wrapper" @click="handleOpenDoor">
|
||||
<view class="door-btn">开门</view>
|
||||
</view>
|
||||
|
||||
<view class="tab-item" :class="{ active: currentTab === 'mine' }" @click="switchTab('mine')">
|
||||
<view class="mine-icon">
|
||||
<view class="mine-head"></view>
|
||||
<view class="mine-body"></view>
|
||||
</view>
|
||||
<text class="tab-text">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 我的内容 ========== -->
|
||||
<view v-if="currentTab === 'mine'" class="page-content mine-page">
|
||||
<!-- 蓝色个人信息区域 -->
|
||||
<view class="user-header">
|
||||
<view class="user-info">
|
||||
<view class="avatar-wrapper">
|
||||
<image class="avatar" :src="userInfo.avatar || '/static/default-avatar.png'" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="user-detail">
|
||||
<text class="nickname">{{ userInfo.nickname || '未登录' }}</text>
|
||||
<text class="phone">{{ maskedPhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="content-area">
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleMenuClick(item)">
|
||||
<text class="menu-text">{{ item.title }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 开发调试工具(仅开发环境显示) -->
|
||||
<view v-if="isDev" class="dev-tools">
|
||||
<view class="dev-tools-title">开发工具</view>
|
||||
<view class="menu-item">
|
||||
<text class="menu-text">vConsole 调试</text>
|
||||
<switch :checked="vconsoleEnabled" color="#2979ff" @change="onVConsoleChange" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部左右箭头导航条 -->
|
||||
<page-nav-bar
|
||||
:hasPrev="true"
|
||||
:hasNext="false"
|
||||
@prev="handleNavPrev"
|
||||
@next="handleNavNext"
|
||||
/>
|
||||
|
||||
<!-- 公众号二维码弹窗 -->
|
||||
<view v-if="showQrcodePopup" class="qrcode-popup-mask" @click="showQrcodePopup = false">
|
||||
<view class="qrcode-popup-content" @click.stop>
|
||||
<view class="qrcode-popup-title">关注公众号</view>
|
||||
<image class="qrcode-img" src="/static/qrcode.png" mode="aspectFit" />
|
||||
<view class="qrcode-popup-tip">扫码关注获取更多优惠信息</view>
|
||||
<view class="qrcode-popup-close" @click="showQrcodePopup = false">关闭</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageNavBar from '@/components/page-nav-bar/page-nav-bar.vue';
|
||||
import config from '@/config/env.js';
|
||||
import { get } from '@/utils/request.js';
|
||||
import { checkLoginWithPrompt, getCachedUserInfo } from '@/utils/auth-guard.js';
|
||||
import { isVConsoleEnabled, showVConsole, hideVConsole } from '@/utils/vconsole';
|
||||
|
||||
export default {
|
||||
components: { PageNavBar },
|
||||
data() {
|
||||
return {
|
||||
deviceId: '',
|
||||
currentTab: 'home',
|
||||
showQrcodePopup: false,
|
||||
loading: false,
|
||||
categoryList: [],
|
||||
activeCategory: 'all',
|
||||
userInfo: { nickname: '', phone: '', avatar: '' },
|
||||
menuList: [
|
||||
{ title: '我的订单', action: 'order' },
|
||||
{ title: '常见问题', action: 'faq' },
|
||||
{ title: '联系客服', action: 'call' },
|
||||
{ title: '公众号信息', action: 'qrcode' }
|
||||
],
|
||||
servicePhone: config.servicePhone,
|
||||
isDev: process.env.NODE_ENV === 'development',
|
||||
vconsoleEnabled: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
maskedPhone() {
|
||||
const p = this.userInfo.phone;
|
||||
return p ? p.substring(0,3) + '****' + p.substring(7) : '';
|
||||
},
|
||||
// 全部商品:合并所有分类下的商品
|
||||
allProducts() {
|
||||
let all = [];
|
||||
this.categoryList.forEach(cat => {
|
||||
if (cat.products) all = all.concat(cat.products);
|
||||
});
|
||||
return all;
|
||||
},
|
||||
// 当前显示的商品列表(根据选中的分类 tab)
|
||||
displayProducts() {
|
||||
if (this.activeCategory === 'all') return this.allProducts;
|
||||
const cat = this.categoryList.find(c => c.category === this.activeCategory);
|
||||
return cat ? cat.products || [] : [];
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 支持通过 tab 参数切换到"我的"页面
|
||||
if (options.tab === 'mine') {
|
||||
this.currentTab = 'mine';
|
||||
}
|
||||
|
||||
if (options.device_id) {
|
||||
this.deviceId = options.device_id;
|
||||
this.fetchProducts();
|
||||
} else if (!options.tab) {
|
||||
// 无设备编号且非 tab 跳转,跳回扫码页
|
||||
uni.redirectTo({ url: '/pages/index/scan' });
|
||||
}
|
||||
|
||||
// 加载缓存的用户信息
|
||||
this.loadUserInfo();
|
||||
|
||||
// 初始化 vConsole 状态
|
||||
this.vconsoleEnabled = isVConsoleEnabled();
|
||||
},
|
||||
onShow() {
|
||||
// 每次显示页面时刷新用户信息(授权回调后可能已更新)
|
||||
this.loadUserInfo();
|
||||
},
|
||||
onReady() {
|
||||
// 页面渲染完成后再次检查状态
|
||||
this.vconsoleEnabled = isVConsoleEnabled();
|
||||
},
|
||||
methods: {
|
||||
switchTab(tab) {
|
||||
// 切换到"我的"Tab 时检查登录
|
||||
if (tab === 'mine' && !checkLoginWithPrompt('mine')) return;
|
||||
this.currentTab = tab;
|
||||
},
|
||||
switchCategory(cat) { this.activeCategory = cat; },
|
||||
getDisplayPrice(item) {
|
||||
// 优先显示促销价,没有促销价则显示销售价
|
||||
if (item.promotion_price != null) return item.promotion_price;
|
||||
return item.sale_price;
|
||||
},
|
||||
// 从缓存加载用户信息
|
||||
loadUserInfo() {
|
||||
const cached = getCachedUserInfo();
|
||||
if (cached) {
|
||||
this.userInfo = { ...this.userInfo, ...cached };
|
||||
} else {
|
||||
this.userInfo = { nickname: '', phone: '', avatar: '' };
|
||||
}
|
||||
},
|
||||
async fetchProducts() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const res = await get('/device-product/getProductList', {
|
||||
device_id: this.deviceId,
|
||||
});
|
||||
|
||||
const ret = res.data || {};
|
||||
if (ret.code === 0 && Array.isArray(ret.data)) {
|
||||
this.categoryList = ret.data;
|
||||
} else {
|
||||
uni.showToast({ title: ret.message || '获取商品失败', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '网络请求失败', icon: 'none' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleOpenDoor() {
|
||||
uni.showModal({
|
||||
title: '开门',
|
||||
content: '确认打开柜门?',
|
||||
success: r => r.confirm && uni.showToast({ title: '柜门已打开', icon: 'success' })
|
||||
});
|
||||
},
|
||||
handleMenuClick(item) {
|
||||
switch (item.action) {
|
||||
case 'order':
|
||||
// 查看订单需要登录
|
||||
if (!checkLoginWithPrompt('order')) return;
|
||||
uni.navigateTo({ url: '/pages/order/order' });
|
||||
break;
|
||||
case 'faq':
|
||||
uni.navigateTo({ url: '/pages/faq/faq' });
|
||||
break;
|
||||
case 'call':
|
||||
uni.makePhoneCall({ phoneNumber: this.servicePhone, fail: () => {} });
|
||||
break;
|
||||
case 'qrcode':
|
||||
this.showQrcodePopup = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleNavPrev() { this.currentTab = 'home'; },
|
||||
handleNavNext() { uni.showToast({ title: '暂无下一页', icon: 'none' }); },
|
||||
onVConsoleChange(e) {
|
||||
const enabled = e.detail.value;
|
||||
if (enabled) {
|
||||
showVConsole();
|
||||
} else {
|
||||
hideVConsole();
|
||||
}
|
||||
this.vconsoleEnabled = enabled;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
page { height: 100%; background-color: #fff; }
|
||||
.app-container { display: flex; flex-direction: column; height: 100vh; background-color: #fff; }
|
||||
.page-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; position: relative; }
|
||||
|
||||
/* ====== 首页样式 ====== */
|
||||
.home-page { display: flex; flex-direction: column; height: 100%; }
|
||||
.product-area { flex: 1; overflow-y: auto; height: 0; }
|
||||
|
||||
/* 热销商品标题 + 装饰 */
|
||||
.section-title-wrap { display: flex; align-items: center; padding: 24rpx 28rpx 16rpx; }
|
||||
.section-title { font-size: 36rpx; font-weight: bold; color: #333; }
|
||||
.title-decoration { display: flex; align-items: center; margin-left: 16rpx; }
|
||||
.deco-dot { width: 12rpx; height: 12rpx; background: #ff6b6b; border-radius: 50%; }
|
||||
.deco-line { width: 40rpx; height: 6rpx; background: linear-gradient(90deg, #ff6b6b, transparent); margin-left: 6rpx; border-radius: 3rpx; }
|
||||
|
||||
/* 分类 Tab 横向滚动 */
|
||||
.category-tabs { white-space: nowrap; padding: 0 20rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.tabs-inner { display: inline-flex; padding: 0 8rpx; }
|
||||
.tab-item { display: inline-flex; flex-direction: column; align-items: center; padding: 20rpx 24rpx; position: relative; }
|
||||
.tab-text { font-size: 28rpx; color: #666; transition: color 0.2s; }
|
||||
.tab-item.active .tab-text { color: #2979ff; font-weight: bold; }
|
||||
.tab-underline { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 0; height: 4rpx; background: #2979ff; border-radius: 2rpx; transition: width 0.2s; }
|
||||
.tab-item.active .tab-underline { width: 48rpx; }
|
||||
|
||||
/* 商品网格 */
|
||||
.product-grid { display: flex; flex-wrap: wrap; padding: 12rpx; gap: 12rpx; }
|
||||
.product-card { width: calc((100% - 24rpx) / 3); text-align: center; }
|
||||
.product-img { width: 100%; height: 220rpx; border-radius: 8rpx; background-color: #fff; }
|
||||
.product-name { font-size: 24rpx; color: #333; padding: 10rpx 8rpx 6rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.product-price { font-size: 30rpx; font-weight: bold; color: #e4393c; padding: 0 8rpx 16rpx; }
|
||||
.price-yen { font-size: 22rpx; }
|
||||
.price-num { font-size: 32rpx; }
|
||||
|
||||
/* 底部占位 */
|
||||
.bottom-spacer { height: 160rpx; }
|
||||
|
||||
/* 底部导航栏 */
|
||||
.tab-bar { position: absolute; bottom: 0; left: 0; right: 0; height: 120rpx; background: #fff; display: flex; align-items: center; justify-content: space-around; box-shadow: 0 -2rpx 16rpx rgba(0,0,0,0.08); z-index: 999; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }
|
||||
.tab-bar .tab-item { flex: 1; display: flex; align-items: center; justify-content: center; height: 100%; }
|
||||
.tab-bar .tab-text { font-size: 28rpx; color: #999; font-weight: normal; }
|
||||
.tab-bar .tab-item.active .tab-text { color: #2979ff; }
|
||||
|
||||
/* 首页图标 */
|
||||
.tab-bar .home-icon {
|
||||
width: 40rpx;
|
||||
height: 36rpx;
|
||||
position: relative;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.tab-bar .home-roof {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 22rpx solid transparent;
|
||||
border-right: 22rpx solid transparent;
|
||||
border-bottom: 18rpx solid #999;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.tab-bar .home-body {
|
||||
width: 30rpx;
|
||||
height: 18rpx;
|
||||
background-color: #999;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 0 0 3rpx 3rpx;
|
||||
}
|
||||
.tab-bar .tab-item.active .home-roof {
|
||||
border-bottom-color: #2979ff;
|
||||
}
|
||||
.tab-bar .tab-item.active .home-body {
|
||||
background-color: #2979ff;
|
||||
}
|
||||
|
||||
/* 我的图标(与扫一扫页共用) */
|
||||
.tab-bar .mine-icon {
|
||||
width: 40rpx;
|
||||
height: 36rpx;
|
||||
position: relative;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.tab-bar .mine-head {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.tab-bar .mine-body {
|
||||
width: 28rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 14rpx 14rpx 0 0;
|
||||
background-color: #999;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.tab-bar .tab-item.active .mine-head,
|
||||
.tab-bar .tab-item.active .mine-body {
|
||||
background-color: #2979ff;
|
||||
}
|
||||
.door-btn-wrapper { position: relative; display: flex; align-items: center; justify-content: center; margin-top: -80rpx; }
|
||||
.door-btn { width: 160rpx; height: 160rpx; border-radius: 50%; background: linear-gradient(135deg, #2979ff, #1e60e0); color: #fff; font-size: 38rpx; font-weight: bold; display: flex; align-items: center; justify-content: center; box-shadow: 0 10rpx 36rpx rgba(41,121,255,0.55); }
|
||||
|
||||
/* 空状态 / Loading */
|
||||
.empty-tip, .loading-tip { text-align: center; padding: 60rpx; color: #999; font-size: 28rpx; }
|
||||
|
||||
/* ====== 我的页面样式 ====== */
|
||||
.mine-page { background-color: #f5f5f5; position: relative; overflow-y: auto; }
|
||||
.user-header { background: linear-gradient(135deg, #2979ff, #1e60e0); padding: 60rpx 40rpx; }
|
||||
.user-info { display: flex; align-items: center; }
|
||||
.avatar { width: 120rpx; height: 120rpx; border-radius: 50%; border: 4rpx solid rgba(255,255,255,0.8); background-color: #e0e0e0; }
|
||||
.avatar-wrapper { margin-right: 30rpx; }
|
||||
.user-detail { display: flex; flex-direction: column; flex: 1; }
|
||||
.nickname { font-size: 36rpx; font-weight: bold; color: #fff; margin-bottom: 12rpx; }
|
||||
.phone { font-size: 28rpx; color: rgba(255,255,255,0.9); }
|
||||
.content-area { padding: 20rpx; padding-bottom: 140rpx; }
|
||||
.menu-list { background-color: #fff; border-radius: 16rpx; overflow: hidden; }
|
||||
.menu-item { display: flex; justify-content: space-between; padding: 32rpx 28rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.menu-text { font-size: 30rpx; color: #333; }
|
||||
.menu-arrow { font-size: 28rpx; color: #ccc; }
|
||||
|
||||
/* 开发工具区域 */
|
||||
.dev-tools { margin-top: 20rpx; background-color: #fff; border-radius: 16rpx; overflow: hidden; }
|
||||
.dev-tools-title { padding: 20rpx 28rpx 10rpx; font-size: 24rpx; color: #999; }
|
||||
.dev-tools .menu-item { align-items: center; }
|
||||
.dev-tools switch { transform: scale(0.8); }
|
||||
|
||||
/* ====== 公众号二维码弹窗 ====== */
|
||||
.qrcode-popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.qrcode-popup-content {
|
||||
width: 560rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.qrcode-popup-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.qrcode-img {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.qrcode-popup-tip {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.qrcode-popup-close {
|
||||
font-size: 28rpx;
|
||||
color: #2979ff;
|
||||
padding: 16rpx 60rpx;
|
||||
border: 2rpx solid #2979ff;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<view class="scan-page">
|
||||
<!-- 顶部品牌 -->
|
||||
<view class="brand-area">
|
||||
<text class="brand-name">智联方舟</text>
|
||||
</view>
|
||||
|
||||
<!-- 中间扫一扫按钮 -->
|
||||
<view class="scan-area">
|
||||
<view class="scan-btn" @click="handleScan">
|
||||
<!-- 二维码图标 -->
|
||||
<view class="qr-icon">
|
||||
<view class="qr-row">
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-space"></view>
|
||||
<view class="qr-block"></view>
|
||||
</view>
|
||||
<view class="qr-row">
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-space"></view>
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-space"></view>
|
||||
</view>
|
||||
<view class="qr-row">
|
||||
<view class="qr-space"></view>
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-space"></view>
|
||||
<view class="qr-block"></view>
|
||||
</view>
|
||||
<view class="qr-row">
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-block"></view>
|
||||
<view class="qr-space"></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="scan-text">扫一扫</text>
|
||||
</view>
|
||||
<text class="scan-tip">扫描货柜上的二维码开始购物</text>
|
||||
</view>
|
||||
|
||||
<!-- 我的入口 -->
|
||||
<view class="mine-entry" @click="goMine">
|
||||
<view class="mine-icon">
|
||||
<view class="mine-head"></view>
|
||||
<view class="mine-body"></view>
|
||||
</view>
|
||||
<text class="mine-text">我的</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部客服电话 -->
|
||||
<view class="footer">
|
||||
<text class="footer-text">客服电话:</text>
|
||||
<text class="footer-phone" @click="callService">{{ servicePhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/config/env.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
servicePhone: config.servicePhone,
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
// 1. query 参数中携带设备编号
|
||||
if (options.device_id) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?device_id=' + options.device_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// 2. URL 路径中携带设备编号(如 http://xxx/A1036)
|
||||
const path = window.location.pathname;
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
if (segments.length > 0) {
|
||||
const last = segments[segments.length - 1];
|
||||
if (/^[A-Z][A-Z0-9]+$/.test(last)) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?device_id=' + last,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
handleScan() {
|
||||
// #ifdef H5
|
||||
uni.showToast({ title: 'H5 环境暂不支持扫码', icon: 'none' });
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.scanCode({
|
||||
onlyFromCamera: false,
|
||||
success: (res) => {
|
||||
this.parseScanResult(res.result);
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '扫码取消', icon: 'none' });
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
parseScanResult(result) {
|
||||
// 解析扫码结果,提取 device_id
|
||||
// 预期格式:https://domain/A1036 或 device_id=A1036
|
||||
let deviceId = '';
|
||||
|
||||
try {
|
||||
const url = new URL(result);
|
||||
// 取路径最后一段作为设备编号
|
||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||
if (pathParts.length > 0) {
|
||||
deviceId = pathParts[pathParts.length - 1];
|
||||
}
|
||||
} catch {
|
||||
// 非 URL 格式,尝试直接匹配
|
||||
if (/^[A-Z][A-Z0-9]+$/.test(result)) {
|
||||
deviceId = result;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceId) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index?device_id=' + deviceId,
|
||||
});
|
||||
} else {
|
||||
uni.showToast({ title: '无效的二维码', icon: 'none' });
|
||||
}
|
||||
},
|
||||
goMine() {
|
||||
uni.navigateTo({ url: '/pages/index/index?tab=mine' });
|
||||
},
|
||||
callService() {
|
||||
// #ifdef H5
|
||||
window.location.href = 'tel:' + this.servicePhone;
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.makePhoneCall({ phoneNumber: this.servicePhone, fail: () => {} });
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.scan-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 品牌区域 */
|
||||
.brand-area {
|
||||
margin-top: 200rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 96rpx;
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
letter-spacing: 12rpx;
|
||||
}
|
||||
|
||||
/* 扫一扫按钮 */
|
||||
.scan-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 80rpx;
|
||||
}
|
||||
|
||||
.scan-btn {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #2979ff, #1e60e0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 20rpx 60rpx rgba(41, 121, 255, 0.4);
|
||||
}
|
||||
|
||||
/* 二维码图标 */
|
||||
.qr-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.qr-row {
|
||||
display: flex;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.qr-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.qr-block {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background-color: #fff;
|
||||
margin-right: 6rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.qr-space {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
|
||||
.scan-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.scan-tip {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
/* 我的入口 */
|
||||
.mine-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 140rpx;
|
||||
}
|
||||
|
||||
.mine-icon {
|
||||
width: 60rpx;
|
||||
height: 54rpx;
|
||||
position: relative;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.mine-head {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.mine-body {
|
||||
width: 44rpx;
|
||||
height: 26rpx;
|
||||
border-radius: 22rpx 22rpx 0 0;
|
||||
background-color: #999;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.mine-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 底部客服 */
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32rpx 0;
|
||||
padding-bottom: calc(32rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.footer-phone {
|
||||
font-size: 26rpx;
|
||||
color: #2979ff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<!-- 我的内容 -->
|
||||
<view class="page-content">
|
||||
<!-- 蓝色个人信息区域 -->
|
||||
<view class="user-header">
|
||||
<view class="user-info">
|
||||
<!-- 左侧圆形头像 -->
|
||||
<view class="avatar-wrapper">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="userInfo.avatar || '/static/default-avatar.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<!-- 右侧信息 -->
|
||||
<view class="user-detail">
|
||||
<text class="nickname">{{ userInfo.nickname || '未登录' }}</text>
|
||||
<text class="phone">{{ maskedPhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="content-area">
|
||||
<view class="menu-list">
|
||||
<view
|
||||
class="menu-item"
|
||||
v-for="(item, index) in menuList"
|
||||
:key="index"
|
||||
@click="handleMenuClick(item)"
|
||||
>
|
||||
<text class="menu-text">{{ item.title }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<page-nav activeTab="mine" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageNav from '@/components/page-nav/page-nav.vue';
|
||||
import config from '@/config/env.js';
|
||||
import { getCachedUserInfo } from '@/utils/auth-guard.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageNav
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
phone: ''
|
||||
},
|
||||
menuList: [
|
||||
{ title: '我的订单', page: '/pages/order/order' },
|
||||
{ title: '收货地址', page: '/pages/address/address' },
|
||||
{ title: '常见问题', page: '/pages/faq/faq' },
|
||||
{ title: '联系客服', action: 'contact' }
|
||||
]
|
||||
};
|
||||
},
|
||||
onShow() {
|
||||
// 每次显示页面时刷新用户信息
|
||||
this.loadUserInfo();
|
||||
},
|
||||
computed: {
|
||||
maskedPhone() {
|
||||
const phone = this.userInfo.phone;
|
||||
if (!phone || phone.length < 11) return phone;
|
||||
return phone.substring(0, 3) + '****' + phone.substring(7);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadUserInfo() {
|
||||
const cached = getCachedUserInfo();
|
||||
if (cached) {
|
||||
this.userInfo = { ...this.userInfo, ...cached };
|
||||
}
|
||||
},
|
||||
handleMenuClick(item) {
|
||||
if (item.page) {
|
||||
uni.navigateTo({ url: item.page });
|
||||
} else if (item.action === 'contact') {
|
||||
uni.showModal({
|
||||
title: '联系客服',
|
||||
content: '客服电话:' + config.servicePhone,
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
background: linear-gradient(135deg, #2979ff, #1e60e0);
|
||||
padding: 80rpx 40rpx 60rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.8);
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.phone {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 28rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,648 @@
|
||||
<template>
|
||||
<view class="order-page">
|
||||
<!-- 顶部Tab切换 -->
|
||||
<view class="tab-header">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'all' }"
|
||||
@click="switchTab('all')"
|
||||
>
|
||||
<text class="tab-text">全部订单</text>
|
||||
<view class="tab-line" v-if="currentTab === 'all'"></view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'paid' }"
|
||||
@click="switchTab('paid')"
|
||||
>
|
||||
<text class="tab-text">已支付</text>
|
||||
<view class="tab-line" v-if="currentTab === 'paid'"></view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'unpaid' }"
|
||||
@click="switchTab('unpaid')"
|
||||
>
|
||||
<text class="tab-text">未支付</text>
|
||||
<view class="tab-line" v-if="currentTab === 'unpaid'"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数量统计和筛选 -->
|
||||
<view class="filter-bar">
|
||||
<text class="total-count">总数量: {{ filteredOrders.length }}</text>
|
||||
<view class="search-btn" @click="showFilterSidebar = true">
|
||||
<text class="search-icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件显示 -->
|
||||
<view class="filter-condition" v-if="filterStartTime || filterEndTime">
|
||||
<text class="filter-text">{{ filterStartTime }} - {{ filterEndTime }}</text>
|
||||
<text class="filter-clear" @click="clearFilter">✕</text>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<scroll-view class="order-scroll" scroll-y>
|
||||
<view class="order-list">
|
||||
<view
|
||||
class="order-card"
|
||||
v-for="(order, index) in filteredOrders"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 第一行: 日期 + 状态 -->
|
||||
<view class="order-header">
|
||||
<text class="order-date">{{ order.create_time }}</text>
|
||||
<text class="order-status" :class="{ paid: order.status === 'paid', unpaid: order.status === 'unpaid' }">
|
||||
{{ order.status === 'paid' ? '已支付' : '未支付' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 商品列表(整体可点击跳转) -->
|
||||
<view class="product-section" @click="goToDetail(order)">
|
||||
<view class="product-list">
|
||||
<view
|
||||
class="product-item"
|
||||
v-for="(product, pIndex) in order.products"
|
||||
:key="pIndex"
|
||||
>
|
||||
<image class="product-img" :src="product.image" mode="aspectFill" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-price-qty">¥{{ product.price }} x {{ product.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-arrow">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 总计 -->
|
||||
<view class="order-total">
|
||||
<text class="total-qty">共{{ order.totalQuantity }}件</text>
|
||||
<view class="total-amount">
|
||||
<text class="total-label">总计:</text>
|
||||
<text class="total-price">¥{{ order.totalAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="order-action">
|
||||
<view
|
||||
class="action-btn"
|
||||
:class="{ 'pay-btn': order.status === 'unpaid', 'refund-btn': order.status === 'paid' }"
|
||||
@click="handleAction(order)"
|
||||
>
|
||||
{{ order.status === 'unpaid' ? '立即支付' : '申请退款' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="filteredOrders.length === 0">
|
||||
<text class="empty-text">暂无订单</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 筛选侧边栏 -->
|
||||
<view class="filter-sidebar-mask" v-if="showFilterSidebar" @click="showFilterSidebar = false">
|
||||
<view class="filter-sidebar" @click.stop>
|
||||
<view class="sidebar-header">
|
||||
<text class="sidebar-title">筛选条件</text>
|
||||
<text class="sidebar-close" @click="showFilterSidebar = false">✕</text>
|
||||
</view>
|
||||
<view class="sidebar-content">
|
||||
<view class="date-row">
|
||||
<text class="date-label">开始时间</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="tempStartTime"
|
||||
@change="onStartTimeChange"
|
||||
>
|
||||
<view class="date-picker">
|
||||
<text class="date-value">{{ tempStartTime || '请选择' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="date-row">
|
||||
<text class="date-label">结束时间</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="tempEndTime"
|
||||
@change="onEndTimeChange"
|
||||
>
|
||||
<view class="date-picker">
|
||||
<text class="date-value">{{ tempEndTime || '请选择' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sidebar-footer">
|
||||
<view class="sidebar-btn reset-btn" @click="resetFilter">
|
||||
<text class="btn-text">重置</text>
|
||||
</view>
|
||||
<view class="sidebar-btn confirm-btn" @click="confirmFilter">
|
||||
<text class="btn-text">确定</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航条 -->
|
||||
<page-nav-bar
|
||||
:hasPrev="true"
|
||||
:hasNext="false"
|
||||
@prev="handleNavPrev"
|
||||
@next="handleNavNext"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageNavBar from '@/components/page-nav-bar/page-nav-bar.vue';
|
||||
import { checkLoginWithPrompt } from '@/utils/auth-guard.js';
|
||||
|
||||
export default {
|
||||
components: { PageNavBar },
|
||||
data() {
|
||||
return {
|
||||
currentTab: 'all',
|
||||
showFilterSidebar: false,
|
||||
tempStartTime: '',
|
||||
tempEndTime: '',
|
||||
filterStartTime: '',
|
||||
filterEndTime: '',
|
||||
// 模拟订单数据
|
||||
orderList: [
|
||||
{
|
||||
id: 1,
|
||||
create_time: '2026-04-30 10:25:30',
|
||||
status: 'paid',
|
||||
products: [
|
||||
{ name: '君乐宝450ml悦鲜活', price: 0.01, quantity: 2, image: '/static/c1.png' },
|
||||
{ name: '可口可乐330ml', price: 3.00, quantity: 1, image: '/static/c4.png' }
|
||||
],
|
||||
totalQuantity: 3,
|
||||
totalAmount: '0.04'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
create_time: '2026-04-29 15:12:45',
|
||||
status: 'unpaid',
|
||||
products: [
|
||||
{ name: '农夫山泉550ml', price: 2.00, quantity: 3, image: '/static/c5.png' }
|
||||
],
|
||||
totalQuantity: 3,
|
||||
totalAmount: '6.00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
create_time: '2026-04-28 09:30:00',
|
||||
status: 'paid',
|
||||
products: [
|
||||
{ name: '乐事薯片原味75g', price: 7.50, quantity: 1, image: '/static/c6.png' },
|
||||
{ name: '白桔(0.0.0.0-5.5.5)', price: 0.05, quantity: 5, image: '/static/c3.png' }
|
||||
],
|
||||
totalQuantity: 6,
|
||||
totalAmount: '1.00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
create_time: '2026-04-27 18:45:20',
|
||||
status: 'unpaid',
|
||||
products: [
|
||||
{ name: '白红(0.0.0.0-9.9.9)', price: 0.06, quantity: 10, image: '/static/c2.png' }
|
||||
],
|
||||
totalQuantity: 10,
|
||||
totalAmount: '0.60'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
create_time: '2026-04-25 12:00:00',
|
||||
status: 'paid',
|
||||
products: [
|
||||
{ name: '君乐宝450ml悦鲜活', price: 0.01, quantity: 1, image: '/static/c1.png' }
|
||||
],
|
||||
totalQuantity: 1,
|
||||
totalAmount: '0.01'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredOrders() {
|
||||
let list = this.orderList;
|
||||
|
||||
// Tab筛选
|
||||
if (this.currentTab === 'paid') {
|
||||
list = list.filter(o => o.status === 'paid');
|
||||
} else if (this.currentTab === 'unpaid') {
|
||||
list = list.filter(o => o.status === 'unpaid');
|
||||
}
|
||||
|
||||
// 日期筛选
|
||||
if (this.filterStartTime) {
|
||||
list = list.filter(o => o.create_time >= this.filterStartTime);
|
||||
}
|
||||
if (this.filterEndTime) {
|
||||
list = list.filter(o => o.create_time.split(' ')[0] <= this.filterEndTime);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 进入订单页需要登录
|
||||
if (!checkLoginWithPrompt('order')) return;
|
||||
},
|
||||
methods: {
|
||||
switchTab(tab) {
|
||||
this.currentTab = tab;
|
||||
},
|
||||
onStartTimeChange(e) {
|
||||
this.tempStartTime = e.detail.value;
|
||||
},
|
||||
onEndTimeChange(e) {
|
||||
this.tempEndTime = e.detail.value;
|
||||
},
|
||||
resetFilter() {
|
||||
this.tempStartTime = '';
|
||||
this.tempEndTime = '';
|
||||
},
|
||||
confirmFilter() {
|
||||
this.filterStartTime = this.tempStartTime;
|
||||
this.filterEndTime = this.tempEndTime;
|
||||
this.showFilterSidebar = false;
|
||||
},
|
||||
clearFilter() {
|
||||
this.filterStartTime = '';
|
||||
this.filterEndTime = '';
|
||||
},
|
||||
goToDetail(order) {
|
||||
uni.showToast({ title: '订单详情开发中', icon: 'none' });
|
||||
},
|
||||
handleAction(order) {
|
||||
if (order.status === 'unpaid') {
|
||||
// 支付需要登录
|
||||
if (!checkLoginWithPrompt('pay')) return;
|
||||
uni.showModal({
|
||||
title: '立即支付',
|
||||
content: `确认支付¥${order.totalAmount}?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showModal({
|
||||
title: '申请退款',
|
||||
content: '确认申请退款?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '退款申请已提交', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
handleNavPrev() {
|
||||
uni.navigateBack({ fail: () => { uni.switchTab({ url: '/pages/index/index' }); } });
|
||||
},
|
||||
handleNavNext() {
|
||||
uni.showToast({ title: '暂无下一页', icon: 'none' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
page { height: 100%; background-color: #f5f5f5; }
|
||||
|
||||
.order-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 顶部Tab */
|
||||
.tab-header {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 28rpx 0 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
.tab-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
.tab-item.active .tab-text {
|
||||
color: #2979ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 6rpx;
|
||||
background: #2979ff;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 28rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.total-count {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
.search-btn {
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
.search-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
/* 筛选条件显示 */
|
||||
.filter-condition {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx;
|
||||
background: #e8f4ff;
|
||||
}
|
||||
.filter-text {
|
||||
font-size: 26rpx;
|
||||
color: #2979ff;
|
||||
flex: 1;
|
||||
}
|
||||
.filter-clear {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
/* 订单列表 */
|
||||
.order-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
.order-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
/* 订单头部 */
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.order-date {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
.order-status {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.order-status.paid {
|
||||
color: #19be6b;
|
||||
}
|
||||
.order-status.unpaid {
|
||||
color: #e4393c;
|
||||
}
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background: #f0f0f0;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.product-section {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.product-list {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.product-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
.product-img {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.product-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.product-price-qty {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
.product-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16rpx 0 20rpx;
|
||||
}
|
||||
.arrow-icon {
|
||||
font-size: 52rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* 总计 */
|
||||
.order-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.total-qty {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
.total-amount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.total-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
.total-price {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #e4393c;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.order-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.pay-btn {
|
||||
background: linear-gradient(135deg, #2979ff, #1e60e0);
|
||||
color: #fff;
|
||||
}
|
||||
.refund-btn {
|
||||
background: #fff;
|
||||
border: 2rpx solid #2979ff;
|
||||
color: #2979ff;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 筛选侧边栏 */
|
||||
.filter-sidebar-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
}
|
||||
.filter-sidebar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 600rpx;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 32rpx 28rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.sidebar-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.sidebar-close {
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
padding: 8rpx;
|
||||
}
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
padding: 28rpx;
|
||||
}
|
||||
.date-row {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.date-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
.date-picker {
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
.date-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
.sidebar-footer {
|
||||
display: flex;
|
||||
padding: 28rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.sidebar-btn {
|
||||
flex: 1;
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
border-radius: 8rpx;
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
.reset-btn {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.confirm-btn {
|
||||
background: #2979ff;
|
||||
}
|
||||
.reset-btn .btn-text {
|
||||
color: #666;
|
||||
}
|
||||
.confirm-btn .btn-text {
|
||||
color: #fff;
|
||||
}
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 底部占位 */
|
||||
.bottom-spacer {
|
||||
height: 120rpx;
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 470 B |
|
After Width: | Height: | Size: 511 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 365 B |
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 565 B |
@@ -0,0 +1,20 @@
|
||||
@font-face {
|
||||
font-family: "customicons"; /* Project id 2878519 */
|
||||
src:url('/static/customicons.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.customicons {
|
||||
font-family: "customicons" !important;
|
||||
}
|
||||
|
||||
.youxi:before {
|
||||
content: "\e60e";
|
||||
}
|
||||
|
||||
.wenjian:before {
|
||||
content: "\e60f";
|
||||
}
|
||||
|
||||
.zhuanfa:before {
|
||||
content: "\e610";
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1 @@
|
||||
@import '@/uni_modules/uni-scss/variables.scss';
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 用户授权工具
|
||||
* 登录检查、授权跳转、回调处理、绑定手机号
|
||||
*
|
||||
* 授权相关接口(auth/url、auth/callback、sms/send、bindPhone)走网关 apiGatewayUrl
|
||||
* 业务接口(商品列表等)走 apiBaseUrl
|
||||
*/
|
||||
import config from '@/config/env.js';
|
||||
import { gatewayGet, gatewayPost } from './request.js';
|
||||
|
||||
// 场景提示文案
|
||||
const SCENE_TIPS = {
|
||||
order: '登录后即可查看订单',
|
||||
mine: '登录后即可查看个人信息',
|
||||
pay: '登录后即可下单购买',
|
||||
bindPhone: '需要绑定手机号完成登录',
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLoggedIn() {
|
||||
return !!uni.getStorageSync('token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,未登录时弹窗引导授权
|
||||
* @param {string} scene - 触发场景:'order' | 'mine' | 'pay' | 'bindPhone'
|
||||
* @returns {boolean} 是否已登录
|
||||
*/
|
||||
export function checkLoginWithPrompt(scene = '') {
|
||||
if (isLoggedIn()) return true;
|
||||
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: SCENE_TIPS[scene] || '需要登录后操作',
|
||||
confirmText: '去登录',
|
||||
cancelText: '暂不',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
startAuth();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,未登录时静默跳转授权(不弹窗)
|
||||
* @returns {boolean} 是否已登录
|
||||
*/
|
||||
export function requireLogin() {
|
||||
if (isLoggedIn()) return true;
|
||||
startAuth();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起授权跳转
|
||||
* 获取 auth_url 后跳转到微信/支付宝授权页
|
||||
*/
|
||||
export async function startAuth() {
|
||||
const appNo = uni.getStorageSync('app_no') || '';
|
||||
|
||||
if (!appNo) {
|
||||
uni.showToast({ title: '当前环境不支持授权', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await gatewayGet('/api/v1/user/auth/url', {
|
||||
app_no: appNo,
|
||||
scopes: '',
|
||||
redirect_uri: config.authRedirectUri || '',
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
if (data.code === 0 && data.data && data.data.auth_url) {
|
||||
// #ifdef H5
|
||||
window.location.href = data.data.auth_url;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// 小程序环境走其他授权方式
|
||||
uni.setStorageSync('pending_auth', true);
|
||||
// #endif
|
||||
} else {
|
||||
uni.showToast({ title: data.message || '获取授权地址失败', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Auth] startAuth error:', e);
|
||||
uni.showToast({ title: '网络请求失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理授权回调
|
||||
* 解析 URL 中的 code/auth_code 参数,调用后端回调接口
|
||||
* @returns {Promise<Object>} 回调结果
|
||||
*/
|
||||
export async function handleAuthCallback() {
|
||||
// #ifdef H5
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code') || urlParams.get('auth_code') || '';
|
||||
const appNo = urlParams.get('state') || uni.getStorageSync('app_no') || '';
|
||||
|
||||
if (!code) return null;
|
||||
|
||||
try {
|
||||
const res = await gatewayGet('/api/v1/user/auth/callback', {
|
||||
app_no: appNo,
|
||||
code,
|
||||
state: appNo,
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
if (data.code === 0 && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
return null;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权成功后处理
|
||||
* 缴存 token 和用户信息
|
||||
* @param {Object} authResult - 后端返回的授权结果
|
||||
*/
|
||||
export function onAuthSuccess(authResult) {
|
||||
if (!authResult) return;
|
||||
|
||||
const { token, user_info, auth_info } = authResult;
|
||||
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token);
|
||||
}
|
||||
|
||||
if (user_info) {
|
||||
uni.setStorageSync('user_info', user_info);
|
||||
}
|
||||
|
||||
if (auth_info) {
|
||||
uni.setStorageSync('auth_info', auth_info);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} tempToken - 临时 token
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function sendSmsCode(phone, tempToken) {
|
||||
const res = await gatewayPost('/api/v1/user/sms/send', {
|
||||
phone,
|
||||
temp_token: tempToken,
|
||||
purpose: 'bindPhone',
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
return data.code === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* @param {string} tempToken - 临时 token
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} code - 验证码
|
||||
* @returns {Promise<Object>} 绑定结果(含 token 和 user_info)
|
||||
*/
|
||||
export async function bindPhone(tempToken, phone, code) {
|
||||
const res = await gatewayPost('/api/v1/user/bindPhone', {
|
||||
temp_token: tempToken,
|
||||
phone,
|
||||
code,
|
||||
});
|
||||
|
||||
const data = res.data || {};
|
||||
if (data.code === 0 && data.data) {
|
||||
// 绑定成功,缓存 token 和用户信息
|
||||
onAuthSuccess(data.data);
|
||||
uni.removeStorageSync('temp_token');
|
||||
return data.data;
|
||||
}
|
||||
|
||||
uni.showToast({ title: data.message || '绑定失败', icon: 'none' });
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export function logout() {
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('user_info');
|
||||
uni.removeStorageSync('auth_info');
|
||||
uni.removeStorageSync('temp_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的用户信息
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getCachedUserInfo() {
|
||||
try {
|
||||
return uni.getStorageSync('user_info') || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示绑定手机号弹窗
|
||||
* @param {string} tempToken - 临时 token
|
||||
* @param {Function} onSuccess - 绑定成功回调
|
||||
*/
|
||||
export function showBindPhoneDialog(tempToken, onSuccess) {
|
||||
let phone = '';
|
||||
let code = '';
|
||||
let countdown = 0;
|
||||
let timer = null;
|
||||
|
||||
// 使用 uni.showModal 不够灵活,这里用自定义弹窗
|
||||
// 简化实现:先弹出手机号输入
|
||||
uni.showModal({
|
||||
title: '绑定手机号',
|
||||
content: '请先绑定手机号完成注册',
|
||||
confirmText: '去绑定',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到绑定手机号页面(预留)
|
||||
// 后续可实现专门的绑定手机号页面
|
||||
uni.showToast({ title: '绑定手机号功能开发中', icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 环境检测工具
|
||||
* 检测当前运行平台(微信/支付宝/普通浏览器),映射到对应的 app_no
|
||||
*/
|
||||
|
||||
// app_no 编码表:platform + terminalType → app_no
|
||||
const APP_NO_MAP = {
|
||||
'wechat_h5': '60000001102001',
|
||||
'wechat_mp': '600102101',
|
||||
'alipay_h5': '600112001',
|
||||
'alipay_mp': '600112101',
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测当前运行环境
|
||||
* @returns {'wechat' | 'alipay' | 'browser'}
|
||||
*/
|
||||
export function detectPlatform() {
|
||||
// #ifdef H5
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
if (/MicroMessenger/i.test(ua)) {
|
||||
return 'wechat';
|
||||
}
|
||||
if (/AlipayClient/i.test(ua)) {
|
||||
return 'alipay';
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
return 'wechat';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
return 'alipay';
|
||||
// #endif
|
||||
|
||||
return 'browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前终端类型
|
||||
* @returns {'h5' | 'mp'}
|
||||
*/
|
||||
export function detectTerminalType() {
|
||||
// #ifdef H5
|
||||
return 'h5';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
return 'mp';
|
||||
// #endif
|
||||
|
||||
return 'h5';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据平台和终端类型获取 app_no
|
||||
* @param {string} platform - 'wechat' | 'alipay' | 'browser'
|
||||
* @param {string} terminalType - 'h5' | 'mp'
|
||||
* @returns {string} app_no
|
||||
*/
|
||||
export function getAppNo(platform, terminalType = 'h5') {
|
||||
if (platform === 'browser') return '';
|
||||
return APP_NO_MAP[`${platform}_${terminalType}`] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化环境检测并缓存到 storage
|
||||
* 应在 App.vue onLaunch 中调用
|
||||
*/
|
||||
export function initEnv() {
|
||||
const platform = detectPlatform();
|
||||
const terminalType = detectTerminalType();
|
||||
const appNo = getAppNo(platform, terminalType);
|
||||
|
||||
uni.setStorageSync('platform', platform);
|
||||
uni.setStorageSync('terminal_type', terminalType);
|
||||
if (appNo) {
|
||||
uni.setStorageSync('app_no', appNo);
|
||||
}
|
||||
|
||||
return { platform, terminalType, appNo };
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 统一请求封装
|
||||
* 自动注入 Authorization header,处理 401
|
||||
* app_no 通过接口参数传递,不在 header 中
|
||||
*/
|
||||
import config from '@/config/env.js';
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {Object} options - uni.request 选项
|
||||
* @param {string} options.url - 请求地址(可以是相对路径,会拼接 baseUrl)
|
||||
* @param {string} [options.method='GET'] - 请求方法
|
||||
* @param {Object} [options.data] - 请求参数
|
||||
* @param {Object} [options.header] - 额外请求头
|
||||
* @param {string} [options.baseUrl] - 基础地址,默认 apiBaseUrl;传 'gateway' 使用 apiGatewayUrl
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
export function request(options = {}) {
|
||||
const token = uni.getStorageSync('token') || '';
|
||||
|
||||
// 确定 baseUrl
|
||||
let baseUrl = config.apiBaseUrl;
|
||||
if (options.baseUrl === 'gateway') {
|
||||
baseUrl = config.apiGatewayUrl;
|
||||
} else if (options.baseUrl) {
|
||||
baseUrl = options.baseUrl;
|
||||
}
|
||||
|
||||
// 拼接完整 URL
|
||||
let url = options.url || '';
|
||||
if (url && !url.startsWith('http')) {
|
||||
url = baseUrl + url;
|
||||
}
|
||||
|
||||
// 构建 header(app_no 不在 header 中传,通过参数传递)
|
||||
const header = {
|
||||
...options.header,
|
||||
};
|
||||
|
||||
// 公开接口(授权相关)不带 Authorization,避免 CORS 预检和 JWT 拦截
|
||||
const publicPaths = ['/api/v1/user/auth/url', '/api/v1/user/auth/callback'];
|
||||
const isPublic = publicPaths.some((p) => url.includes(p));
|
||||
|
||||
if (token && !isPublic) {
|
||||
header['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 开发环境请求日志
|
||||
if (config.enableRequestLog) {
|
||||
console.log(`[API] ${options.method || 'GET'} ${url}`, options.data || '');
|
||||
console.log('[API] Headers:', header);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header,
|
||||
timeout: config.requestTimeout,
|
||||
success: (res) => {
|
||||
if (config.enableRequestLog) {
|
||||
console.log('[API] StatusCode:', res.statusCode);
|
||||
console.log('[API] Response Headers:', res.header || res.headers);
|
||||
console.log('[API] Response Body:', res.data);
|
||||
}
|
||||
|
||||
// 401 → 清除登录状态
|
||||
if (res.statusCode === 401) {
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('user_info');
|
||||
uni.removeStorageSync('temp_token');
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
if (config.enableRequestLog) {
|
||||
console.error('[API] Error:', err);
|
||||
}
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求(默认走业务 API)
|
||||
*/
|
||||
export function get(url, data, options = {}) {
|
||||
return request({ url, method: 'GET', data, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求(默认走业务 API)
|
||||
*/
|
||||
export function post(url, data, options = {}) {
|
||||
return request({ url, method: 'POST', data, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求(走网关)
|
||||
*/
|
||||
export function gatewayGet(url, data, options = {}) {
|
||||
return request({ url, method: 'GET', data, baseUrl: 'gateway', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求(走网关)
|
||||
*/
|
||||
export function gatewayPost(url, data, options = {}) {
|
||||
return request({ url, method: 'POST', data, baseUrl: 'gateway', ...options });
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* vConsole 调试工具管理
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. URL 参数:?vconsole=1 开启,?vconsole=0 关闭
|
||||
* 2. localStorage:设置 'vconsole' 为 '1' 开启,'0' 关闭
|
||||
* 3. 代码调用:import { showVConsole, hideVConsole, toggleVConsole } from '@/utils/vconsole'
|
||||
*/
|
||||
|
||||
let vConsoleInstance = null;
|
||||
|
||||
// 检测是否为开发环境(兼容多种方式)
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
/**
|
||||
* 获取 vConsole 是否开启
|
||||
*/
|
||||
export function isVConsoleEnabled() {
|
||||
if (!isDev) return false;
|
||||
|
||||
// 优先从 URL 参数获取(H5 环境直接读 window.location)
|
||||
// #ifdef H5
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const vconsoleParam = urlParams.get('vconsole');
|
||||
if (vconsoleParam !== null) {
|
||||
return vconsoleParam === '1' || vconsoleParam === 'true';
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.$page?.options || currentPage.options || {};
|
||||
if (options.vconsole !== undefined) {
|
||||
return options.vconsole === '1' || options.vconsole === 'true';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 其次从 localStorage 获取
|
||||
try {
|
||||
return uni.getStorageSync('vconsole') === '1';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启 vConsole
|
||||
*/
|
||||
export function showVConsole() {
|
||||
if (vConsoleInstance) return;
|
||||
|
||||
// 动态加载 vConsole
|
||||
import('vconsole').then((module) => {
|
||||
const VConsole = module.default || module;
|
||||
vConsoleInstance = new VConsole({
|
||||
theme: 'dark',
|
||||
disableLogScrolling: false,
|
||||
buttonSize: 48,
|
||||
});
|
||||
|
||||
// 增强 vConsole 按钮样式,确保不被遮挡
|
||||
setTimeout(() => {
|
||||
const btn = document.querySelector('#__vconsole');
|
||||
if (btn) {
|
||||
btn.style.zIndex = '999999';
|
||||
btn.style.position = 'fixed';
|
||||
btn.style.bottom = '80px';
|
||||
btn.style.right = '10px';
|
||||
}
|
||||
}, 500);
|
||||
|
||||
uni.setStorageSync('vconsole', '1');
|
||||
}).catch((err) => {
|
||||
console.error('[vConsole] 加载失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 vConsole
|
||||
*/
|
||||
export function hideVConsole() {
|
||||
if (vConsoleInstance) {
|
||||
vConsoleInstance.destroy();
|
||||
vConsoleInstance = null;
|
||||
}
|
||||
try {
|
||||
uni.setStorageSync('vconsole', '0');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 vConsole 状态
|
||||
*/
|
||||
export function toggleVConsole() {
|
||||
if (vConsoleInstance) {
|
||||
hideVConsole();
|
||||
} else {
|
||||
showVConsole();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置初始化 vConsole
|
||||
*/
|
||||
export function initVConsole() {
|
||||
if (!isDev) return;
|
||||
|
||||
if (isVConsoleEnabled()) {
|
||||
showVConsole();
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
isVConsoleEnabled,
|
||||
showVConsole,
|
||||
hideVConsole,
|
||||
toggleVConsole,
|
||||
initVConsole,
|
||||
};
|
||||