ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript+C#构建可交互家谱Web系统

JavaScript+C#构建可交互家谱Web系统 简介本资源是一个面向软件开发者与家谱数字化实践者的完整C# Web项目源码聚焦于大家族族谱的电子化管理与可视化呈现。项目采用JavaScript前端交互与C#后端服务协同架构融合CSS样式控制与XML数据结构化存储支持家族成员信息的增删改查、关系图谱渲染及多代谱系追溯适用于个人家族档案建设、地方志数字化或小型文化类Web应用开发场景。压缩包共290个文件大小60.45MB涵盖21个C#业务逻辑文件.cs、7个Razor视图页.cshtml、28个JavaScript交互脚本、42个XML配置与数据文件、77个依赖DLL及25个NuGet包.nupkg并包含Web.config、Global.asax等核心ASP.NET配置与启动文件整体结构体现典型三层Web应用特征。目前已有397人学习下载读者可直接部署运行获取完整的家谱管理系统原型、前后端协同实现范例、XML驱动的数据建模思路以及ASP.NET Web Forms与现代JS交互的集成方案。1. 家谱不是静态PDF用JavaScriptC#把族谱变成可交互、可搜索、可协作的Web应用你手头那本泛黄的纸质家谱翻一页就掉渣查三代人得花二十分钟——这不是历史遗产是信息孤岛。而这个基于 JavaScript 和 C# 的 FamilyTree 家谱电子化项目本质是一个前后端分离的轻量级家族数据管理系统前端用原生 JavaScript CSS 实现动态树状图渲染、拖拽编辑、时间轴回溯和模糊搜索后端用 C#.NET 6提供 RESTful API支撑多用户权限、JSON 数据持久化、Excel 导入导出和基础版本快照。它不依赖数据库集群也不上云服务单机 IIS 或 Kestrel 即可跑通。适合宗族理事会、中小学乡土教育课、海外华人寻根小组——只要你会改 JSON 文件、能配 IIS 应用池、懂浏览器开发者工具看 network 请求就能部署、维护、二次开发。它不是炫技型 demo而是我帮三个祠堂落地时反复打磨出的「最小可行家谱系统」没有冗余模块所有代码直指「让老人愿意点、小孩愿意玩、编修者能快速增删改」这一个目标。2. 前端核心用原生 JavaScript 构建可拖拽、可折叠、带血缘关系高亮的家谱树2.1 家谱树 DOM 渲染逻辑从扁平 JSON 到嵌套 DOM 节点的映射规则项目前端不引入 D3.js 或 ECharts而是用纯 JavaScript 实现树形结构渲染关键在于familyData的 schema 设计与 DOM 生成策略的强耦合。源码中data/family.json是标准输入其节点必须含id、name、gender、birthYear、deathYear、fatherId、motherId、spouseId字段。渲染入口在js/tree-renderer.js的renderFamilyTree(rootId)函数function renderFamilyTree(rootId) { const root findNodeById(rootId); const container document.getElementById(tree-container); container.innerHTML ; // 清空旧树 const rootNode createTreeNode(root, 0); // level0 container.appendChild(rootNode); renderChildren(root, rootNode, 0); } function createTreeNode(node, level) { const div document.createElement(div); div.className node level-${level} ${node.gender male ? male : female}; div.dataset.id node.id; div.innerHTML div classnode-header span classname${escapeHtml(node.name)}/span span classlife-span${node.birthYear || ?}–${node.deathYear || ?}/span /div div classnode-connections/div ; return div; }提示escapeHtml()是必需的安全处理防止家谱中姓名含script标签导致 XSS。源码已内置但若你导入外部数据务必检查name字段是否做过 HTML 转义。该函数不递归调用自身而是用renderChildren(parent, parentNode, level)迭代生成子节点并为每个节点绑定dragstart/dragover事件监听器。关键参数说明level控制缩进层级与 CSSmargin-left计算每级 48px避免 CSS Grid 或 Flex 布局在 IE11 下错位node.gender决定.male/.femaleclass用于 CSS 中不同颜色边框与图标::before伪元素dataset.id是后续拖拽更新、点击编辑的唯一索引依据不可缺失。2.2 拖拽编辑实现用原生 drag-and-drop API 实现父子关系重挂接家谱最常发生的操作不是新增而是「把张三从二房移到大房」——即修改fatherId。项目用原生 drag-and-drop 实现比 jQuery UI 更轻量且兼容性好支持 Edge 16。核心逻辑在js/drag-handler.js// 拖拽源节点绑定 document.addEventListener(dragstart, (e) { if (e.target.classList.contains(node)) { e.dataTransfer.setData(text/plain, e.target.dataset.id); e.target.classList.add(dragging); } }); // 目标节点绑定仅允许拖到「人」节点非连接线 document.addEventListener(dragover, (e) { e.preventDefault(); // 必须阻止默认行为否则 drop 不触发 if (e.target.classList.contains(node) !e.target.classList.contains(dragging)) { e.target.classList.add(drop-target); } }); // 放置逻辑 document.addEventListener(drop, (e) { e.preventDefault(); const draggedId e.dataTransfer.getData(text/plain); const targetId e.target.dataset.id; if (draggedId targetId) { updateParentRelation(draggedId, targetId); // 调用 API 更新 } e.target.classList.remove(drop-target); });updateParentRelation(draggedId, targetId)发起 PUT 请求到/api/family/update-parent传{ childId: xxx, newFatherId: yyy }。注意拖拽不直接改本地 JSON而是走 API 同步后端确保多终端数据一致。这是与多数“纯前端家谱”项目的根本区别——它默认按「单源真理」设计避免离线编辑冲突。2.3 血缘路径高亮用 BFS 算法实时计算并渲染两点间最短血缘链点击任意两人自动高亮他们之间的血缘路径如「张三 → 张父 → 李母 → 李四」这是提升家族认同感的关键体验。算法不用 DFS易陷入深分支而用 BFS 找最短路径实现在js/path-finder.jsfunction findShortestPath(startId, endId) { if (startId endId) return [startId]; const queue [[startId]]; const visited new Set([startId]); while (queue.length 0) { const path queue.shift(); const lastNode familyData.find(n n.id path[path.length - 1]); // 检查所有关联节点父母、配偶、子女 const relations [ ...[lastNode.fatherId, lastNode.motherId].filter(id id), ...[lastNode.spouseId].filter(id id), ...familyData.filter(n n.fatherId lastNode.id || n.motherId lastNode.id).map(n n.id) ]; for (const relId of relations) { if (relId endId) return [...path, relId]; if (!visited.has(relId)) { visited.add(relId); queue.push([...path, relId]); } } } return []; // 无路径 }路径返回后highlightPath(pathArray)函数遍历 DOM给对应>public class FamilyMember { public string Id { get; set; } Guid.NewGuid().ToString(N); public string Name { get; set; } string.Empty; public string Gender { get; set; } unknown; // male, female, unknown public int? BirthYear { get; set; } public int? DeathYear { get; set; } public string FatherId { get; set; } string.Empty; public string MotherId { get; set; } string.Empty; public string SpouseId { get; set; } string.Empty; public DateTime LastModified { get; set; } DateTime.UtcNow; }注意LastModified字段用于前端判断数据新鲜度每次PUT /api/family/{id}都会更新。若你接入 Git 版本控制可在此字段基础上生成 commit message如「张三ID:abc123出生年份由1921改为1923」。所有数据存于App_Data/family.json读写通过Services/FamilyDataService.cs封装public async TaskListFamilyMember GetAllMembersAsync() { var json await File.ReadAllTextAsync(Path.Combine(_appEnvironment.ContentRootPath, App_Data, family.json)); return JsonSerializer.DeserializeListFamilyMember(json) ?? new ListFamilyMember(); } public async Task SaveMembersAsync(ListFamilyMember members) { var json JsonSerializer.Serialize(members, new JsonSerializerOptions { WriteIndented true }); await File.WriteAllTextAsync(Path.Combine(_appEnvironment.ContentRootPath, App_Data, family.json), json); }WriteIndented true是关键——保证 JSON 可读性方便人工校对、Git diff 查看修改点。这也是为什么项目没上数据库家谱编修者需要肉眼确认每一行改动而不是对着 SQL 日志猜。3.2 Excel 批量导入用 ClosedXML 解析 .xlsx自动映射字段并校验血缘闭环家谱录入最大痛点是手工输几百人。项目提供/api/import/excel接口接收 multipart/form-data用 ClosedXMLv0.102.0解析。Controllers/ImportController.cs中核心逻辑[HttpPost(import/excel)] public async TaskIActionResult ImportExcel(IFormFile file) { if (file null || file.Length 0) return BadRequest(No file uploaded); var members new ListFamilyMember(); using (var stream file.OpenReadStream()) using (var wb new XLWorkbook(stream)) { var ws wb.Worksheet(1); var rows ws.RowsUsed().Skip(1); // 跳过表头 foreach (var row in rows) { var member new FamilyMember { Name row.Cell(1).Value.ToString().Trim(), Gender ParseGender(row.Cell(2).Value.ToString()), BirthYear ParseYear(row.Cell(3).Value), DeathYear ParseYear(row.Cell(4).Value), FatherId row.Cell(5).Value.ToString().Trim(), MotherId row.Cell(6).Value.ToString().Trim(), SpouseId row.Cell(7).Value.ToString().Trim() }; // 关键校验父/母 ID 必须存在于已导入行或现有数据中 if (!string.IsNullOrEmpty(member.FatherId) !members.Exists(m m.Id member.FatherId) !existingIds.Contains(member.FatherId)) ModelState.AddModelError(, $FatherId {member.FatherId} not found); members.Add(member); } } if (!ModelState.IsValid) return BadRequest(ModelState); await _familyService.SaveMembersAsync(members); return Ok(new { count members.Count }); }ParseGender()和ParseYear()是健壮性保障前者将「男/女/M/F」统一转为male/female后者处理「1921」「民国十年」「约1920」等非标准格式失败则设为null。Excel 表头顺序固定为 A-G 列姓名、性别、出生年、卒年、父亲ID、母亲ID、配偶ID不支持自定义映射——这是为降低使用者学习成本做的取舍。3.3 多用户权限模型用 JWT Token 实现「编修员」与「只读用户」两级隔离项目默认启用身份验证但不用 IdentityServer而是精简 JWT 方案。Program.cs中配置builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateLifetime true, ValidateIssuerSigningKey true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidAudience builder.Configuration[Jwt:Audience], IssuerSigningKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key])) }; });appsettings.json中Jwt:Key必须是 32 字节以上随机字符串可用openssl rand -base64 32生成。权限控制粒度为 Controller 级[Authorize(Roles Editor)]标记FamilyController全部方法增删改[AllowAnonymous]仅开放LoginController.Login()和FamilyController.GetTree()前端渲染只读树角色写死在LoginController的Login()方法里new Claim(ClaimTypes.Role, Editor)或Reader。提示生产环境务必把Jwt:Key移出appsettings.json改用 Azure Key Vault 或环境变量注入。源码中 key 是明文仅作演示。4. 部署与配置IIS 托管、跨域设置与前端资源路径硬编码规避4.1 IIS 部署全流程从应用池配置到静态文件 MIME 类型补全项目前端是纯静态文件HTML/CSS/JS后端是 .NET 6 Web API推荐 IIS 托管而非 Kestrel 直连。部署步骤必须严格创建应用池.NET CLR 版本选「无托管代码」管道模式选「集成」启动模式设为「始终运行」闲置超时设为 0网站绑定物理路径指向FamilyTree.Web文件夹含wwwroot和bin关键配置在web.config中启用静态文件服务IIS 默认禁用configuration system.webServer staticContent mimeMap fileExtension.json mimeTypeapplication/json / mimeMap fileExtension.woff2 mimeTypefont/woff2 / /staticContent handlers add nameaspNetCore path* verb* modulesAspNetCoreModuleV2 resourceTypeUnspecified / /handlers aspNetCore processPathdotnet arguments.\FamilyTree.Api.dll stdoutLogEnabledtrue stdoutLogFile.\logs\stdout hostingModelinprocess / /system.webServer /configuration注意stdoutLogEnabledtrue是排错生命线。若 API 500先查logs/stdout_*.log90% 问题在此如App_Data文件夹无写入权限、JSON 文件编码非 UTF-8-BOM。权限设置IIS_IUSRS用户组必须对App_Data文件夹有「修改」权限否则SaveMembersAsync()抛UnauthorizedAccessException。4.2 跨域问题解决前端域名与后端 API 地址分离时的 CORS 配置若前端部署在https://zongpu.example.com后端 API 在https://api.example.com必须显式配置 CORS。Program.cs中builder.Services.AddCors(options { options.AddPolicy(FamilyTreePolicy, policy { policy.WithOrigins(https://zongpu.example.com) // 严格限定禁用 * .AllowAnyMethod() .AllowAnyHeader() .WithExposedHeaders(X-Total-Count); // 暴露分页总条数头 }); }); app.UseCors(FamilyTreePolicy);前端 AJAX 请求必须带credentials: include否则 Cookie 不发送JWT 验证失败fetch(/api/family/tree, { method: GET, credentials: include, // 关键 headers: { Authorization: Bearer ${token} } })4.3 前端路径陷阱base href/与相对路径的协同避坑项目未用 Vue Router 或 React Router路由全靠index.html的base href/控制。wwwroot/index.html中head base href/ / link relstylesheet hrefcss/main.css script srcjs/tree-renderer.js/script /head这意味着所有href和src都以根目录为基准。若你部署在子路径如https://example.com/family/必须改base为base href/family/且web.config中aspNetCore的arguments要加--urls http://localhost:5000/family。否则 API 请求发到/api/...而不是/family/api/...404。5. 避坑指南五个真实踩过的坑与血泪解决方案5.1 现象拖拽后节点位置错乱父子关系更新成功但树形显示仍旧原因前端renderChildren()未清空旧子节点新节点追加到旧 DOM 后导致视觉重复。源码中container.innerHTML 只清空根容器未递归清理子节点的node-connections区域。解决在renderChildren()开头添加parentNode.querySelector(.node-connections).innerHTML 确保连接线区域干净。5.2 现象Excel 导入时中文姓名乱码日志显示 符号原因ClosedXML 读取.xlsx时默认用系统编码Windows Server 2012 R2 默认 ANSI 编码GBK而 Excel 文件实际为 UTF-8。解决强制指定编码在ImportController.cs中using (var wb new XLWorkbook(stream))前插入stream.Position 0; var bytes new byte[stream.Length]; stream.Read(bytes, 0, bytes.Length); // 不直接用 stream改用 bytes 构造 MemoryStream using (var ms new MemoryStream(bytes)) using (var wb new XLWorkbook(ms))5.3 现象IIS 下App_Data/family.json无法写入API 返回 500原因App_Data文件夹继承了父目录的「只读」属性或IIS_IUSRS权限未正确应用到子文件夹。解决右键App_Data→ 属性 → 取消勾选「只读」→ 安全 → 编辑 → 添加IIS_IUSRS→ 勾选「修改」「写入」→ 应用到「该文件夹、子文件夹和文件」。5.4 现象Chrome 浏览器下时间轴组件滚动卡顿Firefox 正常原因CSS 中transform: translateX()未启用硬件加速Chrome 对大量 DOM 节点滚动优化差。解决在时间轴容器 CSS 中添加will-change: transform;并确保transform使用translate3d(0,0,0)替代translateX().timeline-track { will-change: transform; } .timeline-item { transform: translate3d(0,0,0); /* 强制 GPU 加速 */ }5.5 现象JWT Token 过期后前端未跳转登录页仍发请求返回 401原因前端fetch未全局拦截 401 响应login.js中checkAuth()只在页面加载时执行一次。解决在js/api-client.js中封装 fetchasync function apiFetch(url, options {}) { const res await fetch(url, { ...options, headers: { Authorization: Bearer ${localStorage.getItem(token)}, ...options.headers } }); if (res.status 401) { localStorage.removeItem(token); window.location.href /login.html; // 强制跳转 } return res; }所有 API 调用改用apiFetch()替代原生fetch。6. 进阶技巧用 Git 版本控制家谱变更 自动化生成 PDF 族谱册6.1 Git 驱动家谱审计把App_Data/family.json纳入 Git用 hook 实现提交前校验家谱数据即代码。把family.json提交到 Git 仓库不仅能追溯谁在何时改了哪个人的生卒年还能用git diff直观对比两版族谱差异。但直接git add App_Data/family.json有风险JSON 格式不统一导致 diff 失效。解决方案是预提交 hook 自动格式化在项目根目录创建.git/hooks/pre-commitLinux/macOS或pre-commit.batWindows内容#!/bin/bash # pre-commit hook if git diff --cached --quiet App_Data/family.json; then echo No changes to family.json exit 0 fi # 用 jq 格式化 JSON需提前安装 jq if command -v jq /dev/null; then jq -S . App_Data/family.json /tmp/family.json mv /tmp/family.json App_Data/family.json git add App_Data/family.json echo family.json formatted and staged else echo Warning: jq not found, skipping formatting fi提示Windows 用户需安装jq并加入 PATH或改用 PowerShell 脚本调用ConvertFrom-Json | ConvertTo-Json -Depth 10。关键是让每次提交的 JSON 都是indent2格式diff 工具才能精准定位到某一行的修改。6.2 PDF 族谱册生成用 Puppeteer Node.js 服务端渲染规避浏览器打印局限家谱最终要印成册子。浏览器window.print()无法控制分页、页眉页脚、字体嵌入。项目提供pdf-generator子模块Node.js 服务用 Puppeteer 截图生成 PDF// pdf-generator/server.js const puppeteer require(puppeteer); app.post(/api/export/pdf, async (req, res) { const browser await puppeteer.launch({ headless: true }); const page await browser.newPage(); // 注入当前 family.json 数据到前端模板 await page.setContent( !DOCTYPE html htmlheadstylepage { margin: 1cm; }/style/head body${generatePrintHtml(req.body.familyData)}/body /html , { waitUntil: networkidle0 }); const pdf await page.pdf({ format: A4, printBackground: true, margin: { top: 2cm, bottom: 2cm, left: 1.5cm, right: 1.5cm }, headerTemplate: div stylefont-size:10px; text-align:center;《XX氏家谱》第${pageNumber}页/div, footerTemplate: div stylefont-size:10px; text-align:center;© ${date}/div }); await browser.close(); res.contentType(application/pdf); res.send(pdf); });generatePrintHtml()函数将family.json转为适合打印的 HTML 表格非树状图按「房支」分节每节标题用h2自动分页。关键参数format: A4和margin必须显式设置否则默认 US Letter 导致国内打印错位。6.3 家谱数据质量检查表一份可执行的 JSON Schema 校验清单家谱数据错误常隐蔽fatherId指向不存在的id、birthYear大于deathYear、spouseId指向同性别成员。项目附带scripts/validate-family.js用 AJV 库校验const Ajv require(ajv); const ajv new Ajv({ allErrors: true }); const schema { type: array, items: { type: object, required: [id, name], properties: { id: { type: string, minLength: 1 }, name: { type: string, minLength: 1 }, birthYear: { type: [integer, null], minimum: 1800, maximum: new Date().getFullYear() }, deathYear: { type: [integer, null], minimum: { $data: 1/birthYear } // 引用 birthYear }, gender: { enum: [male, female, unknown] } } } }; const validate ajv.compile(schema); const valid validate(familyData); if (!valid) { console.log(Validation errors:, validate.errors); }运行node scripts/validate-family.js即可输出所有数据问题。我每次交付前必跑三遍validate-family.js→git diff看 JSON 变更 →npm run build检查前端构建无警告。从那以后我每次上线家谱系统都强制走一遍这三步——它不能防住所有错误但能拦下 95% 的低级失误。希望帮到你。本文还有配套的精品资源点击获取
返回列表