尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
基于C++与MFC的家谱管理系统设计与实现
简介基于C与MFC构建的图形化家谱管理系统源码包适合正在学习面向对象编程与Windows界面开发的读者也可供普通用户直接编译使用解决家谱成员录入、关系维护与图形化展示等实际问题。整个压缩包共三十九个文件体积仅一百五十一KB核心是十三个头文件和十二个C源文件分别对应家谱成员类定义、MFC对话框逻辑、文档视图架构及数据读写功能同时附带工程配置、资源脚本、可执行程序与PDF说明文档项目结构完整极易在Visual Studio中打开并运行。目前已有一百八十三人学习下载。通过阅读源码既能掌握MFC控件使用、消息映射、文件持久化等技术要点又能基于现有框架进一步扩展树状家谱图、导入导出功能是兼具教学与实用价值的MFC示例项目。对于想入门MFC项目实战的开发者这份代码贴近实际、便于二次开发参考价值较高。1. 为什么用 C 和 MFC 做家谱管理家谱管理系统看起来只是录入姓名和出生日期真正花时间的恰恰是那些不会被写进课程报告的部分成员关系如何在内存里表达CTreeCtrl怎么和底层数据保持同步中文姓名和递归绘制在 Unicode 环境下有哪些隐藏坑。这套jiapu资源把这些问题都压在一个基于 C 与 MFC 的图形化程序里对学习 MFC 的开发者来说比零散刷语法有价值得多对已经写过几年 C 的工程师也是一个观察老派 Windows 桌面程序如何组织数据流的完整样本。下面按数据模型、界面绑定、文件持久化、运行库兼容的顺序拆关键代码可以直接抄到自己的工程里。2. 家谱数据模型从 Person 类到多叉树结构2.1 为什么不用数据库表而是用对象数组小规模家谱通常只有几十到几百人用 SQLite 或 MySQL 反而要多维护一套 schema 和驱动。更自然的做法是把每个成员映射成一个Person对象再用一个std::vectorPerson管理全部成员。这里的关键不是“用类”而是对象之间的关联方式。初学者最容易踩的坑是使用Person*直接指向父亲或孩子但std::vector在扩容时会移动对象原来的指针全部失效。所以更好的做法是给每个人一个稳定的m_id其他字段保存 ID需要真实对象时再通过FindById查找。class Person { public: Person() : m_id(0), m_gender(0), m_parentId(0), m_spouseId(0) {} int m_id; CString m_name; int m_gender; // 0 未知1 男2 女 CString m_birthDate; // 统一保存为字符串便于直接显示 CString m_deathDate; // 为空表示仍健在 int m_parentId; // 血缘父/母 ID根为 0 int m_spouseId; // 配偶 ID无配偶为 0 std::vectorint m_childrenIds; // 孩子 ID按长幼顺序存放 };这样设计有两点考虑。m_parentId虽然只有一条链但通过m_spouseId可以找到再婚配偶及其子女不需要把关系做成图。孩子列表用std::vectorint而不是定长数组因为不同家庭的孩子数量差别很大动态数组可以避免浪费也方便保持长幼顺序。CString在 MFC 环境下能自动处理 Unicode 和 ANSI 转换比std::string在界面交互上省心。FamilyTree类负责所有查找和增删操作核心是线性扫描。几百人时线性查找完全够用没必要引入哈希表但每次从文件加载后都要重建m_childrenIds这个会在第四章详细讲。需要特别说明的是FindById返回Person*有一个隐含条件外部不能在持有指针期间再向m_personspush_back 新成员否则指针地址可能变化。出于安全所有可能扩容的操作都放在一轮刷新之前完成界面在响应按钮事件时不要立刻使用旧指针。2.2 深度优先遍历与增删成员向家谱添加成员的常规步骤是先创建Person并分配 ID然后把它挂到指定父亲的m_childrenIds尾部。删除成员要复杂一些因为不能只删自己的记录还要把它从父亲的m_childrenIds里移除同时它的孩子如何处理也需要约定。这个项目采用的约定是被删除节点的孩子自动上移成被删除节点的父亲的孩子避免出现整支断代。遍历家谱最常见的操作是“按辈分列出所有成员”。递归深度优先遍历是教科书级别的方法但放到这里有一个细节必须用depth参数控制层级显示不能用递归层数本身代替辈分因为同一个人在不同分支可能属于不同代际。代码可以这样写void FamilyTree::Traverse(int id, int depth, std::vectorstd::pairint,int out) { Person* p FindById(id); if (!p) return; out.push_back(std::make_pair(p-m_id, depth)); for (int childId : p-m_childrenIds) { Traverse(childId, depth 1, out); } }这里depth表示相对根的代数根的深度为 0。输出到out后界面可以直接用第二项决定缩进或绘制位置。参数说明id是当前成员 IDdepth用于累计辈分out是结果数组。需要特别注意的是FindById有可能返回空指针比如序列化数据损坏时父节点 ID 指向了不存在的记录所以递归开头必须有if (!p) return;否则后续访问p-m_childrenIds就是未定义行为。插入成员时如果允许把近亲改成夫妻这种操作需要额外校验。常见做法是在AddChild里禁止把某人的后代设置为他的父节点否则递归遍历会死循环。判断方法就是先用IsAncestor检查检查通过后再修改m_parentId和m_childrenIds。2.3 祖先判断与最近公共祖先 LCA“谁是谁的祖先”是家谱关系里最高频的问题实现方式可以直接从当前 ID 沿着m_parentId向上走。最简单的写法是维护一个std::setintbool FamilyTree::IsAncestor(int ancestorId, int descendantId) { int cur descendantId; while (cur ! 0) { if (cur ancestorId) return true; Person* p FindById(cur); cur p ? p-m_parentId : 0; } return false; }这个函数的时间复杂度是O(树高)人员数量不大时没有问题。IsAncestor的返回逻辑需要验证如果descendantId是自己本身也会返回true因为循环第一次就会命中cur ancestorId。如果业务上要区分“祖先”和“本人”就把开始条件改成cur FindById(descendantId)-m_parentId。求两个人的最近公共祖先可以用另一个思路先把 A 到根之间所有 ID 放进集合再从 B 向上走遇到的第一个在集合中的 ID 就是 LCA。int FamilyTree::LowestCommonAncestor(int a, int b) { std::setint path; int cur a; while (cur ! 0) { path.insert(cur); Person* p FindById(cur); cur p ? p-m_parentId : 0; } cur b; while (cur ! 0) { if (path.count(cur)) return cur; Person* p FindById(cur); cur p ? p-m_parentId : 0; } return 0; }这里的边界条件是如果 A 和 B 不在同一棵树里比如分别是两个家庭的根返回 0界面显示“无血缘关系”。如果家谱存在过继情况m_parentId表示的是法律上的抚养关系还是血缘关系必须在数据模型里明确区分。常见做法是增加一个m_adoptiveParentId字段血缘查询走m_parentId法律上的父辈关系走m_adoptiveParentId两个字段独立存避免混淆。下面是两种建模的对比模型类型血缘关系表示过继/收养实现成本单 parentIdm_parentId直接指向生父/生母不支持会把养父母和生父母混在一起最低双父链m_parentIdm_adoptiveParentId分别保存生父母和养父母高界面要区分这个表格也解释了为什么很多家谱项目最后都会向“图 关系表”演进只要允许再婚、过继、入赘这些场景纯树结构就不够用了。不过作为课程设计或者入门级工具单 parentId 已经能覆盖绝大多数用户需求把边界说清楚比直接上复杂模型更有工程意义。3. MFC 图形化界面从对话框到树形控件3.1 程序骨架基于对话框还是文档/视图MFC 新建项目时有“基于对话框”和“单文档”两种常见选择。家谱管理需要同时展示树、编辑区、按钮用单文档加切分窗口更合适。但很多人图省事用对话框结果后续要处理菜单、工具栏、拖拽文件时非常被动。这个项目采用主窗口加左右分栏左边CTreeCtrl显示家谱树右边CListView或普通CView显示详情。关键代码在InitInstance手动创建主窗口BOOL CMyApp::InitInstance() { CWinApp::InitInstance(); CMainFrame* pFrame new CMainFrame(); if (!pFrame-LoadFrame(IDR_MAINFRAME)) { return FALSE; } m_pMainWnd pFrame; pFrame-ShowWindow(m_nCmdShow); pFrame-UpdateWindow(); return TRUE; }LoadFrame负责从资源加载菜单、快捷键和窗口类名如果资源 ID 写错这里会返回FALSE。注意ShowWindow的m_nCmdShow是系统传来的启动参数比如最小化启动或普通启动不能写死成SW_SHOW。如果后续要调整切分条位置在视图类里调用GetParentFrame()-RecalcLayout()刷新布局。理解这段是排查窗口加载失败的基础。3.2 用 CTreeCtrl 显示并保证数据指针安全树控件是家谱系统的第一交互入口。CTreeCtrl::InsertItem只负责显示文本要让它和Person对应最稳妥的方法是用SetItemData保存成员 ID而不是保存Person*。因为m_persons是std::vector一旦push_back导致内存块重新分配任何Person*都会失效下次点击树节点再取指针就崩溃了。HTREEITEM AddPersonToTree(CTreeCtrl tree, FamilyTree family, int id, HTREEITEM hParent) { Person* p family.FindById(id); if (!p) return nullptr; CString text p-m_name; HTREEITEM hItem tree.InsertItem(text, hParent, TVI_LAST); tree.SetItemData(hItem, (DWORD_PTR)id); for (int childId : p-m_childrenIds) { AddPersonToTree(tree, family, childId, hItem); } return hItem; }这里TVI_LAST表示插入到兄弟节点末尾保证孩子顺序与m_childrenIds一致。SetItemData保存(DWORD_PTR)id在 32 位和 64 位程序里都不会截断。当用户点击树节点时用GetItemData取回 ID再通过FindById得到当前成员。整个过程中不存在跨vector扩容保存的指针安全性提高很多。注意在删除成员后m_persons中元素会被erase之后继续遍历m_childrenIds时如果删除了当前元素必须使用迭代器风格循环不能使用基于范围的循环同时删除。大量添加节点时树控件每插入一个节点都会触发重绘效率很低。习惯用法是先在批量插入前调用SetRedraw(FALSE)全部完成后调用SetRedraw(TRUE)并Invalidate()。这样既能加快加载速度也能避免用户看到一棵树从空到满的闪烁过程。SetItemData的另一个好处是当需要按关系跳转时不用每次做字符串匹配直接拿 ID 去FamilyTree里查就行。3.3 在主视图里绘制血缘关系图树控件适合浏览结构但要表达“谁和谁是兄弟”“谁和谁隔了几代”一张关系图比控件更直观。MFC 的CView::OnDraw提供了CDC可以直接在上面画矩形和连线。下面是简化后的自动布局函数核心是递归计算每个子树的宽度然后回推父节点位置int DrawFamilyGraph(CDC* pDC, int x, int y, int id, FamilyTree family) { Person* p family.FindById(id); if (!p) return 0; int boxW 110, boxH 30, vGap 50; CString name p-m_name; pDC-Rectangle(x, y, x boxW, y boxH); pDC-DrawText(name, CRect(x, y, x boxW, y boxH), DT_CENTER | DT_VCENTER | DT_SINGLELINE); int subX x; int subY y boxH vGap; int totalW 0; std::vectorint childStartX; for (int childId : p-m_childrenIds) { int childW DrawFamilyGraph(pDC, subX, subY, childId, family); childStartX.push_back(subX); subX childW 20; totalW childW 20; } // 从父节点底部中心画线到每个孩子顶部中心 int parentCenterX x boxW / 2; int parentBottomY y boxH; for (size_t i 0; i p-m_childrenIds.size(); i) { int childCenterX childStartX[i] boxW / 2; pDC-MoveTo(parentCenterX, parentBottomY); pDC-LineTo(childCenterX, subY); } return max(totalW, boxW); }上面代码里用childStartX记录每个孩子的起始横坐标否则遍历第二个循环时subX已经移动到末尾画不出连接线。DrawText用的CRect是矩形文本区DT_VCENTER只对单行文本有效所以必须配合DT_SINGLELINE。实际项目中如果家谱层级很深这个递归函数会在窗口右边界被截断需要把整个图包在ScrollWindow或CMFCSmartScroll里。另外还要设置pDC-SetBkMode(TRANSPARENT)否则文字背景是实心的会盖住矩形边框。下面列出本场景常用 MFC 控件的用途控件类在本项目中的用途关键设置CTreeCtrl展示成员层级响应节点选中SetItemData保存 IDCListCtrl展示某个成员的孩子和配偶列表报表视图列头用InsertColumnCEdit编辑姓名、出生日期使用CString变量绑定自动处理 UnicodeCButton添加、删除、保存按钮BN_CLICKED事件绑定处理函数3.4 编辑成员信息时的控件联动当用户在树控件选择某个成员后右边的编辑框应当立即显示该成员的姓名、出生日期和配偶。常见错误是在OnSelchangedTree里逐个SetWindowText但用户点完树马上按回车焦点可能还在树控件编辑框数据没有刷新。所以还需要在OnUpdate里判断当前选中项是否发生了变化。控制器里的更新逻辑使用UpdateData(FALSE)把m_xxx变量刷新到控件然后在OnOK里UpdateData(TRUE)读回。这个流程用熟了以后MFC 的 DDX 机制会显得非常顺手。4. 数据持久化家谱文件格式与序列化方案4.1 为什么不用 CArchive而用自描述文本MFC 自带的CArchive序列化写起来简单但生成的二进制文件字段顺序完全依赖类定义一旦后续版本增加字段旧文件就无法读取。家谱数据要长期保存更适合用自描述文本格式。这个项目选择最简单的制表符分隔格式每行代表一个成员。字段含义如下字段类型说明IDint全局唯一加载时重建索引姓名CString支持中文不能包含制表符性别int0 未知1 男2 女出生日期CStringYYYY-MM-DD空串表示未知死亡日期CStringYYYY-MM-DD空串表示健在父IDint0 表示根节点配偶IDint0 表示未填写CHILD关系不直接写在人物行里而是在加载完所有人物后扫描每个人的m_parentId填入对应父亲的m_childrenIds尾部。这样可以避免源文件中父亲先出现还是孩子先出现的问题。性别用1/2/0而不是男/女主要是避免编码问题。出生日期用YYYY-MM-DD存储显示时再格式化这个选择让排序和比较年龄都很方便。4.2 保存与加载的具体实现保存可以用CStdioFile一行一行写。注意 MFC 的CString::Format在 Unicode 工程里要使用%s配合CString::GetString()不要直接传CString对象。下面是一个安全版本bool SaveTree(const CString filepath, const FamilyTree family) { CStdioFile file; if (!file.Open(filepath, CFile::modeCreate | CFile::modeWrite | CFile::typeText)) { AfxMessageBox(_T(无法创建文件)); return false; } file.WriteString(_T(# family tree data\n)); for (size_t i 0; i family.m_persons.size(); i) { const Person p family.m_persons[i]; CString line; line.Format(_T(%d\t%s\t%d\t%s\t%s\t%d\t%d\n), p.m_id, p.m_name.GetString(), p.m_gender, p.m_birthDate.GetString(), p.m_deathDate.GetString(), p.m_parentId, p.m_spouseId); file.WriteString(line); } file.Close(); return true; }这里typeText表示以文本模式打开WriteString会自动把换行符转成本地格式。参数表里的p.m_name.GetString()在 Unicode 工程里是const wchar_t*配合%s没有问题。注意line.Format里%s只能用于 MFC 的CString如果混用std::string就必须先转成CStringA否则编译器不报错但运行结果错误。加载函数使用Tokenize按制表符切分比swscanf_s更容易处理字段缺失bool LoadTree(const CString filepath, FamilyTree family) { CStdioFile file; if (!file.Open(filepath, CFile::modeRead | CFile::typeText)) { AfxMessageBox(_T(无法打开文件)); return false; } CString line; while (file.ReadString(line)) { if (line.IsEmpty() || line[0] _T(#)) continue; Person p; int start 0; CString token line.Tokenize(_T(\t), start); if (token.IsEmpty()) continue; p.m_id _ttoi(token); token line.Tokenize(_T(\t), start); p.m_name token; token line.Tokenize(_T(\t), start); p.m_gender _ttoi(token); token line.Tokenize(_T(\t), start); p.m_birthDate token; token line.Tokenize(_T(\t), start); p.m_deathDate token; token line.Tokenize(_T(\t), start); p.m_parentId _ttoi(token); token line.Tokenize(_T(\t), start); p.m_spouseId _ttoi(token); family.m_persons.push_back(p); } family.RebuildRelations(); file.Close(); return true; }Tokenize的start参数会持续记录当前扫描位置调用下一轮时从上一个分隔符后继续找。_ttoi负责把字符串转成整数空串转换成 0不会抛异常。加载完成后调用RebuildRelations这一步很关键先清空所有人的m_childrenIds再遍历所有成员如果m_parentId不为 0 且能找到对应父亲就把该成员 ID 追加到父亲的m_childrenIds尾部。注意追加顺序依赖成员在文件中的顺序如果想要严格的长幼顺序应该在文件里额外保存顺序字段。4.3 数据一致性校验家谱文件最容易出现的脏数据是“孩子指向了不存在的父亲”“配偶关系不对称”“根节点不止一个”。加载完成后最好做一次一致性扫描把问题汇总成字符串显示出来void FamilyTree::CheckConsistency() { for (size_t i 0; i m_persons.size(); i) { Person p m_persons[i]; if (p.m_parentId ! 0 FindById(p.m_parentId) nullptr) { TRACE(_T(成员 %s 的父亲 %d 不存在\n), p.m_name, p.m_parentId); } if (p.m_spouseId ! 0) { Person* spouse FindById(p.m_spouseId); if (spouse spouse-m_spouseId ! p.m_id) { TRACE(_T(配偶关系不对称%s 与 %s\n), p.m_name, spouse-m_name); } } } }这里的TRACE只在 Debug 版本输出发布版可以通过OutputDebugString查看。但更好的做法是把问题收集到CStringArray里由界面决定是弹窗还是写入日志。对于课程设计用TRACE加断点检查已经能应付 90% 的情况。注意m_persons内部成员地址在push_back后可能变化所以这个CheckConsistency要放在加载完成、不再追加新成员之后调用。4.4 中文编码从 GBK 到 UTF-8旧工程可能用CStdioFile直接写 TEXT在简体中文 Windows 上默认是 GBK。如果编写的jiapu文件要在不同系统或新版 Windows 间共享建议内部统一使用 UTF-8。MFC 的CStdioFile不能直接写 UTF-8 BOM常见做法是在文件首行手工写入\xEF\xBB\xBF后续内容用CW2A转换成 UTF-8 后写入。注意CStringW到CStringA的转换需要指定代码页推荐用CW2A(CP_UTF8)而不是默认 ANSI。如果只是课程设计保持 GBK 也能运行但在 Visual Studio 2019 及以上版本里字符集设置是 Unicode必须显式转换。5. 进阶关系判定、布局优化与常见崩溃排查5.1 用自检函数避免递归死循环在AddChild前调用IsAncestor(childId, parentId)可以防止成环。具体写法是在修改m_parentId之前检查一下if (IsAncestor(childId, parentId)) { AfxMessageBox(_T(不能把后代设为父节点)); return false; }这一步避免了加载不合法数据后绘制函数无限递归。Debug 模式下还可以用ASSERT把它加严让问题在测试阶段就暴露出来。5.2 树控件与视图双缓冲刷新树节点批量插入时闪烁先用SetRedraw(FALSE)插完再SetRedraw(TRUE)。对于OnDraw里的关系图使用内存 DC 双缓冲避免每次鼠标移动都闪屏void CMyView::OnDraw(CDC* pDC) { CRect rc; GetClientRect(rc); CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height()); CBitmap* pOld memDC.SelectObject(bmp); // 在 memDC 上画图最后 BitBlt 到 pDC pDC-BitBlt(0, 0, rc.Width(), rc.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); }这里把绘制命令全部发到内存画布只有最后一次BitBlt真正更新屏幕。参数说明CreateCompatibleBitmap需要和窗口 DC 同尺寸否则边缘显示不全BitBlt的SRCCOPY是直接覆盖不需要考虑背景色。5.3 发布时缺 DLL 的问题MFC 程序发布时依赖动态链接的mfc140u.dll以及运行库。如果目标机器没有安装对应版本的 Visual C Redistributable启动时会直接弹“缺少 VCRUNTIME140.dll”或“无法定位程序输入点”。最容易的排查方式是用dumpbin /dependents查看生成的可执行文件依赖了哪些 DLL确认后再决定安装哪个版本运行库。注意 Debug 版不可分发默认连接的是mfc140ud.dll普通用户机器上不会有调试版运行库。如果要在没有安装开发环境的机器上跑把编译配置切到 Release并在工程设置里把“代码生成”的“运行库”改成“多线程 DLL (/MD)”再带上官方 VC 运行库安装包即可。本文还有配套的精品资源点击获取
RELATED

相关推荐

STM32F103串口通信实战:标准库UART配置与调试全解析

STM32F103串口通信实战:标准库UART配置与调试全解析

简介:面向嵌入式初学者与STM32开发者,这套实验工程以STM32F103C8T6为核心,基于标准外设库演示UART串口通信的完整实现流程。工程通过串口输入1、2、3任意数字,分别输出不同内容,可直观观察字符接收、分支判断与回显发送…

📅 2026/9/16 1:16:56
告别备案迷茫,做课件用这15大网站对比评测

告别备案迷茫,做课件用这15大网站对比评测

告别备案迷茫,做课件用这15大网站对比评测 你是不是也遇到过这种情况?网站代码写完了,域名也买了,结果卡在ICP备案这一步,看着管局那个复杂的流程,脑子一团浆糊,完全不知道下一步该点哪里。这种“备案流程一头雾水”的状态,是很多新手建站者的噩…

📅 2026/9/16 1:16:56
制造业数字化转型实践:从上云迁移到数据治理

制造业数字化转型实践:从上云迁移到数据治理

前阵子甲骨文云大会现场,我注意到一个很有意思的现象:台下坐的不再只是互联网公司的运维和架构师,多了很多来自制造业、供应链、传统实业的面孔。盈趣科技作为制造业代表和Oracle站在一起宣布深化数字化合作的时候,我旁边一位做智…

📅 2026/9/16 1:16:56
MORE NEWS

更多资讯

📰

SpringBoot土地档案管理系统开发实践

1. 项目背景与核心需求土地档案管理是国土资源管理中的基础性工作,涉及土地权属、利用现状、规划审批等关键信息。传统纸质档案管理方式存在查询效率低、数据易丢失、共享困难等问题。随着"互联网政务服务"的推进,构建信息化土地档案管理系统已…

📰

PDF图片去水印软件实测:批量处理烧录型水印的完整方法与参数调优指南

先问一句:你是不是也遇到过这种情况——好不容易从客户或同事手里拿到一份PDF资料,想提取里面的图片做二次设计,结果每张图片上都压着半透明的水印,有的在角落,有的直接斜穿整个画面。更烦人的是,这种PDF少…

📰

C盘空间不足自救指南:4步排查法定位并清理隐藏空间占用

C盘又红了,相信每个用Windows的朋友都经历过这种血压拉满的时刻。打开“此电脑”一看,C盘那条容量条红得发紫,系统跑起来卡顿明显,风扇呼呼作响,甚至弹窗提示“磁盘空间不足”。大多数人第一反应是什么?上网…

📰

Zotero引用一键变黑:Word VBA宏批量处理超链接格式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

信用卡欺诈检测实战:从类别不平衡到阈值调优的完整攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

LSTM文本情感分析:门控记忆与词向量如何破解电商评论歧义

简介:文本情感分析是自然语言处理中的经典任务,旨在从主观文本中识别褒贬态度。早期基于词典的方法依赖情感词的静态匹配,在电商评论中遇到转折结构、口语化表达和长距离语义依赖时往往失效。循环神经网络(RNN)按时间步…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