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 | App({ | 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 | globalData: { | 5 | globalData: { |
| 95 | // domain: 'http://localhost:3003', | 6 | // domain: 'http://localhost:3003', |
| 96 | domain: 'https://api.xwenliang.cn', | 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 | getTypeList(){ | 19 | getTypeList(){ |
| 98 | const globalData = this; | 20 | const globalData = this; |
| 99 | return [ | 21 | return [ |
| @@ -148,6 +70,29 @@ App({ | @@ -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 | // TTS接口请求方法 | 96 | // TTS接口请求方法 |
| 152 | async requestTTS(text, voice) { | 97 | async requestTTS(text, voice) { |
| 153 | return new Promise((resolve, reject) => { | 98 | return new Promise((resolve, reject) => { |
| @@ -201,8 +146,8 @@ App({ | @@ -201,8 +146,8 @@ App({ | ||
| 201 | return wx.getStorageSync(storageKey) || []; | 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 | setAudioList(action, param){ | 152 | setAudioList(action, param){ |
| 208 | const storageKey = 'audioList'; | 153 | const storageKey = 'audioList'; |
app.json
| @@ -7,7 +7,8 @@ | @@ -7,7 +7,8 @@ | ||
| 7 | "backgroundTextStyle": "light", | 7 | "backgroundTextStyle": "light", |
| 8 | "navigationBarBackgroundColor": "#fff", | 8 | "navigationBarBackgroundColor": "#fff", |
| 9 | "navigationBarTitleText": "文字转语音", | 9 | "navigationBarTitleText": "文字转语音", |
| 10 | - "navigationBarTextStyle": "black" | 10 | + "navigationBarTextStyle": "black", |
| 11 | + "navigationStyle": "custom" | ||
| 11 | }, | 12 | }, |
| 12 | "sitemapLocation": "sitemap.json" | 13 | "sitemapLocation": "sitemap.json" |
| 13 | } | 14 | } |
| 14 | \ No newline at end of file | 15 | \ No newline at end of file |
app.wxss
| 1 | /**app.wxss**/ | 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 | .container { | 52 | .container { |
| 3 | width: 100%; | 53 | width: 100%; |
| 4 | height: 100%; | 54 | height: 100%; |
pages/index/index.wxml
| 1 | <!--index.wxml--> | 1 | <!--index.wxml--> |
| 2 | -<view class="container"> | 2 | +<view class="container-custom"> |
| 3 | + <!-- 动态背景 --> | ||
| 4 | + <view class="bg"> | ||
| 5 | + <view class="ball ball-1"></view> | ||
| 6 | + <view class="ball ball-2"></view> | ||
| 7 | + </view> | ||
| 3 | <view class="text-view"> | 8 | <view class="text-view"> |
| 4 | <textarea | 9 | <textarea |
| 5 | auto-focus | 10 | auto-focus |
pages/index/index.wxss
| @@ -7,6 +7,72 @@ page { | @@ -7,6 +7,72 @@ page { | ||
| 7 | box-sizing: border-box; | 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 | button { | 77 | button { |
| 12 | border: none !important; | 78 | border: none !important; |
| @@ -21,7 +87,8 @@ button::before { | @@ -21,7 +87,8 @@ button::before { | ||
| 21 | border: none !important; | 87 | border: none !important; |
| 22 | } | 88 | } |
| 23 | 89 | ||
| 24 | -.container { | 90 | +.container-custom { |
| 91 | + position: relative; | ||
| 25 | width: 100%; | 92 | width: 100%; |
| 26 | max-width: 100vw; | 93 | max-width: 100vw; |
| 27 | overflow-x: hidden; | 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 | +}; |