util.js 15 KB

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