修改组件和接口返回信息

This commit is contained in:
wangxiaowei
2025-11-05 16:28:25 +08:00
parent 9bfdf0b03e
commit 29bf4dae74
15 changed files with 537 additions and 54 deletions

15
env/.env vendored
View File

@ -10,13 +10,22 @@ VITE_APP_PUBLIC_BASE=/
# 登录页面
VITE_LOGIN_URL = '/pages/login/login'
# 第一个请求地址
VITE_SERVER_BASEURL = 'https://mnp.zhuzhuda.cn'
VITE_SERVER_BASEURL = 'https://cz.stnav.com'
VITE_UPLOAD_BASEURL = 'https://mnp.zhuzhuda.cn/upload'
VITE_UPLOAD_BASEURL = 'https://cz.stnav.com/upload'
# h5是否需要配置代理
VITE_APP_PROXY=false
VITE_APP_PROXY_PREFIX = '/api'
# 第二个请求地址 (目前alova中可以使用)
VITE_API_SECONDARY_URL = 'https://mnp.zhuzhuda.cn'
VITE_SERVER_BASEURL = 'https://cz.stnav.com'
# 默认地址
VITE_DEFAULT_LONGITUDE = 113.665412
VITE_DEFAULT_LATITUDE = 34.757975
VITE_DEFAULT_ADDRESS = '上海市'
VITE_DEFAULT_LOCATION_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000 # 30天
VITE_DEFAULT_LOCATION_EXPIRE_KEY = 'location_expire_time'
VITE_DEFAULT_LOCATION_DENY_TIME_KEY = 'location_deny_time'
VITE_DEFAULT_LOCATION_DENY_INTERVAL = 60 * 60 * 1000 # 1小时

View File

