一 Key 調(diào)試 cursor-spacing 實(shí)戰(zhàn))
1. 真機(jī)上鍵盤頂飛輸入框問題到底出在哪小程序里做表單最容易被忽略、又最容易在真機(jī)上翻車的就是 input 和 textarea 在軟鍵盤彈出時(shí)跟輸入框的距離控制。你在開發(fā)者工具里調(diào)得好好的一上真機(jī)鍵盤一彈輸入框要么被擋在鍵盤后面要么整個(gè)頁(yè)面被頂?shù)美细唔敳繕?biāo)題欄都飛出屏幕。這個(gè)現(xiàn)象背后其實(shí)是幾個(gè)屬性在共同作用cursor-spacing、adjust-position、hold-keyboard還有 textarea 特有的 fixed 和 auto-height。先說清楚這幾個(gè)東西分別管什么。cursor-spacing 決定的是「光標(biāo)和鍵盤之間留多少距離」單位是 px默認(rèn) 0。它的計(jì)算邏輯有點(diǎn)繞取輸入框距離頁(yè)面底部的距離和 cursor-spacing 指定的距離兩者取最小值作為光標(biāo)與鍵盤的實(shí)際距離。也就是說你設(shè)了 100但輸入框本身離底部只有 40那最終就是 40。adjust-position 默認(rèn) true鍵盤彈起時(shí)頁(yè)面會(huì)自動(dòng)上推把輸入框頂?shù)芥I盤上方設(shè)成 false 就完全不推鍵盤直接蓋住內(nèi)容。hold-keyboard 默認(rèn) falsefocus 狀態(tài)下點(diǎn)頁(yè)面其他地方鍵盤會(huì)收起設(shè)成 true 就保持不收起。textarea 比 input 多兩個(gè)坑。一個(gè)是 fixed 屬性如果你的 textarea 放在 position:fixed 的容器里必須顯式寫 fixed{{true}}否則鍵盤彈起時(shí)定位會(huì)錯(cuò)亂輸入框跑到奇怪的位置。另一個(gè)是 auto-height開了自動(dòng)增高后 style.height 就不生效了高度完全由內(nèi)容撐開這時(shí)候 cursor-spacing 的表現(xiàn)也會(huì)跟著變。我遇到最典型的一個(gè)場(chǎng)景底部固定一個(gè)輸入欄里面放 textarea用戶點(diǎn)開鍵盤輸入欄被頂上去但頂?shù)奈恢貌粚?duì)光標(biāo)貼著鍵盤邊緣打字時(shí)手指剛好擋住正在輸入的那一行。這就是 cursor-spacing 沒設(shè)或者設(shè)了但被「輸入框距底部距離」這個(gè)上限卡住了。要復(fù)現(xiàn)和修正這類問題光靠開發(fā)者工具不夠得在真機(jī)上按機(jī)型驗(yàn)證而驗(yàn)證過程里會(huì)頻繁發(fā)請(qǐng)求調(diào)試這時(shí)候用 TaoToken 統(tǒng)一管理 Key 和 API 通道就省事很多不用每次換環(huán)境都改一堆配置。這篇就圍繞這幾個(gè)屬性給出可復(fù)制的 WXML/WXSS 片段、真機(jī)驗(yàn)證步驟以及怎么用 TaoToken 把調(diào)試請(qǐng)求集中管起來。適合正在做小程序表單、被鍵盤遮擋折磨過的開發(fā)者。2. 用 TaoToken 統(tǒng)一 Key 管理調(diào)試請(qǐng)求的前置準(zhǔn)備調(diào)試鍵盤遮擋問題表面上是調(diào) UI實(shí)際上你會(huì)反復(fù)做一件事改屬性、真機(jī)預(yù)覽、發(fā)請(qǐng)求看后端返回、再改。如果每次都在代碼里硬編碼 API Key或者在不同環(huán)境之間手動(dòng)切換調(diào)試節(jié)奏會(huì)被打斷。TaoToken 在這里的作用是把 Key 和 API 通道集中管理你只需要在配置里指向統(tǒng)一的 Base URL用同一個(gè) Key就能在調(diào)試階段穩(wěn)定發(fā)請(qǐng)求。先明確幾個(gè)地址。官網(wǎng)入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基礎(chǔ)地址是 https://taotoken.net/api 注意這個(gè)不帶 UTM 參數(shù)。控制臺(tái)在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite API Keys 管理頁(yè)在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。模型對(duì)話調(diào)試可以走 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 接入文檔在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。如果你后面要做長(zhǎng)期編碼或者 Agent 類的東西Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。前置準(zhǔn)備分三步。第一步去 API Keys 頁(yè)面創(chuàng)建一個(gè) Key復(fù)制出來。第二步確定你的請(qǐng)求走哪個(gè)模型Model ID 要記下來比如常見的對(duì)話模型 ID。第三步把 Base URL、Key、Model ID 這三件套寫進(jìn)你的調(diào)試配置里。這三件套是后面所有請(qǐng)求的基礎(chǔ)缺一個(gè)都發(fā)不出去。為什么要強(qiáng)調(diào)「統(tǒng)一」因?yàn)樾〕绦蛘{(diào)試經(jīng)常要在真機(jī)和工具之間來回切有時(shí)候還要用不同的測(cè)試賬號(hào)。如果 Key 散落在各個(gè)文件里改一次要翻半天。集中到一個(gè)配置對(duì)象里改一處全生效。下面給一個(gè)可以直接抄的配置結(jié)構(gòu)放在小程序的 utils 或者 config 目錄下// config/api.js const API_CONFIG { baseURL: https://taotoken.net/api, apiKey: sk-你的Key填這里, modelID: 你的ModelID, timeout: 30000 }; module.exports API_CONFIG;然后在請(qǐng)求封裝里引用它// utils/request.js const API_CONFIG require(../config/api.js); function chatRequest(messages) { return new Promise((resolve, reject) { wx.request({ url: ${API_CONFIG.baseURL}/v1/chat/completions, method: POST, timeout: API_CONFIG.timeout, header: { Content-Type: application/json, Authorization: Bearer ${API_CONFIG.apiKey} }, data: { model: API_CONFIG.modelID, messages: messages }, success: (res) { if (res.statusCode 200) { resolve(res.data); } else { reject(res); } }, fail: reject }); }); } module.exports { chatRequest };這樣你在調(diào)試鍵盤問題時(shí)如果需要一個(gè)后端返回來觸發(fā)某些 UI 狀態(tài)直接調(diào) chatRequest 就行不用關(guān)心 Key 在哪。注意小程序正式發(fā)布時(shí)請(qǐng)求域名要在后臺(tái)配置合法域名調(diào)試階段可以在開發(fā)者工具里勾選「不校驗(yàn)合法域名」。這個(gè)配置結(jié)構(gòu)的好處是等你調(diào)試完鍵盤問題這套請(qǐng)求封裝可以直接復(fù)用到業(yè)務(wù)里不用重寫。3. 可復(fù)制的 WXML/WXSS 配置片段與屬性組合這一節(jié)直接給能跑的代碼。先看一個(gè)底部固定輸入欄的典型結(jié)構(gòu)textarea 放在 fixed 容器里這是最容易出問題的寫法也是最能體現(xiàn)幾個(gè)屬性配合的場(chǎng)景。!-- pages/chat/chat.wxml -- view classpage scroll-view classmsg-list scroll-y view wx:for{{messages}} wx:keyid classmsg-item{{item.text}}/view /scroll-view view classinput-bar textarea classinput-area value{{inputValue}} placeholder說點(diǎn)什么 fixed{{true}} auto-height{{true}} cursor-spacing20 adjust-position{{true}} hold-keyboard{{true}} show-confirm-bar{{false}} bindinputonInput bindfocusonFocus bindbluronBlur / button classsend-btn bindtaponSend發(fā)送/button /view /view對(duì)應(yīng)的 WXSS/* pages/chat/chat.wxss */ .page { display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } .msg-list { flex: 1; overflow: hidden; } .input-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; align-items: flex-end; padding: 12rpx 20rpx; padding-bottom: calc(12rpx env(safe-area-inset-bottom)); background: #ffffff; border-top: 1rpx solid #eeeeee; } .input-area { flex: 1; min-height: 72rpx; max-height: 240rpx; padding: 16rpx 20rpx; font-size: 30rpx; line-height: 1.4; background: #f5f5f5; border-radius: 12rpx; box-sizing: border-box; } .send-btn { margin-left: 16rpx; font-size: 28rpx; }這段代碼里有幾個(gè)關(guān)鍵點(diǎn)要解釋。fixed{{true}} 是必須的因?yàn)?input-bar 用了 position:fixedtextarea 在 fixed 區(qū)域里不寫這個(gè)屬性鍵盤彈起時(shí)定位會(huì)飄。auto-height 開了之后textarea 會(huì)隨內(nèi)容長(zhǎng)高配合 max-height 限制最高高度超過就內(nèi)部滾動(dòng)。cursor-spacing20 是給光標(biāo)和鍵盤之間留 20px避免光標(biāo)貼著鍵盤邊緣。adjust-position 保持 true讓頁(yè)面自動(dòng)上推。hold-keyboard 設(shè) true這樣用戶點(diǎn)發(fā)送按鈕時(shí)鍵盤不會(huì)先收起再觸發(fā)體驗(yàn)更順。如果你用的是 input 而不是 textarea結(jié)構(gòu)類似但要注意 input 沒有 fixed 和 auto-height 這兩個(gè)屬性。input 在 fixed 容器里不需要額外聲明但 cursor-spacing 的計(jì)算邏輯一樣。下面是一個(gè) input 版本的關(guān)鍵片段view classsearch-bar input classsearch-input value{{keyword}} placeholder搜索 cursor-spacing30 adjust-position{{true}} confirm-typesearch confirm-hold{{true}} bindconfirmonSearch / /viewconfirm-hold 設(shè) true 表示點(diǎn)鍵盤右下角按鈕時(shí)鍵盤不收起適合搜索場(chǎng)景連續(xù)輸入。confirm-type 設(shè)成 search鍵盤右下角按鈕會(huì)顯示「搜索」。屬性組合不是隨便堆的給你一張對(duì)照表按場(chǎng)景選場(chǎng)景cursor-spacingadjust-positionhold-keyboard說明底部固定輸入欄20–40truetrue防止光標(biāo)貼鍵盤點(diǎn)按鈕不收起頁(yè)面中部表單0–20truefalse默認(rèn)上推即可全屏聊天20truetrue配合 fixed 容器不希望頁(yè)面被頂0falsefalse自己控制滾動(dòng)位置這里有個(gè)容易踩的坑cursor-spacing 設(shè)得再大也不會(huì)超過「輸入框距底部距離」這個(gè)上限。所以如果你發(fā)現(xiàn)設(shè)了 100 但實(shí)際只留了 30不是屬性沒生效是輸入框本身離底部就只有 30。解決辦法是把輸入框往上挪或者接受這個(gè)上限。4. 真機(jī)驗(yàn)證請(qǐng)求與成功結(jié)果確認(rèn)代碼寫完了得在真機(jī)上驗(yàn)證。開發(fā)者工具的模擬鍵盤和真機(jī)行為差異很大尤其是 adjust-position 和 fixed 的表現(xiàn)必須真機(jī)跑。驗(yàn)證步驟我按順序列一下。第一步用微信開發(fā)者工具打開項(xiàng)目點(diǎn)「預(yù)覽」用手機(jī)掃碼。確保手機(jī)和電腦在同一網(wǎng)絡(luò)下或者直接用真機(jī)調(diào)試模式。第二步進(jìn)入帶輸入框的頁(yè)面點(diǎn)擊 textarea 或 input觀察鍵盤彈起時(shí)輸入框的位置。重點(diǎn)看三件事輸入框有沒有被鍵盤擋住光標(biāo)和鍵盤之間有沒有留出 cursor-spacing 設(shè)定的距離頁(yè)面頂部有沒有被頂出屏幕。第三步在輸入狀態(tài)下點(diǎn)一下頁(yè)面其他區(qū)域看鍵盤是否收起。如果 hold-keyboard 設(shè)了 true點(diǎn)發(fā)送按鈕時(shí)鍵盤應(yīng)該保持直到你手動(dòng)收起。第四步切換不同機(jī)型驗(yàn)證。iOS 和 Android 的鍵盤高度不一樣iOS 還有安全區(qū)域Android 各家輸入法高度也不同。至少測(cè)一臺(tái) iOS 和一臺(tái) Android。驗(yàn)證過程中如果你需要發(fā)請(qǐng)求確認(rèn)后端返回或者觸發(fā)某些依賴接口的 UI 狀態(tài)用前面封裝的 chatRequest。下面給一個(gè)驗(yàn)證用的調(diào)用示例確認(rèn)請(qǐng)求通道是通的// pages/chat/chat.js const { chatRequest } require(../../utils/request.js); Page({ data: { inputValue: , messages: [] }, onInput(e) { this.setData({ inputValue: e.detail.value }); }, onFocus(e) { console.log(focus, 鍵盤高度相關(guān):, e.detail.height); }, onBlur() { console.log(blur); }, async onSend() { const text this.data.inputValue.trim(); if (!text) return; this.setData({ messages: [...this.data.messages, { id: Date.now(), text }], inputValue: }); try { const res await chatRequest([ { role: user, content: text } ]); const reply res.choices res.choices[0] ? res.choices[0].message.content : 無返回; this.setData({ messages: [...this.data.messages, { id: Date.now() 1, text: reply }] }); } catch (err) { console.error(請(qǐng)求失敗, err); } } });成功的結(jié)果是請(qǐng)求返回 200res.choices[0].message.content 有內(nèi)容消息列表里能看到回復(fù)。同時(shí)鍵盤行為符合預(yù)期輸入框在鍵盤上方光標(biāo)和鍵盤之間有間距點(diǎn)發(fā)送鍵盤不閃退。如果你在 onFocus 里打印 e.detail.height能看到當(dāng)前鍵盤的高度這個(gè)值在不同機(jī)型上不一樣可以用來動(dòng)態(tài)調(diào)整 cursor-spacing。比如onFocus(e) { const keyboardHeight e.detail.height; this.setData({ cursorSpacing: keyboardHeight 300 ? 40 : 20 }); }然后在 WXML 里綁定 cursor-spacing{{cursorSpacing}}。這樣能按機(jī)型自適應(yīng)比寫死一個(gè)值穩(wěn)。真機(jī)驗(yàn)證時(shí)還要注意一個(gè)現(xiàn)象iOS 上如果 textarea 在 fixed 容器里沒寫 fixed{{true}}鍵盤彈起時(shí)整個(gè)輸入欄可能會(huì)跳到屏幕中間或者被鍵盤蓋住一半。Android 上則可能表現(xiàn)為輸入欄位置正確但光標(biāo)位置偏移。這些都是 fixed 屬性缺失的典型癥狀。5. 本篇常見報(bào)錯(cuò)與排查對(duì)照調(diào)試過程中會(huì)遇到幾類典型報(bào)錯(cuò)這里按真實(shí)錯(cuò)誤信息對(duì)照排查。第一類請(qǐng)求返回 401。這個(gè)最常見說明 Key 不對(duì)或者沒帶上。檢查 Authorization 頭是不是Bearer sk-xxx格式中間有沒有多余空格。檢查 Key 是不是從 API Keys 頁(yè)面復(fù)制的完整值有沒有漏字符。如果 Key 是對(duì)的還報(bào) 401去控制臺(tái)確認(rèn)這個(gè) Key 有沒有被禁用或者額度用完。排查入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。第二類local proxy failed 或者連接超時(shí)。這個(gè)通常是小程序請(qǐng)求域名沒配或者開發(fā)者工具沒勾「不校驗(yàn)合法域名」。真機(jī)上如果沒在微信后臺(tái)配置 request 合法域名請(qǐng)求會(huì)直接失敗。調(diào)試階段先在工具里勾選不校驗(yàn)真機(jī)調(diào)試用真機(jī)調(diào)試模式。如果確認(rèn)域名沒問題還超時(shí)檢查 baseURL 是不是寫成了 https://taotoken.net/api 后面多加了斜杠或者路徑。第三類reading choices 報(bào)錯(cuò)類似Cannot read property choices of undefined。這是返回結(jié)構(gòu)沒按預(yù)期解析。先打印完整 res 看結(jié)構(gòu)確認(rèn) res.data 里有沒有 choices。如果返回的是錯(cuò)誤對(duì)象先處理錯(cuò)誤分支再取 choices。上面的示例代碼里已經(jīng)做了res.choices res.choices[0]的判斷能避免這個(gè)報(bào)錯(cuò)。第四類OAuth 相關(guān)報(bào)錯(cuò)。如果你用的是某些需要 OAuth 流程的接入方式報(bào)錯(cuò)信息里會(huì)出現(xiàn) OAuth 字樣。這類問題通常是回調(diào)地址或者 token 換取環(huán)節(jié)配置不對(duì)。檢查你的接入方式對(duì)應(yīng)的文檔確認(rèn)回調(diào) URL 和參數(shù)。文檔入口在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。第五類鍵盤相關(guān)的問題不算報(bào)錯(cuò)但表現(xiàn)像 bug。比如輸入框被遮擋、頁(yè)面被頂飛、光標(biāo)貼鍵盤。對(duì)照排查被遮擋看 adjust-position 是不是 false 了被頂飛看是不是沒設(shè) cursor-spacing 導(dǎo)致上推過多光標(biāo)貼鍵盤看 cursor-spacing 是不是 0 或者被輸入框距底部距離卡住fixed 容器里定位錯(cuò)亂看 textarea 有沒有寫 fixed{{true}}。如果你用的是 CC Switch、Cline MCP 或者 Codex 這類工具做輔助調(diào)試配置里同樣要寫全三件套Base URL 填 https://taotoken.net/api Key 填你的 KeyModel ID 填對(duì)應(yīng)模型。三件套缺一個(gè)都連不上。這類工具在調(diào)試鍵盤問題時(shí)能幫你快速發(fā)請(qǐng)求驗(yàn)證后端但配置別漏項(xiàng)。還有一個(gè)隱蔽的坑textarea 的 auto-height 和 cursor-spacing 同時(shí)用時(shí)如果內(nèi)容很少textarea 高度很小cursor-spacing 的實(shí)際生效值會(huì)被「輸入框距底部距離」限制。這時(shí)候要么把輸入框往上抬要么接受較小的間距。實(shí)測(cè)下來把 cursor-spacing 設(shè)在 20 到 40 之間配合 adjust-position 和 fixed大部分機(jī)型都能正常。6. 把調(diào)試通道固定下來后續(xù)接入更省事鍵盤遮擋這類問題調(diào)一次可能就過去了但調(diào)試過程中建立的請(qǐng)求通道可以留下來。把 Base URL、Key、Model ID 三件套固定在一個(gè)配置文件里后面做其他功能時(shí)直接復(fù)用不用每次重新配。模型對(duì)話調(diào)試可以走 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 接入文檔在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。如果你后面要做長(zhǎng)期的編碼輔助或者 Agent 類功能Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite ?;氐芥I盤問題本身記住三個(gè)屬性的分工cursor-spacing 管間距adjust-position 管上推hold-keyboard 管收起時(shí)機(jī)textarea 額外注意 fixed 和 auto-height。真機(jī)驗(yàn)證時(shí)按機(jī)型測(cè)iOS 和 Android 各至少一臺(tái)。遇到遮擋先看 adjust-position遇到頂飛先看 cursor-spacing遇到定位錯(cuò)亂先看 fixed。這套排查順序能覆蓋大部分場(chǎng)景。