-
用华为云码道构建 SoundScape:让音乐在浏览器中“看得见”一、概述1.1 案例介绍传统播放器只负责“播放声音”,SoundScape 则把声音变成实时画面:用户在浏览器中上传本地音频后,应用通过 Web Audio API 解码音频并持续采集频谱数据,再使用 Canvas 2D 绘制柱状、环形和球形三种可视化效果。低频能量、节拍变化和全频强度还会驱动粒子大小、速度、色相与画面节奏,实现音画同步的沉浸式体验。本案例以华为云码道(CodeArts)代码智能体为辅助开发工具,围绕“需求梳理、架构设计、核心引擎实现、页面联调、运行验收和生产构建”完成一个 Vue 3 单页应用。项目全部在浏览器端处理音频,不需要后端服务,也不会主动上传用户的音频文件。案例技术选型如下:华为云码道(CodeArts)代码智能体:辅助分析需求、理解代码、生成实现建议和排查问题。Vue 3 + Vite:构建组件化前端应用并提供开发、构建与预览能力。Web Audio API:完成音频解码、播放控制和实时频谱采集。Canvas 2D:完成频谱、拟 3D 球形点阵、粒子和氛围光效绘制。1.2 适用对象个人开发者高校学生Web 前端开发者对音频处理、数据可视化或创意编程感兴趣的开发者1.3 案例时间本案例总时长预计 90 分钟,不包含首次下载安装开发工具和依赖包所需时间。1.4 案例流程说明:使用码道梳理上传、播放、可视化、主题切换和截图等需求,形成验收标准;准备 Node.js 环境,安装项目锁定的 Vue 3 和 Vite 依赖;使用 AudioEngine 完成音频解码、播放状态管理和频谱数据采集;分析低频、中频、高频和整体强度,驱动频谱、粒子、色彩和节拍效果;联调文件上传、播放控制、模式切换、主题切换、拖拽旋转和截图下载;执行生产构建,检查 dist/ 静态文件并完成案例验收。1.5 资源总览本案例核心功能均使用浏览器原生能力,预计花费 0 元。如将构建产物部署到收费的云资源,请以控制台实时价格为准,并在体验结束后及时释放资源。资源名称规格或版本费用华为开发者空间云开发环境建议 2 vCPUs、4 GiB 及以上,Node.js 20.19+ 或 22.12+以页面展示为准华为云码道(CodeArts)代码智能体体验版或当前账号可用版本以页面展示为准Vue项目锁定版本 3.5.40免费Vite项目锁定版本 8.1.5免费Web Audio API、Canvas 2DChrome、Edge 等现代浏览器的原生能力免费二、环境和资源准备2.1 准备华为云码道开发环境登录华为开发者空间,按账号可用方式进入码道代码智能体或创建云开发环境。也可以在 Windows 本地安装码道 AI IDE 后打开项目目录 music-visualizer。在终端中检查 Node.js 和 npm:node --version npm --version Vite 8 要求使用 Node.js 20.19+ 或 22.12+。如果版本过低,请先切换到符合要求的 Node.js 版本,再安装依赖。2.2 安装项目依赖进入项目目录:cd music-visualizer npm cinpm ci 会严格按照 package-lock.json 安装依赖。安装完成后,可以执行以下命令核对顶层版本:npm list --depth=0 本案例验证时使用的顶层依赖如下:@vitejs/plugin-vue@6.0.8 vite@8.1.5 vue@3.5.402.3 使用码道上下文选择功能当希望码道只分析某个文件或目录时,可先在 AI IDE 中打开目标内容,再在对话框输入 #,选择 File 或 Folder 作为上下文。这样可以缩小分析范围,减少无关内容干扰。例如,选择 #Folder src/engine 后输入:请分析 #Folder src/engine 中音频数据到画面渲染的数据链路,重点检查: 1. AudioBufferSourceNode 的生命周期; 2. AnalyserNode 的参数是否合理; 3. requestAnimationFrame 渲染循环是否正确释放; 4. 节拍检测器和粒子池是否完整初始化。 请先给出问题清单,再给出最小修改建议,不要改动无关文件。本项目不依赖额外的项目级 skills 即可完成案例。如果团队已经配置研发流程或测试类 skills,也可以在需求拆分、代码审查和测试设计阶段按需启用。三、对话码道:构建 SoundScape 在线音乐可视化器3.1 明确项目需求与验收标准在编码前,先向码道描述产品目标和约束:请为一个浏览器端在线音乐可视化器整理需求和验收标准。 约束:Vue 3 + Vite;音频只在本地浏览器处理;不使用后端;使用Web Audio API采集频谱;使用Canvas 2D绘图。 功能:点击或拖拽上传音频、播放/暂停、进度跳转、三种可视化模式、四种主题、粒子效果、画面截图。 请按“功能需求、非功能需求、验收用例”输出,并标出浏览器兼容性和资源释放风险。根据当前项目实现,核心需求如下:编号需求验收标准FR-01本地音频加载可点击或拖拽选择浏览器支持解码的音频文件;界面显示歌曲文件名FR-02播放控制可播放、暂停,并显示当前时间和总时长FR-03进度跳转点击进度条后从目标位置继续播放FR-04多模式可视化可在柱状、环形和球形三种模式间切换FR-05多主题显示可在霓虹、星空、火焰和极光四种主题间切换FR-06音频驱动画面低、中、高频和整体强度能够影响色相、粒子大小、速度和画面尺度FR-07交互旋转加载音频后,可使用鼠标或触摸拖动画面,改变球形效果观察角度FR-08截图保存点击截图按钮后,可将当前 Canvas 保存为 PNG 文件NFR-01隐私性音频在浏览器本地读取和解码,不主动上传到服务器NFR-02资源管理组件卸载时停止动画、断开音频节点并关闭 AudioContext3.2 设计系统架构和数据流SoundScape 采用纯前端分层结构。Vue 组件负责用户交互与状态展示,音频引擎负责音频生命周期,渲染引擎负责逐帧绘图,特征分析与粒子系统作为独立模块被渲染引擎调用。本地音频文件 │ File.arrayBuffer() ▼ AudioContext.decodeAudioData() │ ▼ AudioBufferSourceNode ──▶ AnalyserNode ──▶ 浏览器扬声器 │ 每帧采集1024个频谱值 ▼ 低频 / 中频 / 高频 / 整体强度 │ │ ▼ ▼ 柱状 / 环形 / 球形 节拍与粒子系统 └─────────┬─────────┘ ▼ Canvas 2D数据链路全部位于用户浏览器内部。除非后续自行增加上传接口,否则音频内容不会进入云端。3.3 了解项目结构当前项目的主要目录如下:music-visualizer/ ├── public/ │ ├── favicon.svg │ └── icons.svg ├── src/ │ ├── assets/ │ │ ├── hero.png │ │ └── vite.svg │ ├── components/ │ │ ├── Player.vue │ │ ├── Toolbar.vue │ │ ├── Upload.vue │ │ └── Visualizer.vue │ ├── engine/ │ │ ├── audioEngine.js │ │ ├── particleSystem.js │ │ ├── renderEngine.js │ │ └── themeManager.js │ ├── App.vue │ └── main.js ├── index.html ├── package-lock.json ├── package.json └── vite.config.js文件职责src/App.vue组合主界面,维护播放状态、进度、当前模式和主题,处理文件选择与截图src/components/Visualizer.vue挂载 Canvas,创建和释放渲染引擎,转发模式、主题及拖拽事件src/engine/audioEngine.js解码音频,创建播放源,管理播放、暂停、跳转和频谱数据src/engine/themeManager.js定义四套主题,提取频段特征,检测节拍并估算 BPM 与情绪标签src/engine/particleSystem.js使用固定对象池生成、更新和绘制粒子src/engine/renderEngine.js驱动动画循环,绘制柱状、环形、球形、粒子和背景光效当前运行入口把上传、播放控制和工具栏直接组合在 App.vue 中,只实际挂载 Visualizer.vue。Player.vue、Toolbar.vue 和 Upload.vue 是预留的拆分组件,当前版本未被 App.vue 导入。理解这一点可以避免修改了未挂载组件却看不到页面变化。3.4 实现音频加载和频谱采集AudioEngine.init() 在第一次加载文件时创建 AudioContext 和 AnalyserNode:init() { if (this.ctx) return const AC = window.AudioContext || window.webkitAudioContext if (!AC) return this.ctx = new AC() this.analyser = this.ctx.createAnalyser() this.analyser.fftSize = 2048 this.analyser.smoothingTimeConstant = 0.8 this.analyser.minDecibels = -90 this.analyser.maxDecibels = -10 this.analyser.connect(this.ctx.destination) this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount) this.timeDomainData = new Uint8Array(this.analyser.frequencyBinCount) } 当 fftSize 为 2048 时,frequencyBinCount 为 1024。smoothingTimeConstant 设置为 0.8,可以减少相邻帧之间的突变,使画面更连贯。加载文件时,应用通过 File.arrayBuffer() 读取本地数据,再调用 decodeAudioData() 得到 AudioBuffer:async load(file) { this.init() if (!this.ctx) throw new Error('Web Audio API not supported') this._stopSource() try { const arrayBuffer = await file.arrayBuffer() this.buffer = await this.ctx.decodeAudioData(arrayBuffer) this.pauseOffset = 0 this.playing = false } catch (e) { this.buffer = null this.playing = false throw e } } AudioBufferSourceNode 只能启动一次,因此播放、跳转或重新播放时必须新建播放源。项目使用 pauseOffset 保存暂停位置,并使用 _generation 避免旧播放源的 onended 回调覆盖新播放状态。3.5 将音频特征映射为视觉参数themeManager.js 将频谱数组划分为低频、中频和高频,并计算整体强度指标:const bassEnd = Math.max(1, Math.floor(len * 0.1)) const midEnd = Math.max(bassEnd + 1, Math.floor(len * 0.5)) const bassAvg = bassSum / bassEnd / 255 const midAvg = midSum / (midEnd - bassEnd) / 255 const highAvg = highSum / (len - midEnd) / 255 const rms = Math.sqrt(totalSum / len) / 255 特征与画面参数的映射关系如下:音频特征视觉反馈低频平均值 bassAvg增大粒子尺寸;超过阈值时参与节拍判定并触发粒子爆发中频平均值 midAvg在当前主题基础上偏移色相高频平均值 highAvg提高粒子速度并改变色相整体强度 rms调整粒子数量、透明度、饱和度和环境光尺度四套主题只提供基础色彩和背景,实时音频特征会继续改变视觉参数:主题键显示名称基础风格neon霓虹品红与青色,深蓝黑背景starry星空蓝色系,深空背景flame火焰橙红色系,深棕黑背景aurora极光绿色与蓝色,深绿背景3.6 构建三种可视化模式与粒子系统RenderEngine 使用 requestAnimationFrame() 驱动绘制。每一帧先采集频谱,再进行特征分析、背景绘制、主模式绘制和粒子更新:loop() { if (!this.running) return this.render() this.animId = requestAnimationFrame(() => this.loop()) } render() { const freq = this.audio.getFrequencyData() if (!freq || !this.audio.buffer) { this.drawIdleBackground(this.w, this.h) return } const features = analyzeFeatures(freq) const params = adaptVisualParams(features, this.theme) if (this.mode === 'bars') this.drawBars(freq, params, features) else if (this.mode === 'circular') this.drawCircular(freq, params, features) else this.drawSphere(freq, params, features) } 三种模式的实现重点如下:模式实现方式主要参数柱状 bars对数映射频谱区间,绘制上下对称的渐变柱和倒影64 根频谱柱、逐帧平滑环形 circular将频谱值映射到圆周,向内外绘制径向线段120 根径向频谱线球形 sphere使用黄金角在单位球面生成点阵,经旋转与深度计算投影到二维 Canvas900 个球面点、邻近点连线粒子系统预先创建 600 个 Particle 对象,并循环复用对象池,避免在高频动画循环中持续创建和回收对象。检测到明显低频节拍时,渲染引擎会在画面中心额外发射一组高速粒子。3.6.1 运行前代码核对当前源码中的 RenderEngine 已导入 BPMDetector,并在渲染阶段调用 this.bpmDetector.update(),但构造函数尚未创建其实例。播放音频前,需要在 src/engine/renderEngine.js 的构造函数中补充以下一行:constructor(canvas, audioEngine) { this.canvas = canvas this.ctx = canvas.getContext('2d') this.audio = audioEngine this.particles = new ParticleSystem(600) this.bpmDetector = new BPMDetector() // 其余初始化代码保持不变 } 如果未补充该行,音频加载后执行到 this.bpmDetector.update(beat) 时会出现 TypeError,动画循环随即中断。可将当前文件作为上下文交给码道,让代码智能体检查“导入、实例化、使用、释放”是否形成完整闭环。3.7 完成页面交互和截图下载主界面由全屏 Canvas 和底部控制栏组成:未加载音频时,页面中央显示点击或拖拽上传区域;加载成功后,顶部显示去除扩展名的歌曲名;底部控制栏显示播放/暂停、当前时间、总时长和进度条;右侧提供三种模式、四种主题和截图按钮;鼠标或触摸拖动 Canvas 时,渲染引擎更新旋转角度;点击截图按钮后,调用 canvas.toDataURL('image/png') 下载当前画面。截图功能的核心代码如下:function screenshot() { const canvas = visualizer.value?.getCanvas() if (!canvas) return const link = document.createElement('a') link.download = `soundscape-${Date.now()}.png` link.href = canvas.toDataURL('image/png') link.click() } 3.8 运行项目并完成功能验收在项目根目录执行:npm run dev -- --host 0.0.0.0本地开发通常访问终端输出的 http://localhost:5173/。如果使用云开发环境,请通过环境提供的端口转发或预览入口访问 5173 端口,不要直接暴露不必要的公网端口。按以下顺序进行人工验收:步骤操作预期结果1打开页面显示深色背景、上传区域和底部模式/主题按钮2上传浏览器支持的 MP3、WAV 或 OGG 文件上传区域消失,显示歌曲名并自动开始播放3点击播放/暂停按钮播放状态正确切换,时间显示同步变化4点击进度条不同位置播放位置跳转,画面继续响应音频5依次切换柱状、环形和球形三种画面均能持续渲染,无空白或明显卡顿6依次切换四种主题背景和主色调立即变化7在球形模式拖动画面球体观察角度随拖拽变化8点击截图按钮浏览器下载名称形如 soundscape-时间戳.png 的图片9刷新或关闭页面动画停止,音频上下文和节点被释放如果文件无法播放,首先打开浏览器开发者工具查看控制台,并检查:文件是否为浏览器实际支持的编码格式,而不仅是扩展名符合要求;是否已按 3.6.1 节初始化 BPMDetector;浏览器是否允许当前页面创建或恢复 AudioContext;当前 Node.js 版本是否满足 Vite 8 的要求。3.9 执行生产构建功能验收通过后,执行:npm run build构建成功后,Vite 会在 dist/ 目录生成 index.html、CSS、JavaScript 和公共静态资源。当前项目已验证能够通过 Vite 8.1.5 完成生产构建,终端会显示类似以下结果:✓ 19 modules transformed. ✓ built in ...ms使用本地静态服务器预览生产构建:npm run preview -- --host 0.0.0.0四、案例总结与资源释放4.1 案例阶段性总结通过本案例,已经完成一条可复用的浏览器音频可视化链路:使用 Vue 3 组织应用状态、文件上传和交互界面;使用 Web Audio API 解码本地音频,并通过 AnalyserNode 提供实时频谱;使用 Canvas 2D 实现柱状、环形和拟 3D 球形三种效果;使用频段特征和节拍检测驱动色彩、粒子、光效及 BPM 情绪标签;使用对象池和 requestAnimationFrame() 控制实时绘制开销;使用 Vite 完成开发调试、生产构建与静态预览。码道在本案例中承担需求细化、上下文聚焦、代码链路检查和问题定位等辅助工作。开发者仍需以实际源码、浏览器运行结果和验收用例为依据,核对智能体建议后再实施修改。4.2 释放资源本案例默认不创建收费云资源。完成体验后:在运行 Vite 的终端中按 Ctrl+C 停止开发或预览服务器;如果使用华为开发者空间云开发环境,可按实际需要停止或删除不再使用的环境;如果自行部署到对象存储、CDN 或云服务器,请根据对应产品说明释放收费资源;关闭页面时,应用会在组件卸载流程中调用 audio.destroy(),停止播放源并关闭 AudioContext。五、扩展资料说明5.1 可继续完善的功能在 AudioEngine 中增加 GainNode,实现真正的音量调节;为解码失败、超大文件和不支持的编码增加可见错误提示;增加单元测试和端到端测试,覆盖暂停、跳转、源节点重建和截图下载;将复杂特征分析迁移到 Web Worker,或使用 OffscreenCanvas 降低主线程压力;在高性能版本中评估 WebGL,但当前项目实际渲染后端为 Canvas 2D;将 dist/ 部署到静态网站托管,并按需接入 HTTPS 与 CDN;增加移动端布局、键盘无障碍操作和截图分享能力。5.2 扩展学习资料华为开发者空间
-
基于华为云码道的校园二手书交易平台全栈开发实践1. 案例概述在高校学习生活中,教材和课外书具有明显的阶段性。课程结束、考试完成或学生毕业后,大量书籍进入闲置状态;与此同时,新生和低年级学生仍然需要以较低成本获取教材。传统微信群、朋友圈和线下摆摊存在信息分散、搜索效率低、书籍状态不透明、交易过程无法追踪等问题。本案例围绕“让校园闲置书低成本、安全、可追踪地流转”这一目标,构建一套前后端分离的校园二手书交易平台。项目提供用户注册登录、书籍发布与审核、关键词检索、购物车、站内余额、管理员充值、订单履约、收藏、评价、站内通知和后台统计等功能,最终形成“发布—审核—选购—支付—发货—收货—评价”的完整业务闭环。项目在开发过程中使用华为云码道辅助完成需求梳理、系统设计、任务拆解、功能开发和代码检查,并在仓库中沉淀了 spec.md、design.md 和 tasks.md 等规格文档。当前交付版本采用 Vue 3、Node.js、Express 和 sql.js,能够在本地快速运行,也便于进一步部署到华为云。2. 案例目标本案例不是只完成静态页面,而是以“可正常运行、可完整演示、可自动验证”为交付目标。主要目标如下:实现普通用户、买家、卖家和管理员之间清晰的权限边界。实现真实书籍封面、分类筛选、关键词搜索和书籍详情。实现管理员为用户充值、用户使用站内余额购买书籍的资金闭环。实现购物车、跨卖家拆单、订单支付、卖家发货、买家收货和评价。为充值、支付和订单状态变化保留资金流水与站内通知。使用自动化测试验证权限、状态、余额和异常场景。保留可部署到华为云 ECS、RDS 和 OBS 的演进空间。3. 开发环境与技术选型3.1 前端Vue 3:构建组件化单页应用。Vue Router:管理用户端和管理端路由。Pinia:保存登录用户、余额和公共业务状态。Element Plus:提供表单、表格、弹窗、分页和消息反馈组件。Axios:统一访问后端 REST API。Vite:提供开发服务器和生产构建。3.2 后端Node.js + Express:提供 REST API。JWT:完成身份认证和接口鉴权。bcryptjs:保护用户密码。Multer:处理书籍封面上传。sql.js:使用 SQLite 数据文件完成轻量持久化。Supertest + Node.js Test Runner:实现接口级集成测试。3.3 工程结构项目当前主要由以下部分组成:used-book-web/ Vue 3 前端 used-book-api/ Express 后端与 SQLite 数据 used-book-server/ 早期 Spring Boot 实现(保留版本) .codeartsdoer/ 需求、设计和任务规格文档 README.md 项目启动、账号和功能说明早期系统设计采用 Spring Boot/MySQL。为了降低实习项目演示和单机部署的复杂度,当前默认运行主线演进为 Express/sql.js。案例以实际运行版本为准,同时保留早期版本作为架构演进记录。4. 使用华为云码道完成项目构建4.1 需求分析首先向华为云码道提供校园二手书的业务背景、目标角色和初步功能设想,请其帮助拆分功能域、明确验收条件和识别边界场景。需求阶段重点确认了以下规则:新发布书籍默认进入待审核状态。普通用户不能访问管理接口。用户不能购买自己发布的书籍。订单详情只能由相关买家、卖家或管理员访问。余额不足时必须阻止支付。同一购物车存在多个卖家时,需要按卖家拆分订单。只有卖家可以发货,只有买家可以确认收货和评价。需求分析结果沉淀在 .codeartsdoer/specs/used_book/spec.md 中,为后续设计和测试提供统一依据。4.2 系统设计在需求规格基础上,继续让华为云码道从数据模型、接口、权限、业务状态和异常处理几个方面给出设计建议。系统最终包含 13 张业务表:数据表作用user用户、角色、余额与账户状态category书籍分类book书籍基础信息、卖家、价格与状态book_image书籍图片price_change_log价格变更记录cart购物车order买家、卖家和订单状态order_item订单中的书籍价格快照review交易完成后的评价wallet_transaction充值、购买支出和卖家收入流水notification支付、发货、收货等站内通知favorite用户收藏book_view_log书籍浏览记录系统设计文档和任务拆解分别记录在 design.md 和 tasks.md 中。设计阶段最大的价值是提前发现“余额支付不是单表状态更新”“多卖家订单不能由一个卖家统一履约”等问题。4.3 任务拆解项目按照依赖关系分阶段实施:初始化前后端项目与数据库。完成用户注册、登录和 JWT 鉴权。完成分类、书籍发布、封面上传和审核。完成首页检索、详情、收藏和浏览记录。完成购物车和订单创建。完成管理员充值、余额支付和资金流水。完成发货、收货、评价和通知。完成管理后台和统计。增加集成测试并执行完整代码审查。通过规格驱动的任务拆解,可以减少前后端并行开发时的接口歧义,也方便在每个阶段运行测试。4.4 关键功能迭代在初版完成后,又使用华为云码道对照 README 检查实际代码和需求,重点完成了两轮增强。第一轮增强解决了交易资金来源问题。系统原本具有买卖功能,但缺少适合演示的充值入口。优化后只有管理员可以为指定用户增加站内余额,同时记录充值金额、备注、操作时间和变动后余额。普通用户不能直接修改余额。第二轮增强提升了书籍展示真实性。项目演示数据补充了真实书籍封面,并为图片加载失败保留默认占位图,避免外部图片失效导致页面结构异常。最后对完整仓库进行功能和代码检查,运行后端集成测试和前端生产构建,修复字段命名、参数校验、权限边界、待审核书籍可见性等问题。5. 系统总体方案5.1 逻辑架构浏览器 │ ▼ Vue 3 单页应用 │ Axios / JSON / JWT ▼ Express REST API ├─ 用户与鉴权 ├─ 书籍与审核 ├─ 购物车与订单 ├─ 钱包与资金流水 ├─ 收藏、评价与通知 └─ 管理后台与统计 │ ├─ SQLite 数据文件 └─ 封面上传目录前端通过 Axios 访问 API,后端使用 JWT 判断用户身份和角色。所有关键权限均在服务端再次校验,不能只依赖前端隐藏按钮。业务数据持久化到 SQLite 文件,封面图片由上传目录提供静态访问。5.2 用户与权限普通用户可以浏览和搜索书籍、发布书籍、加入购物车、购买、发货或确认收货,但具体操作受到资源归属限制。管理员拥有用户管理、书籍审核、充值和数据统计权限。管理接口统一检查 admin 角色,普通用户直接调用接口也会被拒绝。5.3 书籍发布与审核用户发布书籍时需要填写书名、作者、分类、价格、成色、出版信息和描述,并可上传封面。前端提供表单提示,后端再次校验日期、价格和分类合法性。新书默认处于待审核状态。只有审核通过并处于在售状态的书籍才进入公共列表,避免违规或错误内容直接公开。首页支持分类、关键词、价格与排序筛选。搜索同时匹配书名和作者,详情页展示封面、价格、成色、卖家和描述。5.4 购物车与跨卖家拆单用户可将不同卖家的书籍加入同一个购物车。创建订单时,后端重新读取每本书的最新状态和价格,并按 sellerId 分组,为每个卖家生成独立订单。这种设计使每个卖家只能处理自己的订单,发货和结算互不影响,也避免某个卖家延迟发货导致其他订单无法推进。5.5 站内余额与管理员充值系统买卖书籍使用平台站内余额。管理员可在用户管理页面选择用户,输入充值金额和备注。服务端校验管理员身份、金额范围和目标用户,并同时更新用户余额和钱包流水。钱包流水包含:管理员充值;购买支出;卖家收入;交易说明;变动后余额;操作时间。这样既能方便训练营项目演示,也能让每次资金变化可追踪。5.6 订单状态订单的主要状态流转如下:待支付 pending_payment │ 买家余额支付 ▼ 已支付 paid │ 卖家发货 ▼ 已发货 shipped │ 买家确认收货 ▼ 已完成 completed │ ▼ 评价 review创建订单后,买家可以支付或取消。支付时后端重新检查订单状态和余额,余额不足则拒绝。卖家只能对已支付订单发货,买家只能对已发货订单确认收货。关键状态变化会生成站内通知。6. 关键技术难点与解决方案6.1 资金与业务状态一致性支付涉及买家余额、钱包流水、订单状态、书籍状态和通知等多项变化。如果只在前端扣减显示余额,刷新页面后数据会失真,也可能产生越权支付。本项目将后端作为唯一可信来源。支付请求到达后,后端依次验证:当前用户是否为订单买家;订单是否处于待支付状态;订单中的书籍是否仍可交易;买家余额是否足够;所有校验通过后再执行扣款、状态更新和流水记录。前端只负责展示后端返回的新余额和订单状态。6.2 多卖家订单拆分购物车是买家维度的数据,但订单履约是卖家维度的数据。如果不同卖家的书籍进入同一订单,一个卖家可能看到或操作另一个卖家的书籍。系统在创建订单时先按卖家分组,再分别创建订单和订单条目。自动化测试专门覆盖跨卖家拆单,验证订单数量、卖家归属和金额计算。6.3 状态机与越权防护订单详情、评价删除、书籍审核和后台统计都存在越权风险。系统在每个接口中校验用户角色、资源归属和前置状态。例如普通用户不能删除他人评价,未参与订单的用户不能查看订单详情,待审核书籍不会出现在公共列表。6.4 真实封面与降级展示真实封面可以显著改善项目效果,但外部图片可能失效、加载缓慢或出现跨域问题。当前演示数据使用可访问封面,同时保留默认封面作为降级展示。部署到生产环境后,建议将确认合规的图片迁移到 OBS 或项目静态资源。6.5 架构演进的一致性项目早期规格采用 Spring Boot/MySQL,当前交付主线采用 Express/sql.js。为了避免评审时出现文档与实现不一致,本案例明确区分“早期设计版本”和“当前可运行版本”,并建议后续以实际云端部署技术栈更新设计文档。7. 测试与验证项目执行统一检查命令:npm run check检查结果如下:后端集成测试:14/14 通过,0 失败;前端生产构建:成功;Vite 版本:8.1.5;前端转换模块:1799 个;本地 Web 服务:http://localhost:5173;本地 API 服务:http://localhost:8088。14 个集成场景覆盖:公共书籍接口字段采用 camelCase;搜索同时匹配书名和作者;非法出版日期和分类被拒绝;不存在的接口返回统一 JSON 404;订单详情权限隔离;普通用户不能删除评价;购物车返回卖家信息;待审核书籍的可见性;管理后台统计;管理员充值、余额购买和卖家入账;跨卖家购物车拆分订单;支付、发货、收货、评价和通知完整闭环;收藏功能;余额不足时拒绝支付,并允许取消订单。除自动化测试外,还进行了真实页面操作。本次演示由管理员为用户充值 100 元,用户以 20 元购买《三体》,支付后余额为 80 元,同时生成购买支出流水和支付成功通知。8. 运行与演示8.1 启动方式在项目根目录执行:npm run dev该命令同时启动前端和 API。执行 npm run check 可以运行后端测试并构建前端生产包。8.2 推荐演示路径在首页搜索并查看《三体》详情。管理员进入后台,为测试用户充值 100 元。买家将书籍加入购物车并创建订单。使用站内余额支付,查看余额由 100 元变为 80 元。查看资金流水和支付成功通知。卖家发货,买家确认收货并评价。回到管理后台查看订单和书籍状态统计。8.3 运行效果截图以下截图展示首页、书籍详情、管理员充值、购物车、订单支付、钱包流水、站内通知和后台统计等核心效果。9. 华为云实际部署项目已部署到华为云 ECS,在线演示地址为 http://124.70.103.252/,健康检查地址为 http://124.70.103.252/health(受论坛外链白名单限制,复制地址时请将中文全角冒号 : 改为英文半角冒号 :)。完整案例 PDF 下载地址:http://124.70.103.252/used-book-case-report.pdf(复制时同样将 : 改为 :)。使用 Nginx 部署 used-book-web/dist,并反向代理 /api、/uploads 和 /health;使用 systemd 运行 Node.js API,支持开机启动和异常重启;SQLite 与 uploads 位于独立持久化目录;API 仅监听 127.0.0.1:8088,8088 不直接对公网开放;部署时生成随机 JWT 密钥,并使种子账号默认密码失效;演示实例计划于 2026 年 7 月 31 日 02:49(GMT+08:00)自动释放。当前部署适合训练营阶段验收。如果后续需要长期运行,可增加域名和 HTTPS,将 SQLite 迁移到华为云 RDS,将上传图片迁移到 OBS,并增加集中日志、监控和备份。10. 案例总结本项目完成了从需求、设计、编码、测试到交付材料整理的完整过程。与简单的增删改查项目相比,校园二手书平台重点解决了多角色权限、订单状态、跨卖家拆单、资金流水和业务一致性问题。华为云码道在项目中的主要价值体现在:将模糊的项目想法转化为可执行的规格和验收条件;从需求中识别状态、权限和异常场景;形成设计文档和任务拆解;辅助实现管理员充值、真实封面和完整交易闭环;对照 README 和代码进行仓库级检查;通过自动化测试验证功能并发现边界问题;将代码、截图、测试结果和指导书要求整理为可交付案例。
-
一、概述1.1 案例介绍银发智伴(ElderlyCare AI) 是一款面向子女、老年家庭成员和应用后台管理员三类用户的智能体检报告管理应用。子女可在 PC 端上传父母的 PDF、JPG 或 PNG 体检报告,系统通过 OCR 识别、指标提取和大模型健康解读,将专业检验结果转换为通俗说明、健康建议和饮食计划;随后生成适老化 H5 分享链接与二维码,父母通过手机即可查看大字版报告并使用语音播报。后台管理员负责用户状态、角色、系统概览和审计信息管理,不参与用户报告内容的日常操作。本案例采用 FastAPI、Vue 3、SQLite、Redis、Nginx 和华为云 ECS 构建,集成华为云 OBS 与 OCR,并使用华为云码道(CodeArts)代码智能体完成需求分析、存量代码理解、任务拆解、增量开发和问题调试。应用还提供家庭成员管理、历年指标趋势、异常变化提醒、报告对比、分享密码、访问次数限制和后台治理,形成“报告上传、智能解析、健康解读、长期跟踪、适老分享、后台治理”的完整业务闭环。GitCode 源码与演示视频:https://gitcode.com/SDSXshlbz/elderlycare-ai本案例中的演示账号、家庭成员和体检指标均为合成数据,不对应任何真实个人。健康解读仅供参考,不构成医疗诊断建议。1.2 适用对象企业开发者个人开发者高校学生学习本案例前,建议具备 Python、JavaScript、Linux 常用命令和 HTTP API 的基础知识。1.3 案例时间本案例总时长预计90分钟,其中云资源准备约20分钟,项目配置与部署约40分钟,功能验证与资源清理约30分钟。1.4 案例流程[1. 准备云资源] | v [2. 配置项目与密钥] -> [3. 构建 PC/H5 前端和 FastAPI 后端] | v [5. H5适老分享] <- [4. OCR识别、指标提取与健康解读]说明:在华为云创建 ECS、VPC、安全组、EIP 和 OBS 桶,公网仅长期开放演示所需的 80 端口;准备华为云 AK/SK、OCR、OBS 和大模型配置,通过 .env 注入运行参数,不将密钥写入源码;将源码上传到 Ubuntu 22.04 ECS,执行 deploy_ecs.sh,自动安装依赖、构建两个前端并配置 Nginx、Redis 和 systemd;用户上传报告后,后端依次完成 OCR 降级识别、指标提取、风险判定、健康解读和饮食计划生成;子女在 PC 端查看趋势和对比结果,并生成带二维码、可选访问密码和语音播报的适老化 H5 分享页面。1.5 资源总览本案例使用按需计费资源,建议将完整体验控制在2小时内。不同区域和活动价格可能变化,实际费用以华为云控制台订单页为准。体验完成后请及时释放 ECS、EIP 和不再使用的 OBS 数据,避免产生多余费用。资源名称规格单价(元)弹性云服务器 ECS通用计算增强型 c7.large.2 | 2 vCPUs | 4 GiB | Ubuntu 22.04 Server | 40 GiB GPSSD以控制台实时价格为准弹性公网IP(Elastic IP,简称EIP)按流量计费 | 5Mbit/s按实际公网流量计费虚拟私有云 VPC 和安全组1个VPC、1个子网、1个安全组VPC和安全组本身免费对象存储服务 OBS标准存储,用于报告文件、二维码等对象按存储量与请求次数计费文字识别 OCR通用文字识别、通用表格识别按调用量计费或使用套餐包华为云码道(CodeArts)代码智能体通用体验版以开通页面为准阿里云百炼大模型 APIqwen-turbo、qwen-vl-ocr、qwen-vl-plus按模型调用量计费二、环境和资源准备2.1 购买华为云 ECS 弹性云服务器登录华为云控制台,依次进入 服务列表 > 计算 > 弹性云服务器 ECS,点击 购买弹性云服务器。计费模式选择 按需计费,区域选择 北京四 cn-north-4,规格选择 通用计算增强型 c7.large.2,2 vCPUs,4 GiB。镜像选择 Ubuntu Server 22.04 64bit,系统盘选择 40 GiB GPSSD。创建或选择 VPC 与子网,购买 5Mbit/s、按流量计费 的弹性公网 IP。创建安全组,长期入方向规则只放行 TCP/80。部署期间如需 SSH,可临时放行 TCP/22,并将源地址限制为管理员当前公网 IP,部署完成后立即删除 22 端口规则。点击 立即购买,确认按需资源并等待 ECS 状态变为“运行中”。记录 ECS 公网 IP,后文用 <ECS公网IP> 表示。关键节点说明:安全组只控制云侧入站流量,Nginx 仍需在实例内监听 80 端口。后端 Uvicorn 仅监听 127.0.0.1:8005,不能绕过 Nginx 直接从公网访问。2.2 准备华为云 OBS、OCR 和访问密钥进入 服务列表 > 存储 > 对象存储服务 OBS,创建私有桶,例如 elderlycare,区域需与 ECS 一致。进入 服务列表 > 人工智能 > 文字识别 OCR,开通通用文字识别和通用表格识别。点击控制台右上角用户名,进入 我的凭证 > 访问密钥,创建并妥善保存 AK/SK。在 我的凭证 > API凭证 中记录项目 ID,项目所属区域应为 cn-north-4。AK/SK 具有云资源访问权限,只能写入服务器上的 .env,不得提交到 Git、案例文档、截图或前端代码中。OBS 桶保持私有,文件由后端 SDK 访问。2.3 准备大模型与 OCR 降级服务登录阿里云百炼控制台,地址:https://dashscope.console.aliyun.com/,开通 DashScope 服务。在 API-KEY 管理 中创建 API Key,并确认账号可调用 qwen-turbo、qwen-vl-ocr 和 qwen-vl-plus。将 API Key 仅配置在 .env 中。项目首先调用华为云 OCR;若该调用失败,再依次尝试 qwen-vl-ocr 和 qwen-vl-plus,避免单一识别服务异常导致整条解析链路不可用。2.4 准备本地和云端环境本案例以 Windows 作为开发端、Ubuntu 作为运行端,建议环境如下:环境软件及版本用途Windows 10/11PowerShell 5.1+、OpenSSH Client、tar.exe源码检查、测试、打包和上传本地 PythonPython 3.10+,本案例测试环境为 Python 3.12后端测试本地 Node.jsNode.js 20、npm 10PC/H5 生产构建Ubuntu ECSUbuntu 22.04、Python 3.10、Node.js 20、Nginx、Redis演示环境运行执行以下命令复制配置模板:Copy-Item .env.example .env编辑 .env,将占位内容替换为实际配置。不要删除未使用的键,也不要在等号两侧添加多余引号:HUAWEI_CLOUD_AK=你的华为云AK HUAWEI_CLOUD_SK=你的华为云SK HUAWEI_CLOUD_REGION=cn-north-4 HUAWEI_CLOUD_PROJECT_ID=你的华为云项目ID OBS_BUCKET_NAME=elderlycare OBS_ENDPOINT=obs.cn-north-4.myhuaweicloud.com JWT_SECRET_KEY=至少32字节的随机字符串 REDIS_URL=redis://localhost:6379/0 DATABASE_URL=sqlite+aiosqlite:///./elderlycare.db MCP_SERVER_URL= LLM_API_KEY=你的千问APIKey LLM_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1 LLM_MODEL_NAME=qwen-turbo ALIYUN_OCR_API_KEY=你的阿里云OCR_APIKey ALIYUN_OCR_MODEL=qwen-vl-ocr SHARE_BASE_URL=http://<ECS公网IP>/h5在 Windows PowerShell 中生成随机 JWT 密钥:$bytes = New-Object byte[] 48 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) [Convert]::ToBase64String($bytes) 三、构建银发智伴应用3.1 使用 CodeArts 代码智能体创建项目登录华为云码道(CodeArts),创建 Python + Vue 3 项目。在 CodeArts IDE 中输入以下需求描述,让代码智能体先理解目标和约束,再结合本案例存量源码进行增量开发:创建“银发智伴”体检报告管理应用。后端使用 Python FastAPI 和 SQLAlchemy Async,PC 端与 H5 端使用 Vue 3。子女在 PC 端上传父母体检报告,后端通过 OCR 提取指标,生成健康解读、饮食计划、趋势分析和报告对比;H5 使用大字号、清晰风险标签和中文语音播报。项目部署到 Ubuntu ECS,由 Nginx 统一提供 PC、H5 和 API 入口。本项目工作区保留了代码智能体生成并持续修订的规格、设计和任务文件,使用过程如下:阶段代码智能体产物或操作人工确认的关键节点需求规格化.codeartsdoer/specs/elderlycare_ai/spec.md明确子女、父母、管理员三类角色,以及“仅提供健康参考、不替代医生诊断”的职责边界存量代码分析.codeartsdoer/specs/elderlycare_ai/design.md对照现有认证、报告和分享模块,判断哪些功能复用、哪些功能增量扩展,避免大幅改变原有结构任务拆解.codeartsdoer/specs/elderlycare_ai/tasks.md将工作拆分为认证、报告、OCR、健康解读、分享、管理员、测试和部署等可验证任务代码开发根据任务逐个生成或补全 FastAPI 服务、Vue 页面和部署配置审查接口权限、异步数据库事务、上传限制、异常映射和前后端字段契约调试验证结合错误日志定位登录、复制、H5 子路径和 Ubuntu 兼容问题每次修复后执行后端测试、两个前端生产构建和 ECS 健康检查本案例使用代码智能体辅助完成的典型调试过程如下:登录错误调试:根据后端日志和认证服务调用链,发现错误凭据触发的业务异常未正确映射。修正空用户判断和异常处理器后,错误账号由 HTTP 500 变为 HTTP 401,并返回“账号或密码错误”;分享链接复制调试:公网演示环境使用 HTTP,浏览器可能禁用安全上下文中的 Clipboard API。智能体协助定位兼容性原因,并在 navigator.clipboard.writeText() 失败时使用 document.execCommand('copy') 回退;Windows 到 Ubuntu 迁移调试:检查 CRLF/LF、文件名大小写、依赖锁文件和路径分隔符;前端使用 npm ci 复现依赖,部署脚本保持 LF,源码导入路径与真实文件名大小写一致;H5 子路径调试:统一 Vite base: '/h5/'、Vue Router 的 import.meta.env.BASE_URL 与 Nginx try_files,解决分享深层路由刷新 404;验证闭环:本地运行 python -m pytest tests -q,分别执行 PC/H5 的 npm run build,部署后访问 /health,并验证登录、趋势、对比、分享和管理员接口。代码智能体用于加速分析、编码和定位问题,不能替代人工评审。代码生成后必须逐项检查权限边界、异常处理、上传限制、密钥加载、医疗免责声明和敏感信息,并通过测试及生产构建后才能部署。3.2 部署项目代码1)解决方案总体设计银发智伴采用“多端访问、统一 API、智能服务可降级、云上统一接入”的解决方案。PC 端承载报告管理、趋势对比和后台治理,H5 端专注适老化查看与语音播报;Nginx 提供统一公网入口,FastAPI 编排认证、报告、分享和智能解析流程;OBS 保存私有报告对象,SQLite 保存结构化业务数据,Redis 保存令牌黑名单和运行期状态。OCR 与大模型均设置超时、错误处理和本地降级,避免第三方服务短暂不可用时整个应用失效。用户上传报告后,请求依次经过文件校验、私有存储、OCR 识别、指标提取、风险判定、大模型健康解读和合规过滤;处理状态通过状态机管理。子女可在 PC 端查看结果、趋势和对比,也可生成带随机状态 ID、有效期、可选密码和访问次数限制的分享链接。父母通过 H5 查看大字版内容;管理员通过角色鉴权后的接口查看系统概览和管理用户。2)系统架构设计用户层:PC 子女端 / H5 父母端 / PC 管理后台 | 接入层:EIP + Nginx(/、/h5/、/api/、/health) | 应用层:FastAPI API / JWT认证 / 报告 / 分享 / 趋势 / 对比 / 管理 | 智能层:华为云OCR -> 百炼OCR -> Qwen VL / MCP大模型 / 合规工具 / 本地规则 | 数据层:SQLite业务数据 / Redis状态与令牌 / OBS私有对象 | 运维层:systemd / 健康检查 / 审计日志 / 华为云安全组架构关键点:公网只暴露 Nginx 的 80 端口,FastAPI 仅监听 127.0.0.1:8005;PC、H5 与 API 共用同一主机,减少跨域配置;智能服务通过统一服务层调用,业务 API 不直接依赖某一家模型厂商;配置和密钥全部由 .env 注入,仓库只提供不含真实值的 .env.example。核心技术难点与解决思路核心难点解决思路验证方式OCR 服务超时、配额不足或结果为空依次调用华为云 OCR、百炼 OCR 和 Qwen VL,仅在获得有效文本时结束;全失败时明确标记报告失败Mock 各级返回值,检查降级顺序和失败状态大模型输出不稳定且健康内容有合规风险传入结构化指标,以系统提示词约束输出;执行合规过滤并固定展示免责声明;调用失败时使用本地规则生成基础解读断开模型服务后仍能生成可查看的基础结果报告解析耗时,用户难以判断进度使用 OCR_PROCESSING、EXTRACTING、INTERPRETING、COMPLETED、FAILED 状态机,并通过 SSE 推送进度检查页面进度变化及异常后最终状态Windows 开发代码迁移到 Ubuntu锁定 Python/Node 依赖,使用 npm ci;Shell 脚本使用 LF;检查源码路径大小写,不使用 Windows 专属路径本地测试、双前端生产构建和 ECS 健康检查均通过H5 部署在 /h5/ 后深层路由刷新 404Vite base、Router base 和 Nginx try_files 使用一致的 /h5/ 前缀直接访问 /h5/share/<state_id> 返回 HTTP 200HTTP 演示站点复制分享链接失败优先使用 Clipboard API,失败时回退到隐藏文本框和 document.execCommand('copy')本地 HTTPS/localhost 与 ECS HTTP 环境分别点击复制医疗报告、账号和后台权限安全bcrypt 保存密码,JWT 鉴权,失败次数锁定,角色依赖保护管理员接口,OBS 私有存储并记录审计日志错误登录返回 401,普通用户访问管理员接口返回 4033)通过 GitCode 下载源码并了解项目结构本案例源码公开托管于 GitCode:https://gitcode.com/SDSXshlbz/elderlycare-ai。在 Windows PowerShell 或 Ubuntu 终端执行:git clone https://gitcode.com/SDSXshlbz/elderlycare-ai.git cd elderlycare-ai仓库不包含 .env、数据库、用户上传文件或真实医疗图片。克隆后需根据 .env.example 创建本地 .env 并填写自己的服务配置。项目结构如下:├── app/ # FastAPI后端 │ ├── api/ # 登录、报告、分享、趋势、对比等API路由 │ ├── core/ # 配置、数据库、安全、OBS、OCR、异常处理 │ ├── mcp/ # 大模型调用和本地工具降级 │ ├── models/ # SQLAlchemy ORM模型 │ ├── schemas/ # 请求和响应数据模型 │ ├── services/ # 认证、报告、解析、解读、分享等业务服务 │ └── main.py # FastAPI入口、路由和健康检查 ├── frontend/ │ ├── pc/ # 子女使用的PC端Vue 3应用 │ │ └── src/views/ # 工作台、详情、趋势、对比、家庭成员等页面 │ └── h5/ # 父母使用的适老化H5应用 │ └── src/views/SharePage.vue # 大字版健康报告和语音播报 ├── mcp_server/ # 可独立运行的模型编排与工具服务 ├── migrations/ # Alembic数据库版本记录 ├── tests/ # 后端单元测试与集成测试 ├── deploy_ecs.sh # Ubuntu ECS一键部署脚本 ├── requirements.txt # 固定版本的Python依赖 └── .env.example # 环境变量模板,不包含真实密钥 项目采用前后端分离和 API、服务、模型分层结构。浏览器请求先到 app/api/ 路由层,Pydantic app/schemas/ 完成输入输出校验,app/services/ 编排业务规则,app/models/ 通过异步 SQLAlchemy 持久化数据;OCR、OBS、安全和异常处理集中在 app/core/,模型调用与合规、营养、参考范围工具位于 app/mcp/。PC 与 H5 分别构建为静态文件,Nginx 将 / 映射到 PC,将 /h5/ 映射到 H5,将 /api/ 和 /health 反向代理到 FastAPI。4)在 Windows 本地执行构建与测试在项目根目录执行后端测试:.\.venv\Scripts\python.exe -m pytest tests -q预期结果:54 passed分别构建 PC 和 H5 生产包:Set-Location frontend\pc npm ci npm run build Set-Location ..\h5 npm ci npm run build Set-Location ..\.. npm ci 会严格按照 package-lock.json 安装依赖,避免 Windows 和 Ubuntu 使用不同依赖版本。两个 npm run build 均应成功生成各自的 dist 目录。PC 端构建时可能提示 ECharts 相关分块超过 Vite 默认的 500 KB 建议阈值,该提示不影响产物生成,后续可通过 manualChunks 进一步拆包。依赖审计警告不会阻断构建,但应单独评估,不建议直接执行可能引入破坏性升级的 npm audit fix --force。5)关键代码讲解OCR 三级降级机制(app/core/ocr_client.py)OCR 是报告解析的入口。代码先调用华为云通用文字和表格识别;如果没有识别到有效文本,再调用百炼 OCR 模型;最后使用千问 VL 多模态模型兜底。每一级仅在返回非空文本时结束链路,所有方法失败时返回空结果并由解析状态机标记失败。async def recognize_medical_report(self, image_data: bytes, file_type: str = "image") -> dict: image_b64 = base64.b64encode(image_data).decode("utf-8") result = await self._try_huawei_ocr(image_b64) if result: return result result = await self._try_aliyun_ocr(image_b64, file_type) if result: return result result = await self._try_qwen_vl(image_b64, file_type) if result: return result logger.error("All OCR methods failed (huawei -> aliyun -> qwen-vl)") return {"text": "", "confidence": 0.0} 关键释义:base64.b64encode() 将图片转为云 OCR API 可接收的 Base64 内容;降级调用相互独立,单个云服务超时或配额不足不会立即终止处理;日志只记录调用结果和错误,不输出图片、AK/SK 或 API Key;最终空文本不是伪造成功结果,业务层会将报告标记为 FAILED,便于用户重新处理。报告解析状态机和指标风险判定(app/services/parse_service.py)报告处理依次进入 OCR_PROCESSING、EXTRACTING、INTERPRETING 和 COMPLETED。前端根据状态显示处理进度;任何未捕获异常都会进入 FAILED,避免报告长期停留在处理中。await self._update_status(report, ParseStatus.OCR_PROCESSING) ocr_text = await self._run_ocr(report) if not ocr_text or not ocr_text.strip(): report.status = ParseStatus.FAILED await self.db.flush() return await self._update_status(report, ParseStatus.EXTRACTING) indicators = self._extract_indicators(ocr_text) abnormal = [i for i in indicators if i.status != IndicatorStatus.NORMAL] if abnormal: names = [i.name for i in abnormal[:5]] report.abnormal_summary = f"发现{len(abnormal)}项异常指标: {', '.join(names)}" await self._update_status(report, ParseStatus.INTERPRETING) report.status = ParseStatus.COMPLETED指标提取阶段会校验名称长度、医学单位、数值与参考范围。高于上限标记为 HIGH,低于下限标记为 LOW;偏离比例达到 1.2 或 1.5 时,风险等级依次提升为中风险或高风险。大模型健康解读与本地降级(app/mcp/mcp_client.py)后端使用 OpenAI 兼容接口调用配置的大模型,系统提示词约束输出范围,用户提示词携带结构化指标。HTTP 状态异常通过 raise_for_status() 进入异常分支;大模型不可用时,generate_interpretation() 会调用本地规则生成基础解读,保证报告仍可查看。async def _call_llm(self, system_prompt: str, user_prompt: str, temperature: float = 0.7) -> str: if not self._can_call_llm(): raise AppException(40004, "LLM API 未配置") async with httpx.AsyncClient(timeout=settings.LLM_TIMEOUT, proxy=None) as client: response = await client.post( f"{self._llm_api_base}/chat/completions", json={ "model": self._llm_model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "temperature": temperature, }, headers={"Authorization": f"Bearer {self._llm_api_key}"}, ) response.raise_for_status() data = response.json() return data.get("choices", [{}])[0].get("message", {}).get("content", "") 登录失败返回业务错误而不是服务器内部错误(app/services/auth_service.py、app/core/exceptions.py)登录时先判断用户是否存在,再校验 bcrypt 密码。账号不存在或密码错误均抛出业务码 60003,统一异常处理器将其映射为 HTTP 401。这样错误凭据不会因为空用户访问、密码哈希异常或未处理业务异常而显示“服务器内部错误”。if user is None: raise AppException(60003, "账号或密码错误") if not verify_password(password, user.password_hash): user.login_fail_count += 1 if user.login_fail_count >= settings.LOGIN_MAX_FAILURES: user.status = AccountStatus.LOCKED user.locked_until = datetime.utcnow() + timedelta( minutes=settings.LOGIN_LOCK_MINUTES ) await self.db.flush() raise AppException( 60002, f"密码错误次数过多,账号已锁定{settings.LOGIN_LOCK_MINUTES}分钟", ) await self.db.flush() raise AppException( 60003, f"账号或密码错误,还剩{settings.LOGIN_MAX_FAILURES - user.login_fail_count}次机会", ) if exc.code in (60002, 50005): status_code = 429 elif exc.code in (60003, 60004): status_code = 401 连续失败达到阈值后账号临时锁定,可以降低暴力尝试风险;成功登录后失败次数会被清零,并签发访问令牌和刷新令牌。报告分享、二维码和访问限制(app/services/share_service.py)分享状态使用随机 UUID,默认30天有效。可选4位数字访问密码使用 bcrypt 哈希保存,数据库中不存储明文;同时支持访问次数上限、失败次数锁定和主动撤销。state_id = str(uuid.uuid4()) expire_time = datetime.utcnow() + timedelta( days=settings.SHARE_DEFAULT_EXPIRE_DAYS ) password_hash = None if access_password: if not access_password.isdigit() or len(access_password) != 4: raise AppException(10001, "访问密码需为4位数字") password_hash = _bcrypt.hashpw( access_password.encode("utf-8"), _bcrypt.gensalt() ).decode("utf-8") share_url = f"{settings.SHARE_BASE_URL}/share/{state_id}" qr = qrcode.QRCode(version=1, box_size=10, border=5) qr.add_data(share_url) qr.make(fit=True) H5 适老化语音播报(frontend/h5/src/views/SharePage.vue)H5 使用浏览器原生 Web Speech API,无需额外安装播放器。播报内容由健康解读、建议、注意事项和指标组成,语速设置为 0.8,优先选择中文语音;用户再次点击按钮时立即停止。function toggleSpeech() { if (!speechSupported.value) return const synth = window.speechSynthesis if (isSpeaking.value) { synth.cancel() isSpeaking.value = false return } speechUtterance = new SpeechSynthesisUtterance(getSpeechText()) speechUtterance.lang = 'zh-CN' speechUtterance.rate = 0.8 speechUtterance.pitch = 1.0 speechUtterance.volume = 1.0 synth.speak(speechUtterance) isSpeaking.value = true } H5 子路径适配(frontend/h5/vite.config.ts、frontend/h5/src/router/index.ts)生产环境将 H5 部署在 /h5/,构建资源路径和 Vue Router 必须使用同一个基础路径,否则在 Ubuntu Nginx 下刷新分享深层路由会出现资源 404 或空白页。export default defineConfig({ base: '/h5/', plugins: [vue()], }) const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/share/:stateId', name: 'SharePage', component: () => import('../views/SharePage.vue'), }, ], }) 6)打包并上传源码不要上传 .env 等本地敏感文件,也不要上传真实 elderlycare.db、测试数据库、医疗图片、.venv、node_modules 和 dist。在 Windows PowerShell 的项目根目录执行:tar.exe -czf elderlycare-deploy.tar.gz ` --exclude='.venv' ` --exclude='*/node_modules' ` --exclude='*/dist' ` --exclude='.env' ` --exclude='elderlycare.db' ` --exclude='test.db' ` --exclude='*.jpg' ` . scp .\elderlycare-deploy.tar.gz root@<ECS公网IP>:/tmp/ scp .\.env root@<ECS公网IP>:/tmp/elderlycare.env ssh root@<ECS公网IP> 如果使用 SSH 密钥,则在三个 SSH/SCP 命令中增加 -i <私钥路径>。上传完成后,在 ECS 中执行:sudo mkdir -p /opt/elderlycare sudo tar -xzf /tmp/elderlycare-deploy.tar.gz -C /opt/elderlycare sudo install -m 600 /tmp/elderlycare.env /opt/elderlycare/.env cd /opt/elderlycare sudo chmod +x deploy_ecs.sh sudo ./deploy_ecs.sh也可以先在 Windows PowerShell 中单独上传仅保存在本地的 .env:scp .\.env root@<ECS公网IP>:/tmp/elderlycare.env ssh root@<ECS公网IP> 登录 ECS 后,直接通过 GitCode 拉取公开源码并部署:/opt/elderlycare 是 Ubuntu ECS 上的绝对安装目录,不是 GitCode 仓库中的 opt 文件夹。git clone 命令的第二个参数会把仓库内容直接检出到该目录;后续部署脚本、Nginx 和 systemd 均统一使用此路径。sudo mkdir -p /opt/elderlycare sudo chown -R "$USER:$USER" /opt/elderlycare git clone https://gitcode.com/SDSXshlbz/elderlycare-ai.git /opt/elderlycare sudo install -m 600 /tmp/elderlycare.env /opt/elderlycare/.env cd /opt/elderlycare sudo chmod +x deploy_ecs.sh sudo ./deploy_ecs.sh关键节点说明:GitCode 只传输可公开的源码和合成演示截图,.env 必须沿独立安全通道上传。后续更新可在确认服务器无未提交修改后执行 git pull --ff-only,再重新运行 deploy_ecs.sh;数据库和用户上传文件应提前备份,不能用代码仓库代替业务数据备份。7)理解并执行一键部署脚本deploy_ecs.sh 使用 set -euo pipefail,任一关键命令失败都会终止部署。脚本执行七个阶段:安装系统依赖、准备运行用户、安装 Python 依赖、构建 PC/H5、配置 Nginx、注册 systemd 服务、执行健康检查。Nginx 的关键配置如下:location /h5/ { alias /opt/elderlycare/frontend/h5/dist/; index index.html; try_files $uri $uri/ /h5/index.html; } location /api/ { proxy_pass http://127.0.0.1:8005; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 120s; } location / { root /opt/elderlycare/frontend/pc/dist; index index.html; try_files $uri $uri/ /index.html; } try_files 是 PC 和 H5 深层路由刷新可用的关键。后端服务使用独立用户运行,并等待网络和 Redis:[Unit] After=network-online.target redis-server.service Wants=network-online.target redis-server.service [Service] User=elderlycare Group=elderlycare WorkingDirectory=/opt/elderlycare ExecStart=/opt/elderlycare/.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 8005 Restart=always RestartSec=5 脚本结束前会轮询后端健康接口。成功时输出:Backend health check passed. ======================================== Deployment completed PC: http://<ECS_PUBLIC_IP>/ H5: http://<ECS_PUBLIC_IP>/h5/ Health: http://<ECS_PUBLIC_IP>/health ========================================8)记录数据库版本并检查服务项目启动时通过 SQLAlchemy create_all() 创建当前模型表。首次部署成功后,执行 alembic stamp head 记录当前迁移基线,不重复执行已由模型创建的历史表结构:cd /opt/elderlycare source .venv/bin/activate alembic stamp head deactivate sudo nginx -t sudo systemctl is-active nginx redis-server elderlycare curl --fail http://127.0.0.1:8005/health预期结果:nginx: configuration file /etc/nginx/nginx.conf test is successful active active active {"status":"ok","version":"1.0.0"}如健康检查失败,使用以下命令定位问题:sudo journalctl -u elderlycare -n 100 --no-pager sudo tail -n 100 /var/log/nginx/error.log常见问题与处理方法:现象原因处理方法deploy_ecs.sh: /bin/bash^MWindows CRLF 换行将脚本保存为 LF 后重新上传H5 分享链接刷新后 404Vite 基础路径或 Nginx 回退错误检查 base: '/h5/' 和 /h5/index.html登录显示服务器内部错误业务异常未映射或数据库字段不完整查看 journalctl,确认 60003 返回 401,数据库版本已记录上传后一直处理中OCR、OBS 或大模型配置错误检查 .env、OBS 区域、OCR权限及后端日志CSS/模块在 Ubuntu 找不到Windows 文件名大小写不敏感统一源码导入路径与真实文件名大小写9)安全收尾部署完成后删除包含密钥的临时文件和部署压缩包,并在安全组中删除临时 22 端口规则:rm -f /tmp/elderlycare.env /tmp/elderlycare-deploy.tar.gz服务器上的 /opt/elderlycare/.env 权限应为 600,所有者应为 elderlycare:sudo stat -c '%U:%G %a %n' /opt/elderlycare/.env预期输出:elderlycare:elderlycare 600 /opt/elderlycare/.env3.3 运行效果展示本案例已部署到华为云北京四 ECS。演示环境仅使用合成数据,环境信息如下:项目演示环境信息区域北京四 cn-north-4ECS实例elderlycare-demo,实例ID 5d96458e-5a74-47e7-8859-b78c14be13a8规格与系统c7.large.2,2 vCPU / 4 GiB,Ubuntu 22.04公网IP1.92.111.234PC端URLhttp://1.92.111.234/H5端URLhttp://1.92.111.234/h5/健康检查http://1.92.111.234/health,HTTP 200GitCode源码https://gitcode.com/SDSXshlbz/elderlycare-ai普通功能测试账号邮箱:demo@elderlycare.cn密码:Demo2026!应用后台管理员测试账号邮箱:admin20260726@elderlycare.cn密码:Sc9!Vt4#Lq7@Hm2普通账号用于演示报告上传、解读、趋势、对比和分享;管理员账号登录后进入后台管理页面,用于演示系统概览、用户列表、账号状态和角色权限。演示 ECS 当前为运行状态,公网安全组不保留 SSH 入站规则,只开放作品 Web 演示所需端口。当前地址使用 HTTP,仅用于作品演示,不要录入真实个人信息、医疗报告或其他敏感数据。正式上线应配置域名、HTTPS、数据库备份、日志脱敏和监控告警。核心功能1:报告上传与年度报告管理登录后进入工作台,选择家庭成员后拖拽或点击上传 PDF、JPG、PNG 文件。页面同步展示解析状态和历史报告。演示账号已准备 2024、2025、2026 三份年度报告,用于展示长期健康变化。关键节点说明:上传请求先校验文件数量、扩展名和大小,再将报告与家庭成员关联。后端完成 OCR、指标提取和解读后,状态变为“已完成”,点击报告名称进入详情。核心功能2:智能健康解读与饮食计划报告详情将异常指标转换为通俗说明,展示风险提示、健康建议、推荐食物、忌口食物、每日食谱和指标参考范围。每份解读固定展示医疗免责声明。执行后效果:2026年度合成报告识别出空腹血糖、总胆固醇、尿酸、收缩压和 BMI 偏高,并给出饮食控制、运动、定期监测等建议;血红蛋白显示为正常。核心功能3:历次指标趋势和异常变化提醒进入 趋势分析,可以按家庭成员和指标筛选。ECharts 折线图按报告时间排序,并使用参考范围背景区间辅助判断。执行后效果:演示数据包含6个指标,每个指标有3个年度数据点。系统可识别“正常转异常”“异常恢复”“状态变化”和“数值变化”,当前演示数据生成12条异常变化记录。核心功能4:两份报告逐项对比进入 报告对比,选择较早报告 A 与较近报告 B,点击 开始对比。后端按指标名称求交集,计算 报告B数值 - 报告A数值,并按变化绝对值排序。执行后效果:2024与2026两份报告共有6项指标。尿酸由 330 umol/L 变为 455 umol/L,收缩压由 128 mmHg 变为 148 mmHg,状态从正常变为偏高;页面同时展示 BMI、空腹血糖、总胆固醇和血红蛋白的变化。核心功能5:适老化 H5 分享与语音播报在报告详情中生成分享链接或二维码,父母通过手机打开 H5。页面采用大字号、高行距、明显风险标签和大尺寸圆形播报按钮;点击播放按钮后,图标切换为暂停,再次点击立即停止。执行后效果:在 390 × 844 移动视口下页面无横向溢出,分享接口返回6项指标;语音播报可以正常开始与暂停,H5 深层路由及生产资源均返回 HTTP 200。核心功能6:家庭成员管理进入 家庭成员,可维护父母等家庭成员的关系、性别、出生日期、身高、体重、慢性病、过敏史、用药和备注。报告关联成员后,趋势与对比功能可以按成员筛选,避免多人报告混合统计。核心功能7:应用后台管理与角色权限管理员登录后可查看系统用户、报告和分享数量,按角色或账号状态筛选用户,并执行受保护的管理操作。注册页面不提供管理员角色选择,普通用户不能通过修改前端参数自行获得管理员权限;管理员接口由后端角色依赖统一保护,并具有禁止管理员自降级和最后一名管理员保护逻辑。执行后效果:管理员账号可进入 /admin 页面,普通演示账号访问同一路由时会被前端守卫拦截,直接请求管理员 API 时后端返回 HTTP 403。角色变更记录写入审计日志,便于追溯操作者、目标用户和变更时间。功能验收结果验证项实际结果正确账号登录HTTP 200,进入工作台错误账号登录HTTP 401,返回“账号或密码错误”,无服务器内部错误报告列表3份合成年度报告趋势分析6项指标,每项3个年度数据点异常变化12条变化记录报告对比6项共有指标及数值、状态变化H5分享返回健康解读、饮食计划和6项指标二维码返回 image/png管理员权限管理员可访问后台,普通用户访问管理员接口返回 HTTP 403PC/H5控制台无错误或警告Ubuntu服务Nginx、Redis、FastAPI均为 active自动化测试54 passed演示视频已上传至gitcode四、释放资源4.1 删除ECS弹性云服务器登录华为云控制台,进入 服务列表 > 计算 > 弹性云服务器 ECS。在 ECS 列表勾选本案例创建的服务器,点击 更多 > 删除。在确认对话框中勾选 释放云服务器绑定的弹性公网IP地址;如创建了额外数据盘,同时勾选删除对应数据盘。核对实例名称和公网 IP,确认无需要保留的数据后点击 是。进入 网络 > 弹性公网IP和带宽,确认 EIP 已释放;进入 对象存储服务 OBS,删除不再需要的报告、二维码和桶;进入安全组确认不存在遗留的公网 22 端口规则。删除 ECS、EIP、系统盘或 OBS 数据属于不可逆操作。释放前请先备份需要保留的源码、数据库和日志。仅关机通常仍会产生系统盘、EIP等费用,应以控制台资源状态和账单为准。五、扩展资料说明华为云 ECS:cid:link_2华为云 OBS:cid:link_4华为云 OCR:cid:link_1华为云开发者空间:cid:link_6华为云码道 CodeArts:cid:link_7银发智伴 GitCode 源码:https://gitcode.com/SDSXshlbz/elderlycare-aiFastAPI:https://fastapi.tiangolo.com/Vue 3:https://vuejs.org/Nginx:https://nginx.org/en/docs/阿里云百炼 Model Studio:https://help.aliyun.com/zh/model-studio/
-
在跨端开发(如 Web、iOS、Android、小程序)中,状态管理是决定应用性能与用户体验的关键枢纽。由于各端渲染机制与存储特性存在差异,构建全局统一的状态管理方案不能仅靠简单的数据共享,而需要从分层架构、模型抽象、同步机制与工程化实践四个核心维度进行系统性设计。首先,在架构设计上,必须采用“分层管理”策略以应对不同端的环境差异。最底层的存储层需适配各端特性,例如在 Web 端结合 IndexedDB 实现大容量离线存储,在移动端则利用原生文件系统保障数据安全。中间的状态逻辑层负责处理所有的状态变更,将复杂的业务逻辑与具体的端环境彻底解耦。最上层的状态交互层则根据各端渲染机制(如 Web 的 DOM 操作或移动端的响应式数据流),将状态变化精准传递给视图层。同时,在应用内部应实行全局与局部状态的分级管控,将用户信息、全局配置等交由 Redux Toolkit 等全局库统一管控,而页面私有状态则通过 Context API 等轻量级方案处理,避免全局状态冗余。其次,定义一套“抽象状态模型”是实现跨端统一的核心。跨端状态管理的本质不是将状态简单复制,而是让各端基于统一规则推导一致状态。通过抽象出如 UserState 这样的通用数据模型,不同端的开发人员都能基于同一套模型进行开发。在状态更新时,各端必须调用统一的状态更新函数,这不仅保证了状态变更的一致性,也极大地方便了状态的序列化与反序列化,为跨端数据传输奠定基础。第三,在多端协同与数据同步场景下,需引入事件驱动与冲突解决机制。当状态发生变化时,通过发布事件通知相关模块,各端视图层订阅事件并自动更新。对于多设备同时在线的复杂场景,必须区分哪些状态需要同步(如服务端权威的业务数据、用户偏好),哪些属于设备局部状态(如窗口大小、滚动位置)。针对多端并发修改引发的冲突,可采用类似 Git 的版本控制思想记录版本号,结合“乐观更新”与“最后写入优先”等策略,在保障数据一致性的同时提供流畅的用户体验。最后,在工程化落地与调试层面,需借助完善的工具链保障方案可靠性。在状态更新时,可引入 Immer 等不可变数据更新库,简化操作并避免直接修改状态带来的隐患。同时,建立严格的测试与调试机制至关重要。通过编写单元测试验证状态更新逻辑的正确性,并利用各端的开发者工具实时监控状态变化。对于复杂应用,还可以引入有限状态机(如 XState)来定义状态流转规则,并将副作用抽离为独立服务,从而确保跨端逻辑的绝对一致。综上所述,跨端项目的全局统一状态管理是一项兼顾架构抽象与工程细节的系统工程。通过分层解耦、统一模型、事件驱动以及严谨的冲突处理机制,开发者能够彻底解决多端状态不同步的痛点,为跨端应用的高效迭代提供坚实保障。
-
OpenTiny NEXT 前端智能化系列直播 ——第五期带你了解TinRobot,讲师将带来《TinyRobot助你一站式搞定AI界面开发》的主题分享。AI应用落地日趋广泛,智能交互产品形态愈发多元,市面通用开发组件难以契合AI交互特质,开发效率与适配效果存在短板,专属开发方案成为刚需。本次分享将介绍TinyRobot组件库定位,展示完整AI界面效果,实操搭建页面并讲解场景扩展能力,带你一站式完成AI界面开发。全程干货、可落地、可实战,更有一手课程资料、贡献者证书、AtomGit & OpenTiny 周边好礼等你来拿!主题:TinyRobot助你一站式搞定AI界面开发时间:05/26(周二) 19:00-20:00 (UTC+08:00)Beijing更多精彩活动1.评论区提问赢好礼,专家现场解答每期直播开始前,前往AtomGit 活动帖评论区提问,围绕本期直播主题与 AI 前端技术发布你的问题,即可参与!活动评论区:cid:link_02.实战出真知,OpenTiny 前端智能化实践营本次 OpenTiny NEXT 系列直播每期配套专属实战任务,让你边学边练、学完即用,真正把 AI 前端技术落地掌握。你只需在 AtomGit 平台关注并 Star OpenTiny 组织,Fork 对应仓库,根据直播内容完成实践任务,提交你的代码 PR,即可参与活动。所有提交的作品将由专业团队评审,优质实践作品将获得 AtomGit & OpenTiny 定制周边奖励,优秀 PR 还会被合并进 OpenTiny 官方仓库,成为开源贡献者,获得贡献者证书,为你的技术履历加分。AtomGit 地址:cid:link_23.参与征文有奖活动期间参与 OpenTiny NEXT 前端智能化系列征文活动,无论你是直播学习者、技术爱好者,还是开源实践者,均可围绕 AI 前端、WebMCP、WebAgent、TinyVue、TinyEngine、GenUI 、TinyRobot、AI Extension等相关技术与实战体验进行创作。征文活动:cid:link_1参与&领奖进群提前锁定福利+免费领取课程资料(见海报下方)仅有效提问、issue、PR、投稿可参与活动,灌水等无意义内容将取消活动资格本次活动解释权归 OpenTiny 团队和 AtomGit 平台所有
-
1.1 问题说明在基于UniApp开发鸿蒙元服务时,开发者无法直接将项目运行到鸿蒙模拟器进行调试。必须通过HBuilderX将项目编译运行到鸿蒙真机设备,然后将生成的编译产物手动迁移至鸿蒙ACEF(ArkUI Compiler & Engine Framework)项目的指定目录,才能完成模拟器运行与调试。此流程复杂且容易出错,增加了开发者的学习成本和调试难度。1.2 原因分析· 核心原因:HBuilderX工具链暂未集成鸿蒙X86模拟器支持当前版本的HBuilderX在编译UniApp项目至鸿蒙平台时,其内置的打包与运行逻辑主要针对ARM架构的真机设备进行优化和适配。由于未包含对鸿蒙官方X86模拟器运行环境的直接支持,导致“运行到模拟器”的选项缺失或无法成功执行。· 衍生问题:双工具链职责分离此限制迫使开发流程必须在两个工具间切换:使用 HBuilderX 完成面向鸿蒙真机的业务代码编译,生成标准的编译产物;再使用 DevEco Studio 作为鸿蒙原生工程的容器和签名工具,将上述产物导入并打包成可在真机上运行的HAP文件。· 结果:调试链路断裂最直接的“编码->模拟器调试”快速验证链路被打断,开发者必须依赖实体真机进行每一步调试,效率降低,且入门门槛提高。1.3 解决思路· 正视工具链限制,确立迂回流程明确接受当前HBuilderX不支持直连模拟器的现状,将“真机编译+产物迁移”确立为标准的手动工作流。· 建立目录映射与手动拷贝机制在鸿蒙ACEF项目中预设固定目录结构,通过清晰的步骤指引,将UniApp编译产物手动复制到对应位置,实现两者集成。· 明确双工具分工清晰界定HBuilderX与DevEco Studio的角色:HBuilderX负责Vue业务逻辑到鸿蒙格式的转换编译;DevEco Studio负责提供原生项目框架、进行签名打包和真机部署。1.4 解决方案HBuilderX中配置鸿蒙平台在manifest.json中启用“鸿蒙(HarmonyOS)”平台支持。配置应用基本信息(包名、版本号等)编译生成鸿蒙适配包并迁移在HBuilderX中,选择菜单栏的【发行】->【原生App-本地打包】->【生成本地打包App资源】。生成目录:项目根目录/unpackage/dist/dev/harmonyos。此目录下的文件即为需要迁移的产物。构建并运行在DevEco Studio顶部工具栏选择模拟器运行。1.5 总结· 问题与痛点: HBuilderX工具链暂不支持鸿蒙X86模拟器,导致无法直接调试;必须采用手动迁移编译产物的迂回方案,流程繁琐。· 技术要点: 明确“HBuilderX编译 -> 手动复制产物 -> DevEco Studio打包签名”的分工协作流程。核心操作是将/unpackage/dist/dev/harmonyos/下的内容复制到ACEF项目的ets/uni_modules/目录。· 实现效果: 通过此标准化手动流程,开发者可以成功将UniApp项目以鸿蒙元服务的形式运行在真机上,完成开发和调试工作。· 适用场景: 当前阶段所有使用UniApp开发鸿蒙元服务的项目。
-
在 Vue 中,不能直接侦听响应式对象的属性值(如 obj.property),而需要使用返回该属性的 getter 函数(如 () => obj.property),这主要与 Vue 的响应式系统实现机制和 JavaScript 的限制有关。以下是具体原因:1. JavaScript 对象的限制在 JavaScript 中,直接传递一个属性引用(如 obj.property)时,你传递的是该属性的当前值,而不是对属性的引用。Vue 无法自动追踪这个值的变化,因为它只是一个静态值。例如:const obj = reactive({ count: 0 }); const value = obj.count; // 这里只是读取了当前值 0 // 后续 obj.count 变化时,value 仍然是 0,没有绑定关系 2. Vue 的响应式依赖收集Vue 的响应式系统(如 watch 或 computed)需要在数据变化时重新执行回调函数。为了实现这一点,它需要在读取数据时收集依赖。当你使用 getter 函数(如 () => obj.property)时,Vue 会在函数执行时(即读取 obj.property 时)捕获当前的响应式依赖,从而建立绑定关系。直接传递属性值无法触发这种依赖收集。3. Getter 函数的动态性Getter 函数(如 () => obj.property)是一个动态操作,每次执行时都会重新读取属性的当前值。Vue 可以通过拦截这个读取操作(通过 Proxy 或 Object.defineProperty)来追踪变化。例如:watch( () => obj.count, // 每次 obj.count 被读取时,Vue 会记录这个依赖 (newVal) => { console.log('count changed:', newVal); } ); 4. 深层嵌套属性的侦听如果直接侦听 obj.property,Vue 无法区分你是想侦听 property 本身还是 obj 的变化。而通过 getter 函数,可以明确指定侦听的目标:// 侦听 obj.property 的变化 watch(() => obj.property, (newVal) => { ... }); // 侦听整个 obj 的变化(包括新增/删除属性) watch(obj, (newObj) => { ... }, { deep: true }); 5. 与 computed 的设计一致性Vue 的 computed 属性也要求通过函数返回依赖值,这样可以在依赖变化时自动重新计算。watch 的 getter 函数设计与之保持一致,简化内部实现。替代方案:直接传递 ref如果是 ref 对象,可以直接传递 .value 的引用(因为 ref 本身是一个包装对象,Vue 内部通过 value 属性实现响应式):const count = ref(0); watch(count, (newVal) => { ... }); // 等价于 watch(() => count.value, ...) 但对于普通响应式对象(reactive),仍然需要 getter 函数。总结Vue 要求通过 getter 函数侦听属性值,是因为:JavaScript 无法直接传递属性引用。Getter 函数允许 Vue 在运行时捕获依赖。这种设计保持了响应式系统的统一性和可预测性。如果需要侦听嵌套属性,还可以使用字符串路径(如 watch(obj, 'property', ...),但 getter 函数是更推荐的方式。
-
在 Vue 中,不能直接侦听响应式对象的属性值(如 obj.property),而需要使用返回该属性的 getter 函数(如 () => obj.property),这主要与 Vue 的响应式系统实现机制和 JavaScript 的限制有关。以下是具体原因:1. JavaScript 对象的限制在 JavaScript 中,直接传递一个属性引用(如 obj.property)时,你传递的是该属性的当前值,而不是对属性的引用。Vue 无法自动追踪这个值的变化,因为它只是一个静态值。例如:const obj = reactive({ count: 0 }); const value = obj.count; // 这里只是读取了当前值 0 // 后续 obj.count 变化时,value 仍然是 0,没有绑定关系 2. Vue 的响应式依赖收集Vue 的响应式系统(如 watch 或 computed)需要在数据变化时重新执行回调函数。为了实现这一点,它需要在读取数据时收集依赖。当你使用 getter 函数(如 () => obj.property)时,Vue 会在函数执行时(即读取 obj.property 时)捕获当前的响应式依赖,从而建立绑定关系。直接传递属性值无法触发这种依赖收集。3. Getter 函数的动态性Getter 函数(如 () => obj.property)是一个动态操作,每次执行时都会重新读取属性的当前值。Vue 可以通过拦截这个读取操作(通过 Proxy 或 Object.defineProperty)来追踪变化。例如:watch( () => obj.count, // 每次 obj.count 被读取时,Vue 会记录这个依赖 (newVal) => { console.log('count changed:', newVal); } ); 4. 深层嵌套属性的侦听如果直接侦听 obj.property,Vue 无法区分你是想侦听 property 本身还是 obj 的变化。而通过 getter 函数,可以明确指定侦听的目标:// 侦听 obj.property 的变化 watch(() => obj.property, (newVal) => { ... }); // 侦听整个 obj 的变化(包括新增/删除属性) watch(obj, (newObj) => { ... }, { deep: true }); 5. 与 computed 的设计一致性Vue 的 computed 属性也要求通过函数返回依赖值,这样可以在依赖变化时自动重新计算。watch 的 getter 函数设计与之保持一致,简化内部实现。替代方案:直接传递 ref如果是 ref 对象,可以直接传递 .value 的引用(因为 ref 本身是一个包装对象,Vue 内部通过 value 属性实现响应式):const count = ref(0); watch(count, (newVal) => { ... }); // 等价于 watch(() => count.value, ...) 但对于普通响应式对象(reactive),仍然需要 getter 函数。总结Vue 要求通过 getter 函数侦听属性值,是因为:JavaScript 无法直接传递属性引用。Getter 函数允许 Vue 在运行时捕获依赖。这种设计保持了响应式系统的统一性和可预测性。如果需要侦听嵌套属性,还可以使用字符串路径(如 watch(obj, 'property', ...),但 getter 函数是更推荐的方式。
-
在 Vue 的事件修饰符中,系统按键修饰符(如 .ctrl、.alt、.shift、.meta)与常规按键修饰符(如 .enter、.esc)的行为不同,尤其是在与 keyup 事件配合使用时。这种差异源于浏览器原生事件的机制和 Vue 的封装逻辑。以下是详细解释和示例:🔍 核心区别1. 常规按键修饰符(如 .enter)行为:直接监听特定按键的 keyup 或 keydown 事件。示例:<input @keyup.enter="submitForm" /> 当用户按下 Enter 键并松开时,submitForm 会被触发。不依赖其他按键状态:无论是否同时按住 Ctrl、Shift 等,只要松开 Enter 就会触发。2. 系统按键修饰符(如 .ctrl)行为:监听 组合键 的 keyup 事件,且要求目标按键松开时,系统修饰键(如 Ctrl)必须仍处于按下状态。示例:<div @keyup.ctrl="handleCtrlRelease">松开 Ctrl 键</div> 不会触发:如果你单独松开 Ctrl 键(没有其他按键操作),handleCtrlRelease 不会被调用。会触发:如果你按住 Ctrl + A,然后松开 A(此时 Ctrl 仍按住),keyup.ctrl 会触发。💡 为什么这样设计?浏览器原生行为:系统修饰键(Ctrl、Alt 等)通常用于组合快捷键(如 Ctrl+C)。浏览器默认不会为单独按下/松开 Ctrl 触发 keyup 事件,除非它与其他键组合使用。Vue 的修饰符遵循了这一逻辑。避免误触:如果 keyup.ctrl 在单独松开 Ctrl 时触发,可能会导致意外行为(例如用户无意松开 Ctrl 时触发操作)。🛠️ 如何监听单独松开 Ctrl 键?如果需要检测 Ctrl 键的单独松开(不依赖其他键),可以使用原生 keydown/keyup 事件配合 event.key 或 event.ctrlKey:方法 1:直接监听 keyup 并检查 event.ctrlKey<div @keyup="handleKeyUp">监听所有按键松开</div> methods: { handleKeyUp(event) { if (event.key === 'Control') { console.log('Ctrl 键被松开'); } } } 方法 2:使用 @keyup.exact(精确匹配无修饰键)如果需要确保 Ctrl 是唯一被按下的键时才触发:<div @keyup.exact.ctrl="handleExactCtrl">仅当松开 Ctrl 且无其他键按下时触发</div> 📌 完整示例对比场景 1:监听 Ctrl + S 组合键松开<input @keyup.ctrl.s="saveData" placeholder="按 Ctrl+S 保存" /> 触发条件:按住 Ctrl + S,然后松开 S(Ctrl 仍按住)。场景 2:监听单独松开 Ctrl 键<div @keyup="checkCtrlRelease" tabindex="0">点击后松开 Ctrl</div> methods: { checkCtrlRelease(event) { if (event.key === 'Control') { console.log('Ctrl 键被单独松开'); } } } ⚠️ 注意事项tabindex 属性:非输入元素(如 <div>)需要添加 tabindex="0" 才能接收键盘事件。浏览器兼容性:event.key 的值(如 'Control')在所有浏览器中一致,但旧版浏览器可能用 keyCode(已废弃)。Vue 的 .exact 修饰符:可以精确控制触发条件,例如 @keyup.ctrl.exact 仅在松开 Ctrl 且无其他修饰键时触发。📝 总结修饰符类型示例触发条件常规按键@keyup.enter松开 Enter 键时触发,无关其他按键状态。系统按键@keyup.ctrl松开某个键时,Ctrl 必须仍处于按下状态(通常用于组合键)。精确匹配@keyup.exact.ctrl仅当松开 Ctrl 且无其他修饰键(如 Shift、Alt)时触发。原生事件检查@keyup + event.key通过 JavaScript 检查 event.key === 'Control' 来监听单独松开 Ctrl。根据需求,选择合适的方式:如果是组合键逻辑,用系统修饰符;如果是单独监听 Ctrl,用原生事件检查。
-
在 Vue 3 的 Composition API 中,ref 和 reactive 都可以用于创建响应式数据,但在你的例子中,使用 ref 而不是 reactive 来包装数组 numbers 主要有以下几个原因:1. ref 更通用,适合任意类型的数据ref 可以包装任意类型的数据(基本类型、对象、数组等),而 reactive 只能包装对象(包括数组,因为数组在 JS 中也是对象)。你的 numbers 是一个数组,虽然 reactive 也能处理数组,但 ref 的通用性更强,尤其是在不确定数据类型会如何变化时(比如未来可能从数组变成基本类型)。2. ref 的 .value 操作更明确ref 通过 .value 访问和修改数据,这种显式操作在复杂逻辑中更清晰,尤其是当你在多个地方使用 numbers 时,可以一眼看出哪些地方在修改响应式数据。例如:numbers.value.push(6); // 明确知道是在修改响应式数据 如果用 reactive,直接操作数组(如 numbers.push(6))可能会让代码的可读性下降(尤其是当 numbers 是从函数返回或解构出来时)。3. computed 的依赖追踪更直观在 computed 中,ref 的 .value 变化会被精确追踪。虽然 reactive 也能触发 computed 更新,但在某些嵌套对象或复杂场景下,ref 的依赖追踪更可靠。你的例子中,evenNumbers 依赖于 numbers.value,用 ref 可以确保 numbers.value 的任何变化(包括整个数组被替换)都能正确触发计算。4. 解构和传递更灵活ref 可以直接解构或传递,而不会丢失响应性(只要保留 .value 的引用)。例如:const { value: numbersValue } = numbers; // 解构后仍可通过 numbersValue 操作 如果用 reactive,解构后会丢失响应性(除非用 toRefs)。5. Vue 官方示例的惯例Vue 官方文档和示例中,基本类型或数组/对象可能变化时,更倾向于用 ref,因为它的行为更一致(总是通过 .value 访问)。reactive 更多用于明确是对象且结构稳定的场景(如一个包含多个属性的配置对象)。❓ 什么时候会用 reactive?如果 numbers 是一个对象,且你需要直接操作它的属性(而不是替换整个对象),reactive 可能更合适:const state = reactive({ numbers: [1, 2, 3, 4, 5], otherData: "foo" }); // 直接操作属性 state.numbers.push(6); // 无需 .value 📌 总结特性refreactive适用类型任意类型(基本类型、对象、数组)仅对象(包括数组)访问方式通过 .value直接访问属性解构/传递需保留 .value 引用需用 toRefs 避免丢失响应性替换整个数据直接赋值 numbers.value = [...]需特殊处理(如 Object.assign)代码清晰度显式 .value 更明确隐式响应式可能让人困惑你的例子中:numbers 是一个数组,且可能被整体替换(如 numbers.value = [10, 20]),用 ref 更安全。如果未来 numbers 的结构变得更复杂(比如变成一个对象 { list: [1, 2, 3], loading: false }),用 reactive 可能更合适。🛠️ 最佳实践建议:优先用 ref,除非你明确需要一个响应式对象且不需要替换整个对象。如果数据是对象且属性需要直接解构,用 reactive + toRefs:import { reactive, toRefs } from "vue"; const state = reactive({ numbers: [1, 2, 3] }); const { numbers } = toRefs(state); // 解构后仍保持响应性 在模板中,ref 会自动解套(无需 .value),所以两者在模板里的用法几乎一样。
-
在 Vue 中,如果组件有多个根元素(即多根节点组件),当父组件传递 class 或 style 时,Vue 不会自动将这些属性合并到所有根节点上,而是需要你显式指定哪个根元素接收这些属性。这是 Vue 3 的一个重要行为变化(Vue 2 不支持多根节点组件)。📌 问题场景假设有一个多根节点的组件:<!-- MyComponent.vue --> <template> <header>标题</header> <main>内容</main> <footer>页脚</footer> </template> 当父组件传递 class 时:<MyComponent class="parent-class" /> Vue 会发出警告:[Vue warn]: Component has multiple root nodes but received `class` attribute. The `class` will be discarded since the component doesn't have a single root element. 🔧 解决方案1. 使用 $attrs 手动绑定到特定根节点在 Vue 3 中,可以通过 $attrs 访问父组件传递的 class 和 style,然后手动绑定到某个根节点:<!-- MyComponent.vue --> <template> <header :class="$attrs.class">标题</header> <main>内容</main> <footer>页脚</footer> </template> 此时,parent-class 会被应用到 <header> 上。2. 使用 v-bind="$attrs" 批量绑定如果希望将所有父组件传递的属性(包括 class、style 等)绑定到某个根节点:<!-- MyComponent.vue --> <template> <div> <header v-bind="$attrs">标题</header> <main>内容</main> <footer>页脚</footer> </div> </template> 或者更灵活地选择目标节点:<template> <header>标题</header> <main v-bind="$attrs">内容</main> <footer>页脚</footer> </template> 3. 用 :class 动态合并类名如果需要合并父组件的 class 和组件自身的 class:<template> <header :class="['my-header', $attrs.class]">标题</header> <main>内容</main> </template> 4. 改用单根节点(推荐)如果逻辑允许,最佳实践是始终使用单根节点组件,避免多根节点的复杂性:<template> <div class="wrapper"> <header>标题</header> <main>内容</main> <footer>页脚</footer> </div> </template> 此时父组件的 class 会自动合并到根节点的 <div> 上。📝 关键点总结方案适用场景示例$attrs.class手动指定某个根节点接收 class<header :class="$attrs.class">v-bind="$attrs"批量绑定所有父组件属性到某个节点<main v-bind="$attrs">动态 :class合并父组件和自身的 class<header :class="['my-class', $attrs.class]">单根节点长期维护性最佳(推荐)用 <div> 包裹所有内容⚠️ 注意事项Vue 2 的限制:Vue 2 不支持多根节点组件,必须用单根节点。style 的处理:与 class 类似,父组件传递的 style 也需要通过 $attrs 显式绑定。Scoped CSS:如果组件启用了 scoped,父组件的 class 仍然可以作用到子组件的多根节点上(通过 $attrs 绑定后)。🛠️ 最终建议优先使用单根节点,避免多根节点的复杂性。如果必须多根节点,通过 $attrs 显式控制 class 和 style 的去向。在复杂组件中,可以结合 useAttrs()(Composition API)更灵活地处理属性:import { useAttrs } from 'vue'; export default { setup() { const attrs = useAttrs(); return { attrs }; } } <template> <header :class="attrs.class">标题</header> </template> 通过这种方式,你可以精确控制多根节点组件中父组件属性的应用位置。
-
在 Vue 的 v-for 中,key 的使用至关重要,但并非所有场景都需要显式指定 key。以下是详细的判断标准和最佳实践:📌 必须使用 key 的场景1. 列表项有动态变化(增删改、排序、过滤)原因:Vue 默认通过 key 跟踪每个节点的身份,从而高效地复用、重新排序或移除元素。如果没有 key,Vue 会使用“就地复用”策略,可能导致状态错乱(如输入框内容交换、动画异常等)。示例:<!-- 错误:无 key,交换顺序后输入框内容会错乱 --> <div v-for="(item, index) in list" :key="index"> <input v-model="item.value"> </div> <!-- 正确:用唯一 id 作为 key --> <div v-for="item in list" :key="item.id"> <input v-model="item.value"> </div> 2. 列表项包含有状态的组件(如 <input>、<select>、自定义组件)原因:组件内部状态(如输入值、选中项)需要与数据绑定。如果无 key,Vue 可能复用错误的组件实例,导致状态丢失或混乱。示例:<!-- 错误:无 key,切换选项后选中状态可能错误 --> <div v-for="item in options" :key="null"> <!-- 隐式使用 index 作为 key --> <CustomSelect v-model="selectedValue" :options="item.options" /> </div> <!-- 正确:用唯一 id 作为 key --> <div v-for="item in options" :key="item.id"> <CustomSelect v-model="selectedValue" :options="item.options" /> </div> 3. 需要动画或过渡效果(如 <transition-group>)原因:Vue 的过渡系统依赖 key 来区分新旧节点,从而正确触发进入/离开动画。示例:<transition-group name="fade" tag="ul"> <li v-for="item in list" :key="item.id">{{ item.text }}</li> </transition-group> ❌ 可以省略 key 的场景1. 静态列表(无动态变化)条件:列表内容固定,不会增删改、排序或过滤,且列表项是无状态的(如纯展示文本)。示例:<!-- 可省略 key(但建议仍用唯一值,避免潜在问题) --> <ul> <li v-for="item in ['苹果', '香蕉', '橙子']">{{ item }}</li> </ul> 2. 性能敏感且列表项完全相同(无状态)条件:列表项是纯展示的,且内容完全一致(如重复渲染相同组件)。此时 Vue 复用 DOM 能提升性能。示例:<!-- 渲染 100 个相同的占位符 --> <div v-for="i in 100" :key="null"> <!-- 显式禁用 key,强制复用 --> <PlaceholderComponent /> </div> 注意:这种场景极少见,且需谨慎使用,因为可能隐藏潜在问题。🔍 key 的最佳实践优先使用唯一标识符:如数据库 ID、UUID 等,而非数组索引(index)。<!-- 推荐 --> <div v-for="user in users" :key="user.id">{{ user.name }}</div> <!-- 不推荐(索引可能变化) --> <div v-for="(user, index) in users" :key="index">{{ user.name }}</div> 避免用随机数作为 key:随机数在每次渲染时会变化,导致 Vue 强制重新创建所有节点,性能极差。<!-- 错误:每次渲染 key 都不同 --> <div v-for="item in list" :key="Math.random()">{{ item.text }}</div> 在 <transition-group> 中必须用 key:否则动画会无法正常工作。📝 总结表场景是否需要 key推荐做法动态列表(增删改、排序)✅ 必须用唯一 ID(如 item.id)有状态组件(如 <input>)✅ 必须用唯一 ID,避免依赖 DOM 状态需要动画(<transition-group>)✅ 必须用唯一 ID静态列表(无变化)❌ 可省略可省略,但建议仍用唯一值性能敏感且列表项完全相同❌ 可省略显式设为 null 强制复用(谨慎使用)核心原则:只要列表可能变化或包含状态,就必须用 key;否则可能引发状态错乱或动画异常。 即使省略 key,Vue 也会隐式使用 index,但这通常不是最佳选择。
-
❌ 问题场景:表单输入错乱假设我们有一个列表,渲染两个输入框,用户可以在输入框中输入内容:<template> <div> <button @click="swapItems">交换列表顺序</button> <div v-for="(item, index) in list" :key="index"> <input :placeholder="item.placeholder"> </div> </div> </template> <script>export default { data() { return { list: [ { id: 1, placeholder: "请输入姓名" }, { id: 2, placeholder: "请输入年龄" } ] }; }, methods: { swapItems() { // 交换列表中两项的顺序 this.list.reverse(); } } }; </script> 操作步骤:用户在第一个输入框(“请输入姓名”)输入 "张三",在第二个输入框(“请输入年龄”)输入 "25"。点击“交换列表顺序”按钮,list 数组顺序被反转。Vue 默认会复用 DOM 元素(因为 key 用了 index,而索引 0 和 1 只是交换了位置)。问题表现:用户输入的内容 会跟着 DOM 元素移动,导致:原本输入 "张三" 的输入框(第一个)现在变成了 "25"(因为复用了第二个 <input>)。原本输入 "25" 的输入框现在变成了 "张三"。用户看到的输入内容突然交换了,但实际数据(list)并没有变,只是 DOM 被复用了。🔍 原因分析Vue 默认通过 key 来判断是否复用 DOM 元素:如果 key 是 index(如 :key="index"),交换数组顺序后,索引 0 和 1 只是换了位置,Vue 会认为这两个 <input> 可以直接复用,只是移动了位置。但 <input> 的当前值(用户输入的内容)是 DOM 的临时状态,Vue 不会自动帮你保存或同步它!结果就是:输入框的值跟着 DOM 元素移动,导致错乱。✅ 正确做法:唯一 key + 数据驱动方法 1:用唯一 id 作为 key,并避免依赖 DOM 状态<template> <div> <button @click="swapItems">交换列表顺序</button> <div v-for="item in list" :key="item.id"> <!-- 用 item.id 作为 key,且输入值绑定到数据 --> <input :placeholder="item.placeholder" v-model="item.value"> </div> <p>当前数据:{{ list }}</p> </div> </template> <script>export default { data() { return { list: [ { id: 1, placeholder: "请输入姓名", value: "" }, { id: 2, placeholder: "请输入年龄", value: "" } ] }; }, methods: { swapItems() { this.list.reverse(); // 交换顺序 } } }; </script> 关键改进:key 使用唯一标识(item.id):Vue 能准确知道每个列表项的身份,不会错误复用 DOM。输入值绑定到数据(v-model):用户输入的内容会实时同步到 list 的 value 字段,而不是依赖 DOM 的临时状态。交换顺序后:Vue 会根据 key 重新匹配 DOM 元素,但输入值已经保存在数据中,不会错乱。📌 总结问题场景(错误)正确做法用 index 作为 key用唯一 id 作为 key依赖 DOM 临时状态(如 <input> 的当前值)用 v-model 绑定到数据交换顺序后输入框内容错乱数据和 DOM 正确同步,无错乱核心原则:Vue 的默认复用策略要求列表渲染结果不依赖 DOM 的临时状态。如果列表项有交互(如表单输入),必须用 v-model 绑定数据,并用唯一 key 确保正确复用。这样就能避免“输入内容突然跑到其他输入框”的诡异问题了!
-
template是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template> <template v-if="ok"> 改为 <div v-if="ok">,在最终渲染的 DOM 结构中,效果看起来可能是一样的(即三个元素都会根据 ok 的值显示或隐藏),但存在一些关键区别:1. DOM 结构差异<template>:作为不可见的包装器,Vue 在渲染时不会创建额外的 <template> DOM 元素。最终渲染结果直接是:<h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> (如果 ok 为 true)<div>:会多出一个 <div> 包装元素,渲染结果为:<div> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </div> (如果 ok 为 true)2. 样式和布局影响如果外层样式或布局依赖于 DOM 结构(例如 CSS 选择器 div > h1),使用 <div> 可能会意外影响样式或布局。<template> 完全不会影响现有样式,因为它不存在于 DOM 中。3. 语义化<template> 更适合纯逻辑分组,不引入多余的语义。<div> 有语义含义(表示一个块级容器),可能不适合所有场景。4. 性能<template> 略微更高效,因为 Vue 会跳过它的实际 DOM 创建。何时用 <div>?如果确实需要一个包装元素(例如为了应用共同的样式或事件监听),或者需要兼容不支持 <template> 的旧版本 Vue,可以用 <div>。总结特性<template v-if><div v-if>渲染额外 DOM 元素❌ 无✅ 有影响样式/布局❌ 不会✅ 可能语义化✅ 更纯净⚠️ 有语义适用场景逻辑分组需要容器时建议:如果只是单纯切换多个元素的显示/隐藏,优先使用 <template v-if>,避免不必要的 DOM 节点。
-
Vue开发,你们现在一般用哪个UI库
推荐直播
-
用码道,让你的AI作品三步上朋友圈2026/08/04 周二 19:00-20:00
林华鼎-华为云AI开发者运营负责人
从入门 · 到做AI应用 · 到企业级开发。不教编程,只教用AI · 零代码、有产出、能带走、可炫耀 · 每课人人动手实操
回顾中 -
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
基于华为云码道,构建你的定制化AI搭子2026/08/14 周五 09:00-11:30
明亮-华为云开发者发展与支持部部长
本期直播将向您全面介绍华为云码道产品,并基于码道手把手教你部署自己的定制化AI陪伴搭子。
回顾中
热门标签