初始化提交

This commit is contained in:
wangxiaowei
2025-12-27 13:33:44 +08:00
commit 833fd1bb66
311 changed files with 53086 additions and 0 deletions

1
src/hooks/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
// 可以将此代码放置于项目src/hooks/useColPickerData.ts中
import { useCascaderAreaData } from '@vant/area-data'
export type CascaderOption = {
text: string
value: string
children?: CascaderOption[]
}
/**
* 使用'@vant/area-data'作为数据源构造ColPicker组件的数据
* @returns
*/
export function useColPickerData() {
// '@vant/area-data' 数据源
const colPickerData: CascaderOption[] = useCascaderAreaData()
// 根据code查找子节点不传code则返回所有节点
function findChildrenByCode(data: CascaderOption[], code?: string): CascaderOption[] | null {
if (!code) {
return data
}
for (const item of data) {
if (item.value === code) {
return item.children || null
}
if (item.children) {
const childrenResult = findChildrenByCode(item.children, code)
if (childrenResult) {
return childrenResult
}
}
}
return null
}
return { colPickerData, findChildrenByCode }
}

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

@ -0,0 +1,188 @@
import { getLocationToCity } from '@/api/tea-room'
const LOCATION_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000 // 定位缓存30天
const LOCATION_DENY_INTERVAL = 60 * 60 * 1000 // 未授权定位弹窗间隔1小时
export const LOCATION_EXPIRE_KEY = 'location_expire_time' // 定位缓存KEY
export const LOCATION_DEFAULT_CITY = '上海市' // 默认城市
export const LOCATION_DEFAULT_LAT = 31.230393 // 上海经度
export const LOCATION_DEFAULT_LNG = 121.473629 // 上海纬度
export const LOCATION_DENY_TIME_KEY = 'location_deny_time' // 未授权定位重新授权时间KEY
export const LOCATION_CITY_KEY = 'city' // 城市缓存KEY
export const LOCATION_LAT_KEY = 'latitude' // 城市缓存KEY
export const LOCATION_LNG_KEY = 'longitude' // 城市缓存KEY
// 检查过期时间
export function handleCheckLocationCacheHooks() {
const expire = uni.getStorageSync(LOCATION_EXPIRE_KEY)
if (expire && Date.now() > expire) {
uni.removeStorageSync(LOCATION_LAT_KEY)
uni.removeStorageSync(LOCATION_LNG_KEY)
uni.removeStorageSync(LOCATION_EXPIRE_KEY)
return false
}
return true
}
// 设置经纬度缓存
export function handleSetLocationCacheHooks(lat: number, lng: number) {
uni.setStorageSync(LOCATION_LAT_KEY, lat)
uni.setStorageSync(LOCATION_LNG_KEY, lng)
uni.setStorageSync(LOCATION_EXPIRE_KEY, Date.now() + LOCATION_EXPIRE_MS)
}
// 初始化经纬度
export async function handleEnsureLocationAuthHooks() {
// 1. 检查缓存
if (handleCheckLocationCacheHooks()) {
const lat = uni.getStorageSync(LOCATION_LAT_KEY)
const lng = uni.getStorageSync(LOCATION_LNG_KEY)
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() {
// 定位失败,返回默认上海
handleSetLocationCacheHooks(LOCATION_DEFAULT_LAT, LOCATION_DEFAULT_LNG)
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({})
}
})
}
// 返回默认上海
handleSetLocationCacheHooks(LOCATION_DEFAULT_LAT, LOCATION_DEFAULT_LNG)
resolve({ lat: LOCATION_DEFAULT_LAT, lng: LOCATION_DEFAULT_LNG })
}
})
})
}
/**
* 检查并弹窗授权地理位置(每小时弹一次,授权后自动获取定位)
* 返回 Promise<boolean>true 表示已授权false 表示未授权
*/
export function checkLocationAuthWithModal(): Promise<{ lat: number, lng: number } | false> {
return new Promise((resolve) => {
uni.getSetting({
success(settingRes) {
const hasAuth = settingRes.authSetting && settingRes.authSetting['scope.userLocation']
if (hasAuth) {
// 已授权,自动获取并返回经纬度
uni.getLocation({
type: 'gcj02',
success(res) {
handleSetLocationCacheHooks(res.latitude, res.longitude)
resolve({ lat: res.latitude, lng: res.longitude })
},
fail() {
resolve(false)
}
})
} else {
// 未授权,判断是否需要弹窗
const lastDeny = uni.getStorageSync(LOCATION_DENY_TIME_KEY)
if (!lastDeny || (Date.now() - lastDeny > LOCATION_DENY_INTERVAL)) {
uni.setStorageSync(LOCATION_DENY_TIME_KEY, Date.now())
uni.showModal({
title: '提示',
content: '需要获取您的地理位置,请授权定位服务',
showCancel: false,
success: () => {
uni.openSetting({
success(openRes) {
const nowAuth = openRes.authSetting && openRes.authSetting['scope.userLocation']
if (nowAuth) {
// 用户授权后,自动获取并返回经纬度
uni.getLocation({
type: 'gcj02',
success(res) {
handleSetLocationCacheHooks(res.latitude, res.longitude)
resolve({ lat: res.latitude, lng: res.longitude })
},
fail() {
resolve(false)
}
})
} else {
resolve(false)
}
}
})
}
})
} else {
resolve(false)
}
}
},
fail() {
resolve(false)
}
})
})
}
/**
* 设置城市
*/
export function setLocationCity(city: string) {
uni.setStorageSync(LOCATION_CITY_KEY, city)
}
/**
* 经纬度转换为城市
* @returns
*/
export async function handleGetLocationCity(lat: number, lng: number) {
try {
console.log("🚀 ~ handleGetLocationCity ~ res:", lat, lng)
const res = await getLocationToCity({ latitude: lat, longitude: lng })
if (res.message == "Success") {
const params = {
latitude: res.result.location.lat,
longitude: res.result.location.lng,
city: res.result.ad_info.city
}
setLocationCity(params.city)
// uni.$emit('locationUpdate', params) // 通知页面
return params
} else {
setLocationCity(LOCATION_DEFAULT_CITY)
}
} catch (error) {
setLocationCity(LOCATION_DEFAULT_CITY)
}
}
// 检查是否需要弹授权框
function shouldShowAuthModal() {
const lastDeny = uni.getStorageSync(LOCATION_DENY_TIME_KEY)
return !lastDeny || (Date.now() - lastDeny > LOCATION_DENY_INTERVAL)
}

