[{"data":1,"prerenderedAt":1636},["ShallowReactive",2],{"app-docsNavigation":3,"docsSearch":64,"app-blogNavigation":600,"blogSearch":662,"blog":1555,"posts-count":1568,"\u002Fblog-page-1":1569},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":63},"备忘录","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17,22,27,32,37,42,47],{"title":10,"path":6,"stem":7,"icon":11},"区块链相关(前端)","i-lucide-box",{"title":13,"path":14,"stem":15,"icon":16},"图片压缩","\u002Fdocs\u002Fgetting-started\u002Fphoto-compression","1.docs\u002F1.getting-started\u002F2.photo-compression","i-lucide-image",{"title":18,"path":19,"stem":20,"icon":21},"组件库","\u002Fdocs\u002Fgetting-started\u002Fcomponent-library","1.docs\u002F1.getting-started\u002F3.component-library","i-lucide-puzzle",{"title":23,"path":24,"stem":25,"icon":26},"图表库","\u002Fdocs\u002Fgetting-started\u002Fcharts","1.docs\u002F1.getting-started\u002F4.charts","i-lucide-bar-chart-3",{"title":28,"path":29,"stem":30,"icon":31},"CSS","\u002Fdocs\u002Fgetting-started\u002Fcss","1.docs\u002F1.getting-started\u002F5.css","i-lucide-palette",{"title":33,"path":34,"stem":35,"icon":36},"调试测试工具","\u002Fdocs\u002Fgetting-started\u002Ftest","1.docs\u002F1.getting-started\u002F6.test","i-lucide-bug",{"title":38,"path":39,"stem":40,"icon":41},"uni-helper","\u002Fdocs\u002Fgetting-started\u002Funi-helper","1.docs\u002F1.getting-started\u002F7.uni-helper","i-lucide-settings",{"title":43,"path":44,"stem":45,"icon":46},"工作流可视化库","\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries","1.docs\u002F1.getting-started\u002F8.workflow-libraries","i-lucide-workflow",{"title":48,"icon":49,"path":50,"stem":51,"children":52,"page":63},"Skills","i-lucide-sparkles","\u002Fdocs\u002Fgetting-started\u002Fskills","1.docs\u002F1.getting-started\u002F9.skills",[53,58],{"title":54,"path":55,"stem":56,"icon":57},"Superpowers","\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers","1.docs\u002F1.getting-started\u002F9.skills\u002F1.superpowers","i-lucide-zap",{"title":59,"path":60,"stem":61,"icon":62},"Frontend Design","\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design","1.docs\u002F1.getting-started\u002F9.skills\u002F2.frontend-design","i-lucide-paintbrush",false,[65,69,74,80,86,91,96,101,105,110,116,119,124,129,132,137,141,146,151,155,159,163,167,170,174,178,182,185,190,194,197,201,206,210,214,219,223,226,231,235,239,244,248,251,256,260,263,268,272,275,278,283,288,293,298,303,306,311,316,321,326,331,336,339,344,347,352,356,361,366,371,375,380,385,389,394,399,404,409,414,417,422,427,432,436,441,446,451,456,460,464,468,472,477,481,486,490,495,500,504,508,512,516,519,523,528,532,537,542,547,551,555,558,562,566,571,576,581,586,591,596],{"id":6,"title":10,"titles":66,"content":67,"level":68},[],"涉及前端连接钱包,签名,读写合约等.",1,{"id":70,"title":71,"titles":72,"content":73,"level":68},"\u002Fdocs\u002Fgetting-started#区块链前端开发常用库","区块链前端开发常用库",[],"",{"id":75,"title":76,"titles":77,"content":78,"level":79},"\u002Fdocs\u002Fgetting-started#web3js","Web3.js",[71],"web3.js 是一个广泛使用的 Ethereum JavaScript API 库，允许开发者与以太坊区块链进行交互。",2,{"id":81,"title":82,"titles":83,"content":84,"level":85},"\u002Fdocs\u002Fgetting-started#主要功能","主要功能",[71,76],"连接以太坊节点发送交易和查询链上数据智能合约交互账户管理",3,{"id":87,"title":88,"titles":89,"content":90,"level":85},"\u002Fdocs\u002Fgetting-started#基本用法示例","基本用法示例",[71,76],"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":92,"title":93,"titles":94,"content":95,"level":85},"\u002Fdocs\u002Fgetting-started#官方文档","官方文档",[71,76],"官方文档GitHub 仓库",{"id":97,"title":98,"titles":99,"content":100,"level":79},"\u002Fdocs\u002Fgetting-started#wagmi","Wagmi",[71],"wagmi 是一个基于 React 的区块链开发工具库，兼容Javascript，Vue，简化了与 EVM 兼容链的交互逻辑，内置了常见的区块链操作钩子，支持多链和钱包集成。",{"id":102,"title":82,"titles":103,"content":104,"level":85},"\u002Fdocs\u002Fgetting-started#主要功能-1",[71,98],"基于 React Hooks 的 API 设计，简化状态管理内置钱包连接（MetaMask、Coinbase Wallet 等）智能合约交互封装（读 \u002F 写操作、事件监听）多链支持与链切换管理交易状态跟踪与错误处理",{"id":106,"title":107,"titles":108,"content":109,"level":85},"\u002Fdocs\u002Fgetting-started#安装以vue为示例","安装(以Vue为示例)",[71,98],"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":111,"title":112,"titles":113,"content":114,"level":115},"\u002Fdocs\u002Fgetting-started#将插件添加到应用程序","将插件添加到应用程序",[71,98,107],"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":117,"title":88,"titles":118,"content":73,"level":85},"\u002Fdocs\u002Fgetting-started#基本用法示例-1",[71,98],{"id":120,"title":121,"titles":122,"content":123,"level":115},"\u002Fdocs\u002Fgetting-started#获取用户钱包地址","获取用户钱包地址",[71,98,88],"\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":125,"title":126,"titles":127,"content":128,"level":115},"\u002Fdocs\u002Fgetting-started#智能合约交互","智能合约交互",[71,98,88],"\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":130,"title":93,"titles":131,"content":95,"level":85},"\u002Fdocs\u002Fgetting-started#官方文档-1",[71,98],{"id":133,"title":134,"titles":135,"content":136,"level":79},"\u002Fdocs\u002Fgetting-started#reown","Reown",[71],"Reown使构建者能够创建安全，用户友好和富有洞察力的钱包和应用程序UX。提供所有工具，以更快地启动和更智能地扩展。",{"id":138,"title":82,"titles":139,"content":140,"level":85},"\u002Fdocs\u002Fgetting-started#主要功能-2",[71,134],"支持多种钱包的无缝连接提供标准化的连接流程支持移动端和桌面端钱包管理多个应用的连接会话提供会话状态的实时同步支持会话的持久化存储支持主流区块链网络",{"id":142,"title":143,"titles":144,"content":145,"level":85},"\u002Fdocs\u002Fgetting-started#安装以vuewagmi为示例","安装(以Vue+wagmi为示例)",[71,134],"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":147,"title":148,"titles":149,"content":150,"level":85},"\u002Fdocs\u002Fgetting-started#申请projectid","申请ProjectId",[71,134],"前往Reown仪表板并立即创建一个新项目！",{"id":152,"title":148,"titles":153,"content":154,"level":115},"\u002Fdocs\u002Fgetting-started#申请projectid-1",[71,134,148],"在Reown仪表板上创建新项目，\n并获取新的项目ID。",{"id":156,"title":112,"titles":157,"content":158,"level":115},"\u002Fdocs\u002Fgetting-started#将插件添加到应用程序-1",[71,134,148],"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":160,"title":121,"titles":161,"content":162,"level":115},"\u002Fdocs\u002Fgetting-started#获取用户钱包地址-1",[71,134,148],"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":164,"title":126,"titles":165,"content":166,"level":115},"\u002Fdocs\u002Fgetting-started#智能合约交互-1",[71,134,148],"\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":168,"title":93,"titles":169,"content":95,"level":85},"\u002Fdocs\u002Fgetting-started#官方文档-2",[71,134],{"id":171,"title":172,"titles":173,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started#viem","Viem",[71],{"id":175,"title":82,"titles":176,"content":177,"level":85},"\u002Fdocs\u002Fgetting-started#主要功能-3",[71,172],"Viem 是 Wagmi 的底层依赖，提供区块链核心功能可与 Wagmi 结合使用，提供更高级的 React Hooks API支持 AppKit 和其他现代 Web3 开发框架",{"id":179,"title":93,"titles":180,"content":181,"level":85},"\u002Fdocs\u002Fgetting-started#官方文档-3",[71,172],"官方文档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":183,"content":184,"level":68},[],"常用的图片压缩网站.",{"id":186,"title":187,"titles":188,"content":189,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fphoto-compression#tinify","tinify",[13],"网站链接",{"id":191,"title":192,"titles":193,"content":189,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fphoto-compression#docsmall","docsmall",[13],{"id":19,"title":18,"titles":195,"content":196,"level":68},[],"常用的组件库.",{"id":198,"title":199,"titles":200,"content":73,"level":68},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#常用组件库","常用组件库",[],{"id":202,"title":203,"titles":204,"content":205,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#vue-bits","Vue Bits",[199],"Vue Bits 是一个开源的 Vue.js 组件库，专注于提供动画、交互和高度可定制的组件。它是 React Bits 的官方 Vue 版本，旨在为 Vue.js 开发者带来类似 React Bits 的动态用户界面体验。",{"id":207,"title":82,"titles":208,"content":209,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能",[199,203],"超过 80 个精心设计的动画组件，涵盖按钮、菜单、卡片、文本效果等多个类别双主题支持：所有组件都支持 CSS 和 Tailwind 一键切换完全使用 TypeScript 开发性能优化：减少不必要的 DOM 操作和重绘响应式设计：在各种设备上保持一致的表现",{"id":211,"title":93,"titles":212,"content":213,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档",[199,203],"官方网站GitHub 仓库",{"id":215,"title":216,"titles":217,"content":218,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#inspira-ui","Inspira UI",[199],"Inspira UI 是一个基于 Vue.js 和 Nuxt 的现代化 UI 组件库，旨在为开发者提供高质量、可复用的前端组件。它结合了优秀设计体系的精髓，既保证了界面的现代美感，又通过模块化架构满足不同项目的定制需求。",{"id":220,"title":82,"titles":221,"content":222,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-1",[199,216],"涵盖按钮、表单、导航、数据展示等 40+ 基础组件响应式设计：基于 Tailwind CSS 构建的弹性布局系统Nuxt 原生支持：针对 Nuxt 框架进行专项优化内置多套预设主题，支持自定义色彩、字体等设计语言组件包含悬停、加载、反馈等微交互动画",{"id":224,"title":93,"titles":225,"content":213,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-1",[199,216],{"id":227,"title":228,"titles":229,"content":230,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#pixel-ui","Pixel UI",[199],"Pixel UI 是一个像素风格的UI组件库，为开发者提供复古像素风格的界面组件。它适用于需要像素艺术风格的游戏、应用程序或网站。",{"id":232,"title":82,"titles":233,"content":234,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-2",[199,228],"提供完整的像素风格组件集合，包括按钮、表单、导航等基础组件复古像素艺术设计风格易于集成和使用支持自定义像素大小和颜色方案轻量级且性能优化",{"id":236,"title":93,"titles":237,"content":238,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-2",[199,228],"官方网站",{"id":240,"title":241,"titles":242,"content":243,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#uiverse","Uiverse",[199],"Uiverse 是一个开源的UI组件库集合，提供了大量精美的开源UI元素，开发者可以免费使用这些组件构建现代化的用户界面。它包含各种独特的按钮、卡片、表单和其他UI元素。",{"id":245,"title":82,"titles":246,"content":247,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-3",[199,241],"提供超过1000个免费的开源UI组件包含各种独特的按钮、卡片、加载动画等元素支持多种框架（React、Vue、Angular等）现代化设计，易于定制和集成社区驱动，持续更新和改进",{"id":249,"title":93,"titles":250,"content":238,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-3",[199,241],{"id":252,"title":253,"titles":254,"content":255,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#nuxt-ui","Nuxt UI",[199],"Nuxt UI 是专为 Nuxt 3 设计的 UI 库，提供了一组高质量的 Vue 3 组件和 composables。它充分利用了 Nuxt 3 的特性，如自动导入、TypeScript 支持等，让开发者能够快速构建美观且功能强大的 Nuxt 应用程序。",{"id":257,"title":82,"titles":258,"content":259,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-4",[199,253],"专为 Nuxt 3 构建，充分利用 Nuxt 的特性和生态系统包含 50+ 高质量的 Vue 3 组件，涵盖常用的 UI 元素内置 20+ composables，提供实用的功能和交互基于 Tailwind CSS 构建，支持轻松定制主题支持暗色模式，提供优秀的用户体验自动 Tree-shaking，只打包实际使用的组件和功能完整的 TypeScript 支持",{"id":261,"title":93,"titles":262,"content":238,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-4",[199,253],{"id":264,"title":265,"titles":266,"content":267,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#tdesign-uniapp","TDesign UniApp",[199],"TDesign UniApp 是腾讯 TDesign 设计体系针对 UniApp 框架的官方实现，提供了一套高质量的跨平台组件库。它支持在微信小程序、支付宝小程序、H5 等多个平台上使用，并包含专门的 AI 聊天组件。",{"id":269,"title":82,"titles":270,"content":271,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#主要功能-5",[199,265],"完整的跨平台支持：微信小程序、支付宝小程序、H5 等多个平台50+ 高质量的 UniApp 组件库专为移动端优化的 AI 聊天组件，支持流式输出和实时交互基于腾讯 TDesign 设计体系，保证设计一致性支持主题定制和暗色模式完整的 TypeScript 支持",{"id":273,"title":93,"titles":274,"content":238,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fcomponent-library#官方文档-5",[199,265],{"id":24,"title":23,"titles":276,"content":277,"level":68},[],"常用的图表库.",{"id":279,"title":280,"titles":281,"content":282,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcharts#echarts","echarts",[23],"网站链接一个基于 JavaScript 的开源可视化图表库",{"id":284,"title":285,"titles":286,"content":287,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcharts#vue-echarts","vue-echarts",[23],"网站链接专为 Vue.js 设计的 ECharts 组件，提供响应式和交互式图表",{"id":289,"title":290,"titles":291,"content":292,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcharts#ucharts","ucharts",[23],"网站链接PC、H5、APP、小程序、Vue、Taro",{"id":294,"title":295,"titles":296,"content":297,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcharts#uni-echarts","uni echarts",[23],"网站链接适用于 uni-app 的 ECharts 组件 与 Vue ECharts 近乎一致的使用体验",{"id":299,"title":300,"titles":301,"content":302,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcharts#klinecharts","klinecharts",[23],"网站链接一款轻量级的金融图表库，主要用于绘制K线图和其他金融图表",{"id":29,"title":28,"titles":304,"content":305,"level":68},[],"常用的CSS库\u002F网站.",{"id":307,"title":308,"titles":309,"content":310,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcss#tailwind-css","Tailwind CSS",[28],"网站链接一个功能优先的CSS框架，用于快速构建定制化的用户界面",{"id":312,"title":313,"titles":314,"content":315,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcss#unocss","UnoCSS",[28],"网站链接即时按需原子化CSS引擎，具有高性能和可扩展性",{"id":317,"title":318,"titles":319,"content":320,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcss#clippy","Clippy",[28],"网站链接一个自定义裁切CSS生成器，可以轻松创建各种形状的CSS clip-path",{"id":322,"title":323,"titles":324,"content":325,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcss#motion-for-vuejs","Motion for Vue.js",[28],"网站链接基于 Vue Composition API 的动画库，提供简单易用的滚入动画和各种流畅的过渡效果",{"id":327,"title":328,"titles":329,"content":330,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcss#css-inspiration","CSS Inspiration",[28],"网站链接一个 CSS 灵感库，展示各种创意的 CSS 技巧、特效和最佳实践",{"id":332,"title":333,"titles":334,"content":335,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fcss#zeoseven-fonts","ZeoSeven Fonts",[28],"网站链接免费商用字体的下载、预览与 API，收录大量中英文字体，支持 Web Fonts API 快速接入",{"id":34,"title":33,"titles":337,"content":338,"level":68},[],"常用的前端调试和测试工具.",{"id":340,"title":341,"titles":342,"content":343,"level":79},"\u002Fdocs\u002Fgetting-started\u002Ftest#下载指定版本chromium内核","下载指定版本Chromium内核",[33],"下载地址可以下载指定历史版本的Chromium内核，方便进行浏览器兼容性测试",{"id":39,"title":38,"titles":345,"content":346,"level":68},[],"专注于 uni-app 生态的工具集.",{"id":348,"title":349,"titles":350,"content":351,"level":79},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#什么是-uni-helper","什么是 uni-helper",[38],"uni-helper 是一个专注于 uni-app 生态的工具集，旨在提升开发者在使用 uni-app 进行跨平台开发时的开发效率和体验。它补全了 uni-app 生态的短板，提供了一系列工具来让 uni-app 的开发体验达到 web 开发的效果。",{"id":353,"title":354,"titles":355,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#核心功能","核心功能",[38],{"id":357,"title":358,"titles":359,"content":360,"level":85},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_1-typescript-类型支持","1. TypeScript 类型支持",[38,354],"@uni-helper\u002Funi-app-types：提供了一套完整的 TypeScript 类型定义文件，专门针对 uni-app 平台设计支持 IDE 自动补全、参数提示等功能提升代码的可读性和可维护性",{"id":362,"title":363,"titles":364,"content":365,"level":85},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_2-网络请求适配","2. 网络请求适配",[38,354],"@uni-helper\u002Faxios-adapter：封装了 axios 的核心功能，并针对 uni-app 做了定制化的配置和优化支持 uni-app 环境下的网络请求保持 axios 熟悉的 API 风格",{"id":367,"title":368,"titles":369,"content":370,"level":85},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_3-开发体验增强","3. 开发体验增强",[38,354],"提供一系列工具让 uni-app 开发体验达到 web 开发效果补全 uni-app 生态的短板",{"id":372,"title":373,"titles":374,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#安装使用","安装使用",[38],{"id":376,"title":377,"titles":378,"content":379,"level":85},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#前置条件","前置条件",[38,373],"Node.js 版本 >= 12.0.0使用 npm 或 yarn 管理依赖",{"id":381,"title":382,"titles":383,"content":384,"level":85},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#安装示例","安装示例",[38,373],"# 安装 TypeScript 类型定义\nnpm install @uni-helper\u002Funi-app-types -D\n\n# 安装 axios 适配器和 axios\nnpm install @uni-helper\u002Faxios-adapter axios",{"id":386,"title":387,"titles":388,"content":73,"level":85},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#使用示例","使用示例",[38,373],{"id":390,"title":391,"titles":392,"content":393,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_1-使用-axios-适配器","1. 使用 axios 适配器",[38,373,387],"\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":395,"title":396,"titles":397,"content":398,"level":115},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#_2-配置-typescript-类型支持","2. 配置 TypeScript 类型支持",[38,373,387],"\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":400,"title":401,"titles":402,"content":403,"level":79},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#支持的平台","支持的平台",[38],"uni-helper 支持 uni-app 覆盖的所有平台，包括： 微信小程序支付宝小程序百度小程序字节跳动小程序QQ 小程序快应用H5App（iOS\u002FAndroid）",{"id":405,"title":406,"titles":407,"content":408,"level":79},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#社区与贡献","社区与贡献",[38],"GitHub 仓库：https:\u002F\u002Fgithub.com\u002Funi-helper官网：https:\u002F\u002Funi-helper.js.org\u002F",{"id":410,"title":411,"titles":412,"content":413,"level":79},"\u002Fdocs\u002Fgetting-started\u002Funi-helper#许可证","许可证",[38],"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":44,"title":43,"titles":415,"content":416,"level":68},[],"Vue Flow 和 Logic Flow 工作流图表库介绍.",{"id":418,"title":419,"titles":420,"content":421,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#什么是工作流可视化库","什么是工作流可视化库",[43],"工作流可视化库是用于构建流程图、工作流编辑器和节点图等应用的前端组件库。它们提供了强大的图形编辑、节点连接、拖拽布局等功能，广泛应用于流程设计、数据流可视化、低代码平台等场景。",{"id":423,"title":424,"titles":425,"content":426,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#vue-flow","Vue Flow",[43],"Vue Flow 是基于 Vue 3 的流程图库，它是 React Flow 的 Vue 版本实现，提供了高度可定制的节点图编辑器。",{"id":428,"title":429,"titles":430,"content":431,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#核心特性","核心特性",[43,424],"Vue 3 原生支持：完全基于 Vue 3 和 Composition API 构建高性能渲染：支持大量节点的流畅渲染和交互自定义节点：可以使用任何 Vue 组件作为节点丰富的交互：支持拖拽、缩放、平移、连线等操作TypeScript 支持：完整的类型定义插件系统：可扩展的插件架构",{"id":433,"title":373,"titles":434,"content":435,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#安装使用",[43,424],"# 使用 pnpm 安装\npnpm add @vue-flow\u002Fcore\n\n# 可选插件\npnpm add @vue-flow\u002Fbackground @vue-flow\u002Fcontrols @vue-flow\u002Fminimap",{"id":437,"title":438,"titles":439,"content":440,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#基础示例","基础示例",[43,424],"\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":442,"title":443,"titles":444,"content":445,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#自定义节点示例","自定义节点示例",[43,424],"\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":447,"title":448,"titles":449,"content":450,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#适用场景","适用场景",[43,424],"工作流编辑器流程图设计工具数据流可视化状态机图表思维导图",{"id":452,"title":453,"titles":454,"content":455,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#logic-flow","Logic Flow",[43],"Logic Flow 是滴滴开源的流程图编辑框架，提供了一套完整的流程可视化解决方案，特别适合于业务流程管理。",{"id":457,"title":429,"titles":458,"content":459,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#核心特性-1",[43,453],"框架无关：可以在 Vue、React 等任何框架中使用业务友好：专注于业务流程场景，提供丰富的业务节点高度可扩展：支持自定义节点、边、主题等BPMN 支持：支持 BPMN 2.0 规范数据转换：支持多种数据格式导入导出完善的 API：提供丰富的 API 和事件系统",{"id":461,"title":373,"titles":462,"content":463,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#安装使用-1",[43,453],"# 核心库\npnpm add @logicflow\u002Fcore\n\n# 扩展包\npnpm add @logicflow\u002Fextension",{"id":465,"title":438,"titles":466,"content":467,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#基础示例-1",[43,453],"\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":469,"title":443,"titles":470,"content":471,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#自定义节点示例-1",[43,453],"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":473,"title":474,"titles":475,"content":476,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#在-vue-中注册自定义节点","在 Vue 中注册自定义节点",[43,453],"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":478,"title":448,"titles":479,"content":480,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#适用场景-1",[43,453],"审批流程设计业务流程管理（BPM）BPMN 流程图低代码平台工作流引擎可视化",{"id":482,"title":483,"titles":484,"content":485,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#对比总结","对比总结",[43],"特性Vue FlowLogic Flow框架支持Vue 3 专用框架无关学习曲线中等较平缓性能优秀良好自定义能力强（Vue 组件）强（类继承）BPMN 支持需自行实现原生支持社区活跃度高中等TypeScript完整支持完整支持适用场景通用流程图业务流程为主",{"id":487,"title":488,"titles":489,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#选择建议","选择建议",[43],{"id":491,"title":492,"titles":493,"content":494,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#选择-vue-flow-如果","选择 Vue Flow 如果：",[43,488],"你的项目使用 Vue 3需要高度自定义的节点和交互追求现代化的开发体验对性能要求较高",{"id":496,"title":497,"titles":498,"content":499,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#选择-logic-flow-如果","选择 Logic Flow 如果：",[43,488],"需要 BPMN 支持专注于业务流程管理需要框架无关的解决方案需要更成熟的企业级特性",{"id":501,"title":502,"titles":503,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#相关资源","相关资源",[43],{"id":505,"title":424,"titles":506,"content":507,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#vue-flow-1",[43,502],"官网：https:\u002F\u002Fvueflow.dev\u002FGitHub：https:\u002F\u002Fgithub.com\u002Fbcakmakoglu\u002Fvue-flow示例：https:\u002F\u002Fvueflow.dev\u002Fexamples\u002F",{"id":509,"title":453,"titles":510,"content":511,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#logic-flow-1",[43,502],"官网：http:\u002F\u002Flogic-flow.org\u002FGitHub：https:\u002F\u002Fgithub.com\u002Fdidi\u002FLogicFlow文档：http:\u002F\u002Flogic-flow.org\u002Fguide\u002Fstart.html",{"id":513,"title":411,"titles":514,"content":515,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fworkflow-libraries#许可证",[43],"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":55,"title":54,"titles":517,"content":518,"level":68},[],"一套增强开发工作流的 Claude Code 技能集，涵盖规划、调试、TDD、代码审查等.",{"id":520,"title":54,"titles":521,"content":522,"level":68},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#superpowers",[],"Superpowers 是一套面向 Claude Code 的开发工作流增强技能集，用结构化流程替代临时发挥，让 AI 辅助开发更可靠、更高效。",{"id":524,"title":525,"titles":526,"content":527,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#核心理念","核心理念",[54],"遇到任务时不要凭直觉直接写代码，而是按照既定的流程技能来执行 — 先规划、再实现、最后验证。",{"id":529,"title":530,"titles":531,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#技能分类","技能分类",[54],{"id":533,"title":534,"titles":535,"content":536,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#流程技能process-skills","流程技能（Process Skills）",[54,530],"这些技能决定如何完成任务，应在其他技能之前使用： Brainstorming — 在创建功能、构建组件、添加功能之前，先探索用户意图和设计需求Writing Plans — 当有需求规格说明或涉及多步骤任务时，先编写实现计划Executing Plans — 按照计划逐步执行，保持任务分解和进度追踪Systematic Debugging — 遇到 Bug 或测试失败时，系统化定位根因而非盲目修复Test-Driven Development — 红-绿-重构循环，先写测试再写代码Verification Before Completion — 完成前验证变更是否符合预期",{"id":538,"title":539,"titles":540,"content":541,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#协作技能collaboration-skills","协作技能（Collaboration Skills）",[54,530],"Requesting Code Review — 提交代码前请求 AI 审查Receiving Code Review — 处理审查反馈Finishing a Development Branch — 实现完成后决定合并、PR 或清理Dispatching Parallel Agents — 将独立子任务并行分派给多个 AgentSubagent-Driven Development — 在当前 session 中执行多 Agent 实现计划",{"id":543,"title":544,"titles":545,"content":546,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#基础设施技能infrastructure-skills","基础设施技能（Infrastructure Skills）",[54,530],"Using Git Worktrees — 使用 Git 工作树隔离并行开发Using Superpowers — 技能的技能，指引如何正确加载和使用技能",{"id":548,"title":373,"titles":549,"content":550,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#安装使用",[54],"Superpowers 通过 Claude Code 插件系统安装： claude plugins install superpowers@anthropic-agent-sdk 安装后，在对话中直接输入 \u002Fsuperpowers:\u003Cskill-name> 即可调用对应技能。",{"id":552,"title":502,"titles":553,"content":554,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Fsuperpowers#相关资源",[54],"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":60,"title":59,"titles":556,"content":557,"level":68},[],"前端设计技能，提供美学方向、字体排版和视觉设计指导，让界面摆脱模板化风格.",{"id":559,"title":59,"titles":560,"content":561,"level":68},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#frontend-design",[],"Frontend Design 是一个专注于视觉美学与设计方向的技能，帮助开发者在构建或重塑 UI 时做出独特、有意图的设计选择，避免呈现出千篇一律的模板化外观。",{"id":563,"title":564,"titles":565,"content":73,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#核心能力","核心能力",[59],{"id":567,"title":568,"titles":569,"content":570,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#美学方向","美学方向",[59,564],"为界面确定独特的视觉风格和设计方向做出有意图的设计选择，而非依赖默认样式让产品看起来不像\"又一个 AI 生成的模板\"",{"id":572,"title":573,"titles":574,"content":575,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#字体排版","字体排版",[59,564],"选择合适的字体组合和排版层次字号、行高、字间距的精细调整通过排版建立清晰的信息层级",{"id":577,"title":578,"titles":579,"content":580,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#色彩与视觉","色彩与视觉",[59,564],"配色方案的选择与搭配避免默认调色板的廉价感通过色彩传达品牌调性和情感",{"id":582,"title":583,"titles":584,"content":585,"level":85},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#布局与空间","布局与空间",[59,564],"打破对称、网格化的呆板布局留白和视觉节奏的把控让页面有\"设计感\"而非仅仅是\"能用\"",{"id":587,"title":588,"titles":589,"content":590,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#使用场景","使用场景",[59],"从零开始构建新页面或组件时对现有界面进行视觉改版感觉当前 UI \"看起来像模板\"想要提升需要确定产品的整体视觉风格方向",{"id":592,"title":593,"titles":594,"content":595,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#使用方式","使用方式",[59],"在 Claude Code 对话中输入 \u002Ffrontend-design 启动该技能，描述你的界面需求，技能会帮助你做出有审美、有意图的设计决策。",{"id":597,"title":502,"titles":598,"content":599,"level":79},"\u002Fdocs\u002Fgetting-started\u002Fskills\u002Ffrontend-design#相关资源",[59],"Frontend Design 技能源码",[601],{"title":602,"path":603,"stem":604,"children":605,"page":63},"Blog","\u002Fblog","3.blog",[606,610,614,618,622,626,630,634,638,642,646,650,654,658],{"title":607,"path":608,"stem":609},"Vue 3 Transition 报错 non-element root node：组件根节点无法被动画化","\u002Fblog\u002Fasian-cuisine","3.blog\u002F1.asian-cuisine",{"title":611,"path":612,"stem":613},"UniApp 踩坑：showModal 按钮字数、iOS 溢出、picker 时区与未选中背景","\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls","3.blog\u002F10.uniapp-wechat-miniprogram-pitfalls",{"title":615,"path":616,"stem":617},"UniApp RootPortal：修复 uview-plus Popup\u002FModal 遮罩层层级异常","\u002Fblog\u002Funiapp-root-portal","3.blog\u002F11.uniapp-root-portal",{"title":619,"path":620,"stem":621},"VS Code Copilot 配置 Apifox MCP：让 AI 直接读 API 文档","\u002Fblog\u002Fvscode-copilot-apifox-mcp","3.blog\u002F12.vscode-copilot-apifox-mcp",{"title":623,"path":624,"stem":625},"UniApp z-paging easycom 与 vite-plugin-uni-components 冲突：H5 组件变成原始标签","\u002Fblog\u002Funiapp-z-paging-easycom-conflict","3.blog\u002F13.uniapp-z-paging-easycom-conflict",{"title":627,"path":628,"stem":629},"Unibest build:app 后 manifest 缺 tabBar：wgt 热更新白屏修复","\u002Fblog\u002Funibest-manifest-missing-config","3.blog\u002F13.unibest-manifest-missing-config",{"title":631,"path":632,"stem":633},"Apifox + openapi-ts-request：自动生成类型安全的 API 请求代码","\u002Fblog\u002Fapifox-api-generation","3.blog\u002F2.apifox-api-generation",{"title":635,"path":636,"stem":637},"UniApp H5 多文件上传：绕过 uni.uploadFile 单文件限制（fetch + FormData）","\u002Fblog\u002Ffile-upload-issue-solution","3.blog\u002F3.file-upload-issue-solution",{"title":639,"path":640,"stem":641},"WangEditor div 包裹的 img 被过滤：商品详情 HTML 导入修复","\u002Fblog\u002Fwangeditor-img-filter-issue","3.blog\u002F4.wangeditor-img-filter-issue",{"title":643,"path":644,"stem":645},"Nuxt Cannot load payload \u002F_payload.json：ssr:false 部署后修复","\u002Fblog\u002Fnuxt-payload-extraction","3.blog\u002F5.nuxt-payload-extraction",{"title":647,"path":648,"stem":649},"Nuxt 4 集成 nuxt-echarts：ECharts 图表安装与 SSR 注意点","\u002Fblog\u002Fnuxt-echarts-implementation","3.blog\u002F6.nuxt-echarts-implementation",{"title":651,"path":652,"stem":653},"UniApp + Vite 集成 Reown \u002F Wagmi：打包失败、兼容性与 RPC 问题","\u002Fblog\u002Freown-wagmi-uniapp-issues","3.blog\u002F7.reown-wagmi-uniapp-issues",{"title":655,"path":656,"stem":657},"Vue 3 数字滚动动画：Composition API + requestAnimationFrame 实现","\u002Fblog\u002Fnumber-scroll-animation","3.blog\u002F8.number-scroll-animation",{"title":659,"path":660,"stem":661},"nvm-windows 安装 Node 卡住：nvm node_mirror 切换淘宝镜像","\u002Fblog\u002Fnvm-install-solution","3.blog\u002F9.nvm-install-solution",[663,666,671,675,680,685,690,695,700,705,710,715,720,725,730,733,737,742,747,752,757,762,766,770,774,779,783,787,791,795,799,803,807,811,815,819,823,828,832,836,840,844,848,852,856,861,865,869,873,877,881,886,890,895,898,903,908,913,918,922,925,930,935,938,943,948,953,958,962,967,970,975,979,983,988,993,998,1003,1008,1013,1018,1023,1027,1032,1037,1042,1047,1051,1055,1058,1063,1068,1073,1077,1082,1087,1092,1097,1101,1104,1109,1114,1119,1122,1127,1132,1137,1141,1144,1149,1154,1159,1164,1169,1174,1179,1182,1187,1192,1197,1202,1207,1210,1215,1220,1225,1229,1232,1236,1241,1246,1250,1255,1260,1264,1269,1274,1278,1282,1285,1288,1293,1298,1303,1308,1313,1316,1321,1326,1331,1334,1338,1342,1346,1350,1354,1358,1362,1366,1370,1374,1378,1382,1386,1390,1394,1398,1402,1406,1410,1414,1418,1422,1426,1430,1435,1439,1442,1447,1452,1457,1461,1466,1471,1476,1481,1486,1491,1496,1499,1504,1509,1514,1517,1521,1526,1531,1536,1541,1546,1551],{"id":608,"title":607,"titles":664,"content":665,"level":68},[],"修复 [Vue warn] Component inside \u003CTransition> renders non-element root node that cannot be animated。常见于多根节点、纯文本根或 Teleport，会导致页面切换后无法渲染。 在 Vue 3 开发中使用 \u003CTransition> 组件实现动画效果时，很多开发者都会遇到一个常见警告：“Transition 组件的直接子元素必须是单一的根 DOM 元素”。本文将深入解析这个问题的原因、常见错误场景，并提供完整的解决方案。",{"id":667,"title":668,"titles":669,"content":670,"level":79},"\u002Fblog\u002Fasian-cuisine#一核心规则transition-组件的子元素要求","一、核心规则：Transition 组件的子元素要求",[607],"Vue 3 的 \u003CTransition> 组件对其直接子元素有明确限制，必须满足： 必须是 单一的根 DOM 元素（即 \u003Cdiv>、\u003Cspan>、\u003Csection> 等真实 HTML 标签）；不能是纯文本、注释、多个根节点；不能是 \u003CTeleport>、\u003CKeepAlive> 等特殊内置组件。 💡 关键提醒：尽管 Vue 3 支持多根组件（Fragments），但 \u003CTransition>、\u003CKeepAlive> 等动画 \u002F 缓存相关组件并不兼容这种结构。",{"id":672,"title":673,"titles":674,"content":73,"level":79},"\u002Fblog\u002Fasian-cuisine#二常见错误场景及示例","二、常见错误场景及示例",[607],{"id":676,"title":677,"titles":678,"content":679,"level":85},"\u002Fblog\u002Fasian-cuisine#场景-1组件模板只有纯文本无根元素","场景 1：组件模板只有纯文本（无根元素）",[607,673],"组件模板中直接写文本，没有任何 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":681,"title":682,"titles":683,"content":684,"level":85},"\u002Fblog\u002Fasian-cuisine#场景-2组件存在多个根节点fragments","场景 2：组件存在多个根节点（Fragments）",[607,673],"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":686,"title":687,"titles":688,"content":689,"level":85},"\u002Fblog\u002Fasian-cuisine#场景-3动态组件未保证单一根节点","场景 3：动态组件未保证单一根节点",[607,673],"使用 \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":691,"title":692,"titles":693,"content":694,"level":79},"\u002Fblog\u002Fasian-cuisine#三解决方案确保单一根-dom-元素","三、解决方案：确保单一根 DOM 元素",[607],"解决思路非常简单：给组件模板添加一个外层包裹元素，将多根节点、纯文本等统一包裹为单一的根 DOM 元素。",{"id":696,"title":697,"titles":698,"content":699,"level":85},"\u002Fblog\u002Fasian-cuisine#方案-1修复纯文本问题","方案 1：修复纯文本问题",[607,692],"用 \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":701,"title":702,"titles":703,"content":704,"level":85},"\u002Fblog\u002Fasian-cuisine#方案-2修复多根节点问题","方案 2：修复多根节点问题",[607,692],"用 \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":706,"title":707,"titles":708,"content":709,"level":85},"\u002Fblog\u002Fasian-cuisine#方案-3处理动态组件场景","方案 3：处理动态组件场景",[607,692],"确保所有可能被切换的组件（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":711,"title":712,"titles":713,"content":714,"level":85},"\u002Fblog\u002Fasian-cuisine#方案-4异步组件-函数式组件处理","方案 4：异步组件 \u002F 函数式组件处理",[607,692],"即使是通过 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":716,"title":717,"titles":718,"content":719,"level":79},"\u002Fblog\u002Fasian-cuisine#四为什么-vue-要做这样的限制","四、为什么 Vue 要做这样的限制？",[607],"\u003CTransition> 组件的核心原理是 通过操作 DOM 元素的 CSS 类名和监听过渡事件实现动画，具体依赖： 向根 DOM 元素添加 \u002F 移除过渡类名（如 v-enter-from、v-enter-active、v-leave-to 等）；监听根 DOM 元素的 transitionend 或 animationend 事件，判断动画是否完成。 如果根节点不是真实 DOM 元素（如纯文本、Fragment），Vue 无法挂载这些类名，也无法监听过渡事件，最终导致动画失效，因此必须强制要求 “单一根 DOM 元素”。",{"id":721,"title":722,"titles":723,"content":724,"level":79},"\u002Fblog\u002Fasian-cuisine#五总结问题与解决方案对照表","五、总结：问题与解决方案对照表",[607],"常见问题解决方案组件模板只有纯文本用 \u003Cspan>\u002F\u003Cdiv> 包裹文本组件存在多个根节点用 \u003Cdiv>\u002F 语义化标签包裹所有节点动态组件切换时报错确保所有候选组件均为单一根 DOM 元素异步 \u002F 函数式组件报错组件内部添加单一外层 DOM 包裹 只要记住核心原则：\u003CTransition>内部的直接子组件，必须有且仅有一个真实的 DOM 根元素，就能彻底消除警告，让过渡动画正常工作。",{"id":726,"title":727,"titles":728,"content":729,"level":79},"\u002Fblog\u002Fasian-cuisine#延伸阅读","延伸阅读",[607],"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":612,"title":611,"titles":731,"content":732,"level":68},[],"UniApp \u002F 微信小程序常见坑：uni.showModal 确认取消按钮文字字数限制、iOS 元素溢出视口、picker 时区偏差、以及未选中项背景色修改方法。 记录在 UniApp 开发中踩过的坑，H5 调试时一切正常，但在真机上表现异常。",{"id":734,"title":735,"titles":736,"content":73,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-1unishowmodal-按钮文字超出字数限制导致弹窗无法唤起","坑 1：uni.showModal 按钮文字超出字数限制导致弹窗无法唤起",[611],{"id":738,"title":739,"titles":740,"content":741,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象","现象",[611,735],"调用 uni.showModal 时，弹窗完全无法弹出，既没有报错也没有任何提示，回调也不会触发。",{"id":743,"title":744,"titles":745,"content":746,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因","原因",[611,735],"微信小程序的 wx.showModal（即 UniApp 封装的 uni.showModal）对 confirmText 和 cancelText 两个按钮文字有严格的字数限制： 微信小程序中，按钮文字最多支持 4 个字符一旦超出限制，整个弹窗将静默失败，不报错、不回调",{"id":748,"title":749,"titles":750,"content":751,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现代码","复现代码",[611,735],"\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":753,"title":754,"titles":755,"content":756,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案","解决方案",[611,735],"将按钮文字控制在 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":758,"title":759,"titles":760,"content":761,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项","注意事项",[611,735],"此问题仅在微信小程序真机中复现，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":763,"title":764,"titles":765,"content":73,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-2ios-端元素定位超出视口h5-正常但小程序app-出现横向滚动条","坑 2：iOS 端元素定位超出视口，H5 正常但小程序\u002FApp 出现横向滚动条",[611],{"id":767,"title":739,"titles":768,"content":769,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-1",[611,764],"页面中有使用 position: fixed 或 position: absolute 做动画、遮罩、气泡等效果，在 H5 调试时表现完全正常，但在 iOS 真机（微信小程序或 App）上，页面出现横向滚动条，可以左右滑动，用户体验很差。",{"id":771,"title":744,"titles":772,"content":773,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-1",[611,764],"部分元素通过定位移出了视口（例如用 transform: translateX(-100%) 做滑入动画、或气泡超出了右侧边界），在 H5 中浏览器默认会做一定程度的裁剪，但在 iOS WebView（UniApp 小程序\u002FApp 的运行容器）中，这些超出视口的元素会撑开整个页面的宽度，触发横向滚动。",{"id":775,"title":776,"titles":777,"content":778,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现场景","复现场景",[611,764],"\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":780,"title":754,"titles":781,"content":782,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-1",[611,764],"在页面根容器或 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":784,"title":759,"titles":785,"content":786,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-1",[611,764],"UniApp 中小程序的根节点是 page，不是 body，需要用 page { overflow-x: hidden } 而不是 body { overflow-x: hidden }H5 调试时因为浏览器行为差异，此问题很难复现，需要在真机上测试如果设置 overflow-x: hidden 后影响了某些需要横向滚动的子元素，可以在该子元素上单独设置 overflow-x: auto 或 overflow-x: scroll 来恢复",{"id":788,"title":789,"titles":790,"content":73,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-3picker-组件在-ios-端出现日期回显错误选-15-号显示-14-号","坑 3：picker 组件在 iOS 端出现日期回显错误（选 15 号显示 14 号）",[611],{"id":792,"title":739,"titles":793,"content":794,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-2",[611,789],"使用 UniApp 的 \u003Cpicker> 组件选择日期时，在 iOS 真机上出现日期回显不一致的问题：用户明明选择了 15 号，但回显却显示 14 号，差了一天。",{"id":796,"title":744,"titles":797,"content":798,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-2",[611,789],"当 \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":800,"title":749,"titles":801,"content":802,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现代码-1",[611,789],"\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":804,"title":754,"titles":805,"content":806,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-2",[611,789],"明确设置 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":808,"title":759,"titles":809,"content":810,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-2",[611,789],"此问题仅在 iOS 真机上复现，Android 和 H5 通常不受影响问题的根源是时区差异，如果用户手机设置为 UTC 时区则不会出现除了设置 fields 属性，另一个思路是在接收日期后手动做时区补偿，但这会增加代码复杂度，不推荐建议在项目中统一封装日期选择组件，默认带上 fields 属性",{"id":812,"title":813,"titles":814,"content":73,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-4uniapp-运行到-ios-xcode-模拟器261-263无法渲染-emoji","坑 4：UniApp 运行到 iOS Xcode 模拟器（26.1-26.3）无法渲染 Emoji",[611],{"id":816,"title":739,"titles":817,"content":818,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-3",[611,813],"在使用 UniApp 开发 App 时，运行到 iOS Xcode 模拟器版本 26.1 至 26.3，页面中的 Emoji 表情符号完全无法渲染，显示为空白或方块。",{"id":820,"title":744,"titles":821,"content":822,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-3",[611,813],"Xcode 26.1 至 26.3 版本的 iOS 模拟器缺少 Emoji 字体包，导致模拟器无法正确渲染 Emoji 表情符号。这是模拟器运行时环境的问题，与应用代码本身无关。 该问题在 React Native 社区中也有相关讨论。",{"id":824,"title":825,"titles":826,"content":827,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#相关链接","相关链接",[611,813],"GitHub Issue: Emoji rendering fails on Xcode 26.1-26.3 simulators",{"id":829,"title":754,"titles":830,"content":831,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-3",[611,813],"升级 iOS 模拟器运行时版本至 26.4+： 打开 Xcode前往 Xcode > Settings > Platforms（或 Xcode > Preferences > Components）下载 iOS 26.4 或更高版本的模拟器运行时删除旧的 26.1-26.3 版本模拟器，或创建新的使用 26.4+ 运行时的模拟器设备 其他临时方案： 使用真机测试：此问题仅在模拟器上出现，真机运行不受影响使用图片替代 Emoji：在关键位置使用 Emoji 图片资源代替原生 Emoji 字符",{"id":833,"title":759,"titles":834,"content":835,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-3",[611,813],"此问题仅影响 iOS 模拟器，真机和 Android 端均不受影响问题根源是模拟器缺少字体包，与 UniApp 或任何框架无关如果你的项目中大量使用 Emoji（如聊天应用、表情选择器等），建议优先使用真机进行开发测试或确保模拟器版本为 26.4+",{"id":837,"title":838,"titles":839,"content":73,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-5canvas-drawimage-无法直接绘制在线图片","坑 5：Canvas drawImage 无法直接绘制在线图片",[611],{"id":841,"title":739,"titles":842,"content":843,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-4",[611,838],"在使用 Canvas 的 drawImage 方法绘制背景图时，直接传入在线图片链接，H5 调试正常，但在微信小程序真机上图片无法绘制，Canvas 上显示空白。",{"id":845,"title":744,"titles":846,"content":847,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-4",[611,838],"微信小程序的 Canvas drawImage 方法不支持直接使用在线图片链接。与 H5 的 Canvas 不同，小程序要求图片必须是本地路径或临时文件路径才能正常绘制。 在线图片需要先通过 uni.getImageInfo 下载到本地，获取临时路径后才能用于 Canvas 绘制。",{"id":849,"title":749,"titles":850,"content":851,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#复现代码-2",[611,838],"\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":853,"title":754,"titles":854,"content":855,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-4",[611,838],"使用 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":857,"title":858,"titles":859,"content":860,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#封装工具函数","封装工具函数",[611,838],"建议封装一个通用的图片预加载函数，方便复用： \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":862,"title":759,"titles":863,"content":864,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-4",[611,838],"此问题仅在微信小程序中出现，H5 的 Canvas 可以直接使用在线图片链接uni.getImageInfo 会自动处理图片下载和缓存，重复调用相同链接会使用缓存对于需要绘制多张图片的场景，建议使用 Promise.all 并行预加载，提升性能如果图片链接需要鉴权或有防盗链，需要确保小程序已配置对应的下载域名白名单",{"id":866,"title":867,"titles":868,"content":73,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#坑-6微信小程序-picker-未选中项有默认背景颜色","坑 6：微信小程序 picker 未选中项有默认背景颜色",[611],{"id":870,"title":739,"titles":871,"content":872,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#现象-5",[611,867],"在微信小程序中使用 \u003Cpicker> 组件时，未选中的选项会显示默认的背景颜色（通常是灰色渐变遮罩），影响自定义 UI 风格的统一性。",{"id":874,"title":744,"titles":875,"content":876,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#原因-5",[611,867],"微信小程序的 picker 组件自带的遮罩层（mask）有默认的背景样式，用于区分选中项和未选中项。这个样式是组件内置的，普通的 CSS 选择器无法直接覆盖。",{"id":878,"title":754,"titles":879,"content":880,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#解决方案-5",[611,867],"通过 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":882,"title":883,"titles":884,"content":885,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#关键配置说明","关键配置说明",[611,867],"属性作用mask-class指定遮罩层的自定义 classmask-style内联样式，直接覆盖遮罩层样式 两个属性同时使用效果更可靠，mask-style 作为内联样式优先级更高，mask-class 配合 !important 作为兜底。",{"id":887,"title":759,"titles":888,"content":889,"level":85},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#注意事项-5",[611,867],"scoped 样式需使用 :deep() 穿透：由于 picker 的遮罩层是组件内部元素，需要使用 :deep() 来穿透 scoped 样式的限制此问题主要在微信小程序中出现，H5 端的 picker 样式表现可能不同如果需要自定义其他遮罩样式（如半透明黑色、模糊效果等），可以修改 background-color 和 background-image 的值",{"id":891,"title":892,"titles":893,"content":894,"level":79},"\u002Fblog\u002Funiapp-wechat-miniprogram-pitfalls#总结","总结",[611],"问题触发条件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":616,"title":615,"titles":896,"content":897,"level":68},[],"UniApp 深层组件里的 Popup、Modal 遮罩被父级 overflow\u002Fz-index 裁切或盖住时，用 RootPortal 传送到页面根节点解决；适用于 uview-plus 等弹层组件。 在 UniApp 项目中使用 uview-plus 等组件库时，经常会遇到一个问题：当 Popup 或 Modal 组件被封装在较深的组件层级中时，遮罩层会出现显示异常——要么遮罩层被父元素裁剪，要么遮罩层的层级不正确导致无法覆盖整个页面。",{"id":899,"title":900,"titles":901,"content":902,"level":79},"\u002Fblog\u002Funiapp-root-portal#问题复现","问题复现",[615],"假设你有一个深层嵌套的组件结构： \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":904,"title":905,"titles":906,"content":907,"level":79},"\u002Fblog\u002Funiapp-root-portal#解决方案rootportal-组件","解决方案：RootPortal 组件",[615],"参考 wot-design-uni 的实现思路，我们可以创建一个 RootPortal 组件，将子元素传送到页面根节点，从而摆脱父元素的层级和样式限制。",{"id":909,"title":910,"titles":911,"content":912,"level":85},"\u002Fblog\u002Funiapp-root-portal#组件实现","组件实现",[615,905],"\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":914,"title":915,"titles":916,"content":917,"level":85},"\u002Fblog\u002Funiapp-root-portal#各平台实现原理","各平台实现原理",[615,905],"平台实现方式说明H5\u003Cteleport to=\"body\">Vue3 内置的传送门组件，将内容渲染到 body 节点微信\u002F支付宝小程序\u003Croot-portal>小程序原生组件，将内容渲染到页面根节点APPrenderjs DOM 操作通过 renderjs 直接操作 DOM，将元素移动到 uni-app 根节点",{"id":919,"title":883,"titles":920,"content":921,"level":85},"\u002Fblog\u002Funiapp-root-portal#关键配置说明",[615,905],"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":923,"title":593,"titles":924,"content":73,"level":79},"\u002Fblog\u002Funiapp-root-portal#使用方式",[615],{"id":926,"title":927,"titles":928,"content":929,"level":85},"\u002Fblog\u002Funiapp-root-portal#基础用法","基础用法",[615,593],"\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":931,"title":932,"titles":933,"content":934,"level":85},"\u002Fblog\u002Funiapp-root-portal#封装业务组件","封装业务组件",[615,593],"如果你有一个需要在多处使用的弹窗组件，可以在内部直接集成 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":936,"title":759,"titles":937,"content":73,"level":79},"\u002Fblog\u002Funiapp-root-portal#注意事项",[615],{"id":939,"title":940,"titles":941,"content":942,"level":85},"\u002Fblog\u002Funiapp-root-portal#_1-钉钉小程序不支持-root-portal","1. 钉钉小程序不支持 root-portal",[615,759],"代码中通过 #ifndef MP-DINGTALK 排除了钉钉小程序，因为钉钉小程序不支持 root-portal 组件。如果你的项目需要支持钉钉小程序，弹窗可能仍会有层级问题，需要另寻解决方案（如调整组件结构、避免深层嵌套）。",{"id":944,"title":945,"titles":946,"content":947,"level":85},"\u002Fblog\u002Funiapp-root-portal#_2-app-端的-renderjs-限制","2. APP 端的 renderjs 限制",[615,759],"在 APP 端使用 renderjs 操作 DOM 时，需要注意： renderjs 只能访问 DOM，无法直接访问 Vue 实例的数据beforeDestroy 钩子确保组件销毁时清理 DOM，避免内存泄漏如果弹窗内有复杂的交互逻辑，可能需要额外处理",{"id":949,"title":950,"titles":951,"content":952,"level":85},"\u002Fblog\u002Funiapp-root-portal#_3-样式穿透","3. 样式穿透",[615,759],"由于内容被传送到了根节点，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":954,"title":955,"titles":956,"content":957,"level":85},"\u002Fblog\u002Funiapp-root-portal#_4-事件冒泡","4. 事件冒泡",[615,759],"传送到根节点后，事件冒泡的路径会改变。如果你依赖事件冒泡来关闭弹窗或处理其他逻辑，需要确认逻辑是否仍然正确。",{"id":959,"title":892,"titles":960,"content":961,"level":79},"\u002Fblog\u002Funiapp-root-portal#总结",[615],"场景问题解决方案Popup 被父元素裁剪overflow: hidden 导致弹窗不完整使用 RootPortal 传送到根节点遮罩层级不正确遮罩无法覆盖整个页面使用 RootPortal 传送到根节点多层嵌套组件中的弹窗层级混乱，样式异常使用 RootPortal 传送到根节点 RootPortal 组件通过条件编译实现了跨平台兼容，是解决 UniApp 中弹层组件层级问题的通用方案。将它集成到你的项目中，可以让 Popup、Modal、Toast 等弹层组件在任何嵌套深度下都能正确显示。",{"id":963,"title":964,"titles":965,"content":966,"level":79},"\u002Fblog\u002Funiapp-root-portal#参考","参考",[615],"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":620,"title":619,"titles":968,"content":969,"level":68},[],"Apifox 官方未写清 VS Code Copilot 的 MCP 接入步骤。本文给出 mcp.json 配置、鉴权与验证方法，让 Copilot 直接读取 Apifox API 文档。",{"id":971,"title":972,"titles":973,"content":974,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#前言","前言",[619],"MCP（Model Context Protocol） 是一种让 AI 助手能够访问外部数据源的协议。Apifox 已经支持 MCP，这意味着我们可以让 GitHub Copilot 直接读取 Apifox 中的 API 文档，在编码时获得更精准的 API 相关建议。 然而，Apifox 的 MCP 官方文档主要介绍了如何在 Claude Desktop 等客户端中配置，并没有说明如何在 VS Code 的 Copilot 中使用。本文将详细介绍完整的配置流程。",{"id":976,"title":377,"titles":977,"content":978,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#前置条件",[619],"在开始之前，请确保你已具备以下条件： ✅ 已安装 VS Code✅ 已安装 GitHub Copilot 扩展✅ 拥有 Apifox 账号并有项目访问权限✅ 已安装 Node.js（用于运行 npx 命令）",{"id":980,"title":981,"titles":982,"content":73,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#配置步骤","配置步骤",[619],{"id":984,"title":985,"titles":986,"content":987,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第一步获取-apifox-项目-id","第一步：获取 Apifox 项目 ID",[619,981],"登录 Apifox进入你想要接入的项目在浏览器地址栏中找到项目 ID，格式类似：https:\u002F\u002Fapp.apifox.com\u002Fproject\u002F123456，其中 123456 就是项目 ID",{"id":989,"title":990,"titles":991,"content":992,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第二步获取-apifox-api-访问令牌","第二步：获取 Apifox API 访问令牌",[619,981],"点击 Apifox 右上角头像，选择「账号设置」进入「API 访问令牌」页面点击「新建令牌」，填写名称并创建复制并妥善保存生成的令牌（只显示一次！）",{"id":994,"title":995,"titles":996,"content":997,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第三步在-vs-code-中配置-mcp","第三步：在 VS Code 中配置 MCP",[619,981],"这是本文的核心步骤，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":999,"title":1000,"titles":1001,"content":1002,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第四步完善-mcp-配置","第四步：完善 MCP 配置",[619,981],"修改 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":1004,"title":1005,"titles":1006,"content":1007,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第五步启动-mcp-服务器","第五步：启动 MCP 服务器",[619,981],"配置完成后，点击服务器名称（apifox）上方的 启动 按钮即可启动 MCP 服务器。",{"id":1009,"title":1010,"titles":1011,"content":1012,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#配置项说明","配置项说明",[619,981],"配置项说明type通信类型，stdio 表示通过标准输入输出通信command执行命令，使用 npx 运行 MCP 服务器args命令参数，-y 表示自动确认安装，--project 指定项目 IDenv环境变量，存放 API 访问令牌",{"id":1014,"title":1015,"titles":1016,"content":1017,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#第六步验证配置","第六步：验证配置",[619,981],"配置完成后，重启 VS Code。你可以通过以下方式验证 MCP 是否正常工作： 打开 Copilot Chat（Ctrl + Shift + I）切换到 Agent 模式尝试询问关于你项目 API 的问题，例如：「列出所有用户相关的 API 接口」 如果配置正确，Copilot 将能够读取你的 Apifox 项目文档并给出准确的回答。",{"id":1019,"title":1020,"titles":1021,"content":1022,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#完整配置示例","完整配置示例",[619],"以下是一个完整的 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":1024,"title":1025,"titles":1026,"content":73,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#常见问题","常见问题",[619],{"id":1028,"title":1029,"titles":1030,"content":1031,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q1-提示找不到-npx-命令","Q1: 提示找不到 npx 命令？",[619,1025],"确保已正确安装 Node.js，并且 npx 命令在系统 PATH 中。可以在终端运行 npx -v 验证。",{"id":1033,"title":1034,"titles":1035,"content":1036,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q2-mcp-服务器启动失败","Q2: MCP 服务器启动失败？",[619,1025],"检查以下几点： 项目 ID 是否正确API 访问令牌是否有效网络是否能访问 Apifox 服务",{"id":1038,"title":1039,"titles":1040,"content":1041,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q3-copilot-没有使用-mcp-数据","Q3: Copilot 没有使用 MCP 数据？",[619,1025],"确保在 Copilot Chat 中切换到 Agent 模式，而非普通的 Chat 模式。",{"id":1043,"title":1044,"titles":1045,"content":1046,"level":85},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#q4-如何配置多个项目","Q4: 如何配置多个项目？",[619,1025],"可以添加多个 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":1048,"title":588,"titles":1049,"content":1050,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#使用场景",[619],"配置完成后，你可以在开发中这样使用： 🔍 查询 API：「帮我找到用户登录的接口」📝 生成代码：「根据 API 文档生成用户注册的请求函数」📖 理解接口：「解释一下订单列表接口的返回数据结构」🔧 调试辅助：「这个接口返回 400 错误可能是什么原因」",{"id":1052,"title":892,"titles":1053,"content":1054,"level":79},"\u002Fblog\u002Fvscode-copilot-apifox-mcp#总结",[619],"通过本文的配置，你可以让 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":624,"title":623,"titles":1056,"content":1057,"level":68},[],"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":1059,"title":1060,"titles":1061,"content":1062,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#问题现象","问题现象",[623],"在 CI\u002FCD 环境执行 H5 打包后，页面上原本应该渲染为分页列表的区域，变成了浏览器无法识别的 \u003Cz-paging> 自定义标签，内容完全不显示。 本地开发环境（pnpm dev）和本地手动打包（pnpm build）均正常，仅在 CI\u002FCD 流水线中打包的产物出现此问题。",{"id":1064,"title":1065,"titles":1066,"content":1067,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#根本原因","根本原因",[623],"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":1069,"title":1070,"titles":1071,"content":1072,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#复现条件","复现条件",[623],"使用 UniApp + Vite 构建 H5pages.json 中配置了 z-paging 的 easycom 规则同时安装并启用了 @uni-helper\u002Fvite-plugin-uni-components在 CI\u002FCD 流水线（如 GitHub Actions、Jenkins 等）中执行打包",{"id":1074,"title":754,"titles":1075,"content":1076,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#解决方案",[623],"两步操作，缺一不可：",{"id":1078,"title":1079,"titles":1080,"content":1081,"level":85},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#第一步删除-easycom-中的-z-paging-配置","第一步：删除 easycom 中的 z-paging 配置",[623,754],"移除 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":1083,"title":1084,"titles":1085,"content":1086,"level":85},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#第二步更新-uni-helpervite-plugin-uni-components","第二步：更新 @uni-helper\u002Fvite-plugin-uni-components",[623,754],"将 @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":1088,"title":1089,"titles":1090,"content":1091,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#验证","验证",[623],"修改完成后，在 CI\u002FCD 中重新触发打包，检查产物是否正确渲染 z-paging 组件。 也可以在本地模拟 CI 环境（清除缓存后重新安装依赖并打包）来提前验证： # 清除依赖缓存，模拟 CI 环境\nrm -rf node_modules\npnpm install --frozen-lockfile\npnpm build:h5",{"id":1093,"title":1094,"titles":1095,"content":1096,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#测试环境","测试环境",[623],"以下为问题复现及修复验证时使用的依赖版本： 依赖版本UniApp (Vue 3)3.0.0-alpha-4080720251125001z-paging2.8.8@uni-helper\u002Fvite-plugin-uni-components0.3.2HBuilderX4.87pnpm9.xNode.js20.x",{"id":1098,"title":892,"titles":1099,"content":1100,"level":79},"\u002Fblog\u002Funiapp-z-paging-easycom-conflict#总结",[623],"项目说明触发条件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":628,"title":627,"titles":1102,"content":1103,"level":68},[],"Unibest 默认安装后执行 build:app，产物 manifest.json 丢失 pages.config.ts 的 tabBar 等配置，导致 wgt 热更新安装后 App 白屏。本文说明原因与修复步骤。 在使用 Unibest 框架做 App 项目时，CI\u002FCD 流水线打出的 wgt 热更新包安装后，App 直接白屏。本地开发时一切正常，只有在干净环境（git clone + pnpm install + build:app）下才必现。",{"id":1105,"title":1106,"titles":1107,"content":1108,"level":79},"\u002Fblog\u002Funibest-manifest-missing-config#问题背景","问题背景",[627],"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":1110,"title":1111,"titles":1112,"content":1113,"level":79},"\u002Fblog\u002Funibest-manifest-missing-config#问题根因","问题根因",[627],"冷 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":1115,"title":1116,"titles":1117,"content":1118,"level":79},"\u002Fblog\u002Funibest-manifest-missing-config#复现步骤","复现步骤",[627],"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":1120,"title":754,"titles":1121,"content":73,"level":79},"\u002Fblog\u002Funibest-manifest-missing-config#解决方案",[627],{"id":1123,"title":1124,"titles":1125,"content":1126,"level":85},"\u002Fblog\u002Funibest-manifest-missing-config#方案一推荐prebuild-脚本初始化-pagesjson不改-gitignore","方案一（推荐）：prebuild 脚本初始化 pages.json（不改 .gitignore）",[627,754],"在 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":1128,"title":1129,"titles":1130,"content":1131,"level":85},"\u002Fblog\u002Funibest-manifest-missing-config#方案二简单粗暴把两个文件从-gitignore-移除","方案二（简单粗暴）：把两个文件从 .gitignore 移除",[627,754],"直接把 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":1133,"title":1134,"titles":1135,"content":1136,"level":79},"\u002Fblog\u002Funibest-manifest-missing-config#对比","对比",[627],"方案一（prebuild 脚本）方案二（移除 .gitignore）实现成本需要新增脚本文件改一行 .gitignore 即可Git 噪声无每次 dev 都会产生 diff适合场景CI\u002FCD 自动化、多人协作项目小型项目或个人项目长期维护推荐不推荐",{"id":1138,"title":892,"titles":1139,"content":1140,"level":79},"\u002Fblog\u002Funibest-manifest-missing-config#总结",[627],"这个问题的本质是冷构建时的竞态：@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":632,"title":631,"titles":1142,"content":1143,"level":68},[],"根据 Apifox OpenAPI 文档，用 openapi-ts-request 自动生成 TypeScript 请求函数与类型，减少手写 API 封装。 在现代前端开发中，与后端 API 的对接是一项重要且重复性高的工作。为了提高开发效率并减少手动编写 API 请求代码的工作量，我们可以使用 openapi-ts-request 工具，根据 Apifox 文档自动生成 TypeScript 类型安全的 API 请求文件。",{"id":1145,"title":1146,"titles":1147,"content":1148,"level":79},"\u002Fblog\u002Fapifox-api-generation#什么是-openapi-ts-request","什么是 openapi-ts-request？",[631],"openapi-ts-request 是一个基于 OpenAPI 规范的代码生成工具，它可以从 API 文档自动生成类型定义和请求函数。通过与 Apifox 平台集成，我们可以直接从项目文档生成完整的 API 客户端代码。",{"id":1150,"title":1151,"titles":1152,"content":1153,"level":79},"\u002Fblog\u002Fapifox-api-generation#配置说明","配置说明",[631],"首先，在项目根目录下创建配置文件，例如 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":1155,"title":1156,"titles":1157,"content":1158,"level":85},"\u002Fblog\u002Fapifox-api-generation#配置项详解","配置项详解",[631,1151],"配置项说明serversPath生成文件的存放路径requestLibPath自定义请求库的引入路径requestOptionsType请求选项的类型定义reactQueryMode设置为 'vue' 以适配 Vue 项目enableLogging是否启用日志输出isGenJavaScript是否生成 JavaScript 代码（false 表示生成 TypeScript）apifoxConfigApifox 项目配置",{"id":1160,"title":1161,"titles":1162,"content":1163,"level":79},"\u002Fblog\u002Fapifox-api-generation#生成的文件结构","生成的文件结构",[631],"运行命令后，工具会在指定位置自动生成以下文件： src\u002F\n└── api\u002F\n    └── openApiRequest\u002F\n        ├── common.ts\n        ├── index.ts\n        ├── types.ts\n        └── v1.ts",{"id":1165,"title":1166,"titles":1167,"content":1168,"level":79},"\u002Fblog\u002Fapifox-api-generation#生成代码示例","生成代码示例",[631],"生成的 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":1170,"title":1171,"titles":1172,"content":1173,"level":79},"\u002Fblog\u002Fapifox-api-generation#优势特点","优势特点",[631],"类型安全：自动生成 TypeScript 类型定义，编译时捕获错误减少重复工作：无需手动编写 API 请求函数与 Apifox 集成：直接从最新的 API 文档生成代码可定制化：通过 hooks 自定义函数命名规则Vue 生态适配：支持 Vue 项目中的请求模式",{"id":1175,"title":1176,"titles":1177,"content":1178,"level":79},"\u002Fblog\u002Fapifox-api-generation#最佳实践","最佳实践",[631],"将生成的 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":636,"title":635,"titles":1180,"content":1181,"level":68},[],"uni.uploadFile \u002F uni.upload 一次只能传一个文件时，在 H5 用 fetch + FormData 一次提交多个文件的实现方式与注意点。 在使用 uni-app 开发过程中，我们经常 会遇到文件上传的需求。然而，uni.uploadFile 方法存在一个限制：它无法在一个请求中上传多个文件。本文将介绍如何在 H5 环境下使用 fetch + FormData 来解决这个问题。",{"id":1183,"title":1184,"titles":1185,"content":1186,"level":79},"\u002Fblog\u002Ffile-upload-issue-solution#问题描述","问题描述",[635],"在开发中，我们常常需要一次性上传多个文件，例如： 上传多张图片作为产品展示上传多个文档文件同时上传头像和背景图 但是，uni-app 提供的 uni.uploadFile API 只能上传单个文件，即使多次调用也可能导致后端处理复杂。",{"id":1188,"title":1189,"titles":1190,"content":1191,"level":79},"\u002Fblog\u002Ffile-upload-issue-solution#解决方案使用-fetch-formdata","解决方案：使用 fetch + FormData",[635],"在 H5 环境中，我们可以使用原生的 fetch 和 FormData 来实现多文件上传。",{"id":1193,"title":1194,"titles":1195,"content":1196,"level":85},"\u002Fblog\u002Ffile-upload-issue-solution#_1-基础实现","1. 基础实现",[635,1189],"\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":1198,"title":1199,"titles":1200,"content":1201,"level":85},"\u002Fblog\u002Ffile-upload-issue-solution#_2-在-vue-组件中的使用示例","2. 在 Vue 组件中的使用示例",[635,1189],"\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":1203,"title":1204,"titles":1205,"content":1206,"level":85},"\u002Fblog\u002Ffile-upload-issue-solution#_3-处理上传进度","3. 处理上传进度",[635,1189],"\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":1208,"title":759,"titles":1209,"content":73,"level":79},"\u002Fblog\u002Ffile-upload-issue-solution#注意事项",[635],{"id":1211,"title":1212,"titles":1213,"content":1214,"level":85},"\u002Fblog\u002Ffile-upload-issue-solution#_1-环境限制","1. 环境限制",[635,759],"此解决方案仅适用于 H5 环境，在小程序和 App 环境中无法使用需要检测当前运行环境，避免在非 H5 环境下执行",{"id":1216,"title":1217,"titles":1218,"content":1219,"level":85},"\u002Fblog\u002Ffile-upload-issue-solution#_2-fetch-响应处理","2. Fetch 响应处理",[635,759],"使用 fetch 时，需要先将响应转换为 JSON 格式在 then 中先进行 response.json() 处理，然后再进行后续操作",{"id":1221,"title":1222,"titles":1223,"content":1224,"level":85},"\u002Fblog\u002Ffile-upload-issue-solution#_3-formdata-使用","3. FormData 使用",[635,759],"FormData 允许我们轻松地构建表单数据可以通过 append() 方法添加多个同名字段，后端会接收到文件数组",{"id":1226,"title":892,"titles":1227,"content":1228,"level":79},"\u002Fblog\u002Ffile-upload-issue-solution#总结",[635],"虽然 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":640,"title":639,"titles":1230,"content":1231,"level":68},[],"WangEditor 会过滤 \u003Cdiv>\u003Cimg>\u003C\u002Fdiv> 结构，导致第三方商品详情图片丢失。本文给出自定义解析\u002F保留图片的处理方式。",{"id":1233,"title":1106,"titles":1234,"content":1235,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#问题背景",[639],"在实际项目开发过程中，我们经常会遇到需要从第三方接口导入商品详情的场景。这些商品详情通常是由富文本编辑器生成的内容，其中可能包含各种HTML标签和结构。然而，在使用Wangeditor时，我们发现了一个常见问题：当图片(img标签)被div标签包裹时，Wangeditor会将其过滤掉，导致图片无法正常显示。 这个问题在处理第三方接口导入的数据时尤为突出，因为这些数据的HTML结构是我们无法控制的。 值得注意的是，这是一个已知的社区问题，在 wangEditor 的官方 GitHub 仓库中有相关的 issue 讨论：Issue #5880，且该问题至今仍未被修复。",{"id":1237,"title":1238,"titles":1239,"content":1240,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#问题分析","问题分析",[639],"Wangeditor默认会过滤掉一些可能不安全的HTML标签和属性，这是出于安全考虑的设计。然而，这种过滤机制有时会过于严格，导致合法的HTML结构（如被div包裹的img标签）也被过滤掉。 具体表现为： 从第三方接口获取的商品详情中包含\u003Cdiv>\u003Cimg src=\"...\" alt=\"...\"\u002F>\u003C\u002Fdiv>结构在Wangeditor中显示时，img标签被移除最终用户无法看到图片内容",{"id":1242,"title":1243,"titles":1244,"content":1245,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#去试试","去试试",[639],"你可以通过下面的在线编辑器链接来复现这个问题： 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":1247,"title":754,"titles":1248,"content":1249,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#解决方案",[639],"经过深入研究和实践，我们找到了一个简单有效的解决方案：将div标签替换为p标签。",{"id":1251,"title":1252,"titles":1253,"content":1254,"level":85},"\u002Fblog\u002Fwangeditor-img-filter-issue#修改前的代码结构","修改前的代码结构：",[639,754],"\u003Cdiv>\n  \u003Cimg src=\"https:\u002F\u002Fimg3.redocn.com\u002F20110418\u002F20110416_6ad206b20544a083fdb0B6Kj0dud4sro.jpg\" alt=\"\" \u002F>\n\u003C\u002Fdiv>",{"id":1256,"title":1257,"titles":1258,"content":1259,"level":85},"\u002Fblog\u002Fwangeditor-img-filter-issue#修改后的代码结构","修改后的代码结构：",[639,754],"\u003Cp>\n  \u003Cimg src=\"https:\u002F\u002Fimg3.redocn.com\u002F20110418\u002F20110416_6ad206b20544a083fdb0B6Kj0dud4sro.jpg\" alt=\"\" \u002F>\n\u003C\u002Fp>",{"id":1261,"title":1262,"titles":1263,"content":73,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#实现步骤","实现步骤",[639],{"id":1265,"title":1266,"titles":1267,"content":1268,"level":85},"\u002Fblog\u002Fwangeditor-img-filter-issue#_1-数据预处理","1. 数据预处理",[639,1262],"在将第三方接口获取的数据加载到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":1270,"title":1271,"titles":1272,"content":1273,"level":85},"\u002Fblog\u002Fwangeditor-img-filter-issue#_2-针对多种html结构的处理","2. 针对多种HTML结构的处理",[639,1262],"在实际应用中，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":1275,"title":759,"titles":1276,"content":1277,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#注意事项",[639],"样式适配：将div替换为p标签后，原有的CSS样式可能需要调整，因为p标签和div标签的默认样式可能不同。语义化考虑：从HTML语义化的角度看，p标签是用于段落的，如果div包含的不仅仅是图片，而是混合内容，需要谨慎使用此方法。测试验证：在应用此解决方案后，需要充分测试以确保没有引入其他问题。",{"id":1279,"title":892,"titles":1280,"content":1281,"level":79},"\u002Fblog\u002Fwangeditor-img-filter-issue#总结",[639],"通过将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":644,"title":643,"titles":1283,"content":1284,"level":68},[],"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":648,"title":647,"titles":1286,"content":1287,"level":68},[],"在 Nuxt 4 中安装配置 nuxt-echarts，渲染 ECharts 图表，并处理客户端-only、按需引入与常见报错。 在Nuxt项目中使用ECharts可以快速实现数据可视化。nuxt-echarts模块为Nuxt提供了开箱即用的ECharts集成方案，本文将介绍完整的集成步骤。",{"id":1289,"title":1290,"titles":1291,"content":1292,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#第一步安装nuxt-echarts模块","第一步：安装nuxt-echarts模块",[647],"使用Nuxt CLI快速添加echarts模块： npx nuxi module add echarts 该命令会自动将nuxt-echarts添加到项目依赖并在nuxt.config.ts中注册模块。",{"id":1294,"title":1295,"titles":1296,"content":1297,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#第二步配置所需的echarts模块必须","第二步：配置所需的ECharts模块（必须）",[647],"在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":1299,"title":1300,"titles":1301,"content":1302,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#第三步在页面中使用vchart组件","第三步：在页面中使用VChart组件",[647],"使用\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":1304,"title":1305,"titles":1306,"content":1307,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#第四步使用typescript类型定义","第四步：使用TypeScript类型定义",[647],"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":1309,"title":1310,"titles":1311,"content":1312,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#完整示例","完整示例",[647],"结合以上步骤的完整页面示例： \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":1314,"title":1025,"titles":1315,"content":73,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#常见问题",[647],{"id":1317,"title":1318,"titles":1319,"content":1320,"level":85},"\u002Fblog\u002Fnuxt-echarts-implementation#图表不显示或报错","图表不显示或报错？",[647,1025],"确认在nuxt.config.ts中声明了对应的图表类型和组件检查是否使用了\u003Cclient-only>包裹确保为图表容器设置了明确的高度",{"id":1322,"title":1323,"titles":1324,"content":1325,"level":85},"\u002Fblog\u002Fnuxt-echarts-implementation#如何响应式调整图表大小","如何响应式调整图表大小？",[647,1025],"\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":1327,"title":1328,"titles":1329,"content":1330,"level":79},"\u002Fblog\u002Fnuxt-echarts-implementation#参考资源","参考资源",[647],"官方文档：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":652,"title":651,"titles":1332,"content":1333,"level":68},[],"UniApp Vite 项目接入 Reown（AppKit）与 Wagmi 时的打包报错、浏览器 API 兼容、RPC 访问失败等常见问题与对应解决办法。 在 UniApp+Vite 项目中集成 Reown 和 Wagmi 实现 Web3 钱包连接时，会遇到一系列打包、兼容性和网络问题。本文总结了实际开发中遇到的问题及对应的解决方案。",{"id":1335,"title":1336,"titles":1337,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-1wagmi-打包后出现异常文件名","问题 1：Wagmi 打包后出现异常文件名",[651],{"id":1339,"title":739,"titles":1340,"content":1341,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象",[651,1336],"Wagmi 在本地开发环境运行一切正常，但执行 build 打包后，生成的文件中出现以 ..node 开头的文件，导致引入时报错。",{"id":1343,"title":754,"titles":1344,"content":1345,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案",[651,1336],"在 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":1347,"title":1348,"titles":1349,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-2插件要求-es2020-编译目标","问题 2：插件要求 ES2020 编译目标",[651],{"id":1351,"title":739,"titles":1352,"content":1353,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-1",[651,1348],"Reown 和 Wagmi 插件在较低的编译目标下无法正常工作。",{"id":1355,"title":754,"titles":1356,"content":1357,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-1",[651,1348],"在 vite.config.ts 中设置编译目标为 es2020： export default defineConfig({\n  build: {\n    target: 'es2020',\n  },\n})",{"id":1359,"title":1360,"titles":1361,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-3低版本手机无法连接钱包一直转圈","问题 3：低版本手机无法连接钱包（一直转圈）",[651],{"id":1363,"title":739,"titles":1364,"content":1365,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-2",[651,1360],"在部分低版本 Android\u002FiOS 设备上，Reown 连接钱包时一直显示加载状态，无法完成连接。",{"id":1367,"title":744,"titles":1368,"content":1369,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#原因",[651,1360],"低版本手机浏览器缺少现代 JavaScript API 的 polyfill 支持。",{"id":1371,"title":754,"titles":1372,"content":1373,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-2",[651,1360],"安装 core-js 并在项目入口引入完整的 polyfill： pnpm add core-js 在 main.ts 或 app.ts 入口文件顶部添加： import 'core-js\u002Fstable' 这将为低版本浏览器提供完整的 ES6+ API polyfill 支持。",{"id":1375,"title":1376,"titles":1377,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-4写入合约后无法推送到区块链","问题 4：写入合约后无法推送到区块链",[651],{"id":1379,"title":739,"titles":1380,"content":1381,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-3",[651,1376],"部分手机在执行写入合约操作时，虽然获取到了交易 hash，但交易始终无法推送到区块链网络，停留在 pending 状态。",{"id":1383,"title":744,"titles":1384,"content":1385,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#原因-1",[651,1376],"Gas 费用估算不准确或未正确设置导致交易无法被矿工打包。",{"id":1387,"title":754,"titles":1388,"content":1389,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-3",[651,1376],"手动获取和设置 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":1391,"title":1392,"titles":1393,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-5reown-默认-rpc-国内无法访问","问题 5：Reown 默认 RPC 国内无法访问",[651],{"id":1395,"title":739,"titles":1396,"content":1397,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-4",[651,1392],"Reown 默认使用的 RPC 节点在国内网络环境下无法访问，导致无法连接区块链网络。",{"id":1399,"title":754,"titles":1400,"content":1401,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-4",[651,1392],"在初始化 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":1403,"title":1404,"titles":1405,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-6typescript-报错实例化过深","问题 6：TypeScript 报错\"实例化过深\"",[651],{"id":1407,"title":739,"titles":1408,"content":1409,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-5",[651,1404],"在读写合约方法时，TypeScript 提示 \"Type instantiation is excessively deep and possibly infinite\"。",{"id":1411,"title":744,"titles":1412,"content":1413,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#原因-2",[651,1404],"完整的合约 ABI 文件过大，TypeScript 在进行类型推断时超出递归深度限制。",{"id":1415,"title":754,"titles":1416,"content":1417,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-5",[651,1404],"不要存储完整的 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":1419,"title":1420,"titles":1421,"content":73,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#问题-7浮点数计算精度损失","问题 7：浮点数计算精度损失",[651],{"id":1423,"title":739,"titles":1424,"content":1425,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#现象-6",[651,1420],"在处理 Token 金额、价格计算等涉及浮点数的场景时，JavaScript 原生运算会导致精度损失。",{"id":1427,"title":754,"titles":1428,"content":1429,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#解决方案-6",[651,1420],"使用专门的大数库处理计算和判断： 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":1431,"title":1432,"titles":1433,"content":1434,"level":85},"\u002Fblog\u002Freown-wagmi-uniapp-issues#金额转换","金额转换",[651,1420],"使用 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":1436,"title":892,"titles":1437,"content":1438,"level":79},"\u002Fblog\u002Freown-wagmi-uniapp-issues#总结",[651],"集成 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":656,"title":655,"titles":1440,"content":1441,"level":68},[],"用 Vue 3 Composition API 与 requestAnimationFrame 实现数字滚动（CountUp）组件，覆盖取消动画、内存泄漏与性能优化。",{"id":1443,"title":1444,"titles":1445,"content":1446,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#概述","概述",[655],"数字滚动动画常见于数据大屏、计数器、统计面板。本文按「需求 -> 原理 -> 实现 -> 性能 -> 实战」的顺序，带你完成一个高性能、可复用、内存友好的 Vue 3 数字滚动组件。",{"id":1448,"title":1449,"titles":1450,"content":1451,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#先明确目标","先明确目标",[655],"设计一个数字滚动组件，要求： 📊 支持整数、小数、负数⚡ 动画流畅，视觉反馈自然🎯 高性能，避免频繁更新时卡顿🔄 内存安全，杜绝定时器堆积与泄漏",{"id":1453,"title":1454,"titles":1455,"content":1456,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#实现原理","实现原理",[655],"数字滚动的本质是「逐位滚动」： 每一位数字都是 0-9 垂直排列通过 translateY 改变可视窗口中的数字数字 N 的偏移量是 N * -100% 例如显示数字 5： 0  ← translateY: 0%\n1\n2\n3\n4\n5  ← translateY: -500% (当前显示)\n6\n7\n8\n9 理解这个映射关系后，再看代码会非常直接。",{"id":1458,"title":1459,"titles":1460,"content":73,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#核心实现要点","核心实现要点",[655],{"id":1462,"title":1463,"titles":1464,"content":1465,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#_1-使用-transform-做位移动画","1. 使用 transform 做位移动画",[655,1459],"\u002F\u002F 修改 margin 或 top 会触发布局重排\nstyle.marginTop = value;\nstyle.top = value;\n\u002F\u002F transform 更容易走 GPU 合成层，动画更稳定\n:style=\"{ transform: `translateY(${value}%)` }\"",{"id":1467,"title":1468,"titles":1469,"content":1470,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#_2-用计算属性拆分字符并计算位移","2. 用计算属性拆分字符并计算位移",[655,1459],"\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":1472,"title":1473,"titles":1474,"content":1475,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#_3-管理定时器避免快速更新时堆积","3. 管理定时器，避免快速更新时堆积",[655,1459],"定时器未清理是常见内存泄漏来源，尤其在高频数据更新场景中。 \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":1477,"title":1478,"titles":1479,"content":1480,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#_4-用贝塞尔曲线优化体感","4. 用贝塞尔曲线优化体感",[655,1459],"\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":1482,"title":1483,"titles":1484,"content":1485,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#完整组件与特性","完整组件与特性",[655],"NumberScroll.vue：可复用的高性能数字滚动组件。 技术栈： Vue 3 Composition APITypeScriptTailwind CSS（无需 SCSS） 关键特性： ✅ 自动清理定时器，无内存泄漏✅ 支持小数点和负数✅ 使用 GPU 友好的 transform 动画✅ 快速更新时自动取消旧动画✅ 无外部依赖，样式可直接复用✅ 支持自定义动画时长",{"id":1487,"title":1488,"titles":1489,"content":1490,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#性能对比","性能对比",[655],"实现方式FPS内存泄漏响应性margin 修改30-45有⚠️ 中等position 修改45-55有⚠️ 中等transform55-60无✅ 优秀",{"id":1492,"title":1493,"titles":1494,"content":1495,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#在线演示","在线演示",[655],"完整交互式演示请访问 数字滚动动画Demos。 你可以在Demos中： 📊 实时观察滚动效果🎮 切换不同数据场景📈 对比不同实现策略🔧 体验组件配置项 前往Demos →",{"id":1497,"title":1025,"titles":1498,"content":73,"level":79},"\u002Fblog\u002Fnumber-scroll-animation#常见问题",[655],{"id":1500,"title":1501,"titles":1502,"content":1503,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#q1-如何处理负数","Q1: 如何处理负数？",[655,1025],"\u002F\u002F 可以在前面加符号\nconst displayValue = value \u003C 0 ? `-${Math.abs(value)}` : `${value}`;",{"id":1505,"title":1506,"titles":1507,"content":1508,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#q2-动画总是要-03s-吗","Q2: 动画总是要 0.3s 吗？",[655,1025],"\u002F\u002F 可以通过 duration prop 自定义\n\u003CNumberScroll :value=\"num\" :duration=\"500\" \u002F>",{"id":1510,"title":1511,"titles":1512,"content":1513,"level":85},"\u002Fblog\u002Fnumber-scroll-animation#q3-如何添加千位分隔符","Q3: 如何添加千位分隔符？",[655,1025],"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":660,"title":659,"titles":1515,"content":1516,"level":68},[],"nvm-windows 下载 Node.js 一直卡住时，不必改 settings.txt。用 nvm node_mirror \u002F npm_mirror 切淘宝镜像或代理即可。",{"id":1518,"title":1106,"titles":1519,"content":1520,"level":79},"\u002Fblog\u002Fnvm-install-solution#问题背景",[659],"刚换新电脑，安装好 nvm-windows 后，准备安装 Node.js 版本时却遇到了经典难题： 进度条一直卡在 \"Downloading node.js version...\"，无论是否开启梯子，最终都报错或无限等待。 网上大部分教程都让你去修改 nvm 安装目录下的 settings.txt 文件。其实完全不需要！ nvm 官方命令行本身就提供了修改镜像源和代理的功能，既安全又方便。",{"id":1522,"title":1523,"titles":1524,"content":1525,"level":79},"\u002Fblog\u002Fnvm-install-solution#解决方案使用-nvm-内置命令","解决方案：使用 nvm 内置命令",[659],"只需在终端（PowerShell 或 CMD）中执行两条命令，即可瞬间解决网络问题。",{"id":1527,"title":1528,"titles":1529,"content":1530,"level":85},"\u002Fblog\u002Fnvm-install-solution#第一步查看帮助可选","第一步：查看帮助（可选）",[659,1523],"首先确认你的 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":1532,"title":1533,"titles":1534,"content":1535,"level":85},"\u002Fblog\u002Fnvm-install-solution#第二步切换国内镜像源核心步骤","第二步：切换国内镜像源（核心步骤）",[659,1523],"将 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":1537,"title":1538,"titles":1539,"content":1540,"level":85},"\u002Fblog\u002Fnvm-install-solution#第三步配置代理如果你必须使用梯子","第三步：配置代理（如果你必须使用梯子）",[659,1523],"如果你所在的网络环境必须通过代理才能访问外网，可以设置代理地址。以 Clash Verge 为例，默认 HTTP 端口通常为 7897，请根据你实际软件的端口调整。 nvm proxy http:\u002F\u002F127.0.0.1:7897 注意：如果你已经设置了淘宝镜像（第二步），通常不需要再设置代理，直接下载即可。如果设置代理后反而报错，请尝试运行 nvm proxy none 关闭代理，仅使用淘宝镜像。",{"id":1542,"title":1543,"titles":1544,"content":1545,"level":79},"\u002Fblog\u002Fnvm-install-solution#最终结果","最终结果",[659],"配置完成后，再次运行安装命令： nvm install 20  # 或者你要安装的具体版本，如 18, 22 等 下载速度瞬间跑满带宽，不再卡住！",{"id":1547,"title":1548,"titles":1549,"content":1550,"level":79},"\u002Fblog\u002Fnvm-install-solution#常用命令速查","常用命令速查",[659],"命令说明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":1552,"title":892,"titles":1553,"content":1554,"level":79},"\u002Fblog\u002Fnvm-install-solution#总结",[659],"遇到 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);}",{"id":1556,"title":1557,"body":1558,"description":1559,"extension":1560,"meta":1561,"navigation":1562,"path":603,"seo":1564,"stem":604,"__hash__":1567},"blog\u002F3.blog.yml","技术博客",null,"UniApp、Nuxt、Vue 实战踩坑与报错修复笔记（z-paging、RootPortal、payload.json、Wagmi 等）","yml",{},{"icon":1563},"i-lucide-smile",{"title":1565,"description":1566},"技术博客 | qibmz - UniApp \u002F Nuxt \u002F Vue 踩坑与报错修复","qibmz 技术博客：UniApp 跨端、Nuxt\u002FVue、WangEditor、Wagmi 等实战问题与报错原文级解决方案，方便按错误信息检索。","znysTqVxYgp79YdNhlq53hiFT6lGWqu4Gac6XopbE40",14,[1570,1582,1591,1601,1611,1620,1627],{"path":628,"title":627,"description":1571,"image":1572,"date":1574,"authors":1575,"badge":1580},"Unibest 默认安装后执行 build:app，产物 manifest.json 丢失 pages.config.ts 的 tabBar 等配置，导致 wgt 热更新安装后 App 白屏。本文说明原因与修复步骤。",{"src":1573},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F0\u002F640\u002F360","2026-07-29",[1576],{"name":1577,"avatar":1578},"qibmz",{"src":1579},"\u002Fimage\u002Favatar.avif",{"label":1581},"踩坑",{"path":624,"title":623,"description":1583,"image":1584,"date":1586,"authors":1587,"badge":1590},"UniApp 配置 z-paging easycom 后与 @uni-helper\u002Fvite-plugin-uni-components 冲突，CICD 打包 H5 构建成功但页面组件渲染为原始标签。本文给出冲突原因与修复方式。",{"src":1585},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F48\u002F640\u002F360","2026-07-27",[1588],{"name":1577,"avatar":1589},{"src":1579},{"label":1581},{"path":620,"title":619,"description":969,"image":1592,"date":1594,"authors":1595,"badge":1599},{"src":1593},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F60\u002F640\u002F360","2026-05-19",[1596],{"name":1577,"to":1597,"avatar":1598},"https:\u002F\u002Fgithub.com\u002Fqibmz",{"src":1579},{"label":1600},"工具",{"path":616,"title":615,"description":1602,"image":1603,"date":1605,"authors":1606,"badge":1609},"UniApp 深层组件里的 Popup、Modal 遮罩被父级 overflow\u002Fz-index 裁切或盖住时，用 RootPortal 传送到页面根节点解决；适用于 uview-plus 等弹层组件。",{"src":1604},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F1\u002F640\u002F360","2026-04-22",[1607],{"name":1577,"avatar":1608},{"src":1579},{"label":1610},"UniApp",{"path":612,"title":611,"description":1612,"image":1613,"date":1615,"authors":1616,"badge":1619},"UniApp \u002F 微信小程序常见坑：uni.showModal 确认取消按钮文字字数限制、iOS 元素溢出视口、picker 时区偏差、以及未选中项背景色修改方法。",{"src":1614},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F2\u002F640\u002F360","2026-04-07",[1617],{"name":1577,"avatar":1618},{"src":1579},{"label":1581},{"path":660,"title":659,"description":1516,"image":1621,"date":1622,"authors":1623,"badge":1626},{"src":1573},"2026-03-19",[1624],{"name":1577,"to":1597,"avatar":1625},{"src":1579},{"label":1600},{"path":656,"title":655,"description":1441,"image":1628,"date":1630,"authors":1631,"badge":1634},{"src":1629},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F103\u002F640\u002F360","2026-02-05",[1632],{"name":1577,"avatar":1633},{"src":1579},{"label":1635},"Vue3",1786513827073]