util.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. import { useAppStore } from "@/stores/app";
  2. import { toLogin, checkLogin } from '@/libs/login'
  3. import { wxLogin, updateOpenId,getUserInfo } from "@/api/user.js";
  4. import { useToast } from "@/hooks/useToast";
  5. import Cache from "./cache";
  6. import {
  7. USER_INFO,
  8. TOKEN
  9. } from "@/config/cache";
  10. const { Toast } = useToast();
  11. import {
  12. HTTP_REQUEST_URL,
  13. TOKENNAME
  14. } from '@/config/app';
  15. // 全局状态管理:防止重复请求
  16. let isLoggingIn = false;
  17. export function telEncrypt(tel = '') {
  18. let str = tel + "";
  19. let enStr = str.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2")
  20. return enStr
  21. }
  22. export function uniLogin() {
  23. console.log('uniLogin', checkLogin());
  24. if (checkLogin()) {
  25. const userInfo = Cache.get(USER_INFO) ? JSON.parse(Cache.get(USER_INFO)) : {};
  26. if (userInfo.openId) return;
  27. uni.getProvider({
  28. service: 'oauth',
  29. success: function(res) {
  30. console.log('getProvider', res.provider)
  31. if (~res.provider.indexOf('weixin')) {
  32. uni.login({
  33. provider: 'weixin',
  34. success: async function(loginRes) {
  35. updateOpenId({
  36. jsCode: loginRes.code
  37. }).then(res => {
  38. if (res.code == 200) {
  39. const appStore = useAppStore();
  40. appStore.USERINFO();
  41. }
  42. });
  43. }
  44. });
  45. }
  46. }
  47. });
  48. }
  49. }
  50. /**
  51. * 统一登录方法:同时获取openid和手机号
  52. * @param {Object} e - 微信获取手机号事件对象
  53. * @param {Object} options - 配置选项
  54. * @returns {Promise}
  55. */
  56. export async function getPhoneNumber(e, options = {}) {
  57. return new Promise(async (resolve, reject) => {
  58. const appStore = useAppStore();
  59. // 防重复点击
  60. if (isLoggingIn) {
  61. // 移除了Toast,只在页面显示
  62. reject(new Error('登录请求中,请稍候'));
  63. return;
  64. }
  65. isLoggingIn = true;
  66. try {
  67. // 用户拒绝授权
  68. if (e.detail.errMsg !== 'getPhoneNumber:ok') {
  69. const errorCode = e.detail.errMsg;
  70. let errorMessage = '请授权手机号以完成登录';
  71. if (errorCode.includes('deny')) {
  72. errorMessage = '您已拒绝授权,如需登录请重新授权';
  73. } else if (errorCode.includes('timeout')) {
  74. errorMessage = '授权超时,请重试';
  75. } else if (errorCode.includes('fail')) {
  76. errorMessage = '授权失败,请重试';
  77. }
  78. // 移除了Toast,只在页面显示
  79. // 执行失败回调
  80. if (options.onFail) {
  81. options.onFail({ type: 'auth_denied', message: errorMessage });
  82. }
  83. reject(new Error(errorMessage));
  84. return;
  85. }
  86. // 授权成功
  87. if (!e.detail.code) {
  88. // 移除了Toast,只在页面显示
  89. if (options.onFail) {
  90. options.onFail({ type: 'auth_code_missing', message: '授权码获取失败' });
  91. }
  92. reject(new Error('授权码获取失败'));
  93. return;
  94. }
  95. console.log('开始登录流程,手机号授权码:', e.detail.code.substring(0, 20) + '...');
  96. // 方案1:先获取微信登录code,再调用后端统一接口
  97. // 这种方式更推荐,因为可以保证两个code都是最新的
  98. const result = await handleLoginWithPhoneCode(e.detail.code, options);
  99. // 执行成功回调
  100. if (options.onSuccess) {
  101. options.onSuccess(result);
  102. }
  103. resolve(result);
  104. } catch (error) {
  105. console.error('登录失败:', error);
  106. // 处理特定错误
  107. let errorMessage = '登录失败,请重试';
  108. if (error.message && error.message.includes('超时')) {
  109. errorMessage = '请求超时,请检查网络';
  110. } else if (error.code === 40029) {
  111. errorMessage = '授权码无效,请重新获取';
  112. } else if (error.code === 40001) {
  113. errorMessage = '登录凭证已过期,请重试';
  114. } else if (error.code === 401 || error.code === 40101) {
  115. errorMessage = '登录已过期,请重新登录';
  116. }
  117. // 移除了Toast,只在页面显示
  118. // 执行失败回调
  119. if (options.onFail) {
  120. options.onFail({
  121. type: 'login_failed',
  122. message: errorMessage,
  123. originalError: error.message || errorMessage
  124. });
  125. }
  126. // 特殊错误处理:token过期
  127. if (error.code === 401 || error.code === 40101) {
  128. // 清除本地token,让用户重新登录
  129. Cache.remove(TOKEN);
  130. Cache.remove(USER_INFO);
  131. appStore.LOGOUT();
  132. setTimeout(() => {
  133. uni.showModal({
  134. title: '登录已过期',
  135. content: '您的登录状态已过期,请重新登录',
  136. showCancel: false,
  137. confirmText: '重新登录'
  138. });
  139. }, 1000);
  140. }
  141. reject(error);
  142. } finally {
  143. isLoggingIn = false;
  144. }
  145. });
  146. }
  147. /**
  148. * 处理登录和获取手机号
  149. * 方案1:先获取微信登录code,再调用后端统一接口
  150. */
  151. async function handleLoginWithPhoneCode(phoneCode, options) {
  152. // 1. 先获取微信登录code
  153. const loginRes = await new Promise((resolve, reject) => {
  154. uni.login({
  155. provider: 'weixin',
  156. timeout: 10000,
  157. success: resolve,
  158. fail: reject
  159. });
  160. });
  161. if (!loginRes.code) {
  162. throw new Error('获取登录凭证失败');
  163. }
  164. console.log('获取到登录code:', loginRes.code.substring(0, 20) + '...');
  165. // 2. 获取本地token(如果有的话)
  166. const localToken = Cache.get(TOKEN);
  167. // 3. 调用后端统一登录接口,同时传递登录code和手机号code
  168. const requestData = {
  169. jsCode: loginRes.code, // 微信登录code,用于获取openid
  170. code: phoneCode, // 手机号授权code
  171. timestamp: Date.now() // 防止缓存
  172. };
  173. // 如果有本地token,也带上(用于关联已有账号)
  174. if (localToken) {
  175. requestData.oldToken = localToken;
  176. }
  177. console.log('发送登录请求,数据:', { ...requestData, phoneCode: '已隐藏' });
  178. // 4. 调用后端接口
  179. const res = await wxLogin(requestData);
  180. console.log('登录接口返回:', res);
  181. // 5. 处理后端响应
  182. if (res.code === 200) {
  183. const appStore = useAppStore();
  184. // 保存token
  185. appStore.UPDATE_TOKEN(res.data.access_token || res.data);
  186. // 获取用户信息
  187. await appStore.USERINFO();
  188. // 移除了Toast,只在页面显示
  189. // 触发登录成功事件
  190. uni.$emit('loginSuccess');
  191. // 登录成功后的处理(例如跳转)
  192. await handleAfterLoginSuccess(options);
  193. return {
  194. success: true,
  195. data: res.data,
  196. message: res.msg || '登录成功',
  197. userInfo: appStore.userInfo
  198. };
  199. } else {
  200. // 处理业务错误
  201. const error = {
  202. code: res.code,
  203. message: res.msg || res.data?.message || '登录失败',
  204. data: res.data
  205. };
  206. throw error;
  207. }
  208. }
  209. /**
  210. * 登录成功后的处理
  211. */
  212. async function handleAfterLoginSuccess(options) {
  213. try {
  214. // 如果有回调URL,则跳转
  215. if (options.redirectUrl) {
  216. setTimeout(() => {
  217. if (options.redirectUrl.startsWith('/')) {
  218. uni.redirectTo({ url: options.redirectUrl });
  219. } else {
  220. uni.switchTab({ url: options.redirectUrl });
  221. }
  222. }, 1500);
  223. return;
  224. }
  225. // 默认跳转逻辑
  226. const pages = getCurrentPages();
  227. const currentRoute = pages[pages.length - 1]?.route || '';
  228. // 如果当前在个人中心页面,刷新页面即可
  229. if (currentRoute.includes('mine')) {
  230. // 可以触发页面刷新
  231. const eventChannel = pages[pages.length - 1];
  232. if (eventChannel && eventChannel.onLoad) {
  233. eventChannel.onLoad();
  234. }
  235. } else {
  236. // 其他页面跳转到个人中心
  237. setTimeout(() => {
  238. uni.switchTab({ url: '/pages/mine/mine' });
  239. }, 1500);
  240. }
  241. } catch (error) {
  242. console.error('登录后处理失败:', error);
  243. }
  244. }
  245. /**
  246. * 快捷登录方法(适用于页面直接调用)
  247. * @param {Object} e - 微信获取手机号事件对象
  248. * @param {Object} customOptions - 自定义配置选项
  249. * @returns {Promise}
  250. */
  251. export function quickLogin(e, customOptions = {}) {
  252. const defaultOptions = {
  253. onSuccess: (result) => {
  254. console.log('登录成功,用户数据:', result);
  255. },
  256. onFail: (error) => {
  257. console.log('登录失败:', error);
  258. },
  259. redirectUrl: '/pages/mine/mine'
  260. };
  261. const options = { ...defaultOptions, ...customOptions };
  262. return getPhoneNumber(e, options);
  263. }
  264. //model是ture,属于重新定位,直接更新位置
  265. export function getLocation(model=true,callback) {
  266. uni.getLocation({
  267. type: 'wgs84',
  268. geocode: true,
  269. success: function (res) {
  270. // console.log('当前位置的经度:' + res.longitude);
  271. // console.log('当前位置的纬度:' + res.latitude);
  272. reverseGeocoder(res.latitude, res.longitude, model,callback);
  273. },
  274. complete: function (res) {
  275. // console.log('getLocation-complete:' ,res);
  276. // const appStore = useAppStore();
  277. // appStore.UPDATE_CITY("洛阳市")
  278. }
  279. });
  280. }
  281. // 逆地理编码示例(使用腾讯地图)
  282. export function reverseGeocoder(latitude, longitude, model,callback) {
  283. getCityInfo({latitude,longitude}).then((res)=>{
  284. console.log('reverseGeocoder-res:' ,res);
  285. if (res.code === 200){
  286. const appStore = useAppStore();
  287. if(model){
  288. appStore.UPDATE_CITYINFO(res.data);
  289. callback();
  290. }else if(res.data?.name && res.data.name != appStore.cityInfo?.name){
  291. uni.showModal({
  292. title: '提示',
  293. content: '你当前的城市有更新,是否切换到'+ res.data.name,
  294. success: function (e) {
  295. if (e.confirm) {
  296. appStore.UPDATE_CITYINFO(res.data)
  297. } else if (e.cancel) {
  298. console.log('用户点击取消');
  299. }
  300. }
  301. });
  302. }
  303. }
  304. })
  305. }
  306. export function getNavbarHeight() {
  307. const appStore = useAppStore();
  308. // 获取系统信息
  309. const sysInfo = uni.getSystemInfoSync()
  310. // 状态栏高度(不同设备不一致)
  311. appStore.UPDATE_statusBarHeight(sysInfo.statusBarHeight)
  312. // 导航栏总高度 = 状态栏高度 + 自定义导航内容高度(通常 44px)
  313. // navbarHeight.value = statusBarHeight.value + 44;
  314. }
  315. export function setClipboardData(data="") {
  316. uni.setClipboardData({
  317. data,
  318. success: function () {
  319. Toast({ title: "复制成功" });
  320. }
  321. });
  322. }
  323. export function checkLoginShowModal() {
  324. return new Promise((resolve, reject) => {
  325. if (!checkLogin()) {
  326. uni.showModal({
  327. title: '温馨提示',
  328. content: '登录后将享受更多优质权益,请先登录',
  329. // showCancel:false,
  330. success: function (res) {
  331. if (res.confirm) {
  332. //跳转到我的页
  333. uni.switchTab({
  334. url: "/pages/mine/mine",
  335. });
  336. } else if (res.cancel) {
  337. console.log('用户点击取消');
  338. }
  339. resolve(false);
  340. }
  341. });
  342. }else {
  343. resolve(true);
  344. }
  345. })
  346. }
  347. // 检测今日免费提问次数
  348. export function checkAiQuotaDailyModal() {
  349. return new Promise((resolve, reject) => {
  350. const appStore = useAppStore();
  351. if (appStore.userInfo?.aiQuotaDaily <= 0) {
  352. let content = '';
  353. if(appStore.userInfo?.rechargeBalance <= 0){
  354. content = ' AI调用次数不足且余额不足,请充值或明日再试';
  355. }else if(appStore.userInfo?.rechargeBalance > 0){
  356. if(appStore.useBalance){
  357. resolve(true);
  358. return;
  359. }
  360. content = '当日免费ai调用次数已用完,是否使用晓豆进行支付';
  361. }
  362. uni.showModal({
  363. title: '温馨提示',
  364. content,
  365. // showCancel:false,
  366. success: function (res) {
  367. if (res.confirm) {
  368. if(appStore.userInfo?.rechargeBalance <= 0){
  369. //跳转到充值页
  370. uni.navigateTo({
  371. url: "/pages/recharge/recharge",
  372. });
  373. }else{
  374. appStore.useBalance = true;
  375. resolve(true);
  376. }
  377. } else if (res.cancel) {
  378. console.log('用户点击取消');
  379. }
  380. resolve(false);
  381. }
  382. });
  383. }else {
  384. resolve(true);
  385. }
  386. })
  387. }
  388. // 方法:按指定key将一维对象数组转为二维数组
  389. export function groupByKey(arr, key) {
  390. // 创建一个临时对象用于分组
  391. const groupObj = {};
  392. // 遍历原数组
  393. arr.forEach(item => {
  394. // 获取当前项的key值作为分组标识
  395. const groupKey = item[key];
  396. // 如果该分组不存在,则初始化一个空数组
  397. if (!groupObj[groupKey]) {
  398. groupObj[groupKey] = [];
  399. }
  400. // 将当前项添加到对应的分组中
  401. groupObj[groupKey].push(item);
  402. });
  403. // 将对象的值转换为数组,得到二维数组
  404. return Object.values(groupObj);
  405. }
  406. // 支付
  407. export function wxPay({timeStamp,nonceStr,packageVal,signType,paySign},callback) {
  408. uni.requestPayment({
  409. provider: 'wxpay',
  410. timeStamp,
  411. nonceStr,
  412. package:packageVal,
  413. signType,
  414. paySign,
  415. success: function (res) {
  416. console.log('wxPay-success:' + JSON.stringify(res));
  417. callback({isSuccess:1});
  418. },
  419. fail: function (err) {
  420. console.log('wxPay-fail:' + JSON.stringify(err));
  421. Toast({ title: "支付失败" });
  422. callback({isSuccess:0});
  423. }
  424. });
  425. }
  426. /**
  427. * 计算两点之间的直线距离(Haversine公式)
  428. * @param {Number} lat1 起点纬度
  429. * @param {Number} lon1 起点经度
  430. * @param {Number} lat2 终点纬度
  431. * @param {Number} lon2 终点经度
  432. * @returns {Number} 距离(千米)
  433. */
  434. export function calculateDistance (lat1, lon1, lat2, lon2) {
  435. // 将角度转换为弧度
  436. const radLat1 = (lat1 * Math.PI) / 180;
  437. const radLat2 = (lat2 * Math.PI) / 180;
  438. const a = radLat1 - radLat2;
  439. const b = (lon1 * Math.PI) / 180 - (lon2 * Math.PI) / 180;
  440. // Haversine公式计算距离(地球半径取6371千米)
  441. let s = 2 * Math.asin(
  442. Math.sqrt(
  443. Math.pow(Math.sin(a / 2), 2) +
  444. Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)
  445. )
  446. );
  447. s = s * 6371; // 地球半径(千米)
  448. return s.toFixed(2);
  449. };
  450. // 获取邀请码
  451. export function getSceneInfo (e) {
  452. console.log("getSceneInfo",e)
  453. if(e.scene){
  454. const decodedScene = decodeURIComponent(e.scene);
  455. // 2. 分割参数(格式如:key1=value1&key2=value2)
  456. const params = {};
  457. if (decodedScene) {
  458. decodedScene.split('&').forEach(item => {
  459. const [key, value] = item.split('=');
  460. if (key && value) {
  461. params[key] = value;
  462. }
  463. });
  464. }
  465. console.log("getSceneInfo-params",params)
  466. // 邀请码
  467. const appStore = useAppStore();
  468. if(params.inviteCode)appStore.UPDATE_inviteCode(params.inviteCode);
  469. return params;
  470. }
  471. };
  472. //获取客户信息
  473. export function getUserInfo (e) {
  474. const appStore = useAppStore();
  475. if(Cache.get(TOKEN))appStore.USERINFO();
  476. };
  477. export async function chooseImageOne(apiUrl="chat/file/imageUpload") {
  478. return new Promise((resolve, reject) => {
  479. uni.chooseImage({
  480. count: 1,
  481. success: (chooseImageRes) => {
  482. const tempFilePaths = chooseImageRes.tempFilePaths;
  483. console.log("chooseImageRes",chooseImageRes);
  484. const appStore = useAppStore();
  485. uni.uploadFile({
  486. url: `${HTTP_REQUEST_URL}/mini/${apiUrl}`, //仅为示例,非真实的接口地址
  487. filePath: tempFilePaths[0],
  488. name: 'file',
  489. header: {
  490. [TOKENNAME]: appStore.token,
  491. },
  492. success: (uploadFileRes) => {
  493. console.log('uni.uploadFile',uploadFileRes);
  494. const data = JSON.parse(uploadFileRes.data);
  495. if(data.code==200){
  496. resolve(data);
  497. }else{
  498. reject(data);
  499. Toast({ title: "图片上传失败" });
  500. }
  501. }
  502. });
  503. }
  504. });
  505. })
  506. }
  507. export async function uploadFile(filePath,apiUrl="chat/file/imageUpload") {
  508. return new Promise((resolve, reject) => {
  509. const appStore = useAppStore();
  510. uni.uploadFile({
  511. url: `${HTTP_REQUEST_URL}/mini/${apiUrl}`, //仅为示例,非真实的接口地址
  512. filePath,
  513. name: 'file',
  514. header: {
  515. [TOKENNAME]: appStore.token,
  516. },
  517. success: (uploadFileRes) => {
  518. console.log('uni.uploadFile',uploadFileRes);
  519. const data = JSON.parse(uploadFileRes.data);
  520. if(data.code==200){
  521. resolve(data);
  522. }else{
  523. reject(data);
  524. Toast({ title: "图片上传失败" });
  525. }
  526. }
  527. });
  528. })
  529. }