56
src/hooks/useOrder.ts Normal file
View File

@ -0,0 +1,56 @@
import { router } from '@/utils/tools'
import { toast } from '@/utils/toast'
import {
cancelTeaRoomOrder,
deleteTeaRoomOrder,
releaseTeaRoomOrder
} from '@/api/order'
/**
* 释放时间
* @param id 订单ID
*/
export async function handleReleaseTeaRoomOrderHookds (id: number) {
try {
await releaseTeaRoomOrder(id)
uni.$emit('refreshOrderList')
uni.$emit('refreshOrderDetail')
toast.info('释放成功')
} catch (error) {
router.navigateBack()
throw error
}
}
/**
* 取消订单
* @param orderId 订单ID
*/
export async function handleCancelOrderHooks(id: number) {
try {
await cancelTeaRoomOrder(id)
uni.$emit('refreshOrderList')
uni.$emit('refreshOrderDetail')
toast.info('取消成功')
} catch (error) {
router.navigateBack()
throw error
}
}
/**
* 删除订单
*/
export async function handleDeleteOrderHooks(id: number) {
try {
await deleteTeaRoomOrder(id)
uni.$emit('refreshOrderList')
uni.$emit('refreshOrderDetail')
toast.info('删除订单成功')
} catch (error) {
router.navigateBack()
throw error
}
}

50
src/hooks/usePageAuth.ts Normal file
View File

