import {getLocation, getUserInfo, setLocation} from "@/util/user"; import {API, MAIN_API} from "@/config"; import {get} from "@/api/request"; import {areaInfoByNameReq, areaInfoReq} from "@/api/common"; import {buildGeocodeUrl, formatLocationText, parseGeocodeResult} from "./geo"; import dayjs from "dayjs"; import md5 from "md5"; export const ORDER_STATUS_LIST = ['已取消', '进行中', '已完成', '申请退款中', '退款完成', '退款失败'] export const CHECK_STATUS_LIST = [ { label: '审核中', color: 'info' }, { label: '审核通过', color: 'success' }, { label: '审核拒绝', color: 'error' }, { label: '已到账', color: 'success' }, ] export const chooseImg = (limit = 1, sourceType = ['album', 'camera']) => { return new Promise(resolveMain => { uni.chooseImage({ count: limit, sourceType, extension: ['png', 'jpg', 'jpeg'], sizeType: ['original'], success: async ({tempFiles}) => { const result = await Promise.all(tempFiles.map(async file => { const uploadRes = await uploadFileToAliOss(file.path) return { path: uploadRes, } })) resolveMain(result) }, complete: () => { uni.hideLoading() } }) }) } let ossUploadSeed = 0 const createOssKey = (filePath) => { ossUploadSeed += 1 const suffix = filePath.substring(filePath.lastIndexOf(".")); const hash = md5(`${filePath}_${Date.now()}_${ossUploadSeed}_${Math.random()}`) return `${dayjs().format('YYYYMMDD')}/${hash}${suffix}` } export const uploadImg = filePath => { const {token} = getUserInfo() uni.showLoading({title: '上传中'}) return new Promise(async resolve => { // uni.uploadFile({ // url: `${MAIN_API}/api/file/upload`, // name: 'file', // filePath, // header: { // Authorization: token, // TenantId: 10325 // }, // formData: { // type: 'header' // }, // success: uploadRes => { // uni.hideLoading() // const {data: {url}} = JSON.parse(uploadRes.data) // resolve({ // path: url, // }) // } // }) const uploadRes = await uploadFileToAliOss(filePath) uni.hideLoading() resolve({ path: uploadRes, }) }) } const getSts = () => get('/oss/getSTSToken') export const uploadFileToAliOss = async (filePath) => { const stsRes = await getSts() const sts = stsRes.data const key = createOssKey(filePath) const url = 'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com'; return new Promise(resolve => { uni.uploadFile({ url, name: 'file', filePath, formData: { key, OSSAccessKeyId: sts.accessKeyId, signature: sts.signature, security_token: sts.token, policy: sts.policy, }, success: () => { resolve(`${url}/${key}`) } }) }) } /** * 调天地图逆地理编码,返回结构化结果(见 util/geo.js)。 * 网络失败 / 限流(429) / 参数错误(400) 一律 reject,不再静默吞掉。 */ const fetchGeocode = async (longitude, latitude) => { const body = await get(buildGeocodeUrl(longitude, latitude)) const geo = parseGeocodeResult(body) if (!geo) throw new Error('天地图逆地理编码返回异常') return geo } /** * 把天地图结果映射到 sys_area,取 area_id / city_id / province_id。 * * 关键修复:**优先按 adcode 精确查**(天地图 county_code 去 156 前缀即 * sys_area.id),从根上绕开区县重名问题。 * * 降级说明:拿不到 adcode 时才按中文名查,此时若区县重名(如「南山区」 * 深圳/鹤岗都有),后端只能稳定返回 id 最小的那条,**可能不是用户实际 * 所在的那个区县**。因此这里的结果仅用于 area_id/city_id/province_id; * 用于展示的省市区文本一律取天地图原始字段(geo.province/city/county), * 不受该歧义影响。 * * 无论哪种方式查不到都返回 null 而不是让整条定位失败——页面提交只需要 * 经纬度与地址文本,行政区划编码缺失不应阻断用户。 */ const resolveAreaInfo = async geo => { if (geo.adcode) { const byCode = await areaInfoByNameReq({id: Number(geo.adcode)}).catch(() => null) if (byCode && byCode.data) return byCode.data } if (geo.county) { // 有歧义风险(见上),仅作为 adcode 缺失时的兜底 const byName = await areaInfoByNameReq({name: geo.county}).catch(() => null) if (byName && byName.data) return byName.data } return null } /** 引导用户开启定位授权。用户此前已拒绝过时 authorize 会直接失败,这里兜底提示。 */ const showLocationSettingGuide = () => { uni.showModal({ title: '提示', content: '为了更好的提供服务,请开启定位服务', success: ({confirm}) => { if (confirm) uni.openSetting({}) } }) } /** * 定位一次并返回结果:授权 → 取经纬度 → 逆地理编码 → 映射行政区划。 * * 所有失败路径都显式 reject,保证调用方的 await 一定结算。 * (原实现在 catch 回调里用了模块作用域的 this.areaId,在严格模式下会抛 * TypeError 并让 Promise 永久悬空,调用方的 locating 标志再也回不到 false, * 表现为「点定位没反应、反复点也没用」。) * * @param {boolean} save 是否写入本地缓存(首页等复用缓存时用) */ const locateOnce = ({save = false} = {}) => { return new Promise((resolve, reject) => { uni.authorize({ scope: 'scope.userLocation', success: () => { uni.getLocation({ isHighAccuracy: true, success: async ({longitude, latitude}) => { try { const geo = await fetchGeocode(longitude, latitude) const area = await resolveAreaInfo(geo) const payload = { longitude, latitude, area_id: area ? area.id : '', city_id: area ? area.fid : '', province_id: area && area.city ? area.city.fid : '', province_str: geo.province, city_str: geo.city, area_str: geo.county, full: formatLocationText(geo) } if (save) setLocation(payload) resolve(payload) } catch (error) { reject(false) } }, fail: () => { reject(false) } }) }, fail: () => { showLocationSettingGuide() reject(false) } }) }) } export const getLocationAction = (save = true) => { // 保持原有判据:缓存中已有 area_id 才复用。刻意不改判据,避免悄悄改变 // 首页/业绩/分享/我的这 4 个调用方的刷新行为(它们只消费 city_str,但 // 「有坐标就跳过重定位」会让跨城移动后的 currentCity 长期陈旧)。 const cached = getLocation() if (cached.area_id) return Promise.resolve(cached) return locateOnce({save}) } export const chooseVideo = (sourceType = ['album', 'camera']) => { return new Promise((resolveMain, reject) => { uni.chooseVideo({ sourceType, compressed: true, maxDuration: 60, success: async ({tempFilePath, duration, size}) => { if (duration > 180) { uni.showToast({ title: '暂时不支持3分钟以上的视频', icon: 'none' }) return reject(false) } // 50M限制 if (size > 50 * 1024 * 1024) { uni.showToast({ title: '暂时不支持50M以上的视频', icon: 'none' }) return reject(false) } const {token} = getUserInfo() uni.showLoading({title: '上传中...'}) // uni.uploadFile({ // url: `${MAIN_API}/api/file/upload`, // name: 'file', // filePath: tempFilePath, // header: { // Authorization: token, // TenantId: 10325 // }, // success: uploadRes => { // const {data} = JSON.parse(uploadRes.data) // uni.hideLoading() // resolveMain({ // path: data.url, // }) // } // }) const uploadRes = await uploadFileToAliOss(tempFilePath) resolveMain({ path: uploadRes, }) } }) }) } /** * 强制实时定位(不读缓存),供拜访页等「每次都要当前位置」的场景使用。 * * 原实现的问题:把天地图 URL 的 postStr 裸拼在查询串里(实测接口返回 * HTTP 400),且 catch 回调里误用 this.areaId 导致 Promise 可能永不结算。 * 现已统一走 locateOnce,保证一定 reject。 */ export const getLocationOnly = () => locateOnce({save: false}) export const getExpressMode = () => parseInt(uni.getStorageSync("express_mode")) export const calcDistanceMeters = (lat1, lng1, lat2, lng2) => { const nLat1 = Number(lat1) const nLng1 = Number(lng1) const nLat2 = Number(lat2) const nLng2 = Number(lng2) if (![nLat1, nLng1, nLat2, nLng2].every(Number.isFinite)) return NaN const rad = value => value * Math.PI / 180 const r = 6371000 const dLat = rad(nLat2 - nLat1) const dLng = rad(nLng2 - nLng1) const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(rad(nLat1)) * Math.cos(rad(nLat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) return r * c } export const formatDistance = meters => { if (!Number.isFinite(meters)) return '' if (meters < 1000) return `${Math.round(meters)}m` const km = (meters / 1000).toFixed(1).replace(/\.0$/, '') return `${km}km` }