Commit 98ac06d9248ea888387dffcc65484e2089e95dd7
1 parent
0a02df01
refactor: use new ui
Showing
6 changed files
with
286 additions
and
97 deletions
app.js
| 1 | -//app.js | |
| 1 | +// zooble@2025-12-14 | |
| 2 | +const { createRequest } = require('./utils/request.js'); | |
| 3 | + | |
| 2 | 4 | App({ |
| 3 | - async onLaunch () { | |
| 4 | - this.login(); | |
| 5 | - // 移除 2022-12-05 之前的本地存储,因为可能有敏感词存在 | |
| 6 | - const timestamp = new Date('2022-12-05').getTime(); | |
| 7 | - const audioList = this.globalData.getAudioList().filter(v => v.time > timestamp); | |
| 8 | - const storageKey = 'audioList'; | |
| 9 | - wx.setStorageSync(storageKey, audioList); | |
| 10 | - }, | |
| 11 | - // 将登录态封装进去 | |
| 12 | - // todo: 多个请求同时发起,且登录态都失效时,ssid 会相互覆盖 | |
| 13 | - async request(config){ | |
| 14 | - let ssid = wx.getStorageSync('ssid'); | |
| 15 | - if(!ssid){ | |
| 16 | - ssid = await this.login(); | |
| 17 | - } | |
| 18 | - const initialSuccess = config.success; | |
| 19 | - config.data = { | |
| 20 | - ...config.data, | |
| 21 | - ssid | |
| 22 | - }; | |
| 23 | - config.success = async resp => { | |
| 24 | - if(resp.data.code === 5001000){ | |
| 25 | - wx.setStorageSync('ssid', ''); | |
| 26 | - delete config.ssid; | |
| 27 | - this.request(config); | |
| 28 | - } | |
| 29 | - else{ | |
| 30 | - initialSuccess(resp); | |
| 31 | - } | |
| 32 | - }; | |
| 33 | - config = { | |
| 34 | - header: { | |
| 35 | - 'content-type': 'application/json' | |
| 36 | - }, | |
| 37 | - dataType: 'json', | |
| 38 | - method: 'POST', | |
| 39 | - ...config | |
| 40 | - }; | |
| 41 | - return wx.request(config); | |
| 42 | - }, | |
| 43 | - login(){ | |
| 44 | - let ssid = wx.getStorageSync('ssid'); | |
| 45 | - if(ssid){ | |
| 46 | - return ssid; | |
| 47 | - } | |
| 48 | - wx.showLoading({ | |
| 49 | - title: '登录中...', | |
| 50 | - mask: true | |
| 51 | - }); | |
| 52 | - return new Promise((resolve, reject) => { | |
| 53 | - wx.login({ | |
| 54 | - success: res => { | |
| 55 | - wx.request({ | |
| 56 | - url: `${this.globalData.domain}/open-api/mp/login`, | |
| 57 | - data: { | |
| 58 | - code: res.code, | |
| 59 | - appid: 'wx330e54aa6000516d' | |
| 60 | - }, | |
| 61 | - header: { | |
| 62 | - 'content-type': 'application/json' | |
| 63 | - }, | |
| 64 | - dataType: 'json', | |
| 65 | - method: 'POST', | |
| 66 | - success: resp => { | |
| 67 | - const { | |
| 68 | - ssid, | |
| 69 | - openid | |
| 70 | - } = resp.data?.data?.user; | |
| 71 | - wx.setStorageSync('ssid', ssid); | |
| 72 | - wx.setStorageSync('openid', openid); | |
| 73 | - resolve(ssid); | |
| 74 | - }, | |
| 75 | - fail: resp => { | |
| 76 | - wx.showModal({ | |
| 77 | - title: '提示', | |
| 78 | - content: '登录失败,点击确定重试', | |
| 79 | - showCancel: false, | |
| 80 | - success: res => { | |
| 81 | - this.login(); | |
| 82 | - } | |
| 83 | - }); | |
| 84 | - reject(resp.errMsg); | |
| 85 | - }, | |
| 86 | - complete: () => { | |
| 87 | - wx.hideLoading(); | |
| 88 | - } | |
| 89 | - }); | |
| 90 | - } | |
| 91 | - }); | |
| 92 | - }); | |
| 93 | - }, | |
| 94 | 5 | globalData: { |
| 95 | 6 | // domain: 'http://localhost:3003', |
| 96 | 7 | domain: 'https://api.xwenliang.cn', |
| 8 | + appid: 'wx330e54aa6000516d', | |
| 9 | + cid: '', | |
| 10 | + safeAreaTop: 0, | |
| 11 | + safeAreaBottom: 0, | |
| 12 | + statusBarHeight: 0, | |
| 13 | + navBarHeight: 0, | |
| 14 | + totalNavHeight: 0, // 导航栏总高度(状态栏 + 导航栏) | |
| 15 | + tabBarHeight: 56, | |
| 16 | + totalTabBarHeight: 0, // tab总高度(tab + 底部安全区) | |
| 17 | + contentHeight: 0, // 内容区域高度(全屏高度 - 导航栏总高度 - 底部安全区) | |
| 18 | + contentWithoutTabbarHeight: 0, // 内容区域高度 - tab 高度 | |
| 97 | 19 | getTypeList(){ |
| 98 | 20 | const globalData = this; |
| 99 | 21 | return [ |
| ... | ... | @@ -148,6 +70,29 @@ App({ |
| 148 | 70 | } |
| 149 | 71 | ]; |
| 150 | 72 | }, |
| 73 | + onLaunch(option){ | |
| 74 | + const { query } = option; | |
| 75 | + // 获取渠道值 | |
| 76 | + this.globalData.cid = query.cid || ''; | |
| 77 | + // 挂载 request | |
| 78 | + const { domain, appid, cid } = this.globalData; | |
| 79 | + this.request = createRequest({ domain, appid, cid }); | |
| 80 | + | |
| 81 | + // 获取系统信息,计算安全区域 | |
| 82 | + const windowInfo = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); | |
| 83 | + const { statusBarHeight, safeArea, windowHeight, screenHeight } = windowInfo; | |
| 84 | + this.globalData.statusBarHeight = statusBarHeight; | |
| 85 | + this.globalData.safeAreaTop = safeArea.top; | |
| 86 | + this.globalData.safeAreaBottom = screenHeight - safeArea.bottom; | |
| 87 | + // 右上角胶囊位置信息 | |
| 88 | + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); | |
| 89 | + // 导航栏高度,是胶囊以及胶囊的上下边距所包含的高度,通常是 32+4+4 | |
| 90 | + this.globalData.navBarHeight = menuButtonInfo.height + (menuButtonInfo.top - statusBarHeight) * 2; | |
| 91 | + this.globalData.totalNavHeight = statusBarHeight + this.globalData.navBarHeight; | |
| 92 | + this.globalData.contentHeight = windowHeight - this.globalData.totalNavHeight; | |
| 93 | + this.globalData.totalTabBarHeight = this.globalData.tabBarHeight + this.globalData.safeAreaBottom; | |
| 94 | + this.globalData.contentWithoutTabbarHeight = this.globalData.contentHeight - this.globalData.totalTabBarHeight; | |
| 95 | + }, | |
| 151 | 96 | // TTS接口请求方法 |
| 152 | 97 | async requestTTS(text, voice) { |
| 153 | 98 | return new Promise((resolve, reject) => { |
| ... | ... | @@ -201,8 +146,8 @@ App({ |
| 201 | 146 | return wx.getStorageSync(storageKey) || []; |
| 202 | 147 | }, |
| 203 | 148 | /** |
| 204 | - * @param {(add|delete)} action - 增或删 | |
| 205 | - * @param {object|number} [param] - 增加时是 audio 描述,删除时是唯一特征 path 值 | |
| 149 | + * @param {(add|delete)} action - 增或删 | |
| 150 | + * @param {object|number} [param] - 增加时是 audio 描述,删除时是唯一特征 path 值 | |
| 206 | 151 | */ |
| 207 | 152 | setAudioList(action, param){ |
| 208 | 153 | const storageKey = 'audioList'; | ... | ... |
app.json
| ... | ... | @@ -7,7 +7,8 @@ |
| 7 | 7 | "backgroundTextStyle": "light", |
| 8 | 8 | "navigationBarBackgroundColor": "#fff", |
| 9 | 9 | "navigationBarTitleText": "文字转语音", |
| 10 | - "navigationBarTextStyle": "black" | |
| 10 | + "navigationBarTextStyle": "black", | |
| 11 | + "navigationStyle": "custom" | |
| 11 | 12 | }, |
| 12 | 13 | "sitemapLocation": "sitemap.json" |
| 13 | 14 | } |
| 14 | 15 | \ No newline at end of file | ... | ... |
app.wxss
| 1 | 1 | /**app.wxss**/ |
| 2 | +page { | |
| 3 | + height: 100%; | |
| 4 | + background: #D7E5FA; | |
| 5 | + background-image: url(https://gallery.xwenliang.cn/miniprogram/2025-11-13-ea3e6a30ac.jpg); | |
| 6 | + background-position: top center; | |
| 7 | + background-size: 100% auto; | |
| 8 | + background-repeat: no-repeat; | |
| 9 | + overflow: hidden; | |
| 10 | + width: 100%; | |
| 11 | + height: 100%; | |
| 12 | +} | |
| 13 | + | |
| 14 | +.container-custom { | |
| 15 | + width: 100%; | |
| 16 | + height: 100%; | |
| 17 | + display: flex; | |
| 18 | + flex-direction: column; | |
| 19 | + box-sizing: border-box; | |
| 20 | + overflow-x: hidden; | |
| 21 | + background-repeat: no-repeat; | |
| 22 | +} | |
| 23 | + | |
| 24 | +.container-default { | |
| 25 | + width: 100%; | |
| 26 | + height: 100%; | |
| 27 | + display: flex; | |
| 28 | + flex-direction: column; | |
| 29 | + overflow-x: hidden; | |
| 30 | +} | |
| 31 | + | |
| 32 | +.container-default-box { | |
| 33 | + width: 100%; | |
| 34 | + overflow-x: hidden; | |
| 35 | + overflow-y: auto; | |
| 36 | + box-sizing: border-box; | |
| 37 | + padding: 0 8px; | |
| 38 | +} | |
| 39 | + | |
| 40 | +/* 全局安全区域样式类 */ | |
| 41 | +.safe-area-top { | |
| 42 | + padding-top: constant(safe-area-inset-top); | |
| 43 | + padding-top: env(safe-area-inset-top); | |
| 44 | +} | |
| 45 | + | |
| 46 | +.safe-area-bottom { | |
| 47 | + padding-bottom: constant(safe-area-inset-bottom); | |
| 48 | + padding-bottom: env(safe-area-inset-bottom); | |
| 49 | +} | |
| 50 | + | |
| 51 | +/* 保留原有的 container 类以兼容其他页面 */ | |
| 2 | 52 | .container { |
| 3 | 53 | width: 100%; |
| 4 | 54 | height: 100%; | ... | ... |
pages/index/index.wxml
pages/index/index.wxss
| ... | ... | @@ -7,6 +7,72 @@ page { |
| 7 | 7 | box-sizing: border-box; |
| 8 | 8 | } |
| 9 | 9 | |
| 10 | +/* 动态背景样式 */ | |
| 11 | +.bg { | |
| 12 | + position: fixed; | |
| 13 | + top: 0; | |
| 14 | + left: 0; | |
| 15 | + width: 100%; | |
| 16 | + height: 100%; | |
| 17 | + filter: blur(120px); | |
| 18 | + z-index: -1; | |
| 19 | +} | |
| 20 | + | |
| 21 | +.ball { | |
| 22 | + position: absolute; | |
| 23 | + border-radius: 50%; | |
| 24 | +} | |
| 25 | + | |
| 26 | +.ball-1 { | |
| 27 | + width: 400rpx; | |
| 28 | + height: 400rpx; | |
| 29 | + background-color: #4472c4; | |
| 30 | + animation: move-1 5s infinite; | |
| 31 | +} | |
| 32 | + | |
| 33 | +.ball-2 { | |
| 34 | + width: 500rpx; | |
| 35 | + height: 500rpx; | |
| 36 | + background-color: #00b050; | |
| 37 | + animation: move-2 5s infinite; | |
| 38 | +} | |
| 39 | + | |
| 40 | +@keyframes move-1 { | |
| 41 | + 0% { | |
| 42 | + transform: translate(0, 0); | |
| 43 | + } | |
| 44 | + 25% { | |
| 45 | + transform: translate(200rpx, 150rpx); | |
| 46 | + } | |
| 47 | + 50% { | |
| 48 | + transform: translate(0, 300rpx); | |
| 49 | + } | |
| 50 | + 75% { | |
| 51 | + transform: translate(-100rpx, 150rpx); | |
| 52 | + } | |
| 53 | + 100% { | |
| 54 | + transform: translate(0, 0); | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +@keyframes move-2 { | |
| 59 | + 0% { | |
| 60 | + transform: translate(0, 0); | |
| 61 | + } | |
| 62 | + 25% { | |
| 63 | + transform: translate(-200rpx, -180rpx); | |
| 64 | + } | |
| 65 | + 50% { | |
| 66 | + transform: translate(0, -320rpx); | |
| 67 | + } | |
| 68 | + 75% { | |
| 69 | + transform: translate(100rpx, -180rpx); | |
| 70 | + } | |
| 71 | + 100% { | |
| 72 | + transform: translate(0, 0); | |
| 73 | + } | |
| 74 | +} | |
| 75 | + | |
| 10 | 76 | /* 全局移除按钮边框 */ |
| 11 | 77 | button { |
| 12 | 78 | border: none !important; |
| ... | ... | @@ -21,7 +87,8 @@ button::before { |
| 21 | 87 | border: none !important; |
| 22 | 88 | } |
| 23 | 89 | |
| 24 | -.container { | |
| 90 | +.container-custom { | |
| 91 | + position: relative; | |
| 25 | 92 | width: 100%; |
| 26 | 93 | max-width: 100vw; |
| 27 | 94 | overflow-x: hidden; | ... | ... |
utils/request.js
0 → 100644
| 1 | +// zooble@2025-12-14 | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 创建一个请求函数,该函数闭包了登录和重试逻辑 | |
| 5 | + * @param {object} baseConfig 基础配置 | |
| 6 | + * @param {string} baseConfig.domain API域名 | |
| 7 | + * @param {string} baseConfig.appid 小程序ID | |
| 8 | + * @param {string} base.cid 渠道ID | |
| 9 | + */ | |
| 10 | +function createRequest(baseConfig) { | |
| 11 | + let loginPromise = null; | |
| 12 | + | |
| 13 | + // 登录函数 | |
| 14 | + const login = () => { | |
| 15 | + if (loginPromise) { | |
| 16 | + return loginPromise; | |
| 17 | + } | |
| 18 | + | |
| 19 | + const ssid = wx.getStorageSync('ssid'); | |
| 20 | + if (ssid) { | |
| 21 | + return Promise.resolve(ssid); | |
| 22 | + } | |
| 23 | + | |
| 24 | + wx.showLoading({ mask: true }); | |
| 25 | + | |
| 26 | + loginPromise = new Promise((resolve, reject) => { | |
| 27 | + wx.login({ | |
| 28 | + success: res => { | |
| 29 | + if (!res.code) { | |
| 30 | + const errorMsg = '登录失败:无法获取code'; | |
| 31 | + wx.showModal({ title: '提示', content: errorMsg, showCancel: false }); | |
| 32 | + return reject(new Error(errorMsg)); | |
| 33 | + } | |
| 34 | + | |
| 35 | + const { domain, appid } = baseConfig; | |
| 36 | + wx.request({ | |
| 37 | + url: `${domain}/open-api/mp/login`, | |
| 38 | + data: { code: res.code, appid }, | |
| 39 | + method: 'POST', | |
| 40 | + header: { 'content-type': 'application/json' }, | |
| 41 | + dataType: 'json', | |
| 42 | + success: resp => { | |
| 43 | + const user = resp.data?.data?.user; | |
| 44 | + if (user && user.ssid) { | |
| 45 | + wx.setStorageSync('ssid', user.ssid); | |
| 46 | + wx.setStorageSync('openid', user.openid); | |
| 47 | + resolve(user.ssid); | |
| 48 | + } else { | |
| 49 | + const errorMsg = resp.data.msg || '登录失败:无效的用户信息'; | |
| 50 | + wx.showModal({ title: '提示', content: errorMsg, showCancel: false }); | |
| 51 | + reject(new Error(errorMsg)); | |
| 52 | + } | |
| 53 | + }, | |
| 54 | + fail: err => { | |
| 55 | + wx.showModal({ title: '提示', content: '登录请求失败', showCancel: false }); | |
| 56 | + reject(err); | |
| 57 | + }, | |
| 58 | + complete: () => { | |
| 59 | + wx.hideLoading(); | |
| 60 | + loginPromise = null; | |
| 61 | + } | |
| 62 | + }); | |
| 63 | + }, | |
| 64 | + fail: err => { | |
| 65 | + wx.hideLoading(); | |
| 66 | + loginPromise = null; | |
| 67 | + wx.showModal({ title: '提示', content: '微信登录失败', showCancel: false }); | |
| 68 | + reject(err); | |
| 69 | + } | |
| 70 | + }); | |
| 71 | + }); | |
| 72 | + return loginPromise; | |
| 73 | + }; | |
| 74 | + | |
| 75 | + // 返回的请求函数 | |
| 76 | + const request = async (config) => { | |
| 77 | + let ssid = wx.getStorageSync('ssid'); | |
| 78 | + if (!ssid) { | |
| 79 | + try { | |
| 80 | + ssid = await login(); | |
| 81 | + } catch (e) { | |
| 82 | + if (config.fail) config.fail(e); | |
| 83 | + // early return on login failure | |
| 84 | + return; | |
| 85 | + } | |
| 86 | + } | |
| 87 | + | |
| 88 | + const finalConfig = { | |
| 89 | + header: { 'content-type': 'application/json' }, | |
| 90 | + dataType: 'json', | |
| 91 | + method: 'POST', | |
| 92 | + ...config, | |
| 93 | + data: { | |
| 94 | + ...config.data, | |
| 95 | + ssid, | |
| 96 | + cid: baseConfig.cid, | |
| 97 | + }, | |
| 98 | + success: (resp) => { | |
| 99 | + // 登录态失效,自动重试 | |
| 100 | + if ([5001000, 5004000].includes(resp.data.code)) { | |
| 101 | + console.log('ssid expired, retrying request'); | |
| 102 | + wx.setStorageSync('ssid', ''); | |
| 103 | + // 传入原始 config 递归调用,避免 success 被多次包装 | |
| 104 | + request(config); | |
| 105 | + } else { | |
| 106 | + if (config.success) { | |
| 107 | + config.success(resp); | |
| 108 | + } | |
| 109 | + } | |
| 110 | + }, | |
| 111 | + }; | |
| 112 | + | |
| 113 | + return wx.request(finalConfig); | |
| 114 | + }; | |
| 115 | + | |
| 116 | + return request; | |
| 117 | +}; | |
| 118 | + | |
| 119 | +module.exports = { | |
| 120 | + createRequest | |
| 121 | +}; | ... | ... |