资讯动态

用Python+Pygame+OpenCV+GPT打造桌面虚拟数字人

发布时间:2026/9/1 18:43:24 来源:尧图企业网站定制
简介本资源是一个基于Python实现的轻量级虚拟数字人直播系统面向AI初学者、计算机视觉与人机交互方向的学习者及数字内容创作者解决实时驱动虚拟形象并融合语音交互的核心问题。项目整合OpenCV进行人脸/动作捕捉、Pygame渲染2D虚拟人动画、GPT提供文本响应逻辑并配套音视频素材实现端到端演示。压缩包共66个文件含38张PNG格式动作帧图用于序列动画、15个MP4口型/动作参考视频、9段WAV音频背景音、音效与语音提示、1个中文字体文件及1个主控Python脚本整体大小39.46MB结构清晰便于理解动画驱动与多模态协同流程。已有4337人学习下载读者可直接运行man.py启动系统获得完整可执行代码、预渲染动作资源、多场景背景图及配套音效库快速掌握虚拟数字人基础架构与跨模块集成方法。 去年年底朋友拉我一起做虚拟直播问我能不能不碰Unity、不写C#就用Python把这套东西跑起来。我下意识觉得可以真动手以后才发现问题不是某个库能不能用而是怎么把摄像头、窗口渲染、对话模型这三件事在同一个进程里流畅地串起来。最终定型的方案就是标题里这套组合Python做胶水层Pygame管窗口和交互OpenCV处理镜头画面GPT负责对话。整套东西做下来我最大的感受是虚拟数字人技术栈的入门门槛比大多数人想象的低得多但坑也比想象的多。这篇文章不聊商业产品只聊一个个人能跑通的桌面端数字人原型摄像头拍你画面里出现一个处理过的虚拟形象它能接住你的话茬跟你聊天窗口布局像直播伴侣的简化版。适合正在学Python、想做个有趣项目练手或者打算用最低成本验证虚拟形象交互方案的朋友我把整套拆解和踩坑过程都放在下面。1. 四个库各管一摊这个数字人的技术分工很多人在类似项目里最容易犯的错是拿到一个库就想让它干所有事。比如有人用OpenCV的窗口函数写界面结果按钮、事件、文字渲染全要自己造轮子做两天就放弃了。还有人让Pygame去读摄像头勉强能显示但图像处理一复杂帧率直接崩。这项目的核心思路是每个库只做自己最擅长的一件事。Pygame的活是窗口、事件循环、文字气泡、头像渲染和音频播放。它有稳定的帧循环、成熟的Surface机制做交互界面比OpenCV的highgui好用一个量级也比Tkinter更适合做需要刷新率的东西。OpenCV的活是摄像头读取、背景替换、人脸定位和图像增强。它背后是C优化过的图像处理管线处理一帧640x480的图只需要几毫秒但如果你跳过错把它拿去做游戏循环那体验就完全不一样了。GPT的活纯粹是对话文本生成。它不碰图像也不碰声音只接收文字返回文字。那些“数字人有表情”“数字人能动”的效果全是用OpenCV和Pygame在GUI层模拟出来的。Python本身的角色是胶水。一个主循环里摄像头线程负责喂帧GPT线程负责等回复Pygame主循环负责渲染三条线通过队列通信互不阻塞。我最初的天真想法是“全部同步执行”摄像头读一帧、做处理、显示、再问一次GPT、等回复、再显示。结果就是画面卡到像放PPT而且每次聊天界面就冻结连窗口拖拽都做不到。后来把GPT请求丢进子线程整个体验立刻脱胎换骨。这一步是这类项目能不能用的分水岭后面在第五章会详说线程与队列的设计。一句话总结技术分工OpenCV造画面Pygame做脸面GPT长脑子Python牵线。你只要让每个模块做自己擅长的事整个系统自然就顺了。2. 环境准备与工程骨架先跑通一个能动的画面环境这块我吃过不少亏。第一次是在Windows上直接装最新版Python 3.12然后pip install pygame编译wheel的时候报了一堆错网上一查是pygame官方还没适配那个Python版本。后来把解释器降到Python 3.10一切顺了。这一步极其关键遇到pygame编译问题先别急着折腾依赖大概率是Python版本太新了。推荐的依赖版本组合组件版本建议说明Python3.10.x兼容性最好各库wheel齐全pygame2.5.x支持Windows/Linux/macOSopencv-python4.8.x不要装opencv-contrib除非真需要mediapipe0.10.x用于人像分割比OpenCV自带方案好requests最新即可调用GPT接口edge-tts最新即可可选用于语音合成安装命令我用一条搞定pip install pygame opencv-python mediapipe requests edge-tts如果你在安装opencv时特别慢换国内镜像源会快得多pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple项目目录我建议按模块拆分不要一个main.py塞几百行virtual_avatar/ ├── main.py # 主循环与调度 ├── modules/ │ ├── camera.py # 摄像头读取与图像处理 │ ├── avatar.py # 虚拟形象渲染 │ ├── dialogue.py # GPT对话管理 │ └── voice.py # 语音合成与播放main.py只干一件事初始化所有模块然后进Pygame主循环。每个模块提供两个接口——update()和draw()主循环每帧调用。这样做的好处是后面你想替换任何模块比如不用GPT改用本地模型只需要改modules里对应文件主循环一行都不用动。开工第一件事先写一个最小的Pygame窗口打开摄像头画面的代码验证整个链路是通的import pygame import cv2 import numpy as np pygame.init() screen pygame.display.set_mode((960, 540)) pygame.display.set_caption(Virtual Avatar Dev) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False ok, frame cap.read() if not ok: continue # 镜像翻转否则画面左右颠倒 frame cv2.flip(frame, 1) # OpenCV是BGRPygame要RGB rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb np.ascontiguousarray(rgb) frame_surf pygame.image.frombuffer(rgb.tobytes(), rgb.shape[1::-1], RGB) screen.blit(frame_surf, (0, 0)) pygame.display.flip() pygame.time.Clock().tick(30) cap.release() pygame.quit()这里有两处容易踩坑OpenCV读出来的是BGR不是RGB直接显示颜色会发蓝摄像头像素格式是底层的连续缓冲区转成pygame Surface之前必须保证数组是C连续内存np.ascontiguousarray就是干这个的。我第一次没加这一步程序直接崩了报错信息指向缓冲区不连续。这段代码能跑通说明你的显卡驱动、摄像头驱动、Pygame和OpenCV的协作没问题后续所有功能都建立在这个基础上。3. OpenCV图像管道把摄像头画面变成虚拟人台数字人的第一步是让镜头里的“你”变成画面里的“虚拟形象”。这个形象可以是保留真人轮廓但换掉背景也可以是简化成卡通头像跟随着人脸移动。我建议从背景替换入手因为视觉效果最直观做出来像直播间的虚拟偶像。3.1 人像分割换背景的两种方案OpenCV自带两种背景分割器createBackgroundSubtractorMOG2和createBackgroundSubtractorKNN。它们适合固定摄像头的场景比如监控里人走过去背景是恒定不变的。但数字人交互场景中人会动、有阴影、光线会变两种算法表现都不尽如人意边缘会出现严重的抖动和空洞。我后来用了MediaPipe的SelfieSegmentation一个专门针对人像的语义分割模型不需要GPU也能跑到实时。它的原理是输出一张每个像素属于人的概率图然后设定阈值决定保留前景还是替换背景人体边缘的头发丝也能处理得很干净。import cv2 import numpy as np import mediapipe as mp class BackgroundReplacer: def __init__(self, bg_color(32, 42, 68)): self.bg_color bg_color self.selfie mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection1) def process(self, frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result self.selfie.process(rgb) mask result.segmentation_mask # 阈值大于0.6的像素保留真人其余换成纯色背景 condition mask 0.6 bg np.zeros_like(frame) bg[:] self.bg_color # 这个函数在mask为True时取frame像素否则取bg像素 output np.where(condition[..., None], frame, bg) return output关于model_selection参数0号模型是通用场景1号模型是针对近距离人物交互优化的测试下来在桌面镜头下1号模型的边缘质量明显更好推荐直接用1。我想顺带说一个OpenCV的热门操作equalizeHist。如果你发现摄像头画面在逆光或暗光环境下人像面部一团黑可以先把帧转成灰度图做直方图均衡再合回彩色通道效果立竿见影。但要注意别对整个画面做很容易把背景噪点也强化正确的做法是只对检测到人脸的区域做局部均衡再拼回原图。这个细节能让暗光环境下的面部识别率大幅提升。3.2 人脸跟随让数字人“看”向你的方向光有背景替换还不够数字人应该随着你的位置变化产生反应。用OpenCV的Haar级联检测人脸在画面中的位置映射成虚拟形象的头部偏移量效果很像视频会议里的“说话人跟踪”。face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def get_face_offset(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) if len(faces) 0: return 0, 0 x, y, w, h faces[0] # 将人脸中心映射到[-0.5, 0.5]区间 cx (x w / 2) / frame.shape[1] - 0.5 cy (y h / 2) / frame.shape[0] - 0.5 return cx, cy这个偏移量传给Pygame渲染模块让虚拟头像的位置跟着人脸动。我做的时候给偏移量加了一个低通滤波也就是每一帧只移动偏移量的20%不然头像会像失控一样来回抖。低通滤波的系数选多少要看实际感受我调到0.2到0.3之间画面既跟手又不晃。3.3 图像增强让画面在线感更好数字人直播感很重要的一个来源是磨皮和提亮OpenCV里有现成的高斯模糊加边缘保留操作。最简单的磨皮手段是双边滤波保留边缘的同时平滑皮肤区域frame cv2.bilateralFilter(frame, 9, 75, 75)当然这一步骤对性能有影响在低性能电脑上可以换成小半径的高斯模糊效果略差但帧率能保住。我现在做的时候是把磨皮强度做成可配置项发布前根据目标机器性能调优。这里值得留意的是处理后的画面应该尽量保持真实感虚化过度的脸既不好看也没有说服力观众一眼就觉得是美颜滤镜开满反而降低了数字人的可信度。4. Pygame渲染与交互窗口里的人和对话气泡OpenCV处理完画面以后剩下的渲染和交互全交给Pygame。我第一次用cv2.imshow来做窗口发现想加一个圆角对话框比登天还难而且Pygame的游戏循环天然适合这种实时刷新场景它的事件系统能直接拿到键盘、鼠标、窗口关闭等所有状态交互起来非常顺手。4.1 帧画面与Surface对象这里有个绕不开的性能点cv2的帧是numpy数组pygame的渲染对象是Surface两者需要转换。我测试了三种方式方式代码帧率640x480备注frombufferpygame.image.frombuffer(bytes, size, RGB)60推荐内存拷贝一次surfarraypygame.surfarray.make_surface(arr)60需要转数组方向复杂逐像素循环设置10千万别用def cv2_to_surface(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb np.ascontiguousarray(rgb) return pygame.image.frombuffer(rgb.tobytes(), rgb.shape[1::-1], RGB)每次调用rgb.tobytes()是一次拷贝但640x480的图很小实测在普通笔记本上完全没问题。如果你的摄像头是1080p建议先压缩到720p再处理否则从OpenCV到Pygame的转换开销和后续blit的GPU显存占用会明显上涨。4.2 场景布局与上下层关系窗口我设计成960x540左侧是处理后的摄像头画面右侧是虚拟形象的形象区加对话气泡。这样既方便调试也能让用户同时看到原始信号和数字人形象对做产品demo很有用。Pygame的渲染顺序是从上往下画的后面画的内容会盖住前面的。我通常这样组织绘制顺序1. 背景底色填充deep color 2. 摄像头画面Surface 3. 右侧信息面板 4. 对话气泡圆角矩形 文本 5. 虚拟形象头像跟随人脸的偏移 6. 底部输入提示条4.3 中文显示与圆角气泡这里有个所有用Pygame做中文项目都会踩的坑pygame.font.Font默认字体没有中文字形直接渲染中文会显示一圈方块。解决方法是加载系统字体。Windows上可以用C:/Windows/Fonts/msyh.ttc微软雅黑macOS用/System/Library/Fonts/PingFang.ttcLinux需要装一个中文字体包然后指定路径。def get_chinese_font(size): import pygame font_path C:/Windows/Fonts/msyh.ttc try: return pygame.font.Font(font_path, size) except: return pygame.font.SysFont(simhei, size)对话框可以画一个圆角矩形再叠文字。Pygame 2.x的draw.rect支持border_radius参数这是做气泡最简单的手段def draw_bubble(surface, text, x, y, max_width280, fontNone, size20): if font is None: font get_chinese_font(size) # 先测量文本宽度确定气泡宽度 words [ch for ch in text] lines [] current_line for ch in words: if font.size(current_line ch)[0] max_width: lines.append(current_line) current_line ch else: current_line ch lines.append(current_line) line_height font.get_height() 4 bubble_w font.size(max(lines, keylambda x: font.size(x)[0]))[0] 30 bubble_h line_height * len(lines) 20 bubble_rect pygame.Rect(x, y, bubble_w, bubble_h) pygame.draw.rect(surface, (255, 255, 255), bubble_rect, border_radius12) for i, line in enumerate(lines): text_surf font.render(line, True, (30, 30, 30)) surface.blit(text_surf, (x 15, y 10 i * line_height)) return bubble_rect麻烦的地方在于Pygame没有自动换行API我必须自己按字符宽度折行。上面这段代码用逐字符判断的方式处理简单的消息足够用。如果你的对话内容偶尔会很长建议把折行逻辑单独抽一个函数输入文本和最大宽度输出字符串列表。4.4 事件系统说话与退出Pygame事件循环里除了响应QUIT我还做了按键输入的简化处理按T键临时弹出文字输入框用Python内置的窗口输入按Enter发送给GPT。做产品原型时这一套完全够用不需要自己实现复杂的文本输入控件。实际交互流程是窗口底部显示“按T说话”用户按一下T键后弹出一个系统输入框输入文字回车文字进入GPT模块返回后渲染到右侧气泡里。这个思路的核心价值在于把复杂的GUI文本输入问题转换成“调用系统输入框”这一个简单操作把时间省下来投入到数字人本身的效果上。如果以后要做出货级产品再考虑深入研究Pygame的文本输入组件或者换用DearPyGui这类方案。5. GPT接入给数字人装上会聊天的脑子对话模块设计的好坏直接决定用户会觉得“这是个聪明的虚拟人”还是“这是个智障问答机”。我从一开始就是把GPT包装成一个独立的对话服务而不是简单地调用一次API。它需要维护上下文、处理超时和失败、并且绝对不能阻塞主渲染循环。5.1 接口设计与上下文管理无论你用的是哪家大模型的服务接口基本都是OpenAI兼容的/chat/completions格式。我在代码里把base_url作为参数这样你既可以用官方地址也能切换到国内服务商提供的兼容接口非常灵活。import requests import threading import queue class DialogueModule: def __init__(self, api_key, base_urlhttps://api.openai.com/v1, modelgpt-4o-mini): self.api_key api_key self.base_url base_url self.model model self.history [ {role: system, content: 你是一个桌面虚拟数字人助手回答简短自然不超过80字。} ] self.reply_queue queue.Queue() def ask_async(self, user_text): self.history.append({role: user, content: user_text}) # 每次请求发完整上下文但只保留最近10轮防止无限膨胀 history_snapshot self.history[-20:] t threading.Thread(targetself._request, args(history_snapshot,), daemonTrue) t.start() def _request(self, history): try: headers { Authorization: fBearer {self.api_key}, Content-Type: application/json } payload { model: self.model, messages: history, temperature: 0.8, max_tokens: 300 } resp requests.post( f{self.base_url}/chat/completions, jsonpayload, headersheaders, timeout30 ) data resp.json() reply data[choices][0][message][content] self.history.append({role: assistant, content: reply}) self.reply_queue.put(reply) except Exception as e: self.reply_queue.put(f[对话服务暂时不可用{e}])几个关键设计点system prompt里明确“简短回答”否则GPT会给你写几百字小作文气泡根本装不下。设定在80字以内测试下来交互节奏正好。history[-20:]意思是保留最近20条消息大约10轮对话。模型有上下文长度上限但更长不意味着更好超过一定轮数后旧的内容对回答质量几乎没有帮助反而白白消耗token。请求放在daemonTrue的子线程里主线程即使关了窗口退出子线程也不会挂住进程不放。5.2 防止阻塞主循环线程与队列GPT请求动不动要一两秒甚至更久这个时间如果放在Pygame主循环里同步等待画面就直接冻结。我用queue.Queue做线程间通信主循环每帧检查队列非空就取回复渲染不阻塞。这套生产者-消费者模型是整个项目不卡顿的核心。主循环里的处理逻辑def update_dialogue(self): try: reply self.dialogue.reply_queue.get_nowait() except queue.Empty: return self.bubble_text reply self.bubble_timer 5.0 # 显示5秒这里加了一个5秒的显示时间防止上次对话内容一直留在屏幕上视线上有干扰。我觉得更优雅的做法是加一个显示状态机空闲、等待回复、展示回复、超时消失四种状态各对应不同气泡颜色视觉效果更专业。5.3 降智问题和接口选择很多人反馈“GPT怎么变笨了”排除模型本身的原因大多数时候是system prompt写得太敷衍。我最初用“你是一个助手”这种空泛设定结果回答经常跑偏。后来把设定改成“你是桌面数字人语气活泼但不过度”并补充“当用户说你好时先简单自我介绍”回答质量立刻上了一个台阶。这属于prompt工程里最容易被忽略的一环预设角色预设场景预设备选行为三项齐全GPT才稳定。模型名称我用的是gpt-4o-mini速度快成本低适合做桌面交互。如果你只是本地测试也可以用更便宜的型号但千万别用最强的模型做原型等待时间长且贵实际产品体验反而不如小模型流畅。模型选型原则是能用小就不上大能用快就不上强。6. 语音与体验打磨让数字人开口“说话”文本对话只是数字人的下半身上半身是听和说。语音合成我选的是edge-tts它免费、音质自然、支持中文发音生成的MP3直接交给pygame.mixer播放就行。整个链路是文本回复 - TTS生成mp3 - 播放 - 气泡同步显示。听起来简单做起来有几个很影响体验的细节。6.1 TTS与播放import asyncio import edge_tts import pygame.mixer VOICE zh-CN-XiaoxiaoNeural async def generate_speech(text, output_pathreply.mp3): communicate edge_tts.Communicate(text, VOICE) await communicate.save(output_path) def speak(text): try: asyncio.run(generate_speech(text)) pygame.mixer.music.load(reply.mp3) pygame.mixer.music.play() except Exception as e: print(TTS error:, e)注意几点pygame.mixer.init()必须在播放前初始化建议固定采样率44100或48000以及输出通道数否则有些音频文件播放会变调edge-tts生成文件是UTF-8的MP3pygame.mixer支持但如果你的本地环境太老可能需要切到旧版MP3编码。我当时在这里踩过一个锲而不舍的坑先播放语音再显示文字气泡给人感觉就像“先听完说话再看到字幕”节奏非常怪。后来改成“先显示气泡再播放语音”人的视觉接收速度比听觉快整体感受自然很多。6.2 对话打断用户正在听数字人说话时按T键输入了新的问题旧的语音还在播就会导致数字人一边说上一句一边处理下一句像一个卡壳的复读机。解决办法是输入新问题时清空队列并停掉当前播放pygame.mixer.music.stop()这一行虽然简单但专业和不专业的体验差距往往就在这种细节上。顺手再做一件事新回复到达时把旧的TTS文件删除或覆盖能省磁盘空间。6.3 虚拟形象状态的实时反馈文字、语音都通了以后我加了三个“状态指示灯”式的视觉反馈等待用户输入时数字人头像显示默认颜色GPT请求中显示一个转圈动画4帧循环回复时头像边缘闪烁微光。这套机制虽然只是画几个圆形和矩形但让整个数字人“活”了起来用户不会在等待的几秒里觉得它死了。状态机的实现思路其实很朴素IDLE - LISTENING - THINKING - SPEAKING - IDLE每次状态切换都伴随视觉效果变化整个交互闭环就成立了。这时候你再去测试会发现自己愿意跟这个数字人多聊几句因为反馈链路完整了。7. 我踩过的坑从阻塞卡顿到系统字体这个项目从0到1我遇到过的坑几乎全在热词搜索里能看到影子比如“安装pygame获取构建wheel的依赖项不成功”“opencv error: the function/feature is not implemented”“pygame.mixer”等。逐条捋一遍。7.1 安装与编译问题最经典的坑是pygame在Python 3.12下没有预编译wheelpip会尝试从源码构建然后漫长的编译过程失败。解决办法是回到Python 3.10或3.11或者用conda环境隔离。还有Linux下面如果缺SDL相关依赖库pygame也会装不上或者运行时闪退需要先安装libsdl2-dev、libsdl2-mixer-dev等系统包。OpenCV安装后运行时常见报错是cv2.error: The function/feature is not implemented我遇到过一次是因为编译的OpenCV里没包含FFmpeg支持导致读不了某些视频格式。解决办法是换官方wheel包或者安装opencv-python-headless纯CPU推理轻量可读帧。7.2 摄像头与画面问题摄像头打不开90%的可能是之前某个进程没有释放摄像头资源重启终端或者杀掉残留的python进程就好。cap.read()偶尔返回空帧直接处理会崩溃所以每次读帧后必须判断是否ok。关于CAP_PROP_FRAME_WIDTH设置不生效这是OpenCV的老毛病部分摄像头驱动对分辨率设置有自己的偏好。解决办法是设置后立刻读一次验证print(cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT))如果设置值没生效摄像头只能支持特定分辨率试着换成640x480或1280x720。7.3 Pygame显示与音频OpenCV帧转Pygame Surface最早我用pygame.image.frombuffer(rgb, size, RGB)后面没有转tobytes()Pygame 2.x版本可以直接接受numpy数组但如果你还带着老版本必须先转bytes。最稳妥的做法是先转bytes再转Surface兼容所有版本。音频播放时遇到变调问题和黑屏一样让人崩溃。有一次用户反馈数字人说话像花栗鼠排查发现是pygame.mixer固定了采样率而edge-tts生成的MP3采样率不是这个值。解决方式是pygame.mixer.pre_init(44100, -16, 2, 512)在init前声明采样率、位深、声道数强制所有音频都按这个标准重采样。7.4 字体与中文显示中文方块问题除了加载系统字体文件之外还有一个细坑中文字体文件巨大微软雅黑有20MB每次启动重新加载会拖慢启动速度。解决办法是启动时加载并缓存所有size的字体对象用字典管理font_cache {} def get_font(size): if size not in font_cache: font_cache[size] get_chinese_font(size) return font_cache[size]7.5 性能瓶颈排查如果整体帧率上不去先别急着优化图像算法用time.time()把每个阶段的耗时打出来。我遇到过帧率突然掉到15帧的情况排查后发现是后台GPT请求触发了内存换页而不是图像处理变慢。后来在对话请求前加了一个节流开关数字人在思考时不处理新帧只保留最后一帧CPU立刻腾出来了。性能排查的工具我用snippets级别的cProfile跑一次性能分析看到耗时在pygame.image.frombuffer的字节拷贝上就把显示分辨率降到480p肉眼观感差别不大帧率从30升到60。如果想要更好性能也可以试pygame.image.frombuffer替换成pygame.surfarray但实测收益有限不建议为此增加复杂度。做这个项目走到现在我最大的心得是虚拟数字人不是某一个算法撑起来的而是图像、渲染、对话、语音四个子系统在时间上的精密配合。最难的部分不是GPT也不是OpenCV而是怎么让它们在同一帧里不打架、不卡顿、不互相拖累。把这套基础架构跑通以后你想换更高级的数字人模型、接入直播推流、甚至做多模态交互都有了稳固的地基。我现在还在继续改的方向是给数字人加上简单的动作触发比如点头、摇头、挥手用OpenCV的手势检测去驱动Pygame里的动画状态位这套架构撑住这些扩展完全不成问题。如果你也在做类似原型照着这个路径往下走一定比我第一次做的顺畅得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价