简介:一套基于ASP.NET与Access数据库的B/S架构项目管理系统源码,使用C#语言开发,适合Web开发学习者、毕业生或需要快速搭建内部任务管理系统的团队作为参考。系统按管理员、员工、网管三种角色划分权限,覆盖员工资料管理、项目任务维护、日志记录与查询、历史项目归档、建议回复以及数据库备份与还原等模块,功能层次清晰。资源包共297个文件,以cs源码、aspx页面、dll库、css/js前端文件及mdb数据库为主,压缩包仅5.09MB,目录结构便于部署和研读。该资源已有505人学习,代码与数据库齐全,可帮助理解多角色权限控制、ASP.NET页面交互与Access数据库操作流程,也可作为课程设计或毕业设计的基础框架。
1. 从零搭一套项目管理系统:为什么还是 ASP.NET + SQL Server + C#
接到过不少这类需求:公司内部的立项、任务分配、进度报工全在 Excel 里,数据散了一桌,领导想要一个能登录、能填工时、能按项目看进度的 Web 页面。预算不高,团队写 C#,服务器是 Windows,这时候用 ASP.NET 搭 Web 项目、SQL Server 存数据,是性价比极高的路线。这套组合成熟、资料多、VS 里一套工具全包,从建表到发布都有现成路径。这篇笔记面向两类人:刚接触企业级 Web 开发、想跟着做出一套完整系统的新手;以及被要求快速交付内部管理系统的全栈工程师。我会把骨架搭建、数据库设计、登录权限、任务流转、部署坑一次讲清楚,你照着能把一个最小可用的项目管理系统落地,而不是只跑通一个 Hello World。
2. 在 VS 里把 Web 项目骨架立起来:创建项目与三层结构的取舍
2.1 用 Visual Studio 创建项目:三个模板怎么选
打开 Visual Studio 2022,新建项目时会看到一大堆 ASP.NET 相关模板,最常见的三个是:ASP.NET Web 应用程序 (.NET Framework)、ASP.NET Core Web 应用、ASP.NET 空网站。很多人一上来就懵。我一般这么选:如果是要维护老系统,项目里全是 .aspx 页面和 .aspx.cs 文件,就用第一个模板里的 Web Forms;如果是从零开始、代码想长期好维护,选 ASP.NET Core 的 MVC 或 Razor Pages 更合适。但很多企业内部的项目管理系统存量代码都还在 .NET Framework 上,今天这篇以 Web Forms 为例,因为它的页面事件模型在增删改查场景下非常好写,而且网上能搜到的老资料几乎都能对上。
创建完项目后,你会看到 Default.aspx 和 Default.aspx.cs。这个代码隐藏文件是 C# 的核心入口,页面加载时先执行 Page_Load。下面是最小结构的示例:
// Default.aspx.cs public partial class _Default : Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { // 第一次加载页面时绑定下拉框、列表等 BindProjectDropDown(); } } private void BindProjectDropDown() { // 此处调用数据访问层,稍后章节会写实现 } }这里要注意IsPostBack这个属性。每次点击按钮、回发页面时,Page_Load 都会重新执行,如果不加判断,下拉框的数据会被重复绑定。新手最容易在这里翻车:明明只绑定一次,回发后却发现列表里出现重复项。把初始化和事件绑定放在if (!IsPostBack)里是 Web Forms 的黄金规则。
2.2 三层架构:UI、BLL、DAL 的项目划分与引用关系
项目管理系统虽然不大,但别把所有 SQL 和业务逻辑都堆在 .aspx.cs 里。我见过最痛苦的项目是 5000 行的页面后台文件,改一个字段要全局搜索。常见做法是分成三层:UI 层放页面,BLL 层放业务规则,DAL 层负责和 SQL Server 打交道。在解决方案里建三个类库项目,UI 引用 BLL,BLL 引用 DAL,UI 不能直接引用 DAL,这样以后改数据库结构时不会把页面层搞得天翻地覆。
解决方案结构大致如下:
- Solution
- WebUI(ASP.NET Web Application)
- BLL(Class Library)
- DAL(Class Library)
- Model(Class Library,放实体类)
命名规范上,控件命名建议用简称:按钮用btnLogin、文本框用txtProjectName、下拉框用ddlStatus、GridView 用gvProjects。这样别人看到代码就知道这是什么控件,不用猜。C# 本身对命名不敏感,但团队协作时这套约定能省很多沟通成本。
DAL 层一个最基本的查询方法长这样:
// DAL/ProjectDAL.cs using System.Data; using System.Data.SqlClient; public class ProjectDAL { private readonly string _connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["ProjectDb"].ConnectionString; public DataTable GetProjectsByStatus(int status) { string sql = "SELECT ProjectId, ProjectName, Status FROM Projects WHERE Status = @Status"; using (SqlConnection conn = new SqlConnection(_connectionString)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@Status", status); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); da.Fill(dt); return dt; } } } }这段代码里有两个关键点。using语句确保 SqlConnection 用完立刻释放,否则连接池会慢慢耗尽。cmd.Parameters.AddWithValue用参数化查询而不是拼接字符串,这是防止 SQL 注入的最基本手段。业务系统里任何用户输入的值,都不该直接拼进 SQL,这条规则没有例外。
2.3 配置文件:连接字符串、身份验证和调试开关
Web 项目里web.config是整个系统运行配置的黑匣子,80% 的部署问题都出在这。连接字符串是第一个必须写对的地方:
<connectionStrings> <add name="ProjectDb" connectionString="Data Source=.;Initial Catalog=ProjectManage;User ID=sa;Password=YourPassword;Encrypt=False" providerName="System.Data.SqlClient" /> </connectionStrings>Data Source=.;里的.代表本机 SQL Server 默认实例。如果装的是命名实例,要写成localhost\SQLEXPRESS或服务器名\实例名。Initial Catalog对应数据库名。Encrypt=False在本地开发时能省掉一堆证书警告,但上生产环境建议改成True,并且用受信任的证书。
身份验证节点也很重要。如果你不想让系统对接 Windows 域账号,就配置表单验证:
<authentication mode="Forms"> <forms loginUrl="Login.aspx" timeout="40" slidingExpiration="true" /> </authentication>timeout是 Session 超时分钟数,slidingExpiration表示用户一直在操作就自动续期。内部管理系统一般设 40 分钟比较合适。开发阶段还需要把compilation debug="true"打开,这样报错时能看到详细堆栈;发布到生产前一定要改成false,并把customErrors设置为RemoteOnly,避免把服务器代码路径暴露给外人。
3. 设计 SQL Server 数据库与数据访问层:表结构、存储过程与 EF 的选型
3.1 项目管理系统核心表设计:用户、项目、任务、工时
在 SQL Server 里建库建表,我习惯先画清楚最小实体。对于项目管理系统,至少要有四张表:用户表、项目表、任务表、工时记录表。用户表和项目表是多对多关系,一个人可以参与多个项目,一个项目有多个人,所以还需要一张用户项目关联表。但第一版可以先砍掉这个,让项目表只有一个负责人字段,等系统长起来再加。
下面是建表脚本,可以直接在 SSMS 或 VS 的 SQL Server 数据库项目里执行:
CREATE DATABASE ProjectManage; GO USE ProjectManage; GO CREATE TABLE Users ( UserID INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, PasswordHash NVARCHAR(128) NOT NULL, DisplayName NVARCHAR(50) NOT NULL, RoleID INT NOT NULL DEFAULT 0 -- 0=成员, 1=项目经理, 2=管理员 ); CREATE TABLE Projects ( ProjectID INT IDENTITY(1,1) PRIMARY KEY, ProjectName NVARCHAR(100) NOT NULL, ManagerUserID INT NOT NULL FOREIGN KEY REFERENCES Users(UserID), Status INT NOT NULL DEFAULT 0, -- 0=未开始, 1=进行中, 2=已结束 CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE Tasks ( TaskID INT IDENTITY(1,1) PRIMARY KEY, ProjectID INT NOT NULL FOREIGN KEY REFERENCES Projects(ProjectID), TaskName NVARCHAR(100) NOT NULL, AssigneeUserID INT NOT NULL FOREIGN KEY REFERENCES Users(UserID), Status INT NOT NULL DEFAULT 0, -- 0=待处理, 1=进行中, 2=已完成 DueDate DATE NULL, UpdateTime DATETIME NULL ); CREATE TABLE WorkLogs ( LogID INT IDENTITY(1,1) PRIMARY KEY, TaskID INT NOT NULL FOREIGN KEY REFERENCES Tasks(TaskID), UserID INT NOT NULL FOREIGN KEY REFERENCES Users(UserID), WorkDate DATE NOT NULL, Hours DECIMAL(5,2) NOT NULL CHECK (Hours > 0 AND Hours <= 24), Remark NVARCHAR(500) NULL );字段类型上有个容易忽略的点:NVARCHAR和VARCHAR的区别。只要字段可能存中文,就一律用NVARCHAR,否则在中文环境下很容易出现乱码。日期字段用DATE还是DATETIME,按粒度来,工时记录按天统计用DATE就够了,好做分组查询。Hours用DECIMAL(5,2),防止浮点误差。这里我没有用存储过程,因为第一版直接写 SQL 语句更直观;等系统跑顺了,再考虑把高频查询封装成存储过程也不迟。
3.2 数据访问层两种做法:ADO.NET 手写还是 EF?
到了数据访问层,很多人纠结用 Entity Framework 还是手写 ADO.NET。我的建议很实际:如果团队里 C# 水平参差不齐,项目又是增删改查为主,手写一个简单的 SqlHelper 比上 EF 更可控。EF 用起来方便,但黑匣子太多:延迟加载、导航属性、上下文生命周期,新手一旦踩坑,排查时间往往比写 SQL 长好几倍。如果未来表结构非常复杂,或者需求里有一堆动态查询,SQL 手写反而更好维护。
下面是一个通用的 SqlHelper,放在 DAL 层里直接调用:
// DAL/SqlHelper.cs using System.Data; using System.Data.SqlClient; public static class SqlHelper { private static readonly string ConnStr = System.Configuration.ConfigurationManager.ConnectionStrings["ProjectDb"].ConnectionString; public static DataTable Query(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(ConnStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) { cmd.Parameters.AddRange(parameters); } SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(ConnStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteNonQuery(); } } }这个类两个方法足够覆盖 80% 的功能。Query返回 DataTable,适合绑定 GridView、下拉框;ExecuteNonQuery返回影响行数,适合插入、更新、删除。params SqlParameter[]让调用方可以像写变长参数一样传参,比如SqlHelper.Query("SELECT * FROM Projects WHERE Status=@status", new SqlParameter("@status", 1))。每个 SqlParameter 都用@开头,对应 SQL 里的变量名,参数顺序无关紧要,只要名字匹配。
3.3 分页、统计与常用查询:用 SQL 把数据整理好
项目管理系统里最常见的查询是任务列表分页和工时统计。SQL Server 里用ROW_NUMBER()做分页是最通用的写法,比OFFSET/FETCH兼容性更好:
DECLARE @PageSize INT = 10; DECLARE @PageIndex INT = 2; SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY CreateTime DESC) AS RowNum, TaskID, TaskName, Status FROM Tasks ) AS T WHERE T.RowNum BETWEEN (@PageIndex - 1) * @PageSize + 1 AND @PageIndex * @PageSize;ROW_NUMBER()按CreateTime倒序给每行编号,然后外层查询截取当页的数据区间。注意BETWEEN的边界计算,第一页是 1 到 10,第二页是 11 到 20,很多人就在这里多写一行少写一行。
工时统计要用到按日期分组,并且把字符串转数字的潜在问题提前规避。假设你在 Excel 里导入工时时有数据是字符串,存入数据库前最好在 SQL 层先做一次转换校验:
SELECT CONVERT(DATE, WorkDate) AS WorkDay, SUM(TRYL_CAST(Hours AS DECIMAL(5,2))) AS TotalHours FROM WorkLogs WHERE TaskID = @TaskID GROUP BY CONVERT(DATE, WorkDate) ORDER BY WorkDay;TRY_CAST是 SQL Server 2012 以后提供的安全转换函数,转数字失败会返回 NULL 而不是直接报错,适合在数据清洗时用。如果你还在用CAST(Hours AS DECIMAL),遇到脏数据时整个查询会崩溃,这是 SQL Server 里一个很经典的字符串转数字踩坑点。
4. Web 层与 C# 业务逻辑:登录、权限和任务流转的实现
4.1 登录校验与 Session:C# 里最直接的一套权限闭环
登录功能是所有管理系统的第一道门。Web Forms 的做法是把登录逻辑写在按钮的 Click 事件里。我见过不少系统直接用明文密码对比数据库,这在内部工具里也最好别干。哪怕不做高强度的加密,至少用哈希加盐存起来。下面是一个可以落地的登录示例:
// Login.aspx.cs protected void BtnLogin_Click(object sender, EventArgs e) { string username = TxtUsername.Text.Trim(); string password = TxtPassword.Text; DataTable dt = SqlHelper.Query( "SELECT UserID, DisplayName, RoleID FROM Users WHERE UserName=@name AND PasswordHash=@hash", new SqlParameter("@name", username), new SqlParameter("@hash", HashPassword(password))); if (dt.Rows.Count > 0) { Session["UserId"] = dt.Rows[0]["UserID"]; Session["DisplayName"] = dt.Rows[0]["DisplayName"]; Session["RoleID"] = dt.Rows[0]["RoleID"]; Response.Redirect("Default.aspx"); } else { LblMessage.Text = "用户名或密码错误"; } } private string HashPassword(string password) { // 实际项目请用 Rfc2898DeriveBytes 或 BCrypt,这里只是演示雏形 using (var sha = System.Security.Cryptography.SHA256.Create()) { byte[] bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); return Convert.ToBase64String(bytes); } }这里有几个关键点。查询条件直接对比哈希值,而不是先查出用户再在 C# 里比较,减少了一次无谓的数据传输。Session["UserId"]存的是用户主键,后续所有页面要判断当前操作者,都从 Session 取。Session 默认是进程内的,如果以后做多服务器部署,要换成 StateServer 或 Redis,这个先记住。
权限控制不能只靠隐藏登录按钮。每个需要权限的页面,在 Page_Load 里要校验 Session 是否为空,否则直接跳回登录页:
protected void Page_Load(object sender, EventArgs e) { if (Session["UserId"] == null) { Response.Redirect("Login.aspx"); return; } int roleId = Convert.ToInt32(Session["RoleID"]); if (roleId < 2) { // 非管理员不允许访问人员管理页 Response.Redirect("Default.aspx"); } }这段逻辑看起来很朴素,但它是整个权限系统的基础。角色 ID 用数字判断,比字符串角色名更不容易拼错。
4.2 任务列表与状态流转:让系统“活”起来的核心交互
任务状态流转是最容易写出“面条代码”的地方。很多新手把更新状态的 SQL 直接写在按钮事件里,每个按钮一个方法,复制粘贴几次后代码就没法看了。我建议至少把状态更新的业务判断放到 BLL 层,页面只做参数传递。
假设页面有一个 GridView,每行有一个“标记完成”按钮。前端事件绑定可以用 GridView 的 CommandField 或者 RowCommand 事件。下面是一个典型的 RowCommand 处理:
// Tasks.aspx.cs protected void GridViewTasks_RowCommand(object sender, GridViewCommandEventArgs e) { if (e.CommandName == "CompleteTask") { int taskId = Convert.ToInt32(e.CommandArgument); int currentUserId = Convert.ToInt32(Session["UserId"]); // 调用 BLL 层,而不是直接写 UPDATE bool ok = TaskManager.CompleteTask(taskId, currentUserId); if (!ok) { LblMessage.Text = "只有负责人才能完成此任务"; } BindTasks(); } }在 GridView 的模板列里,按钮的CommandArgument要绑定TaskID,这样事件才能知道该操作哪一行:
<asp:TemplateField HeaderText="操作"> <ItemTemplate> <asp:LinkButton ID="btnComplete" runat="server" CommandName="CompleteTask" CommandArgument='<%# Eval("TaskID") %>' OnClientClick="return confirm('确认完成任务?');"> 标记完成 </asp:LinkButton> </ItemTemplate> </asp:TemplateField>OnClientClick弹确认框,确认后回发服务器,这是 Web Forms 标准交互。千万不要在服务端再弹 MessageBox,因为服务端的消息框没法在浏览器里显示。任务完成逻辑里要判断当前用户是不是负责人,这个规则适合放在 BLL 层,用 C# 写清楚比藏在 SQL 里更容易测试。
4.3 用 AJAX 提升交互:UpdatePanel 还是 Web API?
系统如果只有回发也能用,但每次点击都要刷新整个页面,体验很生硬。Web Forms 里最简单的是用 UpdatePanel 包住局部区域,但 UpdatePanel 要依赖 ScriptManager,而且局部刷新时整个 ViewState 照样传输,性能并不好。如果你还在用 Ajax Control Toolkit,注意它在 VS 2022 下和现代浏览器的兼容性很多坑,官方更新几乎停了。我现在的做法是,Web Forms 页面直接写 WebMethod 或用一般处理程序,前端用 fetch 调用,轻量又好排查。
后端定义一个静态 WebMethod:
// Projects.aspx.cs [System.Web.Services.WebMethod] public static string GetProjectProgress(int projectId) { // 返回完成任务的百分比 DataTable dt = SqlHelper.Query( "SELECT Total=COUNT(*), Done=SUM(CASE WHEN Status=2 THEN 1 ELSE 0 END) FROM Tasks WHERE ProjectID=@pid", new SqlParameter("@pid", projectId)); decimal percent = 0; if (dt.Rows.Count > 0 && Convert.ToInt32(dt.Rows[0]["Total"]) > 0) { percent = Convert.ToDecimal(dt.Rows[0]["Done"]) / Convert.ToDecimal(dt.Rows[0]["Total"]) * 100; } return percent.ToString("0.0"); }前端 JavaScript 用fetch调用这个 WebMethod:
fetch("Projects.aspx/GetProjectProgress", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectId: 123 }) }) .then(response => response.json()) .then(result => { const percent = result.d; document.getElementById("progressLabel").innerText = "进度: " + percent + "%"; });这里有两个坑。WebMethod 必须是public static,返回值会包一层,通常用.d属性取值。fetch的 URL 要写页面路径和方法名,不能写相对路径少了.aspx,否则返回 404。这种方式脱离了 UpdatePanel 的束缚,局域网内部系统用起来完全够。
5. 项目管理系统避坑手册:从部署到运行的 5 个常见问题
5.1 连接字符串里的实例名和密码:换电脑就翻车
现象:本地开发好好的,把项目拷到同事电脑或服务器上,页面报“建立与 SQL Server 的连接出错”或“找不到指定的实例”。
原因:web.config里的Data Source写死了本机实例名,或者User ID对应的 SQL 登录账户不存在。SQL Server 默认安装时的实例名可能是SQLEXPRESS,也可能是单实例没带名字,我用.连接时在当前机器有效,换机器就废。
解决:先在服务器上确认实例名。在 SQL Server Management Studio 的连接窗口里能看到服务器名。也可以在命令行跑sqlcmd -L列出可用实例。然后把Data Source改成目标机器的机器名\实例名,并确认 SQL Server 启用了“SQL Server 身份验证模式”,给sa账号重置密码或在数据库里新建专门的应用账号。
5.2 IIS 部署后 500 错误:功能没装上
现象:发布后访问网站直接显示 HTTP 500.19 或 500.21 错误,本地一切正常。
原因:IIS 没有安装 ASP.NET 对应版本的功能。Windows Server 默认不带这个,即使装 IIS,应用池也能建,但遇到 .aspx 页面时处理程序无法加载。我遇到过最多的是装完 IIS 忘了勾选“.NET Framework 4.x ASP.NET”功能。
解决:在服务器“服务器管理器”里点击“添加角色和功能”,找到“Web 服务器 (IIS) → 应用程序开发”,勾选ASP.NET 4.x,然后重启 IIS。如果是 .NET Framework 版本不匹配,检查应用程序池的 .NET CLR 版本,Web Forms 项目要选v4.0而不是无托管代码。
5.3 字符串转数字的坑:日志统计里悄悄少算的数据
现象:从 Excel 导入工时后,统计当月总工时比人工算的少很多,甚至查询直接报错“将 varchar 转换为数据类型 numeric 时失败”。
原因:Excel 导入时某列是文本类型,比如工时列里填了"8小时"或"8.5 "带空格,导入 SQL Server 后字段类型是nvarchar。直接SUM(Hours)时 SQL Server 尝试隐式转换,遇到无法转换的字符就报错;如果某些行能转,某些行报错,会导致整个统计不可用。
解决:导入前在 C# 里用decimal.TryParse清洗数据,清洗后转成decimal再写入数据库。如果已经进了临时表,使用TRY_CAST配合ISNULL过滤脏行,并把错误行记录下来反馈给用户。核心原则:业务表里用来计算的字段,从一开始就要在数据库里设成数字类型,别用nvarchar存数字。
5.4 上传文件路径权限:文件写不进去或访问 404
现象:系统里上传项目附件,点击保存后提示“对路径的访问被拒绝”。或者上传成功,但下载时 404。
原因:IIS 进程跑在IIS_IUSRS或应用程序池身份下,默认对网站目录没有写权限。上传文件如果直接保存到项目根目录的uploads文件夹,而这个文件夹没有给进程账户写入权限,就会拒绝。下载 404 则可能是把文件写到了App_Data或另一个虚拟路径下,而 URL 和物理路径对不上。
解决:给上传目录设置最少权限。右键上传文件夹 → 属性 → 安全 → 添加IIS_IUSRS用户,勾选“修改”写入权限,注意不要给整个网站目录加写权限。保存路径用Server.MapPath("~/uploads/"),然后在数据库里存相对路径,页面下载时用~/uploads/文件名拼 URL。如果文件比较多,建议按日期建子目录,避免单目录文件成千上万后查找性能下降。
5.5 改了代码不生效:浏览器缓存和 DLL 缓存
现象:重新发布后,刷新页面还是旧功能。清空了浏览器缓存也无效,甚至删除旧的发布目录再复制新的,页面依旧老样子。
原因:第一层是浏览器缓存,HTML、JS、CSS 都会被缓存;第二层是 IIS 应用程序池的 bin 文件锁——如果文件被 IIS 进程占用,复制新 DLL 不会替换成功,系统可能还在跑旧程序集。
解决:发布前先停止对应应用程序池,再覆盖发布。部署后重启一次应用池,保证所有 DLL 重新加载。前端静态资源在引用时加版本号,例如style.css?v=20250601,或者让 IIS 设置缓存头。为了验证部署的 DLL 是不是新版,可以在页面加一个部署号或编译日期,显示在页脚,这样有没有生效一眼就能看出来。
6. 进阶技巧:把报表导出和页面打印做得像回事
6.1 用 GridView 导出的坑和更稳的 Excel 生成方式
项目管理系统到后期,领导一定会提一句:“这些数据能不能导成 Excel?”新手用GridView.RenderControl生成 HTML 表格然后改 MIME 类型导出,出来的文件虽然能用 Excel 打开,但一到带公式或日期格式就比较脏,而且会报“文件格式与扩展名不匹配”。更稳的做法是在服务端用库生成真正的.xlsx文件。常见做法是引用 ClosedXML(NuGet 包),写入 DataTable 再返回给客户端下载。
// Export.aspx.cs protected void BtnExport_Click(object sender, EventArgs e) { DataTable dt = SqlHelper.Query("SELECT ProjectName, TaskName, Status, DueDate FROM Tasks WHERE ProjectID=@pid", new SqlParameter("@pid", Convert.ToInt32(ddlProject.SelectedValue))); using (var workbook = new ClosedXML.Excel.XLWorkbook()) { var worksheet = workbook.Worksheets.Add("任务清单"); int rowIndex = 1; worksheet.Cell(rowIndex, 1).Value = "项目名"; worksheet.Cell(rowIndex, 2).Value = "任务名"; worksheet.Cell(rowIndex, 3).Value = "状态"; worksheet.Cell(rowIndex, 4).Value = "截止日期"; foreach (DataRow row in dt.Rows) { rowIndex++; worksheet.Cell(rowIndex, 1).Value = row["ProjectName"].ToString(); worksheet.Cell(rowIndex, 2).Value = row["TaskName"].ToString(); worksheet.Cell(rowIndex, 3).Value = row["Status"].ToString(); worksheet.Cell(rowIndex, 4).Value = row["DueDate"].ToString(); } Response.Clear(); Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; Response.AddHeader("Content-Disposition", "attachment;filename=TaskReport.xlsx"); using (var ms = new System.IO.MemoryStream()) { workbook.SaveAs(ms); ms.WriteTo(Response.OutputStream); } Response.End(); } }导出 Excel 最烦的两个细节是拼文件名和处理中文文件名。Content-Disposition里直接写中文文件名在部分浏览器会乱码,我一般用HttpUtility.UrlEncode包一层,或者干脆用英文文件名。另一个坑是Response.End()后代码还在执行,可能抛线程终止异常,这个在 try-catch 里要小心。
6.2 页面打印的 CSS 技巧与 PDF 输出
Web 管理系统的页面打印需求通常集中在任务详情、工时报表。最简单好用的办法不是引入 PDF 库,而是用 CSS 控制打印范围。给页面加一个print.css,只打印目标区域,隐藏导航菜单和按钮。
/* print.css */ @media print { .no-print { display: none !important; } .print-area { width: 100%; border: none; } body { font-size: 12pt; } }页面里的打印按钮调用window.print(),为了规避浏览器页眉页脚,可以在 CSS 里加上@page { margin: 10mm; }。如果要直接生成 PDF,Chrome 的打印对话框本身就支持“另存为 PDF”,不需要再单独集成组件。
这套流程做完,你的系统已经能覆盖项目管理、任务流转、工时统计、报表导出这几件核心事。我刚开始做类似系统时,把大量逻辑挤在页面后台,后来维护升级时每天都在后悔。先拆好层、定好表结构,后面所有功能都是在给这骨架添肉。希望这篇笔记里那几个坑能帮你少走几段弯路。
本文还有配套的精品资源,点击获取