@ -124,12 +124,18 @@ export default defineManifestConfig({
es6: true,
minified: true,
},
requiredPrivateInfos: ["getLocation" ],
optimization: {
subPackages: true,
},
// styleIsolation: 'shared',
usingComponents: true,
// __usePrivacyCheck__: true,
permission: {
'scope.userLocation' : {
desc : "我们需要获取您的位置,以方便推荐附近茶室给您"
}
},
},
'mp-alipay': {
usingComponents: true,

View File

@ -3,9 +3,14 @@
*/
export interface IUserInfoVo {
id: number
username: string
nickname: string
avatar: string
token: string
sn: string
account: string
channel: number
is_new_user: number
mobile: string
}
/**

125
src/components/Pay.vue Normal file
View File

@ -0,0 +1,125 @@
<template>
<view class="pay-radio">
<wd-radio-group v-model="pay" shape="dot" checked-color="#4C9F44" @change="Pay.handleChangePay">
<block v-for="(item, index) in PayList" :key="index">
<wd-radio :value="item.value">
<view
class="flex justify-between items-center"
v-if="!(hidePlatformBalance && item.type === PayCategory.PlatformBalance) && !(hideStoreBalance && item.type === PayCategory.StoreBalance) && !(hideWechat && item.type === PayCategory.WeChatPay)"
>
<view class="flex items-center">
<wd-img width="50rpx" height="50rpx" :src="`${OSS}${item.icon}`"></wd-img>
<view class="ml-20rpx text-30rpx text-[#303133] leading-42rpx">{{ item.name }}</view>
</view>
</view>
<view class="absolute right-0 top-6rpx right-60rpx" v-if="item.type !== PayCategory.WeChatPay">可用{{ userInfo.user_money }}</view>
</wd-radio>
</block>
</wd-radio-group>
</view>
</template>
<script lang="ts" setup name="RechargeBtn">
/**
* Pay 支付组件
* @description 用于展示支付
*/
import { PayList as OriginPayList, PayCategory, PayValue } from '@/utils/pay'
import { IUserInfoResult } from '@/api/types/user'
import { getUserInfo } from '@/api/user'
const OSS = inject('OSS')
const pay = ref<number>() // 支付方式
// 当前用户信息
const userInfo = reactive<IUserInfoResult>({
account: '',
avatar: '',
create_time: '',
has_auth: false,
has_password: false,
id: 0,
mobile: '',
nickname: '',
real_name: '',
sex: '',
sn: 0,
user_money: '',
version: 0
})
onMounted(async () => {
// 获取个人用户信息
const userRes = await getUserInfo()
Object.assign(userInfo, userRes || {})
})
const props = defineProps({
// 是否隐藏平台余额支付
hidePlatformBalance: {
type: Boolean,
default: false
},
// 是否隐藏门店余额支付
hideStoreBalance: {
type: Boolean,
default: false
},
// 是否隐藏微信支付
hideWechat: {
type: Boolean,
default: false
}
})
// 定义emit事件
const emit = defineEmits(['pay'])
const Pay = {
// 支付方式改变
handleChangePay(e: {value: number}) {
emit('pay', e.value)
}
}
const PayList = computed(() => {
return OriginPayList.filter(item => {
if (props.hidePlatformBalance && item.type === PayCategory.PlatformBalance) return false
if (props.hideStoreBalance && item.type === PayCategory.StoreBalance) return false
if (props.hideWechat && item.type === PayCategory.WeChatPay) return false
return true
})
})
watch(PayList, (list) => {
if (list.length > 0) {
pay.value = list[0].value
emit('pay', pay.value )
} else {
pay.value = undefined
}
}, { immediate: true })
</script>
<script lang="ts">
export default {}
</script>
<style lang="scss">
.pay-radio {
:deep() {
.wd-radio {
position: relative;
margin-bottom: 40rpx;
}
.wd-radio:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<view class="w-168rpx h-40rpx relative mr-44rpx">
<view class="absolute left-0 top-0 h-36rpx flex items-start">
<!-- 金牌茶艺师 -->
<wd-img :src="levelMap[level].icon" width="36rpx" height="36rpx"></wd-img>
</view>
<view class="bg-[#F0F6EF] text-[#006C2D] font-400 text-22rpx leading-32rpx rounded-4rpx text-center w-150rpx ml-18rpx pb-4rpx">{{ levelMap[level].text }}</view>
</view>
</template>
<script lang="ts" setup name="TeaSpecialistLevel">
/**
* TeaSpecialistLevel 茶艺师等级
* @description 用于展示茶艺师等级
*/
const OSS = inject('OSS')
defineProps({
level: {
type: String,
default: ''
}
})
// 茶艺师等级对应的icon和文字
const levelMap = {
gold: {
icon: `${OSS}icon/icon_gold_medal.png`,
text: '金牌茶艺师'
},
senior: {
icon: `${OSS}icon/icon_senior_medal.png`,
text: '高级茶艺师'
},
intermediate: {
icon: `${OSS}icon/icon_intermediate_medal.png`,
text: '中级茶艺师'
},
junior: {
icon: `${OSS}icon/icon_junior_medal.png`,
text: '初级茶艺师'
},
enthusiast: {
icon: `${OSS}icon/icon_enthusiast_medal.png`,
text: '茶艺爱好者'
}
}
</script>
<script lang="ts">
export default {}
</script>

82
src/hooks/useLocation.ts Normal file
View File

@ -0,0 +1,82 @@
import { router } from '@/utils/tools'
const LOCATION_EXPIRE_KEY = import.meta.env.VITE_DEFAULT_LOCATION_EXPIRE_KEY // 定位缓存KEY
const LOCATION_EXPIRE_MS = import.meta.env.VITE_DEFAULT_LOCATION_EXPIRE_MS // 30天
const LOCATION_DEFAULT_CITY = import.meta.env.VITE_DEFAULT_ADDRESS // 默认城市
const LOCATION_DEFAULT_LAT = import.meta.env.VITE_DEFAULT_LATITUDE // 上海经度
const LOCATION_DEFAULT_LNG = import.meta.env.VITE_DEFAULT_LONGITUDE // 上海纬度
const LOCATION_DENY_TIME_KEY = import.meta.env.VITE_DEFAULT_LOCATION_DENY_TIME_KEY
const LOCATION_DENY_INTERVAL = import.meta.env.VITE_DEFAULT_LOCATION_DENY_INTERVAL
// 检查过期时间
export function handleCheckLocationCacheHooks() {
const expire = uni.getStorageSync(LOCATION_EXPIRE_KEY)
if (expire && Date.now() > expire) {
uni.removeStorageSync('latitude')
uni.removeStorageSync('longitude')
uni.removeStorageSync(LOCATION_EXPIRE_KEY)
return false
}
return true
}
// 设置经纬度缓存
export function handleSetLocationCacheHooks(lat: number, lng: number) {
uni.setStorageSync('latitude', lat)
uni.setStorageSync('longitude', lng)
uni.setStorageSync(LOCATION_EXPIRE_KEY, Date.now() + LOCATION_EXPIRE_MS)
}
// 初始化经纬度
export async function handleEnsureLocationAuthHooks() {
// 1. 检查缓存
if (handleCheckLocationCacheHooks()) {
const lat = uni.getStorageSync('latitude')
const lng = uni.getStorageSync('longitude')
if (lat && lng) return { lat, lng }
}
// 2. 获取定位
return new Promise<{ lat: number, lng: number }>((resolve) => {
uni.authorize({
scope: 'scope.userLocation',
success() {
uni.getLocation({
type: 'gcj02',
success(res) {
handleSetLocationCacheHooks(res.latitude, res.longitude)
resolve({ lat: res.latitude, lng: res.longitude })
},
fail() {
// 定位失败,返回默认上海
resolve({ lat: LOCATION_DEFAULT_LAT, lng: LOCATION_DEFAULT_LNG })
}
})
},
fail() {
// 用户拒绝授权
if (shouldShowAuthModal()) {
uni.setStorageSync(LOCATION_DENY_TIME_KEY, Date.now())
uni.showModal({
title: '提示',
content: '需要获取您的地理位置,请授权定位服务',
showCancel: false,
success: () => {
// 可引导用户去设置页面
uni.openSetting({})
}
})
}
// 返回默认上海
resolve({ lat: LOCATION_DEFAULT_LAT, lng: LOCATION_DEFAULT_LNG })
}
})
})
}
// 检查是否需要弹授权框
function shouldShowAuthModal() {
const lastDeny = uni.getStorageSync(LOCATION_DENY_TIME_KEY)
return !lastDeny || (Date.now() - lastDeny > LOCATION_DENY_INTERVAL)
}

View File

@ -6,6 +6,8 @@ import { createServerTokenAuthentication } from 'alova/client'
import VueHook from 'alova/vue'
import { toast } from '@/utils/toast'
import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
import { useUserStore } from '@/store'
import { router } from '@/utils/tools'
// 配置动态Tag
export const API_DOMAINS = {
@ -59,11 +61,22 @@ const alovaInstance = createAlova({
console.log('ignoreAuth===>', ignoreAuth)
// 处理认证信息 自行处理认证问题
if (ignoreAuth) {
const token = 'getToken()'
// const token = 'getToken()'
// if (!token) {
// throw new Error('[请求错误]:未登录')
// }
// method.config.headers.token = token;
const userStore = useUserStore()
const { token } = userStore.userInfo as unknown as IUserInfo
if (!token) {
toast.info('请先登录')
router.switchTab('/pages/my/my', 500)
throw new Error('[请求错误]:未登录')
}
// method.config.headers.token = token;
method.config.headers.token = token;
}
// 处理动态域名
@ -96,13 +109,29 @@ const alovaInstance = createAlova({
// }
// 处理业务逻辑错误
const { code, message, data } = rawData as IResponse
// if (code !== ResultEnum.Success) {
// if (config.meta?.toast !== false) {
// toast.warning(message)
// }
// throw new Error(`请求错误[${code}]${message}`)
// }
const { code, msg, data } = rawData as IResponse
if (code === ResultEnum.Unauthorized) {
useUserStore().removeUserInfo()
if (config.meta?.toast !== false) {
toast.info(msg)
router.switchTab('/pages/my/my', 1000)
}
throw new Error(`登录超时[${code}]${msg}`)
}
if (code === ResultEnum.Success) {
if (config.meta?.toast !== false && msg) {
if (msg !== '查询成功') toast.info(msg)
}
}
if (code !== ResultEnum.Success) {
if (config.meta?.toast !== false) {
toast.warning(msg)
}
throw new Error(`请求错误[${code}]${msg}`)
}
// 处理成功响应,返回业务数据
return data
}),

View File

@ -11,7 +11,7 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
export interface IResponse<T = any> {
code: number | string
data: T
message: string
msg: string
status: string | number
}

View File

@ -90,8 +90,16 @@
"minified": true
},
"usingComponents": true,
"requiredPrivateInfos": [
"getLocation"
],
"optimization": {
"subPackages": true
},
"permission": {
"scope.userLocation": {
"desc": "我们需要获取您的位置,以方便推荐附近工厂给您"
}
}
},
"mp-alipay": {

View File

@ -13,13 +13,13 @@
<view class="home-bg w-[100%] fixed top-0 left-0 z-100">
<wd-navbar safeAreaInsetTop :bordered="false" custom-style="background-color: transparent !important;">
<template #left>
<view class="flex items-center line-1 w-130rpx" @click="home.toCity">
<view class="flex items-center line-1 w-130rpx" @click="Index.toCity">
<view class="mr-10rpx font-400 leading-44rpx text-32rpx pl-10rpx line-1">上海市</view>
<wd-img width="14rpx" height="9rpx" :src="`${OSS}icon/icon_arrow_down.png`" />
</view>
</template>
<template #title>
<view class="search-box flex items-center ml-26rpx" @click="home.toSearch">
<view class="search-box flex items-center ml-26rpx" @click="Index.toSearch">
<wd-search placeholder="搜索茶址名称" hide-cancel disabled :placeholder-left="true"
placeholderStyle="text-align:left;padding-left: 24rpx;line-heigt: 44rpx;color: #C9C9C9; font-size: 32rpx;font-weight: normal;">
</wd-search>
@ -32,7 +32,7 @@
<view class="mt-32rpx mx-30rpx">
<wd-swiper value-key="image" height="240rpx" indicatorPosition="bottom-left"
:indicator="{ type: 'dots-bar' }" :list="swiperList" v-model:current="current"
@click="home.handleClick" @change="home.onChange" mode="aspectFit"></wd-swiper>
@click="Index.handleClick" @change="Index.onChange" mode="aspectFit"></wd-swiper>
</view>
<view class="mt-40rpx flex items-center h-36rpx mx-30rpx">
@ -56,9 +56,9 @@
</view>
<view>
<mescroll-body @init="mescrollInit" @down="downCallback" @up="home.upCallback" top="28rpx"
<mescroll-body @init="mescrollInit" @down="downCallback" @up="Index.upCallback" top="28rpx"
:fixed="true">
<view class="relative p-20rp mb-24rpx" v-for="(item, index) in 100" :key="index" @click="home.handleToReserveRoom(item)">
<view class="relative p-20rp mb-24rpx" v-for="(item, index) in 100" :key="index" @click="Index.handleToReserveRoom(item)">
<view class="absolute top--28rpx left-0 z-1">
<wd-img width="110rpx" height="110rpx" :src="`${OSS}images/home/home_image4.png`"/>
</view>
@ -108,7 +108,8 @@
<script lang="ts" setup>
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import useMescroll from "@/uni_modules/mescroll-uni/hooks/useMescroll.js";
import useMescroll from "@/uni_modules/mescroll-uni/hooks/useMescroll.js"
import { handleEnsureLocationAuthHooks } from '@/hooks/useLocation'
const OSS = inject('OSS')
const navbarHeight = inject('navbarHeight')
@ -120,14 +121,18 @@
`${OSS}images/banner1.png`
])
const current = ref<number>(0)
/** 结束 **/
// 分页
const { mescrollInit, downCallback } = useMescroll(onPageScroll, onReachBottom) // 调用mescroll的hook
onLoad(() => {
onLoad(async() => {
// 获取用户经纬度(带缓存和授权逻辑)
const { lat, lng } = await handleEnsureLocationAuthHooks()
// 你可以在这里根据经纬度做后续处理,比如请求附近门店等
console.log('当前定位:', lat, lng)
})
const home = {
const Index = {
toCity: () => {
uni.navigateTo({
url: '/pages/city/city'

View File

@ -10,12 +10,19 @@ import {
} from '@/api/login'
import { toast } from '@/utils/toast'
const defaultAvatar = 'https://shchazhi.oss-cn-hangzhou.aliyuncs.com/fronted/icon/icon_avatar.png'
// 初始化状态
const userInfoState: IUserInfoVo = {
id: 0,
username: '',
avatar: '/static/images/default-avatar.png',
nickname: '',
avatar: defaultAvatar,
token: '',
sn: '',
account: '',
channel: 0,
is_new_user: 1,
mobile: ''
}
export const useUserStore = defineStore(
@ -31,7 +38,7 @@ export const useUserStore = defineStore(
val.avatar = userInfoState.avatar
}
else {
val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige'
val.avatar = defaultAvatar
}
userInfo.value = val
}
@ -103,6 +110,8 @@ export const useUserStore = defineStore(
getUserInfo,
setUserAvatar,
logout,
setUserInfo,
removeUserInfo
}
},
{

View File

@ -0,0 +1,26 @@
// 茶艺师等级枚举
export enum TeaSpecialistLevel {
Gold = '金牌茶艺师',
Senior = '高级茶艺师',
Intermediate = '中级茶艺师',
Junior = '初级茶艺师',
Enthusiast = '茶艺爱好者'
}
// 订单来源对应名称
export const TeaSpecialistLevelValue = {
['金牌茶艺师']: 'gold',
['高级茶艺师']: 'senior',
['中级茶艺师']: 'intermediate',
['初级茶艺师']: 'junior',
['茶艺爱好者']: 'enthusiast',
}
// 茶艺师对象结构
export const TeaSpecialistLevels = [
{ id: 1, value: 'gold', label: TeaSpecialistLevel.Gold},
{ id: 2, value: 'senior', label: TeaSpecialistLevel.Senior },
{ id: 3, value: 'intermediate', label: TeaSpecialistLevel.Intermediate },
{ id: 4, value: 'junior', label: TeaSpecialistLevel.Junior },
{ id: 5, value: 'enthusiast', label: TeaSpecialistLevel.Enthusiast }
];

125
src/utils/tools.ts Normal file
View File

@ -0,0 +1,125 @@
import Decimal from 'decimal.js'
import { allowedNodeEnvironmentFlags } from 'process';
/**
* 页面跳转方法
* @param url 跳转地址
* @param time 延迟时间(毫秒)默认0
*/
export const router = {
//跳转至非table页面
navigateTo: (url: string, time = 0) => {
setTimeout(function() {
uni.navigateTo({
url
})
}, time);
},
//跳转至 table
switchTab: (url: string, time = 0) => {
setTimeout(function() {
uni.switchTab({
url
})
}, time);
},
//返回上页面
navigateBack: (delta: number = 1, time = 0) => {
setTimeout(function() {
uni.navigateBack({
delta
})
}, time);
},
//关闭当前所有页面跳转至非table页面
reLaunch: (url: string, time = 0) => {
setTimeout(function() {
uni.reLaunch({
url
})
}, time);
},
//关闭当前页面跳转至非table页面
redirectTo: (url: string, time = 0) => {
setTimeout(function() {
uni.redirectTo({
url
})
}, time);
},
}
/**
* 乘法,避免浮点数精度问题,不进行四舍五入,直接截断
* @param num1 乘数1
* @param num2 乘数2
* @returns 乘积
*/
export function toTimes(num1: number, num2: number) {
const value1 = new Decimal(num1)
const value2 = new Decimal(num2)
const result = value1.times(value2).toDecimalPlaces(2, Decimal.ROUND_DOWN)
console.log("🚀 ~ toPlus ~ result:", result)
return result.toString()
}
/**
* 加法,避免浮点数精度问题,不进行四舍五入,直接截断
* @param args 任意数量的加数
* @returns 求和结果字符串保留2位小数向下截断
*/
/**
* 加法,支持对象参数(如 {0: '128', 1: '128'}),避免浮点数精度问题,不进行四舍五入,直接截断
* @param args 任意数量的加数或对象
* @returns 求和结果字符串保留2位小数向下截断
*/
export function toPlus(...args: any[]): string {
// 支持 toPlus({0: '128', 1: '128'}) 或 toPlus('128', '128')
let arr: any[] = []
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
// 传入的是对象,取所有值
arr = Object.values(args[0])
} else {
arr = args
}
let sum = new Decimal(0)
for (const num of arr) {
// 自动转为数字
const n = Number(num)
if (!isNaN(n)) {
sum = sum.plus(new Decimal(n))
}
}
// 截断2位小数不四舍五入
const result = sum.toDecimalPlaces(2, Decimal.ROUND_DOWN)
return result.toString()
}
/**
* 减法,支持对象参数(如 {0: '128', 1: '28'}),避免浮点数精度问题,不进行四舍五入,直接截断
* @param args 任意数量的被减数和减数或对象
* @returns 结果字符串保留2位小数向下截断
*/
export function toMinus(...args: any[]): string {
// 支持 toMinus({0: '128', 1: '28'}) 或 toMinus('128', '28')
let arr: any[] = []
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
arr = Object.values(args[0])
} else {
arr = args
}
if (arr.length === 0) return '0.00'
let result = new Decimal(Number(arr[0]) || 0)
for (let i = 1; i < arr.length; i++) {
const n = Number(arr[i])
if (!isNaN(n)) {
result = result.minus(new Decimal(n))
}
}
return result.toDecimalPlaces(2, Decimal.ROUND_DOWN).toString()
}