util.js 15 KB

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