@ -0,0 +1,50 @@
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/store'
import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
const loginRoute = import.meta.env.VITE_LOGIN_URL
const isDev = import.meta.env.DEV
function isLogined() {
const userStore = useUserStore()
return !!userStore.userInfo.username
}
// 检查当前页面是否需要登录
export function usePageAuth() {
onLoad((options) => {
// 获取当前页面路径
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = `/${currentPage.route}`
// 获取需要登录的页面列表
let needLoginPages: string[] = []
if (isDev) {
needLoginPages = getNeedLoginPages()
}
else {
needLoginPages = _needLoginPages
}
// 检查当前页面是否需要登录
const isNeedLogin = needLoginPages.includes(currentPath)
if (!isNeedLogin) {
return
}
const hasLogin = isLogined()
if (hasLogin) {
return true
}
// 构建重定向URL
const queryString = Object.entries(options || {})
.map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
.join('&')
const currentFullPath = queryString ? `${currentPath}?${queryString}` : currentPath
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(currentFullPath)}`
// 重定向到登录页
uni.redirectTo({ url: redirectRoute })
})
}

37
src/hooks/usePay.ts Normal file
View File

@ -0,0 +1,37 @@
export function wxPay(opt) {
return new Promise((resolve, reject) => {
let params;
// #ifdef MP-WEIXIN
params = {
timeStamp: opt.timeStamp,
// 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: opt.nonceStr,
// 支付签名随机串,不长于 32 位
package: opt.package,
// 统一支付接口返回的prepay_id参数值提交格式如prepay_id=***
signType: opt.signType,
// 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: opt.paySign,
}
// #endif
// #ifdef APP-PLUS
params = {
orderInfo: opt
}
// #endif
console.log(params)
uni.requestPayment({
provider: 'wxpay',
...params,
success: res => {
resolve('success');
},
cancel: res => {
resolve('fail');
},
fail: res => {
resolve('fail');
}
});
});
}

51
src/hooks/useRequest.ts Normal file
View File

@ -0,0 +1,51 @@
import type { Ref } from 'vue'
interface IUseRequestOptions<T> {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */
initialData?: T
}
interface IUseRequestReturn<T> {
loading: Ref<boolean>
error: Ref<boolean | Error>
data: Ref<T | undefined>
run: () => Promise<T | undefined>
}
/**
* useRequest是一个定制化的请求钩子用于处理异步请求和响应。
* @param func 一个执行异步请求的函数返回一个包含响应数据的Promise。
* @param options 包含请求选项的对象 {immediate, initialData}。
* @param options.immediate 是否立即执行请求默认为false。
* @param options.initialData 初始化数据默认为undefined。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/
export default function useRequest<T>(
func: () => Promise<IResData<T>>,
options: IUseRequestOptions<T> = { immediate: false },
): IUseRequestReturn<T> {
const loading = ref(false)
const error = ref(false)
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
const run = async () => {
loading.value = true
return func()
.then((res) => {
data.value = res.data
error.value = false
return data.value
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
loading.value = false
})
}
options.immediate && run()
return { loading, error, data, run }
}

160
src/hooks/useUpload.ts Normal file
View File

@ -0,0 +1,160 @@
import { ref } from 'vue'
import { getEnvBaseUploadUrl } from '@/utils'
const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
type TfileType = 'image' | 'file'
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
interface TOptions<T extends TfileType> {
formData?: Record<string, any>
maxSize?: number
accept?: T extends 'image' ? TImage[] : TFile[]
fileType?: T
success?: (params: any) => void
error?: (err: any) => void
}
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
const {
formData = {},
maxSize = 5 * 1024 * 1024,
accept = ['*'],
fileType = 'image',
success,
error: onError,
} = options
const loading = ref(false)
const error = ref<Error | null>(null)
const data = ref<any>(null)
const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
if (size > maxSize) {
uni.showToast({
title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
icon: 'none',
})
return
}
// const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
// const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
// if (!isTypeValid) {
// uni.showToast({
// title: `仅支持 ${accept.join(', ')} 格式的文件`,
// icon: 'none',
// })
// return
// }
loading.value = true
uploadFile({
tempFilePath,
formData,
onSuccess: (res) => {
const { data: _data } = JSON.parse(res)
data.value = _data
// console.log('上传成功', res)
success?.(_data)
},
onError: (err) => {
error.value = err
onError?.(err)
},
onComplete: () => {
loading.value = false
},
})
}
const run = () => {
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后使用本API需要配置隐私协议
const chooseFileOptions = {
count: 1,
success: (res: any) => {
console.log('File selected successfully:', res)
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
// h5的File有以下字段{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
// App的File有以下字段{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
let tempFilePath = ''
let size = 0
// #ifdef MP-WEIXIN
tempFilePath = res.tempFiles[0].tempFilePath
size = res.tempFiles[0].size
// #endif
// #ifndef MP-WEIXIN
tempFilePath = res.tempFilePaths[0]
size = res.tempFiles[0].size
// #endif
handleFileChoose({ tempFilePath, size })
},
fail: (err: any) => {
console.error('File selection failed:', err)
error.value = err
onError?.(err)
},
}
if (fileType === 'image') {
// #ifdef MP-WEIXIN
uni.chooseMedia({
...chooseFileOptions,
mediaType: ['image'],
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage(chooseFileOptions)
// #endif
}
else {
uni.chooseFile({
...chooseFileOptions,
type: 'all',
})
}
}
return { loading, error, data, run }
}
async function uploadFile({
tempFilePath,
formData,
onSuccess,
onError,
onComplete,
}: {
tempFilePath: string
formData: Record<string, any>
onSuccess: (data: any) => void
onError: (err: any) => void
onComplete: () => void
}) {
uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: 'file',
formData,
success: (uploadFileRes) => {
try {
const data = uploadFileRes.data
onSuccess(data)
}
catch (err) {
onError(err)
}
},
fail: (err) => {
console.error('Upload failed:', err)
onError(err)
},
complete: onComplete,
})
}