[{"data":1,"prerenderedAt":2304},["ShallowReactive",2],{"app-docsNavigation":3,"docsSearch":94,"app-blogNavigation":854,"blogSearch":940,"docsNavigation":2203,"\u002Fdocs\u002Fgetting-started\u002Ffile-preview":2224,"\u002Fdocs\u002Fgetting-started\u002Ffile-preview-surround":2301},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":93},"备忘录","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17,22,27,32,37,42,47,52,57,62,67],{"title":10,"path":6,"stem":7,"icon":11},"区块链相关(前端)","i-lucide-box",{"title":13,"path":14,"stem":15,"icon":16},"更新 uni modules 依赖","\u002Fdocs\u002Fgetting-started\u002Funi-modules-update","1.docs\u002F1.getting-started\u002F10.uni-modules-update","i-lucide-refresh-cw",{"title":18,"path":19,"stem":20,"icon":21},"文件预览","\u002Fdocs\u002Fgetting-started\u002Ffile-preview","1.docs\u002F1.getting-started\u002F11.file-preview","i-lucide-file-search",{"title":23,"path":24,"stem":25,"icon":26},"和 AI 一起写代码：五条心智 + Review 四维","\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset","1.docs\u002F1.getting-started\u002F12.ai-coding-mindset","i-lucide-brain-circuit",{"title":28,"path":29,"stem":30,"icon":31},"Bing 搜索结果不显示网站图标","\u002Fdocs\u002Fgetting-started\u002Fbing-search-favicon","1.docs\u002F1.getting-started\u002F13.bing-search-favicon","i-lucide-search",{"title":33,"path":34,"stem":35,"icon":36},"图片压缩","\u002Fdocs\u002Fgetting-started\u002Fphoto-compression","1.docs\u002F1.getting-started\u002F2.photo-compression","i-lucide-image",{"title":38,"path":39,"stem":40,"icon":41},"组件库","\u002Fdocs\u002Fgetting-started\u002Fcomponent-library","1.docs\u002F1.getting-started\u002F3.component-library","i-lucide-puzzle",{"title":43,"path":44,"stem":45,"icon":46},"图表库","\u002Fdocs\u002Fgetting-started\u002Fcharts","1.docs\u002F1.getting-started\u002F4.charts","i-lucide-bar-chart-3",{"title":48,"path":49,"stem":50,"icon":51},"CSS","\u002Fdocs\u002Fgetting-started\u002Fcss","1.docs\u002F1.getting-started\u002F5.css","i-lucide-palette",{"title":53,"path":54,"stem":55,"icon":56},"调试测试工具","\u002Fdocs\u002Fgetting-started\u002Ftest","1.docs\u002F1.getting-started\u002F6.test","i-lucide-bug",{"title":58,"path":59,"stem":60,"icon":61},"uni-helper","\u002Fdocs\u002Fgetting-started\u002Funi-helper","1.docs\u002F1.getting-started\u002F7.uni-helper","i-lucide-settings",{"title":63,"path":64,"stem":65,"icon":66},"工作流可视化库","\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries","1.docs\u002F1.getting-started\u002F8.workflow-libraries","i-lucide-workflow",{"title":68,"icon":69,"path":70,"stem":71,"children":72,"page":93},"Skills","i-lucide-sparkles","\u002Fdocs\u002Fgetting-started\u002Fskills","1.docs\u002F1.getting-started\u002F9.skills",[73,78,83,88],{"title":74,"path":75,"stem":76,"icon":77},"Superpowers","\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers","1.docs\u002F1.getting-started\u002F9.skills\u002F1.superpowers","i-lucide-zap",{"title":79,"path":80,"stem":81,"icon":82},"Frontend Design","\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design","1.docs\u002F1.getting-started\u002F9.skills\u002F2.frontend-design","i-lucide-paintbrush",{"title":84,"path":85,"stem":86,"icon":87},"Component-First Guardian","\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcomponent-first-guardian","1.docs\u002F1.getting-started\u002F9.skills\u002F3.component-first-guardian","i-lucide-shield-check",{"title":89,"path":90,"stem":91,"icon":92},"Caveman","\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman","1.docs\u002F1.getting-started\u002F9.skills\u002F4.caveman","i-lucide-component",false,[95,99,104,110,116,121,126,131,135,140,146,149,154,159,162,167,171,176,181,185,189,193,197,200,204,208,212,215,220,223,228,233,236,241,245,250,255,260,265,270,275,280,285,290,295,300,305,310,313,316,321,326,331,334,339,343,346,350,355,359,363,368,372,375,380,384,388,393,397,400,405,409,412,417,421,424,429,433,437,442,446,450,453,458,463,468,473,478,481,486,491,496,501,506,511,514,519,522,527,531,536,541,546,550,555,560,564,569,574,579,584,589,592,597,602,607,611,616,621,626,631,635,639,643,647,652,656,661,665,670,675,679,683,687,691,694,698,703,707,712,717,722,726,730,733,737,741,746,751,756,761,766,771,775,778,782,787,792,797,802,805,809,812,817,822,827,831,836,841,846,850],{"id":6,"title":10,"titles":96,"content":97,"level":98},[],"涉及前端连接钱包,签名,读写合约等.",1,{"id":100,"title":101,"titles":102,"content":103,"level":98},"\u002Fdocs\u002Fgetting-started#区块链前端开发常用库","区块链前端开发常用库",[],"",{"id":105,"title":106,"titles":107,"content":108,"level":109},"\u002Fdocs\u002Fgetting-started#web3js","Web3.js",[101],"web3.js 是一个广泛使用的 Ethereum JavaScript API 库，允许开发者与以太坊区块链进行交互。",2,{"id":111,"title":112,"titles":113,"content":114,"level":115},"\u002Fdocs\u002Fgetting-started#主要功能","主要功能",[101,106],"连接以太坊节点发送交易和查询链上数据智能合约交互账户管理",3,{"id":117,"title":118,"titles":119,"content":120,"level":115},"\u002Fdocs\u002Fgetting-started#基本用法示例","基本用法示例",[101,106],"import Web3 from 'web3';\n\n\u002F\u002F 连接到钱包提供商\nconst web3 = new Web3(window.ethereum);\n\n\u002F\u002F 获取账户地址\nconst accounts = await web3.eth.getAccounts();\n\n\u002F\u002F 调用智能合约\nconst contract = new web3.eth.Contract(abi, contractAddress);",{"id":122,"title":123,"titles":124,"content":125,"level":115},"\u002Fdocs\u002Fgetting-started#官方文档","官方文档",[101,106],"官方文档GitHub 仓库",{"id":127,"title":128,"titles":129,"content":130,"level":109},"\u002Fdocs\u002Fgetting-started#wagmi","Wagmi",[101],"wagmi 是一个基于 React 的区块链开发工具库，兼容Javascript，Vue，简化了与 EVM 兼容链的交互逻辑，内置了常见的区块链操作钩子，支持多链和钱包集成。",{"id":132,"title":112,"titles":133,"content":134,"level":115},"\u002Fdocs\u002Fgetting-started#主要功能-1",[101,128],"基于 React Hooks 的 API 设计，简化状态管理内置钱包连接（MetaMask、Coinbase Wallet 等）智能合约交互封装（读 \u002F 写操作、事件监听）多链支持与链切换管理交易状态跟踪与错误处理",{"id":136,"title":137,"titles":138,"content":139,"level":115},"\u002Fdocs\u002Fgetting-started#安装以vue为示例","安装(以Vue为示例)",[101,128],"pnpm add @wagmi\u002Fvue viem@2.x @tanstack\u002Fvue-query\nnpm install @wagmi\u002Fvue viem@2.x @tanstack\u002Fvue-query\nyarn add @wagmi\u002Fvue viem@2.x @tanstack\u002Fvue-query\nbun add @wagmi\u002Fvue viem@2.x @tanstack\u002Fvue-query Viem 是以太坊的TypeScript接口，用于执行区块链操作。TanStack Query 是一个处理请求、缓存等的Apache状态管理器。TypeScript是可选的，但强烈推荐。了解更多关于TypeScript support .",{"id":141,"title":142,"titles":143,"content":144,"level":145},"\u002Fdocs\u002Fgetting-started#将插件添加到应用程序","将插件添加到应用程序",[101,128,137],"import { createConfig, http } from '@wagmi\u002Fvue'\nimport { mainnet, sepolia } from '@wagmi\u002Fvue\u002Fchains'\n\nexport const config = createConfig({\n  chains: [mainnet, sepolia],\n  transports: {\n    [mainnet.id]: http(),\n    [sepolia.id]: http(),\n  },\n})\nimport { QueryClient, VueQueryPlugin } from '@tanstack\u002Fvue-query'\nimport { WagmiPlugin } from '@wagmi\u002Fvue'\nimport { createApp } from 'vue'\nimport { config } from '.\u002Fplugins\u002FwagmiConfig'\nimport App from '.\u002FApp.vue'\n\nconst queryClient = new QueryClient()\n\ncreateApp(App)\n        .use(WagmiPlugin, { config })\n        .use(VueQueryPlugin, { queryClient })\n        .mount('#app') 查看 WagmiPlugin文档 以了解有关插件API的更多信息.",4,{"id":147,"title":118,"titles":148,"content":103,"level":115},"\u002Fdocs\u002Fgetting-started#基本用法示例-1",[101,128],{"id":150,"title":151,"titles":152,"content":153,"level":145},"\u002Fdocs\u002Fgetting-started#获取用户钱包地址","获取用户钱包地址",[101,128,118],"\u003Cscript setup lang=\"ts\">\n  import { useAccount, useEnsName } from '@wagmi\u002Fvue'\n\n  const { address } = useAccount() \u002F\u002F获取用户连接账号\n  const { data, error, status } = useEnsName({ address })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"status === 'pending'\">Loading ENS name\u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"status === 'error'\">\n    Error fetching ENS name: {{error.message}}\n  \u003C\u002Fdiv>\n  \u003Cdiv v-else>ENS name: {{data}}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>",{"id":155,"title":156,"titles":157,"content":158,"level":145},"\u002Fdocs\u002Fgetting-started#智能合约交互","智能合约交互",[101,128,118],"\u003Cscript setup lang=\"ts\">\n  import { useReadContract } from 'wagmi'\n  import { wagmiContractConfig } from '.\u002Fcontracts'\n\u002F\u002F读取用户账户的地址为0x03A71968491d55603FFe1b11A9e23eF013f75bCF代币余额\n  const { data: balance } = useReadContract({\n    ...wagmiContractConfig,\n    functionName: 'balanceOf',\n    args: ['0x03A71968491d55603FFe1b11A9e23eF013f75bCF'],\n  })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>Balance: {{ balance?.toString() }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\nexport const wagmiContractConfig = {\naddress: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',\nabi: [\n{\ntype: 'function',\nname: 'balanceOf',\nstateMutability: 'view',\ninputs: [{ name: 'account', type: 'address' }],\noutputs: [{ type: 'uint256' }],\n},\n{\ntype: 'function',\nname: 'totalSupply',\nstateMutability: 'view',\ninputs: [],\noutputs: [{ name: 'supply', type: 'uint256' }],\n},\n],\n} as const\n\u003Cscript setup lang=\"ts\">\n  import {\n    useWaitForTransactionReceipt,\n    useWriteContract\n  } from 'wagmi'\n  import { abi } from '.\u002Fabi'\n\n  const {\n    data: hash,\n    isPending,\n    writeContract\n  } = useWriteContract()\n\n  function submit(event: Event) {\n    const formData = new FormData(e.target as HTMLFormElement)\n    const tokenId = formData.get('tokenId') as string\n    \u002F\u002F0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2为合约地址 调用mint方法 参数为tokenId\n    writeContract({\n      address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',\n      abi,\n      functionName: 'mint',\n      args: [BigInt(tokenId)],\n    })\n  }\n\n  const { isLoading: isConfirming, isSuccess: isConfirmed } =\n          useWaitForTransactionReceipt({\n            hash,\n          })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n\u003Cform @submit.prevent=\"submit\">\n  \u003Cinput name=\"tokenId\" placeholder=\"69420\" required \u002F>\n  \u003Cbutton :disabled=\"isPending\" type=\"submit\">\n    \u003Cspan v-if=\"isPending\">Sending...\u003C\u002Fspan>\n    \u003Cspan v-else>Send\u003C\u002Fspan>\n  \u003C\u002Fbutton>\n  \u003Cdiv v-if=\"hash\">Transaction Hash: {{ hash }}\u003C\u002Fdiv>\n  \u003Cdiv v-if=\"isConfirming\">Waiting for confirmation...\u003C\u002Fdiv>\n  \u003Cdiv v-if=\"isConfirmed\">Transaction Confirmed!\u003C\u002Fdiv>\n\u003C\u002Fform>\n\u003C\u002Ftemplate>\nexport const abi = [\n  {\n    name: 'mint',\n    type: 'function',\n    stateMutability: 'nonpayable',\n    inputs: [{ internalType: 'uint32', name: 'tokenId', type: 'uint32' }],\n    outputs: [],\n  },\n] as const",{"id":160,"title":123,"titles":161,"content":125,"level":115},"\u002Fdocs\u002Fgetting-started#官方文档-1",[101,128],{"id":163,"title":164,"titles":165,"content":166,"level":109},"\u002Fdocs\u002Fgetting-started#reown","Reown",[101],"Reown使构建者能够创建安全，用户友好和富有洞察力的钱包和应用程序UX。提供所有工具，以更快地启动和更智能地扩展。",{"id":168,"title":112,"titles":169,"content":170,"level":115},"\u002Fdocs\u002Fgetting-started#主要功能-2",[101,164],"支持多种钱包的无缝连接提供标准化的连接流程支持移动端和桌面端钱包管理多个应用的连接会话提供会话状态的实时同步支持会话的持久化存储支持主流区块链网络",{"id":172,"title":173,"titles":174,"content":175,"level":115},"\u002Fdocs\u002Fgetting-started#安装以vuewagmi为示例","安装(以Vue+wagmi为示例)",[101,164],"pnpm add @reown\u002Fappkit @reown\u002Fappkit-adapter-wagmi @tanstack\u002Fvue-query @wagmi\u002Fvue viem\nyarn add @@reown\u002Fappkit @reown\u002Fappkit-adapter-wagmi @tanstack\u002Fvue-query @wagmi\u002Fvue viem\nnpm install @reown\u002Fappkit @reown\u002Fappkit-adapter-wagmi @tanstack\u002Fvue-query @wagmi\u002Fvue viem\nbun add @reown\u002Fappkit @reown\u002Fappkit-adapter-wagmi @tanstack\u002Fvue-query @wagmi\u002Fvue viem",{"id":177,"title":178,"titles":179,"content":180,"level":115},"\u002Fdocs\u002Fgetting-started#申请projectid","申请ProjectId",[101,164],"前往Reown仪表板并立即创建一个新项目！",{"id":182,"title":178,"titles":183,"content":184,"level":145},"\u002Fdocs\u002Fgetting-started#申请projectid-1",[101,164,178],"在Reown仪表板上创建新项目，\n并获取新的项目ID。",{"id":186,"title":142,"titles":187,"content":188,"level":145},"\u002Fdocs\u002Fgetting-started#将插件添加到应用程序-1",[101,164,178],"import type { AppKitNetwork } from '@reown\u002Fappkit\u002Fnetworks'\n\nimport { WagmiAdapter } from '@reown\u002Fappkit-adapter-wagmi'\nimport { bsc, bscTestnet } from '@reown\u002Fappkit\u002Fnetworks'\n\nexport const projectId = import.meta.env.VITE_PROJECT_ID\n\nexport const metadata = {\n  name: 'AppKit',\n  description: 'AppKit Example',\n  url: 'http:\u002F\u002F192.168.0.209:9001',\n  icons: ['https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179229932'],\n}\n\u002F\u002F 3. Set the networks\nexport const network = import.meta.env.MODE === 'production' ? bsc : bscTestnet\nexport const networks: [AppKitNetwork, ...AppKitNetwork[]] = [network]\n\n\u002F\u002F 4. Create Wagmi Adapter\nexport const wagmiAdapter = new WagmiAdapter({\n  networks,\n  projectId,\n})\n\n\u003Cscript lang=\"ts\" setup>\n  import {createAppKit} from '@reown\u002Fappkit\u002Fvue'\n  import {metadata, networks, projectId, wagmiAdapter} from '.\u002Fplugins\u002FwagmiConfig'\n\n  const modal = createAppKit({\n    adapters: [wagmiAdapter],\n    networks,\n    projectId,\n    metadata,\n    features: {\n      email: false,\n      analytics: true,\n      socials: [],\n    },\n\n    enableReconnect: true,\n\u002F\u002F --w3m-font-family基本字体系列string\n\u002F\u002F --w3m-accent用于按钮、图标、标签等的颜色。string\n\u002F\u002F --w3m-color-mix与默认颜色混合的颜色string\n\u002F\u002F --w3m-color-mix-strength“—w3m-color-mix” 应该混合的百分比number\n\u002F\u002F --w3m-font-size-master字体的基本像素大小。string\n\u002F\u002F --w3m-border-radius-master基本边框半径（以像素为单位）。string\n\u002F\u002F --w3m-z-index模态框的 z-index。number\n    themeVariables: {\n      '--w3m-accent': '#00BB7F',\n      '--w3m-font-size-master': '10px',\n    },\n  })\n\u003C\u002Fscript>\n\u003Ctemplate>\u003C\u002Ftemplate>\nimport { QueryClient, VueQueryPlugin } from '@tanstack\u002Fvue-query'\nimport { WagmiPlugin } from '@wagmi\u002Fvue'\nimport { createApp } from 'vue'\nimport { VueQueryPlugin } from '@tanstack\u002Fvue-query'\nimport { WagmiPlugin } from '@wagmi\u002Fvue'\nimport App from '.\u002FApp.vue'\nimport { wagmiAdapter } from '.\u002Fplugins\u002FwagmiConfig'\n\nconst queryClient = new QueryClient()\n\ncreateApp(App)\napp.use(VueQueryPlugin)\napp.use(WagmiPlugin, { config: wagmiAdapter.wagmiConfig })\n        .mount('#app') 查看 WagmiPlugin文档 以了解有关插件API的更多信息. 要打开AppKit，可以使用Reown的web组件\n或使用AppKit构建自己的按钮自定义组件\n在本例中，我们将使用组件\nWeb组件是不需要导入的全局html元素 \u003Ctemplate>\n  \u003Cappkit-button \u002F>\n\u003C\u002Ftemplate>",{"id":190,"title":151,"titles":191,"content":192,"level":145},"\u002Fdocs\u002Fgetting-started#获取用户钱包地址-1",[101,164,178],"import { useAppKitAccount } from '@reown\u002Fappkit\u002Fvue'\nconst accountData = useAppKitAccount() \u003Cscript setup lang=\"ts\">\n\n\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003Cappkit-button \u002F>\n\u003C\u002Ftemplate>",{"id":194,"title":156,"titles":195,"content":196,"level":145},"\u002Fdocs\u002Fgetting-started#智能合约交互-1",[101,164,178],"\u003Cscript setup lang=\"ts\">\n  import { readContract } from \"@wagmi\u002Fcore\";\n  import {erc20Abi} from 'viem'\n\n  const USDTAddress = \"0x...\";\n\n  const data = readContract({\n    abi: erc20Abi,\n    address: USDTAddress,\n    functionName: \"symbol\",\n  });\n\u003C\u002Fscript>",{"id":198,"title":123,"titles":199,"content":125,"level":115},"\u002Fdocs\u002Fgetting-started#官方文档-2",[101,164],{"id":201,"title":202,"titles":203,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started#viem","Viem",[101],{"id":205,"title":112,"titles":206,"content":207,"level":115},"\u002Fdocs\u002Fgetting-started#主要功能-3",[101,202],"Viem 是 Wagmi 的底层依赖，提供区块链核心功能可与 Wagmi 结合使用，提供更高级的 React Hooks API支持 AppKit 和其他现代 Web3 开发框架",{"id":209,"title":123,"titles":210,"content":211,"level":115},"\u002Fdocs\u002Fgetting-started#官方文档-3",[101,202],"官方文档GitHub 仓库 html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":14,"title":13,"titles":213,"content":214,"level":98},[],"在 HBuilderX 中更新 uni modules 依赖的方法",{"id":216,"title":217,"titles":218,"content":219,"level":98},"\u002Fdocs\u002Fgetting-started\u002Funi-modules-update#在-hbuilderx-中更新-uni-modules-依赖","在 HBuilderX 中更新 uni modules 依赖",[],"在 uniapp 项目中，如果需要更新 uni modules 依赖，可以在 HBuilderX 中操作： 在项目管理器中右击项目文件夹。选择「从插件市场更新所有插件」。等待 HBuilderX 完成所有插件的更新。 更新完成后，建议重新运行项目并检查相关功能是否正常。",{"id":19,"title":18,"titles":221,"content":222,"level":98},[],"浏览器端多格式文件预览方案.",{"id":224,"title":225,"titles":226,"content":227,"level":109},"\u002Fdocs\u002Fgetting-started\u002Ffile-preview#file-viewer","File Viewer",[18],"文档官网在线 DemoGitHub浏览器端、可自托管的多格式文件预览组件；Office \u002F PDF \u002F CAD \u002F 压缩包等本地解析，无需上传到第三方转换服务支持 Vue \u002F React \u002F Web Component 等接入，适合私有附件预览场景",{"id":229,"title":230,"titles":231,"content":232,"level":109},"\u002Fdocs\u002Fgetting-started\u002Ffile-preview#vue-office","vue-office",[18],"GitHub支持 Word（.docx）、Excel（.xlsx \u002F .xls）、PDF、PPTX 等 Office 文件预览的 Vue 组件集合兼容 Vue 2 \u002F 3，也可在 React 等非 Vue 框架使用，偏轻量的一站式预览方案",{"id":24,"title":23,"titles":234,"content":235,"level":98},[],"用 AI 写代码时先想清楚问题与基础，再谈权衡与沟通；Review 时盯边界、异常、性能与安全，避免「能跑就算完」。",{"id":237,"title":238,"titles":239,"content":240,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#和-ai-一起写代码","和 AI 一起写代码",[],"AI 很会「往前冲」：补全快、脚手架全、看起来也很像成品。真正容易翻车的，往往不是语法，而是问题没找准、基础没对齐、权衡没说清、沟通含糊、缺少系统视角。 这篇备忘录分两块： 五条心智 — 动手前先过一遍Review 四维 — 边界 \u002F 异常 \u002F 性能 \u002F 安全，合代码前再过一遍 可和 AI 代码 Review 的三个时机、开发规范链 一起用。",{"id":242,"title":243,"titles":244,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#一五条心智","一、五条心智",[238],{"id":246,"title":247,"titles":248,"content":249,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_1-理解真正要解决的问题","1. 理解真正要解决的问题",[238,243],"先问「用户 \u002F 业务到底卡在哪」，再问「要不要写代码」。 现象 ≠ 根因：报错、慢、点不动，可能只是表象；先复现、再定位、最后才改。可验收：成功长什么样？失败怎么暴露？明确非目标（这次不做啥）。约束写清楚：环境、兼容、权限、截止时间、能否动库表 \u002F API。给 AI 的输入：背景 + 目标 + 约束 + 相关文件路径；少说「帮我优化一下」这种空话。 提示词里若说不清「真正要解决的问题」，模型只会优化它以为的问题。",{"id":251,"title":252,"titles":253,"content":254,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_2-掌握基础知识","2. 掌握基础知识",[238,243],"AI 不能替你承担「懂不懂」的责任。基础越扎实，越能鉴别胡说。 语言与运行时：异步、类型、错误传播、模块边界。框架约定：Nuxt \u002F Vue 的生命周期、SSR \u002F CSR、数据获取与缓存。协议与数据：HTTP 状态、鉴权、幂等、一致性；别只会调 SDK。读得懂 diff：说不出「这段为什么这样写」，就不要合。 没有基础时，AI 输出看起来「高级」，实际可能是过时 API、错误抽象或安全漏洞包装成最佳实践。",{"id":256,"title":257,"titles":258,"content":259,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_3-做出权衡","3. 做出权衡",[238,243],"工程没有唯一正解，只有在约束下的取舍。让 AI 给方案时，逼它说出代价。 维度常见张力速度 vs 正确先上线再修，还是门禁先过？简单 vs 扩展多一个配置项，还是先写死再抽？一致 vs 特例跟现有模式走，还是这次破例？性能 vs 复杂度缓存 \u002F 预计算值不值得多一层？ 写进 PR \u002F 对话里：选了什么、没选什么、为什么。权衡说不清的改动，Review 成本会指数上升。",{"id":261,"title":262,"titles":263,"content":264,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_4-交流沟通","4. 交流沟通",[238,243],"人和 AI、人和人，吃的都是同一套沟通质量。 对人：意图、范围、风险、验证步骤写在 PR Summary \u002F Test plan 里。对 AI：分步、可贴上下文、指出「不要改哪些文件」；一次一事，少塞整仓愿景。反馈闭环：哪里不对，标具体文件与期望行为，避免「感觉不太对再改改」。术语对齐：同一概念不要混用三套名字（尤其鉴权、缓存、环境）。 沟通越清晰，返工越少；含糊的一句「再优化」，往往换来另一坨无关重构。",{"id":266,"title":267,"titles":268,"content":269,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_5-系统思维","5. 系统思维",[238,243],"改一处，要想整条链路。 上下游：谁调用、谁依赖、失败谁兜底。状态与时间：并发、重试、幂等、过期、时钟。观测：日志、指标、告警；坏了能不能看见。回滚：合并后如何撤；配置能否快速关掉。一致性：本地 \u002F CI \u002F 预发 \u002F 生产是否同一套假设。 AI 擅长局部最优；系统债往往出在「局部都对、拼起来不对」。",{"id":271,"title":272,"titles":273,"content":274,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#二review-四维","二、Review 四维",[238],"自动 Review 和人工 Review，都可以按这四维扫。不是每条必改，但该想过却没想过就要标出来。",{"id":276,"title":277,"titles":278,"content":279,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_1-边界boundary","1. 边界（Boundary）",[238,272],"输入、状态、环境的「边上」会不会炸。 空值、空数组、超长字符串、非法枚举分页极限：第 0 页、最后一页、总数为 0权限边界：未登录、越权、过期 token配置缺失：环境变量没配时是失败还是静默错误行为兼容边界：旧客户端、旧数据形态、灰度开关关\u002F开 自问： 最小输入、最大输入、错误输入，三条路径都有明确行为吗？",{"id":281,"title":282,"titles":283,"content":284,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_2-异常exception","2. 异常（Exception）",[238,272],"失败是常态，要设计「坏掉时的样子」。 错误是否分类：可重试 \u002F 不可重试 \u002F 用户可修复是否吞异常：catch 空、返回假成功、只 console.log用户是否看得到可理解提示（别只剩 500）下游超时、限流、部分失败怎么处理事务 \u002F 多步写入：中途失败是否留下脏数据 自问： 这条路径失败时，系统停在哪、用户看到什么、日志里有没有线索？",{"id":286,"title":287,"titles":288,"content":289,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_3-性能performance","3. 性能（Performance）",[238,272],"先找「会不会在真实数据量下变慢」，再谈微优化。 N+1 查询、循环里请求、无分页的大列表热点路径是否多余 await、重复计算、同步堵事件循环缓存：键、失效、穿透；别缓存不该缓存的私有数据前端：大包、阻塞渲染、无虚拟列表的超长列表流式 \u002F 大文件：内存是否被整包拖垮 自问： 数据乘 100、并发乘 10，哪里先塌？有没有测量而不只是感觉？",{"id":291,"title":292,"titles":293,"content":294,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#_4-安全security","4. 安全（Security）",[238,272],"默认不信任输入，也不信任「看起来像内部」的调用。 注入：SQL \u002F NoSQL \u002F 命令 \u002F 模板 \u002F 富文本 XSS鉴权与授权：能看 ≠ 能改；IDOR、水平越权密钥与会话：硬编码、日志泄露、弱会话密钥、Preview 密钥进生产SSRF、开放重定向、不安全反序列化依赖与供应链：来历不明的脚本、过宽的 CORS、多余的公开接口 自问： 攻击者只改一个参数，能读到或改到不该碰的数据吗？",{"id":296,"title":297,"titles":298,"content":299,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#三怎么用在日常","三、怎么用在日常",[238],"接到需求\n  → 五条心智过一遍（问题 \u002F 基础 \u002F 权衡 \u002F 沟通 \u002F 系统）\n  → 让 AI 出方案或补丁\n  → 自己读 diff\n  → Review 四维扫一遍（边界 \u002F 异常 \u002F 性能 \u002F 安全）\n  → CI +（可选）PR 自动 Review\n  → 人工确认意图与 Test plan 后再合 给 AI 的短清单（可直接贴进对话）： 请先确认：真正要解决的问题、约束与非目标。\n方案需写明权衡（选了什么 \u002F 放弃了什么）。\n改动请说明上下游影响与回滚方式。\n自检：边界、异常、性能、安全各至少想一条。\n不要无关重构；信息不足先问，不要猜。",{"id":301,"title":302,"titles":303,"content":304,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#四把约束写进仓库agentsmd","四、把约束写进仓库：AGENTS.md",[238],"心智和 Review 清单只有写进仓库，Agent 才会稳定遵守。根目录 AGENTS.md 就是给编码 Agent 的操作手册（命令、约定、禁止事项、目录地图），和给人看的 README \u002F 详细架构 CLAUDE.md 分工不同。 本仓库已有根级 AGENTS.md：强调先读再写、先复用再新增、最小改动，避免另起一套平行实现。 写 \u002F 改 AGENTS.md 时，可参考 GitHub 对 2500+ 仓库的归纳：命令要可执行、边界要写清、堆栈要具体、少写空泛口号。 → How to write a great agents.md: Lessons from over 2,500 repositories",{"id":306,"title":307,"titles":308,"content":309,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fai-coding-mindset#相关链接","相关链接",[238],"AI 代码 Review 的三个时机前端全栈项目的开发规范链GitHub 仓库怎么给 PR 加上自动 ReviewComponent-First Guardian — 优先复用、禁止瞎编How to write a great agents.md（GitHub Blog）",{"id":29,"title":28,"titles":311,"content":312,"level":98},[],"Bing 搜索结果页 favicon \u002F ico 空白，但直接打开网站正常；发邮件给 Bing Webmaster 客服即可处理。",{"id":314,"title":28,"titles":315,"content":103,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fbing-search-favicon#bing-搜索结果不显示网站图标",[],{"id":317,"title":318,"titles":319,"content":320,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fbing-search-favicon#现象","现象",[28],"在 Bing 搜索结果里，站点旁的 favicon \u002F .ico 不显示或空白直接进入网站时，浏览器标签页图标正常站点自身 favicon.ico、HTML link rel=\"icon\" 等配置通常没有问题 这更像是 Bing 结果页侧的图标缓存 \u002F 抓取异常，不一定是站点挂了。",{"id":322,"title":323,"titles":324,"content":325,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fbing-search-favicon#处理","处理",[28],"在 Bing Webmaster Tools 里走反馈，或直接发邮件给 Bing Webmaster 客服，说明： 站点域名搜索结果里图标缺失的截图或查询词直接打开页面时图标正常 对方处理后，结果页图标一般会恢复。不必先大改站点图标配置（除非本站本身就加载失败）。",{"id":327,"title":328,"titles":329,"content":330,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fbing-search-favicon#备注","备注",[28],"Google \u002F 其他引擎正常，不能说明 Bing 结果页也一定正常本站已放 public\u002Ffavicon.ico 等静态图标；若直链可访问，优先走客服而非反复改文件",{"id":34,"title":33,"titles":332,"content":333,"level":98},[],"常用的图片压缩网站.",{"id":335,"title":336,"titles":337,"content":338,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fphoto-compression#tinify","tinify",[33],"网站链接",{"id":340,"title":341,"titles":342,"content":338,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fphoto-compression#docsmall","docsmall",[33],{"id":39,"title":38,"titles":344,"content":345,"level":98},[],"常用的组件库.",{"id":347,"title":348,"titles":349,"content":103,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#常用组件库","常用组件库",[],{"id":351,"title":352,"titles":353,"content":354,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#vue-bits","Vue Bits",[348],"Vue Bits 是一个开源的 Vue.js 组件库，专注于提供动画、交互和高度可定制的组件。它是 React Bits 的官方 Vue 版本，旨在为 Vue.js 开发者带来类似 React Bits 的动态用户界面体验。",{"id":356,"title":112,"titles":357,"content":358,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能",[348,352],"超过 80 个精心设计的动画组件，涵盖按钮、菜单、卡片、文本效果等多个类别双主题支持：所有组件都支持 CSS 和 Tailwind 一键切换完全使用 TypeScript 开发性能优化：减少不必要的 DOM 操作和重绘响应式设计：在各种设备上保持一致的表现",{"id":360,"title":123,"titles":361,"content":362,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档",[348,352],"官方网站GitHub 仓库",{"id":364,"title":365,"titles":366,"content":367,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#inspira-ui","Inspira UI",[348],"Inspira UI 是一个基于 Vue.js 和 Nuxt 的现代化 UI 组件库，旨在为开发者提供高质量、可复用的前端组件。它结合了优秀设计体系的精髓，既保证了界面的现代美感，又通过模块化架构满足不同项目的定制需求。",{"id":369,"title":112,"titles":370,"content":371,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-1",[348,365],"涵盖按钮、表单、导航、数据展示等 40+ 基础组件响应式设计：基于 Tailwind CSS 构建的弹性布局系统Nuxt 原生支持：针对 Nuxt 框架进行专项优化内置多套预设主题，支持自定义色彩、字体等设计语言组件包含悬停、加载、反馈等微交互动画",{"id":373,"title":123,"titles":374,"content":362,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-1",[348,365],{"id":376,"title":377,"titles":378,"content":379,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#pixel-ui","Pixel UI",[348],"Pixel UI 是一个像素风格的UI组件库，为开发者提供复古像素风格的界面组件。它适用于需要像素艺术风格的游戏、应用程序或网站。",{"id":381,"title":112,"titles":382,"content":383,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-2",[348,377],"提供完整的像素风格组件集合，包括按钮、表单、导航等基础组件复古像素艺术设计风格易于集成和使用支持自定义像素大小和颜色方案轻量级且性能优化",{"id":385,"title":123,"titles":386,"content":387,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-2",[348,377],"官方网站",{"id":389,"title":390,"titles":391,"content":392,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#uiverse","Uiverse",[348],"Uiverse 是一个开源的UI组件库集合，提供了大量精美的开源UI元素，开发者可以免费使用这些组件构建现代化的用户界面。它包含各种独特的按钮、卡片、表单和其他UI元素。",{"id":394,"title":112,"titles":395,"content":396,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-3",[348,390],"提供超过1000个免费的开源UI组件包含各种独特的按钮、卡片、加载动画等元素支持多种框架（React、Vue、Angular等）现代化设计，易于定制和集成社区驱动，持续更新和改进",{"id":398,"title":123,"titles":399,"content":387,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-3",[348,390],{"id":401,"title":402,"titles":403,"content":404,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#nuxt-ui","Nuxt UI",[348],"Nuxt UI 是专为 Nuxt 3 设计的 UI 库，提供了一组高质量的 Vue 3 组件和 composables。它充分利用了 Nuxt 3 的特性，如自动导入、TypeScript 支持等，让开发者能够快速构建美观且功能强大的 Nuxt 应用程序。",{"id":406,"title":112,"titles":407,"content":408,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-4",[348,402],"专为 Nuxt 3 构建，充分利用 Nuxt 的特性和生态系统包含 50+ 高质量的 Vue 3 组件，涵盖常用的 UI 元素内置 20+ composables，提供实用的功能和交互基于 Tailwind CSS 构建，支持轻松定制主题支持暗色模式，提供优秀的用户体验自动 Tree-shaking，只打包实际使用的组件和功能完整的 TypeScript 支持",{"id":410,"title":123,"titles":411,"content":387,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-4",[348,402],{"id":413,"title":414,"titles":415,"content":416,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#tdesign-uniapp","TDesign UniApp",[348],"TDesign UniApp 是腾讯 TDesign 设计体系针对 UniApp 框架的官方实现，提供了一套高质量的跨平台组件库。它支持在微信小程序、支付宝小程序、H5 等多个平台上使用，并包含专门的 AI 聊天组件。",{"id":418,"title":112,"titles":419,"content":420,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-5",[348,414],"完整的跨平台支持：微信小程序、支付宝小程序、H5 等多个平台50+ 高质量的 UniApp 组件库专为移动端优化的 AI 聊天组件，支持流式输出和实时交互基于腾讯 TDesign 设计体系，保证设计一致性支持主题定制和暗色模式完整的 TypeScript 支持",{"id":422,"title":123,"titles":423,"content":387,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-5",[348,414],{"id":425,"title":426,"titles":427,"content":428,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#zebraswiper","ZebraSwiper",[348],"ZebraSwiper 是一款兼容多端的高性能轮播组件库，支持多种复杂的 3D 轮播效果。它不仅具备丰富的内置功能，还为开发者提供了极大的灵活性，以适应复杂场景的需求。",{"id":430,"title":112,"titles":431,"content":432,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-6",[348,426],"基于 UniApp，兼容小程序、H5、App 等多端支持多种复杂的 3D 轮播效果，可自定义样式对标 Swiper，内置丰富的属性、方法与事件可通过 npm（@zebra-ui\u002Fswiper）或 uni_modules 安装灵活适配复杂业务场景",{"id":434,"title":123,"titles":435,"content":436,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-6",[348,426],"官方网站GitHub 仓库DCloud 插件市场",{"id":438,"title":439,"titles":440,"content":441,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#ai-elements-vue","AI Elements Vue",[348],"AI Elements Vue 是基于 shadcn-vue 的 AI 原生组件库（Vercel AI Elements 的 Vue 移植版），提供会话、消息、推理展示等预构建组件，可快速搭建 AI 聊天与 AI 应用界面。",{"id":443,"title":112,"titles":444,"content":445,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-7",[348,439],"基于 shadcn-vue 设计体系，组件以源码形式安装到项目中，可完全自定义专为 AI 场景设计：会话列表、消息气泡、推理过程、代码块、引用来源等与 Vercel AI SDK 配合，适合 Vue \u002F Nuxt 的聊天与流式交互场景支持通过专用 CLI 或 shadcn-vue registry 按需安装组件TypeScript + Tailwind CSS（CSS Variables 模式）",{"id":447,"title":123,"titles":448,"content":449,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-7",[348,439],"官方网站GitHub 仓库Chatbot 示例",{"id":44,"title":43,"titles":451,"content":452,"level":98},[],"常用的图表库.",{"id":454,"title":455,"titles":456,"content":457,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcharts#echarts","echarts",[43],"网站链接一个基于 JavaScript 的开源可视化图表库",{"id":459,"title":460,"titles":461,"content":462,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcharts#vue-echarts","vue-echarts",[43],"网站链接专为 Vue.js 设计的 ECharts 组件，提供响应式和交互式图表",{"id":464,"title":465,"titles":466,"content":467,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcharts#ucharts","ucharts",[43],"网站链接PC、H5、APP、小程序、Vue、Taro",{"id":469,"title":470,"titles":471,"content":472,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcharts#uni-echarts","uni echarts",[43],"网站链接适用于 uni-app 的 ECharts 组件 与 Vue ECharts 近乎一致的使用体验",{"id":474,"title":475,"titles":476,"content":477,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcharts#klinecharts","klinecharts",[43],"网站链接一款轻量级的金融图表库，主要用于绘制K线图和其他金融图表",{"id":49,"title":48,"titles":479,"content":480,"level":98},[],"常用的CSS库\u002F网站.",{"id":482,"title":483,"titles":484,"content":485,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcss#tailwind-css","Tailwind CSS",[48],"网站链接一个功能优先的CSS框架，用于快速构建定制化的用户界面",{"id":487,"title":488,"titles":489,"content":490,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcss#unocss","UnoCSS",[48],"网站链接即时按需原子化CSS引擎，具有高性能和可扩展性",{"id":492,"title":493,"titles":494,"content":495,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcss#clippy","Clippy",[48],"网站链接一个自定义裁切CSS生成器，可以轻松创建各种形状的CSS clip-path",{"id":497,"title":498,"titles":499,"content":500,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcss#motion-for-vuejs","Motion for Vue.js",[48],"网站链接基于 Vue Composition API 的动画库，提供简单易用的滚入动画和各种流畅的过渡效果",{"id":502,"title":503,"titles":504,"content":505,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcss#css-inspiration","CSS Inspiration",[48],"网站链接一个 CSS 灵感库，展示各种创意的 CSS 技巧、特效和最佳实践",{"id":507,"title":508,"titles":509,"content":510,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fcss#zeoseven-fonts","ZeoSeven Fonts",[48],"网站链接免费商用字体的下载、预览与 API，收录大量中英文字体，支持 Web Fonts API 快速接入",{"id":54,"title":53,"titles":512,"content":513,"level":98},[],"常用的前端调试和测试工具.",{"id":515,"title":516,"titles":517,"content":518,"level":109},"\u002Fdocs\u002Fgetting-started\u002Ftest#下载指定版本chromium内核","下载指定版本Chromium内核",[53],"下载地址可以下载指定历史版本的Chromium内核，方便进行浏览器兼容性测试",{"id":59,"title":58,"titles":520,"content":521,"level":98},[],"专注于 uni-app 生态的工具集.",{"id":523,"title":524,"titles":525,"content":526,"level":109},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#什么是-uni-helper","什么是 uni-helper",[58],"uni-helper 是一个专注于 uni-app 生态的工具集，旨在提升开发者在使用 uni-app 进行跨平台开发时的开发效率和体验。它补全了 uni-app 生态的短板，提供了一系列工具来让 uni-app 的开发体验达到 web 开发的效果。",{"id":528,"title":529,"titles":530,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#核心功能","核心功能",[58],{"id":532,"title":533,"titles":534,"content":535,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_1-typescript-类型支持","1. TypeScript 类型支持",[58,529],"@uni-helper\u002Funi-app-types：提供了一套完整的 TypeScript 类型定义文件，专门针对 uni-app 平台设计支持 IDE 自动补全、参数提示等功能提升代码的可读性和可维护性",{"id":537,"title":538,"titles":539,"content":540,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_2-网络请求适配","2. 网络请求适配",[58,529],"@uni-helper\u002Faxios-adapter：封装了 axios 的核心功能，并针对 uni-app 做了定制化的配置和优化支持 uni-app 环境下的网络请求保持 axios 熟悉的 API 风格",{"id":542,"title":543,"titles":544,"content":545,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_3-开发体验增强","3. 开发体验增强",[58,529],"提供一系列工具让 uni-app 开发体验达到 web 开发效果补全 uni-app 生态的短板",{"id":547,"title":548,"titles":549,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#安装使用","安装使用",[58],{"id":551,"title":552,"titles":553,"content":554,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#前置条件","前置条件",[58,548],"Node.js 版本 >= 12.0.0使用 npm 或 yarn 管理依赖",{"id":556,"title":557,"titles":558,"content":559,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#安装示例","安装示例",[58,548],"# 安装 TypeScript 类型定义\nnpm install @uni-helper\u002Funi-app-types -D\n\n# 安装 axios 适配器和 axios\nnpm install @uni-helper\u002Faxios-adapter axios",{"id":561,"title":562,"titles":563,"content":103,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#使用示例","使用示例",[58,548],{"id":565,"title":566,"titles":567,"content":568,"level":145},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_1-使用-axios-适配器","1. 使用 axios 适配器",[58,548,562],"\u002F\u002F main.js 或 api 配置文件\nimport axios from 'axios'\nimport { createUniAppAxiosAdapter } from '@uni-helper\u002Faxios-adapter'\n\n\u002F\u002F 创建 axios 实例\nconst api = axios.create({\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  timeout: 10000,\n  adapter: createUniAppAxiosAdapter() \u002F\u002F 使用 uni-app 适配器\n})\n\n\u002F\u002F 使用示例\nexport const fetchData = async () => {\n  try {\n    const response = await api.get('\u002Fdata')\n    return response.data\n  } catch (error) {\n    console.error('请求失败:', error)\n    throw error\n  }\n}",{"id":570,"title":571,"titles":572,"content":573,"level":145},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_2-配置-typescript-类型支持","2. 配置 TypeScript 类型支持",[58,548,562],"\u002F\u002F tsconfig.json\n{\n  \"compilerOptions\": {\n    \"types\": [\n      \"@dcloudio\u002Ftypes\",\n      \"@uni-helper\u002Funi-app-types\",\n      \"miniprogram-api-typings\" \u002F\u002F wx API 类型\n    ]\n  }\n}",{"id":575,"title":576,"titles":577,"content":578,"level":109},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#支持的平台","支持的平台",[58],"uni-helper 支持 uni-app 覆盖的所有平台，包括： 微信小程序支付宝小程序百度小程序字节跳动小程序QQ 小程序快应用H5App（iOS\u002FAndroid）",{"id":580,"title":581,"titles":582,"content":583,"level":109},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#社区与贡献","社区与贡献",[58],"GitHub 仓库：https:\u002F\u002Fgithub.com\u002Funi-helper官网：https:\u002F\u002Funi-helper.js.org\u002F",{"id":585,"title":586,"titles":587,"content":588,"level":109},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#许可证","许可证",[58],"uni-helper 采用 MIT 许可证，详情请查看项目仓库中的 LICENSE 文件。 html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":64,"title":63,"titles":590,"content":591,"level":98},[],"Vue Flow 和 Logic Flow 工作流图表库介绍.",{"id":593,"title":594,"titles":595,"content":596,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#什么是工作流可视化库","什么是工作流可视化库",[63],"工作流可视化库是用于构建流程图、工作流编辑器和节点图等应用的前端组件库。它们提供了强大的图形编辑、节点连接、拖拽布局等功能，广泛应用于流程设计、数据流可视化、低代码平台等场景。",{"id":598,"title":599,"titles":600,"content":601,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#vue-flow","Vue Flow",[63],"Vue Flow 是基于 Vue 3 的流程图库，它是 React Flow 的 Vue 版本实现，提供了高度可定制的节点图编辑器。",{"id":603,"title":604,"titles":605,"content":606,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#核心特性","核心特性",[63,599],"Vue 3 原生支持：完全基于 Vue 3 和 Composition API 构建高性能渲染：支持大量节点的流畅渲染和交互自定义节点：可以使用任何 Vue 组件作为节点丰富的交互：支持拖拽、缩放、平移、连线等操作TypeScript 支持：完整的类型定义插件系统：可扩展的插件架构",{"id":608,"title":548,"titles":609,"content":610,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#安装使用",[63,599],"# 使用 pnpm 安装\npnpm add @vue-flow\u002Fcore\n\n# 可选插件\npnpm add @vue-flow\u002Fbackground @vue-flow\u002Fcontrols @vue-flow\u002Fminimap",{"id":612,"title":613,"titles":614,"content":615,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#基础示例","基础示例",[63,599],"\u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { VueFlow, useVueFlow } from '@vue-flow\u002Fcore'\nimport { Background } from '@vue-flow\u002Fbackground'\nimport { Controls } from '@vue-flow\u002Fcontrols'\nimport { MiniMap } from '@vue-flow\u002Fminimap'\n\n\u002F\u002F 导入样式\nimport '@vue-flow\u002Fcore\u002Fdist\u002Fstyle.css'\nimport '@vue-flow\u002Fcore\u002Fdist\u002Ftheme-default.css'\n\nconst elements = ref([\n  {\n    id: '1',\n    type: 'input',\n    label: '开始节点',\n    position: { x: 250, y: 5 }\n  },\n  {\n    id: '2',\n    label: '处理节点',\n    position: { x: 100, y: 100 }\n  },\n  {\n    id: 'e1-2',\n    source: '1',\n    target: '2',\n    animated: true\n  }\n])\n\nconst { onConnect, addEdges } = useVueFlow()\n\n\u002F\u002F 连接节点时的回调\nonConnect((params) => {\n  addEdges([params])\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv style=\"height: 500px\">\n    \u003CVueFlow v-model=\"elements\">\n      \u003CBackground \u002F>\n      \u003CControls \u002F>\n      \u003CMiniMap \u002F>\n    \u003C\u002FVueFlow>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>",{"id":617,"title":618,"titles":619,"content":620,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#自定义节点示例","自定义节点示例",[63,599],"\u003C!-- CustomNode.vue -->\n\u003Cscript setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow\u002Fcore'\n\ndefineProps\u003C{\n  data: {\n    label: string\n  }\n}>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"custom-node\">\n    \u003CHandle type=\"target\" :position=\"Position.Top\" \u002F>\n    \u003Cdiv class=\"node-content\">\n      {{ data.label }}\n    \u003C\u002Fdiv>\n    \u003CHandle type=\"source\" :position=\"Position.Bottom\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.custom-node {\n  padding: 10px 20px;\n  border-radius: 8px;\n  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n  color: white;\n  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\u003C\u002Fstyle>",{"id":622,"title":623,"titles":624,"content":625,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#适用场景","适用场景",[63,599],"工作流编辑器流程图设计工具数据流可视化状态机图表思维导图",{"id":627,"title":628,"titles":629,"content":630,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#logic-flow","Logic Flow",[63],"Logic Flow 是滴滴开源的流程图编辑框架，提供了一套完整的流程可视化解决方案，特别适合于业务流程管理。",{"id":632,"title":604,"titles":633,"content":634,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#核心特性-1",[63,628],"框架无关：可以在 Vue、React 等任何框架中使用业务友好：专注于业务流程场景，提供丰富的业务节点高度可扩展：支持自定义节点、边、主题等BPMN 支持：支持 BPMN 2.0 规范数据转换：支持多种数据格式导入导出完善的 API：提供丰富的 API 和事件系统",{"id":636,"title":548,"titles":637,"content":638,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#安装使用-1",[63,628],"# 核心库\npnpm add @logicflow\u002Fcore\n\n# 扩展包\npnpm add @logicflow\u002Fextension",{"id":640,"title":613,"titles":641,"content":642,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#基础示例-1",[63,628],"\u003Cscript setup lang=\"ts\">\nimport { onMounted, ref } from 'vue'\nimport LogicFlow from '@logicflow\u002Fcore'\nimport '@logicflow\u002Fcore\u002Fdist\u002Fstyle\u002Findex.css'\n\nconst containerRef = ref\u003CHTMLDivElement>()\nlet lf: LogicFlow\n\nonMounted(() => {\n  if (!containerRef.value) return\n\n  \u002F\u002F 初始化 LogicFlow\n  lf = new LogicFlow({\n    container: containerRef.value,\n    grid: true,\n    background: {\n      color: '#f7f9ff'\n    }\n  })\n\n  \u002F\u002F 渲染数据\n  lf.render({\n    nodes: [\n      {\n        id: '1',\n        type: 'rect',\n        x: 100,\n        y: 100,\n        text: '开始'\n      },\n      {\n        id: '2',\n        type: 'circle',\n        x: 300,\n        y: 100,\n        text: '审批'\n      }\n    ],\n    edges: [\n      {\n        id: 'e1',\n        type: 'polyline',\n        sourceNodeId: '1',\n        targetNodeId: '2'\n      }\n    ]\n  })\n\n  \u002F\u002F 监听事件\n  lf.on('node:click', ({ data }) => {\n    console.log('节点被点击:', data)\n  })\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv ref=\"containerRef\" style=\"height: 500px; width: 100%\" \u002F>\n\u003C\u002Ftemplate>",{"id":644,"title":618,"titles":645,"content":646,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#自定义节点示例-1",[63,628],"import { RectNode, RectNodeModel } from '@logicflow\u002Fcore'\n\nclass CustomNode extends RectNode {}\n\nclass CustomNodeModel extends RectNodeModel {\n  initNodeData(data: any) {\n    super.initNodeData(data)\n    \u002F\u002F 自定义节点样式\n    this.width = 120\n    this.height = 60\n    this.radius = 8\n  }\n\n  getNodeStyle() {\n    const style = super.getNodeStyle()\n    style.stroke = '#667eea'\n    style.fill = '#f0f3ff'\n    style.strokeWidth = 2\n    return style\n  }\n}\n\nexport default {\n  type: 'custom-node',\n  view: CustomNode,\n  model: CustomNodeModel\n}",{"id":648,"title":649,"titles":650,"content":651,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#在-vue-中注册自定义节点","在 Vue 中注册自定义节点",[63,628],"import CustomNode from '.\u002FCustomNode'\n\nlf.register(CustomNode)\n\n\u002F\u002F 使用自定义节点\nlf.render({\n  nodes: [\n    {\n      id: '1',\n      type: 'custom-node',\n      x: 100,\n      y: 100,\n      text: '自定义节点'\n    }\n  ]\n})",{"id":653,"title":623,"titles":654,"content":655,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#适用场景-1",[63,628],"审批流程设计业务流程管理（BPM）BPMN 流程图低代码平台工作流引擎可视化",{"id":657,"title":658,"titles":659,"content":660,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#对比总结","对比总结",[63],"特性Vue FlowLogic Flow框架支持Vue 3 专用框架无关学习曲线中等较平缓性能优秀良好自定义能力强（Vue 组件）强（类继承）BPMN 支持需自行实现原生支持社区活跃度高中等TypeScript完整支持完整支持适用场景通用流程图业务流程为主",{"id":662,"title":663,"titles":664,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#选择建议","选择建议",[63],{"id":666,"title":667,"titles":668,"content":669,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#选择-vue-flow-如果","选择 Vue Flow 如果：",[63,663],"你的项目使用 Vue 3需要高度自定义的节点和交互追求现代化的开发体验对性能要求较高",{"id":671,"title":672,"titles":673,"content":674,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#选择-logic-flow-如果","选择 Logic Flow 如果：",[63,663],"需要 BPMN 支持专注于业务流程管理需要框架无关的解决方案需要更成熟的企业级特性",{"id":676,"title":677,"titles":678,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#相关资源","相关资源",[63],{"id":680,"title":599,"titles":681,"content":682,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#vue-flow-1",[63,677],"官网：https:\u002F\u002Fvueflow.dev\u002FGitHub：https:\u002F\u002Fgithub.com\u002Fbcakmakoglu\u002Fvue-flow示例：https:\u002F\u002Fvueflow.dev\u002Fexamples\u002F",{"id":684,"title":628,"titles":685,"content":686,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#logic-flow-1",[63,677],"官网：http:\u002F\u002Flogic-flow.org\u002FGitHub：https:\u002F\u002Fgithub.com\u002Fdidi\u002FLogicFlow文档：http:\u002F\u002Flogic-flow.org\u002Fguide\u002Fstart.html",{"id":688,"title":586,"titles":689,"content":690,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#许可证",[63],"Vue Flow 采用 MIT 许可证Logic Flow 采用 Apache-2.0 许可证 html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}",{"id":75,"title":74,"titles":692,"content":693,"level":98},[],"一套增强开发工作流的 Claude Code 技能集，涵盖规划、调试、TDD、代码审查等.",{"id":695,"title":74,"titles":696,"content":697,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#superpowers",[],"Superpowers 是一套面向 Claude Code 的开发工作流增强技能集，用结构化流程替代临时发挥，让 AI 辅助开发更可靠、更高效。",{"id":699,"title":700,"titles":701,"content":702,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#核心理念","核心理念",[74],"遇到任务时不要凭直觉直接写代码，而是按照既定的流程技能来执行 — 先规划、再实现、最后验证。",{"id":704,"title":705,"titles":706,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#技能分类","技能分类",[74],{"id":708,"title":709,"titles":710,"content":711,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#流程技能process-skills","流程技能（Process Skills）",[74,705],"这些技能决定如何完成任务，应在其他技能之前使用： Brainstorming — 在创建功能、构建组件、添加功能之前，先探索用户意图和设计需求Writing Plans — 当有需求规格说明或涉及多步骤任务时，先编写实现计划Executing Plans — 按照计划逐步执行，保持任务分解和进度追踪Systematic Debugging — 遇到 Bug 或测试失败时，系统化定位根因而非盲目修复Test-Driven Development — 红-绿-重构循环，先写测试再写代码Verification Before Completion — 完成前验证变更是否符合预期",{"id":713,"title":714,"titles":715,"content":716,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#协作技能collaboration-skills","协作技能（Collaboration Skills）",[74,705],"Requesting Code Review — 提交代码前请求 AI 审查Receiving Code Review — 处理审查反馈Finishing a Development Branch — 实现完成后决定合并、PR 或清理Dispatching Parallel Agents — 将独立子任务并行分派给多个 AgentSubagent-Driven Development — 在当前 session 中执行多 Agent 实现计划",{"id":718,"title":719,"titles":720,"content":721,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#基础设施技能infrastructure-skills","基础设施技能（Infrastructure Skills）",[74,705],"Using Git Worktrees — 使用 Git 工作树隔离并行开发Using Superpowers — 技能的技能，指引如何正确加载和使用技能",{"id":723,"title":548,"titles":724,"content":725,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#安装使用",[74],"Superpowers 通过 Claude Code 插件系统安装： claude plugins install superpowers@anthropic-agent-sdk 安装后，在对话中直接输入 \u002Fsuperpowers:\u003Cskill-name> 即可调用对应技能。",{"id":727,"title":677,"titles":728,"content":729,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#相关资源",[74],"Superpowers GitHubClaude Code 文档 html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":80,"title":79,"titles":731,"content":732,"level":98},[],"前端设计技能，提供美学方向、字体排版和视觉设计指导，让界面摆脱模板化风格.",{"id":734,"title":79,"titles":735,"content":736,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#frontend-design",[],"Frontend Design 是一个专注于视觉美学与设计方向的技能，帮助开发者在构建或重塑 UI 时做出独特、有意图的设计选择，避免呈现出千篇一律的模板化外观。",{"id":738,"title":739,"titles":740,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#核心能力","核心能力",[79],{"id":742,"title":743,"titles":744,"content":745,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#美学方向","美学方向",[79,739],"为界面确定独特的视觉风格和设计方向做出有意图的设计选择，而非依赖默认样式让产品看起来不像\"又一个 AI 生成的模板\"",{"id":747,"title":748,"titles":749,"content":750,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#字体排版","字体排版",[79,739],"选择合适的字体组合和排版层次字号、行高、字间距的精细调整通过排版建立清晰的信息层级",{"id":752,"title":753,"titles":754,"content":755,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#色彩与视觉","色彩与视觉",[79,739],"配色方案的选择与搭配避免默认调色板的廉价感通过色彩传达品牌调性和情感",{"id":757,"title":758,"titles":759,"content":760,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#布局与空间","布局与空间",[79,739],"打破对称、网格化的呆板布局留白和视觉节奏的把控让页面有\"设计感\"而非仅仅是\"能用\"",{"id":762,"title":763,"titles":764,"content":765,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#使用场景","使用场景",[79],"从零开始构建新页面或组件时对现有界面进行视觉改版感觉当前 UI \"看起来像模板\"想要提升需要确定产品的整体视觉风格方向",{"id":767,"title":768,"titles":769,"content":770,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#使用方式","使用方式",[79],"在 Claude Code 对话中输入 \u002Ffrontend-design 启动该技能，描述你的界面需求，技能会帮助你做出有审美、有意图的设计决策。",{"id":772,"title":677,"titles":773,"content":774,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#相关资源",[79],"Frontend Design 技能源码",{"id":85,"title":84,"titles":776,"content":777,"level":98},[],"强制优先复用现有模块组件，禁止幻觉编造，禁止重复造轮子",{"id":779,"title":780,"titles":781,"content":103,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcomponent-first-guardian#行为规约","行为规约",[],{"id":783,"title":784,"titles":785,"content":786,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcomponent-first-guardian#_1-禁止幻觉瞎猜","1. 禁止幻觉瞎猜",[780],"所有输出必须基于已给出的上下文、文件、组件库文档。信息不足、不确定存在某个组件\u002F接口时，直接说明，绝不编造不存在模块。",{"id":788,"title":789,"titles":790,"content":791,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcomponent-first-guardian#_2-优先复用原则最高优先级","2. 优先复用原则（最高优先级）",[780],"收到开发需求，执行顺序： 检索当前项目、组件库、成熟开源方案是否已有对应组件如果已有可用组件，必须优先复用仅当现有组件无法满足全部需求，才可自定义实现，并且写明为什么现有组件不可用。",{"id":793,"title":794,"titles":795,"content":796,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcomponent-first-guardian#_3-禁止重复造轮子","3. 禁止重复造轮子",[780],"绝不从零编写UI组件、工具函数，如果生态内已经存在成熟实现。",{"id":798,"title":799,"titles":800,"content":801,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcomponent-first-guardian#_4-决策记录","4. 决策记录",[780],"每次你决定\"新建组件而不是复用\"时，必须显式写出理由。",{"id":90,"title":89,"titles":803,"content":804,"level":98},[],"通过压缩 Agent 的输入与输出，减少 Token 消耗，同时保持回答和代码的准确性.",{"id":806,"title":89,"titles":807,"content":808,"level":98},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#caveman",[],"Caveman 是一个帮助 AI Agent 用更少 Token 完成更多工作的工具与技能集。它可以压缩 Agent 读取的上下文，也可以让 Agent 用更简洁的方式回答，同时保留代码、命令和错误信息的准确性。",{"id":810,"title":739,"titles":811,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#核心能力",[89],{"id":813,"title":814,"titles":815,"content":816,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#精简-agent-输出","精简 Agent 输出",[89,739],"启用 \u002Fcaveman 后，Agent 会使用简洁、直接的表达方式，减少冗长解释。可以根据需要切换不同强度： \u002Fcaveman lite\n\u002Fcaveman full\n\u002Fcaveman ultra\n\u002Fcaveman off",{"id":818,"title":819,"titles":820,"content":821,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#压缩-agent-输入","压缩 Agent 输入",[89,739],"Caveman Proxy 会在每次 Provider 调用前压缩上下文，并保留可恢复的原始内容。不同内容类型会采用不同策略： JSON — 保留键、结构和错误信息，折叠重复数组日志 — 保留错误、堆栈以及首尾内容，移除无关进度信息代码 — 保留导入、签名和类型，省略不影响理解的函数体Diff — 保留文件、分块头和变更行搜索结果 — 保留关键命中、诊断和安全相关结果文本与 HTML — 保留标题、开头、结尾和重要段落",{"id":823,"title":824,"titles":825,"content":826,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#压缩命令输出","压缩命令输出",[89,739],"caveman shrink -- pnpm test 命令输出会被压缩为更易处理的上下文，同时支持恢复原始内容。",{"id":828,"title":829,"titles":830,"content":103,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#安装","安装",[89],{"id":832,"title":833,"titles":834,"content":835,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#安装输出精简技能","安装输出精简技能",[89,829],"npx skills add JuliusBrussee\u002Fcaveman 安装后，在支持的 Agent 中输入 \u002Fcaveman，或者让 Agent 自动激活该技能。",{"id":837,"title":838,"titles":839,"content":840,"level":115},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#安装-caveman-proxy","安装 Caveman Proxy",[89,829],"npm install -g @caveman-ai\u002Fcli && caveman setup --install\ncaveman claude Proxy 支持 Claude Code、Codex、Gemini CLI、Aider、opencode、Hermes Agent、OpenClaw 和 Pi 等工具。它在本地运行，不需要修改 Agent 自己的配置文件。",{"id":842,"title":843,"titles":844,"content":845,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#常用命令","常用命令",[89],"命令用途caveman learn分析本地 Agent 历史，找出 Token 消耗来源caveman learn implement将优化建议交给 Agent，并逐项确认修改caveman explore install使用只读子 Agent 快速定位代码caveman mem remember保存可跨会话使用的记忆caveman stats查看各类内容的压缩统计caveman browse \u003Curl>以压缩后的可访问性树浏览网页",{"id":847,"title":763,"titles":848,"content":849,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#使用场景",[89],"上下文很长、但有效信息比例较低的 Agent 会话日志、测试输出或搜索结果过于庞大的调试任务希望减少回复长度，同时保持技术内容精确想分析真实 Agent 历史中的 Token 浪费位置 注意：Caveman 的收益取决于工作负载。对于本身已经非常简短的任务，额外的技能上下文可能抵消部分节省；建议使用 caveman stats 或基准测试验证实际效果。",{"id":851,"title":677,"titles":852,"content":853,"level":109},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fcaveman#相关资源",[89],"Caveman GitHubCaveman 官网安装说明诚实数据说明 html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}",[855],{"title":856,"path":857,"stem":858,"children":859,"page":93},"Blog","\u002Fblog","3.blog",[860,864,868,872,876,880,884,888,892,896,900,904,908,912,916,920,924,928,932,936],{"title":861,"path":862,"stem":863},"Vue 3 Transition 报错 non-element root node：组件根节点无法被动画化","\u002Fblog\u002Fasian-cuisine","3.blog\u002F1.asian-cuisine",{"title":865,"path":866,"stem":867},"UniApp 踩坑：showModal 按钮字数、iOS 溢出、picker 时区与未选中背景","\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls","3.blog\u002F10.uniapp-wechat-miniprogram-pitfalls",{"title":869,"path":870,"stem":871},"UniApp RootPortal：修复 uview-plus Popup\u002FModal 遮罩层层级异常","\u002Fblog\u002Funiapp-root-portal","3.blog\u002F11.uniapp-root-portal",{"title":873,"path":874,"stem":875},"VS Code Copilot 配置 Apifox MCP：让 AI 直接读 API 文档","\u002Fblog\u002Fvscode-copilot-apifox-mcp","3.blog\u002F12.vscode-copilot-apifox-mcp",{"title":877,"path":878,"stem":879},"Unibest build:app 后 manifest 缺 tabBar：wgt 热更新白屏修复","\u002Fblog\u002Funibest-manifest-missing-config","3.blog\u002F13.unibest-manifest-missing-config",{"title":881,"path":882,"stem":883},"UniApp z-paging easycom 与 vite-plugin-uni-components 冲突：H5 组件变成原始标签","\u002Fblog\u002Funiapp-z-paging-easycom-conflict","3.blog\u002F14.uniapp-z-paging-easycom-conflict",{"title":885,"path":886,"stem":887},"UniApp z-paging 聊天页：安卓键盘弹起输入框飞顶与底部空白","\u002Fblog\u002Funiapp-z-paging-android-keyboard","3.blog\u002F15.uniapp-z-paging-android-keyboard",{"title":889,"path":890,"stem":891},"前端全栈项目的开发规范链：让 AI 写代码也有门禁","\u002Fblog\u002Fdev-spec-chain","3.blog\u002F16.dev-spec-chain",{"title":893,"path":894,"stem":895},"GitHub 仓库怎么给 PR 加上自动 Review","\u002Fblog\u002Fgithub-pr-agent-review","3.blog\u002F17.github-pr-agent-review",{"title":897,"path":898,"stem":899},"AI 代码 Review 的三个时机","\u002Fblog\u002Fai-code-review-three-timings","3.blog\u002F18.ai-code-review-three-timings",{"title":901,"path":902,"stem":903},"UniApp uni-push：receive 里无条件 createPushMessage，iOS 会弹两次，要先判断 aps","\u002Fblog\u002Funiapp-push-receive-aps","3.blog\u002F19.uniapp-push-receive-aps",{"title":905,"path":906,"stem":907},"Apifox + openapi-ts-request：自动生成类型安全的 API 请求代码","\u002Fblog\u002Fapifox-api-generation","3.blog\u002F2.apifox-api-generation",{"title":909,"path":910,"stem":911},"HBuilderX \u002F HBuilderV：给 uni-agent 接自定义 DeepSeek Flash","\u002Fblog\u002Fhbuilder-uni-agent-deepseek-flash","3.blog\u002F20.hbuilder-uni-agent-deepseek-flash",{"title":913,"path":914,"stem":915},"UniApp H5 多文件上传：绕过 uni.uploadFile 单文件限制（fetch + FormData）","\u002Fblog\u002Ffile-upload-issue-solution","3.blog\u002F3.file-upload-issue-solution",{"title":917,"path":918,"stem":919},"WangEditor div 包裹的 img 被过滤：商品详情 HTML 导入修复","\u002Fblog\u002Fwangeditor-img-filter-issue","3.blog\u002F4.wangeditor-img-filter-issue",{"title":921,"path":922,"stem":923},"Nuxt Cannot load payload \u002F_payload.json：ssr:false 部署后修复","\u002Fblog\u002Fnuxt-payload-extraction","3.blog\u002F5.nuxt-payload-extraction",{"title":925,"path":926,"stem":927},"Nuxt 4 集成 nuxt-echarts：ECharts 图表安装与 SSR 注意点","\u002Fblog\u002Fnuxt-echarts-implementation","3.blog\u002F6.nuxt-echarts-implementation",{"title":929,"path":930,"stem":931},"UniApp + Vite 集成 Reown \u002F Wagmi：打包失败、兼容性与 RPC 问题","\u002Fblog\u002Freown-wagmi-uniapp-issues","3.blog\u002F7.reown-wagmi-uniapp-issues",{"title":933,"path":934,"stem":935},"Vue 3 数字滚动动画：Composition API + requestAnimationFrame 实现","\u002Fblog\u002Fnumber-scroll-animation","3.blog\u002F8.number-scroll-animation",{"title":937,"path":938,"stem":939},"nvm-windows 安装 Node 卡住：nvm node_mirror 切换淘宝镜像","\u002Fblog\u002Fnvm-install-solution","3.blog\u002F9.nvm-install-solution",[941,944,949,953,958,963,968,973,978,983,988,993,998,1003,1008,1011,1015,1019,1024,1029,1034,1039,1043,1047,1051,1056,1060,1064,1068,1072,1076,1080,1084,1088,1092,1096,1100,1104,1108,1112,1116,1120,1124,1128,1132,1137,1141,1145,1149,1153,1157,1162,1166,1171,1174,1179,1184,1189,1194,1198,1201,1206,1211,1214,1219,1224,1229,1234,1238,1243,1246,1251,1255,1259,1264,1269,1274,1279,1284,1289,1294,1299,1303,1308,1313,1318,1323,1327,1331,1334,1339,1344,1349,1352,1357,1362,1367,1371,1374,1379,1384,1389,1393,1398,1403,1408,1413,1417,1420,1424,1429,1434,1439,1443,1448,1453,1458,1463,1468,1473,1478,1483,1488,1493,1498,1503,1508,1511,1515,1520,1525,1530,1535,1540,1545,1550,1554,1559,1564,1569,1574,1579,1584,1589,1594,1599,1603,1606,1610,1615,1620,1625,1630,1635,1640,1645,1650,1654,1657,1661,1666,1670,1675,1680,1685,1690,1694,1698,1702,1706,1710,1714,1718,1722,1726,1730,1735,1739,1742,1747,1752,1757,1762,1767,1771,1774,1779,1784,1789,1794,1799,1804,1809,1812,1817,1822,1827,1830,1835,1840,1845,1850,1855,1858,1863,1868,1873,1877,1880,1884,1889,1894,1898,1903,1908,1912,1917,1922,1926,1930,1933,1936,1941,1946,1951,1956,1961,1964,1969,1974,1979,1982,1986,1990,1994,1998,2002,2006,2010,2014,2018,2022,2026,2030,2034,2038,2042,2046,2050,2054,2058,2062,2066,2070,2074,2078,2083,2087,2090,2095,2100,2105,2109,2114,2119,2124,2129,2134,2139,2144,2147,2152,2157,2162,2165,2169,2174,2179,2184,2189,2194,2199],{"id":862,"title":861,"titles":942,"content":943,"level":98},[],"修复 [Vue warn] Component inside \u003CTransition> renders non-element root node that cannot be animated。常见于多根节点、纯文本根或 Teleport，会导致页面切换后无法渲染。 在 Vue 3 开发中使用 \u003CTransition> 组件实现动画效果时，很多开发者都会遇到一个常见警告：“Transition 组件的直接子元素必须是单一的根 DOM 元素”。本文将深入解析这个问题的原因、常见错误场景，并提供完整的解决方案。",{"id":945,"title":946,"titles":947,"content":948,"level":109},"\u002Fblog\u002Fasian-cuisine#一核心规则transition-组件的子元素要求","一、核心规则：Transition 组件的子元素要求",[861],"Vue 3 的 \u003CTransition> 组件对其直接子元素有明确限制，必须满足： 必须是 单一的根 DOM 元素（即 \u003Cdiv>、\u003Cspan>、\u003Csection> 等真实 HTML 标签）；不能是纯文本、注释、多个根节点；不能是 \u003CTeleport>、\u003CKeepAlive> 等特殊内置组件。 💡 关键提醒：尽管 Vue 3 支持多根组件（Fragments），但 \u003CTransition>、\u003CKeepAlive> 等动画 \u002F 缓存相关组件并不兼容这种结构。",{"id":950,"title":951,"titles":952,"content":103,"level":109},"\u002Fblog\u002Fasian-cuisine#二常见错误场景及示例","二、常见错误场景及示例",[861],{"id":954,"title":955,"titles":956,"content":957,"level":115},"\u002Fblog\u002Fasian-cuisine#场景-1组件模板只有纯文本无根元素","场景 1：组件模板只有纯文本（无根元素）",[861,951],"组件模板中直接写文本，没有任何 DOM 元素包裹，会触发警告： \u003C!-- ❌ 错误：无任何包裹元素，纯文本作为子元素 -->\n\n\u003Ctemplate>\n\n Hello, Vue 3 Transition!\n\n\u003C\u002Ftemplate>\n\n\u003C!-- 对应的 Transition 使用方式（同样报错） -->\n\n\u003CTransition name=\"fade\">\n\n \u003CTextComponent \u002F> \\\u003C!-- 该组件无DOM根元素 -->\n\n\u003C\u002FTransition>",{"id":959,"title":960,"titles":961,"content":962,"level":115},"\u002Fblog\u002Fasian-cuisine#场景-2组件存在多个根节点fragments","场景 2：组件存在多个根节点（Fragments）",[861,951],"Vue 3 允许组件有多个根节点，但 \u003CTransition> 不支持这种结构： \u003C!-- ❌ 错误：多根节点（h1 和 p 并列，无外层包裹） -->\n\n\u003Ctemplate>\n\n \u003Ch1>Transition 动画演示\\\u003C\u002Fh1>\n\n \u003Cp>这是一个多根节点的组件\\\u003C\u002Fp>\n\n\u003C\u002Ftemplate>\n\n\u003C!-- 对应的 Transition 使用方式（报错） -->\n\n\u003CTransition name=\"fade\">\n\n \u003CMultiRootComponent \u002F> \u003C!-- 该组件有多个根节点 -->\n\n\u003C\u002FTransition>",{"id":964,"title":965,"titles":966,"content":967,"level":115},"\u002Fblog\u002Fasian-cuisine#场景-3动态组件未保证单一根节点","场景 3：动态组件未保证单一根节点",[861,951],"使用 \u003Ccomponent :is=\"...\"> 切换组件时，如果某个候选组件不符合要求，会触发警告： \u003C!-- 父组件中使用动态组件 -->\n\n\u003CTransition name=\"fade\">\n\n \u003Ccomponent :is=\"currentView\" :key=\"currentView\" \u002F>\n\n\u003C\u002FTransition>\n\n\u003C!-- 问题：若 currentView 对应的组件（如 About.vue）是多根节点，则报错 -->\n\n\u003C!-- About.vue（❌ 错误示例） -->\n\n\u003Ctemplate>\n\n \u003Ch2>关于我们\\\u003C\u002Fh2>\n\n \u003Cul>\n\n   \u003Cli>公司简介\\\u003C\u002Fli>\n\n   \u003Cli>联系我们\\\u003C\u002Fli>\n\n \u003C\u002Ful>\n\n\u003C\u002Ftemplate>",{"id":969,"title":970,"titles":971,"content":972,"level":109},"\u002Fblog\u002Fasian-cuisine#三解决方案确保单一根-dom-元素","三、解决方案：确保单一根 DOM 元素",[861],"解决思路非常简单：给组件模板添加一个外层包裹元素，将多根节点、纯文本等统一包裹为单一的根 DOM 元素。",{"id":974,"title":975,"titles":976,"content":977,"level":115},"\u002Fblog\u002Fasian-cuisine#方案-1修复纯文本问题","方案 1：修复纯文本问题",[861,970],"用 \u003Cspan>、\u003Cdiv> 等行内 \u002F 块级元素包裹纯文本： \u003C!-- ✅ 正确：用 span 包裹纯文本（适合行内内容） -->\n\n\u003Ctemplate>\n\n \u003Cspan>Hello, Vue 3 Transition!\\\u003C\u002Fspan>\n\n\u003C\u002Ftemplate>\n\n\u003C!-- 或用 div 包裹（适合块级内容） -->\n\n\u003Ctemplate>\n\n \u003Cdiv class=\"text-container\">\n\n   Hello, Vue 3 Transition!\n\n \u003C\u002Fdiv>\n\n\u003C\u002Ftemplate>",{"id":979,"title":980,"titles":981,"content":982,"level":115},"\u002Fblog\u002Fasian-cuisine#方案-2修复多根节点问题","方案 2：修复多根节点问题",[861,970],"用 \u003Cdiv> 或语义化标签（\u003Csection>、\u003Carticle> 等）包裹所有根节点： \u003C!-- ✅ 正确：用 div 包裹多根节点 -->\n\n\u003Ctemplate>\n\n \u003Cdiv class=\"component-container\">\n\n   \u003Ch1>Transition 动画演示\\\u003C\u002Fh1>\n\n   \u003Cp>这是一个修复后的单根节点组件\\\u003C\u002Fp>\n\n \u003C\u002Fdiv>\n\n\u003C\u002Ftemplate>\n\n\u003C!-- 或使用语义化标签（推荐，提升可读性） -->\n\n\u003Ctemplate>\n\n \u003Csection class=\"component-section\">\n\n   \u003Ch1>Transition 动画演示\\\u003C\u002Fh1>\n\n   \u003Cp>这是一个修复后的单根节点组件\\\u003C\u002Fp>\n\n \u003C\u002Fsection>\n\n\u003C\u002Ftemplate>",{"id":984,"title":985,"titles":986,"content":987,"level":115},"\u002Fblog\u002Fasian-cuisine#方案-3处理动态组件场景","方案 3：处理动态组件场景",[861,970],"确保所有可能被切换的组件（currentView 对应的候选组件）都满足 “单一根 DOM 元素” 要求： \u003C!-- 父组件：动态组件用法不变 -->\n\n\u003CTransition name=\"fade\">    \n\n \u003Ccomponent :is=\"currentView\" :key=\"currentView\" \u002F>\n\n\u003C\u002FTransition>\n\n\u003C!-- 所有候选组件需修复为单根节点，例如 About.vue（✅ 正确示例） -->\n\n\u003Ctemplate>\n\n \u003Cdiv class=\"about-container\">\n\n   \u003Ch2>关于我们\\\u003C\u002Fh2>\n\n   \u003Cul>\n\n     \u003Cli>公司简介\u003C\u002Fli>\n\n     \u003Cli>联系我们\u003C\u002Fli>\n\n   \u003C\u002Ful>\n\n \u003C\u002Fdiv>\n\n\u003C\u002Ftemplate>",{"id":989,"title":990,"titles":991,"content":992,"level":115},"\u002Fblog\u002Fasian-cuisine#方案-4异步组件-函数式组件处理","方案 4：异步组件 \u002F 函数式组件处理",[861,970],"即使是通过 defineAsyncComponent 加载的异步组件，或函数式组件，也需遵循同样规则： \u003C!-- 异步组件示例（✅ 正确） -->\n\u003Cscript setup lang=\"ts\">\nimport { defineAsyncComponent } from 'vue';\n\nconst AsyncComponent = defineAsyncComponent(() =>\n\n import('.\u002FAsyncComponent.vue')\n\n);\n\u003C\u002Fscript>\n\n\n\u003Ctemplate>\n  \u002F\u002F AsyncComponent.vue 必须有单一根 DOM 元素\n \u003Cdiv class=\"async-container\">\n\n   这是一个异步组件，有单一根元素\n\n \u003C\u002Fdiv>\n\n\u003C\u002Ftemplate>",{"id":994,"title":995,"titles":996,"content":997,"level":109},"\u002Fblog\u002Fasian-cuisine#四为什么-vue-要做这样的限制","四、为什么 Vue 要做这样的限制？",[861],"\u003CTransition> 组件的核心原理是 通过操作 DOM 元素的 CSS 类名和监听过渡事件实现动画，具体依赖： 向根 DOM 元素添加 \u002F 移除过渡类名（如 v-enter-from、v-enter-active、v-leave-to 等）；监听根 DOM 元素的 transitionend 或 animationend 事件，判断动画是否完成。 如果根节点不是真实 DOM 元素（如纯文本、Fragment），Vue 无法挂载这些类名，也无法监听过渡事件，最终导致动画失效，因此必须强制要求 “单一根 DOM 元素”。",{"id":999,"title":1000,"titles":1001,"content":1002,"level":109},"\u002Fblog\u002Fasian-cuisine#五总结问题与解决方案对照表","五、总结：问题与解决方案对照表",[861],"常见问题解决方案组件模板只有纯文本用 \u003Cspan>\u002F\u003Cdiv> 包裹文本组件存在多个根节点用 \u003Cdiv>\u002F 语义化标签包裹所有节点动态组件切换时报错确保所有候选组件均为单一根 DOM 元素异步 \u002F 函数式组件报错组件内部添加单一外层 DOM 包裹 只要记住核心原则：\u003CTransition>内部的直接子组件，必须有且仅有一个真实的 DOM 根元素，就能彻底消除警告，让过渡动画正常工作。",{"id":1004,"title":1005,"titles":1006,"content":1007,"level":109},"\u002Fblog\u002Fasian-cuisine#延伸阅读","延伸阅读",[861],"Vue 3 官方文档 - Transition 组件 （注：文档内容由 Copilot 生成） html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"id":866,"title":865,"titles":1009,"content":1010,"level":98},[],"UniApp \u002F 微信小程序常见坑：uni.showModal 确认取消按钮文字字数限制、iOS 元素溢出视口、picker 时区偏差、以及未选中项背景色修改方法。 记录在 UniApp 开发中踩过的坑，H5 调试时一切正常，但在真机上表现异常。",{"id":1012,"title":1013,"titles":1014,"content":103,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-1unishowmodal-按钮文字超出字数限制导致弹窗无法唤起","坑 1：uni.showModal 按钮文字超出字数限制导致弹窗无法唤起",[865],{"id":1016,"title":318,"titles":1017,"content":1018,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象",[865,1013],"调用 uni.showModal 时，弹窗完全无法弹出，既没有报错也没有任何提示，回调也不会触发。",{"id":1020,"title":1021,"titles":1022,"content":1023,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因","原因",[865,1013],"微信小程序的 wx.showModal（即 UniApp 封装的 uni.showModal）对 confirmText 和 cancelText 两个按钮文字有严格的字数限制： 微信小程序中，按钮文字最多支持 4 个字符一旦超出限制，整个弹窗将静默失败，不报错、不回调",{"id":1025,"title":1026,"titles":1027,"content":1028,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现代码","复现代码",[865,1013],"\u002F\u002F ❌ 以下代码在微信小程序中会导致弹窗无法唤起\nuni.showModal({\n  title: '提示',\n  content: '确认要提交订单吗？',\n  confirmText: '确认提交订单',  \u002F\u002F 超过 4 个字符，导致失败\n  cancelText: '暂不提交',       \u002F\u002F 超过 4 个字符，导致失败\n  success: (res) => {\n    console.log(res) \u002F\u002F 永远不会执行\n  }\n})",{"id":1030,"title":1031,"titles":1032,"content":1033,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案","解决方案",[865,1013],"将按钮文字控制在 4 个字符以内： \u002F\u002F ✅ 正确写法\nuni.showModal({\n  title: '提示',\n  content: '确认要提交订单吗？',\n  confirmText: '确认',   \u002F\u002F ≤ 4 个字符\n  cancelText: '取消',    \u002F\u002F ≤ 4 个字符\n  success: (res) => {\n    if (res.confirm) {\n      \u002F\u002F 用户点击确认\n    }\n  }\n})",{"id":1035,"title":1036,"titles":1037,"content":1038,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项","注意事项",[865,1013],"此问题仅在微信小程序真机中复现，H5 模式下 uni.showModal 使用的是浏览器原生 confirm，不受此限制建议封装一个统一的 showModal 工具函数，在内部做长度校验并给出开发时警告 \u002F\u002F utils\u002Fmodal.ts\nexport function safeShowModal(options: UniApp.ShowModalOptions) {\n  if (import.meta.env.DEV) {\n    if (options.confirmText && options.confirmText.length > 4) {\n      console.warn(`[showModal] confirmText \"${options.confirmText}\" 超过 4 个字符，小程序中将无法唤起弹窗`)\n    }\n    if (options.cancelText && options.cancelText.length > 4) {\n      console.warn(`[showModal] cancelText \"${options.cancelText}\" 超过 4 个字符，小程序中将无法唤起弹窗`)\n    }\n  }\n  return uni.showModal(options)\n}",{"id":1040,"title":1041,"titles":1042,"content":103,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-2ios-端元素定位超出视口h5-正常但小程序app-出现横向滚动条","坑 2：iOS 端元素定位超出视口，H5 正常但小程序\u002FApp 出现横向滚动条",[865],{"id":1044,"title":318,"titles":1045,"content":1046,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-1",[865,1041],"页面中有使用 position: fixed 或 position: absolute 做动画、遮罩、气泡等效果，在 H5 调试时表现完全正常，但在 iOS 真机（微信小程序或 App）上，页面出现横向滚动条，可以左右滑动，用户体验很差。",{"id":1048,"title":1021,"titles":1049,"content":1050,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-1",[865,1041],"部分元素通过定位移出了视口（例如用 transform: translateX(-100%) 做滑入动画、或气泡超出了右侧边界），在 H5 中浏览器默认会做一定程度的裁剪，但在 iOS WebView（UniApp 小程序\u002FApp 的运行容器）中，这些超出视口的元素会撑开整个页面的宽度，触发横向滚动。",{"id":1052,"title":1053,"titles":1054,"content":1055,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现场景","复现场景",[865,1041],"\u003Ctemplate>\n  \u003C!-- 这个元素初始位置在视口左侧外，滑入动画 -->\n  \u003Cdiv class=\"slide-panel\" :class=\"{ active: isOpen }\">\n    侧边栏内容\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle>\n.slide-panel {\n  position: fixed;\n  left: 0;\n  top: 0;\n  \u002F* 初始状态移出视口左侧 *\u002F\n  transform: translateX(-100%);\n  transition: transform 0.3s ease;\n}\n\n.slide-panel.active {\n  transform: translateX(0);\n}\n\u002F* H5 正常，但 iOS WebView 会因为初始的 -100% 撑开页面宽度 *\u002F\n\u003C\u002Fstyle>",{"id":1057,"title":1031,"titles":1058,"content":1059,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-1",[865,1041],"在页面根容器或 body 上手动添加 overflow-x: hidden： \u003C!-- app.vue 或页面根组件 -->\n\u003Cstyle>\n\u002F* 全局设置，防止 iOS WebView 横向溢出 *\u002F\npage {\n  overflow-x: hidden;\n}\n\u003C\u002Fstyle> 或在具体页面的容器上设置： \u003Ctemplate>\n  \u003Cdiv class=\"page-wrapper\">\n    \u003C!-- 页面内容 -->\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.page-wrapper {\n  overflow-x: hidden;\n  width: 100%;\n}\n\u003C\u002Fstyle>",{"id":1061,"title":1036,"titles":1062,"content":1063,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-1",[865,1041],"UniApp 中小程序的根节点是 page，不是 body，需要用 page { overflow-x: hidden } 而不是 body { overflow-x: hidden }H5 调试时因为浏览器行为差异，此问题很难复现，需要在真机上测试如果设置 overflow-x: hidden 后影响了某些需要横向滚动的子元素，可以在该子元素上单独设置 overflow-x: auto 或 overflow-x: scroll 来恢复",{"id":1065,"title":1066,"titles":1067,"content":103,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-3picker-组件在-ios-端出现日期回显错误选-15-号显示-14-号","坑 3：picker 组件在 iOS 端出现日期回显错误（选 15 号显示 14 号）",[865],{"id":1069,"title":318,"titles":1070,"content":1071,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-2",[865,1066],"使用 UniApp 的 \u003Cpicker> 组件选择日期时，在 iOS 真机上出现日期回显不一致的问题：用户明明选择了 15 号，但回显却显示 14 号，差了一天。",{"id":1073,"title":1021,"titles":1074,"content":1075,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-2",[865,1066],"当 \u003Cpicker> 组件的 mode=\"date\" 时，如果没有明确设置 fields 属性（即使你想使用的就是默认值 day），在 iOS 端会调用系统原生的 picker 组件。 iOS 原生 picker 在处理日期时会基于设备本地时区进行转换，而 UniApp\u002FJavaScript 层面的日期处理通常基于 UTC 时间。当用户手机的时区不是 UTC（比如中国的 UTC+8）时，就会出现时区偏移导致的日期差异： 用户选择 2024-04-15（本地时间）iOS 原生组件返回的值经过时区转换后，可能变成 2024-04-14T16:00:00Z（UTC 时间）如果代码层面直接按 UTC 解析，就会显示为 14 号",{"id":1077,"title":1026,"titles":1078,"content":1079,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现代码-1",[865,1066],"\u003Ctemplate>\n  \u003C!-- ❌ 没有设置 fields，iOS 会调用原生 picker，可能出现时区问题 -->\n  \u003Cpicker mode=\"date\" :value=\"date\" @change=\"onDateChange\">\n    \u003Cview>{{ date }}\u003C\u002Fview>\n  \u003C\u002Fpicker>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from 'vue'\n\nconst date = ref('2024-04-15')\n\nconst onDateChange = (e) => {\n  date.value = e.detail.value\n  \u002F\u002F iOS 上可能出现选择 15 号，但这里拿到 14 号的情况\n}\n\u003C\u002Fscript>",{"id":1081,"title":1031,"titles":1082,"content":1083,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-2",[865,1066],"明确设置 fields 属性，即使你需要的就是默认值 day： \u003Ctemplate>\n  \u003C!-- ✅ 明确设置 fields=\"day\"，避免 iOS 调用原生 picker -->\n  \u003Cpicker mode=\"date\" fields=\"day\" :value=\"date\" @change=\"onDateChange\">\n    \u003Cview>{{ date }}\u003C\u002Fview>\n  \u003C\u002Fpicker>\n\u003C\u002Ftemplate> 设置 fields 属性后，UniApp 会使用自己实现的 picker 组件而非 iOS 原生组件，从而避免时区转换问题。",{"id":1085,"title":1036,"titles":1086,"content":1087,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-2",[865,1066],"此问题仅在 iOS 真机上复现，Android 和 H5 通常不受影响问题的根源是时区差异，如果用户手机设置为 UTC 时区则不会出现除了设置 fields 属性，另一个思路是在接收日期后手动做时区补偿，但这会增加代码复杂度，不推荐建议在项目中统一封装日期选择组件，默认带上 fields 属性",{"id":1089,"title":1090,"titles":1091,"content":103,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-4uniapp-运行到-ios-xcode-模拟器261-263无法渲染-emoji","坑 4：UniApp 运行到 iOS Xcode 模拟器（26.1-26.3）无法渲染 Emoji",[865],{"id":1093,"title":318,"titles":1094,"content":1095,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-3",[865,1090],"在使用 UniApp 开发 App 时，运行到 iOS Xcode 模拟器版本 26.1 至 26.3，页面中的 Emoji 表情符号完全无法渲染，显示为空白或方块。",{"id":1097,"title":1021,"titles":1098,"content":1099,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-3",[865,1090],"Xcode 26.1 至 26.3 版本的 iOS 模拟器缺少 Emoji 字体包，导致模拟器无法正确渲染 Emoji 表情符号。这是模拟器运行时环境的问题，与应用代码本身无关。 该问题在 React Native 社区中也有相关讨论。",{"id":1101,"title":307,"titles":1102,"content":1103,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#相关链接",[865,1090],"GitHub Issue: Emoji rendering fails on Xcode 26.1-26.3 simulators",{"id":1105,"title":1031,"titles":1106,"content":1107,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-3",[865,1090],"升级 iOS 模拟器运行时版本至 26.4+： 打开 Xcode前往 Xcode > Settings > Platforms（或 Xcode > Preferences > Components）下载 iOS 26.4 或更高版本的模拟器运行时删除旧的 26.1-26.3 版本模拟器，或创建新的使用 26.4+ 运行时的模拟器设备 其他临时方案： 使用真机测试：此问题仅在模拟器上出现，真机运行不受影响使用图片替代 Emoji：在关键位置使用 Emoji 图片资源代替原生 Emoji 字符",{"id":1109,"title":1036,"titles":1110,"content":1111,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-3",[865,1090],"此问题仅影响 iOS 模拟器，真机和 Android 端均不受影响问题根源是模拟器缺少字体包，与 UniApp 或任何框架无关如果你的项目中大量使用 Emoji（如聊天应用、表情选择器等），建议优先使用真机进行开发测试或确保模拟器版本为 26.4+",{"id":1113,"title":1114,"titles":1115,"content":103,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-5canvas-drawimage-无法直接绘制在线图片","坑 5：Canvas drawImage 无法直接绘制在线图片",[865],{"id":1117,"title":318,"titles":1118,"content":1119,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-4",[865,1114],"在使用 Canvas 的 drawImage 方法绘制背景图时，直接传入在线图片链接，H5 调试正常，但在微信小程序真机上图片无法绘制，Canvas 上显示空白。",{"id":1121,"title":1021,"titles":1122,"content":1123,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-4",[865,1114],"微信小程序的 Canvas drawImage 方法不支持直接使用在线图片链接。与 H5 的 Canvas 不同，小程序要求图片必须是本地路径或临时文件路径才能正常绘制。 在线图片需要先通过 uni.getImageInfo 下载到本地，获取临时路径后才能用于 Canvas 绘制。",{"id":1125,"title":1026,"titles":1126,"content":1127,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现代码-2",[865,1114],"\u002F\u002F ❌ 以下代码在微信小程序中无法绘制图片\nconst ctx = uni.createCanvasContext('myCanvas')\nctx.drawImage('https:\u002F\u002Fexample.com\u002Fimage.png', 0, 0, 300, 200)\nctx.draw()\n\u002F\u002F H5 正常，小程序上图片不显示",{"id":1129,"title":1031,"titles":1130,"content":1131,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-4",[865,1114],"使用 uni.getImageInfo 先获取图片的本地临时路径，再进行绘制： \u002F\u002F ✅ 正确写法\nconst drawImage = async (url: string) => {\n  try {\n    \u002F\u002F 先获取图片信息，会自动下载到本地并返回临时路径\n    const imageInfo = await uni.getImageInfo({\n      src: url\n    })\n\n    const ctx = uni.createCanvasContext('myCanvas')\n    \u002F\u002F 使用返回的临时路径进行绘制\n    ctx.drawImage(imageInfo.path, 0, 0, 300, 200)\n    ctx.draw()\n  } catch (error) {\n    console.error('图片加载失败', error)\n  }\n}\n\n\u002F\u002F 调用\ndrawImage('https:\u002F\u002Fexample.com\u002Fimage.png')",{"id":1133,"title":1134,"titles":1135,"content":1136,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#封装工具函数","封装工具函数",[865,1114],"建议封装一个通用的图片预加载函数，方便复用： \u002F\u002F utils\u002Fcanvas.ts\n\n\u002F**\n * 预加载图片，返回可用于 Canvas 绘制的本地路径\n * @param url 图片链接（支持在线链接和本地路径）\n *\u002F\nexport async function preloadImage(url: string): Promise\u003Cstring> {\n  \u002F\u002F 如果已经是本地路径，直接返回\n  if (url.startsWith('\u002F') || url.startsWith('wxfile:\u002F\u002F') || url.startsWith('http:\u002F\u002Ftmp')) {\n    return url\n  }\n\n  const res = await uni.getImageInfo({ src: url })\n  return res.path\n}\n\n\u002F**\n * 批量预加载图片\n * @param urls 图片链接数组\n *\u002F\nexport async function preloadImages(urls: string[]): Promise\u003Cstring[]> {\n  return Promise.all(urls.map(preloadImage))\n} 使用示例： import { preloadImage, preloadImages } from '@\u002Futils\u002Fcanvas'\n\n\u002F\u002F 单张图片\nconst localPath = await preloadImage('https:\u002F\u002Fexample.com\u002Fbg.png')\nctx.drawImage(localPath, 0, 0, 300, 200)\n\n\u002F\u002F 批量预加载\nconst localPaths = await preloadImages([\n  'https:\u002F\u002Fexample.com\u002Fbg.png',\n  'https:\u002F\u002Fexample.com\u002Favatar.png',\n  'https:\u002F\u002Fexample.com\u002Flogo.png'\n])",{"id":1138,"title":1036,"titles":1139,"content":1140,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-4",[865,1114],"此问题仅在微信小程序中出现，H5 的 Canvas 可以直接使用在线图片链接uni.getImageInfo 会自动处理图片下载和缓存，重复调用相同链接会使用缓存对于需要绘制多张图片的场景，建议使用 Promise.all 并行预加载，提升性能如果图片链接需要鉴权或有防盗链，需要确保小程序已配置对应的下载域名白名单",{"id":1142,"title":1143,"titles":1144,"content":103,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-6微信小程序-picker-未选中项有默认背景颜色","坑 6：微信小程序 picker 未选中项有默认背景颜色",[865],{"id":1146,"title":318,"titles":1147,"content":1148,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-5",[865,1143],"在微信小程序中使用 \u003Cpicker> 组件时，未选中的选项会显示默认的背景颜色（通常是灰色渐变遮罩），影响自定义 UI 风格的统一性。",{"id":1150,"title":1021,"titles":1151,"content":1152,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-5",[865,1143],"微信小程序的 picker 组件自带的遮罩层（mask）有默认的背景样式，用于区分选中项和未选中项。这个样式是组件内置的，普通的 CSS 选择器无法直接覆盖。",{"id":1154,"title":1031,"titles":1155,"content":1156,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-5",[865,1143],"通过 mask-class 和 mask-style 属性来自定义遮罩层样式。以去除背景色为例： \u003Ctemplate>\n  \u003Cpicker\n    mode=\"selector\"\n    :range=\"options\"\n    :value=\"selectedIndex\"\n    mask-class=\"picker-mask-transparent\"\n    mask-style=\"background-image:none;background-color:transparent\"\n    @change=\"onChange\"\n  >\n    \u003Cview>{{ options[selectedIndex] }}\u003C\u002Fview>\n  \u003C\u002Fpicker>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from 'vue'\n\nconst options = ref(['选项一', '选项二', '选项三'])\nconst selectedIndex = ref(0)\n\nconst onChange = (e) => {\n  selectedIndex.value = e.detail.value\n}\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n\u002F* 使用 :deep() 穿透 scoped 样式 *\u002F\n:deep(.picker-mask-transparent) {\n  background-image: none !important;\n  background-color: transparent !important;\n}\n\u003C\u002Fstyle>",{"id":1158,"title":1159,"titles":1160,"content":1161,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#关键配置说明","关键配置说明",[865,1143],"属性作用mask-class指定遮罩层的自定义 classmask-style内联样式，直接覆盖遮罩层样式 两个属性同时使用效果更可靠，mask-style 作为内联样式优先级更高，mask-class 配合 !important 作为兜底。",{"id":1163,"title":1036,"titles":1164,"content":1165,"level":115},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-5",[865,1143],"scoped 样式需使用 :deep() 穿透：由于 picker 的遮罩层是组件内部元素，需要使用 :deep() 来穿透 scoped 样式的限制此问题主要在微信小程序中出现，H5 端的 picker 样式表现可能不同如果需要自定义其他遮罩样式（如半透明黑色、模糊效果等），可以修改 background-color 和 background-image 的值",{"id":1167,"title":1168,"titles":1169,"content":1170,"level":109},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#总结","总结",[865],"问题触发条件H5 是否复现解决方案uni.showModal 无法唤起按钮文字超过 4 个字符不复现按钮文字限制在 4 字符以内iOS 出现横向滚动条定位元素超出视口边界通常不复现根节点添加 overflow-x: hiddenpicker 日期回显错误未设置 fields 属性 + 非 UTC 时区不复现明确设置 fields=\"day\"iOS 模拟器 Emoji 无法渲染Xcode 26.1-26.3 模拟器缺少字体包不复现升级模拟器运行时至 26.4+Canvas drawImage 在线图片不显示直接使用在线图片链接不复现使用 uni.getImageInfo 获取本地路径picker 未选中项有背景颜色微信小程序默认遮罩样式可能不同使用 mask-class 和 mask-style 属性 这些问题的共同特点是：H5 调试正常，真机才暴露，排查时容易忽略平台差异。建议在开发阶段尽早进行真机测试，减少上线前的返工。 html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .s6hCs, html code.shiki .s6hCs{--shiki-light:#9C3EDA;--shiki-light-font-style:italic;--shiki-default:#C792EA;--shiki-default-font-style:italic;--shiki-dark:#C792EA;--shiki-dark-font-style:italic}",{"id":870,"title":869,"titles":1172,"content":1173,"level":98},[],"UniApp 深层组件里的 Popup、Modal 遮罩被父级 overflow\u002Fz-index 裁切或盖住时，用 RootPortal 传送到页面根节点解决；适用于 uview-plus 等弹层组件。 在 UniApp 项目中使用 uview-plus 等组件库时，经常会遇到一个问题：当 Popup 或 Modal 组件被封装在较深的组件层级中时，遮罩层会出现显示异常——要么遮罩层被父元素裁剪，要么遮罩层的层级不正确导致无法覆盖整个页面。",{"id":1175,"title":1176,"titles":1177,"content":1178,"level":109},"\u002Fblog\u002Funiapp-root-portal#问题复现","问题复现",[869],"假设你有一个深层嵌套的组件结构： \u003C!-- pages\u002Findex.vue -->\n\u003Ctemplate>\n  \u003Cview class=\"page\">\n    \u003CParentComponent \u002F>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003C!-- components\u002FParentComponent.vue -->\n\u003Ctemplate>\n  \u003Cview class=\"parent\" style=\"position: relative; overflow: hidden;\">\n    \u003CChildComponent \u002F>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003C!-- components\u002FChildComponent.vue -->\n\u003Ctemplate>\n  \u003Cview>\n    \u003Cu-popup v-model=\"show\" mode=\"bottom\">\n      \u003Cview class=\"popup-content\">弹窗内容\u003C\u002Fview>\n    \u003C\u002Fu-popup>\n    \u003Cbutton @click=\"show = true\">打开弹窗\u003C\u002Fbutton>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate> 当 ParentComponent 设置了 overflow: hidden 或 position: relative 时，u-popup 的遮罩层和弹窗内容会被限制在父元素的范围内，无法正确覆盖整个页面。",{"id":1180,"title":1181,"titles":1182,"content":1183,"level":109},"\u002Fblog\u002Funiapp-root-portal#解决方案rootportal-组件","解决方案：RootPortal 组件",[869],"参考 wot-design-uni 的实现思路，我们可以创建一个 RootPortal 组件，将子元素传送到页面根节点，从而摆脱父元素的层级和样式限制。",{"id":1185,"title":1186,"titles":1187,"content":1188,"level":115},"\u002Fblog\u002Funiapp-root-portal#组件实现","组件实现",[869,1181],"\u003C!-- components\u002FRootPortal.vue -->\n\u003Ctemplate>\n  \u003C!-- #ifdef H5 -->\n  \u003C!-- H5 端使用 Vue3 的 teleport -->\n  \u003Cteleport to=\"body\">\n    \u003C!-- #endif -->\n    \u003C!-- #ifdef MP-WEIXIN || MP-ALIPAY -->\n    \u003C!-- #ifndef MP-DINGTALK -->\n    \u003C!-- 微信\u002F支付宝小程序使用原生 root-portal -->\n    \u003Croot-portal>\n      \u003C!-- #endif -->\n      \u003C!-- #endif -->\n      \u003Cview>\n        \u003Cslot \u002F>\n      \u003C\u002Fview>\n      \u003C!-- #ifdef MP-WEIXIN || MP-ALIPAY -->\n      \u003C!-- #ifndef MP-DINGTALK -->\n    \u003C\u002Froot-portal>\n    \u003C!-- #endif -->\n    \u003C!-- #endif -->\n    \u003C!-- #ifdef H5 -->\n  \u003C\u002Fteleport>\n  \u003C!-- #endif -->\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\">\nexport default {\n  options: {\n    virtualHost: true,\n    addGlobalClass: true,\n    styleIsolation: \"shared\"\n  }\n};\n\u003C\u002Fscript>\n\n\u003Cscript setup>\u003C\u002Fscript>\n\n\u003C!-- #ifdef APP-PLUS -->\n\u003Cscript module=\"render\" lang=\"renderjs\">\nexport default {\n  mounted() {\n    if (this.$ownerInstance.$el) {\n      (document.querySelector('uni-app') || document.body).appendChild(this.$ownerInstance.$el)\n    }\n  },\n  beforeDestroy() {\n    if (this.$ownerInstance.$el) {\n      (document.querySelector('uni-app') || document.body).removeChild(this.$ownerInstance.$el)\n    }\n  }\n}\n\u003C\u002Fscript>\n\u003C!-- #endif -->",{"id":1190,"title":1191,"titles":1192,"content":1193,"level":115},"\u002Fblog\u002Funiapp-root-portal#各平台实现原理","各平台实现原理",[869,1181],"平台实现方式说明H5\u003Cteleport to=\"body\">Vue3 内置的传送门组件，将内容渲染到 body 节点微信\u002F支付宝小程序\u003Croot-portal>小程序原生组件，将内容渲染到页面根节点APPrenderjs DOM 操作通过 renderjs 直接操作 DOM，将元素移动到 uni-app 根节点",{"id":1195,"title":1159,"titles":1196,"content":1197,"level":115},"\u002Fblog\u002Funiapp-root-portal#关键配置说明",[869,1181],"export default {\n  options: {\n    virtualHost: true,      \u002F\u002F 虚拟化组件节点，不生成额外的包裹元素\n    addGlobalClass: true,   \u002F\u002F 允许使用全局样式类\n    styleIsolation: \"shared\" \u002F\u002F 共享样式作用域\n  }\n}; virtualHost: true：避免组件生成额外的包裹节点，减少 DOM 层级addGlobalClass: true：允许 slot 内容使用全局样式styleIsolation: \"shared\"：样式不隔离，确保弹窗样式正常生效",{"id":1199,"title":768,"titles":1200,"content":103,"level":109},"\u002Fblog\u002Funiapp-root-portal#使用方式",[869],{"id":1202,"title":1203,"titles":1204,"content":1205,"level":115},"\u002Fblog\u002Funiapp-root-portal#基础用法","基础用法",[869,768],"\u003Ctemplate>\n  \u003Cview>\n    \u003CRootPortal>\n      \u003Cu-popup v-model=\"show\" mode=\"bottom\">\n        \u003Cview class=\"popup-content\">\n          弹窗内容\n        \u003C\u002Fview>\n      \u003C\u002Fu-popup>\n    \u003C\u002FRootPortal>\n    \u003Cbutton @click=\"show = true\">打开弹窗\u003C\u002Fbutton>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from 'vue'\nimport RootPortal from '@\u002Fcomponents\u002FRootPortal.vue'\n\nconst show = ref(false)\n\u003C\u002Fscript>",{"id":1207,"title":1208,"titles":1209,"content":1210,"level":115},"\u002Fblog\u002Funiapp-root-portal#封装业务组件","封装业务组件",[869,768],"如果你有一个需要在多处使用的弹窗组件，可以在内部直接集成 RootPortal： \u003C!-- components\u002FMyModal.vue -->\n\u003Ctemplate>\n  \u003CRootPortal>\n    \u003Cu-popup v-model=\"modelValue\" mode=\"center\" @close=\"handleClose\">\n      \u003Cview class=\"modal-container\">\n        \u003Cview class=\"modal-header\">\n          \u003Ctext>{{ title }}\u003C\u002Ftext>\n          \u003Cu-icon name=\"close\" @click=\"handleClose\" \u002F>\n        \u003C\u002Fview>\n        \u003Cview class=\"modal-body\">\n          \u003Cslot \u002F>\n        \u003C\u002Fview>\n        \u003Cview class=\"modal-footer\">\n          \u003Cu-button @click=\"handleClose\">取消\u003C\u002Fu-button>\n          \u003Cu-button type=\"primary\" @click=\"handleConfirm\">确认\u003C\u002Fu-button>\n        \u003C\u002Fview>\n      \u003C\u002Fview>\n    \u003C\u002Fu-popup>\n  \u003C\u002FRootPortal>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport RootPortal from '@\u002Fcomponents\u002FRootPortal.vue'\n\nconst props = defineProps({\n  modelValue: Boolean,\n  title: String\n})\n\nconst emit = defineEmits(['update:modelValue', 'confirm'])\n\nconst handleClose = () => {\n  emit('update:modelValue', false)\n}\n\nconst handleConfirm = () => {\n  emit('confirm')\n  handleClose()\n}\n\u003C\u002Fscript>",{"id":1212,"title":1036,"titles":1213,"content":103,"level":109},"\u002Fblog\u002Funiapp-root-portal#注意事项",[869],{"id":1215,"title":1216,"titles":1217,"content":1218,"level":115},"\u002Fblog\u002Funiapp-root-portal#_1-钉钉小程序不支持-root-portal","1. 钉钉小程序不支持 root-portal",[869,1036],"代码中通过 #ifndef MP-DINGTALK 排除了钉钉小程序，因为钉钉小程序不支持 root-portal 组件。如果你的项目需要支持钉钉小程序，弹窗可能仍会有层级问题，需要另寻解决方案（如调整组件结构、避免深层嵌套）。",{"id":1220,"title":1221,"titles":1222,"content":1223,"level":115},"\u002Fblog\u002Funiapp-root-portal#_2-app-端的-renderjs-限制","2. APP 端的 renderjs 限制",[869,1036],"在 APP 端使用 renderjs 操作 DOM 时，需要注意： renderjs 只能访问 DOM，无法直接访问 Vue 实例的数据beforeDestroy 钩子确保组件销毁时清理 DOM，避免内存泄漏如果弹窗内有复杂的交互逻辑，可能需要额外处理",{"id":1225,"title":1226,"titles":1227,"content":1228,"level":115},"\u002Fblog\u002Funiapp-root-portal#_3-样式穿透","3. 样式穿透",[869,1036],"由于内容被传送到了根节点，scoped 样式可能无法生效。建议： 使用全局样式定义弹窗内容的样式或者使用 :deep() 穿透选择器 \u003Cstyle>\n\u002F* 全局样式 *\u002F\n.popup-content {\n  padding: 20rpx;\n  background: #fff;\n}\n\u003C\u002Fstyle>\n\n\u003C!-- 或者使用 :deep() -->\n\u003Cstyle scoped>\n:deep(.popup-content) {\n  padding: 20rpx;\n  background: #fff;\n}\n\u003C\u002Fstyle>",{"id":1230,"title":1231,"titles":1232,"content":1233,"level":115},"\u002Fblog\u002Funiapp-root-portal#_4-事件冒泡","4. 事件冒泡",[869,1036],"传送到根节点后，事件冒泡的路径会改变。如果你依赖事件冒泡来关闭弹窗或处理其他逻辑，需要确认逻辑是否仍然正确。",{"id":1235,"title":1168,"titles":1236,"content":1237,"level":109},"\u002Fblog\u002Funiapp-root-portal#总结",[869],"场景问题解决方案Popup 被父元素裁剪overflow: hidden 导致弹窗不完整使用 RootPortal 传送到根节点遮罩层级不正确遮罩无法覆盖整个页面使用 RootPortal 传送到根节点多层嵌套组件中的弹窗层级混乱，样式异常使用 RootPortal 传送到根节点 RootPortal 组件通过条件编译实现了跨平台兼容，是解决 UniApp 中弹层组件层级问题的通用方案。将它集成到你的项目中，可以让 Popup、Modal、Toast 等弹层组件在任何嵌套深度下都能正确显示。",{"id":1239,"title":1240,"titles":1241,"content":1242,"level":109},"\u002Fblog\u002Funiapp-root-portal#参考","参考",[869],"wot-design-uni RootPortal 实现微信小程序 root-portal 组件文档Vue3 Teleport 文档 html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":874,"title":873,"titles":1244,"content":1245,"level":98},[],"Apifox 官方未写清 VS Code Copilot 的 MCP 接入步骤。本文给出 mcp.json 配置、鉴权与验证方法，让 Copilot 直接读取 Apifox API 文档。",{"id":1247,"title":1248,"titles":1249,"content":1250,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#前言","前言",[873],"MCP（Model Context Protocol） 是一种让 AI 助手能够访问外部数据源的协议。Apifox 已经支持 MCP，这意味着我们可以让 GitHub Copilot 直接读取 Apifox 中的 API 文档，在编码时获得更精准的 API 相关建议。 然而，Apifox 的 MCP 官方文档主要介绍了如何在 Claude Desktop 等客户端中配置，并没有说明如何在 VS Code 的 Copilot 中使用。本文将详细介绍完整的配置流程。",{"id":1252,"title":552,"titles":1253,"content":1254,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#前置条件",[873],"在开始之前，请确保你已具备以下条件： ✅ 已安装 VS Code✅ 已安装 GitHub Copilot 扩展✅ 拥有 Apifox 账号并有项目访问权限✅ 已安装 Node.js（用于运行 npx 命令）",{"id":1256,"title":1257,"titles":1258,"content":103,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#配置步骤","配置步骤",[873],{"id":1260,"title":1261,"titles":1262,"content":1263,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第一步获取-apifox-项目-id","第一步：获取 Apifox 项目 ID",[873,1257],"登录 Apifox进入你想要接入的项目在浏览器地址栏中找到项目 ID，格式类似：https:\u002F\u002Fapp.apifox.com\u002Fproject\u002F123456，其中 123456 就是项目 ID",{"id":1265,"title":1266,"titles":1267,"content":1268,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第二步获取-apifox-api-访问令牌","第二步：获取 Apifox API 访问令牌",[873,1257],"点击 Apifox 右上角头像，选择「账号设置」进入「API 访问令牌」页面点击「新建令牌」，填写名称并创建复制并妥善保存生成的令牌（只显示一次！）",{"id":1270,"title":1271,"titles":1272,"content":1273,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第三步在-vs-code-中配置-mcp","第三步：在 VS Code 中配置 MCP",[873,1257],"这是本文的核心步骤，Apifox 官方文档中未提及的部分： 打开 VS Code按下 Ctrl + Shift + P（macOS 为 Cmd + Shift + P）唤起命令面板输入 MCP: Add Server 并回车在弹出的选择框中选择第一个选项：命令 (stdio)输入命令：npx输入服务器 ID：apifox（如果存在多个 Apifox 文档，需要注意命名避免冲突，如 apifox-user、apifox-order 等）选择配置范围：全局 或 本地（根据自己需要选择）VS Code 会自动打开 mcp.json 配置文件 此时你会看到类似这样的初始配置： {\n  \"servers\": {\n    \"apifox\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": []\n    }\n  }\n}",{"id":1275,"title":1276,"titles":1277,"content":1278,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第四步完善-mcp-配置","第四步：完善 MCP 配置",[873,1257],"修改 args 数组并添加 env 环境变量： {\n  \"servers\": {\n    \"apifox\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"apifox-mcp-server@latest\",\n        \"--project=你的项目ID\"\n      ],\n      \"env\": {\n        \"APIFOX_ACCESS_TOKEN\": \"你的apifox api访问令牌\"\n      }\n    }\n  }\n}",{"id":1280,"title":1281,"titles":1282,"content":1283,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第五步启动-mcp-服务器","第五步：启动 MCP 服务器",[873,1257],"配置完成后，点击服务器名称（apifox）上方的 启动 按钮即可启动 MCP 服务器。",{"id":1285,"title":1286,"titles":1287,"content":1288,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#配置项说明","配置项说明",[873,1257],"配置项说明type通信类型，stdio 表示通过标准输入输出通信command执行命令，使用 npx 运行 MCP 服务器args命令参数，-y 表示自动确认安装，--project 指定项目 IDenv环境变量，存放 API 访问令牌",{"id":1290,"title":1291,"titles":1292,"content":1293,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第六步验证配置","第六步：验证配置",[873,1257],"配置完成后，重启 VS Code。你可以通过以下方式验证 MCP 是否正常工作： 打开 Copilot Chat（Ctrl + Shift + I）切换到 Agent 模式尝试询问关于你项目 API 的问题，例如：「列出所有用户相关的 API 接口」 如果配置正确，Copilot 将能够读取你的 Apifox 项目文档并给出准确的回答。",{"id":1295,"title":1296,"titles":1297,"content":1298,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#完整配置示例","完整配置示例",[873],"以下是一个完整的 mcp.json 配置示例： {\n  \"servers\": {\n    \"apifox\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"apifox-mcp-server@latest\",\n        \"--project=4721539\"\n      ],\n      \"env\": {\n        \"APIFOX_ACCESS_TOKEN\": \"APS-xxxxxxxxxxxxxxxxxxxxxxxx\"\n      }\n    }\n  }\n}",{"id":1300,"title":1301,"titles":1302,"content":103,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#常见问题","常见问题",[873],{"id":1304,"title":1305,"titles":1306,"content":1307,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q1-提示找不到-npx-命令","Q1: 提示找不到 npx 命令？",[873,1301],"确保已正确安装 Node.js，并且 npx 命令在系统 PATH 中。可以在终端运行 npx -v 验证。",{"id":1309,"title":1310,"titles":1311,"content":1312,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q2-mcp-服务器启动失败","Q2: MCP 服务器启动失败？",[873,1301],"检查以下几点： 项目 ID 是否正确API 访问令牌是否有效网络是否能访问 Apifox 服务",{"id":1314,"title":1315,"titles":1316,"content":1317,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q3-copilot-没有使用-mcp-数据","Q3: Copilot 没有使用 MCP 数据？",[873,1301],"确保在 Copilot Chat 中切换到 Agent 模式，而非普通的 Chat 模式。",{"id":1319,"title":1320,"titles":1321,"content":1322,"level":115},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q4-如何配置多个项目","Q4: 如何配置多个项目？",[873,1301],"可以添加多个 MCP 服务器配置，使用不同的名称区分： {\n  \"servers\": {\n    \"apifox-project-a\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"apifox-mcp-server@latest\", \"--project=项目A的ID\"],\n      \"env\": {\n        \"APIFOX_ACCESS_TOKEN\": \"你的令牌\"\n      }\n    },\n    \"apifox-project-b\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"apifox-mcp-server@latest\", \"--project=项目B的ID\"],\n      \"env\": {\n        \"APIFOX_ACCESS_TOKEN\": \"你的令牌\"\n      }\n    }\n  }\n}",{"id":1324,"title":763,"titles":1325,"content":1326,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#使用场景",[873],"配置完成后，你可以在开发中这样使用： 🔍 查询 API：「帮我找到用户登录的接口」📝 生成代码：「根据 API 文档生成用户注册的请求函数」📖 理解接口：「解释一下订单列表接口的返回数据结构」🔧 调试辅助：「这个接口返回 400 错误可能是什么原因」",{"id":1328,"title":1168,"titles":1329,"content":1330,"level":109},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#总结",[873],"通过本文的配置，你可以让 VS Code 中的 GitHub Copilot 直接访问 Apifox 的 API 文档，大大提升 API 相关开发的效率。核心步骤就是通过 Ctrl + Shift + P → MCP: Add Server → 选择命令 (stdio) → 输入 npx → 设置服务器名称 → 完善配置并启动即可。 希望这篇文章能帮助到同样在寻找 VS Code Copilot 配置方法的开发者们！ html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"id":878,"title":877,"titles":1332,"content":1333,"level":98},[],"Unibest 默认安装后执行 build:app，产物 manifest.json 丢失 pages.config.ts 的 tabBar 等配置，导致 wgt 热更新安装后 App 白屏。本文说明原因与修复步骤。 在使用 Unibest 框架做 App 项目时，CI\u002FCD 流水线打出的 wgt 热更新包安装后，App 直接白屏。本地开发时一切正常，只有在干净环境（git clone + pnpm install + build:app）下才必现。",{"id":1335,"title":1336,"titles":1337,"content":1338,"level":109},"\u002Fblog\u002Funibest-manifest-missing-config#问题背景","问题背景",[877],"Unibest 通过 vite-plugin-uni-pages 在运行时自动扫描页面并生成 src\u002Fpages.json，同时把 tabBar、globalStyle、preloadRule 等配置写在 pages.config.ts 里统一管理。 为了避免自动生成文件污染 Git 历史，这两个文件通常被加入 .gitignore： src\u002Fmanifest.json\nsrc\u002Fpages.json 这在本地开发时没有问题，因为 dev 启动时插件会先落盘完整的 pages.json 再让 @dcloudio\u002Funi 读取。",{"id":1340,"title":1341,"titles":1342,"content":1343,"level":109},"\u002Fblog\u002Funibest-manifest-missing-config#问题根因","问题根因",[877],"冷 build（CI 环境）时存在竞态问题： build:app 启动时，@dcloudio\u002Funi 会在 vite-plugin-uni-pages 完成扫描、写入完整 pages.json 之前就去读取 tabBar 配置。 如果此时 src\u002Fpages.json 不存在或只是空文件（{}），@dcloudio\u002Funi 就会用一个只含默认配置的 manifest.json 打包，缺少： plus.tabBar（TabBar 的原生渲染配置）globalStylepreloadRule 最终 dist 目录里的 manifest.json 里没有 plus.tabBar，wgt 包安装后 App 找不到 TabBar 入口，直接白屏。",{"id":1345,"title":1346,"titles":1347,"content":1348,"level":109},"\u002Fblog\u002Funibest-manifest-missing-config#复现步骤","复现步骤",[877],"git clone \u003Cyour-unibest-project>\npnpm install\npnpm build:app   # 产物 dist\u002F...\u002Fmanifest.json 缺少 plus.tabBar 检查产物： # 确认缺失\ncat dist\u002Fbuild\u002Fapp\u002Fmanifest.json | grep -A5 \"tabBar\"\n# 输出为空 → 问题确认",{"id":1350,"title":1031,"titles":1351,"content":103,"level":109},"\u002Fblog\u002Funibest-manifest-missing-config#解决方案",[877],{"id":1353,"title":1354,"titles":1355,"content":1356,"level":115},"\u002Fblog\u002Funibest-manifest-missing-config#方案一推荐prebuild-脚本初始化-pagesjson不改-gitignore","方案一（推荐）：prebuild 脚本初始化 pages.json（不改 .gitignore）",[877,1031],"在 package.json 的 build:app 前挂载一个 prebuild:app 脚本，执行时把 pages.config.ts 里的 tabBar 等关键配置写入 src\u002Fpages.json，保证 @dcloudio\u002Funi 读取时文件已经就绪。 在项目根目录新建 scripts\u002Finit-base-files.mjs： \u002F\u002F scripts\u002Finit-base-files.mjs\n\u002F\u002F 此脚本用于生成 src\u002Fmanifest.json 和 src\u002Fpages.json 基础文件\n\u002F\u002F 由于这两个配置文件会被添加到 .gitignore 中，因此需要通过此脚本确保项目能正常运行\n\u002F\u002F\n\u002F\u002F 注意：冷 build 时 @dcloudio\u002Funi 会在 UniPages 落盘完整 pages.json 之前就读取 tabBar。\n\u002F\u002F 因此这里必须把 pages.config.ts 中的 tabBar \u002F globalStyle \u002F preloadRule 写入 pages.json，\n\u002F\u002F 否则 dist\u002F...\u002Fmanifest.json 会缺 plus.tabBar，App 热更后白屏。\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport { fileURLToPath } from 'node:url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\nconst root = path.resolve(__dirname, '..')\n\nconst manifestPath = path.resolve(root, 'src\u002Fmanifest.json')\nconst pagesPath = path.resolve(root, 'src\u002Fpages.json')\nconst srcDir = path.resolve(root, 'src')\n\nif (!fs.existsSync(srcDir)) {\n  fs.mkdirSync(srcDir, { recursive: true })\n}\n\nconst MIN_SIZE = `{ }`.length\n\n\u002F\u002F 如果 src\u002Fmanifest.json 不存在，就创建它；或者如果文件大小小于等于 MIN_SIZE，也重新创建\nif (!fs.existsSync(manifestPath) || fs.statSync(manifestPath).size \u003C= MIN_SIZE) {\n  fs.writeFileSync(manifestPath, JSON.stringify({}, null, 2))\n}\n\n\u002F** 去掉 UniPages 可能写入的 \u002F\u002F 注释，便于 JSON.parse *\u002F\nfunction stripJsonComments(text) {\n  return text.replace(\u002F^\\s*\\\u002F\\\u002F.*$\u002Fgm, '')\n}\n\nfunction readExistingPages() {\n  if (!fs.existsSync(pagesPath) || fs.statSync(pagesPath).size \u003C= MIN_SIZE) {\n    return {\n      pages: [\n        {\n          path: 'pages\u002Findex\u002Findex',\n          type: 'home',\n          style: {\n            navigationStyle: 'custom',\n            navigationBarTitleText: '首页',\n          },\n        },\n      ],\n      subPackages: [],\n    }\n  }\n\n  try {\n    return JSON.parse(stripJsonComments(fs.readFileSync(pagesPath, 'utf8')))\n  }\n  catch {\n    return { pages: [], subPackages: [] }\n  }\n}\n\nconst require = createRequire(import.meta.url)\nconst jiti = require('jiti')(__filename, {\n  interopDefault: true,\n  esmResolve: true,\n})\n\nconst pagesConfigModule = jiti(path.resolve(root, 'pages.config.ts'))\nconst pagesConfig = pagesConfigModule?.default ?? pagesConfigModule\n\nif (!pagesConfig || typeof pagesConfig !== 'object') {\n  throw new Error('[init-baseFiles] failed to load pages.config.ts')\n}\n\nconst pagesJson = readExistingPages()\n\nif (!Array.isArray(pagesJson.pages)) pagesJson.pages = []\nif (!Array.isArray(pagesJson.subPackages)) pagesJson.subPackages = []\n\nif (pagesConfig.globalStyle) pagesJson.globalStyle = pagesConfig.globalStyle\nif (pagesConfig.preloadRule) pagesJson.preloadRule = pagesConfig.preloadRule\nif (pagesConfig.tabBar) pagesJson.tabBar = pagesConfig.tabBar\nelse delete pagesJson.tabBar\n\nfs.writeFileSync(pagesPath, `${JSON.stringify(pagesJson, null, 2)}\\n`)\n\nif (!pagesJson.tabBar) {\n  throw new Error('[init-baseFiles] pages.config.ts 未提供 tabBar，冷 build 产物会缺 plus.tabBar')\n}\n\nconsole.log(\n  `[init-baseFiles] pages.json ready (tabBar=true, pages=${pagesJson.pages.length})`,\n) 然后在 package.json 里添加 hook： {\n  \"scripts\": {\n    \"prebuild:app\": \"node scripts\u002Finit-base-files.mjs\",\n    \"build:app\": \"uni build -p app\"\n  }\n} npm\u002Fpnpm 会在执行 build:app 之前自动先执行 prebuild:app，无需改动任何构建命令。",{"id":1358,"title":1359,"titles":1360,"content":1361,"level":115},"\u002Fblog\u002Funibest-manifest-missing-config#方案二简单粗暴把两个文件从-gitignore-移除","方案二（简单粗暴）：把两个文件从 .gitignore 移除",[877,1031],"直接把 src\u002Fmanifest.json 和 src\u002Fpages.json 纳入 Git 版本控制，CI 环境拿到的是完整的、已经包含 tabBar 的配置文件。 # .gitignore\n- src\u002Fmanifest.json\n- src\u002Fpages.json 缺点：每次 dev 启动后，vite-plugin-uni-pages 都会重新生成 src\u002Fpages.json，Git 会频繁检测到文件变更，容易产生无意义的 diff 提交噪声。",{"id":1363,"title":1364,"titles":1365,"content":1366,"level":109},"\u002Fblog\u002Funibest-manifest-missing-config#对比","对比",[877],"方案一（prebuild 脚本）方案二（移除 .gitignore）实现成本需要新增脚本文件改一行 .gitignore 即可Git 噪声无每次 dev 都会产生 diff适合场景CI\u002FCD 自动化、多人协作项目小型项目或个人项目长期维护推荐不推荐",{"id":1368,"title":1168,"titles":1369,"content":1370,"level":109},"\u002Fblog\u002Funibest-manifest-missing-config#总结",[877],"这个问题的本质是冷构建时的竞态：@dcloudio\u002Funi 读取配置比 vite-plugin-uni-pages 写入 pages.json 更早，导致 tabBar 等关键配置丢失，最终打出来的 wgt 包缺少 App 运行所需的原生 TabBar 配置，安装后白屏。 推荐方案是在 CI 的 prebuild 阶段执行初始化脚本，提前把 pages.config.ts 中的配置同步到 src\u002Fpages.json，既不污染 Git 历史，也能保证构建产物完整。 html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":882,"title":881,"titles":1372,"content":1373,"level":98},[],"UniApp 配置 z-paging easycom 后与 @uni-helper\u002Fvite-plugin-uni-components 冲突，CICD 打包 H5 构建成功但页面组件渲染为原始标签。本文给出冲突原因与修复方式。 在 UniApp 项目中同时使用 z-paging 的 easycom 配置和 @uni-helper\u002Fvite-plugin-uni-components 时，本地开发一切正常，但一旦走 CI\u002FCD 流程打包到 H5，使用了 z-paging 的组件会直接渲染出 \u003Cz-paging> 原始标签，自动导入完全失效。",{"id":1375,"title":1376,"titles":1377,"content":1378,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#问题现象","问题现象",[881],"在 CI\u002FCD 环境执行 H5 打包后，页面上原本应该渲染为分页列表的区域，变成了浏览器无法识别的 \u003Cz-paging> 自定义标签，内容完全不显示。 本地开发环境（pnpm dev）和本地手动打包（pnpm build）均正常，仅在 CI\u002FCD 流水线中打包的产物出现此问题。",{"id":1380,"title":1381,"titles":1382,"content":1383,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#根本原因","根本原因",[881],"z-paging 官方文档推荐通过 pages.json 的 easycom 字段进行注册： \u002F\u002F pages.json\n{\n  \"easycom\": {\n    \"autoscan\": true,\n    \"custom\": {\n      \"^z-paging\": \"@\u002Funi_modules\u002Fz-paging\u002Fcomponents\u002Fz-paging\u002Fz-paging.vue\"\n    }\n  }\n} 而项目同时使用了 @uni-helper\u002Fvite-plugin-uni-components 来实现 Vite 层面的自动组件导入。 两套机制在本地可以共存，因为本地环境有一定的容错处理。但在 CI\u002FCD 的干净构建环境中，两者发生冲突：easycom 的配置干扰了 vite-plugin-uni-components 的解析，导致插件无法正确识别和导入 z-paging 组件，最终输出的 HTML 里保留了未解析的原始标签。",{"id":1385,"title":1386,"titles":1387,"content":1388,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#复现条件","复现条件",[881],"使用 UniApp + Vite 构建 H5pages.json 中配置了 z-paging 的 easycom 规则同时安装并启用了 @uni-helper\u002Fvite-plugin-uni-components在 CI\u002FCD 流水线（如 GitHub Actions、Jenkins 等）中执行打包",{"id":1390,"title":1031,"titles":1391,"content":1392,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#解决方案",[881],"两步操作，缺一不可：",{"id":1394,"title":1395,"titles":1396,"content":1397,"level":115},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#第一步删除-easycom-中的-z-paging-配置","第一步：删除 easycom 中的 z-paging 配置",[881,1031],"移除 pages.json 中 z-paging 的 easycom 注册规则，让组件完全交由 vite-plugin-uni-components 管理： \u002F\u002F pages.json\n{\n  \"easycom\": {\n    \"autoscan\": true,\n    \"custom\": {\n      \u002F\u002F ❌ 删除这一行\n      \u002F\u002F \"^z-paging\": \"@\u002Funi_modules\u002Fz-paging\u002Fcomponents\u002Fz-paging\u002Fz-paging.vue\"\n    }\n  }\n}",{"id":1399,"title":1400,"titles":1401,"content":1402,"level":115},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#第二步更新-uni-helpervite-plugin-uni-components","第二步：更新 @uni-helper\u002Fvite-plugin-uni-components",[881,1031],"将 @uni-helper\u002Fvite-plugin-uni-components 升级到 0.3.2 及以上版本，旧版本在处理 uni_modules 目录下的组件时存在解析缺陷： pnpm add @uni-helper\u002Fvite-plugin-uni-components@^0.3.2 确认 vite.config.ts 中插件已正确配置（通常保持默认即可）： \u002F\u002F vite.config.ts\nimport UniComponents from '@uni-helper\u002Fvite-plugin-uni-components'\n\nexport default defineConfig({\n  plugins: [\n    UniComponents({\n      \u002F\u002F 默认会自动扫描 src\u002Fcomponents 和 uni_modules 目录\n    }),\n  ],\n})",{"id":1404,"title":1405,"titles":1406,"content":1407,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#验证","验证",[881],"修改完成后，在 CI\u002FCD 中重新触发打包，检查产物是否正确渲染 z-paging 组件。 也可以在本地模拟 CI 环境（清除缓存后重新安装依赖并打包）来提前验证： # 清除依赖缓存，模拟 CI 环境\nrm -rf node_modules\npnpm install --frozen-lockfile\npnpm build:h5",{"id":1409,"title":1410,"titles":1411,"content":1412,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#测试环境","测试环境",[881],"以下为问题复现及修复验证时使用的依赖版本： 依赖版本UniApp (Vue 3)3.0.0-alpha-4080720251125001z-paging2.8.8@uni-helper\u002Fvite-plugin-uni-components0.3.2HBuilderX4.87pnpm9.xNode.js20.x",{"id":1414,"title":1168,"titles":1415,"content":1416,"level":109},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#总结",[881],"项目说明触发条件easycom + vite-plugin-uni-components 同时配置同一组件本地是否复现通常不复现（环境容错）CI\u002FCD 是否复现必现解决方案删除 easycom 中对应配置 + 更新插件版本 这个问题的隐蔽性在于本地和 CI\u002FCD 环境行为不一致，排查时容易忽略构建环境差异。当你遇到\"本地正常但打包后组件不渲染\"的情况，首先检查是否存在多套组件注册机制并行的情况。 html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"id":886,"title":885,"titles":1418,"content":1419,"level":98},[],"z-paging 聊天记录模式在安卓上聚焦后输入框顶到标题下方、中间留出整块空白。根因是系统 softinputMode 与 z-paging 键盘占位双重避让，本文记录排查路径与最终方案。 用 z-paging 的 use-chat-record-mode 做问答页时，iOS 表现正常，安卓一聚焦就翻车：输入框跑到标题正下方，中间空出一大块白，底部 tabbar 还贴在键盘上面。 官方聊天 demo 看起来只有几行配置，真机却踩了不少坑。本文记录完整排查与最终落地方式。",{"id":1421,"title":1376,"titles":1422,"content":1423,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#问题现象",[885],"聚焦输入框、软键盘弹出后： 自定义导航栏仍在顶部输入框紧贴导航栏下方（本应在键盘上方）输入框与底部之间有一块接近键盘高度的空白自定义 tabbar 仍显示在键盘上方 收起键盘后布局恢复。iOS 始终正常：导航栏不动，中间消息列表被挤压，输入框贴在键盘上方。",{"id":1425,"title":1426,"titles":1427,"content":1428,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#页面结构踩坑前提","页面结构（踩坑前提）",[885],"这不是普通二级页，而是 自定义 tabBar 页： z-paging + use-chat-record-mode#top：自定义 Navbar#bottom：textarea + 为自定义 tabbar 预留的约 65px 占位根组件里还有一层 position: fixed 的自定义 tabbar \u003Cz-paging use-chat-record-mode safe-area-inset-bottom>\n  \u003Ctemplate #top>\n    \u003CCustomNavbar \u002F>\n  \u003C\u002Ftemplate>\n\n  \u003C!-- 消息列表 -->\n\n  \u003Ctemplate #bottom>\n    \u003Cview>\n      \u003Ctextarea\n        v-model=\"inputText\"\n        auto-height\n        :adjust-position=\"false\"\n      \u002F>\n      \u003C!-- 避开自定义 tabbar -->\n      \u003Cview class=\"h-[65px] pb-safe\" \u002F>\n    \u003C\u002Fview>\n  \u003C\u002Ftemplate>\n\u003C\u002Fz-paging>",{"id":1430,"title":1431,"titles":1432,"content":1433,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#官方约定其实很简单","官方约定其实很简单",[885],"z-paging 2.7+ 聊天模式的设计是： 输入框设 :adjust-position=\"false\"，不要让系统自动顶页面打开默认的 auto-adjust-position-when-chat，由 z-paging 在 #bottom 下方加一段 键盘高度占位flex 布局挤压中间列表，导航栏留在顶部 所以 demo 代码很少。前提是：只有 z-paging 在做避让。",{"id":1435,"title":1436,"titles":1437,"content":1438,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#根因双重避让","根因：双重避让",[885],"安卓上常常是两套机制同时生效： 机制行为系统 softinputMode默认多为 adjustPan（整页上推），或 adjustResize（压缩 WebView）z-paging auto-adjust-position-when-chat再在底部插入 keyboardHeight 高度的占位 结果就是：页面已经被系统挪过一次，z-paging 又「再抬」一次。 截图里输入框下面的大块空白，基本就是 z-paging 的键盘占位高度；输入框被顶到标题下面，是双重抬升后的视觉结果。 对应源码逻辑（示意）： \u002F\u002F z-paging chat-record-mode\nthis.keyboardHeight = res.height > 0\n  ? res.height + this.finalChatAdjustPositionOffset\n  : res.height \u003C!-- 插在 slot=bottom 下方 -->\n\u003Cview :style=\"{ height: keyboardHeight + 'px' }\" \u002F>",{"id":1440,"title":1441,"titles":1442,"content":103,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#排查过程走过的弯路","排查过程（走过的弯路）",[885],{"id":1444,"title":1445,"titles":1446,"content":1447,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_1-以为是-textarea-的-fixed","1. 以为是 textarea 的 fixed",[885,1441],"早期在 textarea 上写了 fixed。文档说：父级是 position: fixed 时才需要。我们的 #bottom 在默认非页面滚动模式下是 flex 贴底，不是 fixed。 删掉 fixed 后现象依旧。 说明主因不是它。",{"id":1449,"title":1450,"titles":1451,"content":1452,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_2-softinputmode-nothing","2. softinputMode: 'nothing'",[885,1441],"HTML5+ Webview 支持 'adjustPan' | 'adjustResize' | 'nothing'，语义是「不做调整」。注意： 安卓原生叫 adjustNothing，uni-app \u002F HTML5+ 里写的是 nothingpages.json 官方文档只明确写了 adjustResize \u002F adjustPan在 vue 页写 nothing 经常不生效 运行时 webview.setStyle({ softinputMode: 'nothing' }) 也没稳住。",{"id":1454,"title":1455,"titles":1456,"content":1457,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_3-误用编译期-isappandroid","3. 误用编译期 isAppAndroid",[885,1441],"一度改用 @uni-helper\u002Funi-env 的 isAppAndroid。它看的是编译期 UNI_APP_PLATFORM。 项目脚本是 uni -p app（一份 JS 双端跑）时，该常量在产物里恒为 false。于是安卓真机上「关 z-paging 占位」整段被跳过——表现成：代码还在、回滚也没用。 必须用运行时 uni.getSystemInfoSync().platform === 'android'。",{"id":1459,"title":1460,"titles":1461,"content":1462,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_4-以为一定要运行时-setstyle","4. 以为一定要运行时 setStyle",[885,1441],"排查中加过对本页 \u002F parent webview 的 setStyle({ softinputMode: 'adjustResize' })。后来在运行时 platform 判断正确的前提下删掉这段，真机仍然正常。 说明：pages \u002F definePage 里的 softinputMode: 'adjustResize' 已经够用；之前「删了 setStyle 就坏」多半是平台判断失效叠出来的假象。",{"id":1464,"title":1465,"titles":1466,"content":1467,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_5-github-上的临时方案","5. GitHub 上的临时方案",[885,1441],"uni-z-paging#259 里有人提： 设置 chat-adjust-position-offset 为 -键盘高度 本质是把占位算成 kb + (-kb) = 0，和安卓关掉 auto-adjust-position-when-chat 效果接近，还要动态绑高度，并不更简单。 另有人说换 editor 组件。对「发一句话」的聊天框成本偏高，本文未采用。",{"id":1469,"title":1470,"titles":1471,"content":1472,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#最终方案","最终方案",[885],"按平台拆开，只让 一套 机制负责避让： iOSAndroid系统 softinput不依赖pages 配置 adjustResizez-paging 键盘占位开关（运行时判断平台）自定义 tabbar键盘弹起时隐藏同左bottom 的 65px 占位键盘弹起时收起同左Navbar#top 内非 fixed同左",{"id":1474,"title":1475,"titles":1476,"content":1477,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_1页面配置-softinputmode","1）页面配置 softinputMode",[885,1470],"definePage({\n  style: {\n    navigationStyle: 'custom',\n    'app-plus': {\n      softinputMode: 'adjustResize',\n    },\n  },\n})",{"id":1479,"title":1480,"titles":1481,"content":1482,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_2运行时判断平台安卓关掉-z-paging-占位","2）运行时判断平台，安卓关掉 z-paging 占位",[885,1470],"const isAndroidAppRuntime = (() => {\n  \u002F\u002F #ifdef APP-PLUS\n  try {\n    return uni.getSystemInfoSync().platform.toLowerCase() === 'android'\n  }\n  catch {\n    return false\n  }\n  \u002F\u002F #endif\n  \u002F\u002F #ifndef APP-PLUS\n  return false\n  \u002F\u002F #endif\n})()\n\nconst autoAdjustPositionWhenChat = !isAndroidAppRuntime \u003Cz-paging\n  use-chat-record-mode\n  :auto-adjust-position-when-chat=\"autoAdjustPositionWhenChat\"\n  @keyboard-height-change=\"onKeyboardHeightChange\"\n>",{"id":1484,"title":1485,"titles":1486,"content":1487,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_3键盘弹起时隐藏自定义-tabbar","3）键盘弹起时隐藏自定义 tabbar",[885,1470],"\u002F\u002F tabbar store\nkeyboardOpen: false,\nsetKeyboardOpen(open: boolean) {\n  this.keyboardOpen = open\n} \u003C!-- App 根组件 -->\n\u003CCustomTabbar v-if=\"isCurrentPageTabbar && !tabbarStore.keyboardOpen\" \u002F> function onKeyboardHeightChange(res: { height: number }) {\n  tabbarStore.setKeyboardOpen(res.height > 0)\n} bottom 里的 65px 占位同步收起： \u003Cview :class=\"keyboardOpen ? 'h-0' : 'h-[65px] pb-safe'\" \u002F>",{"id":1489,"title":1490,"titles":1491,"content":1492,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_4navbar-不要在-top-里再-fixed","4）Navbar 不要在 #top 里再 fixed",[885,1470],"\u003Ctemplate #top>\n  \u003CCustomNavbar :fixed=\"false\" :placeholder=\"false\" \u002F>\n\u003C\u002Ftemplate>",{"id":1494,"title":1495,"titles":1496,"content":1497,"level":115},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#_5输入框","5）输入框",[885,1470],"\u003Ctextarea\n  v-model=\"inputText\"\n  auto-height\n  :adjust-position=\"false\"\n  :show-confirm-bar=\"false\"\n  confirm-type=\"send\"\n\u002F> 不要再挂无意义的 fixed。",{"id":1499,"title":1500,"titles":1501,"content":1502,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#为什么官方-demo不用这么处理","为什么官方 demo「不用这么处理」",[885],"demo 多半是普通页面，没有自定义 tabbar + 底部 65px 占位约定是「adjust-position=false + z-paging 独占避让」，环境干净时几行就够安卓上官方 demo 也会出现同类空白问题（见 issue #259） 我们这页难在：tabBar 页 + 自定义 fixed tabbar + 系统 softinput + z-paging 占位 叠在一起；再加上误用编译期平台常量，排查会被带偏。",{"id":1504,"title":1505,"titles":1506,"content":1507,"level":109},"\u002Fblog\u002Funiapp-z-paging-android-keyboard#小结","小结",[885],"空白 ≈ z-paging 键盘占位；飞顶 ≈ 系统避让 + 占位叠加pages 用官方的 adjustResize \u002F adjustPan，别指望 nothing \u002F adjustNothing平台判断用运行时 platform === 'android'，不要用 @uni-helper\u002Funi-env 的 isAppAndroid（uni -p app 双端同包时编译期恒为 false）安卓：softinputMode: 'adjustResize' + 关 auto-adjust-position-when-chat；iOS：保留 z-paging 占位运行时 webview.setStyle 不是必须（判断正确后可删）聊天页键盘弹起时藏自定义 tabbar，并去掉对应占位 一次只让一个角色负责键盘避让，这类布局问题会少很多。 html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":890,"title":889,"titles":1509,"content":1510,"level":98},[],"从本地 lint-staged、约定式提交，到 CI、PR 审阅与依赖更新，整理一条可复用的研发规范链，并说明如何给 AI 当上下文使用。",{"id":1512,"title":1248,"titles":1513,"content":1514,"level":109},"\u002Fblog\u002Fdev-spec-chain#前言",[889],"一个产品往往对应多个仓库：后端 API、管理后台、用户前台。单靠口头约定，很难让协作者和 AI 保持一致。 更稳妥的做法是把规范拆成一条可执行的链——本地快检、远程 CI、PR 审阅、合并上线，每一环都有工具兜底。这篇文章分享的是这条链本身，不涉及具体业务。 你也可以把下文直接贴给 Cursor \u002F Copilot 当上下文，减少「能跑但没 lint、没类型、没走 PR」的代码。",{"id":1516,"title":1517,"titles":1518,"content":1519,"level":109},"\u002Fblog\u002Fdev-spec-chain#分支策略建议","分支策略（建议）",[889],"多仓库可以统一采用 develop + main 双分支： 分支用途develop日常开发、联调、测试环境main生产发布，只通过 PR 合并 推荐流程： 本地开发 → commit → push develop\n    → 在测试环境验证\n    → 开 PR：develop → main\n    → CI 全绿 + 看过 diff\n    → 合并 → 生产更新 个别老仓库生产分支可能叫 master，思路一样，注意 PR 的 base 分支即可。",{"id":1521,"title":1522,"titles":1523,"content":1524,"level":109},"\u002Fblog\u002Fdev-spec-chain#规范链总览","规范链总览",[889],"┌─────────────────────────────────────────────────────────┐\n│ ① 本地                                                   │\n│    lint-staged（暂存文件）+ commitlint（提交信息）          │\n└──────────────────────────┬──────────────────────────────┘\n                           │ push develop\n                           ▼\n┌─────────────────────────────────────────────────────────┐\n│ ② 远程 develop                                           │\n│    CI 全量：lint \u002F typecheck \u002F test \u002F build              │\n└──────────────────────────┬──────────────────────────────┘\n                           │ 开 PR\n                           ▼\n┌─────────────────────────────────────────────────────────┐\n│ ③ Pull Request                                           │\n│    CI 再跑 + 自动 review + 人工看 diff（建议）            │\n│    Test plan：手动验收清单                               │\n└──────────────────────────┬──────────────────────────────┘\n                           │ merge\n                           ▼\n┌─────────────────────────────────────────────────────────┐\n│ ④ 生产 main                                              │\n│    按各仓库既有发布方式更新                                │\n└─────────────────────────────────────────────────────────┘ 三条设计原则： 本地要快：只检查本次改动的文件，避免每次 commit 跑全量 test。远程要严：CI 跑全量 lint、类型、测试、构建，作为合并门槛。测试与生产分离：develop 先验证，稳定后再 PR 到生产分支。",{"id":1526,"title":1527,"titles":1528,"content":1529,"level":109},"\u002Fblog\u002Fdev-spec-chain#本地提交前门禁","① 本地：提交前门禁",[889],"用 Husky 挂两个钩子： 钩子作用pre-commitlint-staged，只处理暂存区文件commit-msgcommitlint，强制约定式提交（如 feat:、fix:、ci:） 不同技术栈可以选不同格式化 \u002F lint 工具，常见组合： 类型常见工具Node 后端（Nest 等）ESLint + PrettierReact 管理端Biome，或 ESLint + PrettierNext.js 前台ESLint + Prettier 刻意不在 pre-commit 做的： 全量 typecheck、test、build——这些交给 CI，避免本地提交卡顿。 另外可以提交共享的 .vscode\u002Fsettings.json 与 extensions.json，统一保存时格式化；个人主题、字体等不要写进仓库。",{"id":1531,"title":1532,"titles":1533,"content":1534,"level":109},"\u002Fblog\u002Fdev-spec-chain#cipush-pr-时的全量检查","② CI：push \u002F PR 时的全量检查",[889],"触发时机： main \u002F develop 的 push，以及指向这两者的 PR。 按仓库类型划分 job，思路一致，内容可按栈调整：",{"id":1536,"title":1537,"titles":1538,"content":1539,"level":115},"\u002Fblog\u002Fdev-spec-chain#后端-api","后端 API",[889,1532],"Job典型内容lint全量 ESLinttypechecktsc --noEmittest单元测试（如 Jest）build框架构建命令e2e可选，独立 workflow",{"id":1541,"title":1542,"titles":1543,"content":1544,"level":115},"\u002Fblog\u002Fdev-spec-chain#管理后台","管理后台",[889,1532],"Job典型内容lintBiome \u002F ESLinttypechecktsctestVitest \u002F Jestbuild生产构建coverage可选，独立 workflow",{"id":1546,"title":1547,"titles":1548,"content":1549,"level":115},"\u002Fblog\u002Fdev-spec-chain#用户前台","用户前台",[889,1532],"Job典型内容lintESLinttypechecktscbuildnext build 等test有则加，没有可后续补 要点： CI 是合并前的硬门槛，红灯不应被忽视。",{"id":1551,"title":1552,"titles":1553,"content":103,"level":109},"\u002Fblog\u002Fdev-spec-chain#pr审阅与依赖更新","③ PR：审阅与依赖更新",[889],{"id":1555,"title":1556,"titles":1557,"content":1558,"level":115},"\u002Fblog\u002Fdev-spec-chain#自动-code-reviewpr-agent","自动 Code Review（PR-Agent）",[889,1552],"可在 PR 打开、更新时自动： 给出 review 意见辅助生成 PR 描述 需要配置对应 LLM 的 API Key（如 DeepSeek、OpenAI 等），放在仓库 Secrets。",{"id":1560,"title":1561,"titles":1562,"content":1563,"level":115},"\u002Fblog\u002Fdev-spec-chain#dependabot","Dependabot",[889,1552],"建议统一策略，避免和 Renovate 等工具重复： 包管理器（npm \u002F pnpm）：定期检查，可忽略 patch、分组合并GitHub Actions：定期检查 workflow 依赖",{"id":1565,"title":1566,"titles":1567,"content":1568,"level":115},"\u002Fblog\u002Fdev-spec-chain#test-plan-是什么","Test plan 是什么？",[889,1552],"PR 模板里的 Test plan 是人工验收清单，不是自动测试，例如： CI 全绿 测试环境核心流程点过 合并后生产环境关键项确认 合并前建议逐项勾选。",{"id":1570,"title":1571,"titles":1572,"content":1573,"level":115},"\u002Fblog\u002Fdev-spec-chain#人工-review","人工 Review",[889,1552],"小团队或免费版 GitHub 往往开不了完整的分支保护，因此： 可能无法强制「必须有人 Approve」可能无法从技术上禁止直推 main 这时更要靠流程习惯：走 PR、看 diff、CI 不过不合并。",{"id":1575,"title":1576,"titles":1577,"content":1578,"level":109},"\u002Fblog\u002Fdev-spec-chain#给-ai-的协作约定","给 AI 的协作约定",[889],"把下面几段放进对话，或写入仓库根目录的 AGENTS.md \u002F CLAUDE.md，可显著减少「能跑但不符合项目规范」的代码。",{"id":1580,"title":1581,"titles":1582,"content":1583,"level":115},"\u002Fblog\u002Fdev-spec-chain#必须遵守","必须遵守",[889,1576],"提交信息使用约定式格式：feat: \u002F fix: \u002F chore: \u002F ci: 等。改完代码确保能通过该仓库的 lint 与 typecheck；有单测的仓库改动需考虑测试。不要手改自动生成代码（如 OpenAPI client、ORM entity 生成物等），走项目规定的生成命令。环境变量、密钥不要写进仓库。合并到生产走 PR，并在 Test plan 里说明如何验证。",{"id":1585,"title":1586,"titles":1587,"content":1588,"level":115},"\u002Fblog\u002Fdev-spec-chain#建议-ai-改完后的自检","建议 AI 改完后的自检",[889,1576],"按仓库 package.json 里的脚本跑一遍，常见组合： npm run lint && npm run typecheck && npm run test && npm run build 有 monorepo 或多包时，在对应子包目录执行。",{"id":1590,"title":1591,"titles":1592,"content":1593,"level":115},"\u002Fblog\u002Fdev-spec-chain#pr-描述建议结构","PR 描述建议结构",[889,1576],"## Summary\n- 改了什么、为什么\n\n## Test plan\n- [ ] CI 通过\n- [ ] 测试 \u002F 本地关键路径验证\n- [ ] 合并后需确认的事项",{"id":1595,"title":1596,"titles":1597,"content":1598,"level":109},"\u002Fblog\u002Fdev-spec-chain#常见薄弱项","常见薄弱项",[889],"诚实列出来，方便后续补强： 项目说明分支保护免费版可能开不了，靠流程约束强制 Approve小团队常未配置前台单测很多项目初期没有，可逐步补多仓库分支命名不一致main vs master，开 PR 时注意 base",{"id":1600,"title":1505,"titles":1601,"content":1602,"level":109},"\u002Fblog\u002Fdev-spec-chain#小结",[889],"规范链不是 paperwork，而是分层门禁： 本地 lint-staged 拦住低级格式问题；CI 拦住类型、测试、构建；PR + 自动 review 拦住设计疏漏；分支策略拦住「没测就上生产」。 把这篇文章或 AGENTS.md 交给 AI，等于告诉它：不仅要写出能跑的代码，还要写出能过门禁、能合并、能上线的代码。 html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":894,"title":893,"titles":1604,"content":1605,"level":98},[],"以 PR-Agent + DeepSeek 为例，说明如何在 GitHub Actions 里为 Pull Request 接入自动代码审阅，并附上可直接复用的 workflow 与配置。",{"id":1607,"title":1248,"titles":1608,"content":1609,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#前言",[893],"开了 PR 不等于有人看。人一忙，review 就变成「先合了再说」。 更稳的做法是给仓库加一层 自动 Review：PR 一打开（或更新），机器人先扫一遍 diff，用评论指出明显问题；人再做最终判断。 这篇文章讲的是我们仓库里已经在用的方案：PR-Agent + DeepSeek，跑在 GitHub Actions 上。不依赖 Cursor Bugbot，也不绑死某一家商业 Code Review SaaS。",{"id":1611,"title":1612,"titles":1613,"content":1614,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#方案选型简表","方案选型（简表）",[893],"方案特点适合PR-Agent（本文）Actions 一键接入，模型可自选，可配置中文想自己控模型与提示词GitHub Copilot Review官方集成，省事已买 Copilot，想少维护CodeRabbit \u002F 同类产品化体验好愿意付订阅、少改 YAMLCursor BugbotIDE\u002F产品侧能力团队已深度用 Cursor 我们选 PR-Agent，主要因为： 配置全在仓库里，可复用到多个项目能用自己的 DEEPSEEK_API_KEY可以强制「用中文回复 + 聚焦某技术栈」",{"id":1616,"title":1617,"titles":1618,"content":1619,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#整体链路","整体链路",[893],"开发者开 PR \u002F push 更新\n        │\n        ▼\nGitHub Actions：PR Agent workflow\n        │\n        ├─ checkout 仓库里的 .pr_agent.toml（忽略规则等）\n        ├─ the-pr-agent\u002Fpr-agent@main\n        ├─ GITHUB_TOKEN（写评论）\n        └─ DEEPSEEK_API_KEY（调模型）\n        │\n        ▼\nPR 页面出现自动 Review \u002F 描述建议\n        │\n        ▼\n人工再看 diff + Test plan → 合并 注意：自动 Review 不能替代 CI 和人工。它负责「多一双眼睛」，合并不合仍由人和 CI 门禁决定。",{"id":1621,"title":1622,"titles":1623,"content":1624,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#一步准备-secrets","一步：准备 Secrets",[893],"仓库 → Settings → Secrets and variables → Actions，新增： Name说明DEEPSEEK_API_KEYDeepSeek 开放平台的 API Key GITHUB_TOKEN 一般不用手建，workflow 里用 ${{ secrets.GITHUB_TOKEN }} 即可。\n记得在 job 上给够权限：pull-requests: write、issues: write。 组织级仓库也可以把 Key 配在 Organization secrets，再授权给仓库用。",{"id":1626,"title":1627,"titles":1628,"content":1629,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#二步加-github-actions-workflow","二步：加 GitHub Actions Workflow",[893],"在仓库创建 .github\u002Fworkflows\u002Fpr-agent.yml，核心如下（与现网配置同思路）： # PR-Agent：用 DeepSeek 做 PR 自动 review\nname: PR Agent\n\non:\n  pull_request:\n    types: [opened, reopened, ready_for_review, synchronize]\n  issue_comment:\n    types: [created]\n\njobs:\n  pr_agent_job:\n    if: ${{ github.event.sender.type != 'Bot' && (github.event_name != 'issue_comment' || github.event.issue.pull_request) }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n      issues: write\n    name: Run PR Agent\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          sparse-checkout: |\n            .pr_agent.toml\n          sparse-checkout-cone-mode: false\n\n      - name: PR Agent\n        uses: the-pr-agent\u002Fpr-agent@main\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n          config.model: \"deepseek\u002Fdeepseek-v4-flash\"\n          config.fallback_models: '[\"deepseek\u002Fdeepseek-v4-flash\"]'\n          github_action_config.pr_actions: '[\"opened\", \"reopened\", \"ready_for_review\", \"synchronize\"]'\n          github_action_config.auto_review: \"true\"\n          github_action_config.auto_describe: \"true\"\n          github_action_config.auto_improve: \"false\"\n          pr_reviewer.extra_instructions: >-\n            Reply in Chinese (简体中文). Focus on your stack here.\n            Flag security and correctness issues. Ignore lockfile-only noise. 几个值得注意的点： synchronize 要显式打开\n否则只在 PR 打开时跑一次，后续 push 更新可能不再 review。过滤 Botgithub.event.sender.type != 'Bot'，避免机器人互相触发死循环。sparse-checkout\n只拉 .pr_agent.toml，启动更快；忽略规则仍生效。auto_describe\n可帮 PR 补描述；若你强制手写 Summary \u002F Test plan，也可以关掉，避免和团队模板抢戏。 官方安装说明可参考：PR-Agent GitHub 安装文档。",{"id":1631,"title":1632,"titles":1633,"content":1634,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#三步仓库根目录加-pr_agenttoml","三步：仓库根目录加 .pr_agent.toml",[893],"Workflow 里的 env 优先级通常更高；.pr_agent.toml 适合放默认模型、忽略文件、统一中文指令。 # https:\u002F\u002Fgithub.com\u002Fthe-pr-agent\u002Fpr-agent\u002Fblob\u002Fmain\u002Fpr_agent\u002Fsettings\u002Fconfiguration.toml\n\n[config]\nmodel = \"deepseek\u002Fdeepseek-v4-flash\"\nfallback_models = [\"deepseek\u002Fdeepseek-v4-flash\"]\n\n[github_action_config]\npr_actions = [\"opened\", \"reopened\", \"ready_for_review\", \"synchronize\"]\nauto_review = true\nauto_describe = true\nauto_improve = false\n\n[ignore]\nglob = [\n  \"package-lock.json\",\n  \"pnpm-lock.yaml\",\n  \"yarn.lock\",\n  \".agents\u002F**\",\n  \".agent\u002F**\",\n]\n\n[pr_reviewer]\nextra_instructions = \"Reply in Chinese (简体中文). Focus on security and correctness.\" 按仓库技术栈改 extra_instructions，例如： NestJS \u002F TypeORM：强调 auth、JWT、校验、migrationNuxt \u002F Vue：强调 XSS、服务端权限、数据获取管理后台：强调权限、表单校验、危险操作确认",{"id":1636,"title":1637,"titles":1638,"content":1639,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#四步开一个-pr-验证","四步：开一个 PR 验证",[893],"从 develop 拉小改动，推到远程并开 PR（或 develop → main）看 Actions 里 PR Agent 是否成功回到 PR 页面，应出现机器人评论（Review \u002F 描述建议）再 push 一次，确认 synchronize 会重新跑 若失败，优先查： DEEPSEEK_API_KEY 是否配置、是否有额度job permissions 是否够写 PR 评论模型名是否仍有效（DeepSeek 模型 ID 可能会变）是否被 if: Bot 条件跳过",{"id":1641,"title":1642,"titles":1643,"content":1644,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#和规范的-pr-正文怎么配合","和「规范的 PR 正文」怎么配合",[893],"自动 Review 解决的是「有没有人看代码」。\nPR 模板解决的是「作者有没有说清楚改了什么、怎么测」。 建议 PR 正文至少包含： ## Summary\n\n- 改了什么、为什么\n\n## Test plan\n\n- [ ] 本地\u002F预览如何验证\n- [ ] 关键路径与回归点 机器审 diff，人审意图和验收清单，两者一起才完整。",{"id":1646,"title":1647,"titles":1648,"content":1649,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#多仓库复用建议","多仓库复用建议",[893],"前端、后台、API 可以共用同一套骨架，只改两处： pr_reviewer.extra_instructions（技术栈不同）[ignore].glob（锁文件、生成物路径不同） DEEPSEEK_API_KEY 适合放在 Organization secrets，避免每个仓库复制一份。",{"id":1651,"title":1505,"titles":1652,"content":1653,"level":109},"\u002Fblog\u002Fgithub-pr-agent-review#小结",[893],"给 GitHub 仓库加 PR 自动 Review，落地成本其实不高： 配置 DEEPSEEK_API_KEY添加 .github\u002Fworkflows\u002Fpr-agent.yml添加 .pr_agent.toml开 PR 验证评论是否出现 它不会替你保证线上零事故，但能显著降低「没人看就合并」的概率，也适合作为 AI 辅助研发规范链里的一环。 如果你已经有 CI、约定式提交和双分支策略，把自动 Review 接上之后，PR 门禁会完整很多：本地快检 → 远程 CI → 自动 Review → 人工确认 → 合并发布。 html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"id":898,"title":897,"titles":1655,"content":1656,"level":98},[],"仓库全量、提交时、MR\u002FPR 三个时机怎么选：频率、范围、噪音成本，以及如何和现有规范链、PR 自动 Review 组合使用。",{"id":1658,"title":1248,"titles":1659,"content":1660,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#前言",[897],"「让 AI 看代码」很容易做成两件事之一：要么从不做，要么每次都全量扫一遍。前者漏问题，后者贵、慢、噪音大。 更稳的做法是按时机拆开：不同阶段看不同范围，频率和深度也不一样。 这篇文章只讲选型与节奏，不重复贴 workflow YAML。落地配置可以看： 前端全栈项目的开发规范链 — 本地 \u002F CI \u002F PR 门禁总览GitHub 仓库怎么给 PR 加上自动 Review — PR-Agent 具体接法",{"id":1662,"title":1663,"titles":1664,"content":1665,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#三个时机总览","三个时机总览",[897],"时机范围建议频率目标① 仓库全量 Review整仓或大模块隔一个版本 \u002F 发版周期穿插架构债、死代码、跨模块隐患② 提交时 Review本次 diff按需 \u002F 重要提交拦低级错误，不拖慢日常提交③ MR \u002F PR ReviewPR 变更集每个要合入的 PR合入前再过一眼，配合 CI 与人工 日常开发\n   │\n   ├─ commit ──→ ② 提交时 Review（轻、快、只看 diff）\n   │\n   ├─ 开 PR ──→ ③ MR\u002FPR Review（自动评论 + 人看意图）\n   │\n   └─ 发版\u002F隔版 ──→ ① 全量 Review（重、慢、找系统性债务） 三条原则： 全量贵：适合周期性做，不要绑在每次 commit 上。提交要快：只扫本次改动，结论要可执行、噪音要低。合入要严：MR\u002FPR 是最后一道自动 Review 门，再配上人工看 Test plan。",{"id":1667,"title":1668,"titles":1669,"content":103,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#仓库全量-review","① 仓库全量 Review",[897],{"id":1671,"title":1672,"titles":1673,"content":1674,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#适合什么","适合什么",[897,1668],"整仓或某个大目录做一次「体检」。适合问这类问题： 有没有明显的重复实现、死代码、过期注释？错误处理、鉴权、日志是否在几个模块里风格分裂？有没有跨文件才能看出来的设计问题？ 这类问题看单次 diff 很难发现，适合全量或半全量上下文。",{"id":1676,"title":1677,"titles":1678,"content":1679,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#建议节奏","建议节奏",[897,1668],"隔一个版本做一次，或按发版周期穿插（例如：奇数版做功能冲刺，偶数版留半天做全量 AI Review + 人读结论）。 不要指望每周全量：上下文长、费用高，模型也容易泛泛而谈。",{"id":1681,"title":1682,"titles":1683,"content":1684,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#怎么做最小落地","怎么做（最小落地）",[897,1668],"在 IDE \u002F Agent 里明确范围和目标，例如： 对 server\u002F 做一次缺陷优先的全量 review：\n- 先列高风险问题（安全、数据一致性、错误吞掉）\n- 再列中风险（可维护性、重复逻辑）\n- 不写大段重写建议，每个问题给文件路径 + 为什么 + 改法一句话 输出建议落成 issue 清单 或短文档，挑 2～3 个真要改的进下一迭代，而不是一次改完所有「建议」。",{"id":1686,"title":1687,"titles":1688,"content":1689,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#不适合什么","不适合什么",[897,1668],"紧急修 bug、当天要上线的小改动还没定分支策略、PR 习惯还很乱的阶段（先把 ②③ 和 CI 立住）",{"id":1691,"title":1692,"titles":1693,"content":103,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#提交时-review","② 提交时 Review",[897],{"id":1695,"title":1672,"titles":1696,"content":1697,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#适合什么-1",[897,1692],"只看即将提交或刚改完的 diff：命名、明显逻辑漏洞、漏改测试、敏感信息、和本仓库规范不一致的写法。 目标是：在代码进远程之前，便宜地拦一层。",{"id":1699,"title":1677,"titles":1700,"content":1701,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#建议节奏-1",[897,1692],"不必每个 commit 都跑。更实用的触发： 涉及鉴权、支付、数据库迁移、公共 API 的改动自己觉得「有点绕、怕漏」的提交准备开 PR 前的最后一次整理提交 日常小改（文案、样式微调）可以跳过，靠 lint 和 CI 即可。",{"id":1703,"title":1682,"titles":1704,"content":1705,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#怎么做最小落地-1",[897,1692],"工具任选其一即可，关键是范围锁在 diff： 方式说明Cursor \u002Fcode-review 或 Agent指定「只看当前未提交 \u002F 本分支相对 develop 的改动」IDE 内联助手对改动文件提问，避免「请 review 整个项目」本地 hook（可选）适合团队统一；要控制耗时，别让每次 commit 等几十秒 提问模板可以固定： 缺陷优先 review 当前 diff：\n1. 正确性 \u002F 边界\n2. 安全与敏感信息\n3. 是否破坏现有约定（API、命名、错误处理）\n不要复述代码，只列问题。",{"id":1707,"title":1687,"titles":1708,"content":1709,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#不适合什么-1",[897,1692],"替代 lint \u002F typecheck \u002F test（这些仍应由门禁跑）在 hook 里跑「全仓深度思考」——提交会变得又慢又烦，最后大家 --no-verify",{"id":1711,"title":1712,"titles":1713,"content":103,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#mr-pr-review","③ MR \u002F PR Review",[897],{"id":1715,"title":1672,"titles":1716,"content":1717,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#适合什么-2",[897,1712],"合入 develop \u002F main 前，对 PR 变更集 做自动审阅：评论打在 diff 上，人再看意图和验收清单。 这是三个时机里最该默认开启的一环：和 CI、分支策略绑在一起，噪音可控、收益稳定。",{"id":1719,"title":1677,"titles":1720,"content":1721,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#建议节奏-2",[897,1712],"每个要合入的 PR 都跑（打开与后续 push 更新时可再跑）。\n小团队也可以约定：只有进生产的 PR（如 develop → main）必须有自动 Review + 人工过一眼。",{"id":1723,"title":1682,"titles":1724,"content":1725,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#怎么做最小落地-2",[897,1712],"用 GitHub Actions + PR-Agent（或 Copilot Review \u002F CodeRabbit）即可。本仓库已有一篇完整接法： → GitHub 仓库怎么给 PR 加上自动 Review 和人工怎么分工： 谁看什么AIdiff 上的明显缺陷、风格与规范偏离、可疑边界人为什么改、产品影响、Test plan 是否真测过、该不该合 机器审代码，人审意图——两边都要。",{"id":1727,"title":1687,"titles":1728,"content":1729,"level":115},"\u002Fblog\u002Fai-code-review-three-timings#不适合什么-2",[897,1712],"把自动 Approve 当成合并条件的唯一依据忽略 CI：没有 lint\u002Ftest 的 PR，再漂亮的 AI 评论也兜不住",{"id":1731,"title":1732,"titles":1733,"content":1734,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#怎么组合","怎么组合",[897],"一套可执行的默认组合： 每天 \u002F 每功能\n  → 重要提交用 ②\n  → 开 PR 必开 ③ + CI\n\n每个发版周期（或隔一版）\n  → 抽半天做 ①，结论进 backlog 和规范链对齐时，可以记成： 规范链环节AI Review 时机本地 lint-staged \u002F commitlint② 可选增强远程 CI不替代，并行PR 自动 review + 人工③ 主战场发版后复盘 \u002F 技术债① 周期性 小团队起步顺序建议：先 ③ → 再按需 ② → 稳定后再加 ①。先把「合入前一定有人（或机器人）看 diff」做实，再谈全仓体检。",{"id":1736,"title":1505,"titles":1737,"content":1738,"level":109},"\u002Fblog\u002Fai-code-review-three-timings#小结",[897],"AI 代码 Review 不是开关，是分层节奏： 全量：隔版做，找系统性债务提交时：按需、只看 diff，保本地速度MR\u002FPR：默认开启，合入前多一双眼睛 它补的是「没人看就合并」和「只盯着 diff 永远看不见仓库整体」这两种盲区；替代不了 CI、测试和人对产品意图的判断。 如果你已经在推 开发规范链，把这三个时机嵌进去，比单独堆一个「全能 AI Review」更可持续。",{"id":902,"title":901,"titles":1740,"content":1741,"level":98},[],"照 uni-push 2.0 快速接入在 receive 里补上无条件 createPushMessage 后，iOS 即使 force_notification 为 true 仍会进 receive，点通知还会再来一条带 aps 的 receive，通知弹两次。判断写在常见问题里，快速接入完全没提。 照着 uni-push 2.0 快速接入指南 接在线通知，iOS 真机会踩进一个很隐蔽的坑：官方示例本身没有真正调用 createPushMessage，只是用注释提示「可在此回调里创建通知」；若你按提示在 receive 里补上无条件的 createPushMessage()，通知就会弹两次。 force_notification: true 挡不住。真正要用的 aps 判断，写在另一篇 常见问题 里，快速接入完全没提。",{"id":1743,"title":1744,"titles":1745,"content":1746,"level":109},"\u002Fblog\u002Funiapp-push-receive-aps#官方快速接入怎么写","官方快速接入怎么写",[901],"接入指南 3.1 节的监听示例是这样的（节选；注意 uni.createPushMessage 未加括号，只是点名 API）： uni.onPushMessage((res) => {\n  if (res.type == 'click') {\n    console.log('点击消息', res.data.payload)\n  }\n  \u002F\u002F 若云函数设置了 \"force_notification\":true，则会自动创建通知栏消息，不会触发此 receive。\n  if (res.type == 'receive') {\n    console.log('在线推送，接收到的消息内容', res.data)\n    \u002F\u002F 开发者可以自己在此回调，调用 createPushMessage 创建通知栏消息展示。\n    uni.createPushMessage\n  }\n}) 同一篇前面还写了两条路： 监听到消息后，自己调 uni.createPushMessage服务端传 force_notification: true，客户端自动建通知栏，此时你监听不到消息内容，要点击后才有 click 按字面理解：force_notification: true 时 receive 根本不该来；要自己展示，就在 receive 里 createPushMessage。两条路互斥。 真机不是这样。",{"id":1748,"title":1749,"titles":1750,"content":1751,"level":109},"\u002Fblog\u002Funiapp-push-receive-aps#真机现象","真机现象",[901],"服务端已经把 forceNotification: true 打到云函数，sendMessage 入参也是布尔 true。App 在前台时，iOS 日志仍然是： {\n  \"type\": \"receive\",\n  \"data\": {\n    \"aps\": null,\n    \"force_notification\": true,\n    \"title\": \"通知标题\",\n    \"content\": \"通知正文\",\n    \"payload\": { \"code\": \"daily_care\", \"id\": 1 },\n    \"unipush_version\": \"2.0\"\n  }\n} 要点： 文档承诺真机force_notification: true 不触发 receive照样触发，payload 里还能看到这个字段receive 里直接 createPushMessage前台能补出通知栏，但点系统通知时会再弹一条点通知只有 clickiOS 会 click + 一条带 aps 的 receive 几乎同时到 所以若按快速接入的注释，把无条件的 createPushMessage() 塞进所有 receive，点一次通知等于再造一条本地通知。用户看到的就是弹两次。 force_notification 也不会在应用页面上弹框，它最多让客户端去建系统通知栏。iOS 前台本来就不爱出 banner，很多人会误判「参数没生效」——日志里还有 receive 时，更不能拿「有没有 receive」当这个开关的唯一证据。",{"id":1753,"title":1754,"titles":1755,"content":1756,"level":109},"\u002Fblog\u002Funiapp-push-receive-aps#根因在常见问题里不在快速接入","根因在常见问题里，不在快速接入",[901],"uni-push2.0 常见问题 的「iOS消息推送流程说明」写了安卓没有的行为： 与安卓消息流程不同的是，Ios 走了厂商离线通道，在消息有效期内，app 打开在前台时，透传内容还将通过个推通道传递给客户端；此时可在客户端透传回调中的参数 PushMessage 对象中获取 aps 属性值来判断是否是 APNs 下发的消息。 翻译成客户端逻辑： 通道什么时候来data.aps要不要 createPushMessage个推在线透传App 在前台null \u002F 没有要，否则前台没通知栏APNs（厂商离线）后台\u002F锁屏，或点系统通知伴随有对象不要，系统已经展示过 安卓走了厂商通道就不会再走个推，消息只到一次。iOS 会再补一条透传，所以必须靠 aps 区分「这是 APNs 已经弹过的」还是「纯在线透传、需要自己建栏」。 快速接入把 iOS 当成和安卓一样的「force_notification 生效就不进 receive」。常见问题里写了 aps，但： 放在 FAQ 中段的流程说明，不在快速接入的示例代码里同一篇 FAQ 的「iOS在线收不到消息」又把快速接入那句复制了一遍：force_notification: true 时「监听不到消息内容」跟着 40283 把接入做完的人，几乎不会翻到 40291 去找 aps 文档内部也打架。以真机为准：iOS 上 receive 会来，用 aps 过滤，不要用「有没有 receive」判断 force_notification 有没有生效。",{"id":1758,"title":1759,"titles":1760,"content":1761,"level":109},"\u002Fblog\u002Funiapp-push-receive-aps#正确写法","正确写法",[901],"aps: null 仍然算在线透传，要用 != null 判断「有 APNs 载荷」，不能写成 if (data.aps) 这种把 null 和「没有」混在一起的假布尔。 function asPushData(data: unknown): Record\u003Cstring, unknown> | null {\n  if (data && typeof data === 'object')\n    return data as Record\u003Cstring, unknown>\n  if (typeof data === 'string') {\n    try {\n      const parsed = JSON.parse(data) as unknown\n      return parsed && typeof parsed === 'object'\n        ? parsed as Record\u003Cstring, unknown>\n        : null\n    }\n    catch {\n      return null\n    }\n  }\n  return null\n}\n\nuni.onPushMessage((message) => {\n  if (message.type === 'receive') {\n    \u002F\u002F 部分端上 message.data 可能是 JSON 字符串，先归一成对象再读 aps\n    const data = asPushData(message.data)\n    if (!data)\n      return\n    \u002F\u002F 带 aps：APNs \u002F 点系统通知伴随的 receive，再 create 会弹两次\n    if (Reflect.get(data, 'aps') != null) {\n      return\n    }\n    const title = data.title as string | undefined\n    const content = data.content as string | undefined\n    const payload = data.payload\n    if (!content && !title)\n      return\n    uni.createPushMessage({\n      title,\n      content: content || title,\n      payload,\n    })\n    return\n  }\n\n  if (message.type !== 'click')\n    return\n  \u002F\u002F 只在 click 里解析 payload 跳转\n}) click 才跳转。receive 只负责「纯透传要不要补通知栏」，不要在这里 navigateTo。 Android 前台要更容易出悬浮横幅，可以先把默认渠道 DcloudChannelID 的 importance 设成 4，再 createPushMessage。实际效果仍受系统限制，官方也承认不是所有机型都有浮动。",{"id":1763,"title":1764,"titles":1765,"content":1766,"level":109},"\u002Fblog\u002Funiapp-push-receive-aps#排查对照","排查对照",[901],"日志含义动作receive 且 aps 为 null个推在线透传createPushMessagereceive 且 aps 为对象APNs 或点通知伴随直接 returnclick用户点了通知栏解析 payload 跳转只有 receive、界面无反应没建本地通知，也没应用内 banner补上面的 receive 分支，不要先做自定义顶栏 不要用「还有没有 receive」证明 force_notification 失效。iOS 上它经常和 receive 同时存在。",{"id":1768,"title":1505,"titles":1769,"content":1770,"level":109},"\u002Fblog\u002Funiapp-push-receive-aps#小结",[901],"快速接入（40283）的 receive 示例缺 iOS 过滤；force_notification: true 不会触发 receive 这句话在 iOS 上不成立真正的判断在常见问题（40291）：用 PushMessage.aps 区分 APNs 和个推透传aps != null 才跳过 createPushMessage；aps: null 仍要自己建栏点通知会 click + 带 aps 的 receive 一起来，无条件 create 就会弹两次跳转只放 click，receive 只处理展示 html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":906,"title":905,"titles":1772,"content":1773,"level":98},[],"根据 Apifox OpenAPI 文档，用 openapi-ts-request 自动生成 TypeScript 请求函数与类型，减少手写 API 封装。 在现代前端开发中，与后端 API 的对接是一项重要且重复性高的工作。为了提高开发效率并减少手动编写 API 请求代码的工作量，我们可以使用 openapi-ts-request 工具，根据 Apifox 文档自动生成 TypeScript 类型安全的 API 请求文件。",{"id":1775,"title":1776,"titles":1777,"content":1778,"level":109},"\u002Fblog\u002Fapifox-api-generation#什么是-openapi-ts-request","什么是 openapi-ts-request？",[905],"openapi-ts-request 是一个基于 OpenAPI 规范的代码生成工具，它可以从 API 文档自动生成类型定义和请求函数。通过与 Apifox 平台集成，我们可以直接从项目文档生成完整的 API 客户端代码。",{"id":1780,"title":1781,"titles":1782,"content":1783,"level":109},"\u002Fblog\u002Fapifox-api-generation#配置说明","配置说明",[905],"首先，在项目根目录下创建配置文件，例如 openapi-ts.config.ts： import type { GenerateServiceProps } from 'openapi-ts-request'\n\nexport default [\n  {\n    serversPath: '.\u002Fsrc\u002Fapi\u002FopenApiRequest',\n    requestLibPath: `import request from '@\u002Fhttp\u002FopenApiRequest';\\n import { CustomRequestOptions } from '@\u002Fhttp\u002Finterceptor';`,\n    requestOptionsType: 'CustomRequestOptions',\n    reactQueryMode: 'vue',\n    enableLogging: true,\n    isGenJavaScript: false,\n    apifoxConfig: {\n      oasVersion: '3.1',\n      projectId: '66666',\u002F\u002F项目ID 需要拥有项目导出权限\n      apifoxToken: 'APS-xxxx',  \u002F\u002Fapifox中获取的token\n    },\n    hook: {\n      \u002F\u002F 生成自定义方法名称的函数\n      customFunctionName: (data) => {\n        \u002F\u002F 查找路径中第一个'{'的位置，用于处理带参数的路径\n        const index = data.path.indexOf('{')\n        let newPath = data.path\n        if (index !== -1) {\n          \u002F\u002F 如果路径中有参数（用{}表示），则截取参数前的部分\n          newPath = data.path.slice(0, index - 1)\n        }\n        \u002F\u002F 定义首字母大写函数\n        const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)\n        \u002F\u002F 分割路径并过滤空字符串\n        const paths = newPath.split('\u002F').filter(path => path)\n        \u002F\u002F 获取倒数第二个路径段并首字母大写\n        const secondLastPath = capitalize(paths[paths.length - 2])\n        \u002F\u002F 获取最后一个路径段并首字母大写\n        const lastPath = capitalize(paths[paths.length - 1])\n        \u002F\u002F 返回组合的方法名：HTTP方法名 + 倒数第二路径 + 最后路径(如：get:\u002Fapi\u002Fuser\u002Flist -> getUserList,post:\u002Fapi\u002Fuser\u002Fadd -> postAddUser)\n        return `${data.method}${secondLastPath}${lastPath}`\n      },\n    },\n  },\n] as GenerateServiceProps[]\n{\n  \"scripts\": {\n    \"openapi-ts-request\": \"openapi-ts\"\n  }\n}\n# 安装依赖\npnpm add -D openapi-ts-request\n\n# 运行生成命令\npnpm openapi-ts-request",{"id":1785,"title":1786,"titles":1787,"content":1788,"level":115},"\u002Fblog\u002Fapifox-api-generation#配置项详解","配置项详解",[905,1781],"配置项说明serversPath生成文件的存放路径requestLibPath自定义请求库的引入路径requestOptionsType请求选项的类型定义reactQueryMode设置为 'vue' 以适配 Vue 项目enableLogging是否启用日志输出isGenJavaScript是否生成 JavaScript 代码（false 表示生成 TypeScript）apifoxConfigApifox 项目配置",{"id":1790,"title":1791,"titles":1792,"content":1793,"level":109},"\u002Fblog\u002Fapifox-api-generation#生成的文件结构","生成的文件结构",[905],"运行命令后，工具会在指定位置自动生成以下文件： src\u002F\n└── api\u002F\n    └── openApiRequest\u002F\n        ├── common.ts\n        ├── index.ts\n        ├── types.ts\n        └── v1.ts",{"id":1795,"title":1796,"titles":1797,"content":1798,"level":109},"\u002Fblog\u002Fapifox-api-generation#生成代码示例","生成代码示例",[905],"生成的 API 请求函数具有完整的类型支持： \u002F** 派遣战舰远征 POST \u002Fapi\u002Fv1\u002Fgame\u002FwarshipAdventure *\u002F\nexport async function postGameWarshipAdventure({\n  body,\n  options,\n}: {\n  body: API.PostGameWarshipAdventureBody;\n  options?: CustomRequestOptions;\n}) {\n  return request\u003CAPI.PostGameWarshipAdventureResponse>(\n    '\u002Fapi\u002Fv1\u002Fgame\u002FwarshipAdventure',\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application\u002Fjson',\n      },\n      data: body,\n      ...(options || {}),\n    }\n  );\n}",{"id":1800,"title":1801,"titles":1802,"content":1803,"level":109},"\u002Fblog\u002Fapifox-api-generation#优势特点","优势特点",[905],"类型安全：自动生成 TypeScript 类型定义，编译时捕获错误减少重复工作：无需手动编写 API 请求函数与 Apifox 集成：直接从最新的 API 文档生成代码可定制化：通过 hooks 自定义函数命名规则Vue 生态适配：支持 Vue 项目中的请求模式",{"id":1805,"title":1806,"titles":1807,"content":1808,"level":109},"\u002Fblog\u002Fapifox-api-generation#最佳实践","最佳实践",[905],"将生成的 API 文件加入版本控制定期重新生成以同步最新的 API 变更结合 Git hooks 在提交前自动更新 API 代码在 CI\u002FCD 流程中验证生成的代码 通过使用 openapi-ts-request，我们可以显著提高 API 对接的效率，同时确保类型安全，减少人为错误。 （注：文档内容由 Copilot 生成） html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":910,"title":909,"titles":1810,"content":1811,"level":98},[],"在 HBuilderX 和 HBuilderV 的 uni-agent 里切到「自定义Key」，用 DeepSeek Flash 的环境变量接入自己的模型，不必走 DCloud 订阅。 uni-agent 内置在 HBuilderX 和 HBuilderV 里。不想用 DCloud 订阅时，可以在设置里切到 自定义Key，填自己的模型环境变量。下面以 DeepSeek Flash 为例。 HBuilderV 是基于 VS Code 改的 IDE，专门给 uni-app x 用；HBuilderX 则覆盖 uni-app \u002F uni-app x 等更广的工程。两边的 uni-agent 设置入口一致：聊天视图右上角齿轮。",{"id":1813,"title":1814,"titles":1815,"content":1816,"level":109},"\u002Fblog\u002Fhbuilder-uni-agent-deepseek-flash#打开-uni-agent","打开 uni-agent",[909],"HBuilderX： 工具栏最右侧 AI，或菜单「视图 → 插件扩展视图 → uni-agent」。快捷键 Windows Ctrl+Alt+I，macOS ⌃⌘I。首次点 AI 会安装插件。 HBuilderV： 已内置，点右上角 AI，快捷键同上。",{"id":1818,"title":1819,"titles":1820,"content":1821,"level":109},"\u002Fblog\u002Fhbuilder-uni-agent-deepseek-flash#切到自定义-key","切到自定义 Key",[909],"打开 uni-agent 聊天视图。点右上角 设置。模型选择 选 自定义Key，保存。 只把环境变量填上还不够。不选「自定义Key」时，插件仍走 DCloud 订阅，自定义变量不会生效。",{"id":1823,"title":1824,"titles":1825,"content":1826,"level":109},"\u002Fblog\u002Fhbuilder-uni-agent-deepseek-flash#deepseek-flash-环境变量","DeepSeek Flash 环境变量",[909],"在设置里的环境变量输入框粘贴（把 api-key 换成自己的 DeepSeek API Key）： # uni-agent 配置\nUNI_AGENT_BASE_URL=https:\u002F\u002Fapi.deepseek.com\nUNI_AGENT_API_KEY=api-key\nUNI_AGENT_MODEL=deepseek-flash\nUNI_AGENT_SMALL_FAST_MODEL=deepseek-flash\nUNI_AGENT_MODEL_CONTEXT_WINDOW=258000 UNI_AGENT_MODEL 和 UNI_AGENT_SMALL_FAST_MODEL 都用 deepseek-flash：主对话和小任务同一模型。UNI_AGENT_MODEL_CONTEXT_WINDOW 按自定义 Key 声明上下文窗口，这里是 258000。 保存后新开一轮对话再试。连不上时对照 uni-agent 自定义 Key 说明 检查 Base URL、Key 和模型名。",{"id":914,"title":913,"titles":1828,"content":1829,"level":98},[],"uni.uploadFile \u002F uni.upload 一次只能传一个文件时，在 H5 用 fetch + FormData 一次提交多个文件的实现方式与注意点。 在使用 uni-app 开发过程中，我们经常 会遇到文件上传的需求。然而，uni.uploadFile 方法存在一个限制：它无法在一个请求中上传多个文件。本文将介绍如何在 H5 环境下使用 fetch + FormData 来解决这个问题。",{"id":1831,"title":1832,"titles":1833,"content":1834,"level":109},"\u002Fblog\u002Ffile-upload-issue-solution#问题描述","问题描述",[913],"在开发中，我们常常需要一次性上传多个文件，例如： 上传多张图片作为产品展示上传多个文档文件同时上传头像和背景图 但是，uni-app 提供的 uni.uploadFile API 只能上传单个文件，即使多次调用也可能导致后端处理复杂。",{"id":1836,"title":1837,"titles":1838,"content":1839,"level":109},"\u002Fblog\u002Ffile-upload-issue-solution#解决方案使用-fetch-formdata","解决方案：使用 fetch + FormData",[913],"在 H5 环境中，我们可以使用原生的 fetch 和 FormData 来实现多文件上传。",{"id":1841,"title":1842,"titles":1843,"content":1844,"level":115},"\u002Fblog\u002Ffile-upload-issue-solution#_1-基础实现","1. 基础实现",[913,1837],"\u002F\u002F 在 H5 环境下上传多文件\nfunction uploadMultipleFiles(files: File[], uploadUrl: string): Promise\u003Cany> {\n  \u002F\u002F 检查是否在 H5 环境中\n  if ((globalThis as any).__PLATFORM__ !== 'h5') {\n    console.warn('此方法仅支持 H5 环境');\n    return Promise.reject(new Error('此方法仅支持 H5 环境'));\n  }\n\n  const formData = new FormData();\n  \n  \u002F\u002F 将多个文件添加到 FormData 中\n  files.forEach((file: File, index: number) => {\n    \u002F\u002F 可以使用相同字段名，后端会接收为数组\n    formData.append('files', file, file.name);\n    \u002F\u002F 或者使用不同字段名\n    \u002F\u002F formData.append(`file_${index}`, file, file.name);\n  });\n\n  \u002F\u002F 添加其他参数\n  formData.append('userId', '123');\n  formData.append('category', 'images');\n\n  return fetch(uploadUrl, {\n    method: 'POST',\n    body: formData\n  })\n  .then(response => response.json()) \u002F\u002F 注意：fetch 的 then 需先进行 json 处理\n  .then(data => {\n    console.log('上传成功:', data);\n    return data;\n  })\n  .catch(error => {\n    console.error('上传失败:', error);\n    throw error;\n  });\n}",{"id":1846,"title":1847,"titles":1848,"content":1849,"level":115},"\u002Fblog\u002Ffile-upload-issue-solution#_2-在-vue-组件中的使用示例","2. 在 Vue 组件中的使用示例",[913,1837],"\u003Ctemplate>\n  \u003Cdiv class=\"upload-container\">\n    \u003Cinput \n      type=\"file\" \n      ref=\"fileInput\" \n      multiple \n      accept=\"image\u002F*\" \n      @change=\"handleFileSelect\"\n    \u002F>\n    \u003Cbutton @click=\"uploadFiles\" :disabled=\"!selectedFiles.length\">\n      上传选中的 {{ selectedFiles.length }} 个文件\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from 'vue';\n\nconst fileInput = ref(null);\nconst selectedFiles = ref([]);\n\nconst handleFileSelect = (event) => {\n  \u002F\u002F 获取用户选择的文件列表\n  const files = Array.from(event.target.files);\n  selectedFiles.value = files;\n};\n\nconst uploadFiles = async () => {\n  if (!selectedFiles.value.length) {\n    alert('请先选择文件');\n    return;\n  }\n\n  \u002F\u002F 检查是否在 H5 环境\n  if (process.client && typeof window !== 'undefined' && window.fetch) {\n    try {\n      const result = await uploadMultipleFiles(selectedFiles.value, '\u002Fapi\u002Fupload');\n      console.log('上传结果:', result);\n      alert('上传成功');\n    } catch (error) {\n      console.error('上传失败:', error);\n      alert('上传失败: ' + error.message);\n    }\n  } else {\n    alert('此功能仅在 H5 环境下可用');\n  }\n};\n\n\u002F\u002F 多文件上传函数\nasync function uploadMultipleFiles(files, uploadUrl) {\n  const formData = new FormData();\n  \n  \u002F\u002F 将多个文件添加到 FormData 中\n  files.forEach(file => {\n    formData.append('files', file, file.name);\n  });\n\n  \u002F\u002F 添加其他参数\n  formData.append('timestamp', Date.now().toString());\n\n  const response = await fetch(uploadUrl, {\n    method: 'POST',\n    body: formData\n  });\n\n  \u002F\u002F 注意：fetch 的 then 需先进行 json 处理\n  if (response.ok) {\n    const result = await response.json();\n    return result;\n  } else {\n    throw new Error(`上传失败，状态码: ${response.status}`);\n  }\n}\n\u003C\u002Fscript>",{"id":1851,"title":1852,"titles":1853,"content":1854,"level":115},"\u002Fblog\u002Ffile-upload-issue-solution#_3-处理上传进度","3. 处理上传进度",[913,1837],"\u002F\u002F 带进度的上传实现\ninterface UploadProgressCallback {\n  (progress: number): void;\n}\n\ninterface FileUploadResponse {\n  [key: string]: any; \u002F\u002F 根据实际后端响应类型调整\n}\n\nfunction uploadMultipleFilesWithProgress(\n  files: File[], \n  uploadUrl: string, \n  onProgress?: UploadProgressCallback\n): Promise\u003CFileUploadResponse> {\n  return new Promise((resolve, reject) => {\n    if ((globalThis as any).__PLATFORM__ !== 'h5') {\n      reject(new Error('此方法仅支持 H5 环境'));\n      return;\n    }\n\n    const formData = new FormData();\n    \n    files.forEach(file => {\n      formData.append('files', file, file.name);\n    });\n\n    const xhr = new XMLHttpRequest();\n\n    \u002F\u002F 监听上传进度\n    xhr.upload.addEventListener('progress', (event: ProgressEvent) => {\n      if (event.lengthComputable) {\n        const percentComplete = (event.loaded \u002F event.total) * 100;\n        onProgress && onProgress(percentComplete);\n      }\n    });\n\n    xhr.addEventListener('load', () => {\n      if (xhr.status >= 200 && xhr.status \u003C 300) {\n        try {\n          const result = JSON.parse(xhr.response);\n          resolve(result);\n        } catch (error) {\n          reject(new Error('响应解析失败'));\n        }\n      } else {\n        reject(new Error(`上传失败: ${xhr.statusText}`));\n      }\n    });\n\n    xhr.addEventListener('error', () => {\n      reject(new Error('网络错误'));\n    });\n\n    xhr.addEventListener('abort', () => {\n      reject(new Error('上传被取消'));\n    });\n\n    xhr.open('POST', uploadUrl);\n    xhr.send(formData);\n  });\n}",{"id":1856,"title":1036,"titles":1857,"content":103,"level":109},"\u002Fblog\u002Ffile-upload-issue-solution#注意事项",[913],{"id":1859,"title":1860,"titles":1861,"content":1862,"level":115},"\u002Fblog\u002Ffile-upload-issue-solution#_1-环境限制","1. 环境限制",[913,1036],"此解决方案仅适用于 H5 环境，在小程序和 App 环境中无法使用需要检测当前运行环境，避免在非 H5 环境下执行",{"id":1864,"title":1865,"titles":1866,"content":1867,"level":115},"\u002Fblog\u002Ffile-upload-issue-solution#_2-fetch-响应处理","2. Fetch 响应处理",[913,1036],"使用 fetch 时，需要先将响应转换为 JSON 格式在 then 中先进行 response.json() 处理，然后再进行后续操作",{"id":1869,"title":1870,"titles":1871,"content":1872,"level":115},"\u002Fblog\u002Ffile-upload-issue-solution#_3-formdata-使用","3. FormData 使用",[913,1036],"FormData 允许我们轻松地构建表单数据可以通过 append() 方法添加多个同名字段，后端会接收到文件数组",{"id":1874,"title":1168,"titles":1875,"content":1876,"level":109},"\u002Fblog\u002Ffile-upload-issue-solution#总结",[913],"虽然 uni.uploadFile 有其局限性，但在 H5 环境下，我们可以利用原生的 fetch 和 FormData 来实现多文件上传功能。需要注意的是： 此方法仅适用于 H5 环境fetch 响应需要先进行 JSON 处理需要适当处理错误和上传进度后端需要支持接收多文件数组 通过这种方式，我们可以在 H5 环境中实现更灵活的多文件上传功能。 （注：文档内容由 Copilot 生成） html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":918,"title":917,"titles":1878,"content":1879,"level":98},[],"WangEditor 会过滤 \u003Cdiv>\u003Cimg>\u003C\u002Fdiv> 结构，导致第三方商品详情图片丢失。本文给出自定义解析\u002F保留图片的处理方式。",{"id":1881,"title":1336,"titles":1882,"content":1883,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#问题背景",[917],"在实际项目开发过程中，我们经常会遇到需要从第三方接口导入商品详情的场景。这些商品详情通常是由富文本编辑器生成的内容，其中可能包含各种HTML标签和结构。然而，在使用Wangeditor时，我们发现了一个常见问题：当图片(img标签)被div标签包裹时，Wangeditor会将其过滤掉，导致图片无法正常显示。 这个问题在处理第三方接口导入的数据时尤为突出，因为这些数据的HTML结构是我们无法控制的。 值得注意的是，这是一个已知的社区问题，在 wangEditor 的官方 GitHub 仓库中有相关的 issue 讨论：Issue #5880，且该问题至今仍未被修复。",{"id":1885,"title":1886,"titles":1887,"content":1888,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#问题分析","问题分析",[917],"Wangeditor默认会过滤掉一些可能不安全的HTML标签和属性，这是出于安全考虑的设计。然而，这种过滤机制有时会过于严格，导致合法的HTML结构（如被div包裹的img标签）也被过滤掉。 具体表现为： 从第三方接口获取的商品详情中包含\u003Cdiv>\u003Cimg src=\"...\" alt=\"...\"\u002F>\u003C\u002Fdiv>结构在Wangeditor中显示时，img标签被移除最终用户无法看到图片内容",{"id":1890,"title":1891,"titles":1892,"content":1893,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#去试试","去试试",[917],"你可以通过下面的在线编辑器链接来复现这个问题： Vue3 Wangeditor Demo 操作步骤： 打开链接后，在代码中找到绑定值（如 valueHtml）或模拟异步获取内容的地方。将其初始内容或设置的新内容改为：\n\u003Cdiv>\u003Cimg src=\"https:\u002F\u002Fimg3.redocn.com\u002F20110418\u002F20110416_6ad206b20544a083fdb0B6Kj0dud4sro.jpg\" alt=\"\" \u002F>\u003C\u002Fdiv>\n你会发现编辑器中原本应该显示的图片变成了 \u003Cp>\u003Cbr>\u003C\u002Fp>，导致图片无法显示。",{"id":1895,"title":1031,"titles":1896,"content":1897,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#解决方案",[917],"经过深入研究和实践，我们找到了一个简单有效的解决方案：将div标签替换为p标签。",{"id":1899,"title":1900,"titles":1901,"content":1902,"level":115},"\u002Fblog\u002Fwangeditor-img-filter-issue#修改前的代码结构","修改前的代码结构：",[917,1031],"\u003Cdiv>\n  \u003Cimg src=\"https:\u002F\u002Fimg3.redocn.com\u002F20110418\u002F20110416_6ad206b20544a083fdb0B6Kj0dud4sro.jpg\" alt=\"\" \u002F>\n\u003C\u002Fdiv>",{"id":1904,"title":1905,"titles":1906,"content":1907,"level":115},"\u002Fblog\u002Fwangeditor-img-filter-issue#修改后的代码结构","修改后的代码结构：",[917,1031],"\u003Cp>\n  \u003Cimg src=\"https:\u002F\u002Fimg3.redocn.com\u002F20110418\u002F20110416_6ad206b20544a083fdb0B6Kj0dud4sro.jpg\" alt=\"\" \u002F>\n\u003C\u002Fp>",{"id":1909,"title":1910,"titles":1911,"content":103,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#实现步骤","实现步骤",[917],{"id":1913,"title":1914,"titles":1915,"content":1916,"level":115},"\u002Fblog\u002Fwangeditor-img-filter-issue#_1-数据预处理","1. 数据预处理",[917,1910],"在将第三方接口获取的数据加载到Wangeditor之前，我们需要对数据进行预处理： \u002F\u002F 预处理函数：将div标签替换为p标签\nfunction preprocessEditorContent(content) {\n  \u002F\u002F 将被div包裹的img标签替换为被p标签包裹的img标签\n  const processedContent = content.replace(\n    \u002F&lt;div&gt;(\\s*&lt;img[^&gt;]*&gt;)\\s*&lt;\\\u002Fdiv&gt;\u002Fg,\n    '&lt;p&gt;$1&lt;\u002Fp&gt;'\n  );\n  \n  return processedContent;\n}\n\n\u002F\u002F 使用示例\nconst rawContent = getThirdPartyData(); \u002F\u002F 从第三方接口获取的原始数据\nconst processedContent = preprocessEditorContent(rawContent);\neditor.txt.html(processedContent); \u002F\u002F 加载到Wangeditor中",{"id":1918,"title":1919,"titles":1920,"content":1921,"level":115},"\u002Fblog\u002Fwangeditor-img-filter-issue#_2-针对多种html结构的处理","2. 针对多种HTML结构的处理",[917,1910],"在实际应用中，HTML结构可能更加复杂，我们需要处理更多的情况： function preprocessEditorContent(content) {\n  \u002F\u002F 替换被div包裹的img标签为被p标签包裹的img标签\n  let processedContent = content.replace(\n    \u002F&lt;div&gt;(\\s*&lt;img[^&gt;]*&gt;)\\s*&lt;\\\u002Fdiv&gt;\u002Fg,\n    '&lt;p&gt;$1&lt;\u002Fp&gt;'\n  );\n\n  \u002F\u002F 处理包含class等属性的div标签\n  processedContent = processedContent.replace(\n    \u002F&lt;div([^&gt;]*)&gt;(\\s*&lt;img[^&gt;]*&gt;)\\s*&lt;\\\u002Fdiv&gt;\u002Fg,\n    '&lt;p$1&gt;$2&lt;\u002Fp&gt;'\n  );\n\n  \u002F\u002F 处理嵌套结构\n  processedContent = processedContent.replace(\n    \u002F&lt;div([^&gt;]*)\\s*class=\"([^\"]*)\"[^&gt;]*&gt;(\\s*&lt;img[^&gt;]*&gt;)\\s*&lt;\\\u002Fdiv&gt;\u002Fg,\n    function(match, divAttr, classAttr, imgTag) {\n      \u002F\u002F 将div的class属性复制到p标签\n      return `&lt;p class=\"${classAttr}\"${divAttr.replace(`class=\"${classAttr}\"`, '')}&gt;${imgTag}&lt;\u002Fp&gt;`;\n    }\n  );\n\n  return processedContent;\n}",{"id":1923,"title":1036,"titles":1924,"content":1925,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#注意事项",[917],"样式适配：将div替换为p标签后，原有的CSS样式可能需要调整，因为p标签和div标签的默认样式可能不同。语义化考虑：从HTML语义化的角度看，p标签是用于段落的，如果div包含的不仅仅是图片，而是混合内容，需要谨慎使用此方法。测试验证：在应用此解决方案后，需要充分测试以确保没有引入其他问题。",{"id":1927,"title":1168,"titles":1928,"content":1929,"level":109},"\u002Fblog\u002Fwangeditor-img-filter-issue#总结",[917],"通过将div标签替换为p标签的方式，我们成功解决了Wangeditor过滤被div包裹的img标签的问题。这种解决方案简单有效，且不会对Wangeditor的安全机制造成影响。但是鉴于Wangeditor已经不在维护，如果没有从外部导入富文本数据的功能还是可以使用，否则不建议使用Wangeditor。 （注：文档内容由 Copilot 生成） html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":922,"title":921,"titles":1931,"content":1932,"level":98},[],"Nuxt 关闭 SSR（ssr: false）部署后出现 Cannot load payload \u002F_payload.json 或 payload 404。本文说明原因与 payloadExtraction 等相关配置修复。 在把 Nuxt 项目设置为非 SSR（例如 ssr: false）并部署到某些托管环境后，可能会在运行时看到报错： Cannot load payload \u002F_payload.json 原因 这是由 Nuxt 的 payload 提取（payload extraction）机制在一些部署 \u002F 运行时场景下与静态资源访问不兼容导致的（例如当运行时无法正确提供或解析 \u002F_payload.json）。 解决方法 在 nuxt.config.ts 中关闭实验性的 payload 提取： export default defineNuxtConfig({\n  ssr: false,\n  experimental: {\n    \u002F\u002F 关闭 payload 提取以避免运行时去加载 \u002F_payload.json\n    payloadExtraction: false\n  }\n}) 说明 将 payloadExtraction 设置为 false 后，Nuxt 不会采用提取并生成 \u002F_payload.json 的方式来加载页面数据，从而避免在某些部署环境中出现缺失或无法访问该资源导致的错误。如果你的项目仍需 SSR 或静态预渲染场景，请谨慎评估该设置的影响。 参考 GitHub 讨论与示例：https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt\u002Fissues\u002F22068#issuecomment-1629076435 （注：文档内容由 Copilot 生成） html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":926,"title":925,"titles":1934,"content":1935,"level":98},[],"在 Nuxt 4 中安装配置 nuxt-echarts，渲染 ECharts 图表，并处理客户端-only、按需引入与常见报错。 在Nuxt项目中使用ECharts可以快速实现数据可视化。nuxt-echarts模块为Nuxt提供了开箱即用的ECharts集成方案，本文将介绍完整的集成步骤。",{"id":1937,"title":1938,"titles":1939,"content":1940,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#第一步安装nuxt-echarts模块","第一步：安装nuxt-echarts模块",[925],"使用Nuxt CLI快速添加echarts模块： npx nuxi module add echarts 该命令会自动将nuxt-echarts添加到项目依赖并在nuxt.config.ts中注册模块。",{"id":1942,"title":1943,"titles":1944,"content":1945,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#第二步配置所需的echarts模块必须","第二步：配置所需的ECharts模块（必须）",[925],"在nuxt.config.ts中添加echarts配置，必须显式声明要使用的图表类型和组件： export default defineNuxtConfig({\n  modules: [\n    'nuxt-echarts'\n  ],\n  echarts: {\n    \u002F\u002F 指定需要的图表类型\n    charts: ['PieChart', 'BarChart', 'LineChart'],\n    \u002F\u002F 指定需要的组件\n    components: [\n      'DatasetComponent',\n      'GridComponent',\n      'TooltipComponent',\n      'LegendComponent',\n      'TitleComponent'\n    ]\n  }\n}) 注意：nuxt-echarts采用按需加载机制，只会打包你声明的图表和组件，未声明的功能将无法使用。",{"id":1947,"title":1948,"titles":1949,"content":1950,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#第三步在页面中使用vchart组件","第三步：在页面中使用VChart组件",[925],"使用\u003Cclient-only>包裹图表组件，因为ECharts依赖浏览器环境： \u003Ctemplate> \n  \u003Cdiv>\n    \u003Ch1>图表示例\u003C\u002Fh1>\n    \u003Cclient-only>\n      \u003CVChart :option=\"option\" style=\"height: 400px;\" \u002F>\n    \u003C\u002Fclient-only>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport type { Option } from 'nuxt-echarts\u002Fruntime\u002Ftypes'\n\nconst option = ref\u003COption>({\n  title: {\n    text: '示例饼图'\n  },\n  tooltip: {\n    trigger: 'item'\n  },\n  legend: {\n    orient: 'vertical',\n    left: 'left'\n  },\n  series: [\n    {\n      name: '访问来源',\n      type: 'pie',\n      radius: '50%',\n      data: [\n        { value: 1048, name: '搜索引擎' },\n        { value: 735, name: '直接访问' },\n        { value: 580, name: '邮件营销' },\n        { value: 484, name: '联盟广告' },\n        { value: 300, name: '视频广告' }\n      ]\n    }\n  ]\n})\n\u003C\u002Fscript> 为什么必须使用\u003Cclient-only>？\nECharts需要访问DOM和浏览器API（如Canvas），在SSR环境下会导致错误。\u003Cclient-only>确保组件只在客户端渲染。",{"id":1952,"title":1953,"titles":1954,"content":1955,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#第四步使用typescript类型定义","第四步：使用TypeScript类型定义",[925],"nuxt-echarts提供了完整的类型支持，使用官方类型定义来实现option： import type { Option } from 'nuxt-echarts\u002Fruntime\u002Ftypes'\n\n\u002F\u002F 示例：柱状图配置\nconst barOption = ref\u003COption>({\n  xAxis: {\n    type: 'category',\n    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n  },\n  yAxis: {\n    type: 'value'\n  },\n  series: [\n    {\n      data: [120, 200, 150, 80, 70, 110, 130],\n      type: 'bar'\n    }\n  ]\n})",{"id":1957,"title":1958,"titles":1959,"content":1960,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#完整示例","完整示例",[925],"结合以上步骤的完整页面示例： \u003Ctemplate>\n  \u003Cdiv class=\"container mx-auto p-4\">\n    \u003Ch1 class=\"text-3xl font-bold mb-4\">销售数据分析\u003C\u002Fh1>\n    \n    \u003Cdiv class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n      \u003C!-- 饼图 -->\n      \u003Cdiv class=\"bg-white p-4 rounded shadow\">\n        \u003Cclient-only>\n          \u003CVChart :option=\"pieOption\" style=\"height: 400px;\" \u002F>\n        \u003C\u002Fclient-only>\n      \u003C\u002Fdiv>\n      \n      \u003C!-- 柱状图 -->\n      \u003Cdiv class=\"bg-white p-4 rounded shadow\">\n        \u003Cclient-only>\n          \u003CVChart :option=\"barOption\" style=\"height: 400px;\" \u002F>\n        \u003C\u002Fclient-only>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport type { Option } from 'nuxt-echarts\u002Fruntime\u002Ftypes'\n\nconst pieOption = ref\u003COption>({\n  title: { text: '销售渠道分布' },\n  tooltip: { trigger: 'item' },\n  legend: { orient: 'vertical', left: 'left' },\n  series: [{\n    name: '销售额',\n    type: 'pie',\n    radius: '50%',\n    data: [\n      { value: 1048, name: '线上商城' },\n      { value: 735, name: '实体店' },\n      { value: 580, name: '代理商' }\n    ]\n  }]\n})\n\nconst barOption = ref\u003COption>({\n  title: { text: '月度销售趋势' },\n  tooltip: { trigger: 'axis' },\n  xAxis: {\n    type: 'category',\n    data: ['1月', '2月', '3月', '4月', '5月', '6月']\n  },\n  yAxis: { type: 'value' },\n  series: [{\n    data: [120, 200, 150, 80, 170, 210],\n    type: 'bar'\n  }]\n})\n\u003C\u002Fscript>",{"id":1962,"title":1301,"titles":1963,"content":103,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#常见问题",[925],{"id":1965,"title":1966,"titles":1967,"content":1968,"level":115},"\u002Fblog\u002Fnuxt-echarts-implementation#图表不显示或报错","图表不显示或报错？",[925,1301],"确认在nuxt.config.ts中声明了对应的图表类型和组件检查是否使用了\u003Cclient-only>包裹确保为图表容器设置了明确的高度",{"id":1970,"title":1971,"titles":1972,"content":1973,"level":115},"\u002Fblog\u002Fnuxt-echarts-implementation#如何响应式调整图表大小","如何响应式调整图表大小？",[925,1301],"\u003Cscript setup lang=\"ts\">\nconst chartRef = ref()\n\n\u002F\u002F 监听窗口大小变化\nuseEventListener('resize', () => {\n  chartRef.value?.resize()\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cclient-only>\n    \u003CVChart ref=\"chartRef\" :option=\"option\" style=\"height: 400px;\" \u002F>\n  \u003C\u002Fclient-only>\n\u003C\u002Ftemplate>",{"id":1975,"title":1976,"titles":1977,"content":1978,"level":109},"\u002Fblog\u002Fnuxt-echarts-implementation#参考资源","参考资源",[925],"官方文档：https:\u002F\u002Fecharts.nuxt.dev\u002FECharts配置项手册：https:\u002F\u002Fecharts.apache.org\u002Fzh\u002Foption.html 通过以上步骤，你就可以在Nuxt项目中轻松集成和使用ECharts图表了！ （注：文档内容由 Copilot 生成） html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":930,"title":929,"titles":1980,"content":1981,"level":98},[],"UniApp Vite 项目接入 Reown（AppKit）与 Wagmi 时的打包报错、浏览器 API 兼容、RPC 访问失败等常见问题与对应解决办法。 在 UniApp+Vite 项目中集成 Reown 和 Wagmi 实现 Web3 钱包连接时，会遇到一系列打包、兼容性和网络问题。本文总结了实际开发中遇到的问题及对应的解决方案。",{"id":1983,"title":1984,"titles":1985,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-1wagmi-打包后出现异常文件名","问题 1：Wagmi 打包后出现异常文件名",[929],{"id":1987,"title":318,"titles":1988,"content":1989,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象",[929,1984],"Wagmi 在本地开发环境运行一切正常，但执行 build 打包后，生成的文件中出现以 ..node 开头的文件，导致引入时报错。",{"id":1991,"title":1031,"titles":1992,"content":1993,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案",[929,1984],"在 vite.config.ts 的 build 配置中添加 rollupOptions，手动控制代码分包策略： export default defineConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks: (id) => {\n          if (!id || typeof id !== 'string')\n            return 'vendor'\n          \n          \u002F\u002F 统一路径分隔符\n          const normalizedId = id.replace(\u002F\\\\\u002Fg, '\u002F')\n          if (!normalizedId.includes('node_modules'))\n            return\n          \n          \u002F\u002F 处理 @scope\u002Fpackage 和普通包\n          const match = normalizedId.match(\u002Fnode_modules\\\u002F(@[^\u002F]+\\\u002F[^\u002F]+|[^\u002F]+)\u002F)\n          return match ? `vendor-${match[1].replace('@', '')}` : 'vendor'\n        },\n      },\n    },\n  },\n}) 此配置可以避免 Vite 生成异常的文件名，确保所有 node_modules 依赖都被正确打包到独立的 vendor chunk 中。",{"id":1995,"title":1996,"titles":1997,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-2插件要求-es2020-编译目标","问题 2：插件要求 ES2020 编译目标",[929],{"id":1999,"title":318,"titles":2000,"content":2001,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-1",[929,1996],"Reown 和 Wagmi 插件在较低的编译目标下无法正常工作。",{"id":2003,"title":1031,"titles":2004,"content":2005,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-1",[929,1996],"在 vite.config.ts 中设置编译目标为 es2020： export default defineConfig({\n  build: {\n    target: 'es2020',\n  },\n})",{"id":2007,"title":2008,"titles":2009,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-3低版本手机无法连接钱包一直转圈","问题 3：低版本手机无法连接钱包（一直转圈）",[929],{"id":2011,"title":318,"titles":2012,"content":2013,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-2",[929,2008],"在部分低版本 Android\u002FiOS 设备上，Reown 连接钱包时一直显示加载状态，无法完成连接。",{"id":2015,"title":1021,"titles":2016,"content":2017,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#原因",[929,2008],"低版本手机浏览器缺少现代 JavaScript API 的 polyfill 支持。",{"id":2019,"title":1031,"titles":2020,"content":2021,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-2",[929,2008],"安装 core-js 并在项目入口引入完整的 polyfill： pnpm add core-js 在 main.ts 或 app.ts 入口文件顶部添加： import 'core-js\u002Fstable' 这将为低版本浏览器提供完整的 ES6+ API polyfill 支持。",{"id":2023,"title":2024,"titles":2025,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-4写入合约后无法推送到区块链","问题 4：写入合约后无法推送到区块链",[929],{"id":2027,"title":318,"titles":2028,"content":2029,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-3",[929,2024],"部分手机在执行写入合约操作时，虽然获取到了交易 hash，但交易始终无法推送到区块链网络，停留在 pending 状态。",{"id":2031,"title":1021,"titles":2032,"content":2033,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#原因-1",[929,2024],"Gas 费用估算不准确或未正确设置导致交易无法被矿工打包。",{"id":2035,"title":1031,"titles":2036,"content":2037,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-3",[929,2024],"手动获取和设置 Gas 价格参数： import { estimateFeesPerGas } from '@wagmi\u002Fcore'\n\nexport async function getGasPrice() {\n  const network = useAppKitNetwork()\n  const fees = await estimateFeesPerGas(wagmiAdapter.wagmiConfig, {\n    chainId: Number(network.value.chainId),\n  })\n  const maxFeePerGas = fees.maxFeePerGas\n  const maxPriorityFeePerGas = fees.maxPriorityFeePerGas\n  return { maxFeePerGas, maxPriorityFeePerGas }\n} 在写入合约时传入这两个参数： const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPrice()\n\nawait writeContract({\n  \u002F\u002F ... 其他参数\n  maxFeePerGas,\n  maxPriorityFeePerGas,\n})",{"id":2039,"title":2040,"titles":2041,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-5reown-默认-rpc-国内无法访问","问题 5：Reown 默认 RPC 国内无法访问",[929],{"id":2043,"title":318,"titles":2044,"content":2045,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-4",[929,2040],"Reown 默认使用的 RPC 节点在国内网络环境下无法访问，导致无法连接区块链网络。",{"id":2047,"title":1031,"titles":2048,"content":2049,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-4",[929,2040],"在初始化 wagmiAdapter 和 createAppKit 时添加 customRpcUrls 参数，使用国内可访问的 RPC 节点： const wagmiAdapter = new WagmiAdapter({\n  \u002F\u002F ... 其他配置\n  customRpcUrls: {\n    1: 'https:\u002F\u002Fyour-custom-rpc-url.com', \u002F\u002F Ethereum Mainnet\n    56: 'https:\u002F\u002Fbsc-dataseed.binance.org', \u002F\u002F BSC\n    \u002F\u002F 添加更多链的自定义 RPC\n  },\n})\n\ncreateAppKit({\n  adapters: [wagmiAdapter],\n  \u002F\u002F ... 其他配置\n  customRpcUrls: {\n    1: 'https:\u002F\u002Fyour-custom-rpc-url.com',\n    56: 'https:\u002F\u002Fbsc-dataseed.binance.org',\n  },\n})",{"id":2051,"title":2052,"titles":2053,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-6typescript-报错实例化过深","问题 6：TypeScript 报错\"实例化过深\"",[929],{"id":2055,"title":318,"titles":2056,"content":2057,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-5",[929,2052],"在读写合约方法时，TypeScript 提示 \"Type instantiation is excessively deep and possibly infinite\"。",{"id":2059,"title":1021,"titles":2060,"content":2061,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#原因-2",[929,2052],"完整的合约 ABI 文件过大，TypeScript 在进行类型推断时超出递归深度限制。",{"id":2063,"title":1031,"titles":2064,"content":2065,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-5",[929,2052],"不要存储完整的 ABI，只保留项目实际使用的方法： \u002F\u002F ❌ 不要这样做\nimport fullABI from '.\u002Fcontract-full-abi.json'\n\n\u002F\u002F ✅ 应该这样做\nconst contractABI = [\n  {\n    name: 'transfer',\n    type: 'function',\n    inputs: [\n      { name: 'to', type: 'address' },\n      { name: 'amount', type: 'uint256' },\n    ],\n    outputs: [{ type: 'bool' }],\n  },\n  \u002F\u002F 只包含项目需要使用的方法\n] as const",{"id":2067,"title":2068,"titles":2069,"content":103,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-7浮点数计算精度损失","问题 7：浮点数计算精度损失",[929],{"id":2071,"title":318,"titles":2072,"content":2073,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-6",[929,2068],"在处理 Token 金额、价格计算等涉及浮点数的场景时，JavaScript 原生运算会导致精度损失。",{"id":2075,"title":1031,"titles":2076,"content":2077,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-6",[929,2068],"使用专门的大数库处理计算和判断： pnpm add big.js\n# 或者\npnpm add bignumber.js\n# 或者\npnpm add decimal.js 示例： import Big from 'big.js'\n\n\u002F\u002F 使用 Big.js 进行精确计算\nconst amount1 = new Big('0.1')\nconst amount2 = new Big('0.2')\nconst result = amount1.plus(amount2) \u002F\u002F 0.3（精确）",{"id":2079,"title":2080,"titles":2081,"content":2082,"level":115},"\u002Fblog\u002Freown-wagmi-uniapp-issues#金额转换","金额转换",[929,2068],"使用 viem 提供的工具函数处理 Token 金额转换： import { formatUnits, parseUnits } from 'viem'\n\n\u002F\u002F 将 wei 转换为可读单位\nconst displayAmount = formatUnits(1000000000000000000n, 18) \u002F\u002F \"1\"\n\n\u002F\u002F 将可读单位转换为 wei\nconst weiAmount = parseUnits('1.5', 18) \u002F\u002F 1500000000000000000n",{"id":2084,"title":1168,"titles":2085,"content":2086,"level":109},"\u002Fblog\u002Freown-wagmi-uniapp-issues#总结",[929],"集成 Reown 和 Wagmi 到 UniApp+Vite 项目需要注意： 配置正确的 Vite 打包策略避免文件名异常设置 ES2020 编译目标以支持现代特性为低版本设备添加 polyfill 支持手动设置 Gas 参数确保交易成功使用自定义 RPC 节点解决网络访问问题精简 ABI 避免 TypeScript 类型推断问题使用专业库处理浮点数和大数运算 （注：文档内容由 Copilot 生成） html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":934,"title":933,"titles":2088,"content":2089,"level":98},[],"用 Vue 3 Composition API 与 requestAnimationFrame 实现数字滚动（CountUp）组件，覆盖取消动画、内存泄漏与性能优化。",{"id":2091,"title":2092,"titles":2093,"content":2094,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#概述","概述",[933],"数字滚动动画常见于数据大屏、计数器、统计面板。本文按「需求 -> 原理 -> 实现 -> 性能 -> 实战」的顺序，带你完成一个高性能、可复用、内存友好的 Vue 3 数字滚动组件。",{"id":2096,"title":2097,"titles":2098,"content":2099,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#先明确目标","先明确目标",[933],"设计一个数字滚动组件，要求： 📊 支持整数、小数、负数⚡ 动画流畅，视觉反馈自然🎯 高性能，避免频繁更新时卡顿🔄 内存安全，杜绝定时器堆积与泄漏",{"id":2101,"title":2102,"titles":2103,"content":2104,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#实现原理","实现原理",[933],"数字滚动的本质是「逐位滚动」： 每一位数字都是 0-9 垂直排列通过 translateY 改变可视窗口中的数字数字 N 的偏移量是 N * -100% 例如显示数字 5： 0  ← translateY: 0%\n1\n2\n3\n4\n5  ← translateY: -500% (当前显示)\n6\n7\n8\n9 理解这个映射关系后，再看代码会非常直接。",{"id":2106,"title":2107,"titles":2108,"content":103,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#核心实现要点","核心实现要点",[933],{"id":2110,"title":2111,"titles":2112,"content":2113,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#_1-使用-transform-做位移动画","1. 使用 transform 做位移动画",[933,2107],"\u002F\u002F 修改 margin 或 top 会触发布局重排\nstyle.marginTop = value;\nstyle.top = value;\n\u002F\u002F transform 更容易走 GPU 合成层，动画更稳定\n:style=\"{ transform: `translateY(${value}%)` }\"",{"id":2115,"title":2116,"titles":2117,"content":2118,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#_2-用计算属性拆分字符并计算位移","2. 用计算属性拆分字符并计算位移",[933,2107],"\u002F\u002F 字符串转数组，用于逐位显示\nconst displayedChars = computed(() => targetValue.value.split(\"\"));\n\n\u002F\u002F 计算每一位的偏移距离\nconst getTranslateY = (char: string): string => {\n  if (char === \".\") return \"0\";\n  const digit = parseInt(char, 10);\n  return digit * -100 + \"%\";\n};",{"id":2120,"title":2121,"titles":2122,"content":2123,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#_3-管理定时器避免快速更新时堆积","3. 管理定时器，避免快速更新时堆积",[933,2107],"定时器未清理是常见内存泄漏来源，尤其在高频数据更新场景中。 \u002F\u002F ❌ 问题代码\nwatch(\n  () => props.value,\n  () => {\n    setTimeout(() => updateValue(), 1000); \u002F\u002F 快速更新时会堆积多个 timer\n  },\n);\n\n\u002F\u002F ✅ 正确做法\nlet timerId: ReturnType\u003Ctypeof setTimeout> | null = null;\n\nconst clearTimer = () => {\n  if (timerId) clearTimeout(timerId);\n};\n\nwatch(\n  () => props.value,\n  () => {\n    clearTimer(); \u002F\u002F 先清理旧 timer\n    timerId = setTimeout(() => updateValue(), 1000);\n  },\n);\n\nonUnmounted(() => clearTimer()); \u002F\u002F 卸载时清理",{"id":2125,"title":2126,"titles":2127,"content":2128,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#_4-用贝塞尔曲线优化体感","4. 用贝塞尔曲线优化体感",[933,2107],"\u002F\u002F cubic-bezier(0.34, 1.56, 0.64, 1) 有轻微弹性回落效果\ntransition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 参数含义： 0.34, 1.56：前半段加速并略微超调0.64, 1：后半段回落并稳定停住",{"id":2130,"title":2131,"titles":2132,"content":2133,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#完整组件与特性","完整组件与特性",[933],"NumberScroll.vue：可复用的高性能数字滚动组件。 技术栈： Vue 3 Composition APITypeScriptTailwind CSS（无需 SCSS） 关键特性： ✅ 自动清理定时器，无内存泄漏✅ 支持小数点和负数✅ 使用 GPU 友好的 transform 动画✅ 快速更新时自动取消旧动画✅ 无外部依赖，样式可直接复用✅ 支持自定义动画时长",{"id":2135,"title":2136,"titles":2137,"content":2138,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#性能对比","性能对比",[933],"实现方式FPS内存泄漏响应性margin 修改30-45有⚠️ 中等position 修改45-55有⚠️ 中等transform55-60无✅ 优秀",{"id":2140,"title":2141,"titles":2142,"content":2143,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#在线演示","在线演示",[933],"完整交互式演示请访问 数字滚动动画Demos。 你可以在Demos中： 📊 实时观察滚动效果🎮 切换不同数据场景📈 对比不同实现策略🔧 体验组件配置项 前往Demos →",{"id":2145,"title":1301,"titles":2146,"content":103,"level":109},"\u002Fblog\u002Fnumber-scroll-animation#常见问题",[933],{"id":2148,"title":2149,"titles":2150,"content":2151,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#q1-如何处理负数","Q1: 如何处理负数？",[933,1301],"\u002F\u002F 可以在前面加符号\nconst displayValue = value \u003C 0 ? `-${Math.abs(value)}` : `${value}`;",{"id":2153,"title":2154,"titles":2155,"content":2156,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#q2-动画总是要-03s-吗","Q2: 动画总是要 0.3s 吗？",[933,1301],"\u002F\u002F 可以通过 duration prop 自定义\n\u003CNumberScroll :value=\"num\" :duration=\"500\" \u002F>",{"id":2158,"title":2159,"titles":2160,"content":2161,"level":115},"\u002Fblog\u002Fnumber-scroll-animation#q3-如何添加千位分隔符","Q3: 如何添加千位分隔符？",[933,1301],"const formatted = value.toLocaleString(); \u002F\u002F 1,234,567\n\u002F\u002F 然后显示格式化后的字符串 相关资源： Vue 3 Composition APICSS transform 性能贝塞尔曲线可视化 （注：文档内容由 Copilot 生成） html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":938,"title":937,"titles":2163,"content":2164,"level":98},[],"nvm-windows 下载 Node.js 一直卡住时，不必改 settings.txt。用 nvm node_mirror \u002F npm_mirror 切淘宝镜像或代理即可。",{"id":2166,"title":1336,"titles":2167,"content":2168,"level":109},"\u002Fblog\u002Fnvm-install-solution#问题背景",[937],"刚换新电脑，安装好 nvm-windows 后，准备安装 Node.js 版本时却遇到了经典难题： 进度条一直卡在 \"Downloading node.js version...\"，无论是否开启梯子，最终都报错或无限等待。 网上大部分教程都让你去修改 nvm 安装目录下的 settings.txt 文件。其实完全不需要！ nvm 官方命令行本身就提供了修改镜像源和代理的功能，既安全又方便。",{"id":2170,"title":2171,"titles":2172,"content":2173,"level":109},"\u002Fblog\u002Fnvm-install-solution#解决方案使用-nvm-内置命令","解决方案：使用 nvm 内置命令",[937],"只需在终端（PowerShell 或 CMD）中执行两条命令，即可瞬间解决网络问题。",{"id":2175,"title":2176,"titles":2177,"content":2178,"level":115},"\u002Fblog\u002Fnvm-install-solution#第一步查看帮助可选","第一步：查看帮助（可选）",[937,2171],"首先确认你的 nvm 支持以下命令： nvm -h 你会看到如下关键信息： nvm node_mirror [url]        : Set the node mirror. Defaults to https:\u002F\u002Fnodejs.org\u002Fdist\u002F\nnvm proxy [url]              : Set a proxy to use for downloads.",{"id":2180,"title":2181,"titles":2182,"content":2183,"level":115},"\u002Fblog\u002Fnvm-install-solution#第二步切换国内镜像源核心步骤","第二步：切换国内镜像源（核心步骤）",[937,2171],"将 Node.js 的下载源修改为淘宝镜像（npmmirror），国内下载速度飞快。 nvm node_mirror https:\u002F\u002Fnpmmirror.com\u002Fmirrors\u002Fnode\u002F 提示：如果也需要加速 npm 包的下载，可以顺便设置：nvm npm_mirror https:\u002F\u002Fnpmmirror.com\u002Fmirrors\u002Fnpm\u002F",{"id":2185,"title":2186,"titles":2187,"content":2188,"level":115},"\u002Fblog\u002Fnvm-install-solution#第三步配置代理如果你必须使用梯子","第三步：配置代理（如果你必须使用梯子）",[937,2171],"如果你所在的网络环境必须通过代理才能访问外网，可以设置代理地址。以 Clash Verge 为例，默认 HTTP 端口通常为 7897，请根据你实际软件的端口调整。 nvm proxy http:\u002F\u002F127.0.0.1:7897 注意：如果你已经设置了淘宝镜像（第二步），通常不需要再设置代理，直接下载即可。如果设置代理后反而报错，请尝试运行 nvm proxy none 关闭代理，仅使用淘宝镜像。",{"id":2190,"title":2191,"titles":2192,"content":2193,"level":109},"\u002Fblog\u002Fnvm-install-solution#最终结果","最终结果",[937],"配置完成后，再次运行安装命令： nvm install 20  # 或者你要安装的具体版本，如 18, 22 等 下载速度瞬间跑满带宽，不再卡住！",{"id":2195,"title":2196,"titles":2197,"content":2198,"level":109},"\u002Fblog\u002Fnvm-install-solution#常用命令速查","常用命令速查",[937],"命令说明nvm node_mirror \u003Curl>设置 Node.js 下载镜像源nvm npm_mirror \u003Curl>设置 npm 下载镜像源nvm proxy \u003Curl>设置下载代理（例：http:\u002F\u002F127.0.0.1:7890）nvm proxy none取消代理（切回直连时用）nvm list available查看可安装的 Node.js 版本nvm install \u003Cversion>安装指定版本nvm use \u003Cversion>切换使用该版本",{"id":2200,"title":1168,"titles":2201,"content":2202,"level":109},"\u002Fblog\u002Fnvm-install-solution#总结",[937],"遇到 nvm 下载卡住的问题，优先尝试修改镜像源，其次再考虑配置代理。利用 nvm 自带命令行的方式，避免了手动修改配置文件可能带来的路径错误或权限问题，优雅又高效！ html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",[2204],{"title":5,"path":6,"stem":7,"children":2205,"icon":93},[2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218],{"title":10,"path":6,"stem":7,"icon":11},{"title":13,"path":14,"stem":15,"icon":16},{"title":18,"path":19,"stem":20,"icon":21},{"title":23,"path":24,"stem":25,"icon":26},{"title":28,"path":29,"stem":30,"icon":31},{"title":33,"path":34,"stem":35,"icon":36},{"title":38,"path":39,"stem":40,"icon":41},{"title":43,"path":44,"stem":45,"icon":46},{"title":48,"path":49,"stem":50,"icon":51},{"title":53,"path":54,"stem":55,"icon":56},{"title":58,"path":59,"stem":60,"icon":61},{"title":63,"path":64,"stem":65,"icon":66},{"title":68,"icon":69,"path":70,"stem":71,"children":2219,"page":93},[2220,2221,2222,2223],{"title":74,"path":75,"stem":76,"icon":77},{"title":79,"path":80,"stem":81,"icon":82},{"title":84,"path":85,"stem":86,"icon":87},{"title":89,"path":90,"stem":91,"icon":92},{"id":2225,"title":18,"body":2226,"description":222,"extension":2294,"meta":2295,"name":2296,"navigation":2297,"path":19,"rawbody":2298,"seo":2299,"stem":20,"__hash__":2300},"docs\u002F1.docs\u002F1.getting-started\u002F11.file-preview.md",{"type":2227,"value":2228,"toc":2290},"minimark",[2229,2233,2274,2276],[2230,2231,225],"h2",{"id":2232},"file-viewer",[2234,2235,2236,2247,2254,2261,2268,2271],"ul",{},[2237,2238,2239],"li",{},[2240,2241,2246],"a",{"href":2242,"rel":2243,"target":2245},"https:\u002F\u002Fdoc.file-viewer.app\u002F",[2244],"nofollow","_blank","文档",[2237,2248,2249],{},[2240,2250,2253],{"href":2251,"rel":2252,"target":2245},"https:\u002F\u002Ffile-viewer.app\u002F",[2244],"官网",[2237,2255,2256],{},[2240,2257,2260],{"href":2258,"rel":2259,"target":2245},"https:\u002F\u002Fdemo.file-viewer.app\u002F",[2244],"在线 Demo",[2237,2262,2263],{},[2240,2264,2267],{"href":2265,"rel":2266,"target":2245},"https:\u002F\u002Fgithub.com\u002Fflyfish-dev\u002Ffile-viewer",[2244],"GitHub",[2237,2269,2270],{},"浏览器端、可自托管的多格式文件预览组件；Office \u002F PDF \u002F CAD \u002F 压缩包等本地解析，无需上传到第三方转换服务",[2237,2272,2273],{},"支持 Vue \u002F React \u002F Web Component 等接入，适合私有附件预览场景",[2230,2275,230],{"id":230},[2234,2277,2278,2284,2287],{},[2237,2279,2280],{},[2240,2281,2267],{"href":2282,"rel":2283,"target":2245},"https:\u002F\u002Fgithub.com\u002F501351981\u002Fvue-office",[2244],[2237,2285,2286],{},"支持 Word（.docx）、Excel（.xlsx \u002F .xls）、PDF、PPTX 等 Office 文件预览的 Vue 组件集合",[2237,2288,2289],{},"兼容 Vue 2 \u002F 3，也可在 React 等非 Vue 框架使用，偏轻量的一站式预览方案",{"title":103,"searchDepth":109,"depth":109,"links":2291},[2292,2293],{"id":2232,"depth":109,"text":225},{"id":230,"depth":109,"text":230},"md",{},null,{"icon":21},"---\ntitle: 文件预览\ndescription: 浏览器端多格式文件预览方案.\nnavigation:\n  icon: i-lucide-file-search\n---\n\n## File Viewer\n\n- [文档](https:\u002F\u002Fdoc.file-viewer.app\u002F){target=\"_blank\"}\n- [官网](https:\u002F\u002Ffile-viewer.app\u002F){target=\"_blank\"}\n- [在线 Demo](https:\u002F\u002Fdemo.file-viewer.app\u002F){target=\"_blank\"}\n- [GitHub](https:\u002F\u002Fgithub.com\u002Fflyfish-dev\u002Ffile-viewer){target=\"_blank\"}\n- 浏览器端、可自托管的多格式文件预览组件；Office \u002F PDF \u002F CAD \u002F 压缩包等本地解析，无需上传到第三方转换服务\n- 支持 Vue \u002F React \u002F Web Component 等接入，适合私有附件预览场景\n\n## vue-office\n\n- [GitHub](https:\u002F\u002Fgithub.com\u002F501351981\u002Fvue-office){target=\"_blank\"}\n- 支持 Word（.docx）、Excel（.xlsx \u002F .xls）、PDF、PPTX 等 Office 文件预览的 Vue 组件集合\n- 兼容 Vue 2 \u002F 3，也可在 React 等非 Vue 框架使用，偏轻量的一站式预览方案\n",{"title":18,"description":222},"IlsS2pbq8CUoXMYtrY6nZkqAPAhXY3bQcEbQHdmBsCw",[2302,2303],{"title":13,"path":14,"stem":15,"description":214,"icon":16,"children":-1},{"title":23,"path":24,"stem":25,"description":235,"icon":26,"children":-1},1791265471877]